算法打卡第十七天 BM27 按之字形顺序打印二叉树、BM28 二叉树的最大深度、BM29 二叉树中和为某一值的路径(一)和BM30 二叉搜索树与双向链表

        今天是算法打卡第十七条,今天写了三道题,昨天有一道题没写博客,加到今天的里面,这两天的算法题对递归的使用较为频繁,今天写的时候发现对递归还是理解不够深,不知道怎么写逻辑,不怎么理解递归压栈等,需要补一下。

     问题1:BM27 按之字形顺序打印二叉树

      描述:

      解题方法:

        1、队列

        我们做二叉树层次遍历时,每一层是从左到右进行遍历的,因此后序遍历可以使用队列,将每一层的节点入队列,每次队列都是当前层次的节点个数,然后将节点个数记录,因为遍历每层时,会将本层元素出队列,每次入队列的都是本层节点的下一层子节点,那么当本层节点遍历完后,队列就只剩下下一层的节点,此时队列的大小就是本层节点的个数。依次将本层节点遍历出来,再将其子节点入队列,直到队列为空。而按之字形顺序打印二叉树只是改变了遍历顺序,相当于奇数层从左到右遍历,偶数层从右到左遍历,我们可以使用层次遍历的方法,然后添加一个辅助数,记录当前遍历的层数,根据它的奇偶性,然后分别遍历

        具体实现

1)创建一个二维数组存储遍历结果
2)判断树是否为空,空则返回空二维数组,反之,则继续
3)创建一个队列存储节点,进行遍历
4)将根节点入队列,并设置一个辅助数记录当前层数
5)遍历本层节点,即依次将本层节点出队列,创建一个一维数组记录本层的元素值,并获取本层节点数,并将节点值存入一维数组
6)将结点值存入一维数组时,将根据辅助数的奇偶性存入,若是奇数,表示本层从左到右,若是偶数,表示本层从右到左
7)遍历完成后将本层的元素,即一维数组存入二维数组中,并增加层数依次循环,直到所有节点都为叶子节点,此时队列为空

         代码:

import java.util.*;
import java.util.ArrayList;
public class Solution {
    public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        //存储遍历结果
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        //对树进行判断
        if(pRoot == null){
            return res;
        }
        //辅助队列
        Queue<TreeNode> queue = new ArrayDeque<TreeNode>();
        //将根节点存入队列
        queue.add(pRoot);
        //辅助数
        int col = 1;
        //遍历树
        while(!queue.isEmpty()){
            //记录本层节点值
            ArrayList<Integer> list = new ArrayList<>();
            //本层节点个数
            int num = queue.size();
            //遍历本层节点
            for(int i = 0; i < num; i ++){
                //出队列的节点
                TreeNode tmp = queue.poll();
                //根据层数将节点值存入一维数组
                if(col % 2 == 1){
                //奇数层,子节点从左往右遍历,添加到一维数组尾部
                    list.add(tmp.val);
                }
                else{
                //偶数层,子节点从右往左遍历,添加到一维数组首部
                    list.add(0, tmp.val);
                }
                //存在左子节点,入队列
                if(tmp.left != null){
                    queue.add(tmp.left);
                }                    
                //存在右子节点,入队列
                if(tmp.right != null){
                    queue.add(tmp.right);
                }
            }
            //本层遍历完成,将一维数组存入二维数组中
            res.add(list);
            //层数增加
            col++;
        }
        return res;
    }

}

        复杂度分析:

  • 时间复杂度:O(n),每个节点访问一次,因为reverse的时间复杂度为O(n),按每层元素reverse也相当于O(n)
  • 空间复杂度:O(n),队列的空间最长为O(n)

        2、双栈

        既然奇数层要从左到右遍历,偶数层从右到左遍历,那么我们可以在存储每一层的子节点时就确认其遍历顺序,奇数层就将其子节点从右到左存储,偶数层就从左到右存储(在第一次使用辅助空间时,我打算使用队列来完成,发现只使用一个队列是无法实现的,它只能添加到队首或者队尾,导致无法实现,添加到队首会影响本层节点遍历,添加到队尾会导致它只是本节点的子节点是从左到右(从右到左)的顺序,而相邻节点的子节点只能排在它的后面)

        利用两个栈遍历这棵二叉树,第一个栈s1从根节点开始记录第一层,然后依次遍历两个栈,遍历第一个栈时遇到的子节点依次加入第二个栈s2中,即是第二层,而遍历第二个栈s2的时候因为是先进后出,因此就是逆序的,再将第二个栈s2的子节点依次加入第一个栈s1中,于是原本的逆序在第一个栈s1中又变回了正序,如果反复交替直到两个栈都空为止。        

        具体做法:

  • step 1:首先判断二叉树是否为空,空树没有打印结果。
  • step 2:建立两个辅助栈,每次依次访问第一个栈s1与第二个栈s2,根节点先进入s1.
  • step 3:依据依次访问的次序,s1必定记录的是奇数层,访问节点后,将它的子节点(如果有)依据先左后右的顺序加入s2,这样s2在访问的时候根据栈的先进后出原理就是右节点先访问,正好是偶数层需要的从右到左访问次序。偶数层则正好相反,要将子节点(如果有)依据先右后左的顺序加入s1,这样在s1访问的时候根据栈的先进后出原理就是左节点先访问,正好是奇数层需要的从左到右访问次序。
  • step 4:每次访问完一层,即一个栈为空,则将一维数组加入二维数组中,并清空以便下一层用来记录。

         

