4 数据结构--树 (树的定义+遍历)

数据结构-树

KEY:
  1. 树的定义与术语
  2. 树的遍历
  3. 树的顺序存储结构和链式存储结构实现

考研时间紧张,以后有空再传代码啦

一、树的定义与术语

1.1 定义

树型结构是一种动态的,非线性的,可描述结 构层次特性的数据结构。这种结构是按分枝关 系把信息联系起来的数据组织形式。

一棵树(tree)T是由一个或一个以上结点组成的有限集

  • 其中有一个特定的结点R 称为T的根结点
  • 集合(T-{R})中的其余结点可被划分为n≥0个互 不相交的子集T1,T2,…, Tn,其中每个子集本身 又是一棵树,并且其相应的根结点R1,R2,…, Rn 是R的子结点

子集Ti(1≤i≤n)称为树T的子树(subtree)。

  • 子树可如下排序:Ti排在Tj之前,当且仅当i<j
  • 为方便起见, 子树从左到右排列, 其中T1被称为R 的最左子树, R1被称为R的最左子结点

1.2 术语

结点:数据元素+若干指向子树的分支

结点的度:分支的个数

树的度:树中所有结点的度的最大值

根结点:是一个没有双亲结点的结点。一棵树中最多有一个根结点

:边表示双亲结点与孩子结点的链接

叶子结点:度为零的结点

分支结点:度大于零的结点

结点的大小∶是指子孙的个数,包括其自身

路径:由从根到该结点所经分支和结点构成

**结点的层次:**假设根结点的层次为1,第i层的结点的子树根结点的层次为i+1

树的深度:树中叶子结点所在的最大层次

**树的高度:**从根结点到树中最深结点的路径长度

森林:是m(m≥0)棵互 不相交的树的集合

任何一棵非空树是一个二元组 Tree = (root,F) 其中:root 被称为根结点 F 被称为子树森林

如果树中除了叶子结点外,其余每一结点 只有一个孩子结点,这种树称做斜树。 对于每个结点仅有一个左孩子结点的树叫做左斜树。对于每个结点仅有右孩子结点的树叫做右斜树。

二、ADT

数据对象D: D是具有相同特性的数据元素的集合。

数据关系R:

若D为空集,则称为空树。 否则: (1) 在D中存在唯一的称为根的数据元素root; (2) 当n>1时,其余结点可分为m (m>0)个互 不相交的有限集T1, T2, …, Tm,其中每一 棵子集本身又是一棵符合本定义的树, 称为根root的子树。

基本操作:

➕ 结构构造/销毁型操作
➕ 引用型操作:
获取根结点,查找,遍历
➕加工型操作:
插入,删除元素结点

// General tree node ADT
template <class Elem> class GTNode
{
public:
    GTNode(const Elem&); // Constructor
    ~GTNode();           // Destructor
    Elem value();        // Return value
    bool isLeaf();       // TRUE if is a leaf
    GTNode* parent();    // Return parent
    GTNode* leftmost_child(); // First child
    GTNode* right_sibling();  // Right sibling
    void setValue(Elem&);     // Set value
    void insert_first(GTNode<Elem>* n);
    void insert_next(GTNode<Elem>* n);
    void remove_first(); // Remove first child
    void remove_next();  // Remove sibling };
};
template<classElem> class GenTree
{
private:
  void printhelp (GTNode *); //Print helper function
public:
    GenTree();//Constructor
    ~GenTree();//Destructor
    voidclear();//Sendnodestofreestore
    GTNode*root();//Returntheroot
    voidnewroot(Elem&, GTNode<Elem>*, GTNode<Elem>*); //Combinetwosubtrees
    voidprint();//Printatree};
};

三、树的遍历

  • 先根(次序)遍历
    若树不空,则先访问根结点,然后依次先根遍历各棵子树。
  • 后根(次序)遍历
    若树不空,则先依次后根遍历各棵子树,然后访问根结点。
  • 按层次(广度)遍历
    若树不空,则自上而下自左至右访问树中每个结点。

遍历

先根:A B E F C D G H I J K

后根:E F B C I J K H G D A

层次:A B C D E F G H I J K

先根遍历算法(递归)

template<typename Elem>
voidGenTree<Elem>::printhelp(GTNode<Elem>*root)
{
    if(root->isLeaf())cout<<"Leaf:";
    else cout<<"Internal:";
    cout<<root->value()<<"\n";
    for(GTNode<Elem>*temp=root->leftmostChild(); temp!=NULL; temp=temp->rightSibling())
        printhelp(temp);
}

