数据结构——树(04二叉树,二叉搜索树专项,代码练习)

文章目录

一、概念

二叉搜索树(Binary Search Tree,简称 BST)是一种特殊的二叉树,其核心特性围绕 "节点值的有序性" 展开。

定义性特征:对于二叉搜索树中的任意节点,满足:

  • 左子树所有节点的值 < 当前节点的值

  • 右子树所有节点的值 > 当前节点的值

  • 左、右子树本身也必须是二叉搜索树(递归满足上述条件)。

  • 标准 BST 中通常不允许存在值相等的节点。

    衍生特征:

  • 中序遍历结果为升序序列,得到的节点值序列是严格递增的。这是 BST 最常用的特性之一,可用于验证一棵树是否为 BST,或通过升序序列反向构造 BST。

  • 树中最小值一定是最左侧的叶子节点(一路向左子树遍历,直到无左孩子);
    树中最大值一定是最右侧的叶子节点(一路向右子树遍历,直到无右孩子)。

  • 查找目标值target的过程类似二分查找:若当前节点值等于target,找到目标;若target小于当前节点值,递归查找左子树;若target大于当前节点值,递归查找右子树。时间复杂度为O(log n),最坏情况(退化为链表)为O(n)。

  • 平衡二叉树是 BST 的进阶,通过额外机制保证左右子树高度差不超过 1,避免了 BST 在极端情况下退化为链表。

二、构造

序号 题目 链接
1 先序序列构造 https://leetcode.cn/problems/construct-binary-search-tree-from-preorder-traversal/description/?envType=problem-list-v2\&envId=tree
2 中序序列构造 https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/?envType=problem-list-v2\&envId=tree
3 中序有序链表构造 https://leetcode.cn/problems/convert-sorted-list-to-binary-search-tree/?envType=problem-list-v2\&envId=tree
4 BST转换为中序有序链表 https://leetcode.cn/problems/increasing-order-search-tree/description/?envType=problem-list-v2\&envId=tree
5 BST的序列化和反序列化 https://leetcode.cn/problems/serialize-and-deserialize-bst/description/?envType=problem-list-v2\&envId=tree
1 BST的种数1【总数】 https://leetcode.cn/problems/unique-binary-search-trees/description/?envType=problem-list-v2\&envId=tree
2 BST的种数2【列举】 https://leetcode.cn/problems/unique-binary-search-trees-ii/solutions/339143/bu-tong-de-er-cha-sou-suo-shu-ii-by-leetcode-solut/?envType=problem-list-v2\&envId=tree

1.1先序序列 构造BST

先序序列的特点是:根左右。结合BST的特性可知:

  • 序列的第一个元素一定是根节点
  • 根【左(比根小)】【右(比根大)】。根节点后面的元素,只要比第一个元素小的都是BST的左子树,右子树同理。
cpp 复制代码
TreeNode* build(vector<int>& pre,int start,int end){
	  //递归的出口
	  if (start > end) return nullptr;
	  //根节点
	  int rootVal=pre[start];
	  TreeNode* root=new TreeNode(rootVal);
	  // 找到左子树与右子树的分割点(第一个大于根节点值的位置为右子树的第一个元素)
	  int index;
	  for(index=start+1;index<=end;index++){
		if(pre[index]>rootVal) break;
	}
	//递归构造左子树和右子树
	root->left = build(pre, start + 1, index - 1);
	root->right = build(pre, index, end);
	return root;
}

1.2中序序列 转换为BST

与先序序列转换一样。中序序列的特点是:左根右。结合BST的性质。

  • 序列的中间元素为树的根节点。
  • 【左】根【右】。在根节点左边的为左子树,根节点右边的为右子树(递归构造)
cpp 复制代码
TreeNode* build(vector<int>& nums,int start,int end){
    if(start>end) return nullptr;

    int mid=(start+end)/2;
    int rootVal=nums[mid];
    TreeNode* root=new TreeNode(rootVal);

    root->left=build(nums,start,mid-1);
    root->right=build(nums,mid+1,end);

    return root;
}

