二叉树刷题总结

之前说到,算法是有框架的,刷题可以从二叉树开始,

因为二叉树是最容易培养框架思维的,而且大部分算法技巧,本质上都是树的遍历问题

/* 基本的二叉树节点 */
public class TreeNode {

    int val;
    TreeNode left;
    TreeNode right;

    TreeNode() {}

    TreeNode(int val) { this.val = val; }

    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

一、深度优先遍历(DFS)

深度优先搜索(DFS):主要思路是从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底…,不断递归重复此过程,直到所有的顶点都遍历完成,它的特点是不撞南墙不回头,先走完一条路,再换一条路继续走。

1.1、二叉树遍历框架(递归)

二叉树遍历框架,典型的非线性递归遍历结构(深度优先搜索)。

public static void recursionTraversal(TreeNode root) {
    if (root == null) {
        return;
    }

    //前序遍历
    //System.out.println(root.getVal());

    recursionTraversal(root.getLeft());

    //中序遍历
    //System.out.println(root.getVal());

    recursionTraversal(root.getRight());

    //后序遍历
    System.out.println(root.getVal());

}

框架在这,做题时就试着用这个来解题。

1.2、二叉树遍历框架(迭代)

虽然递归遍历用的最多,但只会一种方式也不好,来看一下迭代的方式。

它还是深度优先遍历。

两种方式是等价的,区别在于递归的时候隐式地维护了一个栈,而我们在迭代的时候需要显式地将这个栈模拟出来,其他都相同,具体实现可以看下面的代码。

整体思路还是比较清晰的,使用栈来将要遍历的节点压栈,然后出栈后检查此节点是否还有未遍历的节点,有的话压栈,没有的话不断回溯(出栈)。

package com.wlw.binaryTree;

import com.wlw.base.TreeNode;
import java.util.Deque;
import java.util.LinkedList;

/**
 * 二叉树的遍历:前序、中序、后序 (迭代)
 * 先根次序:访问根结点,遍历左子树,遍历右子树。
 * 中根次序:遍历左子树,访问根结点,遍历右子树。
 * 后根次序:遍历左子树,遍历右子树,访问根结点。
 *
 * 栈的性质,先进后出,只需看下Java里对于他的方法有哪些
 * Stack的入栈和出栈的操作:
 * ①把元素压栈:push(E);
 * ②把栈顶的元素“弹出”:pop(E);
 * ③取栈顶元素但不弹出:peek(E)。
 *
 * 在Java中,我们用Deque可以实现Stack的功能:
 * ①把元素压栈:push(E)/addFirst(E);
 * ②把栈顶的元素“弹出”:pop(E)/removeFirst();
 * ③取栈顶元素但不弹出:peek(E)/peekFirst()。
 *
 * Java的集合类并没有单独的Stack接口,因为有个遗留类名字就叫Stack,
 * 出于兼容性考虑,所以没办法创建Stack接口,
 * 只能用Deque接口来“模拟”一个Stack了。
 * 当我们把Deque作为Stack使用时,注意只调用push()/pop()/peek()方法,不要调用addFirst()/removeFirst()/peekFirst()方法,那样会破坏栈的本质。
 */
public class TraversalOfBinaryTree {

    public static void main(String[] args) {
        TreeNode treeNode1 = new TreeNode(1);
        TreeNode treeNode2 = new TreeNode(2);
        TreeNode treeNode3 = new TreeNode(3);
        TreeNode treeNode4 = new TreeNode(4);
        TreeNode treeNode5 = new TreeNode(5);

        treeNode1.setLeft(treeNode2);
        treeNode1.setRight(treeNode3);
        treeNode2.setLeft(treeNode4);
        treeNode2.setRight(treeNode5);

        //preTraversal(treeNode1);
        //midTraversal(treeNode1);
        postorderTraversal(treeNode1);
    }

    // 前序遍历
    /**
     * 思路:一开始先将根节点入栈,确保栈中有个元素,不空,可以让循环正常结束
     * 循环体:先出栈(弹出父节点),将右、左孩子结点一次入栈(因为栈是先进后出,要让左孩子先出。)
     */
    public static void preTraversal(TreeNode root) {
        //栈
        Deque<TreeNode> stk = new LinkedList<TreeNode>();
        //入栈
        stk.push(root);

        while (!stk.isEmpty()) {
            //出栈
            TreeNode node = stk.pop();
            System.out.println(node.getVal());

            //先将右孩子结点入栈,再左孩子结点入栈(因为栈是先进后出)
            if (node.getRight() != null) {
                stk.push(node.getRight());
            }
            if (node.getLeft() != null) {
                stk.push(node.getLeft());
            }
        }
    }

    // 中序遍历
    /**
     * 思路:因为要首先打印左子树,所以在一个栈中先插入根节点,然后一直插入左子树,直到左子树为空,
     * 开始打印(入栈循环体)。
     * 这样栈顶元素一定是左节点,
     * 然后出栈打印,
     * 再将右子树按照之前的方法插入到栈中
     */
    public static void midTraversal(TreeNode root) {
        //栈
        Deque<TreeNode> stk = new LinkedList<TreeNode>();

        while (root != null || !stk.isEmpty()) {
            //入栈循环体
            while (root != null) {
                stk.push(root);
                root = root.getLeft();
            }
            //出栈
            root = stk.pop();
            System.out.println(root.getVal());

            //切换右孩子
            root = root.getRight();
        }

    }

    // 后序遍历
    /**
     * 思路:和中序遍历一样因为要先打印左子树所以入栈循环体不变,
     * 不同之处在于要先打印右子树,后打印根节点,
     * 打印根节点的前提是判断上次打印的节点是右子树还是左子树,
     *    如果是右子树,直接打印当前节点,
     *    如果是左子树则需要进入右子树,之后再打印该父节点,
     * 因此我们定义一个前驱节点来记录上次打印的节点,如果这节点不等于右子树则需要进入右子树
     */
    public static void postorderTraversal(TreeNode root) {
        //栈
        Deque<TreeNode> stk = new LinkedList<TreeNode>();
        TreeNode currentNode = root;
        TreeNode preNode = null;
        //入栈循环体
        while (currentNode != null) {
            stk.push(currentNode);
            currentNode = currentNode.getLeft();
        }

        while (!stk.isEmpty()) {
            //出栈
            TreeNode node = stk.pop();
            //当前节点有右孩子,并且上一个出栈的不是该节点的右孩子,就把当前节点和当前节点的右子树放进栈中
            if (node.getRight() != null && node.getRight() != preNode) {
                stk.push(node);
                currentNode = node.getRight();

                //入栈循环体
                while (currentNode != null) {
                    stk.push(currentNode);
                    currentNode = currentNode.getLeft();
                }
            } else {
                System.out.println(node.getVal());
                preNode = node;
            }
        }
    }

}

二、广度优先遍历(BFS)

广度优先遍历,指的是从图的一个未遍历的节点出发,先遍历这个节点的相邻节点,再依次遍历每个相邻节点的相邻节点。

也就是一层一层的去遍历,所以广度优先遍历也叫层序遍历。

深度优先遍历用的是栈,而广度优先遍历要用队列来实现。

先看一下Queue 中的方法

public interface Queue<E> extends Collection<E> {

//将指定的元素插入此队列(如果立即可行且不会违反容量限制),在成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException。
boolean  add(E e) 
    
//将指定的元素插入此队列(如果立即可行且不会违反容量限制),当使用有容量限制的队列时,此方法通常要优于 add(E),后者可能无法插入元素,而只是抛出一个异常。
boolean  offer(E e) 
    
//获取,但是不移除此队列的头。
E        element() 


//获取但不移除此队列的头;如果此队列为空,则返回 null。
E        peek(); 
    
//获取并移除此队列的头,如果此队列为空,则返回 null。    
E        poll(); 
    
//获取并移除此队列的头。    
E        remove();
}

以二叉树为例

package com.wlw.binaryTree;

import com.wlw.base.TreeNode;
import java.util.LinkedList;
import java.util.Queue;

/**
 * 广度优先遍历
 */
public class bfsTest {

