Leetcode 1028. Recover a Tree From Preorder Traversal

#String #Tree #Depth-First Search #Binary Tree

Table of Contents

題目資訊

題目敘述

我們在二元樹的 root 上運行前序深度優先搜尋 (DFS)。

在這次遍歷中的每個節點,我們輸出 D 個短橫線(其中 D 是此節點的深度),然後我們輸出此節點的值。如果一個節點的深度是 D,那麼它的直接子節點的深度是 D + 1root 節點的深度是 0

如果一個節點只有一個子節點,那麼這個子節點一定是左子節點

給定這次遍歷的輸出 traversal,重建樹並返回 root

範例 1:

範例 1

輸入: traversal = "1-2--3--4-5--6--7"
輸出: [1,2,5,3,4,6,7]

範例 2:

範例 2

輸入: traversal = "1-2--3---4-5--6---7"
輸出: [1,2,5,3,null,6,null,4,null,7]

範例 3:

範例 3

輸入: traversal = "1-401--349---90--88"
輸出: [1,401,null,349,88,90]

約束:

  • 原始樹中的節點數在範圍 $[1, 1000]$ 內。
  • $1 \leq \text{Node.val} \leq 10^9$。

直覺

為了解決這個樹的復原問題,主要任務是理解在給定的遍歷字符串中,深度與節點值之間的關係。輸入字符串是對原始二叉樹進行先序深度優先搜索 (DFS) 後的結果。每個節點由一個數字表示,該數字前面有一定數量的破折號,這些破折號指示該節點的深度。透過準確解釋這些前綴,可以重建二叉樹的結構。挑戰在於使用這些破折號來管理樹的深度,同時確保節點插入到正確的位置。

解法

該算法利用堆疊來管理二叉樹的構建過程,以根據輸入字符串中提供的先序遍歷詳細信息進行構建。以下是該方法的逐步解析:

  1. 初始化和根節點創建:

    • 首先,識別字符串中第一個破折號 (-) 出現的位置。如果沒有破折號,則該字符串表示的是一個單節點樹,並且可以從該字符串創建並返回一個單一的 TreeNode
    • 如果存在破折號,從字符串的開始處提取直到第一個破折號的數字以創建樹的根節點。然後初始化一個堆疊並將此根節點推入堆疊。
  2. 通過深度計算進行迭代:

    • 從第一個破折號開始遍歷字符串,對於字符串的每個部分,通過計數連續的破折號計算當前節點的深度。
    • 利用深度來對應堆疊的大小;深度較小或相等的節點應該是當前節點的祖先。堆疊中比當前深度大的節點需要被彈出。
  3. 節點值提取和樹結構構建:

    • 在確定深度之後,從字符串中提取節點的值。如果在該節點之後沒有後續的破折號,則表示該節點是字符串中的最後一個。
    • 檢查當前節點應放置為堆疊頂節點的左子節點還是右子節點。如果沒有左子節點存在,則將當前節點指定為左子節點;否則,設定為右子節點。
    • 將當前節點推入堆疊,將其註冊為最近的“父”節點,以便接收具有適當深度的即將到來的節點作為其子節點。
  4. 完成:

    • 一旦整個字符串處理完成,堆疊可以有效保持來自不同深度的節點之間的關係。堆疊上的根節點代表復原樹的根。
    • 返回此根節點,代表完全重建的二叉樹。

這種方法通過堆疊操作有效地管理節點深度,將遍歷的線性表示小心地映射回層次結構樹結構。每一步都確保堆疊能夠適當地反映樹構建過程中的當前層次結構,並遵循由先序遍歷和深度所確定的規則。

程式碼

C++

