浅谈堆-Heap(一)

应用场景和前置知识复习

  • 堆排序html

排序咱们都很熟悉,如冒泡排序、选择排序、希尔排序、归并排序、快速排序等,其实堆也能够用来排序,严格来讲这里所说的堆是一种数据结构,排序只是它的应用场景之一数组

  • Top N的求解数据结构

  • 优先队列ide

堆得另外一个重要的应用场景就是优先队列测试

咱们知道普通队列是:先进先出优化

而 优先队列:出队顺序和入队顺序无关;和优先级相关this

实际生活中有不少优先队列的场景,如医院看病,急诊病人是最优先的,虽然这一类病人可能比普通病人到的晚,可是他们可能随时有生命危险,须要及时进行治疗. 再好比 操做系统要"同时"执行多个任务,实际上现代操做系统都会将CPU的执行周期划分红很是小的时间片断,每一个时间片断只能执行一个任务,究竟要执行哪一个任务,是有每一个任务的优先级决定的.每一个任务都有一个优先级.操做系统动态的每一次选择一个优先级最高的任务执行.要让操做系统动态的选择优先级最高的任务去执行,就须要维护一个优先队列,也就是说全部任务都会进入这个优先队列.spa

 

基本实现

首先堆是一颗二叉树,这个二叉树必须知足两个两条件操作系统

  1. 这个二叉树必须是一颗彻底二叉树,所谓彻底二叉树就是除了最后一层外,其余层的节点的个数必须是最大值,且最后一层的节点都必须集中在左侧.即最后一层从左往右数节点必须是紧挨着的,不能是中间空出一个,右边还有兄弟节点.3d

  2. 这个二叉树必须知足 左右子树的节点值必须小于或等于自身的值(大顶堆) 或者 左右子树的节点值必须大于或等于自身的值(小顶堆)

下图分别是一个大顶堆和小顶堆的示例

 

看到这两颗二叉树,咱们首先就能定义出树节点的结构:

 1 Class Node {  2     //节点自己的值
 3     private Object value;  4     
 5     private Node left;  6     
 7     private Node right;  8     
 9  ....getter and setter 10     
11 }

可是这里咱们利用彻底二叉树的性质用数组来构建这棵树.先从上到下,自左至右的来给树的每个节点编上号.

以大顶堆为例

标上编号后,咱们发现每一个节点的左子节点(若是存在)的序号都是其自身的2倍,右子节点(若是存在)的序号是其自身的2倍加1. 相反,若是已知某个节点的序号,父节点的序号是其自身的二分之一(计算机中整型相除,舍弃余数)下面来用代码构建一个堆的骨骼

public class MaxHeap { /* * 堆中有多少元素 */
    private int count; /* * 存放堆数据的数组 */
    private Object[] data; public MaxHeap(int capacity) { /* * 由于序号是从1 开始的,咱们不用下标是0的这个位置的数 */
        this.data = new Object[capacity + 1]; } /** * 返回堆中有多少数据 * @return
     */
    public int size() { return count; } /** * 堆是否还有元素 * @return
     */
    public boolean isEmpty() { return count == 0; } }
View Code

骨骼是构建好了,乍一看堆中存放的数据是一个object类型的数据, 父子节点按节点值 没法比较,这里再调整一下

 1 public class MaxHeap<T extends Comparable<T>> {  2 
 3     /*
 4  * 堆中有多少元素  5      */
 6     private int count;  7 
 8     /*
 9  * 存放堆数据的数组 10      */
11     private Object[] data; 12     
13     /**
14  * 堆的容量 15      */
16     private int capacity; 17 
18     /**
19  * @param clazz 堆里放的元素的类型 20  * @param capacity 堆的容量 21      */
22     public MaxHeap(int capacity) { 23         /*
24  * 由于序号是从1 开始的,咱们不用下标是0的这个位置的数 25          */
26         this.data = new Object[capacity + 1]; 27         this.capacity = capacity; 28  } 29 
30     /**
31  * 返回堆中有多少数据 32  * 33  * @return
34      */
35     public int size() { 36         return count; 37  } 38 
39     /**
40  * 堆是否还有元素 41  * 42  * @return
43      */
44     public boolean isEmpty() { 45         return count == 0; 46  } 47 
48     public Object[] getData() { 49         return data; 50  } 51 }

这样骨架算是相对无缺了,下面实现向堆中添加数据的过程,首先咱们先把上面的二叉树的形式按标号映射成数组的形式如图对比(已经说了0号下标暂时不用)

