王道数据结构与算法树(八——2)

Posted 生命是有光的

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了王道数据结构与算法树(八——2)相关的知识,希望对你有一定的参考价值。

✍、目录脑图

1、线索二叉树

普通二叉树有如下两个问题:

  • 普通二叉树遍历只能从根节点开始遍历,不能从一个指定结点开始中序遍历。

  • 普通二叉树中如果只知道指向当前结点的指针,没法找到当前结点在中序遍历序列的前驱。(例如下图中只知道指向 F 结点的指针 p,如何找到结点在中序遍历序列的前驱呢?)

那么我们如何解决上述问题呢?

思路:

  • 从根结点出发,重新进行一次中序遍历,指针 q 记录当前访问的结点,指针 pre 记录上一个被访问的结点(前驱)

  • 下一个结点被访问(visit)之前,我们需要将 pre 前驱指针指向 q 所指向的结点,然后 q 指针指向下一个要访问的结点

  • 所以现在 pre 指针所指向的结点就是 q 指针所指向结点的中序遍历的前驱。用这样的思路我们可以让 q 不断的指向后一个被访问的结点,然后 pre 也跟着依次的向后移动。

  • 当 q 和 p 指向了同一个结点,也就是 q == p,那么就说明 pre 所指向的结点就是 p 所指向结点的前驱

  • 继续 pre 向后移, q 也向后移。此时 pre == p,则 q 所指向的结点就是 p 所指向结点的后继

上述的操作是非常不方便的,线索二叉树就是为了解决上述问题的。

  • n 个结点的二叉树,有 n + 1 个空链域。我们可以利用这些空链域来记录前驱、后继的信息。

  • 如上图,D 的左子树是空链域,因为 D 没有前驱结点,是第一个结点,所以让其左子树指向NULL
  • G 的左右子树都是空链域,让左子树指向其前驱结点 D,让右子树指向其后继结点 B
  • E 的左右子树都是空链域,让左子树指向其前驱结点 B,让右子树指向其后继结点 A
  • F 的左右子树都是空链域,让左子树指向其前驱结点 A,让右子树指向其后继结点 C
  • C 的右子树是空链域,因为 C 没有后继结点,是最后一个结点,所以让其右子树指向 NULL

定义:一个结点的左孩子指针和右孩子指针指向的是前驱和后继而不是左右孩子的话,我们把这种类型的指针称为线索,指向前驱的是前驱线索,指向后继的是后继线索。

那么还存在一个问题,如果我们的右孩子指针指向的就是右孩子而不是后继,那么如何找后继呢?

1.1、线索二叉树的存储结构

普通二叉树的链式存储结点:

// 二叉树的结点(链式存储)
typedef struct BiTNode {
    ElemType data;
    struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;

线索二叉树的链式存储结点:

// 线索二叉树的结点
typedef struct ThreadNode{
    ElemType data;
    struct ThreadNode *lchild,*rchild;
    int ltag,rtag;						// 左、右线索标志
}ThreadNode,*ThreadTree;

1.1.1、中序线索二叉树的存储

1.2、先序线索二叉树

1.3、后序线索二叉树

1.4、三种线索二叉树的对比

  • 中序线索二叉树 ➡ 线索指向中序前驱、中序后继
  • 先序线索二叉树 ➡ 线索指向先序前驱、先序后继
  • 后序线索二叉树 ➡ 线索指向后序前驱、后序后继

2、二叉树的线索化

2.1、中序线索化

中序遍历二叉树,一边遍历一边线索化

  • 首先中序遍历左子树,首先访问到的是 D 结点,D 结点没有前驱,所以将 pre 指针指向NULL,并将标志修改为 1 (代表这个孩子指针是线索)
  • 之后访问下一个结点,pre 指向 q 指向的结点,q 指向下一个结点

  • 此时 q 指向的结点为 G,判断左子树,如果为空,则建立前驱线索并修改标志为 1 ,判断其右子树,如果为空,则建立后继线索并修改标志为 1

  • 当访问最后一个结点时,pre 和 q 均指向最后一个结点,要检查 pre-> rchild 是否为 NULL,如果是,则令标志为 1

// 线索二叉树的结点
typedef struct ThreadNode{
    ElemType data;
    struct ThreadNode *lchild,*rchild;
    int ltag,rtag;						// 左、右线索标志
}ThreadNode,*ThreadTree;

// 中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T){
    if(T != NULL){
        InThread(T->lchild);		// 中序遍历左子树
        visit(T);					// 访问根节点
        InThread(T->rchild);		// 中序遍历右子树
    }
}

