你有没有过这种崩溃:老板让你从10万个数据里找出“第5大的数”,你傻乎乎地把数组全排序,等了10分钟才出结果,结果被同事用另一种方法3秒搞定?其实找第k大元素根本不用全排序,Java里藏着“堆”和“快速选择”两大神器,像“挑西瓜只看熟不熟,不用全切开”一样聪明!今天用“选演唱会前排座位”的段子给你讲透,看完笑到会选,再也不用当“笨办法工具人”~
先吐槽“全排序法”:像给1000人排队,只为选第5个,笨到离谱!
新手找第k大元素,最爱用“全排序法”:把整个数组从大到小排好序,然后直接取第k-1个索引(数组从0开始)。比如数组`[3,1,4,2,5]`找第2大,排序后是`[5,4,3,2,1]`,取索引1的“4”,结果是对的,但过程太折腾!
这就像演唱会选前排座位:
- 现场有1000个观众,你要选“第5个能坐前排的人”,却让所有人按身高从高到矮排了1小时队,最后只把第5个人请上台——剩下995人白排了,纯属浪费时间!
全排序法的时间复杂度是O(nlogn)(n是数组长度),10万个元素排序要算半天;要是遇到1000万数据,电脑风扇能转得像直升机,老板看了都得骂“效率太低”!
神器1:优先队列(最小堆)——像“选5个保镖,最矮的就是第5强”
优先队列(Java里的`PriorityQueue`)默认是“最小堆”,就像一个“自动筛选器”:只留k个最大的元素,堆顶(最上面的元素)就是这k个里最小的,也就是整个数组的第k大元素。
用“选演唱会前排”类比一下,秒懂原理:
- 老板要选“第5大的元素”,相当于选“能坐第5排前排的人”,你只需要找5个最高的观众站成一圈,最矮的那个就是第5高(第5大);
- 流程:先让前5个人站成圈(堆大小=5),后面的人挨个来比,比圈里最矮的高,就把最矮的挤出去,自己进来;全部比完,圈里最矮的就是第5高。
Java代码实现(最小堆法):
import java.util.PriorityQueue;
public class FindKthLargest {
public int findKthLargest(int[] nums, int k) {
// 初始化最小堆,默认容量11,也可以指定k
PriorityQueue<Integer> minHeap = new PriorityQueue<>(k);
for (int num : nums) {
if (minHeap.size() < k) {
// 堆还没满,直接加人(前k个元素先入堆)
minHeap.add(num);
} else {
// 堆满了,新来的和堆顶(最矮的)比
if (num > minHeap.peek()) {
minHeap.poll(); // 挤走最矮的
minHeap.add(num); // 新的人进来
}
}
}
// 堆顶就是k个最大元素里最小的,即第k大
return minHeap.peek();
}
// 测试:数组[3,1,4,2,5]找第2大,应该返回4
public static void main(String[] args) {
System.out.println(new FindKthLargest().findKthLargest(new int[]{3,1,4,2,5}, 2));
}
}
为啥这招牛?
- 时间复杂度O(nlogk):比全排序的O(nlogn)快太多!k要是5,logk≈2,10万个元素只需要20万次操作,比全排序的10万×17≈170万次快8倍;
- 空间复杂度O(k):只占k个元素的空间,不像全排序要整个数组的空间,省内存!
神器2:快速选择(快排变种)——像“选班长,不用全投票,找到支持者够的人就行”
快速选择是快速排序的“亲戚”,核心思路和快排的“分区”一样:选一个“基准值”,把数组分成“比基准大的左区”和“比基准小的右区”,然后看基准值的位置是不是第k大,不用全排序。
用“选班长”类比,秒懂:
- 班里100人选“第5受欢迎的人”(第5大),你随便抓一个人当“候选人”,让喜欢他的站左边,不喜欢的站右边;
- 如果左边有4个人(比他受欢迎的有4个),那他就是第5受欢迎(左边4个+他自己=5),直接当选;
- 如果左边有6个人,说明要找的人在左边(比他更受欢迎);如果左边有3个人,要找的人在右边(没他受欢迎);
- 反复这个过程,直到找到“左边刚好有k-1个人”的候选人。
Java代码实现(快速选择法):
public class FindKthLargest {
public int findKthLargest(int[] nums, int k) {
// 第k大 = 数组排序后索引为 (nums.length - k) 的元素
return quickSelect(nums, 0, nums.length - 1, nums.length - k);
}
// 找索引为target的元素(类似快排分区)
private int quickSelect(int[] nums, int left, int right, int target) {
int pivotIndex = partition(nums, left, right); // 分区,得到基准值索引
if (pivotIndex == target) {
return nums[pivotIndex]; // 找到目标,返回
} else if (pivotIndex < target) {
// 目标在右边,继续找
return quickSelect(nums, pivotIndex + 1, right, target);
} else {
// 目标在左边,继续找
return quickSelect(nums, left, pivotIndex - 1, target);
}
}
// 快排分区:选最右边元素当基准,比基准小的放左边,大的放右边
private int partition(int[] nums, int left, int right) {
int pivot = nums[right]; // 基准值(选最右边)
int i = left - 1; // i是“比基准小的区域”的边界
for (int j = left; j < right; j++) {
if (nums[j] <= pivot) {
i++;
// 交换nums[i]和nums[j],把小的放左边
swap(nums, i, j);
}
}
// 把基准值放到正确位置(i+1)
swap(nums, i + 1, right);
return i + 1; // 返回基准值索引
}
private void swap(int[] nums, int a, int b) {
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
// 测试:数组[3,1,4,2,5]找第2大,返回4
public static void main(String[] args) {
System.out.println(new FindKthLargest().findKthLargest(new int[]{3,1,4,2,5}, 2));
}
}
这招更牛!
- 平均时间复杂度O(n):运气好的话,一次分区就找到,比堆还要快;
- 空间复杂度O(logn)(递归栈):比堆的O(k)更省空间,适合大数据量!
- 唯一缺点:最坏情况(基准值每次选到最小/最大)时间复杂度O(n^2),但实际中很少遇到,加个“随机选基准”就能优化。
3种方法大PK:选对方法,效率差100倍!
用“选演唱会座位”类比,给3种方法做个接地气的对比:
| 方法 | 像啥操作 | 时间复杂度 | 适合场景 | 槽点 |
|--------------|-----------------------------------|------------|-----------------------------------|-------------------------------|
| 全排序法 | 1000人全排队,只为选第5个 | O(nlogn) | 数据量小(n<1000),懒得想复杂方法 | 笨!数据量大时慢到哭 |
| 最小堆法 | 只留5个最高的人,最矮的就是第5高 | O(nlogk) | k很小(比如k=10),数据量极大 | k接近n时,和全排序差不多慢 |
| 快速选择法 | 选候选人分区,找到刚好够k-1支持者 | O(n)(平均)| 数据量大(n>10万),追求极致效率 | 最坏情况可能慢,需优化基准值 |
举个真实例子:1000万条数据找第100大,全排序要10秒,堆法要1秒,快速选择法只要0.3秒——这就是“方法选对,事半功倍”!
互动时间:来测测你的“选k大实战力”!
1. 数组`[5,3,1,7,2,9]`找第3大元素,用最小堆法(k=3),堆里最后会剩下哪3个元素?堆顶是啥?(提示:第3大是5)
2. 用快速选择法处理上面的数组,第一次选9当基准,分区后基准值的索引是多少?(提示:比9小的都在左边,索引5)
3. 你平时处理数据时,有没有用“笨办法”浪费时间的经历?比如为了找个排名,把所有数据全排序?评论区说说你的“踩坑史”!
评论区交出你的答案,前3名答对的送“Java数组算法手册”(含第k大、TopK、中位数查找的通俗讲解+代码模板)!关注我,下期揭秘“如何用快速选择法找数组中位数”——中位数就是“第n/2大”,一招搞定,实用到爆~