二叉树(C语言版)
文章目录
- 二叉树
- 完全二叉树和满二叉树
- 二叉搜索树
- 基本操作
- 实现
- 代码
- 运行结果
- 分析
- 红黑树
- 2-3-4树(理论模型)
- 红黑树(实际实现)
二叉树
树是一种层次结构,它在现实生活中是广泛存在的,比如:族谱(family tree),组织机构,目录结构等。
不过今天我们只讲二叉树。
二叉树有多重要?单就面试而言,在 leetcode 中二叉树相关的题目占据了 300 多道,近三分之一。同时,二叉树在整个算法板块中还起到承上启下的作用:不但是数组和链表的延伸,又可以作为图的基础。总之,非常重要!
那什么是二叉树?
定义:二叉树是一棵树,并且二叉树的每个结点最多有两棵子树。二叉树的子树又分为左子树和右子树。

完全二叉树和满二叉树
二叉树有两种特殊的形态:完全二叉树和满二叉树。
完全二叉树:若二叉树的深度为 h,除第 h 层外,其它各层(1~h-1)的结点数目都达到最大值,第 h 层的结点都连续排列在最左边,这样的二叉树就是完全二叉树。
满二叉树:每一层的结点数目都达到最大值(包括最下面一层)。

二叉搜索树
Binary Search Tree (BST) 又叫二叉排序树。要求树中的结点可以按照某个规则进行比较,其定义如下:
-
.左子树中所有结点的 key 值都比根结点的 key 值小,并且左子树也是二叉搜索树。
-
右子树中所有结点的 key 值都比根结点的 key 值大,并且右子树也是二叉搜索树。

基本操作
search:若 BST 为空,则直接NULL。若 BST 非空,则和根结点比较,若和根结点相等,表明找到了。若比根结点小,则在左子树中递归查找;若比根结点大,则在右子树中递归查找。
insert:若 BST 为空,则创建结点,将其作为根结点。若 BST 非空,则和根结点比较,若和根结点相等,则返回。若比根结点小,则在左子树中递归插入;若比根结点大,则在右子树中递归插入。
delete:分三种情况处理。
-
如果要删除结点没有孩子,那么直接将该结点删除就行了。

-
如果要删除结点只有一个孩子,那么需要将父亲结点对应的指针,指向它唯一 的孩子结点。

-
如果要删除结点有两个孩子,那么我们可以找到这个结点的右子树中最小结点 (或者左子树中最大结点),把它替换到要删除的结点上,然后再删除右子树的最小结点 (或左子树的最大结点)。

实现
代码
// BST.h
typedef char K;typedef struct tree_node {K key;struct tree_node* left;struct tree_node* right;
} TreeNode;typedef struct {TreeNode* root;
} BST;// API
BST* bst_create();
void bst_destroy(BST* tree);void bst_insert(BST* tree, K key);
TreeNode* bst_search(BST* tree, K key);
void bst_delete(BST* tree, K key);void bst_preorder(BST* tree); // 前序
void bst_inorder(BST* tree); // 中序
void bst_postorder(BST* tree); // 后序
void bst_levelorder(BST* tree); // 层序遍历
// BST.c
#include "BST.h"
#include <stdlib.h>
#include <stdio.h>BST* bst_create() {BST* bst = (BST*)malloc(sizeof(BST));bst->root = NULL;return bst;
}void freeTraversal(TreeNode* node) {// 终止条件if (node == NULL) {return;}freeTraversal(node->left); // 前freeTraversal(node->right); // 后printf("已释放结点 %d\n", node->key);free(node); // 中
}void bst_destroy(BST* tree) {TreeNode* root = tree->root;// 1.释放所有结点freeTraversal(root);// 2.释放BSTfree(tree);printf("已释放BST\n");
}void bst_insert(BST* tree, K key) {if (tree->root == NULL) { // 空树,直接作为根结点插入TreeNode* newNode = (TreeNode*)calloc(1, sizeof(TreeNode));newNode->key = key;tree->root = newNode;return;}// 迭代法TreeNode* cur = tree->root;while (cur) {if (key > cur->key) {// 大于if (cur->right == NULL) {TreeNode* newNode = (TreeNode*)calloc(1, sizeof(TreeNode));newNode->key = key;cur->right = newNode;return;} else {cur = cur->right;}}else if (key < cur->key) {// 小于if (cur->left == NULL) {TreeNode* newNode = (TreeNode*)calloc(1, sizeof(TreeNode));newNode->key = key;cur->left = newNode;return;} else {cur = cur->left;}}else return;} // cur == NULL
}TreeNode* bst_search(BST* tree, K key) {TreeNode* cur = tree->root;while (cur) {if (key > cur->key)cur = cur->right;else if (key < cur->key)cur = cur->left;else {printf("找到结点,结点值 %d\n", cur->key);return cur;}}printf("没找到结点\n");return NULL;
}TreeNode* deleteOneNode(TreeNode* targetNode) {if (targetNode == NULL) return NULL;if (targetNode->right == NULL) { // 右空 左空或不空TreeNode* tmpNode = targetNode->left;free(targetNode);return tmpNode;}// 右不空,左不空或空TreeNode* cur = targetNode->right;while (cur->left) {cur = cur->left;} // 右子树的最左结点 cur->left == NULLcur->left = targetNode->left;TreeNode* tmpNode = targetNode->right;free(targetNode);return tmpNode;
}void bst_delete(BST* tree, K key) {TreeNode* cur = tree->root;TreeNode* prev = NULL;while (cur) {if (cur->key == key) {if (prev == NULL) {// 要删除根节点tree->root = deleteOneNode(cur);}else if (prev->left && prev->left->key == key) {prev->left = deleteOneNode(cur);}else if (prev->right && prev->right->key == key) {prev->right = deleteOneNode(cur);}break;}prev = cur;if (key > cur->key) cur = cur->right;if (key < cur->key) cur = cur->left;}}/* **************************************** */
/* 深度优先遍历 */
/* **************************************** */// 前序遍历(递归法)
void preorder_traversal(TreeNode* node) {// 终止条件if (node == NULL) {return;}printf(" %d", node->key); // 中preorder_traversal(node->left); // 左preorder_traversal(node->right); // 后
}void bst_preorder(BST* tree) {TreeNode* rootNode = tree->root;preorder_traversal(rootNode);
}// 中序遍历(递归法)
void inorder_traversal(TreeNode* node) {// 终止条件if (node == NULL) {return;}inorder_traversal(node->left); // 前printf(" %d", node->key); // 中inorder_traversal(node->right); // 后
}void bst_inorder(BST* tree) {TreeNode* rootNode = tree->root;inorder_traversal(tree->root);
}// 后序遍历(递归法)
void postorder_traversal(TreeNode* node) {// 终止条件if (node == NULL) {return;}postorder_traversal(node->left); // 前postorder_traversal(node->right); // 后printf(" %d", node->key); // 中
}void bst_postorder(BST* tree) {TreeNode* rootNode = tree->root;postorder_traversal(tree->root);
}/* **************************************** */
/* 广度优先遍历 */
/* **************************************** */// 实现队列(基于链表)
typedef TreeNode* V;typedef struct que_node{V node;struct que_node* next;
} queNode;typedef struct {queNode* front;queNode* rear;int size;
} Queue;Queue* create_queue() {Queue* que = (Queue*)malloc(sizeof(Queue));que->front = NULL; // 队头que->rear = NULL; // 队尾que->size = 0;return que;
}void push_queue(Queue* q, V val) {queNode* newNode = (queNode*)malloc(sizeof(queNode));newNode->node = val;newNode->next = NULL;if (q->front == NULL) { // 空队列q->front = newNode;q->rear = newNode;q->size++;return;}q->rear->next = newNode;q->rear = newNode;q->size++;
}V pop_queue(Queue* q) {if (q->front == NULL) {return NULL;}queNode* tmpNode = q->front;if (q->size == 1)q->rear = NULL;q->front = tmpNode->next;q->size--;return tmpNode->node;
}V peek_queue(Queue* q) {if (q->front == NULL) {return NULL;}return q->front->node;
}bool is_empty(Queue* q) {return (q->front == NULL);
}/* 层序遍历(迭代法) */
// 层序遍历二叉树
void bst_levelorder(BST* tree) {Queue* que = create_queue();TreeNode* cur = tree->root;if (cur == NULL)return;push_queue(que, cur);while (!(is_empty(que))) {int num = que->size;while (num--) {cur = que->front->node;printf(" %d", que->front->node->key);pop_queue(que);if (cur->left) {push_queue(que, cur->left);}if (cur->right) {push_queue(que, cur->right);}}}
}
// main.c
#include "BST.h"
#include <stdio.h>int main(void) {BST* bst = bst_create();bst_insert(bst, 1);bst_insert(bst, 2);bst_insert(bst, 5);bst_insert(bst, 7);bst_insert(bst, 5);bst_insert(bst, 9);bst_insert(bst, 4);// bst_search(bst, 5);// bst_search(bst, 100);printf("前序遍历结果:");bst_preorder(bst);printf("\n");printf("中序遍历结果:");bst_inorder(bst);printf("\n");printf("后序遍历结果:");bst_postorder(bst);printf("\n");printf("层序遍历结果:");bst_levelorder(bst);printf("\n");printf("已删除结点2\n");bst_delete(bst, 2);printf("已删除结点5\n");bst_delete(bst, 5);printf("前序遍历结果:");bst_preorder(bst);printf("\n");printf("中序遍历结果:");bst_inorder(bst);printf("\n");printf("后序遍历结果:");bst_postorder(bst);printf("\n");printf("层序遍历结果:");bst_levelorder(bst);printf("\n");bst_destroy(bst);
}
运行结果

分析
BST 增加、删除和查找的效率取决于它的高度 h。
insert:O(h)
search:O(h)
delete:O(h)
有 n 个结点的二叉树,高度最低时 h = log2n (完全二叉树),高度最高时 h = n (退化成单链表)。但是我们上面实现的 BST 并不能保证树的高度为 O(logn),更糟糕的是,随着动态的插入和删除元素,整棵树会慢慢地向一边倾斜。
要想保证二叉树增加,查找,删除的时间复杂度为 O(logn),我们需要在添加结点和删除结点后,做一些调整操作,以保证二叉树的平衡。
常见的平衡二叉树有:AVL树、红黑树、伸展树、树堆等。其中应用最广,名气最大的当属红黑树了。
红黑树
2-3-4树(理论模型)
2-3-4 树有以下两个性质:
-
2-3-4 树,在普通的二叉查找树上进行了扩展,它允许一个结点包含多个 key 值。
2-结点:一个 key 值,两个孩子。
3-结点:两个 key 值,三个孩子。
4-结点:三个 key 值,四个孩子
-
2-3-4树可以动态地保持完美平衡。
所谓完美平衡,就是从根结点到任意一个叶子结点的路径都是一样长的。

查找
2-3-4 树的查找和普通 BST 的查找方式几乎一致。

插入
2-3-4 树的插入和普通 BST 相比,稍微复杂一点。
-
如果是在2-结点中插入,直接将2-结点转换为3-结点。

-
如果是在3-结点中插入,直接将3-结点转换为4-结点。

-
但是,如果是在4-结点中插入,该怎么办呢?

这就需要将4-结点进行分裂了。

但是,如果父结点也是4-结点,又该怎么办呢?
有两种解决方案:(不变式:当前结点肯定不是4-结点。)
a. 自底向上(Bayer, 1972) 用同样的方法分裂父结点
如果需要,我们会沿着查找路径自底向上依次分裂4-结点
b. 自顶向下(Guibas-Sedgewick, 1978)
在查找插入位置的时候,遇到4-结点就分裂
找到插入位置的时候,就可以直接插入了(当前结点肯定不是4-结点)

2-3-4树生长的例子

性能分析
2-3-4树增加,删除,查找操作的时间复杂度取决于它的高度 h.
- 最坏情况:h = log2N [所有结点都是2-结点]
- 最好情况:h = log4N = (1/2)log2N [所有结点都是4-结点]
2-3-4树的性能是非常优秀的,举个例子
- N = 100万时,2-3-4树的高度在 10 到 20 之间。
- N = 10亿时,2-3-4树的高度在 15 到 30 之间。
实现
那我们如何实现 2-3-4 树呢?如果为2-结点,3-结点,4-结点分别编写不同的结点类型,则处理起来会非常麻烦,我们不得不处理各个结点类型之间得转换。那 2-3-4 树有没有一种更好得实现方式呢?
有的,答案就是红黑树!
红黑树(实际实现)
我们可以用普通的 BST 来表示 2-3-4 树。可是,我们该如何表示3-结点和4-结点呢?
红黑树给出了一个很好的解决方案,我们可以用"红色"的边来表示3-结点和4-结点。如下图所示:

3-结点有两种表示形式,而4-结点只有一种表示形式。
可是"边"是不存在的呀,它只是逻辑上的一个结构,我们又该如何表示边的颜色呢?
孩子结点到父亲结点的边是唯一的,所以我们可以用孩子结点的颜色,来表示孩子结点到父亲结点的边的颜色。
typedef struct treenode_s {int val;struct treenode_s* left;struct treenode_s* right;bool color;
}
这就是红黑树!
我们来看一看经典教科书(算法导论)对红黑树的定义:
一棵红黑树是满足下面红黑性质的二叉搜索树:
1. 每个结点或者是红色的,或者是黑色的
2. 根结点是黑色的
3. 叶子结点 (Nil) 是黑色的 (注:在算法导论中,叶子结点指得是 NULL 结点)
4. 如果一个结点是红色的,则它的两个子结点都是黑色的 (4-node 只有一种编码方式)
5. 对每个结点,从该结点到其所有后代叶子结点的路径上,包含相同数目的黑色结点。(黑高平衡, 2-3-4树是一个完美平衡的树)
2-3-4 树和红黑树之间是有一张对应关系的,不过这种对应关系不是 1-1 的 (3-结点可以倾向任意一边)。

相关文章:
二叉树(C语言版)
文章目录 二叉树完全二叉树和满二叉树二叉搜索树基本操作实现代码运行结果 分析红黑树2-3-4树(理论模型)红黑树(实际实现) 二叉树 树是一种层次结构,它在现实生活中是广泛存在的,比如:族谱(family tree),组织机构,目录…...
ASP.NET Core 面试宝典【刷题系列】
文章目录 引言1、什么是 dot net core 的 startup class?2、什么是中间件?3、application builder 的 use 和 run 方法有什么区别?4、dot net core 管道里面的map拓展有什么作用?5、dot net core 里面的路径是如何处理的?6、如何在 dot net core 中激活 session 功能?7、…...
案例-02.部门管理-查询
一.查询部门-需求 二.查询部门-思路 API接口文档 三.代码实现 1.controller层:负责与前端进行交互,接收前端所发来的请求 注:Slf4j用于记录日志使用,可以省略private static Logger log LoggerFactory.getLogger(DeptControlle…...
src和href区别
src和href区别 (1)请求资源类型不同(2)作用结果不同(3)解析方式不同 (1)请求资源类型不同 href 用来建立文档和元素之间的链接(是引用),常用的有a、linksrc 在请求src资源时候会将指向的资源下载并且应用到文档中(引入),常用的有script、iframe、image。 (2)作用结果不同 hr…...
Java每日精进·45天挑战·Day19
第一部分:移除数字以形成最小数的贪心算法实现 在编程的世界里,我们经常遇到需要对字符串表示的数字进行操作的问题。今天,我们要深入探讨一个具体的挑战:给定一个以字符串形式表示的非负整数 num 和一个整数 k,我们的…...
区块链的交易管理和共识机制
区块链的交易管理和共识机制是其核心功能,以下为你详细介绍它们的实现方式: 交易管理的实现 交易发起 • 用户使用钱包软件创建一笔交易,该交易包含发送方地址、接收方地址、转账金额等关键信息。同时,发送方会使用自己的私钥对…...
最新国内 ChatGPT Plus/Pro 获取教程
最后更新版本:20250202 教程介绍: 本文将详细介绍如何快速获取一张虚拟信用卡,并通过该卡来获取ChatGPT Plus和ChatGPT Pro。 # 教程全程约15分钟开通ChatGPT Plus会员帐号前准备工作 一个尚未升级的ChatGPT帐号!一张虚拟信用卡…...
Apollo 9.0 速度动态规划决策算法 – path time heuristic optimizer
文章目录 1. 动态规划2. 采样3. 代价函数3.1 障碍物代价3.2 距离终点代价3.3 速度代价3.4 加速度代价3.5 jerk代价 4. 回溯 这一章将来讲解速度决策算法,也就是SPEED_HEURISTIC_OPTIMIZER task里面的内容。Apollo 9.0使用动态规划算法进行速度决策,从类名…...
Apache Iceberg 与 Apache Hudi:数据湖领域的双雄对决
在数据存储和处理不断发展的领域中,数据湖仓的概念已经崭露头角,成为了一种变革性的力量。数据湖仓结合了数据仓库和数据湖的最佳元素,提供了一个统一的平台,支持数据科学、商业智能、人工智能/机器学习以及临时报告等多种关键功能…...
【LeetCode Hot100 普通数组】最大子数组和、合并区间、旋转数组、除自身以外数组的乘积、缺失的第一个正整数
普通数组 1. 最大子数组和(Maximum Subarray)解题思路动态规划的优化解法(Kadane 算法)核心思想 代码解析 2. 合并区间(Merge Intervals)解题思路代码实现 3. 旋转数组(Rotate Array)…...
共享存储-一步一步部署ceph分布式文件系统
一、Ceph 简介 Ceph 是一个开源的分布式文件系统。因为它还支持块存储、对象存储,所以很自 然的被用做云计算框架 openstack 或 cloudstack 整个存储后端。当然也可以单独作 为存储,例如部署一套集群作为对象存储、SAN 存储、NAS 存储等。 二、ceph 支…...
19.Python实战:实现对博客文章的点赞系统
Flask博客点赞系统 一个基于Flask的简单博客系统,具有文章展示和点赞功能。系统使用MySQL存储数据,支持文章展示、点赞/取消点赞等功能。 功能特点 文章列表展示文章详情查看(模态框展示)点赞/取消点赞功能(每个IP只…...
【stm32】定时器输出PWM波形(hal库)
一. PWM基本原理 PWM是一种通过调节信号的占空比(Duty Cycle)来控制输出平均电压的技术。占空比是指高电平时间与整个周期时间的比值。例如: - 占空比为50%时,输出平均电压为电源电压的一半。 - 占空比为100%时,输出始…...
当Ollama遇上划词翻译:我的Windows本地AI服务搭建日记
🚀 实现Windows本地大模型翻译服务 - 基于OllamaFlask的划词翻译实践 🛠️ 步骤概要1️⃣ python 环境准备2️⃣ Ollama 安装3️⃣ 一个 Flask 服务4️⃣ Windows 服务化封装5️⃣ 测试本地接口6️⃣ 配置划词翻译自定义翻译源7️⃣ 效果展示8️⃣ debug…...
Linux上Elasticsearch 集群部署指南
Es 集群部署 Es 集群部署 Es 集群部署 准备好三台服务器。示例使用:110.0.5.141/142/143 1、es用户和用户组创建,使用root账号 groupadd esuseradd -g es es2、将es安装包和ik分词器上传到:/home/es/目录下(任意目录都行&#…...
字节Trae使用感想(后端)
前言 昨天分享了字节哥的Trae从0到1创作模式构建一个vue前端项目,今天又来试试她的后端项目能力。不是我舔,不得不说确实不错。可惜现在曾经没有好好学习,进不了字节。既然进不了字节,那我就用字节哥的产品吧。 后面有惊喜…...
国产编辑器EverEdit - 二进制模式下观察Window/Linux/MacOs换行符差异
1 换行符格式 1.1 应用场景 稍微了解计算机历史的人都知道, 计算机3大操作系统: Windows、Linux/Unix、MacOS,这3大系统对文本换行的定义各不相同,且互不相让,导致在文件的兼容性方面存在一些问题,比如它们…...
文心一言4月起全面免费,6月底开源新模型:AI竞争进入新阶段?
名人说:莫听穿林打叶声,何妨吟啸且徐行。—— 苏轼 Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 目录 一、文心一言免费化的背后:AI成本与应用的双重驱动1️⃣成本下降,推动文心一言普及2…...
解锁机器学习算法 | 线性回归:机器学习的基石
在机器学习的众多算法中,线性回归宛如一块基石,看似质朴无华,却稳稳支撑起诸多复杂模型的架构。它是我们初涉机器学习领域时便会邂逅的算法之一,其原理与应用广泛渗透于各个领域。无论是预测房价走势、剖析股票市场波动࿰…...
如何使用Three.js制作3D月球与星空效果
目录 1. 基本设置2. 创建星空效果3. 创建月球模型4. 添加中文3D文字5. 光照与相机配置6. 动画与控制7. 响应式布局8. 结语 在本文中,我们将一起学习如何利用Three.js实现一个3D月球与星空的效果,并添加一些有趣的元素,比如中文3D文字和互动功…...
为OpenClaw构建基于时间线的知识图谱大脑:Graphiti插件实战指南
1. 项目概述:为OpenClaw构建一个基于时间线的知识大脑 如果你和我一样,长期使用OpenClaw这类AI助手进行项目协作、知识整理和深度对话,你可能会遇到一个核心痛点:对话是线性的、易逝的。一次长达数小时的头脑风暴,一旦…...
3步解锁Unity游戏无限可能:MelonLoader模组加载器深度解析
3步解锁Unity游戏无限可能:MelonLoader模组加载器深度解析 【免费下载链接】MelonLoader The Worlds First Universal Mod Loader for Unity Games compatible with both Il2Cpp and Mono 项目地址: https://gitcode.com/gh_mirrors/me/MelonLoader 你是否曾…...
ARM GIC PMU架构与中断性能监控实践
## 1. GIC PMU架构概述在现代多核SoC设计中,中断控制器(GIC)的性能监控对系统调优至关重要。GIC PMU作为ARM架构中专用的性能监控单元,其设计具有以下关键特性:- **两级监控体系**:同时支持IRS(…...
OramaCore:一体化AI应用运行时引擎部署与开发实战指南
1. 项目概述:一站式AI应用运行时引擎如果你正在构建一个需要结合搜索、推理和智能对话的应用,比如一个智能客服、一个内部知识库问答系统,或者一个能理解复杂查询的文档分析工具,那么你很可能需要同时部署和维护好几个组件&#x…...
ESP32物联网入门:用MicroPython和MicroDot做个能网页控制的智能灯(附完整代码)
ESP32物联网实战:从零搭建网页遥控智能灯系统 项目概述与核心价值 想象一下,躺在沙发上用手机浏览器就能控制客厅的灯光,这种物联网的魔力现在用ESP32开发板就能轻松实现。本项目将带你完整经历一个物联网智能灯系统的开发全流程,…...
告别混乱!用泛微E9 ESB的模块与接口管理,搭建清晰的企业服务目录
企业级ESB治理实战:用泛微E9构建高可维护的服务目录体系 当企业数字化进程加速,ERP、CRM、MES等系统间的接口数量呈指数级增长。某制造业客户曾向我展示他们的ESB平台——超过2000个未分类的接口像一团纠缠的线球,每次系统升级都像在雷区排爆…...
虚拟平台如何实现芯片早期功耗分析:从原理到工程实践
1. 虚拟平台:从功能验证到功耗分析的范式跃迁在芯片设计这个行当里干了十几年,我越来越觉得,我们很多时候都在重复一个“先造车,后测油耗”的尴尬循环。项目初期,架构师和软件工程师们基于PPT和电子表格,雄…...
Edge 浏览器保存密码真的安全吗?一次讲清“明文内存”争议、真实风险和正确防护
一、先说结论:这不是“Edge 一无是处”,而是浏览器密码管理器的老问题被放大了 这次争议之所以引起关注,不是因为“Edge 把密码明文存在硬盘上”。这点要先纠正。 Microsoft Edge 官方文档明确说明:Edge 保存的密码在磁盘上会加密…...
教育机构在AI课程实验中采用Taotoken管理学生模型调用的实践
教育机构在AI课程实验中采用Taotoken管理学生模型调用的实践 在高校或职业培训机构的AI课程中,让学生亲手调用大模型API完成实验是提升实践能力的关键环节。然而,直接让学生使用个人账户或共享密钥会带来成本不可控、权限混乱、行为难以追溯等一系列管理…...
开发者AI实战指南:从工具选型到应用落地的系统化路径
1. 项目概述:一份面向开发者的AI实战指南最近几年,AI工具的发展速度,用“日新月异”来形容都显得有些保守。作为一名在技术一线摸爬滚打了十多年的开发者,我深切感受到,从最初的惊叹于GPT-3的对话能力,到如…...
