(1.3.7.1)选择排序:堆排序

来源:互联网 发布:开讲啦大数据 编辑:程序博客网 时间:2024/06/06 00:40

堆排序(Heap Sort):使用堆这种数据结构来实现排序。

先看下堆的定义:

最小堆(Min-Heap)是关键码序列{k0,k1,…,kn-1},它具有如下特性:

ki<=k2i+1,

ki<=k2i+2(i=0,1,…)

简单讲:孩子的关键码值大于双亲的。

同理可得,最大堆(Max-Heap)的定义:

ki>=k2i+1,

ki>=k2i+2(i=0,1,…)

同样的:对于最大堆,双亲的关键码值大于两个孩子的(如果有孩子)。

堆的特点:

  1. 堆是一种树形结构,而且是一种特殊的完全二叉树。
  2. 其特殊性表现在:它是局部有序的,其有序性只体现在双亲节点和孩子节点的关系上(树的每一层的大小关系也是可以体现的)。兄弟节点无必然联系。
  3. 最小堆也被称为小顶堆(根节点是最小的),最大堆也被称为大顶堆(根节点是最大的)。我们常利用最小堆实现从小到大的排序,最大堆实现从大到小的排序。
最小堆和最大堆的两个示例图:

要想实现排序,第一个问题:如何建堆?(以最小堆为例,最大堆同理)

建堆:从最后一个内部节点开始,不断地向下调整,直到根节点。

画个流程图,看得更明白:矩形框表示要调整的位置




仔细看上面的流程图,相信你一定可以清楚明白整个调整过程。

建堆代码:我们使用顺序结构存储堆(可不要以为树形结构一定得使用链表来实现),向下调整(heapSiftDown())的方法是关键,建堆的代码如下:

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. void heapSiftDown(int a[], int n, int pos)   //从pos位置向下调整  
  2. {  
  3.     int i = pos;  
  4.     int j = 2 * i + 1;   //j为i的左孩子  
  5.     while (j < n)  
  6.     {  
  7.         if (j + 1 < n && a[j + 1]<a[j])   //如果右孩子存在,并且右孩子<左孩子  
  8.             j++;  
  9.         if(a[i] < a[j])   //已满足堆序,不需调整  
  10.             break;   //为什么不是continue?因为子树已经排好堆序,结合流程图想想?  
  11.         swap(a[i], a[j]);   //交换元素  
  12.         i = j;  
  13.         j = 2 * i + 1;  
  14.     }  
  15. }  
  16. void createHeap(int a[], int n)   //建堆  
  17. {  
  18.     if (a && n > 1)  
  19.     {  
  20.         for (int i = (n - 2) / 2; i >= 0; i--)  //(n-2)/2是最后一个内部节点的下标  
  21.             heapSiftDown(a, n, i);  
  22.     }  
  23. }  

建堆时间复杂度:

建堆的时间复杂度是O(n),推导比较复杂。下面粘贴出从资料上找到的推导过程:

对于n个节点的堆,其对应的完全二叉树的层数是logn。若i为层数,则第i层上的节点数最多为2^i(i>=0)。建堆时,每个非叶子节点都调用了一次heapSiftDown()函数,并且每个节点最多调整到最底层,即第i层上的节点调整到最底层的调整次数为logn-i(最大的),则建堆的时间复杂度为


以上复杂度分析参考张铭等《数据结构与算法》,推导过程其实并不重要,关键在于我们可以肯定的是建堆是很快的,最多是线性的。

建好堆之后,如何实现堆排序呢?排序之前,我们先看有关堆的两个操作:插入和删除。理解了这两个操作,排序就自然清楚了。

堆的插入:插入时总是把新节点插入到堆的最后,并从插入位置向上调整,直到根节点或在此之前已满足堆序。

举个例子解释下这个过程:


红色的3是新添的节点。

注意:向上调整的时候,只关注插入位置到根节点的路径,其它路径上的节点是不用调整的。理由很简单:它们已是堆序。这一点可要想清楚了!

