Data structure refresher: Day 14

 

Table of contents

1. Verify binary search tree

1. Recursion

Ideas and algorithms

Complexity analysis

2. In-order traversal

Ideas and algorithms

Complexity analysis

Two, the sum of two numbers IV - input binary search tree

See the solution: 

3. The nearest common ancestor of the binary search tree

1. Two traversals

Ideas and Algorithms

Complexity analysis

2. One traversal

Ideas and Algorithms

Complexity analysis

1. Verify binary search tree

98. Validate binary search tree - LeetCode https://leetcode.cn/problems/validate-binary-search-tree/?plan=data-structures&plan_progress=ggfacv7

1. Recursion

Ideas and algorithms

To solve this problem, we first need to understand what properties of the binary search tree can be used by us. From the information given in the question, we can know: If the left subtree of the binary tree is not empty, then the values ​​of all nodes on the left subtree The values ​​are all less than the value of its root node; if its right subtree is not empty, the values ​​of all nodes on the right subtree are greater than the value of its root node; its left and right subtrees are also binary search trees.

This inspires us to design a recursive function helper(root, lower, upper) for recursive judgment. The function means considering the subtree with root as the root and judging whether the values ​​of all nodes in the subtree are within the range of (l, r) ( Note that it is an open interval). If the value val of the root node is not within the range of (l, r), it means that the condition is not met and returns directly. Otherwise, we have to continue the recursive call to check whether its left and right subtrees are satisfied. If they are all satisfied, it means that this is a binary search tree. .

Then according to the nature of the binary search tree, when calling the left subtree recursively, we need to change the upper bound upper to root.val, that is, call helper(root.left, lower, root.val), because all the objects in the left subtree The value of a node is less than the value of its root node. Similarly, when calling the right subtree recursively, we need to change the lower bound to root.val, that is, call helper(root.right, root.val, upper).

The entry point for recursive function calls is helper(root, -inf, +inf), inf represents an infinite value.

class Solution {
public:
    bool helper(TreeNode* root, long long lower, long long upper) {
        if (root == nullptr) {
            return true;
        }
        if (root -> val <= lower || root -> val >= upper) {
            return false;
        }
        return helper(root -> left, lower, root -> val) && helper(root -> right, root -> val, upper);
    }
    bool isValidBST(TreeNode* root) {
        return helper(root, LONG_MIN, LONG_MAX);
    }
};

Complexity analysis

Time complexity: O(n), where n is the number of nodes of the binary tree. During the recursive call, each node of the binary tree is visited at most once, so the time complexity is O(n).

Space complexity: O(n), where n is the number of nodes of the binary tree. Recursive functions need to allocate stack space for each level of recursive functions during the recursive process, so additional space is required here and this space depends on the depth of the recursion, that is, the height of the binary tree. In the worst case, the binary tree is a chain, the height of the tree is n, and the deepest recursion reaches n levels, so the space complexity in the worst case is O(n).

2. In-order traversal

Ideas and algorithms

Based on the properties mentioned in Method 1, we can further know that the sequence of values ​​obtained by "in-order traversal" of the binary search tree must be in ascending order. This inspires us to check in real time whether the value of the current node is greater than The value of the node traversed in the previous in-order is enough. If both are greater than that, it means that the sequence is in ascending order, and the entire tree is a binary search tree. Otherwise, it is not. In the following code, we use the stack to simulate the process of in-order traversal.

Some readers may not know what in-order traversal is, so we will briefly mention it here. In-order traversal is a traversal method of binary trees. It first traverses the left subtree, then traverses the root node, and finally traverses the right subtree. Our binary search tree ensures that the values ​​of the nodes of the left subtree are less than the value of the root node, and the values ​​of the root node are less than the value of the right subtree. Therefore, the sequence obtained after in-order traversal must be an ascending sequence.

class Solution {
public:
    bool isValidBST(TreeNode* root) {
        stack<TreeNode*> stack;
        long long inorder = (long long)INT_MIN - 1;

        while (!stack.empty() || root != nullptr) {
            while (root != nullptr) {
                stack.push(root);
                root = root -> left;
            }
            root = stack.top();
            stack.pop();
            // 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树
            if (root -> val <= inorder) {
                return false;
            }
            inorder = root -> val;
            root = root -> right;
        }
        return true;
    }
};

Complexity analysis

Time complexity: O(n), where n is the number of nodes of the binary tree. Each node of the binary tree is visited at most once, so the time complexity is O(n).

