1.简介
堆是什么?堆是一种特殊的完全二叉树,就像下面这棵树一样:
这棵树有一个很显著的特点,那就是所有父结点都要比子结点要小。符合这样要求的完全二叉树我们成为“最小堆”。反之,如果所有父结点都要比子结点大,这样的完全二叉树被称为”最大堆“。
2.下移ShiftDown
如果我们现在要删除掉最小的数字,并重新插入一个数字,再从中找出最小的数字。目前只能够先扫描所有的数字,找到最小的数,插入新的数字后再扫描所有的数字。这样子的时间复杂复是O(N²),那么有没有更好的方法呢?”堆“的结构恰好能够很好地解决了这个问题。
首先将数字按照最小堆的要求放进一颗完全二叉树,就像下面这棵树一样。然后用数组h存储这些数字。
很显然最小的数就在堆顶,假设存储这个堆的数组是h,那么最小的数字就是h[1]。接下来将堆顶的数字删除。将新增的数字28放入堆顶。此时显然不满足小顶堆的特性,我们需要将新增的数字调整到合适的位置。如何调整呢?
向下调整!我们需要将这个数字与它的两个儿子2和5比较,选择较小的一个和它交换,交换之后如下:
我们发现此时还是不符合最小堆的特性,因此还需要继续向下调整。于是继续将28与两个儿子12和7比较,选择较小的一个进行交换
此时还是不满足,继续向下调整:
现在我们已经找到符合最小堆的特性了。综上所述,当新增加一个数被放到顶堆时候,如果发现不符合最小堆的特性,就必须要向下调整,直到找到合适的位置位置。使其重新符合最小堆的特性。
向下调整的代码如下:
void shiftdown(int i) { int tmp; //临时变量,用来存储更新结点编号 int flag=0;//用来标记此顶点需不需要向下调整,0是需要调整,1是不需要 while(i*2<=n && flag==0)//当结点存在儿子时,至少存在左儿子,执行循环 { //首先判断与左儿子的辨析,用tmp记录值较小的结点编号 if(h[i]>h[i*2]) { tmp=i*2; } else { tmp=i; } if(i*2+1<=n) //如果有右儿子,再对右儿子进行讨论 { if(h[i*2+1]<h[i]) { tmp=i*2+1; } } if(tmp!=i) //如果发现最小的结点不是自己,说明子结点中还有值更小的结点 { swap(tmp,i); i=tmp; //更新刚才交换之后的儿子结点的编号,便于接下来向下调整。 } } return; }
3. 上移ShiftUp
如果只是想新增一个值,而不是删除最小值呢?即在原有的堆上面直接插入一个新元素。其实只需要在堆尾直接插入,再根据情况判断是否需要将新元素上移,直到满足最小堆的特性位置。例如我们现在要新增一个元素3:
先将3和它的父节点25比较,发现比父结点要小,交换。
交换之后发现还是比此时的父结点6小,再进行交换。此时满足了最小堆的特性。
向上调整的代码如下:
void shiftup(int i) { int flag=0; if(i==1) return;//此时是堆顶,不需要上调 while(i!=1 && flag=0) { if(h[i]<h[i/2]) { swap(i,i*2); } else { flag=1; } i=i/2;//更新编号i为它父结点的编号。 } return; }
4.建堆
说了半天,我们忽略了一个很重要的问题,就是如何建立这个堆。我们可以把所有数值直接放入一个完全二叉树中,并且用一个数组存储:
在这课完全二叉树中,我们从最有一个结点开始,依次判断以这个结点为根的子树是否符合最小堆的特性。如果所有的子树都符合最小堆的特性,那么这个就是最小堆了。
首先我们从叶结点开始。因为叶结点没有儿子,所以所有以叶结点为根结点的子树都符合最小堆的特性。因此所有叶结点可以不处理,直接跳过。
完全二叉树有一个很重要的性质:最有一个非叶结点是第n/2个结点。
利用这个性质,我们从第n/2个结点开始处理这颗完全二叉树。
下面是已经调整好的状态:
目前这棵树还是不符合最小堆的特性,我们继续调整:
调整完毕后:
实现的部分代码:
void create() { for(int i=n/2;i>=1;i--) { shiftdown(i); } return ; }
5.堆排序
堆还有一个作用就是堆排序,与快速排序一样,堆排序的时间复杂度是O(NlogN)。
堆排序的实现很简单,比如说我们现在要进行从小到大的排序,可以先建立最小堆,然后每次删除顶部元素并将顶部元素输出或者放入一个新的数组中,直到堆为空为止。最终输出或者存放在新数组中的数就已经是排序好了的。
int deletemin() //删除方法的堆排序 { int tmp; tmp=h[1];//临时记录第一个结点的值 h[1]=h[n];//将最大的值放到第一个位置。 n--;//将堆的大小-1 shiftdown(1); return tmp;//返回最小值。 }
当然堆排序还有一种更好的办法。从小到大排序的时候不建立”最小堆“而是建立”最大堆“!
最大堆建立好之后,最大的元素是h[1],因为我们需要从小到大排序,希望最大的数放在最后,那么我们将h[1]和h[n]交换,此时h[n]就是数组中最大的元素。最大的元素归位后,将堆的大小减1,n--,并将交换后的新h[1]向下调整维持特性。如此递归,直到堆的大小是1位置。此时数组h已经是排好序了。
void HeapSort() //交换思维的堆排序 { while(n>1) { swap(1,n); //将最小的元素放到堆顶,最大的元素放到h[n],此时h[n]就是最大的元素。 n--; //将堆的大小减小1 shiftdown(1); //向下调整 } }
6.源代码
#include <iostream> #include <algorithm> #include <cstdio> #include <cstdlib> #include <cstring> using namespace std; int h[1111]; //用来存储二叉树的数组。 int n;//堆的大小 void swap(int x,int y) { int temp; temp=h[x]; h[x]=h[y]; h[y]=temp; return; } void shiftdown(int i) { int tmp; //临时变量,用来存储更新结点编号 int flag=0;//用来标记此顶点需不需要向下调整,0是需要调整,1是不需要 while(i*2<=n && flag==0)//当结点存在儿子时,至少存在左儿子,执行循环 { //首先判断与左儿子的辨析,用tmp记录值较小的结点编号 if(h[i]<h[i*2]) { tmp=i*2; //用tmp记录较大的结点编号 } else { tmp=i; } if(i*2+1<=n) //如果有右儿子,再对右儿子进行讨论 { if(h[i*2+1]<h[tmp]) { tmp=i*2+1; } } if(tmp!=i) //如果发现最小的结点不是自己,说明子结点中还有值更小的结点 { swap(tmp,i); i=tmp; //更新刚才交换之后的儿子结点的编号,便于接下来向下调整。 } } return; } void shiftup(int i) { int flag=0; if(i==1) return;//此时是堆顶,不需要上调 while(i!=1 && flag==0) { if(h[i]<h[i/2]) { swap(i,i*2); } else { flag=1; } i=i/2;//更新编号i为它父结点的编号。 } return; } void create() { for(int i=n/2; i>=1; i--) { shiftdown(i); } return ; } void HeapSort() //交换思维的堆排序 { while(n>1) { swap(1,n); //将最小的元素放到堆顶,最大的元素放到h[n],此时h[n]就是最大的元素。 n--; //将堆的大小减小1 shiftdown(1); //向下调整 } } int main() { int num; cout << "请输入元素的个数:"<< endl; cin >> num; cout << "请输入各结点的元素:" << endl; for (int i=1;i<=num;i++) { cin >> h[i]; } n=num; create();//建堆 HeapSort();//排序 for (int i=1;i<=num;i++) { cout << h[i]; } cout << endl; return 0; }