今天在知乎上看到一个回答《为何前端工程师那么难招?》,做者提到说有不少前端工程师甚至连单链表翻转都写不出来。说实话,来面试的孩子们原本就紧张,你要冷不丁问一句单链表翻转怎么写,估计不少人都会蒙掉。前端
因而我在leetcode 上找了一下这道题,看看我能不能写得出来。程序员
题目的要求很简单:面试
反转一个单链表。示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL算法
最后的解决就是这样的一行代码:编程
const reverseList = (head, q = null) => head !== null ? reverseList(head.next, { val: head.val, next: q }) : q;
答案并不重要,有意思的是整个的解题思路。json
在解题以前,咱们先来聊聊算法。严格来讲,单链表翻转这种问题只是对于链表这种数据结构的一种操控而已,根本谈不上是什么算法。固然,宽泛地来讲,只要涉及到循环和递归的都把它纳入到算法也能够。在这里,咱们采用一种宽容的定义。后端
算法须要背吗?我以为算法是不须要背的,你也不可能背的下来,光leetcode
就有上千道题目,而且还在增长,怎么可能背的下来?因此对于现阶段的程序员来讲,算法分为两类,一类是你本身能推算出来的,这种不用背,一类是你推算不出来的,好比KMP算法,这种也不用背,须要的时候直接Google
就能够了。特别是对于前端以及80%的后端程序员来讲,你须要什么算法,就直接使用如今的库就好了,数组排序直接array.sort
就能够,谁没事还非要去写一个快速排序?数组
那为何面试前端的时候还必需要考算法?这个道理基本上相似于经过考脑筋急转弯来测试智商同样,实际工做中是彻底用不上的,就像高考的时候考一大堆物理、化学、生物,巴不得你上知天文,下知地理,上下五千年,精通多国语言,但其实你参加工做之后发现根本用不上同样,这其实就是一个智商筛子,过滤一下而已。前端工程师
因此,别管工做中用不用获得,若是你想经过这道筛子的话,算法的东西多少仍是应该学习一些的。数据结构
说实话,我刚作这道题的时候,我也有点蒙。虽然上学的时候学过数据结构,链表、堆栈、二叉树这些东西,但这么多年实际工做中用的不多,几乎都快忘光了,不过不要紧,咱们就把它当成是脑筋急转弯来作一下好了。
咱们先来看一下它的数据结构是什么样的:
var reverseList = function(head) { console.log(head); };
ListNode { val: 1, next: ListNode { val: 2, next: ListNode { val: 3, next: [ListNode] } } }
一个对象里包含了两个属性,一个属性是val
,一个属性是next
,这样一层一层循环嵌套下去。
一般来说,在前端开发当中,咱们最经常使用的是数组。若是是用数组的话,就太简单了,js
数组自带reverse
方法,直接array.reverse
反转就好了。可是题目非要弄成链表的形式,说实在的,我真没有见过前端什么地方还须要用链表这种结构的(除了面试的时候),因此说这种题目对于实际工做是没什么用处的,可是脑筋急转弯的智商题既然这样出了,咱们就来看看怎么解决它吧。
首先想到的,这确定是一个while
循环,循环到最后,发现next
是null
就结束,这个很容易想。但关键是怎么倒序呢?这个地方须要稍微动一下脑子。咱们观察一下,倒序以后的结果,1
变成了最后一个,也就是说1
的next
是null
,而2
的next
是1
。因此咱们一上来先构建一个next
是null
的1
结点,而后读到2
的时候,把2
的next
指向1
,这样不就倒过了吗?因此一开始的程序写出来是这样的:
var reverseList = function(head) { let p = head; let q = { val: p.val, next: null }; while (p.next !== null) { p = p.next; q = { val: p.val, next: q }; } return q; };
先初始化了一个q
,它的next
是null
,因此它就是咱们的尾结点,而后再一个一个指向它,这样整个链表就倒序翻转过来了。
第一个测试用例没有问题,因而就提交了,可是提交完了发现不对,若是head
自己是null
的话,会报错,因此修改了一下:
var reverseList = function(head) { let p = head; if (p === null) { return null; } let q = { val: p.val, next: null }; while (p.next !== null) { p = p.next; q = { val: p.val, next: q }; } return q; };
这回就过了。
解决是解决了,可是这么长的代码,明显不够优雅,咱们尝试用递归的方法对它进一步优化。
若是有全局变量的话,递归自己并不复杂。但由于leetcode
里不容许用全局变量,因此咱们只好构造一个双参数的函数,把倒序以后的结果也做为一个参数传进去,这样刚一开始的时候q
是一个null
,随着递归的层层深刻,q
逐渐包裹起来,直到最后一层:
const reverseList = function(head) { let q = null; return r(head, q); } const r = function(p, q) { if (p === null) { return q; } else { return r(p.next, { val: p.val, next: q }); } }
这里咱们终于理清了出题者的思路,用递归的方式咱们能够把这个if
判断做为整个递归结束的必要条件。若是p
不是null
,那么咱们就再作一次,把p
的下一个结点放进来,好比说1
的下一个是2
,那么咱们这时候就从2
开始执行,直到最后走到5
,5
的下一个结点是null
,而后咱们退回上一层,这样一层层钻下去,最后再一层层返回来,就完成了整个翻转的过程。
递归成功以后,后面的事情就相对简单了。
怎么能把代码弄简短一些呢?咱们注意到这里这个if
语句里面都是直接return
,那咱们干脆直接作个三元操做符就行了:
const reverseList = function(head) { let q = null; return r(head, q); } const r = function(p, q) { return p === null ? q : r(p.next, { val: p.val, next: q }); }
更进一步,咱们用箭头函数来表示:
const reverseList = (head) => { let q = null; return r(head, q); } const r = (p, q) => { return p === null ? q : r(p.next, { val: p.val, next: q }); }
箭头函数还有一个特点是若是你只有一条return
语句的话,连外面的花括号和return
关键字均可以省掉,因而就变成了这样:
const reverseList = (head) => { let q = null; return r(head, q); } const r = (p, q) => (p === null ? q : r(p.next, { val: p.val, next: q }));
这样是否是看着就短多了呢?可是还能够更进一步简化,咱们把上面的函数再精简,这时候你仔细观察的话,会发现第一个函数和第二个函数很相似,都是在调用第二个函数,那么咱们能不能精简一下把它们合并呢?咱们先把第一个函数变换为和第二函数的参数数目一致的形式:
const reverseList = (head, q) => r(head, q); const r = (p, q) => (p === null ? q : r(p.next, { val: p.val, next: q }));
但这时候出现了一个问题,若是q
没有初始值的话,它是undefined
,不是null
,因此咱们还须要给q
一个初始值:
const reverseList = (head, q = null) => r(head, q); const r = (p, q) => (p === null ? q : r(p.next, { val: p.val, next: q }));
这时候咱们的两个函数长的基本一致了,咱们来把它们合并一下:
const reverseList = (head, q = null) => (head === null ? q : reverseList(head.next, { val: head.val, next: q }));
看,这样你就获得了一个一行代码的递归函数能够解决单链表翻转的问题。
实话说,即便是像我这样有多年经验的程序员,要解决这样的一个问题,都须要这么长的时间这么多步骤才能优化完美,更况且说一个大学刚毕业的孩子,很难当场就一次性回答正确,能把思路说出来就很不容易了,但你能够从这个过程当中看到程序代码是如何逐渐演进的。背诵算法没有意义,我以为咱们更多须要的是这一个思考的过程,毕竟编程是一个脑筋急转弯的过程,不是唐诗三百首。