「算法歸納」二叉樹

  • 一種分層數據的抽象模型
  • 前端工作中常見的樹包括:DOM樹、級聯選擇器、樹形控件
  • JS中沒有樹,但是可以用Object和Array構建樹
  • 樹的常用操作:深度/廣度優先遍歷、先中后序遍歷

樹的深度與廣度優先遍歷

  • 深度優先遍歷:盡可能深的搜索樹的分支


    image.png
  • 廣度優先遍歷:先訪問離根節點最近的節點


    image.png

深度優先遍歷

  • 訪問根節點
  • 對根節點的children挨個進行深度優先遍歷
  • 遞歸
const tree = {
    val: 'a',
    children: [
        {
            val: 'b',
            children: [
                {
                    val: 'd',
                    children: []
                },
                {
                    val: 'e',
                    children: []
                }
            ]
        },
        {
            val: 'c',
            children: [
                {
                    val: 'f',
                    children: []
                }
            ]
        }
    ]
}

const dfs = (root) => {
    console.log(root)
    root.children.forEach(dfs)
}

dfs(tree)

廣度優先遍歷

  • 新建一個隊列,把根節點入隊
  • 把隊頭出隊并訪問
  • 把隊頭的children挨個入隊
  • 重復第二步、第三步直到隊列為空
const tree = {
    val: 'a',
    children: [
        {
            val: 'b',
            children: [
                {
                    val: 'd',
                    children: []
                },
                {
                    val: 'e',
                    children: []
                }
            ]
        },
        {
            val: 'c',
            children: [
                {
                    val: 'f',
                    children: []
                }
            ]
        }
    ]
}
const bfs = (root) => {
    const q = [root]
    while(q.length > 0) {
        const n = q.shift()
        console.log(n.val)
        n.children.forEach(child => {
            q.push(child)
        })
    }
}

bfs(tree)

二叉樹

  • 樹中每個節點最多只能有兩個子節點
  • 在JS中通常用Object來模擬二叉樹
const bt ={
    val: 1,
    left: {
        val: 2,
        left: {
            val: 4,
            left: null,
            right: null
        },
        right: {
            val: 5,
            left: null,
            right: null
        }
    },
    right: {
        val: 3,
        left: {
            val: 6,
            left: null,
            right: null
        },
        right: {
            val: 7,
            left: null,
            right: null
        }
    }
}

先序遍歷

  • 訪問根節點
  • 對根節點的左子樹進行先序遍歷
  • 對根節點的右子樹進行先序遍歷
image.png
const preorder = (root) => {
    if (!root) return
    console.log(root.val)
    preorder(root.left)
    preorder(root.right)
}
preorder(bt)

中序遍歷

  • 對根節點的左子樹進行中序遍歷
  • 訪問根節點
  • 對根節點的右子樹進行中序遍歷
image.png
const inorder = (root) => {
    if (!root) return
    inorder(root.left)
    console.log(root.val)
    inorder(root.right)
}

inorder(bt)

后序遍歷

  • 對根節點的左子樹進行后序遍歷
  • 對根節點的右子樹進行后序遍歷
  • 訪問根節點
image.png
const postorder = (root) => {
    if (!root) return
    postorder(root.left)
    postorder(root.right)
    console.log(root.val)
}

postorder(bt)

二叉樹的先中后序遍歷(非遞歸)

二叉樹先序遍歷

// 遞歸版
const preorder = root => {
    ...
    preorder(...)
    ...
}

如果在函數里面,調用另外一個函數,就會形成一個函數調用堆棧,調用完畢后又被釋放

所以說,堆棧就是我們實現非遞歸版先中后序遍歷的關鍵,我們可以用堆棧來模擬遞歸操作

const preorder = root => {
    if (!root) return
    const stack = [root]
    while (stack.length) {
        const n = stack.pop()
        console.log(n)
        if (n.right) stack.push(n.right)
        if (n.left) stack.push(n.left)
    }
}

二叉樹中序遍歷