/**
 * 二元樹節點的定義。
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* recoverFromPreorder(string traversal) {
        // 找出第一個連字符的位置,它表示深度
        auto firstDashIdx = traversal.find('-');

        // 如果沒有連字符,則表示這是個單一節點的樹
        if (firstDashIdx == string::npos) 
            return new TreeNode(stoi(traversal));

        // 使用第一個連字符之前的子字串來創建根節點
        TreeNode* root = new TreeNode(stoi(traversal.substr(0, firstDashIdx)));

        // 使用堆疊來跟踪每個深度的樹節點
        stack<TreeNode*> treeStack;
        treeStack.push(root);

        // 遍歷整個輸入字符串
        for (int i = firstDashIdx; i < traversal.size(); i++) {
            int depthCount = 0;

            // 計算連續連字符的數量以確定深度
            while (i < traversal.size() && traversal[i] == '-') {
                depthCount++;
                i++;
            }

            // 從堆疊中彈出節點直到堆疊大小與當前深度相符
            while (treeStack.size() > depthCount) 
                treeStack.pop();

            // 找到下一個連字符或字符串結尾以獲取當前節點的值
            auto nextDashIdx = traversal.find('-', i);
            TreeNode* currentNode;

            if (nextDashIdx == string::npos) {
                // 如果沒有更多連字符,這是路徑中的最後一個節點
                currentNode = new TreeNode(stoi(traversal.substr(i)));
                i = traversal.size(); // 移動到字符串的末尾
            } else {
                // 提取當前位置和下一個連字符之間的節點值
                currentNode = new TreeNode(stoi(traversal.substr(i, nextDashIdx - i)));
                i = nextDashIdx - 1; // 將索引更新到下一個連字符之前
            }

            // 確定當前節點應該作為左子節點還是右子節點
            if (treeStack.top()->left == nullptr) {
                // 如果沒有左子節點,將當前節點分配為左節點
                treeStack.top()->left = currentNode;
            } else {
                // 否則,將當前節點分配為右節點
                treeStack.top()->right = currentNode;
            }

            // 將當前節點推入堆疊
            treeStack.push(currentNode);
        }

        // 返回重建的二元樹的根節點
        return root;
    }
};

Python

class TreeNode:
    def __init__(self, x, left=None, right=None):
        self.val = x
        self.left = left
        self.right = right

class Solution:
    def recoverFromPreorder(self, traversal: str) -> TreeNode:
        # 找到第一個短線的位置,以指示深度
        firstDashIdx = traversal.find('-')

        # 如果沒有短線,則是單一節點的樹
        if firstDashIdx == -1:
            return TreeNode(int(traversal))

        # 使用第一個短線之前的子字串建立根節點
        root = TreeNode(int(traversal[:firstDashIdx]))

        # 使用堆疊記錄每個深度的樹節點
        treeStack = [root]

        # 遍歷剩餘的遍歷字串
        i = firstDashIdx
        while i < len(traversal):
            depthCount = 0

            # 計數連續的短線以確定深度
            while i < len(traversal) and traversal[i] == '-':
                depthCount += 1
                i += 1

            # 從堆疊中彈出節點,直到大小符合當前深度
            while len(treeStack) > depthCount:
                treeStack.pop()

            # 找到下一個短線或字串結尾以獲取當前節點的值
            nextDashIdx = traversal.find('-', i)
            if nextDashIdx == -1:
                # 如果沒有更多短線,則是路徑中的最後一個節點
                currentNode = TreeNode(int(traversal[i:]))
                i = len(traversal)  # 移動到字串的結尾
            else:
                # 提取當前位置和下一個短線之間的節點值
                currentNode = TreeNode(int(traversal[i:nextDashIdx]))
                i = nextDashIdx - 1  # 更新索引至緊鄰的下一個短線之前

            # 檢查當前節點應為左子或右子
            if treeStack[-1].left is None:
                # 如果沒有左子,則將當前節點分配給左子
                treeStack[-1].left = currentNode
            else:
                # 否則,將當前節點分配給右子
                treeStack[-1].right = currentNode

            # 將當前節點推入堆疊
            treeStack.append(currentNode)

        # 返回重建後二元樹的根
        return root

複雜度分析

  • 時間複雜度: 演算法以線性方式精確地處理輸入字串的每個字符,迴圈內的操作使用堆疊進行高效管理。因此,時間複雜度為 $O(n)$,其中 $n$ 為輸入字串 traversal 的長度。

  • 空間複雜度: 空間複雜度由堆疊的最大大小決定,這對應於樹的最大深度。在最壞情況下,樹的深度與節點數成正比,使得空間複雜度為 $O(d)$,其中 $d$ 為樹的深度。在最壞情況下,當每個節點代表一個完全不平衡樹的深度層級時,空間複雜度也可能為 $O(n)$。

Disclaimer: All reference materials on this website are sourced from the internet and are intended for learning purposes only. If you believe any content infringes upon your rights, please contact me at csnote.cc@gmail.com, and I will remove the relevant content promptly.


Feedback Welcome: If you notice any errors or areas for improvement in the articles, I warmly welcome your feedback and corrections. Your input will help this blog provide better learning resources. This is an ongoing process of learning and improvement, and your suggestions are valuable to me. You can reach me at csnote.cc@gmail.com.