数据结构与算法实战 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 }

 

posted @ 2021-01-08 09:55  WriteOnce_layForever  阅读(103)  评论(0)    收藏  举报