数据结构:二叉树详解

目录

概念(在做习题中常用的概念)

两种特殊的二叉树

二叉树的性质

二叉树的遍历(重点)

如上图:

二叉树的构建(代码表示一颗二叉树和一些操作二叉树的方法)

二叉树的oj习题讲解(重点)

1. 检查两棵树是否相同 力扣

2. 另一颗树的子树   力扣

3. 反转二叉树    力扣

4. 判断一颗二叉树是否为平衡二叉树( 分别在O(N*2)和O(N)的时间复杂度代码来写 )    力扣

5. 对称二叉树   力扣

6. 二叉树的构建及遍历(根据先序遍历创建一颗二叉树)   二叉树遍历_牛客题霸_牛客网

7. 二叉树的层序遍历   力扣

8. 二叉树中两个指定节点的最近公共祖先   力扣

9. 根据前序遍历和中序遍历构造一颗二叉树   力扣

10. 根据后序遍历和中序遍历构建一颗二叉树   力扣

11. 二叉树创建字符串   力扣

12. 二叉树的前序 非递归遍历   力扣

13. 二叉树的中序 非递归遍历   力扣

14. 二叉树的后序 非递归遍历   力扣

oj题代码详解:


前言

    二叉树的一些oj题是在面试中常考的,同时概念也是很重要的,在做二叉树习题中掌握概念以及一些推导公式才能提高做题效率。

概念(在做习题中常用的概念)

节点的度一个节点含有子树的个数
树的度在一棵树中所有节点度的最大值
叶子节点(终端节点)度为0的节点称为叶子节点
父亲节点(双亲节点)一个含有子节点的节点,这个节点称为其子节点的父亲节点
根节点一棵树中没有双亲节点的节点
节点的层从根节点开始,根节点就是第一层,根的子节点是第二层,一次类推
树的高度(深度)树种节点的最大层次

二叉树如何用代码表示(定义一个静态内部类)

static class TreeNode{
        public int val;
        public TreeNode left;
        public TreeNode right;
        public TreeNode(char val) {
            this.val = val;
        }
    }

    由于二叉树每个节点都有值域(value)、左子树(left)、右子树(right),所以将二叉树的节点定义成一个静态内部类,加上构造方法,就构成一颗二叉树中的完整的一个节点。

 两种特殊的二叉树

1. 满二叉树

     每层节点数都达到最大值,这颗二叉树就是满二叉树,或者一颗二叉树的层数为k,且节点总数是2^k - 1,此时这棵树就是满二叉树。

2. 完全二叉树

    完全二叉树是由满二叉树引出的,从根节点开始编号,顺序从上往下,每一行是从左往右,没有编号为空的节点,此时这棵树就是完全二叉树。

二叉树的性质

1. 根节点的层数为1,则一颗非空二叉树的第i层节点个数最多为:2 ^ (i -1)
2. 根节点的二叉树的深度为1,则深度为k的二叉树的最大节点数为:2^k - 1

3. 叶子节点个数为n0,度为2的节点个数为n2,则 n0 = n2 + 1

4.  n个节点的完全二叉树的深度为 log2 (n  + 1)

5. n个节点的完全二叉树,如果按层序遍历编号,对于i号节点有:

     (1)双亲节点编号:(i-1)/  2

     (2)左孩子节点编号:2*i + 1

     (3)右孩子节点编号:2 * i + 2

 二叉树的遍历(重点)

1. 前序遍历(先根遍历)根 ---> 左 ---> 右
2. 中序遍历左 ---> 根 ---> 右
3. 后序遍历左 ---> 右 ---> 根
4. 层序遍历按照顺序:从上往下,每一行从左往右依次编号

如上图:

    1. 先序遍历: 1 2 4 5 3 6 7

    2. 中序遍历: 4 2 5 1 6 3 7

    3. 后序遍历: 4 5 2 6 7 3 1

    4. 层序遍历: 1 2 3 4 5 6 7

二叉树的构建(代码表示一颗二叉树和一些操作二叉树的方法)

