在过去的几年里,我面试了十几家公司,完成了大约 50 道个人算法问题。我经常收到反馈说我在算法问题上做得很好。在这篇文章中,我将分享我如何解决算法问题。
思考过程
我使用的一个指导原则是,每个面试问题都是为了解决而设计的。你不会被要求在面试中证明费马大定理。如果给你一些不可能的问题,那么无论如何你都不会有太多的机会。
根据我的经验,大约 80% 的算法问题会归结为几个核心数据结构和算法。我看到最多的数据结构是:
- 哈希表
- 链表
- 二叉搜索树
- 深度优先搜索
- 二分查找
- 排序算法
您还应该了解另外两种编程技术:
- 动态规划
- 递归
算法
算法是这样的:
- 在给出算法问题后,询问您的解决方案需要的特定运行时间。几乎可以肯定,面试官会告诉你。
- 过滤掉明显与手头问题无关的数据结构和算法。这将消除大部分列表,通常会留下 2-3 个数据结构和算法。
- 您可以过滤掉速度太慢的数据结构。如果你需要在 O(1) 时间内解决一个问题,那么在解决方案中不可能使用二叉树,因为二叉树总是至少需要 O(log n) 时间。
- 如果算法无法用于给定的问题,您可以过滤掉它们。如果问题中没有图形,您就知道深度优先搜索不相关。
- 浏览其余数据结构的用例,看看哪些与手头的问题相关。该问题的解决方案将是这些用例的组合。您需要做的就是将这些不同的用例拼凑在一起,然后您就会提出问题的解决方案。
案例
让我们来看看每个数据结构和算法的运行时和主要用例。然后通过几个示例,您可以了解使用该算法是多么容易。
数据结构:哈希表O(1)
运行:插入、查找和删除。
用例:
- 当您只需要存储和查找对象时。
- 当您需要按某些属性对不同组的对象列表进行分区时(基本上是 SQL 中的 group by 所做的)
- 您需要计算列表中不同项目的数量。
数据结构:链表O(1)
运行: 在您已经有指针指向的节点的末尾或旁边插入、查找和删除。
用例:
链表的主要用例围绕着这样一个事实,即链表维护元素的相对顺序。在编程面试中,链表主要用作堆栈或队列。
数据结构:二叉树O(log n)
运行: 插入、查找和删除。
用例:当您需要按排序顺序保存数据时使用。这使您可以快速回答问题,例如有多少元素落入给定范围或树中第 N 个最高的元素是什么。
数据结构:二分查找
运行:O(log n)
用例:
- 您需要在最接近另一个数字的排序数组中找到该数字。
- 您需要在排序数组中找到比另一个数字大的最小数字。
- 您需要在排序数组中找到小于另一个数字的最大数字。
- 如果由于某种原因无法使用哈希表,则可以使用二分搜索来检查给定元素是否在排序数组中。
数据结构:深度优先搜索
运行:O(n)遍历整个图。
用例:
- 在图中查找特定元素。
- 找出图的连通分量。
数据结构:排序
运行:O(n log n)
用例:
- 如果您需要按特定顺序处理元素,则可以使用。首先按该顺序排序,然后遍历元素。
- 可用于对稍后将执行二分搜索的数组进行排序。
复杂案例
现在让我们来看看几个不同的面试问题,以及我们如何使用算法来解决这些问题。
- 问题 1:构建速率限制器
您需要编写一个在任何 1 分钟窗口内最多只能调用 N 次的函数。例如,它在一分钟内最多可以调用 10 次。如果函数被调用超过 N 次,它应该抛出异常。该函数应该具有预期的 O(1) 性能。
看看我们可以使用的数据结构和算法,看看哪些可以用来解决问题。然后尝试看看如何使用这些数据结构来解决问题。在转向解决方案之前先试一试。
- 哈希表
- 链表
- 二叉树
- 深度优先搜索
- 二分查找
- 排序
- 动态规划
- 递归
让我们从消除所有显然不能使用的数据结构和算法开始:
- 哈希表
- 链表
- 二叉树 - 太慢了。
- 深度优先搜索- 太慢了。也没有运行 DFS 的图表。
- 二分查找- 太慢了。也没有排序数组。
- 排序- 太慢了。也没有要排序的元素。
- 动态规划 – 无法将动态规划应用于问题。
- 递归 – 无法对问题应用递归。
剩下的唯一数据结构是链表。查看用例,只有两个用于堆栈和队列。我们可以使用其中任何一个来跟踪函数在最后一分钟被调用的次数吗?是的!我们可以做的是保留一个队列,该队列在最后一分钟内每次调用该函数时都有一个条目。每当调用该函数时,我们都会从队列中删除一分钟前插入的所有条目。如果队列的长度仍然大于 N,我们将抛出异常。否则,我们将使用当前时间向队列添加一个新条目。通过使用计数器跟踪队列的长度,我们可以确定 O(1) 的预期时间,该函数将具有 O(1) 的预期性能。
- 问题#2:字谜
在阅读解决方案之前,再次尝试自己解决问题。以下是完整的数据结构和算法列表,供参考:
- 哈希表
- 链表
- 二叉树
- 深度优先搜索
- 二分查找
- 排序
- 动态规划
- 递归
让我们首先从列表中删除不可能用于此问题的项目:
- 哈希表
- 链表
- 二叉树 - 太慢了。
- 深度优先搜索 – 没有图表。
- 二分查找 – 没有排序数组。
- 排序 - 太慢了。
- 动态规划 – 无法将 DP 应用于问题。
- 递归 – 无法对问题应用递归。
此处似乎相关的唯一哈希表用例是能够根据属性将列表中的元素拆分为单独的组。在这种情况下,如果有办法根据哪些单词是彼此的字谜将列表分成不同的组,那将给我们一个解决问题的方法:
[list=1]
- 按字母顺序对单词的字符进行排序。因为字谜都是由相同的字母组成的。对于作为每个字谜的任何一对单词,这将为我们提供相同的字符串。
- 制作每个单词中每个字母出现的次数的字典。这个解决方案有点棘手,因为您需要以某种方式使用字典作为哈希表中的键。有些语言有办法做到这一点,而另一些则没有。
- 问题 #3:K 排序
像往常一样,这里是算法和数据结构的列表:
- 哈希表
- 链表
- 二叉树
- 深度优先搜索
- 二分查找
- 排序
- 动态规划
- 递归
让我们首先看看我们是否可以推断出有关运行时的任何信息。我们可能实现的最快运行时间是 O(n),因为这是遍历列表所需的时间。我们也可以始终对列表执行正常排序,这为我们提供 O(n log n) 的运行时间。让我们看看是否有可能比 O(n log n) 做得更好。是否有可能像 O(n) 一样快?好吧,如果 k=n,这个问题就变成了对列表进行排序的问题,所以不可能一直达到 O(n)。也许仍然有可能实现比 O(n log n) 更好的东西。现在让我们看看哪些数据结构和算法是相关的:
- 哈希表
- 链表
- 二叉树
- 深度优先搜索 – 没有图表。
- 二分查找 – 没有排序的数组。
- 排序 - 太慢了。
- 动态规划 - 不相关。
- 递归 - 不相关。
- 将输入数组的前 k 个元素插入二叉树。
- 遍历输入数组的其余部分。在每次迭代中,从二叉树中删除最小的元素并将其添加到结果数组中。然后将输入列表中的当前元素添加到二叉树中。
- 一旦我们到达输入数组的末尾,重复从二叉树中删除最小的元素,直到树为空。
假设您有一个比 O(n log k) 更快的算法。我们可以使用该算法提出比 O(n log n) 更快的排序算法,这是不可能的。假设我们有 n/k 个单独的列表,每个列表的大小为 k,每个列表的元素严格大于前一个列表的元素。如果将所有列表连接在一起,对它们运行 k-sorted,然后将每 k 个元素拆分为单独的列表,则您将在不到 O(n log k) 的时间内对 n/k 个列表进行排序。这意味着平均而言,您在不到 O(n/(n/k) log k) = O(k log k) 的时间内对每个列表进行了排序,这是不可能的。因此,没有任何 k 排序算法比 O(n log k) 快。
这意味着我们已经找到了问题的最佳解决方案。
希望此时我已经让您相信该算法是解决算法问题的有效方法。请注意,它不仅对解决面试中的问题有效,而且如果您在现实世界中遇到算法问题,您可以使用它来检查问题是否有由我们列表中的基本数据结构组成的解决方案。
如果你想学习其他解决问题的方法,我强烈推荐《如何解决它》这本书。这本书涵盖了大量不同的方法,您可以使用它们来解决任何问题。如何解决它对我今天处理任何类型的问题的方式产生了巨大的影响。