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

数据结构入门 (十):“左小右大”的秩序 —— 深入二叉搜索树

文章目录

引言:对“效率”的极致追求

在之前的探索中,我们已经掌握了多种数据结构。我们学习了数组,它能通过索引实现 O(1) 的随机访问,但前提是你知道索引;如果我们想在其中查找某个特定值,只能从头到尾遍历,效率是 O(n)

后来我们学习了二分查找,它能将查找效率一举提升 O(log n)。但它有一个苛刻的前提:数据必须存储在有序的数组中。这个前提,使得插入和删除操作变成了 O(n) ,因为你必须移动大量元素来维持秩序。

那么有没有一种“两全其美”的结构?它既能像二分查找那样高效(O(log n)),又能像链表那样灵活地插入和删除(O(log n))?

于是,我们利用二分查找的思想,与“树”的结构巧妙结合,创建了另一类至关重要的二叉树结构——二叉搜索树(BST)。它通过一条规则——左子树所有节点小于根,右子树所有节点大于根,使得二叉树变得有序,查找、插入、删除等这些操作在平均情况下都达到对数级的时间复杂度。二叉搜索树是后续平衡二叉搜索树等更复杂结构的基础。

一、“左小右大”:二叉搜索树的铁律

二叉搜索树,也常被称为二叉排序树。它之所以能实现高效查找,只因它在二叉树的基础上,坚定不移地执行着一条“铁律”:

  1. 若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值
  2. 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值
  3. 它的左右树也分别为二叉搜索树

这个递归的定义,赋予了整棵树一种全局的有序性。这种有序性,让我们在查找、插入、删除时,拥有了和二分查找一样的“决策能力”——在任何一个节点,我们都能通过一次比较,立刻知道下一步该向左走,还是向右走。

结构查找元素插入元素删除元素
普通数组O(n)O(n)O(n)
顺序数组O(logn)O(n)O(n)
二叉搜索树O(logn)O(logn)O(logn)

二、二叉搜索树递归实现

二叉搜索树的递归定义,天然适合用递归函数来操作。

1.结构设计与创建

首先,我们定义节点(BSNode)和树的“管家”(BSTree)。这个“管家”结构体(树头)不存储实际数据,而是负责维护 root 指针和 count 等元信息,这与我们之前实现链表的思想(头节点)一致。

typedef int Element;
// 定义二叉搜索树的节点结构
typedef struct _bs_node {Element data;struct _bs_node *left;struct _bs_node *right;
} BSNode;// 定义二叉搜索树的头节点(树头)
typedef struct {BSNode *root;int count;const char *name;
} BSTree;BSTree* createBSTree(const char* name) {BSTree* tree = malloc(sizeof(BSTree));if (tree == NULL) {return NULL;}tree->name = name;tree->root = NULL;tree->count = 0;return tree;
}

2.插入:按“规矩”找到自己的位置

插入操作是BST递归思想的完美体现。我们提供一个对外接口 insertBSTree,它内部调用一个 static (私有) 的辅助函数 insertBSNode。BST插入必须通过值比较来确保结构的有序性。

// 辅助函数:创建新节点
static BSNode *createBSNode(Element e) {BSNode *node = malloc(sizeof(BSNode));if (node == NULL) {return NULL;}node->data = e;node->left = node->right = NULL;return node;
}// 辅助函数:递归插入的“引擎”
static BSNode* insertBSNode(BSTree *tree, BSNode *node, Element e) {// 1. 递归终止条件:找到了插入的空位if (node == NULL) {tree->count++;return createBSNode(e); // 创建新节点并返回}// 2. 递归查找:根据“左小右大”规则,决定向哪边走if (e < node->data) {// 递归插入到左子树,并更新左子树的链接node->left = insertBSNode(tree, node->left, e);} else if (e > node->data) {// 递归插入到右子树,并更新右子树的链接node->right = insertBSNode(tree, node->right, e);}// (如果 e == node->data,我们默认不做操作,保持树的唯一性)// 3. 返回当前节点(保持链接不变)return node;
}// 对外接口:插入操作
void insertBSTree(BSTree* tree, Element e) {// 递归的“启动”tree->root = insertBSNode(tree, tree->root, e);
}

3. 遍历:天然的“排序器”

由于“左小右大”的铁律,当我们对二叉搜索树进行中序遍历(左 -> 根 -> 右)时,会得到一个严格递增的有序序列。这不仅是验证BST正确性的绝佳手段,也是它“排序树”别名的由来。

