chapter_6 树

树的定义

树的形式定义

树:T={D, R}。D是包含n个结点的有限集合 (n≥0)。

当 n=0 时为空树, 否则关系R满足以下条件:

有且仅有一个结点 d0∈D,它对于关系 R 来说没有前驱结点,结点 d0 称作树的根结点。

除根结点外, 每个结点有且仅有一个前驱结点。

D 中每个结点可以有零个或多个后继结点。

树的递归定义

树是由 n(n≥0) 个结点组成的有限集合(记为T)。其中:

如果 n=0,它是一棵空树,这是树的特例;

如果 n>0,这 n 个结点中存在一个唯一结点作为树的根结点 (root),

其余结点可分为 m(m≥0) 个互不相交的有限子集 T1、T2、…、Tm,

而每个子集本身又是一棵树,称为根结点root的子树。

树中所有结点构成一种层次关系!

树的表示

  1. 树形表示法。使用一棵倒置的树表示树结构, 非常直观和形象。

  2. 文氏图表示法。使用集合以及集合的包含关系描述树结构。

  3. 凹入表示法。使用线段的伸缩关系描述树结构。

  4. 括号表示法。用一个字符串表示树。

    基本形式:根(子树1, 子树2, ..., 子树m)
    样例:A(B(E, F), C(G(J)), D(H, I(K, L, M)))

抽象数据类型

ADT Tree {
    数据对象 D:D是具有相同特性的数据元素的集合。
    数据关系 R:
        若D为空集,则称为空树。否则:
        (1) 在 D 中存在唯一的称为根的数据元素 root;
        (2) 当 n>1 时,其余结点可分为 m(m>0)个互不相交的有限集 T1,T2,…,Tm,
            其中每一棵子集本身又是一棵符合本定义的树,称为根root的子树。
    基本操作:
        初始化置空树
        按定义构造树
        判定树是否为空树
        给当前结点赋值
        求树的深度
        遍历
        将以c为根的树插入为结点p的第i棵子树
        将树清空
        销毁树的结构
        删除结点p的第i棵子树
        求树的根结点
        求当前结点的元素值
        求当前结点的双亲结点
        求当前结点的右兄弟
        ...
};

对比线性结构和树型结构的结构特点

第一个数据元素   (无前驱)
最后一个数据元素 (无后继)
其它数据元素    (一个前驱、一个后继)
------------------------------
根结点         (无前驱)
多个叶子结点    (无后继)
其它数据元素    (一个前驱、多个后继)

基本术语

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

结点的度:树中一个结点的子树的个数称为该结点的度。

树的度:树中各结点的度的最大值称为树的度,通常将度为m的树称为m次树或者m叉树。

叶子结点:度为零的结点称为终端结点或叶结点(或叶子结点)。

分支结点:度不为零的结点称为非终端结点,又叫分支结点;

度为 1 的结点称为单分支结点;度为 2 的结点称为双分支结点,依此类推。

路径:两个结点 di 和 dj 之间的结点序列 (di, di1, di2, …, dj) 称为路径,其中 <dx,dy> 是分支。

路径长度:路径长度等于路径所通过的结点数目减1(即路径上分支数目)。

孩子结点:在一棵树中,每个结点的后继,被称作该结点的孩子结点(或子女结点)。

双亲结点:在一棵树中,每个结点的前继,被称作该结点的双亲结点(或父母结点)

兄弟结点:具有同一双亲的孩子结点互为兄弟结点。

堂兄弟:双亲在同一层的节点互为堂兄弟或者说深度相同,但父节点不同的节点互为堂兄弟节点。

祖先结点:从根结点到达一个结点的路径上经过的所有结点被称作该结点的祖先结点。

子孙结点:在一棵树中,一个结点的所有子树中的结点称为该结点的子孙结点。

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

树的深度:树中结点的最大层次称为树的高度(或深度),也是树中叶子结点所在的最大层次。

有序树和无序树:若树中各结点的子树是按照一定的次序从左向右安排的,且相对次序是不能随意变换的,则称为有序树,否则称为无序树。

有向树:有确定的根,且树根和子树根之间为有向关系。

森林:是 m(m≥0)棵互不相交的树的集合,任何一棵非空树是一个二元组 Tree=(root, F),其中 root 被称为根结点,F 被称为子树森林。

树的性质

【性质1】树中的结点数等于所有结点的度数之和加 1。

\(n\) 表示总结点个数,\(n_i\) 表示度为 \(i (0≤i≤m)\) 的结点个数,则有:\(n = \sum_{i=0}^{m} n_i = \sum_{i=0}^{m} i*n_i +1\)

【例】一棵度为 4 的树 T 中,若有 20 个度为 4 的结点,10 个度为 3 的结点,1 个度为 2 的结点,10 个度为 1 的结点,则树 T 的叶子结点个数是( )。

A. 41
B. 82
C. 113
D. 122
结点个数表示:n 为总结点个数,ni 为度为 i(0≤i≤m) 的结点个数
n = n0+n1+n2+n3+n4 = n0+10+1+10+20 = n0+41;
n = 度之和 + 1 = n1+2n2+3n3+4n4+1 = 123;
n0 = n-41 = 123-41 = 82,答案为B。

【性质2】度为 \(m\) 的树中第 \(k\) 层上至多有 \(m^{k-1}\) 个结点。

如:度为 3 的树第 2 层至多有 3 个结点

【性质3】高度为 \(k\)\(m\) 叉树至多有 \(\frac{m^k-1}{m-1}\) 个结点。

m 叉树每层最多结点数:第 1 层:1,第 2 层:\(m^1\),第 3 层:\(m^2\),...,第 k 层:\(m^{k-1}\)

等比公式:\(Sn = a1 * (1-q^n)/(1-q); (q!=1)\)

【性质4】具有 \(n\) 个结点的 \(m\) 叉树的最小高度为 \(ceil(log_m(n(m-1)+1))\)

证明:m 叉树每层最多结点数:第 1 层:1,第 2 层:\(m^1\),第 3 层:\(m^2\),...,第 k 层:\(m^{k-1}\)

按照满叉树计算,其总结点数量:\(n=m^0+m^1+m^2+...+m^{k-1} = (m^k-1)/(m-1)\)

解得: \(k = log_m(n*(m-1)+1)\)

