《剑指offer》各编程题Java版分析 -- 解决面试题的思路

130 阅读13分钟

面试题27 -- 二叉树的镜像

题目:请完成一个函数,输入一颗二叉树,该函数输出它的镜像。

image.png

本章书中的意思是建议大家面对暂时想不到解决办法的题目时,多通过 画图举例子 这样的方式将题目文字描述抽象出具体的概念,并通过举例子(即归纳总结)的方式将题目分析清楚。其实图像记忆和归纳总结也是人类大脑最擅长的工作,在解答题目时充分利用人脑更擅长的能力去处理往往事半功倍。

至于本题,通过画图可以看出,就是通过递归的手段把所有节点的left和right翻转一下。

public class Solution {

    public TreeNode mirrorTree(TreeNode root) {
        if (root == null) {
            return null;
        }
        switchLeftAndRight(root);
        if (root.left != null) {
            mirrorTree(root.left);
        }
        if (root.right != null) {
            mirrorTree(root.right);
        }
        return root;
    }

    private void switchLeftAndRight(TreeNode root) {
        if (root == null) {
            return;
        }
        TreeNode temp = root.left;
        root.left = root.right;
        root.right = temp;
    }

}

面试题28 -- 对称的二叉树

题目:请实现一个函数,用来判断一颗二叉树是不是对称的。如果一颗二叉树和它的镜像一样,那么它是对称的。例如,在如图所示的3棵二叉树中,第一棵二叉树实对称的,而另外两棵二叉树不是。

image.png

通过举例画图的手段可以归纳总结出对称二叉的特征,
isSymmetric(root) = (root.left.val == root.right.val) && isSymmetric(root.left, root.right)

一个树是对称二叉树,即根节点的左右孩子的值相等,且左右子树是对称的

接下来需要定义两棵树的对称,
isSymmetric(root1, root2) = (root1.val == root2.val) && isSymmetric(root1.left, root2.right) && isSymmetric(root1.right, root2.left)

两个树是对称的,即两个树的根节点值相等,且root1的左子树与root2的右子树对称,root1的右子树与root2的左子树对称

image.png

public class Solution {

    /*
            1
           / \
          2   2
         / \ / \
        3  4 4  3
     */
    public boolean isSymmetric(TreeNode root) {
        if (root == null) {
            return true;
        }
        return areSymmetric(root.left, root.right);
    }

    private boolean areSymmetric(TreeNode root1, TreeNode root2) {
        if (root1 == null && root2 == null) {
            return true;
        } else if (root1 == null || root2 == null) {
            return false;
        }
        return root1.val == root2.val
                && areSymmetric(root1.left, root2.right)
                && areSymmetric(root1.right, root2.left);
    }

}

面试题29 -- 顺时针打印矩阵

题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。例如,如果输入如下矩阵: 1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
则依次打印出数字 1,2,3,5,8,12,16,15...

书中的解法是将顺时针旋转拆分成从外到里的n个圆圈,然后每一层圆圈遍历。

interview29_2.gif

笔者的方式是控制上下左右边界,逐步走到up>=down left>=right的时候为止。

interview29.gif

public class Solution {
    public int[] spiralOrder(int[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return new int[0];
        }
        int[] result = new int[matrix.length * matrix[0].length];
        int resultIndex = 0;
        int i = 0;
        int j = 0;
        int up = 0;
        int down = matrix.length;
        int left = 0;
        int right = matrix[0].length;
        while(up < down && left < right) {
            while(j < right) {
                result[resultIndex++] = matrix[i][j];
                j++;
            }
            j--;
            up++;
            i++;
            if (up >= down || left >= right) {
                break;
            }
            while(i < down) {
                result[resultIndex++] = matrix[i][j];
                i++;
            }
            i--;
            right--;
            j--;
            if (up >= down || left >= right) {
                break;
            }
            while(j >= left) {
                result[resultIndex++] = matrix[i][j];
                j--;
            }
            j++;
            down--;
            i--;
            if (up >= down || left >= right) {
                break;
            }
            while(i >= up) {
                result[resultIndex++] = matrix[i][j];
                i--;
            }
            i++;
            left++;
            j++;
        }
        return result;
    }
}

面试题30 -- 包含min函数的栈

题目:定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的min函数。在该栈中,调用min、push及pop的时间复杂度都是O(1)

