1. 程式人生 > 其它 >演算法刷題之八樹

演算法刷題之八樹

樹是一種很有規律的資料結構,如果使用合適的訪問方法,可以很便捷高效的解決問題。比如遞迴

題目:

  1. 樹的最大深度
  2. 驗證二叉搜尋樹
  3. 對稱二叉樹
  4. 路徑總和
  5. 二叉樹的序列化與反序列化
  6. 樹的公共父節點

樹的最大深度

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

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

    3
   / \
  9  20
    /  \
   15   7
返回它的最大深度 3 。

從底向上

class Solution:
    def maxDepth(self, root: TreeNode) -> int:

        if not root:
            return 0
        else:
            left = self.maxDepth(root.left)
            right = self.maxDepth(root.right)
            print("%d--%d" % (left,right))
            return max(left,right) + 1

從頂向下
該呼叫方式是所謂的尾遞迴呼叫,所以比普通的呼叫方式更加高效和節省記憶體

class Solution:
    def maxDepth(self, root: TreeNode) -> int:
        
        def top_down(node,h):
            if node is None:
                return h
            
            left = top_down(node.left, h+1)
            right = top_down(node.right, h+1)
            return max(left, right)
        
        ret = top_down(root,0)
        return ret

驗證二叉搜尋樹

驗證二叉搜尋樹
給定一個二叉樹,判斷其是否是一個有效的二叉搜尋樹。
假設一個二叉搜尋樹具有如下特徵:

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

示例 1:
輸入:
    2
   / \
  1   3
輸出: true

示例 2:
輸入:
    5
   / \
  1   4
     / \
    3   6
輸出: false
解釋: 輸入為: [5,1,4,null,null,3,6]。
     根節點的值為 5 ,但是其右子節點值為 4 。

方法:這道題的規律也很明顯,左邊的值小於父節點,右邊的值大於父節點。其子節點也是類似。有兩種方式:1.中序遍歷可以得到一個升序的陣列,只要判斷陣列是否為完全升序即可;2.判斷當前節點值在不在某個正常範圍中

首先,我們來看二叉搜尋樹的兩個特徵:

節點的左子樹只包含小於當前節點的數。
節點的右子樹只包含大於當前節點的數。
仔細思考這兩句話,你可以理解為:

當前節點的值是其左子樹的值的上界(最大值)
當前節點的值是其右子樹的值的下界(最小值)
OK,有了這個想法,你可以將驗證二叉搜尋樹的過程抽象成下圖(-00代表無窮小,+00代表無窮大)

class Solution:
    def isValidBST(self, root: TreeNode) -> bool:

        def dfs(node, min_val, max_val):
            if not node:  # 邊界條件,如果node為空肯定是二叉搜尋樹
                return True
            if not min_val < node.val < max_val:  # 如果當前節點超出上下界範圍,肯定不是
                return False
            # 走到下面這步說明已經滿足瞭如題所述的二叉搜尋樹的前兩個條件
            # 那麼只需要遞迴判斷當前節點的左右子樹是否同時是二叉搜尋樹即可
            return dfs(node.left, min_val, node.val) and dfs(node.right, node.val, max_val)

        return dfs(root, float('-inf'), float('inf')) # 對於根節點,它的上下限為無窮大和無窮小

對稱二叉樹

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

