2

DS博客作业03--树

0.PTA得分截图

1.本周学习总结(0-5分)

1.1 总结树及串内容

串的BF算法:

原理:

BF算法,即暴风(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。BF算法是一种蛮力算法。

举例:

例如:
判断串A(abcac)是否为串B(ababcabacabab)的子串的判断过程如下:
首先,将串 A 与串 B 的首字符对齐,然后逐个判断相对的字符是否相等,如下图所示:

从上图中可以看出,串A和串B的第3个字符匹配失败,因此需要将串A后移一个字符的位置,继续同串B匹配,如下图所示:

从上图中可以看出,两串匹配失败,串A继续后移一个字符的位置,如下图所示:

从上图中可以看出,两串匹配失败,串A继续后移,一直移动到下图位置才匹配成功:

由此可得,串A与串B是经历了6次的匹配才成功,通过整个模式匹配的过程,证明了串A是串B的子串。

代码实现:

#include<iostream>
#include <string>
using namespace std;
int mate(char* B, char* A);//串BF算法的实现函数,其中 B是主串,A是子串
int main() 
{
    char A[100] = "abcac";
    char B[1000] = "ababcabcacbab";
    int number = mate(B,A);
    cout << number;
    return 0;
}
int mate(char* B, char* A)//串BF算法的实现函数,其中 B是主串,A是子串
{
    int i = 0, j = 0;
    while (i < strlen(B) && j < strlen(A))
    {
        if (B[i] == A[j])
        {
            i++;
            j++;
        }
        else
        {
            i = i - j + 1;
            j = 0;
        }
    }
    //跳出循环有两种可能,i=strlen(B)说明已经遍历完主串,匹配失败;j=strlen(A),说明子串遍历完成,在主串中成功匹配
    if (j == strlen(A))
    {
        return i - strlen(A) + 1;
    }
    //运行到此,为i==strlen(B)的情况
    return 0;
}

时间复杂度:

该算法最理想的时间复杂度 O(n),n表示串A的长度,即第一次匹配就成功。
最坏情况的时间复杂度为 O(n*m),n表示串A的长度,m为串B的长度。
如:串B为“0000000000000001”,串A为“01”,这种情况下,两串每次匹配,都必须匹配至串A的最末尾才能判断匹配失败,因此运行了n×m次。

串的KMP算法:

原理:

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特--莫里斯--普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。
假设现在文本串S匹配到i位置,模式串P匹配到j位置。
如果j=-1,或者当前字符匹配成功(即S[i]==P[j]),都令i++,j++,继续匹配下一个字符;
如果j!=-1,且当前字符匹配失败(即S[i]!=P[j]),则令i不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j-next[j]位。
可知,当匹配失败时,模式串向右移动的位数为:匹配失败的字符所在位置 - 匹配失败的字符对应的next 值;
即移动的实际位数为:j - next[j],且此值大于等于1。

举例:

例如:
当字符‘A’跟字符‘D’匹配失败时,执行指令:“如果j!=-1,且当前字符匹配失败(即S[i]!=P[j]),则令i不变,j=next[j]”,即j从6变到2,所以相当于模式串向右移动的位数为j-next[j](j-next[j]=6-2=4)。如下图所示:

向右移动4位后,字符‘A’跟字符‘C’继续匹配。为什么要向右移动4位呢,因为移动4位后,模式串中又有个“AB”可以继续跟“AB”对应着,从而不用让i回溯。相当于在除去字符D的模式串子串中寻找相同的前缀和后缀,然后根据前缀后缀求出next 数组,最后基于next 数组进行匹配。如下图所示:

代码实现:

int KMP(char* s, char* p)
{
    int i = 0;
    int j = 0;
    int sLen = strlen(s);
    int pLen = strlen(p);
    while (i < sLen && j < pLen)
    {
        if (j == -1 || s[i] == p[j])//如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
        {
            i++;
            j++;
        }
        else
        {        
            j = next[j];   //如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]  next[j]即为j所对应的next值
        }
    }
    if (j == pLen)
        return i - j;
    else
        return -1;
}

时间复杂度:O(m+n)。

next数组:

简便算法(不同于老师介绍的)
重点在于观察前缀与后缀的相同个数
直接上例子:数组a[5]

代码实现:
void GetNext(char* p, int next[])
{
    int len = strlen(p);
    next[0] = -1;
    int k = -1;
    int j = 0;
    while (j < len - 1)
    {
        //p[k]表示前缀,p[j]表示后缀  
        if (k == -1 || p[j] == p[k])
        {
            ++k;
            ++j;
            next[j] = k;
        }
        else
        {
            k = next[k];
        }
    }
}

二叉树的存储结构:

顺序存储:

原理:

二叉树的顺序存储是将二叉树的所有结点,按照一百定的次序,存储到一片连续的存储单元中。二叉树的顺序存储必须将结点排成一个适当的线性序列,使得结点度在这个序列中的相应位置能反映出结点之间的逻辑关系。这种结构特别适用于近似满二叉树。在一棵具有n个结点的近似满二叉树中,当从树根起,自上层到下层,逐层从左到右给所有结点编号时,就能得到一个足以反映整个二叉树结构的线性序列。其中每个结点的编号就作为结点。

二叉树的示意图:

建法代码:
#define Maxsize 100     //假设一维数组最多存放100个元素
typedef struct
{ 
    char bt[Maxsize];
    int btnum;
}Btseq;

链式存储:

原理:

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。
通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。

二叉树示意图:

建法代码:
typedef struct  node   //定义结点由数据域,左右指针组成
{ 
    char data;
    struct node *lchild,*rchild;
}Bitree;

递归遍历:

如下图一棵二叉树:

先序遍历:先访问根节点——左子树——右子树。所以遍历结果:ABCDEFGHK
中序遍历:先访问左子树——根节点——右子树。所以遍历结果:BDCAEHGKF
后序遍历:先访问左子树——右子树——根节点。所以遍历结果:DCBHKGFEA
代码实现:
先序遍历:
void PreOrder(BiTree T)//先序递归遍历
{
    if(T!=NULL)
    {
       cout<<T->data<<" ";
       PreOrder(T->lchild);
       PreOrder(T->rchild);
     }
}
中序遍历:
void InOrder(BiTree T)//中序递归遍历
{
    if(T!=NULL)
    {
       InOrder(T->lchild);
       cout<<T->data<<" ";
       InOrder(T->rchild);
    }
}
后序遍历:
void PostOrder(BiTree T)//后序递归遍历
{
    if(T!=NULL)
    {
        PostOrder(T->lchild);
        PostOrder(T->rchild);
        cout<<T->data<<" ";
    }
}

层次遍历:

层次遍历,就是从上到下一层一层的遍历

举例:

依旧沿用上面的那个例子:
层次遍历的结果为:ABECFDGHK

代码实现:
void BinaryTreeLevelOrder(BTNode* root)
{
    Queue q;
    if (root == NULL) //树为空,直接返回
    {
        return;
    }
    QueueInit(&q);
    QueuePush(&q, root);    //先将根节点入队
    while (QueueEmpty(&q))
    {
        BTNode* front = QueueFront(&q);        //出队保存队头并访问
        printf("%c", front->_data);
        QueuePop(&q);
        if (front->_left)        //将出队结点的左子树根入队
            QueuePush(&q, front->_left);
        if (front->_right)        //将出队结点的右子树根入队
            QueuePush(&q, front->_right);
    }
}

应用:

前序遍历:pta 6-1 先序输出叶结点
中序遍历:pta 6-2 中序输出度为1的结点
后序遍历:pta 7-6 根据后序和中序遍历输出先序遍历
层次遍历:pta 7-2 jmu-ds-二叉树层次遍历

数的结构:

表示一棵树的方法有:双亲表示法,孩子表示法,孩子兄弟表示法

双亲表示法:

以双亲作为索引的关键词的一种存储方式
每个结点只有一个双亲,所以选择顺序存储占主要
以一组连续空间存储树的结点,同时在每个结点中,附设一个指示其双亲结点位置的指针域

结构体定义:
#define MAXSIZE 100
typedef int TElemType;
typedef struct PTNode    //结点结构
{
    TElemType data;    //结点数据
    int parent;        //双亲位置
}PTNode;
typedef struct //树结构
{
    PTNode nodes[MAXSIZE];    //结点数组
    int r, n;    //r是根位置,n是结点数
}PTree;
过程表示:


优缺点:

优点:parent指针域指向数组下标,所以找双亲结点的时间复杂度为O(1),向上一直找到根节点也快
缺点:由上向下找就十分慢,若要找结点的孩子或者兄弟,要遍历整个树

孩子表示法:

主要关注孩子结点

结构体定义:
#define MAXSIZE 100
typedef int TElemType;
typedef struct PTNode    //结点结构
{
    TElemType data;    //结点数据
    int child1;    //孩子1结点
    int child2;    //孩子2结点
    int child3;    //孩子3结点
}PTNode;
typedef struct //树结构
{
    PTNode nodes[MAXSIZE];    //结点数组
    int r, n;    //r是根位置,n是结点数
}PTree;
过程表示:


缺点:占用了大量不必要的孩子域空指针
孩子兄弟表示法:

任意一棵树,他的结点的第一个孩子如果存在就是唯一结点,他的右兄弟如果存在,也是唯一的,因此,我们设置两个指针,分别指向该结点的第一个孩子和该结点的右兄弟

结构体定义:
typedef int TElemType;
typedef struct Node
{
    TElemType data;
    struct Node* firstchild, *rightsib;
}Node,*Tree;

遍历方式(与二叉树相同)

如下图一棵二叉树:

先序遍历:先访问根节点——左子树——右子树。所以遍历结果:ABCDEFGHK
中序遍历:先访问左子树——根节点——右子树。所以遍历结果:BDCAEHGKF
后序遍历:先访问左子树——右子树——根节点。所以遍历结果:DCBHKGFEA
代码实现:
先序遍历:
void PreOrder(BiTree T)//先序递归遍历
{
    if(T!=NULL)
    {
       cout<<T->data<<" ";
       PreOrder(T->lchild);
       PreOrder(T->rchild);
     }
}
中序遍历:
void InOrder(BiTree T)//中序递归遍历
{
    if(T!=NULL)
    {
       InOrder(T->lchild);
       cout<<T->data<<" ";
       InOrder(T->rchild);
    }
}
后序遍历:
void PostOrder(BiTree T)//后序递归遍历
{
    if(T!=NULL)
    {
        PostOrder(T->lchild);
        PostOrder(T->rchild);
        cout<<T->data<<" ";
    }
}

应用:

pta 6-3 求二叉树高度
pta 6-4 jmu-ds-表达式树
pta 7-4 jmu-ds-输出二叉树每层节点
pta 7-7 目录树

线索二叉树

原理:

利用原来的空链域存放指针,指向树中其他结点。这种指针称为线索。
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。
由于前驱和后继信息只有在遍历该二叉树时才能得到,所以,线索化的过程就是在遍历的过程中修改空指针的过程。
记ptr指向二叉链表中的一个结点,以下是建立线索的规则:
(1)如果ptr->lchild为空,则存放指向中序遍历序列中该结点的前驱结点。这个结点称为ptr的中序前驱;
(2)如果ptr->rchild为空,则存放指向中序遍历序列中该结点的后继结点。这个结点称为ptr的中序后继;
显然,在决定lchild是指向左孩子还是前驱,rchild是指向右孩子还是后继,需要一个区分标志的。因此,我们在每个结点再增设两个标志域ltag和rtag,注意ltag和rtag只是区分0或1数字的布尔型变量,其占用内存空间要小于像lchild和rchild的指针变量。
结点结构如下所示。

其中:
(1)ltag为0时指向该结点的左孩子,为1时指向该结点的前驱;
(2)rtag为0时指向该结点的右孩子,为1时指向该结点的后继;
(3)因此对于上图的二叉链表图可以修改为下图的养子。

结构体定义:

typedef struct BitNode  
{  
    char data;                                      //结点数据  
    struct BitNode *lchild, *rchild;                //左右孩子指针  
    PointerTag  Ltag;                               //左右标志  
    PointerTag  rtal;  
}BitNode, *BiTree;  

中序遍历:

void InThreading(BiTree p)  
{  
    if(p)  
    {  
        InThreading(p->lchild);          //递归左子树线索化  
        if(!p->lchild)           //没有左孩子  
        {  
            p->ltag = Thread;    //前驱线索  
            p->lchild = pre; //左孩子指针指向前驱  
        }  
        if(!pre->rchild)     //没有右孩子  
        {  
            pre->rtag = Thread;  //后继线索  
            pre->rchild = p; //前驱右孩子指针指向后继(当前结点p)  
        }  
        pre = p;  
        InThreading(p->rchild);      //递归右子树线索化  
    }  
}  

哈夫曼树、并查集

哈夫曼树:

带权路径长度(WPL):
定义:树的所有叶结点的带权路径长度之和,称为树的带权路径长度表示为WPL。

树的带权路径长度记为WPL=(W1L1+W2L2+W3L3+...+WnLn),N个权值Wi(i=1,2,...n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,...n)。可以证明哈夫曼树的WPL是最小的。
WPL是衡量一个带权二叉树优劣的关键。
无论如何,对于n个带权节点,总可以用他们作为叶节点构造出一颗最小WPL值得树,并称满足这个条件的二叉树为哈夫曼树。
最优二叉树或哈夫曼树: WPL最小的二叉树

哈夫曼树的特点:

1.没有度为1的结点
2.n个叶子结点的哈夫曼树共有2n-1个结点
3.哈夫曼树的任意非叶节点的左右子树交换后仍是哈夫曼树
4.对同一组权值{w1 ,w2 , …… , wn},存在不同构的两棵哈夫曼树

哈夫曼树的构造:

每次把权值最小的两颗二叉树合并.(利用堆)
如下图所示:




代码实现:
typedef struct TreeNode
{
    int weight;
    HuffmanTree left,right;
}HuffmanTree, *HuffmanTree; 
 
HuffmanTree Huffman(MinHeap H)
{
    HuffmanTree T;
    BuildMinHeap(H);//将H->data[]按权值调整为最小堆 
    for(int i = 1; i < H->Size; i++)//做H->Size-1次合并 
    {
        T = (HuffmanTree)malloc(sizeof(struct TreeNode));//建立新结点 
        T->left = DeleteMin(H);    //从最小堆中删除一个结点,作为新T的左子结点 
        T->right = DeleteMin(H);//从最小堆中删除一个结点,作为新T的右子结点
        T->weight = T->left->weight + T->right->weight;//计算新权值
        Insert(H,T);//将新T插入最小堆
    }
    T = DeleteMin(H);
    return T;
}
哈夫曼树的应用:

pta 7-10 哈夫曼树
pta 7-11 修理牧场

哈夫曼树编码:

原理:

哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。
例:如果需传送的电文为 ‘ABACCDA’,它只用到四种字符,用两位二进制编码便可分辨。假设 A, B, C, D 的编码分别为 00, 01,10, 11,则上述电文便为 ‘00010010101100’(共 14 位),译码员按两位进行分组译码,便可恢复原来的电文。

并查集:

原理:

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。

树的表示:

基本操作:
1.makeSet(s):建立一个新的并查集,其中包含 s 个单元素集合。

#######原理:
假设使用一个足够长的数组来存储树节点,构造出如下图的森林,其中每个元素都是一个单元素集合,即父节点是其自身:

#######代码实现:

const int MAXSIZE = 500;
int uset[MAXSIZE];
void makeSet(int size) 
{
    for(int i = 0;i < size;i++) 
        uset[i] = i;
}

#######时间复杂度:O(n)

2.find(x):找到元素 x 所在的集合的代表,该操作也可以用于判断两个元素是否位于同一个集合,只要将它们各自的代表比较一下就可以了。

#######原理:
如果每次都沿着父节点向上查找,那时间复杂度就是树的高度,完全不可能达到常数级。这里需要应用一种非常简单而有效的策略——路径压缩。
路径压缩,就是在每次查找时,令查找路径上的每个节点都直接指向根节点,如下图所示:


#######代码实现:

int find(int x) 
{
    if (x != uset[x]) uset[x] = find(uset[x]);
    return uset[x];
}
3.unionSet(x, y):把元素 x 和元素 y 所在的集合合并,要求 x 和 y 所在的集合不相交,如果相交则不合并。

#######原理:
并查集的合并就是将一个集合的树根指向另一个集合的树根,如下图所示:


#######代码实现:

void unionSet(int x, int y) 
{
    if ((x = find(x)) == (y = find(y))) 
        return;
    if (rank[x] > rank[y]) 
        uset[y] = x;
    else
     {
        uset[x] = y;
        if (rank[x] == rank[y]) 
            rank[y]++;
    }
}
应用:

pta 7-12 朋友圈

1.2.谈谈你对树的认识及学习体会。

树的内容很多也很杂,树的多种存储结构,孩子树,双亲树,孩子兄弟树,二叉树,结构体中的内容差不多都是值和指针的组合。树的操作一般从建树开始,然后遍历,遍历又分为多种顺序的遍历,比如二叉树的遍历就有四种,其中先序中序后序遍历可用递归完成,其后就是一些求高度求宽度找路径,插入删除的操作。运用树可以解决高效编码的问题,比如哈夫曼树。

2.阅读代码(0--5分)

2.1 题目及解题代码

题目:


解题代码:

class Solution {
public:
    TreeNode *s1 = NULL, *s2 = NULL, *pre = NULL;
    void recoverTree(TreeNode* root) {
        TreeNode* cur = root; 
        while(cur != NULL){           
            if(cur->left != NULL){  
                TreeNode* predecessor = cur->left;
                while(predecessor->right != NULL && predecessor->right != cur){
                    predecessor = predecessor->right;
                }
                if(predecessor->right == NULL){
                    predecessor->right = cur;
                    cur = cur->left;
                }else{
                    if(pre != NULL && cur->val < pre->val){
                        if(s1 == NULL) s1 = pre;
                        s2 = cur;
                    }
                    pre = cur;
                    predecessor->right = NULL;
                    cur = cur->right;
                }
            }else{ 
                if(pre != NULL && cur->val < pre->val){
                    if(s1 == NULL) s1 = pre;
                    s2 = cur;
                }
                pre = cur;
                cur = cur->right;
            }
        }
        // 进行交换
        int t = s1->val;
        s1->val = s2->val;
        s2->val = t;
        return;
    }
};

2.1.1 该题的设计思路

使用Morris遍历
检查当前结点的左孩子:
如果当前结点的左孩子为空,说明要不没有前驱,要不前驱是它的父结点,所以进行检查,然后进入右孩子。
如果当前结点的左孩子不为空,说明左子树里肯定有它的前驱,那就找到这个前驱
如果前驱结点的右孩子是空,说明还没检查过左子树,那么把前驱结点的右孩子指向当前结点,然后进入当前结点的左孩子。
如果当前结点的前驱结点其右孩子指向了它本身,说明左子树已被检查过,就直接进行检查,然后把前驱结点的右孩子设置为空,恢复原树,再进入右孩子。

时间复杂度:时间复杂度为O(n)

空间复杂度:空间复杂度为O(1)

2.1.2 该题的伪代码

while(cur)
    if(左子树为空时,直接比较,然后进入右子树)
        pre = cur;
        cur = cur->right;
    end if;
    else//进入左子树
        TreeNode* preceesor = cur->left;
        while(preceesor->right && preceesor->right != cur)
            preceesor = preceesor->right;
        end while;
        // 前驱已经指向自己了,直接比较,然后进入右子树
        if(preceesor->right == cur)
            断开连接,恢复原树
            pre = cur;
            cur = cur->right;
        end if
        else // 前驱还没有指向自己,说明左边还没有遍历,将前驱的右指针指向自己,后进入前驱
            preceesor->right = cur;
            cur = cur->left;
        end else;
    end else;
end while;

2.1.3 运行结果

2.1.4分析该题目解题优势及难点。

优势:本题使用树的中序遍历,利用Morris遍历方法,实现降低空间复杂度的操作。
难点:中序遍历 + 双指针找逆序对
二叉搜索树的中序遍历结果可以等效于一个升序数组
如果原始的二叉搜索树为1, 2, 3, 4, 5
如果将其中2,4两个元素进行交换,变成1, 4, 3, 2, 5
那么我们可以使用双指针的方法,检查这个数组里的逆序对,将逆序对找出来就可以解决问题
观察数组
第一对逆序对4, 3,是索引小的那个是被交换元素
第二对逆序对3, 2,是索引大的那个是被交换元素
所以我们在遇到逆序对的时候,如果是第一次遇见,则存储索引小的那个,如果不是,则存储索引大的那个
(超级棒的一个方法)

2.2 题目及解题代码

题目:

解题代码:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> inorder, postorder;
    unordered_map<int, int> indexMap;
    
    TreeNode* builder(int mostLeft, int mostRight) { 
        
        if (mostLeft > mostRight) return nullptr;
        
        int curVal = postorder.back();
        postorder.pop_back();
        TreeNode* cur = new TreeNode(curVal);
        cur->right = builder(indexMap[curVal] + 1, mostRight);
        cur->left = builder(mostLeft, indexMap[curVal] - 1);
        return cur;
    }
    
    TreeNode* builderStack() {
        int rootVal = postorder.back();
        postorder.pop_back();
        TreeNode* root = new TreeNode(rootVal);
       
        stack<TreeNode*> travelStack;
        travelStack.push(root);
        while (postorder.size()) {
            // 
            TreeNode* tmp = nullptr;
            while (!travelStack.empty() && travelStack.top()->val == inorder.back()) {
                inorder.pop_back();
                tmp = travelStack.top();
                travelStack.pop();
            }
            TreeNode* cur = new TreeNode(postorder.back());
            postorder.pop_back();
            if (tmp == nullptr) {
                travelStack.top()->right = cur;
            } else {
                tmp->left = cur;
            }
            travelStack.push(cur);
        }
        return root;
    }
    
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        this->inorder = inorder;
        this->postorder = postorder;        
        int tSize = this->inorder.size();
        if (tSize == 0) return nullptr;
        return builderStack();
    }
};