如今这个大顶堆被映射成数组,因此向堆中插入元素,至关于给数组添加元素,这里咱们规定每新插入一个元素就插在当前数组最后面,也即数组最大标 + 1的位置处.对于一颗彻底二叉树来讲就是插在最后一层的靠左处,若是当前二叉树是一颗满二叉树,则新开辟一层,插在最后一层最左侧.可是这样插入有可能破坏堆的性质. 如插入节点45

 

插入新节点后已经破坏了大顶堆的性质,由于45比父节点17大, 这里咱们只要把新插入的节点45和父节点17 交换,相似依次比较与父节点的大小作交换便可

第一次交换:

第二次交换:

这里咱们发现通过两次交换,已经知足了堆的性质,这样咱们就完成了一次插入,这个过程,咱们发现待插入的元素至底向顶依次向树根上升,咱们给这个过程起个名叫shiftUp,用代码实现即是:

 1 /**
 2  * 插入元素t到堆中  3  * @param t  4      */
 5     public void insert(T t) {  6         //把这个元素插入到数组的尾部,这时堆的性质可能被破坏
 7         data[count + 1] = t;  8         //插入一个元素,元素的个数增长1
 9         count++; 10         //移动数据,进行shiftUp操做,修正堆
11  shiftUp(count); 12 
13  } 14 
15     private void shiftUp(int index) { 16         while (index > 1 && ((((T) data[index]). 17                 compareTo((T) data[index >> 1]) > 0))) { 18             swap(index, index >>> 1); 19             index >>>= 1; 20  } 21  } 22 
23     /**
24  * 这里使用引用交换,防止基本类型值传递 25  * @param index1 26  * @param index2 27      */
28     private void swap(int index1, int index2) { 29         T tmp = (T) data[index1]; 30         data[index1] = data[index2]; 31         data[index2] = tmp; 32     }

这里有一个隐藏的问题,初始化咱们指定了存放数据数组的大小,随着数据不断的添加,总会有数组越界的这一天.具体体如今以上代码 data[count + 1] = t 这一行

 1    /**
 2  * 插入元素t到堆中  3  * @param t  4      */
 5     public void insert(T t) {  6         //把这个元素插入到数组的尾部,这时堆的性质可能被破坏
 7         data[count + 1] = t;   //这一行会引起数组越界异常  8         //插入一个元素,元素的个数增长1
 9         count++; 10         //移动数据,进行shiftUp操做,修正堆
11  shiftUp(count); 12 
13     }

 

咱们能够考虑在插入以前判断一下容量

 1     /**
 2  * 插入元素t到堆中  3  * @param t  4      */
 5     public void insert(T t) {  6         //插入的方法加入容量限制判断
 7         if(count + 1 > capacity)  8             throw new IndexOutOfBoundsException("can't insert a new element...");  9         //把这个元素插入到数组的尾部,这时堆的性质可能被破坏
10         data[count + 1] = t;   //这一行会引起数组越界异常 11         //插入一个元素,元素的个数增长1
12         count++; 13         //移动数据,进行shiftUp操做,修正堆
14  shiftUp(count); 15 
16     }

 

至此,整个大顶堆的插入已经还算完美了,来一波儿数据测试一下,应该不是问题

可能上面插入时咱们看到有shiftUp这个操做,可能会想到从堆中删除元素是否是shiftDown这个操做. 没错就是shiftDown,只不过是删除堆中元素只能删除根节点元素,对于大顶堆也就是剔除最大的元素.下面咱们用图说明一下.

 

删除掉根节点,那根节点的元素由谁来补呢. 简单,直接剁掉原来数组中最后一个元素,也就是大顶堆中最后一层最后一个元素,摘了补给根节点便可,相应的堆中元素的个数要减一

 

最终咱们删除了大顶堆中最大的元素,也就是根节点,堆中序号最大的元素变成了根节点.

 

此时整个堆不知足大顶堆的性质,由于根节点17比其子节点小,这时,shiftDown就管用了,只须要把自身与子节点交换便可,但是子节点有两个,与哪一个交换呢,若是和右子节点30交换,30变成父节点,比左子节点45小,仍是不知足大顶堆的性质.因此应该依次与左子节点最大的那个交换,直至父节点比子节点大才可.因此剔除后新被替换的根节点依次下沉,因此这个过程被称为shiftDown,最终变成

