215. 数组中的第K个最大元素 https://leetcode.cn/problems/kth-largest-element-in-an-array/description/?envType=study-plan-v2&envId=top-100-liked 给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
示例 1:
输入: [3,2,1,5,6,4], k = 2 输出: 5 示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4 输出: 4
提示:
1 <= k <= nums.length <= 105 -104 <= nums[i] <= 104
方法:维护一个大小为 k 的小顶堆 核心思想 我们遍历整个数组,并始终维护一个包含 k 个元素的集合,这个集合里存的是我们到目前为止遇到的最大的 k 个数。遍历结束后,这个集合里的最小的那个数,就是整个数组中第 k 大的元素。
为了高效地实现这个思想,我们选择“小顶堆”作为存储这个集合的数据结构。
为什么是“小顶堆”?
小顶堆 (Min-Heap) :一种数据结构,它的特点是堆顶永远是整个堆中最小的元素。
为什么用它 :我们用一个大小固定为 k 的小顶堆来存储“目前为止最大的k个数”。堆顶的元素自然就是这 k 个数中最小的那个。当来了一个新数字,我们只需要将它和堆顶元素比较:
如果新数字比堆顶元素还小(或相等),那它肯定没资格进入“最大的k个数”的行列,直接忽略。
如果新数字比堆顶元素大,说明它更有资格。我们就把堆顶的最小元素踢出去,把这个新数字加进来,维持堆的大小仍然是 k。
这样,我们只需要 O(log k) 的时间就能完成一次比较和替换操作。
详细步骤
初始化 :创建一个小顶堆(在很多语言中也叫优先队列,设置为最小的元素优先级最高)。
遍历数组 :逐个处理输入数组 nums 中的每一个数字 num。
维护堆 :在处理每个数字 num 时,进行如下判断:
如果堆内元素不足 k 个 :直接将 num 放入堆中。
如果堆内元素已有 k 个 :
获取堆顶元素 top(也就是当前已知的第 k 大的元素)。
比较 num 和 top。
如果 num > top,说明 num 比当前“第k大”的元素还要大,那么它有资格成为新的“k大元素”之一。此时,我们先从堆中移除堆顶元素 top,再将新元素 num 加入堆。
如果 num <= top,说明 num 没有资格,我们什么都不做。
返回结果 :当遍历完整个数组后,堆里剩下的就是整个数组中最大的 k 个元素。而堆顶的那个元素,就是这 k 个数中最小的,也就是我们最终要找的第 k 个最大的元素 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.util.PriorityQueue;class Solution { public int findKthLargest (int [] nums, int k) { PriorityQueue<Integer> minHeap = new PriorityQueue <>(k); for (int num : nums) { if (minHeap.size() < k){ minHeap.offer(num); }else { if (num > minHeap.peek()){ minHeap.poll(); minHeap.offer(num); } } } return minHeap.peek(); } }
时间O(nlog(k)),空间O(k)
快速选择
对 n 个元素进行一次 分区 (Partition) 操作。这一步的成本依然是 O(n) 。
分区后,基准 (pivot) 到位,假设它的索引是 p。
判断 p 和我们目标索引的关系 (我们要找的第 k 大的元素,在排序后数组中的索引是 n-k)。
如果 p 正好是 n-k,那么我们找到了答案,算法结束。
如果 p 大于 n-k,说明目标元素在基准的左边 。我们只需要在左子数组中继续寻找 。
如果 p 小于 n-k,说明目标元素在基准的右边 。我们只需要在右子数组中继续寻找 。
和快排不同的是,我们直接抛弃了一半。平均情况就是n+(n/2)+(n/4)+……+1,求和后是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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import java.util.Random;class Solution { private final Random random = new Random (); public int findKthLargest (int [] nums, int k) { int n = nums.length; int targetIndex = n - k; int left = 0 ; int right = n - 1 ; while (true ){ int pivotIndex = partition(nums,left,right); if (pivotIndex == targetIndex){ return nums[pivotIndex]; } else if (pivotIndex > targetIndex) { right = pivotIndex - 1 ; }else { left = pivotIndex + 1 ; } } } private int partition (int [] nums,int left,int right) { int randomIndex = random.nextInt(left,right+1 ); swap(nums,left,randomIndex); int i = left; int j = right; int pivot = nums[left]; while (i < j){ while (i < j && nums[j] >= pivot){ j--; } if (i < j){ nums[i] = nums[j]; } while (i<j && nums[i] <= pivot){ i++; } if (i < j){ nums[j] = nums[i]; } } nums[i] = pivot; return i; } private void swap (int [] nums,int i,int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } }
347. 前 K 个高频元素 https://leetcode.cn/problems/top-k-frequent-elements/description/?envType=study-plan-v2&envId=top-100-liked
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入:nums = [1,1,1,2,2,3], k = 2
输出:[1,2]
示例 2:
输入:nums = [1], k = 1
输出:[1]
示例 3:
输入:nums = [1,2,1,2,1,2,3,1,3,2], k = 2
输出:[1,2]
提示:
1 <= nums.length <= 105 k 的取值范围是 [1, 数组中不相同的元素的个数] 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的
进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。
小顶堆法
使用一个哈希表统计频数
类似上一道topk问题
维护一个长度k的小顶堆。如果元素数目小于k直接入堆,否则和堆顶比,只要比堆顶大,就把堆顶出堆并且这个元素入堆,直到最后就是目标的k个元素。
(注意相等的时候也不操作,因为题目保证了频率最高的k个元素是唯一的。如果堆数目满足了k,并且堆顶和待测元素相等,最终必然两个都不在最终的目标集合里)
入堆出堆复杂度都是logk,最终复杂度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 import java.util.HashMap;import java.util.Map;import java.util.PriorityQueue;class Solution { public int [] topKFrequent(int [] nums, int k) { Map<Integer,Integer> frequencyMap = new HashMap <>(); for (int num : nums){ frequencyMap.put(num,frequencyMap.getOrDefault(num,0 ) + 1 ); } PriorityQueue<Map.Entry<Integer,Integer>> priorityQueue = new PriorityQueue <>((a,b) -> a.getValue() - b.getValue()); for (Map.Entry<Integer,Integer> entry : frequencyMap.entrySet()){ if (priorityQueue.size() < k){ priorityQueue.offer(entry); }else { if (entry.getValue() > priorityQueue.peek().getValue()){ priorityQueue.poll(); priorityQueue.offer(entry); } } } int [] result = new int [k]; for (int i = 0 ;i < k;i++){ result[i] = priorityQueue.poll().getKey(); } return result; } }
桶排序
先用哈希表统计频率
之后构造桶,桶索引代表频率,里面存的是频率为索引的元素
倒叙遍历桶,取出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 import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;class Solution { public int [] topKFrequent(int [] nums, int k) { Map<Integer, Integer> frequencyMap = new HashMap <>(); for (int num : nums) { frequencyMap.put(num, frequencyMap.getOrDefault(num, 0 ) + 1 ); } List<Integer>[] buckets = new List [nums.length + 1 ]; for (int i = 0 ; i < buckets.length;i++){ buckets[i] = new ArrayList <Integer>(); } for (Map.Entry<Integer,Integer> entry : frequencyMap.entrySet()){ int frequency = entry.getValue(); buckets[frequency].add(entry.getKey()); } int [] result = new int [k]; int index = 0 ; for (int i = buckets.length - 1 ;i >= 0 && index < k;i--){ if (!buckets[i].isEmpty()){ for (int num : buckets[i]){ if (index < k){ result[index++] = num; }else { break ; } } } } return result; } }
时间复杂度:O(n+m+m+k),m是不同元素数目,总共还是O(n)
空间O(n)
快速选择
统计频率 :使用 HashMap 统计数组 nums 中每个元素的出现频率。
提取唯一元素 :将 HashMap 中的所有键(即唯一元素)存入一个列表中。
执行快速选择 :
我们的目标是找到频率最高的 k 个元素。这等价于:如果我们将所有唯一元素按其频率从小到大 排序,那么我们需要的 k 个元素就位于排序后列表的末尾。
具体来说,如果共有 m 个唯一元素,那么频率第 k 高的元素的最终位置应该在索引 m - k 处。
快速选择算法的目的就是高效地找到这个“分割点”,使得位于索引 m - 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 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 import java.util.Arrays;import java.util.HashMap;import java.util.Map;import java.util.Random;class Solution { private int [] uniqueNums; private Map<Integer,Integer> frequenceMap; public int [] topKFrequent(int [] nums, int k) { frequenceMap = new HashMap <>(); for (int num : nums) { frequenceMap.put(num,frequenceMap.getOrDefault(num,0 )+1 ); } int m = frequenceMap.size(); uniqueNums = new int [m]; int i = 0 ; for (int num : frequenceMap.keySet()){ uniqueNums[i++] = num; } quickSelect(0 ,m-1 ,m-k); int [] result = new int [k]; System.arraycopy(uniqueNums,m-k,result,0 ,k); return result; } private void quickSelect (int left,int right,int targetIndex) { while (left < right){ int pivotIndex = partition(left,right); if (pivotIndex == targetIndex){ return ; }else if (pivotIndex < targetIndex){ left = pivotIndex+1 ; }else { right = pivotIndex - 1 ; } } } private int partition (int left,int right) { Random random = new Random (); int pivotIndex = random.nextInt(left,right+1 ); int pivotFrequence = frequenceMap.get(uniqueNums[pivotIndex]); swap(right,pivotIndex); int slow = left - 1 ; int fast = left; while (fast < right){ int frequence = frequenceMap.get(uniqueNums[fast]); if (frequence <= pivotFrequence){ slow++; swap(slow,fast); } fast++; } swap(slow+1 ,right); return slow+1 ; } private void swap (int i ,int j) { int temp = uniqueNums[i]; uniqueNums[i] = uniqueNums[j]; uniqueNums[j] = temp; } }
时间平均是O(n)
空间O(n)
295. 数据流的中位数 中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
例如 arr = [2,3,4] 的中位数是 3 。 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。 实现 MedianFinder 类:
MedianFinder() 初始化 MedianFinder 对象。
void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。
示例 1:
输入 [“MedianFinder”, “addNum”, “addNum”, “findMedian”, “addNum”, “findMedian”] [[], [1], [2], [], [3], []] 输出 [null, null, null, 1.5, null, 2.0]
解释 MedianFinder medianFinder = new MedianFinder(); medianFinder.addNum(1); // arr = [1] medianFinder.addNum(2); // arr = [1, 2] medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2) medianFinder.addNum(3); // arr[1, 2, 3] medianFinder.findMedian(); // return 2.0 提示:
-105 <= num <= 105 在调用 findMedian 之前,数据结构中至少有一个元素 最多 5 * 104 次调用 addNum 和 findMedian
用列表### 更优的解法:双堆(最大堆 + 最小堆)
这道题最经典和高效的解法是使用两个堆:
一个 最大堆 (Max Heap)
一个 最小堆 (Min Heap)
核心思想:
我们维护两个堆,使得数据流中的所有数字被分成两半。
最大堆 maxHeap 用于存储数据流中较小的那一半 数字。
最小堆 minHeap 用于存储数据流中较大的那一半 数字。
通过维持以下两个平衡条件,我们就可以在 O(1) 的时间内获取中位数:
maxHeap 中的所有元素都小于或等于 minHeap 中的所有元素。
两个堆的大小尽可能相等,否则maxHeap比minHeap多1。
如何工作:
addNum(num) :
将新数字 num 加入 maxHeap。
为了维持平衡条件1,将 maxHeap 的堆顶(最大值)弹出,并加入到 minHeap 中。
为了维持平衡条件2,如果 maxHeap 的大小小于 minHeap,则将 minHeap 的堆顶(最小值)弹出,并加入到 maxHeap 中。
findMedian() :
如果两个堆的大小相等(说明总元素个数是偶数),中位数就是两个堆顶元素的平均值:(maxHeap.top() + minHeap.top()) / 2。
如果 maxHeap 比 minHeap 多一个元素(说明总元素个数是奇数),中位数就是 maxHeap 的堆顶元素。 存储数据
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 import java.util.List;import java.util.PriorityQueue;class MedianFinder { private PriorityQueue<Integer> minHeap = new PriorityQueue <>(); private PriorityQueue<Integer> maxHeap = new PriorityQueue <>((a,b) -> b - a); public void addNum (int num) { maxHeap.add(num); int temp = maxHeap.poll(); minHeap.add(temp); if (maxHeap.size() < minHeap.size()){ int min_poll = minHeap.poll(); maxHeap.offer(min_poll); } } public double findMedian () { if (maxHeap.size() == minHeap.size()){ return (maxHeap.peek()+minHeap.peek())/2.0 ; }else { return (double ) maxHeap.peek(); } } }
每次add复杂度:O(logn)
查找中位数:O(1)
空间O(n)