// 全局变量 pre,指向当前访问结点的前驱(开始访问第一个结点,无前驱,所以指向NULL)
ThreadNode *pre = NULL;

void visit(ThreadNode *q){
    if(q->lchild == NULL){			// 如果左子树为空,则建立前驱线索
        q->lchild = pre;
        q->ltag = 1;
    }
    if(pre != NULL && pre->rchild == NULL){
        pre->rchild = q;			// 建立前驱结点的后继线索
        pre->rtag = 1;
    }
    pre = q;
}

// 中序线索化二叉树T
void CreateInThread(ThreadTree T){
    pre=NULL;						// pre初始为NULL
    if(T != NULL){					// 非空二叉树才能线索化
        InThread(T);				// 中序线索化二叉树
        if(pre->rchild == NULL){
            pre->rtag=1;			// 处理遍历的最后一个结点
        }
    }
}

2.2、先序线索化

先序遍历二叉树,一边遍历一边线索化

  • 首先访问根结点,判断左右子树,发现左右子树都非空,则不操作,让 pre 指向 q所指向的结点,q指向下一结点

  • 访问结点 B,发现 B 的左右子树都非空,则不操作,让 pre 指向 q所指向的结点,q指向下一结点

  • 之后访问 D 结点,发现其左子树为空,则建立前驱线索(将其左子树指向前驱结点B),之后我们就要先序遍历 D结点的左子树,但是发现此时左子树为 B,这样下去就会造成循环。所以我们在先序遍历二叉树时,要让左子树不是前驱线索时再进行
// 先序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T){
    if(T != NULL){
        visit(T);				// 先处理根结点
        if(T->ltag == 0){		// lchild不是前驱线索
            PreThread(T->lchild);
        }
        PreThread(T->rchild);	// 先序遍历右子树
    }
}

// 全局变量 pre,指向当前访问结点的前驱
ThreadNode *pre = NULL;
void visit(ThreadNode *q){
    if(q->lchild == NULL){			// 如果左子树为空,则建立前驱线索
        q->lchild = pre;
        q->ltag = 1;
    }
    if(pre != NULL && pre->rchild == NULL){
        pre->rchild = q;			// 建立前驱结点的后继线索
        pre->rtag = 1;
    }
    pre = q;
}

// 先序线索化二叉树T
void CreateInThread(ThreadTree T){
    pre=NULL;						// pre初始为NULL
    if(T != NULL){					// 非空二叉树才能线索化
        InThread(T);				// 中序线索化二叉树
        if(pre->rchild == NULL){
            pre->rtag=1;			// 处理遍历的最后一个结点
        }
    }
}

2.3、后序线索化

// 线索二叉树的结点
typedef struct ThreadNode{
    ElemType data;
    struct ThreadNode *lchild,*rchild;
    int ltag,rtag;						// 左、右线索标志
}ThreadNode,*ThreadTree;

// 后序遍历二叉树,一边遍历一边线索化
void PostThread(ThreadTree T){
    if(T != NULL){
        PostThread(T->lchild);		// 中序遍历左子树
        PostThread(T->rchild);		// 中序遍历右子树
        visit(T);					// 访问根节点
        
    }
}

// 全局变量 pre,指向当前访问结点的前驱(开始访问第一个结点,无前驱,所以指向NULL)
ThreadNode *pre = NULL;

void visit(ThreadNode *q){
    if(q->lchild == NULL){			// 如果左子树为空,则建立前驱线索
        q->lchild = pre;
        q->ltag = 1;
    }
    if(pre != NULL && pre->rchild == NULL){
        pre->rchild = q;			// 建立前驱结点的后继线索
        pre->rtag = 1;
    }
    pre = q;
}

// 后序线索化二叉树T
void CreateInThread(ThreadTree T){
    pre=NULL;						// pre初始为NULL
    if(T != NULL){					// 非空二叉树才能线索化
        InThread(T);				// 中序线索化二叉树
        if(pre->rchild == NULL){
            pre->rtag=1;			// 处理遍历的最后一个结点
        }
    }
}

2.4、总结

3、线索二叉树找前驱/后继

3.1、中序线索二叉树找中序后继

