浙大《数据结构》第三章:树(上)

注:本文使用的网课资源为中国大学MOOC

https://www.icourse163.org/course/ZJU-93001


查找

查找:根据某个给定的关键字K,从集合R中找出关键字与K相同的记录。

静态查找:集合中的记录是固定的,没有插入删除的操作,只有查找;

动态查找:集合中记录是动态变化的,除查找,还可能发生插入和删除。


静态查找

方法1:顺序查找

int SequentialSearch( StaticTable *Tbl, ElementType K)
{
    //在表Tbl[1]~Tbl[n]中查找关键字为K的数据元素
    int i;
    Tbl->Element[0] = K; //建立哨兵
    for (i=Tbl->Length; Tbl->Element[i]!=K; i--);
    return i; //查找成果则返回所在单元下标;不成功则返回0
}

顺序查找的时间复杂度为O(n)。


方法2:二分查找

int BinarySearch( StaticTable *Tbl, ElementType K)
{
    int left, right, mid, NotFound = -1;
    
    left = 1; //初始左边界
    right = Tbl->Length; //初始右边界
    while ( left <= right )
    {
        mid = (left+right)/2; //计算中间元素坐标
        if ( K < Tbl->Element[mid] )
            right = mid-1; //调整右边界
        else if ( K > Tbl->Element[mid] )
            left = mid+1; //调整左边界
        else
            return mid; //查找成功,返回数据元素的下标
    }
    return NotFound; //查找不成功,返回-1
}

二分查找算法具有对数时间复杂度O(log(N))。


树的定义

树(Tree): n(n≥0)个结点构成的有限集合。当n=0时,称为空树,对于任意一棵非空树,它具备以下性质:

  • 树中有一个称为“根(root)”的特殊结点,用r表示;
  • 其余结点可分为若干个互不相交的有限集,其中每一个集合本身又是一棵树,称为原来树的子树(SubTree).
  • 子树是不相交的;
  • 除了根结点外,每个结点有且仅有一个父结点;
  • 一棵N个结点的树有N-1条边。

树的基本术语

  1. 结点的度(Degree):结点的子树个数
  2. 树的度:树的所有结点中最大的度数
  3. 叶结点(Leaf):度为0的结点
  4. 父结点(parent):有子树的结点是其子树的根结点的父结点
  5. 子节点(child):若A结点是B结点的父结点,则称B结点是A结点的子结点;子结点也称为孩子结点
  6. 兄弟结点(sibling):具有同意父结点的各结点是彼此的兄弟结点
  7. 路径和路径长度:从结点\(n_1\)\(n_k\)的路径为一个结点序列\(n_1,n_2,...,n_k\)\(n_i\)\(n_{i+1}\)的父结点。路径所包含的边的个数为路径的长度。
  8. 祖先结点(ancestor):沿树根到某一结点路径上的所有结点都是这个结点的祖先结点。
  9. 子孙结点(descendant):某一结点的子树中的所有结点是这个结点的子孙
  10. 结点的层次(level):规定根结点在1层,其他任意结点的层数是其父结点的层数加1。
  11. 树的深度(depth):树中所有结点中最大层次是这棵树的深度。

二叉树及其存储结构

定义

二叉树T:一个有穷的结点组合

  • 这个集合可以为空
  • 若不为空,则它是由根结点和称为其左子树\(T_L\)和右子树\(T_R\)的两个不相交的二叉树组成。

特殊的二叉树


性质

  1. 一个二叉树第i层的最大结点数为:\(2^{i-1},i \geq 1.\).
  2. 深度为K的二叉树有最大结点总数为\(2^K-1,K \geq 1.\).
  3. 对于任何非空二叉树T,若\(n_0\)表示叶结点的个数(没有子树)、\(n_2\)是度为2的非叶结点个数(有左右两个子树),那么二者满足关系\(n_0=n_2+1\)
    (二叉树的结点总数为\(n_0+n_1+n_2\))

抽象数据类型定义

类型名称:二叉树

