Data structure refresher: Day 10

Table of contents

 1. Preorder traversal of binary tree

1. Recursion

Ideas and Algorithms

Complexity analysis

2. Iteration

Ideas and Algorithms

Complexity analysis

3. Morris traversal

Ideas and Algorithms

Complexity analysis

2. In-order traversal of binary tree

 1. Recursion

Ideas and Algorithms

Complexity analysis

3. Morris in-order traversal

Ideas and Algorithms

Complexity analysis

3. Postorder sorting of binary trees

1. Recursion

Ideas and Algorithms

Complexity analysis

2. Iteration

Complexity analysis

3. Morris traversal

Ideas and Algorithms

Complexity analysis

 1. Preorder traversal of binary tree

144. Preorder traversal of binary trees - LeetCode https://leetcode.cn/problems/binary-tree-preorder-traversal/?plan=data-structures&plan_progress=ggfacv7

1. Recursion

Ideas and Algorithms

First of all, we need to understand what preorder traversal of a binary tree is: traverse the tree in the same way as the root node - left subtree - right subtree. When visiting the left subtree or right subtree, we follow the same method. Traverse until the entire tree is traversed. Therefore, the entire traversal process is naturally recursive, and we can directly use a recursive function to simulate this process.

Define preorder(root) to represent the answer currently traversed to the root node. According to the definition, we only need to first add the value of the root node to the answer, then call preorder(root.left) recursively to traverse the left subtree of the root node, and finally call preorder(root.right) recursively to traverse the right subtree of the root node, that is Yes, the condition for recursion termination is encountering an empty node.

class Solution {
public:
    void preorder(TreeNode *root, vector<int> &res) {
        if (root == nullptr) {
            return;
        }
        res.push_back(root->val);
        preorder(root->left, res);
        preorder(root->right, res);
    }

    vector<int> preorderTraversal(TreeNode *root) {
        vector<int> res;
        preorder(root, res);
        return res;
    }
};

Complexity analysis

Time complexity: O(n), where n is the number of nodes of the binary tree. Each node is traversed exactly once.

Space complexity: O(n), which is the overhead of the stack during the recursive process. In the average case, it is O(logn). In the worst case, the tree is chain-shaped, which is O(n).

2. Iteration

Ideas and Algorithms

We can also use iteration to implement the recursive function of method 1. The two methods are equivalent. The difference is that a stack is implicitly maintained during recursion, and we need to explicitly simulate this stack during iteration. , the rest of the implementation and details are the same, please refer to the code below for details.

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        if (root == nullptr) {
            return res;
        }

        stack<TreeNode*> stk;
        TreeNode* node = root;
        while (!stk.empty() || node != nullptr) {
            while (node != nullptr) {
                res.emplace_back(node->val);
                stk.emplace(node);
                node = node->left;
            }
            node = stk.top();
            stk.pop();
            node = node->right;
        }
        return res;
    }
};

Complexity analysis

Time complexity: O(n), where nn is the number of nodes of the binary tree. Each node is traversed exactly once.

Space complexity: O(n), which is the overhead of the explicit stack during the iteration process. In the average case, it is O(logn). In the worst case, the tree is chain-shaped, which is O(n).

3. Morris traversal

Ideas and Algorithms

There is a clever way to implement preorder traversal in linear time and only occupying constant space. This method was first proposed by JH Morris in his 1979 paper "Traversing Binary Trees Simply and Cheaply", so it is called Morris traversal.

The core idea of ​​Morris traversal is to utilize a large number of free pointers in the tree to achieve the ultimate reduction in space overhead. Its preorder traversal rules are summarized as follows:

Create a new temporary node and make the node root;

If the left child node of the current node is empty, add the current node to the answer and traverse the right child node of the current node;

If the left child node of the current node is not empty, find the predecessor node of the current node under in-order traversal in the left subtree of the current node:

If the right child node of the predecessor node is empty, set the right child node of the predecessor node to the current node. Then add the current node to the answer and update the right child node of the predecessor node to the current node. The current node is updated to the left child node of the current node.

If the right child node of the predecessor node is the current node, reset its right child node to empty. The current node is updated to the right child node of the current node.

Repeat steps 2 and 3 until the traversal is complete.

In this way, we can use the Morris traversal method to traverse the binary tree in preorder to achieve linear time and constant space traversal.

class Solution {
public:
    vector<int> preorderTraversal(TreeNode *root) {
        vector<int> res;
        if (root == nullptr) {
            return res;
        }

        TreeNode *p1 = root, *p2 = nullptr;

        while (p1 != nullptr) {
            p2 = p1->left;
            if (p2 != nullptr) {
                while (p2->right != nullptr && p2->right != p1) {
                    p2 = p2->right;
                }
                if (p2->right == nullptr) {
                    res.emplace_back(p1->val);
                    p2->right = p1;
                    p1 = p1->left;
                    continue;
                } else {
                    p2->right = nullptr;
                }
            } else {
                res.emplace_back(p1->val);
            }
            p1 = p1->right;
        }
        return res;
    }
};

