今天在知乎上看到一個回答《爲何前端工程師那麼難招?》,做者提到說有不少前端工程師甚至連單鏈表翻轉都寫不出來。說實話,來面試的孩子們原本就緊張,你要冷不丁問一句單鏈表翻轉怎麼寫,估計不少人都會蒙掉。前端
因而我在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 }));
看,這樣你就獲得了一個一行代碼的遞歸函數能夠解決單鏈表翻轉的問題。
實話說,即便是像我這樣有多年經驗的程序員,要解決這樣的一個問題,都須要這麼長的時間這麼多步驟才能優化完美,更況且說一個大學剛畢業的孩子,很難當場就一次性回答正確,能把思路說出來就很不容易了,但你能夠從這個過程當中看到程序代碼是如何逐漸演進的。背誦算法沒有意義,我以爲咱們更多須要的是這一個思考的過程,畢竟編程是一個腦筋急轉彎的過程,不是唐詩三百首。