堆的概念
堆(Heap)可以定义为一颗二叉树,且这个树满足两个条件:
- 是颗完全二叉树(essentially complete): 只有树的最后一层的最右边可以有缺位
- 满足堆特性(heap property)—父母优势(parental dominance): 每一个节点的键都大于等于它子女的键(键指该节点在堆中的位置,根节点的位置是1)
堆中,键值从上到下排序
键值之间不存在从左到右的次序(同一节点的左右子树之间没有任何关系)
堆的根包含了堆的最大元素
堆的一个节点以及该节点的子孙也是一个堆
堆和堆的数组表示
坐标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
值 | 10 | 8 | 7 | 5 | 2 | 1 | 6 | 3 | 5 | 1 |
可以使用数组的方式来实现堆,方法是从上到下从左到右来记录堆中的元素。为了后面计算方便,需要将数组的第0位空出来。在这种条件下,数组中的数值就有了下面2个特性:
- 1.父母节点会在数组的前
⌊
n
/
2
⌋
\lfloor n/2 \rfloor
⌊n/2⌋个位置中,叶子节点则会在后面
⌈
n
/
2
⌉
\lceil n/2\rceil
⌈n/2⌉个位置。
例如对于上面的堆,前 ⌊ n / 2 ⌋ \lfloor n/2 \rfloor ⌊n/2⌋是坐标5及坐标5以前的位置:10,8,7,5,2。(此时根节点10是坐标1)。 - 2.对于坐标为i的父母节点(1≤i≤
⌊
n
/
2
⌋
\lfloor n/2 \rfloor
⌊n/2⌋),它的子女的坐标在2i和2i+1
同时对于坐标为i的子女节点来说,它的父节点坐标为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋
构建堆
构建堆指的是,给定一个乱序数组,需要将它转换成堆数组的形式(即满足上面的两个特性),有两种方法来实现堆排序。
自底向上(bottom-up heap construction)
从最后的父母节点开始,到根节点为止,检查这些节点的值是否满足父母优势,如果不满足父母优势,就将该节点的值和子女节点中值更大的那个交换。迭代一次之后,如果此时数组还不是堆数组,就再迭代一次,直到最后一次迭代时不再有父母节点和子女节点交换值的过程。
以数组[2,9,7,6,5,8]为例,首先这个数组不是个堆数组。数组中有6个数,前三个是父母节点中的数。构建堆的过程为:
坐标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
值 | 2 | 9 | 7 | 6 | 5 | 8 |
坐标为3 的节点值为7,子节点坐标为6,7。
坐标7处已经没有节点了忽略,由于7小于8,坐标3和6的值交换,变为:
坐标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
值 | 2 | 9 | 8 | 6 | 5 | 7 |
坐标为2 的节点值为9,子节点坐标为4,5。
父节点的值大于子节点的值,不做处理、
坐标为1 的节点值为2,子节点坐标为2,3。
此时交换坐标1和坐标2处节点的值,数组变为:
坐标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
值 | 9 | 2 | 8 | 6 | 5 | 7 |
到此,第一次迭代完成。由于这次迭代中有父节点和子节点交换数值的情况,所以不能确定数组是否已迭代完成。所以要再从坐标3开始进行一轮新的迭代。
最后迭代完的堆数组为:
坐标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
值 | 9 | 6 | 8 | 2 | 5 | 7 |
自底向上的算法可以让规模为n的数组在2n次以内完成堆的构造过程。
自顶向下堆构造(top-down heap construction)
自顶向下的堆构造方式为:每次选取一个新的节点放在数组的最末端,然后将该节点和父节点比较,如果该节点大于父节点,则进行交换,并继续比较该节点和交换后的新的父节点的值,直到该节点的值小于其父节点或者该节点成为根节点。
由于堆的高度大约为 l o g 2 n log_2n log2n,所以插入的时间效率为 O ( l o g 2 n ) O(log_2n) O(log2n)
public static int[] createMaxHeap(int[] nums) {
// 自顶向下构造堆,时间复杂度为O(logn)
if(nums==null) return null;
int[] heap=new int[nums.length+1];
int p=1, pCur, pParent, tmp;
for(int n:nums) {
heap[p] = n;
pCur = p;
pParent = pCur/2;
while(pParent>=1&&heap[pParent]<heap[pCur]) {
tmp = heap[pParent];
heap[pParent] = heap[pCur];
heap[pCur] = tmp;
pCur = pParent;
pParent /= 2;
}
p++;
}
return heap;
}
删除堆中的节点
如果需要删除一个节点,则将该节点和堆中的最后的一个节点交换,删除交换后的最后一个节点,然后按照自底向上的方式重新构造堆。
删除的时间效率同样为
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)
public static int[] deleteNode(int[] heap, int num) {
/*
* 删除堆中的节点
* 输入:heap 堆数据,num指定删除的节点值
* 输出:删除该节点后堆数组,长度是heap的长度-1
* 如果堆中没有这个元素,返回null
*/
Boolean hasThisNum = false;
for(int i=1;i<heap.length;i++)
if(heap[i] == num) {
heap[i]=heap[heap.length-1];
hasThisNum = true;
break;
}
if(!hasThisNum)
return null;
int[] newHeap = new int[heap.length-2];
for(int i=0;i<heap.length-2;i++)
newHeap[i] = heap[i+1];
return createMaxHeap(newHeap);
}
堆排序(heapsort)
堆排序的过程为:
- 构造堆—删除根节点
- 重复上述操作直到堆中不再有节点
时间效率θ( n l o g n nlogn nlogn)