    public static void main(String[] args) {
        TreeNode treeNode1 = new TreeNode(1);
        TreeNode treeNode2 = new TreeNode(2);
        TreeNode treeNode3 = new TreeNode(3);
        TreeNode treeNode4 = new TreeNode(4);
        TreeNode treeNode5 = new TreeNode(5);

        treeNode1.setLeft(treeNode2);
        treeNode1.setRight(treeNode3);
        treeNode2.setLeft(treeNode4);
        treeNode2.setRight(treeNode5);

        bfs(treeNode1);
    }

    private static void bfs(TreeNode root) {
        if (root == null) {
            return;
        }
        Queue<TreeNode> stack = new LinkedList<>();
        stack.add(root);

        while (!stack.isEmpty()) {
            TreeNode node = stack.poll();
            System.out.println("value = " + node.getVal());
            System.out.println("------------------");
            
            if (node.getLeft() != null) {
                stack.add(node.getLeft());
            }
            if (node.getRight() != null) {
                stack.add(node.getRight());
            }
        }
    }
}

/*
value = 1
------------------
value = 2
------------------
value = 3
------------------
value = 4
------------------
value = 5
------------------
*/

三、100. 相同的树

100. 相同的树

问题:给你两棵二叉树的根节点 pq ,编写一个函数来检验这两棵树是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

解决思路:这就是一个简单的遍历,在遍历过程中去判断节点是否相等。

第一种方法:递归遍历,与遍历一棵树不同的是,同时遍历两棵树


class Solution {
    public boolean isSameTree(TreeNode p, TreeNode q) {
        if (p == null && q == null) {
            return true;
        } else if (p == null || q == null) {
            return false;
        } else if (p.val != q.val) {
            return false;
        } else {
            //同时遍历两棵树,比较两个节点是否相等
            return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
        }
    }

}

第二种方法:迭代遍历,维护一个栈,与遍历一棵树不同的是,同时入栈两个元素,然后出栈比较,再入栈两个左孩子,两个右孩子。

class Solution {
    // 套用的是 迭代的前序遍历模板
    public boolean isSameTree(TreeNode p, TreeNode q) {
        Deque<TreeNode> stack = new LinkedList<TreeNode>();
        //入栈:一次入两
        stack.push(p);
        stack.push(q);
        
        while(!stack.isEmpty()) {
            //一次出两
            q = stack.pop();
            p = stack.pop();

            //比较
            if (p == null && q == null) {
                continue;
            } else if (p == null || q == null) {
                return false;
            } else if (p.val != q.val) {
                return false;
            }

            //入栈:一次入两
            stack.push(p.left);
            stack.push(q.left);

            //入栈:一次入两
            stack.push(p.right);
            stack.push(q.right);
        }
        return true;
    }
}

四、101. 对称二叉树

101. 对称二叉树

问题:给你一个二叉树的根节点 root , 检查它是否轴对称。

解决思路:和 [100. 相同的树] 很像,最终都是检查两个节点是否一样,不同的是这次检查的是对称的两个节点。所以在遍历时,就要指定遍历的节点。

第一种方法:递归遍历


class Solution {
    public boolean isSymmetric(TreeNode root) {
        if (root == null) {
            return true;
        } else {
            //递归的方式
            return recursionHandle(root.left, root.right);
        }
    }

    public boolean recursionHandle(TreeNode leftNode, TreeNode rightNode) {
        if (leftNode == null && rightNode == null) {
            return true;
        } else if (leftNode == null || rightNode == null) {
            return false;
        } else if (leftNode.val != rightNode.val) {
            return false;
        } else {
            //让左树的左孩子节点 与 右树的右孩子节点做对比
            //让左树的右孩子节点 与 左树的右孩子节点做对比
            return recursionHandle(leftNode.left, rightNode.right) && recursionHandle(leftNode.right, rightNode.left);
        }

    }
}

第二种方法:迭代遍历

class Solution {
    public boolean isSymmetric(TreeNode root) {
        if (root == null) {
            return true;
        } else {
            //迭代的方式
            return check(root.left, root.right);
        }
    }

    // 套用的是 迭代的前序遍历模板
    public boolean check(TreeNode leftNode, TreeNode rightNode) {
        Deque<TreeNode> stack = new LinkedList<TreeNode>();
        //入栈:一次入两
        stack.push(leftNode);
        stack.push(rightNode);

        while(!stack.isEmpty()) {
            //出栈:一次出两
            rightNode = stack.pop();
            leftNode = stack.pop();

            if (leftNode == null && rightNode == null) {
                continue;
            } else if (leftNode == null || rightNode == null) {
                return false;
            } else if (leftNode.val != rightNode.val) {
                return false;
            }

             //入栈:让左树的左孩子节点 与 右树的右孩子节点做对比
             stack.push(leftNode.left);
             stack.push(rightNode.right);

            //入栈:让左树的右孩子节点 与 左树的右孩子节点做对比
             stack.push(leftNode.right);
             stack.push(rightNode.left);
        }
        
        return true;

    }
}

五、104. 二叉树的最大深度

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

第一种:递归,深度优先遍历

思路:如果我们知道了左子树和右子树的最大深度 l和 r,那么该二叉树的最大深度即为max(l,r) + 1 ,而左子树和右子树的最大深度又可以以同样的方式进行计算。因此我们可以用「深度优先搜索」的方法来计算二叉树的最大深度。具体而言,在计算当前二叉树的最大深度时,可以先递归计算出其左子树和右子树的最大深度,然后在 O(1) 时间内计算出当前二叉树的最大深度。递归在访问到空节点时退出。

class Solution {
    public int maxDepth(TreeNode root) {
        if (root == null) {
            return 0;
        } else {
            int leftDepth = maxDepth(root.left);

            int rightDepth = maxDepth(root.right);

            return Math.max(leftDepth, rightDepth) + 1;
        }

    }
}

第二种:广度优先遍历

思路:我们也可以用「广度优先搜索」的方法来解决这道题目,但我们需要对其进行一些修改,此时我们广度优先搜索的队列里存放的是「当前层的所有节点」。每次拓展下一层的时候,不同于广度优先搜索的每次只从队列里拿出一个节点,我们需要将队列里的所有节点都拿出来进行拓展,这样能保证每次拓展完的时候队列里存放的是当前层的所有节点,即我们是一层一层地进行拓展,最后我们用一个变量depth 来维护拓展的次数,该二叉树的最大深度即为depth 。


class Solution {
    public int maxDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        Deque<TreeNode> deque = new LinkedList<TreeNode>();

        deque.add(root);
        int depth = 0;

        while(!deque.isEmpty()) {
            int size = deque.size();

            for (int i = size; i > 0; i--) {
                TreeNode node = deque.poll();
                if (node.left != null) {
                    deque.add(node.left);
                }
                if (node.right != null) {
                    deque.add(node.right);
                }
            }

            depth++;
        }

        return depth;
    }
}

六、111. 二叉树的最小深度

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

说明:叶子节点是指没有子节点的节点。

第一种:递归,深度优先遍历DFS

思路:首先可以想到使用深度优先搜索的方法,遍历整棵树,记录最小深度。对于每一个非叶子节点,我们只需要分别计算其左右子树的最小叶子节点深度。这样就将一个大问题转化为了小问题,可以递归地解决该问题。