因为需要min函数的时间复杂度为O(1),传统的栈结构存储方式想要从栈中捞出最小的元素需要O(n)的时间,所以肯定需要对栈内部的存储结构进行扩展,即增加一个内部的最小元素栈,结构如下图。

structure of min stack

push的过程如下:

interview30.gif

pop的过程如下:

interview30_2.gif

基于以上分析,可以得到实现代码:

public class MinStack {

    private Deque<Integer> dataStack;
    private Deque<Integer> minDataStack;

    /** initialize your data structure here. */
    public MinStack() {
        dataStack = new LinkedList<>();
        minDataStack = new LinkedList<>();
    }

    public void push(int x) {
        dataStack.push(x);
        if (minDataStack.isEmpty() || x <= minDataStack.peek()) {
            minDataStack.push(x);
        }
    }

    public void pop() {
        int data = dataStack.pop();
        if (data == minDataStack.peek()) {
            minDataStack.pop();
        }
    }

    public int top() {
        return dataStack.peek();
    }

    public int min() {
        return minDataStack.peek();
    }

}

面试题31 -- 栈的压入、弹出序列

题目:输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列{1,2,3,4,5}是某栈的压栈序列,序列{4,5,3,2,1}是该压栈序列对应的一个弹出序列,但{4,3,5,1,2}就不可能是该压栈序列的弹出序列。

此题很直观的解决办法就是 根据序列模拟栈的压入弹出过程,当执行到明显无法做到的操作时返回false,否则执行完成则返回true。过程如下图:

interview31.gif

面试题32 -- 从上到下打印二叉树

题目一:不分行从上到下打印二叉树

从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。例如,输入如下图的二叉树,一次打印出 8,6,10,5,7,8,11

image.png

基础的层次遍历,题目一不再赘述。

public int[] levelOrder(TreeNode root) {
    if (root == null) {
        return new int[0];
    }
    List<Integer> result = new LinkedList<>();
    Deque<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    while(!queue.isEmpty()) {
        TreeNode node = queue.poll();
        result.add(node.val);
        if (node.left != null) {
            queue.offer(node.left);
        }
        if (node.right != null) {
            queue.offer(node.right);
        }
    }
    int[] array = new int[result.size()];
    int i = 0;
    for (Integer val : result) {
        array[i++] = val;
    }
    return array;
}

题目二:分行从上到下打印二叉树

从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。例如,上图中的二叉树的结果为:

8
6 10
5 7 9 11

和基础层次遍历不同的是,这里需要在遍历的时候关注当前的层数。笔者一开始想到的办法是通过扩展出一个对象<Node, currentLevel>,在基础的层级遍历时,当发现level第一次增多时打印换行。

书中另一种比较简单的办法是在基础层次遍历的基础上,增加一个临时队列,保证当前队列中只有当前层级的节点,将这些节点的左右孩子存到另一个新的临时队列中,处理完当前队列后打印换行并且切换临时队列与当前队列的引用,继续进行下一层的遍历。

interview32_2.gif

public List<List<Integer>> levelOrder(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }
    List<List<Integer>> result = new ArrayList<>();
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    while (!queue.isEmpty()) {
        Queue<TreeNode> newQueue = new LinkedList<>();
        List<Integer> line = new ArrayList<>();
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            line.add(node.val);
            if (node.left != null) {
                newQueue.offer(node.left);
            }
            if (node.right != null) {
                newQueue.offer(node.right);
            }
        }
        result.add(line);
        queue = newQueue;
    }
    return result;
}

leetcode上提供了更巧妙的方法,here
在每一层处理完后当前队列的size就是当前层包含的节点个数,所以需要遍历消费完size个元素就打印换行,然后再继续记录新的size继续遍历。具体可以参考链接的做法,不再重复画图了。

public List<List<Integer>> levelOrderFasterVersion(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }
    List<List<Integer>> result = new ArrayList<>();
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    while (!queue.isEmpty()) {
        List<Integer> line = new ArrayList<>();
        for (int i = queue.size(); i > 0; i--) { // 当前queue.size()就是这一层的节点个数,只是控制queue.poll的次数
            TreeNode node = queue.poll();
            line.add(node.val);
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
        result.add(line);
    }
    return result;
}

题目三:之字形打印二叉树

