从 “猜数字游戏” 入门 BST:C 语言从零实现与核心操作

从 "猜数字游戏" 入门 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:

  1. 先看根节点 5 → 4<5,去左边找;

  2. 再看节点 3 → 4>3,去右边找;

  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 个坑

  1. 验证 BST 只看父子节点:这是最常见的错误!一定要记住 BST 的大小规则约束的是 "所有子孙节点",推荐用 "中序遍历升序" 的方法验证,最直观。
  2. BST 允许重复数字:默认 BST 不允许重复数字,如果有重复需求,可以在节点里加一个 "计数" 变量,或者调整规则(比如左边≤根,右边 > 根),但验证逻辑也要同步改。
  3. 忘记释放内存 :C 语言手动管理内存,用完 BST 一定要用freeBST释放,否则会造成内存泄漏。

七、总结:BST 的核心与适用场景

BST 的核心

  1. 两个简单规则:普通二叉树结构 + 左小右大;
  2. 一个神奇性质:中序遍历升序;
  3. 一个核心逻辑:猜数字(比根小去左,比根大去右)。

BST 的适用场景

  1. 快速查找数字:平均时间复杂度 O (log n),比数组挨个找快;
  2. 给数据排序:插入完数据后,中序遍历一下就是升序的;
  3. 简单的有序数据存储:比如存学生成绩、电话号码,需要快速查找和插入。

BST 的核心就是这么简单,只要掌握了 "左小右大" 和 "中序遍历升序",你就彻底搞懂了 BST!

相关推荐
Yupureki2 小时前
《C++实战项目-高并发内存池》5.PageCache构造
c语言·开发语言·c++·单例模式·github
lichenyang4532 小时前
OSI(Open System interconnection)
网络
MinterFusion2 小时前
云主机服务单价 —— 评估云主机性价比的指标
网络·云主机·明德融创·价格评价·云主机服务单价·服务单价
阿拉斯攀登2 小时前
第 2 篇 小白前置知识急救包!RK 安卓驱动开发必备知识点,一篇补全
c语言·嵌入式·rk3568·安卓驱动
zh路西法3 小时前
【C语言简明教程提纲】(四):结构体与文件定义和操作
android·c语言·redis
hy____1233 小时前
Linux_网络基础2
linux·服务器·网络
喵叔哟3 小时前
6. 【Blazor全栈开发实战指南】--组件通信与共享状态
linux·网络·windows
江南西肥肥4 小时前
养虾日记[特殊字符]:OpenClaw 多 Agent 与飞书对接实战
网络·飞书·openclaw
wsoz4 小时前
GCC编译
linux·c语言·嵌入式·gcc