当前位置: 首页 > news >正文

二叉树与二叉搜索树(BST):从基础到应用

一、树的基本概念:从"一对多"到层次结构

什么是树?

树是一种非线性数据结构,它像自然界的树一样有"根"和"分支"。简单说,树是由n个节点组成的集合,满足:

  • 有且仅有一个根节点(没有"上级"节点)
  • 除根节点外,每个节点有且仅有一个"上级"(直接前驱)
  • 每个节点可以有0个或多个"下级"(直接后继)

这种"一对多"的关系,让树天然适合表达层次结构

生活中的树结构

树状结构在生活中无处不在,比如:

  • 公司架构:CEO(根)→ 部门经理(中层)→ 员工(叶子)
  • 文件夹系统:根目录→子文件夹→文件
  • 家谱:祖先(根)→ 父母→子女→孙辈
  • 足球世界杯:决赛(顶层)→ 半决赛→四分之一决赛→小组赛

树的核心术语(附通俗解释)

为了统一描述,我们需要明确这些术语:

术语通俗解释例子
根节点(Root)树的"起点",没有上级公司的CEO
双亲节点(Parent)某节点的直接上级你的直属领导
孩子节点(Child)某节点的直接下级你管理的团队成员
层次(Level)节点所在的"深度",根为第1层CEO是1层,经理是2层,员工是3层
度(Degree)一个节点有多少个直接下级经理带3个员工,度就是3
叶子节点(Leaf)没有下级的节点(度为0)基层员工、最终文件
树的高度(Height)树的最大层次数从CEO到基层员工共3层,高度就是3

二、二叉树:每个节点最多"分两叉"

什么是二叉树?

二叉树是一种特殊的树,它的每个节点最多有2个孩子(左孩子和右孩子),且左右孩子的顺序不能颠倒(有序树)。

可以理解为:每个节点最多"分两叉",左边一个分支,右边一个分支。

二叉树的3个重要规律

这些规律能帮你快速计算二叉树的节点数量:

  1. 第i层最多有 2^(i-1) 个节点(比如第3层最多8个节点)
  2. 高度为k的二叉树,最多有 2^k - 1 个节点(比如高度3的树最多7个节点)
  3. 叶子节点数(n₀)= 度为2的节点数(n₂)+ 1(数学可证,记住即可)

常见的二叉树类型

根据结构特点,二叉树可分为:

  • 满二叉树:每一层都长满节点(符合"最多节点"规律),比如高度3的树有7个节点
  • 完全二叉树:除最后一层外都长满,最后一层节点从左到右连续排列(像"填格子")
  • 平衡二叉树:任意节点的左右子树高度差≤1(不会"一边倒")
  • 退化二叉树:所有节点都只有左孩子或只有右孩子,变成了"链表"(性能最差)

二叉树的两种存储方式

存储二叉树主要有两种方式,各有优劣:

1. 顺序存储(用数组)

把节点按层次顺序存入数组,通过下标计算节点关系:

  • 根节点存在下标1的位置(方便计算)
  • 下标为n的节点,父节点在下标 n/2
  • 下标为n的节点,左孩子在 2n 处,右孩子在 2n+1

优点:访问快,不用存指针
缺点:非完全二叉树会浪费空间(比如退化树)

2. 链式存储(用指针)

每个节点包含3部分:

// 二叉树节点结构
typedef struct Node {int data;          // 存储的数据struct Node* left; // 左孩子指针(指向左边分支)struct Node* right;// 右孩子指针(指向右边分支)
} Node;

优点:灵活,不浪费空间
缺点:需要额外空间存指针

三、二叉树的遍历:如何"走遍"所有节点?

遍历是指按一定规则访问树中所有节点,且每个节点只访问一次。二叉树有4种核心遍历方式,区别在于访问根节点的时机

1. 前序遍历(根→左→右)

规则:先访问根节点,再遍历左子树,最后遍历右子树。
口诀:“先根,再左,后右”

// 前序遍历函数
void preOrder(Node* root) {if (root == NULL) return; // 空节点,直接返回printf("%d ", root->data); // 1. 先访问根节点preOrder(root->left);      // 2. 遍历左子树preOrder(root->right);     // 3. 遍历右子树
}

2. 中序遍历(左→根→右)

规则:先遍历左子树,再访问根节点,最后遍历右子树。
口诀:“先左,再根,后右”

// 中序遍历函数
void inOrder(Node* root) {if (root == NULL) return; // 空节点,直接返回inOrder(root->left);       // 1. 遍历左子树printf("%d ", root->data); // 2. 访问根节点inOrder(root->right);      // 3. 遍历右子树
}

