前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >数据结构之堆

数据结构之堆

作者头像
别先生
发布2020-03-20 17:56:31
5850
发布2020-03-20 17:56:31
举报
文章被收录于专栏:别先生别先生

树结构的不同形态,堆、线段树、字段树、并查集,四种不同的树形数据结构。

1、优先队列,本身就是一种队列。普通队列,先进先出,后进后出。优先队列,出队顺序和入队顺序无关,和优先级有关,优先级高者先得,优先级高的先出队。

类别

入队

出队(拿出最大元素)

普通线性结构(数组、链表等)

O(1),直接将新的元素扔进去。

O(n),需要扫描一遍元素,找出其优先级最高的元素,拿出队列。对于数据结构来说,如果有一项操作是O(n)级别的,那么进行n个元素的操作,整个过程就会是n的平方的过程,相对来说,比较慢的。

顺序线性结构(维持线性结构的顺序,动态数组、链表等)

O(n),入队操作,需要找到需要插入的线性结构的位置。

O(1),只需要拿出队首或者队尾的元素即可。

O(logn),最差情况下

O(logn),最差情况下

2、完全二叉树,把元素顺序排列成树的形状。当我们把元素一层一层的排列成二叉树的形状的时候,得到的这棵树就是完全二叉树。

  二叉堆的性质,堆中某个节点的值总是不大于其父节点的值,就是根节点的元素是最大的元素,根节点的值总是大于等于其左右孩子的值。这样得到的堆称为最大堆,相应的可以定义最小堆。

3、最大堆,可以使用定义二分搜索树的Node类来实现堆,这里也可以使用数组,因为完全二叉树,就是一个一个节点按照顺序一层一层的码放出来,所以可以使用数组的方式来表示完全二叉树。

注意,此时将数组索引0位置空置出来了。如果不将索引为0位置空置出来,可以使用这样的计算方式,父节点的索引parent(i) = (i - 1) /2,左孩子节点的索引left child(i) = 2 * i + 1,右孩子节点的索引right child(i) = 2 * i + 2;

4、向堆中添加元素和Sift Up,从用户的角度来看是添加元素,从堆的角度来看,设计到一个非常基础的内部的操作,通常英文叫做Sift Up(堆中元素的上浮)。

堆中元素的上浮,最后的效果如下所示:

5、取出堆中的最大元素和Sift Down。最大堆,我们从堆中取出元素只取出堆顶的这个元素,也就是二叉树的根节点所在的这个元素,这个元素是堆中存储元素最大的元素,取出操作只能取出这个元素,而不能取出其他的元素,根节点很好访问,就是对于数组来说,索引为0这个位置的元素。

此时,在数组表示中,索引为0的位置存放就是16这个元素了,数组最后一个元素的位置也是16,此时将最后一个元素16删除掉,这样从元素的个数上减少了一,此时减少的元素就是堆顶的元素,此时数组表示满足完全二叉树的性质的。

此时,需要进行调整,这个调整的过程,调整的是堆顶根节点的元素16,我们需要将这个节点向下调,所以叫做Sift Down(数据的下沉),需要下沉的这个位置的元素,和它的左右孩子进行比较,选择它的两个孩子中最大的那个元素52,如果它的两个孩子中最大的那个元素52比它自己还大,它就和左右孩子中最大的进行交换位置,此时,交换完位置之后,此时的根节点的元素52比它左右孩子的元素都要大。

此时,交换完元素之后,对于16这个新的位置,它很有可能依然不满足堆的性质,它可能依然要进行下沉下去,这个过程继续进行。此时16的索引为1,来看它的左右孩子,最大的那个孩子的元素41比16大,所以他们两个交换位置,此时可以保证41这个节点它比左右孩子还要大。

此时,让元素41和元素16进行替换,效果如下所示:

此时,此时,交换完元素之后,对于16这个新的位置,它很有可能依然不满足堆的性质,它可能依然要进行下沉下去,这个过程继续进行。此时16的索引为4,来看它的左右孩子,此时只有左孩子,并且16大于左孩子15,所以此时,不用进行任何操作。下沉操作结束了,此时完成了从堆中取出一个元素,取出元素之后依然维持了堆的性质。

6、Heapify将任意数组整理成堆的形状,由于堆是一棵完全二叉树,所以可以使用数组来表示,此时,如何将任意数组整理成堆的形状。

此时,从最后一个非叶子节点22开始向前遍历,进行siftDown下沉操作,22和它的左右孩子进行比较,22比左孩子62小,就进行下沉。此时22已经是叶子节点了,下沉操作就结束了。

之后,看索引为3对应的节点13,找到13的左右孩子的最大值,然后13和左右孩子最大值41进行比较,13小于41,13和41交换位置,此时,13变成了叶子节点,无法进行下沉了,这次下沉操作结束。

