数据结构与算法实战 5.1堆(优先队列 二叉堆)
1 *****优先队列与集合**** 2 **优先队列** priority_queue 3 优先队列:对元素进行优先级的区分,入队没有要求,出对的时候按照优先级出队 4 数组、链表、二叉树、二叉搜索树 都可以做成优先队列。 5 但是他们各有各的优缺点,如图所示:
6 更好的实现方案?如图
所示:![]()
![]()
7 8 课程地址: 9 https://www.icourse163.org/learn/QDU-1206503801?tid=1464154451#/learn/content?type=detail&id=1242523902&cid=1265642403&replay=true 10 *******代码实现******* 11 *******注意 下面实现的是小顶堆****** 12 //2020.12.27 代码可能有注释不够清楚的地方,请参考青岛大学周强老师的《数据结构实战》,下次复习时候我再更新代码。#include<stdlib.h> 13 #include<cstdio> 14 #include<cstdlib>//引入free函数 销毁malloc申请的内存空间 15 16 typedef int Elem; 17 18 /*写在最前面:我们所学的是二叉堆(用完全二叉树(数组)储存) 19 但是我们还有二项堆、Fib堆,这两种堆就不是用完全二叉树实现的 20 二叉堆只是prorityqueue这个结构实现的一种方式,而不是说优先队列等同于二叉堆 21 priortiyqueue还能用二项堆、avl树、线段树还有二进制分组的vector实现 22 */ 23 /* 24 堆的其他操作: 25 全部操作都首先要看这个堆是最大堆还是最小堆,细节上有些差异 26 1.提高元素优先级:提高优先级 + percolateup(因为提高优先级之后,这个位置不合适了,向上过滤) 27 2.降低元素优先级:降低优先级 + percolatedown(和提高优先级的的理由一致) 28 3.删除元素(不是堆顶的元素): 欲想让其灭亡,先让其膨胀,不是堆顶就提高它的优先级变成堆顶,那么我们的操作就变成了删除堆顶hh 29 */ 30 struct heap{ 31 //暂时用int代替Elem 32 //动态分配空间 33 Elem *data; 34 int capcity; 35 int size; 36 }; 37 38 typedef struct heap* Heap; 39 40 Heap initHeap(int max){ 41 Heap h; 42 h = (Heap)malloc(sizeof(struct heap)); 43 //不够内存分配时候 44 if(h == 0) return NULL; 45 h->data = (Elem*)malloc(sizeof(Elem)*(max+1)); 46 if(h->data == 0) return NULL; 47 h->size = 0; 48 return h; 49 //free自己写 50 } 51 52 void printHeap(Heap h){ 53 //为了方便查找,0号位置不放东西 54 for(int i = 1; i <= h->size; i ++){ 55 printf("%d ",h->data[i]); 56 } 57 putchar('\n'); 58 } 59 60 bool isEmpty(Heap h){ 61 return h->size == 0; 62 } 63 bool isFull(Heap h){ 64 return h->capcity == h->size; 65 } 66 67 //建立小顶堆 68 //从位置k开始向上过滤 69 //向上渗透一直到堆顶为止,或者直至它的上一个元素不大于它为止 70 void percolateUp(int k, Heap h){ 71 Elem x; 72 x = h->data[k]; 73 //父子相比大小,最后如果到了树根,就没法再往上走了,不用再和父亲比了,父亲是0号位置 74 //当然与0号位置相比可以采取“哨兵”的做法,我们这个程序不采用这样的写法 75 //另外,i是要继续用的,所以不能定义为for的局部变量 76 //循环判断条件解释:=1时为树根,小于父亲节点时候进行交换 77 int i; 78 for(i = k; i > 1 && h->data[i] < h->data[i/2];i = i / 2){ 79 //将小于x的父亲节点数据copy到这个位置 80 //我们不需要临时变量保存它,因为开头x就是临时变量 81 h->data[i] = h->data[i/2]; 82 } 83 //出来循环有两种情况: i到了树根位置 or 父亲节点比x小 84 //pps:我们当前的堆以最小堆为样例 85 h->data[i] = x; 86 } 87 //因为可能会失败,用boolean作为返回值 88 //插入的元素,插入的位置 89 bool insertHeap(Elem x, Heap h){ 90 if(isFull(h)) return false; 91 //size从0开始增长,可不就是++size的地方开始存放元素嘛 92 h->data[++h->size] = x; 93 percolateUp(h->size, h); 94 return true; 95 } 96 97 /*删除操作:堆顶元素为最大或者最小 所以删除显然是删除堆顶比较方便 98 本测试程序中以最小堆为示例,删除最小的元素 99 考虑堆是空的时候,抛出异常 100 */ 101 /*如果是删除堆顶这个位置,会导致堆顶空了一个位置,接着堆顶这个空位会与它的 102 左儿子和右儿子进行交换,把这个空位给交换下去,导致一块地方给空了出来 103 最终会破坏掉整棵完全二叉树的结构 104 (二叉树的深度为k,除第 k 层外,其它各层 (1~k-1) 的结点数都达到最大个数,第k 层所有的结点都连续集中在最左边,这就是完全二叉树) 105 那么我们可以交换最后一个元素和堆顶元素的位置,这样删除的就是最后一个元素的位置 106 和堆顶的值,交换之后向下渗透就完事 107 */ 108 109 //由于向上想下过滤并不一定只是适用于最后一个位置/最开始的位置 (比如堆排序.) 110 //所以我们在参数里面考虑用int k来指明位置 111 void percolateDown(int k, Heap h){ 112 //向下渗透 113 Elem x; 114 x = h->data[k]; 115 /*****/ 116 printf("已经保存的临时变量为:%d,从x:%d开始过滤\n",h->data[k],h->data[k]); 117 printf("当前h:"); 118 for(int i = 1; i <= h->size; i ++){ 119 printf("%d ",h->data[i]); 120 } 121 putchar('\n'); 122 /*****/ 123 //用i获取位置 124 //循环终止条件:没有儿子当然要停下来啦,当你比下一步的儿子还小的时候当然也要停下来啦 125 //i需要用来查看停止位置即原先第k号元素应该放的新位置 126 //所以i必须是一个for外面的变量 127 int i,child; 128 129 //疑惑:i*2=h->size的时候,下面语句中child++也就是i*2+1不会越界??? 130 //2021.7.25 听课不够认真。。。下面已经用child!=h->size限制了越界 不可能会发生越界的 131 for(i = k; i*2 <= h->size; i = child){ 132 //为什么在for里面不直接像percolateUp那样同理赋值成一个特定的儿子? 133 //因为我们在(小顶堆)向下过滤的时候,我们要找的是小儿子进行替换,而不是有儿子就进行替换 134 //先看左儿子 在下标1 2 3 4...这样顺序的情况下,完全二叉树的i节点的左右儿子位置为i*2 i*2+1 父节点位置i/2 135 //需不需要写成移位加快速度?不论是i+i,还是i*2,现在编译器都会优化成i<<1 左移,所以i+i 和i*2都可以 136 child = i * 2; 137 //注意越界 138 //判断条件解释:child不是最后一个,左儿子大于右儿子 139 /*****/ 140 printf("左儿子:%d,右儿子:%d\n",h->data[i*2],h->data[i*2+1]); 141 /*****/ 142 //不能直接写h->data[child] < h->data[child + 1],因为可能会越界 143 //用child!=h->size就能判断child不是堆h的最后一个元素,只要不是最后一个元素,child++就不会越界 144 //最大child = h->size - 1的时候,child++刚好是最后一个元素 145 //pps:child 左儿子 child+1就是右儿子嘛 146 //这个判断语句帮忙找出左儿子和右儿子哪个比较小 147 if(child != h->size && h->data[child] > h->data[child+1]){ 148 //更小的是右儿子 149 //否则就是维持默认的最小是左儿子,上面child的缺省值为i*2(左儿子) 150 child ++; 151 } 152 /****/ 153 printf("当前比较小的是:%d\n",h->data[child]); 154 /****/ 155 //i位置的数据大于小儿子 因为这个是最小堆 所以小儿子要往上挪 156 if(h->data[i] > h->data[child]){ 157 /***/ 158 printf("i位置元素:%d大于小儿子,小儿往上挪",h->data[i]); 159 /***/ 160 h->data[i] = h->data[child]; 161 }else{//只要没有发生交换 就已经可以停止了 162 break; 163 } 164 /****/ 165 printf("当前h:"); 166 printHeap(h); 167 /****/ 168 } 169 //出来以后,i到了叶节点 要么i处于比小儿子还小的位置上 170 /***/ 171 printf("最终i的位置为:%d\n",i); 172 printf("x为:%d",x); 173 /***/ 174 h->data[i] = x; 175 } 176 177 //参数:返回删除的元素,指定堆 178 //C语言没有异常的机制,需要一个返回值来确认是否成功 179 //删除的堆可能会是空的,就会异常,所以不直接用返回值来作为删除掉的元素 180 int removeHeap(Elem *px, Heap h){ 181 if(isEmpty(h)) return 0;//堆是空的,返回0 182 *px = h->data[1];//堆顶元素存到*px中 183 h->data[1] = h->data[h->size];//用最后一个元素上去顶替1号位置的堆顶 184 h->size --;//删除了少了一个 185 percolateDown(1,h);//把堆顶向下过滤 186 return 1; 187 } 188 189 //参数:指定列表?size?堆的最大容量 190 Heap buildHeap(Elem *a, int size, int max){ 191 Heap h; 192 h = initHeap(max); 193 //h为空 194 if(!h) return NULL; 195 h->size = size; 196 //不需要写h->capacity = max 因为initHeap里面已经有了 197 for(int i = 1; i <= size; i ++){//谨记 为了找父节点和儿子节点方便 我们是不用0号位置的 198 //a是从0开始的 堆是从1开始的!!! 199 h->data[i] = a[i - 1]; 200 printf("a[%d]:%d\n",i-1,a[i - 1]); 201 printf("h->data[%d]:%d\n",i,a[i - 1]); 202 } 203 //size / 2是最后一个节点的父亲,所以i=size/2就是找到最后一个有儿子的结点 204 //ppps:找父亲 i/2 找左节点 i*2 找右节点i*2 + 1 205 //放进去之后开始调整元素的顺序 206 //从什么地方开始调?最后一个有儿子的元素向下过滤 207 //不停地往上找父节点进行向下过滤,树根1号位置的数据可能也要调整,将所有的树调整为最小堆 208 //最后一个元素为h->data[size],那么它的父节点就是size/2 209 for(int i = size / 2; i > 0; i --){ 210 percolateDown(i, h); 211 } 212 return h; 213 } 214 215 void destroy(Heap h){ 216 free(h); 217 } 218 219 int main(){ 220 /*调试堆的代码*/ 221 Heap h; 222 h = initHeap(10); 223 insertHeap(20,h); 224 printHeap(h); 225 insertHeap(10,h); 226 printHeap(h); 227 insertHeap(5,h); 228 printHeap(h); 229 insertHeap(15,h); 230 insertHeap(30,h); 231 insertHeap(18,h); 232 printHeap(h); 233 Elem x; 234 removeHeap(&x,h); 235 printf("%d\n",x); 236 printHeap(h); 237 238 /*****基于一个列表创建堆***** 239 给出列表:90 20 70 10 30 40 15 25 35 45 11 80 240 创建最小堆 241 创建空堆O(logN),然后放进去 O(N) 242 总体效率为O(N*logN) 243 实际上我们还可以简化为O(N)的效率 244 分析一下:上面慢在哪里?因为每个元素插入都是放在最后面,要往上走最多要O(logN) 245 后来会越来越多元素,就越来越慢,我们就发现叶子点占据的操作时间非常多 246 能不能让这些数量最多的点的操作用的时间更少? 247 我们可以认为,叶子节点已经是一个堆,因为一个元素嘛,不论最大最小都是一个堆了, 248 那么叶子节点的上一层,这些点作为树根的树要成为最小堆,最多只需要调节一次, 249 直至最上面,树根,我们发现,只有这个调节时间是最长的,是logN,,而且它只会有一个 250 最终我们发现,占一半数量的叶子节点是不需要调节的,1/4的点调一层,1/8的点调两层 251 最后那个点要调logN层,总的时间为O(N). 252 //以下为根据列表创建堆的代码 253 254 /*Elem a[6] = {10, 50, 60, 5, 30, 20}; 255 //创建capcity为50的堆,首先将上面的六个元素放进去初始化 256 Heap h; 257 h = buildHeap(a, 6, 50); 258 printHeap(h); 259 for(int i = 1; i <= 6; i ++){ 260 printf("%d ",h->data[i]); 261 } */ 262 destroy(h); 263 return 0; 264 } 265 ***堆的其他操作:提高元素优先级 降低元素优先级 删除元素(不只是堆顶) 266 ***大顶堆,只要加大这个数,就能提高它的优先级,小顶堆则相反 267 ***如何提高元素优先级? 提高优先级后 + percolateUp 就好了 268 ***如何降低元素优先级? 降低优先级后 + percolateDown 就好了 269 ***实际中的应用:例如病人的病情加重,或者减弱... 270 ***删除元素?我们之前只学过删除堆顶,那么如果我们要删除一个特定的元素,只要把这个元素弄膨胀 271 ***让它变成堆顶,我们就能删除它了。 272 273 ***下面来看一下其他的语言如何使用堆*** 274 C++: 275 #include<iostream> 276 #include<queue> 277 278 using namespace std; 279 280 int main(){ 281 //默认为大顶堆 282 //小顶堆要写成priorit_queue<int, vector<int>, greater<int>> q; 283 priority_queue<int> q;//堆就是优先队列的一种形式 284 q.push(111); 285 q.push(222); 286 q.push(333); 287 q.push(11); 288 q.push(22); 289 cout << q.top() << endl; 290 q.pop(); 291 cout << q.top() << endl; 292 return 0; 293 } 294 295 JAVA: 296 297 import java.util.PriorityQueue; 298 299 public class Main{ 300 public static void main(String[] args){ 301 //默认为小顶堆 302 PriorityQueue<Integer> q = new PriorityQueue<>(); 303 //大顶堆写法 304 //补充知识点:因为JAVA的函数,必须依靠类存在,所以在下面我们采取一种叫匿名内部类的东西 305 //注意,10虽然传入了大小,但是即使add多了,也会自动扩容,无影响 306 //实现了comparator这个接口的话 必须要实现compare方法 307 //写comparator比较麻烦,我们可以采取写lambda表达式的方法 308 PriorityQueue<Integer> q1 = new PriorityQueue<>(10, 309 new Comparator<Integer>(){ 310 @override 311 public int compare(Integer o1, Integer o2){ 312 return o2 - o1; 313 } 314 }) 315 316 //Lambda表达式作为参数写法 317 PriorityQueue<Integer> q2 = new PriorityQueue<>(10, (a, b)->{return b - a;}); 318 //***** 319 q.add(111); 320 q.add(222); 321 q.add(333); 322 q.add(444); 323 q.add(11); 324 q.add(22); 325 //java的弹出会返回弹出的元素 和C++有点不一样 326 System.out.println(q.poll()); 327 } 328 }