1.3中序序列链表转换为BST

有序链表转换成二叉搜索树的原理同 中序遍历转换为二叉树。

cpp 复制代码
TreeNode* build(ListNode* head,int left,int right){
    if(left>right) return nullptr;
    int mid=(left+right)/2;
    //访问链表在mid处的值 p指向的是根节点
    ListNode* p=head;
    for(int i=1;i<=mid;i++){
        p=p->next;
    }
    TreeNode* root=new TreeNode(p->val);
    //递归左右子树
    root->left=build(head,left,mid-1);
    root->right=build(head,mid+1,right);
    return root;
}

1.4BST转换为中序序列链表

  • 将树按照中序遍历,遍历结果放在res中。为中序序列。
  • 按照res构建链表。【next变成right】
cpp 复制代码
void dfs(TreeNode* root, vector<int>& result){
    if(root==nullptr) return ;
    dfs(root->left,result);
    result.push_back(root->val);
    dfs(root->right,result);
}
TreeNode* increasingBST(TreeNode* root) {
    vector<int> res;
    dfs(root,res);

    TreeNode* dummy=new TreeNode(-1);
    TreeNode* p=dummy;
    for(int val:res){
        p->right=new TreeNode(val);
        p=p->right;
    }
    return dummy->right;
}

1.7BST的序列化和反序列化

  • 序列化:BST先序遍历------遍历结果字符串
  • 反序列化:遍历结果字符串------BST
cpp 复制代码
// 前序遍历:根->左->右,将节点值拼接为字符串(用逗号分隔)
void preorder(TreeNode* node, string& result) {
    if (node == nullptr) return;
    // 拼接当前节点值
    if (!result.empty()) {
        result += ",";
    }
    result += to_string(node->val);
    // 递归遍历左右子树
    preorder(node->left, result);
    preorder(node->right, result);
}

// 参考【先序序列构造BST】
TreeNode* buildBST(vector<int>& values, int start, int end) {}

// Encodes a tree to a single string.
string serialize(TreeNode* root) {
    string result;
    preorder(root, result);
    return result;
}

// Decodes your encoded data to tree.
TreeNode* deserialize(string data) {
    if (data.empty()) return nullptr;
    // 将字符串分割为整数列表(前序遍历结果)
    vector<int> values;
    stringstream ss(data);
    string item;
    while (getline(ss, item, ',')) {
        values.push_back(stoi(item));
    }
    // 根据前序遍历结果构建BST
    return buildBST(values, 0, values.size() - 1);
}

1.6BST的种数

  • 对于 i 个节点,我们可以选择 j(1 ≤ j ≤ i)作为根节点
  • 此时左子树有 j-1 个节点,右子树有 i-j 个节点
  • 以 j 为根的二叉搜索树数量 = 左子树数量 × 右子树数量,即 dp[j-1] × dp[i-j]
  • 对所有可能的根节点求和,得到 dp[i]
cpp 复制代码
int numTrees(int n) {
    vector<int> dp(n+1,0);
    dp[0]=1;
    dp[1]=1;
    for(int i=2;i<=n;i++){
        for(int j=1;j<=i;j++){
            dp[i] += dp[j-1] * dp[i-j];
        }
    }
    return dp[n];
}

上述题目是BST有几种可能,下列代码是BST的每一种可能都列举出来。

cpp 复制代码
// 生成[start, end]范围内所有可能的BST
vector<TreeNode*> generate(int start, int end) {
    vector<TreeNode*> trees;
    // 递归终止条件:区间为空,返回空节点
    if (start > end) {
        trees.push_back(nullptr);
        return trees;
    }
    // 尝试以每个数作为根节点
    for (int i = start; i <= end; ++i) {
        // 生成左右子树
        vector<TreeNode*> leftTrees = generate(start, i - 1);
        vector<TreeNode*> rightTrees = generate(i + 1, end);
        // 组合左子树和右子树,形成以i为根的树
        for (TreeNode* left : leftTrees) {
            for (TreeNode* right : rightTrees) {
                TreeNode* root = new TreeNode(i);
                root->left = left;
                root->right = right;
                trees.push_back(root);
            }
        }
    }
    return trees;
}
vector<TreeNode*> generateTrees(int n) {
    if (n == 0) return {};
    return generate(1, n);
}