public class TextBinaryTree {
    static class TreeNode{
        public int val;
        public TreeNode left;
        public TreeNode right;
        public TreeNode(char val) {
            this.val = val;
        }
    }
    public TreeNode root;//定义二叉树的根节点
    //非正规创建一颗二叉树
    public TreeNode createTree() {
        TreeNode A = new TreeNode('A');
        TreeNode B = new TreeNode('B');
        TreeNode C = new TreeNode('C');
        TreeNode D = new TreeNode('D');
        TreeNode E = new TreeNode('E');
        TreeNode F = new TreeNode('F');
        TreeNode G = new TreeNode('G');
        TreeNode H = new TreeNode('H');
        A.left = B;
        A.right = C;
        B.left = D;
        B.right = E;
        E.right = H;
        C.left = F;
        C.right = G;
        this.root = A;
        return root;
    }
    //前序遍历
    public void preOrder(TreeNode root) {
        if (root == null) return;
        System.out.print(root.val + " ");
        preOrder(root.left);
        preOrder(root.right);
    }
    //中序遍历
    public void inOrder(TreeNode root) {
        if (root == null) return;
        inOrder(root.left);
        System.out.print(root.val + " ");
        inOrder(root.right);
    }
    //后序遍历
    public void postOrder(TreeNode root) {
        if (root == null) return;
        postOrder(root.left);
        postOrder(root.right);
        System.out.print(root.val + " ");
    }
    //前序遍历有返回值
    //此时这个list需要放在外边,否则每次递归都会产生一个新的list
    List<Integer> list = new ArrayList<>();
    public List<Integer> preorderTraversal(TreeNode root) {
        if (root == null) return list;
        list.add(root.val);
        preorderTraversal(root.left);
        preorderTraversal(root.right);
        return list;
    }
    //二叉树的中序遍历有返回值
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if (root == null) return list;
        List<Integer> leftTree = inorderTraversal(root.left);
        list.addAll(leftTree);
        list.add(root.val);
        List<Integer> rightTree = inorderTraversal(root.right);
        list.addAll(rightTree);
        return list;
    }
    //二叉树的后序遍历有返回值
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if (root == null) return list;
        List<Integer> leftTree = postorderTraversal(root.left);
        list.addAll(leftTree);
        List<Integer> rightTree = postorderTraversal(root.right);
        list.addAll(rightTree);
        list.add(root.val);
        return list;
    }
    //获取二叉树中的节点个数
    //子问题:左树的节点 + 右树的节点 + 1
    //时间复杂度:O(N)  空间复杂度:O(logN),如果是单分支的树,空间复杂度就是
    //O(N)
    public int size(TreeNode root) {
        if (root == null) return 0;
        return size(root.left) + size(root.right) + 1;
    }
    //遍历思路
    public int count = 0;
    public int size1(TreeNode root) {
        if (root == null) return 0;
        //只要根不为空就++,然后遍历左树和右树
        count++;
        size1(root.left);
        size1(root.right);
        return count;
    }
    //获取叶子节点的个数   子问题思路
    public int getLeafNodeCount(TreeNode root) {
        if (root == null) return 0;//注意:当root为空的时候是没有叶子节点的
        if (root.left == null && root.right == null)
            return 1;
        return getLeafNodeCount(root.left) +
                getLeafNodeCount(root.right);
    }
    //遍历思路
    public int leafCount;
    public void leafNodeCount(TreeNode root) {
        if (root == null) return;
        if (root.left == null && root.right == null)
            leafCount++;
        leafNodeCount(root.left);
        leafNodeCount(root.right);
    }
    //获取第k层的节点个数
    //第k层的节点个数就相当于第k-1层的孩子的节点个数
    public int getLevelNodeCount(TreeNode root, int k) {
        if (root == null) return 0;
        if (k == 1) return 1;
        int left = getLevelNodeCount(root.left, k-1);
        int right = getLevelNodeCount(root.right, k-1);
        return left + right;
    }
    //获取二叉树的高度
    //时间复杂度:O(n)(会遍历到每一个节点)
    public int getHeight(TreeNode root) {
        if (root == null) return 0;
        int leftHeight = getHeight(root.left);
        int rightHeight = getHeight(root.right);
        return Math.max(leftHeight,rightHeight) + 1;
    }
    //二叉树的层序遍历
    public void levelOrder1(TreeNode root) {
        if (root == null) return;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            TreeNode cur = queue.poll();
            System.out.print(cur.val + " ");
            if (cur.left != null) {
                queue.offer(cur.left);
            }
            if (cur.right != null) {
                queue.offer(cur.right);
            }
        }
    }
    //二叉树层序遍历有返回值
    public List<List<Integer>> levelOrder2(TreeNode root) {
        List<List<Integer>> list = new ArrayList<>();
        if (root == null) return list;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            List<Integer> ret = new ArrayList<>();
            while (size != 0) {
                TreeNode cur = queue.poll();
                ret.add(cur.val);
                size--;
                if (cur.left != null) {
                    queue.offer(cur.left);
                }
                if (cur.right != null) {
                    queue.offer(cur.right);
                }
            }
            list.add(ret);
        }
        return list;
    }
}

