Morris序遍历二叉树

本文章纯属是根据最近所学记录的笔记,如果相关的资料或者文档有侵权,麻烦请联系我一下。当然,如果文章中有什么不对的地方,或者有更好的方法,请不吝赐教。

Morris序遍历二叉树

1、Morris序的过程

Morris序

参考左神视屏:(从14:34开始的)

链接

2、代码

void Mid_Morris(TreeNode* root) {

        TreeNode* Cur = root;//当前的节点
        TreeNode* Right_Most = nullptr;//左树最右的节点

        //此树是空的二叉树
        if (root == nullptr) {

            return;
        }
        //循环结束的标志:就是找到Cur是nullptr。要是还有节点的话,会在前面就把右节点改掉。指向前面的节点
        while (Cur) {
            //Morris序打印
            //cout << Cur->val << '\t';

            //要找左树的最右节点,得先找到左树
            Right_Most = Cur->left;

            //说明此时是有左子树
            if (Right_Most != nullptr) {
                //找到最右节点
                while ((Right_Most->right != nullptr) && (Right_Most->right != Cur))                {
                    Right_Most = Right_Most->right;
                }
                //情况1:找到最右节点退出来的:需要将指针指向当前的Cur
                if (Right_Most->right == nullptr)
                {
                    Right_Most->right = Cur;
                }
                //情况2:找到了Cur,说明已经改过一次节点了,需要将节点重新置为nullptr,且将当前的Cur = Cur->right;并使用continue结束此次循环。这样就不会执行下面那句Cur = Cur->left。
                else if (Right_Most->right == Cur)//第二次来到节点
                {
                    Right_Most->right = nullptr;
                    Cur = Cur->right;
                    continue;
                }
                Cur = Cur->left;
            }
            //无左树,直接找右树
            else {
                Cur = Cur->right;
            }
                
        }
    }

3、Morris遍历二叉树

3.1 中序遍历

Morris算法中序遍历二叉树:有左树的,在Morris序中都会出现两遍,而,中序遍历的的关键就是,出现两次的,打印第二次。只出现一次的,就直接打印

    void Mid_Morris(TreeNode* root) {

        TreeNode* Cur = root;//当前的节点
        TreeNode* Right_Most = nullptr;//左树最右的节点

        //此树是空的二叉树
        if (root == nullptr) {

            return;
        }
        //循环结束的标志:就是找到Cur是nullptr。要是还有节点的话,会在前面就把右节点改掉。指向前面的节点
        while (Cur) {
            //Morris序打印
            //cout << Cur->val << '\t';

            //要找左树的最右节点,得先找到左树
            Right_Most = Cur->left;

            //说明此时是有左子树
            if (Right_Most != nullptr) {
                //找到最右节点
                while ((Right_Most->right != nullptr) && (Right_Most->right != Cur)) {
                    Right_Most = Right_Most->right;
                }
                //情况1:找到最右节点退出来的:需要将指针指向当前的Cur
                if (Right_Most->right == nullptr)
                {
                    Right_Most->right = Cur;
                }
                //情况2:找到了Cur,说明已经改过一次节点了,需要将节点重新置为nullptr,且将当前的Cur = Cur->right;并使用continue结束此次循环。这样就不会执行下面那句Cur = Cur->left。
                else if (Right_Most->right == Cur)//第二次来到节点
                {
                    cout << Cur->val << '\t';
                    Right_Most->right = nullptr;
                    Cur = Cur->right;
                    continue;
                }
                Cur = Cur->left;
            }
            //无左树,直接找右树
            else {

                cout << Cur->val << '\t';
                Cur = Cur->right;
                
            }
                
        }
    }

3.2 前序遍历

Morris算法前序遍历二叉树:有左树的,在Morris序中都会出现两遍,而,前序遍历的的关键就是,出现两次的,出现第一次就直接打印