二、BST的增删改查

序号 题目 链接
1 验证BST是否有效【中序序列】 https://leetcode.cn/problems/validate-binary-search-tree/solutions/2020306/qian-xu-zhong-xu-hou-xu-san-chong-fang-f-yxvh/?envType=problem-list-v2\&envId=tree
2 查找值为val的节点 https://leetcode.cn/problems/search-in-a-binary-search-tree/?envType=problem-list-v2\&envId=tree
3 插入1个节点 https://leetcode.cn/problems/insert-into-a-binary-search-tree/description/?envType=problem-list-v2\&envId=tree
4 删除1个节点 https://leetcode.cn/problems/delete-node-in-a-bst/description/?envType=problem-list-v2\&envId=tree
5 恢复BST https://leetcode.cn/problems/recover-binary-search-tree/description/?envType=problem-list-v2\&envId=tree
6 修剪BST https://leetcode.cn/problems/trim-a-binary-search-tree/description/?envType=problem-list-v2\&envId=tree
7 平衡化 https://leetcode.cn/problems/balance-a-binary-search-tree/description/?envType=problem-list-v2\&envId=tree

2.1验证是否为BST

验证所给的树是否为有效的二叉搜索树

二叉搜索树的中序遍历是有序的,且是升序的。【数组 or 递归 or 非递归】

  • 解法1:将中序遍历的结果放在数组中。比较【当前节点】与【当前节点的前一个节点】
  • 解法2:pre 记录中序遍历序列中 "当前节点的前一个节点"。比较当前节点与 pre 的值。
cpp 复制代码
bool isValidBST(TreeNode* root) {
    vector<int> inorder;
    // 中序遍历
    inorderTraversal(root, inorder);
    // 检查是否严格递增
    for (int i = 0; i < inorder.size()-1; i++) {
    	// 注意:必须严格小于,不能等于
        if (inorder[i+1] <= inorder[i]) return 0;
    }
    return 1;
}
cpp 复制代码
TreeNode* pre=nullptr;
bool isValidBST(TreeNode* root) {
    if(root==nullptr) return 1;
    if(isValidBST(root->left)==false)  return 0;  //左
    if(pre!=nullptr && root->val <= pre->val){    //中
        return 0;
    }
    pre=root;
    return isValidBST(root->right);              //右
}

2.2查找值为val的节点

二叉树的特性就是:左小右大。因此想在二叉树中进行搜索某个值,可以进行比较。

  • 如果val小于当前节点值------左子树中继续查找
  • 如果val等于当前节点值------查找成功
  • 如果val大于当前节点值------右子树中继续查找
cpp 复制代码
TreeNode* searchBST(TreeNode* root, int val) {
        if(root==nullptr) return nullptr;
        if(root->val==val) return root;
        else if(val<root->val) return searchBST(root->left,val);
        else if(val>root->val) return searchBST(root->right,val);
        return nullptr;
    }

2.3插入一个值为val的节点

BST的性质:递归处理左子树和右子树

cpp 复制代码
TreeNode* insertIntoBST(TreeNode* root, int val) {
    if (root == nullptr)  return new TreeNode(val);
    if (val < root->val)  root->left = insertIntoBST(root->left, val);
    else  root->right = insertIntoBST(root->right, val);
    return root;
}

2.4删除一个值为val的节点

