树
树是n ( n>=0 )个结点的有限集。n=0时称为空树。在任意一棵非空树中:
- 有且仅有一个特定的称为根(Root)的结点;
- 当n>1时,其余结点可分为m (m>0)个互不相交的有限集,其中每一个集合本身又是一棵树,并且称为根的子树;
- n>0时根结点是唯一的,不可能存在多个根结点;
- m>0时,子树的个数没有限制,但它们一定是互不相交的;
如下图两个就不是树因为它们都有相交的子树;
下面讲解树中的几个名词:
- 度:结点拥有的子树的个数称为结点的度,树的度是树内各结点的度的最大值。
- 叶结点:度为0的结点称为叶结点(Leaf)或终端结点
- 分支结点:度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。
- 深度:这是之树的最大层次根为第一层,根的孩子为第二层以此类推
- 孩子:结点的子树的根称为该结点的孩子(Child)
- 双亲:孩子节点的上一级结点叫做双亲
- 兄弟 :同一个双亲的孩子之间互称兄弟
图中的G H I J为叶子节点,B C D E F为分支结点也叫做内部结点,整颗树的度为3因为节点D的度为3
二叉树
二叉树是一种特殊的树,它的每个节点最大只能由两个度,如下图所示
- 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的。
- 左子树和右子树是有顺序的,次序不能任意颠倒。就像人是双手、双脚,但显然左手、左脚和右手、右脚是不一样的,右手戴左手套、右脚穿左鞋都会极其别扭和难受。
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
下面再介绍二叉树的几种特殊形态:
- 斜树:顾名思义,斜树一定要是斜的,但是往哪斜还是有讲究。所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树
- 满二叉树 :在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在 同一层上,这样的二叉树称为满二叉树。
- 叶子只能出现在最下一层。出现在其他层就不可能达成平衡。
- 非叶子结点的度一定是2。否则就是"缺胳膊少腿" 了。
- 在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
- 完全二叉树 :对一棵具有n个结点的二叉树按层序编号,如果编号为i (1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树,对于完全二叉树最重要的一点是,每一层的编号必须是连续的不能由断
- 叶子结点只能出现在最下两层。
- 最下层的叶子一定集中在左部连续位置。
- 倒数二层,若有叶子结点,一定都在右部连续位置。
- 如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情.况。
- 同样结点数的二叉树,完全二叉树的深度最小。
二叉树的性质
-
在二叉树的第i层上至多有
2的i-1次
方个结点(i>=1); -
深度为k的二叉树至多有
2的k次方-1
个结点(k>=1); -
对任何一棵二叉树T,如果其终端结点数为N,度为2的结点数为U,则 N=U+1
-
具有n个结点的完全二叉树的深度为
|log2n| + 1
-
如果对一棵有n个结点的完全二叉树(其深度为
|log2n| + 1
)的结点按层序编号(从第1层到第|log2n| + 1
层,每层从左到右),对任一结点i (1<=i<=n) 有: -
如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点
|i/2|
-
如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i
-
如果2i + 1>n,则结点i无右孩子;否则其右孩子是结点2i + 1。
二叉树的存储形式
- 顺序存储
顺序存储是一种适合完全二叉树的存储方式,完全二叉树按层序编号后,每个结点的编号对应了数组中的索引
![[完全二叉树顺序存储.jpg]]
考虑一种极端的情况,一棵深度为k的右斜树,它只有k个结点,却需要分配2k-1个存储单元空间,这显然是对存储空间的浪费。所以,顺序存储结构一般只用于完全二叉树。 - 二叉链表
我们将一个二叉树结点的数据结构定义为如下:
cpp
struct BinaryTreeNode{
T value;
BinaryTreeNode* left;
BinaryTreeNode* right;
BinaryTreeNode(int val) : data(val), left(nullptr), right(nullptr) {}
};
一个节点包含值,与指向左右孩子的指针,当没有某个孩子时我们只需要将对应的节点置为空就可以,这就解决了顺序存储的空间两位问题
下面演示一个简单二叉树的实现:
cpp
class BinaryTree {
public:
BinaryTree(BinaryTreeNode* node) : root(node) {}
~BinaryTree() { clear(root); }
// 创建新节点(用户需自行管理节点连接)
BinaryTreeNode* createNode(int data) {
return new BinaryTreeNode(data);
}
BinaryTreeNode* getRoot() const {
return root;
}
void setLeftChild(BinaryTreeNode* parent, BinaryTreeNode* child) {
if (parent) parent->left = child;
}
void setRightChild(BinaryTreeNode* parent, BinaryTreeNode* child) {
if (parent) parent->right = child;
}
private:
BinaryTreeNode* root;
// 递归释放节点内存
void clear(BinaryTreeNode* node) {
if (node) {
clear(node->left);
clear(node->right);
delete node;
}
}
};
root这是一个指向 BinaryTreeNode
类型的指针,代表二叉树的根节点。由于它被声明为私有成员,外部代码无法直接访问,只能通过类提供的公共方法来操作。createNode
该方法接收一个整数 data
作为参数,使用 new
运算符动态分配一个新的 BinaryTreeNode
对象,并将 data
作为节点的值进行初始化。最后返回指向该节点的指针。需要注意的是,用户需要自行管理这些节点之间的连接关系。
setLeftChild
与setRightChild
负责将指定结点,放到parent结点的左孩子或者右孩子中
cpp
BinaryTree tree(new BinaryTreeNode(1));
BinaryTreeNode* root = tree.getRoot();
BinaryTreeNode* node2 = tree.createNode(2);
BinaryTreeNode* node3 = tree.createNode(3);
BinaryTreeNode* node4 = tree.createNode(4);
BinaryTreeNode* node5 = tree.createNode(5);
// 连接节点
tree.setLeftChild(root, node2);
tree.setRightChild(root, node3);
tree.setLeftChild(node2, node4);
tree.setRightChild(node2, node5);
// 构建树结构:
// 1
// / \
// 2 3
// / \
// 4 5
遍历二叉树
二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
- 前序遍历
先访问根结点,然后前序遍历左子树,再前序遍历右子树。
访问顺序 :根节点 → 左子树 → 右子树
特点 :优先访问当前节点,再递归遍历左、右子树。
应用 :常用于复制二叉树结构(先创建父节点,再复制子节点),或生成表达式的前缀表示 (如+ a * b c
)。
cpp
void _preorderTraversal(BinaryTreeNode* root){
if (root == nullptr) return;
std::cout << root->data << " ";
_preorderTraversal(root->left);
_preorderTraversal(root->right);
}
前序遍历是一种深度优先遍历方式,其访问顺序为根节点、左子树、右子树。函数首先检查 root
是否为空指针,如果为空则直接返回,因为空树没有节点可遍历。若 root
不为空,函数会先输出当前根节点的数据,也就是打印 root->data
的值,这体现了前序遍历中先访问根节点的特点。接着,函数递归调用自身,对左子树进行前序遍历,即 _preorderTraversal(root->left)
,以同样的规则去遍历左子树中的所有节点。
最后,函数再次递归调用自身对右子树进行前序遍历,即 _preorderTraversal(root->right)
,完成整个子树的前序遍历操作。通过不断递归调用,该函数可以遍历整棵二叉树并按前序顺序输出所有节点的数据。
- 中序遍历
从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。
访问顺序 :左子树 → 根节点 → 右子树
特点 :先遍历左子树到底,再回溯访问根节点,最后处理右子树。
应用 :在二叉搜索树(BST)中,中序遍历会按升序输出节点 (如排序结果)。也可用于生成表达式的中缀表示 (如a + b * c
)。
cpp
void _inorderTraversal(BinaryTreeNode* root){
if (root == nullptr) return;
_inorderTraversal(root->left);
std::cout << root->data << " ";
_inorderTraversal(root->right);
}
中序遍历函数先检查 root
是否为空指针,若为空则直接返回,因为空树没有节点可遍历。若 root
不为空,函数会先递归调用自身对左子树进行中序遍历,即执行 _inorderTraversal(root->left)
,以相同规则遍历左子树中的所有节点;接着输出当前根节点的数据,也就是打印 root->data
的值,这符合中序遍历先访问左子树,再访问根节点的特点;最后,函数再次递归调用自身对右子树进行中序遍历,即执行 _inorderTraversal(root->right)
,完成整个子树的中序遍历操作。通过不断递归调用,该函数能够遍历整棵二叉树并按中序顺序输出所有节点的数据。
- 后序遍历
从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。
访问顺序 :左子树 → 右子树 → 根节点
特点 :最后访问根节点,确保子节点处理完成后才处理父节点。
应用 :适合需要先删除子节点再删除父节点 的场景(如释放二叉树内存),或计算表达式的后缀表示 (如a b c * +
)。
cpp
void _postorderTraversal(BinaryTreeNode* root){
if (root == nullptr) return;
_postorderTraversal(root->left);
_postorderTraversal(root->right);
std::cout << root->data << " ";
}
函数先检查 root
是否为空,若为空指针则直接返回,因为空树没有节点可处理。若 root
不为空,函数先递归调用自身对左子树进行后序遍历,即 _postorderTraversal(root->left)
,按同样规则遍历左子树所有节点;接着又递归调用自身对右子树进行后序遍历,也就是 _postorderTraversal(root->right)
;最后输出当前根节点的数据,即打印 root->data
的值,这符合后序遍历先访问左子树、再访问右子树、最后访问根节点的顺序。通过不断递归,该函数可遍历整棵二叉树并按后序顺序输出所有节点的数据。
- 层序遍历
从树的第一层,也就是根结点开始访问, 从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。
访问顺序 :按层从上到下、每层从左到右访问
特点 :使用队列 逐层记录节点,实现广度优先搜索(BFS)。
应用 :适合计算树的层级属性 (如最大深度、最小深度),或按层输出节点(如打印树的结构)。
cpp
void levelorderTraversal(){
if (root == nullptr) return;
std::queue<BinaryTreeNode*> q;
q.push(root);
while (!q.empty()) {
BinaryTreeNode* node = q.front();
q.pop();
std::cout << node->data << " ";
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
}
层序遍历是按照二叉树的每一层,从左到右依次访问节点。函数首先检查二叉树的根节点 root
是否为空,若为空则直接返回,因为空树没有节点可遍历。接着,函数创建了一个 std::queue
类型的队列 q
,并将根节点 root
入队。之后进入一个循环,只要队列不为空,就从队列头部取出一个节点 node
,并将其从队列中移除,然后输出该节点的数据 node->data
。随后,检查该节点是否有左子节点,如果有则将左子节点入队;再检查该节点是否有右子节点,如果有则将右子节点入队。这样,队列中始终存储着当前层和下一层待访问的节点,且能保证按层从左到右的顺序依次访问节点,直至队列为空,完成整个二叉树的层序遍历。
二叉搜索树(BST树)
二叉查找树简写BST,是满足某些条件的特殊二叉树。任何一个节点的左子树上的点,都必须小于当前节点。任何一个节点的右子树上的点,都必须大于当前节点。任何一棵子树,也都满足上面两个条件。另外二叉查找树中,是不存在重复节点 的。
二叉搜索树必须满足以下条件:
- 有序性 :对于树中的任意节点:
- 其左子树 上的所有节点值 ≤ 该节点的值。
- 其右子树 上的所有节点值 ≥ 该节点的值。
- 递归性 :左子树和右子树本身也必须是二叉搜索树。
对于二叉搜索树若插入数据本身有序(如 1, 2, 3, 4
),BST 会退化为链表 ,时间复杂度退化为 O(N) 。这时候可以使用平衡二叉搜索树 (如AVL树、红黑树),强制保持树高接近 log N
。
- 查找
从根节点开始,逐层比较目标值与当前节点 - 若目标值 < 当前节点值 → 进入左子树。
- 若目标值 > 当前节点值 → 进入右子树。
- 若相等 → 找到目标节点。
cpp
BinaryTreeNode* _search(BinaryTreeNode* root, int data){
if (root == nullptr || root->data == data){
return root;
}
if (data < root->data){
return _search(root->left, data);
}else{
return _search(root->right, data);
}
}
- 插入
类似查找,找到合适的位置插入新节点,保持有序性。
cpp
void _insert(BinaryTreeNode* root, BinaryTreeNode* node){
if (node->data < root->data){//左
if (root->left == nullptr) {
root->left = node;
return;
}
_insert(root->left, node);
}else if (node->data > root->data){//右
if (root->right == nullptr) {
root->right = node;
return;
}
_insert(root->right, node);
}else{
std::cout << "Data already exists" << std::endl;
}
}
- 删除
删除分为三种情况:- 叶子节点:直接删除。
- 有一个子节点:用子节点替换被删除节点。
- 有两个子节点:找到右子树的最小节点(或左子树的最大节点)替换被删除节点,再递归删除该最小/大节点。
cpp
void _remove(BinaryTreeNode* root, int data, BinaryTreeNode* parent = nullptr){
if (root == nullptr){
return;
}
if (data < root->data){
return _remove(root->left, data, root);
}else if (data > root->data){
return _remove(root->right, data, root);
}else{
if (root->left == nullptr && root->right == nullptr) { // 叶子节点
if (parent->left == root) {
parent->left = nullptr;
} else {
parent->right = nullptr;
}
delete root;
return;
} else if (root->left != nullptr && root->right != nullptr) {
BinaryTreeNode* minNode = _findMin(root->right);
root->data = minNode->data;
_remove(root->right, minNode->data, root);
} else if (root->left != nullptr || root->right != nullptr) {
BinaryTreeNode* child =
root->left != nullptr ? root->left : root->right;
*root = *child;
delete child;
}
}
}
函数接收三个参数,root
是当前子树的根节点,data
是要删除的节点的数据值,parent
是 root
的父节点,默认为 nullptr
。函数首先检查 root
是否为空,若为空则直接返回,因为空树中不存在要删除的节点。若 data
小于 root
的数据值,说明要删除的节点在左子树中,递归调用 _remove
函数继续在左子树中查找并删除。若 data
大于 root
的数据值,表明要删除的节点在右子树中,递归调用 _remove
函数在右子树中查找并删除。
当 data
等于 root
的数据值时,说明找到了要删除的节点,此时分三种情况处理:若该节点是叶子节点(即没有左右子节点),根据它是父节点的左子节点还是右子节点,将父节点对应的子节点指针置为 nullptr
,然后删除该节点;若该节点有左右两个子节点,先调用 _findMin
函数找到其右子树中的最小节点,将该最小节点的数据值赋给当前节点,再递归调用 _remove
函数在右子树中删除该最小节点;若该节点只有一个子节点,将该子节点的数据和指针信息复制到当前节点,然后删除该子节点。通过这种方式,函数能正确地从二叉搜索树中删除指定数据的节点。
完整代码如下:
cpp
#ifndef _TEST_H
#define _TEST_H
#include <iostream>
#include <vector>
#include <functional>
#include <queue>
#include <vector>
#include <iomanip>
// 二叉树节点结构
struct BinaryTreeNode {
int data;
BinaryTreeNode* left;
BinaryTreeNode* right;
BinaryTreeNode(int val) : data(val), left(nullptr), right(nullptr) {}
};
class BinarySearchTree {
public:
BinarySearchTree(BinaryTreeNode* node) : _rootNode(node) {}
~BinarySearchTree() { clear(_rootNode); }
void insert(int data) {
BinaryTreeNode* node = new BinaryTreeNode(data);
_insert(_rootNode, node);
}
BinaryTreeNode* search(int data) {
return _search(_rootNode, data);
}
void remove(int data) {
_remove(_rootNode, data);
}
void inorderTraversal(){
_inorderTraversal(_rootNode);
std::cout << std::endl;
}
void levelorderTraversal(){
if (_rootNode == nullptr) return;
std::queue<BinaryTreeNode*> q;
q.push(_rootNode);
while (!q.empty()) {
BinaryTreeNode* node = q.front();
q.pop();
std::cout << node->data << " ";
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
std::cout << std::endl;
}
private:
void clear(BinaryTreeNode* node) {
if (node) {// 递归释放节点内存
clear(node->left);
clear(node->right);
delete node;
}
}
void _insert(BinaryTreeNode* root, BinaryTreeNode* node){
if (node->data < root->data){//左
if (root->left == nullptr) {
root->left = node;
return;
}
_insert(root->left, node);
}else if (node->data > root->data){//右
if (root->right == nullptr) {
root->right = node;
return;
}
_insert(root->right, node);
}else{
std::cout << "Data already exists" << std::endl;
}
}
void _remove(BinaryTreeNode* root, int data, BinaryTreeNode* parent = nullptr){
if (root == nullptr){
return;
}
if (data < root->data){
return _remove(root->left, data, root);
}else if (data > root->data){
return _remove(root->right, data, root);
}else{
if (root->left == nullptr && root->right == nullptr) { // 叶子节点
if (parent->left == root) {
parent->left = nullptr;
} else {
parent->right = nullptr;
}
delete root;
return;
} else if (root->left != nullptr && root->right != nullptr) {
BinaryTreeNode* minNode = _findMin(root->right);
root->data = minNode->data;
_remove(root->right, minNode->data, root);
} else if (root->left != nullptr || root->right != nullptr) { // 只有一个节点
BinaryTreeNode* child = root->left != nullptr ? root->left : root->right;
*root = *child;
delete child;
}
}
}
BinaryTreeNode* _findMin(BinaryTreeNode* root){
if (root == nullptr) return nullptr;
while (root->left != nullptr) {
root = root->left;
}
return root;
}
BinaryTreeNode* _search(BinaryTreeNode* root, int data){
if (root == nullptr || root->data == data){
return root;
}
if (data < root->data){
return _search(root->left, data);
}else{
return _search(root->right, data);
}
}
void _inorderTraversal(BinaryTreeNode* root){
if (root == nullptr) return;
_inorderTraversal(root->left);
std::cout << root->data << " ";
_inorderTraversal(root->right);
}
private:
BinaryTreeNode* _rootNode;
};
#endif