最小高度需要向上取整,所以: \(k = ceil(log_m(n*(m-1)+1))\)

\(floor(x)\) 称作 \(x\) 的底,表示不大于 \(x\) 的最大整数,常记作 \(⌊x⌋\)

\(ceil(x)\) 称作 \(x\) 的高,表示不小于 \(x\) 的最大整数,常记作 \(⌈x⌉\)

n=10,m=3
最小高度 = ceil(log3(10 ×(3-1)+1))
= ceil(log3(21))
= 3

二叉树的类型定义

定义:二叉树是有限的结点集合,这个集合或者是空,或者由一个根结点和两棵互不相交的称为左子树和右子树的二叉树组成。

二叉树的 5 种情况

  1. 空树
  2. 只含根结点
  3. 右子树为空树
  4. 左子树为空树
  5. 左右子树均不为空树

两类特殊的二叉树,下面给出不同的定义,但实质相同:

满二叉树:在一棵二叉树中,所有分支结点都有双分结点,并且叶结点都集中在二叉树的最下一层。

满二叉树:在一棵二叉树中,高度为 \(k\) 的满二叉树恰好有 \(2^k-1\) 个结点。

完全二叉树:树中所含的 n 个结点和满二叉树中编号为 1 至 n 的结点一一对应。

完全二叉树:在一棵二叉树中,最多只有下面两层的结点的度数小于 2,并且最下面一层的叶结点都依次排列在该层最左边的位置上。

完全二叉树实际上是对应的满二叉树删除叶结点层最右边若干个结点得到的。

二叉树的性质

【性质1】在二叉树的第 \(i\) 层上至多有 \(2^{i-1}\) 个结点 \((i≥1)\)

用归纳法证明:

归纳基:\(i=1\) 层时,只有一个根结点:\(2^{i-1} = 2^0 = 1\);

归纳假设:假设对所有的 \(j,1 ≤ j < i\),命题成立;

归纳证明:二叉树上每个结点至多有两棵子树,则第 \(i\) 层的结点数 = \(2^{i-2} * 2 = 2^{i-1}\)

【性质2】深度为 k 的二叉树上至多含 \(2^k-1\) 个结点 \((k≥1)\)

证明:深度为 k 的二叉树上的结点数至多为 $2^0 + 2^1+ ... +2^{k-1} =2^k-1 $。

【性质3】非空二叉树上叶结点数等于双分支结点数加 1,即:n0=n2+1。

证明: 设二叉树上结点总数 n = n0 + n1 + n2,又二叉树上分支总数 b = n1+2n2,

而 b = n-1 = n0 + n1 + n2 - 1,由此:n0 = n2 + 1。

思考:m 叉树有什么样的同类性质,并证明一下。

【性质4】具有 \(n\) 个结点的完全二叉树的深度为 \(floor(log_2 n) +1\)

证明:设完全二叉树的深度为 \(k\),则有 \(2^{k-1}-1<n≤2^k-1\) 或者 \(2^{k-1}≤n<2^k\)

\(k-1≤log_2 n<k\),因为 \(k\) 只能是整数,因此 \(k=floor(log_2 n)+1\)

【性质5】若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点:

(1) 若 i=1,则该结点是二叉树的根,无双亲,否则编号为 floor(i/2) 的结点为其双亲结点;

(2) 若 2i>n,则该结点无左孩子,否则,编号为 2i 的结点为其左孩子结点;

(3) 若 2i+1>n,则该结点无右孩子结点,否则,编号为2i+1 的结点为其右孩子结点。

【例】一棵二叉树中有 7 个度为 2 的结点和 5 个度为 1 的结点,其总共有( )个结点。

A. 16
B. 18
C. 20
D. 30
n2 = 7
n0 = n2+1 = 8,n1=5
结点总数 = n0+n1+n2 = 20,答案为 C。

【例】已知一棵完全二叉树的第6层(设根为第1层)有8个叶子结点,则该完全二叉树的结点个数最多是 。

A. 39
B. 52
C. 111
D. 119

完全二叉树的叶子结点只能在最下两层,而结点个数最多的情况一定是存在第 7 层。

所以第 1~6 层 构成一棵满二叉树,这棵树的总结点数:\(n1=2^6-1\)

第 7 层的结点数为第 6 层非叶子结点的子节点数,计算出第 6 层非叶子结点数:\(n2=2^{6-1}-8\)

则第 7 层结点数:\(n3=2*n2\)

所以该完全二叉树的结点个数最多是:\(2^6-1 + 2*(2^{6-1}-8) = 111\)

二叉树的存储结构

二叉树的顺序存储表示

按照满二叉树的结点层次编号,依次存放二叉树中的数据元素。

在顺序存储结构中,找一个结点的双亲和孩子都很容易。

#define MAX_TREE_SIZE 100                   // 二叉树的最大结点数
typedef TElemType SqBiTree[MAX_TREE_SIZE];  // 0号单元存储根结点
SqBiTree bt;

现有一棵树:A(B(D,E), C(F,G)),使用顺序存储的结果:bt = "ABCDEFG";

对于完全二叉树来说,其顺序存储是十分合适的。

对于一般的二叉树,特别是对于那些单分支结点较多的二叉树来说是很不合适的,
因为可能只有少数存储单元被利用,特别是对退化的二叉树(即每个分支结点都是单分支的),空间浪费更是惊人。

最好的情况就是满二叉树的情况;

最坏情况就是深度为 \(k\) 的且只有 \(k\) 个结点的单支数需要长度为 \(2^k-1\) 空间。

二叉树的链式存储表示

二叉链表

typedef struct BiTNode { // 结点结构
    TElemType data;
    struct BiTNode *lchild, *rchild; // 左右孩子指针
} BiTNode, *BiTree;

【思考】在 n 个结点的二叉链表中,有多少空指针域。

结点数为 n,则指针域数为 2n,
每个节点都应当是被一个指针域指向的(根节点除外),
现有节点数量除根节点外有 n-1 个,也就是有 n-1 个指针域被使用,
则空指针域数量:2n-(n-1) = n+1。

三叉链表

typedef struct TriTNode { // 结点结构
    TElemType data;
    struct TriTNode *lchild, *rchild; // 左右孩子指针
    struct TriTNode *parent;          //双亲指针
} TriTNode, *TriTree;

双亲链表

