LeetCode 二叉树问题

@


构建二叉查找树

对于每个父节点,其左子树中所有节点的值小于等于父结点的值,其右子树中所有节点的值大于等于父结点的值,因此对于一个二叉查找树,我们可以在$O(nlogn)$ 的时间内查找一个值是否存在:从根节点开始,若查找值小于当前节点的值则向左下走,若查找值大于当前节点的值则向右下走。

二叉查找树的中序遍历结果即为排序好的数组

  • 二叉查找树的实现:
    • 清空操作:
      • 当前节点为空:返回空指针
      • 当前节点不为空:递归清空左、右子树,删除当前节点,返回空指针
    • 插入操作:
      • 当前节点为空:直接插入元素,并令其左、右子节点为空
      • 当前节点不为空:
        • 插入值小于当前节点的值:递归插入左子树
        • 插入值大于当前节点的值:递归插入右子树
        • 返回当前节点
    • 查找操作:
      • 当前节点为空: 返回空指针
      • 当前节点不为空:
        • 查找值小于当前节点的值:返回递归查找左子树的结果
        • 查找值大于当前节点的值:返回递归查找右子树的结果
        • 查找值等于当前节点的值:返回当前节点
    • 删除操作:
      • 当前节点为空:返回空指针
      • 当前节点不为空:
        • 删除值小于当前节点的值:令左子树等于递归删除左子树的结果
        • 删除值大于当前节点的值:令右子树等于递归删除右子树的结果
        • 删除值等于当前节点的值:
          • 左、右子节点不为空:找到右子树的最小值,令当前节点的值等于该最小值,令右子树等于递归删除右子树最小值节点的结果
          • 左、右子节点至少一个为空:左子树为空,令当前节点等于右子树;右子树为空,令当前节点等于左子树
template <class T>
class BST {
    struct Node {
        T data;
        Node* left;
        Node* right;
    };
    Node* root;
    Node* makeEmpty(Node* t) { // 清空操作
        if (t == NULL)
            return NULL;
        makeEmpty(t->left);
        makeEmpty(t->right);
        delete t;
        return NULL;
    }
    Node* insert(Node* t, T x) { // 插入操作
        if (t == NULL) {
            t = new Node;
            t->data = x;
            t->left = t->right = NULL;
        } else if (x < t->data) {
            t->left = insert(t->left, x);
        } else if (x > t->data) {
            t->right = insert(t->right, x);
        }
        return t;
    }
    Node* find(Node* t, T x) { // 查找操作
        if (t == NULL)
            return NULL;
        if (x < t->data)
            return find(t->left, x);
        if (x > t->data)
            return find(t->right, x);
        return t;
    }
    Node* findMin(Node* t) {
        if (t == NULL || t->left == NULL)
            return t;
        return findMin(t->left);
    }
    Node* findMax(Node* t) {
        if (t == NULL || t->right == NULL)
            return t;
        return findMax(t->right);
    }
    Node* remove(Node* t, T x) { // 删除操作
        Node* temp;
        if (t == NULL)
            return NULL;
        else if (x < t->data)
            t->left = remove(t->left, x);
        else if (x > t->data)
            t->right = remove(t->right, x);
        else if (t->left && t->right) {
            temp = findMin(t->right);
            t->data = temp->data;
            t->right = remove(t->right, t->data);
        } else {
            temp = t;
            if (t->left == NULL)
                t = t->right;
            else if (t->right == NULL)
                t = t->left;
            delete temp;
        }
        return t;
    }

   public:
    BST()
        : root(NULL) {}
    ~BST() {
        root = makeEmpty(root);
    }
    void insert(T x) {
        insert(root, x);
    }
    void remove(T x) {
        remove(root, x);
    }
};

1. 遍历问题

105. 从前序与中序遍历序列构造二叉树

给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点。

  • 思路分析:
    • preorder 的第一个数一定是整个二叉树的根节点
    • preorder 的第一个数在 inorder 中的索引可以将 inorder 分为左右两部分,分别为左右子树
    • 左右子树继续根据性质递归划分