数据对象集:一个有穷的结点集合。若不为空,则由根结点和其左、右二叉子树组成。

操作集:\(BT \in BinTree\),\(Item \in ElementType\),重要操作有:

Boolean IsEmpty( BinTree BT); //判别BT是否为空
void Traversal( BinTree BT ); //遍历,按某顺序访问每一个结点
BinTree CreatBinTree();//创建一个二叉树

/* 常见的遍历方法 */
void PreOrderTraversal( BinTree BT ); //先序:根-左-右
void InOrderTraversal( BinTree BT ); //中序:左-根-右
void PostOrderTraversal( BinTree BT ); //后序:左-右-根
void LevelOrderTraversal( BinTree BT ); //层次遍历:从上到下,从左到右

存储结构

顺序存储结构

完全二叉树:按从上到下、从左到右顺序存储n个结点的完全二叉树的结点父子关系:

  • 非根结点(序号i>1)的父结点的序号是(i/2)。
  • 结点(序号i)的左孩子结点序号是2i(需2i<=n,否则没有左孩子)
  • 结点(序号i)的右孩子结点序号是2i+1(需2i+1<=n,否则没有右孩子)

链表存储结构

typedef struct TreeNode *Position
typedef Position BinTree
struct TreeNode
{
    ElementTyoe Data;
    BinTree Left;
    BinTree right;
}

二叉树的遍历

递归遍历

(1) 先序遍历

遍历过程:

  1. 访问根结点
  2. 先序遍历其左子树
  3. 先序遍历其右子树
void PreOrderTraversal( BinTree BT )
{
    if ( BT )
    {
        printf("%d", BT->Data); //根结点
        PreOrderTraversal( BT->Left ); //左子树
        PreOrderTraversal( BT->Right ); //右子树
    }
}

(2) 中序遍历

遍历过程:

  1. 中序遍历其左子树
  2. 访问根结点
  3. 中序遍历其右子树
void InOrderTraversal( BinTree BT )
{
    if ( BT )
    {
        InOrderTraversal( BT->Left ); //左子树
        printf("%d", BT->Data); //根结点
        InOrderTraversal( BT->Right ); //右子树
    }
}

(3) 后序遍历

遍历过程:

  1. 后序遍历其左子树
  2. 后序遍历其右子树
  3. 访问根结点
void PostOrderTraversal( BinTree BT )
{
    if ( BT )
    {
        PostOrderTraversal( BT->Left ); //左子树
        PostOrderTraversal( BT->Right ); //右子树
        printf("%d", BT->Data); //根结点
    }
}

非递归遍历

先序、中序和后序遍历过程中经过结点的路线是一样的,只是访问各结点的时机不同。

  • 先序遍历是第一次"遇到"该结点时访问
  • 中序遍历是第二次"遇到"该结点(此时该结点从左子树返回)时访问
  • 后序遍历是第三次"遇到"该结点(此时该结点从右子树返回)时访问

非递归算法实现的基本思路:使用堆栈

(1) 中序遍历

  1. 遇到一个结点,就把它压栈,并去遍历它的左子树;
  2. 当这个左子树遍历结束后,从栈顶弹出这个结点并访问它;
  3. 然后按其右指针再去中序遍历该结点的右子树;
void InOrderTraversal( BinTree BT )
{
    BinTree T = BT;
    Stack S = CreatStack(MaxSize); //创建并初始化堆栈S
    while (T || !IsEmpty(S))
    {
        while (T) //一直向左并将沿途结点压入堆栈
        {
            Push(S, T); 
            T = T->Left;
        }
        if ( !IsEmpty(S))
        {
            T = Pop(S); //结点弹出堆栈
            printf("%5d", T->Data); //打印结点
            T = T->Right; //转向右子树
        }
    }
}

(2) 先序遍历

