堆也是指二叉堆,是完全二叉树或者近似完全二叉树。
最大堆:当父结点的键值总是大于或等于任何一个子节点的键值。
最小堆:当父结点的键值总是小于或等于任何一个子节点的键值。
结点拥有的子树的个数称为结点的度(Degree)。
二叉树的特性
- 在二叉树的第 i \mathrm{ i } i 层上最多有 2 i − 1 \mathrm{ 2^{i-1}} 2i−1个结点( i ≥ 1 \mathrm{i\geq1} i≥1)。
- 深度为 k \mathrm{ k } k 的二叉树至多有 2 k − 1 \mathrm{2^k-1} 2k−1个结点( k ≥ 1 \mathrm{k\geq1} k≥1)。
- 对任何一颗二叉树 T { T } T,如果其叶子结点个数为 n 0 \mathrm{n_0} n0,度为2的结点个数为 n 2 \mathrm{n_2} n2,则 n 0 = n 2 + 1 \mathrm{n_0=n_2+1} n0=n2+1。
完全二叉树的特性
- 具有 n \mathrm{ n } n个节点的完全二叉树的深度为 ⌊ l o g 2 n ⌋ + 1 \mathrm{\left \lfloor log_2^n\right \rfloor+1} ⌊log2n⌋+1,按层序编号,编号为i的结点的层次在 ∣ l o g 2 i ∣ + 1 \mathrm{\left| log_2^i \right|+1} ∣ ∣log2i∣ ∣+1
- 一棵深度为 k \mathrm{ k } k 的完全二叉树至少有 2 k − 1 \mathrm{2^{k-1}} 2k−1的节点,至多 2 k − 1 \mathrm{2^k-1} 2k−1个结点。
完全二叉树的逻辑结构

使用数组来存储元素值。

下标 0 0 0代表二叉树的根结点,将树按层从上至下,在同一层按从左往右,依次将结点的值存到数组中。如下图:

若数组下标为 i \Large i i,则其左子树的下标为 2 ∗ i + 1 \Large 2*i+1 2∗i+1,右子树的下标为 2 ∗ i + 2 \Large 2*i+2 2∗i+2。其父结点的下标为 i − 1 2 ( i > 0 ) \Large\frac{i-1}{2} \normalsize (i>0) 2i−1(i>0)。
主体思路:
heapify,满足二叉堆的特性。(按最大堆处理)heapify,使得下标
0
0
0存储着剩余数组元素中的最大值,将下标
0
0
0与下标
7
7
7的值进行交换。此时下标
7
7
7,下标
8
8
8已经是经过排序的了。heapify和首尾值交换。直到数组完全排序。
package sort;
public class HeapSort {
/**
* 将数组进行heapify
*/
private static void buildHeap(int[] o, int i, int length) {
int leftChild = 2 * i + 1;
int rightChild = leftChild + 1;
int larger = i;
if (leftChild < length && o[leftChild] > o[larger]) {
larger = leftChild;
}
if (rightChild < length && o[rightChild] > o[larger]) {
larger = rightChild;
}
if (larger != i) {
int temp = o[i];
o[i] = o[larger];
o[larger] = temp;
buildHeap(o, larger, length);
}
}
public static void main(String[] args) {
int[] o = {7, 6, 9, 3, 1, 5, 2, 4, 8};
int count = 1;
System.out.print("heapify前: ");
for (int t : o) {
System.out.print(t);
System.out.print(" ");
}
System.out.println();
// 算法部分
// init heapify
int i = o.length / 2 - 1;
for (; i >= 0; i--) {
buildHeap(o, i, o.length);
}
System.out.print("第" + count + "趟heapify后: ");
for (int t : o) {
System.out.print(t);
System.out.print(" ");
}
System.out.println();
// sort
for (int j = o.length - 1; j > 0; j--) {
int temp = o[j];
o[j] = o[0];
o[0] = temp;
if (j == 1) {
break;
}
buildHeap(o, 0, j);
count++;
System.out.print("第" + count + "趟heapify后: ");
for (int t : o) {
System.out.print(t);
System.out.print(" ");
}
System.out.println();
}
System.out.print("排序后: ");
for (int t : o) {
System.out.print(t);
System.out.print(" ");
}
System.out.println();
}
}
结果
heapify前: 7 6 9 3 1 5 2 4 8
第1趟heapify后: 9 8 7 6 1 5 2 4 3
第2趟heapify后: 8 6 7 4 1 5 2 3 9
第3趟heapify后: 7 6 5 4 1 3 2 8 9
第4趟heapify后: 6 4 5 2 1 3 7 8 9
第5趟heapify后: 5 4 3 2 1 6 7 8 9
第6趟heapify后: 4 2 3 1 5 6 7 8 9
第7趟heapify后: 3 2 1 4 5 6 7 8 9
第8趟heapify后: 2 1 3 4 5 6 7 8 9
排序后: 1 2 3 4 5 6 7 8 9
整个堆排序的算法包含了初始化堆和重建堆两部分。
在数组的初始堆化中,是从倒数第二层中的最后一个非叶子结点开始的。一个深度为 k k k的二叉堆,堆化过程中关键字比较次数一共最多 2 ( k − 1 ) 2(k-1) 2(k−1)。
那么深度为 h h h, 元素个数为n的二叉堆, i i i代表堆的第几层,那么 i i i的取值就是从 h − 1 ∼ 1 h-1 \thicksim 1 h−1∼1。第i层上至多有 2 i − 1 2^{i-1} 2i−1个结点,若以 i i i为根,那深度为 h − i + 1 h-i+1 h−i+1,得到数组初始堆化的比较耗时公式: S = ∑ i = h − 1 1 2 i − 1 ∗ 2 ∗ ( h − i ) {\displaystyle S=\sum_{i=h-1}^1 2^{i-1} * 2 * (h-i) } S=i=h−1∑12i−1∗2∗(h−i)。 2 ∗ ( h − i ) 由 2 ∗ ( h − i + 1 − 1 ) 2*(h-i)由2*(h-i+1-1) 2∗(h−i)由2∗(h−i+1−1)而来,这代表一个,所以还有乘以这个层的所有结点数。简化后结合 h = log 2 n {h=\log_2^n} h=log2n,可以知 S ≤ 4 n S \leq 4n S≤4n,时间复杂度为 O ( n ) O(n) O(n)。
S = ∑ i = h − 1 1 2 i − 1 ∗ 2 ∗ ( h − i ) = ∑ i = 1 h − 1 2 i ( h − i ) = ∑ i = 1 h − 1 h ∗ 2 j − ∑ i = 1 h − 1 2 i + 1 = 4 ∗ 2 h − 2 h = 4 ∗ n − 2 ∗ l o g 2 n ≤ 4 n ⟹ O ( n ) \displaystyle S=\sum_{i=h-1}^1 2^{i-1} * 2 * (h-i) =\sum_{i=1}^{h-1} 2^i(h-i)=\sum_{i=1}^{h-1} h*2^j-\sum_{i=1}^{h-1} 2^{i+1}=4*2^h-2h=4*n-2*log_2^n \leq 4n \implies O(n) S=i=h−1∑12i−1∗2∗(h−i)=i=1∑h−12i(h−i)=i=1∑h−1h∗2j−i=1∑h−12i+1=4∗2h−2h=4∗n−2∗log2n≤4n⟹O(n)
n n n个结点的完全二叉树的深度为 ⌊ l o g 2 n ⌋ + 1 \mathrm{\left \lfloor log_2 n\right \rfloor+1} ⌊log2n⌋+1,会使用数组堆化方法n-1次,总的比较次数
2 ( ⌊ l o g 2 ( n − 1 ) ⌋ + ⌊ l o g 2 ( n − 2 ) ⌋ + ⌊ l o g 2 ( n − 3 ) ⌋ + . . . + l o g 2 2 ) < 2 n ( ⌊ l o g 2 ( n ) ⌋ ) 2(\left \lfloor log_2 {(n-1)}\right \rfloor + \left \lfloor log_2 {(n-2)}\right \rfloor +\left \lfloor log_2 {(n-3)}\right \rfloor+...+log_2 2) < 2n(\left \lfloor log_2 {(n)}\right \rfloor) 2(⌊log2(n−1)⌋+⌊log2(n−2)⌋+⌊log2(n−3)⌋+...+log22)<2n(⌊log2(n)⌋)
2 ( ⌊ l o g 2 2 ⌋ + ⌊ l o g 2 3 ⌋ + ⌊ l o g 2 4 ⌋ + . . . + ⌊ l o g 2 n − 1 ⌋ ) = 2 ∗ ⌊ l o g 2 ( n − 1 ) ! ⌋ < 2 ⌊ l o g 2 n ! ⌋ { 2( \lfloor log_2^2 \rfloor + \lfloor log_2^3 \rfloor + \lfloor log_2^4 \rfloor +...+\lfloor log_2^{n-1} \rfloor )=2*\lfloor log_2^{(n-1)!} \rfloor < 2\lfloor log_2^{n!} \rfloor} 2(⌊log22⌋+⌊log23⌋+⌊log24⌋+...+⌊log2n−1⌋)=2∗⌊log2(n−1)!⌋<2⌊log2n!⌋
l o g 2 n ! = ∑ i = 1 n l o g 2 i ≤ ∑ i = 1 n l o g 2 n = n l o g 2 n ⟹ O ( n l o g 2 n ) \displaystyle log_2^n!=\sum_{i=1}^n log_2^i \leq \sum_{i=1}^n log_2^n=nlog_2^n \implies O(nlog_2^n) log2n!=i=1∑nlog2i≤i=1∑nlog2n=nlog2n⟹O(nlog2n)
时间复杂度为 O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n)
堆排序的最终复杂时间复杂度为 O ( n ) O(n) O(n)+ O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n) ⟹ \implies ⟹ O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n)
算法只申请了几个固定的大小的空间来协助排序,空间复杂度为 O ( 1 ) O(1) O(1)