【数据结构与算法】树专题

发布时间 2023-05-30 11:00:05作者: 起司头_棕裤裤

树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。

在任意一棵非空树中:

(1)有且仅有一个特定的称为根(Root)的结点;

(2)当 n>1 时,其余结点可分为 m(m>0)个互不相交的有限集 T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。

  • 树的定义也是一种递归定义。
  • 根结点是唯一的,不可能存在多个根结点
  • 子树的个数没有限制,但它们一定是互不相交的。
image-20221204184708629
image-20221204184837932
子树

结点分类

  • 树的结点包含一个数据元素及若干指向其子树的分支。

  • 结点拥有的子树数称为结点的度(Degree)

  • 度为 0 的结点称为叶结点(Leaf)或终端结点

  • 度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点

  • 树的度是树内各结点的度的最大值。

image-20221204185309287
结点分类

结点间关系

  • 结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent)

  • 同一个双亲的孩子之间互称兄弟(Sibling)

  • 结点的祖先是从根到该结点所经分支上的所有结点

  • 以某结点为根的子树中的任一结点都称为该结点的子孙

image-20221204191730990
结点间的关系

其他相关概念

  • 结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。

  • 双亲在同一层的结点互为堂兄弟

  • 树中结点的最大层次称为树的深度(Depth)或高度

  • 如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树

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

image-20221205210229226
image-20221205210349127

树的存储结构

双亲表示法、孩子表示法、孩子兄弟表示法。

双亲表示法

以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。

image-20221206192605279
#define MAX_TREE_SIZE 100

typedef int TElemType;

/// 双亲表示法结点结构
typedef struct PTNode
{
    TElemType data;///< 结点的数据信息
    int parent;///< 双亲的下标
}PTNode;

/// 树的结构体
typedef struct
{
    PTNode nodes[MAX_TREE_SIZE];///< 结点数组
    int r;///< 根的位置
    int n;///< 结点数
}PTree;
image-20221206194638529
图示树用双亲表示法表示
下标 data parent
0 A -1
1 B 0
2 C 0
3 D 1
4 E 2
5 F 2
6 G 3
7 H 3
8 I 3
9 J 4

已知结点找其双亲的时间复杂度为 \(O(1)\) ,但是找结点的孩子需要遍历完整个结构。

多重链表表示法

  • 每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法。

  • 树的每个结点的度,也就是它的孩子个数是不同的。有两种解决方案:

方案一 指针域的个数等于树的度

  • 树的度是树各个结点度的最大值。指针域的个数等于树的度,这样每个结点的指针域都是够用的。
image-20221206200014959
image-20221206195739846
  • 树中各结点的度相差很大时,会浪费空间,因为有很多结点的指针域是空的。

方案二 指针域的个数等于该节点的度

image-20221206200052864 image-20221206200120903
  • 空间利用率是很高,但是由于各个结点的链表是不相同的结构,加上要维护结点的度的数值,在运算上就会带来时间上的损耗。

孩子表示法(邻接表)

  • 把每个结点的孩子结点排列起来,以单链表作存储结构,做该结点的孩子链表

  • 有 n 个结点,就有 n 个孩子链表,如果是叶结点则它的孩子链表为空。

  • n 个头指针组成一个线性表,采用顺序存储结构,存放进一个一维数组中

image-20221206200950087
  • 该结构有两种结点:
image-20221206201237816
孩子链表结点,child 存储孩子结点在表头数组中的下标,next指向下一个孩子结点
image-20221206201403062
表头结点,data 存储该结点的数据信息,firstchild 是该结点孩子链表的头指针
/// 孩子结点
typedef struct CTNode
{
    int child;          ///< 存储孩子结点在表头数组的下标
    struct CTNode* next;///< 指向双亲结点的下一个孩子
} *ChildPtr;

/// 表头结点
typedef struct
{
    TElemType data;      	///< 该结点的数据信息
    ChildPtr firstchild;	///< 孩子结点的头指针
}CTBox;

/// 树
typedef struct
{
    CTBox nodes[MAX_TREE_SIZE];///< 结点数组
    int r;///< 根的位置
    int n;///< 结点数
}CTree;

该方法的问题是,如果要找结点的双亲,需要遍历整棵树才行。

孩子兄弟表示法

  • 结点的第一个孩子如果存在就是唯一的。

  • 结点的右兄弟如果存在也是唯一的(❗❗❗右兄弟不是堂兄弟)。

  • 设置结点,除了数据域存储结点数据外,还设置两个指针域 firstchild 和 rightsib。

    • firstchild 指向该结点的第一个孩子。
    • rightsib 指向该结点的右兄弟。
