資料結構的故事之二叉樹, 字首樹, N叉樹

利維亞的傑洛特發表於2019-03-19

image

前言

資料結構和演算法的知識博大精深, 這裡只是對這幾種資料結構做一些簡單的介紹。並對leetcode上部分相關的簡單和中等題做出解答。還請各位看官見諒

二叉樹

二叉樹是一種典型的樹狀結構, 二叉樹每一個節點最多有兩個子樹的結構。以下是遍歷二叉樹的幾種方式, 總的來說使用遞迴的方式, 還是非常好理解的。

image

深度優先遍歷 前序遍歷

前序遍歷首先訪問根節點,然後遍歷左子樹,最後遍歷右子樹

節點遍歷的順序: F, B, A, D, C, E, G, I, H


var preorderTraversal = function (root) {
  let result = []

  const traversing = (node) => {
    // 結束遞迴
    if (!node) return []

    // 首先遍歷當前的節點
    result.push(node.val)
    // 如果有左子樹優先遍歷左子樹
    if (node.left) {
      result.concat(traversing(node.left))
    }
    // 遍歷又子樹
    if (node.right) {
      result.concat(traversing(node.right))
    }
  }

  traversing(root)

  return result
}
複製程式碼

深度優先遍歷 中序遍歷

中序遍歷是先遍歷左子樹,然後訪問根節點,然後遍歷右子樹

節點遍歷的順序: A, B, C, D, E, F, G, H, I


var inorderTraversal = function (root) {
  let result = []
  const traversing = (node) => {
    if (!node) return
    // 優先遍歷左子樹
    if (node.left) {
      traversing(node.left)
    }
    // 然後獲取當前的節點
    if (node.val) {
      result.push(node.val)
    }
    // 然後遍歷右子樹
    if (node.right) {
      traversing(node.right)
    }
  }
  traversing(root)
  return result
}
複製程式碼

深度優先遍歷 後序遍歷

先遍歷左子樹,然後遍歷右子樹,最後訪問樹的根節點

節點遍歷的順序: A, C, E, D, B, H, I, G, F


var postorderTraversal = function (root) {
  let result = []
  const traversing = (node) => {
    if (!node) return
    if (node.left) {
      traversing(node.left)
    }
    if (node.right) {
      traversing(node.right)
    }
    if (node.val) {
      result.push(node.val)
    }
  }
  traversing(root)
  return result
};
複製程式碼

廣度優先遍歷

廣度優先搜尋是一種廣泛運用在樹或圖這類資料結構中,遍歷或搜尋的演算法。該演算法從一個根節點開始,首先訪問節點本身。然後依次遍歷它的二級鄰節點、三級鄰節點,以此類推。我們這裡依然使用遞迴遍歷, 但是我們在遞迴中新增level引數用來確定當前節點的層級。

image


var levelOrder = function (root) {
  let result = []

  const traversing = (node, level) => {
    if (!node) return
    if (!result[level]) result[level] = []
    result[level].push(node.val)
    if (node.left) {
      traversing(node.left, level + 1)
    }
    if (node.right) {
      traversing(node.right, level + 1)
    }
  }

  traversing(root, 0)
  return result
}
複製程式碼

二叉樹的最大深度

題目

給定一個二叉樹,找出其最大深度。二叉樹的深度為根節點到最遠葉子節點的最長路徑上的節點數。

思路

對當前的二叉樹使用後序遍歷。如果當前節點沒有左子樹並且沒有右子樹, 說明這個節點是當前分支中最深的節點, 我們記錄它自身的最大深度為1(因為它自身沒有子節點)。如果當前節點有左子樹和右子樹, 我們取左右子樹中最大的深度(因為是後序遍歷, 在遍歷當前根節點時, 左右樹已經被遍歷了)。取最大深度後加一就是當前節點的深度。

解答


