目录
堆
堆的概念
堆的性质
堆的创建
1、堆向下调整
2、堆的创建
3、建堆的时间复杂度
堆的插入和删除
1、堆的插入
2、堆的删除
堆的应用
1、优先级队列的实现
2、堆排序
3、Top-k问题
堆 (Heap)
堆的概念
前面介绍的优先级队列在JDK1.8中其底层使用了堆的数据结构,而堆实际就是在完全二叉树的基础之上进行了一些元素的调整。
如果有一个
关键码的集合
K = {k0
,
k1
,
k2
,
…
,
kn-1}
,把它的所有元素
按完全二叉树的顺序存储方式存储 在一
个一维数组中
,并满足:
Ki <= K2i+1
且
Ki<= K2i+2
(Ki >= K2i+1
且
Ki >=K2i+2) i = 0
,
1
,
2…
,则
称为小堆
(
或大堆)
。(即双亲比孩子的数值小(大)——小(大)堆)将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
下面来看一下堆的可视化操作堆的可视化操作https://visualgo.net/zh/heap
堆的创建
1、堆向下调整
对于集合
{ 27,15,19,18,28,34,65,49,25,37 }
中的数据,如果将其创建成堆呢?

仔细观察上图后发现:
根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可
。
向下过程
(
以小堆为例
)
:
1.
让
parent
标记需要调整的节点,
child
标记
parent
的左孩子
(
注意:
parent
如果有孩子一定先是有左孩子
)
2.
如果
parent
的左孩子存在,即
:child < size
, 进行以下操作,直到
parent
的左孩子不存在
- parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标记
- 将parent与较小的孩子child比较,如果:parent小于较小的孩子child,调整结束。否则:交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后继续2。
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public void shiftDown(int[] array, int parent) { // child先标记parent的左孩子,因为parent可能右左没有右 int child = 2 * parent + 1; int size = array.length; while (child < size) { // 如果右孩子存在,找到左右孩子中较小的孩子,用child进行标记 if(child+1 < size && array[child+1] < array[child]){ child += 1; } // 如果双亲比其最小的孩子还小,说明该结构已经满足堆的特性了 if (array[parent] <= array[child]) { break; }else{ // 将双亲与较小的孩子交换 int t = array[parent]; array[parent] = array[child]; array[child] = t; // parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整 parent = child; child = parent * 2 + 1; } } }
注意:在调整以
parent
为根的二叉树时,必须要满足
parent
的左子树和右子树已经是堆了才可以向下调整。
时间复杂度分析:
最坏的情况即图示的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为O(log₂N)
2、堆的创建
那对于普通的序列
{ 1,5,3,8,7,6 }
,即根节点的左右子树不满足堆的特性,又该如何调整呢?

复制代码
1
2
3
4
5
6public static void createHeap(int[] array) { // 找倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整 for(int root = (array.length-2)/2; root >= 0; root--){ shiftDown(array, array.length, root); } }
3、建堆的时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明
(
时间复杂度本来看的就是近似值,多几个节点不影响最终结果)
:

因此:建堆的时间复杂度为O(N)
堆的插入和删除
1、堆的插入
堆的插入总共需要两个步骤:
- 先将元素放入到底层空间中(注意:空间不够时需要扩容)
- 将最后新插入的节点向上调整,直到满足堆的性质
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public void shiftUp(int child) { // 找到child的双亲 int parent = (child - 1) / 2; while (child > 0) { // 如果双亲比孩子大,parent满足堆的性质,调整结束 if (array[parent] > array[child]) { break; }else{ // 将双亲与孩子节点进行交换 int t = array[parent]; array[parent] = array[child]; array[child] = t; // 小的元素向下移动,可能到值子树不满足对的性质,因此需要继续向上调增 child = parent; parent = (child - 1) / 2; } } }
2、堆的删除
堆的删除一定删除的是堆顶元素。
堆的删除步骤如下:
- 将堆顶元素对堆中最后一个元素交换
- 将堆中有效数据个数减少一个
- 对堆顶元素进行向下调整
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public static void shiftDown(int[] array, int size, int parent){ int child = parent*2+1; while(child < size){ // 找左右孩子中较大的孩子 if(child+1 < size && array[child+1] > array[child]){ child += 1; } // 双亲小于交大的孩子 if(array[parent] < array[child]){ swap(array, parent, child); parent = child; child = parent*2+1; }else{ return; } } }
堆的应用
1、优先级队列的实现
用堆作为底层结构
封装优先级队列
复制代码
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141public class MyPriorityQueue { Integer[] array; int size; // 有效元素的个数 public MyPriorityQueue(){ array = new Integer[11]; size = 0; } public MyPriorityQueue(int initCapacity){ if(initCapacity < 1){ throw new IllegalArgumentException("初始容量小于1"); } array = new Integer[initCapacity]; size = 0; } public MyPriorityQueue(Integer[] arr){ // 1. 将arr中的元素拷贝到数组中 array = new Integer[arr.length]; for(int i = 0; i < arr.length; ++i){ array[i] = arr[i]; } size = arr.length; // 2. 找当前完全二叉树中倒数第一个叶子节点 // 注意:倒数第一个叶子节点刚好是最后一个节点的双亲 // 最后一个节点的编号size-1 倒数第一个非叶子节点的下标为(size-1-1)/2 int lastLeafParent = (size-2)/2; // 3. 从倒数第一个叶子节点位置开始,一直到根节点的位置,使用向下调整 for(int root = lastLeafParent; root >= 0; root--){ shiftDown(root); } } boolean offer(Integer e){ if(e == null){ throw new NullPointerException("插入时候元素为null"); } ensureCapacity(); array[size++] = e; // 注意:当新元素插入之后,可能会破坏对的性质---需要向上调整 shiftUp(size-1); return true; } // 将堆顶的元素删除掉 public Integer poll(){ if(isEmpty()){ return null; } Integer ret = array[0]; // 1. 将堆顶元素与堆中最后一个元素交换 swap(0, size-1); // 2. 将堆中有效元素个数减少一个 size--; // size -= 1; // 3. 将堆顶元素往下调整到合适位置 shiftDown(0); return ret; } public int size(){ return size; } public boolean isEmpty(){ return size == 0; } public void clear(){ size = 0; } // 功能:调整以parent为根的二叉树 // 前提:必须要保证parent的左右子树已经满足堆的特性 // 时间复杂度:O(logN) private void shiftDown(int parent){ // 默认让child先标记左孩子---因为:parent可能有左没有右 int child = parent*2 + 1; // while循环条件可以保证:parent的左孩子一定存在 // 但是不能保证parent的右孩子是否存在 while(child < size){ // 1. 找到左右孩子中较小的孩子 if(child+1 < size && array[child+1] < array[child]){ child += 1; } // 2. 较小的孩子已经找到了 // 检测双亲和孩子间是否满足堆的特性 if(array[parent] > array[child]){ swap(parent, child); // 大的双亲往下走了,可能会导致子树又不满足堆的特性 // 因此需要继续往下调整 parent = child; child = parent*2 + 1; }else{ // 以parent为根的二叉树已经是堆了 return; } } } private void shiftUp(int child){ int parent = (child-1)/2; while(child != 0){ if(array[child] < array[parent]){ swap(child, parent); child = parent; parent = (child-1)/2; }else{ return; } } } private void ensureCapacity(){ if(array.length == size){ int newCapacity = array.length*2; array = Arrays.copyOf(array, newCapacity); } } // 注意:left和right是数组的下标 private void swap(int left, int right){ int temp = array[left]; array[left] = array[right]; array[right] = temp; }
2、堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1.
建堆
- 升序:建大堆
- 降序:建小堆
2.
利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序

复制代码
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
42public static void swap(int[] array, int left, int right){ int temp = array[left]; array[left] = array[right]; array[right] = temp; } public static void shiftDown(int[] array, int size, int parent){ int child = parent*2+1; while(child < size){ // 找左右孩子中较大的孩子 if(child+1 < size && array[child+1] > array[child]){ child += 1; } // 双亲小于交大的孩子 if(array[parent] < array[child]){ swap(array, parent, child); parent = child; child = parent*2+1; }else{ return; } } } // 假设:升序 public static void heapSort(int[] array){ // 1. 建堆----升序:大堆 降序:小堆---向下调整 for(int root = (array.length-2)/2; root >= 0; root--){ shiftDown(array, array.length, root); } // 2. 利用堆删除的思想来排序---向下调整 int end = array.length-1; // end标记最后一个元素 while(end != 0){ swap(array,0,end); shiftDown(array, end,0); end--; } }
3、Top-k问题
TOP-K
问题:即求数据结合中前
K
个最大的元素或者最小的元素,一般情况下数据量都比较大
。
对于
Top-K
问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了
(
可能数据都不能一下子全部加载到内存中)
。最佳的方式就是用堆来解决,基本思路如下:
1.
用数据集合中前
K
个元素来建堆
- 前k个最大的元素,则建小堆
- 前k个最小的元素,则建大堆
2.
用剩余的
N-K
个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余
N-K
个元素依次与堆顶元素比完之后,堆中剩余的
K
个元素就是所求的前
K
个最小或者最大的元素。
Top-k问题
复制代码
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
26class Solution { public int[] smallestK(int[] arr, int k) { int[] vec = new int[k]; if (k == 0) { // 排除 0 的情况 return vec; } PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>() { public int compare(Integer num1, Integer num2) { return num2 - num1; } }); for (int i = 0; i < k; ++i) { queue.offer(arr[i]); } for (int i = k; i < arr.length; ++i) { if (queue.peek() > arr[i]) { queue.poll(); queue.offer(arr[i]); } } for (int i = 0; i < k; ++i) { vec[i] = queue.poll(); } return vec; } }
复杂度分析
时间复杂度:O(nlog k),其中 n 是数组 arr 的长度。由于大根堆实时维护前 k 小值,所以插入删除都是O(logk) 的时间复杂度,最坏情况下数组里 n 个数都会插入,所以一共需要 O(nlogk) 的时间复杂度。
空间复杂度:O(k),因为大根堆里最多 k 个数
最后
以上就是清新花瓣最近收集整理的关于数据结构——堆(带图详解)堆 (Heap)的全部内容,更多相关数据结构——堆(带图详解)堆内容请搜索靠谱客的其他文章。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复