向上调整的代码如下:

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. const int MAX=20;  
  2. void heapSiftUp(int a[], int n)   //向上调整   
  3. {  
  4.     int i,j;  
  5.     j = n-1;  
  6.     i = (i-1)/2;   //i为j的父节点  
  7.     while(i>=0)  
  8.     {  
  9.         if(a[j] >= a[i])  
  10.         break;  
  11.         swap(a[i], a[j]);  
  12.         j = i;  
  13.         i = (j-1)/2;  //更精确的写法: i=j%2?(j-1)/2:(j-2)/2;  
  14.     }   
  15. }  
  16. void addToHeap(int a[], int n, int data)  
  17. {  
  18.     /* 
  19.     前提:数组a已排好堆序且数组还有多余位置存放新节点  
  20.     */  
  21.     if(n+1>MAX)  
  22.     {  
  23.         printf("数组已满!无法插入\n");  
  24.         return;  
  25.     }  
  26.     n++;  
  27.     a[n-1]=data;  //把新节点加到最后  
  28.     heapSiftUp(a, n);     
  29. }  


堆的删除:删除操作总是在堆顶进行(也有的说,可以在任意位置删除,但做法一样),我们把最后一个节点填入待删除位置。然后从该位置向下调整

同样给个示例图:


结合上面以给出的向下调整代码,则很好得到堆删除的代码,为了通用性,我们给出指定位置删除的代码:

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. void deleteAt(int a[], int &n, int pos)  //删除pos位置的节点  
  2. {  
  3.     if(pos >= n)  
  4.     {  
  5.         printf("删除的位置不对!\n");  
  6.         return;  
  7.     }  
  8.     a[pos] = a[n-1];  //把最后一个节点填到待删除位置   
  9.     n--;  
  10.     heapSiftDown(a, n, pos);   //向下调整   
  11. }   

特别地,删除堆顶就是 deleteAt(a, n, 0);

有了上面的铺垫,堆排序就呼之欲出了。

堆排序步骤

  1. 先建好堆。
  2. 不断地删除堆顶即可(删除前记得打印堆顶元素),直到只剩下一个元素。
似乎堆的插入操作没有用到。其实,当有新的元素加入到一个已建好堆序的序列中,就用到了。

下面看一个完整的堆排序代码:
[cpp] view plaincopy
  1. #include<iostream>    
  2. #include<ctime>  
  3. using namespace std;  
  4. void heapSiftDown(int a[], int n, int pos)   //从pos位置向下调整    
  5. {  
  6.     int i = pos;  
  7.     int j = 2 * i + 1;   //j为i的左孩子    
  8.     while (j<n)  
  9.     {  
  10.         if (j + 1 < n && a[j + 1]<a[j])   //如果右孩子存在,并且右孩子<左孩子    
  11.             j++;  
  12.         if (a[i] < a[j])   //已满足堆序,不需调整    
  13.             break;   //为什么不是continue?因为子树已经排好堆序    
  14.         swap(a[i], a[j]);   //交换元素    
  15.         i = j;  
  16.         j = 2 * i + 1;  
  17.     }  
  18. }  
  19. void createHeap(int a[], int n)   //建堆    
  20. {  
  21.     if (a && n > 1)  
  22.     {  
  23.         for (int i = (n - 2) / 2; i >= 0; i--)  
  24.             heapSiftDown(a, n, i);  
  25.     }  
  26. }  
  27. void deleteAt(int a[], int &n, int pos)  //删除pos位置的节点    
  28. {  
  29.     if (pos >= n)  
  30.     {  
  31.         printf("删除的位置不对!\n");  
  32.         return;  
  33.     }  
  34.     a[pos] = a[n - 1];  //把最后一个节点填到待删除位置     
  35.     n--;  
  36.     heapSiftDown(a, n, pos);   //向下调整     
  37. }  
  38. void HeapSort(int a[], int n)    //堆排序    
  39. {  
  40.     if (a && n > 1)  
  41.     {  
  42.         createHeap(a, n);  
  43.         while (n > 1)  
  44.         {  
  45.             printf("%4d", a[0]);  
  46.             deleteAt(a, n, 0);  
  47.         }  
  48.         printf("%4d\n", a[0]);  
  49.     }  
  50. }  
  51. int main()  
  52. {  
  53.     printf("******堆排序演练***by David***\n");  
  54.     printf("原序列\n");  
  55.     const int N = 12;  
  56.     int *a = new int[N];  
  57.     srand((unsigned)time(NULL));  
  58.     for (int i = 0; i < N; i++)  
  59.     {  
  60.         a[i] = rand() % 100;  
  61.         printf("%4d", a[i]);  
  62.     }  
  63.     printf("\n");  
  64.     printf("经过堆排序\n");  
  65.     HeapSort(a, N);  
  66.     delete[]a;  
  67.     system("pause");  
  68.     return 0;  
  69. }  
    