复杂度分析:

  • 时间复杂度:O(N),其中 N 是树的节点数。对每个节点访问一次。
  • 空间复杂度:O(H),其中 H 是树的高度。空间复杂度主要取决于递归时栈空间的开销,最坏情况下,树呈现链状,空间复杂度为 O(N)。平均情况下树的高度与节点数的对数正相关,空间复杂度为 O(logN)。
class Solution {
    public int minDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        // 计算左子树的深度
        int leftDepth = minDepth(root.left);
        // 计算右子树的深度
        int rightDepth = minDepth(root.right);

        // 如果左子树或右子树的深度不为 0,即存在一个子树,那么当前子树的最小深度就是该子树的深度+1
        if (root.left == null || root.right == null) {
            return leftDepth + rightDepth + 1;
        } else {
            // 如果左子树和右子树的深度都不为 0,即左右子树都存在,那么当前子树的最小深度就是它们较小值+1
            return Math.min(leftDepth, rightDepth) + 1;
        }
    }
}

另外一种DFS

class Solution {
    public int minDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }

        if (root.left == null && root.right == null) {
            return 1;
        }

        int min_depth = Integer.MAX_VALUE;
        if (root.left != null) {
            min_depth = Math.min(minDepth(root.left), min_depth);
        }
        if (root.right != null) {
            min_depth = Math.min(minDepth(root.right), min_depth);
        }

        return min_depth + 1;
    }
}

第二种:BFS 广度优先遍历

思路:同样,我们可以想到使用广度优先搜索的方法,遍历整棵树。
当我们找到一个叶子节点时,直接返回这个叶子节点的深度。广度优先搜索的性质保证了最先搜索到的叶子节点的深度一定最小。

复杂度分析:

  • 时间复杂度:O(N),其中 N 是树的节点数。对每个节点访问一次。
  • 空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于队列的开销,队列中的元素个数不会超过树的节点数。
class Solution {
    class QueueNode{
        TreeNode node;
        int depth;

        public QueueNode(TreeNode node, int depth){
            this.node = node;
            this.depth = depth;
        }
    }

    public int minDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }

       Queue<QueueNode> queue= new LinkedList<QueueNode>();
        //入队列
        queue.offer(new QueueNode(root, 1));

        while (!queue.isEmpty()) {
            //出队
            QueueNode queueNode = queue.poll();
            TreeNode node = queueNode.node;
            int depth = queueNode.depth;

            //如果叶子节点,结束,返回结果
            if (node.left == null && node.right == null) {
                return depth;
            }

            //左孩子入队
            if (node.left != null) {
                queue.offer(new QueueNode(node.left, depth + 1));
            }
            //右孩子入队
            if (node.right != null) {
                queue.offer(new QueueNode(node.right, depth + 1));
            }
        }

        return 0;
    }
}

七、112. 路径总和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。

叶子节点:是指没有子节点的节点。

第一种:递归,深度优先遍历DFS:

解题思路:此题看上去是要做加法,但是转换为做减法会更简单。我们去遍历路径,也就是遍历节点,每遍历到一个节点,就用目标值减去当前节点的值,直到遍历到叶子节点(也就是没孩子节点的节点),看当前做过减法后的目标值与该叶子节点的值是否相等,如果有相等的,返回true。

复杂度分析:

  • 时间复杂度:O(N),其中 N 是树的节点数。对每个节点访问一次。

  • 空间复杂度:O(H),其中 H 是树的高度。空间复杂度主要取决于递归时栈空间的开销,最坏情况下,树呈现链状,空间复杂度为 O(N)。平均情况下树的高度与节点数的对数正相关,空间复杂度为 O(logN)。

class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null) {
            return false;
        }
        //寻找的是从根节点到叶子节点的路径之和
        //所以需要先去判断当前节点是否还有孩子节点,再去判断当前节点的值与目标值
        if (root.left == null && root.right == null) {
            return root.val == targetSum;
        }
        
        return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);
    }
}

第二种:迭代,广度优先遍历BFS:

解题思路:使用广度优先搜索的方式,记录从根节点到当前节点的路径和,以防止重复计算。这样我们使用两个队列,分别存储将要遍历的节点,以及根节点到这些节点的路径和即可。

可以做加法,也可以做减法。

复杂度分析:

  • 时间复杂度:O(N),其中 N 是树的节点数。对每个节点访问一次。
  • 空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于队列的开销,队列中的元素个数不会超过树的节点数。
//使用两个队列
class Solution {
    //做加法
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null) {
            return false;
        }
        Queue<TreeNode> treeQueue = new LinkedList<TreeNode>();
        Queue<Integer> targetQueue = new LinkedList<Integer>();

        //入队列
        treeQueue.offer(root);
        targetQueue.offer(root.val);

        while (!treeQueue.isEmpty()) {
            TreeNode node = treeQueue.poll();
            int sum = targetQueue.poll();

            //先去判断当前节点是否还有孩子节点,
            //再去判断当前遍历过的节点之和 与 目标值
            if (node.left == null && node.right == null) {
                if (sum == targetSum) {
                    return true;
                }
                continue;
            }

            if (node.left != null) {
                treeQueue.offer(node.left);
                targetQueue.offer(sum + node.left.val);
            }
            if (node.right != null) {
                treeQueue.offer(node.right);
                targetQueue.offer(sum + node.right.val);
            }
        }
        return false;
    }
}
//只使用一个队列,创建一个新类
//做减法
class Solution {
    class QueueNode{
        TreeNode node;
        int value;

        public QueueNode(TreeNode node, int value) {
            this.node = node;
            this.value = value;
        }
    }

    //做减法
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null) {
            return false;
        }
        
        Queue<QueueNode> queue = new LinkedList<QueueNode>();
        //入队列
        queue.offer(new QueueNode(root, targetSum));

        while (!queue.isEmpty()) {
            //出队列
            QueueNode queueNode = queue.poll();

            TreeNode node = queueNode.node;
            int sum = queueNode.value;

            //先去判断当前节点是否还有孩子节点,
            //再去判断当前叶子节点的值与目标值
            if (node.left == null && node.right == null) {
                if (node.val == sum) {
                    return true;
                }
                continue;
            }

            if (node.left != null) {
                queue.offer(new QueueNode(node.left, sum - node.val));
            }
            if (node.right != null) {
                queue.offer(new QueueNode(node.right, sum - node.val));
            }
        }

        return false;
    }

}

八、113. 路径总和 II

题:给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 叶子节点 是指没有子节点的节点。

解题思路:与112题相似,在112题的基础上找出所有满足条件的路径,所以我们需要一个容器(这里用的集合)来存放遍历到叶子节点的路径。

第一种方法:深度优先遍历DFS,递归

复杂度分析:

  • 时间复杂度:O(N^2),其中 N是树的节点数。在最坏情况下,树的上半部分为链状,下半部分为完全二叉树,并且从根节点到每一个叶子节点的路径都符合题目要求。此时,路径的数目为 O(N),并且每一条路径的节点个数也为 O(N),因此要将这些路径全部添加进答案中,时间复杂度为 O(N^2)。
  • 空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于栈空间的开销,栈中的元素个数不会超过树的节点数。
class Solution {
    //深度优先遍历DFS
    //返回最终结果
    List<List<Integer>> result = new ArrayList<List<Integer>>();
    
