算法 | 第4章 树与图相关《程序员面试金典》

前言

本系列笔记主要记录笔者刷《程序员面试金典》算法的一些想法与经验总结,按专题分类,主要由两部分构成:经验值点和经典题目。其中重点放在经典题目上;

本章有10题,标号到12,没有第7和第11题;


0. *经验总结

0.1 程序员面试金典 P85

  • 树的基本组成单位是节点node,节点可以封装左右指针、父节点指针等;
  • 可以使用一个名为Tree的类来封装节点,但在面试中不常用;如果使用Tree类能使代码简单或完善,可以使用Tree类;
  • 注意区分以下概念,以及一些树的基本特点与遍历特点《详情见第0.2点 各种树的特点》:
    • 树与二叉树;
    • 二叉树和二叉搜索树(又称二叉排序树);
    • 平衡与不平衡;
    • 完整二叉树;
    • 满二叉树;
    • 完美二叉树;
    • 二叉堆(小顶堆和大顶堆)
    • 单次查找树(前序树)
    • 顺序存储二叉树;
    • 线索化二叉树;
    • 霍夫曼树;
    • B树、B+树和B*树;

0.2 各种树的特点

1. 二叉搜索树(二叉排序树)

二叉搜索树
  • 全部左边子孙节点 <= n <= 全部右边子孙节点;
  • 碰到二叉树时,很多面试者会假定面试官问的是二叉搜索树,此时要务必问清是否的二叉搜索树;

2. 平衡二叉树

  • 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树;
  • 树是否平衡需要跟面试官确认;
  • 思考方向可以有:“平衡”树实际上多半意味着“不是非常不平衡的树”。其平衡性足以保证执行insert和find操作可以在O(log n)的时间复杂度内完成,但不一定是严格意义上的平衡树;
  • 平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等;

3. 完整二叉树

完整二叉树
  • 除最后一层,其它层被填满,并且最后一层节点从左到右填充;
  • 中序遍历结果按顺序排列;

4. 满二叉树

满二叉树
  • 每个节点都有0个或两个子节点,不存在只有一个子节点的节点;

5. 完满二叉树

完满二叉树
  • 完满二叉树既是完整二叉树,又是满二叉树;
  • 正好有2k-1个节点;

6. 二叉堆(小顶堆和大顶堆)

二叉堆(小顶堆和大顶堆)
  • 一个二叉堆是一个完整二叉树,每个节点值小于其左右子节点;
  • 最小堆插入元素,总是从最底部、最右边节点开始,通过与其祖先节点交换来向上传递较小值;
  • 删除最小堆最小元素,将堆中最后一个元素放在顶节点,通过与子节点交换来向下传递较大值;
  • 上述两个算法时间复杂度为O(log n);

7. 单次查找树(前序树)

单次查找树(前序树)
  • *节点(有时称空节点)代表一个完整单次;
  • 相比散列表可以识别字符串是否是任何有效单次的前缀,可以在O(K)的时间复杂度内检查字符串是否为有效前缀;
  • 涉及一组有效单词的问题可以使用单次查找树进行优化;

8. 顺序存储二叉树

顺序存储二叉树
  • 从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组;
  • 顺序二叉树通常只考虑完全二叉树;
  • 第n个元素的左子节点为 2*n+1;
  • 第n个元素的右子节点为 2*n+2;
  • 第n个元素的父节点为 (n-1)/2;
  • n : 表示二叉树中的第几个元素(按0开始编号如图所示);

9. 线索化二叉树

线索化二叉树
  • n个结点的二叉链表中含有 2n-(n-1)=n+1 个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索");

10. 霍夫曼树

霍夫曼树
  • 给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree);

11. B树、B+树和B*树

B树

B+树

B*树

0.3 树的三种遍历方式

中序遍历

void inOrderTraversal(TreeNode node){
    if(node != null){
        inOrderTraversal(node.left);
        visit(node);
        inOrderTraversal(node.right);
    }
}