运行:

pdate:2014-7-1 00:04
《算法导论》中介绍了一种使用最大堆实现从小到大排序的方法。
伪代码:
HeapSort(A)
1 BuildMaxHeap(A)
2 for i = A.length downto 2
3     exchange A[1] with A[i]
4     A.heap_size = A.heap_size-1
5     MaxHeapify(A, 1)

如果你看懂了上面的排序方法,这里的伪代码是很好明白的:当我们使用最小堆时,总是先把堆顶元素(当前最小)输出,再把最后一个元素调换到堆顶。使用最大堆(堆顶即为最大关键字)时,就没有这么麻烦了,直接交换首尾元素,这样最大的关键字就被调到最后了,再接着从堆顶向下调整,再次交换,次关键字就调到了倒数第二的位置……。最后原数组按从小到大排好序。
直接给出一个完整的实例:
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. #include<stdio.h>  
  2. #include<stdlib.h>  
  3. #include<time.h>  
  4. void Swap(int& a, int& b)  
  5. {  
  6.     if(a!=b)  
  7.     {  
  8.         a ^= b;  
  9.         b ^= a;  
  10.         a ^= b;  
  11.     }  
  12. }  
  13. //维护最大堆(从指定位置pos,向下调整)  
  14. void MaxHeapify(int a[], int n, int pos)  
  15. {  
  16.     int i, j;  
  17.     i = pos;  
  18.     j = 2 * i + 1;   //节点i的左孩子  
  19.     while (j < n)  
  20.     {  
  21.         if (j + 1 < n && a[j] < a[j + 1])  
  22.             j++;  
  23.         if (a[i] < a[j])  
  24.             Swap(a[i], a[j]);  
  25.         i = j;  
  26.         j = 2 * i + 1;  
  27.     }  
  28. }  
  29. //构建最大堆  
  30. void BuildMaxHeap(int a[], int n)  
  31. {  
  32.     if (a && n > 1)  
  33.     {  
  34.         int i, j;  
  35.         i = (n - 2) >> 1;  
  36.         while (i >= 0)  
  37.         {  
  38.             MaxHeapify(a, n, i);  
  39.             i--;  
  40.         }  
  41.     }  
  42. }  
  43. //堆排序  
  44. void HeapSort(int a[], int n)  
  45. {  
  46.     if (a && n > 1)  
  47.     {  
  48.         //先建堆  
  49.         BuildMaxHeap(a, n);  
  50.         while (n > 1)  
  51.         {  
  52.             //交换首尾元素  
  53.             Swap(a[0], a[n - 1]);  
  54.             //堆规模减一  
  55.             n--;  
  56.             //再次从堆顶调增  
  57.             MaxHeapify(a, n, 0);  
  58.         }  
  59.     }  
  60. }  
  61. int main()  
  62. {  
  63.     printf("******最大堆实现从小到大排序***by David***\n");  
  64.     srand((unsigned)time(0));  
  65.     int a[12];  
  66.     printf("原序列\n");  
  67.     for (int i = 0; i < 12; i++)  
  68.     {  
  69.         a[i] = rand() % 100;  
  70.         printf("%4d", a[i]);  
  71.     }  
  72.     printf("\n经过堆排序\n");  
  73.     HeapSort(a, 12);  
  74.     for (int i = 0; i < 12; i++)  
  75.         printf("%4d",a[i]);  
  76.     printf("\n");  
  77.     system("pause");  
  78.     return 0;  
  79. }  

运行:


转载请注明出处,本文地址:http://blog.csdn.net/zhangxiangdavaid/article/details/30069623


0 0