    //记录遍历的路径
    List<Integer> list = new ArrayList<Integer>();

    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        dfs(root, targetSum);
        return result;
    }

    public void dfs(TreeNode root, int targetSum){
        if (root == null) {
            return;
        }
        list.add(root.val);
        
        //判断是否 是叶子节点,是否满足题意中的条件
        if (root.left == null && root.right == null && root.val == targetSum) {
            result.add(new ArrayList(list));
        }
        targetSum = targetSum - root.val;

        dfs(root.left, targetSum);
        dfs(root.right, targetSum);

        //把最后一个节点去掉,去遍历另外一条路径
        list.remove(list.size() - 1);
    }
}

九、257. 二叉树的所有路径

问题:给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。叶子节点 是指没有子节点的节点。

解题思路:这道题就是遍历二叉树,本身不难,遍历到叶子节点返回路径即可,注意的是怎么来记录路径。

第一种:递归,深度优先遍历DFS

复杂度分析:

  • 时间复杂度:O(N^ 2),其中 NN 表示节点数目。在深度优先搜索中每个节点会被访问一次且只会被访问一次,每一次会对 path 变量进行拷贝构造,时间代价为 O(N),故时间复杂度为 O(N^2)。
  • 空间复杂度:O(N^ 2),其中 N表示节点数目。除答案数组外我们需要考虑递归调用的栈空间。在最坏情况下,当二叉树中每个节点只有一个孩子节点时,即整棵二叉树呈一个链状,此时递归的层数为 N,此时每一层的 path 变量的空间代价的总和为O(N^ 2), 空间复杂度为 O(N^ 2)。最好情况下,当二叉树为平衡二叉树时,它的高度为logN,此时空间复杂度为 O((log N)^2)
class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        List<String> result = new ArrayList<>();
        dfs(root, "", result);
        return result;
    }

    public void dfs (TreeNode root, String path,List<String> result) {
        if (root == null) {
            return;
        }
        StringBuffer pathSb = new StringBuffer(path);
        pathSb.append(Integer.toString(root.val));

        //到达叶子节点
        if (root.left == null && root.right == null) {
            result.add(pathSb.toString());
        } else {
            pathSb.append("->");
            dfs(root.left, pathSb.toString(), result);
            dfs(root.right, pathSb.toString(), result);
        }
    }
}

第二种:广度优先遍历BFS

复杂度分析:

  • 时间复杂度:O(N^2),其中 N表示节点数目。分析同方法一。
  • 空间复杂度:O(N^ 2),其中 N 表示节点数目。在最坏情况下,队列中会存在 N 个节点,保存字符串的队列中每个节点的最大长度为 NN,故空间复杂度为 O(N^ 2)。
class Solution {
    public List<String> binaryTreePaths(TreeNode root) {

        List<String> result = new ArrayList<>();
        if (root == null) {
            return result;
        }
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        Queue<String> strPath = new LinkedList<String>();
        //入
        queue.offer(root); 
        strPath.offer(Integer.toString(root.val));

        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();//出
            String path =  strPath.poll();//出

            //叶子节点
            if (node.left == null && node.right == null) {
                result.add(path);
            } else {
                if (node.left != null) {
                    queue.offer(node.left);
                    strPath.offer(new StringBuffer(path).append("->").append(node.left.val).toString());
                } 
                if (node.right != null) {
                    queue.offer(node.right);
                    strPath.offer(new StringBuffer(path).append("->").append(node.right.val).toString());
                }
            } 
        }

        return result;
    }
}

十、404. 左叶子之和

题:给定二叉树的根节点 root ,返回所有左叶子之和。

解题思路:遍历二叉树即可,到“左叶子”节点,把这种节点的值加起来。

第一种方法:DFS深度优先遍历,递归

复杂度分析:

  • 时间复杂度:O(n),其中 n 是树中的节点个数。
  • 空间复杂度:O(n)。空间复杂度与深度优先搜索使用的栈的最大深度相关。在最坏的情况下,树呈现链式结构,深度为 O(n),对应的空间复杂度也为 O(n)。
class Solution {
    int sum = 0;
    public int sumOfLeftLeaves(TreeNode root) {
        if (root == null) {
            return 0;
        }
        //左叶子节点的判断
        if (root.left != null && root.left.left == null && root.left.right == null) {
            sum += root.left.val;
        }
        sumOfLeftLeaves(root.left);
        sumOfLeftLeaves(root.right);
        return sum;
    }

}

第二种方法:BFS 广度优先遍历

复杂度分析:

  • 时间复杂度:O(n),其中 n 是树中的节点个数。
  • 空间复杂度:O(n)。空间复杂度与广度优先搜索使用的队列需要的容量相关,为 O(n)。
class Solution {
    // 重点在于怎么判断出是左叶子节点
    // BFS
    public int sumOfLeftLeaves(TreeNode root) {
        // 对根结点进行判定
        if (root == null) {
            return 0;
        }
        
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);
        int sum = 0;
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if (node.left != null) {
                //左孩子节点是叶子节点
                if (node.left.left == null && node.left.right == null) {
                    sum = sum + node.left.val;
                } else {
                //此else判断也可以不加
                    queue.offer(node.left);
                }
            }
            if (node.right != null) {
                //右孩子不是叶子结点,继续循环
                //此判断也可以不加
                if (!(node.right.left == null && node.right.right == null)) {
                    queue.offer(node.right);
                }
            }
        }

        return sum;
    }


}

十一、110. 平衡二叉树

题:给定一个二叉树,判断它是否是高度平衡的二叉树。本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

解题思路:有两个要点:1、平衡二叉树的所有子树也是平衡二叉树,2、判断平衡二叉树的条件:每个节点 的左右两个子树的高度差的绝对值不超过 1

第一种解法:DFS 深度优先遍历,递归,自底向上.
自底向上递归的做法类似于后序遍历,对于当前遍历到的节点,先递归地判断其左右子树是否平衡,再判断以当前节点为根的子树是否平衡。如果一棵子树是平衡的,则返回其高度(高度一定是非负整数),否则返回 -1−1。如果存在一棵子树不平衡,则整个二叉树一定不平衡。

复杂度分析:

  • 时间复杂度:O(n),其中 n 是二叉树中的节点个数。使用自底向上的递归,每个节点的计算高度和判断是否平衡都只需要处理一次,最坏情况下需要遍历二叉树中的所有节点,因此时间复杂度是 O(n)。
  • 空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n。
class Solution {
    // 此题与 求二叉树的高度相似
    //DFS 自底向上,套用框架,相当于后序遍历
    public boolean isBalanced(TreeNode root) {
        return dfs(root) >= 0;
    }

    public int dfs (TreeNode node) {
        if (node == null) {
            return 0;
        }

        //计算左子树的深度
        int leftDepth = dfs(node.left);    

        //计算右子树的深度
        int rightDepth = dfs(node.right);

        //node节点的左右两个子树的高度差 > 1 ,不符合题意,返回-1
        //左子树或者右子树的深度 为-1,也是不符合题意
        if (Math.abs(leftDepth - rightDepth) > 1 || leftDepth == -1 || rightDepth ==-1) {
            return -1;
        } else {
            //返回的是一个节点的最大深度
            return Math.max(leftDepth, rightDepth) + 1;
        }
    }
}

第二种方法:DFS,自顶向下。
类似于二叉树的前序遍历,即对于当前遍历到的节点,首先计算左右子树的高度,如果左右子树的高度差是否不超过 1,再分别递归地遍历左右子节点,并判断左子树和右子树是否平衡。这是一个自顶向下的递归的过程。由于是自顶向下递归,因此对于同一个节点,函数 height 会被重复调用,导致时间复杂度较高

复杂度分析:

  • 时间复杂度:O(n^2),其中 n 是二叉树中的节点个数。
  • 空间复杂度:O(n)O,其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n。