前序遍历

void preOrderTraversal(TreeNode node){
    if(node != null){
        visit(node);
        preOrderTraversal(node.left);
        preOrderTraversal(node.right);
    }
}

后序遍历

void postOrderTraversal(TreeNode node){
    if(node != null){
        postOrderTraversal(node.left);
        postOrderTraversal(node.right);
        visit(node);
    }
}

0.4 图的表示形式与搜索

图的表示方式

  • 邻接链表法;
  • 邻接矩阵法;
  • 邻接矩阵法使用BFS搜索,效率会比较低,因为需要迭代所有结点以便找到相邻节点;

图的搜索

  • 深度优先搜索(DFS);
    • 访问图中所有节点,或者访问最少节点直至找到某节点,DFS一般比较简单;
  • 广度优先搜索(BFS);
    • 如果想找到两个节点的最短路径(或任意路径),BFS一般比较适宜;
    • BFS是通过队列实现;
  • 双向搜索;
    • 双向搜索用于查找起始节点和目的节点间的最短路径;
    • 本质上是从起始节点和目的节点同时开始的两个广度优先搜索;
    • 当两个搜索相遇时,即找到一条路径
图的搜索

双向搜索

深度优先搜索(DFS):

  • 前序和树遍历的其他形式都是一种DFS;
void search(Node root){
    if(root == null){
        return;
    }
    visit(root);
    root.visited = true;
    for each(Node n in root.adjacent){
        if(n.visited == false){
            search(n);
        }
    }
}

广度优先搜索(BFS):

void search(Node root){
    Queue queue = new Queue();
    root.marked = true;
    queue.enqueue(root); //加入队尾
    
    while(!queue.isEmpty()){
        Node r = queue.dequeue(); //从队列头部删除
        visit(r);
        for each(Node n in r.adjacent){
            if(n.mark == false){
                n.marked = true;
                queue.enqueue(n);
            }
        }
    }
}


1. 节点间通路 [medium]

节点间通路

1.1 考虑点

  • 询问面试官重复的边是否是双向边,本题中不是。笔者以为是双向边,第五种解法给出思路;
  • 可以跟面试官讨论广度优先搜索和深度优先搜索的利弊:
    • 广度优先搜索:适合查找最短路径;
    • 深度优先搜索:实现比较简单,递归即可;在访问邻近节点之前,可能会深度遍历其中一个邻近节点;

1.2 解法

1.2.1 广度优先遍历法(优)

public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target) {
    Queue queue = new LinkedList<>();
    boolean[] isVisit = new boolean[n];
    isVisit[start] = true;
    queue.offer(start);
    while( !queue.isEmpty() ){
        int thisNode = queue.poll();
        int toNode;
        for(int i = 0; i < graph.length; i++){
            if(graph[i][0] == thisNode){
                toNode = graph[i][1];
                if(toNode == target){
                    return true;
                }
                if(!isVisit[toNode]){
                    isVisit[toNode] = true;
                    queue.offer(toNode);
                }
            }
        }
    }
    return false;
}
  • 执行时间:91.21%;内存消耗:92.42%;
  • 适用于n较少的时候,当n过大会超时;

1.2.2 深度优先遍历法

private boolean[] visited = null;
public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target) {
    this.visited = new boolean[graph.length];
    return helper(graph, start, target);
}

private boolean helper(int[][] graph, int start, int target) {
    for (int i = 0; i < graph.length; ++i) {
        // 确保当前路径未被访问(该判断主要是为了防止图中自环出现死循环的情况)
        if (!visited[i]) {
            // 若当前路径起点与终点相符,则直接返回结果
            if (graph[i][0] == start && graph[i][1] == target) {
                return true;
            }
            // 设置访问标志
            visited[i] = true;
            // DFS关键代码,思路:同时逐渐压缩搜索区间
            if (graph[i][1] == target && helper(graph, start, graph[i][0])) {
                return true;
            }
            // 清除访问标志
            visited[i] = false;
        }
    }
    return false;
}
  • 执行时间:81.17%;内存消耗:97.41%;
  • 首先设置访问状态数组
  • 使用DFS「深度优先搜索」进行递归搜索,逐渐压缩搜索区间,直至最终找到start与target在同一个路径内,则说明查找成功;

