堆



任务管理
操作系统如何管理运行的进程。
听音乐、下载文件和写文章。



队列
- 短时间的任务可能需要等待很长时间。
- 高优先级任务无法获得优先处理。



数组列表
每次新任务到达时都需要排序吗?
每次插入都要维护顺序吗?



堆(最小堆、最大堆)
堆非常适合快速获取最低/最高优先级元素。



堆(最小堆、最大堆)
- 堆是一种基于树的专用数据结构。
- 如果节点 A 是节点 B 的父节点,则节点 A 的键(key)按照与节点 B 相同的排序方式进行排序,堆中所有节点的排序方式都是相同的。
- 最小堆:根节点的值始终小于其子节点的值。
- 最大堆:根节点的值始终大于其子节点的值。
-
常见的实现方式是二叉堆,它是一种基于完全二叉树的数据结构。



堆(最小堆、最大堆)
100
/ \
19 36
/ \ / \
17 3 25 1
2
/ \
19 3
/ \ / \
20 31 5 8
最大堆
最小堆



堆(最小堆、最大堆)
- 最小堆
-
根节点是整个树中最小的节点
-
- 最大堆
-
根节点是整个树中最大的节点
-
- 完全二叉树
-
如果你要手动构建一个堆,那么你只需要一个数组。
-



基本操作
-
插入操作(insert 或 offer)
-
删除操作
-
删除根节点操作(delete root 或 poll)
-
-
初始化操作



基本操作
- 插入操作
-
每次将元素插入到堆的末尾
-
然后进行上移操作
-
最多上移到根节点,操作次数为深度,即 log(N)
-
时间复杂度为 O(logN)
-



基本操作
-
插入操作 -> 上移操作
90
/ \
70 50
/ \ / \
65 44 30 20
/ \ /
35 21 8
80
90
/ \
70 50
/ \ / \
65 44 30 20
/ \ / \
35 21 8 80
90
/ \
80 50
/ \ / \
65 70 30 20
/ \ / \
35 21 8 44



基本操作
- 插入操作
- 删除操作(pop 或 poll)
-
交换后,最多下移到叶子节点,操作次数为深度,即 log(N)。
-
时间复杂度为 O(logN)
-
删除根节点的操作的时间复杂度为 O(logN)。
- 获取堆中最大/最小元素的操作的时间复杂度为 O(1)。
-
时间复杂度为 O(logN),是相对于常数较为适中的复杂度。
-



基本操作
- 插入操作
- 删除操作
90
/ \
70 50
/ \ / \
65 44 30 20
/ \ /
35 21 8
90
/ \
8 50
/ \ / \
65 44 30 20
/ \
35 21
90
/ \
65 50
/ \ / \
35 44 30 20
/ \
8 21



基本操作
- 插入操作
- 删除操作
- 初始化操作



基本操作
- 插入操作
- 删除操作
- 初始化操作
-
逐个插入元素,时间复杂度为O(NlogN)。
-



基本操作
- 插入操作
- 删除操作
- 初始化操作
-
将每个元素逐一插入堆中的时间复杂度为O(NlogN)。
-
26, 45, 21, 37, 89, 12, 9
-



Basic Operations
- insert
- delete
- initialize
- Insert elements one by one, O(NlogN).
- 26, 45, 21, 37, 89, 12, 9
26



Basic Operations
- insert
- delete
- initialize
- Insert elements one by one, O(NlogN).
- 26, 45, 21, 37, 89, 12, 9
26
/
45



Basic Operations
- insert
- delete
- initialize
- Insert elements one by one, O(NlogN).
- 26, 45, 21, 37, 89, 12, 9
45
/
26



Basic Operations
- insert
- delete
- initialize
- Insert elements one by one, O(NlogN).
- 26, 45, 21, 37, 89, 12, 9
45
/ \
26 21



Basic Operations
- insert
- delete
- initialize
- Insert elements one by one, O(NlogN).
- 26, 45, 21, 37, 89, 12, 9
45
/ \
26 21
/
37



Basic Operations
- insert
- delete
- initialize
- Insert elements one by one, O(NlogN).
- 26, 45, 21, 37, 89, 12, 9
45
/ \
37 21
/
26



Basic Operations
- insert
- delete
- initialize
- Insert elements one by one, O(NlogN).
- 26, 45, 21, 37, 89, 12, 9
45
/ \
37 21
/ \
26 89



Basic Operations
- insert
- delete
- initialize
- Insert elements one by one, O(NlogN).
- 26, 45, 21, 37, 89, 12, 9
45
/ \
89 21
/ \
26 37



