一、什么是树?
在前几篇的博文中主要讲述的是链式存储这种数据结构,它们的用途非常广泛,但是在实际的应用中,还存在着另一种非常重要的数据结构,它就是树。树的结构示意图如下所示:
![](https://img-my.csdn.net/uploads/201301/10/1357823339_1267.jpg)
上图就是一种数据结构----树,之所以在每个框中都留出空白,主要原因是这种结构如果根据上下文是能够传达一些重要的结构信息,比如我们可以作如下思考:
1、上图可以表示某公司的职能组织结构;
2、可以表示某公司从上层到底层的各种职位分布图;
3、可以表示某家族的族谱血缘关系;
4、可以表示计算机中的目录结构;
........
后面可以发挥自己的想象,只要能符合以上结构的都可以称之为树型结构。通过上述思考就可以发现树型结构的用途之广泛,它表示的是数据之间的一种层次关系,层与层之间存在着某种关系,我认为这就是树型结构的重要特点,这与其它结构有着很大的区别。
对树而言,先了解以下基本概念:
1
/ \
2 3
/ \ /
4 5 6
1、每棵树都有一个“根”,这是树的“根基”,称为root,通过root我们可以很容易的找到树上的各个支点,上图中“1”为树的root;
2、一棵树上的每个节点,它们有可能有分支,有可能没有分支,分支的数目称为分支因子。如上图中,最大的分支结因子为2,"3"结点的分支因子为1
3、每棵树都有一个高度,数据的层次数就是树的高度,上图中树的高度为3。
4、通用概念:
1与2,3之间的关系为:1是父,2是其左孩子,3是其右孩子。2与3相互之间称为兄弟。 没有孩子的结点称为叶子结点,如4\5\6结点。
二、什么是二叉树?
对树而言,需要重点掌握二叉树。二叉树是一种特殊的顺序树,它规定有左右两个孩子,即左右孩子顺序不能替换,所以二叉树是一种有序树。二叉树的结点数为大于0小于等于2。对于二叉树,需要掌握以下性质:
性质1 在二叉树的第i层上至多有
个结点(i>=1)
由数据归纳法即可证明,
i=1,结点数为1
i=2,结点数为2
i=3,结点数为4
i=4,结点数为8
I=n, 结点数为.![](https://img-my.csdn.net/uploads/201301/16/1358295381_3708.jpg)
性质2 深度为K的二叉树至多有
-1个结点(k >=1)
换言之,如果二叉树的深度确定,则其最大的结点数也是确定的。
证明(可以利用性质1)
深度为K的二叉树的结点个数=二叉树中每一层结点个数的总和。即为:
= 1 + 2 + 4 + 8 + … +
=
-1(等比公式)
性质3 二叉树中,终端结点个数
与度为2的结点个数
有如下关系:
=
+ 1
(注:度表示分支的个数,也指分支因子,终端结点也指叶子结点)
分析:二叉树中结点的度可以为0,1,2,也就是说需要证明结点的度为0与度为2的结点之间的关系是不变的。
证明:设二叉树中度为i的结点数为![](https://img-my.csdn.net/uploads/201301/16/1358295984_5011.jpg)
则整棵二叉树的结点总数为:n =
+
+
------------(1)
除根结点外,每一个结点都是另一个结点的孩子,所以孩子数为n-1 --------(2)
度为i(I = 0,1,2)的结点,有i个孩子,
孩子数 =
x 0 +
x 1 +
x 2 = 2
+
---------------(3)
因为:(3) = (2),所以,
n-1 = 2
+
------(4)
(4) – (1) ,得,
-1=
-
, 即
=
+ 1证毕.
性质1、2、3是二叉树的通用特性。
在介绍其它性质之前,先了解另一种特殊的二叉树,即满二叉树,其定义如下:
满二叉树是指深度为K,且有
-1个结点的二叉树。
特点:(1) 每层上结点数都达到最大
(2) 度为1的结点个数=0,即不存在分支数为1的结点
如下即为一棵满二叉树:(注意其顺序:结点层序编号方法,从根结点起从上至下逐层从左至右对二叉树的结点进行连续编号)
1
/ \
2 3
/ \ / \
4 5 6 7
当K = 3, 结点数
-1 = 7
完全二叉树:深度为k,结点数为n的二叉树,当且仅当每个结点的编号都与相同深度的满二叉树中从1到n的结点一一对应时,称为完全二叉树。
根据定义可以理解:深度为k的完全二叉树,其结点总数比深度k-1的满二叉树要多,但一定比深度为k的满二叉树要少。即有
:完全二叉树示意如下:
1
/ \
2 3
/ \
4 5 (注意编号顺序,与满二叉树一一对应)
性质4:结点数为n的完全二叉树,其深度为
(向下取整)+ 1
由性质2及完全二叉树的定义有:
结点数满足:
![](https://img-my.csdn.net/uploads/201301/16/1358296728_2065.jpg)
性质5:在按层序编号的n个结点的完全二叉树中,任意一个结点i(
)有:
(1) i = 1时,结点i是树的根,否则(i> 1),结点i的双亲为i/2(向下取整),如
,取i = 2.
(2) 2i > n时,结点i无左孩子,为叶结点,否则结点i的左孩子为结点2i
(3) 2i+1 > n时,结点i无右孩子,否则结点i的右孩子为结点2i +1.
性质4与性质五是针对完全二叉树而言的。性质6是针对二叉树的链式存储结构而言。
性质6: 含有n个结点的二叉链表中,有n + 1个空链域。
三、二叉树代码实现
在代码的实现中,二叉树究竟是什么?请看下面代码:
-
-
-
-
-
-
- #ifndef BITREE_H
- #define BITREE_H
-
- #include <stdlib.h>
-
-
- typedef struct BiTreeNode_
- {
- void *data;
- struct BiTreeNode_ *left;
- struct BiTreeNode_ *right;
- }BiTreeNode;
这是一段关于二叉树结点的数据结构,一共有3个域,数据域和左右指针域,数据域包含了二叉树每个结点的关键信息,左右指针域分别指向它的左右孩子结点。
- /* define a binary tree */
- typedef struct BiTree_
- {
- int size; //number of the elements in the tree.
- BiTreeNode *root; //root node.
- int (*compare)(const void *key1, const void *key2);
- void (*destroy)(void *data);
- }BiTree;
这里定义了一个结构体,这个结构体就是一棵二叉树了。因为它维护了一棵二叉树的重要信息,如,二叉树中结点总数size,根结点的位置root,结点数据信息的比较操作,销毁二叉树的destroy函数等。可以通过这个结构体的root域就可以方便的按深度及广度遍历整个二叉树,寻找到任何一个结点了。
四、深入理解二叉树
二叉树究竟是如何建立的?凡事产生均有一个过程,二叉树的建立也有一个过程。它是由不同的结点组成,按照实际情况逐一将这些结点插入从而形成二叉树,当然,也面临着结点的删除操作等,总而言之,它有以下基本操作(接口):
-
- void bitree_init( BiTree *tree, void (*destroy)(void *data) );
- void bitree_destroy(BiTree *tree);
- int bitree_ins_left(BiTree *tree, BiTreeNode *node, const void *data);
- int bitree_ins_right(BiTree *tree, BiTreeNode *node, const void *data);
- void bitree_rem_left(BiTree *tree, BiTreeNode *node);
- void bitree_rem_right(BiTree *tree, BiTreeNode *node);
- int bitree_merge(BiTree *merge, BiTree *left, BiTree *right, const void *data);
-
- #define bitree_size(tree) ((tree)->size) //获取大小
- #define bitree_root(tree) ((tree)->root) //获取根结点
- #define bitree_is_eob(node) ((node) == NULL) //判断分支是否结束
- #define bitree_is_leaf(node) ((node)->left == NULL && (node)->right == NULL) //判断是否是叶子结点
- #define bitree_data(node) ((node)->data) //获取数据域
- #define bitree_left(node) ((node)->left) //获取左结点(左孩子)
- #define bitree_right(node) ((node)->right)//获取右结点(右孩子)
-
- #endif
1 二叉树的初始化(bitree_init):此操作完成后,一棵空的二叉树就建立了,此时它没有任何结点,这是二叉树进行后续操作的前提。
2 二叉树的销毁(bitree_destroy):此操作用于销毁一棵二叉树
3 二叉树插入操作(bitree_ins_left):将data中的信息插入到当前node结点的左指针域,成为当前node结点的左孩子。当node为NULL时,从根结点位置插入。
4二叉树插入操作(bitree_ins_right):同3,不同的是其插入的是右指针域。
5 二叉树删除操作(bitree_rem_left):删除以node结点为根的子树的左子树。当node = NULL时,则为删除整棵二叉树
6二叉树删除操作(bitree_rem_right):同5,不同的是其删除的是右子树。
7 二叉树的合并(bitree_merge):将两棵二叉树,分别合并成以data域为根的新二叉树,原来这两棵二叉树分别成为新二叉树的左右子树。
8其它宏定义:代码中已经说明清楚,这里不再累述。
9二叉树的三种遍历操作:先序遍历、中序遍历和后序遍历。(放在后面说明)
五、实现二叉树
1、二叉树初始化的实现(bitree_init)
-
-
-
-
-
-
- #include <string.h>
- #include <stdlib.h>
-
- #include "bitree.h"
-
-
- void bitree_init( BiTree *tree, void (*destroy)(void *data) )
- {
-
- tree->size = 0;
- tree->root = NULL;
- tree->destroy = destroy;
- return;
- }
完成对维护二叉树结构体的各域值的初始化。
2、二叉树的销毁操作(bitree_destroy)
-
- void bitree_destroy(BiTree *tree)
- {
-
- bitree_rem_left(tree, NULL);
-
- memset(tree, 0, sizeof(BiTree) );
- return;
- }
先删除二叉树的所有结点,然后清空二叉树结构体。
3、二叉树插入操作(bitree_ins_left及bitree_ins_right)
先是插入左子树操作:
-
- int bitree_ins_left(BiTree *tree, BiTreeNode *node, const void *data)
- {
- BiTreeNode *new_node, **position;
-
- if( node == NULL )
- {
- if( bitree_size(tree) > 0 )
- return -1;
-
- position = &tree->root;
- }
- else
- {
- if( bitree_left(node) != NULL )
- return -1;
-
- position = &node->left;
- }
-
-
- new_node = (BiTreeNode *)malloc(sizeof(BiTreeNode));
- if( new_node == NULL )
- return -1;
-
-
- new_node->data = (void *)data;
- new_node->left = NULL;
- new_node->right = NULL;
-
- *position = new_node;
-
- tree->size++;
-
- return 0;
- }
接着是插入右子树操作:
-
- int bitree_ins_right(BiTree *tree, BiTreeNode *node, const void *data)
- {
- BiTreeNode *new_node, **position;
-
- if( node == NULL )
- {
- if( bitree_size(tree) > 0 )
- return -1;
-
- position = &tree->root;
- }
- else
- {
- if( bitree_right(node) != NULL )
- return -1;
-
- position = &node->right;
- }
-
-
- new_node = (BiTreeNode *)malloc(sizeof(BiTreeNode));
- if( new_node == NULL )
- return -1;
-
- new_node->data = (void *)data;
- new_node->left = NULL;
- new_node->right = NULL;
-
- *position = new_node;
-
- tree->size++;
- return 0;
- }
通过代码可以看出,这两个函数的实现几乎一样,我们这里只需要学会其内在思想:
(1) 找准需要插入的位置:是在根结点位置,当前结点的左指针还是右指针位置。
(2) 分配新结点,在适当的地方插入结点: *position = new_node完成了这个插入操作
(3) 更新二叉树size域。
4、二叉树删除操作(bitree_rem_left及bitre_rem_right)
先是删除左子树操作:
-
- void bitree_rem_left(BiTree *tree, BiTreeNode *node)
- {
- BiTreeNode **position;
-
-
- if( bitree_size(tree) == 0 )
- return;
-
- if( node == NULL )
- {
- position = &tree->root;
- }
- else
- {
- position = &node->left;
- }
-
-
- if( *position != NULL )
- {
- bitree_rem_left(tree, *position);
- bitree_rem_right(tree, *position);
-
- if( tree->destroy != NULL )
- {
- tree->destroy((*position)->data);
- }
- free(*position);
- *position = NULL;
-
-
- tree->size--;
- }
- return;
- }
接着是删除右子树操作:
-
- void bitree_rem_right(BiTree *tree, BiTreeNode *node)
- {
- BiTreeNode **position;
-
- if( bitree_size(tree) == 0 )
- return;
-
- if( node == NULL )
- {
- position = &tree->root;
- }
- else
- {
- position = &node->right;
- }
-
-
- if( *position != NULL )
- {
- bitree_rem_left(tree, *position);
- bitree_rem_right(tree, *position);
-
- if( tree->destroy != NULL )
- {
- tree->destroy((*position)->data);
- }
-
- free(*position);
- *position = NULL;
- tree->size--;
- }
-
- return;
- }
同样的,我们需要掌握其实现的思想:
通过采用递归的思想,后序遍历逐层深入到最底层(深度优先搜索),从下至上逐一删除各个结点,释放被删除结点的数据域空间,更新二叉树size值大小。注意递归退出的条件:
(1) 树为空时退出
(2) *Position为空时退出
可以思考:为何删除操作不能采用前序或中序遍历?
5、二叉树的合并(bitree_merge)
-
- int bitree_merge(BiTree *merge, BiTree *left, BiTree *right, const void *data)
- {
-
- bitree_init(merge, left->destroy);
-
-
- if( bitree_ins_left(merge, NULL, data) != 0 )
- {
- bitree_destroy(merge);
- return -1;
- }
-
-
- bitree_root(merge)->left = bitree_root(left);
- bitree_root(merge)->right = bitree_root(right);
-
-
- merge->size = merge->size + bitree_size(left) + bitree_size(right);
-
-
- left->root = NULL;
- left->size = 0;
- right->root = NULL;
- right->size = 0;
-
- return 0;
- }
二叉树的合并操作非常简单,有以下几个步骤:
(1) 初始化新二叉树,并插入data域成为新二叉树的根结点
(2) 新二叉树的左指针指向左子树的根结点
(3) 新二叉树的右指针指向右子树的根结点
(4) 新二叉树结点个数 =左、右子树结点之和+1
(5) 对原左、右子树结构体相关域的清空操作。
六、遍历二叉树
遍历二叉树是指按照一定的规律,对二叉树的每个结点访问且仅访问一次的处理过程。这里的“访问”是泛指对结点数据的某种处理操作,如可以是printf打印显示,可以是链表的插入操作,也可以是某些数学运算,等等。
遍历的目的:通过一次遍历后,可以使树中结点的非线性结构按访问的先后顺序转变为某种线性序列。如:可以按照访问的先后顺序将数据域逐一填入某一数组中,当然,也可以插入某个链表中,然后通过链表的方式对数据域进行访问。
遍历的次序: DLR先序遍历、LDR中序遍历、LRD后序遍历
可以看出,先、中、后序的遍历主要根据根结点的访问次序命名,而L左结点总是先于R右结点访问。无论是哪种遍历方式,其核心的思想是总是递归,以中序遍历二叉树为例,说明其算法思想:
若二叉树非空,则:
1)中序遍历左子树
2)访问根结点
3)中序遍历右子树
(1)先序遍历二叉树
-
- int preorder(const BiTreeNode *node, List *list)
- {
- if( !bitree_is_eob(node) )
- {
-
- if( list_ins_next(list, list_tail(list), bitree_data(node) ) != 0 )
- {
- return -1;
- }
-
- if( !bitree_is_eob( bitree_left(node) ) )
- {
- if( preorder(bitree_left(node), list) != 0 )
- return -1;
- }
-
- if( !bitree_is_eob( bitree_right(node) ) )
- {
- if( preorder(bitree_right(node), list) != 0 )
- return -1;
- }
- }
- return 0;
- }
(2)中序遍历二叉树
- /* inorder */
- int inorder(const BiTreeNode *node, List *list)
- {
- if( !bitree_is_eob(node) )
- {
- if( !bitree_is_eob(bitree_left(node)) )
- {
- if( inorder( bitree_left(node), list ) != 0 )
- return -1;
- }
-
- if( list_ins_next(list, list_tail(list), bitree_data(node)) != 0 )
- {
- return -1;
- }
-
- if( !bitree_is_eob(bitree_right(node)) )
- {
- if( inorder( bitree_right(node), list ) != 0 )
- return -1;
- }
- }
- return 0;
- }
(3)后序遍历二叉树
-
- int postorder(const BiTreeNode *node, List *list)
- {
- if( !bitree_is_eob(node) )
- {
- if( !bitree_is_eob(bitree_left(node)) )
- {
- if( postorder(bitree_left(node), list) != 0 )
- return -1;
- }
-
- if( !bitree_is_eob(bitree_right(node)) )
- {
- if( postorder(bitree_right(node), list) != 0 )
- return -1;
- }
-
- if( list_ins_next(list, list_tail(list), bitree_data(node)) != 0 )
- return -1;
- }
-
- return 0;
- }
在本例的三种遍历代码中,“访问”的方式是将结点的数据域插入至某一链表结构中。
七、二叉树简单应用
对上述所有二叉树的代码如果未进行使用或验证,无疑是一些基本符号,没有任何的意义,所以本节主要对前面二叉树的各个接口进行简单的应用测试,在进入实际的应用之前,有几个函数需要先实现,如下:
-
- void destroy(void *data)
- {
- free(data);
- return;
- }
这是创建销毁函数的代码,在bitree_init()初始化二叉树时需要传递它的函数入口地址。接着是创建二叉树的函数:
-
- int create_tree(BiTree *tree, BiTreeNode *node)
- {
-
- int ret;
- int *int_ptr = NULL;
- char ch;
-
- scanf("%c", &ch);
-
- if( ch == '#' )
- {
- return 0;
- }
- int_ptr = (int *)malloc(sizeof(int));
- if( int_ptr == NULL )
- return -1;
-
- *int_ptr = ch-48;
-
- if( node == NULL )
- {
- bitree_init(tree, destroy);
- ret = bitree_ins_left(tree, NULL, (void *)int_ptr);
- if( ret != 0 )
- {
- free(int_ptr);
- return -1;
- }
- printf("root is %d\n", *(int *)bitree_data(tree->root));
- create_tree(tree, tree->root);
- }
- else
- {
-
- ret = bitree_ins_left(tree, node, (void *)int_ptr);
- if( ret != 0 )
- {
- free(int_ptr);
- return -1;
- }
- printf("node: %d 's left node is :%d\n", *(int *)bitree_data(node), *(int *)bitree_data(node->left));
- ret = create_tree(tree, node->left);
-
- scanf("%c", &ch);
-
- if( ch == '#')
- return 0;
-
- int_ptr = (int *)malloc(sizeof(int));
- if( int_ptr == NULL )
- return -1;
-
- *int_ptr = ch-48;
-
- ret = bitree_ins_right(tree, node, (void *)int_ptr);
- if( ret != 0 )
- {
- free(int_ptr);
- return -1;
- }
- printf("node: %d 's right node is :%d\n", *(int *)bitree_data(node), *(int *)bitree_data(node->right));
- ret = create_tree(tree, node->right);
- }
-
- return 0;
- }
它的实现逻辑比较简单,在于采用递归的思想来创建一棵二叉树,并且其递归退出的条件是输入“#”符号,注意:本代码的实现只能插入简单的数字(范围0-9),这对于简单测试来说已经足够了。
下面是具体的关于二叉树各接口代码的简单测试应用,如下:
-
- int main(int argc, char **argv)
- {
- int ret;
- int *int_ptr;
- BiTree tree1, tree2, tree_merge;
- List list;
- ListElmt *member;
- BiTreeNode *nd;
-
-
-
-
-
-
-
-
- create_tree(&tree1, NULL);
- printf("\nstep1:tree1 build success\n");
-
-
-
-
-
-
-
-
- int_ptr = NULL;
- create_tree(&tree2, NULL);
- printf("step2:tree2 build success\n");
-
- int_ptr = (int *)malloc(sizeof(int));
- if( int_ptr == NULL )
- return -1;
- *int_ptr = 11;
-
-
-
-
-
-
-
-
-
-
- ret = bitree_merge(&tree_merge, &tree1, &tree2, int_ptr);
- printf("step3: after merged: there are %d number nodes in the tree_merge.\n", bitree_size(&tree_merge));
-
-
-
-
-
-
-
-
-
-
-
- printf("\nstep4: remove the right tree in tree_merge.\n");
- bitree_rem_right(&tree_merge, bitree_root(&tree_merge) );
- printf("after remove the right tree, there are %d number nodes in the tree_merge.\n", bitree_size(&tree_merge));
-
- printf("\nstep5: preorder traverse the tree and insert the nodes into the list\n");
- list_init(&list, destroy);
- ret = preorder( bitree_root(&tree_merge), &list );
-
- printf("according to the sequence of the preorder traversing the tree:\n");
- for(member = list_head(&list); member != NULL; member = list_next(member) )
- {
- printf("%d ", *(int *)list_data(member));
- }
- printf("\n");
-
-
- printf("\nsetp6: inorder traverse the tree and insert the nodes into the list\n");
- list_init(&list, destroy);
-
- ret = inorder( bitree_root(&tree_merge), &list );
-
- printf("according to the sequence of the inorder traversing the tree:\n");
- for(member = list_head(&list); member != NULL; member = list_next(member) )
- {
- printf("%d ", *(int *)list_data(member));
- }
- printf("\n");
-
- printf("\nsetp7: postorder traverse the tree and insert the nodes into the list\n");
- list_init(&list, destroy);
-
- ret = postorder( bitree_root(&tree_merge), &list );
- printf("according to the sequence of the postorder traversing the tree:\n");
- for(member = list_head(&list); member != NULL; member = list_next(member) )
- {
- printf("%d ", *(int *)list_data(member));
- }
- printf("\n");
-
- printf("\nstep8: delete all the nodes in the tree.\n");
- bitree_rem_left( &tree_merge, NULL );
- printf("there are %d number nodes.\n", bitree_size(&tree_merge) );
-
- bitree_destroy(&tree_merge);
-
- return 0;
- }
具体的含义不再说明,注释以及printf已经非常详细。代码的编译过程如下:
![](https://img-my.csdn.net/uploads/201301/16/1358342403_9589.jpg)
程序执行过程如下:
![](https://img-my.csdn.net/uploads/201301/16/1358342499_9864.jpg)