1.2.3 邻接矩阵的幂的性质

public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target) {
    int[][] adjacentMatrix = new int[n][n];
    for (int[] edge : graph) {
        adjacentMatrix[edge[0]][edge[1]] = 1;
    }
    for (int i = 1; i <= n; i++) {
        int[][] matrix = matrixPow(adjacentMatrix, i);
        if (matrix[start][target] != 0) {
            return true;
        }
    }
    return false;
}

public int[][] matrixPow(int[][] matrix, int n) {
    int size = matrix.length;
    int[][] res = new int[size][];
    for (int i = 0; i < size; i++) {
        res[i] = Arrays.copyOf(matrix[i], size);
    }
    for (int i = 1; i < n; i++) {
        for (int row = 0; row < size; row++) {
            int[] tmp = new int[size];
            for (int col = 0; col < size; col++) {
                for (int j = 0; j < size; j++) {
                    tmp[col] += res[row][j] * matrix[j][col];
                }
            }
            res[row] = Arrays.copyOf(tmp, size);
        }
    }
    return res;
}
  • 在本测试用例中超时;
  • 用到邻接矩阵的幂的性质;

1.2.4 当重复边为双向边时

public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target) {
    int[] count = new int[n*(n-1)/2];
    for(int i = 0; i < graph.length; i++){
        int mark = (2*n-1-graph[i][0])*graph[i][0]/2-1+graph[i][1]-graph[i][0];
        count[mark]++;
        if(count[mark] == 2){
            int cache = graph[i][0];
            graph[i][0] = graph[i][1];
            graph[i][1] = cache;
        }
    }

    Queue queue = new LinkedList<>();
    boolean[] isVisit = new boolean[n*(n-1)/2];
    queue.offer(start);

    while( !queue.isEmpty() ){
        int thisNode = queue.poll();
        int toNode;
        for(int i = 0; i < graph.length; i++){
            int mark = (2*n-1-graph[i][0])*graph[i][0]/2-1+graph[i][1]-graph[i][0];
            if(graph[i][0] == thisNode && !isVisit[mark]){
                toNode = graph[i][1];
                if(toNode == target){
                    return true;
                }
                queue.offer(toNode);
            }
        }
    }
    return false;
}
  • 相比前面做法增加了个方向倒转,也就是把重复边的方向倒转;


2. 最小高度树 [easy]

最小高度树

2.1 考虑点

  • 要创建最小生成树,就必须让左右子树的节点数量尽可能接近;

2.2 解法

2.2.1 递归法(优)

public TreeNode sortedArrayToBST(int[] nums) {
    if(nums.length == 0){
        return null;
    }
    int l = 0;
    int r = nums.length-1;
    return recur(l, r, nums);
}

public TreeNode recur(int l, int r, int[] nums){
    if(l > r){
        return null;
    }
    int mid = (l+r)/2;
    int headVal = nums[mid];
    TreeNode head = new TreeNode(headVal);
    head.left  = recur(l, mid-1, nums);
    head.right = recur(mid+1, r, nums);
    return head;
}
  • 执行时间:100.00%;内存消耗:53.37%;
  • 时间复杂度:O(n)。数组中的元素都使用1次,时间复杂度为O(n);
  • 空间复杂度:O(log n )。递归使用栈辅助空间,空间复杂度O(log n );
  • 注意递归结束条件,没有等于是因为递归里有对mid增减进行操作;
  • 注意递归的参数lr,表示需要递归的范围,不包括mid头结点;

2.2.2 BFS广度优先遍历