cpp 复制代码
TreeNode* deleteNode(TreeNode* root, int key) {
    if(root==nullptr) return nullptr;
    
    // 查找目标节点
    if(key < root->val) root->left=deleteNode(root->left,key);
    else if(key > root->val) root->right=deleteNode(root->right,key);
    else{
        // 情况1:叶子节点,直接删除
        if(root->left==nullptr && root->right==nullptr){
            delete root;
            return nullptr;
        }
        // 情况2:只有右子树(修正指针指向)
        else if(root->left==nullptr){
            TreeNode* temp=root->right;  
            delete root;
            return temp;
        }
        // 情况3:只有左子树(修正指针指向)
        else if(root->right==nullptr){
            TreeNode* temp=root->left;  
            delete root;
            return temp;
        }
        // 情况4:有左右子树
        else{
            // 找到右子树最小节点
            TreeNode* minNode=root->right;
            while(minNode->left!=nullptr){
                minNode=minNode->left;
            }
            // 替换当前节点值
            root->val=minNode->val;
            root->right=deleteNode(root->right, minNode->val);
        }
    }
    return root;
}

2.5恢复错误的两节点

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

cpp 复制代码
// 中序遍历
void inorder(TreeNode* root) {
    if (root == nullptr) return;
    inorder(root->left); // 遍历左子树
    if (prev != nullptr && root->val < prev->val) {
        // 第一次发现逆序对,记录前一个节点
        if (first == nullptr) first = prev;
        // 第二次发现逆序对(或相邻交换的情况),记录当前节点
        second = root;
    }
    prev = root;  // 更新前一个节点
    inorder(root->right); // 遍历右子树
}
void recoverTree(TreeNode* root) {
    inorder(root);
    swap(first->val, second->val);
}

2.6修剪BST

给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 。

  • 若当前节点值 < low:左子树所有值都 < low,直接用右子树的修剪结果替换当前节点。
  • 若当前节点值 > high:右子树所有值都 > high,直接用左子树的修剪结果替换当前节点。
  • 若当前节点值在范围内:保留该节点,递归修剪其左右子树并重新连接。
cpp 复制代码
TreeNode* trimBST(TreeNode* root, int low, int high) {
    if(root==nullptr) return nullptr;
    if(root->val < low) return trimBST(root->right,low,high);
    if(root->val > high) return trimBST(root->left,low,high);
    root->left=trimBST(root->left,low,high);
    root->right=trimBST(root->right,low,high);
    return root;
}

2.7平衡化

  • 对原 BST 进行中序遍历,得到一个有序的节点值序列【参考树的中序遍历】
  • 利用有序序列构建平衡 BST------ 通过选择序列的中间元素作为根节点,左侧元素构建左子树,右侧元素构建右子树,确保左右子树高度差不超过 1。【参考中序序列构造BST】
cpp 复制代码
// 中序遍历BST,获取有序序列
void inorder(TreeNode* node, vector<int>& values) {
    if (node == nullptr) return;
    inorder(node->left, values);
    values.push_back(node->val);
    inorder(node->right, values);
}
// 从有序数组的[left, right]范围构建平衡BST
TreeNode* buildBalancedBST(vector<int>& values, int left, int right) {
    if (left > right) return nullptr;
    //选择中间元素作为根节点
    int mid = left + (right - left) / 2;
    TreeNode* node = new TreeNode(values[mid]);
    // 递归构建左右子树
    node->left = buildBalancedBST(values, left, mid - 1);
    node->right = buildBalancedBST(values, mid + 1, right);
    
    return node;
}
TreeNode* balanceBST(TreeNode* root) {
    vector<int> values;
    inorder(root, values);
    return buildBalancedBST(values, 0, values.size() - 1);
}

三、BST的其他问题

序号 题目 链接
1 BST第K小的元素【中序第K元素】 https://leetcode.cn/problems/kth-smallest-element-in-a-bst/description/?envType=problem-list-v2\&envId=tree
2 BST的众数【哈希表】 https://leetcode.cn/problems/find-mode-in-binary-search-tree/description/?envType=problem-list-v2\&envId=tree
3 BST中的两数之和是否为target https://leetcode.cn/problems/two-sum-iv-input-is-a-bst/description/?envType=problem-list-v2\&envId=tree
4 BST给定范围的节点之和 https://leetcode.cn/problems/range-sum-of-bst/description/?envType=problem-list-v2\&envId=tree
5 BST节点最小距离 https://leetcode.cn/problems/minimum-distance-between-bst-nodes/description/?envType=problem-list-v2\&envId=tree
6 BST的最小绝对差【同上】 https://leetcode.cn/problems/minimum-absolute-difference-in-bst/description/?envType=problem-list-v2\&envId=tree
7 二叉搜索树转换为累加树 https://leetcode.cn/problems/convert-bst-to-greater-tree/description/?envType=problem-list-v2\&envId=tree
8 BST的最近公共祖先【同二叉树】 https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/description/?envType=problem-list-v2\&envId=tree
9 最近节点查询 https://leetcode.cn/problems/closest-nodes-queries-in-a-binary-search-tree/description/?envType=problem-list-v2\&envId=tree

