1、堆的概念
堆分为两种:最大堆和最小堆,两者的差别在于节点的排序方式。
在最大堆中,父节点的值比每一个子节点的值都要大。在最小堆中,父节点的值比每一个子节点的值都要小。这就是所谓的“堆属性”,并且这个属性对堆中的每一个节点都成立。
例子:
这是一个最大堆,,因为每一个父节点的值都比其子节点要大。10 比 7 和 2 都大。7 比 5 和 1都大。
由于堆是存储在数组(长度为n,下标从0开始)中的,因此,在堆中给定下标为i的结点时:
- 如果 i = 0,结点 i 是根结点,无父结点;否则结点 i 的父结点为结点 (i - 1 )//2;
- 如果 2i + 1 > n - 1,则结点 i 无左子女;否则结点 i 的左子女为结点 2i + 1
- 如果 2i + 2 > n - 1,则结点 i 无右结点;否则结点 i 的右子女为结点 2i + 2
2、堆与二叉搜索树的区别
堆并不能取代二叉搜索树,它们之间有相似之处也有一些不同。我们来看一下两者的主要差别:
-
节点的顺序。在二叉搜索树中,左子节点必须比父节点小,右子节点必须必比父节点大。但是在堆中并非如此。在最大堆中两个子节点都必须比父节点小,而在最小堆中,它们都必须比父节点大。
-
内存占用。普通树占用的内存空间比它们存储的数据要多。你必须为节点对象以及左/右子节点指针分配额为是我内存。堆仅仅使用一个数组来存储数据,且不使用指针。
-
平衡。二叉搜索树必须是“平衡”的情况下,其大部分操作的复杂度才能达到O(log n)。你可以按任意顺序位置插入/删除数据,或者使用 AVL 树或者红黑树,但是在堆中实际上不需要整棵树都是有序的。我们只需要满足对属性即可,所以在堆中平衡不是问题。因为堆中数据的组织方式可以保证O(log n) 的性能。
-
搜索。在二叉树中搜索会很快,但是在堆中搜索会很慢。在堆中搜索不是第一优先级,因为使用堆的目的是将最大(或者最小)的节点放在最前面,从而快速的进行相关插入
2、堆的常用操作
有两个原始操作用于保证插入或删除节点以后堆是一个有效的最大堆或者最小堆:
shiftUp():
如果一个节点比它的父节点大(最大堆)或者小(最小堆),那么需要将它与它的父节点进行交换,不断向上移动,直到它到达堆中应该存在的位置。这样是这个节点在数组的位置上升。shiftDown():
如果一个节点比它的子节点小(最大堆)或者大(最小堆),那么需要将它与它的其中一个子节点交换,不断向下移动,直到它到达堆中应该存在的位置位置。这个操作也称作“堆化(heapify)”。
由于上述两个操作的时间复杂度与堆的高度有关,所以shiftUp 或者 shiftDown 的时间复杂度是 O(log n)。
基于这两个原始操作还有一些其他的操作:
insert(value):
在堆的尾部添加一个新的元素,然后使用 shiftUp 来修复堆。remove():
移除并返回最大值(最大堆)或者最小值(最小堆)。为了将这个节点删除后的空位填补上,需要将最后一个元素移到根节点的位置,然后使用 shiftDown 方法来修复堆。removeAtIndex(index
):
和 remove() 一样,差别在于可以移除堆中任意节点,而不仅仅是根节点。当它与子节点比较位置不是无序时使用 shiftDown(),如果与父节点比较发现无序则使用 shiftUp()。replace(index, value):
将一个更小的值(最小堆)或者更大的值(最大堆)赋值给一个节点。由于这个操作破坏了堆属性,需要修复堆属性,以最大堆为例,如果替换的值比原来的值大,则使用shiftUp,否则使用shiftDown 。
上面所有的操作的时间复杂度都是 O(log n),因为 shiftUp 和 shiftDown 都很费时。还有少数一些操作需要更多的时间:
search(value):
堆不是为快速搜索而建立的,但是 replace() 和 removeAtIndex() 操作需要找到节点在数组中的index,所以你需要先找到这个index。时间复杂度:O(n)。buildHeap(array):
可以通过反复调用 insert() 往堆中插入元素的方法生成堆。假设 n n n个元素构成的堆高度为 h = l o g 2 n h=log_2n h=log2n,最坏的情况,第 k k k层有 2 k − 1 2^{k-1} 2k−1 个节点,这层每插入一个节点,调整时需要比较 k − 1 k-1 k−1次。那么总的需要比较的次数为 ∑ k = 1 h 2 k − 1 ∗ ( k − 1 ) = O ( n l o g 2 n ) \sum_{k=1}^{h}2^{k-1}*(k-1)=O(nlog_