2.2.1 该题的设计思路

递归法是自顶向下的,每次求取当前节点然后求取子节点
而子节点的求取依赖根节点传下来的最左和最右限制值,还有当前节点在中序的下标值
这样才能继续构建下面的左右子树        
中序的最左和最右分别是左右的最远叶子节点
从后序中弹出节点值(第一次一定是根节点,然后就可以构建左右子树了)
构建左右子树
先构建右子树(后序遍历的尾巴是先是右节点)
而中序遍历是左-中-右的结构,所以通过值索引到下标后就可以开始构建了
换句话说,中序遍历找到一个节点后就能知道这个值的左边一定是它的左子树的节点
而右边就是它的右子树节点
mostLeft和mostRight就是用来限制这一层左右子树的范围的(震惊,被震撼到了)
再去构建左子树(这时后续遍历的尾巴就是左节点了)
迭代法是自底向上的,每次迭代不要求从上面传下来的值
反而需要先解决子问题后才可以解决父问题
换句话说就是得先构建完最远的叶子,然后一步一步往上回溯,最后回到根节点
所以迭代法不需要保存mostLeft和mostRight节点,也不需要知道下标的位置
弹出最根节点值 并构建该节点
根节点入栈,迭代构建左右子树
迭代终止条件是后序遍历节点全部弹出,证明遍历完成
循环条件:栈不为空,且栈顶刚好是中序的最后一个节点
中序定义:一颗完整的树(左右孩子均有)最后一个节点是右节点,然后根节点,然后左节点
为什么是while不是if:while可以保证连续地弹出栈顶节点
如果栈顶的元素是中序的最后一个元素,那么证明已经构建到当前的位置了
换句话说就是右子树已经全部构建完毕了,那么接下来的节点一定是左子树
否则的话不会进入循环,tmp是空值,那么证明这个节点的右子树还没有构建完成
弹出后序的最后一个节点值,构建节点
后序节点弹出的值顺序是根-右-左的,tmp的取值在while循环内已经阐述过了
如果是空值,那么证明栈顶元素的右子树还没有构建完成,继续构建右子树
换句话说就是刚刚构建的cur节点一定是栈顶的右子叶
否则栈顶的右子树已经完成了,而刚刚构建的cur是栈顶的左子叶
将这个子叶压栈,循环检查