image-20221206203745857
结点结构

可见这种存储结构与广义表的存储结构有点类似。

/// 树的孩子兄弟表示法
typedef struct CSNode
{
    TElemType data;     ///< 该结点的数据信息
    struct CSNode* firstchild;  ///< 指向第一个孩子结点的指针
    struct CSNode* rightsib;    ///< 指向该结点右兄弟的指针
}CSNode, *CSTree;
image-20221206204944047
  • 这种表示法,给查找某个结点的某个孩子带来了方便,只需要通过fistchild找到此结点的长子,然后再通过长子结点的rightsib找到它的二弟,接着一直下去,直到找到要找的孩子。
  • 但是对于找双亲还是有缺陷,可以再增加一个 parent 指针域来解决快速查找双亲的问题。
  • 孩子兄弟表示法的最大好处是它把一棵复杂的树变成了一棵二叉树。这样就可以充分利用二叉树的特性和算法来处理这棵树了。
image-20221206205415635

二叉树

二叉树(Binary Tree)是 n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。

image-20221206205713750
一个二叉树

二叉树的特点

  • 每个结点最多有两棵子树,所以二叉树中不存在度大于 2 的结点。
  • 左子树和右子树是有顺序的,次序不能任意颠倒。
  • 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
image-20221206205941480
树1只有左子树,树2只有右子树
image-20221206210153808
3 个结点的二叉树有 5 种可能的形态

特殊二叉树

斜树

  • 斜树就是斜着长的树。

    • 所有的结点都只有左子树的二叉树叫左斜树
    • 所有结点都是只有右子树的二叉树叫右斜树
  • 斜树结点的个数等于二叉树的深度。

image-20221206210735893
左斜树
image-20221206210809328
右斜树
  • 斜树的结构与线性表几乎相似,线性表结构就可以理解为是树的一种极其特殊的表现形式。

满二叉树

在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。

image-20221206211707674
满二叉树

完全二叉树

