基础算法 - 堆
堆(Heap)作为其中一种重要的数据结构,以其独特的特性和广泛的应用领域,在算法和软件开发中扮演着关键角色。无论是在排序算法的实现中,还是在优先队列的管理中,堆都展现了其高效和强大的特性。
堆是一种完全二叉树,分为两种主要类型:
- 最大堆(Max Heap):每个节点的值都大于或等于其子节点的值。
- 最小堆(Min Heap):每个节点的值都小于或等于其子节点的值。
堆通常通过数组来实现,其主要操作包括:
- 插入(Insert):将新元素添加到堆中,并保持堆的特性。
- 删除根节点(Remove Root):移除并返回根节点,同时调整堆以保持其特性。
- 堆化(Heapify):将一个无序数组转换成堆的过程。
通过这些操作,堆能够高效地支持优先级队列等应用,是解决许多实际问题的重要工具之一。
相关题目
难度:中等
分治:
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
| public class Solution { private int quickSelect(List<Integer> nums, int k) { Random rand = new Random(); int pivot = nums.get(rand.nextInt(nums.size())); List<Integer> big = new ArrayList<>(); List<Integer> equal = new ArrayList<>(); List<Integer> small = new ArrayList<>(); for (int num : nums) { if (num > pivot) big.add(num); else if (num < pivot) small.add(num); else equal.add(num); } if (k <= big.size()) return quickSelect(big, k); if (big.size() + equal.size() < k) return quickSelect(small, k - big.size() - equal.size()); return pivot; } public int findKthLargest(int[] nums, int k) { List<Integer> numList = new ArrayList<>(); for (int num : nums) { numList.add(num); } return quickSelect(numList, k); } }
|
计数排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class Solution { public int findKthLargest(int[] nums, int k) { int min = -10000; int max = 10000; int[] cnt = new int[max-min+1]; for(int i=0;i<nums.length;i++){ cnt[nums[i]-min]++; } int idx = 0; for(int i=max-min;i>=0;i--){ k -= cnt[i]; if(k<=0)return i + min; } return 0; } }
|
这是最快的方法,因为nums[i]的大小已经被确定了,并且不算特别大,可以用空间换时间。
cnt[i] = j
表示 值i+min
出现了j
次,从后往前遍历cnt
数组,更新k
值,满足k <= 0
时就可以确定第k大的值了。
堆
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class Solution {
public int findKthLargest(int[] nums, int k) { int len = nums.length; PriorityQueue<Integer> minHeap = new PriorityQueue<>(k); for (int i = 0; i < k; i++) { minHeap.offer(nums[i]); } for (int i = k; i < len; i++) { Integer topElement = minHeap.peek(); if (nums[i] > topElement) { minHeap.poll(); minHeap.offer(nums[i]); } } return minHeap.peek(); } }
|
需要获得第K大的元素,一般场景下,K值都很小,小于总长度的一半(但是,题目场景中,K值可能会特别大)
当K比较小时,可以维护一个大小为K的最小堆,入堆的规则如下:
当新的元素要入堆时
- 堆为空,直接入堆
- 堆不为空,当前元素与堆顶(堆中所有元素的最小值)比较,如果当前元素小则跳过;如果当前元素更大,则让当前元素入堆。
- 如果当前堆还有空间,则直接入堆
- 如果当前堆已满,需要删除原堆顶(此时会产生新的堆顶)之后将新元素入堆。
这样,当遍历完数组的所有元素之后,留在堆中的K个元素是数组中前K大的元素。而堆顶元素即为这K个元素中最小的那个,因此它就是整个数组中的第K大元素。
这种方法利用了堆的特性,高效地维护了前K大的元素集合。由于堆的插入和删除操作的时间复杂度均为O(log K),因此在遍历数组时,每次操作的平均时间复杂度也是O(log K)。总的时间复杂度为O(n log K),其中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
| class Solution { public int[] topKFrequent(int[] nums, int k) { int[] res = new int[k]; HashMap<Integer, Integer> map = new HashMap<>(); for(int num : nums){ map.put(num, map.getOrDefault(num,0)+1); } PriorityQueue<Integer> heap = new PriorityQueue<>(k,(a, b) -> map.get(a) - map.get(b)); for(Integer key : map.keySet()){ if(heap.size()<k){ heap.offer(key); }else{ Integer peek = heap.peek(); if(map.get(peek)<map.get(key)){ heap.poll(); heap.offer(key); } } } for(int i=0;i<k;i++){ res[i] = heap.poll(); } return res; } }
|
同样也是维护大小为K的最小堆(使用Lambda表达式规定了比较大小的方式)
难度:困难
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
| class MedianFinder {
PriorityQueue<Integer> small; PriorityQueue<Integer> big;
public MedianFinder() { small = new PriorityQueue<>(); big = new PriorityQueue<>((a, b) -> b - a); }
public void addNum(int num) { if (small.size() == big.size()) { if (big.isEmpty() || num < small.peek()) big.offer(num); else { big.offer(small.poll()); small.offer(num); } } else { if (num > big.peek()) small.offer(num); else { small.offer(big.poll()); big.offer(num); } } }
public double findMedian() { if (small.size() != big.size() && !big.isEmpty()) { return (double) big.peek(); } else if (small.size() == big.size() && !small.isEmpty()) { return (double) (small.peek() + big.peek()) / 2; } return -1; } }
|
它的巧妙之处在于如何利用两个堆来高效地获取数据流的中位数。具体来说,一个堆(最大堆)用于存储数据流中较小的一半元素,另一个堆(最小堆)用于存储较大的一半元素,从而使得两个堆的组合能够快速确定中位数。
当两个堆的大小相等时,优先将新的数添加到 big
中(大顶堆)。
当 big
中的元素多一个时,将新的数添加到 small
中(小顶堆)。
在添加过程中,始终保持 big
中的所有元素都小于 small
中的所有元素。
这个方法能够处理动态数据流,可以随时添加新元素并获取当前中位数
1 2
| 大堆堆底 < 大堆堆顶 < 小堆堆顶 < 小堆堆底 根据当前进入的num,与大堆堆顶和小堆堆顶作比较,判断它应该去那个堆
|