import java.util.*;
public class Solution {
    public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        TreeNode head = pRoot;
        ArrayList<ArrayList<Integer> > res = new ArrayList<ArrayList<Integer>>();
        if(head == null)
            //如果是空,则直接返回空list
            return res; 
        Stack<TreeNode> s1 = new Stack<TreeNode>();
        Stack<TreeNode> s2 = new Stack<TreeNode>();
        //放入第一次
        s1.push(head); 
        while(!s1.isEmpty() || !s2.isEmpty()){ 
            ArrayList<Integer> temp = new ArrayList<Integer>();
            //遍历奇数层
            while(!s1.isEmpty()){ 
                TreeNode node = s1.pop();
                //记录奇数层
                temp.add(node.val); 
                //奇数层的子节点加入偶数层
                if(node.left != null)  
                    s2.push(node.left);
                if(node.right != null) 
                    s2.push(node.right);
            }
            //数组不为空才添加
            if(temp.size() != 0)  
                res.add(new ArrayList<Integer>(temp));
            //清空本层数据
            temp.clear(); 
            //遍历偶数层
            while(!s2.isEmpty()){ 
                TreeNode node = s2.pop();
                //记录偶数层
                temp.add(node.val);  
                //偶数层的子节点加入奇数层
                if(node.right != null)  
                    s1.push(node.right);
                if(node.left != null) 
                    s1.push(node.left);
            }
            //数组不为空才添加
            if(temp.size() != 0) 
                res.add(new ArrayList<Integer>(temp));
            //清空本层数据
            temp.clear(); 
        }
        return res;
    }

}

        复杂度分析:

  • 时间复杂度:O(n),其中n为二叉树的节点数,遍历二叉树的每个节点
  • 空间复杂度:O(n),两个栈的空间最坏情况为n

     问题2:二叉树的最大深度

     描述:

      解题方法:

       方法一:递归

        最大深度是所有叶子节点的深度的最大值,深度是指树的根节点到任一叶子节点路径上节点的数量,因此从根节点每次往下一层深度就会加1。因此二叉树的深度就等于根节点这个1层加上左子树和右子树深度的最大值,即rootdepth=max(leftdepth,rightdepth)+1root_{depth} = max(left_{depth}, right_{depth})+1rootdepth​=max(leftdepth​,rightdepth​)+1。而每个子树我们都可以看成一个根节点,继续用上述方法求的深度,于是我们可以对这个问题划为子问题,利用递归来解决:

  • 终止条件: 当进入叶子节点后,再进入子节点,即为空,没有深度可言,返回0.
  • 返回值: 每一级按照上述公式,返回两边子树深度的最大值加上本级的深度,即加1.
  • 本级任务: 每一级的任务就是进入左右子树,求左右子树的深度。

        具体做法:

  • step 1:对于每个节点,若是不为空才能累计一次深度,若是为空,返回深度为0.
  • step 2:递归分别计算左子树与右子树的深度。
  • step 3:当前深度为两个子树深度较大值再加1。

        代码:

import java.util.*;
public class Solution {
    public int maxDepth (TreeNode root) {
        //空节点没有深度
        if(root == null) 
            return 0;
        //返回子树深度+1
        return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; 
    }
}

        复杂度分析:

  • 时间复杂度:O(n),其中n为二叉树的节点数,遍历整棵二叉树
  • 空间复杂度:O(n),最坏情况下,二叉树化为链表,递归栈深度最大为n

         方法二:层次遍历

        既然是统计二叉树的最大深度,除了根据路径到达从根节点到达最远的叶子节点以外,我们还可以分层统计。对于一棵二叉树而言,必然是一层一层的,那一层就是一个深度,有的层可能会很多节点,有的层如根节点或者最远的叶子节点,只有一个节点,但是不管多少个节点,它们都是一层。因此我们可以使用层次遍历,二叉树的层次遍历就是从上到下按层遍历,每层从左到右,我们只要每层统计层数即是深度。

        具体做法:

  • step 1:既然是层次遍历,我们遍历完一层要怎么进入下一层,可以用队列记录这一层中节点的子节点。队列类似栈,只不过是一个先进先出的数据结构,可以理解为我们平时的食堂打饭的排队。因为每层都是按照从左到右开始访问的,那自然记录的子节点也是从左到右,那我们从队列出来的时候也是从左到右,完美契合。
  • step 2:在刚刚进入某一层的时候,队列中的元素个数就是当前层的节点数。比如第一层,根节点先入队,队列中只有一个节点,对应第一层只有一个节点,第一层访问结束后,它的子节点刚好都加入了队列,此时队列中的元素个数就是下一层的节点数。因此遍历的时候,每层开始统计该层个数,然后遍历相应节点数,精准进入下一层。
  • step 3:遍历完一层就可以节点深度就可以加1,直到遍历结束,即可得到最大深度。

        代码:

import java.util.*;
public class Solution {
    public int maxDepth (TreeNode root) {
        //空节点没有深度
        if(root == null) 
            return 0;
        //队列维护层次后续节点
        Queue<TreeNode> q = new LinkedList<TreeNode>(); 
        //根入队
        q.offer(root); 
        //记录深度
        int res = 0; 
        //层次遍历
        while(!q.isEmpty()){ 
            //记录当前层有多少节点
            int n = q.size(); 
            //遍历完这一层,再进入下一层
            for(int i = 0; i < n; i++){ 
                TreeNode node = q.poll();
                //添加下一层的左右节点
                if(node.left != null) 
                    q.offer(node.left);
                if(node.right != null)
                    q.offer(node.right);
            }
            //深度加1
            res++; 
        }
        return res; 
    }
}

         复杂度分析:

  • 时间复杂度:O(n),其中nnn为二叉树的节点数,遍历整棵二叉树
  • 空间复杂度:O(n),辅助队列的空间最坏为n

     问题三:二叉树中和为某一值的路径(一)

     描述:

      解题方法:

       方法一:递归        

        既然是检查从根到叶子有没有一条等于目标值的路径,那肯定需要从根节点遍历到叶子,我们可以在根节点每次往下一层的时候,将sum减去节点值,最后检查是否完整等于0. 而遍历的方法我们可以选取二叉树常用的递归前序遍历,因为每次进入一个子节点,更新sum值以后,相当于对子树查找有没有等于新目标值的路径,因此这就是子问题,递归的三段式为:

  • 终止条件: 每当遇到节点为空,意味着过了叶子节点,返回。每当检查到某个节点没有子节点,它就是叶子节点,此时sum减去叶子节点值刚好为0,说明找到了路径。
  • 返回值: 将子问题中是否有符合新目标值的路径层层往上返回。
  • 本级任务: 每一级需要检查是否到了叶子节点,如果没有则递归地进入子节点,同时更新sum值减掉本层的节点值。

        具体做法:

  • step 1:每次检查遍历到的节点是否为空节点,空节点就没有路径。
  • step 2:再检查遍历到是否为叶子节点,且当前sum值等于节点值,说明可以刚好找到。
  • step 3:检查左右子节点是否可以有完成路径的,如果任意一条路径可以都返回true,因此这里选用两个子节点递归的或。

        代码:

import java.util.*;
public class Solution {
    public boolean hasPathSum (TreeNode root, int sum) {
        //空节点找不到路径
        if(root == null) 
            return false;
        //叶子节点,且路径和为sum
        if(root.left == null && root.right == null && sum - root.val == 0) 
            return true;
        //递归进入子节点
        return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val); 
    }
}

        复杂度分析:

  • 时间复杂度:O(n),其中n为二叉树所有节点,前序遍历二叉树所有节点
  • 空间复杂度:O(n),最坏情况二叉树化为链表,递归栈空间最大为n

        方法二:非递归

        在二叉树中能够用递归解决的问题,很多时候我们也可以用非递归来解决。这里遍历过程也可以使用栈辅助,进行dfs(深度优先搜索)遍历,检查往下的路径中是否有等于sum的路径和。

        注意,这里仅是dfs,而不是前序遍历,左右节点的顺序没有关系,因为每次往下都是单独添加某个节点的值相加然后继续往下,因此左右节点谁先遍历不管用。

        具体做法:

  • step 1:首先检查空节点,空树没有路径。
  • step 2:使用两个栈同步遍历,一个栈记录节点,辅助深度优先搜索,另一个栈跟随记录到该节点为止的路径和(C++中可以在一个栈中嵌套pair实现)。根节点及根节点值先进栈。
  • step 3:遍历的时候每次弹出两个栈中的内容,判断是否是叶子节点且路径和是否等于目标值。
  • step 4:没有到叶子节点就将左右子节点(如果有)加入栈中,并跟随加入路径和。
  • step 5:如果遍历结束也没有找到路径和,则该二叉树中没有。

        代码:

import java.util.*;
public class Solution {
    public boolean hasPathSum (TreeNode root, int sum) {
        //空节点找不到路径
        if(root == null) 
            return false;
        //栈辅助深度优先遍历
        Stack<TreeNode> s1 = new Stack<TreeNode>(); 
        //跟随s1记录到相应节点为止的路径和
        Stack<Integer> s2 = new Stack<Integer>(); 
        s1.push(root);
        s2.push(root.val);
        while(!s1.isEmpty()){
            //弹出相应节点
            TreeNode temp = s1.pop(); 
            //弹出到该点为止的当前路径和
            int cur_sum = s2.pop(); 
            //叶子节点且当前路径和等于sum
            if(temp.left == null && temp.right == null && cur_sum == sum)
                return true;
            //左节点及路径和入栈
            if(temp.left != null){ 
                s1.push(temp.left);
                s2.push(cur_sum + temp.left.val);
            }
            //右节点及路径和入栈
            if(temp.right != null){ 
                s1.push(temp.right);
                s2.push(cur_sum + temp.right.val);
            }
        }
        return false;
    }
}

        复杂度分析:

  • 时间复杂度:O(n),其中n为二叉树所有节点,遍历二叉树所有节点
  • 空间复杂度:O(n),最坏情况二叉树化为链表,递归栈空间最大为n

      问题4:二叉搜索树与双向链表

      描述:

     解题方法:

        前提知识1: 二叉搜索树

        二叉搜索树是一种特殊的二叉树,它的每个节点值大于它的左子节点,且大于全部左子树的节点值,小于它右子节点,且小于全部右子树的节点值。因此二叉搜索树一定程度上算是一种排序结构。

        方法一:递归中序遍历

        二叉搜索树最左端的元素一定最小,最右端的元素一定最大,符合“左中右”的特性,因此二叉搜索树的中序遍历就是一个递增序列,我们只要对它中序遍历就可以组装称为递增双向链表。

        具体做法:

  • step 1:创建两个指针,一个指向题目中要求的链表头(head),一个指向当前遍历的前一节点(pre)。
  • step 2:首先递归到最左,初始化head与pre。
  • step 3:然后处理中间根节点,依次连接pre与当前节点,连接后更新pre为当前节点。
  • step 4:最后递归进入右子树,继续处理。
  • step 5:递归出口即是节点为空则返回。

        代码:

public class Solution {
    //返回的第一个指针,即为最小值,先定为null
    public TreeNode head = null;  
    //中序遍历当前值的上一位,初值为最小值,先定为null
    public TreeNode pre = null;   
    public TreeNode Convert(TreeNode pRootOfTree) {
        if(pRootOfTree == null)
            //中序递归,叶子为空则返回
            return null;     
        //首先递归到最左最小值  
        Convert(pRootOfTree.left); 
        //找到最小值,初始化head与pre
        if(pre == null){       
            head = pRootOfTree;
            pre = pRootOfTree;
        }
        //当前节点与上一节点建立连接,将pre设置为当前值
        else{       
            pre.right = pRootOfTree;
            pRootOfTree.left = pre;
            pre = pRootOfTree;
        }
        Convert(pRootOfTree.right);
        return head;
    }
}

        复杂度分析

  • 时间复杂度:O(n),其中n为二叉树节点数,中序遍历所有节点
  • 空间复杂度:O(n),递归栈所需要的最大空间

        方法二:非递归中序遍历

        二叉树中序遍历除了递归方法,我们还可以尝试非递归解法,与常规的非递归中序遍历几乎相同,还是借助栈来辅助,只是增加了连接节点。

        具体做法:

  • step 1:创建两个指针,一个指向题目中要求的链表头(head),一个指向当前遍历的前一节点(pre),创建一个布尔型变量,标记是否是第一次到最左,因为第一次到最左就是链表头。
  • step 2:判断空树不能连接。
  • step 3:初始化一个栈辅助中序遍历。
  • step 4:依次将父节点加入栈中,直接进入二叉树最左端。
  • step 5:第一次进入最左,初始化head与pre,然后进入它的根节点开始连接。
  • step 6:最后将右子树加入栈中,栈中依次就弹出“左中右”的节点顺序,直到栈为空。

         代码:

import java.util.*;
public class Solution {
    public TreeNode Convert(TreeNode pRootOfTree) {
        if (pRootOfTree == null)
            return null;
        //设置栈用于遍历
        Stack<TreeNode> s = new Stack<TreeNode>(); 
        TreeNode head = null;
        TreeNode pre = null;
        //确认第一个遍历到最左,即为首位
        boolean isFirst = true; 
        while(pRootOfTree != null || !s.isEmpty()){
            //直到没有左节点
            while(pRootOfTree != null){  
                s.push(pRootOfTree);
                pRootOfTree = pRootOfTree.left;
            }
            pRootOfTree = s.pop();
            //最左元素即表头
            if(isFirst){  
                head = pRootOfTree;
                pre = head;
                isFirst = false;
            //当前节点与上一节点建立连接,将pre设置为当前值
            }else{  
                pre.right = pRootOfTree;
                pRootOfTree.left = pre;
                pre = pRootOfTree;
            }
            pRootOfTree = pRootOfTree.right;
        }
        return head;
    }
}

        复杂度分析:

  • 时间复杂度:O(n),其中n为二叉树节点数,中序遍历二叉树所有节点
  • 空间复杂度:O(n),栈s最大空间为为O(n)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值