数据机构之(六):二叉树

树的概念及树的深度
基础概念
” —— 递归的数据结构 由根节点和它的孩子,以及孩子的孩子等等组成

其中没有子节点的节点 叫做叶子节点。如 EFCG ,都是叶子节点。
节点的度:指拥有的子节点个数。如 A 的度是 3 B 的度是 2 D 的度是 1 ,叶子节点的度都是 0
节点的关系:子节点是父节点的孩子节点,父节点也是子节点的双亲节点。有相同父节点的节点为兄弟
节点。如 A B 的双亲节点, B A 的孩子节点, B C 是兄弟节点。
节点的层次: A 在第一层, BCD 在第二层, EFG 在第三层
树的深度:是最大层次数,即为 3
二叉树 ,最多可以分出两个叉的树。
https://leetcode-cn.com/problems/er-cha-shu-de-shen-du-lcof/
剑指 Offffer 55 - I. 二叉树的深度
难度简单 59
输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)
形成树的一条路径,最长路径的长度为树的深度。
例如:
给定二叉树 [3,9,20,null,null,15,7]
     3
    /  \
  9  20
   /    \
 15    7
返回它的最大深度 3
    public static int maxDepth(TreeNode root) {
// 树的深度 左子树的深度和右子树的深度中 更大的值 +1
        if (root == null) return 0;
        int leftDepth = maxDepth(root.left);
        int rightDepth = maxDepth(root.right);
        return Math.max(leftDepth, rightDepth) + 1;
    }
二叉树的分类和比较
二叉树的分类
满二叉树
所有叶子节点都在同一层 所有分支节点(非叶子节点)都有左右子树
每一层的节点个数 (
i 表示层数) 2^ i-1 )个节点
对于深度为 h 的树,共有节点 2^h - 1

 

斜树
所有的子节点都向一个方向倾斜,只分叉出左子树或右子树

 

完全二叉树
对于高度 / 深度为 h 的二叉树而言, h-1 层都是满的, h 层节点在左侧连续排列,空位都在右侧。
满二叉树一定是完全二叉树,反之未必。

 

 

将完全二叉树的节点进行编号,对于编号为 k 的节点:
1 ) 父节点就是 k/2
2) 如果有孩子节点,先有左孩子 2k ,后有右孩子 2k+1
【构造出完全二叉树】
    // 0 1 2 3 4
// 2k+1 2k+2
    private static int[] array = {1, 2, 3, 4, 5};
    // 存储生成的节点
    private static List<TreeNode> nodeList = new LinkedList<>();
    public static TreeNode createTree() {
// 构造节点
        for (int i = 0; i < array.length; i++) {
            TreeNode node = new TreeNode(array[i]);
            nodeList.add(node);
        }
// 构造节点之间的关系
        for (int i = 0; i < nodeList.size() / 2; i++) {
            TreeNode node = nodeList.get(i);
            node.left = nodeList.get(i * 2 + 1);
// 最后一个父节点 可能没有右孩子 需要额外判断
            if (i * 2 + 2 < nodeList.size()) {
                node.right = nodeList.get(i * 2 + 2);
            }
        }
        return nodeList.get(0);
    }
【相同的树】
https://leetcode-cn.com/problems/same-tree/
100. 相同的树
给定两个二叉树,编写一个函数来检验它们是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
示例 1:
输入 : 1   1
  / \   / \
2  3 2  3
[1,2,3], [1,2,3]
输出 : true
示例 2:
输入 : 1   1
   /     \
 2       2
[1,2], [1,null,2]
输出 : false
示例 3:
输入 : 1    1
   /   \     /   \
  2    1 1     2
[1,2,1], [1,1,2]
输出 : false
如何判断是否相同?
1 )两棵树,如果有左右子树,树 A 的左子树 == B 的左子树,同时树 A 的右子树 == B 的右子树
2 )如果两棵树都是空,是相同的
3 )如果一棵为空,一棵不为空,是不同的
4 )如果数值不同,也是不同的
   public boolean isSameTree(TreeNode p, TreeNode q) {
// 递归出口
        if (p == null && q == null) {
            return true;
        }
        if (p == null || q == null) {
            return false;
        }
        if (p.val != q.val) {
            return false;
        }
// 递归规律
        return isSameTree(p.left, q.left)
                && isSameTree(p.right, q.right);
    }
二叉树的遍历之递归实现
遍历
遍历:对树中的每个节点都访问一次,且只访问一次。
深度优先遍历 DFS = Deep First Search
广度优先遍历 BFS = Breath First Search
先序遍历(前序遍历): 根节点 —> 左子树 -> 右子树
中序遍历:左子树 —> 根节点 -> 右子树
后序遍历:左子树 -> 右子树 —> 根节点

 