对一棵具有 n 个结点的二叉树按层序编号,如果编号为i(1≤i≤n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。

image-20221206212247633
完全二叉树

完全二叉树的性质

  • 叶子结点只能出现在最下两层。
  • 最下层的叶子一定集中在左部连续位置。
  • 倒数二层,若有叶子结点,一定都在右部连续位置。
  • 如果结点度为 1,则该结点只有左孩子,即不存在只有右子树的情况。
  • 同样结点数的二叉树,完全二叉树的深度最小。

二叉树的性质

性质 1

  • 在二叉树的第 \(i\) 层上至多有 \(2^{i-1}\) 个结点;

性质 2

  • 深度为 k (即层数为 k)的二叉树至多有 \(2^k-1\) 个结点;

第一层最多 1 个结点,第二层最多 2 个结点,... 第 k 层最多 \(2^{k-1}\) 个结点,由等比数列求和公式,深度为 k 的树的结点最大个数为:

\[\sum_{i = 1}^{k}2^{i-1} = 2^{k - 1} \]

性质 3

  • 设树的终端结点数(即叶子结点数)为 \(n_0\),度为 1 的结点数为 \(n_1\),度为 2 的结点数为 \(n_2\)
  • 结点总数 \(n=n_0 + n_1 + n_2\)
  • 分支线程总数:\(n-1=n_0+n_1+n_2-1\),分支线程总数还等于 \(n_1+2n_2\) ,得 \(n_0 = n_2 + 1\)

性质 4

  • 具有 n 个结点的完全二叉树的深度为 \(\lfloor\log_2n\rfloor+1\) (或 \(\lceil\log_2n\rceil\)

证明,设具有 n 个结点的完全二叉树的深度为 k,对于深度为 k 的满二叉树,它的结点数为 \(2^k-1\) ,对于深度为 \(k-1\) 的满二叉树,结点数为 \(2^{k-1}-1\) 。则有:

\[2^{k-1}-1 < n < 2^k-1\\ \Rightarrow 2^{k-1} < n + 1 < 2^k\\ \Rightarrow k-1 <\log_2(n+1) <k\\ \Rightarrow k-1 <\log_2n+\log_2(1+\frac{1}{n}) <k\\ \]

因此,二叉树的深度为 \(\lfloor\log_2n\rfloor+1\) ,或写成 \(\lceil\log_2n\rceil\)

性质 5

  • 性质5:如果对一棵有n个结点的完全二叉树(其深度为⌊log2n⌋+1)的结点按层序编号(从第1层到第⌊log2n⌋+1层,每层从左到右),对任一结点i(1≤i≤n)有:
    • 如果 \(i=1\),则结点 \(i\) 是二叉树的根,无双亲;如果 \(i>1\),则其双亲是结点 \(⌊i/2⌋\)
    • 如果 \(2i>n\),则结点 \(i\) 无左孩子(结点 \(i\) 为叶子结点);否则其左孩子是结点 \(2i\)
    • 如果 \(2i+1>n\) ,则结点 \(i\) 无右孩子;否则其右孩子是结点 \(2i+1\)

二叉树的存储结构

二叉树的顺序存储结构

  • 对于完全二叉树,完全可以用顺序存储结构:
image-20221208162253165 image-20221208162317384
完全二叉树的顺序存储
  • 对于一般二叉树,可以将其按完全二叉树编号,把不存在的结点设置为空: "^"。
image-20221208162519859 image-20221208162539519
一般二叉树的顺序存储
image-20221208162646646 image-20221208162710251
右斜树的顺序存储
  • 顺序存储一般只用于完全二叉树,用于其他二叉树可能会造成较大的空间浪费。

二叉链表

  • 使用链式存储结构,每个结点设计一个数据域存储数据,两个指针域指向左孩子和右孩子。这样的链表称为二叉链表
image-20221208163123956
二叉链表结点结构
/// 二叉树结点结构
typedef struct BiTNode
{
    TElemType data;///< 结点数据
    struct BiTNode* lchild;///< 左孩子指针
    struct BiTNode* rchild;///< 右孩子指针
}BiTNode, *BiTree;
image-20221208163937073
二叉链表存储结构示意
  • 如果有需要,还可以再增加一个指向其双亲的指针域,那样就称之为三叉链表

遍历二叉树

二叉树遍历原理

  • 二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。

  • 深度优先搜索(dfs):前序遍历、中序遍历、后序遍历

  • 宽度优先遍历(bfs):层序遍历

  • 二叉树的前中后序遍历一般使用递归算法

  • 递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。,因此用栈可以可以将递归方法转换为迭代方法,使用栈可以实现二叉树的前中后序的遍历了。

  • 层序遍历采用队列数据结构,每轮循环,同一层结点出栈,然后从左到右遍历该层结点的所有孩子结点,遍历的同时将孩子结点入队,下一轮循环,继续讲这一层的结点出栈,然后遍历孩子结点的孩子结点,依此类推。

前序遍历

规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。

image-20221208164603604
/// 二叉树前序遍历递归算法
void PreOrderTraverse(BiTree T)
{
    if (T == NULL)
        return;
    printf("%c", T->data);// 先访问根结点
    PreOrderTraverse(T->lchild);// 再先前序遍历左子树
    PreOrderTraverse(T->rchild);// 最后前序遍历右子树
}
/// 二叉树前序遍历迭代算法
vector<int> preorderTraversal(TreeNode* root) {
    vector<int> result;
    if(root == nullptr) return result;
    stack<TreeNode*> st;
    st.push(root);
    while(!st.empty()){
        TreeNode* cur = st.top();						// 中
        st.pop();
        result.emplace_back(cur->val);
        if(cur->right != nullptr) st.push(cur->right);	// 右(空节点不入栈)
        if(cur->left != nullptr) st.push(cur->left);	// 左(空节点不入栈)
    }
    return result;
}
中序遍历

规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。

image-20221208165040931
/// 二叉树中序遍历递归算法
void InOrderTraverse(BiTree T)
{
    // 如果为空即返回
    if (T == NULL)
        return;
    InOrderTraverse(T->lchild);// 先中序遍历左子树
    printf("%c", T->data);     // 然后访问根节点
    InOrderTraverse(T->rchild);// 最后中序遍历右子树
}
/// 二叉树中序遍历迭代算法
vector<int> inorderTraversal(TreeNode* root) {
    vector<int> result;
    stack<TreeNode*> st;
    TreeNode* cur = root;
    while (cur != NULL || !st.empty()) {
        if (cur != NULL) { // 指针来访问节点,访问到最底层
            st.push(cur); // 将访问的节点放进栈
            cur = cur->left;                // 左
        } else {
            cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
            st.pop();
            result.push_back(cur->val);     // 中
            cur = cur->right;               // 右
        }
    }
    return result;
}
后序遍历

规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。

image-20221208165246421
/// 后序遍历算法
void PostOrderTraverse(BiTree T)
{
    // 如果为空即返回
    if (T == NULL)
        return;
    PostOrderTraverse(T->lchild);// 先后序遍历左子树
    PostOrderTraverse(T->rchild);// 然后后序遍历右子树
    printf("%c", T->data);       // 最后访问根节点
}
image-20230527140222407
vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> result;
        if (root == NULL) return result;
        st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();
            st.pop();
            result.push_back(node->val);
            if (node->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)
            if (node->right) st.push(node->right); // 空节点不入栈
        }
        reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了
        return result;
}
层序遍历

