【Java数据结构】二叉树

目录


一棵树是由若干个不相交的子树组成的,所以树是递归定义的.

做二叉树相关的 oj 题会经常使用递归的方法解决.

树的特征

  • 子树之间是不相交的。
  • 除了根节点外,每个节点有且仅有一个父亲节点。
  • 一棵有 N 个节点的树有 N - 1 条边

树的概念

下图就是一棵树,用这棵树来举例树的概念。

  • 节点的度: 一个节点含有的子树个数。例如在上面这棵树中 A 的度为 6,E 的度为2, F 的度为 3。
  • 树的度: 一棵树中,所有 节点的度 的最大值。 例如在上面这棵树中 树的度就是 6,因为节点度的最大值是 A 的度。
  • 叶子节点 / 终端节点: 指 度为 0 的节点。例如在上面这棵树中 B C H I P Q K L M N 都是叶子节点。
  • 父亲节点 / 父节点:若一个节点有 孩子节点,那么这个节点就是其 孩子节点 的父节点。例如 A 是 B 的父亲节点。
  • 孩子节点 / 子节点:一个节点 含有一棵子树的根节点,这棵子树的根节点 就是 该节点 的孩子节点。例如 B 是 A 的子节点。
  • 根节点: 在一棵树中,没有 父亲节点 的节点。 例如上面这棵树中只有节点 A 是根节点。
  • 节点的层次: 从根开始,根是第一层,根的子节点是第二层,根的子节点的子节点是第三层,以此类推。例如:P 在第 4 层。
  • 树的高度 / 树的深度: 树中节点的最大层次。例如上面这棵树的高度是 4.

二叉树

根据树的递归定义,如果这棵树是二叉树,那么他的每棵子树都是二叉树。
二叉树要么为空,要么有一个根节点加上左子树和右子树。

二叉树中每个节点的度 <= 2

两种特殊的二叉树

  • 完全二叉树: 每一层(除最后一层)都被完全填满,且最后一层的节点必须从左到右依次排列,没有空缺。另外在完全二叉树中,如果节点总个数为奇数,则没有度为1的节点,如果节点总个数为偶数,只有一个度为1的节点。
  • 满二叉树:每个节点要么是叶子节点,要么有两个孩子节点,且每一层都必须被完全填满,没有空缺。满二叉树满足:层数为 k , 则节点总数就是 2k - 1.

二叉树的性质

  1. 若根节点的层数为 1 ,则一棵非空二叉树的第 i 层上最多有 2i-1 (i > 0) 个节点
  2. 若只有根节点的深度为 1 , 则深度为 k 的二叉树的最大节点就是 2k-1 (k >= 0)
  3. 对于任何一棵二叉树,如果其叶子节点的个数为 n0, 节点的度 为 2 的非叶子节点个数为 n2. 则有 n0 = n2 + 1.也就是说,对于任何一棵二叉树,叶子节点个数永远比度为 2 的节点个数多一个。 证明如下:
  4. 具有 n 个节点的完全二叉树的深度 k 为 log2(n + 1) 向上取整。推导:由性质 2 可得知 n = 2k-1 --> 2k = n + 1 --> k = log2(n + 1).
  5. 对于具有 n 个节点的完全二叉树,如果按照从上到下从左到右的顺序对所有节点从 0 开始编号,则:

二叉树的基本操作

4 种遍历二叉树的方式

前序遍历,中序遍历,后序遍历,层序遍历。

前中后序遍历的区别就是 访问根节点的时机不同

前序遍历:遇到根就打印 --> 遍历左子树 --> 遍历右子树。

中序遍历:左子树全部遍历完 --> 打印根 --> 遍历右子树。

后序遍历:左子树全部遍历完 --> 右子树全部遍历完 --> 打印根

已知前序遍历和中序遍历的结果,可以把二叉树创建出来,

已知中序遍历和后序遍历的结果,可以把二叉树创建出来,

已知前序遍历和后序遍历的结果,不能 把二叉树创建出来。

因为前序遍历和后序遍历可以确定根节点,中序遍历可以确定左右树有那些节点。

接下来分别用 java 代码实现 前中后序遍历 和 层序遍历,其中前中后序遍历分别用递归和非递归两种方法实现。

前序遍历题目链接: 点击这里

前序遍历递归思路:遇到根就打印 --> 遍历左子树 --> 遍历右子树

