概述

二叉树是树的一种特殊形式,‘二’意味着每个结点最多有2个的子节点树(tree。除了根节点以外每个节点都只有一个父亲节点

image.png
二叉树的节点的两个孩子节点,左边的子节点称为左孩子,右边的子节点称为右孩子,具体类表示为如下

  1. class Node {
  2. E e;
  3. Node left;
  4. Node right;
  5. }

根据左右子树特点,分为以下特殊的二叉树

分类 说明
满二叉树 一个二叉树所有非叶子节点都存在左右子树,并且所有的叶子节点都在同一层级上
完全二叉树 和满二叉树类似,区别在与最底层,也就是叶子节点那一层,叶子节点从左到友依次排列,不需要填满二叉树
平衡二叉树 左右子树的高度差至多为1,也就是说任何节点的左子树的叶子节点到节点的最长距离,不能超过右子树的叶子节点到节点的最长距离+1


验证二叉搜索树

https://leetcode-cn.com/problems/validate-binary-search-tree/
图片.png

递归

  1. class Solution {
  2. public boolean helper(TreeNode node, Integer lower, Integer upper) {
  3. if (node == null) return true;
  4. int val = node.val;
  5. if (lower != null && val <= lower) return false;
  6. if (upper != null && val >= upper) return false;
  7. if (! helper(node.right, val, upper)) return false;
  8. if (! helper(node.left, lower, val)) return false;
  9. return true;
  10. }
  11. public boolean isValidBST(TreeNode root) {
  12. return helper(root, null, null);
  13. }
  14. }

复杂度分析

  • 时间复杂度 : O(N)O(N)。每个结点访问一次。
  • 空间复杂度 : O(N)O(N)。我们跟进了整棵树。

    迭代

  1. class Solution {
  2. LinkedList<TreeNode> stack = new LinkedList();
  3. LinkedList<Integer> uppers = new LinkedList(),
  4. lowers = new LinkedList();
  5. public void update(TreeNode root, Integer lower, Integer upper) {
  6. stack.add(root);
  7. lowers.add(lower);
  8. uppers.add(upper);
  9. }
  10. public boolean isValidBST(TreeNode root) {
  11. Integer lower = null, upper = null, val;
  12. update(root, lower, upper);
  13. while (!stack.isEmpty()) {
  14. root = stack.poll();
  15. lower = lowers.poll();
  16. upper = uppers.poll();
  17. if (root == null) continue;
  18. val = root.val;
  19. if (lower != null && val <= lower) return false;
  20. if (upper != null && val >= upper) return false;
  21. update(root.right, val, upper);
  22. update(root.left, lower, val);
  23. }
  24. return true;
  25. }
  26. }

中序遍历

左子树 -> 结点 -> 右子树的顺序。
上面的结点按照访问的顺序标号,你可以按照 1-2-3-4-5 的顺序来比较不同的策略。
左子树 -> 结点 -> 右子树 意味着对于二叉搜索树而言,每个元素都应该比下一个元素小。
因此,具有 {O}(N)O(N) 时间复杂度与 {O}(N)O(N) 空间复杂度的算法十分简单:
计算中序遍历列表 inorder.
检查 inorder中的每个元素是否小于下一个。
我们需要保留整个inorder列表吗?事实上不需要。每一步最后一个添加的元素就足以保证树是(或不是)二叉搜索树。因此,我们可以将步骤整合并复用空间。

  1. class Solution {
  2. public boolean isValidBST(TreeNode root) {
  3. Stack<TreeNode> stack = new Stack();
  4. double inorder = - Double.MAX_VALUE;
  5. while (!stack.isEmpty() || root != null) {
  6. while (root != null) {
  7. stack.push(root);
  8. root = root.left;
  9. }
  10. root = stack.pop();
  11. // If next element in inorder traversal
  12. // is smaller than the previous one
  13. // that's not BST.
  14. if (root.val <= inorder) return false;
  15. inorder = root.val;
  16. root = root.right;
  17. }
  18. return true;
  19. }
  20. }

复杂度分析
时间复杂度 : 最坏情况下(树为二叉搜索树或破坏条件的元素是最右叶结点)为 {O}(N)O(N)。
空间复杂度 : {O}(N)O(N) 用于存储 stack。

存储方式

链式存储结构

二叉树的链式存储结构中,每一个结点包含三个关键属性:指向左子树的指针,数据域,指向右子树的指针;根据这个叙述,我们可以按如下结构定义结点。
image.png

结点定义
  1. public class BinaryTree <T>{
  2. private BinaryTree<T> leftChild;
  3. private BinaryTree<T> rightChild;
  4. private T data;
  5. public BinaryTree() {
  6. }
  7. public BinaryTree(T data) {
  8. this(data, null, null);
  9. }
  10. public BinaryTree(T data, BinaryTree<T> leftChild, BinaryTree<T> rightChild) {
  11. this.leftChild = leftChild;
  12. this.rightChild = rightChild;
  13. this.data = data;
  14. }
  15. public T getData() {
  16. return data;
  17. }
  18. public BinaryTree<T> getLeftChild() {
  19. return leftChild;
  20. }
  21. public BinaryTree<T> getRightChild() {
  22. return rightChild;
  23. }
  24. }

顺序存储结构

基于数组的顺序存储
image.png
按照层级顺序把二叉树的节点放到数组对应的位置上,如果某一个节点的左孩子或右孩子空缺,则数组对应的位置也空出来。
父节点的下标是parent,那么它的左孩子节点下标是2_parent+1;右孩子节点下标是就是2_parnet+2
反过来假设一个左孩子节点是lefChild,那么它的父节点下标就是(leftChild-1)/2

二叉树的最大深度

通过递归方式实现数的最大深度

  1. class Solution {
  2. public int maxDepth(TreeNode root) {
  3. if (root == null) {
  4. return 0;
  5. } else {
  6. int left_height = maxDepth(root.left);
  7. int right_height = maxDepth(root.right);
  8. return java.lang.Math.max(left_height, right_height) + 1;
  9. }
  10. }
  11. }

我们的想法是使用 DFS 策略访问每个结点,同时在每次访问时更新最大深度。
所以我们从包含根结点且相应深度为 1 的栈开始。然后我们继续迭代:将当前结点弹出栈并推入子结点。每一步都会更新深度。

  1. import javafx.util.Pair;
  2. import java.lang.Math;
  3. class Solution {
  4. public int maxDepth(TreeNode root) {
  5. Queue<Pair<TreeNode, Integer>> stack = new LinkedList<>();
  6. if (root != null) {
  7. stack.add(new Pair(root, 1));
  8. }
  9. int depth = 0;
  10. while (!stack.isEmpty()) {
  11. Pair<TreeNode, Integer> current = stack.poll();
  12. root = current.getKey();
  13. int current_depth = current.getValue();
  14. if (root != null) {
  15. depth = Math.max(depth, current_depth);
  16. stack.add(new Pair(root.left, current_depth + 1));
  17. stack.add(new Pair(root.right, current_depth + 1));
  18. }
  19. }
  20. return depth;
  21. }
  22. };

复杂度分析

时间复杂度:O(N)O(N)。
空间复杂度:O(N)O(N)。

二叉树遍历

遍历是一种线性操作,数组和链表都是线性结构,对其进行遍历是非常容易的事情,而二叉树是典型的非线性数据结构,因此遍历时需要把非线性关联的节点转成一个线性的序列。

前序遍历:对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。
中序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。
后序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。

  1. BinaryTree<String> nodeH = new BinaryTree<>("H");
  2. BinaryTree<String> nodeG = new BinaryTree<>("G");
  3. BinaryTree<String> nodeF = new BinaryTree<>(nodeH, "F", null);
  4. BinaryTree<String> nodeE = new BinaryTree<>(nodeG, "E", null);
  5. BinaryTree<String> nodeD = new BinaryTree<>("D");
  6. BinaryTree<String> nodeC = new BinaryTree<>(null, "C", nodeF);
  7. BinaryTree<String> nodeB = new BinaryTree<>(nodeD, "B", nodeE);
  8. BinaryTree<String> root = new BinaryTree<>(nodeB, "A", nodeC);

前序遍历

输出顺序是根节点,左子树,右子树。即根节点—>左子树—>右子树

  1. public static void preTraversal(BinaryTree node) {
  2. if (node != null) {
  3. System.out.print(node.getData().toString());
  4. preTraversal(node.getLeftChild());
  5. preTraversal(node.getRightChild());
  6. }
  7. }

非递归

  1. public static void preTraversalIteration(BinaryTree node) {
  2. // 创建一个栈
  3. Stack<BinaryTree> mStack = new Stack<>();
  4. while (true) {
  5. while (node != null) { // 非叶子结点的子树
  6. // 前序遍历,先访问根结点
  7. System.out.print(node.getData().toString());
  8. // 将当前结点压入栈
  9. mStack.push(node);
  10. // 对左子树继续进行前序遍历
  11. node=node.getLeftChild();
  12. }
  13. if (mStack.isEmpty()) {
  14. //所有元素已遍历完成
  15. break;
  16. }
  17. // 弹出栈顶结点
  18. node=mStack.pop();
  19. // 右子树前序遍历
  20. node=node.getRightChild();
  21. }
  22. }

中序遍历

输出顺序是左子树,根节点,右子树,即左子树—>根节点—>右子树
步骤:首先访问根节点的左孩子,如果左孩子还拥有左孩子,则继续深入访问下去,一直找不到左孩子的节点,并输出该节点

递归方法实现

  1. public static void preTraversal(BinaryTree node) {
  2. if (node != null) {
  3. preTraversal(node.getLeftChild());
  4. System.out.print(node.getData().toString());
  5. preTraversal(node.getRightChild());
  6. }
  7. }

堆栈实现

  1. /**
  2. * 中序遍历-迭代实现
  3. * @param node
  4. */
  5. public static void TraversalIteration(BinaryTree node) {
  6. // 创建一个栈
  7. Stack<BinaryTree> mStack = new Stack<>();
  8. while (true) {
  9. while (node != null) { // 非叶子结点的子树
  10. // 将当前结点压入栈
  11. mStack.push(node);
  12. // 对左子树继续进行中序遍历
  13. node=node.getLeftChild();
  14. }
  15. if (mStack.isEmpty()) {
  16. //所有元素已遍历完成
  17. break;
  18. }
  19. // 弹出栈顶结点
  20. node=mStack.pop();
  21. // 中序遍历,访问根结点
  22. System.out.print(node.getData().toString());
  23. // 右子树中序遍历
  24. node=node.getRightChild();
  25. }
  26. }

后序遍历

输出顺序是左子树,右子树,根节点,即左子树—>右子树—>根节点

  1. public static void postTraversal(BinaryTree node) {
  2. if (node != null) {
  3. preTraversal(node.getLeftChild());
  4. preTraversal(node.getRightChild());
  5. System.out.print(node.getData().toString());
  6. }
  7. }
  1. /**
  2. * 后序遍历-迭代实现
  3. * @param node
  4. */
  5. public static void postTraversalIteration(BinaryTree node) {
  6. // 创建一个栈
  7. Stack<BinaryTree> mStack = new Stack<>();
  8. while (true) {
  9. if (node != null) {
  10. //当前结点非空,压入栈
  11. mStack.push(node);
  12. // 左子树继续遍历
  13. node=node.getLeftChild();
  14. }else {
  15. // 左子树为空
  16. if(mStack.isEmpty()){
  17. return;
  18. }
  19. if (mStack.lastElement().getRightChild() == null) {
  20. // 栈顶元素右子树为空,则当前结点为叶子结点,输出
  21. node=mStack.pop();
  22. System.out.print(node.getData().toString());
  23. while (node == mStack.lastElement().getRightChild()) {
  24. System.out.print(mStack.lastElement().getData().toString());
  25. node=mStack.pop();
  26. if (mStack.isEmpty()) {
  27. break;
  28. }
  29. }
  30. }
  31. if (!mStack.isEmpty()) {
  32. node=mStack.lastElement().getRightChild();
  33. }else {
  34. node=null;
  35. }
  36. }
  37. }
  38. }

层次遍历

层序遍历就是从上到下按层,从左至右依次访问每个结点。这种遍历非常用规律,就是从根节点下一层开始,优先访问每一层所有的双亲结点,然后依次访问每个结点的左右儿子。也就是说,从上到下,先遇见到结点先访问,后遇到的结点后访问,这典型的就是队列的思想

  1. public static void levelTraversal(BinaryTree node) {
  2. //创建队列
  3. Queue<BinaryTree> mNodeQueue = new LinkedList<>();
  4. // 根结点加入队列
  5. mNodeQueue.add(node);
  6. BinaryTree temp;
  7. while (!mNodeQueue.isEmpty()) {
  8. //元素出队列
  9. temp=mNodeQueue.poll();
  10. //输出
  11. System.out.print(temp.getData().toString());
  12. if (temp.getLeftChild() != null) {
  13. // 左子树入队列
  14. mNodeQueue.add(temp.getLeftChild());
  15. }
  16. if (temp.getRightChild() != null) {
  17. //右子树入队列
  18. mNodeQueue.add(temp.getRightChild());
  19. }
  20. }
  21. }

遍历方法总结

递归

递归方法最为直观易懂,但考虑到效率,我们通常不推荐使用递归。

  1. class Solution {
  2. public List < Integer > inorderTraversal(TreeNode root) {
  3. List < Integer > res = new ArrayList < > ();
  4. helper(root, res);
  5. return res;
  6. }
  7. public void helper(TreeNode root, List < Integer > res) {
  8. if (root != null) {
  9. if (root.left != null) {
  10. helper(root.left, res);
  11. }
  12. res.add(root.val);
  13. if (root.right != null) {
  14. helper(root.right, res);
  15. }
  16. }
  17. }
  18. }

时间复杂度:O(n)O(n)。递归函数 T(n) = 2 \cdot T(n/2)+1T(n)=2⋅T(n/2)+1。
空间复杂度:最坏情况下需要空间O(n)O(n),平均情况为O(\log n)O(logn)。

利用栈,去模拟递归。递归压栈的过程,就是保存现场,就是保存当前的变量,而解法一中当前有用的变量就是 node,所以我们用栈把每次的 node 保存起来即可。
模拟下递归的过程,只考虑 node 的压栈。

  1. //当前节点为空,出栈
  2. if (node == null) {
  3. return;
  4. }
  5. //当前节点不为空
  6. getAns(node.left, ans); //压栈
  7. ans.add(node.val); //出栈后添加
  8. getAns(node.right, ans); //压栈
  9. //左右子树遍历完,出栈
  1. 1
  2. / \
  3. 2 3
  4. / \ /
  5. 4 5 6
  6. push push push pop pop push pop pop
  7. | | | | |_4_| | | | | | | | | | |
  8. | | |_2_| |_2_| |_2_| | | |_5_| | | | |
  9. |_1_| |_1_| |_1_| |_1_| |_1_| |_1_| |_1_| | |
  10. ans add 4 add 2 add 5 add 1
  11. [] [4] [4 2] [4 2 5] [4 2 5 1]
  12. push push pop pop
  13. | | | | | | | |
  14. | | |_6_| | | | |
  15. |_3_| |_3_| |_3_| | |
  16. add 6 add 3
  17. [4 2 5 1 6] [4 2 5 1 6 3]
public class Solution {
   public List < Integer > inorderTraversal(TreeNode root) {
       List < Integer > res = new ArrayList < > ();
       Stack < TreeNode > stack = new Stack < > ();
       TreeNode curr = root;
       while (curr != null || !stack.isEmpty()) {
           while (curr != null) {
               stack.push(curr);
               curr = curr.left;
           }
           curr = stack.pop();
           res.add(curr.val);
           curr = curr.right;
       }
       return res;
   }
}

复杂度分析
时间复杂度:O(n)O(n)。
空间复杂度:O(n)O(n)。

莫里斯遍历

使用一种新的数据结构:线索二叉树。方法如下:
Step 1: 将当前节点current初始化为根节点
Step 2: While current不为空,
若current没有左子节点
a. 将current添加到输出
b. 进入右子树,亦即, current = current.right
否则
a. 在current的左子树中,令current成为最右侧节点的右子节点
b. 进入左子树,亦即,current = current.left
举例而言:

     1<br />       /   \<br />      2     3<br />     / \   /<br />    4   5 6

首先,1 是根节点,所以将 current 初始化为 1。1 有左子节点 2,current 的左子树是

    2<br />       / \<br />      4   5<br />在此左子树中最右侧的节点是 5,于是将 current(1) 作为 5 的右子节点。令 current = cuurent.left (current = 2)。<br />现在二叉树的形状为:

    2<br />       / \<br />      4   5<br />           \<br />            1<br />             \<br />              3<br />             /<br />            6<br />对于 current(2),其左子节点为4,我们可以继续上述过程

   4<br />        \<br />         2<br />          \<br />           5<br />            \<br />             1<br />              \<br />               3<br />              /<br />             6<br />由于 4 没有左子节点,添加 4 为输出,接着依次添加 2, 5, 1, 3 。节点 3 有左子节点 6,故重复以上过程。<br />最终的结果是 [4,2,5,1,6,3]。
class Solution {
   public List < Integer > inorderTraversal(TreeNode root) {
       List < Integer > res = new ArrayList < > ();
       TreeNode curr = root;
       TreeNode pre;
       while (curr != null) {
           if (curr.left == null) {
               res.add(curr.val);
               curr = curr.right; // move to next right node
           } else { // has a left subtree
               pre = curr.left;
               while (pre.right != null) { // find rightmost
                   pre = pre.right;
               }
               pre.right = curr; // put cur after the pre node
               TreeNode temp = curr; // store cur node
               curr = curr.left; // move cur to the top of the new tree
               temp.left = null; // original cur left be null, avoid infinite loops
           }
       }
       return res;
   }
}

复杂度分析
时间复杂度:O(n)O(n)。 想要证明时间复杂度是O(n)O(n),最大的问题是找到每个节点的前驱节点的时间复杂度。乍一想,找到每个节点的前驱节点的时间复杂度应该是 O(n\log n)O(nlogn),因为找到一个节点的前驱节点和树的高度有关。
但事实上,找到所有节点的前驱节点只需要O(n)O(n) 时间。一棵 nn 个节点的二叉树只有 n-1n−1 条边,每条边只可能使用2次,一次是定位节点,一次是找前驱节点。
故复杂度为 O(n)O(n)。
空间复杂度:O(n)O(n)。使用了长度为 nn 的数组。

// 莫里斯遍历利用叶子节点左右空域存储遍历前驱和后继
// 达到时间复杂度O(N),空间复杂度O(1)
// 二叉树的串行遍历
// 莫里斯中序遍历
func MorrisTraverMid(root *TreeNode) []int {
    if root == nil {
 return nil
    }
    var result []int
    // 游标节点初始化为根节点
    cur := root
    // 定义前驱节点
    var pre *TreeNode
    // 当没有遍历到最后情况
    for cur != nil {
 // 当游标节点没有左孩子
 if cur.Left == nil {
     // 加游标节点值加入结果集(visit)
     result = append(result, cur.Val)
     // 因为没有左孩子,进入右孩子
     cur = cur.Right
     continue
 }
 // 当游标有左孩子
 // 1.找到左子树最右节点作为游标节点前驱
 // 得到左子树
 pre = cur.Left
 // 找到左子树最右叶子节点
 for pre.Right != nil && pre.Right != cur {
     pre = pre.Right
 }
 // 最右叶子节点
 if pre.Right == nil {
     // 最右叶子节点与cur连接
     pre.Right = cur
     // 进入左子树
     cur = cur.Left
     continue
 }
 // 最右节点与cur相等(成环情况)
 // visit cur
 result = append(result, cur.Val)
 // 破环
 pre.Right = nil
 // 进入右子树
 cur = cur.Right
    }
    return result
}

标记法

使用颜色标记节点的状态,新节点为白色,已访问的节点为灰色。
如果遇到的节点为白色,则将其标记为灰色,然后将其右子节点、自身、左子节点依次入栈。
如果遇到的节点为灰色,则将节点的值输出。

class Solution {
    class ColorNode {
        TreeNode node;
        String color;

        public ColorNode(TreeNode node,String color){
            this.node = node;
            this.color = color;
        }
    }
    public List<Integer> inorderTraversal(TreeNode root) {
        if(root == null) return new ArrayList<Integer>();

        List<Integer> res = new ArrayList<>();
        Stack<ColorNode> stack = new Stack<>();
        stack.push(new ColorNode(root,"white"));

        while(!stack.empty()){
            ColorNode cn = stack.pop();

            if(cn.color.equals("white")){
                if(cn.node.right != null) stack.push(new ColorNode(cn.node.right,"white"));
                stack.push(new ColorNode(cn.node,"gray"));
                if(cn.node.left != null)stack.push(new ColorNode(cn.node.left,"white"));
            }else{
                res.add(cn.node.val);
            }
        }

        return res;
    }
}