typedef struct BPTNode {// 结点结构
    TElemType data;
    int *parent;        // 指向双亲的指针
    char LRTag;         // 左、右孩子标志域
} BPTNode;

typedef struct BPTree { // 树结构
    BPTNode nodes[MAX_TREE_SIZE];
    int num_node;       // 结点数目
    int root;           // 根结点的位置
} BPTree;

二叉树的遍历

很多时候都需要进行二叉树的遍历,如:
查找满足某种特定关系的结点,查找当前结点的双亲结点;
插入或删除某个结点,在树的当前结点上插入一个新结点或删除当前结点的第i个孩子结点;
遍历树中每个结点。

树的遍历运算是指按某种方式访问树中的每一个结点且每一个结点只被访问一次。

主要的遍历方法:

先根遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树。

后根遍历:若树不空,则先依次后根遍历各棵子树,然后访问根结点。

层次遍历:若树不空,则自上而下、自左至右访问树中每个结点。

注意:先根和后根遍历算法都是递归的。

如果不考虑子树的先后顺序那么最后的遍历方案数量为:\(A_n^n\)

现有一棵树:A(B(D,E), C(F,G)),在各种遍历方式下的不同情况。

  1. 先序遍历【根左右】:ABDECFG
  2. 中序遍历【左根右】:DBEAFCG
  3. 后序遍历【左右根】:DEBFGCA
  4. 层序遍历【上下左右】:ABCDEFG

递归遍历算法的时空复杂度分析:

由于每个节点都被访问一次,所以时间复杂度为 O(n);

如果采用结点存储,则需要建立 n个结点,所以空间复杂度为 O(n);

如果采用顺序存储,则最多需要建立 2*n+1个结点,所以空间复杂度任然为 O(n)。

非递归遍历算法:借助于栈/队列来实现。

将当前元素入栈,根据遍历顺序决定是否出栈。

同一棵二叉树(假设每个结点值唯一)具有唯一先序序列、中序序列和后序序列。

但不同的二叉树可能具有相同的先序序列、中序序列或后序序列。

  1. 同时给定一棵二叉树的先序序列和中序序列就能唯一确定这棵二叉树。【正确】
  2. 同时给定一棵二叉树的后序序列和中序序列就能唯一确定这棵二叉树。【正确】
  3. 同时给定一棵二叉树的层序序列和中序序列就能唯一确定这棵二叉树。【正确】
  4. 同时给定一棵二叉树的先序序列和后序序列就能唯一确定这棵二叉树。【错误】

【例】由一个固定的先序序列(含n个不同的结点),构造的二叉树个数?

【答案】num = C(n, 2n)/(n+1) = (2n)!/(n+1)!/n!; // 第n个Catalan数

令其为h(n)的话,满足 \(h(n)= h(0)*h(n-1)+h(1)*h(n-2) + ... + h(n-1)*h(0) (n>=2)\);

【例】若某非空二叉树的先序序列和中序序列正好相反,则该二叉树的形态是什么?

【答案】先序序列:N L R,中序序列的反序:R N L;

则 R=NULL,所有结点没有右子树的单支树

二叉树的建树

【例】给定一个层数小于等于 10 的二叉树,输出对其遍历的节点名序列。

输入包括一行,为由空格分隔开的各节点,按照二叉树的分层遍历顺序给出,
每个节点形式如 X(Y,num),X 表示该节点,Y 表示父节点,num 为 0,1,2 中的一个,
0 表示根节点,1 表示为父节点的左子节点,2 表示为父节点的右子节点。

输出为三行,为前序遍历,中序遍历,后序遍历的结果。

输入样例:

A(0,0) B(A,1) C(A,2) D(B,1) E(B,2) F(C,1) G(D,1) H(D,2)

输出样例:

A B D G H E C F
G D H B E A F C
G H D E B F C A
#include<iostream>
using namespace std;
const int N=(1<<10);
char a[100], tree[N];
int n=0, m=0;
void preorder(char root){
    if(root=='\0') return;
    if(tree[0]==root) printf("%c", root);
    else printf(" %c", root);
    preorder(tree[2*root]);
    preorder(tree[2*root+1]);
}
void inorder(char root){
    if(root=='\0') return;
    inorder(tree[2*root]);
    if(m==n-1) {
        printf("%c", root); m++;
    } else {
        printf("%c ", root); m++;
    }
    inorder(tree[2*root+1]);
}
void postorder(char root){
    if(root=='\0') return;
    postorder(tree[2*root]);
    postorder(tree[2*root+1]);
    if(tree[0]==root) printf("%c", root);
    else printf("%c ", root);
}
int main(){
    freopen("data.in", "r", stdin);
    while(~scanf("%s", a)){
        char x,y,num; sscanf(a, "%c(%c,%c)", &x,&y,&num);
        if(num=='0'){
            tree[0] = x;
        }else if(num=='1'){
            tree[2*y] = x;// y是单个字符,在内存中以ASCII码的形式进行存储
        }else if(num=='2'){
            tree[2*y+1] = x;
        }
    }
    preorder(tree[0]); printf("\n");
    inorder(tree[0]);  printf("\n");
    postorder(tree[0]);printf("\n");
    return 0;
}

【题目描述】输入一串先序遍历字符串,根据此字符串建立一个二叉树。

例如如下的先序遍历字符串:ABC##DE#G##F###

其中 '#' 表示的是空格,空格字符代表空树。

建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。

【提示】

由先序遍历建立二叉树的过程:

  1. 确定更节点
  2. 确定根节点的左子树
  3. 确定根节点的右子树

建立之后的二叉树为:
image

【输入格式】输入包括 1 行字符串,长度不超过 100。

【输出格式】可能有多组测试数据,对于每组数据,

输出将输入字符串建立二叉树后中序遍历的序列,每个字符后面都有一个空格。

每个输出结果占一行。

【样例输入】abc##de#g##f###

【样例输出】c b e g d f a