void Pre_Morris(TreeNode* root) { 
	TreeNode* Cur = root;//当前的节点
   TreeNode* Right_Most = nullptr;//左树最右的节点

   //此树是空的二叉树
   if (root == nullptr) {

       return;
   }
   //循环结束的标志:就是找到Cur是nullptr。要是还有节点的话,会在前面就把右节点改掉。指向前面的节点
   while (Cur) {
       //Morris序打印
       //cout << Cur->val << '\t';

       //要找左树的最右节点,得先找到左树
       Right_Most = Cur->left;

       //说明此时是有左子树
       if (Right_Most != nullptr) {
           //找到最右节点。注意注意!!!要先判断Right_Most是否为NULL,才能访问它的左节点和右节点
           while ((Right_Most->right != nullptr) && (Right_Most->right != Cur)) {
               Right_Most = Right_Most->right;
           }
           //情况1:找到最右节点退出来的:需要将指针指向当前的Cur
           if (Right_Most->right == nullptr)
           {
               cout << Cur->val << '\t';
               Right_Most->right = Cur;
           }
           //情况2:找到了Cur,说明已经改过一次节点了,需要将节点重新置为nullptr,且将当前的Cur = Cur->right;\
           并使用continue结束此次循环。这样就不会执行下面那句Cur = Cur->left。
           else if (Right_Most->right == Cur)//第二次来到节点
           {
               Right_Most->right = nullptr;
               Cur = Cur->right;
               continue;
           }
           Cur = Cur->left;
       }
       //无左树,直接找右树
       else {

           cout << Cur->val << '\t';
           Cur = Cur->right;

       }

   }
}

3.3 后序遍历

翻转链表
返回值为啥是指针?因为,需要将root指向头结点,要不然在内存中是无效的。因为没有头结点的话,翻转完了找不到这个链表到底在哪里。当然,如果知道最后那个元素的地址可以不用,即翻转完之后的头结点。

TreeNode* Reverse_List(TreeNode* root) {
	//记录下下个节点地址
   TreeNode* pre = root;
   //记录链表下一个节点地址
   TreeNode* cur = nullptr;
   //空链表
   if (root == nullptr)
   {
       return NULL;
   }

   //cur抓取到下一个节点的地址
   cur = root->right;

   //此时可以将pre->right置nullptr,第一个需要置nullptr。
   root->right = nullptr;
   while (cur != nullptr) {

       pre = cur->right;
       cur->right = root;
       root = cur;
       cur = pre;
   }
   return root;
}

Morris算法后序遍历二叉树:有左树的,在Morris序中都会出现两遍,而,
后序遍历的关键:在第二次来到时,反向打印右边界。注意不是打印当前节点。是打印边界,还是反向打印
最后就是将整棵树的右边界打印一下。

void Last_Morris(TreeNode* root) {

       TreeNode* Cur = root;//当前的节点
       TreeNode* Right_Most = nullptr;//左树最右的节点
       TreeNode* New = root;//当前的节点
       TreeNode* New1 = root;//当前的节点

       //此树是空的二叉树
       if (root == nullptr) {

           return;
       }
       //循环结束的标志:就是找到Cur是nullptr。要是还有节点的话,会在前面就把右节点改掉。指向前面的节点
       while (Cur) {
           //Morris序打印
           //cout << Cur->val << '\t';

           //要找左树的最右节点,得先找到左树
           Right_Most = Cur->left;

           //说明此时是有左子树
           if (Right_Most != nullptr) {
               //找到最右节点
               while ((Right_Most->right != nullptr) && (Right_Most->right != Cur)) {
                   Right_Most = Right_Most->right;
               }
               //情况1:找到最右节点退出来的:需要将指针指向当前的Cur
               if (Right_Most->right == nullptr)
               {
                   Right_Most->right = Cur;
               }
               //情况2:找到了Cur,说明已经改过一次节点了,需要将节点重新置为nullptr,且将当前的Cur = Cur->right;\
               并使用continue结束此次循环。这样就不会执行下面那句Cur = Cur->left。
               else if (Right_Most->right == Cur)//第二次来到节点
               {
                   Right_Most->right = nullptr;
                   //此时将链表翻转一下
                   New = Reverse_List(Cur->left);
                   New1 = New;//保存翻转完之后的链表
                   while(New)
                   {
                       cout << New->val << '\t';
                       New = New->right;
                   }
                   //翻转回来
                   New1 = Reverse_List(New1);
                   Cur = Cur->right;
                   continue;
               }
               Cur = Cur->left;
           }
           //无左树,直接找右树
           else {

               Cur = Cur->right;

           }

       }
       //最后还要单独打印一下整棵树的右边界
       New = Reverse_List(root);
       New1 = New;
       while (New)
       {
           cout << New->val << '\t';
           New = New->right;
       }
       //翻转回来
       New1 = Reverse_List(New1);
}

