堆排序

堆排序

一、什么是堆排序?

  • 基于堆实现的排序算法
  • 利用堆顶的有序性(最大值或最小值)来实现对数组的排序

二、为什么要用堆排序?

  • 唯一能够同时最优地利用空间和时间的排序算法
  • 缺点是很少和相邻元素比较,导致无法利用缓存

三、怎么实现堆排序?

堆排序可以分为 2 步:

  • 构建堆:将数组构建成堆
  • 堆排序:一直移除堆顶元素,直到堆为空为止

先将数据建成二叉树的形式,再进行排序。

3.1 构建堆

因为每次都是移除堆顶,而堆顶一直都是最大值/最小值,所以移除堆元素就是排序的过程。

比如说,数组 [2, 7, 1, 4, 3, 6, 5],要从小到大排序。

第一步是构建堆,将数组构建出大顶堆的结构:

1
2
3
4
5
6
7
                            1=>6                       7                      2=>7=>4

2 2 2 7
/ \ / \ / \ / \
7 1 7 6 7 6 4 6
/ \ / \ / \ / \ / \ / \ / \ / \
4 3 6 5 4 3 1 5 4 3 1 5 2 3 1 5

构建完堆以后,数组变成了这样:

1
2
3
4
5
6
7
8
              7      
/ \
4 6 -- 逻辑结构(二叉树)
/ \ / \
2 3 1 5

___ ___ ___ ___ ___ ___ ___
| 7 | 4 | 6 | 2 | 3 | 1 | 5 | -- 物理结构(数组)

3.2 堆排序

接下来是排序,排序过程就是一直移除堆顶元素。

被移除的堆顶元素放到数组的末尾空位(相当于末尾已经不算在堆内了):

1
2
3
4
5
6
7
              7                                            6                 
/ \ / \
4 6 => 4 5
/ \ / \ / \ /
2 3 1 5 2 3 1
___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
| 7 | 4 | 6 | 2 | 3 | 1 | 5 | | 6 | 4 | 5 | 2 | 3 | 1 | | 7 |

注意,移除堆顶的同时会调整堆结构(这个是堆的性质)。

以此类推,一直移除堆顶元素,直到堆变成空为止。

移除堆顶 6:

1
2
3
4
5
6
7
        6                                                  5                    
/ \ / \
4 5 => 4 1
/ \ / / \
2 3 1 2 3
___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
| 6 | 4 | 5 | 2 | 3 | 1 | | 7 | | 5 | 4 | 1 | 2 | 3 | | 6 | 7 |

移除堆顶 5:

1
2
3
4
5
6
7
        5                                                  4                  
/ \ / \
4 1 => 3 1
/ \ /
2 3 2
___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
| 5 | 4 | 1 | 2 | 3 | | 6 | 7 | | 4 | 3 | 1 | 2 | | 5 | 6 | 7 |

移除堆顶 4:

1
2
3
4
5
6
7
        4                                              3                  
/ \ / \
3 1 => 2 1
/
2
___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
| 4 | 3 | 1 | 2 | | 3 | 6 | 7 | | 3 | 2 | 1 | | 4 | 5 | 6 | 7 |

移除堆顶 3:

1
2
3
4
5
    3                                                2                  
/ \ => \
2 1 1
___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
| 3 | 2 | 1 | | 4 | 5 | 6 | 7 | | 2 | 1 | | 3 | 4 | 5 | 6 | 7 |

移除堆顶 2:

1
2
3
4
5
  2                                     =>           1                  
\
1
___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
| 2 | 1 | | 3 | 4 | 5 | 6 | 7 | | 1 | | 2 | 3 | 4 | 5 | 6 | 7 |

至此,堆排序结束,数据已经排好序了。

四、算法分析

4.1 时间复杂度

  • 最好时间复杂度:O(n)
  • 最坏时间复杂度:O(nlogn)
  • 平均时间复杂度:O(nlogn)

4.2 空间复杂度

  • 空间复杂度:O(1)
  • 原地算法

4.3 稳定性

  • 不稳定排序算法

总结

  • 基于堆实现的排序算法
  • 利用堆顶的有序性(最大值或最小值)来实现对数组的排序
  • 优缺点
    • 优点:唯一能够同时最优地利用空间和时间的排序算法
    • 缺点:很少和相邻元素比较,导致无法利用缓存
  • 堆排序步骤
    • 构建堆:将数组构建成堆
    • 堆排序:一直移除堆顶元素,直到堆为空为止

参考

《算法(第4版)》

https://juejin.cn/post/7119807721766912030

附录

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
/**
* 堆排序
*
* @author weijiaduo
* @since 2023/2/26
*/
public class HeapSort implements Sort {

@Override
public void sort(int[] arr) {
// 建堆
int n = arr.length;
for (int i = (n - 1) / 2; i >= 0; i--) {
sink(arr, i, n);
}

// 排序
for (int i = n; i > 0; i--) {
swap(arr, 0, i - 1);
sink(arr, 0, i - 1);
}
}

/**
* 下沉
*
* @param arr 数组
* @param i 当前索引 i
* @param n 数组长度
*/
private void sink(int[] arr, int i, int n) {
while (i < n - 1) {
int m = i;
// 左子节点
int l = 2 * i + 1;
if (l < n && arr[l] > arr[m]) {
m = l;
}
// 右子节点
int r = 2 * i + 2;
if (r < n && arr[r] > arr[m]) {
m = r;
}
if (m == i) {
break;
}
swap(arr, i, m);
i = m;
}
}

}
作者

jiaduo

发布于

2023-02-26

更新于

2023-10-06

许可协议