一、几个概念
二叉树(Binary Tree),是 n(n >= 0)个结点(每个结点最多只有2棵子树)的有限集合,该集合或为空集,称为空二叉树,或由一个根节点和两颗互不相交的,称为根节点的左子树和右子树组成,且其左右子树也分别是一颗二叉树。
二叉查找树(Binary Search Tree,简称 BST),又称为二叉搜索树、有序二叉树(Ordered Binary Tree)。二叉查找树或是空二叉树,或是满足以下三个性质的二叉树。
- 若其左子树非空,则左子树上所有节点的值都小于根节点的值
- 若其右子树非空,则右子树上所有节点的值都大于根节点的值
- 其左右子树也分别是一棵二叉查找树
前序遍历二叉树(Pre-order Traversal):从根结点出发,先访问该结点,然后前序遍历该结点的左子树,再然后前序遍历该结点的右子树
中序遍历二叉树(In-order Traversal)的规则为:从根结点出发,先中序遍历该结点的左子树,然后访问该结点,再然后中序遍历该结点的右子树
后序遍历二叉树(Post-order Traversal)的规则为:从根结点出发,先后序遍历该结点的左子树,然后后序遍历该结点的右子树,再然后访问该结点
二叉链表:是二叉树的一种链式存储结构,其中每个结点包含三个字段:一个数据字段(data)和两个指针字段(*lchild、*rchild),分别指向该结点的左孩子和右孩子。如果某个结点没有左孩子或右孩子,那么对应的指针字段为NULL。
用 C 语言可以表述为:
typedef struct BST_node {
int data;
struct BST_node *lchild, *rchild;
}*BST;
二、二叉查找树的特性
通过中序遍历二叉查找树,得到的序列是有序的递增序列
如下图所示的一颗二叉查找树,其中序遍历的序列为:{3,6,8,10,15,20}
三、二叉查找树的查找
因为通过中序遍历二叉查找树,得到的序列是有序的递增序列,因此二叉查找树的查找跟二分查找法类似。
假设有一棵二叉查找树:T,每个结点有数据字段(data),有两个指针字段(*lchild、*rchild),分别指向结点的左孩子和右孩子。查找 data,如果找到则返回该结点,否则返回 NULL
1、算法思路
- 如果二叉查找树为空树,则查找失败,返回 NULL
- 如果二叉查找树不为空
(1)如果 T->data == data,则返回该结点
(2)如果 T->data > data,则递归查找左子树
(3)如果 T->data < data,则递归查找右子树
时间复杂度
- 最好的情况,根结点就是要查找的结点,所以最好时间复杂度为: O(1)
- 最差的情况,二叉查找树退化为链表时,所以最差时间复杂度为: O(n)
- 平均时间复杂度为:O()
2、实现代码
#include <stdio.h>
#include <stdlib.h>
// 扩展二叉树前序序列
char *g_bad_str = "7654321########";
char *g_average_str = "421##3##65##7##";
int g_index = 0;
char *g_str = NULL;
int g_search_times = 0;
typedef enum BST_TYPE {
BAD,
AVERAGE
}BST_TYPE;
typedef struct BST_node {
int data;
struct BST_node *lchild, *rchild;
}*BST;
// 二叉查找树查找
BST bst_search(BST const T, const char data) {
if (!T)
return NULL;
g_search_times++;
if (T->data == data)
return T;
else if (T->data > data)
return bst_search(T->lchild, data);
else
return bst_search(T->rchild, data);
}
void bst_create(BST *T, BST_TYPE type) {
if (type == BAD)
g_str = g_bad_str;
else
g_str = g_average_str;
if (g_str[g_index] == '#') {
*T = NULL;
g_index++;
} else {
*T = malloc(sizeof(**T));
(*T)->data = g_str[g_index];
(*T)->lchild = NULL;
(*T)->rchild = NULL;
g_index++;
bst_create(&(*T)->lchild, type);
bst_create(&(*T)->rchild, type);
}
}
void bst_destroy(BST T) {
if (!T)
return;
bst_destroy(T->lchild);
bst_destroy(T->rchild);
printf("free %c\n", T->data);
free(T);
}
int main(int argc, char *argv[]) {
BST tree_bad = NULL;
BST tree_average = NULL;
BST tree_search = NULL;
g_index = 0;
bst_create(&tree_bad, BAD);
g_index = 0;
bst_create(&tree_average, AVERAGE);
g_search_times = 0;
char data = '1';
tree_search = bst_search(tree_bad, data);
if (tree_search)
printf("bad search_times = %d, data = %c\n", g_search_times, tree_search->data);
else
printf("can't find %c, bad search_times = %d\n", data, g_search_times);
g_search_times = 0;
data = '7';
tree_search = bst_search(tree_average, data);
if (tree_search)
printf("average search_times = %d, data = %c\n", g_search_times, tree_search->data);
else
printf("can't find %c, average search_times = %d\n", data, g_search_times);
printf("----------\n");
bst_destroy(tree_bad);
printf("----------\n");
bst_destroy(tree_average);
return 0;
}
四、二叉查找树的插入
因为通过中序遍历二叉查找树,得到的序列是有序的递增序列,因此二叉查找树的插入跟二分插入法类似。
假设有一个二叉查找树指针:T,每个结点有数据字段(data),有两个指针字段(*lchild、*rchild),分别指向结点的左孩子和右孩子,并将数据 data 插入到其中。
1、算法思路
(1)如果二叉查找树指针 T 为 NULL,则创建新结点,并插入到当前位置
(2)如果 T->data > data,则递归插入到左子树中
(3)如果 T->data <= data,则递归插入到右子树中
时间复杂度
- 最好的情况,根结点为空时,所以最好时间复杂度为: O(1)
- 最差的情况,二叉查找树退化为链表时,所以最差时间复杂度为: O(n)
- 平均时间复杂度为:O()
2、实现代码
#include <stdio.h>
#include <stdlib.h>
int g_insert_times = 0;
typedef struct BST_node {
int data;
struct BST_node *lchild, *rchild;
}*BST;
// 二叉查找树插入
void bst_insert(BST *T, int data) {
g_insert_times++;
if (!(*T)) {
*T = malloc(sizeof(**T));
(*T)->data = data;
(*T)->lchild = NULL;
(*T)->rchild = NULL;
printf("insert %c success, insert times = %d\n", data, g_insert_times);
} else if ((*T)->data > data) {
bst_insert(&(*T)->lchild, data);
} else {
bst_insert(&(*T)->rchild, data);
}
}
void bst_destroy(BST T) {
if (!T)
return;
bst_destroy(T->lchild);
bst_destroy(T->rchild);
printf("free %c\n", T->data);
free(T);
}
void visit_node(BST T) {
printf("%c\n", T->data);
}
void in_order_tree(BST T) {
if (!T)
return;
in_order_tree(T->lchild);
visit_node(T);
in_order_tree(T->rchild);
}
void bst_test(BST *T) {
g_insert_times = 0;
bst_insert(T, '4');
g_insert_times = 0;
bst_insert(T, '2');
g_insert_times = 0;
bst_insert(T, '6');
g_insert_times = 0;
bst_insert(T, '1');
g_insert_times = 0;
bst_insert(T, '5');
g_insert_times = 0;
bst_insert(T, '3');
g_insert_times = 0;
bst_insert(T, '7');
}
void bst_test1(BST *T) {
g_insert_times = 0;
bst_insert(T, '7');
g_insert_times = 0;
bst_insert(T, '6');
g_insert_times = 0;
bst_insert(T, '5');
g_insert_times = 0;
bst_insert(T, '4');
g_insert_times = 0;
bst_insert(T, '3');
g_insert_times = 0;
bst_insert(T, '2');
g_insert_times = 0;
bst_insert(T, '1');
}
int main(int argc, char *argv[]) {
BST T = NULL;
BST T1 = NULL;
bst_test(&T);
printf("-------------\n");
bst_test1(&T1);
printf("---------\n");
bst_destroy(T);
printf("---------\n");
bst_destroy(T1);
return 0;
}
五、二叉查找树的删除
1、算法思路
(1)如果被删除结点的左子树为空时,将该结点的父结点中,让指向该结点的指针域指向该结点的右子树,然后删除该结点
(2)如果被删除结点的右子树为空时,将该结点的父结点中,让指向该结点的指针域指向该结点的左子树,然后删除该结点
(3)如果被删除结点的左右子树都不为空时,情况复杂的多,因为父结点的左指针或右指针不能同时指向被删除结点的左右子树。我们可以采用
- 合并删除法
合并删除法:将被删除结点的左右子树合并得到一棵树,然后把这棵树挂到被删除结点的父结点中。
如何将被删除结点的左右子树合并得到一棵树呢?
因为二叉查找树的特性是:左子树的每个结点的值都比右子树每个结点的值小,所以,可以把被删除结点的左子树中值最大的结点(左子树中,沿着右结点找,结点的右指针为空的结点)作为被删除结点的右子树的父结点 或 被删除结点的右子树中,值最小的结点(右子树中,沿着左结点找,结点的左指针为空的结点)作为被删除结点的左子树的父结点,从而合并得到一棵树,然后挂到被删除结点的父结点中。
时间复杂度
- 最好的情况,被删除的结点是根结点,且二叉查找树退化为链表时,此时查找的时间复杂度为 O(1),删除的时间复杂度为 O(1),所以最好时间复杂度为: O(1)
- 最差的情况,二叉查找树退化为链表时,此时查找的时间复杂度为 O(n),删除的时间复杂度为 O(1),所以最差时间复杂度为: O(n)
- 平均时间复杂度为:O()
2、代码实现
#include <stdio.h>
#include <stdlib.h>
typedef struct BST_node {
int data;
struct BST_node *lchild, *rchild;
}*BST;
// 二叉查找树查找
BST* bst_search(BST* const T, int data) {
if (!(*T))
return NULL;
if ((*T)->data == data)
return T;
else if ((*T)->data > data)
return bst_search(&(*T)->lchild, data);
else
return bst_search(&(*T)->rchild, data);
}
// 二叉查找树插入
void bst_insert(BST *T, int data) {
if (!(*T)) {
*T = malloc(sizeof(**T));
(*T)->data = data;
(*T)->lchild = NULL;
(*T)->rchild = NULL;
} else if ((*T)->data > data) {
bst_insert(&(*T)->lchild, data);
} else {
bst_insert(&(*T)->rchild, data);
}
}
void bst_destroy(BST T) {
if (!T)
return;
bst_destroy(T->lchild);
bst_destroy(T->rchild);
free(T);
}
void visit_node(BST T) {
printf("%d\n", T->data);
}
void in_order_tree(BST T) {
if (!T)
return;
in_order_tree(T->lchild);
visit_node(T);
in_order_tree(T->rchild);
}
void bst_create1(BST *T) {
// 2346
bst_insert(T, 4);
bst_insert(T, 2);
bst_insert(T, 6);
bst_insert(T, 3);
in_order_tree(*T);
printf("----------\n");
}
void bst_create2(BST *T) {
// 1246
bst_insert(T, 4);
bst_insert(T, 2);
bst_insert(T, 6);
bst_insert(T, 1);
in_order_tree(*T);
printf("----------\n");
}
void bst_create3(BST *T) {
// 12345678
bst_insert(T, 7);
bst_insert(T, 4);
bst_insert(T, 8);
bst_insert(T, 2);
bst_insert(T, 6);
bst_insert(T, 1);
bst_insert(T, 3);
bst_insert(T, 5);
in_order_tree(*T);
printf("----------\n");
}
void bst_delete_by_merge(BST *T, int data) {
BST* del_node = bst_search(T, data);
if (!(*del_node))
return;
BST tmp = (*del_node);
if (!(*del_node)->lchild) // 左孩子为空
*del_node = (*del_node)->rchild;
else if (!(*del_node)->rchild) // 右孩子为空
*del_node = (*del_node)->lchild;
else { // 左右子树都不为空
// 把被删除结点左子树中值最大的结点(左子树中,沿着右结点找,结点的右指针为空的结点)作为被删除右子树的父结点
BST rmax = (*del_node)->lchild;
while(rmax->rchild) {
rmax = rmax->rchild;
}
rmax->rchild = (*del_node)->rchild;
// 挂到被删除结点的父结点中
*del_node = (*del_node)->lchild;
}
printf("delete %d success\n", tmp->data);
free(tmp);
}
int main(int argc, char *argv[]) {
BST T1 = NULL;
BST T2 = NULL;
BST T3 = NULL;
bst_create1(&T1);
bst_delete_by_merge(&T1, 2);
in_order_tree(T1);
printf("--------\n");
bst_create2(&T2);
bst_delete_by_merge(&T2, 2);
in_order_tree(T2);
printf("--------\n");
bst_create3(&T3);
bst_delete_by_merge(&T3, 4);
in_order_tree(T3);
printf("--------\n");
bst_destroy(T1);
bst_destroy(T2);
bst_destroy(T3);
return 0;
}