时间复杂度O(n)

空间复杂度O(n)

2.2.2 该题的伪代码

class Solution 
{
public:
    vector<int> inorder, postorder;
    unordered_map<int, int> indexMap;    
    TreeNode* builder(int mostLeft, int mostRight) {
        if (mostLeft > mostRight) return nullptr;
        从后序中弹出节点值
        postorder.pop_back();
        TreeNode* cur = new TreeNode(curVal);
        先构建右子树
        再去构建左子树
        return cur;
    }
    
    TreeNode* builderStack() {
        弹出最根节点值 并构建该节点
        TreeNode* root = new TreeNode(rootVal);
        根节点入栈,迭代构建左右子树
        while (后序遍历节点未全部弹出) 
            TreeNode* tmp = nullptr;
            while (栈不为空,且栈顶刚好是中序的最后一个节点)
                 一颗完整的树(左右孩子均有)最后一个节点是右节点,然后根节点,然后左节点
            end while; 
           弹出后序的最后一个节点值,构建节点 
            if (tmp是空值) 
                travelStack.top()->right = cur;
           end if;
            else 
                tmp->left = cur;
            end else;
            将这个子叶压栈,循环检查
        end while;
        return root;
    }    
};

2.2.3 运行结果

2.2.4分析该题目解题优势及难点。