class Solution {
    // 此题与 求二叉树的高度相似
    //DFS 自顶向下
    public boolean isBalanced(TreeNode root) {
        if (root == null) {
            return true;
        } else {
            return Math.abs(height(root.left) - height(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
        }
    }

    //计算一个节点的深度
    public int height (TreeNode node) {
        if (node == null) {
            return 0;
        }
        //计算左子树的深度
        int leftDepth = height(node.left);    

        //计算右子树的深度
        int rightDepth = height(node.right);

        //返回的是一个节点的最大深度
        return Math.max(leftDepth, rightDepth) + 1;
    }
}

十二、108. 将有序数组转换为平衡二叉搜索树

题:给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。 高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。

解题: 根节点选用升序数组中间数,这样就把数组分成了左右两部分,那根节点的左节点就是左部分数组的中间数,根节点的右节点就是右部分数组的中间数,依次递归

复杂度分析:

  • 时间复杂度:O(n),其中 n 是数组的长度。每个数字只访问一次。
  • 空间复杂度:O(logn),其中 n 是数组的长度。空间复杂度不考虑返回值,因此空间复杂度主要取决于递归栈的深度,递归栈的深度是 O(logn)。
class Solution {
    //转成一个平衡二叉树
    //根节点选用升序数组中间数,这样就把数组分成了左右两部分
    //那根节点的左节点就是左部分数组的中间数,根节点的右节点就是右部分数组的中间数,依次递归
    public TreeNode sortedArrayToBST(int[] nums) {
        return recursion(nums, 0, nums.length - 1);
    }

    public TreeNode recursion(int[] nums, int left, int right) {
        if (left > right) {
            return null;
        }

        int mid = (left + right)/2;
        TreeNode node = new TreeNode(nums[mid]);
        node.left = recursion(nums, left, mid-1);
        node.right = recursion(nums, mid+1, right);

        return node;
    }
}

十三、235. 二叉搜索树的最近公共祖先

题:给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

解题:因为是二叉搜索树,是排好序的,所以我们可以根据qp与根节点的大小关系来找出答案,如果一个比根节点大,一个比根节点小,那根节点就是答案,不管qp是都大于根节点,还是都小于根节点,我们总能找到上面那个“根节点”。

第一种方法:递归,DFS,套用模板,同时遍历q,p,找到一个大于q,小于p的目标节点。

复杂度分析:

  • 时间复杂度:O(n),其中 n 是给定的二叉搜索树中的节点个数。
  • 空间复杂度:O(1)。
class Solution {
    //DFS,递归

    //同时遍历q,p
    //因为是二叉搜索树,是排好序的,如果q>root,p<root,那结果就是root;如果qp都<root,继续判断root.left;如果qp都>root,继续判断root.right。循环下去就可找到了
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (p.val < root.val && q.val < root.val) {
            return lowestCommonAncestor(root.left, p, q);
        } 
        if (p.val > root.val && q.val > root.val) {
            return lowestCommonAncestor(root.right, p, q);
        } 
        return root;
    }
}
//不用递归
class Solution {
    //同时遍历q,p
    //因为是二叉搜索树,是排好序的,如果q>root,p<root,那结果就是root;如果qp都<root,继续判断root.left;如果qp都>root,继续判断root.right。循环下去就可找到了
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        TreeNode node = root;
        while (true) {
            if (p.val < node.val && q.val < node.val) {
                node = node.left;
            } else if (p.val > node.val && q.val > node.val) {
                node = node.right;
            } else {
                break;
            }
        }
        return node;
    }
}

第二种方法:分别找到根节点到目标节点的路径集合,对比两个路径集合,有相同的则找到答案。

复杂度分析:

  • 时间复杂度:O(n),其中 n 是给定的二叉搜索树中的节点个数。上述代码需要的时间与节点 p和 q 在树中的深度线性相关,而在最坏的情况下,树呈现链式结构,p 和 q 一个是树的唯一叶子结点,一个是该叶子结点的父节点,此时时间复杂度为O(n)。
  • 空间复杂度:O(n),我们需要存储根节点到 p 和 q 的路径。和上面的分析方法相同,在最坏的情况下,路径的长度为O(n),因此需要 O(n) 的空间。
class Solution {
    //找到根节点到目标节点的路径,
    //对比两个路径集合,有相同的则找到答案
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        List<TreeNode> pPath = getPath(root, p);
        List<TreeNode> qPath = getPath(root, q);
        TreeNode result = null;
        //对比是否有相同节点
        for (int i = 0; i < pPath.size() && i < qPath.size(); i++) {
            if (pPath.get(i) == qPath.get(i)) {
                result = pPath.get(i);
            } else {
                break;
            }
        }

        return result;
    }

    //找路径
    public List<TreeNode> getPath(TreeNode root, TreeNode target) {
        List<TreeNode> path = new ArrayList<TreeNode>();
        TreeNode node = root;
        while(target.val != node.val) {
            path.add(node);
            if (target.val < node.val) {
                node = node.left;
            } else {
                node = node.right;
            }
        }
        //到达目标节点
        path.add(node);
        return path;

    }
}

十四、530. 二叉搜索树的最小绝对差

题:给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。差值是一个正数,其数值等于两值之差的绝对值。

解:因为是二叉搜索树,中序遍历出来的是一个升序序列,对一个升序数组,求任意两个元素之差的绝对值的最小值,答案一定为相邻两个元素之差的最小值;用一个全局变量来记录上一次遍历到的节点 与 当前节点 进行减法。用一个全局变量来记录最小差值,在遍历二叉树的过程中比较差值,取最小的。

复杂度分析:

  • 时间复杂度:O(n),其中 n 为二叉搜索树节点的个数。每个节点在中序遍历中都会被访问一次且只会被访问一次,因此总时间复杂度为 O(n)。
  • 空间复杂度:O(n)。递归函数的空间复杂度取决于递归的栈深度,而栈深度在二叉搜索树为一条链的情况下会达到 O(n) 级别。
class Solution {
        // 因为是二叉搜索树,中序遍历出来的是一个升序序列,对一个升序数组,求任意两个元素之差的绝对值的最小值,答案一定为相邻两个元素之差的最小值
    // 用一个全局变量来记录最小差值,在遍历二叉树的过程中比较差值,取最小的。
    // 用一个变量来记录上一次遍历到的节点 与 当前节点 进行减法。 
    int result = Integer.MAX_VALUE;
    TreeNode pre = null; 

    public int getMinimumDifference(TreeNode root) {
        dfs(root);
        return result;
    }

    public void dfs(TreeNode root) {
        if (root == null) {
            return;
        }
        dfs(root.left);

        if (pre == null) {
            pre = root;
        } else {
            result = Math.min(result, root.val - pre.val);
            pre = root;
        }

        dfs(root.right);
    }
}

十五、124. 二叉树中的最大路径和

题:路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。

解题思路:dfs,递归遍历,后序遍历,自底向上,我们就可以得到每个节点的最大路径和,维护一个全局变量存储最大路径和,在递归过程中更新该值,最后得到的值即为二叉树中的最大路径和。
还是套用模板。
复杂度分析:

  • 时间复杂度:O(N),其中 N 是二叉树中的节点个数。对每个节点访问不超过 2次。
  • 空间复杂度:O(N),其中 N 是二叉树中的节点个数。空间复杂度主要取决于递归调用层数,最大层数等于二叉树的高度,最坏情况下,二叉树的高度等于二叉树中的节点个数。
class Solution {
    //记录当前最大路径和
    int result = Integer.MIN_VALUE;

    public int maxPathSum(TreeNode root) {
        dfsMax(root);
        return result;
    }

