面试 TopK 的五种解法

本文探讨了TopK问题的多种解决方案,包括全局排序、局部排序、堆、随机选择等方法,详细分析了各种方法的时间复杂度及优化思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

面试中,TopK,是问得比较多的几个问题之一,到底有几种方法,这些方案里蕴含的优化思路究竟是怎么样的,今天和大家聊一聊。

问题描述

从arr[1, n]这n个数中,找出最大的k个数,这就是经典的TopK问题。

栗子

从arr[1, 12]={5,3,7,1,8,2,9,4,7,2,6,6} 这n=12个数中,找出最大的k=5个。

一、排序

排序是最容易想到的方法,将n个数排序之后,取出最大的k个,即为所得。

伪代码

sort(arr, 1, n);

return arr[1, k];

时间复杂度:O(n*lg(n))

分析:明明只需要TopK,却将全局都排序了,这也是这个方法复杂度非常高的原因。那能不能不全局排序,而只局部排序呢?这就引出了第二个优化方法。

二、局部排序

不再全局排序,只对最大的k个排序。

冒泡是一个很常见的排序方法,每冒一个泡,找出最大值,冒k个泡,就得到TopK。

伪代码

for(i=1 to k){

bubble_find_max(arr,i);

}

return arr[1, k];

时间复杂度:O(n*k)

分析:冒泡,将全局排序优化为了局部排序,非TopK的元素是不需要排序的,节省了计算资源。不少朋友会想到,需求是TopK,是不是这最大的k个元素也不需要排序呢?这就引出了第三个优化方法。

三、堆

思路:只找到TopK,不排序TopK。

先用前k个元素生成一个小顶堆,这个小顶堆用于存储,当前最大的k个元素。

接着,从第k+1个元素开始扫描,和堆顶(堆中最小的元素)比较,如果被扫描的元素大于堆顶,则替换堆顶的元素,并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。

直到,扫描完所有n-k个元素,最终堆中的k个元素,就是猥琐求的TopK。

伪代码

heap[k] = make_heap(arr[1, k]);

for(i=k+1 to n){

adjust_heap(heep[k],arr[i]);

}

return heap[k];

时间复杂度:O(n*lg(k))

画外音:n个元素扫一遍,假设运气很差,每次都入堆调整,调整时间复杂度为堆的高度,即lg(k),故整体时间复杂度是n*lg(k)。

分析:堆,将冒泡的TopK排序优化为了TopK不排序,节省了计算资源。堆,是求TopK的经典算法,那还有没有更快的方案呢?

四、随机选择

随机选择算在是《算法导论》中一个经典的算法,其时间复杂度为O(n),是一个线性复杂度的方法。

这个方法并不是所有同学都知道,为了将算法讲透,先聊一些前序知识,一个所有程序员都应该烂熟于胸的经典算法:快速排序。

画外音:

(1)如果有朋友说,“不知道快速排序,也不妨碍我写业务代码呀”…额...

(2)除非校招,我在面试过程中从不问快速排序,默认所有工程师都知道;

其伪代码是

void quick_sort(int[]arr, int low, inthigh){

if(low== high) return;

int i = partition(arr, low, high);

quick_sort(arr, low, i-1);

quick_sort(arr, i+1, high);

}

其核心算法思想是,分治法。

分治法(Divide&Conquer),把一个大的问题,转化为若干个子问题(Divide),每个子问题“”解决,大的问题便随之解决(Conquer)。这里的关键词是“都”。从伪代码里可以看到,快速排序递归时,先通过partition把数组分隔为两个部分,两个部分“都”要再次递归。

分治法有一个特例,叫减治法。

减治法(Reduce&Conquer),把一个大的问题,转化为若干个子问题(Reduce),这些子问题中“”解决一个,大的问题便随之解决(Conquer)。这里的关键词是“只”

二分查找binary_search,BS,是一个典型的运用减治法思想的算法,其伪代码是:

int BS(int[]arr, int low, inthigh, int target){

if(low> high) return -1;

mid= (low+high)/2;

if(arr[mid]== target) return mid;

if(arr[mid]> target)

return BS(arr, low, mid-1, target);

else

return BS(arr, mid+1, high, target);

}