3.1第K小的元素

BST的中序序列是升序序列。因此中序遍历遍历整个树,同时计数,遍历到第K个的时候返回值。

cpp 复制代码
int count = 0;  // 记录当前遍历到第几个元素
int result = 0; // 存储第k小的元素值
// 中序遍历(左→根→右)
void inorder(TreeNode* root, int k) {
    if (root == nullptr) return;        
    inorder(root->left, k);// 先遍历左子树
    // 处理当前节点:计数+1,若达到k则记录结果
    count++;
    if (count == k) {
        result = root->val;
        return; // 找到后可提前返回,无需继续遍历
    }
    inorder(root->right, k);// 再遍历右子树
}
int kthSmallest(TreeNode* root, int k) {
    inorder(root, k);
    return result;
}

3.2众数

  • 方法1(对于普通的二叉树同样适用):遍历整个二叉树,在遍历过程中,用哈希表记录每个元素的个数,然后找到元素出现最多的次数记录下来,并将该元素返回给结果。
  • 方法2:搜索树的中序遍历是有序序列。利用这一个性质,
cpp 复制代码
class Solution {
public:
    unordered_map<int,int> M;
    //遍历并统计个数
    void f(TreeNode* root){
        if(root==nullptr) return;
        M[root->val]++;
        f(root->left);
        f(root->right);
    }
    vector<int> findMode(TreeNode* root) {
        vector<int> result;
        int count=1;
        if(root==nullptr) return result;
        f(root);
        //找到最大值
        for(auto m:M){
            if(m.second>count) count=m.second;
        }
        //填入result
        for(auto m:M){
            if(m.second==count) result.push_back(m.first);
        }
        return result;

    }
};

3.3两数之和

给定一个二叉搜索树 root 和一个目标结果 k,如果二叉搜索树中存在两个元素且它们的和等于给定的目标结果,则返回 true。

  • BST按照中序遍历,将结果存储在result中。
  • 利用双指针快速定位目标。
cpp 复制代码
vector<int> result;
void dfs(TreeNode* root){
    if(root==nullptr) return ;
    dfs(root->left);
    result.push_back(root->val);
    dfs(root->right);
}
bool findTarget(TreeNode* root, int k) {
    dfs(root);
    //利用双指针,快速定位目标值
    int left=0;
    int right=result.size()-1;
    while(left<right){
        int sum=result[left]+result[right];
        if(sum==k) return 1;
        else if(sum<k) left++;
        else right--;
    }
    return 0;
}

3.4给定范围的节点之和

给定二叉搜索树的根结点 root,返回值位于范围 [low, high] 之间的所有结点的值的和。

依然用BST的中序遍历解决问题

cpp 复制代码
int rangeSumBST(TreeNode* root, int low, int high) {
    if(root==nullptr) return 0;
    result.clear();
    dfs(root);
    int sum=0;
    for(int i=0;i<result.size();i++){
        if(result[i]>=low && result[i]<=high ) sum+=result[i];
    }
    return sum;
}

3.5节点最小距离

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。

转换为中序有序序列之后计算。由于中序序列是有序的,因此离得越近,差值越小。

cpp 复制代码
int minDiffInBST(TreeNode* root) {
    if(root==nullptr) return 0;
    result.clear();
    dfs(root);
    int m=INT_MAX;
    for(int i=0;i<result.size()-1;i++){
        m=min(m,result[i+1]-result[i]);
    }
    return m;
    
}