var maxDepth = function(root) {
  if (!root) return 0

  const traversing = (node) => {
    if (!node) return

    if (!node.left && !node.right) {
      node.depth = 1
      return
    }
    if (node.left) {
      traversing(node.left)
    }
    if (node.right) {
      traversing(node.right)  
    }
    let max_left_depth = 0
    let max_right_depth = 0

    if (node.left) {
      max_left_depth = node.left.depth
    }
    if (node.right) {
      max_right_depth = node.right.depth
    }

    node.depth = Math.max(max_left_depth, max_right_depth) + 1
  }

  traversing(root)

  return root.depth
}
複製程式碼

對稱二叉樹

給定一個二叉樹,檢查它是否是映象對稱的

// 對稱二叉樹
    1
   / \
  2   2
 / \ / \
3  4 4  3

// 不是對稱二叉樹
    1
   / \
  2   2
   \   \
   3    3
複製程式碼

思路

採用BFS遍歷, 獲取每一級的所有節點結果集, 不存在的子節點使用null代替。判斷每一級的節點是否能構成迴文字串即可。

解答


var isSymmetric = function(root) {
  // BFS遍歷
  let result = []
  const traversing = (node, level) => { 
      
    if (!result[level]) result[level] = []
    
    // 不存在的節點使用null填充
    if (!node) {
      // 終止遞迴
      return result[level].push('null')
    } else {
      result[level].push(node.val)
    }
      
    if (node.left) {
      traversing(node.left, level + 1)
    } else {
      traversing(null, level + 1)  
    }
      
    if (node.right) {
      traversing(node.right, level + 1)
    } else {
      traversing(null, level + 1) 
    }
      
  }
  
  traversing(root, 0)
  
  // 判斷每一級的結果能否構成迴文字串
  for (let i = 0; i < result.length - 1; i++) {
    if (result[i].join('') !== result[i].reverse().join('')) {
      return false
    }
  }
  return true
};
複製程式碼

路徑總和

題目

給定一個二叉樹和一個目標和,判斷該樹中是否存在根節點到葉子節點的路徑,這條路徑上所有節點值相加等於目標和。

// 給定目標sum = 22
// 5->4->11->2和為22, 返回true

              5
             / \
            4   8
           /   / \
          11  13  4
         /  \      \
        7    2      1
複製程式碼

思路

我們採用前序遍歷, 每次遍歷使用目標減去當前節點的值,並將新的目標帶入下一次的遞迴中。如果當遍歷到最深處的節點,並且節點的值等於目標的值。說明二叉樹擁有路徑的和等於目標值。

解答


var hasPathSum = function(root, sum) {
  let result = []
  const traversing = (root, sum) => { 
      
    if (!root) return false
    
    // 說明擁有路徑等於目標的和
    if (!root.left && !root.right && root.val === sum) {
        result.push(root.val)
    }
    
    if (root.left) {
        traversing(root.left, sum - root.val) 
    }
    
    if (root.right) {
        traversing(root.right, sum - root.val)
    } 
  }
   
  traversing(root, sum)

  return result.length > 0
};
複製程式碼

從中序與後序遍歷序列構造二叉樹

題目

根據一棵樹的中序遍歷與後序遍歷構造二叉樹。


// 中序遍歷 inorder = [9,3,15,20,7]
// 後序遍歷 postorder = [9,15,7,20,3]

// 構建結果
    3
   / \
  9  20
    /  \
   15   7
複製程式碼

思路

思路與從前序與中序遍歷序列構造二叉樹題類似,這裡不在贅述

解答