Basic Operations
- insert
- delete
- initialize
- Insert elements one by one, O(NlogN).
- 26, 45, 21, 37, 89, 12, 9
89
/ \
45 21
/ \
26 37



Basic Operations
- insert
- delete
- initialize
- Insert elements one by one, O(NlogN).
- 26, 45, 21, 37, 89, 12, 9
89
/ \
45 21
/ \ /
26 37 12



Basic Operations
- insert
- delete
- initialize
- Insert elements one by one, O(NlogN).
- 26, 45, 21, 37, 89, 12, 9
89
/ \
45 21
/ \ / \
26 37 12 9



基本操作
- 插入操作
- 删除操作
- 初始化操作
-
逐层下沉,时间复杂度O(N)
-



基本操作
- 插入操作
- 删除操作
- 初始化操作
-
逐层下沉,时间复杂度O(N)
-
45, 36, 18, 53, 72, 30, 48, 93, 15, 35
-
45
/ \
36 18
/ \ / \
53 72 30 48
/ \ /
93 15 35



Basic Operations
- insert
- delete
- initialize
- sift down level by level, O(N)
- 45, 36, 18, 53, 72, 30, 48, 93, 15, 35
45
/ \
36 18
/ \ / \
53 72 30 48
/ \ /
93 15 35



Basic Operations
- insert
- delete
- initialize
- sift down level by level, O(N)
- 45, 36, 18, 53, 72, 30, 48, 93, 15, 35
45
/ \
36 18
/ \ / \
53 72 30 48
/ \ /
93 15 35
45
/ \
36 18
/ \ / \
93 72 30 48
/ \ /
53 15 35



Basic Operations
- insert
- delete
- initialize
- sift down level by level, O(N)
- 45, 36, 18, 53, 72, 30, 48, 93, 15, 35
45
/ \
36 18
/ \ / \
93 72 30 48
/ \ /
53 15 35
45
/ \
36 48
/ \ / \
93 72 30 18
/ \ /
53 15 35



Basic Operations
- insert
- delete
- initialize
- sift down level by level, O(N)
- 45, 36, 18, 53, 72, 30, 48, 93, 15, 35
45
/ \
36 48
/ \ / \
93 72 30 18
/ \ /
53 15 35
45
/ \
93 48
/ \ / \
36 72 30 18
/ \ /
53 15 35
45
/ \
93 48
/ \ / \
53 72 30 18
/ \ /
36 15 35



Basic Operations
- insert
- delete
- initialize
- sift down level by level, O(N)
- 45, 36, 18, 53, 72, 30, 48, 93, 15, 35
45
/ \
93 48
/ \ / \
53 72 30 18
/ \ /
36 15 35
93
/ \
45 48
/ \ / \
53 72 30 18
/ \ /
36 15 35
93
/ \
72 48
/ \ / \
53 45 30 18
/ \ /
36 15 35



Basic Operations
- insert
- delete
- initialize
- sift down level by level, O(N)
- Last level, do not need to move
- second last level, at most sift down once



基本操作
-
插入,时间复杂度O(logN)
- 删除,时间复杂度O(logN)
-
初始化,时间复杂度O(N)
-
逐个插入元素,时间复杂度O(NlogN)
-
逐层下沉,时间复杂度O(N)
-



Java中的堆 - 优先队列(PriorityQueue)
- 操作
- E peek();
- E poll();
- boolean offer(E element);
- 构造函数
- PriorityQueue(int capacity)
- Java使用什么方式来比较堆中的元素。



Comparator和Comparable的区别
-
Comparator(比较器)
- Comparator帮助其他对象进行比较。
-
Comparable(可比较的)
- 如果某个类实现了Comparable接口,那么它的实例知道如何与其他实例进行比较。



Comparator和Comparable的区别
class MyClass implements Comparable<MyClass> {
@override
public int compareTo(final MyClass o) {
return this.val - o.val; // increasing (minHeap)
}
}
PriorityQueue<> heap = new PriorityQueue<>(capacity);



Comparator和Comparable的区别
class MyComparator implements Comparator<MyClass> {
public int compare(MyClass a, MyClass b) {
return a.val - b.val; // increasing (minHeap)
}
}
MyComparator myComparator = new MyComparator();
PriorityQueue<MyClass> heap = new PriorityQueue<MyClass>(capacity, myComparator);



Comparator和Comparable的区别
PriorityQueue<MyClass> heap = new PriorityQueue<MyClass>(cap, new Comparator<MyClass>() {
public int compare(MyClass a, MyClass b) {
return a.val - b.val; // increasing (minHeap)
}
});



