二叉树

417 阅读5分钟
二叉树的节点结构

今天我们来看一下二叉树。二叉树的节点结构是:

    public static class Node{
        public int value;
        public Node left;
        public Node right;
        public Node(int data){
            this.value = data;
        }    
    }
前中后序遍历(递归实现)

用递归的方式实现前中后序遍历,这里讲解一下前序遍历,中序后序可以类推: 在调用前序遍历的时候,如果现在我们不打印第一层次,而是每一层次都打印的话,应该是这样的

image.png

全层次打印的话:1 2 4 4 4 2 5 5 5 2 1 3 6 6 6 3 7 7 7 3 1

注意到每一个会打印三次,而前序遍历只需要打印出来第一次出现的数就可以也就是:1 2 4 5 3 6 7

注意:打印的是节点的值而不是节点本身

相关的代码是:

    public static void main(String[] args) {
        Node head = new Node(5);
        head.left = new Node(3);
        head.right = new Node(8);
        head.left.left = new Node(2);
        head.left.right = new Node(4);
        head.left.left.left = new Node(1);
        head.right.left = new Node(7);
        head.right.left.left = new Node(6);
        head.right.right = new Node(10);
        head.right.right.left = new Node(9);
        head.right.right.right = new Node(11);
        System.out.println("==============用递归做==============");
        System.out.print("pre-order:");
        preOrderRecur(head);
        System.out.println();
        System.out.print("in-order:");
        inOrderRecur(head);
        System.out.println();
        System.out.print("pos-order:");
        posOrderRecur(head);
    }
    
    public static class Node{
        public int value;
        public Node left;
        public Node right;
        public Node(int data){
            this.value = data;
        }
    }
    //前序
    public static void preOrderRecur(Node head){
        if (head == null){
            return;
        }
        //第一层次打印出来
        System.out.print(head.value + " ");
        preOrderRecur(head.left);
        preOrderRecur(head.right);
    }
    //中序
    public static void inOrderRecur(Node head){
        if (head == null){
            return;
        }
        inOrderRecur(head.left);
        //第二层次打印出来
        System.out.print(head.value + " ");
        inOrderRecur(head.right);
    }
    //后序
    public static void posOrderRecur(Node head){
        if (head == null){
            return;
        }
        posOrderRecur(head.left);
        posOrderRecur(head.right);
        ////第三层次打印出来
        System.out.print(head.value + " ");
    }

那么不用递归怎么实现呢?

这里可以用栈,可以记住,比较难想。

前序遍历(深度优先遍历)(dfs)
  • 前提条件:先将根结点压入栈中
    1. 从栈中弹出一个节点cur
    1. 打印cur
    1. 这个节点cur如果有对应的,先压进栈它的右节点后压入它的左节点
    1. 回到1.直到栈为空 image.png

相关的代码是:

    public static void preOrderUnRecur(Node head){
        if (head != null){
            Stack<Node> stack = new Stack<>();
            //先将头结点压入栈,作为前提条件
            stack.add(head);
            while (!stack.isEmpty()){
                //弹出
                head = stack.pop();
                System.out.print(head.value + " ");
                if (head.right != null){
                    //压入右节点
                    stack.push(head.right);
                }
                if (head.left != null){
                    //压入左节点
                    stack.push(head.left);
                }
            }
        }
    }
后序遍历

我们发现前序遍历是先压右节点再压左节点,最后得到的是前序遍历也就是前左右,那么如果我们先压左节点后压右节点,就会得到前右左,我们会发现将前右左反转之后就是左右前,也即后序遍历,所以我们准备一个收集栈来收集原来栈弹出的节点。

  • 前提条件:先将根结点压入原始栈
    1. 原始栈中弹出一个节点cur,并放到收集栈
    1. 这个节点cur如果有对应的,先压进原始栈它的左节点后压入它的右节点
    1. 回到步骤1.直到原始栈为空
    1. 单独打印收集栈中的东西 相关的代码是:
    public static void posOrderUnRecur(Node head){
        if (head != null){
            Stack<Node> s1 = new Stack<>();
            Stack<Node> s2 = new Stack<>();
            //前提条件,先将头节点压入栈
            s1.push(head);
            while (!s1.isEmpty()){
                //弹出
                head = s1.pop();
                s2.push(head);
                if (head.left != null){
                    //压入左节点
                    s1.push(head.left);
                }
                if (head.right != null){
                    //压入右节点
                    s1.push(head.right);
                }
            }
            while (!s2.isEmpty()){
                System.out.print(s2.pop().value + " ");
            }
        }
    }