例如:我们要在中序线索二叉树中找到指定结点 *p 的中序后继 next,next = p的右子树中最左下结点

  • 若 p -> rtag == 1, 说明右子树被线索化,那么右子树就是其中序后继,则 next = p -> rchild
  • 若 p-> rtag == 0,说明右子树未被线索化,右子树肯定是非空,则 p 必然有右孩子。
    • 因为是中序遍历,那么右孩子中第一个被中序遍历的结点必然是 p 的中序后继。
    • 假设 p 只有 1 个右孩子,并且这个右孩子只有一个结点,那么这个结点就是 p 的后继
    • 假设 p 右孩子有下一层,那么下一层最先被访问的结点(最左下结点),就是 p 的后继
    • 所以中序后继 next = p的右子树中最左下结点
// 找到以 P 为根的子树中,第一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p){
    // 循环找到最左下结点(不一定是叶结点)
    while(p->ltag == 0){
        p = p->lchild;
    }
    return p;
}

// 在中序线索二叉树中找到结点 p 的后继结点
ThreadNode *Nextnode(ThreadNode *p){
    // 右子树最左下结点(右子树当中第一个被遍历到的结点)
    if(p->rtag == 0){
        return Firstnode(p->rchild);
    }else{
        return p->rchild;		// rtag == 1 直接返回后继线索
    }
}

既然我们能遍历到结点的后继结点,那么我们就可以对中序线索二叉树进行遍历

// 对中序线索二叉树进行中序遍历(利用线索实现的非递归算法) 
// 传入我们要遍历的树的根节点的指针T
void Inorder(ThreadNode *T){
    for(ThreadNode *p = Firstnode(T);p != NULL;p=Nextnode(p)){
        visit(p);
    }
}

3.2、中序线索二叉树找中序前驱

例如:我们要在中序线索二叉树中找到指定结点 *p 的中序前驱pre,pre= p的左子树中最右下结点

  • 若 p -> ltag == 1, 说明左子树被线索化,那么左子树就是其中序后继,则 pre = p -> lchild

  • 若 p-> ltag == 0,说明左子树未被线索化,那么左子树肯定非空,则 p 必然有左孩子。

    • 因为是中序遍历,那么左孩子中最后一个中序遍历的结点必然是 p 的中序前驱。

    • 假设 p 只有 1 个左孩子,并且这个左孩子只有一个结点,那么这个结点就是 p 的前驱

    • 假设 p 左孩子有下一层,那么下一层最后访问的结点(最右下结点),就是 p 的前驱

    • 所以中序前驱 pre = p 的左子树中最右下结点

// 找到以p为根的子树中,最后一个被中序遍历的结点
ThreadNode *Lastnode(ThreadNode *p){
    // 循环找到最右下结点(不一定是叶节点)
    while(p->rtag == 0){
        p = p->rchild;
        return p;
    }
}

// 在中序线索二叉树中找到结点p的前驱结点
ThreadNode *Prenode(ThreadNode *p){
    // 左子树中最右下结点
    if(p->ltag == 0){
        return Lastnode(p->lchild);
    }else{
        return p->lchild;			//ltag==1 直接返回前驱结点
    }
}

既然我们能遍历到结点的前驱结点,那么我们就可以对中序线索二叉树进行逆向中序遍历

// 对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode *T){
    for(ThreadNode *p = Lstnode(T);p != NULL;p = Prenode(p){
        visit(p);
    })
}

3.3、先序线索二叉树找先序后继

  • 若 p -> rtag == 1, 说明右子树被线索化,那么右子树就是其先序后继,则 next= p -> rchild

  • 若 p-> rtag == 0,说明右子树未被线索化,那么右子树肯定非空,则 p 必然有右孩子。

4、树的存储结构

4.1、树的逻辑结构回顾

树是 n(n≥0)个结点的有限集合,n=0时,称为空树,这是一种特殊情况。在任意一棵非空树中应满足:

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

4.2、双亲表示法(顺序存储)

#define MAX_TREE_SIZE 100 // 树中最多结点数
typedef struct{			  // 树的结点定义
    ElemType data;		  // 数据元素
    int parent;			  // 双亲位置域
}PTNode;
typedef struct{				// 树的类型定义
    PTNode nodes[MAX_TREE_SIZE]; // 双亲表示
    int n;					// 结点数
}PTree;
  • 若我们要新增数据元素,无需按逻辑上的次序存储,只需要存储其父节点的数组下标即可

  • 若我们要删除G这个结点,有两种删除方法
    • 将G这个结点的双亲指针设为 -1
    • 将尾部数据移动,填充G处的 data 和 parent

