理解堆排序

複習外排序算法時,設計到敗者樹,而這個又和堆排序相關。剛好忙完一個項目,有時間複習一下堆排序,加深理解。
堆就是滿足下列條件的一個數組,一個數組A[n],A[i]<=A[2i+1],A[i]<=[2i+2],0<=i<=n/2-1或者A[i]>=A[2i+1],A[i]>=[2i+2],0<=i<=n/2-1。滿足前者條件稱爲小頂堆;滿足後者條件的稱爲大頂堆。小頂堆也可以定義爲:一個數組A[n](n>=1),A[j]<=A[i],1<=j<=n-1,i=(j-1)/2。類似的大頂堆也可以定義爲:一個數組A[n](n>=1),A[j]>=A[i],1<=j<=n-1,i=(j-1)/2。

根據堆的定義,第一個元素一定是最大或者最小的。將一個數組A[0...n-1]調整爲堆,把第一個元素和最後一個元素調換,A[n-1]是最大或者最小的元素。再把A[0...n-2]調整爲堆,A[0]就是次大或者次小的元素,交換A[0]和A[n-2]。如此操作,知道A[0...1]也變調整成堆。這樣整個數組A[0...n-1]就變成了一個有序數組。

嘗試去理解WILLIAMS初始算法和FLOYD的算法有什麼差異。堆排序中有兩個主要的問題:1.如果由一個無序序列建成一個初始堆。2.如何輸出堆頂元素後,把剩下的元素調整成一個堆;也就是一個序列對應一個完全二叉樹,根節點的左右子樹都是堆,但整個樹不是堆,如何來調整使得整個樹是堆。我理解對第二個問題初始算法和FLOYD算法處理方式是一致的。對第一個問題處理方法有所不同。

WILLIAMS對第一個問題的處理思路是:如果A[0...i]是堆,插入一個新元素後得到A[0...i+1],然後把A[0...i+1]調整爲堆,參考InHeap函數。。A[0...0]只有一個元素是堆,加入A[1],調整A[0...1]得到堆,然後依次調整A[0...2],...A[0...n-1],最終無序序列調整爲堆,參考SetHeap函數而FLOYD算法的處理思路是與第二個問題的處理思路相同:如果A[k...m]是堆,在這個序列之前插入一個元素A[k-1...m],調整使得A[k-1...m]是一個堆子樹(A[k-1...m]對應一個完全二叉樹)或者其所包含的子樹(A[k-1...m]對應多個完全二叉樹)都是堆子樹,參考ReHeap,AdjustHeap兩個函數。。A[n/2-1]是最後一個非葉節點,剩下的節點A[n/2...n-1]都是葉節點,所以它是堆,調整A[n/2-1...n-1]爲堆,再依次調整A[n/2-1-1...n-2],...A[0...n-1],最終無序序列調整爲堆,參考InitHeap函數。

I.對WILLIAMS算法的理解。
A[n] is a small heap. remove the first element A[0], then Rearrage the elements left into a heap。
初始時我的思路是:從根節點開始,找最小的子節點填充父節點,直到用來填充的節點是葉節點。根據這個思路我寫出一下的程序。
double reHeap(double *A,int n){
    if(A == NULL){
        return MAX_DOUBLE;
    }

    int index =0,j=-1;
    double temp=0;

    index=0;
    while(index<n){
        j = 2*index+1;
        if(j>=n){//no children.
            break;
        }
        if(j<n){
            temp = A[j];
        }
        if(j+1<n){
            if(A[j+1]<A[j]){
                temp = A[j+1];
                index = j+1;
            }
        }
        
        A[index] = temp;
       
    }    
}

在理解這個思路時,發現了一個問題:變換後的樹不再是完全二叉樹,這也導致新數組不再是堆。可能導致兩個節點之間有空節點。例如一個堆A(3,14,18,20,23,30,25,33,45,80,60)。取走第一個元素3後對剩餘的集合重新安排,使之還爲一個堆。用上面的程序調整後的數值是A(14,20,18,33,23,30,25,XX,45,80,60),33這個節點移位後,堆的性質被破壞,需要對後面的節點進行調整。就本例,一個調整方案是A(14,20,18,33,23,30,25,60,45,80),移動次數還不算多。但有些情況下,調整的次數就是比較多。如果初始堆是A(3,14,18,20,23,30,25,33,45,24,27),用上面的算法調整後,數組變爲A(14,20,18,33,23,30,25,XX,45,24,27),把這個數組調整爲堆,需要調整更多的節點後得到一個方案A(14,20,18,27,23,30,25,33,45,24)。
現在的方法就是利用最後一個節點來填充第一個節點被移除後的空缺。爲什麼用最後一個節點來填充?一個堆對應一個完全二叉樹,移除根節點,剩下的元素還要組成一個完全二叉樹,那麼只能移除最深層的最右邊的葉節點。如果移除其它節點,就存在兩個節點間有空節點的情況,就不是完全二叉樹。最深層的最右邊的葉節點對應數組的最後一個元素。
新的思路:
1.從根節點開始,找子節點、最後一個元素中最小的填充父節點。
2.如果最小的是最後一個元素,填充這個元素到父節點。調整完畢。
3.如果此節點沒有葉節點,填充最後一個元素到這個節點。調整完畢。
4.如果最小的是某個子節點,將子節點填充到父節點中。把這個子節點看做父節點,尋找這個子節點、最後一個元素中最小的填充這個父節點。 重複這一步,直到2或者3的某種情況出現。調整完畢。
/*
把第一個元素保存在到最後的位置上。剩下的元素進行調整,並把原來最後一個元素插入到合適的位置,使得剩下的元素又形成一個堆。
*/
double ReHeap(double *A,int n){
    if(A == NULL || n<=0){
        return MAX_DOUBLE;
    }

    int index =0,j=-1,subNodeInx=-1;
    double temp=0;

    double lastEle = A[n-1];
    A[n-1] = A[0];//第一個元素放到最後

    index=0;
    
    while(index<n){
        j = 2*index+1;
        if(j>=n){//no children.
            break;
        }
        if(j<n){
            temp = A[j];
            subNodeInx = j;
        }
        if(j+1<n){
            if(A[j+1]<A[j]){
                temp = A[j+1];
                subNodeInx = j+1;
            }
        }
        if(lastEle>temp){ //輸入的新元素小於葉節點
            A[index] = temp;            
            index = subNodeInx;
        }else{
            break; //最後一個元素比子節點小
        }
    }
    A[index]=lastEle;
}

