堆 底层:数组
1. 什么是堆?
堆是一种数据结构,它是一颗完全二叉树。
堆分为最大堆和最小堆:
最大堆:任意节点的值不大于其父亲节点的值。
最小堆:任意节点的值不小于其父亲节点的值。
如下图所示,就是个最大堆:
2. 堆有什么用途?
堆最常用于优先队列以及相关动态问题。
优先队列指的是元素入队和出队的顺序与时间无关,既不是先进先出,也不是先进后出,而是根据元素的重要性来决定的。
例如,操作系统的任务执行是优先队列。一些情况下,会有新的任务进入,并且之前任务的重要性也会改变或者之前的任务被完成出队。而这个出队、入队的过程利用堆结构,时间复杂度是O(log2_n)
。
3. 实现堆结构 3.1 元素存储 堆中的元素存储,一般是借用一个数组:这个数组是从 1 开始计算的 。更方便子节点和父节点的表示。
3.2 入堆 入堆即向堆中添加新的元素,然后将元素移动到合适的位置,以保证堆的性质。
在入堆的时候,需要shift_up
操作,如下图所示:
插入 52 元素后,不停将元素和上方父亲元素比较,如果大于,则交换元素;直到达到堆顶或者小于等于父亲元素。
3.3 出堆 出堆只能弹出堆顶元素(最大堆就是最大的元素),调整元素为止保证堆的性质。
在入堆的时候,需要shift_down
操作,如下图所示:
已经取出了堆顶元素,然后将位于最后一个位置的元素放入堆顶(图中是16
被放入堆顶)。
重新调整元素位置。此时元素应该和子节点比较,如果大于等于子节点或者没有子节点,停止比较;否则,选择子节点中最大的元素,进行交换,执行此步,直到结束。
3.4 实现优化 在优化的时候,有两个部分需要做:
swap
操作应该被替换为:单次赋值,减少赋值次数
入堆操作:空间不够的时候,应该开辟 2 倍空间,防止数组溢出
3.5堆排序 根据实现的MaxHeap
类,实现堆排序很简单:将元素逐步insert
进入堆,然后再extract_max
逐个取出即可。当然,这个建堆的平均时间复杂度是O(n*log2_n)
代码如下:
过程叫做heapify
,实现思路如下:
将数组的值逐步复制到this->data
从第一个非叶子节点开始,执行shift_down
重复第 2 步,直到堆顶元素
这种建堆方法的时间复杂度是: O(n)
。因此, 编写heap_sort2
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public MaxHeap (Item arr[]) { int n = arr.length; data = (Item[])new Comparable[n+1 ]; capacity = n; for (int i = 0 ; i<n;i++) data[i+1 ] = arr[i]; count = n; for (int i = count/2 ;i>=1 ;i--) shiftDown(i); }
整体代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 import java.util.*;public class MaxHeap <Item extends Comparable > { protected Item[] data; protected int count; protected int capacity; public MaxHeap (int capacity) { data = (Item[]) new Comparable[capacity+1 ]; count = 0 ; this .capacity = capacity; } public MaxHeap (Item arr[]) { int n = arr.length; data = (Item[])new Comparable[n+1 ]; capacity = n; for (int i = 0 ; i<n;i++) data[i+1 ] = arr[i]; count = n; for (int i = count/2 ;i>=1 ;i--) shiftDown(i); } public int size () { return count; } public boolean isEmpty () { return count == 0 ; } public void insert (Item item) { assert count+1 <= capacity; data[count+1 ] = item; count++; shiftUp(count); } public Item extractMax () { assert count>0 ; Item ret = data[1 ]; swap(1 ,count); count--; shiftDown(1 ); return ret; } public Item getMax () { assert (count>0 ); return data[1 ]; } private void swap (int i,int j) { Item t = data[i]; data[i] = data[j]; data[j] = t; } private void shiftUp (int idx) { while (idx>1 && data[idx/2 ].compareTo(data[idx])<0 ) { swap(idx/2 ,k); idx = idx/2 ; } } private void shiftDown (int idx) { while (2 *idx <= count) { int j = 2 *idx; if (j+1 <= count && data[idx+1 ].compareTo(data[j])>0 ) j++; if (data[idx].compareTo(data[j])>=0 ) break ; swap(idx,j); idx = j; } } public static void main (String[] args) { MaxHeap<Integer> maxHeap = new MaxHeap<Integer>(100 ); int N = 100 ; int M = 100 ; for ( int i = 0 ; i < N ; i ++ ) maxHeap.insert( new Integer((int )(Math.random() * M)) ); Integer[] arr = new Integer[N]; for ( int i = 0 ; i < N ; i ++ ){ arr[i] = maxHeap.extractMax(); System.out.print(arr[i] + " " ); } System.out.println(); for ( int i = 1 ; i < N ; i ++ ) assert arr[i-1 ] >= arr[i]; } }
Python代码–最大堆的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 import randomclass Array (object ): def __init__ (self,size=32 ): self._size = size self._items = [None ]*size def __getitem__ (self,index ): return self._items[index] def __setitem__ (self, index, value ): self._items[index] = value def __len__ (self ): return self._size def clear (self,value=None ): for i in range (self._items): self._items[i]=value def __iter__ (self ): for item in self._items: yield item class MaxHeap (object ): def __init__ (self,capacity=None ): self.capacity = capacity+1 self._data = Array(self.capacity) self._count = 0 def __len__ (self ): return self._count def add (self,item ): if self._count>=self.capacity: raise Exception('full' ) self._count += 1 self._data[self._count] = item self.__shiftUp(self._count) def size (self ): return self.count def isEmpty (self ): return self.count ==0 def extractMax (self ): if self._count <=0 : raise Exception("empty" ) ret = self._data[1 ] self._data[1 ],self._data[self._count] = self._data[self._count],self._data[1 ] self._count-=1 self.__shiftDown(1 ) return ret def __shiftUp (self,k ): while (k>1 and self._data[k]>self._data[k//2 ]): self._data[k//2 ],self._data[k]=self._data[k],self._data[k//2 ] k //=2 def __shiftDown (self,k ): while (2 *k<=self._count): j = k*2 if (j+1 <=self._count and self._data[j+1 ]>self._data[j]): j+=1 if (self._data[k]>=self._data[j]): break self._data[k],self._data[j]=self._data[j],self._data[k] k=j def test_MaxHeap (): arr = list (range (33 )) random.shuffle(arr) print(arr) heap = MaxHeap(len (arr)) res = [] for i in range (len (arr)): heap.add(arr[i]) for i in range (len (arr)): res.append(heap.extractMax()) print(res) for i in range (len (arr) // 2 ): res[i], res[len (res) - 1 - i] = res[len (res) - 1 - i], res[i] print(res)
数据结构–索引堆 何为索引堆?
索引堆是对堆进行了优化。关于堆的介绍可以查看数据结构–堆 。
优化了什么?
在堆中,构建堆、插入、删除过程都需要大量的交换操作。在之前的实现中,进行交换操作是直接交换datas
数组中两个元素。而索引堆交换的是这两个元素的索引,而不是直接交换元素 。
有什么好处?
主要有两个好处:
减小交换操作的消耗,尤其是对于元素交换需要很多资源的对象来说,比如大字符串。
可以根据原位置找到元素,即便这个元素已经换了位置。
如何做到的? 索引堆使用了一个新的int
类型的数组,用于存放索引信息。部分代码如下:
1 2 3 // 属性 T[] datas; // 存放数据的数组 datas[1..n] int[] indexes; // 索引数组
这里这个indexes
数组,存放的是什么信息呢?它是如何工作的呢?假如我们有这样一个最小堆:
用数组表示是:
datas: [-, 1, 15, 20, 34, 7]
现在要维护最小堆的有序性,就需要交换15
和7
这两个元素。交换之后的元素数组是:
datas: [-, 1, 7, 20, 34, 15]
而此时,我们再想找到原来在datas[2]位置的元素,已经找不到了。因为此时data[2]已经换成了7
,而系统并没有记录15
被换到了什么地方。
这个时候,想要得到i位置的元素,直接datas[i]
就可以了。
使用索引堆 元素比较的时候是datas的数据,而交换时候是indexes的数据(索引),datas不做任何改动。
使用索引堆后,初始化两个数组应该是这样的:
datas: [-, 1, 15, 20, 34, 7]
indexes: [-, 1, 2, 3, 4, 5]
这个时候,我们就交换indexes
数组里面的索引2
和5
,而不操作datas
数组。交换后两个数组是这个样子:
datas: [-, 1, 15, 20, 34, 7]
indexes: [-, 1, 5, 3, 4, 2]
这个时候,想要得到i位置的元素,就需要datas[indexes[i]]
来获取。
用图来理解:
构建前:
构建后:
索引堆的优化 前面的index heap 虽然解决了改变堆中元素,由于data位置是不改变的,所以可以通过 data[i] = newItem; 但是,改变index数组以维护队的性质这个操作就无法通过一步实现了,此时,需要先遍历indexes数组,时间复杂度是O(N),再调整的时间复杂度是O(logN),所以T(N) = O(N0+O(logN)。
比如,我们想要改变所以位置4的data,更改后要维护这个堆,这个堆中存储的元素是上一行的索引,所以要在indexes数组中找到4所在位置,需要遍历indexes数组,找到是第9个位置,然后对滴9个位置调整,如果,我们在对这个类中添加一个reverse属性,关系如下:
rev与index的关系: