二叉树递归非递归遍历,层次遍历,反转,输出路径等常见操作详细总结

本文深入探讨二叉树的定义、初始化、遍历方法(包括前序、中序、后序及层次遍历),并讲解了二叉树的反转、深度、宽度、节点数等关键属性的计算方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

项目github地址:bitcarmanlee easy-algorithm-interview-and-practice
欢迎大家star,留言,一起学习进步

1.序言

在实际工作中,很多业务场景其实也需要一些比较巧妙的算法来支撑,并不是业务逻辑就全是复制粘贴或者说重复的代码写一百遍。越是随着算法研究的深入,越是发现数据结构的重要性。或者说,数据结构中就蕴藏着无数精妙算法的思想,很多算法的思想在数据结构中体现得非常突出。而作为一种非线性的数据结构,二叉树是非常重要非常常见也非常牛逼的一种数据结构,里面包含有递归,栈,队列,dfs等等很多常见的操作。因此,特意写一篇比较长的文章,记录一下二叉树里面的一些常见操作以及里面包含的思想。

2.二叉树节点定义

二叉树内部是由一个一个的Node组成的。因此我们一般定义一个Node类。这个没什么好说的,直接上代码。

    static class Node<T> {
        T data;
        Node left = null;
        Node right = null;

        Node(T data) {
            this.data = data;
        }
    }

3.初始化二叉树

这个也没太多好说的,直接上代码。

    public static Node init() {
        Node n1 = new Node(1);
        Node n2 = new Node(2);
        Node n3 = new Node(3);
        Node n4 = new Node(4);
        Node n5 = new Node(5);
        Node n6 = new Node(6);
        Node n7 = new Node(7);
        Node n8 = new Node(8);
        n1.left = n2;
        n1.right = n3;
        n2.left = n4;
        n2.right = n5;
        n3.left = n6;
        n3.right = n7;
        n4.left = n8;
        return n1;
    }

返回的是树的根节点。

4.前序中序后续遍历递归

这个其实也没有太多好说的。但还是稍微说两句。
前序遍历是按根节点->左子树->右子树的顺序访问二叉树。
中序遍历是按左子树->根节点->右子树的顺序访问二叉树。
后序遍历是按左子树->根节点->右子树的顺序访问二叉树。
还是直接上代码

    public static void preOrder(Node root) {
        if (root != null) {
            System.out.print(root.data + " ");
            preOrder(root.left);
            preOrder(root.right);
        }
    }
    public static void midOrder(Node root) {
        if (root != null) {
            midOrder(root.left);
            System.out.print(root.data + " ");
            midOrder(root.right);
        }
    }
    public static void postOrder(Node root) {
        if (root != null) {
            postOrder(root.left);
            postOrder(root.right);
            System.out.print(root.data + " ");
        }

    }

递归的方式简单明了,相信也不用过多解释。

5.前序中序非递归

前序中序遍历非递归的方式较为简单一些。先说说这两种情况。

    public static void preOrder2(Node root) {
        Stack<Node> stack = new Stack();
        if (root == null) {
            return;
        }
        stack.push(root);
        while (stack.size() > 0) {
            Node tmp = stack.pop();
            System.out.print(tmp.data + " ");
            if (tmp.right != null) {
                stack.push(tmp.right);
            }
            if (tmp.left != null) {
                stack.push(tmp.left);
            }
        }
    }

    public static void midOrder2(Node root) {
        Stack<Node> stack = new Stack<>();
        while (root != null || ! stack.empty()) {
            while(root != null) {
                stack.push(root);
                root = root.left;
            }
            if (! stack.empty()) {
                Node tmp = stack.pop();
                System.out.print(tmp.data + " ");
                root = tmp.right;
            }
        }
    }

核心思想是利用栈这种数据结构的特点来模拟递归。
在前序遍历中,因为每一次遇到新的节点就要访问,所以直接用一个栈模拟即可。每次pop出一个最后一个压入栈顶端的节点并且访问,然后将该节点的左右子节点压入栈。注意因为栈的特点是后进先出,而前序遍历是先访问左节点再访问右节点,所以压栈的时候应该是先压右子节点再压左子节点,这样保证pop的时候是先pop出的左子节点。
而在中序遍历中,可以想象是先沿着左子树一直遍历直到某个节点的左子树为空,在这个过程中所有的节点自然都被压入栈中。当某个节点的左子树为空时,将该节点pop访问,并开始遍历该节点的右子树。

6.后续遍历的非递归

后续遍历的非递归方式最复杂,先上代码。

    private void postorder2(Node node) {
        if (node == null) return;

        Node cur = node;
        Node pre = node;

        Stack<Node> stack = new Stack<>();
        // cur移动到左子树最下面
        while (cur != null) {
            stack.push(cur);
            cur = cur.left;
        }
        while (! stack.isEmpty()) {
            // pop栈顶
            cur = stack.pop();
            if (cur.right != null && cur.right != pre) {
                // 根节点再入栈
                stack.push(cur);
                // 处理右子树
                cur = cur.right;
                while (cur != null) {
                    // 到右子树最下面
                    stack.push(cur);
                    cur = cur.left;
                }
            } else {
                System.out.print(cur.data + " ");
                pre = cur;
            }
        }
    }

后续遍历要先将左右子树都访问完毕以后再访问根节点。所以步骤如下:
1.首先肯定是沿着左子树往下搜索,并且一直做压栈操作。
2.当达到左子树为空以后,此时栈顶元素出栈。如果该元素右子树不为空,并且该元素的右子树未被访问,则先将该元素再压栈回去(因为该元素此时未被访问)。
3.将该元素的右子树也压栈,并且沿右子树的的左子树继续搜索。
4.当右子树为空或者已经被访问,此时元素可以被访问,出栈,访问,并且将当前访问节点标记。

7.层次遍历

层次遍历是从根节点开始,沿着二叉树的宽度一层一层往下遍历。由这个特点不难看出,我们可以利用队列先进先出的特点来模拟层次遍历。
更具体地说,我们先让根节点入队列,并且访问根节点。然后让根节点的左节点入队,再让右节点入队。这样左结点就存储在队头的位置,将首先被访问。
访问完根节点以后,左节点出队,同时访问左节点。访问完毕让左节点的左右节点依次入队。此时队列中的头节点为根节点的右节点。
上述过程循环,一直到队列为空即可。

    public static void levelOrder(Node root) {
        if (root == null) {
            return;
        }
        Queue<Node> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            Node tmp = queue.poll();
            System.out.print(tmp.data + " ");
            if (tmp.left != null) {
                queue.offer(tmp.left);
            }
            if (tmp.right != null) {
                queue.offer(tmp.right);
            }
        }
    }

8.二叉树反转递归与非递归

反转二叉树,因为Homebrew 的作者Max Howell去google面试手写不出来被fxxx off而天下闻名。二话不多说,直接上代码。

    public static Node invert(Node root) {
        if (root == null) {
            return null;
        }

        Node tmp = root.left;
        root.left = root.right;
        root.right = tmp;

        invert(root.left);
        invert(root.right);
        return root;
    }

递归的方式,是不是跟交换两个数很像有木有。
再看看非递归的方式。

    public static Node invert2(Node root) {
        if (root == null) {
            return null;
        }

        Deque<Node> deque = new LinkedList<>();
        deque.offer(root);
        while (! deque.isEmpty()) {
            Node parent = deque.poll();
            Node tmp = parent.left;
            parent.left = parent.right;
            parent.right = tmp;

            if (parent.left != null) {
                deque.offer(parent.left);
            }
            if (parent.right != null) {
                deque.offer(parent.right);
            }
        }
        return root;
    }

非递归的方式,则是用一个队列来模拟上述的交换过程,具体过程可以参考层次遍历的分析。

9.求树的深度

如果使用递归的方式求树的深度,那么先求左子树与右子树的深度。而递归的出口为节点为空,此时返回的树深度为0。

    public static int findDepth(Node root) {
        if (root == null) {
            return 0;
        }
        else {
            int leftDepth = findDepth(root.left);
            int rightDepth = findDepth(root.right);
            return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
        }
    }

对于非递归的方式,依旧可以参考层次遍历的思路。假设level为当前节点的层数,last为当前层最后一个节点。当处理完当前层的最后一个节点时level加1。设置变量cur记录当前层已经访问的节点的个数,当cur等于last时,本层访问结束。

    public static int findDepth2(Node root) {
        if (root == null) {
            return 0;
        }
        Node current;
        Deque<Node> deque = new LinkedList<>();
        deque.offer(root);
        int level = 0;
        int cur, last;
        while (! deque.isEmpty()) {
            cur = 0;
            last = deque.size();
            while(cur < last) {
                current = deque.poll();
                cur++;
                if (current.left != null) {
                    deque.offer(current.left);
                }
                if (current.right != null) {
                    deque.offer(current.right);
                }
            }
            level++;
        }

        return level;
    }

10. 求树的宽度

求树的宽度与求树的深度比较类似,具体逻辑可以参考代码。

    public static int findWidth(Node root) {
        if (root == null) {
            return 0;
        }
        Deque<Node> deque = new LinkedList<>();
        deque.offer(root);
        Node current;
        int maxwidth = 0;
        int cur, last;
        while (! deque.isEmpty()) {
            cur = 0;
            last = deque.size();
            while (cur < last) {
                maxwidth = maxwidth > last ? maxwidth : last;
                current = deque.poll();
                cur++;
                if (current.left != null) {
                    deque.offer(current.left);
                }
                if (current.right != null) {
                    deque.offer(current.right);
                }
            }
        }
        return maxwidth;
    }

11. 求树的节点数递归与非递归

递归的思路
1.如果二叉树为空树,节点为0。
2.如果二叉树不为空,则总节点个数=左子树节点个数+右子树节点个数+1,其中1为自身节点。


    public static int countNum(Node root) {
        if (root == null) {
            return 0;
        }

        int leftsum = countNum2(root.left);
        int rightsum = countNum2(root.right);
        return leftsum + rightsum + 1;
    }

非递归解法
如果用非递归的方式求解节点个数,思路就比较简单直接了。用任何一种方式遍历整棵树,只需要在遍历的时候加一个count计数即可。

    public static int countNum(Node root) {
        if (root == null) {
            return 0;
        }

        int count = 0;
        Stack<Node> stack = new Stack();
        stack.push(root);
        while (! stack.isEmpty()) {
            Node tmp = stack.pop();
            count += 1;

            if (tmp.right != null) {
                stack.push(tmp.right);
            }
            if (tmp.left != null) {
                stack.push(tmp.left);
            }
        }

        return count;
    }

12.求第k层节点数递归与非递归

递归思路:
1.如果二叉树不为空并且k为1,返回1.
2.如果二叉树不为空且k>1,返回左子树中k-1层的节点个数与右子树k-1层节点个数之和

    public static int countK(Node root, int k) {
        if (root == null || k < 1) {
            return 0;
        }

        if (k == 1) {
            return 1;
        }

        int leftCount = countK(root.left, k - 1);
        int rightCount = countK(root.right, k - 1);

        return leftCount + rightCount;
    }

非递归的思路:
既然是求k层节点数,很自然想到层次遍历的方式。在层次遍历中添加一个level的计数器,当level为k时,此时这一层的节点数即为所求。

    public static int countK2(Node root, int k) {
        if (root == null || k < 1) {
            return 0;
        }

        Deque<Node> queue = new LinkedList<>();
        queue.offer(root);
        int level = 1;
        int cur, last;
        while (! queue.isEmpty() && level < k) {
            cur = 0;
            last = queue.size();
            while (cur < last) {
                Node tmp = queue.poll();
                if (tmp.left != null) {
                    queue.offer(tmp.left);
                }
                if (tmp.right != null) {
                    queue.offer(tmp.right);
                }
                cur++;
            }
            level++;
        }
        return queue.size();
    }

13. 求叶子节点个数递归与非递归

与求节点个数解法很类似,唯一的区别是叶子节点的条件是左右子树均为null。

    public static int countLeafNum(Node root) {
        if (root == null) {
            return 0;
        }

        if (root.left == null && root.right == null) {
            return 1;
        }

        int leftNum = countLeafNum(root.left);
        int rightNum = countLeafNum(root.right);
        return leftNum + rightNum;
    }

    public static int countLeafNum2(Node root) {
        if (root == null) {
            return 0;
        }

        Deque<Node> queue = new LinkedList<>();
        queue.offer(root);
        int count = 0;
        while (!queue.isEmpty()) {
            Node tmp = queue.poll();
            if (tmp.left != null) {
                queue.offer(tmp.left);
            }
            if (tmp.right != null) {
                queue.offer(tmp.right);
            }
            if (tmp.left == null && tmp.right == null) {
                count += 1;
            }
        }
        return count;
    }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值