数据结构: 树

声明:本文为学习数据结构与算法分析(第三版) Clifford A.Shaffer 著的学习笔记,代码有参考该书的示例代码。

定义与术语

这没什么好说的,照搬书上的吧。
一棵树 T 是由一个或一个以上结点组成的有限集,其中有一个特定的结点 R 称为 T 的根结点。如果集合 (T -{R}) 非空,那么集合中的这些结点被划分为 n 个不相交的子集 T0, T1, ……, Tn, 其中每个子集都是树,并且其相应的根结点 R0, R1, ……, Rn是 R 的子结点。
结点的出度(out degree)定位该结点的子结点数目。

结点的ADT

//General Tree Node
template<typename E>
class GTNode
{
    protected:
    virtual void removeFirst();
    virtual void removeNext();
    public:
    virtual ~GTNode() {}
    virtual E value() const;
    virtual GTNode* leftMostChild() const;
    virtual GTNode* rightSibling() const;
    virtual void setValue(const E& e);
    virtual void insertFirst(GTNode* infirst);
    virtual void insertNext(GTNode* innext);;
};

*由于removeFirstremoveNext函数在考虑整颗树的情况下,比较复杂,所以暂时将其访问权限限制为 protected

树的遍历

类似二叉树,树的遍历有前序,中序,后序三种。
前序遍历和后序遍历很好定义,而中序遍历却并不是确定的。常用的定义是先遍历最左子结点,然后遍历根结点,然后遍历其他子结点。
实现:

template<typename E>
void preorderTree(E* root, void (*visit)(E*) )
{
    if(root==nullptr)   return;
    visit(root);
    auto temp = root->leftMostChild();
    while(temp!=nullptr)
    {
        preorderTree(temp, visit);
        temp = temp->rightSibling();
    }
}

template<typename E>
void inorderTree(E* root, void(*visit)(E*) )
{
    if(root==nullptr) return;
    auto temp = root->leftMostChild();
    inorderTree(temp, visit);
    visit(root);
    while(temp!=nullptr)
    {
        temp = temp->rightSibling();
        inorderTree(temp, visit);
    }
}

template<typename E>
void postorderTree(E* root, void(*visit)(E*) )
{
    if(root==nullptr)   return;
    auto temp = root->leftMostChild();
    while(temp!=nullptr)
    {
        postorderTree(temp, visit);
        temp = temp->rightSibling();
    }
    visit(root);
}

树的回收其实是后序遍历的一种,实现如下:

template<typename E>
void clear(E* root)
{
    if(root == nullptr) return;
    auto temp = root->leftMostChild();
    while(temp!=nullptr)
    {
        auto next = temp->rightSibling();
        clear(temp);
        temp = next;
    }
    delete root;
}

一个简单的例子

这里写图片描述
按照书上的这颗树,建立如下:

#include<iostream>
#include"Tree.h"
using namespace std;

template<typename E>
void visit(E* root)
{
    cout<<root->value()<<"  ";
}

int main()
{
    char c[] = { 'R', 'A', 'B', 'C', 'D', 'E', 'F'};
    int i=0;
    GTNode<char>* root = new GTNode<char>(c[i++]);//R
    root->insertFirst( new GTNode<char>(c[i++]));
    root->leftMostChild()->insertNext( new GTNode<char>(c[i++]));

    auto temp = root->leftMostChild();
    temp->insertFirst( new GTNode<char>(c[i++]));
    temp = temp->leftMostChild();
    temp->insertNext( new GTNode<char>(c[i++])); temp = temp->rightSibling();
    temp->insertNext( new GTNode<char>(c[i++])); temp = temp->rightSibling();

    temp = root->leftMostChild()->rightSibling();
    temp->insertFirst(new GTNode<char>(c[i++]));   


    preorderTree(root, visit);  cout<<endl;
    inorderTree(root, visit);   cout<<endl;
    postorderTree(root, visit);
    cout<<endl;

    clear(root);    root = nullptr;
}

输出:

R A C D E B F
C A D E R F B
C D E A F B R

树的另一种表示:父指针表示法

父指针表示树,即结点只存储父结点的指针。
但是很明显,这样的表示对于一些常用的操作,如查找左兄弟结点并不支持。
这样的表示是有特殊的目的的,它能解答以下的问题:
“给出两个结点,它们是否在同一棵树中?”
父指针表示法常常用来维护由一些不相交子集构成的集合。对于不相交的集合,希望提供如下两种操作:

  1. 判断两个节点是否在同一集合中
  2. 归并两个集合。(UNION)