#include<stdio.h>
#include<stdlib.h>
#define N 110
char a[N];
int cnt=0;
typedef struct Node{
    char data;
    struct Node *lch, *rch;
}Node, *Tree;
//递归建树
struct Node* build(){
    struct Node *root = NULL;
    if(a[cnt++]!='#'){
        root = (struct Node*)malloc(sizeof(struct Node));
        root->data = a[cnt-1];
        root->lch = build();
        root->rch = build();
    }
    return root;
}
//中序遍历
void inorder(struct Node *root){
    if(root!=NULL){
        inorder(root->lch);
        printf("%c ", root->data);
        inorder(root->rch);
    }
}
//销毁二叉树
void del(struct Node* root){
    if(root!=NULL){
        del(root->lch);
        del(root->rch);
        free(root);
        root = NULL;
    }
}
int main(){
    while(~scanf("%s", a)){
        cnt=0;
        struct Node *root = build();
        inorder(root);  printf("\n");
        del(root);
    }
    return 0;
}

【例】由二叉树的先序和中序序列建树

仅知二叉树的先序序列 “abcdefg” 不能唯一确定一棵二叉树,

如果同时已知二叉树的中序序列 “cbdaegf”,则会如何?

image

#include<stdio.h>
#include<string.h>
#define N 110
char pre[N],in[N],post[N];
typedef struct {
    char data;
    struct Node *lch, *rch;
}Node;
//根据先序遍历+中序遍历建树
Node* build(int pl,int pr,int il,int ir) {
    if(pl>pr) return NULL;
    Node* root = (Node*)malloc(sizeof(Node));
    root->data = pre[pl];
    int k=il;
    while(pre[pl]!=in[k] && k<=ir) k++;
    root->lch = build(pl+1, pl+k-il, il, k-1);
    root->rch = build(pl+k-il+1, pr, k+1, ir);
    return root;
}
//根据后序遍历+中序遍历建树
Node* build2(int pl,int pr,int il,int ir) {
    if(pl>pr) return NULL;
    Node* root = (Node*)malloc(sizeof(Node));
    root->data = post[pr];
    int k=il;
    while(post[pr]!=in[k] && k<=ir) k++;
    root->lch = build2(pl, pl+k-il-1, il, k-1);
    root->rch = build2(pl+k-il, pr-1, k+1, ir);
    return root;
}
//先序遍历
void preorder(Node* root) {
    if(root==NULL) return;
    printf("%c", root->data);
    preorder(root->lch);
    preorder(root->rch);
}
//中序遍历
void inorder(Node* root) {
    if(root==NULL) return;
    inorder(root->lch);
    printf("%c", root->data);
    inorder(root->rch);
}
//后序遍历
void postorder(Node* root) {
    if(root==NULL) return;
    postorder(root->lch);
    postorder(root->rch);
    printf("%c", root->data);
}
//层序遍历
void levorder(Node* root) {
    int head=0, rear=-1;
    Node* que[110];
    que[++rear]=root;
    while(head <= rear) {
        Node* node = que[head];
        if(node->lch!=NULL) que[++rear] = node->lch;
        if(node->rch!=NULL) que[++rear] = node->rch;
        ++head;
        printf("%c", node->data);
    }
}
int main() {
    freopen("data.in", "r", stdin);
    while(~scanf("%s%s%s",pre,in,post)) {
        int len = strlen(in)-1;
        Node* tree = build(0,len,0,len);
//        Node* tree = build2(0,len,0,len);
        preorder(tree);  puts("");
        inorder(tree);   puts("");
        postorder(tree); puts("");
        levorder(tree);  puts("");
        memset(pre, 0, sizeof(pre));
        memset(in, 0, sizeof(in));
        memset(post, 0, sizeof(post));
    }
    return 0;
}

二叉树的一些算法

  • 复制二叉树
  1. 如果当前根节点为空,直接返回 NULL;
  2. 否则先复制根节点;
  3. 再复制左子树,右子树。
struct Node* Copy(Node* tree){
    if(tree==NULL) return NULL;
    struct Node* root=(struct Node*)malloc(sizeof(struct Node));
    root->data = tree->data;
    root->lch = Copy(tree->lch);
    root->rch = Copy(tree->rch);
    return root;
}
  • 计算二叉树深度
  1. 如果当前根节点为空,直接返回 0;
  2. 否则返回左右子树最大深度 + 1。
int max(int a,int b){
    return a > b ? a : b;
}
int depth(struct Node* root){
    if(root==NULL) return 0;
    return max(depth(root->lch), depth(root->rch))+1;
}
  • 计算二叉树结点个数
  1. 如果当前根结点为空,直接返回 0;
  2. 否则返回左右子树结点数 + 1。
int count(struct Node* root){
    if(root==NULL) return 0;
    return count(root->lch) + count(root->rch) + 1;
}
  • 计算二叉树叶子结点数
  1. 如果当前根结点为空,直接返回 0;
  2. 否则返回当前左子树叶子结点数 + 右子树叶子结点数。
int count2(struct Node* root){
    if(root==NULL) return 0;
    if(root->lch==NULL && root->rch==NULL) return 1;
    return count2(root->lch)+count2(root->rch);
}

树和森林

树:是由 n(n≥0) 个结点组成的有限集合(记为T)。其中:
如果 n=0,它是一棵空树,这是树的特例;
如果 n>0,这 n 个结点中存在一个唯一结点作为树的根结点 (root),
其余结点可分为 m(m≥0) 个互不相交的有限子集 T1、T2、…、Tm,
而每个子集本身又是一棵树,称为根结点root的子树。

森林:是 m(m≥0)棵互不相交的树的集合,任何一棵非空树是一个二元组 Tree=(root, F),其中 root 被称为根结点,F 被称为子树森林。

删除树的根节点,会变成森林,给森林加入一个根节点会生成一棵树。

树的存储结构:

  1. 双亲表示法
  2. 孩子表示法
  3. 孩子兄弟表示法(又称二叉树表示法,或二叉链表表示法)

三种表示方法,各自表示形式要明白

树与二叉树的转换

森林与二叉树的转换

树和森林的遍历

树的三种遍历方式:先,后,层

二叉树的四种遍历方式:先,中,后,层

森林的两种遍历方式:先,中

线索二叉树

二叉树的遍历本质上是将一个复杂的非线性结构转换为线性结构,使每个结点都有了唯一前驱和后继。
(第一个结点无前驱,最后一个结点无后继)

对于二叉树的一个结点,查找其左右子女是方便的,其前驱后继只有在遍历中得到。

为了容易找到前驱和后继,有两种方法。

  1. 在结点结构中增加向前和向后的指针,这种方法增加了存储开销,不可取。
  2. 利用二叉树的空链指针。