在这里插入图片描述
参考题解

  • 具体实现:
    • 建立哈希表存储 inorder 中节点值与其索引的映射
    • 假设根节点的在 inorder 的索引为 index
      • 左子树的节点数为 leftLen = (index - 1) - s0 + 1
      • 左子树的根节点在前序遍历数组的索引为 s1 + 1
      • 右子树的根节点在前序遍历数组的索引为 s1 + leftLen + 1
  • 参数说明:
    • s0 树在中序遍历的起始索引
    • e0 树在中序遍历的终止索引
    • s1 树的根节点在前序遍历数组的索引
    • s0、e0为子树在中序遍历结果中的起止索引,s1为子树根节点在前序遍历中的索引,前序遍历只起到提供子树根节点值的作用
class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if (preorder.empty()) {
            return nullptr;
        }
        unordered_map<int, int> hash;
        // 建立中序遍历结果中节点值与索引的哈希映射
        for (int i = 0; i < preorder.size(); i++) {
            hash[inorder[i]] = i;
        }
        return helper(hash, preorder, 0, preorder.size() - 1, 0);
    }
    TreeNode* helper(unordered_map<int, int>& hash, vector<int>& preorder, int s0, int e0, int s1) {
        if (s0 > e0) {
            return nullptr;
        }
        int mid = preorder[s1], index = hash[mid], leftLen = index - 1 - s0 + 1;
        TreeNode *node = new TreeNode(mid);
        node->left = helper(hash, preorder, s0, index - 1, s1 + 1); // 构建左子树
        node->right = helper(hash, preorder, index + 1, e0, s1 + leftLen + 1); // 构建右子树
        return node;
    }
};
  • 简洁实现法
    • 通过构建类的变量存储哈希映射
    • 根据前序遍历结果确定根节点的索引 root
    • 根据根节点索引 root 以及中序遍历的哈希映射,确定树的左、右边界 left right
class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        this->preorder = preorder;
        for (int i = 0; i < inorder.size(); i++) {
            hash[inorder[i]] = i;
        }
        return helper(0, 0, inorder.size() - 1);
    }

private:
    vector<int> preorder;
    unordered_map<int, int> hash;
    TreeNode* helper(int root, int left, int right) {
        if (left > right) {
            return nullptr;
        }
        int rootval = preorder[root];
        int m = hash[rootval]; // 根节点在中序遍历的索引
        TreeNode* node = new TreeNode(rootval);
        node->left = helper(root + 1, left, m - 1); // 左子树的左边界一直为 left = 0
        node->right = helper(root + m - left + 1, m + 1, right); // 右子树的右边界一直味 right = size() - 1
        return node;
    }
};

124. 二叉树中的最大路径和

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中至多出现一次 。该路径至少包含一个节点,且不一定经过根节点。路径和 是路径中各节点值的总和。Link

  • 递归求解
  • 当前节点的最大值为 val + leftGain + rightGain
  • 当前节点向上一节点的返回值为 val + max(leftGain,rightGain)
    • 因为同一节点不能经过两次,因此必须选择当前节点左、右子树中较大的一个进行返回
    • 例如,节点 11 的最大值为 20,但其向上返回的最大值为 7 + 11 = 18,因此一个节点只能出现一次,而要同时访问 7 2 必须经过 11 两次

二叉树实例

class Solution {
private:
    int maxSum = INT_MIN;
public:
    int maxPathSum(TreeNode* root) {
        maxGain(root);
        return maxSum;
    }
    int maxGain(TreeNode* root) {
        if (!root) return 0;
        int leftGain = max(maxGain(root->left), 0);
        int rightGain = max(maxGain(root->right), 0);
        int innerMax = root->val + leftGain + rightGain; // 以当前节点为根节点的最大值
        maxSum = max(maxSum, innerMax);
        return root->val + max(leftGain, rightGain); // 经过当前节点的最大增益
    }
};

2. 二叉查找树

99. 恢复二叉搜索树

给你二叉搜索树的根节点 root ,该树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。

  • 递归思想:
    • 终止条件:节点为空
    • 递归操作:
      • 中序遍历:设一个指针指向中序遍历的前一个节点,与当前节点的值做对比,如果小于前一个节点,说明排序错误
      • 技巧: 整个遍历过程只出现一次排序错误,交换两个相邻节点;出现两次排序错误,交换这两个节点
      • 节点为空时不进行操作直接返回上一级递归