如果只能记住一个,那就始终使用Comparator。
向堆中添加元素
- 默认:小根堆(PriorityQueue)
-
如果需要大根堆
- Comparator 和Comparable
-
将(-num)添加到堆中



C++中的堆 - 优先队列(priority_queue)
- 操作(Operations)
- top()
- pop()
- push()
- size()
-
与Java类似。但是,C++中不使用Comparator或Comparable,而是需要重载运算符:>、<或编写一个compare方法。



堆的总结
- 插入,时间复杂度O(logN)
- 删除,时间复杂度O(logN)
- 初始化,时间复杂度O(N)
-
堆适用于维护动态数据流,在需要保持最大值/最小值/某个位置值的同时,无需进行排序。



合并有序数组/链表
-
两个有序数组/链表
- 使用双指针法,将指向较小值的指针向后移动。
-
K个有序数组/链表
-
手动维护k个指针几乎不可能。
- 需要一种数据结构来维护k个指针并能够高效地返回最大/最小的元素。
- 堆
-



合并k个有序链表
合并k个有序链表,返回一个排序后的链表,并分析描述其时间复杂度。
public ListNode merge(ListNode[] lists);






合并k个有序链表
合并k个有序链表,返回一个排序后的链表,并分析描述其时间复杂度。
将所有链表的头节点插入小根堆(minHeap)中。
while (minHeap不为空)
root = minHeap.pop();
将root添加到结果链表中
将root.next插入到minHeap中。
返回结果链表



合并k个有序链表
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0)
return null;
Comparator<ListNode> comparator = new Comparator<ListNode> () {
public int compare(ListNode node1, ListNode node2) {
return node1.val - node2.val;
}
};
PriorityQueue<ListNode> minHeap =
new PriorityQueue<ListNode>(lists.length, comparator);
for (int i = 0; i < lists.length; i++) {
if (lists[i] != null) {
minHeap.add(lists[i]);
}
}
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
while (!minHeap.isEmpty()) {
cur.next = minHeap.poll();
cur = cur.next;
if (cur.next != null)
minHeap.add(cur.next);
}
return dummy.next;
}



合并k个有序链表
这种方法可以用于外部排序(external merge sort)
但通常人们会使用锦标赛树(tournament tree)来执行外部排序,而不是使用堆。



10,2,8,5,20,30
7,9,0,-2,35,21
1,90,80,15,-1,6
2,5,8,10,20,30
-2,0,7,9,21,35
-1,1,6,15,80,90
-2,-1,0,1,2,5
6,7,8,9,10,15
20,21,30,35,80,90
查找最大数
- 时间复杂度为O(N)的方法查找最大数
- 如果需要经常更新列表,也就是说,当最大数被移除后,会立即有一个新数字加入,那么使用堆来查找最大数是一种有效的方法。由于堆的插入和删除操作的时间复杂度均为O(logN),因此可以在对数时间内更新最大数。
- 每次更新最大数都需要再次比较n次,因此时间复杂度仍为O(N)。
- 排序?需要每次都排序
- 第一次排序时间复杂度为O(NlogN),每次更新最大数时间复杂度为O(N)。
- 插入数字是非常耗时的
- 堆
- 堆适用于处理数据流。



滑动窗口最大值问题
给定一个数组nums,有一个大小为k的滑动窗口从数组的最左边移动到最右边。你只能看到窗口中的k个数字。每次滑动窗口向右移动一个位置。
例如,
给定nums = [1,3,-1,-3,5,3,6,7]和k = 3。
返回[3,3,5,5,6,7]。
注意:
可以假设k始终有效,即对于非空数组,有1≤k≤输入数组的大小。



滑动窗口最大值问题
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0) {
return nums;
}
int[] result = new int[nums.length - k + 1];
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k,
new Comparator<Integer>() {
public int compare(Integer a, Integer b) {
return b - a;
}
});
for (int i = 0; i < nums.length; i++) {
if (i >= k) {
maxHeap.remove(nums[i-k]);
}
maxHeap.offer(nums[i]);
if (i >= k - 1) {
result[i - k + 1] = maxHeap.peek();
}
}
return result;
}



滑动窗口最大值问题
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0) {
return nums;
}
int[] result = new int[nums.length - k + 1];
PriorityQueue<Integer> minHeap = new PriorityQueue<>(k);
for (int i = 0; i < nums.length; i++) {
if (i >= k) {
minHeap.remove(-nums[i-k]);
}
minHeap.offer(-nums[i]);
if (i >= k - 1) {
result[i - k + 1] = -minHeap.peek();
}
}
return result;
}