从伪代码可以看到,二分查找,一个大的问题,可以用一个mid元素,分成左半区,右半区两个子问题。而左右两个子问题,只需要解决其中一个,递归一次,就能够解决二分查找全局的问题。

通过分治法与减治法的描述,可以发现,分治法的复杂度一般来说是大于减治法的:

快速排序:O(n*lg(n))

二分查找:O(lg(n))

话题收回来,快速排序的核心是:

i = partition(arr, low, high);

这个partition是干嘛的呢?

顾名思义,partition会把整体分为两个部分。

更具体的,会用数组arr中的一个元素(默认是第一个元素t=arr[low])为划分依据,将数据arr[low, high]划分成左右两个子数组:

  • 左半部分,都比t大
  • 右半部分,都比t小
  • 中间位置i是划分元素

以上述TopK的数组为例,先用第一个元素t=arr[low]为划分依据,扫描一遍数组,把数组分成了两个半区:

  • 左半区比t大
  • 右半区比t小
  • 中间是t

partition返回的是t最终的位置i。

很容易知道,partition的时间复杂度是O(n)。

画外音:把整个数组扫一遍,比t大的放左边,比t小的放右边,最后t放在中间N[i]。

partition和TopK问题有什么关系呢?

TopK是希望求出arr[1,n]中最大的k个数,那如果找到了第k大的数,做一次partition,不就一次性找到最大的k个数了么?

画外音:即partition后左半区的k个数。

问题变成了arr[1, n]中找到第k大的数。

再回过头来看看第一次partition,划分之后:

i = partition(arr, 1, n);

  • 如果i大于k,则说明arr[i]左边的元素都大于k,于是只递归arr[1, i-1]里第k大的元素即可;
  • 如果i小于k,则说明说明第k大的元素在arr[i]的右边,于是只递归arr[i+1, n]里第k-i大的元素即可;

画外音:这一段非常重要,多读几遍。

这就是随机选择算法randomized_select,RS,其伪代码如下:

int RS(arr, low, high, k){

if(low== high) return arr[low];

i= partition(arr, low, high);

temp= i-low; //数组前半部分元素个数

if(temp>=k)

return RS(arr, low, i-1, k); //求前半部分第k大

else

return RS(arr, i+1, high, k-i); //求后半部分第k-i大

}

这是一个典型的减治算法,递归内的两个分支,最终只会执行一个,它的时间复杂度是O(n)。

再次强调一下:

  • 分治法,大问题分解为小问题,小问题都要递归各个分支,例如:快速排序
  • 减治法,大问题分解为小问题,小问题只要递归一个分支,例如:二分查找,随机选择

通过随机选择(randomized_select),找到arr[1, n]中第k大的数,再进行一次partition,就能得到TopK的结果。

五、总结

TopK,不难;其思路优化过程,不简单:

  • 全局排序,O(n*lg(n))
  • 局部排序,只排序TopK个数,O(n*k)
  • 堆,TopK个数也不排序了,O(n*lg(k))
  • 分治法,每个分支“都要”递归,例如:快速排序,O(n*lg(n))
  • 减治法,“只要”递归一个分支,例如:二分查找O(lg(n)),随机选择O(n)
  • TopK的另一个解法:随机选择+partition
