堆结构

通关进度

题目 说明
构造堆 通关
添加堆元素 通关
删除堆元素 通关

参考资料

应该是全网最详细的解释算法里堆的课时

堆中的每一个节点值都大于等于(或小于等于)子树中所有节点的值。或者说,任意一个节点的值都大于等于(或小于等于)所有子节点的值

  • 最大堆:堆中的每一个节点的值都大于等于子树中所有节点的值
  • 最小堆:堆中的每一个节点的值都小于等于子树中所有节点的值

堆的存储

堆的插入

1.将要插入的元素放到最后

2.从底向上,如果父结点比该元素小,则该节点和父结点交换,直到无法交换

堆的删除

根据堆的性质可知,最大堆的堆顶元素为所有元素中最大的,最小堆的堆顶元素是所有元素中最小的。当我们需要多次查找最大元素或者最小元素的时候,可以利用堆来实现。

删除堆顶元素后,为保持堆的性质,需要对堆的结构进行调整,我们将这个过程称之为”堆化”,堆化的方法分为两种:

  • 一种是自底向上的堆化,上述的插入元素所使用的就是自底向上的堆化,元素从最底部向上移动。
  • 另一种是自顶向下堆化,元素由最顶部向下移动

自底向上堆化

首先删除堆顶元素,使得数组中下标为 1 的位置空出。

比较根结点的左子节点和右子节点,也就是下标为 2,3 的数组元素,将较大的元素填充到根结点(下标为 1)的位置。

一直循环比较空出位置的左右子节点,并将较大者移至空位,直到堆的最底部

自顶向下堆化

自顶向下的堆化用一个词形容就是“石沉大海”,那么第一件事情,就是把石头抬起来,从海面扔下去。这个石头就是堆的最后一个元素,我们将最后一个元素移动到堆顶。

然后开始将这个石头沉入海底,不停与左右子节点的值进行比较,和较大的子节点交换位置,直到无法交换位置。

堆经典问题

通关进度

题目 说明
堆排序 通关
数组中第 K 大的元素 通关
合并 K 个排序链表 通关

堆排序

堆排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void heapSort(int[] arr) {
// 非法情况
if (arr == null || arr.length < 2) {
return;
}
// 插入操作
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}

int size = arr.length;

// 将大顶堆的堆顶元素和末尾元素交换位置
swap(arr, 0, --size);
while (size > 0) {
heapify(arr, 0, size);
swap(arr, 0, --size);
}
}

堆的插入

