摘要:遞歸版本尾遞歸很多遞歸沒(méi)辦法自然的寫(xiě)成尾遞歸,本質(zhì)原因是無(wú)法在多次遞歸過(guò)程中維護(hù)共有的變量,這也是循環(huán)的優(yōu)勢(shì)所在。這是因?yàn)殡m然用的,但并沒(méi)有開(kāi)啟尾遞歸優(yōu)化。
TL;DR
為一個(gè)已排序的鏈表去重,考慮到很長(zhǎng)的鏈表,需要尾調(diào)用優(yōu)化。系列目錄見(jiàn) 前言和目錄 。
需求實(shí)現(xiàn)一個(gè) removeDuplicates() 函數(shù),給定一個(gè)升序排列過(guò)的鏈表,去除鏈表中重復(fù)的元素,并返回修改后的鏈表。理想情況下鏈表只應(yīng)該被遍歷一次。
var list = 1 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5 -> null removeDuplicates(list) === 1 -> 2 -> 3 -> 4 -> 5 -> null
如果傳入的鏈表為 null 就返回 null 。
這個(gè)解決方案需要考慮鏈表很長(zhǎng)的情況,遞歸會(huì)造成棧溢出,所以遞歸方案必須用到尾遞歸。
因?yàn)槠拗疲@里并不解釋什么是尾遞歸,想詳細(xì)了解的可以先看看 尾調(diào)用 的定義。
遞歸版本 - 非尾遞歸對(duì)數(shù)組或者鏈表去重本身是個(gè)花樣很多的算法,但如果鏈表是已排序的,解法就單一很多了,因?yàn)橹貜?fù)的元素都是相鄰的。假定鏈表為 a -> a1 -> a2 ... aN -> b ,其中 a1 到 aN 都是對(duì) a 的重復(fù),那么去重就是把鏈表變成 a -> b 。
因?yàn)檫f歸版本沒(méi)有循環(huán),所以一次遞歸操作只能減去一個(gè)重復(fù)元素,比如第一次去除 a1 ,第二次去除 a2 。
先看一個(gè)簡(jiǎn)單的遞歸版本,這個(gè)版本遞歸的是 removeDuplicates 自身。先取鏈表的頭結(jié)點(diǎn) head,如果發(fā)現(xiàn)它跟之后的節(jié)點(diǎn)有重復(fù),就讓 head 指向之后的節(jié)點(diǎn)(減去一個(gè)重復(fù)),然后再把 head 放入下一個(gè)遞歸里。如果沒(méi)有重復(fù),則遞歸 head 的下一個(gè)節(jié)點(diǎn),并把結(jié)果指向 head.next 。
function removeDuplicates(head) { if (!head) return null const nextNode = head.next if (nextNode && head.data === nextNode.data) { head.next = nextNode.next return removeDuplicates(head) } head.next = removeDuplicates(nextNode) return head }
這個(gè)版本只有第一個(gè) return removeDuplicates(head) 處是尾遞歸,最后的 return head 并不是。所以這個(gè)解法并不算完全的尾遞歸,但性能并不算差。經(jīng)我測(cè)試可以處理 30000 個(gè)節(jié)點(diǎn)的鏈表,但 40000 個(gè)就一定會(huì)棧溢出。
遞歸版本 - 尾遞歸很多遞歸沒(méi)辦法自然的寫(xiě)成尾遞歸,本質(zhì)原因是無(wú)法在多次遞歸過(guò)程中維護(hù)共有的變量,這也是循環(huán)的優(yōu)勢(shì)所在。上面例子中的 head.next = removeDuplicates(nextNode) 就是一個(gè)典型,我們需要保留 head 這個(gè)變量,好在遞歸結(jié)束把結(jié)果賦值給 head.next 。尾遞歸優(yōu)化的基本思路,就是把共有的變量繼續(xù)傳給下一個(gè)遞歸過(guò)程,這種做法往往需要用到額外的函數(shù)參數(shù)。下面是一個(gè)改變后的尾遞歸版本:
function removeDuplicatesV2(head, prev = null, re = null) { if (!head) return re re = re || head if (prev && prev.data === head.data) { prev.next = head.next } else { prev = head } return removeDuplicatesV2(head.next, prev, re) }
我們加了兩個(gè)變量 prev 和 re 。prev 代表 head 的前一個(gè)節(jié)點(diǎn),在遞歸過(guò)程中我們判斷的是 prev 和 head 是否有重復(fù)。為了最后能返回鏈表的頭我們加了 re 這個(gè)參數(shù),它是最后的返回值。re 僅僅指向最開(kāi)始的 head ,也就是第一次遞歸的鏈表的頭結(jié)點(diǎn)。因?yàn)檫@個(gè)算法是修改鏈表自身,只要鏈表非空,頭結(jié)點(diǎn)作為返回值就是確定的,即使鏈表開(kāi)頭就有重復(fù),被移除的也是頭結(jié)點(diǎn)之后的節(jié)點(diǎn)。
如何測(cè)試尾遞歸首先我們需要一個(gè)支持尾遞歸優(yōu)化的環(huán)境。我測(cè)試的環(huán)境是 Node v7 。Node 應(yīng)該是 6.2 之后就支持尾遞歸優(yōu)化,但需要指定 harmony_tailcalls 參數(shù)開(kāi)啟,默認(rèn)并不啟動(dòng)。我用的 Mocha 寫(xiě)測(cè)試,所以把參數(shù)寫(xiě)在 mocha.opts 里,配置如下:
--use_strict --harmony_tailcalls --require test/support/expect.js
其次我們需要一個(gè)方法來(lái)生成很長(zhǎng)的,隨機(jī)重復(fù)的,生序排列的鏈表,我的寫(xiě)法如下:
// Usage: buildRandomSortedList(40000) function buildRandomSortedList(len) { let list let prevNode let num = 1 for (let i = 0; i < len; i++) { const node = new Node(randomBool() ? num++ : num) if (!list) { list = node } else { prevNode.next = node } prevNode = node } return list } function randomBool() { return Math.random() >= 0.5 }
然后就可以測(cè)試了,為了方便同時(shí)測(cè)試溢出和不溢出的情況,寫(xiě)個(gè) helper ,這個(gè) helper 簡(jiǎn)單的判斷函數(shù)是否拋出 RangeError 。因?yàn)楹瘮?shù)的邏輯已經(jīng)在之前的測(cè)試中保證了,這里就不測(cè)試結(jié)果是否正確了。
function createLargeListTests(fn, { isOverflow }) { describe(`${fn.name} - max stack size exceed test`, () => { it(`${isOverflow ? "should NOT" : "should"} be able to handle a big random list.`, () => { Error.stackTraceLimit = 10 expect(() => { fn(buildRandomSortedList(40000)) })[isOverflow ? "toThrow" : "toNotThrow"](RangeError, "Maximum call stack size exceeded") }) }) } createLargeListTests(removeDuplicates, { isOverflow: true }) createLargeListTests(removeDuplicatesV2, { isOverflow: false })
完整的測(cè)試見(jiàn) GitHub 。
順帶一提,以上兩個(gè)遞歸方案在 Codewars 上都會(huì)棧溢出。這是因?yàn)?Codewars 雖然用的 Node v6 ,但并沒(méi)有開(kāi)啟尾遞歸優(yōu)化。
循環(huán)版本思路一致,就不贅述了,直接看代碼:
function removeDuplicatesV3(head) { for (let node = head; node; node = node.next) { while (node.next && node.data === node.next.data) node.next = node.next.next } return head }
可以看到,因?yàn)檠h(huán)體外的共有變量 node 和 head ,這個(gè)例子代碼比遞歸版本要簡(jiǎn)單直觀很多。
總結(jié)循環(huán)和遞歸沒(méi)有孰優(yōu)孰劣,各有合適的場(chǎng)合。這個(gè) kata 就是一個(gè)循環(huán)比遞歸簡(jiǎn)單的例子。另外,尾遞歸因?yàn)橐獋鬟f中間變量,所以寫(xiě)起來(lái)的感覺(jué)會(huì)更類似循環(huán)而不是正常的遞歸思路,這也是為什么我對(duì)大部分 kata 沒(méi)有做尾遞歸的原因 -- 這個(gè)教程的目的是展示遞歸的思路,而尾遞歸有時(shí)候達(dá)不到這一點(diǎn)。
算法相關(guān)的代碼和測(cè)試我都放在 GitHub 上,如果對(duì)你有幫助請(qǐng)幫我點(diǎn)個(gè)贊!
參考資料Codewars Kata
GitHub 的代碼實(shí)現(xiàn)
GitHub 的測(cè)試
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/88095.html
摘要:微信公眾號(hào)記錄截圖記錄截圖目前關(guān)于這塊算法與數(shù)據(jù)結(jié)構(gòu)的安排前。已攻略返回目錄目前已攻略篇文章。會(huì)根據(jù)題解以及留言內(nèi)容,進(jìn)行補(bǔ)充,并添加上提供題解的小伙伴的昵稱和地址。本許可協(xié)議授權(quán)之外的使用權(quán)限可以從處獲得。 Create by jsliang on 2019-07-15 11:54:45 Recently revised in 2019-07-15 15:25:25 一 目錄 不...
摘要:月下半旬攻略道題,目前已攻略題。目前簡(jiǎn)單難度攻略已經(jīng)到題,所以后面會(huì)調(diào)整自己,在刷算法與數(shù)據(jù)結(jié)構(gòu)的同時(shí),攻略中等難度的題目。 Create by jsliang on 2019-07-30 16:15:37 Recently revised in 2019-07-30 17:04:20 7 月下半旬攻略 45 道題,目前已攻略 100 題。 一 目錄 不折騰的前端,和咸魚(yú)有什么區(qū)別...
摘要:我打算寫(xiě)一個(gè)鏈表操作的系列,來(lái)自的系列,實(shí)現(xiàn)語(yǔ)言是。通過(guò)自己實(shí)現(xiàn)一個(gè)鏈表和常用操作,可以加深理解這類數(shù)據(jù)結(jié)構(gòu)的優(yōu)缺點(diǎn)。鏈表經(jīng)常用來(lái)訓(xùn)練指針操作,雖然這只對(duì)適用,但等高級(jí)語(yǔ)言中控制引用的思路其實(shí)也差不多。 TL;DR 我打算寫(xiě)一個(gè)鏈表操作的系列,來(lái)自 Codewars 的 Linked List 系列 kata ,實(shí)現(xiàn)語(yǔ)言是 JavaScript 。這篇是開(kāi)篇,簡(jiǎn)單描述了一下我寫(xiě)這個(gè)的目...
摘要:題目要求從有序鏈表中刪除重復(fù)的數(shù)字,并且返回刪除后的頭結(jié)點(diǎn)例如輸入鏈表為返回這題和相似,只是數(shù)據(jù)結(jié)構(gòu)從數(shù)組變成了鏈表若還有更好的思路,請(qǐng)多多指教想要了解更多開(kāi)發(fā)技術(shù),面試教程以及互聯(lián)網(wǎng)公司內(nèi)推,歡迎關(guān)注我的微信公眾 題目要求: 從有序鏈表中刪除重復(fù)的數(shù)字,并且返回刪除后的頭結(jié)點(diǎn)例如輸入鏈表為1->1->2,返回1->2 這題和leetcode26相似,只是數(shù)據(jù)結(jié)構(gòu)從數(shù)組變成了鏈表 /*...
摘要:題目要求翻譯將鏈表中重復(fù)的元素全部刪除,返回新的頭結(jié)點(diǎn)。相比于,這里將重復(fù)的元素全部刪除。除此以外,我們還需要知道重復(fù)元素的前一個(gè)值和重復(fù)元素的最后一個(gè)值。如果存在重復(fù)值,則跳過(guò)重復(fù)值后,前節(jié)點(diǎn)不變,否則前節(jié)點(diǎn)跟隨后節(jié)點(diǎn)同時(shí)向后移動(dòng)。 題目要求 Given a sorted linked list, delete all nodes that have duplicate number...
閱讀 1342·2021-11-25 09:43
閱讀 1902·2021-11-12 10:36
閱讀 6002·2021-09-22 15:05
閱讀 3485·2019-08-30 15:55
閱讀 2013·2019-08-26 14:06
閱讀 3645·2019-08-26 12:17
閱讀 504·2019-08-23 17:55
閱讀 2456·2019-08-23 16:23