void PreOrderTraversal( BinTree BT )
{
	BinTree T = BT;
	Stack S = CreateStack();  // 创建并初始化堆栈 S
	while(T || !IsEmpty(S))  // 当树不为空或堆栈不空 
	{  
		while(T)
		{     
			Push(S,T);    // 压栈,第一次遇到该结点 
			printf("%d",T->Data);  // 访问结点
			T = T->Left;   // 遍历左子树 
		}
		if(!IsEmpty(S)) // 当堆栈不空 
		{  
			T = Pop(S);    // 出栈,第二次遇到该结点 
			T = T->Right;  // 访问右结点 
		}
	} 
} 

(3) 后序遍历

先序的访问顺序是root, left, right 假设将先序左右对调,则顺序变成root, right, left,暂定称之为“反序”。后序遍历的访问顺序为left, right,root ,刚好是“反序”结果的逆向输出。

  1. 反序遍历二叉树,具体方法为:将先序遍历代码中的left 和right 对调即可。数据存在堆栈S中。
  2. 在先序遍历过程中,每次Push节点后紧接着print结点。对应的,在反序遍历时,将print结点改为把当前结点Push到堆栈N中。
  3. 反序遍历完成后,堆栈N的压栈顺序即为反序遍历的输出结果。此时再将堆栈N中的结果pop并print,即为“反序”结果的逆向,也就是后序遍历的结果。
void PostOrderTraversal( BinTree BT )
{
    BinTree T = BT;
    Stack S = CreatStack();
    Stack N = CreatStack(); //创建并初始化栈N
    wihile ( T || !IsEmpty(S) )
    {
        while(T) /*一直向右并将沿途结点压入堆栈*/
        { 
            Push(S,T);
            Push(N,T);  //将遍历到的结点压栈,用于反向
            T = T->Right;  //这里是Right
        }
        if ( !IsEmpty(S) )
        {
            T = Pop(S);
            T = T->Left;
        }
    }
    while ( !IsEmpty(N) )
    {
        T = Pop(N);
        printf("%5d", T->Data); //将 N 栈中的数据依次弹出并打印
    }
}

层次遍历

队列实现:

  1. 根结点入队;
  2. 从队列中取出一个元素,并访问该元素;
  3. 若该元素所指结点的左右孩子结点非空,则将其左右孩子的指针入队。
void LevelTraversal( BinTree BT )
{
    if ( !BT )
        return; //如果是空树直接返回
    BinTree T;
    Queue Q = CreatQueue(MaxSize); //创建并初始化队列Q
    AddQ( Q, BT );
    while (!IsEmpty(Q))
    {
        T = DeleteQ( Q );
        printf("%d\n", T->Data); // 访问取出队列的结点
        if ( T->Left )
            AddQ( Q, T->Left );
        if ( T->Right )
            AddQ( Q, T->Right );    
    }
}

举例

(1) 求二叉树的高度

int PostOrderGetHeight( BinTree BT )
{
    int HL, HR, MaxH;
    if ( BT )
    {
        HL = PostOrderGetHeight( BT->Left );
        HR = PostOrderGetHeight( BT->Right );
        MaxH = (HL>HR) ? HL :HR; // 取左右子树较大的深度
        return ( MaxH+1 );
    }
    else
        return 0;  // 空树深度为0
}

(2) 根据先序和中序遍历来确定一棵二叉树

分析:

  1. 根据先序遍历序列的第一个结点确定根结点;
  2. 根据根结点在中序遍历序列中分割出左右两个子序列;
  3. 对左子树和右子树分别递归使用相同的方法继续分解;

例如:

前序:ABCDEFG

中序:CBDAFEG

先序遍历为"根左右",则 A 是根,对应可以划分出中序中:(CBD)A(FEG),CBD 为左子树,FEG 为右子树,

再根据前序的 BCD,B 为根,划分出中序中(C(B)D)A(FEG),则 C D 分别是 B 的左右子树…最后可得树为:

     A
    /  \
   B    E
  / \  / \
 C   D F  G

二叉树的同构

题意理解

给定两棵树T1和T2。如果T1可以通过若干次左右孩子互换就变成T2,则我们称这两棵树是“同构”的。