对于具有 n 个结点的二叉树,采用二叉链存储结构时,每个结点有两个指针域,总共有 2n 个指针域。
其中只有 n-1 个结点被有效指针所指向,即有 n-1 个非空指针域,所以共有 2n-(n-1) = n+1 个空链域。

在二叉链表的结点中增加两个标志域,并作如下规定:

若该结点的左子树不空,则 Lchild 域的指针指向其左子树,且左标志域的值为 “指针 Link”;
否则,Lchild域的指针指向其“前驱”,且左标志的值为“线索 Thread” 。

若该结点的右子树不空,则rchild域的指针指向其右子树,且右标志域的值为 “指针 Link”;
否则,rchild域的指针指向其“后继”,且右标志的值为“线索 Thread”。

现将二叉树的结点结构重新定义如下

typedef struct node {
    ElemType data;          //结点数据域
    int ltag,rtag;          //增加的线索标记
    struct node *lchild;    //左孩子或线索指针
    struct node *rchild;    //右孩子或线索指针
}  TBTNode;                 //线索树结点类型定义

其中:
ltag=0 时,lchild 指向左子女;
ltag=1 时,lchild 指向前驱;
rtag=0 时,rchild 指向右子女;
rtag=1 时,rchild 指向后继;

采用某种方法遍历二叉树的结果是一个结点的线性序列。

修改空链域改为存放指向结点的前驱和后继结点的地址。

这样的指向该线性序列中的“前驱”和“后继”的指针,称作线索(thread)。

创建线索的过程称为线索化。

线索化的二叉树称为线索二叉树(Threaded BinaryTree)。

根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。

线索二叉树的目的是提高该遍历过程的效率。

加上了线索的二叉链表称为线索链表。

线索链表解决了无法直接找到该结点在某种遍历序列中的前驱和后继结点的问题,解决了二叉链表找左、右孩子困难的问题。

#include<stdio.h>
#include<stdlib.h>
#define ElemType char
typedef struct node {
    ElemType data;         //结点数据域
    int ltag,rtag;         //增加的线索标记
    struct node *lchild;   //左孩子或线索指针
    struct node *rchild;   //右孩子或线索指针
}  TBTNode;                //线索树结点类型定义
TBTNode *pre;              //全局变量,始终指向前驱结点

//对二叉树进行中序线索化
void Thread(TBTNode *p) {
    if(p!=NULL){
        Thread(p->lchild);     //左子树线索化
        if(p->lchild==NULL){   //前驱线索化
            p->lchild=pre;     //建立当前结点的前驱线索
            p->ltag=1;
        } else  p->ltag=0;
        if(pre->rchild==NULL){//后继线索化
            pre->rchild=p;    //建立前驱结点的后继线索
            pre->rtag=1;
        } else  pre->rtag=0;
        pre=p;
        Thread(p->rchild);    //递归调用右子树线索化
    }
}
//创建头结点,中序线索化二叉树
TBTNode *CreatThread(TBTNode *b) {
    TBTNode *root=(TBTNode *)malloc(sizeof(TBTNode));  //创建头结点
    root->ltag=0;
    root->rtag=1;
    root->rchild=b;
    if (b==NULL) root->lchild=root;    //空二叉树
    else {
        root->lchild=b;
        pre=root;            //pre是p的前驱结点,供加线索用
        Thread(b);           //中序遍历线索化二叉树
        pre->rchild=root;    //最后处理,加入指向头结点的线索
        pre->rtag=1;
        root->rchild=pre;    //头结点右线索化
    }
    return root;
}
//中序线索二叉树遍历
void ThInOrder(TBTNode *root) {
    TBTNode *p=root->lchild;              //p指向根结点
    while (p!=root) {
        while (p->ltag==0) p=p->lchild;   //找开始结点
        printf("%c ",p->data);            //访问开始结点
        while (p->rtag==1 && p->rchild!=root) {
            p=p->rchild;
            printf("%c ",p->data);
        }
        p=p->rchild;
    }
}
//递归建树
TBTNode* build(){
    char c; scanf("%c", &c);
    TBTNode *root = NULL;
    if(c!='#'){
        root = (TBTNode*)malloc(sizeof(TBTNode));
        root->data = c;
        root->lchild = build();
        root->rchild = build();
    }
    return root;
}
//中序遍历
void inorder(TBTNode* root){
    if(root==NULL) return;
    inorder(root->lchild);
    printf("%c ", root->data);
    inorder(root->rchild);
}
int main() {
    printf("please input preorder:");
    TBTNode *root=build();
    inorder(root); printf("\n");

    TBTNode* tree = CreatThread(root);
    ThInOrder(tree); printf("\n");
    return 0;
}

对二叉树中序遍历,递归算法:时间复杂度均为O(n),空间复杂度均为O(h)

对二叉树中序遍历,非递归算法:时间复杂度均为O(n),空间复杂度均为O(h)

对中序线索二叉树中序遍历,时间复杂度均为O(n),空间复杂度均为O(1)

二叉排序树/BST

二叉排序树(Binary Sort Tree),又称二叉查找树或者二叉搜索树,它首先是一个二叉树,而且必须满足下面的条件:

  1. 若左子树不空,则左子树上所有结点的值均小于它的根节点的值;
  2. 若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  3. 左、右子树也分别为二叉排序树;
  4. 没有键值相等的节点。

(1)二叉排序树的查找

由于二叉排序树可以看成是一个有序表,所以在二叉排序树上进行查找类似于折半查找,即逐步缩小查找范围的过程。

具体步骤:
若查找的关键字等于根结点的关键字,查找成功;
若查找的关键字小于根结点的关键字,递归查找左子树;
若查找的关键字大于根结点的关键字,递归查找右子树。
若子树为空,则查找不成功。

  • 程序实现
Node* search(Node* root, int x){    // BST 查找
    if(root==NULL || root->data==x)  return root;
    if(root->data > x)  return search(root->lchild, x);
    else  return search(root->rchild, x);
}

一棵树的平均查找长度(ASL,Average Search Length)为所有结点深度之和的平均值。

\(ASL = \sum_{i=0}^{i=n}{depth[i]} / N\)

      45
     / \
   24   53
   /\    \
 12  37   93    ASL = (1+2+2+3+3+3)/6 = 14/6;