四、树的实现

相比于二叉树,通用的树的度可能大于2;可能是一个很大的值。因此用孩子指针的方式就不现实,因此大多都考虑使用父指针。

4.1 父指针表示法

对每个结点只保存一个指针域,指向其父结点。

在这里插入图片描述

4.1.1 特点
  • 优点:顺着结点的父指针链追溯祖先结点很简单
  • 缺点:找结点的子孙和兄弟结点很复杂
  • 缺点:兄弟结点之间的左右次序没有存储
4.2.2 应用

父指针表示法常用于维护由一些不相交子集构成的集合

对于不相交集合,希望提供两种基本操作

  • 判断两个结点是否同一个集合;

  • 归并两个集合

因此命名为并查算法(并查集)

并查算法用一棵树代表一个集合。如果两个结点在同一棵树中,则认为它们在同一个集合中。

算法思想:

一开始每个元素都在独立的只包含一个结点的树中,而他自己就是根节点。通过使用函数differ可以检查一个等价对中的两个元素是否在同一棵树中。如果不是就通过union函数归并两个等价类。归并时通常让节点个数少的指向结点个数多的。

例如:等价对**(A,B)** (A,C) (A,H) (B,H) (C,H) (H,E) (E,D) (E,F) (E,G) (D,F) (F,G) (I,F)

  1. 所有结点都为独立根节点
  2. 归并ab ch gf de if 归并gf时由于g指向f,因为归并fi时,f的结点个数更多,因而I指向F
  3. 归并HA EG 以(E,G)为例,G的父节点为F,E是D,由于D树结点个数比F树少,因此D指向F
  4. 最后归并(H,E) 同理,A指向F

这种方法可以把结点的深度控制在 l o g n log_n logn

在这里插入图片描述

在这里插入图片描述

对于c,如果采用路径压缩进行合并,那么找H根节点的时候,会同时把H指向A,然后把E指向F,再进行合并。结果如下图

在这里插入图片描述

这种方法使得对n个结点进行n次FIND操作的路径压缩代价约为 θ ( n l o n g ∗ n ) \theta(n long^*n) θ(nlongn)

在计算机科学中,n的重对数,写为 l o g ∗ n log^*n logn(通常读作“log star”),是对数函数在结果小于或等于1之前必须迭代应用的次数。

def log_star(n):
    ret = 1
    foo = log(n, 2)
    while foo != 1:
        foo = log(foo, 2)
        ret += 1
    return ret
#出自Kmbingbing

4.2 子结点表表示法 ----- 链式实现

4.2.1 存储形式

在数组中存储树结点。每个结点包括:结点值、父指针(或索引)及一个指向子结点链表的指针

在这里插入图片描述

4.2.1 特点
  • 优点:结点的最左子结点由链表的第一个表项直接找到
  • 优点:两颗树存储在同一数组中时,归并两颗树简单
  • 缺点:找结点的右兄弟结点比较困难
  • 缺点:两颗树分别存储在不同数组中时,归并两颗树困难

4.3 左子结点/右兄弟结点表示法-----链式实现

4.3.1 存储形式

每个结点存储结点的值以及指向父结点、最左子结点和右侧兄弟结点。

left:指向最左子结点

right:指向第一个右兄弟结点的指针或索引

在这里插入图片描述

4.3.2 特点

比子结点表表示法空间效率更高,且每个结点的空间是固定长度的。

对于树的归并需要调整三个指针,一个是R的右兄弟,一个是R的父节点,和R’的最左儿子。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EkQxTBVR-1603616163369)(C:\Users\53263\Desktop\review\image-20201025115300709.png)]

4.4 动态结点表示法(1)-----链式实现

4.4.1 存储形式

动态结点表示法:每个结点存储一个基于数组的子结点指针表

struct dynamicNode { //动态表示树的树节点。假设限制有5个子节点
  dynamicNode *children[5];
  int data;
};

在这里插入图片描述

4.4.2 特点

对任意一个结点可能的子结点个数加以限制,对每个结点分配确定数目的指针域。因此假设在结点生成时就已经知道它的子结点数目,对子结点的数目强加了不必要的限制

4.5动态结点表示法(2) -----链式实现

4.5.1 存储形式

在这里插入图片描述

template<typename E>
struct node
{
node *next;
E elem;
}

struct dynamicNode
{
int val;
int size;
node<dynamicNode> * children;
};
  • 为每个结点分配可变的存储空间。
  • 每个结点存储一条子结点指针链表。