public TreeNode sortedArrayToBST(int[] num) {
    if (num.length == 0)
        return null;
    Queue rangeQueue = new LinkedList<>();
    Queue nodeQueue = new LinkedList<>();
    int lo = 0;
    int hi = num.length - 1;
    int mid = (lo + hi) >> 1;
    TreeNode node = new TreeNode(num[mid]);
    rangeQueue.add(new int[]{lo, mid - 1});
    rangeQueue.add(new int[]{mid + 1, hi});
    nodeQueue.add(node);
    nodeQueue.add(node);
    while (!rangeQueue.isEmpty()) {
        int[] range = rangeQueue.poll();
        TreeNode currentNode = nodeQueue.poll();
        lo = range[0];
        hi = range[1];
        if (lo > hi) {
            continue;
        }
        mid = (lo + hi) >> 1;
        int midValue = num[mid];
        TreeNode newNode = new TreeNode(midValue);
        if (midValue > currentNode.val)
            currentNode.right = newNode;
        else
            currentNode.left = newNode;
        if (lo < hi) {
            rangeQueue.add(new int[]{lo, mid - 1});
            rangeQueue.add(new int[]{mid + 1, hi});
            nodeQueue.add(newNode);
            nodeQueue.add(newNode);
        }
    }
    return node;
}
  • 执行时间:100.00%;内存消耗:5.01%;
  • 把数组不停的分为两部分,保存在队列中,然后不停的出队,创建结点;


3. 特定深度节点链表 [medium]

特定深度节点链表

3.1 考虑点

  • 不需要逐一遍历每一层,可以用任意方式遍历整棵树,只需要记住节点位于哪一层即可;

3.2 解法

3.2.1 使用栈暴力法

public ListNode[] listOfDepth(TreeNode tree) {
    if(tree == null){
        return null;
    }
    Queue queue = new LinkedList<>();
    queue.offer(tree);
    int floor = 1; //第x-1层节点个数
    List listArr = new ArrayList<>();
    //当队列不为空
    while(!queue.isEmpty()){
        //按层处理
        int count = 0; //count用来储存第x层节点个数
        ListNode head = null;
        ListNode cur = null;
        for(int i = 0; i < floor; i++){
            TreeNode node = queue.poll();
            //创建链表
            if(i == 0){
                head = new ListNode(node.val);
                cur = head;
            } else {
                ListNode nodeL = new ListNode(node.val);
                cur.next = nodeL;
                cur = nodeL;
            }
            //加入队列
            if( node.left != null){
                count++;
                queue.offer(node.left);
            }
            if(node.right != null){
                count++;
                queue.offer(node.right);
            }
        }
        listArr.add(head);
        floor = count; //替换floor
    }
    ListNode[] list = new ListNode[listArr.size()];
    for(int i = 0; i < listArr.size(); i++){
        list[i] = listArr.get(i);
    }
    return list;
}
  • 执行时间:89.59%;内存消耗:75.08%;
  • 需要注意几个细节:
    • ListNode cur = null:需要对cur初始化;
    • floor = count:记得要替换floor;
  • 由于事先不知道数组个数,先用一个ArrayList装起来,然后再转换成数组;

3.2.2 使用栈暴力法(代码优化缩短)

public ListNode[] listOfDepth(TreeNode tree) {
    if(tree == null){
        return null;
    }
    Queue queue = new LinkedList<>();
    queue.offer(tree);
    List listArr = new ArrayList<>();
    //当队列不为空
    while(!queue.isEmpty()){
        //按层处理
        int size = queue.size();
        ListNode head = new ListNode(0);
        ListNode cur = head;
        for(int i = 0; i < size; i++){
            TreeNode node = queue.poll();
            //创建链表
            cur.next = new ListNode(node.val);
            //加入队列
            if( node.left != null){
                queue.offer(node.left);
            }
            if(node.right != null){
                queue.offer(node.right);
            }
            cur = cur.next;
        }
        listArr.add(head.next);
    }
    return listArr.toArray(new ListNode[] {});
}
  • 执行时间:89.59%;内存消耗:25.97%;
  • 思路同上,更多调用java的api简化代码;