3. 后序遍历(左→右→根)

规则:先遍历左子树,再遍历右子树,最后访问根节点。
口诀:“先左,再右,后根”

// 后序遍历函数
void postOrder(Node* root) {if (root == NULL) return; // 空节点,直接返回postOrder(root->left);     // 1. 遍历左子树postOrder(root->right);    // 2. 遍历右子树printf("%d ", root->data); // 3. 访问根节点
}

4. 按层遍历(广度优先)

规则:从上到下、从左到右,一层一层访问节点(像"扫楼梯")。
实现:通常用队列辅助,先入队根节点,然后出队时依次入队其左右孩子。

遍历示例(直观理解)

假设有一棵简单二叉树:

    1/ \2   3/ \
4   5
  • 前序遍历:1 → 2 → 4 → 5 → 3
  • 中序遍历:4 → 2 → 5 → 1 → 3
  • 后序遍历:4 → 5 → 2 → 3 → 1
  • 按层遍历:1 → 2 → 3 → 4 → 5

四、二叉搜索树(BST):会"自动排序"的树

什么是BST?

二叉搜索树(Binary Search Tree)是一种特殊的二叉树,它的节点值满足左小右大原则:

  • 任意节点的左子树中,所有节点值都小于该节点值
  • 任意节点的右子树中,所有节点值都大于该节点值

这个特性让BST的查找效率极高(类似二分查找)。

BST的核心操作(附代码实现)

1. 准备工作:创建节点

首先需要一个函数创建新节点:

// 创建新节点(初始化数据和指针)
Node* createNode(int value) {Node* newNode = (Node*)malloc(sizeof(Node));newNode->data = value;    // 存储数据newNode->left = NULL;     // 左孩子初始为空newNode->right = NULL;    // 右孩子初始为空return newNode;
}
2. 插入节点(核心)

插入时必须保持"左小右大"原则,步骤:

  • 若当前树为空,新节点就是根
  • 若新值 < 当前节点值,插入左子树
  • 若新值 > 当前节点值,插入右子树
  • 若值相等(重复),通常不插入(避免混乱)
// 插入节点到BST
Node* insert(Node* root, int value) {// 情况1:当前位置为空,直接放入新节点if (root == NULL) {return createNode(value);}// 情况2:根据值的大小决定插入左/右子树if (value < root->data) {// 插入左子树,并用返回的新左子树更新当前左指针root->left = insert(root->left, value);} else if (value > root->data) {// 插入右子树,并用返回的新右子树更新当前右指针root->right = insert(root->right, value);} else {// 情况3:值相等,不插入(BST通常不允许重复值)printf("值 %d 已存在,不重复插入\n", value);}// 返回当前节点(保持树结构)return root;
}
3. 查找节点

利用"左小右大"特性,每次可排除一半节点:

// 在BST中查找值为value的节点
Node* search(Node* root, int value) {// 情况1:树空或找到目标节点if (root == NULL || root->data == value) {return root;}// 情况2:目标值小于当前节点,去左子树找if (value < root->data) {return search(root->left, value);}// 情况3:目标值大于当前节点,去右子树找else {return search(root->right, value);}
}
4. 删除节点(难点)

删除后必须保持BST特性,分3种情况:

情况处理方式
删除叶子节点(无孩子)直接释放该节点
删除只有一个孩子的节点用孩子节点"替代"它的位置
删除有两个孩子的节点用"左子树最大节点"或"右子树最小节点"替代,再删除被替代的节点
// 找到右子树中最小的节点(用于删除操作)
Node* findMin(Node* node) {Node* current = node;// 一直向左找(左子树最小)while (current && current->left != NULL) {current = current->left;}return current;
}// 从BST中删除值为value的节点
Node* deleteNode(Node* root, int value) {// 情况1:树空,直接返回if (root == NULL) return root;// 步骤1:找到要删除的节点if (value < root->data) {root->left = deleteNode(root->left, value); // 去左子树删} else if (value > root->data) {root->right = deleteNode(root->right, value); // 去右子树删} else {// 步骤2:找到节点,执行删除// 子情况A:只有一个孩子或没有孩子if (root->left == NULL) {Node* temp = root->right; // 用右孩子替代free(root);return temp;} else if (root->right == NULL) {Node* temp = root->left; // 用左孩子替代free(root);return temp;}// 子情况B:有两个孩子Node* temp = findMin(root->right); // 找右子树最小节点root->data = temp->data; // 用最小值替代当前节点值root->right = deleteNode(root->right, temp->data); // 删除那个最小节点}return root;
}
5. 销毁BST(避免内存泄漏)