规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。

image-20221208165415816
/// 层序遍历迭代法
vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int>> result;
    if(root == nullptr) return result;
    queue<TreeNode*> que;
    que.push(root);
    while(!que.empty()){
        int n = que.size();
        vector<int> row;
        for(int i = 0; i < n; i++){
            TreeNode* cur = que.front();
            que.pop();
            row.emplace_back(cur->val);
            if(cur->left != nullptr) que.push(cur->left);
            if(cur->right != nullptr) que.push(cur->right);
        }
        result.emplace_back(row);
    }

    return result;
}
/// 层序遍历 递归法
void bfs(vector<TreeNode*>& nodes, vector<vector<int>>& result){
    if(nodes.empty()) return;
    vector<TreeNode*> nextNodes;
    vector<int> row;
    for(auto& elem : nodes){
        row.emplace_back(elem->val);
        if(elem->left != nullptr) nextNodes.emplace_back(elem->left);
        if(elem->right != nullptr) nextNodes.emplace_back(elem->right);
    }
    result.emplace_back(row);
    bfs(nextNodes, result);
}

推导遍历结果

已知一棵二叉树的前序遍历序列为ABCDEF,中序遍历序列为CBAEDF,请问这棵二叉树的后序遍历结果是多少?

  • 由 ABCDEF 知树的根节点为 A,结合中序遍历CBAEDF 知 A 的 左子树包括 B、C 两个结点,右子树包括 D、E、F 三个结点。
image-20221208194216643
  • A 的左子树的前序遍历为 BC ,则 B 为 A 的左孩子。左子树的中序遍历为 CB,则 C 为 B 的左孩子
image-20221208194436340
  • A 的右子树的前序遍历为 DEF,说明 D 是 A 的右孩子,E 和 F 是 D 的子孙(不一定是左右孩子)。
  • A 的右子树的中遍历为 EDF,说明 E 是 D 的左孩子,F 是 D 的右孩子
image-20221208195103063
  • 由此可得后续遍历的结果为:CBEFDA

二叉树的中序序列是ABCDEFG,后序序列是BDCAFGE,求前序序列。

  • 由后序序列 BDCAFGE,可得 E 为树的根结点。在结合中序序列是 ABCDEFG 得 E 的左子树包括 ABCD,E的右子树包括 FG
  • E 的左子树后序序列为 BDCA ,可知 A 为 E 的左孩子。再结合中序序列是 ABCD 得 A 的右子树包括 BCD。
  • A 的右子树后序序列为 BDC,可知 C 为 A 的右孩子。再结合中序序列为 BCD 得 B 是 C 的左孩子,D 是 C 的右孩子。目前前序序列的分析结果为 EACBD
  • E 的右子树后序序列为FG,可知 G 是 E 的右孩子。再结合中序序列为 FG 得 F 为 G 的左孩子。E 的左子树的前序序列则为 GF
  • 最后整棵树的前序序列为 EACBDGF。

已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。

已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。

注意:已知前序和后序遍历,是不能确定一棵二叉树的。

二叉树的建立

通过输入遍历序列来构建二叉树。

  • 为了能让每个结点确认是否有左右孩子,对树进行了扩展:
image-20221209135035913
  • 如上图所示,扩展后的前序遍历就为 AB#D##C##
/// 以扩展二叉树的前序遍历来构造二叉树
void CreateBiTree(BiTree* T)
{
    TElemType ch;
    scanf_s("%c", &ch);
    if (ch == '#')
    {
        *T = NULL;
    }
    else
    {
        *T = malloc(sizeof(BiTNode));
        if (!*T)
            exit(1);// 溢出,异常退出
        (*T)->data = ch;// 生成根节点
        CreateBiTree(&(*T)->lchild);// 构造左子树
        CreateBiTree(&(*T)->rchild);// 构造右子树
    }
}