此时,看索引为2所对应的节点19,找到19的左右孩子的最大值,然后19和左右孩子最大值28进行比较,19小于28,19和28交换位置,此时,19变成了叶子节点,无法进行下沉了,这次下沉操作结束。

此时,看索引为1所对应的节点17,找到17的左右孩子的最大值,然后17和左右孩子最大值62进行比较,17小于62,17和62交换位置,此时,17还有一个左孩子22,17和左孩子22进行比较,17小于22,所以17和22进行交换,此时17变成了叶子节点,无法进行下沉了,这次下沉操作结束。

此时,看索引为0所对应的节点15,找到15的左右孩子的最大值,然后15和左右孩子最大值62进行比较,15小于62,15和62交换位置。此时,15还有两个左右孩子,15和左右孩子最大值41进行比较,15小于41,所以15和41进行交换。此时,15还有两个左右孩子,15和左右孩子最大值30进行比较,15小于30,所以15和30进行交换,此时15变成了叶子节点,无法进行下沉了,这次下沉操作结束。

7、最大堆的实现代码,如下所示:

代码语言:javascript
复制
  1 package com.maxHeap;
  2 
  3 import com.company.Array;
  4 
  5 import java.util.Random;
  6 
  7 /**
  8  * 最大堆,由于规定了每个节点的值都要大于等于左右孩子节点的值,所以要具有可比较性
  9  */
 10 public class MaxHeap<E extends Comparable<E>> {
 11 
 12     // 动态数组
 13     public Array<E> data;
 14 
 15     // ctrl + f12查看方法,变量等等
 16     // fn + alt + insert 构造方法
 17 
 18     /**
 19      * 如果直到动态数组的大小,可以直接创建
 20      *
 21      * @param capacity
 22      */
 23     public MaxHeap(int capacity) {
 24         data = new Array<>(capacity);
 25     }
 26 
 27 
 28     /**
 29      * 传递的数组参数转换为堆的形状,放到data的数组中。
 30      * <p>
 31      * <p>
 32      * Heapify的算法复杂度,将n个元素逐个插入到一个空堆中,算法复杂度是O(nlogn)级别的。
 33      * <p>
 34      * <p>
 35      * 如果使用的是heapify的过程,算法复杂度是O(n)级别的。
 36      *
 37      * @param arr
 38      */
 39     public MaxHeap(E[] arr) {
 40         // 首先拷贝一份用户传递的数组元素,放入到动态数组中。
 41         // 根据传递的数组,生成一个新的动态数组
 42         data = new Array<>(arr);
 43         // 此时data已经存放了用户传递的arr数组元素的值
 44 
 45         // 从最后一个非叶子节点开始向前依次遍历,进行元素的下沉
 46         // 从最后一个非叶子节点开始,最后一个节点的索引的父亲节点的索引。
 47         for (int k = this.parent(arr.length - 1); k >= 0; k--) {
 48             this.siftDown(k);
 49         }
 50     }
 51 
 52     /**
 53      * 无参构造函数,如果不知道动态数组的大小,默认大小长度是10
 54      */
 55     public MaxHeap() {
 56         data = new Array<>();
 57     }
 58 
 59     /**
 60      * 返回堆中的元素个数
 61      *
 62      * @return
 63      */
 64     public int size() {
 65         return data.getSize();
 66     }
 67 
 68     /**
 69      * 返回一个布尔值,表示堆中是否为空
 70      *
 71      * @return
 72      */
 73     public boolean isEmpty() {
 74         return data.isEmpty();
 75     }
 76 
 77     /**
 78      * 返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
 79      *
 80      * @param index
 81      * @return
 82      */
 83     private int parent(int index) {
 84         if (index == 0) {
 85             throw new IllegalArgumentException("index-0 doesn't hava parent.");
 86         }
 87         return (index - 1) / 2;
 88     }
 89 
 90     /**
 91      * 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引。
 92      *
 93      * @param index
 94      * @return
 95      */
 96     private int leftChild(int index) {
 97         return index * 2 + 1;
 98     }
 99 
100     /**
101      * 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引。
102      *
103      * @param index
104      * @return
105      */
106     private int rightChild(int index) {
107         return index * 2 + 2;
108     }
109 
110 
111     /**
112      * 向堆中添加元素。时间复杂度是O(logn)
113      *
114      * @param e
115      */
116     public void add(E e) {
117         // 向动态数组中添加一个元素
118         data.addLast(e);
119 
120         // 开始维护堆的性质
121         // 参数是,希望上浮的那个元素所对应的索引是多少,即最后一个元素的索引
122         siftUp(data.getSize() - 1);
123     }
124 
125     /**
126      * 元素上浮过程
127      *
128      * @param k
129      */
130     private void siftUp(int k) {
131         // 首先,不能已经达到了根节点,所以k必须大于零的。
132         // 如果该节点的父亲节点小于当前节点就进行上浮操作
133         while (k > 0 && data.get(this.parent(k)).compareTo(data.get(k)) < 0) {
134             // 将当前节点所处的元素和父亲节点的元素进行比较
135             // 如果当前节点所处的元素大于父亲节点的元素的值,将它们进行交换,进行上浮的动作。
136             data.swap(k, this.parent(k));
137             // 此时,让该节点元素的父亲节点的索引赋值给当前元素,
138             // 下一轮循环的时候,可以看当前的k已经来到了新的位置,
139             // 对于新的位置,是不是依然不满足堆的性质,就是它在新的位置
140             // 上的父亲节点所在的元素还要大,还要大就进行交换,依次类推。
141             // 直到走到了索引为0或者该节点小于父亲节点的元素。
142             k = parent(k);
143         }
144     }
145 
146     /**
147      * 看堆中的最大元素
148      *
149      * @return
150      */
151     public E findMax() {
152         if (data.getSize() == 0) {
153             throw new IllegalArgumentException("Can not findMax when heap is empty.");
154         }
155         return data.get(0);
156     }
157 
158     /**
159      * 取出堆中最大元素。时间复杂度是O(logn)
160      *
161      * @return
162      */
163     public E extractMax() {
164         // 首先,保存堆中的最大元素,暂存最大元素
165         E ret = this.findMax();
166         // 将堆顶元素和最后一个元素交换位置
167         data.swap(0, data.getSize() - 1);
168         // 交换完位置之后,此时已经将堆顶的元素放到数组的末尾了,数组末尾元素放在了堆顶位置
169 
170         // 删除最大元素,删除处于数组末尾的堆顶元素
171         data.removeLast();
172 
173         // 开始进行元素的下沉,参数是索引位置为0
174         siftDown(0);
175 
176         // 将最大元素返回,然后将最大元素从堆中删除
177         return ret;
178     }
179 
180     /**
181      * @param k 索引K
182      */
183     private void siftDown(int k) {
184         // 循环遍历,什么时候结束呢,
185         // 最极端的情况,k这个位置所处的节点,已经没有孩子了,当下沉到叶子节点的时候就结束了,
186         // 即,如果对于k这个节点的左孩子所在的索引小于数组的长度的时候,
187         // 此时k的左孩子都已经越界了,肯定一个孩子都没有了,此时右孩子的索引比左孩子的索引大。
188         // 循环的意思是,如果左孩子小于数组长度,就一直循环,否则就终止循环。
189         while (leftChild(k) < data.getSize()) {
190             // 比较k这个节点和k的左右孩子中最大的那个节点,看看k和这个最大的节点到底那个比较大,
191             // 先找到k左右孩子中最大的节点。
192             // 左孩子肯定是存在的,因为leftChild(k)小于数组的长度。
193             int j = this.leftChild(k);
194             // 但是右孩子不一定存在,所以此时进行判断,j + 1就是k的右孩子所在的索引,
195             // 如果K的右孩子小于数组的长度,说明有右孩子。
196             // 如果k的右孩子大于k的左孩子
197             if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) {
198                 // data[j]是leftChild和rightChild中的最大值
199                 // j自增。此时j存储的是右孩子所对应的索引了。
200                 // j++;
201                 j = rightChild(k);
202                 // 如果k没有右孩子,或者k的左孩子大于右孩子,那么j存储的就是左孩子所对应的索引。
203             }
204 
205 
206             // 此时,将k节点和左右孩子最大的节点进行比较,如果发现了就终止本次循环。
207             // 因为此时,父亲节点的值大于左右孩子的值了,下沉操作结束了。
208             if (data.get(k).compareTo(data.get(j)) >= 0) {
209                 break;
210             }
211 
212             // 否则,就开始交换k这个索引位置和j这个索引位置的值。
213             data.swap(k, j);
214             // 交换结束之后,将j的赋值给k。进行下一轮循环,对于新的一轮k来说,是否需要继续下沉。
215             k = j;
216         }
217 
218     }
219 
220 
221     /**
222      * 取出堆中的最大元素,并且替换成元素e。
223      * <p>
224      * <p>
225      * Replace是取出最大元素后,放入一个新元素。此时,堆中元素的总数未发生改变,
226      * 此时可以使用这样方法来实现,首先可以先extractMax,然后再add操作,
227      * 两次O(logn)的操作。但是呢,可以直接将堆顶元素替换以后Sift Down(元素进行下沉操作),
228      * 这样的话是一次O(logn)的操作。
229      *
230      * @param e
231      * @return
232      */
233     public E replace(E e) {
234         // 取出最大元素的值
235         E ret = findMax();
236         // 此时,将0索引位置的元素替换成e
237         data.set(0, e);
238         // 然后使用下沉元素,使得根节点元素符合堆的性质
239         siftDown(0);
240 
241         // 将最大元素的值返回
242         return ret;
243     }
244 
245 
246     /**
247      * 对使用heapify或者不使用的时候进行性能测试
248      * <p>
249      * 使用heapify的方式进行元素添加,或者自己将元素添加到空堆中。
250      *
251      * @param data
252      * @param isHeapify 是否使用heapify
253      * @return
254      */
255     private static double performanceTesting(Integer[] data, boolean isHeapify) {
256         // 创建一个变量存储开始时间
257         long startTime = System.nanoTime();
258 
259         // 创建一个变量
260         MaxHeap<Integer> maxHeap;
261         // 判断是否使用heapify
262         if (isHeapify) {
263             // true是使用heapify,此时将数组传入到构造函数中就实现了使用heapify方法
264             maxHeap = new MaxHeap<>(data);
265         } else {
266             // 创建一个maxHeap对象
267             maxHeap = new MaxHeap<>();
268             // 循环遍历,将所有的元素值都新增到堆中
269             for (int i = 0; i < data.length; i++) {
270                 maxHeap.add(data[i]);
271             }
272         }
273 
274         // 验证堆的正确性
275         // 创建一个一百万长度的数组arr
276         int[] arr = new int[data.length];
277         // 取出最大元素。此时是循环一百万次,是对堆进行了一遍排序,从大到小进行排序的。
278         for (int i = 0; i < data.length; i++) {
279             // 将堆中最大的元素依次放入到数组中
280             arr[i] = maxHeap.extractMax();
281         }
282 
283         // 循环判断,如果下一个元素的值小于这个元素,就抛出异常
284         // 即前一个数小于后一个数,此时不是降序的序列了。
285         for (int i = 1; i < data.length; i++) {
286             if (arr[i - 1] < arr[i]) {
287                 throw new IllegalArgumentException("Error");
288             }
289         }
290 
291         // 如果是降序数组,此时正常执行这句话
292         System.out.println("Test MaxHeap completed.");
293 
294         // 创建一个变量存储结束时间
295         long endTime = System.nanoTime();
296         // 将纳秒转换为秒返回即可
297         return (endTime - startTime) / 1000000000.0;
298     }
299 
300 
301     public static void main(String[] args) {
302 //        // 初始化一百万
303 //        int n = 1000000;
304 //        // 初始化堆
305 //        MaxHeap<Integer> maxHeap = new MaxHeap<>();
306 //        // 创建一个随机对象
307 //        Random random = new Random();
308 //        // 将一百万个随机数扔到堆中
309 //        for (int i = 0; i < n; i++) {
310 //            // 0-到Integet最大值
311 //            maxHeap.add(random.nextInt(Integer.MAX_VALUE));
312 //        }
313 //
314 //        // 创建一个一百万长度的数组arr
315 //        int[] arr = new int[n];
316 //        // 取出最大元素。此时是循环一百万次,是对堆进行了一遍排序,从大到小进行排序的。
317 //        for (int i = 0; i < n; i++) {
318 //            // 将堆中最大的元素依次放入到数组中
319 //            arr[i] = maxHeap.extractMax();
320 //        }
321 //
322 //        // 循环判断,如果下一个元素的值小于这个元素,就抛出异常
323 //        // 即前一个数小于后一个数,此时不是降序的序列了。
324 //        for (int i = 1; i < n; i++) {
325 //            if (arr[i - 1] < arr[i]) {
326 //                throw new IllegalArgumentException("Error");
327 //            }
328 //        }
329 //
330 //        System.out.println("Test MaxHeap completed.");
331 
332 
333         // 初始化一百万
334         int n = 1000000;
335         // 初始化数组
336         Integer[] data = new Integer[n];
337         // 创建一个随机对象
338         Random random = new Random();
339         // 将一百万个随机数扔到堆中
340         for (int i = 0; i < n; i++) {
341             // 0-到Integet最大值
342             data[i] = random.nextInt(Integer.MAX_VALUE);
343         }
344 
345         // 第一次使用的是一个一个元素新增到堆中
346         double time1 = MaxHeap.performanceTesting(data, false);
347         System.out.println("Without heapify time1 : " + time1);
348 
349         // 第二次使用heapify的方式将元素添加到堆中
350         double time2 = MaxHeap.performanceTesting(data, true);
351         System.out.println("with heapify time2 : " + time2);
352 
353     }
354 
355 }
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-03-18 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档