《C和指针》中只是对最简单的二叉索引树进行了简单的讲解和最基本的实现,毕竟这不是一本关于数据结构的书。
如图所示,这就是二叉搜索树,只有一个规则那就是:每个节点的值比它的左子树要大,但比它的右子树要小。所以二叉搜索树中不能存在值相同的节点。
1、插入、删除、查找、遍历
1.1、插入
基本算法如下:
如果树为空:
——把新值作为根节点插入
否则:
——如果新值小于当前节点的值:
————把新值插入到当前节点的左子树
——否则:
————把新值插入到当前节点的右子树
1.2、删除
从树中删除一个节点有三种情况:删除没有孩子的节点;删除只有一个孩子的节点;删除有两个孩子的节点。第一、二种2情况比较简单。最后一种情况的一种解决方法是不删除这个节点,而是删除它的左子树中值最大的那个节点,并用这个值代替原来应被删除的那个节点的值。
1.3、查找
查找算法如下:
如果树为空:
——这个值不存在于树中
否则:
——如果这个值和根节点的值相等:
————成功找到这个值
——否则:
————如果这个值小于根节点的值:
——————查找左子树
————否则:
——————查找右子树
1.4、遍历
遍历树的节点有几种不同的次序,最常用的是前序(pre-order)、中序(in-order)、后序(post-order)和层次遍历(breadth-first)。细节就不多说了,只有一点:这里的前序、中序、后序都是相对根节点而言,根节点在前就是前序,根节点在中间就是中序,根节点在最后就是后序;层次遍历就是从根节点开始、从左到右一层一层的进行。
2、二叉搜索树接口
这里提供一个简单的二叉树接口,包括插入insert_TreeNode、删除delete_TreeNode、查找find_TreeNode、前序遍历pre_ordor_TreeNode、中序遍历in_ordor_TreeNode和后序遍历post_ordor_TreeNode。
/*
** 二叉树接口,数组实现方式的接口
*/
#ifndef TREE_H_
#define TREE_H_
#include <stdlib.h>
#define TREE_TYPE int /*树的值类型*/
/*
** insert_TreeNode
** 向树中插入一个新值
*/
void insert_TreeNode(TREE_TYPE value);
/*
** delete_TreeNode
** 删除树中一个节点
*/
void delete_TreeNode(TREE_TYPE value);
/*
** find_TreeNode
** 在树中查找返回节点的指针
*/
TREE_TYPE *find_TreeNode(TREE_TYPE value);
/*
** pre_ordor_TreeNode
** 前序遍历
*/
void pre_ordor_TreeNode(void (*callback)(TREE_TYPE value));
#endif
前序、中序、后序只是顺序不同,代码都一样,就不重复了。
3、数组形式的二叉搜索树
/*
** 静态数组实现二叉树
*/
#include "tree.h"
#include <assert.h>
#include <stdio.h>
#define TREE_SIZE 100 /*树中最多能保存多少个数*/
#define ARRAY_SIZE (TREE_SIZE + 1)
/*
** 用于存储树的所有节点的数组
*/
static TREE_TYPE tree[ARRAY_SIZE];
/*
** left_child
** 计算一个节点左孩子的下标,非用户接口
*/
static int left_child(int current)
{
return current * 2;
}
/*
** right_child
** 计算一个节点右孩子的下标,非用户接口
*/
static int right_child(int current)
{
return current * 2 + 1;
}
/*
** insert_TreeNode
*/
void insert_TreeNode(TREE_TYPE value)
{
int current;
/*
** 确保值为非零,因为零用于指示一个未使用的节点
*/
assert(value != 0);
/*
** 从根节点开始
*/
current = 1;
/*
** 从合适的子树开始,直到到达一个叶节点
*/
while(tree[current] != 0)
{
/*
** 根据情况进入叶节点或右子树(确信未出现重复的值)
*/
if(value < tree[current])
current = left_child(current);
else
{
assert(value != tree[current]);
current = right_child(current);
}
assert(current < ARRAY_SIZE);
}
tree[current] = value;
}
/*
** find_subscript_TreeNode
** 返回找到的节点的下标,专用于数组实现方式,非用户接口
*/
static int find_subscript_TreeNode(TREE_TYPE value)
{
int current;
/*
** 从根节点开始,直到找到那个值,进入合适的子树
*/
current = 1;
while(current < ARRAY_SIZE && tree[current] != value)
{
/*
** 根据情况进入左子树或右子树
*/
if(value < tree[current])
current = left_child(current);
else
current = right_child(current);
}
if (current < ARRAY_SIZE)
return current;
else
return 0;
}
/*
** find_TreeNode
** 返回找到的值的指针
*/
TREE_TYPE *find_TreeNode(TREE_TYPE value)
{
int current = find_subscript_TreeNode(value);
if(current != 0)
return tree + current;
else
return tree; /*如果没找到则返回第一个节点,第一个节点永远为空*/
}
/*
** delete_one_node
** 删除下标对应的一个节点,非用户接口
*/
static void delete_one_node(int current)
{
int left_node = left_child(current);
int right_node = right_child(current);
if(tree[left_node] == 0 && tree[right_node] == 0) /*第一种情况如果该节点没有孩子,直接删除*/
{
tree[current] = 0;
}
else if(tree[left_node] == 0 && tree[right_node] != 0)/*第二种情况如果该节点只有一个右孩子*/
{
tree[current] = tree[right_node];
delete_one_node(right_node);
}
else /*其余情况*/
{
tree[current] = tree[left_node];
delete_one_node(left_node);
}
}
/*
** delete_TreeNode
** 删除节点的用户接口,数组实现方式
*/
void delete_TreeNode(TREE_TYPE value)
{
/*
** 首先找到value对应节点的下标
*/
int current = find_subscript_TreeNode(value);
/*
** 删除该下标对应的节点
*/
delete_one_node(current);
}
/*
** do_pre_order_traverse
** 执行一层前序遍历,这个帮助函数用于保存我们正在处理的节点的信息
** 非用户接口
*/
static void do_pre_order_traverse(int current, void(*callback)(TREE_TYPE))
{
if(current < ARRAY_SIZE && tree[current] != 0)
{
callback(tree[current]);
do_pre_order_traverse(left_child(current), callback);
do_pre_order_traverse(right_child(current), callback);
}
}
/*
** pre_ordor_TreeNode
** 用户接口
*/
void pre_ordor_TreeNode(void (*callback)(TREE_TYPE value))
{
do_pre_order_traverse(1, callback);
}
数组表示树的关键是使用下标来寻找特定的值的双亲和孩子,规则是:
节点N的双亲是节点N/2
节点N的左孩子是节点2N
节点N的右孩子是节点2N+1
忽略数组下标为0的元素,这种方法会浪费空间。
数组不适合用来实现树,对于一颗不平衡树,大部分数组空间都被浪费了。更好的方法是使用链式二叉树而不是数组树。
4、链式二叉搜索树
/*
** 树的链表实现
*/
#include "tree.h"
#include <assert.h>
#include <stdio.h>
#include <malloc.h>
/*
** TreeNode结构包含了值和两个指向某个树节点的指针
*/
typedef struct TREE_NODE{
TREE_TYPE value;
struct TREE_NODE *left;
struct TREE_NODE *right;
}TreeNode;
/*
** 指向树根节点的指针
*/
static TreeNode *tree;
/*
** insert
*/
void insert_TreeNode(TREE_TYPE value)
{
TreeNode *current;
TreeNode **link;
/*
** 从根节点开始
*/
link = &tree;
/*
** 持续查找值,进入合适的子树
*/
while((current = *link) != NULL)
{
/*
** 根据情况,进入左子树或右子树(确认没有出现重复的值)
*/
if(value < current->value)
link = ¤t->right;
else
{
assert(value != current->value);
link = ¤t->right;
}
}
/*
** 分配一个新节点,使适当节点的link字段指向它
*/
current = malloc(sizeof(TreeNode));
assert(current != NULL);
current->value = value;
current->left = NULL;
current->right = NULL;
*link = current;
}
/*
** find
*/
TREE_TYPE *find_TreeNode(TREE_TYPE value)
{
TreeNode *current;
/*
** 从根节点开始,直到找到这个值,进入合适的子树
*/
current = tree;
while(current != NULL && current->value != value)
{
/*
** 根据情况,进入左子树或右子树
*/
if(value < current->value)
current = current->left;
else
current = current->right;
}
if(current != NULL)
return current;
}
/*
** do_pre_order_traverse
** 执行一层前序遍历,这帮助函数用于保存我们当前正在处理的节点的信息
** 并非用户接口
*/
static void do_pre_order_traverse(TreeNode *current, void (*callback)(TREE_TYPE value))
{
if(current != NULL)
{
callback(current->value);
do_pre_order_traverse(current->left, callback);
do_pre_order_traverse(current->right, callback);
}
}
/*
** pre_order_traverse
*/
void pre_order_traverse(void (*callback)(TREE_TYPE value))
{
do_pre_order_traverse(tree, callback);
}
链表方式实现的树可以与数组方式实现的树接口一样。还需要一个函数destroy_tree来释放所有的树内存。树节点中如果有一个节点指向双亲节点会更方便操作。
参考文献
[1] Kenneth A. Reek. C和指针(第二版)[M]. 人民邮电出版社.