3.6二叉搜索树转换为累加树

  • 二叉搜索树的中序遍历是一个单调递增的有序序列。如果我们反序地中序遍历该二叉搜索树,即可得到一个单调递减的有序序列。
  • 只需要反序中序遍历该二叉搜索树,记录过程中的节点值之和,并不断更新当前遍历到的节点的节点值,即可得到题目要求的累加树。
cpp 复制代码
int sum=0;
TreeNode* convertBST(TreeNode* root) {
    if(root==nullptr) return root;
    convertBST(root->right);
    sum+=root->val;
    root->val=sum;
    convertBST(root->left);
    return root;
}

3.7最近结果查询

给你一个 二叉搜索树 的根节点 root ,和一个由正整数组成、长度为 n 的数组 queries 。

请你找出一个长度为 n 的 二维 答案数组 answer ,其中 answer[i] = [mini, maxi] :

  • mini 是树中小于等于 queries[i] 的 最大值 。如果不存在这样的值,则使用 -1 代替。
  • maxi 是树中大于等于 queries[i] 的 最小值 。如果不存在这样的值,则使用 -1 代替。
    返回数组 answer 。
  • lower_bound 是 C++ 标准库 头文件中的一个二分查找函数,用于在有序序列中查找第一个大于等于目标值的元素,返回该元素的迭代器
  • upper_bound:找第一个 > 目标值的元素(不包含等于的情况)。

中序遍历序列为:[1,2,3,4,6,9,10]

查询 q=5:

upper_bound找第一个 > 5 的元素是 6,前一个元素4→mini=4。

lower_bound找第一个≥5 的元素是 6→maxi=6。

结果为[4,6]。

cpp 复制代码
// 中序遍历BST,生成升序序列
void inorder(TreeNode* node, vector<int>& seq) {
    if (node == nullptr) return;
    inorder(node->left, seq);
    seq.push_back(node->val);
    inorder(node->right, seq);
}
vector<vector<int>> closestNodes(TreeNode* root, vector<int>& queries) {
    // 1.中序序列
    vector<int> inorderSeq;
    inorder(root, inorderSeq);
    vector<vector<int>> answer;
    for (int q : queries) {
        // 2. 对每个查询,在有序序列中用二分查找找mini和maxi
        int mini = -1, maxi = -1;
        // 找mini:小于等于q的最大值(最后一个 <= q 的元素)
        auto it_mini = upper_bound(inorderSeq.begin(), inorderSeq.end(), q);
        if (it_mini != inorderSeq.begin()) {
            mini = *prev(it_mini);
        }
        // 找maxi:大于等于q的最小值(第一个 >= q 的元素)
        auto it_maxi = lower_bound(inorderSeq.begin(), inorderSeq.end(), q);
        if (it_maxi != inorderSeq.end()) {
            maxi = *it_maxi;
        }
        answer.push_back({mini, maxi});
    }
    return answer;
}
相关推荐
Elylicery6 小时前
【职业】算法与数据结构专题
数据结构·算法
雷达学弱狗7 小时前
广度优先搜索(BFS, Breadth-First Search)
数据结构·算法·宽度优先
桦说编程8 小时前
数据丢失,而且不抛出并发异常,多线程使用HashMap踩坑
java·数据结构·后端
闪电麦坤9510 小时前
数据结构:选择排序 (Selection Sort)
数据结构·算法·排序算法
LaoZhangGong12310 小时前
MQTT报文的数据结构
c语言·网络·数据结构·mqtt·w5500
胡萝卜3.010 小时前
【LeetCode&牛客&数据结构】单链表的应用
数据结构·学习·算法·leetcode·单链表
睡不醒的kun12 小时前
leetcode算法刷题的第二十四天
数据结构·c++·算法·leetcode·职场和发展·贪心算法
古译汉书13 小时前
蓝桥杯算法之基础知识(5)
数据结构·算法·蓝桥杯
歪歪10014 小时前
SQL Server 数据库创建与用户权限绑定
大数据·数据结构·数据库·sql·oracle·sqlserver·数据库开发