/*
在一個堆中插入一個新元素,插入之後還是堆數組A的大小是大於n。
思路:新元素放到最後一個元素後面,就是新元素成爲最後一個元素。把新元素看做子元素,和其父元素比較。
      1. 如果子元素小於父元素。結束。
      2. 如果子元素大於父元素,這個元素和父元素交換位置。把這個父元素看做子元素,與其父元素比較,直至場景1發生。
      
*/
void InHeap(double * A, int n, double in){
    if(NULL == A || n<=0){
        return;
    }
    
    int index=n,fNodeInx=-1;
    bool isFindPos=false;
    while(index>=0 && !isFindPos){
        fNodeInx=(index-1)/2;
        if(fNodeInx>=0){
            if(A[fNodeInx]>in){
                A[index]=A[fNodeInx];
                index = fNodeInx;
            }else{
                isFindPos=true;//find the right position for the new element "in"
            }
        }else{
            isFindPos=true;//root element.
        }
    }
    A[index] = in;  
}

/*
把一個數組初始化成一個堆。
每次循環InHeap(A,i-1,A[i])保證A[0...i]個元素是堆。
*/
void SetHeap(double *A,int n){
    if(NULL == A || n<=0){
        return;
    }    
    
    for(int i=1; i<n;i++){
        InHeap(A,i-1,A[i]);
    }
}


/*
堆排序
*/
void HeapSort(double *A,int n){
    //構建初始堆
    SetHeap(A,n);
    //排序
    for(int i=n;i>1;--i){
        ReHeap(A,i);
    }
    
}

II. FLOYD算法

/*
數組的第k+1個元素到第m個元素(m>k+1)是具有堆的性質,把第k個元素到第m個元素都調整成具有堆的性質這個函數和InHeap本質上是一致的。
*/
void AdjustHeap(double * A,int k,int m){
    if(NULL == A || k>m || k<=0 || m<=0){
        return;
    }
    int subNodeInx=-1,i=k;
    while(i<=m){
        subNodeInx = 2*i+1;
        if(subNodeInx<=m){
            if(subNodeInx+1<=m && A[subNodeInx+1]<A[subNodeInx]{ //找到子節點中最小的
                ++subNodeInx;
            }
            if(A[i]>A[subNodeInx]){ //父節點大於左右子節點,交換父節點與子節點
                Swap(A,i,subNodeInx);
                i=subNodeInx;
            }else{
                break;
            }
        }else{
            break;
        }
    }
}

//第k個節點的左右節點都具有堆的性質,要調整使得k爲根的子樹都有堆的性質。
void AdjustHeap(double * A,int k,int m){
    if(NULL == A || k>m || k<=0 || m<=0){
        return;
    }
    int fNodeInx=k;
    double s=A[k];
    for(int i=2*fNodeInx+1; i<=m; i=i*2+1){//沒有孩子的節點在這裏就得到判斷
        if(i+1<=m){ //左孩子小於父節點
            if(A[i+1]<A[i]){
                ++i;
            }
        }
        if(A[i]>A[fNodeInx]){ //左右孩子節點都大於於父節點
            break;
        }else{
            A[fNodeInx] = A[i];
            fNodeInx = i;
        }
    }
    A[fNodeInx]=s; //原來的第k個元素調整到合適的位置。
}

void InitHeap(double *A,int n){
    int i=-1;
    for(i=n/2-1; i>=0; i--){//建成初始堆
        AdjustHeap(A,i,n-1);
    }
}

//堆排序
void HeapSort(double * A,int n){
    
    InitHeap(A,n);

    for(int i=n-2;i>0;i--){
        Swap(A,0,i+1);
        AdjustHeap(A,0,i);
    }
}
發佈了36 篇原創文章 · 獲贊 3 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章