必备代码(三):二叉树的三种遍历(非递归写法)+层序遍历(递归写法)

目录

  • 理解递归
  • 非递归遍历
    • 前序遍历
    • 中序遍历
    • 后序遍历
  • 递归层序遍历

理解递归

程序的执行离不开方法的调用,程序执行的入口总是指向主方法。当一个程序开始运行,操作系统便为之创建进程映像,执行引擎扫描代码,一旦发现方法的调用,就会在栈内存中创建一个栈帧(入栈),位于栈顶的就是当前方法,方法执行完毕,这个栈帧就会出栈,其中涉及到的变量(这里指基本类型变量+引用变量本身的内存空间)都会随着栈帧的销毁而被释放。
递归方法的调用和普通方法的调用在执行上没有本质区别,递归方法中发生递归,也可以完全看作方法A在执行过程中调用了方法B,只有当方法B返回时,方法A才会返回。只不过递归方法中,方法A和方法B的签名完全一致,这使得用户会感到奇怪。你完全可以把它们当做不同的方法,甚至调用方法B时,完全可以认为它是一个已经实现好的方法。因为递归方法A和递归方法B虽然签名相同,但是传入的参数是不同的。

    private List<Integer> list = new ArrayList<Integer>();
    public List<Integer> preorderTraversal(TreeNode root) {
        if(root == null) return list;  一般作为调用返回的出口
        list.add(root.val);		根层次的逻辑
        preorderTraversal(root.left); 看作普通的方法调用,输入root.left,按照前序打印出以root.left为根的遍历结果
        preorderTraversal(root.right);
        return list;  根层次的退出出口
    }

以先序遍历的递归写法为例,当前要实现的功能是:按照先序打印以root为根的树。这个问题可以拆分成若干个子问题:【1】先打印当前节点【2】按照先序打印以root.left为根的树【3】按照先序打印以root.right为根的树。完成以上三步直接返回即可。当然了,打印前如果判断出当前root是一个空节点则直接返回。

因此,写递归题的关键:将问题看作若干个子问题,将给定的待实现的递归函数看作已经被实现完毕的函数——看成一个功能明确的API,我们要做的,就是利用这个API去将“根层次”的逻辑去实现。同时,递归的核心就是把握好退出的时机。一般将API的出口写在最上方。而将“根层次”的出口写在最下方。

非递归遍历

递归的调用是基于系统栈实现的,而迭代通常配合着某些数据结构(一般是栈),去模拟函数调用的过程。因为递归的本质就是将同一个函数,改变参数,然后执行“根层次”的逻辑罢了。如果在迭代的过程中模拟这个过程,改变参数,然后循环中不断执行“根逻辑”,不也达成递归的效果了吗。

前序遍历

题目地址

    public List<Integer> preorderTraversal(TreeNode root) {
        LinkedList<TreeNode> stack = new LinkedList<>();
        LinkedList<Integer> res = new LinkedList<>();
        while(!stack.isEmpty()||root!=null){
            while(root!=null){
                res.add(root.val);
                stack.push(root);
                root=root.left;
            }
            root=(stack.pop()).right;
        }
        return res;
    }

在遍历的过程中,入参root不断被改变,相当于模拟左分支递归。而当root==null时左分支递归返回,其中stack().pop()其实就是左分支最后一层递归退出的逻辑。而root=(stack.pop()).right就是模拟右分支递归,而当外循环进入下一轮迭代时,就相当于右分支递归开始执行。
迭代通过进栈和出栈,来模拟参数的改变以及递归层次的变化。

root=(stack.pop()).right模拟的是递归函数的调用,当迭代进入下一轮时才相当于进入下一“层次”

中序遍历

原题地址

    public List<Integer> inorderTraversal(TreeNode root) {
        LinkedList<TreeNode> stack = new LinkedList<>();
        LinkedList<Integer> res = new LinkedList<>();
        while(!stack.isEmpty()||root!=null){
            while(root!=null){
                stack.push(root);
                root=root.left; 左递归调用
            }
            root = stack.pop(); 递归返回
            res.add(root.val);  根逻辑
            root = root.right;  右递归调用
        }
        return res;
    }

BST(二叉搜索树)的中序遍历是升序的,元素的添加总是在左递归返回之后进行的,可以保证添加的元素是当前序列最小的元素。

后序遍历

后序遍历

    public List<Integer> postorderTraversal(TreeNode root) {
        LinkedList<TreeNode> stack = new LinkedList<>();
        LinkedList<Integer> res = new LinkedList<>();
        while(!stack.isEmpty()||root!=null){
            while(root!=null){
                stack.push(root);
                res.addFirst(root.val);
                root=root.right;
            }
            root=(stack.pop()).left;
        }
        return res;
    }

后序遍历可以在前序遍历的基础上改写,相当于先进行右递归调用,每次打印的元素存放第一个位置(或者所有打印完进行一个反转),然后进行左递归调用。

递归层序遍历

层序遍历

    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new  LinkedList<>();
        if(root==null)return res;
        LinkedList<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        while(!queue.isEmpty()){
            LinkedList<Integer> list = new LinkedList<>();
            int size = queue.size(); 这里的易错点是:直接使用queue.size()遍历
            for(int i=0;i<size;i++){
                TreeNode node =queue.remove(0);
                if(node.left!=null)queue.add(node.left);
                if(node.right!=null)queue.add(node.right);
                list.add(node.val);
            }
            res.add(list);
        }
        return res;
    }

非递归版本的队列BFS估计大家都很熟悉了,但是面试总是希望你把DFS也写出来。BFS的思想就是每次只把第一层入队,当第一层节点处理完出队时,就把与之关联的第二层节点放入队中。其中两个层次通过第一层的size去区分。(这里的易错点就是:size应该是计算当前层保存的,而不是使用queue.size(),后者是动态获取的,是变化的!!!)

如果说BFS是拿刀子一层层砍,那DFS就是拿剑一次次往里面插。

    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        levelOrder(root,0,res);
        return res;
    }
    public void levelOrder(TreeNode root,int curDepth,List<List<Integer>> res) {
    if(root == null) return;
    //初始化list
    if(curDepth==res.size())res.add(new ArrayList<>());
    res.get(curDepth).add(root.val);
    levelOrder(root.left,curDepth+1,res);
    levelOrder(root.right,curDepth+1,res);
    }

可以看出来,每一层要添加的值和当前的层数直接相关,虽然每次只往一个地方钻,但是哪一层的元素要放入哪个位置,这些都是和容器对应好的。
容器一开始是空的,因此第一次存放之前都有对容器当前位置进行初始化,然后就是从容器中取出当前位置对应的集合,依次追加即可

你可能感兴趣的:(数据结构与算法,java,队列,数据结构,算法,树结构)