【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

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

  • 2019-02-28
  • 本文字数:4368 字

    阅读完需:约 14 分钟

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

不管是为了求职面试,还是为了提高自己的算法基础能力,“刷算法题”都是每个程序员的必经之路。如何对待刷题?如何让刷题变得更高效?我们搜集了来自《算法面试通关 40 讲》的用户分享,他们也许可以给你一点启发。

@jason :最优解永远在探索中

刚看老师的课程没多久, 收获不多, 我就把自己的第一道练习题解题心得发出来吧。这个是老师讲的第一道 leetcode 算法题: 两数之和


题目:


给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。


示例:


给定 nums = [2, 7, 11, 15], target = 9


因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]


解答如下:


完成这道题,第一次花了半个小时,时间还是蛮长的,毕竟是自己完成的第一道 leetcode 算法题。不过之前确实算法练习得很少,解法很简单,用的是穷举法,连续两次遍历,时间复杂度为 O(n²)。


int[] twoSum(int[] nums, int target) {    if (nums.length < 2) {        return new int[0];    }    for (int i = 0; i < nums.length; i++) {        for (int j = i + 1; j < nums.length; j++) {            if (nums[i] + nums[j] == target) {                return new int[]{i, j};            }        }    }    return new int[0];}
复制代码


后来看了一下评论里其他同僚的解法,发现有很多很优化的解法。于是我把代码优化了一下,变成了下面这样:


int[] twoSum2(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<Integer, Integer>(SIZE); // 默认给 hashmap 初始化大小, 能够减少内部动态扩展空间, 复制速度造成的时间开销
// 将数组存入 hashmap
for (int i = 0; i < nums.length; i++) {
// 值为 key, 索引为 value
map.put(nums[i], i);
}
// 遍历数组里的每一个元素
for (int i = 0; i < nums.length; i++) {
// 计算需要从 hashmap 里面找出的元素
int complement = target - nums[i];
// 判断 hashmap 里面是否存在该元素, 并且该元素不能与当前 nums[i] 是同一个元素
if (map.containsKey(complement) && map.get(complement) != i) {
return new int[]{i, map.get(complement)};
}
}
return new int[0];
}
复制代码


将传入的数组转换成 hashmap, 利用 hashmap 查询速度快的优势 O(1),将整体查询时间降到 O(n),hashmap 通过以空间换取速度的方式,将查询速度提高到了 O(n),这里用到了分别两次的循环,虽然时间复杂度变成了 O(n),但实际上是两倍的 O(n)。


之后又思考了一下,发现一次循环也能解决问题,时间复杂度可以再次减半,变成真正的 O(n),于是便有了下面的代码。