同时使用迭代法和递归法,构建树。 最震撼的地方:mostLeft和mostRight就是用来限制这一层左右子树的范围的。
可以悄悄地借用此题解学习迭代法和递归建哈希表。
但是该题解感觉有些复杂(在理解上),看起来十分头痛(不写注释),佩服写出该题解的人(抓起来暴打)。

2.3 题目及解题代码

题目:


解题代码:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    bool isCompleteTree(TreeNode* root) {
        if(root == NULL){
            return true;
        }
        queue<TreeNode*> q;
        q.push(root);
        bool isNULL = false;//之后的节点是否出行NULL
        while(!q.empty()){
            TreeNode* node = q.front();
            q.pop();
            if(node != NULL){
                if(isNULL){
                    return false;
                }
                q.push(node->left);
                q.push(node->right);
            }
            else{
                isNULL = true;
            }
        }
        return true;
    }
};

2.3.1 该题的设计思路

如下图,图中箭头的顺序,就是层次遍历的顺序。将所有节点排成一列,包括缺失的null节点。null节点之后必须全部是null节点。否则,就不是完全二叉树。
例如:

节点排成一列后,1,2,3,4,5,6,null.是完全二叉树。

节点排成一列后, 1,2,3,4,5,null,7.null后有了节点7。不是完全二叉树。