3.2.3 递归法(优)

public ListNode[] listOfDepth(TreeNode tree) {
    List list = new ArrayList<>();
    dfs(list, tree, 1);
    ListNode[] arr = new ListNode[list.size()];
    for (int i = 0; i < arr.length; i++) {
        arr[i] = list.get(i);
    }
    return arr;
}
private void dfs(List list, TreeNode node, int deep) {
    if (node == null) {
        return;
    }
    if (deep > list.size()) {
        list.add(new ListNode(node.val));
    } else {
        ListNode n = list.get(deep - 1);
        while (n.next != null) {
            n = n.next;
        }
        n.next = new ListNode(node.val);
    }
    dfs(list, node.left, deep + 1);
    dfs(list, node.right, deep + 1);
}
  • 执行时间:100.00%;内存消耗:96.63%;
  • 定义树深度为deep,同一个深度的保存到同一个ListNode;


4. 检查平衡性 [easy]

检查平衡性

4.1 考虑点

  • 其实,只需要一个checkHeight函数即可,它既可以计算高度,也可以平衡检查。可以使用整数返回值表示两者;

4.2 解法

4.2.1 自顶向下的递归

public boolean isBalanced(TreeNode root) {
    if(root == null){
        return true;
    }
    int deep = 1;
    int rootLeftDeep = findDeep(root.left, deep);
    int rootRightDeep = findDeep(root.right, deep);
    if(rootLeftDeep == -1 || rootRightDeep == -1){
        return false;
    }
    int result = Math.abs(rootLeftDeep - rootRightDeep);
    return result<2; 
}

public int findDeep(TreeNode node, int deep){
    if(node == null){
        return deep-1;
    }
    if(node.left == null && node.right == null){
        return deep;
    }
    deep++;
    int leftDeep = findDeep(node.left,deep);
    int rightDeep = findDeep(node.right,deep);
    int deepDiff = Math.abs(leftDeep-rightDeep);
    return deepDiff>1 ? -1 : Math.max(leftDeep,rightDeep);
}
  • 执行时间:88.57%;内存消耗:20.48%;
  • 方法虽然可行,但不高效,Math.abs()方法会被反复调用计算同一个节点的高度;

4.2.2 自顶向下的递归

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 root) {
    if (root == null) {
        return 0;
    } else {
        return Math.max(height(root.left), height(root.right)) + 1;
    }
}
  • 执行时间:88.57%;内存消耗:74.16%;

  • 时间复杂度:O(n2),其中 n 是二叉树中的节点个数。最坏情况下,二叉树是满二叉树,需要遍历二叉树中的所有节点,时间复杂度是 O(n)。对于节点 p,如果它的高度是 d,则 height(p) 最多会被调用 d 次(即遍历到它的每一个祖先节点时)。对于平均的情况,一棵树的高度 h 满足 O(h)=O(logn),因为 d≤h,所以总时间复杂度为 O(nlogn)。对于最坏的情况,二叉树形成链式结构,高度为 O(n),此时总时间复杂度为 O(n2);

  • 空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n;

4.2.3 自底向上的递归(优)

public boolean isBalanced(TreeNode root) {
    return height(root) >= 0;
}

public int height(TreeNode root) {
    if (root == null) {
        return 0;
    }
    int leftHeight = height(root.left);
    int rightHeight = height(root.right);
    if (leftHeight == -1 || rightHeight == -1 || Math.abs(leftHeight - rightHeight) > 1) {
        return -1;
    } else {
        return Math.max(leftHeight, rightHeight) + 1;
    }
}
  • 执行时间:88.57%;内存消耗:96.72%;
  • 时间复杂度:O(n),其中 n 是二叉树中的节点个数。使用自底向上的递归,每个节点的计算高度和判断是否平衡都只需要处理一次,最坏情况下需要遍历二叉树中的所有节点,因此时间复杂度是 O(n);
  • 空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n;