Complexity analysis

Time complexity: O(n), where nn is the number of nodes of the binary tree. A node without a left subtree is visited only once, and a node with a left subtree is visited twice.

Space complexity: O(1). Only operates on pointers that already exist (the tree's free pointer), so only a constant amount of extra space is required.

2. In-order traversal of binary tree

94. In-order traversal of binary trees - LeetCode https://leetcode.cn/problems/binary-tree-inorder-traversal/?plan=data-structures&plan_progress=ggfacv7

 1. Recursion

Ideas and Algorithms

First of all, we need to understand what in-order traversal of a binary tree is: traverse the tree in the same way as the left subtree - the root node - the right subtree, and when we visit the left subtree or the right subtree, we follow the same method. Traverse until the entire tree is traversed. Therefore, the entire traversal process is naturally recursive, and we can directly use a recursive function to simulate this process.

Define inorder(root) to represent the answer currently traversed to the root node. Then according to the definition, we only need to call inorder(root.left) recursively to traverse the left subtree of the root node, then add the value of the root node to the answer, and then call inorder recursively. (root.right) to traverse the right subtree of the root node. The condition for recursive termination is encountering an empty node.

class Solution {
public:
    void inorder(TreeNode* root, vector<int>& res) {
        if (!root) {
            return;
        }
        inorder(root->left, res);
        res.push_back(root->val);
        inorder(root->right, res);
    }
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        inorder(root, res);
        return res;
    }
};

Complexity analysis

Time complexity: O(n), where nn is the number of binary tree nodes. In a binary tree traversal, each node will be visited once and only once.

Space complexity: O(n). The space complexity depends on the recursive stack depth, and the stack depth will reach the O(n) level when the binary tree is a chain.

2. Iteration

We can also implement the recursive function of method 1 using iteration. The two methods are equivalent. The difference is that a stack is implicitly maintained during recursion, and we need to explicitly simulate this stack during iteration. , everything else is the same, the specific implementation can be seen in the code below.

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> stk;
        while (root != nullptr || !stk.empty()) {
            while (root != nullptr) {
                stk.push(root);
                root = root->left;
            }
            root = stk.top();
            stk.pop();
            res.push_back(root->val);
            root = root->right;
        }
        return res;
    }
};

Complexity analysis

Time complexity: O(n), where nn is the number of binary tree nodes. In a binary tree traversal, each node will be visited once and only once.

Space complexity: O(n). The space complexity depends on the stack depth, and the stack depth will reach the O(n) level when the binary tree is a chain.

3. Morris in-order traversal

Ideas and Algorithms

Morris traversal algorithm is another method for traversing binary trees, which can reduce the space complexity of non-recursive in-order traversal to O(1).

The overall steps of the Morris traversal algorithm are as follows (assuming that the node currently traversed is x):

1. If x has no left child, first add the value of x to the answer array, and then access the right child of x, that is, x =x.right
2. If x has a left child, find the rightmost node on the left subtree of x ( That is, the last node of the left subtree in the in-order traversal, the predecessor node of x in the in-order traversal), we record it as predecessor. Depending on whether the right child of \textit{predecessor}predecessor is empty, perform the following operations.

                If the right child of predecessor is empty, point its right child to x, and then access the left child of x, that is, x =x.left.
                If the right child of the predecessor is not empty, then its right child points to x at this time, indicating that we have traversed the left subtree of x. We leave the right child of the predecessor empty, add the value of x to the answer array, and then access the x The right child is x=x.right.

Repeat the above operations until the complete tree is visited.

In fact, we only need to do one more step in the whole process: assuming that the currently traversed node is x, point the right child of the rightmost node in the left subtree of x, and can know through this pointer that we have traversed the left subtree without having to maintain it through the stack, eliminating the space complexity of the stack.

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        TreeNode *predecessor = nullptr;

        while (root != nullptr) {
            if (root->left != nullptr) {
                // predecessor 节点就是当前 root 节点向左走一步,然后一直向右走至无法走为止
                predecessor = root->left;
                while (predecessor->right != nullptr && predecessor->right != root) {
                    predecessor = predecessor->right;
                }
                
                // 让 predecessor 的右指针指向 root,继续遍历左子树
                if (predecessor->right == nullptr) {
                    predecessor->right = root;
                    root = root->left;
                }
                // 说明左子树已经访问完了,我们需要断开链接
                else {
                    res.push_back(root->val);
                    predecessor->right = nullptr;
                    root = root->right;
                }
            }
            // 如果没有左孩子,则直接访问右孩子
            else {
                res.push_back(root->val);
                root = root->right;
            }
        }
        return res;
    }
};

Complexity analysis

Time complexity: O(n), where nn is the number of nodes in the binary search tree. Each node in Morris traversal will be visited twice, so the total time complexity is O(2n)=O(n).

Space complexity: O(1).

3. Postorder sorting of binary trees

145. Postorder traversal of binary trees - LeetCode https://leetcode.cn/problems/binary-tree-postorder-traversal/?plan=data-structures&plan_progress=ggfacv7