    //dfs 递归寻找一个节点的最大路径和:(根+左+右)
    public int dfsMax(TreeNode root) {
        if(root == null) {
            return 0;
        }

        //返回当前节点的 左节点 的 最大值
        int leftSum = Math.max(0, dfsMax(root.left));
        //返回当前节点的 右节点 的 最大值
        int rightSum = Math.max(0,dfsMax(root.right));

        //当前节点的 最大路径和
        int nodeSum = root.val + leftSum + rightSum;

        //对比,当前 与 之前记录的最大路径和,选大的。
        result = Math.max(result, nodeSum);

        //返回当前节点的 与 左右两节点。之间 最大的路径和。
        return Math.max(leftSum, rightSum) + root.val;
    }
}

十六、面试题 04.12. 求和路径

题目链接
题:给定一棵二叉树,其中每个节点都含有一个整数数值(该值或正或负)。设计一个算法,打印节点数值总和等于某个给定值的所有路径的数量。注意,路径不一定非得从二叉树的根节点或叶节点开始或结束,但是其方向必须向下(只能从父节点指向子节点方向)。

解:由题目最后一句话可知,此题要前序遍历,自上而下,但要是条件是:从根节点到叶子节点,那么此题就差不多和112. 路径总和 很是相似了,但是题意是从任意节点开始到任意节点结束。所以需要对每个节点都进行前序遍历。也就是双dfs。

class Solution {
    int result = 0;
    public int pathSum(TreeNode root, int sum) {
        if(root == null) {
            return result;
        }
        // 找结果
        dfs(root, sum);

        // 对左节点进行遍历
        pathSum(root.left, sum);
        // 对右节点进行遍历
        pathSum(root.right, sum);

        return result;
    }

    //对一个节点进行遍历
    public void dfs(TreeNode root, int sum) {
        if (root == null) {
            return;
        }
        sum = sum - root.val;
        if (sum == 0) {
            result++;
        }
        dfs(root.left, sum);
        dfs(root.right, sum);
    }
}

十七、98. 验证二叉搜索树

题:给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

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

第一种方法:递归,前序遍历

解:这启示我们设计一个递归函数 helper(root, lower, upper) 来递归判断,函数表示考虑以 root 为根的子树,判断子树中所有节点的值是否都在 (l,r)的范围内(注意是开区间)。如果 root 节点的值 val 不在 (l,r) 的范围内说明不满足条件直接返回,否则我们要继续递归调用检查它的左右子树是否满足,如果都满足才说明这是一棵二叉搜索树。那么根据二叉搜索树的性质,在递归调用左子树时,我们需要把上界 upper 改为 root.val,即调用 helper(root.left, lower, root.val),因为左子树里所有节点的值均小于它的根节点的值。同理递归调用右子树时,我们需要把下界 lower 改为 root.val,即调用 helper(root.right, root.val, upper)。

复杂度分析:

  • 时间复杂度 : O(n),其中 n为二叉树的节点个数。在递归调用的时候二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n)。
  • 空间复杂度 : O(n),其中 n 为二叉树的节点个数。递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,即二叉树的高度。最坏情况下二叉树为一条链,树的高度为 n ,递归最深达到 n 层,故最坏情况下空间复杂度为 OO(n) 。
class Solution {
    public boolean isValidBST(TreeNode root) {
        return dfs(root, Long.MIN_VALUE, Long.MAX_VALUE);
    }

    // dfs 判断当前节点
    public boolean dfs(TreeNode root, long small, long big){
        if(root == null) {
            return true;
        }
        // 判断值,
        if (root.val <= small || root.val >= big) {
            //不符合题意
            return false;
        }

        //左节点是小的, ,右节点是大的
        return dfs(root.left, small, root.val) && dfs(root.right, root.val, big);
    }

}

第二种:递归,中序遍历

解:二叉搜索树,中序遍历是升序,所以可以在中序遍历中比较前面一个,都是递增的才是正确的

复杂度分析:

  • 时间复杂度 : O(n),其中 n 为二叉树的节点个数。二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n)。
  • 空间复杂度 : O(n),其中 n 为二叉树的节点个数。栈最多存储 n 个节点,因此需要额外的 O(n) 的空间。
class Solution {
    // 二叉搜索树,中序遍历是升序,所以可以在中序遍历中比较前面一个,都是递增的才是正确的
    long pre = Long.MIN_VALUE;;
    public boolean isValidBST(TreeNode root) {

        if(root == null) {
            return true;
        }

        //左子树
        if (!isValidBST(root.left)) {
            return false;
        }

        //和前一个节点值比较
        if (pre >= root.val) {
            return false;
        }
        pre = root.val;
        
        //右子树
        return isValidBST(root.right);

    }
}

十八、99. 恢复二叉搜索树

题:给你二叉搜索树的根节点 root ,该树中的 恰好 两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树 。

解: 二叉搜索树:中序遍历升序,题中说只有两个节点是错误的,中序遍历找出这两个,交换。

复杂度分析:

  • 时间复杂度:最坏情况下(即待交换节点为二叉搜索树最右侧的叶子节点)我们需要遍历整棵树,时间复杂度为 O(N),其中 N 为二叉搜索树的节点个数。
  • 空间复杂度:O(H),其中 H 为二叉搜索树的高度。中序遍历的时候栈的深度取决于二叉搜索树的高度。

class Solution {
    //二叉搜索树:中序遍历升序,题中说只有两个节点是错误的,中序遍历找出这两个,交换
    TreeNode first = null;
    TreeNode last = null;
    TreeNode pre = new TreeNode(Integer.MIN_VALUE);

    public void recoverTree(TreeNode root) {
        dfs(root);
        if (first != null && last != null) {
            int temp = first.val;
            first.val = last.val;
            last.val = temp;
        }
    }

    // 中序遍历找两数
    public void dfs(TreeNode root) {
        if (root == null) {
            return;
        }

        dfs(root.left);
        //当前节点是要大于上一个节点的,小于说明顺序出错
        //第一次判断成功时 first == pre,第二次判断成功时 last = root,还是因为是升序序列
        if(root.val < pre.val) {
            last = root;
            if (first == null) {
                first = pre;
            }
        }
        pre = root;

        dfs(root.right);
    }
}

十九、102. 二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]

解:在普通的层序遍历的基础上,增加一点,首先根元素入队
当队列不为空的时候,求当前队列的长度 s,依次从队列中取 s
个元素进行拓展,然后进入下一次迭代。这和求 (104. 二叉树的最大深度),用bfs求解差不多。

复杂度分析 ,记树上所有节点的个数为 n。:

  • 时间复杂度:每个点进队出队各一次,故渐进时间复杂度为 O(n)。
  • 空间复杂度:队列中元素的个数不超过 n 个,故渐进空间复杂度为 O(n)。
class Solution {

    public List<List<Integer>> levelOrder(TreeNode root) {
        
        List<List<Integer>> result = new ArrayList<List<Integer>>();

        if(root == null) {
            return result;
        }
        Queue<TreeNode> stack = new LinkedList<>();
        stack.add(root);

        while(!stack.isEmpty()) {
            List<Integer> temp = new ArrayList<Integer>();
            int size = stack.size();

            for(int i = size; i > 0; i--) {
                TreeNode node = stack.poll();
                temp.add(node.val);

                if (node.left != null) {
                    stack.add(node.left);
                }
                if(node.right != null) {
                    stack.add(node.right);
                }
            }
            result.add(temp);
        }
        return result;
    }
}

二十、105. 从前序与中序遍历序列构造二叉树

题:给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

解:对于任意一颗树而言,前序遍历的形式总是
[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]
即根节点总是前序遍历中的第一个节点。而中序遍历的形式总是
[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果]