中序遍历
    1. 每棵子树的整棵树左边界进栈
    1. 从栈中弹出一个节点cur,并打印
    1. 这个节点cur如果有对应的右树,将它的右树返回步骤1.

image.png

相关的代码是:

    public static void inOrderUnRecur(Node head){
        if (head != null){
            Stack<Node> stack = new Stack<>();
            while (!stack.isEmpty() || head != null){
                //左边界全进栈
                if (head != null){
                    stack.push(head);
                    head = head.left;
                }else {
                    //弹出
                    head = stack.pop();
                    System.out.print(head.value + " ");
                    //将其的左边界全进栈
                    head = head.right;
                }
            }
        }
    }

代码集锦,包括用递归和不用递归的:

    public static void main(String[] args) {
        Node head = new Node(5);
        head.left = new Node(3);
        head.right = new Node(8);
        head.left.left = new Node(2);
        head.left.right = new Node(4);
        head.left.left.left = new Node(1);
        head.right.left = new Node(7);
        head.right.left.left = new Node(6);
        head.right.right = new Node(10);
        head.right.right.left = new Node(9);
        head.right.right.right = new Node(11);
        System.out.println("==============用递归做==============");
        System.out.print("pre-order:");
        preOrderRecur(head);
        System.out.println();
        System.out.print("in-order:");
        inOrderRecur(head);
        System.out.println();
        System.out.print("pos-order:");
        posOrderRecur(head);
        System.out.println();
        System.out.println("==============不用递归做==============");
        System.out.print("pre-order:");
        preOrderUnRecur(head);
        System.out.println();
        System.out.print("in-order:");
        inOrderUnRecur(head);
        System.out.println();
        System.out.print("pos-order:");
        posOrderUnRecur(head);
    }
    
    public static class Node{
        public int value;
        public Node left;
        public Node right;
        public Node(int data){
            this.value = data;
        }
    }

    public static void preOrderRecur(Node head){
        if (head == null){
            return;
        }
        //第一层次打印出来
        System.out.print(head.value + " ");
        preOrderRecur(head.left);
        preOrderRecur(head.right);
    }

    public static void inOrderRecur(Node head){
        if (head == null){
            return;
        }
        inOrderRecur(head.left);
        //第二层次打印出来
        System.out.print(head.value + " ");
        inOrderRecur(head.right);
    }

    public static void posOrderRecur(Node head){
        if (head == null){
            return;
        }
        posOrderRecur(head.left);
        posOrderRecur(head.right);
        ////第三层次打印出来
        System.out.print(head.value + " ");
    }

    public static void preOrderUnRecur(Node head){
        if (head != null){
            Stack<Node> stack = new Stack<>();
            //先将头结点压入,作为前提条件
            stack.add(head);
            while (!stack.isEmpty()){
                head = stack.pop();
                System.out.print(head.value + " ");
                if (head.right != null){
                    stack.push(head.right);
                }
                if (head.left != null){
                    stack.push(head.left);
                }
            }
        }
    }

    public static void posOrderUnRecur(Node head){
        if (head != null){
            Stack<Node> s1 = new Stack<>();
            Stack<Node> s2 = new Stack<>();
            //前提条件,先将头节点压入
            s1.push(head);
            while (!s1.isEmpty()){
                head = s1.pop();
                s2.push(head);
                if (head.left != null){
                    s1.push(head.left);
                }
                if (head.right != null){
                    s1.push(head.right);
                }
            }
            while (!s2.isEmpty()){
                System.out.print(s2.pop().value + " ");
            }
        }
    }

    public static void inOrderUnRecur(Node head){
        if (head != null){
            Stack<Node> stack = new Stack<>();
            while (!stack.isEmpty() || head != null){
                //左边界全进栈
                if (head != null){
                    stack.push(head);
                    head = head.left;
                }else {
                    head = stack.pop();
                    System.out.print(head.value + " ");
                    head = head.right;
                }
            }
        }
    }
