《C和指针》学习笔记之树

《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 = &current->right;
        else
        {
            assert(value != current->value);
            link = &current->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]. 人民邮电出版社.

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

巴普蒂斯塔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值