1. Recursion

Ideas and Algorithms

First of all, we need to understand what post-order traversal of a binary tree is: traverse the tree by visiting the left subtree-right subtree-root node, and when visiting the left subtree or right subtree, we follow the same method Traverse until the entire tree is traversed. Therefore, the entire traversal process is naturally recursive, and we can directly use a recursive function to simulate this process.

Define postorder(root) to represent the answer currently traversed to the root node. According to the definition, we only need to recursively call postorder(root->left) to traverse the left subtree of the root node, then recursively call postorder(root->right) to traverse the right subtree of the root node, and finally add the value of the root node to the answer That’s it, the condition for recursion termination is encountering an empty node.

class Solution {
public:
    void postorder(TreeNode *root, vector<int> &res) {
        if (root == nullptr) {
            return;
        }
        postorder(root->left, res);
        postorder(root->right, res);
        res.push_back(root->val);
    }

    vector<int> postorderTraversal(TreeNode *root) {
        vector<int> res;
        postorder(root, res);
        return res;
    }
};

Complexity analysis

Time complexity: O(n), where n is the number of nodes in the binary search tree. Each node is traversed exactly once.

Space complexity: O(n), which is the overhead of the stack during the recursive process. In the average case, it is O(logn). In the worst case, the tree is chain-shaped, which is O(n).

2. Iteration

We can also use iteration to implement the recursive function of method 1. The two methods are equivalent. The difference is that a stack is implicitly maintained during recursion, and we need to explicitly simulate this stack during iteration. , the rest of the implementation and details are the same, please refer to the code below for details.

class Solution {
public:
    vector<int> postorderTraversal(TreeNode *root) {
        vector<int> res;
        if (root == nullptr) {
            return res;
        }

        stack<TreeNode *> stk;
        TreeNode *prev = nullptr;
        while (root != nullptr || !stk.empty()) {
            while (root != nullptr) {
                stk.emplace(root);
                root = root->left;
            }
            root = stk.top();
            stk.pop();
            if (root->right == nullptr || root->right == prev) {
                res.emplace_back(root->val);
                prev = root;
                root = nullptr;
            } else {
                stk.emplace(root);
                root = root->right;
            }
        }
        return res;
    }
};

Complexity analysis

Time complexity: O(n), where n is the number of nodes in the binary search tree. Each node is traversed exactly once.

Space complexity: O(n), which is the overhead of the explicit stack during the iteration process. In the average case, it is O(logn). In the worst case, the tree is chain-shaped, which is O(n).

3. Morris traversal

Ideas and Algorithms

There is a clever way to implement postorder traversal in linear time and only occupying constant space. This method was first proposed by JH Morris in his 1979 paper "Traversing Binary Trees Simply and Cheaply", so it is called Morris traversal.

The core idea of ​​Morris traversal is to utilize a large number of free pointers in the tree to achieve the ultimate reduction in space overhead. The rules for postorder traversal are summarized as follows:

Create a new temporary node and make the node root;

If the left child node of the current node is empty, traverse the right child node of the current node;

If the left child node of the current node is not empty, find the predecessor node of the current node under in-order traversal in the left subtree of the current node;

If the right child node of the predecessor node is empty, the right child node of the predecessor node is set to the current node, and the current node is updated to the left child node of the current node.

If the right child node of the predecessor node is the current node, reset its right child node to empty. Output all nodes on the path from the left child node of the current node to the predecessor node in reverse order. The current node is updated to the right child node of the current node.

Repeat steps 2 and 3 until the traversal is complete.

In this way, we can use the Morris traversal method to traverse the binary search tree in post-order to achieve linear time and constant space traversal.

class Solution {
public:
    void addPath(vector<int> &vec, TreeNode *node) {
        int count = 0;
        while (node != nullptr) {
            ++count;
            vec.emplace_back(node->val);
            node = node->right;
        }
        reverse(vec.end() - count, vec.end());
    }

    vector<int> postorderTraversal(TreeNode *root) {
        vector<int> res;
        if (root == nullptr) {
            return res;
        }

        TreeNode *p1 = root, *p2 = nullptr;

        while (p1 != nullptr) {
            p2 = p1->left;
            if (p2 != nullptr) {
                while (p2->right != nullptr && p2->right != p1) {
                    p2 = p2->right;
                }
                if (p2->right == nullptr) {
                    p2->right = p1;
                    p1 = p1->left;
                    continue;
                } else {
                    p2->right = nullptr;
                    addPath(res, p1->left);
                }
            }
            p1 = p1->right;
        }
        addPath(res, root);
        return res;
    }
};

Complexity analysis

Time complexity: O(n), where n is the number of nodes of the binary tree. A node without a left subtree is visited only once, and a node with a left subtree is visited twice.

Space complexity: O(1). Only operates on pointers that already exist (the tree's free pointer), so only a constant amount of extra space is required.

Guess you like

Origin blog.csdn.net/m0_63309778/article/details/126831933