5. 合法二叉搜索树 [medium]

合法二叉搜索树

5.1 考虑点

  • 有两种思路:
    • 一是利用中序遍历;
    • 二是建立在 left

5.2 解法

5.2.1 递归法

class Solution {
    Stack stack = new Stack<>();
    boolean isFlag = false;
    public boolean isValidBST(TreeNode root) {
        if(root == null){
            return true;
        }
        inOrderTraversal(root);
        return !isFlag;
    }

    public void inOrderTraversal(TreeNode node){
        if(isFlag){
            return;
        }
        if(node == null){
            return;
        }
        inOrderTraversal(node.left);
        if(stack.isEmpty()){
            stack.push(node.val);
        } else {
            if(stack.peek() >= node.val){
                isFlag = true;
                return;
            } else {
                stack.push(node.val);
            }           
        }
        inOrderTraversal(node.right);
    }
}
  • 执行时间:32.30%;内存消耗:91.41%;

5.2.2 中序遍历非递归

public boolean isValidBST(TreeNode root) {
    Deque stack = new LinkedList();
    double inorder = -Double.MAX_VALUE;
    while (!stack.isEmpty() || root != null) {
        while (root != null) {
            stack.push(root);
            root = root.left;
        }
        root = stack.pop();
            // 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树
        if (root.val <= inorder) {
            return false;
        }
        inorder = root.val;
        root = root.right;
    }
    return true;
}
  • 执行时间:24.37%;内存消耗:98.61%;
  • 时间复杂度 : O(n),其中 n 为二叉树的节点个数。二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n);
  • 空间复杂度 : O(n),其中 n 为二叉树的节点个数。栈最多存储 n 个节点,因此需要额外的 O(n) 的空间;

5.2.3 中序遍历递归(优)

//前一个结点,全局的
TreeNode prev;

public boolean isValidBST(TreeNode root) {
    if (root == null)
        return true;
    //访问左子树
    if (!isValidBST(root.left))
        return false;
    //访问当前节点:如果当前节点小于等于中序遍历的前一个节点直接返回false。
    if (prev != null && prev.val >= root.val)
        return false;
    prev = root;
    //访问右子树
    if (!isValidBST(root.right))
        return false;
    return true;
}
  • 执行时间:100.00%;内存消耗:65.74%;
  • 中序遍历时,判断当前节点是否大于中序遍历的前一个节点,也就是判断是否有序,如果不大于直接返回 false


6. 后继者 [medium]

后继者

6.1 考虑点

  • 因为是二叉搜索树,可以很方便找到节点,再根据是否有右子树分类判断;

6.2 解法

6.2.1 中序遍历递归法

class Solution {
    Stack stack = new Stack<>();
    TreeNode findNode = null;
    public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
        if(root == null || p == null){
            return null;
        }
        inOrderTravarsal(root,p);
        return findNode!=null ? findNode : null;
    }

    public void inOrderTravarsal(TreeNode node, TreeNode p){
        if(findNode != null){
            return;
        }
        if(node == null){
            return;
        }
        inOrderTravarsal(node.left, p);
        if(stack.isEmpty()){
            stack.push(node);
        } else {
            if(p.equals(stack.peek())){
                findNode = node;
                stack.pop(); //忘记pop
                return;
            } else {
                stack.push(node);
            }
        }
        inOrderTravarsal(node.right, p);
    }
}
  • 执行时间:72.07%;内存消耗:9.51%;

6.2.2 中序遍历非递归法(优)