Space complexity: O(n), where n is the number of nodes of the binary tree. The stack can store up to n nodes, so it requires additional O(n) space.

Two, the sum of two numbers IV - input binary search tree

653. Sum of Two Numbers IV - Input Binary Search Tree - LeetCode https://leetcode.cn/problems/two-sum-iv-input-is-a-bst/?plan=data-structures&plan_progress= ggfacv7

See the solution: 

Sum of two numbers IV - input BST - sum of two numbers IV - input binary search tree - LeetCode https://leetcode.cn/problems/two-sum-iv-input-is-a-bst/ solution/liang-shu-zhi-he-iv-shu-ru-bst-by-leetco-b4nl/

3. The nearest common ancestor of the binary search tree

235. The nearest common ancestor of a binary search tree - LeetCode https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/?plan=data-structures&plan_progress=ggfacv7

1. Two traversals

Ideas and Algorithms

Note that what is given in the question is a "binary search tree", so we can quickly find a node in the tree and the path from the root node to the node. For example, we need to find node p:

We start traversing from the root node;

If the current node is p, then the node is found successfully;

If the value of the current node is greater than the value of p, it means that p should be in the left subtree of the current node, so the current node is moved to its left child node;

If the value of the current node is less than the value of p, it means that p should be in the right subtree of the current node, so the current node is moved to its right child node.

The same goes for node q. In the process of finding nodes, we can record the nodes we pass by, so that we can get the path from the root node to the node being searched for.

After we get the paths from the root node to p and q respectively, we can easily find their nearest common ancestor. Obviously, the nearest common ancestor of p and q is the "bifurcation point" on their paths from the root node, which is the last identical node. Therefore, if we assume that the path from the root node to p is the array path_p, and the path from the root node to q is the array path_q, then we only need to find the largest number i, which satisfies
path_p[i]=path_q[i]

Then the corresponding node is the "bifurcation point", that is, the nearest common ancestor of p and q is path_p[i] (or path_q[i]).

class Solution {
public:
    vector<TreeNode*> getPath(TreeNode* root, TreeNode* target) {
        vector<TreeNode*> path;
        TreeNode* node = root;
        while (node != target) {
            path.push_back(node);
            if (target->val < node->val) {
                node = node->left;
            }
            else {
                node = node->right;
            }
        }
        path.push_back(node);
        return path;
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        vector<TreeNode*> path_p = getPath(root, p);
        vector<TreeNode*> path_q = getPath(root, q);
        TreeNode* ancestor;
        for (int i = 0; i < path_p.size() && i < path_q.size(); ++i) {
            if (path_p[i] == path_q[i]) {
                ancestor = path_p[i];
            }
            else {
                break;
            }
        }
        return ancestor;
    }
};

Complexity analysis

Time complexity: O(n), where nn is the number of nodes in the given binary search tree. The time required by the above code is linearly related to the depth of nodes p and q in the tree, and in the worst case, the tree presents a chain structure, one p and q is the only leaf node of the tree, and the other is the leaf node. The parent node of , the time complexity at this time is Θ(n).

Space complexity: O(n), we need to store the path from the root node to p and q. The same as the above analysis method, in the worst case, the length of the path is Θ(n), so Θ(n) space is required.

2. One traversal


Ideas and Algorithms

In method one, we start from the root node and find the path to nodes p and q through traversal, which requires a total of two traversals. We can also consider traversing these two nodes together.

The overall traversal process is similar to that in method one:

We start traversing from the root node;

If the value of the current node is greater than the value of p and q, it means that p and q should be in the left subtree of the current node, so the current node is moved to its left child node;

If the value of the current node is less than the value of p and q, it means that p and q should be in the right subtree of the current node, so the current node is moved to its right child node;

If the value of the current node does not meet the above two requirements, then the current node is a "bifurcation point". At this time, p and q are either in different subtrees of the current node, or one of them is the current node.

It can be found that if we traverse these two nodes together, we save the space required to store the path.

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        TreeNode* ancestor = root;
        while (true) {
            if (p->val < ancestor->val && q->val < ancestor->val) {
                ancestor = ancestor->left;
            }
            else if (p->val > ancestor->val && q->val > ancestor->val) {
                ancestor = ancestor->right;
            }
            else {
                break;
            }
        }
        return ancestor;
    }
};

Complexity analysis

  • Time complexity: O(n), where n is the number of nodes in a given binary search tree. The analysis idea is the same as method one.

  • Space complexity: O(1).

Guess you like

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