目录
二叉树的定义
二叉树是每个节点最多只有两个子树的数结构,两个子树称为左子树和右子树。
二叉树的分类
1. 完全二叉树
假设其深度为 d(d>1)。除了第 d 层外,其它各层的节点数目均已达最大值,且第 d 层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树。
2. 满二叉树
所有叶子节点全都出现在最底层的完全二叉树就叫满二叉树。就相当于这棵树的第一层是 1 个节点,第二层是 2 个节点,第三层是 4 个节点,第五层是 8 个节点,以此类推。
3. 斜树
分为左斜树和右斜树:
- 左斜树:所有节点都只有左子树
- 右斜树:所有节点都只有右子树
4. 二叉搜索树
若它的左子树不为空,则左子树上所有节点的值均小于它的根节点的值;若它的右子树不为空,则右子树上所有节点的值均大于它的根节点的值;它的左、右子树也分别是二叉排序树。说明它是一颗有顺序的树,左子树节点的值小于根节点的值,右子树节点的值大于根节点的值。
5. 平衡二叉树
它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。
二叉树的存储方式
以下面这棵树为例:
1. 数组存储
用一维数组存储二叉树中的节点,并且节点的存储位置,也就是数组的下标要能体现节点之间的逻辑关系。如果某个节点的索引为 i,(假设根节点的索引为 0)则在它左子节点的索引会是 2i+1,以及右子节点会是 2i+2。
2. 链表存储
在二叉树中,一个父节点最多只允许 2 个子节点,所以我们只需要一个存储数据和左右子节点的指针,这样的结构就是链式存储,也叫二叉链表。
二叉树的遍历方式
以下面这棵树为例:
1. 前序遍历
先访问根节点,然后前序遍历左子树,再前序遍历右子树。根据上面的二叉树前序遍历结果是 ECBADGFH。
2. 中序遍历
从根节点开始(注意并不是先访问根节点),中序遍历根节点的左子树,然后是访问根节点,最后中序遍历右子树。根据上面的二叉树中序遍历结果是 ABCDEFGH。
3. 后序遍历
从左到右先叶子节点后父节点的方式遍历访问左右子树,最后是访问根节点。根据上面的二叉树后序遍历结果是 ABDCFHGE。
4. 层序遍历
从树的第一层,也就是根节点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对节点逐个访问。根据上面的二叉树层序遍历结果是 ECGBDFHA。
二叉树的代码实现
树结构用代码实现:
struct bstree_node
{
int data;
struct bstree_node* left;
struct bstree_node* right;
};
struct bstree
{
struct bstree_node* root;
};
struct bstree* tree;
不过这样写使数据结构和业务耦合度太高,可以用模板的方式隔离:
#define BSTRRR_ENTRY(NAME, TYPE) \
struct NAME{ \
struct TYPE *left; \
struct TYPE *right; \
} \
// 比如一个协程有多种状态
struct bstree_node
{
BSTRRR_ENTRY(, bstree_node) ready;
BSTRRR_ENTRY(, bstree_node) wait;
BSTRRR_ENTRY(, bstree_node) sleep;
int data;
};
struct bstree
{
struct bstree_node* root;
};
创建一个节点:
struct bstree_node *bstree_create_node(int key)
{
struct bstree_node* node = (struct bstree_node*)malloc(sizeof(struct bstree_node));
if(node == NULL)
{
assert(0);
}
node->data = key;
node->left = node->right = NULL;
return node;
}
插入一个节点:
bool Insert_node(struct bstree *tree, int key)
{
if(tree == NULL)
{
return false;
}
if(tree->root == NULL)
{
tree->root = bstree_create_node(key);
}
struct bstree_node* node1= tree->root;
struct bstree_node* node2= node1;
while(ndoe1)
{
node2= node1;
if(key < node1->data)
{
node1 = node1->left;
}
else if(key > node1->data)
{
node1 = node1->right;
}
else
{
return false; // 已经存在 覆盖或者丢弃
}
}
if(key < node2->data)
{
node2->left = bstree_create_node(key);
}
else
{
node2->right = bstree_create_node(key);
}
return true;
}
遍历这棵树:
int bstree_traversal(struct bstree_node *node) {
if (node == NULL) return 0;
bstree_traversal(node->left);
printf("%4d ", node->data);
bstree_traversal(node->right);
}
打印的代码在前面输出结果就是前序遍历,中间打印就是中序,最后打印就是后序遍历,上面的代码是中序遍历。前序遍历和后序遍历在实际业务应用的场景很少。
总结
由于线性查找需要遍历全部数据,在数据量大的时候效率就非常低下,到底有多低,在数据库有几百万几千万以上数据量写过查询语句的就知道有索引和没索引的区别。那为什么有索引的就那么快呢?当然数据库的索引不是用二叉树来实现的,想想如果使用二叉树来实现,那这个索引树得多高,mysql采用的是更适合查找的 B+树 来实现,当然B+树也是由二叉树进化而来。
二叉搜索树由于它是有序的,左子树节点的值小于根节点的值,右子树节点的值大于根节点的值,那么就可以利用二分查找来查找对应的值,也叫折半查找。但二叉搜索树最坏的情况下可能变异成斜树,斜树的查找时间复杂度就是 O(n),就跟线性查询没区别了。那么平衡二叉树就是来解决这个问题的,因为它限定了左右两个子树的高度差的绝对值不超过 1,当插入和删除时,不满足这种情况时,通过左旋和右旋来保持这个规则。所以,它是时间复杂度是 O(log(n))。
了解和掌握二叉树,有助于学习后面更复杂的树结构:红黑树,b+树等。
技术参考
1. 数据结构之「二叉树」