理解堆排序

复习外排序算法时,设计到败者树,而这个又和堆排序相关。刚好忙完一个项目,有时间复习一下堆排序,加深理解。
堆就是满足下列条件的一个数组,一个数组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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章