二叉树遍历方法

1、递归

//中序遍历

void Midorder(TreeNode* root)
{
    //遍历到最后的节点
    if (NULL == root)
        return;
    Midorder(root->left);
    cout<< root->val;
    Midorder(root->right);
}

//前序遍历

void Preorder(TreeNode* root)
{
    if (NULL == root)
        return;
    cout << root->val<<'\t';
    Preorder(root->left);
    Preorder(root->right);
}

//后序遍历

void Lastorder(TreeNode* root)
{
    if (NULL == root)
        return;
    Lastorder(root->left);
    Lastorder(root->right);
    cout << root->val << '\t';
}

2、使用stack的方式

中序遍历

vector<int> inorderTraversal(TreeNode* root) {
        
        int flag = -1;//记录当前stack里面有多少元素。   其实在C++里面完全没有这个必要去维护这样一个变量。可以使用arr.empty()判断里面是否为空
        TreeNode* left = root;
        TreeNode* pMove = root;
        TreeNode* stack[10] = { root };//自己手动创建的一个stack空间。但是有stack<TreeNode*> stk//自动创建一个stack空间
        vector<int> arr = {};
        if (root == NULL)
            return arr;
        //找到最左边的数
        do{
            while (left != NULL)//用的是left不是left->left,因为,如果刚开始就是NULL的话。这样的访问就是无效的!!!
            {
                stack[++flag] = left;
                left = left->left;
            }
            pMove = stack[flag];
            cout << pMove->val<<'\t';
            arr.push_back(pMove->val);
            flag--;
            left = pMove->right;
            if (left != NULL)//和上面那个while语句重复了。使用的do...while加上while。表示两个都要满足。但是如果只用一个while的话。while((left != NULL)||(flag ! =-1))就可以了。
            {
                stack[++flag] = left;
                left = left->left;
            }
        } while (flag != -1);
        return arr;
}

前序遍历

vector<int> preorderTraversal(TreeNode* root) {
        int flag = -1;//记录当前stack里面有多少元素。 
        TreeNode* left = root;
        TreeNode* pMove = root;
        TreeNode* stack[10] = { root };//自己手动创建的一个stack空间。但是有stack<TreeNode*> stk//自动创建一个stack空间
        vector<int> arr = {};
        if (root == NULL)
            return arr;
        //找到最左边的数
        do {
            while (left != NULL)//用的是left不是left->left,因为,如果刚开始就是NULL的话。这样的访问就是无效的!!!
            {
                stack[++flag] = left;
                cout << left->val << '\t';
                arr.push_back(left->val);
                left = left->left;
            }
            pMove = stack[flag];
            flag--;
            left = pMove->right;
            if (left != NULL)//和上面那个while语句重复了。使用的do...while加上while。表示两个都要满足。但是如果只用。一个while的话。while((left != NULL)||(flag ! =-1))就可以了。
            {
                stack[++flag] = left;
                cout << left->val << '\t';
                arr.push_back(left->val);
                left = left->left;
            }
        } while (flag != -1);
        return arr;
    }

后序遍历(自己写的)

后序遍历相较于前序和中序都稍微难一点,现接触到的主要的思路有两个:

可以使用stack<TreeNode*> stk,自动创建一个stack空间,但是不知道为啥要放在第一行才行,当然要包含头文件#include,也可以自己用数组来模拟一个栈空间。

  1. 使用两个栈

    • 思路:这个要参考左神的前序压栈方法:

      • 前序的压栈方法:先压头,再把头弹出来。压完右节点再压左节点。然后利用栈的后进先出,再弹出左节点,将左节点的右节点压入栈,再压左节点的左节点。以此循环下去。

        总结就是:先压头,再压右,最后压左。

      • 参考完前序的方法之后,可以将头右左的顺序换成::头左右的顺序。此时得到的结果可以发现是后序遍历的,反过来的顺序。此时就是需要再利用一个栈来接收反过来的顺序。最后在输出的时候反过来即可。

  2. 使用一个栈加上一个指针变量

    • 思路:打印的顺序是:左右头,因为是,压栈的过程,所以肯定是有往上走的,此时就可以用一个标志位来记录当前节点的右节点是否被打印,即变量的作用是跟踪被打印的节点。如果当前节点有右树且打印了,就直接打印当前节点。如果有右节点,但是没有被打印过,此时就要将右节点压入栈里面,注意不要被覆盖掉了。如果没有右节点直接打印。
    • 要注意的地方
      • 注意栈空间的覆盖,就是将右树压入到栈里面的时候,可能会把,当前节点给覆盖掉。因为有flag–的存在。此时可以选择将节点重新压栈。
      • 防止二次 压栈,因为left的值是随时可变的额,比如2后面跟着4和5,当5打印完了之后,left返回到2,此时又会将4压入栈里面。这样就循环了。是不对的。所以,可以用New==left来防止这个问题。left如果是New(用来跟踪打印的变量),说明会是二次压栈了,此时将left的值置成nullptr,不让它压栈。
vector<int> lastorderTraversal(TreeNode* root) {

        int flag = -1;//记录当前stack里面有多少元素。  
        TreeNode* left = root;
        TreeNode* pMove = root;
        TreeNode* stack[10] = { root };//自己手动创建的一个stack空间。
        vector<int> arr;
        TreeNode* New = nullptr;//记录右树是否被打印。跟踪打印的节点.

        if (root == NULL)
            return arr;

        do {
            //将左树全部压倒stack里面.这里要注意防止二次压栈。因为之后会弹出来节点,节点到时时候又会循环压栈。
            while (left != NULL)//用的是left不是left->left,因为,如果left就是NULL的话。这样的访问就是无效的!!!
            {
                stack[++flag] = left;
                left = left->left;
            }
            pMove = stack[flag];//弹出节点
            flag--;
            left = pMove->right;
            //当前节点有右树,且当前节点右树还没有被打印。此时节点就不能被弹出来,因为弹出来了之后就找不到了。但是因为flag--,此时如果还是 stack[flag] = pMove,当前节点就被覆盖了。所以需要将当前节点重新压栈!!!,只有这样不会被覆盖掉,如果没有flag--的话,可能不会存在覆盖的情况.如果是自动创建的stack,可以使用stack.top(),访问栈顶元素,不会被弹出来。弹出来有另外的元素。
            if ((left != NULL) && (New != pMove->right))
            {
                stack[++flag] = pMove;
                stack[++flag] = left;
                left = left->left;
            }
            //注意::pMove->right不可以用left替换,因为:上面那个if语句将left改了,如果改了的话,结果是不对的。
            //当前节点有右节点,但是已经被被打印过了。此时也要打印
            if ((pMove->right != nullptr) && (New == pMove->right))
            {
                cout << pMove->val << '\t';
                arr.push_back(pMove->val);
                New = pMove;
                //防止flag超出范围。导致访问违法。
                if (flag == -1)
                    break;
                //此时的stack[flag]就是当前节点的上一个节点。因为打印完了,所以要换成右树,否则可能会再重新压栈。
                left = stack[flag]->right;
            }
            //没有右节点,直接打印当前节点
            else if(pMove->right == nullptr)
            {
                cout << pMove->val << '\t';
                arr.push_back(pMove->val);
                New = pMove;
            }
            //当进行二次压缩时,发现打印过的节点,又在被压栈,此时就要不应该再继续执行最上面的while。
            //至于给left赋什么值好,只要不满足while的条件即可。因为while下面紧接着就是对left的重新赋值。
            if (New == left)
            {
                left = nullptr;
            }
        } while (flag != -1);
        return arr;
    }

