要期中考了……我真的是什么也不会啊,书都没看过TAT。
好吧整理一下二叉堆,这里就以最大堆为例好了。
首先二叉堆其实是一棵CBT,满足父节点的键值大于左右子节点的键值(wikipedia把这个叫键值,其实我也不知道该叫什么),并且左右子树也是一个二叉堆。二叉堆一般是用数组来存储的(斜堆、左式堆是用树的结构)。
如果根节点在数组下标1的位置的话,数组下标n的位置的节点的儿子坐标是2n和2n+1。如果根节点在下标为x的位置的话,记住这个结论就可以求得此情况下数组下标为n的位置的儿子的下标,我们可以先假设将根节点挪到下标为1的位置上,相当于整个堆向左移(x - 1)个位置,那么原来下标为n的位置移动后的下标是(n - (x - 1)),代入之前的公式可以求得移动后,下标为(n - (x - 1))的位置的两个儿子此时的下标为2(n - (x - 1))和2(n - (x - 1))+ 1,最后再将整个堆向右移(x - 1)个位置,得到在原先的堆中,下标为n的位置的节点的儿子的下标为2(n - (x - 1))+ (x - 1)和2(n - (x - 1))+ (x - 1)+ 1,即2n - x + 1和2n - x + 2。
如果根节点在数组下标为1的位置的话,数组下标为n的位置的节点的父节点坐标是floor(n / 2),这里的floor是下取整函数,用上面同样的思想可以求得当根节点下标为x时,下标为n位置的节点的父节点下标为floor((n + x - 1)/ 2)。
如果是根节点下标为1的d堆,则下标位置为n的节点的父节点和最左子节点及最右子节点的下标值分别为:floor((n + d - 2)/ d),(n - 1)d + 2,nd + 1。更一般的结果,节点下标为n的第k个孩子的下标为(n - 1)d + 1 + k。
如果二叉堆有n个节点,则堆的高度为floor(logn),这个和二叉树的性质是一样的。如果是d堆的话,则有floor(logd(n(d - 1)))。
如果二叉堆有n个节点,那么下标大于floor(n / 2)的位置的节点(即堆在数组中最后一个节点的父节点)均为叶节点,证明这个结论可以用反证法,假设下标大于floor(n / 2)的节点中有一个不是叶节点,那么这个节点的子节点的存储位置的下标必然大于n,而这个堆在数组中的最大下标已知是n,产生矛盾,这样就可以证明这个结论。而最大堆中的最小值也一定在某个叶节点中,因为非叶节点的子节点值一定比它自身小。
由无序数组构建堆
然后讲一下构建堆的操作。
由一个无序数组构建堆最直观的方法是将其一个个插入一个空树中,这样的方法是最low的,时间复杂度是O(NlgN)。
其次是利用Max-Heapify算法。Max-Heapify算法就是对任意一个节点,假设它的左右子树都已经满足堆序(如果无左右子树则认为其已经满足堆序,如果只有左子树则比较左子树即可),那么只要比较左右子树的根节点和自身的大小,如果三个节点也满足堆序那么就不需要做任何操作,如果不满足就交换三者中两者的位置使三者满足堆序并对相应的根节点被改变的那棵子树使用Max-Heapify算法进行调整,这样的算法如果用递归实现的话时间复杂度可以表示为 T ( N ) <= T (2 N / 3) + Θ (1)(因为对子树进行递归调用,子树最坏情况大约为2n/3个节点,即原来一整棵树的最后一层恰好是左半边全满的情况),解得为O(lgN)。
而如果对树的每一个节点均调用一次Max-Heapify,这样也还是比较浪费,因为可以知道节点总数为n的堆下标大于floor(n / 2)的位置的节点均为叶节点(上面有讲过这一点),叶节点可以认为是一个已经符合要求的二叉树,那么只需对非叶节点调用Max-Heapify就可以了,这样构建堆的时间复杂度为O(N)计算如下:
其中N是节点数,n是节点height。
下面是这种建堆算法的代码:
void Max_Heapify(int H[], int size, int root){ int l_child = root * 2; int r_child = root * 2 + 1; if(l_child <= size&&r_child <= size){ if(H[l_child] < H[r_child]&&H[root] < H[r_child]){ swap(H, r_child, root); Max_Heapify(H, size, r_child); } else if(H[r_child] < H[l_child]&&H[root] < H[l_child]){ swap(H, l_child, root); Max_Heapify(H, size, l_child); } else{ return; } } else if(l_child <= size){ if(H[root] < H[l_child]){ swap(H, l_child, root); Max_Heapify(H, size, l_child); } else{ return; } } else{ return; } } void Build_Max_Heapify(int H[], int size){ int i; for(i = size / 2; i >= 1; i--){ Max_Heapify(H, size, i); } }
插入节点
插入节点的时间复杂度是O(logN),注意循环中如果没有满足跳出循环的条件,tmp要整除2,代码如下:
void Insert(int H[], int size, int val){ int tmp = size + 1; if(tmp >= MaxSize) return; while(tmp / 2 >= 1){ if(H[tmp / 2] < val){ H[tmp] = H[tmp / 2]; tmp = tmp / 2; } else{
break; } } H[tmp] = val; }
当然插入节点也可以用Max-Heapify来实现:
void Insert_2(int H[], int size, int val){ int i; if(size + 1 >= MaxSize) return; H[size + 1] = val; for(i = (size + 1) / 2; i >= 1; i = i / 2){ Max_Heapify(H, size + 1, i); } }
删除根节点
删除节点的时间复杂度是O(logN),注意如果最后tmp是某叶节点下标,则直接赋值并跳出循环,代码如下:
void Delete_Root(int H[], int size){ int tmp = 1, val = H[size]; while(1){ if(tmp * 2 + 1 <= size - 1){ if(H[tmp * 2] < H[tmp * 2 + 1]&&val < H[tmp * 2 + 1]){ H[tmp] = H[tmp * 2 + 1]; tmp = tmp * 2 +1; } else if(H[tmp * 2 + 1] < H[tmp * 2]&&val < H[tmp * 2]){ H[tmp] = H[tmp * 2]; tmp = tmp * 2; } else{
break; } } else if(tmp * 2 <= size - 1){ if(H[tmp * 2] > val){ H[tmp] = H[tmp * 2]; tmp = tmp * 2; } else{
break; } } else{
break; } }
H[tmp] = val; }
当然删除节点也可以用Max-Heapify来实现:
void Delete_Root_2(int H[], int size){ H[1] = H[size]; Max_Heapify(H, size - 1, 1); }
调整节点的值
调整节点的值主要有增大和减小两种情况,增大节点值时,以该节点为根的子树一定符合堆序,所以只要向上延其祖先节点的路径调整堆序即可,减小节点值时,以该节点为根的子树不一定符合堆序,而整棵树的其余部分一定符合堆序,所以只要向下对子树调整堆序即可。代码就不写了,反正就是调用Max_Heapify。
删除任意节点
删除任意节点的算法和调整节点值的算法差不多,将数组最后一个节点的值移动到被删除节点的位置,如果该节点值变大了,则相当于使用增大节点值的算法,反之则相当于使用减小节点值的算法。
当然也可以使用增大节点值的算法将被删除节点的值变为无穷大,则其最终将出现在根节点位置上,最后再调用删除根节点的算法。
void Delete_Node(int H[], int index, int size){ if(H[index] < H[size]){ while(index / 2 >= 1){ if(H[index / 2] < H[size]){ H[index] = H[index / 2]; } else{ break; } index /= 2; } H[index] = H[size]; } else{ while(index * 2 <= size - 1){ if(index * 2 + 1 <= size - 1&&H[index * 2] < H[index * 2 + 1]){ if(H[index * 2 + 1] > H[size]){ H[index] = H[index * 2 + 1]; index = index * 2 + 1; } else{ break; } } else{ if(H[index * 2] > H[size]){ H[index] = H[index * 2]; index = index * 2; } else{ break; } } } H[index] = H[size]; } }
合并二叉堆
合并操作并不是二叉堆所擅长的,左式堆和斜堆什么的会好一点,斐波那契堆我不会啊……事实上左式堆的插入和删除操作都是通过合并操作来实现的。合并二叉堆的话,有两种方法,一种是将第二个堆一个个的插入第一个堆,算法复杂度为O(MlogN),另一种是直接接在第一个堆的数组后面,再维护,算法复杂度是O(2M + N),这里注意一下直接接在后面依次赋值也是需要时间的,为O(M)。
最后是完整的测试代码:
// // main.c // Binary Heap // // Created by 余南龙 on 2016/11/18. // Copyright © 2016年 余南龙. All rights reserved. // #include <stdio.h> #define MaxSize 100 void swap(int H[], int a, int b){ int tmp = H[a]; H[a] = H[b]; H[b] = tmp; } void Max_Heapify(int H[], int size, int root){ int l_child = root * 2; int r_child = root * 2 + 1; if(r_child <= size){ if(H[l_child] < H[r_child]&&H[root] < H[r_child]){ swap(H, r_child, root); Max_Heapify(H, size, r_child); } else if(H[r_child] < H[l_child]&&H[root] < H[l_child]){ swap(H, l_child, root); Max_Heapify(H, size, l_child); } else{ return; } } else if(l_child <= size){ if(H[root] < H[l_child]){ swap(H, l_child, root); Max_Heapify(H, size, l_child); } else{ return; } } else{ return; } } void Build_Max_Heapify(int H[], int size){ int i; for(i = size / 2; i >= 1; i--){ Max_Heapify(H, size, i); } } int Initial(int H[]){ int size = 0, val; while(1){ scanf("%d", &val); if(-1 == val){ break; } else{ H[++size] = val; } } return size; } void Output(int H[], int size){ int i; for(i = 1; i <= size; i++){ printf("%d ", H[i]); } putchar('\n'); } void Insert(int H[], int size, int val){ int tmp = size + 1; if(tmp >= MaxSize) return; while(tmp / 2 >= 1){ if(H[tmp / 2] < val){ H[tmp] = H[tmp / 2]; tmp = tmp / 2; } else{ H[tmp] = val; break; } } if(tmp == 1){ H[tmp] = val; } } void Insert_2(int H[], int size, int val){ int i; if(size + 1 >= MaxSize) return; H[size + 1] = val; for(i = (size + 1) / 2; i >= 1; i = i / 2){ Max_Heapify(H, size + 1, i); } } void Delete_Root(int H[], int size){ int tmp = 1, val = H[size]; while(1){ if(tmp * 2 + 1 <= size - 1){ if(H[tmp * 2] < H[tmp * 2 + 1]&&val < H[tmp * 2 + 1]){ H[tmp] = H[tmp * 2 + 1]; tmp = tmp * 2 +1; } else if(H[tmp * 2 + 1] < H[tmp * 2]&&val < H[tmp * 2]){ H[tmp] = H[tmp * 2]; tmp = tmp * 2; } else{ H[tmp] = val; break; } } else if(tmp * 2 <= size - 1){ if(H[tmp * 2] > val){ H[tmp] = H[tmp * 2]; tmp = tmp * 2; } else{ H[tmp] = val; break; } } else{ H[tmp] = val; break; } } } void Delete_Root_2(int H[], int size){ H[1] = H[size]; Max_Heapify(H, size - 1, 1); } void Delete_Node(int H[], int index, int size){ if(H[index] < H[size]){ while(index / 2 >= 1){ if(H[index / 2] < H[size]){ H[index] = H[index / 2]; } else{ break; } index /= 2; } H[index] = H[size]; } else{ while(index * 2 <= size - 1){ if(index * 2 + 1 <= size - 1&&H[index * 2] < H[index * 2 + 1]){ if(H[index * 2 + 1] > H[size]){ H[index] = H[index * 2 + 1]; index = index * 2 + 1; } else{ break; } } else{ if(H[index * 2] > H[size]){ H[index] = H[index * 2]; index = index * 2; } else{ break; } } } H[index] = H[size]; } } int main(){ int H[MaxSize], size, val, index; printf("Input an array using -1 as an end:"); size = Initial(H); Build_Max_Heapify(H, size); printf("Which number do you want to insert to the Heap:"); scanf("%d", &val); Insert_2(H, size, val); size++; Output(H, size); printf("Delete the root:\n"); Delete_Root_2(H, size); size--; Output(H, size); printf("Which node do you want to delete:"); scanf("%d", &index); Delete_Node(H, index, size); size--; Output(H, size); }