1. 程式人生 > >演算法導論學習之堆+堆排序+堆構成優先佇列

演算法導論學習之堆+堆排序+堆構成優先佇列

注:堆分為最大堆和最小堆兩種,下面我們討論的堆都是指的最大堆,最小堆的性質與其是類似的。

堆資料結構是一種陣列物件,可以被視為一棵完全二叉樹(這棵二叉樹除最後一層外,其餘每層都是填滿的);我們用一個數組來儲存一個堆,表示堆的陣列有兩個屬性:length[A]表示的是陣列中的元素個數,headsize[A]表示堆中元素個數(也就是說陣列中的元素不一定都是堆中的元素)。
下面不加證明的給出一些堆的性質:
對於任何一個節點,其父節點為i/2(i>1);左兒子:2*i,右兒子:2*i+1;
對於最大堆每個節點滿足:A[parent[i]]>=A[i]
含n個元素堆的高度為lgn
含n個元素堆的葉子節點的編號為:n/2+1,n/2+2,……,n。

一.堆的調整函式—保持堆的性質
考慮這樣一種情況,如果對於節點i,其左右子樹都是一個合法的大根堆,但是節點i不滿足堆的性質,那麼我們可以通過調整使得以i為根節點的樹滿足堆的性質。那麼具體的調整過程是怎樣的呢?我們令largest表示A[i],A[i*2],A[2*i+1]中最大的那個,如果largest不是i那麼我們就交換A[i]和A[largest]並且遞迴的進入到以largest為根節點的子樹中繼續進行調整。具體的程式碼實現如下:

void MaxHeadfly(int *a,int i,int HeadSize)
{  ///堆調整函式:調整以i為根的子樹,使得其符合大根堆的特性(在其左右子樹都是合法的大根堆的情況下)
///最大堆調整函式是堆排序中最重要的函式,是後面幾個函式的重要組成部分; ///其主要思想是:先是調整根節點然後遞迴的調整受影響的左(右)子樹,直到葉子節點 int l=2*i,r=2*i+1; ///取出節點i的左右兒子節點的編號 int largest; ///記錄最大值的節點編號 if(a[i]<a[l]&&l<=HeadSize) largest=l; else largest=i; if(a[largest]<a[r]&&r<=HeadSize) largest=r; if
(largest!=i) {///遞迴調整受影響的子樹 swp(a[i],a[largest]); MaxHeadfly(a,largest,HeadSize); } }

二.堆的建立
堆的建立其實就是一個不斷呼叫調整函式的過程。我們知道堆的葉子節點可以看成只含有一個元素合法的大根堆,這樣如果我們從第一個非葉子節點開始依次呼叫調整函式,就可以保證被調整的節點的左右子樹都是合法的大根堆,進而可以保證調整得到的樹也是合法的大根堆。具體的程式碼實現如下:

void BuildMaxHead(int *a,int n)
{ ///最大堆建立函式:通過對陣列元素有序的進行調整,使得陣列成為一個大根堆
  ///由完全二叉樹的性質:含n個元素的堆,其葉子節點編號為n/2+1,n/2+2。。。n;
  ///對於葉子節點可以看成合法的單個元素的大根堆,由MaxHeadfly函式的性質可以知道
  ///如果節點i不滿足大根堆的性質,但是其左右子樹都是合法的大根堆,那麼呼叫MaxHeadfly
  ///調整後以i為根節點可以構成一個合法的大根堆,而一個節點的左右兒子節點編號都是大於其的
  ///所以我們可以從編號n/2--1的節點依次呼叫堆的調整函式,然後就可以得到一個大根堆了。

  for(int i=n/2;i>=1;i--)
          MaxHeadfly(a,i,n);
}

三.堆排序
顧名思義堆排序就是利用堆的性質對陣列進行排序,對於一個堆來說我們能利用的性質就是其根節點中的元素是最大的,這次每次我們都可以將A[1]與A[HeadSize]進行交換,交換以後HeadSize的值減1,並且呼叫MaxHeadfly(a,1,HeadSize)對堆進行調整,使得A[1]又變成剩下元素中最大的,然後繼續這個過程。最後得到一個有序的陣列。堆排序的時間複雜度是nlgn;具體的程式碼實現如下:

void HeadSort(int *a,int n)
{///堆排序函式:實現對陣列中的n個元素進行從小到大的排序,時間複雜度nlgn
  ///我們知道如果一個數組構成大根堆後,那麼其根節點a[1]必定是其中最大的元素
  ///此時如果我們將a[1]與a[n]互換,那麼a[n]就是最大的元素了;
  ///但是a[1--n-1]構成的堆可能不再滿足最大堆的性質,我們再呼叫MaxHeadfly(a,1,n-1)調整成最大堆
  ///然後a[1]就成了a[1--n-1]中最大的元素了,然後我們交換a[1]和a[n-1],那麼a[n-1]和a[n]就是有序的了。
  ///繼續這個過程直到堆的規模減少到2,然後進行最後一次調整交換,a陣列就有小到大排列了。
  ///時間複雜度:建立大根堆時間O(n),每次調整時間O(lgn)共調整n-1次,所以總的複雜度nlgn
        BuildMaxHead(a,n);  ///建立一個大根堆
        int HeadSize = n;
        for(int i=n;i>=2;i--) ///堆的規模不斷減少
        {
               swp(a[1],a[i]);
               HeadSize--;   ///交換以後就要減少堆的大小
               MaxHeadfly(a,1,HeadSize);   ///根節點改變了,重新調整成大根堆
        }
}

下面附上一份完整的堆排序的程式碼:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

int swp(int &a,int &b)
{
       int t=a;
        a=b;
        b=t;
}

void MaxHeadfly(int *a,int i,int HeadSize)
{  
        int l=2*i,r=2*i+1; ///取出節點i的左右兒子節點的編號
        int largest;  ///記錄最大值的節點編號
        if(a[i]<a[l]&&l<=HeadSize)
            largest=l;
        else
            largest=i;
       if(a[largest]<a[r]&&r<=HeadSize)
            largest=r;
        if(largest!=i)
        {///遞迴調整受影響的子樹
                swp(a[i],a[largest]);
                MaxHeadfly(a,largest,HeadSize);
        }
}

void BuildMaxHead(int *a,int n)
{ 
  for(int i=n/2;i>=1;i--)
          MaxHeadfly(a,i,n);
}

void HeadSort(int *a,int n)
{
        BuildMaxHead(a,n);  ///建立一個大根堆
        int HeadSize = n;
        for(int i=n;i>=2;i--) ///堆的規模不斷減少
        {
               swp(a[1],a[i]);
               HeadSize--;   ///交換以後就要減少堆的大小
               MaxHeadfly(a,1,HeadSize);   ///根節點改變了,重新調整成大根堆
        }
}

int main()
{
      int n=5,a[10];
      cout<<"請輸入"<<n<<"個數:"<<endl;
      for(int i=1;i<=n;i++)
           cin>>a[i];
      HeadSort(a,n);
      cout<<"排序以後的陣列:"<<endl;
      for(int i=1;i<=n;i++)
          cout<<a[i]<<" ";
      cout<<endl;
   return 0;
}

四.優先佇列
對於一般的佇列來說,滿足先進先出的性質(隊尾入隊,對首出隊),但對於優先佇列來說,每次都是最大的/最小的元素出隊。堆可以實現優先佇列的基本操作,利用的也是堆的性質。優先佇列一些具體的函式操作及其原理見下面的程式碼。

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;

#define INF 0x3f3f3f3f

///一般的佇列滿足先進先出的性質,而優選佇列不同,其
///總是最大的/最小的先出隊。藉助於堆的性質,我們可以
///在lgn的時間內實現優選佇列的任何操作
void swp(int &a,int &b)
{
      int t=a;
      a=b;
      b=t;
}

void MaxHeadfly(int *a,int i,int HeadSize)///堆調整函式
{
        int largest;
        int l=i*2,r=2*i+1;
        if(a[i]<a[l]&&l<=HeadSize)
             largest=l;
        else
             largest=i;
        if(a[largest]<a[r])
             largest=r;
        if(largest!=i)
        {
             swp(a[i],a[largest]);
             MaxHeadfly(a,largest,HeadSize);
        }
}

void BuildMaxHead(int *a,int n) ///建立一個最大堆
{
        int k=n/2;
        for(int i=k;i>=1;i--)
               MaxHeadfly(a,i,n);
}

///以下為優先佇列的操作

int MaxNum(int *a) ///取出最大的元素
{
      return a[1];   ///直接返回
}

void IncreaseKey(int *a,int x,int k)
{   ///將堆中編號為x的元素的值提升到k,k不能小於原來的值。
     ///這種提升只可能會導致父節點不滿足最大堆的性質,所以直接向上遞迴的檢查
     a[x]=k;
     while(x>1&&a[x/2]<a[x])
     {
             swp(a[x],a[x/2]);
             x=x/2;
     }
}

int ExtractMax(int *a,int &HeadSize)
{    ///去掉堆中最大的元素,並返回其值
      ///思想類似於堆排序的一次調整:我們先交換a[1]與a[HeadSize]的值
      ///然後再將HeadSize的值減1(相當於去掉一個葉子節點);然後再呼叫
      ///MaxHeadfly(a,1,HeadSize)對堆進行調整,保證堆的性質。
      swp(a[1],a[HeadSize]);
      HeadSize--;
      MaxHeadfly(a,1,HeadSize);
      return a[HeadSize];
}

void Insert(int *a,int &HeadSize,int x)
{    ///向堆中加入一個元素x
     ///思想:先在堆中加入一個葉子節點,並將其值賦值為-INF
     ///然後再呼叫上面的調整函式將該葉子節點的值調整為x

     HeadSize++;  ///增加一個葉子節點
     a[HeadSize]=-1*INF;
     IncreaseKey(a,HeadSize,x);
}

int main()
{
        int a[10];
        int n=5;
        cout<<"請輸入5個數: "<<endl;
        for(int i=1;i<=n;i++)
             cin>>a[i];
        int HeadSize=n;
        BuildMaxHead(a,n);
        ///取出佇列中 最大的那個元素
        cout<<"最大的元素是: "<<MaxNum(a)<<endl;
        ExtractMax(a,HeadSize);
        cout<<"移除最大元素後,剩下元素中最大的是: "<<MaxNum(a)<<endl;
        ///將第4個元素增加到100
        IncreaseKey(a,4,100);
        cout<<"將第4個元素增加到100後,最大元素是: "<<MaxNum(a)<<endl;
        ///將110插入到佇列中
        Insert(a,HeadSize,110);
        cout<<"插入110後,最大元素是: "<<MaxNum(a)<<endl;
   return 0;
}

相關推薦

演算法導論學習+排序+構成優先佇列

注:堆分為最大堆和最小堆兩種,下面我們討論的堆都是指的最大堆,最小堆的性質與其是類似的。 堆資料結構是一種陣列物件,可以被視為一棵完全二叉樹(這棵二叉樹除最後一層外,其餘每層都是填滿的);我們用一個數組來儲存一個堆,表示堆的陣列有兩個屬性:length[A]表

演算法導論學習插入排序

《演算法導論》買了好久了,基本上沒怎麼看,最近思想上有了轉變,覺得學習才是王道。準備重新拾起來學習,下面我就《演算法導論》中的排序演算法中的 插入排序做了個c++的簡單實現,附加解釋一下自己對下面的這段程式碼的理解。 #include "iostream" using n

演算法導論學習快排+各種排序演算法時間複雜度總結

快排是一種最常用的排序演算法,因為其平均的時間複雜度是nlgn,並且其中的常數因子比較小。 一.快速排序 快排和合並排序一樣都是基於分治的排序演算法;快排的分治如下: 分解:對區間A[p,r]進行分解,返回q,使得A[p–q-1]都不大

演算法導論思考題13-4 treap-樹

treap樹,是一種以節點的值和附加的優先順序來實現的搜尋樹。正如演算法導論中所介紹的那樣。 ”如果將一個n個元素的集合插入到一顆二叉搜尋樹中,所得到的樹可能極不平衡,從而導致某些平均查詢長度變長。然後有引用了隨機構造的二叉搜尋樹是趨於平衡的。這裡我存在一個疑問。先不管他們

Java學習棧記憶體與記憶體

堆:(物件) 引用型別的變數,其記憶體分配在堆上或者常量池(字串常量、基本資料型別常量),需要通過new等方式來建立。 堆記憶體主要作用是存放執行時建立(new)的物件。 (主要用於存放物件,存取速度慢,可以執行時動態分配記憶體,生存期不需要提前確定)

演算法導論學習||選擇排序

選擇排序簡單得說就是找出最小的和第一個數交換,再找出次小的數與第二個數交換……以這樣的方法進行排序。 下面是自己用java實現的演算法: public class selection_sort {

演算法導論學習--紅黑樹詳解刪除(含完整紅黑樹程式碼)

前面我們討論了紅黑樹的插入的實現,基本思想是分類討論;然後分情況討論以後我們發現插入操作調整函式只需要處理三種情況,並不是太複雜。但是刪除操作會更復雜一點,因為二叉搜尋樹的刪除操作本身就分成了多種情況,這樣在執行刪除操作後要處理的情況會更多;下面對於刪除操作我們

演算法導論學習筆記攤還分析

在攤還分析中,我們求資料結構的一個操作序列中所執行的所有操作的平均時間,來評價操作的代價。這樣,就可以說明即使一個序列的某個操作很複雜,平均代價還是很低的。 攤還分析中最常用三種技術:  聚合分析,這種方法用來確定一個n個操作的序列的總代價的上界T(n),因而每個操作的代價

演算法學習分治--歸併排序

分治的基本概念: 把一個任務,分成形式和原任務相同,但規模更小的幾個部分任務(通常是兩個部分),分別完成,或只需要選一部完成。然後再處理完成後的這一個或幾個部分的結果,實現整個任務的完成。 陣列排序

牛耕田學習 shell冒泡排序

des auth 接下來 ++ bsp scripts eat one 繼續 #!/bin/bash############################################################### File Name: /server/scri

Oracle基礎學習空值排序

初始 font 結果 eat 工資 dep varchar2 com creat 新建表: 1 -- Create table 2 create table EMP 3 ( 4 empno NUMBER(4), 5 ename VARCHAR

oracle菜鳥學習 分析函式-排序

oracle菜鳥學習之 分析函式-排序 排序函式 1.row_number:返回連續的排序,無論值是否相等2.rank:具有相等值得行排序相同,序數值隨後跳躍3.dense_rank:具有相等值得行排序相同,序號是連續得 實驗表 create table chengji(sno number,km v

演算法導論動態規劃重構解

動態規劃(dynamic programming)就是把問題分解成一個個的子問題,然後對子問題進行求解。下面用通俗易懂的語言來解釋下動態規劃中關於重構解的理解。既儲存最優切割收益也儲存切割方案! 重構解 1、儲存最優切割收益也儲存切割方案的演算法 用 s[ j

演算法導論最優二叉搜尋樹

最優二叉搜尋樹 假定我們正在設計一個程式,實現英語文字到法語的翻譯。對英語文字中出現的每個單詞,我們需要查詢對應的法語單詞。為了實現這些查詢操作,可以建立一棵二叉搜尋樹,將n個英語單詞作為關鍵字,對應的法語單詞作為關聯資料。由於文字中的每個單詞都要進行搜尋,我們

Java學習路——氣泡排序與選擇排序

Java學習之路——for迴圈實現氣泡排序 基本步驟為: 利用for迴圈輸入並輸出陣列各個元素的值 利用冒泡法(選擇法)進行排序 利用for迴圈將排序完成的陣列元素進行輸出   冒泡法方法:氣泡排序採取從頭至尾以此比較相鄰的兩個元素;小數放前,大數放後。第一輪

演算法導論學習-數學歸納法與迴圈不變式理解

數學歸納法: 最簡單和常見的數學歸納法是證明當n等於任意一個自然數時某命題成立。證明分下面兩步: 證明當n= 1時命題成立。 假設n=m時命題成立,那麼可以推導出在n=m+1時命題也成立。(m代表任意自然數) 這種方法的原理在於:首先證明在某個起點值時命題成立,然後證

【java版】資料結構與演算法分析學習路【一】前言

一.資料結構和演算法概述?【框範圍】 基礎資料結構主要包括表【陣列+連結串列】、棧、佇列【散列表】、樹、圖、堆。高階資料結構包括伸展樹、紅黑樹、確定性跳躍表、AA樹、treap樹、k-d樹、配對堆。

23.VUE學習-列表的排序sort

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <!--<script src="https://cdn.jsdelivr.net/npm/vue/dist

演算法工程師學習

之前是通訊行業工程師,作為流水線上的一顆螺絲釘N年,只是耕耘在自己熟悉的領域,視野比較窄。 興趣是最好的老師,興趣引我轉向了這個行業。 最開始 我對資料分析比較感興趣, 後來是資料探勘,再是機器學習,深度學習..  自然走在這條路上。本人資質也只是勉強夠用,學習過程中也

演算法導論】8.線性時間排序(計數排序,基數排序,桶排序

三種線性時間複雜度的排序演算法:計數排序,基數排序,桶排序。 8.1排序演算法的下界 給定兩個元素ai和aj,如果使用比較排序,可以有ai<aj,ai<=aj,ai=aj,ai>aj,ai>=aj五種操作來確定其相對次序。本節假設所有的比較採用ai<=aj形