var buildTree = function(inorder, postorder) {
    let binaryTree = {}
   
  const iteration = (postorder, inorder, tree) => {
       
      if (!postorder.length) {
          binaryTree = null
          return
      }
       
      tree.val = null
      tree.left = {
          val: null,
          left: null,
          right: null
      }
      tree.right = {
          val: null,
          left: null,
          right: null
      }

     // 前序遍歷第一個節點為當前樹的根節點
     let rootVal = postorder.splice(postorder.length - 1, 1)[0]
     // 中序遍歷根節點的索引
     let rootIndex = inorder.indexOf(rootVal)
     // 中序遍歷的左子樹
     let inorderLeftTree = inorder.slice(0, rootIndex)
     // 中序遍歷的右子樹
     let inorderRightTree = inorder.slice(rootIndex + 1)
     // 前序遍歷的左子樹
     let postorderLeftTree = postorder.slice(0, inorderLeftTree.length)
     // 前序遍歷的右子樹
     let postorderRightTree = postorder.slice(inorderLeftTree.length)

       
     tree.val = rootVal
      
     if (postorderLeftTree.length === 1 || inorderLeftTree.length === 1) {
         tree.left.val = postorderLeftTree[0]
     } else if (postorderLeftTree.length > 1 || inorderLeftTree.length > 1) {
         iteration(postorderLeftTree, inorderLeftTree, tree.left)
     } else {
          tree.left = null
     }
       
     if (postorderRightTree.length === 1 || inorderRightTree.length === 1) {
         tree.right.val = postorderRightTree[0]
     } else if (postorderRightTree.length > 1 || inorderRightTree.length > 1) {
         iteration(postorderRightTree, inorderRightTree, tree.right)
     } else {
      tree.right = null
     }
  }
   
  iteration(postorder, inorder, binaryTree)
   
  return binaryTree
}
複製程式碼

從前序與中序遍歷序列構造二叉樹

思路

本題依然採用遞迴的思路, 前序遍歷的第一個節點為二叉樹的根節點,以此作為突破口。

本題的前置條件是樹中不存在重複的元素。可以由中序遍歷的結果以及根節點值獲取根節點的左子樹以及右子樹。

我們這時可以獲得根節點左子樹和右子樹的長度。反過來可以獲取前序遍歷結果中的左右子樹。我們這時,把左右子樹再當成一顆二叉樹,使用遞迴的形式重複此過程。既可以推匯出整顆二叉樹。

解答


var buildTree = function(preorder, inorder) {
     
  let binaryTree = {}
   
  const iteration = (preorder, inorder, tree) => {
       
      if (!preorder.length) {
          binaryTree = null
          return
      }
       
      tree.val = null
      tree.left = {
          val: null,
          left: null,
          right: null
      }
      tree.right = {
          val: null,
          left: null,
          right: null
      }

     // 前序遍歷第一個節點為當前樹的根節點
     let rootVal = preorder.splice(0, 1)[0]
     // 中序遍歷根節點的索引
     let rootIndex = inorder.indexOf(rootVal)
     // 中序遍歷的左子樹
     let inorderLeftTree = inorder.slice(0, rootIndex)
     // 中序遍歷的右子樹
     let inorderRightTree = inorder.slice(rootIndex + 1)
     // 前序遍歷的左子樹
     let preorderLeftTree = preorder.slice(0, inorderLeftTree.length)
     // 前序遍歷的右子樹
     let preorderRightTree = preorder.slice(inorderLeftTree.length)

       
     tree.val = rootVal
      
     if (preorderLeftTree.length === 1 || inorderLeftTree.length === 1) {
         tree.left.val = preorderLeftTree[0]
     } else if (preorderLeftTree.length > 1 || inorderLeftTree.length > 1) {
         iteration(preorderLeftTree, inorderLeftTree, tree.left)
     } else {
          tree.left = null
     }
       
     if (preorderRightTree.length === 1 || inorderRightTree.length === 1) {
         tree.right.val = preorderRightTree[0]
     } else if (preorderRightTree.length > 1 || inorderRightTree.length > 1) {
         iteration(preorderRightTree, inorderRightTree, tree.right)
     } else {
      tree.right = null
     }
  }
   
  iteration(preorder, inorder, binaryTree)
   
  return binaryTree
}
複製程式碼

二叉搜尋樹

二叉搜尋樹是二叉樹的一種特殊形式。 二叉搜尋樹具有以下性質:每個節點中的值必須大於(或等於)其左側子樹中的任何值,但小於(或等於)其右側子樹中的任何值。

對於二叉搜尋樹,我們可以通過中序遍歷得到一個遞增的有序序列

驗證二叉搜尋樹

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

思路

可以通過中序DFS遍歷二叉搜尋樹, 判斷遍歷的結果是否為遞增的陣列判斷是否為搜尋二叉樹

解答