4.5.2 特点

灵活,动态分配结点空间

4.6 动态左子结点/右兄弟结点表示法-----链式实现

左子结点/右兄弟结点表示法本质上使用二叉树来替换树.使用这种方法可以把任意树,甚至是森林替换为二叉树。

其中,左子结点是原树中结点的最左子结点。右子结点是原树中结点的右侧兄弟结点。

在这里插入图片描述

可以看到,给定一棵树可以通过左孩子右兄弟的办法找到一棵唯一的二叉树。从物理结构来看,他与二叉链表是相同的,只是解释不同(left_Child right_Sibling 和 left_Child right_Child)

那么森林(不相交的树们)怎么转换为二叉树?

  1. 增加一个虚拟的根结点,它的儿子为各棵树的根。
  2. 那么,就变成了树转换成二叉树的问题。

4.7 K叉树

K叉树的结点有K个子结点

子结点的数目是固定的(K个)二叉树的许多特性可以推广到K叉树

4.8 树的顺序表示法

问题:怎么建立存储内容,怎么恢复成森林或二叉树

树的顺序表示法把结点的值按照它们在先根遍历中出现的顺序存储起来,描述树形状的充足信息同时也被存储起来。

4.8.1 先根遍历序列表示法

把树的结点值按照先根遍历序列的顺序列出,把所有非空结点看成分支结点,只有空指针NULL才被当做叶结点。

应用中的问题:

  • 为了找到A的右孩子,需要把其左子树都查找一遍。
  • 空间开销分析比使用指针高效
  • 根据满二叉树定理,其中空指针数目=n+1,结构性开销占了1/2(前面二叉树部分提到过)

目的:存储结点的值,尽可能少的存储用于重建树结构的信息

优点:节省空间,不需要存储指针

缺点:只允许顺序查找

应用:高效率磁盘存储分布式环境中计算机之间传输数据结构

4.8.2 带标记的先根序列表示法

显式地在每个结点后标识出它是叶结点还是分支结点

  • 分支结点加标记(’),而叶结点不加任何标记
  • 分支结点的空子结点以“/”表示,而叶结点的空子结点不加表示。
  • 因为不需要存储叶子结点的空指针,所需开销更小n结点中需要存储标记位的空间

在这里插入图片描述

以此图为例,表示为:A’ B’ / D C’ E’ G / F’ H I

  • 用顺序表示法存储树不仅要给出一个结点是分支结点还是叶节点,还必须给出有多少个子结点的信息。

  • 一种替代的方法是给出一个结点的子结点表结束的位置,如:用特殊标记“)”来标明子结点表的结束。