public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
    TreeNode pre = null;
    while(root.val!=p.val){
        //右边
        if(p.val > root.val){          
            root = root.right;
        }
        //左边
        else{   
            pre = root;
            root = root.left;
        }
    }
    //假如没有右子树
    if(root.right==null){
        return pre;
    } else {
        root = root.right;
        while(root.left!=null){
            root = root.left;
        }
        return root;
    }  
}
  • 执行时间:100.00%;内存消耗:100.00%;
  • 找到节点后,如果右子树存在,那就是右子树最左边的节点。如果右子树不存在,表示已经访问n层子树,需要回到n的父节点q;如果n在q的左边,那就是q。反之需要往上访问,直到找到还未完全遍历的节点x;


8. 首个共同祖先 [medium]

首个共同祖先

8.1 考虑点

  • 如果是二叉搜索树,可以看看两条路径在哪开始分叉;

8.2 解法

8.2.1 深度优先遍历

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if(root == null){
        return null;
    } else if(root.equals(p)){
        return p;
    } else if(root.equals(q)){
        return q;
    }
    TreeNode leftNode = lowestCommonAncestor(root.left,p,q);
    TreeNode rightNode = lowestCommonAncestor(root.right,p,q);
    if(leftNode == null && rightNode == null){
        return null;
    }
    if(leftNode != null && rightNode == null){
        if(leftNode.equals(q)){
            return q;
        } else if(leftNode.equals(p)){
            return p;
        } else {
            return leftNode;
        }
    }
    if(leftNode == null && rightNode != null){
        if(rightNode.equals(q)){
            return q;
        } else if(rightNode.equals(p)){
            return p;
        } else {
            return rightNode;
        }
    }
    //注意非空校验
    if((leftNode.equals(p) && rightNode.equals(q)) || (leftNode.equals(q) && rightNode.equals(p))){
        return root;
    } 
    return null;
}
  • 执行时间:52.31%;内存消耗:5.19%;

8.2.2 深度优先遍历的简便写法(优)

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    // 到底了还没找到,返回 null
    if (root == null) {
        return null;
    }
    // 如果找到了 p 或 q,返回它
    if (root == p || root == q) {
        return root;
    }
    TreeNode left = lowestCommonAncestor(root.left, p, q);  
    TreeNode right = lowestCommonAncestor(root.right, p, q); 
    // 如果 left 和 right 都记录了找到的节点,那么肯定是一个记录了 p ,另一个记录了 q
    // 它们分别在以 root 为根的左右子树中,所以 root 就是它们的最近公共祖先
    if (left != null && right != null) {
        return root;
    }
    // 由于节点 p,q 一定在二叉树中,left和right不会同时为null
    // 若 left != null && right == null,说明在左子树中找到 p 或 q,而在右子树找不到 p 或 q,则剩下一个也在左子树
    // 所以 left 就是最近公共祖先
    // 另一种情况同理
    return (left != null) ? left : right;
}
  • 执行时间:100.00%;内存消耗:91.55%;


9. 二叉搜索树序列 [hard]

二叉搜索树序列

9.1 考虑点

  • 数组的第一个数必须为顶节点;
  • 与左右节点的插入顺序无关紧要,但子节点的添加一定要在父节点之后;

9.2 解法

9.2.1 回溯法+广度优先遍历

private List> ans;

public List> BSTSequences(TreeNode root) {
    ans = new ArrayList<>();
    List path = new ArrayList<>();
    // 如果 root==null 返回 [[]]
    if (root == null) {
        ans.add(path);
        return ans;
    }
    List queue = new LinkedList<>();
    queue.add(root);
    // 开始进行回溯
    bfs(queue, path);
    return ans;
}