宽度优先遍历(BFS)
  • 前提条件:先将根结点压入队列
    1. 从队列中弹出一个节点cur, 并打印
    1. 这个节点cur如果有对应的,先压进队列它的左节点后压入它的右节点
    1. 返回步骤1.直到队列为空

image.png

相关的代码是:

    public static void widthOrder(Node head){
        if (head == null){
            return;
        }
        //注意:java中的双向链表可以实现队列
        Queue<Node> queue = new LinkedList<>();
        //先将头结点压入,作为前提条件
        queue.add(head);
        while (!queue.isEmpty()){
            //弹出队列的头
            head = queue.poll();
            System.out.print(head.value + " ");
            if (head.left != null){
                //加入左节点
                queue.add(head.left);
            }
            if (head.right != null){
                //加入右节点
                queue.add(head.right);
            }
        }
    }
二叉搜索树

二叉搜索树的定义:Binary Search Tree

  • 节点的左子树只包含小于当前节点的数。
  • 节点的右子树只包含大于当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。 二叉搜素树的中序遍历是有顺序的

image.png

    //是否是搜索二叉树可以依据中序遍历改,如果中序遍历是升序就是搜索二叉树,这里给出三种方法
    //方法1:从递归上面改
    public static int preValue = Integer.MIN_VALUE;
    public static boolean isBST(Node head){
        if (head == null){
            return true;
        }
        boolean isLeftBst = isBST(head.left);
        if (!isLeftBst ){
            return false;
        }
        if (head.value <= preValue){
            return false;
        }else {
            preValue = head.value;
        }
        return isBST(head.right);
    }

    //方法2:用非递归改
    public static boolean isBST2(Node head){
        if (head != null){
            int preValue = Integer.MIN_VALUE;
            Stack<Node> stack = new Stack<>();
            while (!stack.isEmpty() || head != null){
                if (head != null){
                    stack.push(head);
                    head = head.left;
                }else {
                    head = stack.pop();
                    if (head.value <= preValue){
                        return false;
                    }else {
                        preValue = head.value;
                    }
                    head = head.right;
                }
            }
        }
        return true;
    }

    //方法3:判断最后输出的中序遍历是不是升序的
    public static boolean isBST3(Node head){
        if (head == null){
            return true;
        }
        LinkedList<Node> inOrdeList = new LinkedList<>();
        process(head, inOrdeList);
        int pre = Integer.MIN_VALUE;
        for (Node cur : inOrdeList){
            if (pre >= cur.value){
                return false;
            }
            pre = cur.value;
        }
        return true;
    }

    public static void process(Node node, LinkedList<Node> inOrdeList) {
        if (node == null){
            return;
        }
        process(node.left, inOrdeList);
        inOrdeList.add(node);
        process(node.right, inOrdeList);
    }
完全二叉树

完全二叉树的定义:Complete Binary Tree

  • 除了最底层节点可能没填满外,其余每层节点数都达到最大值
  • 最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。 可以简记为:
  • 任一节点不会存在有右节点没有左节点
  • 如果遇到了第一个左右节点不全的节点,那么后续的节点都是叶节点