例如,二叉樹 [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

方法:原理就是分別比較對應位置上的值,如果相同則繼續,如果不相同則退出


迭代方式:每次將對應的兩組資料加入的佇列中,然後取出比較,如果相等,再將其子樹加入到佇列中。
知識點:層次遍歷

class Solution:
    def isSymmetric(self, root: TreeNode) -> bool:
        if not root:
            return True
        
        queue = [root.left,root.right]
        result = []

        while queue:
            left = queue.pop(0)
            right = queue.pop(0)

            if not left and not right:
                continue
            if not left or not right:
                return False
            
            if left.val != right.val:
                return False
            queue.append(left.left)
            queue.append(right.right)

            queue.append(left.right)
            queue.append(right.left)
        return True

遞迴方式:遞迴比較,從root節點的左右開始,左邊和右邊是否相等。左邊的子節點和右邊的子節點是否相等。迭代比較下

class Solution:
    def isSymmetric(self, root: TreeNode) -> bool:
        if not root:
            return True
        
        def new_trees(left, right):

            if not left and not right:
                return True
            
            if not left or not right:
                return False
            
            if left.val != right.val:
                return False
            return all((new_trees(left.left,right.right), new_trees(left.right,right.left)))
    
        return new_trees(root.left,root.right)

路徑總和

給定一個二叉樹和一個目標和,判斷該樹中是否存在根節點到葉子節點的路徑,這條路徑上所有節點值相加等於目標和。
說明: 葉子節點是指沒有子節點的節點。
示例:
給定如下二叉樹,以及目標和 sum = 22,

              5
             / \
            4   8
           /   / \
          11  13  4
         /  \      \
        7    2      1
返回 true, 因為存在目標和為 22 的根節點到葉子節點的路徑 5->4->11->2。

解法一:一邊遍歷一邊做減法

def hasPathSum(self, root: TreeNode, Sum: int) -> bool:
        
        if not root:
            return False
        if Sum == root.val and not root.left and not root.right:
            return True
        
        left = self.hasPathSum(root.left, Sum-root.val)
        right = self.hasPathSum(root.right, Sum-root.val)

        return left or right

解法二: 遍歷出所有路徑,最後做加法

class Solution(object):
    def hasPathSum(self, root, sum):
        if not root: return False
        res = []
        return self.dfs(root, sum, res, [root.val])
        
    def dfs(self, root, target, res, path):
        if not root: return False
        print(path)
        if sum(path) == target and not root.left and not root.right:
            return True
        left_flag, right_flag = False, False
        if root.left:
            left_flag = self.dfs(root.left, target, res, path + [root.left.val])
           
        if root.right:
            right_flag = self.dfs(root.right, target, res, path + [root.right.val])
            
        return left_flag or right_flag
>>>
[5]
[5, 4]
[5, 4, 11]
[5, 4, 11, 7]
[5, 4, 11, 2]
[5, 8]
[5, 8, 13]
[5, 8, 4]
[5, 8, 4, 1]

path 保留每一層的遍歷結果,同時,多層的遍歷中函式儲存的path值是不一樣的。每一層函式的遍歷path列表的值都是儲存當前的狀態,互不影響。
原因在於:

if root.left:
    left_flag = self.dfs(root.left, target, res, path + [root.left.val])
           
if root.right:
    right_flag = self.dfs(root.right, target, res, path + [root.right.val])

簡約版

def lijin(head,arr,target):
    if not head :
        print(arr)
        if sum(arr) == target:
            return True
        return False
    

    left = lijin(head.left,arr+[head.value],target)
    right = lijin(head.right,arr+[head.value],target)
    return left or right

分別在left和right中都使用了path,這個path在遞迴的層次中互相不干擾,如果想單純的遍歷出一棵樹的路徑,可以使用如下的程式碼:

def output(self,root,arr):
        # result = []
        if not root:
            print(arr)
            return None
        else:
            # self.result.append(root.value)
            # arr.append(root.value)
            self.output(root.left,arr+[root.value])
            self.output(root.right,arr+[root.value])

二叉樹的層序遍歷

給你一個二叉樹,請你返回其按 層序遍歷 得到的節點值。 (即逐層地,從左到右訪問所有節點)。

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

    3
   / \
  9  20
    /  \
   15   7
   
返回其層次遍歷結果:
[
  [3],
  [9,20],
  [15,7]
]

方法:樹的遍歷很簡單,分為DFS和BFS。其中這一道題比較適合BFS即廣度優先遍歷。

class Solution:
    def levelOrder(self, root: TreeNode) -> List[List[int]]:
        if not root: return []  # 特殊情況,root為空直接返回
        from collections import deque
        # 下面就是BFS模板內容,BFS關鍵在於佇列的使用
        layer = deque()
        layer.append(root)  # 壓入初始節點
        res = []  # 結果集
        while layer:
            cur_layer = []  # 臨時變數,記錄當前層的節點
            for _ in range(len(layer)):  # 遍歷某一層的節點
                node = layer.popleft()  # 將要處理的節點彈出
                cur_layer.append(node.val)
                if node.left:  # 如果當前節點有左右節點,則壓入佇列,根據題意注意壓入順序,先左後右,
                    layer.append(node.left)
                if node.right:
                    layer.append(node.right)
            res.append(cur_layer)  # 某一層的節點都處理完之後,將當前層的結果壓入結果集
        return res

二叉樹的序列化與反序列化

序列化是將一個數據結構或者物件轉換為連續的位元位的操作,進而可以將轉換後的資料儲存在一個檔案或者記憶體中,同時也可以通過網路傳輸到另一個計算機環境,採取相反方式重構得到原資料。
請設計一個演算法來實現二叉樹的序列化與反序列化。這裡不限定你的序列 / 反序列化演算法執行邏輯,你只需要保證一個二叉樹可以被序列化為一個字串並且將這個字串反序列化為原始的樹結構。

 def serialize(self, root):
        """Encodes a tree to a single string.
        
        :type root: TreeNode
        :rtype: str
        """
        if not root: return "[]"
        queue = collections.deque()
        queue.append(root)
        res = []
        while queue:
            node = queue.popleft()
            if node:
                res.append(str(node.val))
                queue.append(node.left)
                queue.append(node.right)
            else: res.append("null")
        return '[' + ','.join(res) + ']'


    def deserialize(self, data):
        """Decodes your encoded data to tree.
        
        :type data: str
        :rtype: TreeNode
        """
        if data=='[]':
            return None
        vals, i = data[1:-1].split(','), 1
        root = TreeNode(int(vals[0]))
        queue = collections.deque()
        queue.append(root)
        while queue:
            node = queue.popleft()
            if vals[i] != "null":
                node.left = TreeNode(int(vals[i]))
                queue.append(node.left)
            i += 1
            if vals[i] != "null":
                node.right = TreeNode(int(vals[i]))
                queue.append(node.right)
            i += 1
        return root

二叉樹的最近公共祖先

方法:使用前序遍歷的方式,一直遍歷找到一個節點,然後返回該節點,如果沒有則直到葉子節點返回None。
當兩個底層的節點不斷被上浮,一定會相遇。像相遇點就是公共祖先節點。

def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
    if not root or root == p or root == q:
        return root
    left = self.lowestCommonAncestor(root.left, p, q)
    right = self.lowestCommonAncestor(root.right, p, q)
    if not left:
        return right
    if not right:
        return left
    return root 

樹的小結

樹這種資料結構在演算法中我還沒有遇到,估計是面試官也不好駕馭這種資料結構。對於樹來說,很多演算法題我都非常懵,不看答案想破腦袋都想不出的,最好仰天長嘯:原來還可以這樣。
對於樹來來說需要明白的技巧前序遍歷中序遍歷後序遍歷BFSDFS等。多看套路,少走彎路。