请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左打印,第三行再按照从左到右的顺序打印,其他行以此类推。例如,图中的二叉树结果为:

1
3 2
4 5 6 7
15 14 13 12 11 10 9 8

image.png

对题目二的进一步扩展,

  • 增加一个bool类型的变量reverted
  • 遍历的时候根据reverted决定是从队列末尾poll还是队列头部poll(需要用Deque)
  • 在每一层遍历完之后根据reverted决定是左右顺序入队列还是右左顺序入队列

interview32_3.gif

public List<List<Integer>> levelOrder(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }
    List<List<Integer>> result = new ArrayList<>();
    Deque<TreeNode> queue = new LinkedList<>();
    queue.offerLast(root);
    boolean reverted = false;
    while (!queue.isEmpty()) {
        List<Integer> line = new ArrayList<>();
        for (int i = queue.size(); i > 0; i--) {
            TreeNode node = reverted ? queue.pollLast() : queue.pollFirst();
            line.add(node.val);
            if (!reverted) {
                if (node.left != null) queue.offerLast(node.left);
                if (node.right != null) queue.offerLast(node.right);
            } else {
                if (node.right != null) queue.offerFirst(node.right);
                if (node.left != null) queue.offerFirst(node.left);
            }
        }
        reverted = !reverted;
        result.add(line);
    }
    return result;
}

面试题33 -- 二叉搜索树的后序遍历序列

题目:输入一个整数数组,判断该数组是不是某二叉搜索树的后续遍历结果。如果是则返回true,否则返回false。假设输入的数组的任意两个数字互不相同。例如,输入数组{5,7,6,9,11,10,8},则返回true,因为这个整数序列是如图的二叉搜索树的额后续遍历结果。如果输入的数组是{7,4,6,5},则由于没有哪棵二叉树的后续遍历结果是这个序列,因此返回false。

image.png

二叉搜索树的特性是左孩子小于根节点、右孩子大于根节点。后续遍历的特性是最后的一个节点为根节点,剩余的部分前半部分为左子树,右半部分为右子树,如下图:

image.png

基于以上分析,可以得到二叉搜索树后序遍历序列的判断逻辑如下:

  • 序列的长度为1时,只有一个元素的序列一定是,返回true(同时也是递归出口)
  • 序列的最后一个值是root,
  • 遍历序列直到遇见第一个大于root的数字,则说明走到了left的最右边,接下来我们需要判断剩余的部分是否全部都大于root
    • 如果右子树节点不是全部大于root,则返回false,这不是一个正确的二叉搜索树后序遍历序列
    • 如果右子树全部大于root,则再递归判断左右子树是否是正确的二叉搜索树后序遍历序列
public boolean verifyPostorder(int[] postorder) {
    if(postorder == null || postorder.length == 0) {
        return true;
    }
    return verifyPostorderCore(postorder, 0, postorder.length);
}

private boolean verifyPostorderCore(int[] postorder, int start, int end) {
    if (start >= end || end - start == 1) {
        return true;
    }
    int firstOfRight = findFirstOfRight(postorder, start, end);
    return allBiggerThanRoot(postorder, firstOfRight, end)
            && verifyPostorderCore(postorder, start, firstOfRight)
            && verifyPostorderCore(postorder, firstOfRight, end - 1);
}

private int findFirstOfRight(int[] postorder, int start, int end) {
    int root = postorder[end-1];
    for(int i = start; i < end; i++) {
        if(postorder[i] > root) {
            return i;
        }
    }
    return end-1;
}

private boolean allBiggerThanRoot(int[] postorder, int start, int end) {
    int root = postorder[end-1];
    for(int i = start; i < end; i++) {
        if(postorder[i] < root) {
            return false;
        }
    }
    return true;
}

面试题34 -- 二叉树中和为某一值的路径

题目:输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。

这道题可以用常规的DFS回溯法解决,通过递归向下一直走到叶子节点,累加路径上节点的和,对比target,如果相等则加入结果集合;不相等则直接返回。返回到上一层之后移除掉末尾的元素。

interview34.gif

public List<List<Integer>> pathSum(TreeNode root, int target) {
    List<List<Integer>> result = new ArrayList<>();
    pathSumCore(root, target, 0, new ArrayList<>(), result);
    return result;
}