时间复杂度:O(n)

空间复杂度:O(n)

2.3.2 该题的伪代码

class Solution {
public:
    bool isCompleteTree(TreeNode* root){
        queue<TreeNode*> q;
        q.push(root);
        bool类型为了判断之后的节点是否为NULL
        while(队非空)
            if(node != NULL)
                if(isNULL)
                    return false;
                end if;
                左右孩子入队;
            end if;
            else
                isNULL = true;
            end else;
        end while;
        return true;
    }
};

2.3.3 运行结果

2.3.4分析该题目解题优势及难点。

将二叉树转化为队列形式,从二叉树的深度的性质入手,来判断是否为二叉树。
难点在于isNULL的设定上,对于末尾NULL的处理。

2.4 题目及解题代码

题目:



解题代码:

class Solution {
public:
    int index = 0;      
    vector<int> result; 
    bool dfs(TreeNode* root, vector<int>& voyage){
        if(nullptr == root || index >= voyage.size())
            return true;
        if(root->val != voyage[index])
            return false;
        index++;
        if(nullptr != root->left && root->left->val != voyage[index]){
            result.push_back(root->val);
            return dfs(root->right, voyage) && dfs(root->left, voyage);
        }
        else{
            return dfs(root->left, voyage) && dfs(root->right, voyage);
        }
        
    }
    vector<int> flipMatchVoyage(TreeNode* root, vector<int>& voyage) {
        if(dfs(root, voyage))
            return result;
        return vector<int>{-1};
    }
};

