LeetCode 热题 100-堆

LeetCode 热题 100-堆

数组中的第k个最大元素

堆排序

建立一个大小为k的小根堆,遍历数组中的元素,如果有大于堆顶的,则替换堆顶并调整堆,最终堆顶就是第k个最大的元素。

  • 小根堆操作:将以节点 i 为根的子树进行小根堆调整
    • 获取左右子节点索引和当前节点索引
    • 如果左子节点存在且小于当前节点,则更新最小节点索引;如果右子节点存在且小于当前节点或左子节点,则更新最小节点索引
    • 如果最小节点索引不等于当前节点索引,则交换当前节点和最小节点,并继续调整最小堆。
  • 建立小根堆: 从最后一个非叶子节点开始,依次向前调整节点,保证以每个节点为根的子树都是小根堆。
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
class Solution {
public int findKthLargest(int[] nums, int k) {
int n = nums.length;
// 建立小根堆
buildMinHeap(nums, k);
// 遍历剩余元素
for (int i = k; i < n; i++) {
if (nums[i] > nums[0]) {
swap(nums, i, 0);
minHeapify(nums, 0, k);
}
}
return nums[0];
}

private void minHeapify(int[] nums, int index, int heapSize) {
// 获取左右子节点索引和当前节点索引
int left = 2 * index + 1;
int right = 2 * index + 2;
int smallest = index;
// 如果左子节点存在且小于当前节点,则更新最小节点索引
if (left < heapSize && nums[left] < nums[smallest]) {
smallest = left;
}
// 如果右子节点存在且小于当前节点或左子节点,则更新最小节点索引
if (right < heapSize && nums[right] < nums[smallest]) {
smallest = right;
}
// 如果最小节点索引不等于当前节点索引,则交换当前节点和最小节点,并继续调整最小堆
if (smallest != index) {
swap(nums, index, smallest);
minHeapify(nums, smallest, heapSize);
}
}

private void buildMinHeap(int[] nums, int heapSize) {
// 从最后一个非叶子节点开始,依次向前调整节点,保证以每个节点为根的子树都是小根堆
for (int i = heapSize / 2; i >= 0; i--) {
minHeapify(nums, i, heapSize);
}
}

private void swap(int[] nums, int a, int b) {
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
  • 时间复杂度:O(nlong)
  • 空间复杂度:O(logn)

快速排序

使用快速排序在每次调整时,都会确定一个元素的最终位置,且以该元素为界限,将数组分成了左右两个子数组,左子数组中的元素都比该元素小,右子树组中的元素都比该元素大。

这样,只要某次划分的元素恰好是第k个下标就找到了答案。并且我们只需关注第k个最大元素所在区间的排序情况,与第k个最大元素无关的区间排序都可以忽略。这样进一步减少了执行步骤。

  • 分区函数:
    • 选择左边界元素作为基准元素;
    • 初始化左右指针;
    • 开始分区:
      • 从左往右找到第一个大于等于基准元素的元素;
      • 从右往左找到第一个小于等于基准元素的元素;
      • 如果左右指针相遇,则分区完成,退出循环;
      • 交换不符合条件的元素;
    • 分区完成,将基准元素放置在正确的位置上;
    • 返回基准元素的位置。
  • 快速选择:
    • 分区并获取基准元素的位置;
    • 将 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Solution {
public int findKthLargest(int[] nums, int k) {
return quickSelect(nums, 0, nums.length - 1, k);
}

private int quickSelect(int[] nums, int left, int right, int k) {
int pivotIndex = partion(nums, left, right);
int target = nums.length - k;
if (pivotIndex == target) {
return nums[pivotIndex];
} else if (pivotIndex < target) {
return quickSelect(nums, pivotIndex + 1, right, k);
} else {
return quickSelect(nums, left, pivotIndex - 1, k);
}
}
private int partion(int[] nums, int left, int right) {
// 选择左边界元素作为基准元素
int pivot = nums[left];
// 初始化左右指针
int i = left, j = right + 1;
// 开始分区
while (i < j) {
// 从左往右找到第一个大于等于基准元素的元素
while(++i < right && nums[i] < pivot);
// 从右往左找到第一个小于等于基准元素的元素
while(--j > left && nums[j] > pivot);
// 分区完成
if (i >= j) {
break;
}
// 交换
swap(nums, i, j);
}
// 将基准元素放置在正确的位置上
swap(nums, left, j);
return j;
}
private void swap(int[] nums, int a, int b) {
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(logn)

对于快排左右指针初始化及边界判断问题需要再好好想想。

前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
26
27
28
29
30
31
32
33
34
35
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 统计次数
HashMap<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
if (map.containsKey(num)) {
map.put(num, map.get(num) + 1);
} else{
map.put(num, 1);
}
}
// 建立最小堆
PriorityQueue<Integer> heap = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return map.get(a) - map.get(b);
}
});
for (Integer key : map.keySet()) {
if (heap.size() < k) {
heap.add(key);
} else if (map.get(key) > map.get(heap.peek())) {
heap.remove();
heap.add(key);
}
}
// 返回结果
int[] res = new int[k];
int i = 0;
while (!heap.isEmpty()) {
res[i++] = heap.remove();
}
return res;
}
}
  • 时间复杂度:O(nlogk)
  • 空间复杂度:O(n)