private void pathSumCore(TreeNode root, int target, int sum, List<Integer> temp, List<List<Integer>> result) {
    if (root == null) {
        return;
    }
    sum += root.val;
    temp.add(root.val);
    if (root.left == null && root.right == null) { // it's a leaf node
        if (sum == target) {
            result.add(new ArrayList<>(temp));
        }
        return;
    }
    if (root.left != null) {
        pathSumCore(root.left, target, sum, temp, result);
        temp.remove(temp.size() - 1);
    }
    if (root.right != null) {
        pathSumCore(root.right, target, sum, temp, result);
        temp.remove(temp.size() - 1);
    }
}

面试题35 -- 复杂链表的复制

题目:请实现函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个next指针指向下一个节点,还有一个random指针指向链表中的任意节点或者null。

时间复杂度O(n^2)、空间复杂度O(1)的解法

这个问题有很多解决办法,时间复杂度最高的解决办法是先顺着next复制一遍节点,然后再对每个节点的

image.png

image.png

时间复杂度O(n)、空间复杂度O(n)但是需要额外hash表空间的方法

通过hash表记录 原节点->复制节点 的映射,

  • 如果没有映射关系时,创建复制节点,并且创建映射关系
  • 如果有映射关系,从hash表中读取对应的复制节点,设置指针
public Node copyRandomList(Node head) {
    Map<Node, Node> nodeMap = new HashMap<>();
    Node originNode = head;
    Node resultHead = new Node(0);
    Node copyNode = resultHead;
    while(originNode != null) {
        copyNode.next = copyOrFindNode(originNode, nodeMap);
        copyNode.next.random = copyOrFindNode(originNode.random, nodeMap);
        copyNode = copyNode.next;
        originNode = originNode.next;
    }
    return resultHead.next;
}

private Node copyOrFindNode(Node originNode, Map<Node, Node> nodeMap) {
    if (originNode == null) {
        return null;
    }
    if (nodeMap.get(originNode) != null) {
        return nodeMap.get(originNode);
    }
    Node copyNode = new Node(originNode.val);
    nodeMap.put(originNode, copyNode);
    return copyNode;
}

时间复杂度O(n)、空间复杂度O(n)但是不需要辅助空间的方法

书中提供了一种不需要辅助空间的解决办法:

  1. 在原链表中每个节点后插入该节点的复制,如下图
  2. 然后每个原始节点的random指针指向的节点,它的复制节点的random都指向原节点的random节点的后一个(即random的复制节点)
  3. 把原始节点和复制节点拆分开

image.png

public Node copyRandomList(Node head) {
    if (head == null) {
        return null;
    }
    copyNodeToNeighbor(head); // 1
    copyRandom(head); // 2
    return extractCopyList(head); // 3
}

private void copyNodeToNeighbor(Node head) {
    Node originNode = head;
    while(originNode != null) {
        Node nextNode = originNode.next;
        originNode.next = new Node(originNode.val);
        originNode.next.next = nextNode;
        originNode = nextNode;
    }
}

private void copyRandom(Node head) {
    Node originNode = head;
    Node copyNode = head.next;
    while(originNode != null) {
        if (originNode.random != null) {
            copyNode.random = originNode.random.next;
        }
        if (copyNode.next == null) {
            break;
        }
        originNode = originNode.next.next;
        copyNode = copyNode.next.next;
    }
}

private Node extractCopyList(Node head) {
    Node originHead = head;
    Node originNode = originHead;
    Node copyHead = head.next;
    Node copyNode = copyHead;
    while(originNode.next.next != null) {
        originNode.next = originNode.next.next;
        originNode = originNode.next;
        copyNode.next = copyNode.next.next;
        copyNode = copyNode.next;
    }
    originNode.next = null;
    return copyHead;
}

面试题36 -- 二叉搜索树与双向链表

题目:输入一颗二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。比如,输入图中左边的二叉搜索树,则输出转换后的排序双向链表。

image.png

二叉搜索树从小到大的排列正好是这个二叉树的中序遍历结果,因此我们只需要在中序遍历的过程中处理节点的左右指针,缝合成一个双向链表就可以。