2.4.1 该题的设计思路

先序遍历一棵树,若不符合,
则尝试交换左右子树,若还是不符合,返回-1;

时间复杂度:O(n)

空间复杂度:O(n)

2.4.2 该题的伪代码

class Solution {
public:
    定义int型下标index;
    vector<int> result; 
    bool dfs(TreeNode* root, vector<int>& voyage){// 返回是否可通过交换来满足预期
        if(nullptr == root || index >= voyage.size())
            return true;
        end if;
        if(当前结点不符合预期)
            return false;
        end if;
        index++;//不赋初值,为啥也可以???
        if(先序遍历结果与预期不符)
            记录这次交换
            返回交换后的遍历结果
        end if;
        else
            返回交换后的遍历结果
        end else;
        
    }
    vector<int> flipMatchVoyage(TreeNode* root, vector<int>& voyage) {
        // 若可,
        if(通过交换来满足预期))
        返回交换的结点值
        end if;
    }
};

2.4.3 运行结果

2.4.4分析该题目解题优势及难点。

首先可以肯定的是该题解比较简洁,其次呢,应用vector容器(值得学习)来实现返回节点值。
难点就是在于vector容器的使用。
(在?为什么大多数题解不写注释?是因为懒吗?还是不想让人知道你在写什么?)

posted @ 2020-04-12 20:36  1911-林威  阅读(308)  评论(0编辑  收藏  举报
复制代码