1 、递归实现方式

 

    // 先序 中序 和后序的遍历
    public static void preOrder(TreeNode node) {
// 递归出口
        if (node == null) return;
        System.out.print(node.val + " ");
        preOrder(node.left);
        preOrder(node.right);
    }
    public static void inOrder(TreeNode node) {
        if (node == null) return;
        inOrder(node.left);
        System.out.print(node.val + " ");
        inOrder(node.right);
    }
    public static void postOrder(TreeNode node) {
        if (node == null) return;
        postOrder(node.left);
        postOrder(node.right);
        System.out.print(node.val + " ");
    }
2 、广度优先遍历
通过队列实现 从根节点开始存储到队列中
对队列元素的处理是 将队头节点的孩子存入队列中,取出队头节点
直到队列为空 所有节点处理完成 同时节点的顺序是按照层级的

 

    public static void bfs(TreeNode node) {
        if (node == null) return;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(node);
        while (queue.size() > 0) {
// 取出队首元素
            TreeNode node1 = queue.poll();
            System.out.print(node1.val + " ");
// 如果有左孩子 或者右孩子 将其存入队列
            if (node1.left != null) {
                queue.offer(node1.left);
            }
            if (node1.right != null) {
                queue.offer(node1.right);
            }
        }
    }
前序和中序遍历之非递归实现
非递归实现方式
递归 —— 有去有回
先解决子问题 再基于子问题 解决当前问题
也可以理解为 是解决有依赖关系的多个问题
fifib(3) A -> fifib(2) B -> fifib(1) C
调用关系
开始处理 A
因为 A 依赖 B 开始处理 B
因为 B 依赖 C 开始处理 C
C 处理完成
B 处理完成
A 处理完成
后进先出的处理关系 --
a ) 先序和中序遍历的非递归处理分析

 

 

 

    public static void preOrderByLoop(TreeNode node) {
        Stack<TreeNode> stack = new Stack<>();
// 使用指针 记录遍历到哪个节点
        TreeNode p = node;
        while (p != null || !stack.isEmpty()) {
// 入栈 把当前能读到的所有左孩子 存入栈中
            while (p != null) {
                System.out.print(p.val + " ");
                stack.push(p);
                p = p.left;
            }
// 1 2 4 8
// 1 2 9
// 1 5
// 3 6
// 7
// 出栈 弹出栈顶元素 并找到其右孩子
            if (!stack.isEmpty()) {
                p = stack.pop();
                p = p.right;
            }
        }
    }

 先序是入栈时打印 中序是出栈时打印

    public static void inOrderByLoop(TreeNode node) {
        Stack<TreeNode> stack = new Stack<>();
// 使用指针 记录遍历到哪个节点
        TreeNode p = node;
        while (p != null || !stack.isEmpty()) {
// 入栈 把当前能读到的所有左孩子 存入栈中
            while (p != null) {
                stack.push(p);
                p = p.left;
            }
// 出栈 弹出栈顶元素 并找到其右孩子
            if (!stack.isEmpty()) {
                p = stack.pop();
                System.out.print(p.val + " ");
                p = p.right;
            }
        }
    }

 

后序遍历之非递归实现
后序遍历的非递归方式
经过的路径 - -
后序遍历时 根节点不先从栈中弹出 要在右子树被遍历后 再弹出
如何判断右子树被遍历完成
通过记录上一次遍历的节点
如果上一次遍历的是当前节点的右子树 代表此根节点也可以被遍历出来
入栈顺序 1 2 4 8 9 5 3 6 7
出栈顺序 8 4 9 2 5 1 6 3 7
1 2 4 8
1 2 9
1 5
3 6
7
入栈顺序 1 2 4 8 9 5 3 6 7
出栈顺序 8 9 4 5 2 6 7 3 1
1 2 4 8
1 2 4
1 2 4 9
1 2 4
1 2
1 2 5
1 2
1 3 6
1 3 7
1 3
1
节点出栈的逻辑有两种情况:
1 )当前节点是叶子节点
2 )上一次出栈的节点是当前节点的右孩子
    public static void postOrderByLoop(TreeNode node) {
        Stack<TreeNode> stack = new Stack<>();
// 使用指针 记录遍历到哪个节点
        TreeNode p = node;
// 记录上一次访问的节点
        TreeNode prev = null;
        while (p != null || !stack.isEmpty()) {
// 入栈 把当前能读到的所有左孩子 存入栈中
            while (p != null) {
                stack.push(p);
                p = p.left;
            }
// 出栈 弹出栈顶元素 并找到其右孩子
            if (!stack.isEmpty()) {
// 节点出栈的逻辑有两种情况:
// 1)当前节点是叶子节点
// 2)上一次出栈的节点是当前节点的右孩子
                p = stack.pop();
                if (p.right == null || prev == p.right){
                    System.out.print(p.val + " ");
                    prev = p;
                    p = null;
                }else {
// 不满足出栈条件 是因为节点有右孩子 且右孩子没有被访问到
                    stack.push(p);
                    p = p.right;
                }
            }
        }
    }