var isValidBST = function(root) {
    if (!root) return true
    
    // 中序DFS
    let result = []    

    const iteration = (root) => {
       if (root.left) {
           iteration(root.left)
       }
       result.push(root.val)
       if (root.right) {
           iteration(root.right)
       }
    }
    iteration(root)
    let resultString = result.join(',')
    let result2String = [...new Set(result.sort((a, b) => a - b))].join(',')
    return resultString === result2String
};
複製程式碼

在二叉搜尋樹中實現搜尋操作

如果目標值等於節點的值,則返回節點, 如果目標值小於節點的值,則繼續在左子樹中搜尋, 如果目標值大於節點的值,則繼續在右子樹中搜尋。

image

// 遞迴就完事了
var searchBST = function(root, val) {
    if (!root) return null
    
    let result = null
    
    const seatch = (node) => {
        if (node.val === val) {
            return result = node
        } else if (val > node.val && node.right) {
            seatch(node.right)
        } else if (val < node.val && node.left) {
            seatch(node.left)
        }
    }
    
    seatch(root)
        
    return result
};
複製程式碼

在二叉搜尋樹中實現插入操作

在二叉搜尋樹中的插入操作和搜尋操作類似。根據節點值與目標節點值的關係,搜尋左子樹或右子樹。當節點沒有左右子樹時。判斷目標值和當前節點的關係,執行插入的操作。


var insertIntoBST = function(root, val) {
    const insert = (root) => {
        if (val > root.val) {
            if (root.right) {
               insert(root.right) 
            } else {
               root.right = new TreeNode(val)
            }
        } else if (val < root.val) {
            if (root.left) {
               insert(root.left)  
            } else {
               root.left = new TreeNode(val)
            }
        }
    }
    
    insert(root)
    
    return root
};
複製程式碼

在二叉搜尋樹中實現刪除操作

刪除二叉樹的節點的操作複雜度遠遠大於搜尋和插入的操作。刪除搜尋二叉樹節點時, 需要考慮多種狀態

image

刪除的節點沒有子節點的時候, 直接移除改節點(從它的父節點上移除)

image

刪除的節點只有一個子節點的時候, 需要將需要刪除的節點的父節點, 連結上刪除節點的子節點。即可完成刪除

image

刪除的節點有兩個子節點的時候, 需要將刪除節點右子樹中的最小值, 賦予刪除的節點。然後刪除右子樹中的最小值即可。


/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {number} key
 * @return {TreeNode}
 */
var deleteNode = function(root, key) {
    
    
    // 根節點為空的情況
    if (!root) {
        return null
    }
    
    if (!root.left && !root.right && root.val === key) {
        root = null
        return root
    }
    
    if (!root.left && root.right && root.val === key) {
        root = root.right
        return root
    }
    
    if (root.left && !root.right && root.val === key) {
       root = root.left
        return root
    }
    
    // 根節點替換的情況
    
    // 尋找當前樹的最小節點
    const findMin = (root) => {
        let min = root
        while (min.left) {
            min = min.left
        }
        return min
    }
    
    let parentNode = null
    
    // 找到最近的父級
    const searchParent = (node, searchValue) => {
        console.log('???')
        let current = node
        let breaker = false
        
        while (!breaker) {
            console.log('查詢父親')
            if (
                (current.left && searchValue === current.left.val) ||
                (current.right && searchValue === current.right.val)
            ) {
              breaker = true
            } else if (searchValue < current.val) {
              current = current.left
            } else if (searchValue > current.val) {
              current = current.right
            } else {
              current = null
            }

            if (!current) break
        }
        
        parentNode = current
    }
    
    const remove = (node, deleteValue) => {
        if (node.val === deleteValue) {
            console.log('1')
            // node為要刪除的節點
            if (!node.left && !node.right) {
                console.log('3')
                // 如果沒有任何子節點
                searchParent(root, node.val)
                if (parentNode.left && parentNode.left.val === deleteValue) {
                    parentNode.left = null
                } else {
                    parentNode.right = null
                }
            } else if (!node.left && node.right) {
                console.log('4')
                // 如果只有一個子節點
                searchParent(root, node.val)
                if (parentNode.left && parentNode.left.val === deleteValue) {
                    parentNode.left = node.right
                } else {
                    parentNode.right = node.right
                }
            } else if (node.left && !node.right) {
                console.log('5')
                // 如果只有一個子節點
                searchParent(root, node.val)
                if (parentNode.left && parentNode.left.val === deleteValue) {
                    parentNode.left = node.left
                } else {
                    parentNode.right = node.left
                }
            } else {
                console.log('6')
                // 如果有兩個子節點
                // 找到右子樹中最小的節點
                let minNode = findMin(node.right)
                console.log('7')
                let minNodeValue = minNode.val
                console.log('8')
                remove(root, minNodeValue)
                console.log('9')
                node.val = minNodeValue
                console.log('10')
            }
        } else if (deleteValue > node.val && node.right) {
            console.log('2')
            remove(node.right, deleteValue)
        } else if (deleteValue < node.val && node.left) {
            console.log('3')
            remove(node.left, deleteValue)
        }
    }
    
    remove(root, key)
    
    return root
};
複製程式碼

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

