数据结构二叉树普通树与森林的定义与应用

Posted SimbaWang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构二叉树普通树与森林的定义与应用相关的知识,希望对你有一定的参考价值。

  1. 普通\\(m\\)叉树的性质(普通二叉树也满足)

    1. 各层的最大结点个数

    \\[第i层最多有m^{i-1}个结点,其中1\\le i\\le h \\]

    1. 高度为h的\\(m\\)叉树最多结点个数

    \\[等比数列求和公式:\\frac{m^h-1}{m-1} \\]

    1. 具有\\(n\\)个结点的\\(m\\)叉树至少有多高

    也就是说完全m叉树的高度是多少?有以下两种表示方式:

    1. 高为\\(h\\)的完全二叉树最多有\\(\\frac{m^h-1}{m-1}\\)个结点,所以\\(h=⌈\\small{\\log_m[n(m-1)+1]}\\normalsize⌉\\)
    2. 高为\\(h\\)的完全二叉树至少有\\(\\frac{m^{h-1}-1}{m-1}+1\\)个结点,所以\\(h=⌈\\small{\\log_m[(n-1)(m-1)+1]}\\normalsize⌉+1\\)
    1. \\(n\\)个结点对应\\(n-1\\)度,也就有\\(n-1\\)条边;

  2. 二叉树的常考性质(\\(n\\)为总结点数,\\(n_i\\)为度为\\(i\\)的结点数)

    1. 二叉树总结点数与各类型结点数的关系

    \\[\\begin{cases} n=n_0+n_1+n_2 \\\\ n=0\\times n_0+1\\times n_1+2\\times n_2+1 \\end{cases} \\Longrightarrow n_0=n_2+1 \\]

    1. 高度为\\(h\\)的霍夫曼树有\\(2h-1\\)个结点

    高度为\\(h\\)的二叉树上只有度为0和度为2的结点,则此类二叉树中所包含的结点数至少为\\(2h-1\\)

    1. 计算二叉树各种类型的节点的个数

    1. 双分支结点

    \\[f(T)= \\begin{cases} 0,&如果T是空树; \\\\ f(T\\rightarrow lchild)+f(T\\rightarrow rchild),&如果T不是双分支结点; \\\\ f(T\\rightarrow lchild)+f(T\\rightarrow rchild)+1,&如果T是双分支结点; \\end{cases} \\]

    int TwoBranchNodes(BiTree T){
           if(T == nullptr){
               return 0;
           }
           else if(T->lchild != nullptr && T->rchild != nullptr){
               return TwoBranchNodes(T->lchild) + TwoBranchNodes(T->rchild) + 1;
           }
           else{
               return TwoBranchNodes(T->lchild) + TwoBranchNodes(T->rchild);
           }
    }
    
    1. 单分支结点

    \\[f(T)= \\begin{cases} 0,&如果T是空树; \\\\ f(T\\rightarrow lchild)+f(T\\rightarrow rchild),&如果T是双或者叶子; \\\\ f(T\\rightarrow lchild)+f(T\\rightarrow rchild)+1,&如果T是单分支结点; \\end{cases} \\]

    int OneBranchNodes(BiTree T){
           if(T == nullptr){
               return 0;
           }
           else if((T->lchild == nullptr) ^ (T->rchild == nullptr)){
               return OneBranchNodes(T->lchild) + OneBranchNodes(T->rchild) + 1;
           }
           else{
               return OneBranchNodes(T->lchild) + OneBranchNodes(T->rchild);
           }
    }
    
    1. 叶子结点

    \\[f(T)= \\begin{cases} 0,&如果T是空树; \\\\ f(T\\rightarrow lchild)+f(T\\rightarrow rchild),&如果T是分支结点; \\\\ 1,&如果T是叶子结点; \\end{cases} \\]

    int NoneBranchNodes(BiTree T){
           if(T == nullptr){
               return 0;
           }
           else if(T->lchild == nullptr && T->rchild == nullptr){
               return 1;
           }
           else{
               return NoneBranchNodes(T->lchild) + NoneBranchNodes(T->rchild);
           }
    }
    
  3. 完全二叉树常考性质

    1. 完全二叉树中各类型结点的个数

    \\[完全二叉树中根据n可以推出n_i的个数 \\begin{cases} 若n=2k&\\Longrightarrow n_1=1,n_0=k,n_2=k-1; \\\\ 若n=2k-1&\\Longrightarrow n_1=0,n_0=k,n_2=k-1; \\end{cases} \\]

    1. 完全\\(m\\)叉树编号的性质

    1. 编号\\(i\\)的首个孩子结点(若存在)的编号

    \\[(i-1)m+1+1 \\]

    1. \\((i-1)m\\)表示前\\(i-1\\)个结点一共产生的结点数;
    2. 第一个1指的是根结点;
    3. 第二个1表示这是首个孩子;
    1. 编号为\\(i\\)的结点的第\\(k\\)个孩子结点(若存在)的编号

    \\[(i-1)m+1+k,其中1指的是根结点,k表示这是第k个孩子 \\]

    1. 编号为\\(i\\)的结点的双亲结点(若存在)的编号

    \\[\\small{⌊\\frac{i-2}{m}⌋+1,问题b和c的逆问题。由于k不确定具体指,但k-1\\lt m,所以统一减2后向下取整} \\]

    1. 编号为\\(i\\)的结点有右兄弟的条件,以及该右兄弟结点的编号

    结点\\(i\\)不是其双亲的第\\(m\\)个子女时才有右兄弟。有以下两种阐述方式:

    1. 对于结点\\(j\\),其第\\(k\\)个子女结点的编号\\(i\\)\\((j-1)m+1+k\\),只有当\\(k=m\\)\\((i-1)\\%{m}=0\\)才会成立,若想要使得\\(i\\)不是第\\(m\\)个孩子,只需要满足$(i-1)%m!=0 $ 即可;
    2. 由于第\\(m\\)个孩子结点的编号为\\(jm+1\\),所以满足\\(i\\le jm=(⌊\\frac{i-2}{m}⌋+1)\\cdot m\\)也行;
    1. 总数为\\(n\\)个结点的\\(m\\)叉树中叶子结点的个数

    由于最后一个分支结点是最后一个结点的父节点,所以其编号为\\(⌊\\frac{n-2}{m}⌋+1\\),所以叶子结点的总数为\\(n-⌊\\frac{n-2}{m}⌋+1\\)。特别地,当\\(m=2\\)时可被化简为:\\(⌊\\frac{n}{2}⌋\\)

    1. 已知两结点的编号求最近的公共祖先

    循环不变量:编号大的向上走。循环退出条件:编号相等。

    ElemType NearestAncestor(SeqTree T, int i, int j){
           if(T[i]!=\'#\' && T[j]!=\'#\'){
               while(i!=j){
                   if(i>j){
                       i = i / 2;
                   }
                   else{
                       j = j / 2;
                   }
               }
               return T[i];
           }
           else{
               cout << "传入的结点不存在!" << endl;
               exit(0);
           }
    }
    
    1. 完全二叉树的高度

    具有\\(n\\)个(\\(n\\gt0\\))结点的完全二叉树的高度\\(h=⌈\\log_2(n+1)⌉\\)或者\\(⌊\\log_2(n)+1⌋\\)

    1. 完全二叉树的判定

    空结点后不能出现非空结点(层次遍历纳入空结点)

    bool IsComplete(BiTree T){
           InitQueue(Q);
           if(!T){
               return true;
           }
           else{
               EnQueue(Q, T);
               while(!IsEmpty(Q)){
                   DeQueue(Q, parentNode);
                   if(parentNode != nullptr){
                       EnQueue(Q, parentNode->lchild);
                       EnQueue(Q, parentNode->rchild);
                   }
                   else{
                       // 空结点之后必须全为空结点才能返回TRUE
                       while(!IsEmpty(Q)){
                           DeQueue(Q, parentNode);
                           if(parentNode != nullptr){	
                               // 如果不是空节点,则说明叶子结点后面还存在非叶子结点,这个非叶子节点的孩子节点就是此非空节点
                               return false;
                           }
                       }// End While
                   }// End parentNode==nullptr
               }// End While
               return true;
           }// End T!=nullptr
    }
    
    1. 满二叉树确定二叉链表的结构

    可以仅通过先序或者后序或者层序(不需要中序)!因为对于一棵满二叉树而言,每一个双分支结点都有相等长度的左右子树,故而不需要中序序列确定左右子树的长度。

    1. 先序遍历序列唯一确定

    BiTree PreOrder(char order[], int preLow, int preHigh) {
           if (preHigh >= preLow) {
               // 左右子树的长度为half
               int half = (preHigh - preLow) / 2;
               BiTree root = (BiTree)malloc(sizeof(BiTreeNode));
               // 创建根结点
               root->data = order[preLow];
               // 创建左子树
               root->lchild = PreOrder(order, preLow+1, preLow+half);
               // 创建左子树
               root->rchild = PreOrder(order, preLow+half+1, preHigh);
               return root;
           }
           else {
               return nullptr;
           }
    }
    
    1. 后序遍历序列唯一确定

    BiTree PostOrder(char order[], int preLow, int preHigh) {
           if (preHigh >= preLow) {
               // 左右子树的长度为half
               int half = (preHigh - preLow) / 2;
               BiTree root = (BiTree)malloc(sizeof(BiTreeNode));
               // 创建左子树
               root->lchild = PostOrder(order, preLow, preLow+half-1);
               // 创建左子树
               root->rchild = PostOrder(order, preLow+half, preHigh-1);
               // 创建根结点
               root->data = order[preHigh];
               return root;
           }
           else {
               return nullptr;
           }
    }
    
    1. 层序遍历序列唯一确定

    这个可以用满二叉树编号的性质。

    BiTree LevelOrder(char order[], int levelRoot){
           if(levelRoot <= N){
               BiNode* root = (BiNode*)malloc(sizeof(BiNode));
               // 创建根结点
               root->data = order[levelRoot];
               // 创建左子树
               root->lchild = LevelOrder(order, 2*levelRoot);
               // 创建右子树
               root->rchild = LevelOrder(order, 2*levelRoot+1);
           }
           else{
               return nullptr;
           }
    }
    
    1. 其中先序和后序的转化是最简单的

      只要递归地将根结点与子树的节点换位即可。如下图所示:

      void PreToPost(BiTree pre[], int preLow, int preHigh, BiTree post[], int postLow, int postHigh) {
             int half;
             if (preHigh >= preLow) {
                 // 转换根结点(根结点后置)
                 post[postHigh] = pre[preLow];
                 half = (preHigh - preLow) / 2;
                 // 转换左子树(左子树前移)
                 PreToPost(pre, preLow + 1, preLow + half, post, postLow, postLow + half - 1);
                 // 转换右子树(右子树前移)
                 PreToPost(pre, preLow + half + 1, preHigh, post, postLow + half, postHigh - 1);
             }
      }
      
  4. 二叉树遍历的应用

    1. 求二叉树树高

    1. 递归算法(后序遍历)

    // 树高 = 左右子树的最大值 + 1(根节点)
    int BiTreeDepth(BiTree TRoot){
           if(TRoot == nullptr){
               return 0;
           }
           else{
               int leftDepth = BiTreeDepth(TRoot->lchild);
               int rightDepth = BiTreeDepth(TRoot->rchild);
               return Max(leftDepth, rightDepth) + 1;
           }
    }
    

    时间复杂度:\\(O(n)\\),空间复杂度:\\(O(\\mathbf{Height}(T))\\)【平衡时才是:\\(O(⌈\\log_2(n+1)⌉)\\)】;

    1. 非递归算法(层序遍历+辅助队列)

    int GetBiTreeWidth(BiTree T){
           if(!T){
               return 0;
           }
           else if(T->lchild == nullptr && T->rchild == nullptr){
               return 1;
           }
           else{
               // 创建一个辅助队列并初始化
               AuxQue auxque;
               InitQueue(auxque);
               auxque->front = auxque->rear = -1;
               // 记录父层的层号、最右端结点的序号,子层的结点个数(已访问)
               // 父层:上一层(删除结点层);子层:当前层(插入结点层)
               int depth, lastNodeOrder, nodeCount;
               // 将根节点插入队列中(插入后根节点所在的层变成父层)
               auxque->data[++auxque->rear] = T;
               depth = 0, lastNodeOrder = auxque->rear, nodeCount = 0;
               while(auxque->front < auxque->rear){
                   // 将父层的元素取出,移动front指针
                   TreeNode parentNode = auxque->data[++auxque->front];
                   // 插入孩子结点,并使结点计数器自增1
                   if(parentNode->lchild != nullptr){
                       auxque->data[++auxque->rear] = parentNode->lchild;
                       nodeCount++;
                   }
                   if(parentNode->rchild != nullptr){
                       auxque->data[++auxque->rear] = parentNode->rchild;
                       nodeCount++;
                   }
                   // front指针指向了父层的最右端元素,父层的最后一个元素被弹出
                   if(auxque->front == lastNodeOrder){
                       // 同时子层的最后一个节点也插入完成了,子层变成了新的父层
                       lastNodeOrder = auxque->rear;
                       depth++;
                       nodeCount = 0;
                   }
               }// End While
               return depth;
           }
    }
    

    时间复杂度:\\(O(n)\\),空间复杂度:\\(O(n)\\)

    1. 求二叉树树宽

    1. 递归算法(先序遍历+辅助数组)

    #define MAX_HEIGHT 100
    void PreOrder(BiTree T, int level, int* width){
           if(!T){
               return;
           }
           else{
               width[level]++;
               PreOrder(T->lchild, level + 1, width);
               PreOrder(T->rchild, level + 1, width);
           }
    }
    
    int GetBiTreeWidth(BiTree T){
           int* width = (int*)malloc(sizeof(int) * MAX_HEIGHT);
           for(int i=0; i<MAX_HEIGHT; i++){
               width[i] = 0;
           }
           PreOrder(T, 1, width);
           int maxWidth = 0;
           for(int i=0; i<MAX_HEIGHT; i++){
               maxWidth = maxWidth < width[i] ? width[i] : maxWidth;
           }
           return maxWidth;
    }
    

    时间复杂度:\\(O(n)\\),空间复杂度:\\(O(\\mathbf{Height}(T))\\);【平衡时才是:\\(O(⌈\\log_2(n+1)⌉)\\)

    1. 非递归算法(层序遍历+辅助队列)

    int GetBiTreeWidth(BiTree T){
           if(!T){
               return 0;
           }
           else if(T->lchild == nullptr && T->rchild == nullptr){
               return 1;
           }
           else{
               // 创建一个辅助队列并初始化
               AuxQue auxque;
               InitQueue(auxque);
               auxque->front = auxque->rear = -1;
               // 记录父层的宽度、最右端结点的序号,子层的结点个数(已访问)
               // 父层:上一层(删除结点层);子层:当前层(插入结点层)
               int width, lastNodeOrder, nodeCount;
               // 将根节点插入队列中(插入后根节点所在的层变成父层)
               auxque->data[++auxque->rear] = T;
               width = 1, lastNodeOrder = auxque->rear, nodeCount = 0;
               while(auxque->front < auxque->rear){
                   // 将父层的元素取出,移动front指针
                   TreeNode parentNode = auxque->data[++auxque->front];
                   // 插入孩子结点,并使结点计数器自增1
                   if(parentNode->lchild != nullptr){
                       auxque->data[++auxque->rear] = parentNode->lchild;
                       nodeCount++;
                   }
                   if(parentNode->rchild != nullptr){
                       auxque->data[++auxque->rear] = parentNode->rchild;
                       nodeCount++;
                   }
                   // front指针指向了父层的最右端元素,父层的最后一个元素被弹出
                   if(auxque->front == lastNodeOrder){
                       // 同时子层的最后一个节点也插入完成了,子层变成了新的父层
                       lastNodeOrder = auxque->rear;
                       width = Max(width, nodeCount);
                       nodeCount = 0;
                   }
               }// End While
               return width;
           }
    }
    

    时间复杂度:\\(O(n)\\),空间复杂度:\\(O(n)\\)

    1. 遍历序列确定二叉链表(思想是重点)

    1. 先中序列

    先序遍历序列确定根结点,中序遍历中确定左右子树及各子树的长度,回到先序遍历序列通过子树长度确定子树所在的范围,然后再确定左右子树的根结点。

    BiTree CreateBiTree(ElemType* PreOrder, ElemType* InOrder, int PreLow, int PreHigh, int InLow, int InHigh){
           int parentIndexPre, parentIndexIn;
           // 在InOrder序列中从InLow到InHigh查找根结点PreOrder[parentIndexPre]
           parentIndexPre = PreLow, parentIndexIn = FindIndex(InOrder, PreOrder[parentIndexPre], InLow, InHigh);
           // 创建根结点
           BiNode* parentNode = (BiNode*)malloc(sizeof(BiNode));
           parentNode->data = InOrder[parentIndexIn];
           // 通过中序遍历中根结点的编号确定左右子树的长度
           int lchildLength = parentIndexIn - InLow;
           int rchildLength = InHigh - parentIndexIn;
           // 创建左子树
           if(lchildLength != 0)
           {
               parentNode->lchild = CreateBiTree(PreOrder, InOrder, PreLow+1, PreLow+lchildLength, InLow, InLow+lchildLength-1);
           }
           else
           {
               parentNode->lchild = nullptr;
           }
           // 创建右子树
           if(rchildLength != 0)
           {
               parentNode->rchild = CreateBiTree(PreOrder, InOrder, PreHigh-rchildLength+1, PreHigh, InHigh-rchildLength+1, InHigh);
           }
           else
           {
               parentNode->rchild = nullptr;
           }
           return parentNode;
    }
    
    1. 后中序列

    后序遍历序列确定根结点,中序遍历中确定左右子树及各子树的长度,回到后序遍历序列通过子树长度确定子树所在的范围,然后再确定左右子树的根结点。

    BiTree CreateBiTree(ElemType* PostOrder, ElemType* InOrder, int PostLow, int PostHigh, int InLow, int InHigh){
           int parentIndexPost, parentIndexIn;
           // 在InOrder序列中从InLow到InHigh查找根结点PostOrder[parentIndexPost]
           parentIndexPost = PostHigh, parentIndexIn = FindIndex(InOrder, PostOrder[parentIndexPost], InLow, InHigh);
           // 创建根结点
           BiNode* parentNode = (BiNode*)malloc(sizeof(BiNode));
           parentNode->data = InOrder[parentIndexIn];
           // 通过中序遍历中根结点的编号确定左右子树的长度
           int lchildLength = parentIndexIn - InLow;
           int rchildLength = InHigh - parentIndexIn;
           // 创建左子树
           if(lchildLength != 0)
           {
               parentNode->lchild = CreateBiTree(PostOrder, InOrder, PostLow, PostLow+lchildLength-1, InLow, InLow+lchildLength-1);
           }
           else
           {
               parentNode->lchild = nullptr;
           }
           // 创建右子树
           if(rchildLength != 0)
           {
               parentNode->rchild = CreateBiTree(PostOrder, InOrder, PostHigh-rchildLength, PostHigh-1, InHigh-rchildLength+1, InHigh);
           }
           else
           {
               parentNode->rchild = nullptr;
           }
           return parentNode;
    }
    
    1. 层中序列

    层序遍历的规则是:遍历孩子结点之前先把孩子结点的所有祖先结点和其左(堂)兄弟遍历一遍。所以当我们拿到层序遍历序列中的某个结点时,我们可以肯定的说:“我们有能力将其所有祖先结点构造好!”,故而只需要在中序序列中确定它与根结点的相对位置就行了。

    #include<map>
    #define MAX_SIZE 100
    // 定义层序序列和先序序列
    ElemType levelOrder[MAX_SIZE], inOrder[MAX_SIZE];
    // 定义记录中序序列各个结点位置的字典(映射):{结点值:结点在中序序列中的编号}
    map<ElemType, int> inPos;
    
    // 在指定levelIndex位置创建root结点
    void CreateBiTreeNode(BiTreeNode*& root, int levelIndex){
           // 如果走到了现有的树的底部,则新创建结点(构造祖先结点)
           if(root == nullptr){
               root = (BiTreeNode*)malloc(sizeof(BiTreeNode));
               root->data = levelOrder[levelIndex];
               root->lchild = root->rchild = nullptr;
               return;
           }
           // 如果层序中的结点在中序序列中的位置靠左,
           // 表明该结点在其根结点的左边,让根结点移向其左孩子
           else if(inPos[root->data] >= inPos[levelOrder[levelIndex]]){
               CreateBiTreeNode(root->lchild, levelIndex);
           }
           // 否则表明在根结点的右边,让根结点移向其右孩子
           else{
               CreateBiTreeNode(root->rchild, levelIndex);
           }
    }
    
    // 创建二叉树
    void CreateBiTree(BiTree& BT){
           for(int i=0; i<n; i++){
               CreateBiTreeNode(BT, i);
           }
    }
    
    1. 总结(给啥序列就按照啥规则办事)

    1. 给先序序列:(前序规则)从先序数组起始处确定根结点,再在中序中查找根结点的位置,然后确定根结点的左右子树;
    2. 给后序序列:(后序规则)从后序数组末尾处确定根结点,再在中序中查找根结点的位置,然后确定根结点的左右子树;
    3. 给层序序列:(层序规则)从头到尾遍历层序数组,再在中序序列中确定该结点与根结点的关系,并一层一层下坠;
    1. 先序、中序和后序线索二叉树的构造

    #define THREAD 1
    #define LINK 0
    // 将二叉链表线索化的函数
    void Threading(BiTreeNode current, BiTreeNode& previous){
           // 左子树为空,则建立前驱线索
           if(current != nullptr && current->lchild == nullptr){
               // 判断cur!=nullptr是因为美观,cur一直不会为空,因为递归遍历有if保证
               current->lchild = previous;
               current->ltag = THREAD;
           }
           // 右子树为空,则建立后继线索
           if(previous != nullptr && previous->rchild == nullptr){
               // 判断pre!=nullptr是因为pre初始化为nullptr
               previous->rchild = current;
               prevoius->rtag = THREAD;
           }
           // 向前移动
           previous = current;
    }
    
    1. 递归算法(树的递归特性)

    // 定义函数指针类型指明传入的是哪一种遍历方式
    typedef void (*OrderThreading)(BiTree current,BiTree& previous);
    
    // 创建线索化二叉链表
    void CreateThreadTree(BiTree T, OrderThreading OT){
           BiTreeNode previous = nullptr;
           if(T != nullptr){
               OT(T->lchild, prevoius);
               // 为最后一个结点做后继线索化
               if(previous->rchild == nullptr){
                   previous->rtag = THREAD;
               }
           }
    }
    
    // 先序遍历
    void PreOrderThreading(BiTree current,BiTree& previous){
           if(current != nullptr){
               Threading(current, previous);
               // 防止先序遍历时产生“爱滴魔力转圈圈”的情况
               if(current->ltag == LINK){
                   PreOrderThreading(current->lchild, previous);
               }
               PreOrderThreading(current->lchild, previous);
           }
    }
    // 中序遍历
    void InOrderThreading(BiTree current,BiTree& previous){
           if(current != nullptr){
               PreOrderThreading(current->lchild, previous);
               Threading(current, previous);
               PreOrderThreading(current->lchild, previous);
           }
    }
    // 后序遍历
    void PostOrderThreading(BiTree current,BiTree& previous){
           if(current != nullptr){
               PreOrderThreading(current->lchild, previous);
               PreOrderThreading(current->lchild, previous);
               Threading(current, previous);
           }
    }
    

    【注】出现“爱滴魔力转圈圈”问题是因为:在访问后继结点之前[1],后继结点的前驱线索指针与后继指针重合[2]。[1]指明根结点需要先访问,所以只会发生在先序遍历中;[2]只能是左孩子指针才能“身兼二职”。

    1. 非递归算法(用辅助栈实现非递归的遍历)

    先序遍历和中序遍历很相似,一个是在压入栈之前访问(入栈序列),一个是弹出栈之后访问(出栈序列)。由于头插法的性质,辅助栈栈顶元素即是前驱结点。

    // 先序遍历
    void PreOrderThreading(BiTree T){
           InitStack(S);
           BiTreeNode current = T;
           // 当前指针不空或者辅助栈不空就继续
           while(current != nullptr || !IsEmpty(S)){
               // 防止出现“爱滴魔力转圈圈”的情况
               if(current != nullptr && current->ltag == LINK){
                   BiTreeNode previous = nullptr;
                   GetTop(S, previous);
                   Threading(current, previous);
                   // 将线索化后的根结点压入栈中,并继续向左移动
                   Push(S, current);
                   current = current->lchild;
               }
               else{
                   // 将根结点取出并向右移动
                   Pop(S, current);
                   current = current->rchild;
               }
           }
    }
    
    // 中序遍历
    void InOrderThreading(BiTree T){
           InitStack(S);
           BiTreeNode current = T;
           // 当前指针不空或者辅助栈不空就继续
           while(current != nullptr || !IsEmpty(S)){
               if(current != nullptr){
                   // 将根结点压入栈中,并继续向左移动
                   Push(S, current);
                   current = current->lchild;
               }
               else{
                   // 将根结点取出,线索化后再向右移动
                   Pop(S, current);
                   BiTreeNode previous = nullptr;
                   GetTop(S, previous);
                   Threading(current, previous);
                   current = current->rchild;
               }
           }
    }
    

    后序遍历就完全不同了,因为必须保证左右子树都要访问完成,所以必须借助“前驱指针\\(\\mathbf{previous}\\)证明是否被访问过”进行流程控制,这就大大增加了难度。

    // 后序遍历
    void PostOrderThreading(BiTree T){
           // 倘若不想使用栈,则需要使用三叉链表的存储结构(第三叉指向双亲节点)
           InitStack(StackOfParent);
           BiNode* cursor, previous;
           cursor = T, previous = nullptr;
           while(cursor || !IsEmpty(StackOfParent)){
               // 因为我们需要将cursor压入栈内,所以不能仅仅判断cursor有无左孩子
               if(cursor){
                   // 如果当前位置存在节点,则将其作为“双亲节点”入栈
                   Push(StackOfParent, cursor);
                   // 并指向自己的左孩子(继续向左走)
                   cursor = cursor->lchild;
               }
               else{
                   // 如果当前位置不存在节点,说明向左走走到头了,需要向右转后者向上走(如果没有右孩子或者已经向右转过则不需要了)
                   GetTop(StackOfParent, parent);
                   // 如果右兄弟存在且没有被访问过,则向右转
                   if(parent->rchild!=nullptr && parent->rchild!=previous){
                       // 不判断右兄弟是否存在会造成死循环
                       cursor = parent->rchild;
                   }
                   // 向上走
                   else{
                       // 说明该双亲结点的左右子树都已经访问完成了(或者没有孩子),所以弹出双亲节点
                       Pop(StackOfParent, parent);
                       // 访问“根”(也就是双亲结点)
                       BiTreeNode previous = nullptr;
                       GetTop(StackOfParent, previous);
                       Threading(parent, previous);
                       // 移动前驱指针
                       previous = parent;
                       // 为了后续操作为向右转向(因为根节点访问完了就代表这一整棵子树已经访问完成,所以需要向上向右走转向右兄弟树)
                       cursor = nullptr;
                   }
               }
           }
    }
    
    1. 先中后序遍历查找的前驱后继(手算的方法)

    1. 先序遍历查找前驱后继(需要三叉链表获取双亲结点)

    1. p->rtag == THREAD,即结点已被后继线索化,则next = p->rchild

    2. p->ltag == THREAD,即结点已被前驱线索化,则previous = p->lchild

    3. p->rtag == LINK,即结点未被后继线索化(分支结点),则该节点必有右孩子,按照“根[左]右”的规则可以分2种情况:

    1. 有左孩子,即p->lchild != nullptrnext = p->lchild
    2. 无左孩子,即p->lchild == nullptrnext = p->rchild
    1. p->ltag == LINK,即结点未被前驱线索化(分支结点),但是先序遍历的前驱是比不会存在于其左右子树中的,所以需要借助三叉链表获取其双亲结点。
    1. p是左孩子:则由“根(p左右)[右]”可知,previous = p->parent

    2. p是右孩子,且没有左兄弟:则由“根(p左右)”可知,previous = p->parent

    3. p是右孩子,存在左兄弟:则由“根(根左右)(p左右)”可知,previous = 左兄弟的右优先叶子结点

    1. 中序遍历查找前驱后继

    1. p->rtag == THREAD,即结点已被后继线索化,则next = p->rchild
    2. p->ltag == THREAD,即结点已被前驱线索化,则previous = p->lchild
    3. p->rtag == LINK,即结点未被后继线索化(分支结点),则该节点必有右孩子,所以next = 右子树最左下结点
    4. p->ltag == LINK,即结点未被前驱线索化(分支结点),则该节点必有左孩子,所以previous = 左子树最右下结点;(出现右子树向左走,否则向左)
    1. 后序遍历查找前驱后继(需要三叉链表获取双亲结点)

    1. p->rtag == THREAD,即结点已被后继线索化,则next = p->rchild

    2. p->ltag == THREAD,即结点已被前驱线索化,则previous = p->lchild

    3. p->ltag == LINK,即结点未被前驱线索化(分支结点),则该节点必有左孩子,按照“左[右]根”的规则可以分2种情况:

    1. 有右孩子,即p->rchild != nullptrprevious = p->rchild
    2. 无右孩子,即p->rchild == nullptrprevious = p->lchild
    1. p->rtag == LINK,即结点未被后继线索化(分支结点),但是后序遍历的后继是比不会存在于其左右子树中的,所以需要借助三叉链表获取其双亲结点。
    1. p是右孩子:则由“[左](左右p)根”可知,next = p->parent

    2. p是左孩子,且没有右兄弟:则由“(左右p)根”可知,next = p->parent

    3. p是左孩子,存在右兄弟:则由“(左右p)(左右根)根”可知,next = 右兄弟的左优先叶子结点;(出现左子树向左走,否则向右)

    先序线索二叉树 中序线索二叉树 后序线索二叉树
    找前驱 ×
    找后继 ×

    【注】湖南大学866数据结构是不考察线索化的,所以重点是如何查找没有线索化的前驱后继

    1. 二叉树的溯源问题(如何保存遍历路径:栈+后序!)

    问题:打印出值为\\(x\\)的结点的所有祖先结点(假设值为\\(x\\)的结点最多有1个)

    由于要保存所有祖先结点,所以必须使用后序遍历,保证根结点最后访问!

    void SearchAncestors(BiTree T, ElemType x){
           // 倘若不想使用栈,则需要使用三叉链表的存储结构(第三叉指向双亲节点)
           InitStack(StackOfAncestor);
           BiTree cursor, previous;
           cursor = T, previous = nullptr;
           while(cursor || !IsEmpty(StackOfAncestor)){
               // 因为我们需要将cursor压入栈内,所以不能仅仅判断cursor有无左孩子
               if(cursor){
                   // 如果当前位置存在节点,则将其作为“双亲节点”入栈
                   Push(StackOfAncestor, cursor);
                   // 并指向自己的左孩子(继续向左走)
                   cursor = cursor->lchild;
               }
               else{
                   // 如果当前位置不存在节点,说明向左走走到头了,需要向右转后者向上走(如果没有右孩子或者已经向右转过则不需要了)
                   BiTree parent = nullptr;
                   GetTop(StackOfAncestor, previous);
                   // 如果右兄弟存在且没有被访问过,则向右转
                   if(parent->rchild!=nullptr && parent->rchild!=previous){
                       // 不判断右兄弟是否存在会造成死循环
                       cursor = parent->rchild;
                   }
                   // 向上走
                   else{
                       // 说明该双亲结点的左右子树都已经访问完成了(或者没有孩子),所以弹出双亲节点
                       Pop(StackOfAncestor, parent);
                       // 访问“根”(也就是双亲结点)
                       if(parent->data == x){
                           printf("所查结点的所有祖先的值为:");
                           while(!IsEmpty(StackOfAncestor)){
                               BiTree ancestor = nullptr;
                               Pop(StackOfAncestor, ancestor);
                               printf(&(ancestor->data));
                               printf(" ");
                           }
                           // 直接退出函数体
                           exit(1);
                       }
                       // 将previous指针指向最近访问过的双亲结点(前驱)
                       previous = parent;
                       // 为了后续操作为向右转向(因为根节点访问完了就代表这一整棵子树已经访问完成,所以需要向上向右走转向右兄弟树)
                       cursor = nullptr;
                   }
               }
           }
    }
    

    王道给出的更简洁的方法,但是与之前所学的后序遍历非递归实现不同,换了一种栈的数据结构,并且换了一种遍历方式:首先遍历左子树,与此同时访问根结点(可以理解为利用先序遍历节省时间,因为如果在遍历左子树时就找到了x,我们就能直接跳出循环了);如果左子树中没有找到,就去最近的没有被访问过的右子树中搜索(由于栈中保存了\\(\\mathbf{tag}\\)作为对左右子树访问完成的标识,所以不需要用\\(\\mathbf{previous}\\)指针了)。所以这很难说是严格的后序遍历,不过要说是“打了小抄”或者“乐于剧透”的后序遍历我倒是不否认。

    typedef struct{
           BiTree bt;
           int tag; // 值为0表示左孩子已经被访问,值为1表示右孩子已经被访问
    }Stack;
    
    void Search(BiTree bt, char x) {
           Stack s[10]; //栈容量足够大 
           int top = 0;
           while (bt != nullptrptr || top > 0) {
               // 首先遍历左子树,这里访问根结点:bt->data != x(打个小抄,剧透一下)
               if (bt != nullptrptr && bt->data != x) {
                   // 如果恰好x出现在左子树中我们就能跳过之后的遍历了
                   s[++top].bt = bt;
                   s[top].tag = 0;
                   bt = bt->lchild;
               }
               else
               {
                   // 如果在左子树中找到了x结点(并不是在访问“根结点”)
                   if (bt && bt->data == x) {
                       printf("所查结点的所有祖先结点的值为∶\\n");
                       for (i = 1; i <= top; i++)
                           printf("%c", s[i].bt->data);
                       exit(1);
                   }
                   // 继续在右子树中搜索x结点
                   else
                   {
                       // 如果右孩子被访问了则返回到:最近的、没有访问右孩子的结点处
                       while(top != 0 && s[top].tag == 1)
                           top--;
                       // 如果没有退到根结点,表示还可以继续向右执行
                       if(top != 0) {
                           s[top].tag = 1;
                           bt = s[top].bt->rchild;
                       }
                   }
               }
           }
    }
    
    1. 寻找指定两个结点的最近公共祖先结点

    问题:设一棵二叉树的结点结构为\\((\\mathbf{lchild},\\mathbf{info},\\mathbf{rchild})\\)\\(\\mathbf{root}\\)为指向该二叉树根结点的指针,\\(p\\)\\(q\\)分别为指向该二叉树中任意两个结点的指针,试编写算法\\(\\mathbf{Ancestor}(\\mathbf{root},p,g,r)\\),找到\\(p\\)\\(q\\)的最近公共祖先结点\\(r\\)

    由(g)可知此问题可以被转化为求两个栈的第一个公共元素。故核心思想为:采用后序遍历的方式在栈中保存所有“根结点”。找到\\(p\\)结点后将当前栈中所有元素复制到另一辅助栈中,然后继续寻找\\(q\\)结点。最后比较两个栈中的元素,第一个匹配(相等)的即是\\(p\\)\\(q\\)结点的最近公共祖先结点\\(r\\)

    typedef struct{
           BiTree bt;
           int tag; // 值为0表示左孩子已经被访问,值为1表示右孩子已经被访问
    }Stack;
    // 栈的深拷贝
    void StackCopy(Stack s1[], int tops1, Stack s2[], int& tops2){
           for(int i=0; i<=tops1; i++){
               s2[i] = s1[i];
           }
           tops2 = tops1;
    }
    // 寻找两个栈的第一个公共元素
    BiTree SearchFirstPublicElem(Stack s1[], int tops1, Stack s2[], int tops2){
           for (int i1 = tops1; i1 > 0; i1--) {
               for (int i2 = tops2; i2 > 0; i2--) {
                   if (s1[i1].bt == s2[i2].bt) {
                       return s1[i1].bt;
                   }
               }
           }
           return nullptr;
    }
    Stack s[10], auxs[10] // 定义栈和辅助栈
    
    BiTreeNode* SearchNearestPublicAncestor(BiTree root, BiTreeNode* p, BiTreeNode* q){
           // 定义流程控制变量:控制栈的拷贝
           bool isTwiceFind = false;
           // 定义两个栈的栈顶游标
           int tops, topauxs;
           // 将根结点入栈
           BiTreeNode* cursor = root;
           // 索引为0的位置不用(与数据结构吻合)
           tops = topauxs = 0, s[++tops] = cursor;
           // 如果没到尽头,或者栈非空,则继续循环
           while(cursor != nullptr || tops > 0){
               // 首先在左子树中寻找p结点和q结点,并“剧透一下”
               if(cursor != nullptr && cursor != p && cursor != q){
                   s[++tops].bt = cursor;
                   s[tops].tag = 0;
                   cursor = cursor->lchild;
               }
               else{
                   // 如果在左子树中找到了p结点或者q结点
                   if(cursor != nullptr){
                       // 第一次找到p或者q,则拷贝栈中元素
                       if(!isTwiceFind){
                           StackCopy(s,tops,auxs,topauxs);
                           isTwiceFind = true;
                           // 回到开始的if继续在左子树中搜索
                       }
                       // 第二次找到p或者q,则寻找两个栈中的最近公共祖先结点
                       else{
                           return SearchFirstPublicElem(s,tops,auxs,topauxs);
                       }
                   }
                   // 如果两个结点都没有在左子树中找到,则去右子树中搜索
                   else{
                       // 退回至最近的、没有被访问右子树的“根结点”
                       while(tops != 0 && s[tops].tag == 1){
                           tops--;
                       }
                       // 如果没有退回到根结点,则继续向右子树中寻找
                       if(tops != 0){
                           BiTreeNode* parent = s[tops].bt;
                           cursor = parent->rchild;
                       }
                   }
               }
           }
    }
    
    1. 求先、中和后序遍历序列中的第\\(k(1\\le k\\le n)\\)个结点的值

    1. 先序遍历

    先序遍历的思想是“根左右”,当根结点不是\\(\\mathbf{KNode}\\)时,我们在左右子树中搜索。先在左子树中搜索\\(\\mathbf{KNode}\\),如果存在则返回k,不再继续搜索右子树;否则返回其左子树最后一个被先序遍历的结点的编号,然后转向其右子树进行搜索,如果搜索到了则返回k。否则表明左右子树中也没有搜索到,则返回至双亲结点并在下一棵树中搜索\\(\\mathbf{KNode}\\)

    // cursor表示当前节点在先序遍历中的序号(初始值为0)
    int FindKNode_Pre1(BiTree T, int cursor, int k, BiTree& KNode){
           if(T != nullptr){
               // 在根结点处搜索
               if(++cursor == k){
                   KNode = T;
                   // 赶紧return,不再进入子树中搜索(节约时间)
                   return cursor;
               }
    
               // 继续向左走并记录左子树最后一个被遍历的结点的编号
               cursor = FindKNode_Pre(T->lchild, cursor, k, KNode);
               // 在左子树中找到了,则向上一直返回k,不再遍历其右子树;
               if(cursor == k){
                   return k;
               }
    
               // 左子树全部走完之后都没有找到,则向右走在右子树中搜索;
               else{
                   return FindKNode_Pre(T->rchild, cursor, k, KNode);
               }
               // 如果k>n则会将树完全遍历,不会产生无限递归
           }
           else{
               // 返回到其双亲结点
               return cursor;
           }
    }
    
    // 方法二:王道的解法。思想是差不多的,只不过我是将left==k作为标记,而王道则是重新创造了一个#作为标记,这样更加简洁
    int i = 1;
    char FindKNode_Pre2(BiTree T, int k){
           if (!T)
               return \'#\';
           if (i == k)	// 如果是第k个,则返回该节点的值
               return T->data;
           i++;
           char ch = FindKNode_2(T->lchild, k);
           // 如果不是空结点表示在左子树中找到了KNode,否则在右子树中继续搜索
           return ch != \'#\' ? ch : FindKNode_2(T->rchild, k);
    }
    
    1. 中序遍历(二叉查找树的第k个结点)

    // cursor表示当前节点在中序遍历中的序号(初始值为0)
    int FindKNode_In1(BiTree T, int cursor, int k, BiTree& KNode) {
           if (T != nullptrptr) {
               // 先在左子树中搜索,并记录左子树在中序遍历中的最后一个结点编号
               cursor = FindKNode_In(T->lchild, cursor, k, KNode);
               // 如果在左子树中搜索到了则向上返回k,
               if (cursor == k) {
                   return k;
               }
    
               // 左子树中没有找到,搜索根结点和右子树
               else {
                   // 搜索根结点,如果根结点是KNode则返回
                   if (++cursor == k) {
                       KNode = T;
                       return cursor;
                   }
    
                   // 否则继续向右子树中搜索KNode
                   else {
                       return FindKNode_In(T->rchild, cursor, k, KNode);
                   }
               }
           }
           else {
               return cursor;
           }
    }
    
    // 方法二:王道的Style
    int i = 1;
    char FindKNode_In2(BiTree T, int k) {
           if(!T)
               return \'#\';
           // 首先搜索左子树,如果搜索到了ch!=\'#\'
           char ch = FindKNode_In(T->lchild, k);
           if(ch != \'#\')
               return ch;
           // 左子树没有搜索到则搜索根结点
           if(++i == k)
               return T->data;
           // 根结点没有则继续在右子树中搜索
           else
               return FindKNode_In(T->rchild, k);
    }
    
    1. 后序遍历

    // cursor表示当前节点在后序遍历中的序号(初始值为0)
    int FindKNode_Post1(BiTree T, int cursor, int k, BiTree& KNode) {
           if (T != nullptrptr) {
               // 先在左子树中搜索,并记录左子树在中序遍历中的最后一个结点编号
               cursor = FindKNode_Post(T->lchild, cursor, k, KNode);
               // 如果在左子树中搜索到了则向上返回k,否则
               if (cursor == k) {
                   return k;
               }
    
               // 左子树

    树的存储结构;树与二叉树的转换;树和森林的遍历算法

    树的存储结构

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    树、森林与二叉树的转换

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    树和森林的遍历

    在这里插入图片描述
    在这里插入图片描述

    以上是关于数据结构二叉树普通树与森林的定义与应用的主要内容,如果未能解决你的问题,请参考以下文章

    (王道408考研数据结构)第五章树-第三节4:树与二叉树的转换

    王道数据结构——树与二叉树

    树森林与二叉树的相互转换

    树的知识补充

    树与二叉树的相互转换以及森林和二叉树的相互转换

    森林树与二叉树相互转换