用后序遍历销毁(先删孩子,再删根):

// 销毁整个BST
void destroyBST(Node* root) {if (root == NULL) return;destroyBST(root->left);  // 先销毁左子树destroyBST(root->right); // 再销毁右子树free(root);              // 最后销毁根节点
}

BST的性能特点

  • 优点:查找、插入、删除的平均时间复杂度为 O(log n)(和二分查找一样快)
  • 缺点:如果插入顺序有序(如1,2,3,4),会退化为链表,性能降为 O(n)
  • 解决办法:使用平衡二叉树(如AVL树、红黑树),自动保持树的平衡

五、BST的实际应用场景

BST的"有序性"和"高效查找"让它在很多场景中发挥作用:

  1. 数据库索引:数据库用BST的变种(B树、B+树)加速查询,比如查找某条记录
  2. 自动补全:搜索框的"输入提示"功能,用BST快速匹配前缀
  3. 排序:中序遍历BST可直接得到有序序列(类似快速排序的思想)
  4. 范围查询:比如查找"成绩在80-90分之间的学生",BST可高效定位范围

六、学习指南:如何掌握树结构?

初学者常见误区

  • 分不清"高度"和"深度":高度是从下往上数(根的高度是树的总高度),深度是从上往下数(根的深度是1)
  • 递归理解困难:树的操作天然适合递归,可先手动模拟小案例(如3层树的遍历)
  • 忽略内存管理:忘记释放节点会导致内存泄漏,销毁树时一定要用后序遍历

练习建议(从易到难)

  1. 手动模拟3种遍历方式(用前面的示例树)
  2. 实现计算二叉树高度的函数
  3. 验证一棵二叉树是否为BST(检查是否满足左小右大)
  4. 用迭代法实现前/中/后序遍历(不用递归,用栈)
  5. 尝试实现平衡二叉树(AVL树)

七、总结

树和二叉搜索树是数据结构中的"基石",它们的核心价值在于:

  • 表达层次关系(树的本质)
  • 高效的查找与排序(BST的优势)

掌握它们,不仅能解决很多实际问题,还能为学习更复杂的数据结构(如堆、B树、线段树)打下基础。记住:多画图、多模拟、多编码,树结构其实并不难!

http://www.dtcms.com/a/391049.html

相关文章:

  • 【一天一个Web3概念】区块链分叉(Fork)全面解析:类型、案例与影响
  • PHP低代码工作流创新,为企业数字化转型添翼
  • 低代码+AI生态:企业数字化起步阶段的“核聚变”冲击波
  • 【Linux基础知识系列:第一百三十四篇】理解Linux的进程调度策略
  • 主机windows虚拟机centos的hadoop调试mapreduce访问hdfs文件
  • 嵌入式Linux C语言程序设计
  • 【开题答辩全过程】以 基于Python的电影数据爬取及可视化分析为例,包含答辩的问题和答案
  • 推荐一些适合新手的Java项目教程
  • 探索PV操作:并发编程的核心钥匙
  • 一计算机网络基本概念-体系结构-思考题
  • Teslasuit动捕服的实际应用,系统利用电肌肉刺激为用户在VR中提供逼真的感觉和触觉
  • 【DMA】深入解析DMA控制器架构与运作原理
  • wayland 下 带特殊权限的 Qt GUI 程序 部署为 开机自启+守护进程
  • 无事随笔——mp踩坑
  • 根据后端给定的swagger文档生成对应的ts接口
  • 《黑天鹅》
  • docker编写java的jar步骤
  • HDR简介
  • 视觉Slam14讲笔记第4讲李群李代数【更新中】
  • 【无人机】ardupilot事项笔记
  • 大端模式与小端模式
  • Openwrt 平台下移植rk3568 rknn_yolov5_demo 应用程序问题分析
  • Dioxus后端代码
  • 概念篇:ReactJS + AppSync + DynamoDB 性能优化核心概念
  • 实践篇:ReactJS + AppSync + DynamoDB 性能优化实践
  • GPS 定位:守护财产安全的 “隐形防盗锁”
  • Vue3 + Three.js 进阶实战:批量 3D 模型高效可视化、性能优化与兼容性解决方案
  • 海外VPS索引版本兼容性检查,版本兼容问题检测与多系统适配方法
  • uniapp 常用
  • C语言入门教程 | 阶段一:基础语法讲解(数据类型与运算符)