image.png

    //是否是完全二叉树
    public static boolean isCBT(Node head) {
        if (head == null) {
            return true;
        }
        LinkedList<Node> queue = new LinkedList<>();
        //用于界定是否第一次遇到了有左节点没有右节点的节点
        boolean leaf = false;
        Node l = null;
        Node r = null;
        queue.add(head);
        while (!queue.isEmpty()) {
            head = queue.pop();
            l = head.left;
            r = head.right;
            //如果该节点是左节点为空,右节点不为空返回false,如果已经遇到了有左节点没有右节点的节点,即leaf为true
            //那么之后的节点必须都为叶节点,也即之后的节点没有左节点和右节点
            if ((l == null && r != null) || (leaf && (l != null || r != null))) {
                return false;
            }
            if (l != null) {
                queue.add(l);
            }
            if (r != null) {
                queue.add(r);
            } else {
                leaf = true;
            }
        }
        return true;
    }
平衡二叉树

平衡二叉树的定义:

  • 任意节点的子树的高度差都小于等于1
  • 常见的符合平衡树的有,B树(多路平衡搜索树)、AVL树(二叉平衡搜索树)等。 image.png

接下来讲一个适合二叉树的套路:

  • 整理题目的条件信息
  • 左树和右树需要返回的信息,左右树返回的信息要一样
  • 可以用递归做 那么这个题就可以整理为:
  1. 整个题目的条件信息为:左子树是平衡二叉树、右子树是平衡二叉树、|左高 - 右高| <= 1
  2. 左树返回左树是否是平衡二叉树、高度是多少
  3. 右树返回右树是否是平衡二叉树、高度是多少
    public static class Node {
	public int value;
	public Node left;
        public Node right;

	public Node(int data) {
            this.value = data;
	}
    }
    
    //判断是不是平衡树的方法
    public static boolean isBalanced(Node head) {
	return process(head).isBalanced;
    }
    
    //自定义的返回结构,是否是平衡二叉树以及相应的高度
    public static class ReturnType {
	public boolean isBalanced;
	public int height;

	public ReturnType(boolean isB, int hei) {
		isBalanced = isB;
		height = hei;
	}
    }
    
    //递归操作
    public static ReturnType process(Node x) {
	if (x == null) {
		return new ReturnType(true, 0);
	}
        //返回对应的高度、是否是平衡二叉树
	ReturnType leftData = process(x.left);
	ReturnType rightData = process(x.right);
        //以整体来看,返回左右树是否是平衡二叉树以及对应的高度后,判断整体的高度也就可以知道
        //,整体是不是平衡二叉树也就知道了
        //加1是因为包括自己的节点
	int height = Math.max(leftData.height, rightData.height) + 1;
	boolean isBalanced = leftData.isBalanced && rightData.isBalanced && Math.abs(leftData.height - rightData.height) < 2;
	return new ReturnType(isBalanced, height);
    }

这个套路也可以解答是否是搜索二叉树:

  1. 整个题目的条件信息为:左树是搜索二叉树、右树是搜索二叉树、左树的max < x < 右树的min

  2. 左树返回是否是搜索二叉树以及max值

  3. 右树返回是否是搜索二叉树以及min值 但是左树和右树返回的信息就不一样了,所以改成

  4. 左树返回是否是搜索二叉树以及max值、min值

  5. 右树返回是否是搜索二叉树以及max值、min值

    public static boolean isValidBST(Node root) {
        if(root == null){
            return true;
        }
        return(process(root).isBST);
    }
    
    public static class ReturnType{
        public boolean isBST;
        public int min;
        public int max;
        public ReturnType(boolean is, int mi, int ma){
            isBST = is;
            min = mi;
            max = ma;
        }
    }

    public static ReturnType process(Node x){
        //因为是空的话,min和max没法填写,所以写成了null,
        //但是写成null之后,底下就要多做判断,以防空指针
        if (x == null){
            return null;
        }
        ReturnType left = process(x.left);
        ReturnType right = process(x.right);
        //先给min 和 max设置初始值,作为比较
        int min = x.value;
        int max = x.value;
        //每一项都要排除null
        if (left != null){
            min = Math.min(min, left.min);
            max = Math.max(max, left.max);
        }
        //每一项都要排除null,以这棵树为整体求出整体min和max
        if (right != null){
            min = Math.min(min, right.min);
            max = Math.max(max, right.max);
        }
        //每一项都要排除null
        boolean isBST = true;
        //每一项都要排除null,如果左侧不是搜索二叉树以及左侧的max大于了当前值,都要返回false
        if (left != null && (!left.isBST || left.max >= x.value)){
            isBST = false;
        }
        //每一项都要排除null,如果右侧不是搜索二叉树以及右侧的min小于了当前值,都要返回false
        if (right != null && (!right.isBST || right.min <= x.value)){
            isBST = false;
        }
        return new ReturnType(isBST, min, max);
    }