归并两个集合的过程常常称为”UNION”,且整个操作旨在通过归并找出两个结点是否在同一个集合中,因此以“并查算法”(UNION/FIND,也称并查集)命令。
下面是实现:

class ParPtrTree
{
    int* array;
    int len;

    enum{ ROOT = -1 };

    int find(int curr)
    {
        if(array[curr] == ROOT) return curr;
        array[curr] = find(array[curr]);
        return array[curr];
    }
    int getChildNum(int curr)
    {
        int sum = 0;
        for(int i = 0;i<len;++i)
            if(array[i] == curr)    ++sum;
        return sum;
    }
    public:
    ParPtrTree(int n)
    {
        array = new int[n];
        len = n;
        for(int i=0;i<len;++i)
            array[i] = ROOT;
    }

    virtual ~ParPtrTree()
    {
        delete[] array;
    }
    void Union(int a, int b)
    {
        a = find(a);
        b = find(b);
        if(getChildNum(b) > getChildNum(a) )
            array[a] = b;
        else
            array[b] = a;

    }
    bool differ(int a, int b)
    {
        return find(a)!=find(b);
    }
};

*在课本上找不到完整的实现代码,这里的实现仅是我个人的实现,仅供参考
array 数组存储的是相应结点的父结点的下标。
find 函数使用了递归。这是一种路径压缩的方法,查找到当前结点的父结点,并把当前结点的所有祖先结点的父指针都指向根结点。
并查算法的输入是一系列等价对,考虑以下包含两个连通分支的图:
这里写图片描述

等价对可以是 A-H 或者 C-H .
因为传递性, A-C 也是等价对。

树的实现

树的实现有几种形式,这里只讲方法(将用到课本的图),并不给出代码实现。

子结点表表示法

子结点表表示法在数组中存储树的结点,每个结点包括结点值,一个父指针(或索引)及一个指向子结点链表的指针,顺序是从左到右。
这里写图片描述

左子结点/右兄弟结点表示法

左子结点/右兄弟结点表示法,顾名思义,就是表中记录了一个结点的左子结点和右兄弟
这里写图片描述

动态结点表示法

每个结点存储了结点的值和一个动态数组,动态数组中存储的是子节点的指针域和数组的大小。
这里写图片描述

另外一种利用链表的实现更加灵活:
这里写图片描述

动态左子结点/右兄弟结点表示法

在二叉树中,有左右子树。扩展到树的表示,即可变成左子节点表示树的左子节点,右子结点表示树的右兄弟结点。
这个表示方法只有两个指针域。
还可以扩展的表示森林。即根结点互为兄弟。
一般的表示如下:
这里写图片描述

树的顺序表示法

存储一系列结点的值,其中包含了尽可能少,但是对于重建树结构必不可少的信息,这种方法称为顺序树表示法。它的优点是节省空间。
考虑如下二叉树,可以根据先序遍历记录下来,其中 / 表示NULL
这里写图片描述
那么这棵树的顺序表示如下:

AB/D//CEG///FH//I//

上面的表示并不区分内部结点和叶子结点。
我们可以用另外一种方法表示,标记处内部结点,用以区分叶子结点。

A’B’/DC’E’G’/F’HI

这样的表示更省空间,但是我们还要额外记录一个标识信息。
标识信息可以使用位来记录,假如存储结点值的是 int 整型字段,而结点的值是正数,那么我们可以使用最高位的符号位来表示内部结点。
我们还可以在书中额外记录一个位向量。上面的位向量如下:

11001110100

然而使用顺序表示法来表示树的话,还需要记录结构更多的显示信息,如子结点的数目。
作为一种替代方法,可以在记录子结点表结束的位置。
下面使用了特殊的标记来表示子结点表的结束”)”.
考虑如下的树:
这里写图片描述
它的顺序表示法是:

RAC)D)E))BF)))

其中,F 后面跟了三个括号,因此表示 F 子结点、B 子结点、R 子结点的结束。
则重建树的代码可以如下:

GTNode<char>* rebuildTree(const char* str, int& i)
{
    if(str[i] == ')' || str[i] == '\0')  return nullptr;
    GTNode<char>* root = new GTNode<char>(str[i]);
    root->insertFirst(rebuildTree(str, ++i));
    root->insertNext(rebuildTree(str, ++i));
    return root;
}

所有实现均可在github上找到

其实并没有什么内容的github
–END–

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值