1
2
3
4
5
6
7
8
9
10
11
// 堆的插入
public void heapInsert(int[] arr, int index) {
// 若孩子节点大于父亲节点,则破坏大根堆性质,需要向上调整
while (arr[index] > arr[index - 1] / 2) {
int temp = arr[index];
arr[index] = arr[(index - 1) / 2];
arr[(index - 1) / 2] = temp;

index = (index - 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
// 堆调整
public void heapify(int[] arr, int index, int size) {
// left 左孩子
int left = index * 2 + 1;
while (left < size) {
// largest 表示index节点两个孩子较大的节点
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
// 当前节点和largest比较
largest = arr[largest] > arr[index] ? largest : index;
// 如果当前节点大于较大孩子节点,则不进行操作
if (largest == index) {
break;
}
// 如果当前节点小于较大孩子节点,则与孩子节点交换
int temp = arr[index];
arr[index] = arr[largest];
arr[largest] = temp;

// 向下继续调整
index = largest;
left = index * 2 + 1;
}
}

数组中第 K 大的元素

215. 数组中的第K个最大元素

问题

【LeetCode 215】:给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

算法思想

建立一个大根堆,做 k−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
public int findKthLargest(int[] nums, int k) {
int heapSize = nums.length;
// 建立初始大根堆
buildMaxHeap(nums, heapSize);
// 进行 K-1 次删除操作
for (int i = nums.length - 1; i >= nums.length - k + 1; --i) {
swap(nums, 0, i);
--heapSize;
// 堆化
heapify(nums, 0, heapSize);
}
return nums[0];
}

public void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}

// 构造大根堆
public void buildMaxHeap(int[] arr, int heapSize) {

// 从最后一个非叶子节点开始调整堆,编号从 0 开始
// heapSize / 2 是最后一个非叶子节点
for (int i = heapSize / 2; i >= 0; --i) {
heapify(arr, i, heapSize);
}
}

堆化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 堆调整
public void heapify(int[] arr, int index, int size) {
// left 左孩子
int left = index * 2 + 1;
while (left < size) {
// largest 表示index节点两个孩子较大的节点
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
// 当前节点和largest比较
largest = arr[largest] > arr[index] ? largest : index;
// 如果当前节点大于较大孩子节点,则不进行操作
if (largest == index) {
break;
}
// 如果当前节点小于较大孩子节点,则与孩子节点交换
int temp = arr[index];
arr[index] = arr[largest];
arr[largest] = temp;

// 向下继续调整
index = largest;
left = index * 2 + 1;
}
}

合并 K 个排序链表

23. 合并 K 个升序链表

问题

【LeetCode 23】:给你一个链表数组,每个链表都已经按升序排列。

小顶堆合并

  • 维护当前每个链表未合并的元素的最前面一个,k 个链表就最多有 k 个满足这样条件的元素
  • 每次在这些元素里面选取 val 属性最小的元素合并到答案中
  • 使用小顶堆数据结构选取最小元素
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
// 构造小顶堆
PriorityQueue<ListNode> queue;

public ListNode mergeKLists(ListNode[] lists) {

queue = new PriorityQueue<>(Comparator.comparing(listNode -> listNode.val));

// lists = [[1,4,5],[1,3,4],[2,6]]
// 此处的每个 node,其实是 lists 中链表数组的头指针
for (ListNode node : lists) {
// for loop:把 1, 1, 2 装入小顶堆,
// 即将 lists 中每个链表数组中最小元素装入小顶堆
if (node != null) {
queue.offer(node);
}
}

// 虚拟节点和尾指针
ListNode L = new ListNode(0);
ListNode tail = L;

while (!queue.isEmpty()) {
// 弹出(小顶)堆的堆顶元素
tail.next = queue.poll();
tail = tail.next;

// 如果弹出的 top 节点所属的链表还有剩余节点的情况下
if (tail.next != null) {
// 将 top 下一个指向节点送入优先队列
queue.offer(tail.next);
}
}
return L.next;
}

数据流的中位数

通关进度

题目 说明
数据流的中位数 通关

295. 数据流的中位数

问题

【LeetCode 295】:中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

左边大顶堆,右边小顶堆,小的加左边,大的加右边,平衡俩堆数,新加就弹出,堆顶给对家,奇数取多的,偶数取除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
// 小顶堆存储较大一半元素
PriorityQueue<Integer> minHeap;
// 大顶堆存储较小一半元素
PriorityQueue<Integer> maxHeap;

public MedianFinder() {
// 小顶堆
this.minHeap = new PriorityQueue<>();
// 大顶堆
this.maxHeap = new PriorityQueue<>((a, b) -> (b - a));
}

public void addNum(int num) {

if (minHeap.isEmpty() || num > minHeap.peek()) {
minHeap.offer(num);
// 小顶堆元素多,平衡一下
if (minHeap.size() - maxHeap.size() > 1) {
maxHeap.offer(minHeap.poll());
}
} else {
maxHeap.offer(num);
if (maxHeap.size() - minHeap.size() > 0) {
minHeap.offer(maxHeap.poll());
}
}
}

public double findMedian() {
if (minHeap.size() > maxHeap.size()) {
return minHeap.peek();
} else if (minHeap.size() < maxHeap.size()) {
return maxHeap.peek();
} else {
return (maxHeap.peek() + minHeap.peek()) / 2.0;
}
}