int[] twoSum3(int[] nums, int target) {        Map<Integer, Integer> map = new HashMap<Integer, Integer>(SIZE); // 默认给 hashmap 初始化大小, 能够减少内部动态扩展空间, 复制速度造成的时间开销        for (int i = 0; i < nums.length; i++) {            int complement = target - nums[i];            if (map.containsKey(complement) && map.get(complement) != i) {                return new int[]{i, map.get(complement)};            }            map.put(nums[i], i);        }        return new int[0];    }
复制代码


之后我写了一个简单的测试案例来测试这 3 种算法的耗时,创建了一个长度为 10 万的数组,分别执行这 3 种算法,发现当数据量大的时候,第 2 种和第 3 种算法比第 1 种快了不是一个数量级。而且数据量越大,速度差异越明显:


int[] nums = new int[100 * 1000];
for (int i = 0; i < nums.length; i++) { if (i == nums.length - 1 - 1) nums[i] = 2; else if (i == nums.length - 1) nums[i] = 7; else nums[i] = 1; }
long start = System.currentTimeMillis(); //int[] indexResult = twoSum(nums, 9); // 数组长度 100000 耗时 1578 ms //int[] indexResult = twoSum2(nums, 9); // 数组长度 100000 耗时 20 ms int[] indexResult = twoSum3(nums, 9);// 数组长度 100000 耗时 14 ms
long end = System.currentTimeMillis(); System.out.printf("%s, %s", nums[indexResult[0]], nums[indexResult[1]]);
System.out.printf("\r\n 时间花费: %d ms", end - start);
复制代码


查看经典面试题:两数之和

@ elbowrocket:用理论指导实践

之前做 leetcode 的题目一直没什么思路,我从课程中学到了如何运用所学理论去思考。


下面是今天刚看的题目:滑动窗口的最大值


给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口 k 内的数字。滑动窗口每次只向右移动一位。返回滑动窗口最大值。


示例:


输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3 输出: [3,3,5,5,6,7]


解释:


滑动窗口的位置 最大值[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


注意:你可以假设 k 总是有效的,1 ≤ k ≤ 输入数组的大小,且输入数组不为空。进阶:你能在线性时间复杂度内解决此题吗?


下面是通过学习后得到的思路:


思路:


1、根据优先队列的概念,我们假设一个大顶堆,那么一开始的 [1,3,-1],这样一排列成堆的样子就是 3 在最上面,-1 在左下角,1 在右下角… 下一步就是 [3,-1,-3] 了,1 就要被挤开了,挤开了也不影响什么,-3 再加进来就好了。总之我们需要做的是:


(1)、维护我们的 Heap,也就是删除离开窗口的元素,加入新的元素。这里时间复杂度是 logK


(2)、Max->Top,就是让结果是堆顶的元素。复杂度是 O(1),最后整体的复杂度是 NLogK。有没有更好的解法?


2、直接用队列,而且是双端队列,也就是两边都能进能出的队列。首先就是入队列,每次滑动窗口都把最大值左边小的数给杀死,也就是出队,后面再滑动窗口进行维护,这样相当于每一个数走过场,时间复杂度就是 O(N*1),比思路 1 要小。


代码如下:


class Solution:    def maxSlidingWindow(self, nums, k):        """        :type nums: List[int]        :type k: int        :rtype: List[int]        """        # 严谨判断输入的数字是否合法        if not nums:return []        window, res = [], []        for i, x in enumerate(nums):            if i>=k and window[0] <= i-k: # 窗口滑动时的规律                window.pop(0)            while window and nums[window[-1]] <= x: # 把最大值左边的数小的就清除。                window.pop()            window.append(i)            if i >= k-1:                res.append(nums[window[0]])        return res
复制代码


希望能学习到更多东西。


查看白板理论讲解:滑动窗口的最大值

@梦想家罗西:学而时习之,不亦乐乎?

看老师的课程大概一周了,之前看数据结构和算法懵懵懂懂的,老师结合实例题,一下子清楚很多了,特别是动态规划哪一课,一下子茅塞顿开。



刷的一些算法题。


第一步:递归 + 暂存


function fib(n) {    let memo = [];    let r = null;    if (n <= 1) {      r = n;    } else if (memo[n]) {      r = memo[n];    } else {      memo[n] = fib(n - 1) + fib(n - 2);    }    return r;  }
复制代码


使用这种方法试了下 n=100 的时候直接挂了,效率依然很低。所以接下来第二步:


第二步:动态规划


function fib(n) {    let f = [0, 1];    for (let i = 2; i <= n; i++) {      f[i] = f[i - 2] + f[i - 1];    }    return f;  }
复制代码


其实矩阵相乘效率更高,等我学会了再来更新代码,学会了简单得动态规划已经很开心了。


戳此查看:让你茅塞顿开的动态规划

@Chenng:做题是一种享受,高效的思考模式受益终身

听完最后一课,突然有点不舍。本来学习算法的初衷是为了面试,现在发现做题本身就是一种享受。


课上学到很多收益终身的思考模式:


五种算法模式:


  • 递归


function recursion(level, param1, param2) {  // 递归终止条件  if (level > MAX_LEVEL) {    // 打印结果    return;  }
// 处理当前层级的逻辑 processData(level, data);
// 递归 recursion(level + 1, p1, p2);
// 如果需要,反向当前层级 reverseState(level);}
复制代码


  • 递归 DFS


const visited = new Set();function dfs(node, visited) {  visited.add(node);
// 处理当前的 node
for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; if (!visited.has(child)) { dfs(child, visited); } }}
复制代码


  • BFS


const visited = new Set();function bfs(grapg, start, end) {  const queue = [];  queue.push(start);  visited.add(start);
while (queue.length) { node = queue.pop(); visited.add(node);
process(node); nodes = generateRelatedNodes(node); queue.push(nodes); }}
复制代码


  • 二分查找


function binarySearch(arr, x) {  let left  = 0;  let right = arr.length - 1;
while(left <= right) { const mid = Math.floor((left + right) / 2);
if (x === arr[mid]) { return mid; }
if (x < arr[mid]) { right = mid - 1; continue; }
if (x > arr[mid]) { left = mid + 1; continue; } }
return -1;}
复制代码


  • DP 方程


// 状态定义const dp = [[]];
// 初始状态dp[0][0] = x;dp[0][1] = y;
// DP 状态推导
for (let i = 0; i <= n; i++) { for (let j = 0; j <= matchMedia; j++) { dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]); }}
return dp[m][n]
复制代码

四个切题步骤

  • Clarification:思考边界条件

  • Possible Solution:所有可能的解法、最优解

  • Coding

  • Test Cases

写在最后

课程的结束不是终点,而是起点,加油,开启自己成为真正工程师的道路。


查看课程回顾:面试切题四件套


感谢上面四位同学的精彩留言,欢迎大家在文末分享你的刷题故事和经验,我们一起进步。

