剑指offer(27-38)
前言
【解决面试题的思路】
- 画图让抽象问题形象化
- 举例让抽象问题具体化
- 分解让复杂问题简单化
27. 二叉树的镜像
【题目】
操作给定的二叉树,将其变换为源二叉树的镜像。
二叉树的镜像定义:源二叉树
8
/ \
6 10
/ \ / \
5 7 9 11
镜像二叉树
8
/ \
10 6
/ \ / \
11 9 7 5
【思路】
二叉树就是要用递归!
用递归一定要想清楚递归出口!
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public void Mirror(TreeNode root) {
//满足结束条件
if(root == null){
return;
if(root.left == null && root.right == null){
return;
}
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
if(root.left != null){
Mirror(root.left);
}
if(root.right != null){
Mirror(root.right);
}
}
}
28 对称的二叉树
【题目】
请实现一个函数,用来判断一棵二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。
【思路】
对称前序遍历
找到递归出口(true或者false)和满足继续递归的条件
/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
boolean isSymmetrical(TreeNode pRoot)
{
return isSymmetrical(pRoot, pRoot);
}
private boolean isSymmetrical(TreeNode root1, TreeNode root2){
// 递归出口
if(root1 == null && root2 == null){
return true;
}
if(root1 == null || root2 == null){
return false;
}
if(root1.val != root2.val){
return false;
}
return (isSymmetrical(root1.left, root2.right) && isSymmetrical(root1.right, root2.left));
}
}
29. 顺时针打印矩阵
【题目】
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字
下图的矩阵顺时针打印结果为:1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10
【思路】
宏观调度(找全局点)
全局点:左上,右下 或者 右上,左下
分圈递归打印,收缩全局点,注意一行一列的情况
import java.util.ArrayList;
public class Solution {
private ArrayList<Integer> arr = new ArrayList<>();
public ArrayList<Integer> printMatrix(int [][] matrix) {
if(matrix == null || matrix.length == 0 || matrix[0].length == 0){
return null;
}
int rows = matrix.length;
int cols = matrix[0].length;
printInCircle(matrix, 0, 0, rows - 1, cols - 1);
return arr;
}
private void printInCircle(int[][] matrix, int row1, int col1, int row2, int col2){
//递归出口
if(row1 > row2 || col1 > col2){
return;
}
if(row1 == row2){
for(int i = col1; i <= col2; i++){
arr.add(matrix[row1][i]);
}
}else if(col1 == col2){
for(int i = row1; i <= row2; i++){
arr.add(matrix[i][col1]);
}
}else{
for(int i = col1; i <= col2; i++){ // 【多次错误处】:未考虑一行一列的情况
arr.add(matrix[row1][i]);
}
for(int i = row1 + 1; i <= row2; i++){
arr.add(matrix[i][col2]);
}
for(int i = col2 - 1; i >= col1; i--){
arr.add(matrix[row2][i]);
}
for(int i = row2 - 1; i > row1; i--){
arr.add(matrix[i][col1]);
}
printInCircle(matrix, row1 + 1, col1 + 1, row2 - 1, col2 - 1);
}
}
}
30. 包含 min 函数的栈
【题目】
定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。
注意:保证测试中不会当栈为空的时候,对栈调用pop()或者min()或者top()方法。
【思路】
两个栈,用辅助栈存储当前最小值,压入时更新,弹出时跟着弹出
改进:
- push时小于等于辅助栈顶,才压入辅助栈
- pop时等于辅助栈顶,才弹出辅助栈顶
//基本
import java.util.Stack;
public class Solution {
private Stack<Integer> stack = new Stack<>();
private Stack<Integer> min = new Stack<>();
public void push(int node) {
if(stack.isEmpty()){
min.push(node);
}else{
min.push(min.peek() > node ? node : min.peek());
}
stack.push(node);
}
public void pop() {
if(stack.isEmpty()){
throw new RuntimeException();
}
stack.pop();
min.pop();
}
public int top() {
if(stack.isEmpty()){
throw new RuntimeException();
}
return stack.peek();
}
public int min() {
if(min.isEmpty()){
throw new RuntimeException();
}
return min.peek();
}
}
//改进
import java.util.Stack;
public class Solution {
private Stack<Integer> stack = new Stack<>();
private Stack<Integer> min = new Stack<>();
public void push(int node) {
if(stack.isEmpty()){
min.push(node);
}else{
if(node <= min.peek()){
min.push(node);
}
}
stack.push(node);
}
public void pop() {
if(stack.isEmpty()){
throw new RuntimeException();
}
if(stack.peek() == min.peek()){
min.pop();
}
stack.pop();
}
public int top() {
if(stack.isEmpty()){
throw new RuntimeException();
}
return stack.peek();
}
public int min() {
if(min.isEmpty()){
throw new RuntimeException();
}
return min.peek();
}
}
31. 栈的压入、弹出序列
【题目】
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)
【思路】
模拟进栈出栈过程,如果弹出序列正确,则最后栈一定为空
import java.util.ArrayList;
import java.util.Stack;
public class Solution {
public boolean IsPopOrder(int [] pushA,int [] popA) {
if(pushA == null || popA == null){
return false;
}
Stack<Integer> stack = new Stack<>();
int popIndex = 0;
for(int i = 0; i < pushA.length; i++){
stack.push(pushA[i]);
while(stack.peek() == popA[popIndex]){
stack.pop();
popIndex++;
if(popIndex == popA.length || stack.isEmpty()){
break;
}
}
}
return stack.isEmpty();
}
}
32.1 从上往下打印二叉树
【题目】
从上往下打印出二叉树的每个节点,同层节点从左至右打印。
例如,以下二叉树层次遍历的结果为:1,2,3,4,5,6,7
【思路】
先进先出,用队列存储
- 先将头节点放入队列
- 如果队列不为空则出队打印该节点,并将其孩子节点入队
- 迭代 1、2步骤,直到队列为空
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
public class Solution {
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
ArrayList<Integer> arr = new ArrayList<>();
queue.add(root);
TreeNode cur = null;
while(!queue.isEmpty()){
cur = queue.poll();
if(cur == null){
continue; // 【错误处】:没有考虑到为 null 的情况
}
arr.add(cur.val);
queue.add(cur.left);
queue.add(cur.right);
}
return arr;
}
}
32.2 把二叉树打印成多行
【题目】
从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
【思路】
与上题一样
import java.util.*;
/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
queue.add(pRoot);
TreeNode cur = null;
while(!queue.isEmpty()){
int size = queue.size();
ArrayList<Integer> arr = new ArrayList<>(); // 【错误处】:arr每一次要初始化清空,否则会重复打印
while(size-- > 0){
cur = queue.poll();
if(cur == null){
continue;
}
arr.add(cur.val);
queue.add(cur.left);
queue.add(cur.right);
}
if(!arr.isEmpty()){
res.add(arr);
}
}
return res;
}
}
32.3 按之字形顺序打印二叉树
【题目】
请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
【思路】
双端队列
相当与两个连在一起的栈,需要用指针分别记录各自的栈底;
除了头节点外,从哪里进就从哪里出,下一层则反着进
import java.util.ArrayList;
import java.util.*;
/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer>> result = new ArrayList<>();
if(pRoot == null){
return result;
}
Deque<TreeNode> deque = new LinkedList<>();
ArrayList<Integer> newlist = new ArrayList<>();
boolean lr = true;
deque.offerLast(pRoot);
TreeNode last = pRoot;
TreeNode nextLast = null;
TreeNode cur = null;
while(!deque.isEmpty()){
//ArrayList<Integer> newlist = new ArrayList<>(); //【错误处1】:应该每一层都有一个ArrayList,切不可清空
if(lr){
cur = deque.pollLast(); // 当前层从哪进从哪出
newlist.add(cur.val);
if(cur.left != null){ // 下一层反则进
nextLast = nextLast == null ? cur.left : nextLast; //相当于栈底相连的两个栈,所以压入的第一个就是最终点
deque.offerFirst(cur.left);
}
if(cur.right != null){
nextLast = nextLast == null ? cur.right : nextLast;
deque.offerFirst(cur.right);
}
}else{
cur = deque.pollFirst();
newlist.add(cur.val);
if(cur.right != null){
nextLast = nextLast == null ? cur.right : nextLast;
deque.offerLast(cur.right);
}
if(cur.left != null){
nextLast = nextLast == null ? cur.left : nextLast;
deque.offerLast(cur.left);
}
}
if(cur == last){
lr = !lr;
last = nextLast;
nextLast = null;
result.add(newlist);
newlist = new ArrayList<Integer>(); //更新初始化下一层
}
}
return result;
}
}
33. 二叉搜索树的后序遍历序列
【题目】
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。
【思路】
二叉搜索树特点:左子树小于等于根节点,右子树大于根节点;
后续遍历序列特点:序列最后一位为根节点;
步骤:
首先确定递归出口
- 找到根节点
- 遍历前面的节点,找出左右子树的边界
- 如果在右子树中发现小于根节点的值,直接返回 false
- 递归1,2,3
public class Solution {
public boolean VerifySquenceOfBST(int [] sequence) {
if (sequence == null || sequence.length == 0){
return false;
}
return VerifySquenceOfBST(sequence, 0 ,sequence.length - 1);
}
private boolean VerifySquenceOfBST(int[] sequence, int first, int last){
//递归出口:成功条件
if(first == last){
return true;
}
//找下一次递归参数,过程中可剪枝
int sFirst = first;
int sLast = first;
int bFirst = first;
int bLast = first;
int index = first;
if(sequence[index] < sequence[last]){ // 找左子树边界
sFirst = index++;
while(index < last && sequence[index] < sequence[last]){
sLast = index++;
}
}
if(sequence[index] > sequence[last]){ //找右子树边界
bFirst = index++;
while(index < last){
if(sequence[index] > sequence[last]){
bLast = index++;
}else{
return false;
}
}
}
return (VerifySquenceOfBST(sequence, sFirst, sLast) && VerifySquenceOfBST(sequence, bFirst, bLast));
}
}
34. 二叉树中和为某一值的路径
【题目】
输入一颗二叉树的根节点和一个整数,按字典序打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
【思路】
回溯剪枝
- 找递归出口
- 找待选择列表
import java.util.ArrayList;
public class Solution {
private ArrayList<ArrayList<Integer>> result = new ArrayList<>();
private ArrayList<Integer> path = new ArrayList<>();
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
backtrack(root, target);
return result;
}
public void backtrack(TreeNode root, int target){
//满足结束条件
if(root == null || root.val > target){
return;
}
//回溯
path.add(root.val);
target -= root.val;
if(root.left == null && root.right == null && target == 0){
result.add(new ArrayList<>(path));
}
backtrack(root.left, target);
backtrack(root.right, target);
path.remove(path.size() - 1);
}
}
35. 复杂链表的复制
【题目】
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
【思路】
基本:用哈希表存储当前节点和对应节点
进阶:将拷贝的节点放在当前节点下一位,对复制节点的random赋值,拆分
//基本
import java.util.*;
public class Solution {
public RandomListNode Clone(RandomListNode pHead)
{
if(pHead == null){
return null;
}
HashMap<RandomListNode, RandomListNode> map = new HashMap<>();
RandomListNode cur = pHead;
while(cur != null){
map.put(cur, new RandomListNode(cur.label));
cur = cur.next; // 【错误处】:没有更新cur
}
cur = pHead;
while(cur != null){
map.get(cur).next = map.get(cur.next);
map.get(cur).random = cur.random != null? map.get(cur.random) : null;
cur = cur.next;
}
return map.get(pHead);
}
}
//进阶
public class Solution {
public RandomListNode Clone(RandomListNode pHead)
{
if(pHead == null){
return null;
}
RandomListNode cur = pHead;
RandomListNode next = null;
while(cur != null){
next = cur.next;
cur.next = new RandomListNode(cur.label);
cur.next.next = next;;
cur = next;
}
cur = pHead;
while(cur != null){
next = cur.next.next;
cur.next.random = cur.random != null ? cur.random.next : null; // 【错误处2】没有判空
cur = next;
}
RandomListNode head = pHead.next;
cur = pHead;
while(cur != null){
next = cur.next.next;
cur.next.next = next != null ? next.next : null; // 【错误处】:没有判空
cur.next = next;
cur = next;
}
return head;
}
}
36. 二叉搜索树与双向链表
【题目】
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
【思路】
基本:用队列存储中序遍历结果,然后连接 O(N)+O(N)
进阶:递归 O(N)+O(H)
先把以 X 为头的搜索二叉树的左子树转换为有序双向链表,并且返回左子树有序双向链表的头和尾,然后 把以 X 为头的搜索二叉树的右子树转换为有序双向链表,并且返回右子树有序双向链表的头和尾, 接着通 过 X 把两部分接起来即可 。
终极:Morris遍历 O(N)+O(1)
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
//递归
public class Solution {
public class ReturnType {
public TreeNode pre;
public TreeNode last;
public ReturnType(TreeNode pre, TreeNode last){
this.pre = pre;
this.last = last;
}
}
public TreeNode Convert(TreeNode pRootOfTree) {
if(pRootOfTree == null){
return null;
}
return process(pRootOfTree).pre;
}
public ReturnType process(TreeNode root){
if(root == null){
return new ReturnType(null, null);
}
ReturnType leftList = process(root.left);
ReturnType rightList = process(root.right);
if(leftList.last != null){
leftList.last.right = root;
}
root.left = leftList.last;
root.right = rightList.pre;
if(rightList.pre != null){
rightList.pre.left = root;
}
return new ReturnType(leftList.pre != null ? leftList.pre : root, rightList.last != null ? rightList.last : root);
}
}
37. 序列化二叉树
【题目】
请实现两个函数,分别用来序列化和反序列化二叉树
二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),以 ! 表示一个结点值的结束(value!)。
二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。
例如,我们可以把一个只有根节点为1的二叉树序列化为"1,",然后通过自己的函数来解析回这个二叉树
【思路】
/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
import java.util.*;
public class Solution {
String Serialize(TreeNode root) {
if(root == null){
return "#!";
}
String res = root.val + "!";
res += Serialize(root.left);
res += Serialize(root.right);
return res;
}
TreeNode Deserialize(String str) {
if(str == null){
return null;
}
String[] values = str.split("!");
Queue<String> queue = new LinkedList<>();
for(int i = 0; i < values.length; i++){
queue.offer(values[i]);
}
return reconPreOrder(queue);
}
TreeNode reconPreOrder(Queue<String> queue){
String value = queue.poll();
if(value.equals("#")){
return null;
}
TreeNode root = new TreeNode(Integer.parseInt(value));
root.left = reconPreOrder(queue);
root.right = reconPreOrder(queue);
return root;
}
}
38. 字符串的排列
【题目】
输入一个字符串, 长度不超过9(可能有字符重复), 字符只包括大小写字母。按字典序打印出该字符串中字符的所有排列。例如输入字符串 abc , 则打印出由字符 a,b,c 所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
【思路】
回溯
从第一个位置开始,和后面的字符交换,递归下去
每一个位置用 set 存储已经使用过的 字符,防止重复
import java.util.*;
public class Solution {
ArrayList<String> res = new ArrayList<>();
public ArrayList<String> Permutation(String str){
if(str == null || "".equals(str)){
return res;
}
char[] charArr = str.toCharArray();
backtrack(charArr, 0);
Collections.sort(res); // 【注意】如果要求字典序,要排序
return res;
}
public void backtrack(char[] arr, int length){
if(length == arr.length){
StringBuilder string = new StringBuilder(); //【错误处1】:用Arrays.toString() 会将数组的 边框 [] 也变成字符
for(int i = 0; i < arr.length; i++){
string.append(arr[i]);
}
res.add(string.toString());
return ;
}
HashSet<Character> set = new HashSet<>();
for(int i = length; i < arr.length; i++){
if(!set.contains(arr[i])){
set.add(arr[i]);
swap(arr, length, i);
backtrack(arr, length + 1);
swap(arr, length, i);
}
}
}
public void swap(char[] arr, int i, int j){
char t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
n res;
}
public void backtrack(char[] arr, int length){
if(length == arr.length){
StringBuilder string = new StringBuilder(); //【错误处1】:用Arrays.toString() 会将数组的 边框 [] 也变成字符
for(int i = 0; i < arr.length; i++){
string.append(arr[i]);
}
res.add(string.toString());
return ;
}
HashSet<Character> set = new HashSet<>();
for(int i = length; i < arr.length; i++){
if(!set.contains(arr[i])){
set.add(arr[i]);
swap(arr, length, i);
backtrack(arr, length + 1);
swap(arr, length, i);
}
}
}
public void swap(char[] arr, int i, int j){
char t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}