4.3、孩子兄弟表示法(链式存储)🔥

  • 左指针指向第一个孩子结点,右指针指向兄弟结点

  • A 是根节点,A的第一个孩子是B,所以A左连接B,B的兄弟结点是C,所以让B右连接C,D是C的兄弟结点,所以让C右连接D。

  • B的第一个孩子是E,所以让B左连接E,E的兄弟结点是F,所以让E右连接F。

  • E的第一个孩子是K,所以让E左连接K

  • C的第一个孩子是G,所以让C左连接G

  • D的第一个孩子是H,所以让D左连接H,I、J都是是H的兄弟结点,所以让H右连接I、J

  • A是根节点,左边的B是第一个孩子,B的右边C、F、L都是B的兄弟

  • D连在B的左边,所以D是B的第一个孩子,H是D的兄弟

  • G连在D的左边,所以G是D的孩子

  • E连在C的左边,所以E是C的第一个孩子,I连在E的左边,所以I是E的第一个孩子,J连在E的右边,所以J是E的兄弟

4.4、森林和二叉树的转换

森林转化为二叉树

  • B、C、D 是兄弟结点,各个树的根节点视为兄弟关系,所以将C、D右连接B
  • E是B的第一个孩子,所以将E左连接B,F是E的兄弟结点,所以将F右连接E
  • K是E的第一个孩子,所以将K左连接E,L是K的兄弟结点,所以将L右连接L

二叉树转化为森林

  • A、C、F、L是兄弟结点,分别为二叉树的跟结点

  • A的左边连接了B,所以B是A的第一个孩子,B的左边连接了D,所以D是B的第一个孩子,D的左边连接了G,所以G是D的第一个孩子,D的右边连接了H,所以D和H是兄弟结点

  • C的左边连接了E,所以E是C的第一个孩子,E的左边连接了I,所以I是E的第一个孩子,E的右边连接了J,所以E和J是兄弟结点。

  • F的左边连了K,所以K是F的第一个孩子

4.5、小结

5、树、森林的遍历

5.1、树的先根遍历

先根遍历:若树非空,先访问根节点,再依次对每棵子树进行先根遍历。

// 树的先根遍历
void PreOrder(TreeNode *R){
    if(R!=NULL){
        visit(R); // 访问根结点
        while(R还有下一个子树T){
            PreOrder(T);			// 先根遍历下一棵子树
        }
    }
}

5.2、树的后根遍历

  • 后根遍历:若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。
  • 树的后根遍历也叫做深度优先遍历。

5.3、树的层次遍历

树的层次遍历也叫做广度优先遍历。

5.4、森林的先序遍历

先序遍历森林:

  • 若森林为空,则按如下规则进行遍历
  • 访问森林中第一棵树的根结点,先序遍历第一棵树中根结点的子树森林,先序遍历除去第一棵树之后剩余的树构成的森林。(效果等同于依次对各个树进行先根遍历)

或者将其转化为对应的二叉树,先序遍历森林效果等同于依次对二叉树的先序遍历。

5.5、森林的中序遍历

另一种方法就是将其转换成与之对应的二叉树,中序遍历森林效果等同于依次对二叉树的中序遍历。

5.5、小结

6、二叉排序树

6.1、二叉排序树的定义

二叉排序树,又称二叉查找树(BST,Binary Search Tree),一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:

  • 左子树上所有结点的关键字均小于根结点的关键字
  • 右子树上所有结点的关键字均大于根结点的关键字
  • 左子树和右子树又各是一棵二叉排序树

左子树的结点值<根节点值<右子树结点值

6.2、二叉排序树的查找

若树非空目标值与根节点的值比较,若相等,则查找成功。若小于根节点,则在左子树上查找,否则在右子树上查找。

// 二叉排序树结点
typedef struct BSTNode{
    int key;
    struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;

// 在二叉排序树中查找值为key的结点(传入根节点指针和值)
BSTNode *BST_Search(BSTree T,int key){
    while(T!=NULL&&key!=T->key){		// 若树空或等于根结点,则结束循环
        if(key<T->key){
            T=T->lchild;				// 小于,则在左子树上查找
        }else{
            T=T->rchild;				// 大于,则在右子树上查找
        }
        return T;
    }
}

当然我们也可以递归实现

// 在二叉排序树中查找值为key的结点(递归实现)
BSTNode *BSTSearch(BSTree T,int key){
    if(T==NULL){
        王卓数据结构与算法树(八——3)

(王道408考研数据结构)第六章图-第四节2:最小生成树之克鲁斯卡尔算法(思想代码演示答题规范)

(王道408考研数据结构)第六章图-第四节1:最小生成树之普利姆算法(思想代码演示答题规范)

最小生成树算法:Prim算法和Kruskal算法(来源:王道数据结构)

王道数据结构6.2(图的应用)

王道数据结构6.2(图的应用)