经典应用之对称二叉树
对称二叉树
https://leetcode-cn.com/problems/symmetric-tree/
101. 对称二叉树
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的 :
1
/ \
2 2
\  \
3  3
如何将数组转化为二叉树
     1
   /    \
   2   2
  / \   / \
 3 4 4 3
0 1 2 3 4 5 6
Integer[] arr = {1,2,2,3,4,4,3}
索引 元素 左子树索引 右子树索引
0   1   1    2
1   2   3    4
2   2   5    6
......
i      2 i+1 2 i+2
判断是否对称
将树拆解为 左子树 A 右子树 B
A 的左子树和 B 的右子树要对称
A 的右子树和 B 的左子树要对称
非递归的方式 —— 广度优先遍历
存入左子树和右子树 再取出两个子树 判断是否对称
    public static boolean isSymmetric(TreeNode root) {
// 排除空树的情况
        if (root == null) return true;
// 拆分成左右子树 递归判断
        return isMirror(root.left, root.right);
    }
    // 将树拆解为 左子树A 右子树B
// A的左子树和B的右子树要对称
// A的右子树和B的左子树要对称
    public static boolean isMirror(TreeNode node1, TreeNode node2) {
// 递归的出口
        if (node1 == null && node2 == null) return true;
// 判断是否不对称
        if (node1 == null || node2 == null) return false;
        if (node1.val != node2.val) return false;
        return isMirror(node1.left, node2.right)
                && isMirror(node1.right, node2.left);
    }
    public static boolean isSymmetric1(TreeNode root) {
        if (root == null) return true;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root.left);
        queue.offer(root.right);
        while (!queue.isEmpty()) {
            TreeNode node1 = queue.poll();
            TreeNode node2 = queue.poll();
// 递归的出口
            if (node1 == null && node2 == null) continue;
// 判断是否不对称
            if (node1 == null || node2 == null) return false;
            if (node1.val != node2.val) return false;
            queue.offer(node1.left);
            queue.offer(node2.right);
            queue.offer(node1.right);
            queue.offer(node2.left);
        }
        return true;
    }
经典应用之翻转二叉树
翻转二叉树
https://leetcode-cn.com/problems/invert-binary-tree
翻转一棵二叉树。
示例:
输入:
4
/ \
2 7
/ \ / \
1 3 6 9
输出:
4
/ \
7 2
/ \ / \
9 6 3 1
翻转的过程
方式一:
(1)
       4
     /   \
    2   7
   / \   / \
  1 3 6 9
(2)
       4
     /   \
    7    2
   / \   / \
  6 9 1 3
(3)
      4
    /    \
  7      2
  / \    / \
 9 6  3 1
先将左右子树交换 然后再分别 翻转左右子树
方式二:
(1)
       4
      /  \
    2    7
   / \    / \
  1 3  6 9
(2)
       4
      /   \
     2   7
    / \   / \
   3 1 6 9
(3)
       4
     /    \
    7    2
   / \    / \
  6 9  3 1
(4)
      4
     /  \
   7    2
   / \   / \
  9 6  3 1
先翻转左子树 然后交换左右子树 再翻转左子树(原来的右子树)
方式三:
(1)
         4
       /   \
      2    7
     / \    / \
     1 3 6 9
(2)
          4
         /  \
       2    7
      / \    / \
     3 1  6 9
(3)
          4
         /  \
       2    7
      / \    / \
     3 1  9 6
(4)
          4
         /  \
        7   2
       / \   / \
      9 6  3 1
先翻转左子树 再翻转右子树 最后交换左右子树
分别对应 前序、中序、后序遍历
    // 前序遍历
    public static TreeNode invertTree(TreeNode root) {
// 递归的出口
        if (root == null) return null;
// 方案一
// 先将左右子树交换 然后再分别 翻转左右子树
        TreeNode tmp = root.left;
        root.left = root.right;
        root.right = tmp;
        invertTree(root.left);
        invertTree(root.right);
        return root;
    }
    // 中序遍历
    public static TreeNode invertTree1(TreeNode root) {
// 递归的出口
        if (root == null) return null;
// 方案二
// 先翻转左子树 然后交换左右子树 再翻转左子树(原来的右子树)
        invertTree1(root.left);
        TreeNode tmp = root.left;
        root.left = root.right;
        root.right = tmp;
        invertTree1(root.left);
        return root;
    }
    // 后序遍历
    public static TreeNode invertTree2(TreeNode root) {
// 递归的出口
        if (root == null) return null;
        invertTree2(root.left);
        invertTree2(root.right);
// 方案三
// 先翻转左子树 再翻转右子树 最后交换左右子树
        TreeNode tmp = root.left;
        root.left = root.right;
        root.right = tmp;
        return root;
    }
    // 广度优先遍历
    public static TreeNode invertTree3(TreeNode root) {
        if (root == null) return null;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (queue.size() > 0) {
// 取出队首元素
            TreeNode current = queue.poll();
// 交换此节点的左右子树
            TreeNode tmp = current.left;
            current.left = current.right;
            current.right = tmp;
// 如果有左孩子 或者右孩子 将其存入队列
            if (current.left != null) {
                queue.offer(current.left);
            }
            if (current.right != null) {
                queue.offer(current.right);
            }
        }
        return root;
    }
