从 "猜数字游戏" 入门 BST:C 语言从零实现与核心操作
一、引言:用 "猜数字" 搞懂 BST 的核心逻辑
你一定玩过 "猜数字" 游戏吧?比如我心里想一个 1-100 的数,你猜,我只说 "大了""小了" 或 "对了"。聪明的你肯定不会从 1 开始挨个猜,而是先猜 50------ 如果我说 "大了",你就猜 25;如果我说 "小了",你就猜 75,每次都把范围缩小一半,很快就能猜对。
其实这个 "猜数字" 的逻辑,就是 ** 二叉搜索树(BST,Binary Search Tree)** 的核心!今天我们就用这个简单的游戏,从零开始学会 BST 的核心思路和 C 语言代码实现,即使是完全没学过高级数据结构的新手也能轻松掌握。
二、先搞懂:什么是 BST?
BST 的核心就是一棵 "帮你快速猜数字的二叉树",它有两个非常简单的规则,记住这两个规则,你就懂了 BST 的一半。
1. BST 的两个简单规则
规则 1:结构规则
就是一棵普通的二叉树 ------ 每个节点可以有 0 个、1 个或 2 个子节点,不需要像二叉堆那样必须是 "完全二叉树"(这是和二叉堆的核心区别)。
规则 2:大小规则(核心!)
对于任意一个节点:
- 它左边的所有节点 ,都比它小;
- 它右边的所有节点 ,都比它大。
就这么简单!举个直观的例子,比如我们要存数字 [5,3,7,2,4,6,8],对应的 BST 长这样:
5
/ \
3 7
/ \ / \
2 4 6 8
验证一下规则:
- 5 的左边(2,3,4)都 <5,右边(6,7,8)都> 5;
- 3 的左边 2<3,右边 4>3;
- 7 的左边 6<7,右边 8>7;
- 完美符合规则!
2. BST 的核心优势:快速查找
就像 "猜数字" 一样,每次查找都能把范围缩小一半,平均时间复杂度是O(log n),比从数组里挨个找(O (n))快多了!
比如我们要找数字 4:
-
先看根节点 5 → 4<5,去左边找;
-
再看节点 3 → 4>3,去右边找;
-
找到节点 4 → 对了!
只找了 3 次就找到了,是不是很快?
3. BST 的一个神奇性质:中序遍历升序
这是 BST 最有用的性质,没有之一!如果我们按照 "左→根→右 " 的顺序遍历 BST(这个顺序叫 "中序遍历"),得到的序列一定是从小到大排好序的。
比如上面的 BST,中序遍历的结果就是:[2,3,4,5,6,7,8],完美升序!
这个性质是我们验证 BST、给数据排序的 "黄金标准",一定要记住。
三、问题分析:验证 BST 到底要我们做什么?
我们来看一个简单的题目:给你一棵二叉树,你怎么判断它是不是一棵合格的 BST?
示例 1:合格的 BST
输入树:
2
/ \
1 3
- 大小规则:2>1、2<3,符合;
- 中序遍历:
[1,2,3],升序; - 结论:是合格的 BST。
示例 2:不合格的 BST(新手易错!)
输入树:
5
/ \
1 6
/ \
3 7
- 新手易错点:只看父子节点 ------5>1、5<6、6>3、6<7,看似没问题;
- 实际错误:3 在 6 的左边,但 3<5,违反了 "5 的右边所有节点都> 5" 的规则;
- 中序遍历:
[1,5,3,6,7],不是升序; - 结论:不是合格的 BST。
划重点:BST 的大小规则约束的是 "所有子孙节点",不只是 "左右子节点"!
四、BST 解法思路:两个核心点搞定所有操作
我们可以把 BST 的所有操作,都转化为两个核心点的应用,只要掌握了这两个核心点,所有问题都能迎刃而解。
核心点 1:中序遍历升序(验证 BST 的黄金标准)
只要中序遍历的结果是从小到大排好序的,就一定是合格的 BST;反之则不是。这个方法最直观,最不容易出错。
核心点 2:"猜数字" 逻辑(查找、插入的核心)
对于任意节点:
- 要找的数 < 当前节点 → 去左边找;
- 要找的数 > 当前节点 → 去右边找;
- 相等 → 找到了!
就这么简单,和 "猜数字" 一模一样。
五、代码实现与逐行详解(C 语言版)
我们只实现最核心、最简单的操作:创建节点、插入节点、查找节点、验证 BST(用中序遍历法),代码简单,注释详细,保证你能看懂。
1. 结构体定义与辅助宏
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <limits.h> // 用于INT_MIN,设定初始前一个值
// BST节点结构体:就三个东西------值、左孩子指针、右孩子指针
typedef struct TreeNode {
int val; // 节点存的数字
struct TreeNode* left; // 指向左孩子的指针(左边比它小)
struct TreeNode* right; // 指向右孩子的指针(右边比它大)
} TreeNode;
2. 辅助函数:创建一个新节点
// 创建一个新节点,值为val,左右孩子都设为空
TreeNode* createNode(int val) {
// 申请内存
TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
if (node == NULL) {
printf("内存申请失败!\n");
return NULL;
}
// 初始化节点
node->val = val;
node->left = NULL; // 左孩子先设为空
node->right = NULL; // 右孩子先设为空
return node;
}
3. 核心操作 1:插入节点(用 "猜数字" 逻辑)
// 向BST中插入一个值为val的节点,返回插入后的根节点
TreeNode* insertIntoBST(TreeNode* root, int val) {
// 情况1:如果树是空的,直接创建一个新节点作为根
if (root == NULL) {
return createNode(val);
}
// 情况2:树不是空的,用"猜数字"逻辑找位置
if (val < root->val) {
// 要插入的数 < 当前节点 → 插到左边
root->left = insertIntoBST(root->left, val);
} else if (val > root->val) {
// 要插入的数 > 当前节点 → 插到右边
root->right = insertIntoBST(root->right, val);
}
// 如果val == root->val,说明数字重复了,BST不允许重复,直接返回
return root;
}
4. 核心操作 2:查找节点(用 "猜数字" 逻辑)
// 在BST中查找值为target的节点,找到返回true,没找到返回false
bool searchBST(TreeNode* root, int target) {
// 情况1:树是空的,或者找到了叶子节点还没找到 → 没找到
if (root == NULL) {
return false;
}
// 情况2:找到了!
if (root->val == target) {
return true;
}
// 情况3:用"猜数字"逻辑继续找
if (target < root->val) {
// 要找的数 < 当前节点 → 去左边找
return searchBST(root->left, target);
} else {
// 要找的数 > 当前节点 → 去右边找
return searchBST(root->right, target);
}
}
5. 核心操作 3:验证 BST(用中序遍历升序法,最直观)
// 辅助变量:记录中序遍历的前一个节点值,初始设为INT_MIN(最小的整数)
long prevVal = LONG_MIN;
// 中序遍历辅助函数:左→根→右,同时判断是否升序
bool inorderCheck(TreeNode* root) {
// 情况1:空节点是合格的
if (root == NULL) {
return true;
}
// 第一步:先遍历左边
if (!inorderCheck(root->left)) {
return false; // 左边不合格,直接返回false
}
// 第二步:检查当前节点是否 > 前一个节点(保证升序)
if (root->val <= prevVal) {
return false; // 不是升序,不合格
}
prevVal = root->val; // 更新前一个节点值为当前节点
// 第三步:再遍历右边
return inorderCheck(root->right);
}
// 主函数:验证BST
bool isValidBST(TreeNode* root) {
prevVal = LONG_MIN; // 每次验证前,先把前一个值重置为最小
return inorderCheck(root);
}
6. 辅助工具函数:中序遍历打印(用于测试)
// 中序遍历打印BST:左→根→右,打印出来就是升序的
void inorderPrint(TreeNode* root) {
if (root == NULL) {
return;
}
inorderPrint(root->left); // 先打左边
printf("%d ", root->val); // 再打当前
inorderPrint(root->right); // 最后打右边
}
// 辅助函数:释放BST内存(避免内存泄漏)
void freeBST(TreeNode* root) {
if (root == NULL) {
return;
}
freeBST(root->left);
freeBST(root->right);
free(root);
}
7. 主函数测试
int main() {
// 1. 构建一棵合格的BST
printf("===== 构建合格BST并测试 =====\n");
TreeNode* root = NULL; // 先建一棵空树
// 依次插入数字:5,3,7,2,4,6,8
root = insertIntoBST(root, 5);
root = insertIntoBST(root, 3);
root = insertIntoBST(root, 7);
root = insertIntoBST(root, 2);
root = insertIntoBST(root, 4);
root = insertIntoBST(root, 6);
root = insertIntoBST(root, 8);
// 中序遍历打印(应该是升序)
printf("BST中序遍历:");
inorderPrint(root); // 输出:2 3 4 5 6 7 8
printf("\n");
// 验证BST
printf("是否为合格BST:%s\n", isValidBST(root) ? "是" : "否"); // 是
// 查找节点
int target1 = 4;
printf("查找数字%d:%s\n", target1, searchBST(root, target1) ? "找到了" : "没找到"); // 找到了
int target2 = 9;
printf("查找数字%d:%s\n", target2, searchBST(root, target2) ? "找到了" : "没找到"); // 没找到
// 释放内存
freeBST(root);
// 2. 构建一棵不合格的BST并测试
printf("\n===== 构建不合格BST并测试 =====\n");
// 手动建一棵不合格的树:5的右子树有3
TreeNode* badRoot = createNode(5);
badRoot->left = createNode(1);
badRoot->right = createNode(6);
badRoot->right->left = createNode(3); // 这里错了!3<5但在右边
badRoot->right->right = createNode(7);
// 中序遍历打印(不是升序)
printf("不合格BST中序遍历:");
inorderPrint(badRoot); // 输出:1 5 3 6 7
printf("\n");
// 验证BST
printf("是否为合格BST:%s\n", isValidBST(badRoot) ? "是" : "否"); // 否
// 释放内存
freeBST(badRoot);
return 0;
}
六、关键细节:新手容易踩的 3 个坑
- 验证 BST 只看父子节点:这是最常见的错误!一定要记住 BST 的大小规则约束的是 "所有子孙节点",推荐用 "中序遍历升序" 的方法验证,最直观。
- BST 允许重复数字:默认 BST 不允许重复数字,如果有重复需求,可以在节点里加一个 "计数" 变量,或者调整规则(比如左边≤根,右边 > 根),但验证逻辑也要同步改。
- 忘记释放内存 :C 语言手动管理内存,用完 BST 一定要用
freeBST释放,否则会造成内存泄漏。
七、总结:BST 的核心与适用场景
BST 的核心
- 两个简单规则:普通二叉树结构 + 左小右大;
- 一个神奇性质:中序遍历升序;
- 一个核心逻辑:猜数字(比根小去左,比根大去右)。
BST 的适用场景
- 快速查找数字:平均时间复杂度 O (log n),比数组挨个找快;
- 给数据排序:插入完数据后,中序遍历一下就是升序的;
- 简单的有序数据存储:比如存学生成绩、电话号码,需要快速查找和插入。
BST 的核心就是这么简单,只要掌握了 "左小右大" 和 "中序遍历升序",你就彻底搞懂了 BST!