满二叉树

这个套路解答是否是满二叉树:Full Binary Tree

  1. 整个题目的条件信息为:整体的个数 = 2 ^ (整体的高度) - 1;
  2. 左树返回左树的高度和节点数
  3. 右树返回右树的高度和节点数
    public static boolean isValidFBT(Node root) {
        if(root == null){
            return true;
        }
        ReturnType data = process(head);
        return data.nodes == (1 << data.height - 1);
    }
    
    public static class ReturnType{
        public int height;
        public int nodes;

        public ReturnType(int height, int nodes){
            this.height = height;
            this.nodes = nodes;
        }
    }

    public static ReturnType process(Node x){
        if (x == null){
            return new ReturnType(0, 0);
        }
        ReturnType left = process(x.left);
        ReturnType right = process(x.right);
        int height = Math.max(left.height, right.height) + 1;
        int nodes = left.nodes + right.nodes+ 1;
        return new ReturnType(height, nodes);
    }
补充题目(寻找公共祖先)

寻找公共祖先,各个节点的值是唯一的

image.png

相关的代码:法一

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
 //整体思路是:
 //将所有节点和其对应的父节点填入Map中,从map中依次找到o1的父节点的父节点...放到新的Map中,
 //最后看o2的父节点第一次在o1的父节点Map中找到时,对应的就是答案
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null || p == root || q == root){
            return root;
        }
        HashMap<TreeNode, TreeNode> fatherMap = new HashMap<>();
        fatherMap.put(root, root);
        process(root, fatherMap);
        TreeNode cur = p;
        HashSet<TreeNode> fatherSet = new HashSet<>();
        //注意:cur != fatherMap.get(cur)   和 !(fatherMap.get(cur) == root) 不同,因为右边少加了顶点下面的点
        while(cur != fatherMap.get(cur)){
            //也需要压入自己的值,因为可能它们的公共祖先就是p
            fatherSet.add(cur);
            cur = fatherMap.get(cur);
        }
        //最顶点没有压进去,所以压入
        fatherSet.add(root);
        cur = q;
        while(cur != fatherMap.get(cur)){
            if(fatherSet.contains(cur)){
                break;
            }
            cur = fatherMap.get(cur);
        }
        return cur;
    }
    
    public void process(TreeNode root, HashMap<TreeNode, TreeNode> fatherMap){
        if(root == null){
            return;
        }
        //下面这两行代码因为返回的是void所以放在哪里都可以,但是要区别前中后序遍历代码的不同
        fatherMap.put(root.left, root);
        fatherMap.put(root.right, root);
        process(root.left, fatherMap);
        process(root.right, fatherMap);
    }
}

上面有两行代码因为返回的是void所以放在哪里都可以,但是要区别前中后序遍历代码的不同,注意这个细节

相关的代码:法二

    public static Node lowestAncestor(Node head, Node o1, Node o2){
        if (head == null || head == o1 || head == o2){
            return head;
        }
        Node left = lowestAncestor(head.left, o1, o2);
        Node right = lowestAncestor(head.right, o1, o2);
        if (left != null && right != null){
            return head;
        }
        return left != null ? left : right;
    }

这个方法很难想,下面解释一下,我们知道o1, 和 o2只有两种可能

  1. 其中一个是另一个的祖先
  2. 两个节点共有另外一个祖先
  • 我们先看情况1,示意图如下:

image.png

根据代码,root节点左边返回的和右边返回的示意图是:

image.png

  • 再看情况2:示意图是:

image.png

根据代码,root节点左边返回的和右边返回的示意图是:

image.png

补充题目(序列化和反序列化)
  • 序列化指的是将二叉树以字符串的形式表示出来
  • 反序列化指的是将表达出来的字符串反转成原来的二叉树 方法:
  • 序列化:可以通过前序、中序、后序、宽度优先转化成相应的字符串,如null变成#!,其他节点变成节点的值再加上!
  • 反序列化:经过相同的前序、中序、后序、宽度优先方法转成对应的二叉树 相关的代码:
    public static class Node {
	public int value;
	public Node left;
	public Node right;

	public Node(int data) {
		this.value = data;
	}
    }
    
    
    
    //运用先序遍历序列化二叉树
    public static String serialByPre(Node head) {
	if (head == null) {
		return "#!";
	}
	String res = head.value + "!";
	res += serialByPre(head.left);
	res += serialByPre(head.right);
	return res;
    }
    
    //以!为分隔点转化成数组,并加入队列,方便取出
    //为什么反序列化分成两个函数,因为其中一个函数需要递归的操作
    //其实队列不一定必须,可以用在递归函数中设置index,用values[index++]即可
    public static Node reconByPreString(String preStr) {
	String[] values = preStr.split("!");
	Queue<String> queue = new LinkedList<String>();
	for (int i = 0; i != values.length; i++) {
                //队列的add()和offer()都是加入的方法,区别是:
                //两者都是往队列尾部插入元素,不同的是,当超出队列界限的时候,
                //add()方法是抛出异常让你处理,而offer()方法是直接返回false
		queue.offer(values[i]);
	}
	return reconPreOrder(queue);
    }
    
    //反序列化
    public static Node reconPreOrder(Queue<String> queue) {
	String value = queue.poll();
	if (value.equals("#")) {
		return null;
	}
        //形式和前序遍历一样
	Node head = new Node(Integer.valueOf(value));
	head.left = reconPreOrder(queue);
	head.right = reconPreOrder(queue);
	return head;
    }
    
    
    
    //运用宽度优先序列化二叉树
    //就按照宽度优先遍历来写,但是也有一些区别,比如
    public static String serialByLevel(Node head) {
	if (head == null) {
		return "#!";
	}
	String res = head.value + "!";
	Queue<Node> queue = new LinkedList<Node>();
	queue.offer(head);
	while (!queue.isEmpty()) {
		head = queue.poll();
		if (head.left != null) {
                        //是在这里进行字符串的相加,但是大体一样
			res += head.left.value + "!";
			queue.offer(head.left);
		} else {
			res += "#!";
		}
		if (head.right != null) {
			res += head.right.value + "!";
			queue.offer(head.right);
		} else {
			res += "#!";
		}
	}
	return res;
    }   
    
    //宽度优先和前序还是有些区别的,一是不需要递归,而是方法本身就用到了队列,需要加入节点,再弹出
    //所以反序列化的时候需要将以,分隔成数组里的值转化成节点的形式
    public static Node generateNodeByString(String val) {
        ////注意用equals()方法,因为是比较的值
	if (val.equals("#")) {
		return null;
	}
	return new Node(Integer.valueOf(val));
    }
    
    //反序列化
    public static Node reconByLevelString(String levelStr) {
	String[] values = levelStr.split("!");
	int index = 0;
	Node head = generateNodeByString(values[index++]);
	Queue<Node> queue = new LinkedList<Node>();
	if (head != null) {
		queue.offer(head);
	}
	Node node = null;
	while (!queue.isEmpty()) {
		node = queue.poll();
		node.left = generateNodeByString(values[index++]);
		node.right = generateNodeByString(values[index++]);
                //反序列化的时候也需要压入队列,这是由宽度优先这个方法决定的
		if (node.left != null) {
			queue.offer(node.left);
		}
		if (node.right != null) {
			queue.offer(node.right);
		}
	}
	return head;
    }