class Solution {
public:
    void recoverTree(TreeNode* root) {
        TreeNode *mistake1 = nullptr, *mistake2 = nullptr, *prev = nullptr;
        inorder(root, mistake1, mistake2, prev);
        if (mistake1 && mistake2) {
	        // 将错误节点的值暂存
            int temp = mistake1->val;
            mistake1->val = mistake2->val;
            mistake2->val = temp;
        }
    }
    void inorder(TreeNode* root, TreeNode* &mistake1, TreeNode* &mistake2, TreeNode* &prev) {
        if (!root) {
            return;
        }
        if (root->left) {
            inorder(root->left, mistake1, mistake2, prev); // 中序遍历左子节点
        }
        if (prev && root->val < prev->val) {
            if (!mistake1) {
                mistake1 = prev; // 一次排序错误
                mistake2 = root;
            }else {
                mistake2 = root; // 两次排序错误
            }
        }
        prev = root; // 访问根节点
        if (root->right) {
            inorder(root->right, mistake1, mistake2, prev); // 中序遍历右子节点
        }
    }
};

669. 修剪二叉搜索树

给你二叉搜索树的根节点 root ,同时给定最小边界 low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树不应该改变保留在树中的元素的相对结构(即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在唯一的答案。

  • 递归思想:
    • 终止条件:当前节点为空
    • 递归操作:
      • 当前节点小于 low,返回其右子树的修剪结果
      • 当前节点大于 high,返回其左子树的修剪结果
      • 当前节点值在[low, high]中,左、右子节点等于其对应的修剪结果
    • 返回值:根据当前节点与边界值的大小返回对应的节点指针
class Solution {
public:
    TreeNode* trimBST(TreeNode* root, int low, int high) {
		// 终止条件
        if (!root) {
            return root;
        }
        // 当前节点值大于 high,则右子树均不满足条件
        if (root->val > high) {
            return trimBST(root->left, low, high);
        }
        if (root->val < low) {
            return trimBST(root->right, low, high);
        }
        // 当前节点值在 [low, high]中
        root->left = trimBST(root->left, low, high);
        root->right = trimBST(root->right, low, high);
        return root;
    }
};

3. 字典树

208. 实现 Trie (前缀树)

尝试建立一个字典树,支持快速插入单词、查找单词、查找单词前缀的功能。

  • 一个字符相当于一个字典树的节点,其含有 26 个子节点指针指向下一个字符
  • 单词最后一个字符的节点,设置一个 true 标志着单词的结束
  • 不同单词前 N 个相同的字符可以共用该字符节点

  • 直接利用类建立前缀树
  • 1、建立一个指针指向当前节点
  • 2、根据指针查找单词是否存在
  • 3、根据查找情况建立新的节点
  • 4、将指针指向下一个字符节点
class Trie {
public:
    bool isEnd;
    vector<Trie *> next;
    Trie() {
        isEnd = false;
        for(int i = 0; i < 26; i++) {
            next.push_back(nullptr);
        }
    }
    
    void insert(string word) {
        Trie *cur = this; // 重要位置
        for (char &c : word) {
            if (cur->next[c - 'a'] == nullptr) {
                cur->next[c - 'a'] = new Trie();
            }
            cur = cur->next[c - 'a'];
        }
        cur->isEnd = true;
    }
    
    bool search(string word) {
        Trie *cur = this;
        for (char &c : word) {
            if (cur->next[c - 'a'] == nullptr) {
                return false;
            }
            cur = cur->next[c - 'a'];
        }
        return cur->isEnd;
    }
    
    bool startsWith(string prefix) {
        Trie *cur = this;
        for (char &c : prefix) {
            if (cur->next[c - 'a'] == nullptr) {
                return false;
            }
            cur = cur->next[c - 'a'];
        }
        return true;
    }
};

posted @ 2023-02-15 21:16  GreyWang  阅读(8)  评论(0编辑  收藏  举报