12
 \
  24
   \
    37
     \
      45
       \
        53
         \
          93    ASL = (1+2+3+4+5+6)/6 = 21/6;

(2)二叉排序树的插入

首先查找待插入的记录是否在树中,如果存在,则不允许插入重复关键字;
如果直到找到叶子结点仍没有发现重复关键字,则把待插结点作为新的叶子结点插入。

具体步骤:
若二叉排序树为空树,则新插入的结点为新的根结点;
否则,新插入的结点必为一个新的叶子结点,其插入位置由查找不成功的位置确定。

  • 程序实现
Node* insert(Node* root, int x){     // BST 插入
    if(root==NULL){
        root=(Node*)malloc(sizeof(Node));
        root->lchild = root->rchild = NULL;
        root->data = x;
        return root;
    }
    if(root->data > x)  root->lchild=insert(root->lchild, x);
    else  root->rchild=insert(root->rchild, x);
    return root;
}

(3)二叉排序树的删除

由于二叉排序树要求关键字之间满足一定的大小关系,这就使得从树中删除一个结点的算法相对复杂。

具体步骤:
首先在二叉排序树中查找待删结点,如果不存在则不作任何操作;
否则,分以下三种情况进行讨论:
(1)待删结点是叶子结点:只需要将待删结点的父结点的相应孩子结点置为NULL。
(2)待删结点只有左/右子树:只需要将待删结点的左/右子树置为其父节点的左/右子树。
(3)待删结点既有左子树,也有右子树:找到替换待删结点的结点(就是待删结点的直接前驱或直接后继结点)进行替换。

寻找替换待删结点位置的步骤:
以待删结点为划分,
其左子树就是它的前驱结点,左子树的最右下角的结点即为待删结点的直接前驱结点;
其右子树就是它的后继结点,右子树的最左下角的结点即为待删结点的直接后继结点。

所以替换结点或者恰好就是个叶子结点,或者就是只带了左子树或右子树的情况。
下列程序中使用的是前驱节点的方式。

  • 程序实现
Node* deleteBST(Node* root,int x) { // BST 删除
    Node *pre, *cur=root;
    while(cur!=NULL) { // 查找 x 所在位置
        if(cur->data==x)  break;
        pre = cur;
        if(cur->data > x)  cur=cur->lchild;
        else if(cur->data < x)  cur=cur->rchild;
    }
    if(cur==NULL) {
        printf("要删除的结点不在数中\n"); return root;
    }
    if(cur->lchild==NULL && cur->rchild==NULL) {
        printf("0 0\n"); //测试结点左右子树情况,看是否跑对路
        // 叶子节点,直接删除
        if(pre->lchild!=NULL && pre->lchild->data==cur->data) pre->lchild=NULL;
        else if(pre->rchild!=NULL && pre->rchild->data==cur->data) pre->rchild=NULL;
    } else if(cur->lchild!=NULL && cur->rchild==NULL) {
        printf("1 0\n"); 
        // 只有左子树,待删除结点的左子树直接继承该结点
        if(pre->lchild!=NULL && pre->lchild->data==cur->data) {
            pre->lchild=cur->lchild;
        } else if(pre->rchild!=NULL && pre->rchild->data==cur->data) {
            pre->rchild=cur->lchild;
        }
    } else if(cur->lchild==NULL && cur->rchild!=NULL) {
        printf("0 1\n");
        // 只有右子树,待删除结点的右子树直接继承该结点
        if(pre->lchild!=NULL && pre->lchild->data==cur->data) {
            pre->lchild=cur->rchild;
        } else if(pre->rchild!=NULL && pre->rchild->data==cur->data) {
            pre->rchild=cur->rchild;
        }
    } else { // 左右子树都不为空,寻找替换结点
        printf("1 1\n");
        Node *temp=cur->lchild;
        while(temp->rchild!=NULL) temp=temp->rchild;
        temp->rchild = cur->rchild;
        root=temp;
    }
    free(cur); // 释放结点空间
    return root;
}
  • 完整程序
#include<stdio.h>
#include<stdlib.h>
typedef struct Node {
    int data;
    struct Node *lchild, *rchild;
} Node;

Node* insert(Node* root, int x){     // BST 插入
    if(root==NULL){
        root=(Node*)malloc(sizeof(Node));
        root->lchild = root->rchild = NULL;
        root->data = x;
        return root;
    }
    if(root->data > x)  root->lchild=insert(root->lchild, x);
    else  root->rchild=insert(root->rchild, x);
    return root;
}
Node* search(Node* root, int x) {   // BST 查找
    if(root==NULL || root->data==x)  return root;
    if(root->data > x)  return search(root->lchild, x);
    else  return search(root->rchild, x);
}
Node* deleteBST(Node* root,int x) { // BST 删除
    Node *pre, *cur=root;
    while(cur!=NULL) { // 查找 x 所在位置
        if(cur->data==x)  break;
        pre = cur;
        if(cur->data > x)  cur=cur->lchild;
        else if(cur->data < x)  cur=cur->rchild;
    }
    if(cur==NULL) {
        printf("要删除的结点不在数中\n"); return root;
    }
    if(cur->lchild==NULL && cur->rchild==NULL) {
        printf("0 0\n"); //测试结点左右子树情况,看是否跑对路
        // 叶子节点,直接删除
        if(pre->lchild!=NULL && pre->lchild->data==cur->data) pre->lchild=NULL;
        else if(pre->rchild!=NULL && pre->rchild->data==cur->data) pre->rchild=NULL;
    } else if(cur->lchild!=NULL && cur->rchild==NULL) {
        printf("1 0\n"); 
        // 只有左子树,待删除结点的左子树直接继承该结点
        if(pre->lchild!=NULL && pre->lchild->data==cur->data) {
            pre->lchild=cur->lchild;
        } else if(pre->rchild!=NULL && pre->rchild->data==cur->data) {
            pre->rchild=cur->lchild;
        }
    } else if(cur->lchild==NULL && cur->rchild!=NULL) {
        printf("0 1\n");
        // 只有右子树,待删除结点的右子树直接继承该结点
        if(pre->lchild!=NULL && pre->lchild->data==cur->data) {
            pre->lchild=cur->rchild;
        } else if(pre->rchild!=NULL && pre->rchild->data==cur->data) {
            pre->rchild=cur->rchild;
        }
    } else { // 左右子树都不为空,寻找替换结点
        printf("1 1\n");
        Node *temp=cur->lchild;
        while(temp->rchild!=NULL) temp=temp->rchild;
        temp->rchild = cur->rchild;
        root=temp;
    }
    free(cur); // 释放结点空间
    return root;
}
void preorder(Node* root) { //先序遍历
    if(root==NULL) return;
    printf("%d ", root->data);
    preorder(root->lchild);
    preorder(root->rchild);
}
void inorder(Node* root) { //中序遍历
    if(root==NULL) return;
    inorder(root->lchild);
    printf("%d ", root->data);
    inorder(root->rchild);
}
int depth(Node* root) { // 求二叉树深度
    if(root==NULL) return 0;
    int ldep = depth(root->lchild);
    int rdep = depth(root->rchild);
    return (ldep > rdep ? ldep : rdep) +1;
}
int main() {
    freopen("data.in", "r", stdin);
    Node* root = NULL;
    int x; // in: 4 3 2 1 0 5 6 7 8 9
    while(~scanf("%d", &x)) root = insert(root, x);
    preorder(root); printf("\n");
    inorder(root); printf("\n");
    printf("depth = %d\n", depth(root));

    x = -1;
    Node* node=search(root, x);
    printf("search(%2d) = %2d\n", x, node==NULL ? -1: node->data);

    x = 1; node=search(root, x);
    printf("search(%2d) = %2d\n", x, node==NULL ? -1 : node->data);

    x = 2; node=search(root, x);
    printf("search(%2d) = %2d\n", x, node==NULL ? -1 : node->data);

    for(int i=2; i>=-1; i--) {
        printf("删除结点:%d\n", i);
        root = deleteBST(root, i);
        preorder(root); printf("\n");
        inorder(root);  printf("\n");
    }
    for(int i=4; i<7; i++) {
        printf("删除结点:%d\n", i);
        root = deleteBST(root, i);
        preorder(root); printf("\n");
        inorder(root);  printf("\n");
    }
    return 0;
}