根据遍历结果构造二叉树
根据遍历结果构造二叉树
只能通过前序和中序,或者中序和后续唯一确认一棵二叉树,而前序和后序无法做到。
1 )前序和中序结果倒推二叉树
思路:
先根据前序结果,找到根节点
找到中序结果中根节点的位置,确认左右子树的个数
切出左子树的前序和中序结果,再按照此方法推进,
直到推导出叶子节点的位置,反倒推出整棵树。
0 1 2 3 4
1 2 3 4 5
3 4 2 1 5
根节点在中序中的位置是 3 (左子树的大小)
leftSize = 3
左子树的前序和中序
切分的区间是左闭右开
2 3 4 [1,leftSize+1
3 4 2 [0,leftSize)
右子树的前序和中序
5 [leftSize+1,end)
5 [leftSize+1,end)

 

    public static TreeNode buildTree(int[] preOrder, int[] inOrder) {
// 递归出口
        if (preOrder.length == 0) return null;
// 在前序结果中 找到根节点
        int rootValue = preOrder[0];
        TreeNode root = new TreeNode(rootValue);
// 查找根节点在中序结果中的位置
        int leftSize = find(inOrder, rootValue);
// 切分出左子树的前序和中序结果
// 使用 Arrays.copyOfRange方法 三个参数 原始数组 起始位置 终止位置
// [起始位置,终止位置)
//
// 2 3 4 [1,leftSize+1)
//
// 3 4 2 [0,leftSize)
        int[] leftPreOrder = Arrays.copyOfRange(preOrder, 1, leftSize + 1);
        int[] leftInOrder = Arrays.copyOfRange(inOrder, 0, leftSize);
        root.left = buildTree(leftPreOrder, leftInOrder);
// 切分出右子树的前序和中序结果
//
// 5 [leftSize+1,end)
// 5 [leftSize+1,end)
        int[] rightPreOrder = Arrays.copyOfRange(preOrder, leftSize + 1,
                preOrder.length);
        int[] rightInOrder = Arrays.copyOfRange(inOrder, leftSize + 1,
                inOrder.length);
        root.right = buildTree(rightPreOrder, rightInOrder);
        return root;
    }
    public static int find(int[] array, int value) {
        for (int i = 0; i < array.length; i++) {
            if (array[i] == value) {
                return i;
            }
        }
        return -1;
    }
2 )中序和后序结果倒推二叉树

0 1 2 3 4
3 4 2 1 5
4 3 2 5 1
根节点在中序中的位置是 3 (左子树的大小)
leftSize = 3
左子树的中序和后序
切分的区间是左闭右开
3 4 5 [0,leftSize)
4 3 2 [0,leftSize)
右子树的中序和后序
5 [leftSize+1,end)
5 [leftSize,end-1)
    public static TreeNode buildTree1(int[] inOrder, int[] postOrder) {
// 递归出口
        if (inOrder.length == 0 || postOrder.length == 0) return null;
// 在前序结果中 找到根节点
        int rootValue = postOrder[postOrder.length - 1];
        TreeNode root = new TreeNode(rootValue);
// 查找根节点在中序结果中的位置
        int leftSize = find(inOrder, rootValue);
// 切分出左子树的中序和后序结果
// 使用 Arrays.copyOfRange方法 三个参数 原始数组 起始位置 终止位置
// [起始位置,终止位置)
//
// 3 4 5 [0,leftSize)
// 4 3 2 [0,leftSize)
        int[] leftInOrder = Arrays.copyOfRange(inOrder, 0, leftSize);
        int[] leftPostOrder = Arrays.copyOfRange(postOrder, 0, leftSize);
        root.left = buildTree1(leftInOrder, leftPostOrder);
// 切分出右子树的中序和后序结果
//
// 5 [leftSize+1,end)
// 5 [leftSize,end-1)
        int[] rightInOrder = Arrays.copyOfRange(inOrder, leftSize + 1,
                inOrder.length);
        int[] rightPostOrder = Arrays.copyOfRange(postOrder, leftSize,
                postOrder.length - 1);
        root.right = buildTree1(rightInOrder, rightPostOrder);
        return root;
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值