// 回溯法+广度优先遍历
private void bfs(List queue, List path) {
    // queue 为空说明遍历完了,可以返回了
    if (queue.isEmpty()) {
        ans.add(new ArrayList<>(path));
        return;
    }
    // 将 queue 拷贝一份,用于稍后回溯
    List copy = new ArrayList<>(queue);
    // 对 queue 进行循环,每循环考虑 “是否 「将当前 cur 节点从 queue 中取出并将其左右子
    // 节点加入 queue ,然后将 cur.val 加入到 path 末尾」 ” 的情况进行回溯
    for (int i = 0; i < queue.size(); i++) {
        TreeNode cur = queue.get(i);
        path.add(cur.val);
        queue.remove(i);
        // 将左右子节点加入队列
        if (cur.left != null) queue.add(cur.left);
        if (cur.right != null) queue.add(cur.right);
        bfs(queue, path);
        // 恢复 path 和 queue ,进行回溯
        path.remove(path.size() - 1);
        queue = new ArrayList<>(copy);
    }
}
  • 执行时间:90.30%;内存消耗:93.98%;
  • 对于这种找出所有情况的题目,回溯法是最容易想到的方法之一了,这道题也可以用回溯法,可以发现刚才选元素的过程和层序遍历的过程其实是一致的:
    • 最开始 queue 中只有 12 ,只能选12,将 12 出队并将它的两个子节点入队,得到 [12];
      选了12之后 queue 中剩下 5、19 ,就从 5 和 19 中选一个,得到 [12,5],[12,19];
      • 如果选了 5 ,将 5 出队并将它的两个子节点入队,那么此时 queue 中剩下 19、2、9,得到 [12,5,2],[12,5,9],[12,5,19];
      • 如果选了 19 ,将 19 出队并将它的子节点入队,那么此时 queue 中剩下 5、15,得到 [12,19,5],[12,19,15];
    • 后续同理;

10. 检查子树 [medium]

检查子树

10.1 考虑点

  • 这里的“万”是干扰的;

10.2 解法

10.2.1 递归法(优)

boolean isFound =false;
public boolean checkSubTree(TreeNode t1, TreeNode t2) {
    if(t1 == null){
        return false;
    }
    if(t1.val == t2.val){
        isFound = true;
        return true;
    } else {
        isFound = false;
    }
    boolean left;
    boolean right;
    if(isFound){
        left = checkSubTree(t1.left, t2.left);
        right = checkSubTree(t1.right, t2.right);
        return left && right;
    } else {
        left = checkSubTree(t1.left, t2);
        right = checkSubTree(t1.right, t2);
        if(left){
            return checkSubTree(t1.left, t2);
        }
        if(right){
            return checkSubTree(t1.right, t2);
        }
    }
    return false;   
}
  • 执行时间:100.00%;内存消耗:46.52%;


12. 求和路径 [medium]

求和路径

12.1 考虑点

  • 可以使用散列表优化算法,下面解法没给出;

12.2 解法

12.2.1 暴力法(优)

class Solution {
    int res = 0;

    public int pathSum(TreeNode root, int sum) {
        int dep = depth(root);
        int[] paths = new int[dep];
        pathSum(root, sum, 0, paths);
        return res;
    }
    public void pathSum(TreeNode root, int sum, int level, int[] paths) {
        if (root == null) {
            return;
        }
        paths[level] = root.val;
        int t = 0;
        for (int i = level; i >= 0; i--) {
            t += paths[i];
            if (t == sum) {
                res += 1;
            }
        }
        pathSum(root.left, sum, level + 1, paths);
        pathSum(root.right, sum, level + 1, paths);
    }
    public int depth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        return Math.max(depth(root.left), depth(root.right)) + 1;
    }
}
  • 执行时间:100.00%;内存消耗:69.08%;

12.2.2 回溯法

private int res = 0;

public int pathSum(TreeNode root, int sum) {
    if(root == null) return res;

    helper(root, sum);
    pathSum(root.left, sum);
    pathSum(root.right, sum);
    return res;
}

private void helper(TreeNode node, int sum){
    if(node == null) return;

    sum -= node.val;
    if(sum == 0)
        res ++;
    helper(node.left, sum);
    helper(node.right, sum);
    sum += node.val;
}
  • 执行时间:58.34%;内存消耗:10.27%;



最后

你可能感兴趣的:(算法 | 第4章 树与图相关《程序员面试金典》)