目录
前言
昨晚面试后想到自己磕磕绊绊啃下的数据结构和算法,多少人对算法都是又爱又恨。面试过程中不少人都倒在了算法这一关,可见其重要性。
对于算法,一开始想到的就是《剑指Offer》这一本经典著作。但是对于前端学习者来讲其实并不友好,因为这本书里使用的语言是Java。于是我没来得及仔细看这本书,和许多朋友一样开始疯狂去刷牛客剑指Offer的JS试题。好不容易刷完了67道题,回想起来,似乎少了一些东西,显然这本书不是让我们去一味地刷题。。。
有点当初高考前的味道了,半夜3点,夜不能寐,下床翻看这本还挺新的《剑指Offer》,从目录开始看,我好像又读懂了一些,《剑指Offer》真正想告诉我们的不是题解,而是编程需要的八大能力!
一、对基础知识的掌握
不管你在计算机编程中使用的是哪一门语言,Java也好,C++、Python也罢,亦或是JavaScript这样人见人爱的语言,都必须要了解的常见数据结构和算法的使用——正是这本书开篇想要告诉我们的第一大能力:对基础知识掌握的扎实程度,决定了作为程序员的起点高度。
项目中使用最多的往往是对字符串、数组、链表、栈、树的操作,由此引申出查找(特别是二分查找)、排序(快排、归并)、回溯(迷宫类问题)、动态规划、位运算等算法。例题只是为了让作为初学者的我们更好理解这些算法的特点,最重要的还是以不变应万变、举一反三的能力。
比较难以理解的可能是动态规划,我推荐这篇掘金上的好文,简单易懂:浅谈javascript动态规划DP算法,比较有难度的例题是“正则表达式匹配”,这道题可以用递归写会比较容易理解,当然,用dp实现的话最好。
二、写出高质量的代码
1.规范性
相信在初学一门编程语言时老师或教程里都会提到过代码的规范性,写代码和写文章一样,i、j、k啥的就一个单词,小学生都会,多敲几个字母,表达清楚每行的意思,对项目维护有很大帮助——清晰的书写和布局以及合理的命名会极大地增加代码的可读性,我们要尽可能让自己写的代码赏心悦目。
实际上,不同公司会有自己的开发规范,前端我比较推荐阿里云的《前端JavaScript开发规范》。
2.完整性
切记切记,看到问题后莫要提笔就写、抬手就敲,面试官会非常关注应聘者考虑问题是否全面。有时候,面试官给出的问题往往存在摸棱两可的条件,所以一定要在写前问清楚,确定好。
作为开发人员,书中给了3个方面确保代码的完整性:
1. 功能测试
2. 边界测试
3. 负面测试
首先保证普通功能的实现,能够完成功能的基本需求;其次,对于循环和递归,往往我们需要确认其边界值是否正确,是否在合理范围之内;如,“打印从1到最大的n位数”,看似简单,但如果用常规的循环去写,如果n特别大,那么则会溢出,所以此时应该使用字符串去拼接。最后,我们还要考虑各种可能的错误输入,也就是负面测试。
三种错误处理的方法:
1. 通过return告知是否出错
2. 比前一种方便,设置全局变量,把函数返回值赋值给该变量传递错误信息
3. throw异常
3.鲁棒性
“鲁棒”是Robust的英译,也翻译成“健壮性”。鲁棒性对软件开发非常重要,也是面试官非常关注的考核内容。在编写程序时,要让程序判断输入是否合乎规范要求,并对不合乎要求的输入予以合理的处理。
最简单的就是防御性编程:在函数入口添加代码验证用户输入是否符合要求。鲁棒性还可以进一步分为稳定鲁棒性和性能鲁棒性。
三、解决问题的思路
面试者在编程前讲自己的思路是一个考察指标,陈述自己的分析过程,能更好地解决问题。
1、画图让抽象问题形象化
例如,“顺时针打印矩阵”这一题,起初我做起来的时候凭空想象会造成逻辑的混乱,通过画图可知,按从外圈到内圈顺序打印,对n×n矩阵,n>3时,首圈要走4步;只有两行时,一圈只许3步;1×3时,一圈2步;2×1时,只需1步。
function printMatrix(matrix)
{
// write code here
// 右、下、左、上顺序,边界规律
// [0][0],[len-1][0],[len-1][len-1],[0][len-1],
// [0][1],[len-2][1],[len-2][len-2],[1][len-2],
// [1][2],[len-3][2],[len-3][len-3],[2][len-3],
// [2][3],......
const arr = [];
if(!matrix.length || !matrix[0].length) return arr;
let left = 0,
right = matrix[0].length - 1,
up = 0,
down = matrix.length - 1;
while(true) {
for(let i = left; i <= right; i++) {
arr.push(matrix[up][i])
}
if (++up > down) break;
for(let i = up; i <= down; i++) {
arr.push(matrix[i][right])
}
if(--right < left) break;
for(let i = right; i >= left; i--) {
arr.push(matrix[down][i])
}
if(--down < up) break;
for(let i = down; i >= up; i--) {
arr.push(matrix[i][left])
}
if(++left > right) break;
}
return arr;
}
module.exports = {
printMatrix : printMatrix
};
2、举例让抽象问题具体化
问题太过于抽象时,不妨可以从一个具体的例子开始,一步步分析路径上包含哪些节点,这样就能找出其中的规律来。这一部分主要通过遍历二叉树的相关问题考察应聘者。
(PS:上面的顺时针打印矩阵我就是使用举例+画图法分析出来的)
3、分解让复杂问题简单化
总结起来就是一个词——各个击破。集中兵力打败小弟,最后大哥自然就倒台了。说白了其实就是分而治之的分治法。在“复杂链表的复制”这道题中,将链表的复制分三步走,问题便解决了:
第一步,复制原始链表的任意节点 N 并创建新节点 N’ ,再把 N’ 链接到 N 的后面
第二部,如果原始链表上的节点 N 的 next 指向 S , 则它对应的复制节点 N’ 的 next 指向 S 的复制节点 S’
第三步,把这个链表拆分成两个链表,奇数位置上的节点组成原始链表,偶数位置上的节点组成复制出来的链表
四、优化时间效率和空间效率
代码的时间效率体现应聘者对数据结构和算法工地的掌握程度。暂且抛开例题不说,看到这里让我想起来数组的排序算法对应的不同的时间效率和空间效率:
时间复杂度 | 空间复杂度 | 稳定性 | |
---|---|---|---|
冒泡排序 | O(n(n+1)/2)=O(n²) | O(1) | 稳定 |
选择排序 | O(n(n+1)/2)=O(n²) | O(1) | 稳定 |
插入排序 | O(n) ~ O(n²) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(n) | 稳定 |
希尔排序 | O(nlogn) ~ O(n²) | O(1) | 不稳定 |
快速排序 | O(nlog2n) ~ O(n²) | O(log2n) ~ O(n) | 不稳定 |
一般情况下,面试官更关注时间复杂度。
入门的话推荐这篇:算法的时间与空间复杂度(一看就懂)
余下的能力
总结
在编程种,真正的束缚不是一道道难题,其背后应该是思维的升华,这就是头秃的原因吧[狗头保命]