(1.8w字)負重前行,前端工程師如何系統練習數據結構和算法?【上】

準備了很長一段時間的練習攻略,同時也放到了 github倉庫, 也有其它的 在線閱讀地址。原創代碼和攻略文章不易,若是以爲不錯,請給倉庫點個 star 哦 : )

練習以前,你須要瞭解的

適用人羣

若是你正在爲面試作準備,卻對於龐雜的數據結構和算法知識,不知道何從下手;前端

若是你之前曾經學過一些基礎的數據結構或者算法的基礎知識,卻根本沒有理解清楚,更不能獨立完成算法的設計, 想從新鞏固這方面的知識;node

若是已經有了一小段工做經驗,但卻總是在成天用輪子調API當中度過,想看一看底層的源碼卻經常因編程的內力不足而放棄;git

若是你據說過LeetCode這個網站,想要一刷到底,邁向算法巔峯,卻由於浩瀚的題量缺少系統訓練感到無力,三天打魚兩天曬網,進而感到焦慮,甚至放棄......程序員

若是上面任何一條符合你的現狀,那麼恭喜你,你來對了地方。做爲一個圈內小有名氣的前端博主,我想借着個人影響力,分享出我係統梳理和練習的過程,但願可以幫助到更多跟我同樣遇到相似困難的人,讓你少一些沒必要要的折騰。github

全程使用的語言是 JavaScript,所以標題上說的是前端xxx, 但實際上你也知道,數據結構和算法這東西主要是考驗一我的的思惟,至於語言,其中並無用到任何 JS 的高深語法特性,只要有所編程經驗,能理解代碼是徹底沒有問題的,這一點你們放心。面試

算法沒有用?

在練習以前,首先闡明一下個人觀點,以避免你們對數據結構和算法或者這個系列產生更多的誤解。算法

我想各位當中確定有準備面試的同窗,那麼你確定據說過面試造火箭,工做擰螺絲, 很多人也拿這句話拿來詬病當前一些互聯網大廠的算法面試,所以就有這樣的言論: 除了應付面試,學算法其實沒啥用編程

這句話我並不徹底反對,由於如今隨着技術生態的發展,各位走在領域前沿的大牛們已經給你們備足了輪子,遇到通常的業務問題直接把人家的方案拿到用就能夠了,另外我也看到過一句話,剛開始以爲扯淡,後來想一想以爲有那麼一絲道理:後端

凡是須要跨過必定智商門檻才能掌握的技術,都不會輕易的流行。數組

換句話說:技術變得更簡單,別人更願意用,更容易流行。

這也是當前各類技術框架的真實寫照: 足夠好用,足夠簡單,簡單到你不須要知道底層複雜的細節

那麼問題來了,做爲一個集智慧和才華於一身的程序員,本身的價值在哪裏?

我以爲價值的大小取決於你可以解決的問題,若是說照着設計稿畫出一個簡單的 Button,你能完成,別的前端也能完成,甚至後後端的同窗都能把效果差很少作出來,那這個時候就談何我的價值?只不過在一個隨時可替代的崗位上完成了大多數人能輕易作到的事情,張三來完成,或者李四來完成,其實沒什麼區別。

可是如今若是面對的是一個複雜的工程問題,須要你來開發一個輔助業務的腳手架工具,改造框架源碼來提升項目的擴展性,或者面對嚴重的性能問題能立刻分析出緣由,而後給出解決的思路並在不一樣因素中平衡,這些都不是一個業餘的玩家可以在短期內勝任的,這就是體現本身價值的地方。

回到算法自己,它表明的是你解決更加複雜問題能力的一部分。

可能幹講不容易理解,咱們以 Vue 這個框架爲例,若是你之前沒有接觸過深度優先遍歷遞歸的概念,沒有看過相應的代碼,那麼虛擬 DOM 整個patch的源碼你是基本不可能看懂的;若是你沒有系統掌握過先進後出這種特色的應用,你也是很難理解 Vue 模板編譯階段爲何要用棧來檢查標籤是否正常閉合;一樣的,若是你沒有回溯這種算法的代碼經驗,你也是很難理解 Vue 模板編譯的優化階段,究竟是怎樣在從父到子深度優先遍歷的過程當中檢查到非靜態的子節點後給父節點打上標記;而且,若是你之前不知道 LRU 緩存淘汰算法到底是個什麼東西,你看到keep-alive組件的實現這裏會很是納悶:

if (cache[key]) {
  vnode.componentInstance = cache[key].componentInstance
  // make current key freshest
  remove(keys, key)
  keys.push(key)
}
複製代碼

緩存命中了,爲何還要維護一個 keys 數組,並且把這個 key 從數組中刪了,又要放到末尾,啥操做呢?

若是以前有相應算法基礎,你反而會以爲這個很是天然的事情。

看到了吧?你以爲尤大能寫出優秀的「明星」項目,會沒有一點扎實的數據結構和算法功底?

固然不只僅是前端領域,我想服務端也是差很少的狀況,這裏就很少舉例了。

因此各位,我認爲基本的算法能力對於一個想要解決複雜問題的工程師而言,不是一個加分項,並且是必備項。算法有大用。

如何系統練習?

接下來我來分享一下練習數據結構和算法的一些心得,或者方法,分兩個關鍵字來講: 系統練習

專題突破

如何作到系統的訓練?

我想按照LeetCode上的順序一題一題刷確定不是系統,大部分狀況是相鄰的幾個題毫無聯繫,這一題作了個鏈表相關的,下一題又是一個哈希表,再下一題又了一個二分搜索樹,思惟不斷的跳轉,一方面可能你的基礎很薄弱,各個數據結構和算法理解的並不深入,反覆跳到你不熟悉的地方,會挫敗你的信心,增長焦慮,甚至直接勸退,另外一方面,可能再遇到難一點的鏈表題,你又不會了,可能你會納悶: 不是剛剛纔寫出來了一個鏈表題嗎?怎麼如今又不會了?容易讓你產生自我懷疑,也會影響你訓練的自驅力。

所以我以爲分專題各個訓練突破是一個相對合理的戰略。在作到的同時,也會讓你掌握的更快,從而更容易解出相似的題,加強自信心。

另外要介紹的就是本系列的專題系統了,一共會分爲這些模塊:

本次分享的是鏈表篇、棧與隊列和二叉樹部分。

刻意練習

異類:不同的成功啓示錄》這本書裏面談到一萬小時刻意練習成爲高手的理論,後面也不少人談到這個理論,我幾年前就據說過,但真正體會到它的實際意義倒是最近在訓練算法的過程當中,並且是走了不少彎路以後。可見理論到實際的過程是多麼艱難。

我所理解的刻意練習,運用到算法的訓練上面來,就是兩點:

  1. 常常性地作你不會作的題

  2. 多種解法方式輪流試,最大化地挖掘一道題的價值

刻意練習有一個重要的觀點是走出溫馨圈。對於算法練習也是同樣,爲何不少人以爲刷算法沒用,那是由於他們老是在作本身熟悉的題型,用本身熟悉的方法,蹲在本身的溫馨圈,這樣作的再多也意義不大,可是若是老是在作本身不熟練的題型,用不同的方法,對於本身思惟的成長是至關有幫助的,因此我以爲把握常常性地作不會的題,這樣作的越多越好,固然了,時間有限,咱們須要根據本身的須要來取捨。

之因此我會把這個系列叫作練習,而不是刷題,就是由於練習的本質是,而不是簡單的AC就能夠。好比下面這一題:

可能有經驗的同窗,用遞歸的方式很容易就解出來了。可是,你有想過如何用非遞歸的方式嗎?若是能用非遞歸來是實現一遍,相信給本身帶來的幫助和提高會比遞歸解大得多。

另外說一句,本系列中全部的代碼都是原創的,並且不只僅給出遞歸代碼,在絕大多數狀況下會給出對應的非遞歸解法,深刻地挖掘一道題的最大價值,達到練習而不是刷題的效果。

最後的最後,我要強調的是: 對於這種修煉內功的練習,任何視頻或者專欄都僅僅只是輔助做用,最重要的是仍是本身的堅持和獨立思考,若是你經過我這個系列可以讓本身的算法能力更上一層樓,或者說可以有所收穫,你應該感謝的是你本身。若是以爲這個系列還不錯,但願能進GitHub 地址,給這個項目點一個 star,很是感謝!

鏈表篇

反轉鏈表

反轉鏈表這裏一共有三個題目供你們訓練。分別是原地單鏈表的反轉兩個一組反轉鏈表K個一組反轉鏈表,難度由階梯式上升。

而在面試當中凡是遇到鏈表,反轉類的題目出現的頻率也是首屈一指的,所以把它當作鏈表開篇的訓練類型,但願你們能引發足夠的重視💪。

No.1 簡單的反轉鏈表

反轉一個單鏈表。

示例:

輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL
複製代碼

來源: LeetCode 第 206 題

循環解決方案

這道題是鏈表中的經典題目,充分體現鏈表這種數據結構操做思路簡單, 可是實現上並無那麼簡單的特色。

那在實現上應該注意一些什麼問題呢?

保存後續節點。做爲新手來講,很容易將當前節點的 next指針直接指向前一個節點,但其實當前節點下一個節點的指針也就丟失了。所以,須要在遍歷的過程中,先將下一個節點保存,而後再操做next指向。

鏈表結構聲定義以下:

function ListNode(val) {
  this.val = val;
  this.next = null;
}
複製代碼

實現以下:

/** * @param {ListNode} head * @return {ListNode} */
let reverseList =  (head) => {
    if (!head)
        return null;
    let pre = null, cur = head;
    while (cur) {
        // 關鍵: 保存下一個節點的值
        let next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
};
複製代碼

因爲邏輯比較簡單,代碼直接一鼓作氣。不過僅僅寫完還不夠,對於鏈表問題,邊界檢查的習慣能幫助咱們進一步保證代碼的質量。具體來講:

  • 當 head 節點爲空時,咱們已經處理,經過✅
  • 當鏈表只包含一個節點時, 此時咱們但願直接返回這個節點,對上述代碼而言,進入循環後 pre 被賦值爲cur,也就是head,沒毛病,經過✅

運行在 LeetCode, 成功 AC ✌

但做爲系統性的訓練而言,單單讓程序經過未免太草率了,咱們後續會盡量地用不一樣的方式去解決相同的問題,達到融會貫通的效果,也是對本身思路的開拓,有時候或許能達到更優解

遞歸解決方案

因爲以前的思路已經介紹得很是清楚了,所以在這咱們貼上代碼,你們好好體會:

let reverseList = (head) =>{
  let reverse = (pre, cur) => {
    if(!cur) return pre;
    // 保存 next 節點
    let next = cur.next;
    cur.next = pre;
    reverse(cur, next);
  }
  return reverse(null, head);
}
複製代碼

No.2 區間反轉

反轉從位置 m 到 n 的鏈表。請使用一趟掃描完成反轉。

說明: 1 ≤ m ≤ n ≤ 鏈表長度。

示例:

輸入: 1->2->3->4->5->NULL, m = 2, n = 4
輸出: 1->4->3->2->5->NULL
複製代碼

來源: LeetCode 第 92 題

思路

這一題相比上一個整個鏈表反轉的題,實際上是換湯不換藥。咱們依然有兩種類型的解法:循環解法遞歸解法

須要注意的問題就是先後節點的保存(或者記錄),什麼意思呢?看這張圖你就明白了。

關於前節點和後節點的定義,你們在圖上應該能看的比較清楚了,後面會常常用到。

反轉操做上一題已經拆解過,這裏再也不贅述。值得注意的是反轉後的工做,那麼對於整個區間反轉後的工做,其實就是一個移花接木的過程,首先將前節點的 next 指向區間終點,而後將區間起點的 next 指向後節點。所以這一題中有四個須要重視的節點: 前節點後節點區間起點區間終點。接下來咱們開始實際的編碼操做。

循環解法

/** * @param {ListNode} head * @param {number} m * @param {number} n * @return {ListNode} */
var reverseBetween = function(head, m, n) {
    let count = n - m;
    let p = dummyHead = new ListNode();
    let pre, cur, start, tail;
    p.next = head;
    for(let i = 0; i < m - 1; i ++) {
        p = p.next;
    }
    // 保存前節點
    front = p;
    // 同時保存區間首節點
    pre = tail = p.next;
    cur = pre.next;
    // 區間反轉
    for(let i = 0; i < count; i++) {
        let next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    // 前節點的 next 指向區間末尾
    front.next = pre;
    // 區間首節點的 next 指向後節點(循環完後的cur就是區間後面第一個節點,即後節點)
    tail.next = cur;
    return dummyHead.next;
};
複製代碼

遞歸解法

對於遞歸解法,惟一的不一樣就在於對於區間的處理,採用遞歸程序進行處理,你們也能夠趁着複習一下遞歸反轉的實現。

var reverseBetween = function(head, m, n) {
  // 遞歸反轉函數
  let reverse = (pre, cur) => {
    if(!cur) return pre;
    // 保存 next 節點
    let next = cur.next;
    cur.next = pre;
    return reverse(cur, next);
  }
  let p = dummyHead = new ListNode();
  dummyHead.next = head;
  let start, end; //區間首尾節點
  let front, tail; //前節點和後節點
  for(let i = 0; i < m - 1; i++) {
    p = p.next;
  }
  front = p; //保存前節點
  start = front.next;
  for(let i = m - 1; i < n; i++) {
    p = p.next;
  }
  end = p;
  tail = end.next; //保存後節點
  end.next = null;
  // 開始穿針引線啦,前節點指向區間首,區間首指向後節點
  front.next = reverse(null, start);
  start.next = tail;
  return dummyHead.next;
}
複製代碼

No.3 兩個一組翻轉鏈表

給定一個鏈表,兩兩交換其中相鄰的節點,並返回交換後的鏈表。

你不能只是單純的改變節點內部的值,而是須要實際的進行節點交換。

來源: LeetCode 第 24 題

示例:

給定 1->2->3->4, 你應該返回 2->1->4->3.
複製代碼

思路

如圖所示,咱們首先創建一個虛擬頭節點(dummyHead),輔助咱們分析。

首先讓 p 處在 dummyHead 的位置,記錄下 p.next 和 p.next.next 的節點,也就是 node1 和 node2。

隨後讓 node1.next = node2.next, 效果:

而後讓 node2.next = node1, 效果:

最後,dummyHead.next = node2,本次翻轉完成。同時 p 指針指向node1, 效果以下:

依此循環,若是 p.next 或者 p.next.next 爲空,也就是找不到新的一組節點了,循環結束。

循環解決

思路清楚了,其實實現仍是比較容易的,代碼以下:

var swapPairs = function(head) {
    if(head == null || head.next == null) 
        return head;
    let dummyHead = p = new ListNode();
    let node1, node2;
    dummyHead.next = head;
    while((node1 = p.next) && (node2 = p.next.next)) {
        node1.next = node2.next;
        node2.next = node1;
        p.next = node2;
        p = node1;
    }
    return dummyHead.next;
};
複製代碼

遞歸方式

var swapPairs = function(head) {
    if(head == null || head.next == null)
        return head;
    let node1 = head, node2 = head.next;
    node1.next = swapPairs(node2.next);
    node2.next = node1;
    return node2;
};
複製代碼

利用遞歸方式以後,是否是感受代碼特別簡潔?😃😃😃

但願你能好好體會一下遞歸調用的過程,相信理解以後對本身是一個很大的提高。

No.4 K個一組翻轉鏈表

給你一個鏈表,每 k 個節點一組進行翻轉,請你返回翻轉後的鏈表。

k 是一個正整數,它的值小於或等於鏈表的長度。

若是節點總數不是 k 的整數倍,那麼請將最後剩餘的節點保持原有順序。

示例 :

給定這個鏈表:1->2->3->4->5
當 k = 2 時,應當返回: 2->1->4->3->5
當 k = 3 時,應當返回: 3->2->1->4->5
複製代碼

說明 :

  • 你的算法只能使用常數的額外空間。
  • 你不能只是單純的改變節點內部的值,而是須要實際的進行節點交換。

來源: LeetCode 第 25 題

思路

思路相似No.3中的兩個一組翻轉。惟一的不一樣在於兩個一組的狀況下每一組只須要反轉兩個節點,而在 K 個一組的狀況下對應的操做是將 K 個元素的鏈表進行反轉。

遞歸解法

這一題我以爲遞歸的解法更容易理解,所以,先貼上遞歸方法的代碼。

如下代碼的註釋中`首節點`、`尾結點`等概念都是針對反轉前的鏈表而言的。

/** * @param {ListNode} head * @param {number} k * @return {ListNode} */
var reverseKGroup = function(head, k) {
    let pre = null, cur = head;
    let p = head;
    // 下面的循環用來檢查後面的元素是否能組成一組
    for(let i = 0; i < k; i++) {
        if(p == null) return head;
        p = p.next;
    }
    for(let i = 0; i < k; i++){
        let next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    // pre爲本組最後一個節點,cur爲下一組的起點
    head.next = reverseKGroup(cur, k);
    return pre;
};
複製代碼

循環解法

重點都放在註釋裏面了。

var reverseKGroup = function(head, k) {
    let count = 0;
    // 看是否能構成一組,同時統計鏈表元素個數
    for(let p = head; p != null; p = p.next) {
        if(p == null && i < k) return head;
        count++;
    }
    let loopCount = Math.floor(count / k);
    let p = dummyHead = new ListNode();
    dummyHead.next = head;
    // 分紅了 loopCount 組,對每個組進行反轉
    for(let i = 0; i < loopCount; i++) {
        let pre = null, cur = p.next;
        for(let j = 0; j < k; j++) {
            let next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        // 當前 pre 爲該組的尾結點,cur 爲下一組首節點
        let start = p.next;// start 是該組首節點
        // 開始穿針引線!思路和2個一組的狀況如出一轍
        p.next = pre;
        start.next = cur;
        p = start;
    }
    return dummyHead.next;
};
複製代碼

環形鏈表

No.1 如何檢測鏈表造成環?

給定一個鏈表,判斷鏈表中是否造成環。

思路

思路一: 循環一遍,用 Set 數據結構保存節點,利用節點的內存地址來進行判重,若是一樣的節點走過兩次,則代表已經造成了環。

思路二: 利用快慢指針,快指針一次走兩步,慢指針一次走一步,若是二者相遇,則代表已經造成了環。

可能你會納悶,爲何思路二用兩個指針在環中必定會相遇呢?

其實很簡單,若是有環,二者必定同時走到環中,那麼在環中,選慢指針爲參考系,快指針每次相對參考系向前走一步,終究會繞回原點,也就是回到慢指針的位置,從而讓二者相遇。若是沒有環,則二者的相對距離愈來愈遠,永遠不會相遇。

接下來咱們來編程實現。

方法一: Set 判重

/** * @param {ListNode} head * @return {boolean} */
var hasCycle = (head) => {
    let set = new Set();
    let p = head;
    while(p) {
        // 同一個節點再次碰到,表示有環
        if(set.has(p)) return true;
        set.add(p);
        p = p.next;
    }
    return false;
}
複製代碼

方法二: 快慢指針

var hasCycle = function(head) {
    let dummyHead = new ListNode();
    dummyHead.next = head;
    let fast = slow = dummyHead;
    // 零個結點或者一個結點,確定無環
    if(fast.next == null || fast.next.next == null) 
        return false;
    while(fast && fast.next) {
        fast = fast.next.next;
        slow = slow.next;
        // 二者相遇了
        if(fast == slow) {
            return true;
        }
    } 
    return false;
};
複製代碼

No.2 如何找到環的起點

給定一個鏈表,返回鏈表開始入環的第一個節點。 若是鏈表無環,則返回 null。

**說明:**不容許修改給定的鏈表。

思路分析

剛剛已經判斷了如何判斷出現環,那如何找到環的節點呢?咱們來分析一波。

看上去比較繁瑣,咱們把它作進一步的抽象:

設快慢指針走了x秒,慢指針一秒走一次。

對快指針,有: 2x - L = m * S + Y -----①

對慢指針,有: x - L = n * S + Y -----②

其中,m、n 均爲天然數。

① * 2 - ② 得:

L = (m - n) * S - Y-----③

好,這是一個很是重要的等式。咱們如今假設有一個新的指針在 L 段的最左端,慢指針如今還在相遇處。

新指針慢指針都每次走一步,那麼,當新指針走了 L 步以後到達環起點,而與此同時,咱們看看慢指針狀況如何

由③式,慢指針走了(m - n) * S - Y個單位,以環起點爲參照物,相遇時的位置爲 Y,而如今由Y + (m - n) * S - Y(m - n) * S,得知慢指針實際上參照環起點,走了整整(m - n)圈。也就是說,慢指針此時也到達了環起點。 :::tip 結論 如今的解法就很清晰了,當快慢指針相遇以後,讓新指針從頭出發,和慢指針同時前進,且每次前進一步,二者相遇的地方,就是環起點。 :::

編程實現

懂得原理以後,實現起來就容易不少了。

/** * @param {ListNode} head * @return {ListNode} */
var detectCycle = function(head) {
    let dummyHead = new ListNode();
    dummyHead.next = head;
    let fast = slow = dummyHead;
    // 零個結點或者一個結點,確定無環
    if(fast.next == null || fast.next.next == null) 
        return null;
    while(fast && fast.next) {
        fast = fast.next.next;
        slow = slow.next;
        // 二者相遇了
        if(fast == slow) {
           let p = dummyHead;
           while(p != slow) {
               p = p.next;
               slow = slow.next;
           }
           return p;
        }
    } 
    return null;
};
複製代碼

鏈表合併

No.1 合併兩個有序鏈表

將兩個有序鏈表合併爲一個新的有序鏈表並返回。新鏈表是經過拼接給定的兩個鏈表的全部節點組成的。

示例:

輸入:1->2->4, 1->3->4
輸出:1->1->2->3->4->4
複製代碼

來源: LeetCode第21題

遞歸解法

遞歸解法更容易理解,咱們先用遞歸來作一下:

/** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */
var mergeTwoLists = function(l1, l2) {
    const merge = (l1, l2) => {
        if(l1 == null) return l2;
        if(l2 == null) return l1;
        if(l1.val > l2.val) {
            l2.next = merge(l1, l2.next);
            return l2;
        }else {
            l1.next = merge(l1.next, l2);
            return l1;
        }
    }
    return merge(l1, l2);
};
複製代碼

循環解法

var mergeTwoLists = function(l1, l2) {
    if(l1 == null) return l2;
    if(l2 == null) return l1;
    let p = dummyHead = new ListNode();
    let p1 = l1, p2 = l2;
    while(p1 && p2) {
        if(p1.val > p2.val) {
            p.next = p2;
            p = p.next;
            p2 = p2.next;
        }else {
            p.next = p1;
            p = p.next;
            p1 = p1.next;
        }
    }
    // 循環完成後務必檢查剩下的部分
    if(p1) p.next = p1;
    else p.next = p2;
    return dummyHead.next;
};
複製代碼

No.2 合併 K 個有序鏈表

合併 k 個排序鏈表,返回合併後的排序鏈表。請分析和描述算法的複雜度。

示例:

輸入:
[
  1->4->5,
  1->3->4,
  2->6
]
輸出: 1->1->2->3->4->4->5->6
複製代碼

來源: LeetCode第23題

自上而下(遞歸)實現

/** * @param {ListNode[]} lists * @return {ListNode} */
var mergeKLists = function(lists) {
    // 上面已經實現
    var mergeTwoLists = function(l1, l2) {/*上面已經實現*/};
    const _mergeLists = (lists, start, end) => {
        if(end - start < 0) return null;
        if(end - start == 0)return lists[end];
        let mid = Math.floor(start + (end - start) / 2);
        return mergeTwoList(_mergeLists(lists, start, mid), _mergeLists(lists, mid + 1, end));
    }
    return _mergeLists(lists, 0, lists.length - 1);
};
複製代碼

自下而上實現

在這裏須要提醒你們的是,在自下而上的實現方式中,我爲每個鏈表綁定了一個虛擬頭指針(dummyHead),爲何這麼作?

這是爲了方便鏈表的合併,好比 l1 和 l2 合併以後,合併後鏈表的頭指針就直接是 l1 的 dummyHead.next 值,等於說兩個鏈表都合併到了 l1 當中,方便了後續的合併操做。

var mergeKLists = function(lists) {
    var mergeTwoLists = function(l1, l2) {/*上面已經實現*/};
    // 邊界狀況
    if(!lists || !lists.length) return null;
    // 虛擬頭指針集合
    let dummyHeads = [];
    // 初始化虛擬頭指針
    for(let i = 0; i < lists.length; i++) {
        let node = new ListNode();
        node.next = lists[i];
        dummyHeads[i] = node;
    }
    // 自底向上進行merge
    for(let size = 1; size < lists.length; size += size){
        for(let i = 0; i + size < lists.length;i += 2 * size) {
            dummyHeads[i].next = mergeTwoLists(dummyHeads[i].next, dummyHeads[i + size].next);
        }
    }
    return dummyHeads[0].next;
};
複製代碼

多個鏈表的合併到這裏就實現完成了,在這裏順便告訴你這種歸併的方式同時也是對鏈表進行歸併排序的核心代碼。但願你能好好體會自上而下自下而上兩種不一樣的實現細節,相信對你的編程內功是一個不錯的提高。

求鏈表中間節點

判斷迴文鏈表

請判斷一個單鏈表是否爲迴文鏈表。

示例1:

輸入: 1->2
輸出: false
複製代碼

示例2:

輸入: 1->2->2->1
輸出: true
複製代碼

你可否用 O(n) 時間複雜度和 O(1) 空間複雜度解決此題?

來源: LeetCode第234題

思路分析

這一題若是不考慮性能的限制,實際上是很是簡單的。但考慮到 O(n) 時間複雜度和 O(1) 空間複雜度,恐怕就值得停下來好好想一想了。

題目的要求是單鏈表,沒有辦法訪問前面的節點,那咱們只得另闢蹊徑:

找到鏈表中點,而後將後半部分反轉,就能夠依次比較得出結論了。下面咱們來實現一波。

代碼實現

其實關鍵部分的代碼就是找中點了。先亮劍:

let dummyHead = slow = fast = new ListNode();
  dummyHead.next = head;
  // 注意注意,來找中點了
  while(fast && fast.next) {
      slow = slow.next;
      fast = fast.next.next;
  }
複製代碼

你可能會納悶了,爲何邊界要設成這樣?

咱們不妨來分析一下,分鏈表節點個數爲奇數偶數的時候分別討論。

  • 當鏈表節點個數爲奇數

試着模擬一下, fast 爲空的時候,中止循環, 狀態以下:

  • 當鏈表節點個數爲偶數

模擬走一遍,當 fast.next 爲空的時候,中止循環,狀態以下:

對於 fast 爲空fast.next爲空兩個條件,在奇數的狀況下,老是 fast爲空先出現,偶數的狀況下,老是fast.next先出現.

也就是說: 一旦fast爲空, 鏈表節點個數必定爲奇數,不然爲偶數。所以兩種狀況能夠合併來討論,當 fast 爲空或者 fast.next 爲空,循環就能夠終止了。

完整實現以下:

/** * @param {ListNode} head * @return {boolean} */
var isPalindrome = function(head) {
    let reverse = (pre, cur) => {
        if(!cur) return pre;
        let next = cur.next;
        cur.next = pre;
        return reverse(cur, next);
    }
    let dummyHead = slow = fast = new ListNode();
    dummyHead.next = head;
    // 注意注意,來找中點了, 黃金模板
    while(fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
    }
    let next = slow.next;
    slow.next = null;
    let newStart = reverse(null, next);
    for(let p = head, newP = newStart; newP != null; p = p.next, newP = newP.next) {
        if(p.val != newP.val) return false;
    }
    return true;
};
複製代碼

棧和隊列篇

棧&遞歸

有效括號

給定一個只包括 '(',')','{','}','[',']' 的字符串,判斷字符串是否有效。

有效字符串需知足:

左括號必須用相同類型的右括號閉合。 左括號必須以正確的順序閉合。 注意空字符串可被認爲是有效字符串。

示例:

輸入: "()"
輸出: true
複製代碼

來源: LeetCode第20題

代碼實現

/** * @param {string} s * @return {boolean} */
var isValid = function(s) {
    let stack = [];
    for(let i = 0; i < s.length; i++) {
        let ch = s.charAt(i);
        if(ch == '(' || ch == '[' || ch == '{') 
            stack.push(ch);
        if(!stack.length) return false;
        if(ch == ')' && stack.pop() !== '(') return false;
        if(ch == ']' && stack.pop() !== '[' ) return false;
        if(ch == '}' && stack.pop() !== '{') return false;
    }
    return stack.length === 0;
};
複製代碼

多維數組 flatten

將多維數組轉化爲一維數組。

示例:

[1, [2, [3, [4, 5]]], 6] -> [1, 2, 3, 4, 5, 6]
複製代碼

代碼實現

/** * @constructor * @param {NestedInteger[]} nestedList * @return {Integer[]} */
let flatten = (nestedList) => {
    let result = [];
    let fn = function (target, ary) {
        for (let i = 0; i < ary.length; i++) {
            let item = ary[i];
            if (Array.isArray(ary[i])) {
                fn(target, item);
            } else {
                target.push(item);
            }
        }
    }
    fn(result, nestedList)
    return result;

複製代碼

同時可採用 reduce 的方式, 一行就能夠解決,很是簡潔。

let flatten = (nestedList) =>  nestedList.reduce((pre, cur) => pre.concat(Array.isArray(cur) ? flatten(cur): cur), [])
複製代碼

二叉樹層序遍歷

二叉樹的層序遍歷本是下一章的內容,可是其中隊列的性質又體現得太明顯,所以就以這樣一類問題來讓你們練習隊列的相關操做。這裏會不只僅涉及到普通的層序遍歷, 並且涉及到變形問題,讓你們完全掌握。

普通的層次遍歷

給定一個二叉樹,返回其按層次遍歷的節點值。 (即逐層地,從左到右訪問全部節點)。

示例:

3
  / \
9  20
  /  \
  15   7
複製代碼

結果應輸出:

[
  [3],
  [9,20],
  [15,7]
]
複製代碼

來源: LeetCode第102題

實現

/** * @param {TreeNode} root * @return {number[][]} */
var levelOrder = function(root) {
    if(!root) return [];
    let queue = [];
    let res = [];
    let level = 0;
    queue.push(root);
    let temp;
    while(queue.length) {
        res.push([]);
        let size = queue.length;
        // 注意一下: size -- 在層次遍歷中是一個很是重要的技巧
        while(size --) {
            // 出隊
            let front = queue.shift();
            res[level].push(front.val);
            // 入隊
            if(front.left) queue.push(front.left);
            if(front.right) queue.push(front.right);
        }        
        level++;
    }
    return res;
};
複製代碼

二叉樹的鋸齒形層次遍歷

給定一個二叉樹,返回其節點值的鋸齒形層次遍歷。(即先從左往右,再從右往左進行下一層遍歷,以此類推,層與層之間交替進行)。

例如:

給定二叉樹 [3,9,20,null,null,15,7], 輸出應以下:

3
   / \
  9  20
    /  \
   15   7
複製代碼

返回鋸齒形層次遍歷以下:

[
  [3],
  [20,9],
  [15,7]
]
複製代碼

來源: LeetCode第103題

思路

這一題思路稍微不一樣,但若是把握住層次遍歷的思路,就會很是簡單。

代碼實現

var zigzagLevelOrder = function(root) {
    if(!root) return [];
    let queue = [];
    let res = [];
    let level = 0;
    queue.push(root);
    let temp;
    while(queue.length) {
        res.push([]);
        let size = queue.length;
        while(size --) {
            // 出隊
            let front = queue.shift();
            res[level].push(front.val);
            if(front.left) queue.push(front.left);
            if(front.right) queue.push(front.right);
        }  
        // 僅僅增長下面一行代碼便可
        if(level % 2) res[level].reverse();      
        level++;
    }
    return res;
};
複製代碼

二叉樹的右視圖

給定一棵二叉樹,想象本身站在它的右側,按照從頂部到底部的順序,返回從右側所能看到的節點值。

示例:

輸入: [1,2,3,null,5,null,4]
輸出: [1, 3, 4]
解釋:

   1            <---
 /   \
2     3         <---
 \     \
  5     4       <---
複製代碼

來源: LeetCode第199題

思路

右視圖?若是你以 DFS 即深度優先搜索的思路來想,你會感受異常的痛苦。沒錯,當初我就是這樣被坑的:)

但若是用廣度優先搜索的思想,即用層序遍歷的方式,求解這道題目也變得垂手可得。

代碼實現

/** * @param {TreeNode} root * @return {number[]} */
var rightSideView = function(root) {
    if(!root) return [];
    let queue = [];
    let res = [];
    queue.push(root);
    while(queue.length) {
        res.push(queue[0].val);
        let size = queue.length;
        while(size --) {
            // 一個size的循環就是一層的遍歷,在這一層只拿最右邊的結點
            let front = queue.shift();
            if(front.right) queue.push(front.right);
            if(front.left) queue.push(front.left);
        }
    }
    return res;
};
複製代碼

無權圖 BFS 遍歷

徹底平方數

給定正整數 n,找到若干個徹底平方數(好比 1, 4, 9, 16, ...)使得它們的和等於 n。你須要讓組成和的徹底平方數的個數最少。

示例:

輸入: n = 12
輸出: 3 
解釋: 12 = 4 + 4 + 4.
複製代碼

來源: LeetCode第279題

思路

這一題其實最容易想到的思路是動態規劃,咱們放到後面專門來拆解。實際上用隊列進行圖的建模,也是能夠順利地用廣度優先遍歷的方式解決的。

看到這個圖,你可能會有點懵,我稍微解釋一下你就明白了。

在這個無權圖中,每個點指向的都是它可能通過的上一個節點。舉例來講,對 5 而言,多是 4 加上了1的平方轉換而來,也多是1 加上了2的平方轉換而來,所以跟12都有聯繫,依次類推。

那麼咱們如今要作了就是尋找到從 n 轉換到 0 最短的連線數

舉個例子, n = 8 時,咱們須要找到它的鄰居節點47,此時到達 4 和到達 7 的步數都爲 1, 而後分別從 4 和 7 出發,4 找到鄰居節點30,達到 3 和 0 的步數都爲 2,考慮到此時已經到達 0,遍歷終止,返回到達 0 的步數 2 便可。

Talk is cheap, show me your code. 咱們接下來來一步步實現這個尋找的過程。

實現

接下來咱們來實現初版的代碼。

/** * @param {number} n * @return {number} */
var numSquares = function(n) {
    let queue = [];
    queue.push([n, 0]);
    while(queue.length) {
        let [num, step] = queue.shift();
        for(let i = 1; ; i ++) {
            let nextNum = num - i * i;
            if(nextNum < 0) break;
            // 還差最後一步就到了,直接返回 step + 1
            if(nextNum == 0) return step + 1;
            queue.push([nextNum, step + 1]);
        }
    }
    // 最後是不須要返回另外的值的,由於 1 也是徹底平方數,全部的數都能用 1 來組合
};
複製代碼

這個解法從功能上來說是沒有問題的,可是其中隱藏了巨大的性能問題,你能夠去LeetCode去測試一下,基本是超時。

那爲何會出現這樣的問題?

出就出在這樣一行代碼:

queue.push([nextNum, step + 1]);
複製代碼

只要是大於 0 的數,通通塞進隊列。要知道 2 - 1 = 1, 5 - 4 = 1, 9 - 8 = 1 ......這樣會重複很是多的 1, 依次類推,也會重複很是多的2,3等等等等。

這樣大量的重複數字不只僅消耗了更多的循環次數,同時也形成更加巨大的內存空間壓力。

所以,咱們須要對已經推入隊列的數字進行標記,避免重複推入。改善代碼以下:

var numSquares = function(n) {
    let map = new Map();
    let queue = [];
    queue.push([n, 0]);
    map.set(n, true);
    while(queue.length) {
        let [num, step] = queue.shift();
        for(let i = 1; ; i++) {
            let nextNum = num - i * i;
            if(nextNum < 0) break;
            if(nextNum == 0) return step + 1;
            // nextNum 未被訪問過
            if(!map.get(nextNum)){
                queue.push([nextNum, step + 1]);
                // 標記已經訪問過
                map.set(nextNum, true);
            }
        }
    }
};
複製代碼

單詞接龍

給定兩個單詞(beginWord 和 endWord)和一個字典,找到從 beginWord 到 endWord 的最短轉換序列的長度。轉換需遵循以下規則:

  • 每次轉換隻能改變一個字母。
  • 轉換過程當中的中間單詞必須是字典中的單詞。

說明:

  1. 若是不存在這樣的轉換序列,返回 0。
  2. 全部單詞具備相同的長度。
  3. 全部單詞只由小寫字母組成。
  4. 字典中不存在重複的單詞。
  5. 你能夠假設 beginWord 和 endWord 是非空的,且兩者不相同。

示例:

輸入:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]

輸出: 5

解釋: 一個最短轉換序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog",
     返回它的長度 5。
複製代碼

來源: LeetCode第127題

思路

這一題是一個更加典型的用圖建模的問題。若是每個單詞都是一個節點,那麼只要和這個單詞僅有一個字母不一樣,那麼就是它的相鄰節點。

這裏咱們能夠經過 BFS 的方式來進行遍歷。實現以下:

代碼實現

/** * @param {string} beginWord * @param {string} endWord * @param {string[]} wordList * @return {number} */
var ladderLength = function(beginWord, endWord, wordList) {
    // 兩個單詞在圖中是否相鄰
    const isSimilar = (a, b) => {
        let diff = 0
        for(let i = 0; i < a.length; i++) {
            if(a.charAt(i) !== b.charAt(i)) diff++;
            if(diff > 1) return false; 
        }
        return true;
    }
    let queue = [beginWord];
    let index = wordList.indexOf(beginWord);
    if(index !== -1) wordList.splice(index, 1);
    let res = 2;
    while(queue.length) {
        let size = queue.length;
        while(size --) {
            let front = queue.shift();
            for(let i = 0; i < wordList.length; i++) {
                if(!isSimilar(front, wordList[i]))continue;
                // 找到了
                if(wordList[i] === endWord) {
                    return res;
                }
                else {
                    queue.push(wordList[i]);
                }
                // wordList[i]已經成功推入,如今不須要了,刪除便可
                // 這一步性能優化,至關關鍵,否則100%超時
                wordList.splice(i, 1);
                i --;
            }
        }
        // 步數 +1
        res += 1;
    }
    return 0;
};
複製代碼

實現優先隊列

所謂優先隊列,就是一種特殊的隊列, 其底層使用的結構,使得每次添加或者刪除,讓隊首元素始終是優先級最高的。關於優先級經過什麼字段、按照什麼樣的比較方式來設定,能夠由咱們本身來決定。

要實現優先隊列,首先來實現一個堆的結構。

關於堆的說明

可能你之前沒有接觸過這種數據結構,可是實際上是很簡單的一種結構,其本質就是一棵二叉樹。可是這棵二叉樹比較特殊,除了用數組來依次存儲各個節點(節點對應的數組下標和層序遍歷的序號一致)以外,它須要保證任何一個父節點的優先級大於子節點,這也是它最關鍵的性質,由於保證了根元素必定是優先級最高的。

舉一個例子:

如今這個堆的數組就是: [10, 7, 2, 5, 1]

所以也會產生兩個很是關鍵的操做——siftUpsiftDown

對於siftUp操做,咱們試想一下如今有一個正常的堆,知足任何父元素優先級大於子元素,這時候向這個堆的數組末尾又添加了一個元素,那如今可能就不符合的結構特色了。那麼如今我將新增的節點和其父節點進行比較,若是父節點優先級小於它,則二者交換,不斷向上比較直到根節點爲止,這樣就保證了的正確結構。而這樣的操做就是siftUp

siftDown是與其相反方向的操做,從上到下比較,原理相同,也是爲了保證堆的正確結構。

實現一個最大堆

最大堆,即堆頂元素爲優先級最高的元素。

// 以最大堆爲例來實現一波
/** * @param {number[]} nums * @param {number} k * @return {number[]} */
class MaxHeap {
  constructor(arr = [], compare = null) {
    this.data = arr;
    this.size = arr.length;
    this.compare = compare;
  }
  getSize() {
    return this.size;
  }
  isEmpty() {
    return this.size === 0;
  }
  // 增長元素
  add(value) {
    this.data.push(value);
    this.size++;
    // 增長的時候把添加的元素進行 siftUp
    this._siftUp(this.getSize() - 1);
  }
  // 找到優先級最高的元素
  findMax() {
    if (this.getSize() === 0)
      return;
    return this.data[0];
  }
  // 讓優先級最高的元素(即隊首元素)出隊
  extractMax() {
    // 1.保存隊首元素
    let ret = this.findMax();
    // 2.讓隊首和隊尾元素交換位置
    this._swap(0, this.getSize() - 1);
    // 3. 把隊尾踢出去,size--
    this.data.pop();
    this.size--;
    // 4. 新的隊首 siftDown
    this._siftDown(0);
    return ret;
  }

  toString() {
    console.log(this.data);
  }
  _swap(i, j) {
    [this.data[i], this.data[j]] = [this.data[j], this.data[i]];
  }
  _parent(index) {
    return Math.floor((index - 1) / 2);
  }
  _leftChild(index) {
    return 2 * index + 1;
  }
  _rightChild(index) {
    return 2 * index + 2;
  }
  _siftUp(k) {
    // 上浮操做,只要子元素優先級比父節點大,父子交換位置,一直向上直到根節點
    while (k > 0 && this.compare(this.data[k], this.data[this._parent(k)])) {
      this._swap(k, this._parent(k));
      k = this._parent(k);
    }
  }
  _siftDown(k) {
    // 存在左孩子的時候
    while (this._leftChild(k) < this.size) {
      let j = this._leftChild(k);
      // 存在右孩子並且右孩子比左孩子大
      if (this._rightChild(k) < this.size &&
        this.compare(this.data[this._rightChild(k)], this.data[j])) {
        j++;
      }
      if (this.compare(this.data[k], this.data[j]))
        return;
      // 父節點比子節點小,交換位置
      this._swap(k, j);
      // 繼續下沉
      k = j;
    }
  }
}
複製代碼

實現優先隊列

有了最大堆做鋪墊,實現優先隊列就易如反掌,廢話很少說,直接放上代碼。

class PriorityQueue {
  // max 爲優先隊列的容量
  constructor(max, compare) {
    this.max = max;
    this.compare = compare;
    this.maxHeap = new MaxHeap([], compare);
  }

  getSize() {
    return this.maxHeap.getSize();
  }

  isEmpty() {
    return this.maxHeap.isEmpty();
  }

  getFront() {
    return this.maxHeap.findMax();
  }

  enqueue(e) {
    // 比當前最高的優先級的還要高,直接不處理
    if (this.getSize() === this.max) {
      if (this.compare(e, this.getFront())) return;
      this.dequeue();
    }
    return this.maxHeap.add(e);
  }

  dequeue() {
    if (this.getSize() === 0) return null;
    return this.maxHeap.extractMax();
  }
}
複製代碼

怎麼樣,是否是很是簡單?傳說中的優先隊列也不過如此。

且慢,可能會有人問: 你怎麼保證這個優先隊列是正確的呢?

咱們不妨來作一下測試:

let pq = new PriorityQueue(3);
pq.enqueue(1);
pq.enqueue(333);
console.log(pq.dequeue());
console.log(pq.dequeue());
pq.enqueue(3);
pq.enqueue(6);
pq.enqueue(62);
console.log(pq.dequeue());
console.log(pq.dequeue());
console.log(pq.dequeue());
複製代碼

結果以下:

333
1
62
6
3
複製代碼

可見,這個優先隊列的功能初步知足了咱們的預期。後面,咱們將經過實際的例子來運用這種數據結構來解決問題。

優先隊列應用

前 K 個高頻元素

給定一個非空的整數數組,返回其中出現頻率前 k 高的元素。

示例:

輸入: nums = [1,1,1,2,2,3], k = 2
輸出: [1,2]
複製代碼

說明:

  • 你能夠假設給定的 k 老是合理的,且 1 ≤ k ≤ 數組中不相同的元素的個數。
  • 你的算法的時間複雜度必須優於 O(n log n) , n 是數組的大小。

來源: LeetCode第347題

思路

首先要作的確定是統計頻率,那以後如何來選取頻率前 K 個元素同時又保證時間複雜度小於 O(n log n)呢?

固然,這是一道典型的考察優先隊列的題,利用容量爲 K 的優先隊列每次踢出不符合條件的值,那麼最後剩下的即爲所求。整個時間複雜度成爲 O(n log K),明顯是小於 O(n log n) 的。

既然是優先隊列,就涉及到如何來定義優先級的問題。

假若咱們以高頻率爲高優先級,那麼隊首始終是高頻率的元素,所以每次出隊是踢出出現頻率最高的元素,假設優先隊列容量爲 K,那照這麼作,剩下的是頻率最低的 K 個元素,顯然不符合題意。

所以,咱們須要的是每次出隊時踢出頻率最低的元素,這樣最後剩下來的就是頻率最高 K 個元素。

是否是咱們爲了踢出頻率最低的元素,還要從新寫一個小頂堆的實現呢?

徹底不須要!就像我剛纔所說的,合理地定義這個優先級的比較邏輯便可。接下來咱們來具體實現一下。

代碼實現

var topKFrequent = function(nums, k) {
   let map = {};
   let pq = new PriorityQueue(k, (a, b) => map[a] - map[b] < 0);
   for(let i = 0; i < nums.length; i++) {
       if(!map[nums[i]]) map[nums[i]] = 1;
       else map[nums[i]] = map[[nums[i]]] + 1;
   }
   let arr = Array.from(new Set(nums));
   for(let i = 0; i < arr.length; i++) {
       pq.enqueue(arr[i]);
   }
   return pq.maxHeap.data;
};
複製代碼

合併 K 個排序鏈表

合併 k 個排序鏈表,返回合併後的排序鏈表。請分析和描述算法的複雜度。

示例:

輸入:
[
  1->4->5,
  1->3->4,
  2->6
]
輸出: 1->1->2->3->4->4->5->6
複製代碼

這一題咱們以前在鏈表篇實現過,卻不知,它也能夠利用優先隊列完美解決。

來源: LeetCode第23題

/** * @param {ListNode[]} lists * @return {ListNode} */
var mergeKLists = function(lists) {
    let dummyHead = p = new ListNode();
    // 定義優先級的函數,重要!
    let pq = new PriorityQueue(lists.length, (a, b) => a.val <= b.val);
    // 將頭結點推入優先隊列
    for(let i = 0; i < lists.length; i++) 
        if(lists[i]) pq.enqueue(lists[i]);
    // 取出值最小的節點,若是 next 不爲空,繼續推入隊列
    while(pq.getSize()) {
        let min = pq.dequeue();
        p.next = min;
        p = p.next;
        if(min.next) pq.enqueue(min.next);
    }
    return dummyHead.next;
};
複製代碼

怎麼樣,是否是被驚豔到!原來優先隊列能夠這樣來使用!

雙端隊列及應用

什麼是雙端隊列?

雙端隊列是一種特殊的隊列,首尾均可以添加或者刪除元素,是一種增強版的隊列。

JS 中的數組就是一種典型的雙端隊列。push、pop 方法分別從尾部添加和刪除元素,unshift、shift 方法分別從首部添加和刪除元素。

滑動窗口最大值

給定一個數組 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       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7
複製代碼

要求: 時間複雜度應爲線性。

來源: LeetCode第239題

思路

這是典型地使用雙端隊列求解的問題。

創建一個雙端隊列 window,每次 push 進來一個新的值,就將 window 中目前前面全部比它小的值都刪除。利用雙端隊列的特性,能夠從後往前遍歷,遇到小的就刪除之,不然中止。

這樣能夠保證隊首始終是最大值,所以尋找最大值的時間複雜度能夠降到 O(1)。因爲 window 中會有愈來愈多的值被淘汰,所以總體的時間複雜度是線性的。

代碼實現

代碼很是的簡潔,可是若是要寫出 bug free 的代碼仍是有至關的難度的,但願你能本身獨立實現一遍。

var maxSlidingWindow = function(nums, k) {
    // 異常處理
    if(nums.length === 0 || !k) return [];
    let window = [], res = [];
    for(let i = 0; i < nums.length; i++) {
        // 先把滑動窗口以外的踢出
        if(window[0] !== undefined && window[0] <= i - k) window.shift();
        // 保證隊首是最大的
        while(nums[window[window.length - 1]] <= nums[i])  window.pop();
        window.push(i);
        if(i >= k - 1) res.push(nums[window[0]]) 
    }
    return res;
};
複製代碼

棧和隊列的相互實現

棧實現隊列

使用棧實現隊列的下列操做:

push(x) -- 將一個元素放入隊列的尾部。 pop() -- 從隊列首部移除元素。 peek() -- 返回隊列首部的元素。 empty() -- 返回隊列是否爲空。

示例:

let queue = new MyQueue();

queue.push(1);
queue.push(2);  
queue.peek();  // 返回 1
queue.pop();   // 返回 1
queue.empty(); // 返回 false
複製代碼

來源: LeetCode第232題

思路

既然棧是先進後出, 要想獲得先進先出的效果,咱們不妨用兩個棧。

當進行push操做時,push 到 stack1,而進行poppeek的操做時,咱們經過stack2

固然這其中有一個特殊狀況,就是stack2是空,如何來進行poppeek? 很簡單,把stack1中的元素依次 pop 並推入stack2中,而後正常地操做 stack2便可,以下圖所示:

這就就能保證先入先出的效果了。

代碼實現

var MyQueue = function() {
    this.stack1 = [];
    this.stack2 = [];
};

MyQueue.prototype.push = function(x) {
    this.stack1.push(x);
};
// 將 stack1 的元素轉移到 stack2
MyQueue.prototype.transform = function() {
  while(this.stack1.length) {
    this.stack2.push(this.stack1.pop());
  }
}

MyQueue.prototype.pop = function() {
  if(!this.stack2.length) this.transform();
  return this.stack2.pop();
};

MyQueue.prototype.peek = function() {
    if(!this.stack2.length) this.transform();
    return this.stack2[this.stack2.length - 1];
};

MyQueue.prototype.empty = function() {
    return !this.stack1.length && !this.stack2.length;
};
複製代碼

隊列實現棧

和上一題的效果恰好相反,用隊列先進先出的方式來實現先進後出的效果。

思路

以上面的隊列爲例,push 操做好說,直接從在隊列末尾推入。但 pop 和 peek 呢?

回到咱們的目標,咱們的目標是拿到隊尾的值,也就是3。這就好辦了,咱們讓前面的元素通通出隊,只留隊尾元素便可,剩下的元素讓另一個隊列保存。

來源: LeetCode第225題

代碼實現

實現過程當中,值得注意的一點是,queue1 始終保存前面的元素,queue2 始終保存隊尾元素(即棧頂元素 )

可是當 push 的時候有一個陷阱,就是當queue2已經有元素的時候,不能將新值 push 到 queue1,由於此時的棧頂元素應該更新。此時對於新的值來講,應先 push 到 queue2, 而後將舊的棧頂從queue2出隊,推入 queue1,這樣就實現了更新棧頂的操做。

var MyStack = function() {
    this.queue1 = [];
    this.queue2 = [];
};
MyStack.prototype.push = function(x) {
    if(!this.queue2.length) this.queue1.push(x);
    else {
        // queue2 已經有值
        this.queue2.push(x);
        // 舊的棧頂移到 queue1 中
        this.queue1.push(this.queue2.shift());
    }

};
MyStack.prototype.transform = function() {
    while(this.queue1.length !== 1) {
        this.queue2.push(this.queue1.shift())
    }
    // queue2 保存了前面的元素
    // 讓 queue1 和 queue2 交換
    // 如今queue1 包含前面的元素,queue2 裏面就只包含隊尾的元素
    let tmp = this.queue1;
    this.queue1 = this.queue2;
    this.queue2 = tmp;
}
MyStack.prototype.pop = function() {
    if(!this.queue2.length) this.transform();
    return this.queue2.shift();
};
MyStack.prototype.top = function() {
    if(!this.queue2.length) this.transform();
    return this.queue2[0];
};
MyStack.prototype.empty = function() {
    return !this.queue1.length && !this.queue2.length;
};
複製代碼

二叉樹篇

二叉樹的遍歷

前序遍歷

示例:

示例:

輸入: [1,null,2,3]  
   1
    \
     2
    /
   3 

輸出: [1,2,3]
複製代碼

來源: LeetCode第144題

遞歸方式

/** * @param {TreeNode} root * @return {number[]} */
var preorderTraversal = function(root) {
    let arr = [];
    let traverse = (root) => {
      if(root == null) return;
      arr.push(root.val);
      traverse(root.left);
      traverse(root.right); 
    }
    traverse(root);
    return arr;
};
複製代碼

非遞歸方式

var preorderTraversal = function(root) {
    if(root == null) return [];
    let stack = [], res = [];
    stack.push(root);
    while(stack.length) {
        let node = stack.pop();
        res.push(node.val);
        // 左孩子後進先出,進行先左後右的深度優先遍歷
        if(node.right) stack.push(node.right);
        if(node.left) stack.push(node.left);
    }
    return res;
};
複製代碼

中序遍歷

給定一個二叉樹,返回它的中序 遍歷。

示例:

輸入: [1,null,2,3]
   1
    \
     2
    /
   3
輸出: [1,3,2]
複製代碼

來源: LeetCode第94題

遞歸方式:

/** * @param {TreeNode} root * @return {number[]} */
var inorderTraversal = function(root) {
    let arr = [];
    let traverse = (root) => {
      if(root == null) return;
      traverse(root.left);
      arr.push(root.val);
      traverse(root.right); 
    }
    traverse(root);
    return arr;
};
複製代碼

非遞歸方式

var inorderTraversal = function(root) {
    if(root == null) return [];
    let stack = [], res = [];
    let p = root;
    while(stack.length || p) {
        while(p) {
            stack.push(p);
            p = p.left;
        }
        let node = stack.pop();
        res.push(node.val);
        p = node.right;
    }   
    return res;
};
複製代碼

後序遍歷

給定一個二叉樹,返回它的 後序 遍歷。

示例:

輸入: [1,null,2,3]  

   1
    \
     2
    /
   3 

輸出: [3,2,1]
複製代碼

來源: LeetCode第145題

遞歸方式

/** * @param {TreeNode} root * @return {number[]} */
var postorderTraversal = function(root) {
    let arr = [];
    let traverse = (root) => {
      if(root == null) return;
      traverse(root.left);
      traverse(root.right);
      arr.push(root.val);
    }
    traverse(root);
    return arr
};
複製代碼

非遞歸方式

var postorderTraversal = function(root) {
    if(root == null) return [];
    let stack = [], res = [];
    let visited = new Set();
    let p = root;
    while(stack.length || p) {
        while(p) {
            stack.push(p);
            p = p.left;
        }
        let node = stack[stack.length - 1];
        // 若是右孩子存在,並且右孩子未被訪問
        if(node.right && !visited.has(node.right)) {
            p = node.right;
            visited.add(node.right);
        } else {
            res.push(node.val);
            stack.pop();
        }
    }
    return res;
};
複製代碼

最大/最小深度

最大深度

給定一個二叉樹,找出其最大深度。

二叉樹的深度爲根節點到最遠葉子節點的最長路徑上的節點數。

說明: 葉子節點是指沒有子節點的節點。

示例: 給定二叉樹 [3,9,20,null,null,15,7]:

3
   / \
  9  20
    /  \
   15   7
複製代碼

返回它的最大深度 3 。 來源: LeetCode第104題

遞歸實現

實現很是簡單,直接貼出代碼:

/** * @param {TreeNode} root * @return {number} */
var maxDepth = function(root) {
    // 遞歸終止條件 
    if(root == null) return 0;
    return Math.max(maxDepth(root.left) + 1, maxDepth(root.right) + 1);
};
複製代碼

非遞歸實現

採用層序遍歷的方式,很是好理解。

var maxDepth = function(root) {
    if(root == null) return 0;
    let queue = [root];
    let level = 0;
    while(queue.length) {
        let size = queue.length;
        while(size --) {
            let front = queue.shift();
            if(front.left) queue.push(front.left);
            if(front.right) queue.push(front.right);
        }
        // level ++ 後的值表明着如今已經處理完了幾層節點
        level ++;
    }
    return level;
};
複製代碼

最小深度

給定一個二叉樹,找出其最小深度。

最小深度是從根節點到最近葉子節點的最短路徑上的節點數量。

說明: 葉子節點是指沒有子節點的節點。

示例:

給定二叉樹 [3,9,20,null,null,15,7]:

3
   / \
  9  20
    /  \
   15   7
複製代碼

返回它的最小深度 2.

來源: LeetCode第111題

遞歸實現

在實現的過程當中,若是按照最大深度的方式來作會出現一個陷阱,即:

/** * @param {TreeNode} root * @return {number} */
var minDepth = function(root) {
    // 遞歸終止條件 
    if(root == null) return 0;
    return Math.min(minDepth(root.left) + 1, minDepth(root.right)+1);
};
複製代碼

當 root 節點有一個孩子爲空的時候,此時返回的是 1, 但這是不對的,最小高度指的是根節點到最近葉子節點的最小路徑,而不是到一個空節點的路徑。

所以咱們須要作以下的調整:

var minDepth = function(root) {
    if(root == null) return 0;
    // 左右孩子都不爲空才能像剛纔那樣調用
    if(root.left && root.right)
        return Math.min(minDepth(root.left), minDepth(root.right)) + 1;
    // 右孩子爲空了,直接忽略之
    else if(root.left)
        return minDepth(root.left) + 1;
    // 左孩子爲空,忽略
    else if(root.right)
        return minDepth(root.right) + 1;
    // 兩個孩子都爲空,說明到達了葉子節點,返回 1
    else return 1;
};
複製代碼

這樣程序便能正常工做了。

非遞歸實現

相似於最大高度問題,採用了層序遍歷的方式,很容易理解。

var minDepth = function(root) {
    if(root == null) return 0;
    let queue = [root];
    let level = 0;
    while(queue.length) {
        let size = queue.length;
        while(size --) {
            let front = queue.shift();
            // 找到葉子節點
            if(!front.left && !front.right) return level + 1;
            if(front.left) queue.push(front.left);
            if(front.right) queue.push(front.right);
        }
        // level ++ 後的值表明着如今已經處理完了幾層節點
        level ++;
    }
    return level;
};
複製代碼

對稱二叉樹

給定一個二叉樹,檢查它是不是鏡像對稱的。

例如,二叉樹 [1,2,2,3,4,4,3] 是對稱的。

1
   / \
  2   2
 / \ / \
3  4 4  3
複製代碼

可是下面這個 [1,2,2,null,3,null,3] 則不是鏡像對稱的:

1
   / \
  2   2
   \   \
   3    3
複製代碼

來源: LeetCode第101題

遞歸實現

遞歸方式的代碼是很是幹練和優雅的,但願你先本身實現一遍,而後對比改進。

/** * @param {TreeNode} root * @return {boolean} */
var isSymmetric = function(root) {
    let help = (node1, node2) => {
        // 都爲空
        if(!node1 && !node2) return true;
        // 一個爲空一個不爲空,或者兩個節點值不相等
        if(!node1 || !node2 || node1.val !== node2.val) return false;
        return help(node1.left, node2.right) && help(node1.right, node2.left);
    }
    if(root == null) return true;
    return help(root.left, root.right);
};
複製代碼

非遞歸實現

用一個隊列保存訪問過的節點,每次取出兩個節點,進行比較。

var isSymmetric = function(root) {
    if(root == null) return true;
    let queue = [root.left, root.right];
    let node1, node2;
    while(queue.length) {
        node1 = queue.shift();
        node2 = queue.shift();
        // 兩節點均爲空
        if(!node1 && !node2)continue;
        // 一個爲空一個不爲空,或者兩個節點值不相等
        if(!node1 || !node2 || node1.val !== node2.val) return false;
        queue.push(node1.left);
        queue.push(node2.right);
        queue.push(node1.right);
        queue.push(node2.left);
    }
    return true;
};
複製代碼

LCA 問題

LCA (Lowest Common Ancestor)即最近公共祖先問題。

百度百科中最近公共祖先的定義爲:「對於有根樹 T 的兩個結點 p、q,最近公共祖先表示爲一個結點 x,知足 x 是 p、q 的祖先且 x 的深度儘量大(一個節點也能夠是它本身的祖先)。」

二叉樹的最近公共祖先

對於一個普通的二叉樹: root = [3,5,1,6,2,0,8,null,null,7,4]

輸入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
輸出: 3
解釋: 節點 5 和節點 1 的最近公共祖先是節點 3。
複製代碼

來源: LeetCode第236題

思路分析

思路一: 首先遍歷一遍二叉樹,記錄下每一個節點的父節點。而後對於題目給的 p 節點,根據這個記錄表不斷的找 p 的上層節點,直到根,記錄下 p 的上層節點集合。而後對於 q 節點,根據記錄不斷向上找它的上層節點,在尋找的過程當中一旦發現這個上層節點已經包含在剛剛的集合中,說明發現了最近公共祖先,直接返回。

思路二: 深度優先遍歷二叉樹,若是當前節點爲 p 或者 q,直接返回這個節點,不然查看左右孩子,左孩子中不包含 p 或者 q 則去找右孩子,右孩子不包含 p 或者 q 就去找左孩子,剩下的狀況就是左右孩子中都存在 p 或者 q, 那麼此時直接返回這個節點。

祖先節點集合法

/** * @param {TreeNode} root * @param {TreeNode} p * @param {TreeNode} q * @return {TreeNode} */
var lowestCommonAncestor = function(root, p, q) {
    if(root == null || root == p || root == q) return root;
    let set = new Set();
    let map = new WeakMap();
    let queue = [];
    queue.push(root);
    // 層序遍歷
    while(queue.length) {
        let size = queue.length;
        while(size --) {
            let front = queue.shift();
            if(front.left) {
                queue.push(front.left);
                // 記錄父親節點
                map.set(front.left, front);
            }
            if(front.right) {
                queue.push(front.right);
                // 記錄父親節點
                map.set(front.right, front);
            }
        }
    }
    // 構造 p 的上層節點集合
    while(p) {
        set.add(p);
        p = map.get(p);
    }
    while(q) {
        // 一旦發現公共節點重合,直接返回
        if(set.has(q))return q;
        q = map.get(q);
    }
};
複製代碼

能夠看到整棵二叉樹遍歷了一遍,時間複雜度大體是 O(n),可是因爲哈希表的存在,空間複雜度比較高,接下來咱們來用另外一種遍歷的方式,能夠大大減小空間的開銷。

深度優先遍歷法

代碼很是簡潔、美觀,不過更重要的是體會其中遞歸調用的過程,代碼是自頂向下執行的,我建議你們用自底向上的方式來理解它,即從最左下的節點開始分析,相信你會很好的理解整個過程。

var lowestCommonAncestor = function(root, p, q) {
    if (root == null || root == p || root == q) return root;
    let left = lowestCommonAncestor(root.left, p, q);
    let right = lowestCommonAncestor(root.right, p, q);
    if(left == null) return right;
    else if(right == null) return left;
    return root;
};
複製代碼

二叉搜索樹的最近公共祖先

給定以下二叉搜索樹: root = [6,2,8,0,4,7,9,null,null,3,5]

輸入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
輸出: 6 
解釋: 節點 2 和節點 8 的最近公共祖先是 6。
複製代碼

來源: LeetCode第235題

實現

二叉搜索樹做爲一種特殊的二叉樹,固然是能夠用上述的兩種方式來實現的。

不過藉助二叉搜索樹有序的特性,咱們也能夠寫出另一個版本的深度優化遍歷。

/** * @param {TreeNode} root * @param {TreeNode} p * @param {TreeNode} q * @return {TreeNode} */
var lowestCommonAncestor = function(root, p, q) {
    if(root == null || root == p || root == q) return root;
    // root.val 比 p 和 q 都大,找左孩子
    if(root.val > p.val && root.val > q.val) 
        return lowestCommonAncestor(root.left, p, q);
    // root.val 比 p 和 q 都小,找右孩子
    if(root.val < p.val && root.val < q.val) 
        return lowestCommonAncestor(root.right, p, q);
    else 
        return root;
};
複製代碼

同時也能夠採用非遞歸的方式:

var lowestCommonAncestor = function(root, p, q) {
    let node = root;
    while(node) {
        if(p.val > node.val && q.val > node.val)
            node = node.right;
        else if(p.val < node.val && q.val < node.val) 
            node = node.left;
        else return node;
    }
};
複製代碼

是否是被二叉樹精簡而優雅的代碼驚豔到了呢?但願你能好好體會其中遍歷的過程,而後務必本身獨立實現一遍,保證對這種數據結構足夠熟悉,加強本身的編程內力。

二叉樹中的路徑問題

No.1 二叉樹的直徑

給定一棵二叉樹,你須要計算它的直徑長度。一棵二叉樹的直徑長度是任意兩個結點路徑長度中的最大值。這條路徑可能穿過根結點。

示例 : 給定二叉樹

1
         / \
        2   3
       / \     
      4   5  
複製代碼

返回 3, 它的長度是路徑 [4,2,1,3] 或者 [5,2,1,3]。

注意:兩結點之間的路徑長度是以它們之間邊的數目表示。

來源: LeetCode第543題

思路

所謂的求直徑, 本質上是求樹中節點左右子樹高度和的最大值

注意,這裏我說的是樹中節點, 並不是根節點。由於會有這樣一種狀況:

1
         / 
        2   
       / \     
      4   5
     /     \
    8       6
             \
              7
複製代碼

那這個時候,直徑最大的路徑是: 8 -> 4 -> 2-> 5 -> 6 -> 7。交界的元素並非根節點。這是這個問題特別須要注意的地方,否則無解。

初步求解

目標已經肯定了,求樹中節點左右子樹高度和的最大值。開幹!

/** * @param {TreeNode} root * @return {number} */
var diameterOfBinaryTree = function(root) {
    // 求最大深度
    let maxDepth = (node) => {
      if(node == null) return 0;
      return Math.max(maxDepth(node.left) + 1, maxDepth(node.right) + 1);
    }
    let help = (node) => {
        if(node == null) return 0;
        let rootSum = maxDepth(node.left) + maxDepth(node.right);
        let childSum = Math.max(help(node.left), help(node.right));
        return Math.max(rootSum, childSum);
    }
    if(root == null) return 0;
    return help(root);
};
複製代碼

這樣一段代碼放到 LeetCode 是能夠經過,但時間上卻不讓人很滿意,爲何呢?

由於在反覆調用 maxDepth 的過程,對樹中的一些節點增長了不少沒必要要的訪問。好比:

1
         / 
        2   
       / \     
      4   5
     /     \
    8       6
             \
              7
複製代碼

咱們看何時訪問節點 8,maxDepth(節點 2)的時候訪問, maxDepth(節點 4)的時候又會訪問,若是節點層級更高,重複訪問的次數更加頻繁,剩下的節點六、節點 7 都是同理。每個節點訪問的次數大概是 O(logK)(設當前節點在第 K 層)。那能不能把這個頻率降到 O(1) 呢?

答案是確定的,接下來咱們來優化這個算法。

優化解法

var diameterOfBinaryTree = function(root) {
    let help = (node) => {
        if(node == null) return 0;
        let left = node.left ? help(node.left) + 1: 0;
        let right = node.right ? help(node.right) + 1: 0;
        let cur = left + right;
        if(cur > max) max = cur; 
        // 這個返回的操做至關關鍵
        return Math.max(left, right);
    }
    let max = 0;
    if(root == null) return 0;
    help(root);
    return max;
};
複製代碼

在這個過程當中設置了一個max全局變量,深度優先遍歷這棵樹,每遍歷完一個節點就更新max,並經過返回值的方式自底向上把當前節點左右子樹的最大高度傳給父函數使用,使得每一個節點只需訪問一次便可。

如今提交咱們優化後的代碼,時間消耗明顯下降。

No.2 二叉樹的全部路徑

給定一個二叉樹,返回全部從根節點到葉子節點的路徑。

說明: 葉子節點是指沒有子節點的節點。

示例:

輸入:

   1
 /   \
2     3
 \
  5

輸出: ["1->2->5", "1->3"]
複製代碼

解釋: 全部根節點到葉子節點的路徑爲: 1->2->5, 1->3

來源: LeetCode第257題

遞歸解法

利用 DFS(深度優先遍歷) 的方式進行遍歷。

/** * @param {TreeNode} root * @return {string[]} */
var binaryTreePaths = function(root) {
    let path = [];
    let res = [];
    let dfs = (node) => {
        if(node == null) return;
        path.push(node);
        dfs(node.left);
        dfs(node.right);
        if(!node.left && !node.right) 
            res.push(path.map(item => item.val).join('->'));
        // 注意每訪問完一個節點記得把它從path中刪除,達到回溯效果
        path.pop();
    }
    dfs(root);
    return res;
};
複製代碼

非遞歸解法

接下來咱們經過非遞歸的後序遍歷的方式來實現一下, 順便複習一下後序遍歷的實現。 ::: tip 提示 後序遍歷其實也是 DFS 的一種具體實現方式。 :::

var binaryTreePaths = function(root) {
    if(root == null) return [];
    let stack = [];
    let p = root;
    let set = new Set();
    res = [];
    while(stack.length || p) {
        while(p) {
            stack.push(p);
            p = p.left;
        }
        let node = stack[stack.length - 1];
        // 葉子節點
        if(!node.right && !node.left) {
            res.push(stack.map(item => item.val).join('->'));
        }
        // 右孩子存在,且右孩子未被訪問
        if(node.right && !set.has(node.right)) {
            p = node.right;
            set.add(node.right);
        } else {
            stack.pop();
        }
    }
    return res;
};
複製代碼

No.3 二叉樹的最大路徑和

給定一個非空二叉樹,返回其最大路徑和。

本題中,路徑被定義爲一條從樹中任意節點出發,達到任意節點的序列。該路徑至少包含一個節點,且不必定通過根節點。

示例:

輸入: [-10,9,20,null,null,15,7]

   -10
   / \
  9  20
    /  \
   15   7

輸出: 42
複製代碼

來源: LeetCode第124題

遞歸解

/** * @param {TreeNode} root * @return {number} */
var maxPathSum = function(root) {
    let help = (node) => {
        if(node == null) return 0;
        let left = Math.max(help(node.left), 0);
        let right = Math.max(help(node.right), 0);
        let cur = left + node.val + right;
        // 若是發現某一個節點上的路徑值比max還大,則更新max
        if(cur > max) max = cur;
        // left 和 right 永遠是"一根筋",中間不會有轉折
        return Math.max(left, right) + node.val;
    }
    let max = Number.MIN_SAFE_INTEGER;
    help(root);
    return max;
};
複製代碼

二叉搜索樹

No.1 驗證二叉搜索樹

給定一個二叉樹,判斷其是不是一個有效的二叉搜索樹。

假設一個二叉搜索樹具備以下特徵:

節點的左子樹只包含小於當前節點的數。 節點的右子樹只包含大於當前節點的數。 全部左子樹和右子樹自身必須也是二叉搜索樹。

示例 1:

輸入:
    2
   / \
  1   3
輸出: true
複製代碼

來源: LeetCode第98題

方法一: 中序遍歷

經過中序遍歷,保存前一個節點的值,掃描到當前節點時,和前一個節點的值比較,若是大於前一個節點,則知足條件,不然不是二叉搜索樹。

/** * @param {TreeNode} root * @return {boolean} */
var isValidBST = function(root) {
    let prev = null;
    const help = (node) => {
        if(node == null) return true;
        if(!help(node.left)) return false;
        if(prev !== null && prev >= node.val) return false;
        // 保存當前節點,爲下一個節點的遍歷作準備
        prev = node.val;
        return help(node.right);
    }
    return help(root);
};
複製代碼

方法二: 限定上下界進行DFS

二叉搜索樹每個節點的值,都有一個上界和下界,深度優先遍歷的過程當中,若是訪問左孩子,則經過當前節點的值來更新左孩子節點的上界,同時訪問右孩子,則更新右孩子的下界,只要出現節點值越界的狀況,則不知足二叉搜索樹的條件。

parent
  /    \
left   right
複製代碼

假設這是一棵巨大的二叉樹的一個部分(parent、left、right都是實實在在的節點),那麼所有的節點排完序必定是這樣:

...left, parent, right...

能夠看到左孩子的最嚴格的上界是該節點, 同時, 右孩子的最嚴格的下界也是該節點。咱們按照這樣的規則來進行更新上下界。

遞歸實現:

var isValidBST = function(root) {
    const help = (node, max, min) => {
        if(node == null) return true;
        if(node.val >= max || node.val <= min) return false;
        // 左孩子更新上界,右孩子更新下界,至關於邊界要求愈來愈苛刻
        return help(node.left, node.val, min)
                && help(node.right, max, node.val);
    }
    return help(root, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER);
};
複製代碼

非遞歸實現:

var isValidBST = function(root) {
    if(root == null) return true;
    let stack = [root];
    let min = Number.MIN_SAFE_INTEGER;
    let max = Number.MAX_SAFE_INTEGER;
    root.max = max; root.min = min;
    while(stack.length) {
        let node = stack.pop();
        if(node.val <= node.min || node.val >= node.max)
            return false;
        if(node.left) {
            stack.push(node.left);
            // 更新上下界
            node.left.max = node.val;
            node.left.min = node.min;
        }
        if(node.right) {
            stack.push(node.right);
            // 更新上下界
            node.right.max = node.max;
            node.right.min = node.val;
        }
    }
    return true;
};
複製代碼

No.2 將有序數組轉換爲二叉搜索樹

將一個按照升序排列的有序數組,轉換爲一棵高度平衡二叉搜索樹。

本題中,一個高度平衡二叉樹是指一個二叉樹每一個節點 的左右兩個子樹的高度差的絕對值不超過 1。

示例:

給定有序數組: [-10,-3,0,5,9],

一個可能的答案是:[0,-3,9,-10,null,5],它能夠表示下面這個高度平衡二叉搜索樹:

      0
     / \
   -3   9
   /   /
 -10  5
複製代碼

來源: LeetCode第108題

遞歸實現

/** * @param {number[]} nums * @return {TreeNode} */
var sortedArrayToBST = function(nums) {
    let help = (start, end) => {
        if(start > end) return null;
        if(start === end) return new TreeNode(nums[start]);
        let mid = Math.floor((start + end) / 2);
        // 找出中點創建節點
        let node = new TreeNode(nums[mid]);
        node.left = help(start, mid - 1);
        node.right = help(mid + 1, end);
        return node;
    }
    return help(0, nums.length - 1);
};
複製代碼

遞歸程序比較好理解,不斷地調用 help 完成整棵樹樹的構建。那如何用非遞歸來解決呢?我以爲這是一個很是值得你們思考的問題。但願你能動手試一試,若是實在想不出來,能夠參考下面我寫的非遞歸版本。

其實思路跟遞歸的版本是同樣的,只不過實現起來是用棧來實現 DFS 的效果。

/** * @param {number[]} nums * @return {TreeNode} */
var sortedArrayToBST = function(nums) {
    if(nums.length === 0) return null;
    let mid = Math.floor(nums.length / 2);
    let root = new TreeNode(nums[mid]);
    // 說明: 1. index 指的是當前元素在數組中的索引 
    // 2. 每個節點的值都是區間中點,那麼 start 屬性就是這個區間的起點,end 爲其終點
    root.index = mid; root.start = 0; root.end = nums.length - 1;
    let stack = [root];
    while(stack.length) {
        let node = stack.pop();
        // node 出來了,它自己包含了一個區間,[start, ..., index, ... end]
        // 下面判斷[node.start, node.index - 1]之間是否還有開發的餘地
        if(node.index - 1 >= node.start) {
            let leftMid = Math.floor((node.start + node.index)/2);
            let leftNode = new TreeNode(nums[leftMid]);
            node.left = leftNode;
            // 初始化新節點的區間起點、終點和索引
            leftNode.start = node.start;
            leftNode.end = node.index - 1;
            leftNode.index = leftMid;
            stack.push(leftNode);
        }
        // 中間夾着node.index, 已經有元素了,這個位置不能再開發
        // 下面判斷[node.index + 1, node.end]之間是否還有開發的餘地
        if(node.end >= node.index + 1) {
            let rightMid = Math.floor((node.index + 1 + node.end)/2);
            let rightNode = new TreeNode(nums[rightMid]);
            node.right = rightNode;
            // 初始化新節點的區間起點、終點和索引
            rightNode.start = node.index + 1; 
            rightNode.end = node.end;
            rightNode.index = rightMid;
            stack.push(rightNode);
        }
    }
    return root;
};
複製代碼

No.3 二叉樹展開爲鏈表

給定一個二叉(搜索)樹,原地將它展開爲鏈表。

例如,給定二叉樹

1
   / \
  2   5
 / \   \
3   4   6
複製代碼

將其展開爲:

1
 \
  2
   \
    3
     \
      4
       \
        5
         \
          6
複製代碼

來源: LeetCode第114題

遞歸方式

採用後序遍歷,遍歷完左右孩子咱們要作些什麼呢?用下面的圖來演示一下(點擊可放大):

/** * @param {TreeNode} root * @return {void} Do not return anything, modify root in-place instead. */
var flatten = function(root) {
    if(root == null) return;
    flatten(root.left);
    flatten(root.right);
    if(root.left) {
        let p = root.left;
        while(p.right) {
            p = p.right;
        }
        p.right = root.right;
        root.right = root.left;
        root.left = null;
    }
};
複製代碼

非遞歸方式

採用非遞歸的後序遍歷方式,思路跟以前的徹底同樣。

var flatten = function(root) {
    if(root == null) return;
    let stack = [];
    let visited = new Set();
    let p = root;
    // 開始後序遍歷
    while(stack.length || p) {
        while(p) {
            stack.push(p);
            p = p.left;
        }
        let node = stack[stack.length - 1];
        // 若是右孩子存在,並且右孩子未被訪問
        if(node.right && !visited.has(node.right)) {
            p = node.right;
            visited.add(node.right);
        } else {
            // 如下爲思路圖中關鍵邏輯
            if(node.left) {
                let p = node.left;
                while(p.right) {
                    p = p.right;
                }
                p.right = node.right;
                node.right = node.left;
                node.left = null;
            }
            stack.pop();
        }
    }
};
複製代碼

No.4 不一樣的二叉搜索樹II

給定一個整數 n,生成全部由 1 ... n 爲節點所組成的二叉搜索樹。

示例:

輸入: 3
輸出:
[
  [1,null,3,2],
  [3,2,null,1],
  [3,1,null,null,2],
  [2,1,3],
  [1,null,2,null,3]
]
解釋:
以上的輸出對應如下 5 種不一樣結構的二叉搜索樹:

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3
複製代碼

來源: LeetCode第95題

遞歸解法

遞歸建立子樹

/** * @param {number} n * @return {TreeNode[]} */
var generateTrees = function(n) {
    let help = (start, end) => {
        if(start > end) return [null];
        if(start === end) return [new TreeNode(start)];
        let res = [];
        for(let i = start; i <= end; i++) {
            // 左孩子集
            let leftNodes = help(start, i - 1);
            // 右孩子集
            let rightNodes = help(i + 1, end);
            for(let j = 0; j < leftNodes.length; j++) {
                for(let k = 0; k < rightNodes.length; k++) {
                    let root = new TreeNode(i);
                    root.left = leftNodes[j];
                    root.right = rightNodes[k];
                    res.push(root);
                }
            }
        }
        return res;
    }
    if(n == 0) return [];
    return help(1, n);
};
複製代碼

非遞歸解法

var generateTrees = function(n) {
    let clone = (node, offset) => {
        if(node == null) return null;
        let newnode = new TreeNode(node.val + offset);
        newnode.left = clone(node.left, offset);
        newnode.right = clone(node.right, offset);
        return newnode;
    }
    if(n == 0) return [];
    let dp = [];
    dp[0] = [null];
    // i 是子問題中的節點個數,子問題: [1], [1,2], [1,2,3]...逐步遞增,直到[1,2,3...,n]
    for(let i = 1; i <= n; i++) {
        dp[i] = [];
        for(let j = 1; j <= i; j++) {
            // 左子樹集
            for(let leftNode of dp[j - 1]) {
                // 右子樹集
                for(let rightNode of dp[i - j]) {
                    let node = new TreeNode(j);
                    // 左子樹結構共享
                    node.left = leftNode;
                    // 右子樹沒法共享,但能夠借用節點個數相同的樹,每一個節點增長一個偏移量
                    node.right = clone(rightNode, j);
                    dp[i].push(node);
                }
            }
        }
    }
    return dp[n];
};
複製代碼

這一次的分享就到這裏了。能夠看到數據結構和算法是知識是多麼龐大,不過在這個系列的驅動下,相信你必定能拿下數據結構和算法這一板塊的知識體系,下面是本系列的github倉庫,供你們參考學習,咱們下期再見。

github倉庫

本系列在線閱讀地址

另外本人的博客如今已經分類整理完畢,地址在這裏,點擊打開

相關文章
相關標籤/搜索