桶排序

使用哈希表统计频率,统计完成后,创建一个数组,将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标即可。

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
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 统计次数
HashMap<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
if (map.containsKey(num)) {
map.put(num, map.get(num) + 1);
} else{
map.put(num, 1);
}
}

// 桶排序
List<Integer>[] list = new List[nums.length + 1]; // 最多出现n次
for (int key : map.keySet()) {
// 出现次数作为下标
int i = map.get(key);
if (list[i] == null) {
list[i] = new ArrayList();
}
list[i].add(key);
}

// 返回结果
List<Integer> res = new ArrayList();
for (int i = list.length - 1; i >= 0 && res.size() < k; i--) {
if (list[i] != null) {
res.addAll(list[i]);
}
}
return res.stream().mapToInt(Integer::intValue).toArray();
}
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

数据流的中位数

随着 addNum 不断地添加数字,我们需要:

  • 保证 left 的大小和 right 的大小尽量相等。规定:在有奇数个数时,left 比 right 多 1 个数。
  • 保证 left 的所有元素都小于等于 right 的所有元素。

只要时时刻刻满足以上两个要求(满足中位数的定义),我们就可以用 left 中的最大值以及 right 中的最小值计算中位数。

分类讨论

即left是最大堆,right时最小堆。如果当前有奇数个元素,中位数是 left 的堆顶。如果当前有偶数个元素,中位数是 left 的堆顶和 right 的堆顶的平均值。

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
class MedianFinder {
private final PriorityQueue<Integer> left = new PriorityQueue<>((a,b) -> b - a); // 最大堆
private final PriorityQueue<Integer> right = new PriorityQueue<>(); // 最小堆

public MedianFinder() {

}

public void addNum(int num) {
// 加入右堆并移出堆顶至左堆
if (left.size() == right.size()) {
right.offer(num);
left.offer(right.poll());
}
// 加入左堆并移出堆顶至右堆
else {
left.offer(num);
right.offer(left.poll());
}
}

public double findMedian() {
// 奇数个元素
if (left.size() > right.size()) {
return left.peek();
}
// 偶数个
return (left.peek() + right.peek()) / 2.0;
}
}
  • 时间复杂度:O(logq),添加数字。
  • 空间复杂度:O(q)

不得不再次感叹灵神的思路太通透了!

总结

对于堆,可以自己用数组模拟实现,也可以使用PriorityQueue。对于java中的各种数据结构还非常不熟悉,要一边刷题一边复习数据结构的使用。


LeetCode 热题 100-堆
http://example.com/2024/12/16/posts/hot100-13/
作者
Xuan Yang
发布于
2024年12月16日
许可协议