只要我们在中序遍历中定位到根节点,那么我们就可以分别知道左子树和右子树中的节点数目。由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,因此我们就可以对应到前序遍历的结果中,对上述形式中的所有左右括号进行定位。我们就可以递归地对构造出左子树和右子树,再将这两颗子树接到根节点的左右位置。

复杂度分析:

  • 时间复杂度:O(n),其中 n 是树中的节点个数。
  • 空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,我们还需要使用 O(n) 的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h < n,所以总空间复杂度为 O(n)。
class Solution {

    //key 是中序遍历中的值,value是对应的下标
    private Map<Integer, Integer> map;

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        int size = preorder.length;

        map = new HashMap<Integer, Integer>();
        for(int i = 0; i < size ; i++){
            map.put(inorder[i], i);
        }

        return dfs(preorder, inorder, 0, size-1, 0, size-1);
    }

    public TreeNode dfs(int[] preorder, int[] inorder, 
                        int preorderLeft, int preorderRight,
                        int inorderleft, int inorderRight){

        if(preorderLeft > preorderRight){
            return null;
        }

        //根节点
        TreeNode root = new TreeNode(preorder[preorderLeft]);
        //中序遍历中根节点的位置
        int inorderRootLocation = map.get(preorder[preorderLeft]);
        //左子树的范围,节点数组
        int leftTree = inorderRootLocation - inorderleft;
        
        //递归构建左子树,preorder左子树的范围,inorder左子树的范围
        root.left = dfs(preorder, inorder, 
                        preorderLeft + 1, preorderLeft + leftTree, 
                        inorderleft, inorderRootLocation - 1);

        //递归构建右子树,preorder右子树的范围,inorder右子树的范围
        root.right = dfs(preorder, inorder,
                        preorderLeft + leftTree + 1, preorderRight,
                        inorderRootLocation + 1, inorderRight);
        return root;
    }
 
}

二十一、106. 从中序与后序遍历序列构造二叉树

题:给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。

解:和105题可以说是完全相同,根据中序遍历、后序遍历的遍历规则来。关键点就在于递归时确定左右子树的边界,注意都是闭区间
复杂度分析:

  • 时间复杂度:O(n),其中 n 是树中的节点个数。
  • 空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,我们还需要使用 O(n) 的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h < n,所以总空间复杂度为 O(n)。
class Solution {
    //存放中序遍历,用来快速找到节点的下标
    Map<Integer, Integer> map = new HashMap<Integer, Integer>();

    public TreeNode buildTree(int[] inorder, int[] postorder) {

        int size = inorder.length;
        for(int i = 0; i < size; i++) {
            map.put(inorder[i], i);
        }

        return dfs(inorder, postorder, 0, size-1, 0, size - 1);
    }

    public TreeNode dfs(int[] inorder, int[] postorder,
                        int inorderLeft, int inorderRight,
                        int postorderLeft, int postorderRight) {
        
        if(postorderLeft > postorderRight) {
            return null;
        }

        //根节点
        TreeNode root = new TreeNode(postorder[postorderRight]);
        //根节点在中序遍历中的位置
        int inorderRootLocation = map.get(postorder[postorderRight]);
        //右子树有几个元素
        int rightTreeNum = inorderRight - inorderRootLocation;

        //递归的设置左子树
        root.left = dfs(inorder, postorder, 
                        inorderLeft, inorderRootLocation - 1,
                        postorderLeft, postorderRight - rightTreeNum - 1);

        //递归的设置右子树
        root.right = dfs(inorder, postorder,
                        inorderRootLocation + 1, inorderRight,
                        postorderRight - rightTreeNum, postorderRight - 1);

        return root;       
    }
}

二十二、107. 二叉树的层序遍历 II

题:给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
输入:root = [3,9,20,null,null,15,7]
输出:[[15,7],[9,20],[3]]

解:此题与 102. 二叉树的层序遍历 一摸一样,只不过结果集合从ArrayList变为LinkedList

复杂度分析:

  • 时间复杂度:O(n),其中 n 是二叉树中的节点个数。每个节点访问一次,结果列表使用链表的结构时,在结果列表头部添加一层节点值的列表的时间复杂度是 O(1),因此总时间复杂度是 O(n)。
  • 空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度取决于队列开销,队列中的节点个数不会超过 n。
class Solution {

    public List<List<Integer>> levelOrderBottom(TreeNode root) {
        
        List<List<Integer>> result = new LinkedList<List<Integer>>();

        if(root == null) {
            return result;
        }

        //队列
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root); //入栈
        while(!queue.isEmpty()) {

            //过渡集合
            List<Integer> temp = new ArrayList<Integer>();
            int size = queue.size();
            
            for(int i = 0; i < size; i++) {
                TreeNode node = queue.poll();//出栈
                temp.add(node.val);

                if(node.left != null) {
                    queue.offer(node.left);
                }
                if(node.right != null) {
                    queue.offer(node.right);
                }
            }

            result.add(0, temp);
        }

        return result;
    }
}

二十三、109. 有序链表转换二叉搜索树

题:给定一个单链表,其中的元素按升序排序,将其转换为高度平衡的二叉搜索树。 本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
示例:
给定的有序链表: [-10, -3, 0, 5, 9],
一个可能的答案是:[0, -3, 9, -10, null, 5], 它可以表示下面这个高度平衡二叉搜索树:

解:此题和 108. 将有序数组转换为平衡二叉搜索树 思路是基本一样的,所以有个方法就是先将这个有序链表转为有序数组,这样就和108题一摸一样了,就是会多耗费O(n)的空间,我们也可以不转化,直接找到中间元素,只不过单链表和数组不一样,不能直接通过下标来访问,单链表只能next来访问,所以我们的解法是:从头访问单链表+中序遍历(而108题是 直接访问中间节点+前序遍历),两个题也都体现了 “分治” 的思想。

复杂度分析:

  • 时间复杂度O(n),其中 n 是链表的长度。设长度为 n 的链表构造二叉搜索树的时间为 T(n),递推式为 T(n)=2⋅T(n/2)+O(n),根据主定理,T(n)=O(nlogn)。

  • 空间复杂度:O(logn),这里只计算除了返回答案之外的空间。平衡二叉树的高度为 O(logn),即为递归过程中栈的最大深度,也就是需要的空间。

class Solution {

    ListNode tempNode;
    
    public TreeNode sortedListToBST(ListNode head) {
        tempNode = head;

        int length = getLength(head);

        return dfs(0, length - 1);
    }

    //求单链表长度
    public int getLength(ListNode head){
        int length = 0;
        while(head != null) {
            length = length + 1;
            head = head.next;
        }
        return length;
    }

    //中序遍历构造二叉树
    public TreeNode dfs(int left, int right) {
        if(left > right) {
            return null;
        }
        
        int mid = (left + right + 1) / 2;
        TreeNode root = new TreeNode();
        root.left = dfs(left, mid - 1);

        root.val = tempNode.val;
        tempNode = tempNode.next;

        root.right = dfs(mid + 1, right);

        return root;
    }
}

二十四、129. 求根节点到叶节点数字之和

给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:
例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。
计算从根节点到叶节点生成的 所有数字之和 。
叶节点 是指没有子节点的节点。

输入:root = [1,2,3]
输出:25
解释:
从根到叶子节点路径 1->2 代表数字 12
从根到叶子节点路径 1->3 代表数字 13
因此,数字总和 = 12 + 13 = 25

解:第一点求根节点到叶子节点的路径,第二个求和,这是两个关键点,这两步,我们可以分开做,也可以放在一起做。