时间复杂度是多少?
滑动窗口最大值问题
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0) {
return nums;
}
Deque<Integer> deque = new LinkedList<>();
int[] result = new int[nums.length - k + 1];
for (int i = 0; i < nums.length; i++) {
if (!deque.isEmpty() && deque.peekFirst() == i - k) {
deque.poll();
}
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offer(i);
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
一种更好的方法是使用双端队列(deque),可以实现O(n)的时间复杂度。



数组中的第K个最大元素问题
找到未排序数组中第k个最大元素。请注意,这是排序后的第k个最大元素,而不是第k个不同的元素。
例如,
给定[3,2,1,5,6,4]和k = 2,返回5。
注意:
可以假设k始终有效,即1≤k≤数组长度。



Kth Largest Element in an Array
Find the kth largest element in an unsorted array. Note that it is the kth largest element in the sorted order, not the kth distinct element.
- Find largest element for k times. O(kn)
- Sort. O(nlogn)
-
Min-Heap
- Build a min heap with capacity of k.
-
Insert all numbers into the heap.
- If the heap is full, then only insert when number is larger than the root.
- Root of the heap will be the kth largest element.



Kth Largest Element in an Array
Find the kth largest element in an unsorted array. Note that it is the kth largest element in the sorted order, not the kth distinct element.
public int findKthLargest(int[] nums, int k) {
Comparator<Integer> comparator = new Comparator<Integer>() {
public int compare(Integer a, Integer b) {
return a - b;
}
};
PriorityQueue<Integer> minHeap = new PriorityQueue(k, comparator);
for (int i = 0; i < k; i++) {
minHeap.add(nums[i]);
}
for (int i = k; i < nums.length; i++) {
if (nums[i] >= minHeap.peek()) {
minHeap.poll();
minHeap.add(nums[i]);
}
}
return minHeap.poll();
}



Kth Largest Element in an Array
Find the kth largest element in an unsorted array. Note that it is the kth largest element in the sorted order, not the kth distinct element.
- Find largest element for k times. O(kn)
- Sort. O(nlogn)
- Min-Heap. O(nlogk)
- Recursion. O(n)
- Randomly select one number, find its position in array. O(n)
- If pos > k, then recursion in left half.
- Otherwise, recursion in right half.
- Time: n + n/2 + n/4 + .... = 2n = O(n)



数据流中的中位数问题
中位数是有序整数列表中的中间值。如果列表的大小为偶数,则没有中间值。因此,中位数是中间两个值的平均值。
例如:
[2,3,4],中位数为3
[2,3],中位数为(2 + 3) / 2 = 2.5
设计一个数据结构,支持以下两个操作:
- void addNum(int num) - 从数据流中添加一个整数到数据结构中。
- double findMedian() - 返回到目前为止所有元素的中位数。



数据流中的中位数问题
中位数是有序整数列表中的中间值。如果列表的大小为偶数,则没有中间值。因此,中位数是中间两个值的平均值。
class MedianFinder {
public void addNum(int num);
public double findMedian();
}



数据流中的中位数问题
中位数是有序整数列表中的中间值。如果列表的大小为偶数,则没有中间值。因此,中位数是中间两个值的平均值。
minHeap
(较大的数字)
maxHeap
(较小的数字)
中位数
- minHeap和maxHeap的大小应该相同(差值不超过1)。
- minHeap.peek > maxHeap.peek



数据流中的中位数问题
中位数是有序整数列表中的中间值。如果列表的大小为偶数,则没有中间值。因此,中位数是中间两个值的平均值。
minHeap
(较大的数字)
maxHeap
(较小的数字)
中位数
- 如果num > minHeap.peek
- 将num添加到minHeap
- 否则
- 将num添加到maxHeap
- 维护大小差异



数据流中的中位数问题
中位数是有序整数列表中的中间值。如果列表的大小为偶数,则没有中间值。因此,中位数是中间两个值的平均值。
minHeap
(较大的数字)
maxHeap
(较小的数字)
中位数
- 如果num > minHeap.peek
- 将num添加到minHeap
- 否则
- 将num添加到maxHeap
- 维护大小差异
- [4, 2, 3, 1, 5]



Find Median in Data Stream
Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value.
Median
- If num > minHeap.peek
- Add into minHeap
- Otherwise
- Add into maxHeap
- Maintain size diff
- [4, 2, 3, 1, 5]
4



Find Median in Data Stream
Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value.
- If num > minHeap.peek
- Add into minHeap
- Otherwise
- Add into maxHeap
- Maintain size diff
- [4, 2, 3, 1, 5]
4
2



Find Median in Data Stream
Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value.
Median
- If num > minHeap.peek
- Add into minHeap
- Otherwise
- Add into maxHeap
- Maintain size diff
- [4, 2, 3, 1, 5]
4
2



Find Median in Data Stream
Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value.
Median
- If num > minHeap.peek
- Add into minHeap
- Otherwise
- Add into maxHeap
- Maintain size diff
- [4, 2, 3, 1, 5]
4
3
2



Find Median in Data Stream
Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value.
- If num > minHeap.peek
- Add into minHeap
- Otherwise
- Add into maxHeap
- Maintain size diff
- [4, 2, 3, 1, 5]
4
3
2
1



Find Median in Data Stream
Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value.
Median
- If num > minHeap.peek
- Add into minHeap
- Otherwise
- Add into maxHeap
- Maintain size diff
- [4, 2, 3, 1, 5]
4
3
2
1



Find Median in Data Stream
Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value.
Median
- If num > minHeap.peek
- Add into minHeap
- Otherwise
- Add into maxHeap
- Maintain size diff
- [4, 2, 3, 1, 5]
4
3
2
1
5



数据流中的中位数问题
class MedianFinder {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>();
// Adds a number into the data structure.
public void addNum(int num) {
if (!minHeap.isEmpty() && num > minHeap.peek()) {
minHeap.offer(num);
} else {
maxHeap.offer(-num);
}
if (minHeap.size() - maxHeap.size() == 2) {
maxHeap.offer(-minHeap.poll());
} else if (maxHeap.size() - minHeap.size() == 2) {
minHeap.offer(-maxHeap.poll());
}
}
// Returns the median of current data stream
public double findMedian() {
if (minHeap.size() > maxHeap.size()) {
return minHeap.peek();
}
if (minHeap.size() < maxHeap.size()) {
return -maxHeap.peek();
}
return (double)(minHeap.peek() - maxHeap.peek()) / 2.0;
}
};



数据流中的中位数问题
class MedianFinder {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>();
// Adds a number into the data structure.
public void addNum(int num) {
minHeap.offer(num);
maxHeap.offer(-minHeap.poll());
if (maxHeap.size() - minHeap.size() > 1) {
minHeap.offer(-maxHeap.poll());
}
}
// Returns the median of current data stream
public double findMedian() {
if (minHeap.size() == maxHeap.size()) {
return (double)(minHeap.peek() - maxHeap.peek()) / 2.0;
}
return -maxHeap.peek();
}
};



前N个数
给定m行,每行有n个数字,如果从每行中选择一个数字并将它们加起来,您可以得到n^m个不同的结果。请给出这些结果中最大的n个结果。输出也需要按降序排列。
示例:
m= 3, n = 3
5 8 7
2 9 5
0 2 3
结果:
[20, 19, 19]
提示: 20 = 8 + 9 + 3, 19 = 7 + 9 + 3, 19 = 8 + 9 + 2



前N个数字
当然,我们可以使用暴力方法
是否有更好的方法?
思考一下。我们每次添加一行中的数字,然后始终保持前n个。我们知道不在前n个数字中的数据永远不会成为最终结果的一部
因此,我们肯定会减少计算总和的时间。
如何维护前n个数字?我们使用小根堆



public List<Integer> topNumbers(int[][] numbers) {
int m = numbers.length;
int n = numbers[0].length;
PriorityQueue<Integer> minHeap = new PriorityQueue<>(n);
for (int i: numbers[0]) {
minHeap.add(i);
}
int[] list = new int[n];
for (int i = 1; i < m; i ++) {
for (int j = n - 1; j >= 0; j --) {
list[j] = minHeap.poll();
}
int[] cur = numbers[i];
Arrays.sort(cur, comparator);
int largest = cur[0];
for (int j = 0; j < n; j ++) {
minHeap.add(largest + list[j]);
}
for (int j = 1; j < n; j ++) {
for (int r = 0; r < n ; r ++) {
if (cur[j] + list[r] < minHeap.peek()) {
break;
}
minHeap.poll();
minHeap.add(cur[j] + list[r]);
}
}
}
List<Integer> result = new ArrayList<>();
result.addAll(minHeap);
Collections.sort(result, comparator);
return result;
}



Comparator<Integer> comparator = new Comparator<Integer>() {
public int compare(Integer a, Integer b) {
return b - a;
}
};



堆摘要
- 数据流
- 需要不断维护/更新。
- 需要最大或最小值,但无需排序。
- 其他用例:
-
堆排序
-



作业



【直通硅谷】09 堆
By ZhiTongGuiGu
【直通硅谷】09 堆
- 204