二叉树的oj习题讲解

1. 检查两棵树是否相同 力扣
思路:遍历两棵树的每一个节点,若一个空,一个不为空,不相同,两个节点都不为空,判断值是否相等,值不相等,也不是相同的树。
2. 另一颗树的子树   力扣
思路:先判断是不是相同的树,如果是相同的树也算是子树,如果不相同,判断是不是root的左子树,是不是root(根节点)的右子树。
3. 反转二叉树    力扣
思路:反转就是交换左右两个节点的位置,如果是根节点就不用交换,交换root.left 和 root.right 节点,之后递归去反转根节点的左子树的左子树,右子树的右子树。
4. 判断一颗二叉树是否为平衡二叉树( 分别在O(N*2)和O(N)的时间复杂度代码来写 )    力扣

 1.  思路:相邻的左右两个节点高度差如果超过1,就不是平衡,否则就是平衡树。(求高度的过程要写成一个方法)

 2. 思路:在求高度的过程中就判断高度差是否超过1,如果高度差超过1,此时就返回一个-1,如果求高度过程中返回了-1,之后的节点就不用求高度了,时间复杂度就是O(N)

5. 对称二叉树   力扣
思路:判断整棵树是否对称,需要判断这棵树的左子树和右子树是否是对称的,和判断是否为相同的树类似,只是判断的节点换成了左子树的左和右子树的右是否相同。
6. 二叉树的构建及遍历(根据先序遍历创建一颗二叉树)   二叉树遍历_牛客题霸_牛客网
思路:在遍历字符串的过程中,创建节点并且递归创建左子树和右子树,之后按中序遍历进行输出即可。
7. 二叉树的层序遍历   力扣
思路:层序遍历是有顺序的,此时需要用合适的数据结构:队列,先入队列根,之后在弹出节点的时候进行记录,左右不为空的情况就把左和右子节点入队列。
8. 二叉树中两个指定节点的最近公共祖先   力扣
思路:如果在根的两边(p在左子树,q在右子树),此时根节点就是,如果都在左边,就去遍历二叉树,找到一个节点就返回(先找到的节点就是公共祖先),如果都在右边也是同理。
9. 根据前序遍历和中序遍历构造一颗二叉树   力扣
思路:先在前序遍历中找到根,然后拿着根在中序遍历 中找到根的位置,根的左边就是左子树,根的右边就是右子树,(记录左子树和右子树的下标)然后根据左子树和右子树的下标递归创建左树和右树。
10. 根据后序遍历和中序遍历构建一颗二叉树   力扣
思路:在后序遍历中找到根的位置,然后再在中序遍历中找到根的位置,划分左子树,右子树,记录左子树和右子树的下标,根据下标递归创建左树和右树。
11. 二叉树创建字符串   力扣
思路:考虑几种情况;左子树为空的前提下,右子树为空或者不为空,左子树不为空的前提下,右子树为空或者不为空。
12. 二叉树的前序 非递归遍历   力扣
思路:用栈这个数据结构,定义一个cur节点,当cur.left不为空的时候,就一直往左走(同时入栈走过的节点),一直到cur为空了,此时弹出栈顶节点,打印弹出的节点,同时记录这个节点,然后让cur走到弹出节点的右边,继续遍历,直到栈空了,此时也就遍历完成了。
13. 二叉树的中序 非递归遍历   力扣
思路:和前序遍历类似,只是打印的时机不同了。
14. 二叉树的后序 非递归遍历   力扣

思路:和前中序遍历类似,只是需要注意两点:(1)需要判断根以下的右子树是否为空(两种情况代码不一样)

(2)在记录的节点的右子节点打印之后就不要再次进行打印了。

oj题代码详解:

//二叉树的构建及遍历:读入一颗先序遍历的字符串,创建一颗二叉树
    //然后按中序遍历输出二叉树的节点
    //虽然只是一个前序遍历的字符串,但是也是可以创建出一颗二叉树的(有顺序的前序遍历)
    /*此处的字符串下标是不会越界的,因为给的前序遍历本来就是一个合法的字符串,如果多给了
    * 或者是少给了,此时在创建右树的时候就会发生越界,因为字符串已经遍历完了,但是可能树
    * 还没有建完,所以还会递归,但是现在是合法的字符串,此时递归的时候有回退的次数*/
    public static void main(String[] args) {
        //在main方法中将i置零也是可以的,
        Scanner scan = new Scanner(System.in);
        //将字符串整行都读进来
        String str = scan.nextLine();
        TreeNode root = createTree(str);
        inorder(root);
    }
    public static int i = 0;
    public static TreeNode createTree(String str) {
        TreeNode root = null;
        char ch = str.charAt(i);
        if (ch != '#') {
            root = new TreeNode(ch);
            i++;
            root.left = createTree(str);
            root.right = createTree(str);
        }else {
            i++;
        }
        return root;
    }
    public static void inorder(TreeNode root) {
        if (root == null) return;
        inorder(root.left);
        System.out.println(root.val + " ");
        inorder(root.right);
    }
    //二叉树的最近公共祖先
    /*1.在根的两边 2.要么都在根的左边或者右边(其中一个就是公共祖先)*/
    public TreeNode lowestCommonAncestor(
            TreeNode root, TreeNode p, TreeNode q) {
        if (root == null) return null;
        if (root == p || root == q) return root;
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        if (left != null && right != null) return root;
        /*这个地方是找到就返回了,如:在左边先找到了一个节点,此时p和q都在
        * root的左边,但是找到p就结束了,不会再去找另一个节点了,所以返回的
        * 节点就是上边的节点,就是最近的公共祖先*/
        else if (left != null) return left;
        else if (right != null) return right;
        else return null;
    }
    //如果每个节点有了父亲节点的地址,此时就变成了求相交节点
    //但是二叉树没有父亲节点,所以用一个数据结构:栈
    /*把找p和q的路径入到两个栈中,然后元素多的先出差值个元素,之后再一起出
    * 如果元素相等此时这个元素就是最近公共祖先*/

    //找到从根节点到指定节点node路径上所有的节点,然后入栈
    public boolean getPath(TreeNode root,
                           TreeNode node, Deque<TreeNode> stack) {
        if (root == null || node == null) return false;
        stack.push(root);
        if (root == node) return true;
        boolean ret1 = getPath(root.left,node,stack);
        boolean ret2 = getPath(root.right,node,stack);
        if (ret1) return true;
        if (ret2) return true;
        //左边和右边都没找到这个node,此时就回退,让这个元素出栈
        stack.pop();
        return false;//说明这条路径走不通,走到头了,也没找到这个节点
    }
    public TreeNode lowestCommonAncestor2(TreeNode root,
                                          TreeNode p, TreeNode q) {
        //1.两个栈中先存储好数据
        Deque<TreeNode> stack1 = new LinkedList<>();
        getPath(root,p,stack1);
        Deque<TreeNode> stack2 = new LinkedList<>();
        getPath(root,q,stack2);
        //2.判断栈的大小,出差值个元素
        int size1 = stack1.size();
        int size2 = stack2.size();
        //让size1是最大的
        int ret = Math.abs(size1 - size2);
        while (ret != 0) {
            if (size1 > size2) {
                stack1.pop();
            }else {
                stack2.pop();
            }
            ret--;
        }
        //栈中元素个数相等,此时一块出元素
        while (!stack1.isEmpty() && !stack2.isEmpty()) {
            if (stack1.peek() != stack2.peek()) {
                stack1.pop();
                stack2.pop();
            }else {
                return stack1.pop();
            }
        }
        return null;
    }
    //根据前序遍历和中序遍历构造一颗二叉树
    /*1.根据前序遍历找到根   2.在中序遍历中找到根的位置*/
    //public int i = 0;
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        TreeNode root = buildTreeChild(preorder, inorder, 0,
                inorder.length - 1);
        return root;
    }
    public TreeNode buildTreeChild(int[] preorder, int[] inorder,
                                   int begin, int end) {
        if (begin > end) return null;//如果下标越界,此时说明没有节点了
        //TreeNode root = new TreeNode(preorder[i]);
        //找到当前根在中序遍历中的位置
        int rootIndex = findIndex(inorder,begin, end, preorder[i]);
        i++;
        root.left = buildTreeChild(preorder,inorder,begin,rootIndex-1);
        root.right = buildTreeChild(preorder, inorder, rootIndex+1,end);
        return root;
    }

    private int findIndex(int[] inorder, int begin, int end, int key) {
        for (int j = begin; j <= end; j++) {
            if (key == inorder[j]) return j;
        }
        return -1;
    }
    //根据中序遍历和后序遍历来创建一颗二叉树
    /*1.先从后续遍历中找到根,之后在中序遍历中找到根的位置*/
    /*然后i--,要注意,先创建的是右树,然后创建左树*/
    public int a;
    public TreeNode buildTree3(int[] inorder, int[] postorder) {
        a = postorder.length - 1;
        TreeNode root = buildChildTree(inorder,postorder,
                 0, inorder.length - 1);
        return root;
    }
    public TreeNode buildChildTree(int[] inorder,int[] postorder,
                                   int begin, int end) {
        //TreeNode root = new TreeNode(postorder[a]);
        int rootIndex = findIndex(inorder, begin, end, postorder[a]);
        a--;
        root.right = buildChildTree(inorder,postorder,rootIndex + 1,end);
        root.left = buildChildTree(inorder, postorder, begin, rootIndex - 1);
        return root;
    }
    //根据二叉树创建字符串
    /*要以前序遍历的方式来创建这个字符串*/
    public String tree2str(TreeNode root) {
        if (root == null) return null;
        StringBuilder builder = new StringBuilder();
        tree2strChild(root, builder);
        /*1.左边为空,右边为空  2.左边不为空右边为空  3.左边为空右边不为空*/
        return builder.toString();
    }
    public void tree2strChild(TreeNode root, StringBuilder builder) {
        if (root == null) return;
        builder.append(root.val);
        //左子树为空或者不为空
        if (root.left != null) {
            builder.append("(");
            tree2strChild(root.left,builder);
            builder.append(")");
        }else {
            //左边为空的情况下考虑右边
            if (root.right != null) {
                builder.append("()");
            }else {
                return;
            }
        }
        //右子树为空或者不为空
        if (root.right != null) {
            builder.append("(");
            tree2strChild(root.right,builder);
            builder.append(")");
        }else {
            return;
        }
    }
    //判断一棵树是否为完全二叉树
    //用合适的数据结构: 队列
    /*每次从队列中取出一个节点,不为空:把左子树和右子树带进来,
    * 为空:开始判断队列中剩余的元素*/
    public boolean isCompleteTree(TreeNode root) {
        if (root == null) return true;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            TreeNode cur = queue.poll();
            if (cur != null) {
                queue.offer(cur.left);
                queue.offer(cur.right);
            }else {
                //如果cur为空说明左和右都是空的了,此时就去判断队列中的元素是否都是空的
                break;
            }
        }
        while (!queue.isEmpty()) {
            TreeNode tmp = queue.poll();
            if (tmp != null) return false;
        }
        return true;
    }
    //非递归实现前序遍历
    public List<Integer> preorderTraversalNor(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        TreeNode cur = root;
        Stack<TreeNode> stack = new Stack<>();
        while (cur != null || !stack.isEmpty()) {
            while (cur != null) {
                stack.push(cur);
                list.add(cur.val);
                cur = cur.left;
            }
            TreeNode top = stack.pop();
            cur = top.right;
        }
        return list;
    }
    //非递归实现中序遍历
    public List<Integer> inorderTraversalNor(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        TreeNode cur = root;
        Stack<TreeNode> stack = new Stack<>();
        while (cur != null || !stack.isEmpty()) {
            while (cur != null) {
                stack.push(cur);
                cur = cur.left;
            }
            TreeNode top = stack.pop();
            list.add(top.val);
            cur = top.right;
        }
        return list;
    }
    //非递归实现后序遍历
    public List<Integer> postorderTraversalNor(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        TreeNode cur = root;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode prev = null;
        while (cur != null || !stack.isEmpty()) {
            while (cur != null) {
                stack.push(cur);
                cur = cur.left;
            }
            TreeNode top = stack.peek();
            //此时需要注意:当打印之后top.right之后就不要再次打印了
            //所以prev记录的是 是否打印过top.right这个节点
            if (top.right == null || prev == top.right) {
                stack.pop();
                list.add(top.val);
                prev = top;
            }else {
                cur = top.right;
            }
        }
        return list;
    }

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

良月初十♧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值