用户故事 | 刷算法面试题的4种思考方式

\u003cp\u003e不管是为了求职面试,还是为了提高自己的算法基础能力,“刷算法题”都是每个程序员的必经之路。如何对待刷题?如何让刷题变得更高效?我们搜集了来自《算法面试通关 40 讲》的用户分享,他们也许可以给你一点启发。\u003c/p\u003e\n\u003ch2\u003e@jason :最优解永远在探索中\u003c/h2\u003e\n\u003cp\u003e刚看老师的课程没多久, 收获不多, 我就把自己的第一道练习题解题心得发出来吧。这个是老师讲的第一道 leetcode 算法题: 两数之和\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e题目:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e示例:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e给定 nums = [2, 7, 11, 15], target = 9\u003c/p\u003e\n\u003cp\u003e因为 nums[0] + nums[1] = 2 + 7 = 9所以返回 [0, 1]\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e解答如下:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e完成这道题,第一次花了半个小时,时间还是蛮长的,毕竟是自己完成的第一道 leetcode 算法题。不过之前确实算法练习得很少,解法很简单,用的是穷举法,连续两次遍历,时间复杂度为 O(n²)。\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eint[] twoSum(int[] nums, int target) {\n if (nums.length \u0026lt; 2) {\n return new int[0];\n }\n for (int i = 0; i \u0026lt; nums.length; i++) {\n for (int j = i + 1; j \u0026lt; nums.length; j++) {\n if (nums[i] + nums[j] == target) {\n return new int[]{i, j};\n }\n }\n }\n return new int[0];\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e后来看了一下评论里其他同僚的解法,发现有很多很优化的解法。于是我把代码优化了一下,变成了下面这样:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eint[] twoSum2(int[] nums, int target) {\n\nMap\u0026lt;Integer, Integer\u0026gt; map = new HashMap\u0026lt;Integer, Integer\u0026gt;(SIZE); // 默认给 hashmap 初始化大小, 能够减少内部动态扩展空间, 复制速度造成的时间开销\n\n// 将数组存入 hashmap\n\nfor (int i = 0; i \u0026lt; nums.length; i++) {\n\n// 值为 key, 索引为 value\n\nmap.put(nums[i], i);\n\n}\n\n// 遍历数组里的每一个元素\n\nfor (int i = 0; i \u0026lt; nums.length; i++) {\n\n// 计算需要从 hashmap 里面找出的元素\n\nint complement = target - nums[i];\n\n// 判断 hashmap 里面是否存在该元素, 并且该元素不能与当前 nums[i] 是同一个元素\n\nif (map.containsKey(complement) \u0026amp;\u0026amp; map.get(complement) != i) {\n\nreturn new int[]{i, map.get(complement)};\n\n}\n\n}\n\nreturn new int[0];\n\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e将传入的数组转换成 hashmap, 利用 hashmap 查询速度快的优势 O(1),将整体查询时间降到 O(n),hashmap 通过以空间换取速度的方式,将查询速度提高到了 O(n),这里用到了分别两次的循环,虽然时间复杂度变成了 O(n),但实际上是两倍的 O(n)。\u003c/p\u003e\n\u003cp\u003e之后又思考了一下,发现一次循环也能解决问题,时间复杂度可以再次减半,变成真正的 O(n),于是便有了下面的代码。\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eint[] twoSum3(int[] nums, int target) {\n Map\u0026lt;Integer, Integer\u0026gt; map = new HashMap\u0026lt;Integer, Integer\u0026gt;(SIZE); // 默认给 hashmap 初始化大小, 能够减少内部动态扩展空间, 复制速度造成的时间开销\n for (int i = 0; i \u0026lt; nums.length; i++) {\n int complement = target - nums[i];\n if (map.containsKey(complement) \u0026amp;\u0026amp; map.get(complement) != i) {\n return new int[]{i, map.get(complement)};\n }\n map.put(nums[i], i);\n }\n return new int[0];\n }\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e之后我写了一个简单的测试案例来测试这 3 种算法的耗时,创建了一个长度为 10 万的数组,分别执行这 3 种算法,发现当数据量大的时候,第 2 种和第 3 种算法比第 1 种快了不是一个数量级。而且数据量越大,速度差异越明显:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eint[] nums = new int[100 * 1000];\n\n for (int i = 0; i \u0026lt; nums.length; i++) {\n if (i == nums.length - 1 - 1)\n nums[i] = 2;\n else if (i == nums.length - 1)\n nums[i] = 7;\n else\n nums[i] = 1;\n }\n\n long start = System.currentTimeMillis();\n //int[] indexResult = twoSum(nums, 9); // 数组长度 100000 耗时 1578 ms\n //int[] indexResult = twoSum2(nums, 9); // 数组长度 100000 耗时 20 ms\n int[] indexResult = twoSum3(nums, 9);// 数组长度 100000 耗时 14 ms\n\n long end = System.currentTimeMillis();\n System.out.printf(\u0026quot;%s, %s\u0026quot;, nums[indexResult[0]], nums[indexResult[1]]);\n\n System.out.printf(\u0026quot;\\r\\n 时间花费: %d ms\u0026quot;, end - start);\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e查看经典面试题:\u003ca href=\"https://time.geekbang.org/course/detail/130-42703\"\u003e两数之和\u003c/a\u003e\u003c/p\u003e\n\u003ch2\u003e@ elbowrocket:用理论指导实践\u003c/h2\u003e\n\u003cp\u003e之前做 leetcode 的题目一直没什么思路,我从课程中学到了如何运用所学理论去思考。\u003c/p\u003e\n\u003cp\u003e下面是今天刚看的题目:滑动窗口的最大值\u003c/p\u003e\n\u003cp\u003e给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口 k 内的数字。滑动窗口每次只向右移动一位。返回滑动窗口最大值。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e示例:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3输出: [3,3,5,5,6,7]\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e解释:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e滑动窗口的位置 最大值[1 3 -1] -3 5 3 6 7 31 [3 -1 -3] 5 3 6 7 31 3 [-1 -3 5] 3 6 7 51 3 -1 [-3 5 3] 6 7 51 3 -1 -3 [5 3 6] 7 61 3 -1 -3 5 [3 6 7] 7\u003c/p\u003e\n\u003cp\u003e注意:你可以假设 k 总是有效的,1 ≤ k ≤ 输入数组的大小,且输入数组不为空。进阶:你能在线性时间复杂度内解决此题吗?\u003c/p\u003e\n\u003cp\u003e下面是通过学习后得到的思路:\u003c/p\u003e\n\u003cp\u003e思路:\u003c/p\u003e\n\u003cp\u003e1、根据优先队列的概念,我们假设一个大顶堆,那么一开始的 [1,3,-1],这样一排列成堆的样子就是 3 在最上面,-1 在左下角,1 在右下角… 下一步就是 [3,-1,-3] 了,1 就要被挤开了,挤开了也不影响什么,-3 再加进来就好了。总之我们需要做的是:\u003c/p\u003e\n\u003cp\u003e(1)、维护我们的 Heap,也就是删除离开窗口的元素,加入新的元素。这里时间复杂度是 logK\u003c/p\u003e\n\u003cp\u003e(2)、Max-\u0026gt;Top,就是让结果是堆顶的元素。复杂度是 O(1),最后整体的复杂度是 NLogK。有没有更好的解法?\u003c/p\u003e\n\u003cp\u003e2、直接用队列,而且是双端队列,也就是两边都能进能出的队列。首先就是入队列,每次滑动窗口都把最大值左边小的数给杀死,也就是出队,后面再滑动窗口进行维护,这样相当于每一个数走过场,时间复杂度就是 O(N*1),比思路 1 要小。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e代码如下:\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eclass Solution:\n def maxSlidingWindow(self, nums, k):\n \u0026quot;\u0026quot;\u0026quot;\n :type nums: List[int]\n :type k: int\n :rtype: List[int]\n \u0026quot;\u0026quot;\u0026quot;\n # 严谨判断输入的数字是否合法\n if not nums:return []\n window, res = [], []\n for i, x in enumerate(nums):\n if i\u0026gt;=k and window[0] \u0026lt;= i-k: # 窗口滑动时的规律\n window.pop(0)\n while window and nums[window[-1]] \u0026lt;= x: # 把最大值左边的数小的就清除。\n window.pop()\n window.append(i)\n if i \u0026gt;= k-1:\n res.append(nums[window[0]])\n return res\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e希望能学习到更多东西。\u003c/p\u003e\n\u003cp\u003e查看白板理论讲解:\u003ca href=\"https://time.geekbang.org/course/detail/130-41561\"\u003e滑动窗口的最大值\u003c/a\u003e\u003c/p\u003e\n\u003ch2\u003e@梦想家罗西:学而时习之,不亦乐乎?\u003c/h2\u003e\n\u003cp\u003e看老师的课程大概一周了,之前看数据结构和算法懵懵懂懂的,老师结合实例题,一下子清楚很多了,特别是动态规划哪一课,一下子茅塞顿开。\u003c/p\u003e\n\u003cp\u003e\u003cimg src=\"https://static001.infoq.cn/resource/image/ac/bc/ac6385be9519494af227ba1ea959edbc.jpg\" alt=\"\" /\u003e\u003cbr /\u003e\n刷的一些算法题。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第一步:递归 + 暂存\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003efunction fib(n) {\n let memo = [];\n let r = null;\n if (n \u0026lt;= 1) {\n r = n;\n } else if (memo[n]) {\n r = memo[n];\n } else {\n memo[n] = fib(n - 1) + fib(n - 2);\n }\n return r;\n }\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e使用这种方法试了下 n=100 的时候直接挂了,效率依然很低。所以接下来第二步:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e第二步:动态规划\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003efunction fib(n) {\n let f = [0, 1];\n for (let i = 2; i \u0026lt;= n; i++) {\n f[i] = f[i - 2] + f[i - 1];\n }\n return f;\n }\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e其实矩阵相乘效率更高,等我学会了再来更新代码,学会了简单得动态规划已经很开心了。\u003c/p\u003e\n\u003cp\u003e戳此查看:\u003ca href=\"https://time.geekbang.org/course/detail/130-69763\"\u003e让你茅塞顿开的动态规划\u003c/a\u003e\u003c/p\u003e\n\u003ch2\u003e@Chenng:做题是一种享受,高效的思考模式受益终身\u003c/h2\u003e\n\u003cp\u003e听完最后一课,突然有点不舍。本来学习算法的初衷是为了面试,现在发现做题本身就是一种享受。\u003c/p\u003e\n\u003cp\u003e课上学到很多收益终身的思考模式:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e五种算法模式:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e递归\u003c/li\u003e\n\u003c/ul\u003e\n\u003cpre\u003e\u003ccode\u003efunction recursion(level, param1, param2) {\n // 递归终止条件\n if (level \u0026gt; MAX_LEVEL) {\n // 打印结果\n return;\n }\n\n // 处理当前层级的逻辑\n processData(level, data);\n\n // 递归\n recursion(level + 1, p1, p2);\n\n // 如果需要,反向当前层级\n reverseState(level);\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cul\u003e\n\u003cli\u003e递归 DFS\u003c/li\u003e\n\u003c/ul\u003e\n\u003cpre\u003e\u003ccode\u003econst visited = new Set();\nfunction dfs(node, visited) {\n visited.add(node);\n\n // 处理当前的 node\n\n for (let i = 0; i \u0026lt; node.children.length; i++) {\n const child = node.children[i];\n if (!visited.has(child)) {\n dfs(child, visited);\n }\n }\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cul\u003e\n\u003cli\u003eBFS\u003c/li\u003e\n\u003c/ul\u003e\n\u003cpre\u003e\u003ccode\u003econst visited = new Set();\nfunction bfs(grapg, start, end) {\n const queue = [];\n queue.push(start);\n visited.add(start);\n\n while (queue.length) {\n node = queue.pop();\n visited.add(node);\n\n process(node);\n nodes = generateRelatedNodes(node);\n queue.push(nodes);\n }\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cul\u003e\n\u003cli\u003e二分查找\u003c/li\u003e\n\u003c/ul\u003e\n\u003cpre\u003e\u003ccode\u003efunction binarySearch(arr, x) {\n let left = 0;\n let right = arr.length - 1;\n\n while(left \u0026lt;= right) {\n const mid = Math.floor((left + right) / 2);\n\n if (x === arr[mid]) {\n return mid;\n }\n\n if (x \u0026lt; arr[mid]) {\n right = mid - 1;\n continue;\n }\n\n if (x \u0026gt; arr[mid]) {\n left = mid + 1;\n continue;\n }\n }\n\n return -1;\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cul\u003e\n\u003cli\u003eDP 方程\u003c/li\u003e\n\u003c/ul\u003e\n\u003cpre\u003e\u003ccode\u003e// 状态定义\nconst dp = [[]];\n\n// 初始状态\ndp[0][0] = x;\ndp[0][1] = y;\n\n// DP 状态推导\n\nfor (let i = 0; i \u0026lt;= n; i++) {\n for (let j = 0; j \u0026lt;= matchMedia; j++) {\n dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]);\n }\n}\n\nreturn dp[m][n]\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2\u003e四个切题步骤\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eClarification:思考边界条件\u003c/li\u003e\n\u003cli\u003ePossible Solution:所有可能的解法、最优解\u003c/li\u003e\n\u003cli\u003eCoding\u003c/li\u003e\n\u003cli\u003eTest Cases\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2\u003e写在最后\u003c/h2\u003e\n\u003cp\u003e课程的结束不是终点,而是起点,加油,开启自己成为真正工程师的道路。\u003c/p\u003e\n\u003cp\u003e查看课程回顾:\u003ca href=\"https://time.geekbang.org/course/detail/130-73458\"\u003e面试切题四件套\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e感谢上面四位同学的精彩留言,欢迎大家在文末分享你的刷题故事和经验,我们一起进步。\u003c/p\u003e\n\u003ch2\u003e专栏简介\u003c/h2\u003e\n\u003cp\u003e我是覃超,作为 Facebook 早期员工 \u0026amp; 多年面试官,我对各大知名企业算法面试的考察点和面试套路,有非常清晰的理解以及丰富的第一手经验。在《算法面试通关 40 讲》这门课程里,我会帮你建立一套完整的算法切题思路,通过“白板演练 + 代码讲解”的方式,手把手带你掌握高效解题套路,彻底理解题目背后的考点,锻炼算法思维,让你在面试和平时的工作中大显身手。\u003c/p\u003e\n\u003cp\u003e学完这门课,你将收获以下四个方面:\u003c/p\u003e\n\u003cp\u003e1、常见算法知识点理论讲解\u003c/p\u003e\n\u003cp\u003e2、高频面试题目思路剖析\u003c/p\u003e\n\u003cp\u003e3、LeetCode 高效解题四步法\u003c/p\u003e\n\u003cp\u003e4、有效提升算法面试通过率\u003c/p\u003e\n\u003cp\u003e课程已更新完毕,共 62 讲,目前已有超过10000人加入学习,课程广受好评,期待你的加入!\u003ca href=\"https://time.geekbang.org/course/intro/130?utm_term=zeusTEPUZ\u0026amp;utm_source=web\u0026amp;utm_medium=infoq\u0026amp;utm_campaign=130-end\u0026amp;utm_content=article\"\u003e戳我立即订阅\u003c/a\u003e。\u003c/p\u003e\n
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值