堆和堆排序(堆实现优先级队列)
2012-07-26 20:50 youxin 阅读(674) 评论(0) 编辑 收藏 举报
堆是一种特殊类型的二叉树,它具有2个性质:
1.每个节点的值大于等于其每个子节点的值
2该树完全平衡,最后一层的叶子都处于最左侧的位置。
n个元素称为对,当且仅当它的关键字序列k1,k2,.....kn满足
ki<=K2i, Ki<=K(2i+1); 1<=i<=floor(n/2);
或者反过来。
堆有最大堆和最小堆,最大最小堆等。可以直接跳到后面看。
注意,我们用数组表示堆时,根节点存放在H[1]中。
#include<iostream> #include<algorithm> using namespace std; //元素上移操作 /*数组h[]及被上移的元素下标i 输出,维持堆的性质的数组h[] */ template<class T> void sift_up(T h[],int i) { if(i!=1) { while(i!=1) { if(h[i]>h[i/2]) { swap(h[i],h[i/2]); i=i/2; } else break; } } } /*下移操作 和儿子节点关键字大的进行比较 */ template<class T> void sift_down(T h[],int n,int i) { if((2*i)<=n) { while((i=2*i)<=n) { if((i+1<=n)&&(h[i+1]>h[i])) i=i+1; if(h[i/2]<h[i]) swap(h[i/2],h[i]); else break; } } } //删除元素 /*输入:数组h[],数组的元素个数n,被删除的元素下标 输出:维持堆的性质的数组H[],及删除后的元素个数n */ //为了删除H[i],可用堆中最后一个元素取代h[i],然后根据被删除元素和取代 //元素 的矮小,确定是做下移还是上移操作 template<class T> void deleteElem(T h[],int &n,int i) { T x,y; x=h[i],y=h[n]; n=n-1; if(i<=n) { h[i]=y; if(y>x) sift_up(h,i); else sift_down(h,n,i); } } /*删除关键字最大的元素,关键字最大的元素位于根节点,把根节点去掉*/ template<class T> T delete_max(T h[],int &n) { T x; x=h[1]; deleteElem(h,n,1); return x; } //插入操作 /*为了把元素x插入堆中,只要把堆的大小增1后,把x放到堆的末端,然后对x上移即可。 */ template<class T> void insert(T h[],int &n,T x) { n++; h[n]=x; sift_up(h,n); } //构造一个队 //输入数组h[],数组的元素个数n //输出:n个元素的堆H[] template<class T> void make_heap1(T a[],T h[],int n) { int i,m=0; for(i=0;i<n;i++) insert(h,m,a[i]); } //我们可以把数组本身构造成一个堆,调整过程是从最后一个树叶,找到它上面的分支节点,从这个 //节点开始做下移操作,一直到根节点为止,最后就成了一个堆*、 template<class T> void make_heap(T a[],int n) { int i; a[n]=a[0]; for(i=n/2;i>=1;i--) sift_down(a,n,i); } int main() { int a[100]={2,4,5,3,8,18,13,15,20,25}; int h[100]; make_heap1(a,h,10); for(int i=1;i<=10;i++) cout<<h[i]<<ends; cout<<endl; make_heap(a,10); for(int i=1;i<=10;i++) cout<<a[i]<<ends; cout<<endl; int n=10; delete_max(a,n); for(int i=1;i<=n;i++) cout<<a[i]<<ends; }
注意上面两种构造堆方法不同,输出的堆不是一样的,但都是堆:
结果:
堆的排序
可以利用堆的性质,对数组A排序,假定A元素个数为n,根据最大堆的性质,根节点元素a[1]就是最大元素,此时,只要交换a[1]和a[n],则a[n]就成为数组关键字最大的元素。就相当于把a[n]从堆中删去,元素个数减1,而交换到h[1]的新元素,破坏了堆的结构,因此要对a[1]做下移操作,使其恢复堆的结果,经过这样交换后,a[1]----a[n-1]成为新的堆,其个数为n-1,反复进行这种操作,a[1]就是A中最小的元素了。堆排序代码:
template<class T> void make_heap(T a[],int n) { int i; a[n]=a[0]; for(i=n/2;i>=1;i--) sift_down(a,n,i); } void heap_sort(int a[],int n) { make_heap(a,n); for(int i=n;i>1;i--) { swap(a[1],a[i]); sift_down(a,i-1,1); } }
注意我们传入的数组a[]的维度应该大于它的元素个数。
-----------------------------------------------
算法导论:
Heapify
最大堆为例,伪代码:
MAX-HEAPIFY(A, i)
l = LIFT(i)
r = RIGHT(i)
if l <= A.heapsize and A[l] > A[i]
largest = l
else largest = i
if r <= A.heapsize and A[r] > A[largest]
largest = r
if largest != i
exchage A[i] with A[largest]
MAX-HEAPIFY(A, largest)
怎么求时间复杂度?
用主定理求得T(n)=o(lgn),或者说,MAX-HEAPIFY作用域一个高度为h的节点所需的运行时间为O(h)。高度h为lgn。
Build the heap
我们已经知道,当用数组存储了n个元素的堆时,叶子节点的下标为n/2+1,n/2+2.........n;(因为假设节点为i,只要i*2>n就说明
i为叶子节点。这样求的i=n/2+1)。叶子都可以看成只有一个元素的堆,过程build-max-heap对树中的每一个其他节点都调用一次
max-heapify.
BUILD-MAX-HEAP(A)
heap-size[A]=length(A);
for i=length(A)/2 downto 1
MAX-HEAPIFY(A,i)
上面的时间复杂度为O(n)。
可以在线性时间内,将一个无序数组建成一个最小堆。
算法导论6-3-2:为什么i从n/2 down to 1,而不是从1 to n/2.
为了保证在调用Max-heapify(A,i)时,以Left[i]和right(i)为根的2颗二叉树都是最大堆。
因为自底至顶的循环方法可以首先让底部的元素满足堆的形式,最后让顶部元素满足堆的性质。如果自顶至低会有灾难性的后果,会有特殊情况使之树的局部不完备。
我的理解:开始时调用max-heapify(a,1)由于a[1]的左子树和右子树还不是堆,所以有问题。
堆排序算法:
建立了最大堆后,因为数组中最大元素在根A[1],则可以通过它与A[n]互换来达到最终正确的位置。现在,如果从 堆中“去掉节点n”,
可以很容易将A[1..n-1]建成最大堆,原来根的子女仍是最大堆,而新的根元素可能违背了最大堆性质,这时调用MAX-HEAPIFY(A,1)就可以保持这一性质.
for i = A.length downto 2
exchange A[1] with A[i]
A.heap-size = A.heap-size - 1
MAX-HEAPIFY(A, 1)
循环了n-1遍,每次调用后堆的size减1.为什么减1?
时间复杂度为O(nlgn).
代码:
#include<iostream> using namespace std; int heapSize; void maxHeapify(int a[],int heapSize,int i)// { int l=2*i+1;//由于是从0开始,left为2*i+1,,right为2*i+2; int r=2*i+2; int largest; if(l<heapSize && a[l]>a[i]) largest=l; else largest=i; if(r<heapSize && a[r]>a[largest]) largest=r; if(largest!=i) { swap(a[i],a[largest]); maxHeapify(a,heapSize,largest); } } void buildMaxHeap(int a[],int n) { heapSize=n;//赋值 for(int i=n/2-1;i>=0;i--)//这里要特别注意,由于是从0开始,不会n/2 { maxHeapify(a,n,i); } } void heapSort(int a[],int n) { heapSize=n; for(int i=n-1;i>=1;i--) { swap(a[0],a[i]); heapSize--;//很重要 maxHeapify(a,heapSize,0);//第0个元素,也就是根节点 } } #define aSize 10; int main() { int a[10]={4,1,3,2,16, 9,10,14,8,7}; buildMaxHeap(a,10); for(int i=0;i<10;i++) cout<<a[i]<<ends; cout<<endl<<endl; heapSort(a,10); for(int i=0;i<10;i++) cout<<a[i]<<ends; cout<<endl; }
由于我们从0开始,而不是从1开始,产生了很多陷阱,画红线的都是我写的时候不小心出错的地方。要特别注意:
数据来自算法导论P77:
int a[10]={4,1,3,2,16, 9,10,14,8,7};
非递归maxheapify:
void adjust_max_heap(int *datas,int length,int i) { int left,right,largest; int temp; while(1) { left = LEFT(i); //left child right = RIGHT(i); //right child //find the largest value among left and rihgt and i. if(left <= length && datas[left] > datas[i]) largest = left; else largest = i; if(right <= length && datas[right] > datas[largest]) largest = right; //exchange i and largest if(largest != i) { temp = datas[i]; datas[i] = datas[largest]; datas[largest] = temp; i = largest; continue; } else break; } }
最小堆只需要改以下判断条件:
void minHeapify(int a[],int heapSize,int i) { int l=2*i+1; int r=2*i+2; int smallest; if(l<heapSize && a[l]<a[i]) smallest=l; else smallest=i; if(r<heapSize && a[r]<a[smallest]) smallest=r; if(smallest!=i) { swap(a[i],a[smallest]); minHeapify(a,heapSize,smallest); } } void minHeapify2(int *a,int n,int m) { int i=m; int j=2*i+1; int tmp=a[i]; while(j<n) { if(j+1<n&&a[j]>a[j+1]) j++; if(a[j]>=tmp) break; else { a[i]=a[j]; i=j; j=2*i+1; } } a[i]=tmp; }
http://buptdtt.blog.51cto.com/2369962/864190
优先级队列:
优先级队列有两种:最大优先级队列和最小优先级队列,这两种类别分别可以用最大堆和最小堆实现。书中介绍了基于最大堆实现的最大优先级队列。一个最大优先级队列支持的操作如下操作:
insert(S,x)把元素x插入集合S
maximum(s) 返回S中具有最大关键字的元素
extract-max(S) 去掉并返回S中具有最大关键字的元素
increase-key(S,x,k) 将元素x的值增加到k(也就是使值增大为k),这里k值不能小于x原来的值。
heap-maximum 用了O(1)的时间
heap-maximum(A)
return A[1];
extract_max(A)
Heap-Increase-Key(A,i,key):将节点i的值增加到key,这里key要比i节点原来的数大。
新增大的关键字与母亲比较,如果大于母亲则不断往上移动。
heap-insert实现了insert操作,这个程序首先加入一个关键字为负无穷大的叶节点来扩展最大堆,然后调用heap-increase,key来设置新节点的关键字的正确值,并保持最大堆的性质:
总之,一个堆可以在O(lgn)的时间内,支持大小为n的集合上的任意优先队列操作。
#include<iostream> using namespace std; inline int parent(int i) { return i>>1; } inline int left(int i) { return i<<1; } inline int right(int i) { return (i<<1)|1; ////位运算乘2后,结果是偶数所以最后一位一定是0, 所以|1将会把最后一位变成1,从而实现加1的效果 } void maxHeapify(int a[],int heapSize,int i)// { int l=left(i); int r=right(i); int largest; if(l<=heapSize && a[l]>a[i]) largest=l; else largest=i; if(r<=heapSize && a[r]>a[largest]) largest=r; if(largest!=i) { swap(a[i],a[largest]); maxHeapify(a,heapSize,largest); } } void buildMaxHeap(int a[],int n) { int heapSize=n;//赋值 for(int i=n/2;i>=1;i--)//这里要特别注意,由于是从0开始,不会n/2 { maxHeapify(a,n,i); } } void heapSort(int a[],int n) { int heapSize=n; for(int i=n;i>=2;i--) { swap(a[1],a[i]); heapSize--; maxHeapify(a,heapSize,1); } } int maximum(int A[]) { return A[1]; } int extractMax(int A[],int heapSize) { if(heapSize<1) return 0; int max=A[1]; A[1]=A[heapSize]; heapSize--; maxHeapify(A,heapSize,1); return max; } void increaseKey(int A[],int i,int key) { A[i]=key; while(i>1 && A[parent(i)]<A[i]) { swap(A[parent(i)],A[i]); i=parent(i); } } void maxHeapInsert(int A[],int heapSize,int key) { heapSize++; A[heapSize]=-32768; increaseKey(A,heapSize,key); } int main() { int a[15]={0,4,1,3,2,16, 9,10,14,8,7};//第0个为0仅仅是占位,不算,后面有10个元素 buildMaxHeap(a,10); for(int i=1;i<=10;i++) cout<<a[i]<<ends; cout<<endl<<endl; //heapSort(a,10); for(int i=1;i<=10;i++) cout<<a[i]<<ends; cout<<endl; cout<<"maximu"<<maximum(a)<<endl; cout<<"extractMax:"<<extractMax(a,10)<<endl; for(int i=1;i<=9;i++)//extract后heapSize减1了 cout<<a[i]<<ends; cout<<endl; increaseKey(a,6,16); for(int i=1;i<=9;i++)//extract后heapSize减1了 cout<<a[i]<<ends; }
习题:
6.5-8题目如下:请给出一个时间为O(nlgk)、用来将k个已排序链表合并为一个排序链表的算法。此处n为所有输入链表中元素的总数。(提示:用一个最小堆来做k路合并)。
更多:
http://www.cnblogs.com/Anker/archive/2013/01/23/2873422.html
http://www.cnblogs.com/dyingbleed/archive/2013/03/04/2941989.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
2011-07-26 为什么中国的网页设计那么烂?
2011-07-26 可视化的数据结构和算法 陈皓
2011-07-26 开源中最好的Web开发的资源 作者:酷壳
2011-07-26 程序员技术练级攻略 作者:酷壳
2011-07-26 写给新手程序员的一封信