专栏简介

我是覃超,作为 Facebook 早期员工 & 多年面试官,我对各大知名企业算法面试的考察点和面试套路,有非常清晰的理解以及丰富的第一手经验。在《算法面试通关 40 讲》这门课程里,我会帮你建立一套完整的算法切题思路,通过“白板演练 + 代码讲解”的方式,手把手带你掌握高效解题套路,彻底理解题目背后的考点,锻炼算法思维,让你在面试和平时的工作中大显身手。


学完这门课,你将收获以下四个方面:


1、常见算法知识点理论讲解


2、高频面试题目思路剖析


3、LeetCode 高效解题四步法


4、有效提升算法面试通过率


课程已更新完毕,共 62 讲,目前已有超过 10000 人加入学习,课程广受好评,期待你的加入!戳我立即订阅


2019-02-28 15:425661

评论

发布
暂无评论
发现更多内容

🏆【SpringBoot技术专题】「FtpServer文件服务」教你如何基于Springboot开发一个”可移植“的轻量级文件服务项目系统!

洛神灬殇

Apache springboot ftp服务 9月日更 FtpServer

让数据库无惧灾难,华为云GaussDB同城双集群高可用方案正式发布

华为云开发者联盟

数据库 高可用 集群 华为云GaussDB 同城双集群

PerfDog携手Imagination,助力开发者获取GPU关键数据

WeTest

如何撬动企业数字化转型?智能客服是关键支点

ToB行业头条

2021金九银十,啃完这35个Java技术栈,冲刺百万年薪不是梦

Java 架构 面试 程序人生 编程语言

云原生时代,如何构建数字化转型架构?

博文视点Broadview

GK架构营模4作业

Ping

遇见乌镇 | VoneCredit洞见供应链金融新未来

旺链科技

世界互联网大会 供应链金融

刷爆Leetcode!字节算法大佬进阶专属算法笔记:GitHub标星97k+

Java 架构 面试 程序人生 LeetCode

等保测评一次多少钱,收费标准是怎样的?

行云管家

网络安全 等保 等保测评 等保2.0

阿里内部最新“SpringCloudAlibaba学习笔记”(全彩第三版)限时开源

Java 架构 面试 微服务 Alibaba

浅谈语音质量保障:如何测试 RTC 中的音频质量?

阿里云视频云

阿里云 测试 WebRTC 语音 音频

【大咖直播】Elastic 企业搜索实战工作坊(第一期)

腾讯云大数据

elasticsearch

如何管理职场新人?

石云升

团队管理 管理 引航计划 内容合集 9月日更

应用开发中的存储架构进化史——从起步到起飞

Java 编程 架构 面试 后端

垂直CRM,能否走到终局?

ToB行业头条

Linux用户/用户组编辑

在即

9月日更

论文阅读丨神经清洁: 神经网络中的后门攻击识别与缓解

华为云开发者联盟

神经网络 深度学习 论文阅读

金秋国庆|官微掌门人火热征集!期待你的掌舵!

InfoQ写作社区官方

国庆中秋 热门活动

三款Linux文件传输工具简单介绍-行云管家

行云管家

Linux 文件传输 IT运维

音视频编解码 --X264码率控制初探

Fenngton

音视频 ffmpeg 编码 码率控制 引航计划

HarmonyOS荣膺2021世界互联网大会领先科技成果奖

Geek_283163

华为 鸿蒙

AI专家一席谈:复用算法、模型、案例,AI Gallery带你快速上手应用开发

华为云开发者联盟

算法 模型 案例 AI Gallery 应用开发

国庆临近,字节后端开发3+4面,终于拿到秋招第一个offer

Java 架构 面试 后端 计算机

智云盾捕获多个僵尸网络利用最新ConfluenceRCE漏洞的活动

百度开发者中心

安全 漏洞

第5章-《Linux一学就会》- Linux基本操作和服务器硬件选购指南

学神来啦

Linux linux运维 linux学习 Linux教程

如何使用ESD二极管,设计运算放大器电压保护?

不脱发的程序猿

电路设计 ESD二极管 运算放大器 电压保护 嵌入式硬件

金九银十面试如何得到面试官青睐?2021最新大厂Java面试真题合集(附权威答案)

Java 架构 面试 程序人生 编程语言

一文带你掌握工作流引擎flowable所有业务概念

小鲍侃java

后端 引航计划

ThingMap一键城市2.0重新出发:快速生成三维城市

ThingJS数字孪生引擎

地图 物联网 可视化 数字孪生

解密秒杀系统架构,不是所有的系统都能做秒杀!

华为云开发者联盟

架构 秒杀 电商系统

用户故事 | 刷算法面试题的4种思考方式_文化 & 方法_覃超_InfoQ精选文章