以递归的思路来看,例如遍历到10的时候,我们需要做的是将左子树中最后一个(即最大的)节点与当前节点进行连接,通过将当前最大的节点(即10节点)返回,供右子树进行连接操作。整个过程用语言表示如下:

  • 将左子树转换为双向链表(transform left tree to double list)
  • 连接左子树的最后一个节点与当前节点,处理没有左子树的情况,即头节点情况(join last node of left tree with node)
  • 将右子树转换为双向链表(transform right tree to double list)

image.png

class Solution {
    private Node pre;

    private Node head;

    public Node treeToDoublyList(Node root) {
        if (root == null) {
            return null;
        }
        dfs(root);
        head.left = pre; // leetcode上与书中不同的是,在整个遍历完成后还需要连接head和last node,做成一个循环链表
        pre.right = head;
        return head;
    }

    private void dfs(Node node) {
        if (node == null) {
            return;
        }
        dfs(node.left);
        if (pre == null) {
            head = node;
        } else {
            node.left = pre;
            pre.right = node;
        }
        pre = node;
        dfs(node.right);
    }
}

面试题37 -- 序列化二叉树

题目:请实现两个函数,分别用来序列化和反序列化二叉树

序列化:
采用层次遍历,对于空节点写null

image.png

反序列化:
通过观察序列化后的字符串,我们可以看出,每个节点的左右孩子都是在当前读取完的位置之后,如:

  • 1的左右孩子是2、3,这时index走到3
  • 2的左右孩子是4、null,这时index走到5
  • 3的左右孩子是5、6,这时index走到7(出界)

IMG_0007(20210425-135908).JPG

public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if (root == null) {
            return "";
        }
        List<String> resultList = new LinkedList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while(!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if (node == null) {
                resultList.add("null");
            } else {
                resultList.add(node.val + "");
                queue.offer(node.left);
                queue.offer(node.right);
            }
        }
        return listToString(resultList);
    }

    private String listToString(List<String> list) {
        StringBuilder builder = new StringBuilder();
        builder.append("[");
        for (String num : list) {
            builder.append(num).append(",");
        }
        builder.deleteCharAt(builder.length() - 1);
        builder.append("]");
        return builder.toString();
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        if (data == null || data.length() == 0) {
            return null;
        }
        String trimmedData = data.substring(1, data.length() - 1);
        String[] nums = trimmedData.split(",");
        TreeNode root = generateTreeNode(nums[0]);
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        int index = 1;
        while(!queue.isEmpty()) {
            TreeNode node = queue.poll();
            node.left = generateTreeNode(nums[index]);
            if (node.left != null) {
                queue.offer(node.left);
            }
            node.right = generateTreeNode(nums[index + 1]);
            if (node.right != null) {
                queue.offer(node.right);
            }
            index += 2;
        }
        return root;
    }

    private TreeNode generateTreeNode(String num) {
        if (num == null || "null".equals(num)) {
            return null;
        }
        return new TreeNode(Integer.parseInt(num));
    }

    public class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) { val = x; }
    }
}

面试题38 -- 字符串的排列

题目:输入一个字符串,打印出该字符串中字符的所有排列。例如,输入字符串abc,则打印出由字符a、b、c所能排列出来的所有字符串abc、acb、bac、bca、cab和cba。

此题为全排列问题,以 abc 为例

  • 先交换a和a(原地TP),还是abc
    • 然后交换第二个位置b和b,还是abc
      • 最后交换第三个位置c和c --> abc
    • 然后交换第二个位置b和c = acb
      • 最后交换第三个位置b和b --> acb
  • 先交换a和b = bac
    • 然后交换第二个位置a和a = bac
      • 最后交换第三个位置c和c --> bac ...

image.png

每层dfs都把某一位和后面所有位都交换一次

class Solution {

    private Set<String> result = new HashSet<>();

    public String[] permutation(String s) {
        if (s == null || s.length() == 0) {
            return new String[0];
        }
        permutationCore(s.toCharArray(), 0, "");
        return result.toArray(new String[0]);
    }

    private void permutationCore(char[] s, int cur, String temp) {
        if (cur == s.length - 1) {
            result.add(temp + s[cur]);
            return;
        }
        for (int i = cur; i < s.length; i++) {
            swap(s, i, cur);
            permutationCore(s, cur+1, temp+s[cur]);
            swap(s, i, cur);
        }
    }

    private void swap(char[] s, int i, int j) {
        char temp = s[i];
        s[i] = s[j];
        s[j] = temp;
    }
}