以这棵二叉树为例,此时根就是 1 ,立即打印 1 之后遍历左子树,

此时左子树的根就是 2 ,立即打印 2 之后继续遍历 2 的左子树,

2 的左子树的根是 3, 立即打印 3 之后继续遍历 3 的左子树,

发现此时 3 的左子树没有根,即 根为 null 的时候结束递归,返回空.

这时 3 的左子树已经全部遍历完成,遍历 3 的右子树,发现 3 的右子树返回的也是空。

然后此时 根为 3 的二叉树已经全部遍历完成了,根为 2 的左子树的返回值就是 3,之后的过程以此类推。

前序遍历非递归思路:

变量 cur 表示此时遍历到的节点.

变量 top 存储出栈的元素并访问该节点的右树.

如果该节点 (cur) 不为空,把 cur 指向的节点入栈,然后打印,最后让 cur 指向 左边的节点。重复这个步骤。

一直循环到该节点为空的时候,说明 D 的左边已经遍历完成,

出栈 D 并让 top 指向 D 为了访问 D 的右边。

当 D 的左边右边都遍历完成之后, B 的左树都遍历完成了,把 B 出栈并让 top 重新指向 B 把 D 给覆盖了.遍历 B 的右树,以此类推...直到栈为空的时候且 cur 没有指向任何节点的时候,说明遍历完成了。

代码实现:

java 复制代码
//前序遍历递归实现
class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        //创建一个顺序表用于存储遍历的结果
        List<Integer> preorder = new ArrayList<>();
		
		//递归终止条件:如果这个根是空的,返回空的顺序表
        if(root == null) {
            return preorder;
        }

        //打印根
        preorder.add(root.val);

        //遍历左树,并把遍历左树的结果存储到变量 left 上
        List<Integer> left = preorderTraversal(root.left);
        //把遍历左树的结果全部存到 ret 上
        preorder.addAll(left);

        //遍历右树,并把遍历右树的结果存储到变量 right 上
        List<Integer> right = preorderTraversal(root.right);
        //把遍历右树的结果全部存到 ret 上
        preorder.addAll(right);

        return preorder;
    }
}
java 复制代码
//前序遍历非递归实现
class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        
        List<Integer> preorder = new LinkedList<>();

        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;

        TreeNode top = new TreeNode();
        //用非递归方式进行前序遍历
        while(cur != null || !stack.isEmpty()) {
            while(cur != null) {
                stack.add(cur);
                preorder.add(cur.val);//打印节点
                cur = cur.left;
            }
            top = stack.pop();
            cur = top.right;
        }
        
        return preorder;
    }
}

中序遍历题目链接: 点击这里

知道前序遍历的思路就能举一反三,中序遍历主要提一下非递归的注意点:

中序遍历与前序遍历的代码只有一点不同:根节点打印的时机。

代码实现:

java 复制代码
//中序遍历递归实现
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        //创建一个链表用于存储遍历的结果
        List<Integer> inoreder = new ArrayList<>();
 		
 		//递归终止条件
        if(root == null) {
            return inoreder;
        }

        //先遍历左树
        List<Integer> left = inorderTraversal(root.left);
        inoreder.addAll(left);

        //打印根节点
        inoreder.add(root.val);

        //遍历右树
        List<Integer> right = inorderTraversal(root.right);
        inoreder.addAll(right);

        return inoreder;
    }
}
java 复制代码
//中序遍历非递归实现
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        //创建一个链表用于存储中序遍历的结果
        LinkedList<Integer> inoreder = new LinkedList<>();

        //中序遍历的非递归实现
        Stack<TreeNode> stack = new Stack<>();//栈用于暂时保存还未输出的节点
        TreeNode cur = root;

        while(cur != null || !stack.isEmpty()) {
            //遍历左树,左树不为空,把节点记录到栈中并继续遍历该节点的左树
            while(cur != null) {
                stack.push(cur);
                cur = cur.left;
            }
            //如果左树为空,打印栈顶元素,也就是该左树这个整体的 root
            TreeNode top = stack.pop();
            inoreder.add(top.val);

            //找到右树
            cur = top.right;
        }

        return inoreder;
    }
}

后序遍历题目链接: 点击这里

后序遍历的递归思路与前中序遍历相比只是打印元素的时机不同罢了。

主要来看非递归的思路:

遍历左树的时候和中序遍历一样。

java 复制代码
while(cur != null) {
	stack.push(cur);
	cur = cur.left;
}