后序遍历(左神代码)

  • 第一个if是将左树全部放到栈里面
  • stack.peek(),表示取栈顶元素。
  • 刚开始h的值可以随便取,只要不影响把左树的节点压到栈里即可。h是用来跟踪打印节点的。
  • 第二个if是,当前节点有右树,且右树没有被处理的。

使用一个栈

因为才学不是很久的c++,所以对vector的遍历不是很了解,此处记录一下新学的遍历vector的方法:

vector<int>::iterator it;//可以使用typedef把名字变短一点
vector<int> res;

res = solution.inorderTraversal(num1);
for (it = res.begin(); it != res.end(); ++it)
{
	cout << *it << '\t';
}

参考左神视屏:(非递归:24:23开始)

链接

注意:以上程序是依据自己的理解编写的,可能和视屏上的有些出入。但是,大致的思路是相似的。

3、Morris遍历

如最开始的阐述。
Morris遍历的有点:前面两个遍历的时间复杂度和空间复杂度都是O(n),但是Morris遍历的时间复杂度是O(n),空间复杂度是O(1).

二叉树的搭建

1、节点的构成及创建

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    //以下是构造函数,重载
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode* left, TreeNode* right) : val(x), left(left),right(right){}
};

//创建节点,或者说是开辟空间
TreeNode* AddNode(int val) {
    //C6011警告:node->left = NULL.因为没有判断node是不是空指针。
    TreeNode* node = (TreeNode*)malloc(sizeof(struct TreeNode));
    if (NULL == node)
    {
        cout << "空指针" << endl;
    }
    else
    {
        node->left = NULL;
        node->right = NULL;
        node->val = val;
    }
    return node;
}

2、创建数的结构

//创建tree
void Creat_Tree(TreeNode* root, TreeNode* left, TreeNode* right) {

    root->left = left;
    root->right = right;
}

3、搭建数

//创建节点先
 TreeNode* num1 = AddNode(1);
 TreeNode* num2 = AddNode(2);
 TreeNode* num3 = AddNode(3);
 TreeNode* num4 = AddNode(4);
 TreeNode* num5 = AddNode(5);
 TreeNode* num6 = AddNode(6);
 TreeNode* num7 = AddNode(7);
 TreeNode* num8 = AddNode(8);
 TreeNode* num9 = AddNode(9);

  Creat_Tree(num1, num2, num3);
  Creat_Tree(num2, num4, num5);
  Creat_Tree(num3, num6, num7);
  Creat_Tree(num4, num8, num9);

代码工程

94_BinaryTreeInorderTraversal

补充的知识点

1、传值和传址的区别

传值

传值是只把值传过去,函数那边是用,int来接收的。函数里面改变的是形参的值,改的并不是实参的值。因为函数会对实参进行一份拷贝,相当于在别的内存上开辟了一块空间,来存放拷贝的值。此时函数里面的改变,都是针对这块临时开辟的空间进行的。所以两者是互不干扰的。

传址

传址是把地址传过去,函数那边是用,指针接收的,当将指针解引用的话,就得到了值。此时函数里面改了,函数外面也会被改掉。因为操作的都是同一块地址。

2、数组传入函数的方式

当然,因为数组是一块连续的内存,要想把整个数组传过去,只能传地址过去。要不然怎么找到那块的内存。

数组名的几种含义

1、表示数组首元素的地址

基本上都是表示数组首元素的地址,只有以下两种情况不是。

2、sizeof(arr):整个数组的大小

表示数组里面有多少个元素

num = sizeof(arr)/sizeof(arr[0])
3、&arr:取的是整个数组的地址

虽然得到的结果和数组的首元素地址是一样的。但是注意,两者的步长是不一样的,即(&arr) + 1,跨越的是整个数组的长度,但是arr+1,得到的是第二个元素的地址。两者还是有点区别的。

函数接收数组的两种方式:

1、用指针变量接收。

这种方法是比较常见的,因为传过来的就是地址,所以用指针来接收很正常。

如果传过来的是一个指针数组,此时要拿二级指针来接收才对。因为数组是一个地址,而数组里面放的也是地址。

2、用int arr [ ]接收。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值