堆
堆是一棵完全二叉树。
小根堆:每个父节点的值,都小于等于其子节点的值。因此,根节点的值为集合的最小值。
大根堆:每个父节点的值,都大于等于其子节点的值。因此,根节点的值为集合的最大值。
通常使用一维数组来存储堆。根节点的值存放在数组中索引值为1的位置。由于完全二叉树的特性,若父节点在数组的索引为
x
x
x,则其左子节点的索引为
2
x
2x
2x,右子节点的索引为
2
x
+
1
2x+1
2x+1。(凡是完全二叉树,基本上都是用一维数组来存储的)
堆最核心的是up操作和down操作,使用这两个操作可完成以下堆(小根堆)主要支持的函数:
1)插入一个数insert(int x)
heap[++ size] = x;
up(size);
2)求集合当中的最小值getMin()
heap[1];
3)删除最小值deleteMin()
heap[1] = heap[size];
size --;
down(1);
4)删除任意一个元素deleteMin(int k)
heap[k] = heap[size];
size --;
down(k);
up(k);
5)修改任意一个元素modify(int k, int x)
heap[k] = x;
down(k);
up(k);
up操作的代码:
public void up(int u){
while(u / 2 >= 1 && heap[u / 2] > heap[u]){
int tmp = heap[u / 2];
heap[u / 2] = heap[u];
heap[u] = tmp;
u /= 2;
}
}
down操作的代码:
public void down(int u){
int t = u;
if(2 * u <= size && heap[2 * u] < heap[t]) t = 2 * u;
if(2 * u + 1 <= size && heap[2 * u + 1] < heap[t]) t = 2 * u + 1;
if(t != u){
int tmp = heap[u];
heap[u] = heap[t];
heap[t] = tmp;
down(t);
}
}
堆排序
可以使用堆来实现堆排序。堆排序的思路是:先根据待排序数组建堆(小根堆),然后依次从堆中弹出最小值,也就是将堆的根节点删除,弹出的元素即构成为从小到大排序的数组。
1、建堆
建堆的直观想法是,依次将数组中的元素插入到堆中,这当然是没啥问题的,但这种做法的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)。推荐的做法是:将数组元素放入到堆(heap数组)里后,对heap数组中索引值从size/2直到0,进行down操作。注意,对heap数组中索引值从0到size/2进行down操作是不正确的,这会导致排序出错。
首先,上述的推荐做法是正确的,因为:①堆中只有一半的节点是存在子节点的,剩下的一半节点则没有,对这些没有子节点的节点不进行处理当然没有问题;②按照size/2到0的顺序进行down操作,可保证每个节点满足小根堆的要求,递归的处理每个节点后,较大的节点值是”下沉‘的,留在上面的都是较小的节点值,如果down操作顺序反过来,较大的节点值可能因为满足局部最小而被调整到根节点,且后面的down操作也不会再访问该节点,从而导致错误。其次,推荐做法的时间复杂度为 O ( n ) O(n) O(n),证明如下:

如图所示,建堆的时间复杂度可写为: n 4 ∗ 1 + n 8 ∗ 2 + n 16 ∗ 3 + ⋯ + n 2 n ∗ ( n − 1 ) = n ∗ ( 1 2 2 + 2 2 3 + 3 2 4 + ⋯ + n − 1 2 n ) (1) \frac{n}{4}*1 + \frac{n}{8}*2 + \frac{n}{16}*3 + \cdots + \frac{n}{2^n}*(n - 1)=n*(\frac{1}{2^2}+\frac{2}{2^3}+\frac{3}{2^4}+\cdots+\frac{n-1}{2^n}) \tag{1} 4n∗1+8n∗2+16n∗3+⋯+2nn∗(n−1)=n∗(221+232+243+⋯+2nn−1)(1)
令
S = 1 2 2 + 2 2 3 + 3 2 4 + ⋯ + n − 2 2 n − 1 + n − 1 2 n (2) S=\frac{1}{2^2}+\frac{2}{2^3}+\frac{3}{2^4}+\cdots+\frac{n-2}{2^{n-1}}+\frac{n-1}{2^n}\tag{2} S=221+232+243+⋯+2n−1n−2+2nn−1(2)
则有
2 ∗ S = 1 2 + 2 2 2 + 3 2 3 + 4 2 4 + ⋯ + n − 1 2 n − 1 (3) 2*S=\frac{1}{2}+\frac{2}{2^2}+\frac{3}{2^3}+\frac{4}{2^4}+\cdots+\frac{n-1}{2^{n-1}}\tag{3} 2∗S=21+222+233+244+⋯+2n−1n−1(3)
式(2)和式(3)错位相减,可得到:
S = 1 2 − n − 1 2 n + ( 1 2 2 + 1 2 3 + ⋯ + 1 2 n − 1 ) (4) S=\frac{1}{2}-\frac{n-1}{2^n}+(\frac{1}{2^2}+\frac{1}{2^3}+\cdots+\frac{1}{2^{n-1}})\tag{4} S=21−2nn−1+(221+231+⋯+2n−11)(4)
简化后,为: S = 1 − n + 1 2 n ≈ 1 S=1-\frac{n+1}{2^n}\approx1 S=1−2nn+1≈1。因此,式(1)所示的时间复杂度为 O ( n ) O(n) O(n)。
2、从堆中弹出最小值
此操作即对应上述的==deleteMin()==函数。
堆排序的整体代码为:
int[] heap;
int size;
private void down(int u){
int t = u;
if(2 * u <= size && heap[2 * u] < heap[t]) t = 2 * u;
if(2 * u + 1 <= size && heap[2 * u + 1] < heap[t]) t = 2 * u + 1;
if(t != u){
int tmp = heap[u];
heap[u] = heap[t];
heap[t] = tmp;
down(t);
}
}
public static int deleteMin(){
int min = heap[1];
heap[1] = heap[size --];
down(1);
return min;
}
public void heap_sort(int[] a){
int n = a.length;
heap = new int[n + 10];
size = n;
// 建堆
for(int i = 0; i < n; i ++) heap[i + 1] = a[i];
for(int i = size >> 1; i >= 0; i --) down(i);
// 弹出堆的最小值,并打印出来,打印出来的结果为数组a从小到大的排序结果
for(int i = 0; i < size; i ++) System.out.print(deleteMin() + " ");
}