const inorder = root => {
    if (!root) return
    const stack = []
    let p = root
    
    while (stack.length || p) {
        while (p) {
            stack.push(p)
            p = p.left
        }
        const n = stack.pop()
        console.log(n.val)
        p = n.right
    }
}

二叉樹后序遍歷

const postorder = root => {
    if (!root) return
    const stack = [root]
    const outputStack = []
    while (stack.length) {
        const n = stack.pop()
        
        outputStack.push(n)
        if (n.left) stack.push(n.left)
        if (n.right) stack.push(n.right)
    }
    while (outputStack.length) {
        const n = outputStack.pop()
        console.log(n.val)
    }
}

二叉樹的最大深度

  • 求最大深度,考慮使用深度優先遍歷
  • 在深度優先遍歷過程中,記錄每個節點所在層級,找出最大層級
// 104.二叉樹的最大深度
var maxDepth = function(root) {
    let res = 0
    const dfs = (n, l) => {
        if (!n) return
        if (!n.left && !n.right) {
            // 葉子節點再計算最深層級
             res = Math.max(res, l)
        }
        dfs(n.left, l + 1)
        dfs(n.right, l + 1)
    }
    dfs(root, 1)
    return res
};

二叉樹的最小深度

思路:

  • 求最小深度,考慮使用廣度優先遍歷
  • 在廣度優先遍歷過程中,遇到葉子節點停止遍歷,返回節點層級

解題步驟:

  • 廣度優先遍歷整棵樹,并記錄每個節點的層級
  • 遇到葉子節點,返回節點層級,停止遍歷
// 111.二叉樹的最小深度
var minDepth = function(root) {
    if (!root) {return 0}
    const q = [[root, 1]]
    while (q.length > 0) {

        const [n, l] = q.shift()
        if (!n.left && !n.right) return l
        if (n.left) q.push([n.left, l+1])
        if (n.right) q.push([n.right, l + 1])
    }

};

二叉樹的層序遍歷

  • 層序遍歷順序就是廣度優先遍歷
  • 不過在遍歷時候需要記錄當前節點所處的層級,方便將其添加到不同的數組中
// 102.二叉樹的層序遍歷
var levelOrder = function(root) {
    if (!root) return []
    const q = [root]
    const res = []

    while (q.length) {
        let len = q.length
        res.push([])
        while (len--) {
            const n = q.shift()
            res[res.length - 1].push(n.val)
            if (n.left) q.push(n.left)
            if (n.right) q.push(n.right)
        }
        
    }
    return res
};

二叉樹的中序遍歷

// 94.二叉樹的中序遍歷
// 遞歸
var inorderTraversal = function(root) {
    const res = []
    const rec = n => {
        if (!n) return
        rec(n.left)
        res.push(n.val)
        rec(n.right)
    }
    rec(root)
    return res
};
// 非遞歸
var inorderTraversal = function(root) {
    const res = []
    const stack = []
    let p = root
    while (stack.length || p) {
        while(p) {
            stack.push(p)
            p = p.left
        }
        const n = stack.pop()
        res.push(n.val)
        p = n.right
    }
    return res
};

路徑總和

  • 在深度優先遍歷的過程中,記錄當前路徑的節點值的和
  • 在葉子節點處,判斷當前路徑的節點值的和是否等于目標值
// 112.路徑總和
var hasPathSum = function(root, targetSum) {
    if (!root) return false
    let result = false
    const dfs = (n, val) => {
        if (!n.left && !n.right && val === targetSum) result = true
        if (n.left) dfs(n.left, n.left.val + val)
        if (n.right) dfs(n.right, n.right.val + val)
    }
    dfs(root, root.val)
    return result
};

遍歷JSON的所有節點值

const json = {
    a: {
        b: {
            c: 1
        }
    },
    d: [1, 2]
}
dfs = (n, path) => {
    console.log(n)
    Object.keys(n).forEach(k => {
        dfs(n[k], path.concat(k))
    })
}
dfs(json, [])

總結

  • 樹是一種分層數據的抽象模型,在前端廣泛應用
  • 樹的常用操作:深度/廣度優先遍歷、先中后序遍歷...
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容