因此移除最大元素的方法实现:

 1 /**
 2  * 弹出最大的元素并返回  3  *  4  * @return
 5      */
 6     public T popMax() {  7         if (count <= 0)  8             throw new IndexOutOfBoundsException("empty heap");  9         T max = (T) data[1]; 10         //把最后一个元素补给根节点
11         swap(1, count); 12         //补完后元素个数减一
13         count--; 14         //下沉操做
15         shiftDown(1); 16         return max; 17  } 18 
19     /**
20  * 下沉 21  * 22  * @param index 23      */
24     private void shiftDown(int index) { 25         //只要这个index对应的节点有左子节点(彻底二叉树中不存在 一个节点只有 右子节点没有左子节点)
26         while (count >= (index << 1)) { 27             //比较左右节点谁大,当前节点跟谁换位置 28             //左子节点的inedx
29             int left = index << 1; 30             //右子节点则是
31             int right = left + 1; 32             //若是右子节点存在,且右子节点比左子节点大,则当前节点与右子节点交换
33             if (right <= count) { 34                 //有右子节点
35                 if ((((T)data[left]).compareTo((T)data[right]) < 0)) { 36                     //左子节点比右子节点小,且节点值比右子节点小
37                     if (((T)data[index]).compareTo((T)data[right]) < 0) { 38  swap(index, right); 39                         index = right; 40                     } else
41                         break; 42 
43                 } else { 44                     //左子节点比右子节点大
45                     if (((T)data[index]).compareTo((T)data[left]) < 0) { 46  swap(index, left); 47                         index = left; 48                     } else
49                         break; 50  } 51             } else { 52                 //右子节点不存在,只有左子节点
53                 if (((T)data[index]).compareTo((T)data[left]) < 0) { 54  swap(index, left); 55                     index = left; 56                 } else
57                     //index 的值大于左子节点,终止循环
58                     break; 59  } 60  } 61     }

 

至此,大顶堆的插入和删除最大元素就都实现完了.来写个测试

 1 public static void main(String[] args) {  2         MaxHeap<Integer> mh = new MaxHeap<Integer>(Integer.class, 12);  3         mh.insert(66);  4         mh.insert(44);  5         mh.insert(30);  6         mh.insert(27);  7         mh.insert(17);  8         mh.insert(25);  9         mh.insert(13); 10         mh.insert(19); 11         mh.insert(11); 12         mh.insert(8); 13         mh.insert(45); 14         Integer[] data = mh.getData(); 15         for (int i = 1 ; i <= mh.count ; i++ ) { 16             System.err.print(data[i] + " "); 17  } 18  mh.popMax(); 19         for (int i = 1 ; i <= mh.count ; i++ ) { 20             System.err.print(data[i] + " "); 21  } 22 }
View Code

 

嗯,还不错,结果跟上面图上对应的数组同样.结果却是指望的同样,但总感受上面的shiftDown的代码比shiftUp的代码要多几倍,并且看着不少相似同样的重复的代码, 看着难受.因而乎想个办法优化一下. 对我这种强迫症来讲,不干这件事,晚上总是睡不着觉.

思路: 上面咱们不断的循环条件是这个index对应的节点有子节点.若是节点堆的性质破坏,最终是要用这个值与其左子节点或者右子节点的值交换,因此咱们计算出了左子节点和右子节点的序号.其实否则,咱们定义一个抽象的最终要和父节点交换的变量,这个变量多是左子节点,也多是右子节点,初始化成左子节点的序号,只有在其左子节点的值小于右子节点,且父节点的值也左子节点,父节点才可能与右子节点,这时让其这个交换的变量加1变成右子节点的序号便可,其余状况则要么和左子节点交换,要么不做交换,跳出循环,因此shiftDown简化成:

 1    /**
 2  * 下沉  3  *  4  * @param index  5      */
 6     private void shiftDown(int index) {  7         //只要这个index对应的节点有左子节点(彻底二叉树中不存在 一个节点只有 右子节点没有左子节点)
 8         while (count >= (index << 1)) {  9             //比较左右节点谁大,当前节点跟谁换位置 10             //左子节点的inedx
11             int left = index << 1; 12             //data[index]预交换的index的序号
13             int t = left; 14             //若是右子节点存在,且右子节点比左子节点大,则当前节点可能与右子节点交换
15             if (((t + 1) <= count) && (((T) data[t]).compareTo((T) data[t + 1]) < 0)) 16                 t += 1; 17             //若是index序号节点比t序号的节点小,才交换,不然什么也不做, 退出循环
18             if (((T) data[index]).compareTo((T) data[t]) >= 0) 19                 break; 20  swap(index, t); 21             index = t; 22  } 23     }

 

嗯,还不错,这下完美了.简单多了.其余还有待优化的地方留在下篇讨论

总结

  • 首先复习了堆的应用场景,具体的应用场景代码实现留在下一篇.

  • 引入堆的概念,性质和大顶堆,小顶堆的概念,实现了大顶堆的元素添加和弹出

  • 根据堆的性质和弹出时下沉的规律,优化下沉方法代码.

  • 下一篇优化堆的构建,用代码实现其应用场景,如排序, topN问题,优先队列等

 

 

原文出处:https://www.cnblogs.com/blentle/p/10941119.html

相关文章
相关标签/搜索