第一种方法:两步分开做。
时间复杂度为:O(N^ 2),深度优先搜索中每个节点会被访问一次且只会被访问一次,每一次会对 path 变量进行拷贝构造,时间代价为 O(N),故时间复杂度为 O(N^2)。遍历求和,为O(N),综合起来为O(N ^ 2)。
空间复杂度:O(N^ 2)。

class Solution {
    public int sumNumbers(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        dfs(root, "", result);

        int sum = 0;
        int size = result.size();
        if (size == 0) {
            return sum;
        } else {
            for(int i = 0; i < size; i++) {
                sum = sum + result.get(i);
            }
        }
        return sum;
    }

    public void dfs (TreeNode root, String path,List<Integer> result) {
        if (root == null) {
            return;
        }
        StringBuffer pathSb = new StringBuffer(path);
        pathSb.append(Integer.toString(root.val));

        //到达叶子节点
        if (root.left == null && root.right == null) {
            result.add(Integer.valueOf(pathSb.toString()));
        } else {
            dfs(root.left, pathSb.toString(), result);
            dfs(root.right, pathSb.toString(), result);
        }
    }

第二种方法:两步一起做,一边求路径,一边求和。
时间复杂度:O(n),其中 n 是二叉树的节点个数。对每个节点访问一次。
空间复杂度:O(n),其中 n 是二叉树的节点个数。空间复杂度主要取决于递归调用的栈空间,递归栈的深度等于二叉树的高度,最坏情况下,二叉树的高度等于节点个数,空间复杂度为 O(n)。

class Solution {
    public int sumNumbers(TreeNode root) {
        return dfs(root, 0);
    }

    public int dfs (TreeNode root, int sumTemp) {
        if (root == null) {
            return 0;
        }
        //求根节点到目前节点的和
        int sum = sumTemp * 10 + root.val;

        //到达叶子节点
        if (root.left == null && root.right == null) {
           return sum;
        } else {
            return dfs(root.left,sum) + dfs(root.right, sum);
        }
    }

}

二十五、116. 填充每个节点的下一个右侧节点指针

给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:

struct Node {
  int val;
  Node *left;
  Node *right;
  Node *next;
}

填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。

解:此题应该很熟悉了,层序遍历,一次输出一层,所以出栈之前需要判断当前层有几个节点,然后再设置当前节点的右侧节点。
复杂度分析:

  • 时间复杂度:O(N)。每个节点会被访问一次且只会被访问一次,即从队列中弹出,并建立 next 指针。
  • 空间复杂度:O(N)。这是一棵完美二叉树,它的最后一个层级包含 N/2 个节点。广度优先遍历的复杂度取决于一个层级上的最大元素数量。这种情况下空间复杂度为 O(N)。
class Solution {
    public Node connect(Node root) {
        if(root == null) {
            return null;
        }

        Queue<Node> queue = new LinkedList<Node>(); 
        // 入栈 
        queue.offer(root);

        while(!queue.isEmpty()) {
            
            int size = queue.size();

            for(int i = 0; i < size; i++) {
                //出栈
                Node node = queue.poll();

                // 连接
                if (i < size - 1) {
                    node.next = queue.peek();
                }
                
                if(node.left != null) {
                    queue.offer(node.left);
                }
                if(node.right != null) {
                    queue.offer(node.right);
                }
            }    
        }   
        //返回根节点
        return root;
    }
}

二十六、173. 二叉搜索树迭代器

173. 二叉搜索树迭代器
就是中序遍历。
第一种:dfs 递归,先把中序遍历的结果写出来
复杂度分析:

  • 时间复杂度:初始化需要 O(n) 的时间,其中 n 为树中节点的数量。随后每次调用只需要 O(1) 的时间。
  • 空间复杂度:O(n),因为需要保存中序遍历的全部结果。
class BSTIterator {

    int index;
    List<Integer> list;

    public BSTIterator(TreeNode root) {
        index = 0;
        list = new ArrayList<Integer>();
        // 递归实现中序遍历
        dfs(root, list);
    }
    
    public int next() {
        return list.get(index++);
    }
    
    public boolean hasNext() {
        return index < list.size();
    }

    public void dfs(TreeNode root, List<Integer> list) {
        if (root == null) {
            return;
        }
        dfs(root.left, list);
        list.add(root.val);
        dfs(root.right, list);
    }
}

/**
 * Your BSTIterator object will be instantiated and called as such:
 * BSTIterator obj = new BSTIterator(root);
 * int param_1 = obj.next();
 * boolean param_2 = obj.hasNext();
 */

第二种:迭代 中序遍历 (拆开的)
复杂度分析

  • 时间复杂度:显然,初始化和调用hasNext() 都只需要 O(1) 的时间。每次调用 next() 函数最坏情况下需要 O(n) 的时间;但考虑到 n 次调用 next() 函数总共会遍历全部的 n 个节点,因此总的时间复杂度为O(n),因此单次调用平均下来的均摊复杂度为O(1)。
  • 空间复杂度:O(n),其中 n 是二叉树的节点数量。空间复杂度取决于栈深度,而栈深度在二叉树为一条链的情况下会达到O(n) 的级别。
class BSTIterator {

    private TreeNode cur;
    private Deque<TreeNode> stack;

    public BSTIterator(TreeNode root) {
        cur = root;
        stack = new LinkedList<TreeNode>();
    }
    
    public int next() {
        while(cur != null) {
            stack.push(cur);
            cur = cur.left;
        }
        cur = stack.pop();
        int result = cur.val;
        cur = cur.right;
        return result;
    }
    
    public boolean hasNext() {
        return cur != null || !stack.isEmpty();
    }

}

/**
 * Your BSTIterator object will be instantiated and called as such:
 * BSTIterator obj = new BSTIterator(root);
 * int param_1 = obj.next();
 * boolean param_2 = obj.hasNext();
 */

二十七、222. 完全二叉树的节点个数

222. 完全二叉树的节点个数 - 力扣(LeetCode) (leetcode-cn.com)

第一种,暴力求解,dfs递归

时间复杂度O(N),空间复杂度O(N)

class Solution {
    //暴力求解
    public int countNodes(TreeNode root) {
        if(root == null) {
            return 0;
        }

        return countNodes(root.left) + countNodes(root.right) + 1;
    }
}

第二种:利用完全二叉树特性。

回顾一下满二叉的节点个数怎么计算,如果满二叉树的层数为h,则总节点数为:2^h - 1.
那么我们来对 root 节点的左右子树进行高度统计,分别记为 left 和 right,有以下两种结果:

  • left == right。这说明,左子树一定是满二叉树,因为节点已经填充到右子树了,左子树必定已经填满了。所以左子树的节点总数我们可以直接得到,是 2^left - 1,加上当前这个 root 节点,则正好是 2^left。再对右子树进行递归统计。
  • left != right。说明此时最后一层不满,但倒数第二层已经满了,可以直接得到右子树的节点个数。同理,右子树节点 +root 节点,总数为 2^right。再对左子树进行递归查找。

时间复杂度:O(log^2 n)

空间复杂度:O(1)

class Solution {
    //利用完全二叉树的特性
    public int countNodes(TreeNode root) {
        if (root == null) {
            return 0;
        }
        // 计算左右子树的高度
        int left = countHeight(root.left);
        int right = countHeight(root.right);
        if (left == right) {
            // 说明左侧为满二叉树,1 << left 为当前的节点 + 子树的节点的和
            return countNodes(root.right) + (1 << left);
        } else {
            //  说明右侧为满二叉树
            return countNodes(root.left) + (1 << right);
        }
       
    }

    public int countHeight(TreeNode root) {
        int height = 0;
        while(root != null) {
            height++;
            root = root.left;
        }
        return height;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

悬浮海

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

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

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

打赏作者

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

抵扣说明:

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

余额充值