此时不能像中序遍历那样直接 pop,如果 top 的右树不为空,肯定有新的节点入栈,先遍历完 top 的右树之后,再打印根节点。

此时 D 的右树 K 已经打印完成,top 重新指向 D, 此时发现 top.right != null,但是 D 的右树确实已经都打印了,此时应该要把 D 出栈并打印 D。

为了检查 D 的右边 K 是否已经打印了,再创建一个变量 prev 用于指向已经打印完的右树,如果 top.right == prev , 说明 top 的右树已经全部打印完成,把 D 出栈并打印 D。以此类推。

代码实现:

java 复制代码
//后序遍历递归实现
class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        //创建一个顺序表储存遍历的结果
        List<Integer> postorder = new ArrayList<>();

        //终止条件
        if(root == null) {
            return postorder;
        }

        //遍历左树
        List<Integer> left = postorderTraversal(root.left);
        postorder.addAll(left);

        //遍历右树
        List<Integer> right = postorderTraversal(root.right);
        postorder.addAll(right);

        //打印根
        postorder.add(root.val);

        return postorder;
    }
}
java 复制代码
//后序遍历非递归实现
class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        //创建一个顺序表储存遍历的结果
        List<Integer> postorder = new ArrayList<>();

        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        TreeNode prev = null;

        while(cur != null || !stack.isEmpty()) {
            //遍历左树与中序遍历相同
            while(cur != null) {
                stack.push(cur);
                cur = cur.left;    
            }
            //此时不能像中序遍历那样直接 pop,
            //如果 top 的右树不为空,肯定有新的节点入栈,
            //先遍历完 top 的右树之后,再打印根节点。
            TreeNode top = stack.peek();
            
            //prev 指的是最近被打印的那个节点,
            //如果没有设置 prev 这个变量且 top 的右树不为空,就会死循环,
            //cur 永远指向 top.right 无法跳出循环;
            if(top.right == null || top.right == prev) {
                stack.pop();
                postorder.add(top.val);
                //此时 prev 指向最新打印的节点
                prev = top;
            }else {
                cur = top.right;
            }
        }
        
        return postorder;
    }
}

层序遍历题目链接: 点击这里

思路:

代码实现:

java 复制代码
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> list = new ArrayList<>();//存储层序遍历的结果
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);

        while(!queue.isEmpty()) {
            ArrayList arrayList = new ArrayList<>();//存储该层的所有元素
            int qSize = queue.size();//size 记录该层的元素个数

            //把 cur 所有 非null 的左右树入队
            while(qSize != 0) {
                TreeNode cur = queue.poll();
                qSize--; 

                if(cur != null) {
                    queue.offer(cur.left);
                    queue.offer(cur.right);
                    //把 cur 的左右树遍历完成之后存储 cur 的值
                    arrayList.add(cur.val);
                }
                  
            }
            
            //把该层所有的元素存储到 arrayList 
            if(arrayList.size() > 0) {
                list.add(arrayList);
            }
        }
        return list;
    }
}

判断一棵树是不是完全二叉树

思路:

利用层序遍历的方式遍历这棵树,如果这棵树是完全二叉树,在队列中不可能出现既有空且后面又有非空节点的情况。当队列里只有 null 的时候就是完全二叉树。

代码实现:

java 复制代码
class Solution {
// 判断一棵树是不是完全二叉树,
    // 二叉树遍历到 null 的时候停止, 此时队列里只有 null 就是完全二叉树
    boolean isCompleteTree(TreeNode root){
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);

        while(!queue.isEmpty()) {
            TreeNode cur = queue.poll();
            //因为这个方法会在队列中存储 null ,所以会出现 cur == null的情况
            //一旦遍历到 null 的时候就结束循环,检查队列中是否出现 非null 的元素
            if(cur != null) {
                queue.offer(cur.left);
                queue.offer(cur.right);
            }else {
                break;
            }

        }

        //把队列里的元素一个一个出队, 如果有元素 != null 就不是完全二叉树
        while(!queue.isEmpty()) {
            TreeNode cur = queue.poll();
            if(cur != null) {
                return false;
            }
        }
        return true;
}

获取二叉树总共的节点个数

用子问题思路:二叉树总共节点个数 == 左子树的节点个数 + 右子树的节点个数 + 1(根节点)

递归结束条件:当根节点为空时,就是没有节点,返回 0 个。

代码实现:

java 复制代码
// 获取树中 结点的个数 == 左子树的结点个数 + 右子树的结点个数 + 根结点
    int size(TreeNode root) {
        if(root == null) {
            return 0;
        }
        return size(root.left) + size(root.right) + 1;
    }

获取叶子节点的个数

子问题思路: 整棵树的叶子节点个数 == 左子树的叶子节点个数 + 右树叶子节点个数

递归结束条件:

  1. 当根节点为空时,就是没有叶子节点,返回 0 ;
  2. 当该节点的左树和右树都为空时,这个节点就是叶子节点,返回 1.

代码实现:

java 复制代码
    // 子问题思路-求叶子结点个数
    // 整棵树的叶子结点个数 == 左子树的叶子结点个数 + 右子树的叶子结点个数
    int getLeafNodeCount(TreeNode root){
        //判断叶子结点的条件, 如果该结点是叶子结点,返回 1
        if(root == null) {
            return 0;
        }else if(root.left == null && root.right == null){
            return 1;
        }else {
            return getLeafNodeCount(root.left) +
                    getLeafNodeCount(root.right);
        }
    }

获取第 k 层的节点个数

用子问题思路: 获取 k 层的节点个数 == 获取左子树第 k - 1 层的节点个数 + 获取右子树第 k - 1 层的节点个数。

递归结束条件:

  1. 根节点为空时,没有节点,返回 0 ;
  2. 当 k == 1 时,返回 1,因为左子树的第一层和右子树的第一层都只有一个节点。

代码实现:

java 复制代码
    // 获取第K层节点的个数 == 左子树的第 k-1 层结点个数 + 右子树的第 k-1 层结点个数
    int getKLevelNodeCount(TreeNode root,int k){
        if(root == null) {
            return 0;
        }else if(k == 1) {
            return 1;
        }else {
            return getKLevelNodeCount(root.left, k-1) +
                    getKLevelNodeCount(root.right, k-1);
        }

    }

获取二叉树的高度

用子问题思路: 二叉树的高度 == (左子树的高度 与 右子树的高度 取最大值 ) + 1

递归结束条件: 根节点为空时,高度为 0 ,返回 0。

代码实现:

java 复制代码
    // 获取二叉树的高度 == 左子树的高度和右子树的高度取最大值 + 1
    int getHeight(TreeNode root){
        if(root == null) {
            return 0;
        }else {
            int leftHeight = getHeight(root.left);
            int rightHeight = getHeight(root.right);
            return leftHeight > rightHeight ? leftHeight + 1 :
                    rightHeight + 1;
        }
    }

检测值为 value 的元素是否存在

用子问题思路:根节点的左子树是否存在 || 根节点的右子树是否存在

递归结束条件:

  1. 如果根节点为空,就不存在,返回空;
  2. 如果根节点是 value ,即存在,返回该节点的引用.

代码实现:

java 复制代码
    // 检测值为value的元素是否存在 1.空树  2. 根是不是 val 不是继续  3.左子树和右子树
    TreeNode find(TreeNode root, int val){
        if(root == null) {
            return null;
        } else if (root.val == val) {
            return root;
        }else {
            TreeNode left = find(root.left, val);
            TreeNode right = find(root.right, val);
            return left != null ? left : right;
        }
    }

二叉树基本操作完整代码

点击这里查看


相关推荐
超级小忍16 分钟前
服务端向客户端主动推送数据的几种方法(Spring Boot 环境)
java·spring boot·后端
程序无bug20 分钟前
Spring IoC注解式开发无敌详细(细节丰富)
java·后端
小莫分享22 分钟前
Java Lombok 入门
java
程序无bug22 分钟前
Spring 对于事务上的应用的详细说明
java·后端
食亨技术团队24 分钟前
被忽略的 SAAS 生命线:操作日志有多重要
java·后端
苦学编程的谢38 分钟前
Maven
java·maven·intellij-idea
考虑考虑40 分钟前
Maven 依赖范围(Scope)
java·后端·maven
chao_78943 分钟前
回溯题解——子集【LeetCode】二进制枚举法
开发语言·数据结构·python·算法·leetcode
张小洛1 小时前
Spring AOP 设计解密:代理对象生成、拦截器链调度与注解适配全流程源码解析
java·后端·spring·spring aop·aop
秋说1 小时前
【PTA数据结构 | C语言版】将数组中元素反转存放
c语言·数据结构·算法