給定一個二叉搜尋樹, 找到該樹中兩個指定節點的最近公共祖先。

思路

從根節點開始遍歷操作, 如果根節點的值大於目標節點1, 小於目標節點2。說明根節點就是最近的公共祖先。

如果根節點大於目標節點1, 目標節點2,則使用根節點的左子節點重複前一步的操作。

如果根節點小於目標節點1, 目標節點2,則使用根節點的右子節點重複前一步的操作。

解答


/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {TreeNode}
 */
var lowestCommonAncestor = function(root, p, q) {
    if (root.val > p.val && root.val > q.val) {
        return lowestCommonAncestor(root.left, p, q)
    }
    if (root.val < p.val && root.val < q.val) {
        return lowestCommonAncestor(root.right, p, q)
    }
    return root
};
複製程式碼

字首樹

image

字首樹是N叉樹的一種特殊形式。字首樹的每一個節點通常表示一個字元或者字串。每一個節點擁有多個不同的子節點。值得注意的是,根節點表示空字串。

字首樹的一個重要的特性是,節點所有的後代都與該節點相關的字串有著共同的字首。這就是字首樹名稱的由來。

如何表示一個Trie樹?

方法1, 使用長度為26的陣列儲存子節點

方法2, 使用hashMap儲存子節點

實現 Trie (字首樹)


var TrieNode = function (val = null) {
    // 當前的值
    this.val = val
    // 當前節點的子節點
    this.children = {}
    // 表示當前節點是否為一個單詞
    this.isWord = false
}

// 新增到節點
TrieNode.prototype.add = function (val) {
    let child = new TrieNode(val)
    this.children[val] = child
    return this.children[val]
}

// 判斷是否包含子節點
TrieNode.prototype.has = function (val) {
    return this.children[val] ? true : false
}

/**
 * Initialize your data structure here.
 */
var Trie = function() {
    // 初始化根節點
    this.root = new TrieNode('')
};

/**
 * Inserts a word into the trie. 
 * @param {string} word
 * @return {void}
 */
Trie.prototype.insert = function(word) {
    let current = this.root
    let words = word.split('')
    let i = 0
    // 替換最後一個節點
    while (i < words.length) {
        if (!current.has(words[i])) {
           // 如果不存在該子節點
           current = current.add(words[i])
        } else {
           // 如果存在該子節點
           current = current.children[words[i]]
        }
        i += 1
    }
    current.isWord = true
};

/**
 * Returns if the word is in the trie. 
 * 判斷是否存在單詞
 * @param {string} word
 * @return {boolean}
 */
Trie.prototype.search = function(word) {
    let current = this.root
    let words = word.split('')
    let i = 0
    let result = null
    while (i < words.length) {
        if (current.has(words[i])) {
            current = current.children[words[i]]
            i += 1 
        } else {
            return false
        }
    }
    return current.isWord

};