由上述思想如果给定序列是有序情况下,建立的树会退化为线性情况,可以通过随机化处理在一定程度上避免。

参考文章:https://blog.csdn.net/weixin_39651041/article/details/80022177

平衡二叉树/AVL

当数据有序或者接近有序时,使用二叉搜索树进行存储时,得到的二叉搜索树是一颗单支树,其搜索的时间复杂度为O(N)。
因此引入了AVL树,AVL树的名字来源于它的发明作者G.M. Adelson-Velsky 和 E.M. Landis。
AVL树是最先发明的自平衡二叉查找树(Self-Balancing Binary Search Tree,简称平衡二叉树)。

平衡二叉树定义(AVL):
它或者是一颗空树,或者具有以下性质的二叉树:

  1. 是一棵二叉排序树;
  2. 它的左子树和右子树的深度之差(平衡因子)的绝对值不超过1;
  3. 它的左子树和右子树都是一颗平衡二叉树。

某结点的左子树与右子树的高度(深度)差即为该结点的平衡因子(BF,Balance Factor)。
平衡二叉树上所有结点的平衡因子只可能是 -1,0 或 1。
如果某一结点的平衡因子绝对值大于1则说明此树不是平衡二叉树。
为了方便计算每一结点的平衡因子我们可以为每个节点赋予height这一属性,表示此节点的高度。

AVL树的查找、插入、删除操作在平均和最坏的情况下都是O(logn),这得益于它时刻维护着二叉树的平衡。
AVL树也是一颗二叉搜索树,因此它在插入数据时也需要先找到要插入的位置然后在将节点插入。
不同的是,AVL树插入节点后需要对节点的平衡因子进行调整,
如果插入节点后平衡因子的绝对值大于1,则还需要对该树进行旋转,旋转成为一颗高度平衡的二叉搜索树。

往平衡二叉树中添加节点很可能会导致二叉树失去平衡,所以我们需要在每次插入节点后进行平衡的维护操作。
插入节点破坏平衡性有四种情况:LL型, RR型, LR型, RL型。

  1. LL的意思是向左子树(L)的左孩子(L)中插入新节点后导致不平衡,这种情况下需要右旋操作。

  2. RR的意思是向右子树(R)的右孩子(R)中插入新节点后导致不平衡,这种情况下需要左旋操作。

  3. LR的意思是向左子树(L)的右孩子(R)中插入新节点后导致不平衡,这种情况下需要先左后右旋操作。

  4. RL的意思是向右子树(R)的左孩子(L)中插入新节点后导致不平衡,这种情况下需要先右后左旋操作。

很棒的文章:https://blog.csdn.net/isunbin/article/details/81707606

红黑树/RB

R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。
红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

红黑树的特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NULL)是黑色。
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

哈夫曼树与哈夫曼编码

给定 N个权值作为 N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树/赫夫曼树(Huffman Tree)。

哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

结点的路径长度:从根结点到该结点的路径上分支的数目。

树的路径长度:树根到树中每一个结点的路径长度之和。

树的带权路径长度 WPL:就是树中所有的叶结点的权值乘上其到根结点的路径长度,记 WPL=(W1L1+W2L2+W3L3+…+ WnLn) ,N个权值 Wi(i=1,2,…n)构成一棵有 N个叶结点的二叉树,相应的叶结点的路径长度为 Li(i=1,2,…n)。

哈夫曼树不唯一,完全二叉树不一定是哈夫曼树;

哈夫曼树通过构建 n-1 次子树,达成目标,最后构建的二叉树中只含有度为 2 或 0 的结点,其中度为 2 的结点正是新构建的结点。

严格二叉树:树中没有度为1的结点。

  • 构造哈夫曼树

(1)给定的 n个权值 {W1,W2,…,Wn} 构造n棵只有一个叶结点的二叉树,从而得到一个二叉树的集合 F={T1,T2,…,Tn}。
(2)在 F中选取根结点的权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,这棵新的二叉树根结点的权值为其左、右子树根结点权值之和。
(3)在集合 F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到集合F中。
(4)重复(2)、(3)两步,当F中只剩下一棵二叉树时,这棵二叉树便是所要建立的哈夫曼树。

  • 哈夫曼编码(也称赫夫曼编码)