<think>好的,我现在需要帮用户准备LeetCode面试常见题型及解法。首先,用户已经提供了一些参考资料,包括引用[1]、[2]、[3],涉及计数问题、滑动窗口、哈希表等。我需要根据这些信息,结合常见的LeetCode题型,整理出一个结构清晰的回答。 首先,常见的LeetCode题型有哪些呢?通常包括数组和字符串、链表、树与图、动态规划、回溯算法、滑动窗口、哈希表、堆和栈、数学问题等。用户提到的引用中提到了计数类型、滑动窗口、哈希表,这些都需要重点覆盖。 接下来,我需要按照用户的要求,分点列出题型及解法,每个部分给出示例题号和解题思路。同时,要引用用户提供的参考资料中的例子,比如引用[1]中的387题,引用[2]中的3题,引用[3]中的快乐数问题。 然后,生成相关问题,可能需要根据每个题型提出相关问题,比如数组中的双指针应用,或者动态规划的优化方法。但根据用户示例中的相关问题,可能更偏向于针对每个题型的进一步提问,比如如何优化、应用场景等。 需要注意的是,用户要求回答结构清晰,逐步解决问题,保证真实可靠。所以每个题型需要简明扼要地说明解题思路,并附上例题和可能的代码示例,但代码示例需要符合用户给的格式,比如Python。 另外,用户提供的引用中有提到滑动窗口的时间复杂度优化到O(n),哈希表用于检测循环,这些都要在相应的部分提到,并正确标注引用。例如,在滑动窗口部分提到例题3,引用[2],在哈希表部分提到快乐数问题,引用[3]。 还需要确保数学表达式使用正确的格式,比如变量用$...$包裹,独立公式用$$...$$。不过当前内容可能数学公式不多,但如果有的话需要正确应用。 最后,生成相关问题部分,需要针对每个题型提出相关问题,可能包括解法优化、时间复杂度分析、应用场景等。例如,数组和字符串部分可能问如何优化空间复杂度,滑动窗口如何扩展到其他问题等。 现在,整理结构: 1. 数组与字符串:双指针、哈希表计数(例题387引用[1]) 2. 滑动窗口:无重复字符的最长子串(例题3引用[2]) 3. 哈希表应用:快乐数(引用[3]) 4. 链表:虚拟头节点、快慢指针 5. 树与二叉树:递归、DFS/BFS 6. 动态规划:状态转移方程 7. 回溯算法:排列组合问题 8. 堆与优先队列:Top K问题 每个部分给出解题思路、例题和代码示例(如有必要),并正确标注引用。然后生成相关问题,例如数组相关的优化方法,滑动窗口的复杂度分析等。 需要检查是否有遗漏的常见题型,比如栈、队列、位运算等,但考虑到用户提供的引用,可能重点放在提到的部分。不过为了全面性,还是应该覆盖更多常见题型。 现在开始组织内容,确保每个部分简洁,包含例题和引用,最后生成相关问题。</think>### LeetCode 面试常见题型及解法 #### 1. 数组与字符串 **核心方法**:双指针、哈希表计数、原地修改 - **双指针**:用于有序数组去重、两数之和等问题。例如用左右指针夹逼法在$O(n)$时间内找到两数之和。 - **哈希表计数**:统计元素频率,如$387$. 字符串中的第一个唯一字符[^1]。 ```python def firstUniqChar(s: str) -> int: freq = {} for c in s: freq[c] = freq.get(c, 0) + 1 for i, c in enumerate(s): if freq[c] == 1: return i return -1 ``` #### 2. 滑动窗口 **优势**:将暴力遍历优化到$O(n)$时间复杂度[^2]。 - **关键点**:窗口左右指针移动条件,如$3$. 无重复字符的最长子串。 ```python def lengthOfLongestSubstring(s: str) -> int: char_set = set() left = max_len = 0 for right in range(len(s)): while s[right] in char_set: char_set.remove(s[left]) left += 1 char_set.add(s[right]) max_len = max(max_len, right - left + 1) return max_len ``` #### 3. 哈希表与数学 **应用场景**:检测循环、快速查找。 - **快乐数问题**:通过哈希表记录平方和避免无限循环[^3]。 $$n = 19 \rightarrow 1^2 + 9^2 = 82 \rightarrow 8^2 + 2^2 = 68 \rightarrow \dots$$ #### 4. 链表 **核心技巧**:虚拟头节点、快慢指针。 - **虚拟头节点**:简化头节点删除操作。 - **快慢指针**:找中点或检测环,如$141$. 环形链表。 #### 5. 树与二叉树 **高频考点**:递归遍历、DFS/BFS。 - **递归三要素**:终止条件、本级任务、返回值。例如$104$. 二叉树的最大深度。 ```python def maxDepth(root: TreeNode) -> int: if not root: return 0 return 1 + max(maxDepth(root.left), maxDepth(root.right)) ``` #### 6. 动态规划 **核心思想**:定义状态转移方程,避免重复计算。 - **经典问题**:$70$. 爬楼梯($dp[n] = dp[n-1] + dp[n-2]$),背包问题。 #### 7. 回溯算法 **适用场景**:排列、组合、子集问题。 - **模板**:选择→递归→撤销选择,如$46$. 全排列。 #### 8. 堆与优先队列 **典型应用**:Top K 问题、合并K个有序链表。 - **Python实现**:使用`heapq`模块,如$215$. 数组中的第K个最大元素。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值