/**
 * Returns if there is any word in the trie that starts with the given prefix. 
 * 判斷是否包含單詞
 * @param {string} prefix
 * @return {boolean}
 */
Trie.prototype.startsWith = function(prefix) {
    let current = this.root
    let prefixs = prefix.split('')
    let i = 0
    while (i < prefixs.length) {
        if (current.has(prefixs[i])) {
            current = current.children[prefixs[i]]
            i += 1 
        } else {
            return false
        }
    }
    return true
};

/** 
 * Your Trie object will be instantiated and called as such:
 * var obj = Object.create(Trie).createNew()
 * obj.insert(word)
 * var param_2 = obj.search(word)
 * var param_3 = obj.startsWith(prefix)
 */
複製程式碼

單詞替換

在英語中,我們有一個叫做 詞根(root)的概念,它可以跟著其他一些片語成另一個較長的單詞——我們稱這個詞為 繼承詞(successor)。例如,詞根an,跟隨著單詞 other(其他),可以形成新的單詞 another(另一個)。

現在,給定一個由許多詞根組成的詞典和一個句子。你需要將句子中的所有繼承詞用詞根替換掉。如果繼承詞有許多可以形成它的詞根,則用最短的詞根替換它。

輸入: dict(詞典) = ["cat", "bat", "rat"]

sentence(句子) = "the cattle was rattled by the battery"

輸出: "the cat was rat by the bat"

思路

解答


var replaceWords = function(dict, sentence) {
    let sentences = sentence.split(' ')
    let result = []
    
    for (let i = 0; i < sentences.length; i++) {
        let trie = new Trie()
        // 句子中的每一個詞形成一個字首樹
        trie.insert(sentences[i])
        let min = sentences[i]
        for (let j = 0; j < dict.length; j++) {
            // 判斷是否包含詞根
            if (trie.startsWith(dict[j])) {
                // 取最短的詞根
                min = min.length < dict[j].length ? min : dict[j]
            }
        }
        result.push(min)
    }
    
    return result.join(' ')
};
複製程式碼

N叉樹

image

N叉樹的前序遍歷

先訪問根節點,然後以此遍歷根節點的所有子節點。如果子節點存在子節點。同根節點一樣,先遍歷自身然後遍歷它的子節點。


var preorder = function(root) {
    
    let result = []
    
    const iteration = (root) => {
        if (!root) return
        
        result.push(root.val)
        
        if (root.children) {
           for (let i = 0; i < root.children.length; i++) {
                iteration(root.children[i]) 
           } 
        }
    }
    
    iteration(root)
    
    return result
}
複製程式碼

N叉樹的後序遍歷

優先遍歷根節點的所有子節點,如果子節點存在子節點,則優先遍歷其它的子節點


var postorder = function(root) {
    let result = []
    
    const iteration = (root) => {
        if (!root) return
        
        if (root.children) {
            for (let i = 0; i < root.children.length; i++) {
                iteration(root.children[i])
            }
        }
        
        result.push(root.val)
    }
    
    iteration(root)
    
    return result
};
複製程式碼

N叉樹的層序遍歷


var levelOrder = function (root) {
    let reuslt = []

    const iteration = (root, level) => {
        if (!root) return

        if (!reuslt[level]) {
            reuslt[level] = []
        }

        reuslt[level].push(root.val)

        for (let i = 0; i < root.children.length; i++) {
            iteration(root.children[i], level + 1)
        }
    }

    iteration(root, 0)

    return reuslt
};
複製程式碼

N叉樹最大深度

給定一個 N 叉樹,找到其最大深度。最大深度是指從根節點到最遠葉子節點的最長路徑上的節點總數。

思路

思路很簡單, BFS整個N叉樹, 返回結果的長度即可

解答


var maxDepth = function(root) {
    let reuslt = []

    const iteration = (root, level) => {
        if (!root) return

        if (!reuslt[level]) {
            reuslt[level] = []
        }

        reuslt[level].push(root.val)

        for (let i = 0; i < root.children.length; i++) {
            iteration(root.children[i], level + 1)
        }

    }

    iteration(root, 0)

    return reuslt.length
};
複製程式碼

相關文章