在进行数据通讯时,涉及数据编码问题。
所谓数据编码就是将信息原文转换为二进制字符串,译码则是将信息的编码形式转换为原文。

常规的编码会让转换成的二进制序列很长,因为每个字符对应的二进制值长度几乎都是固定的,因此需要使用哈夫曼编码来进行改进,哈夫曼编码能根据字符出现的频率(即权值)来生成每个字符所对应的二进制值,频率越高二进制值长度越短,这样最终哈夫曼编码的编码结果就比常规编码要短很多,从而达到了数据压缩的效果。

哈夫曼编码(哈夫曼编码属0、1二进制编码)

规定哈夫曼树中的左分支为0,右分支为1,则从根结点到每个叶结点所经过的分支对应的 0和 1组成的序列便为该结点对应字符的编码。

哈夫曼编码特点:权值越大的字符编码越短,反之越长。

在一组字符的哈夫曼编码中,不可能出现一个字符的哈夫曼编码是另一个字符哈夫曼编码的前缀。

哈夫曼编码也称为前缀编码,使用前缀编码能设计出长短不等的编码。

  • 哈夫曼编码构造方法

设需要编码的字符集合为 {d1,d2,…,dn},各个字符在电文中出现的次数集合为{w1,w2,…,wn},以 d1,d2,…,dn 作为叶结点,以 w1,w2,…,wn 作为各根结点到每个叶结点的权值构造一棵二叉树,规定哈夫曼树中的左分支为 0,右分支为 1,则从根结点到每个叶结点所经过的分支对应的 0 和 1 组成的序列便为该结点对应字符的编码。

这样的编码称为哈夫曼编码。

利用赫夫曼树可以构造一种不等长的二进制编码,并且构造所得的赫夫曼编码是一种最优前缀编码,即使所传电文的总长度最短。

【例】5个字符有如下4种编码方案,不是前缀编码的是( )。

A. 01,0000,0001, 001,   1。
B.011, 000, 001, 010,   1。
C.000, 001, 010, 011, 100。
D.  0, 100, 110,1110,1100。

【例】对n(n≥2)个权值均不同的字符构成哈夫曼树,关于该树的叙述中,错误的是( )。

A. 该树一定是一棵完全二叉树
B. 该树中一定没有度为1的结点
C. 树中两个权值最小的结点一定是兄弟结点
D. 树中任一非叶子结点的权值一定不小于下一层任一结点的权值

【例】如果一棵哈夫曼树T中共有 255个结点,那么该树用于对几个字符进行哈夫曼编码?

【答案】 n = 255,n1 = 0
n0 = n2+1,总结点数 n = n0+n2 = 2n0-1
n0 = (n+1)/2 = 128,所以该树用于对128个字符进行哈夫曼编码。

并查集

如果已经得到完整的家谱,判断两个人是否是亲戚应该是可行的,但如果两个人的最近公共祖先与他们相隔好几代,使得家谱十分庞大,那么检验亲戚关系就十分复杂。

在这种情况下,就需要应用并查集这样一种数据结构。
并是指合并(Union):把两个不相交的集合合并为一个集合。
查是指查询(Find):查询两个元素是否在同一个集合中。

并查集构建步骤:

  1. 初始化:自己是自己的祖先;
int f[N];  // f[i] 表示 i 的祖先
void init(int n){
    int i=0;
    for(i=1; i<=n; i++) f[i]=i;
}
  1. 合并两个集合:统一两个不相交集合的祖先;
void merge(int a,int b){
    int fa = find(a), fb = find(b);
    if(fa!=fb)  f[fa]=fb;
}
  1. 查询两个元素是否在同一集合中:查询两个集合的祖先是否相同。

如果自己的祖先是自己,则直接返回自己,否则返回祖先的祖先。

int find(int x){
    if(x==f[x])  return x;
    return  find(f[x]);
}

上面我们发现对于每一个 x,都有可能会去查询 f[x],那么何不直接记录下当前的值呢,于是稍作如下修改,就是状态压缩优化。

int find(int x){
    if(x==f[x])  return x;
    return  f[x]=find(f[x]); //状态压缩
}

【例】得到一些亲戚关系的信息,如 Marry 和 Tom 是亲戚,Tom 和 Ben 是亲戚等等。

从这些信息中,可以推出Marry 和 Ben 是亲戚。

输入格式:第一部分以 N,M 开始。N为问题涉及的人的个数(1≤N≤20000)。

这些人的编号为1,2,3,…, N。

下面有 M 行(1≤M≤1000000),每行有两个数 ai、bi,表示已知 ai 和 bi 是亲戚。

第二部分以Q开始,以下 Q 行有 Q 个询问(1≤Q≤1000 000),每行为 ci 和 di,表示询问 ci 和 di 是否为亲戚。

输出格式:对于每个询问 ci、di,输出一行,若 ci 和 di 为亲戚,则输出 “Yes”,否则输出 “No”。

输入样例:

10 7
2 4
5 7
1 3
8 9
1 2
5 6
2 3
3
3 4
7 10
8 9

输出样例:

Yes
No
Yes
  • 参考程序
#define N 20001
int fa[N];
int find(int x){
    if(fa[x]==x) return x;
    return fa[x]=find(fa[x]);
}
void merge(int x,int y){
    int fx = find(x), fy=find(y);
    if(fx!=fy) fa[fx]=fy;
}
int main(){
    freopen("data.in", "r", stdin);
    int n,m,i,a,b,c,d,q; scanf("%d%d", &n, &m);
    for(i=1; i<=n; i++) fa[i]=i;//初始化很重要
    for(i=1; i<=m; i++){
        scanf("%d%d", &a, &b); merge(a, b);
    }
    scanf("%d", &q);
    for(i=1; i<=q; i++){
        scanf("%d%d", &a, &b);
        printf("%s\n", find(a)==find(b) ? "Yes" : "No");
    }
    return 0;
}

【例-课后作业】有 n 个人(编号为1~n),m 对好友关系。如果两个或者多个人是直接或间接的好友,则认为是一个朋友圈。例如 n=8,m=3,好友关系为 {{1,2},{2,3},{4,8}}; 则有{{1, 2, 3}, {4, 8}}两个朋友圈。

设计一个算法求朋友圈个数。


posted @ 2022-03-07 10:52  HelloHeBin  阅读(568)  评论(0)    收藏  举报