void visitBSNode(const BSNode* node) {printf("\t%d", node->data); // 打印节点值
}static void inOrderBSNode(const BSNode *node) {if (node) {inOrderBSNode(node->left);  // 左visit(node);                // 根inOrderBSNode(node->right); // 右}
}void inOrderBSTree(const BSTree *tree) {printf("[%s]Tree:", tree->name);inOrderBSNode(tree->root);printf("\n");
}

4. 查找:高效的“二分”

查找操作是非递归的,它完美地再现了“二分查找”的过程。

BSNode* searchBSTree(const BSTree* tree, Element e) {BSNode *node = tree->root;while (node != NULL) {if (e < node->data) {node = node->left; // 目标更小,去左边找} else if (e > node->data) {node = node->right; // 目标更大,去右边找} else {return node; // 找到了!}}return NULL; // 遍历到底也没找到
}

5.树高:递归丈量“深度”

树的高度,是从根节点到最远叶节点的最长路径上的边数(或节点数,此处按节点数定义)。在二叉搜索树中,高度反映了树的“纵深”程度,也间接影响着查找、插入等操作的时间复杂度——理想情况下,树越“矮胖”,效率越高;越“瘦高”,越接近链表,性能退化。

计算树高天然适合递归:当前节点的高度,等于其左右子树高度的最大值加一。空节点高度为0,这是递归的边界条件。

static int heightBSNode(const BSNode* node) {if (node == NULL) {return 0; // 空节点,高度为0}int leftHeight = heightBSNode(node->left); // 递归求左子树高度int rightHeight = heightBSNode(node->right); // 递归求右子树高度if (leftHeight > rightHeight) {return ++leftHeight;}return ++rightHeight;
}int heightBSTree(const BSTree* tree) {return heightBSNode(tree->root);
}

6.删除:棘手的“重建”艺术

删除是BST中最复杂的操作,因为它必须在移除一个节点后,依然保持“左小右大”的铁律。我们同样使用递归实现,根据被删除节点的“度的数量”分三种情况讨论:

  • 度为 0 (叶子节点):可以直接free,并让其父节点指向 NULL
  • 度为 1 (只有一个孩子):也比较简单,让其父节点“跳过”自己,直接指向自己的那个唯一孩子。
  • 度为 2 (左右孩子俱全):最棘手。
    • 策略:为了不破坏结构,我们不能直接删除它。我们从它的左子树中,找到那个值最大的节点(即它的“前驱”),或者从右子树中找到值最小的节点(即它的“后继”)。
    • 替换:将这个“前驱”(或“后继”)节点的值,复制到当前要删除的节点上。
    • 转化:转而去删除那个“前驱”(或“后继”)节点。由于这个“前驱”(或“后继”)节点一定是其子树中最边缘的,它自己的度必然只可能是0或1,这就把一个复杂问题转化成了我们能解决的情况1或情况2。
// 辅助函数:在 node 的子树中找到值最大的节点
static BSNode* maxValueBSNode(BSNode *node) {while (node && node->right) {node = node->right;}return node;
}static BSNode* deleteBSNode(BSTree* tree, BSNode *node, Element e) {if (node == NULL) {return NULL;}// 1. 递归查找if (e < node->data) {node->left = deleteBSNode(tree, node->left, e);} else if (e > node->data) {node->right = deleteBSNode(tree, node->right, e);} else {// 2. 找到了, e == node->data,开始执行删除BSNode *tmp;if (node->left == NULL) { // 度为0或1tmp = node->right;free(node);tree->count--;return tmp; // 返回右孩子(或NULL)给上一层}if (node->right == NULL) { // 度为1tmp = node->left;free(node);tree->count--;return tmp; // 返回左孩子给上一层}// 此时说明待删除节点的度为2,替换当前节点值(后继或前驱)// 找这个节点的左节点的最大值tmp = maxValueBSNode(node->left);// 复制前驱的值到当前节点node->data = tmp->data;// 递归地从左子树中删除那个前驱节点// (注意:此时删除的 e 变成了 node->data,也就是 tmp->data)node->left = deleteBSNode(tree, node->left, node->data);}return node;
}// 对外接口:删除操作
void deleteBSTree(BSTree* tree, Element e) {if (tree) {tree->root = deleteBSNode(tree, tree->root, e);}
}

7.销毁:“斩草除根”

对于二叉搜索树的销毁,我们需要先把左右子树删除,最后删除根节点。可以用后续遍历的方法。

static void freeBSNode(BSTree *tree, BSNode *node) {if (node) {freeBSNode(tree, node->left); // 左freeBSNode(tree, node->right);// 右free(node);                   // 根tree->count--;}
}void releaseBSTree(BSTree *tree) {if (tree) {freeBSNode(tree, tree->root);printf("There are %d nodes.\n", tree->count);free(tree);}
}

三、迭代实现:告别“栈溢出”的循环

递归虽然优雅,但在树极深的情况下可能导致栈溢出。使用循环(迭代)是更稳健的选择。

1.非递归插入

非递归插入的思路,就是用一个 pre 指针始终“尾随” cur 指针,以便在 cur 找到空位时,pre 能执行链接操作。

void deleteBSTree(BSTree* tree, Element e) {	BSNode *cur = tree->root;BSNode *pre = NULL;while (cur) {pre = cur;if (e < cur->data) {cur = cur->left;} else if (e > cur->data) {cur = cur->right;} else {return;}}// 可能插入的是第一个根元素,可能插入的是一个普通的位置BSNode *node = createBSNode(e);if (pre) {if (e < pre->data) {pre->left = node;} else if (e > pre->data) {pre->right = node;}} else {tree->root = node;}tree->count++;

2.非递归删除

非递归删除是所有操作中最复杂的,因为它需要处理的指针关系和边界情况最多。我们将递归中的三种情况(度为0、1、2)在迭代中实现。

核心思路

  1. 查找:首先,我们必须通过循环找到要删除的节点 cur,并且始终要保留其父节点 pre 的指针。
  2. 处理:根据 cur 的度(0, 1, 2)执行不同的逻辑。
// 辅助函数:度为2时进行的操作
// 策略:找到 cur 的后继节点(右子树中最小的节点),用其值覆盖 cur,然后转化为删除那个后继节点(后继节点度必为0或1)
static void deleteMiniNode(BSNode *node) {// 1. 查找后继节点及其父节点BSNode *mini = node->right;BSNode *pre = node;while (mini->left) {pre = mini;mini = mini->left;}// 2.转化为删除后继节点if (pre->data == mini->data) {pre->right = mini->right;} else {pre->left = mini->right;}// 3.将后继节点的值 复制 到当前节点node->data = mini->data;free(mini);
}void deleteBSTree(BSTree* tree, Element e) {// 查找要删除的节点(cur)及其父节点(pre)BSNode *node = tree->root;BSNode *pre = NULL;while (node) {pre = node;if (e < node->data) {node = node->left;} else if (e > node->data) {node = node->right;} else {break;}}if (node == NULL) {printf("No %d element!\n", e);return;}BSNode *tmp = NULL;if (node->left == NULL) {tmp = node->right;} else if (node->right == NULL) {tmp = node->left;} else {deleteMiniNode(node->right);--tree->count;return;}if (pre) {if (node->data < pre->data) {pre->left = tmp;} else {pre->right = tmp;}} else {tree->root = tmp;}free(node);--tree->count;
}

四、总结:秩序的代价

今天,我们掌握了二叉搜索树——这个为了兼顾“查找”与“增删”效率而生的精妙结构。它依靠“左小右大”的铁律,在平均情况下为我们提供了 O(log n) 的全能表现。

然而,这种秩序是有“代价”的。

请思考一个问题:如果我们按顺序(1, 2, 3, 4, 5)向一棵空的BST中插入数据,会发生什么? 1 成为根,2 成为 1 的右孩子,3 成为 2 的右孩子… 这棵树会“退化”成一条链表

在这种最坏情况下,BST的所有操作都将退化回 O(n) 的复杂度。我们费尽心机构建的“秩序”,反而成了我们的枷锁。

如何让这棵树在插入时保持“平衡”,永不退化?这就是“自我平衡”的艺术,也是我们下一篇文章的主题:AVL树

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

相关文章:

  • 能不能模仿百度一样做搜索引擎网站php网站开发职责
  • 如果做局域网影音网站企业服务中心抖音
  • 常见购物网站功能丹东建设银行网站
  • 徐州建网站网站界面设计形考
  • 最新电大网站开发维护WORDPRESS摘要无效
  • 高端定制网站开发买空间哪个网站好
  • Linux 内存管理 (5):buddy 内存分配简要流程
  • C++ 高精度计算:突破数据类型限制的实现与应用
  • 学做招投标的网站上传空间站的注意事项
  • 黑马JAVAWeb -Vue工程化 - Element Plus- 表格-分页条-中文语言包-对话框-Form表单
  • 甘州区建设局网站wordpress谷歌广告
  • 纪检网站建设动态主题国内建站平台
  • 网站页面的大小写国内seo服务商
  • 如何在关键里程碑已延迟的情况下重新规划项目进度
  • 排版好看的网站界面湖北企业响应式网站建设价位
  • 光伏电站运维-可视化大屏带来的便利
  • 张家港保税区建设规划局网站商标注册查询官网入口官方
  • MySQL 四种隔离级别:从脏读到幻读的全过程
  • 人才网网站建设方案河北建设工程信息网登陆
  • 网站后台不能上传做网站主机选择
  • 网站开发与管理课程设计心得坛墨网站建设
  • 阿里巴巴做网站难吗南京谷歌seo
  • 当 AI 工作流需要“人类智慧“:深度解析 Microsoft Agent Framework 的人工接入机制
  • Linux 内存管理 (3):fixmap
  • 一个视频多平台发布天津网站seo策划
  • 数据管理战略|3数据管理成功的预期衡量标准|螺旋上升
  • 零碳园区的路径选择与方法论:从规划到落地的全链路实践
  • 河间做网站的电话东莞东城社保局电话
  • 晶粒 和晶体、晶格
  • 声网AI技术赋能,智能客服告别机械式应答