面试题27 -- 二叉树的镜像
题目:请完成一个函数,输入一颗二叉树,该函数输出它的镜像。
本章书中的意思是建议大家面对暂时想不到解决办法的题目时,多通过 画图 和 举例子 这样的方式将题目文字描述抽象出具体的概念,并通过举例子(即归纳总结)的方式将题目分析清楚。其实图像记忆和归纳总结也是人类大脑最擅长的工作,在解答题目时充分利用人脑更擅长的能力去处理往往事半功倍。
至于本题,通过画图可以看出,就是通过递归的手段把所有节点的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棵二叉树中,第一棵二叉树实对称的,而另外两棵二叉树不是。
通过举例画图的手段可以归纳总结出对称二叉的特征,
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的左子树对称
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个圆圈,然后每一层圆圈遍历。
笔者的方式是控制上下左右边界,逐步走到up>=down left>=right的时候为止。
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)的时间,所以肯定需要对栈内部的存储结构进行扩展,即增加一个内部的最小元素栈,结构如下图。
push的过程如下:
pop的过程如下:
基于以上分析,可以得到实现代码:
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。过程如下图:
面试题32 -- 从上到下打印二叉树
题目一:不分行从上到下打印二叉树
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。例如,输入如下图的二叉树,一次打印出 8,6,10,5,7,8,11
基础的层次遍历,题目一不再赘述。
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第一次增多时打印换行。
书中另一种比较简单的办法是在基础层次遍历的基础上,增加一个临时队列,保证当前队列中只有当前层级的节点,将这些节点的左右孩子存到另一个新的临时队列中,处理完当前队列后打印换行并且切换临时队列与当前队列的引用,继续进行下一层的遍历。
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
对题目二的进一步扩展,
- 增加一个bool类型的变量reverted
- 遍历的时候根据reverted决定是从队列末尾poll还是队列头部poll(需要用Deque)
- 在每一层遍历完之后根据reverted决定是左右顺序入队列还是右左顺序入队列
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。
二叉搜索树的特性是左孩子小于根节点、右孩子大于根节点。后续遍历的特性是最后的一个节点为根节点,剩余的部分前半部分为左子树,右半部分为右子树,如下图:
基于以上分析,可以得到二叉搜索树后序遍历序列的判断逻辑如下:
- 序列的长度为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,如果相等则加入结果集合;不相等则直接返回。返回到上一层之后移除掉末尾的元素。
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复制一遍节点,然后再对每个节点的
时间复杂度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)但是不需要辅助空间的方法
书中提供了一种不需要辅助空间的解决办法:
- 在原链表中每个节点后插入该节点的复制,如下图
- 然后每个原始节点的random指针指向的节点,它的复制节点的random都指向原节点的random节点的后一个(即random的复制节点)
- 把原始节点和复制节点拆分开
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 -- 二叉搜索树与双向链表
题目:输入一颗二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。比如,输入图中左边的二叉搜索树,则输出转换后的排序双向链表。
二叉搜索树从小到大的排列正好是这个二叉树的中序遍历结果,因此我们只需要在中序遍历的过程中处理节点的左右指针,缝合成一个双向链表就可以。
以递归的思路来看,例如遍历到10的时候,我们需要做的是将左子树中最后一个(即最大的)节点与当前节点进行连接,通过将当前最大的节点(即10节点)返回,供右子树进行连接操作。整个过程用语言表示如下:
- 将左子树转换为双向链表(transform left tree to double list)
- 连接左子树的最后一个节点与当前节点,处理没有左子树的情况,即头节点情况(join last node of left tree with node)
- 将右子树转换为双向链表(transform right tree to double list)
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
反序列化:
通过观察序列化后的字符串,我们可以看出,每个节点的左右孩子都是在当前读取完的位置之后,如:
- 1的左右孩子是2、3,这时index走到3
- 2的左右孩子是4、null,这时index走到5
- 3的左右孩子是5、6,这时index走到7(出界)
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
- 然后交换第二个位置b和b,还是abc
- 先交换a和b = bac
- 然后交换第二个位置a和a = bac
- 最后交换第三个位置c和c --> bac ...
- 然后交换第二个位置a和a = bac
每层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;
}
}