输入格式:

  • 现在一行中给出概述的结点数,随后N行
  • 第i行对应编号第i个结点,给出该结点中存储的字母、其左孩子结点的编号、右孩子结点的编号
  • 如果孩子结点为空,则在相应的位置上给出“-”。

输入样例:


程序框架搭建

int main()
{
    Tree R1, R2; // 利用结构数组表示二叉树,静态链表
    
    R1 = BuildTree(T1); // 建立二叉树1
    R2 = BuildTree(T2); // 建立二叉树2
    if ( Isomorphic(R1, R2) ) // 判别是否同构并输出
        printf("Yes\n");
    else
        printf("No\n");
    
    return 0;
    
}

程序实现

#include <stdio.h>
#include <windows.h> //windows.h里定义了关于创建窗口,消息循环等函数S

#define MaxTree 10
#define Null -1

typedef struct TreeNode
{
    char item;
    int Left;
    int Right;              // 注:这里的left和right分别为输入数据的左右子结点索引
} TNode;

TNode T1[MaxTree], T2[MaxTree]; // 结构数组表示二叉树

/* 创建树并返回根结点 */
int BuildTree(TNode T[])
{
    int i, N;
    int Root = 0;
    int check[MaxTree];
    char cl, cr;
    scanf("%d", &N);
    if ( !N )
        return Null;
    for (i = 0; i < N; i++)
        check[i] = 0;
    for (i = 0; i < N; i++)
    {
        printf("%d: ",i);
        scanf("\n%c %c %c", &T[i].item, &cl, &cr); // 注意输入时不要有空格,且字母要小写,eg.a12
        // scanf("\n"):为了避免下一次的scanf直接读取\n而结束,保证每次都等待键盘输入
        // 注意‘’和”“的区别,双引号括起来的是字符串指针,单引号括起来的才是字符
        if (cl != '-') 
        {
            T[i].Left = cl - '0';
            check[T[i].Left] = 1;
        }
        else
            T[i].Left = Null;
        if (cr != '-')
        {
            T[i].Right = cr - '0';
            check[T[i].Right] = 1;
        }
        else
            T[i].Right = Null;
    }
    for (i = 0; i < N; i++)
        if (!check[i])
            break;
    Root = i;
    printf("Done!\n");
    return Root;
}

/* 判断二叉树是否重构 */
int Ismorphic(int R1, int R2)
{
    if ( R1 == Null && R2 == Null ) // 均为空
        return 1;
    if ( (R1 == Null && R2 != Null) || (R1 != Null && R2 == Null) ) // 其中一个为空
        return 0;
    if ( T1[R1].item != T2[R2].item ) // 两个子树的结点不同
        return 0;
    if ( T1[R1].Left == Null && T2[R2].Left == Null ) // 根结点均没有左子树
        return Ismorphic(T1[R1].Right, T2[R2].Right);
    if ( (T1[R1].Left != Null && T2[R2].Left != Null) && (T1[T1[R1].Left].item == T2[T2[R2].Left].item) )
        // 两树的左儿子不为空且值相等
        return (Ismorphic(T1[R1].Left, T2[R2].Left) && Ismorphic(T1[R1].Right, T2[R2].Right));
    else // 两树的左儿子不为空且值不等  或者某一个左儿子为空
        return (Ismorphic(T1[R1].Left, T2[R2].Right) && Ismorphic(T1[R1].Right, T2[R2].Left));
}

int main()
{
    int R1, R2; // 利用结构数组表示二叉树,静态链表

    printf("input Tree:\n");
    R1 = BuildTree(T1); // 建立二叉树1
    printf("T1 = %d\n", R1);

    printf("input Tree:\n");
    R2 = BuildTree(T2);     // 建立二叉树2
    printf("T2 = %d\n", R2);

    if (Ismorphic(R1, R2)) // 判别是否同构并输出
        printf("Yes\n");
    else
        printf("No\n");

    system("pause"); //程序暂停,显示按下任意键继续
    return 0;
}

运行结果:

posted @ 2020-03-17 23:22  Super_orange  阅读(286)  评论(0编辑  收藏  举报