以此图为例,可以看作一个栈,当到了叶子结点就弹出一个结点。如果当前结点所有子树都遍历完了,就在弹出,弹出对应的就是)。比如RAC时,C为叶子节点,故RAC) 弹出C 观察 A的其他子结点,然后同理D)E).然后A树遍历完了,故RAC)D)E)) 然后到R树,还有一棵右子树 。BF ,F为叶子结点,故弹出一个,RAC)D)E))BF) 然后B树都遍历完了 RAC)D)E))BF) ,再他拿出一个B RAC)D)E))BF)) R树已经遍历完了,RAC)D)E))BF)))

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: (Tree)是一种非线性的数据结构,它由n(n>=0)个结点构成,其中一个结点为根结点,其余结点可分为m(m>=0)个互不相交的子集T1、T2、……、Tm,其中每一个子集本身又是一棵,并称为根的子的特点是:每个结点有零个或多个子结点;没有结点的结点称为根结点;每一个非根结点有且只有一个结点;除了根结点之外,每个子结点可以分为多个不相交的子。 下面是一个基本的的结构定义: ```c // 的结构体定义 typedef struct TreeNode { int data; // 数据域 struct TreeNode *firstChild;// 第一个子节点 struct TreeNode *nextSibling;// 兄弟节点 }TreeNode, *Tree; ``` 其中,data表示结点的数据域,firstChild指向该结点的第一个子节点,nextSibling指向该结点的下一个兄弟节点。 下面是一个创建的函数: ```c // 创建 Tree createTree(int data) { Tree root = (Tree)malloc(sizeof(TreeNode)); root->data = data; root->firstChild = NULL; root->nextSibling = NULL; return root; } ``` 下面是一个向中添加子节点的函数: ```c // 添加子节点 void addChild(Tree parent, int data) { Tree child = (Tree)malloc(sizeof(TreeNode)); child->data = data; child->firstChild = NULL; child->nextSibling = NULL; if (parent->firstChild == NULL) parent->firstChild = child; else { Tree p = parent->firstChild; while (p->nextSibling != NULL) p = p->nextSibling; p->nextSibling = child; } } ``` 下面是一个先序遍历的函数: ```c // 先序遍历 void preOrderTraversal(Tree root) { if (root != NULL) { printf("%d ", root->data); preOrderTraversal(root->firstChild); preOrderTraversal(root->nextSibling); } } ``` 下面是一个后序遍历的函数: ```c // 后序遍历 void postOrderTraversal(Tree root) { if (root != NULL) { postOrderTraversal(root->firstChild); postOrderTraversal(root->nextSibling); printf("%d ", root->data); } } ``` 下面是一个层次遍历的函数: ```c // 层次遍历 void levelOrderTraversal(Tree root) { Queue q; initQueue(&q); if (root != NULL) { enQueue(&q, root); while (!isQueueEmpty(&q)) { Tree p = deQueue(&q); printf("%d ", p->data); if (p->firstChild != NULL) enQueue(&q, p->firstChild); if (p->nextSibling != NULL) enQueue(&q, p->nextSibling); } } } ``` 其中,Queue是一个队列结构体。 以上就是一个简单的的实现。 ### 回答2: 是一种常见的数据结构,用于存储具有层次关系的数据。它由节点和边组成,通常包含一个根节点和若干子节点。 在C语言中,我们可以使用结构体来定义的节点。一个最基本的节点结构体可能包含两个成员变量:数据和指向子节点的指针。例如: ``` typedef struct TreeNode { int data; struct TreeNode* left; struct TreeNode* right; } TreeNode; ``` 上述代码定义了一个名为TreeNode的结构体,它包含一个整型数据成员和两个指向左子节点和右子节点的指针。 为了方便操作,我们可以定义一些基本的函数。其中,创建节点的函数可以使用动态内存分配来分配新节点的内存空间。例如: ``` TreeNode* createNode(int data) { TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode)); newNode->data = data; newNode->left = NULL; newNode->right = NULL; return newNode; } ``` 我们还可以定义插入节点、删除节点、搜索节点等函数来操作的结构。 除此之外,还可以实现一些遍历的算法,如先序遍历、中序遍历和后序遍历。这些遍历方法可以递归地遍历的节点,并对节点进行指定的操作。 编写的操作函数时,需要考虑到不同的特点,例如二叉搜索要保持左子节点的值小于根节点,右子节点的值大于根节点。 总之,编写一个经典C语言数据结构-需要定义节点的结构体,实现节点的创建、插入、删除和搜索等操作函数,同时可以实现遍历的算法。 ### 回答3: 是一种经典的数据结构,C语言可以很方便地实现的相关操作。首先,我们可以定义一个的节点结构体: ```c typedef struct Node { int data; // 节点存储的数据 struct Node* left; // 左子指针 struct Node* right; // 右子指针 } Node; ``` 接下来,我们可以实现一些的基本操作,例如创建、插入节点、删除节点和遍历。下面是一个简单的示例: ```c // 创建一个节点 Node* createNode(int data) { Node* newNode = (Node*)malloc(sizeof(Node)); newNode->data = data; newNode->left = NULL; newNode->right = NULL; return newNode; } // 在中插入一个节点 Node* insertNode(Node* root, int data) { if (root == NULL) { return createNode(data); } else if (data < root->data) { root->left = insertNode(root->left, data); } else if (data > root->data) { root->right = insertNode(root->right, data); } return root; } // 在中删除一个节点 Node* deleteNode(Node* root, int data) { if (root == NULL) { return root; } else if (data < root->data) { root->left = deleteNode(root->left, data); } else if (data > root->data) { root->right = deleteNode(root->right, data); } else { if (root->left == NULL) { Node* temp = root->right; free(root); return temp; } else if (root->right == NULL) { Node* temp = root->left; free(root); return temp; } Node* temp = findMinNode(root->right); root->data = temp->data; root->right = deleteNode(root->right, temp->data); } return root; } // 遍历:前序遍历(中-左-右) void preOrderTraversal(Node* root) { if (root != NULL) { printf("%d ", root->data); preOrderTraversal(root->left); preOrderTraversal(root->right); } } ``` 这只是的基本实现,还有很多其他操作,例如查找节点、判断是否为空、计算的高度等。可以根据具体需求进行扩展。通过以上实现,我们可以使用经典的C语言来构建和操作结构。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值