文章目录
- 点击下方目录,可跳转查看:
- 《剑指Offer》刷题笔记(67题全)
-
- 01. 二维数组中的查找 【中值出发无分支】
- 02. 替换空格 【从后往前扩充】
- 03. 从尾到头打印链表值【无需反转链表,直接倒叙输出val】
- 04. 重建二叉树 【先序pre[0]做根切割中序】
- 05. 用两个栈实现队列 【等栈2排空,栈1全到2】
- 06. 旋转数组的最小数字 【二分探测- 3种情况,相等也要动】
- 07. 斐波那契数列
- 08. 跳台阶
- 09. 跳台阶扩展问题
- 10. 矩形覆盖
- 11. 二进制中1的个数【0x01按位与】
- 12. 数值的整数次方
- 13. 调整数组顺序使奇数位于偶数前面 【保证顺序:新开空间;只区分奇偶:双指针】
- 14. 链表中倒数最后k个结点 【双指针,一个先走k步】
- 15. 反转链表 【三指针法】
- 16. 合并两个排序的链表 【虚拟头结点】
- 17. 树的子结构 【大树每一个节点node,和子树的root进行匹配; 注意:N*if + return(|| &&)】
- 18. 二叉树的镜像 【借temp交换】
- 19. 顺时针打印矩阵 【上下左右四界】
- 20. 包含min函数的栈 【增加辅助栈记录min序列】
- 21. 栈的压入、弹出序列
- 22. 从上往下打印二叉树
- 23. 二叉搜索树的后序遍历序列 【区间最后一个节点,划分左右子树】
- 24. 二叉树中和为某一值的所有路径 【设置全局变量path、sum,且在离开节点时回退;保存path时要new】
- 25. 复杂链表的复制
- 26. 二叉搜索树与双向链表
- 27. 字符串的排列 【递归交换,每层 遍历String和最后一个交换;swap-exchange-swap】
- 28. 数组中出现次数超过一半的数字 【设置候选者candidate+抵消计数power;若不一定有 则需验证】
- 29. 最小的K个数(Top-k 问题)【快排划分:平均O(N)最坏O(N^2);堆排O(NlogK);数组O(N*K)】
- 30. 连续子数组的最大和 【线性扫描,小于零截断】
- 31. 整数中1出现的次数
- 32. 把数组排成最小的数 【字典序,用快排】
- 33. 丑数 【DP:设置i2/i3/i5三个index候选,向后探索,取探索值中最小的为新丑数,然后1~3个index候选++】
- 34. 第一个只出现一次的字符 【数组法:利用ASCII码建立char-->count;Map法】
- 35. 数组中的逆序对 【排序-交换时记录逆序数;O(NlogN) + 稳定性 =>归并排序】
- 36. 两个链表的第一个公共结点
- 37. 数字在升序数组中出现的次数 【两次logN搜索,得左右边界】
- 38. 二叉树的深度 【后序遍历、递归返回深度】
- 39. 平衡二叉树 【递归返回深度,并计算diff判断】
- 40. 数组中只出现一次的两个数字
- 41. 和为S的连续正数序列 【双指针,从一端开始;等于大于小于3种情况:等于也要移动】
- 42. 和为S的两个数字 【双指针,两端开始】
- 43. 左旋转字符串
- 44. 翻转单词序列 【两轮翻转法,空间:Java是O(N);C++是O(1)】
- 45. 扑克牌顺子
- 46. 孩子们的游戏(圆圈中最后剩下的数)
- 47. 求1+2+3+...+n
- 48. 不用加减乘除做加法
- 49. 把字符串转换成整数
- 50. 数组中重复的数字 【归位排序法:【for+while双重循环】时间空间O(N)O(1);辅助数组 / HashSet法:需额外空间】
- 51. 构建乘积数组
- 52. 正则表达式匹配 【递归】
- 53. 表示数值的字符串
- 54. 字符流中第一个不重复的字符【ASCII数组-记录次数 + LinkedList队列-记录顺序】
- 55. 链表中环的入口结点【双指针法(快慢->同速)】
- 56. 删除链表中重复的结点【虚拟头结点、跳过重复节点、最后指向null收尾】
- 57. 二叉树的下一个节点
- 58. 对称的二叉树【递归参数 需要传入两个相互比较的子树;return sym(L.L, R.R)&&sym(L.R, R.L)】
- 59. 按之字形顺序打印二叉树【层次遍历升级版-加栈倒转】
- 60. 把二叉树打印成多行(朴素层次遍历)
- 61. 序列化二叉树(序列化+反序列化)【用#代替null】
- 62. 二叉搜索树的第k个结点
- 63. 数据流中的中位数【大根堆+小根堆,PriorityQueue实现】
- 64. 滑动窗口的最大值
- 65. 矩阵中的路径【二维dfs回溯+flag试错】
- 66. 机器人的运动范围【二维dfs回溯+flag试错】
- 67. 剪绳子【1) 函数求导法 2) 递推型dp】
点击下方目录,可跳转查看:
本文包括刷题的总体思想要义、以及剑指offer67题的全部题解。《剑指Offer》刷题笔记(67题全)
01. 二维数组中的查找 【中值出发无分支】
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
【思路】
左上最小、右下最大。右上or左下出发,快速切边查找;每一步都只有一个选择=》不产生分支
public class Solution {
public boolean Find(int target, int [][] array) {
if(array.length==0 || array[0].length==0)return false;
//尽量都写成左闭右闭区间的风格,在一开始就减一,上下界都是能够达到的值
int row = array.length -1;
int col = array[0].length -1;
//这里从右上开始,左下也可以。 //(不能从左上开始,不然不知道移动的方向。更不能从任意位置开始)
int i = row;
int j =0;
while(i>=0 && j<=col){
//范围用>=和<=,这样配合左闭右闭区间
if(array[i][j]>target)--i;//【每次判断都能剔除一整行或一整列】
else if(array[i][j]<target)++j;//这里的else if 不能用else,因为上面的语句可能会影响array[i][j]的值(改变了i的值)
else return true;//将==放在最后,因为这个情况的概率最小,这样效率更高
}
return false;
}
}
//时间复杂度:O(col+row)
//空间复杂度:O(1)
02. 替换空格 【从后往前扩充】
1) 原版(给StringBuffer):
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
【思路】
如果给StringBuffer就直接在上面改;从后往前,进行扩张。
如果给String则需要新开辟空间,然后从前往后即可。
return str.toString().replace(" ","%20"); //一句搞定
public class Solution {
public String replaceSpace(StringBuffer str) {
//str的类型是StringBuffer,最后要转换成String//一共两轮,第一轮是扫描得到space个数
int space=0;
int L1=str.length();//str需要length();数组一般用length
for (int i=0;i<L1;i++)
{
if (str.charAt(i)==' ')space++; //【str.charAt(i)】
}
int L2=L1+2*space;
str.setLength(L2); //【str.setLength(L2)】一定要修改(加长)str的长度
L1--;L2--; //一定要一次性减1,来对齐数组下标
while (L1>=0&&L2>L1){
if (str.charAt(L1)!=' '){
str.setCharAt(L2--,str.charAt(L1)); //【str.setCharAt(下标,值)】
}
else{
str.setCharAt(L2--,'0');
str.setCharAt(L2--,'2');
str.setCharAt(L2--,'%');
}
L1--;
}
return str.toString(); //【str.toString()】
}
}
//时间复杂度:O(N)
//空间复杂度:O(space) =>直接在原来的StringBuffer上面改
2) 新版(给String):
import java.util.*;
public class Solution {
public String replaceSpace (String s) {
StringBuilder res = new StringBuilder();
int len =s.length()-1;
for(int i=0;i<=len;++i){
if(s.charAt(i)!=' '){
//单引号代表char, 双引号代表String
res.append(s.charAt(i)); //String可用charAt()
}
else res.append("%20");//StringBuilder的append可以是几乎所有类型
}
return res.toString();
}
}
//时间:O(N)
//空间:O(N)
03. 从尾到头打印链表值【无需反转链表,直接倒叙输出val】
输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
【思路】
1)递归方法(系统栈):类似于树只有单分支,持续向下到末端后,后续遍历往上
2)用栈反转:注意,从尾到头打印值,可能只是反转值,而没有反转链表。
3)头插法直接存到ArrayList res,这个效率最低,所以说ArrayList尽量不要用头插法。
1)递归方法(系统栈)
import java.util.ArrayList;
public class Solution {
ArrayList<Integer> res = new ArrayList<Integer>(); //一定要在函数之前定义
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
if(listNode!=null){
printListFromTailToHead(listNode.next); //没有用到printListFromTailToHead的返回值
res.add(listNode.val); //这个在递归后面,则可以做到倒序;如果在递归前就是正序
}
return res;
}
}//空间O(N) 时间O(N)
2)用栈反转
import java.util.ArrayList;
import java.util.Stack;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> res = new ArrayList<Integer>();
Stack<Integer> stack = new Stack<Integer>(); //posh + pop 搞定
while(listNode != null){
stack.push(listNode.val);
listNode = listNode.next;
}
while(!stack.isEmpty()){
res.add(stack.pop());//这里只是反转了val, 如果要反转链表可以新建Node
}
return res;
}
}//空间O(N) 时间O(N)
3)ArrayList中用 - 头插法
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer>mylist=new ArrayList<>();//含有<>和();别忘了new
while(listNode!=null){
//直接用null对应listNode就行
mylist.add(0,listNode.val);//list.add(0,value)在list的头部插入值,单次耗时O(N)
listNode=listNode.next;//Java这样就不用到->指针了,只会用到STL里面定义过的操作
}
return mylist;
}
}//空间O(N) 时间O(N^2)
//时间效率最低,所以说ArrayList尽量不要用头插法
04. 重建二叉树 【先序pre[0]做根切割中序】
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
【思路】
前序+中序=》重建二叉树
先序pre[0]做根切割中序
/**
* Definition for binary tree
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
import java.util.Arrays;//Arrays.copyOfRange(,,); //针对pre[]和in[]数组,功能:选择范围复制==>左闭右开区间
public class Solution {
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
//返回的是根节点
if(pre.length==0||in.length==0)return null;//【递归的终结】 //也可以简化为 pre.length==0 (只判断一个即可) //不能写成 pre==null,因为pre==[]时,数组不是null但长度为零
TreeNode node=new TreeNode(pre[0]);//先序的第一个pre[0]永远是根节点,也是分左右子树的关键
for(int i=0;i<pre.length;i++){
//pre和in的子数组永远是对应相同长度的
if(pre[0]==in[i]){
//每一次for循环,只有一次会执行if里面的语句
node.left=reConstructBinaryTree(Arrays.copyOfRange(pre,1,i+1),Arrays.copyOfRange(in,0,i));
node.right=reConstructBinaryTree(Arrays.copyOfRange(pre,i+1,pre.length),Arrays.copyOfRange(in,i+1,in.length));
}//在建设node.val后,再递归调用获取node.left和node.right,这样3步后,一个node就完整建立起来了
}
return node;
}
}
//复杂度方面:最坏情况下(树是一条直线)每一层递归从O(n)直到O(1),因为每一层都会至少减少1个复制的。
//最坏情况下,N层递归,此时时间空间复杂度都是O(N^2)
//平均情况下,log(N)层递归,此时时间空间复杂度都是O(NlogN)
05. 用两个栈实现队列 【等栈2排空,栈1全到2】
用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
【思路】
如果不控制好栈1栈2的数据流动,可能会造成数据顺序错乱
只有栈2排空了,才会由1到2,必须一次性全部
import java.util.Stack;
public class Solution {
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
public void push(int node) {
stack1.push(node);
}
public int pop() {
if(stack2.isEmpty()){
//【只有stack2排空了,才会由1到2,且必须一次性全部】
while(!stack1.isEmpty())stack2.push(stack1.pop());
}
return stack2.pop();//(无论上面的if语句结果怎样,这里都会pop出一个)
}
}
06. 旋转数组的最小数字 【二分探测- 3种情况,相等也要动】
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
1)O(n) 暴力扫描
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] array) {
if (array.length==0)return 0;
int min=array[0];
for (int i=0;i<array.length;i++){
if (array[i]<min)min=array[i];
}
return min;
}
}
//暴力查找,时间O(N)
2)二分法
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] array) {
int len=array.length;
if(len==0)return 0;
int left=0;int right=len-1;//自己写区间的时候,尽量用“左闭右闭”区间,不然容易出错。(不要用左闭右开区间!!)
while(left<right){
if(array[left]<array[right]){
//严格小于//说明区间里面没有“断层”,单调增/单调不减
return array[left];
}
int mid=(left+right)/2;//左右区间内有“断层”的时候,需要探测mid位置的值
//3种情况:大于小于等于(最好画个图)//最好是mid和right来比较;选mid和left来比较时,还需要再次分类判断(因为只有2个数时,mid和left重合)
if(array[mid]>array[right])left=mid+1;
else if(array[mid]<array[right])right=mid;
else if(array[mid]==array[right])--right;//这种情况容易考虑不到,导致区间无法收敛
}
return array[right]; //此时只有一个元素,所以left==right
}
}
//二分查找,平均时间O(logN)
//最坏情况(全部相等)时间复杂度O(N)
07. 斐波那契数列
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。n≤39
public class Solution {
public int Fibonacci(int n) {
int[] fi=new int[40];//设置数组记录中间结果,不然重复计算太多 //根据题目,放心设置数组大小
fi[0]=0;fi[1]=1;
for(int i=2;i<=n;i++){
fi[i]=fi[i-1]+fi[i-2];
}
return fi[n];
}
}
//动态规划,时间复杂度O(N),空间复杂度O(N)
//如果用递归,时间复杂度O(1.618^N)【上网查的,略小于2^N】,空间复杂度O(1)【不包括系统栈空间】
08. 跳台阶
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
1)斐波拉切-O(N)动态规划
public class Solution {
public int JumpFloor(int target) {
int frog[]=new int[100];
frog[1]=1;frog[2]=2;
for (int i=3;i<=target;i++){
frog[i]=frog[i-1]+frog[i-2];
}
return frog[target];
}
}
//原理同:斐波那契数列
//【动态规划】时间O(N),空间O(N)
//如果只要最后的结果,那么可以撤销数组,使用a/b/c三个变量存储即可。空间复杂度减为O(1)
2)空间O(1)的方法
public class Solution {
public int jumpFloor(int target) {
if(target<=2)return target;
int lastOne = 2; //现在位置上一个,相当于fi[i-1]
int lastTwo = 1; //相当于fi[i-2]
int res = 0;
for(int i=3; i<=target; ++i){
res = lastOne + lastTwo;
lastTwo = lastOne;
lastOne = res;
}
return res;
}
}
//这种方法的空间复杂度为:O(1)
//时间复杂度虽然也为O(N),但是比上一种动态规划的方法耗时,因为循环里面操作较多
//相当于时间换空间,花费时间在不断倒腾地方
09. 跳台阶扩展问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
1)找出公式
public class Solution {
public int JumpFloorII(int target) {
int way=1;for(int i=1;i<target;i++)way*=2;return way;
}
}
//【找出数学公式】2的n-1次方:类似向n个点之间的n-1个空画横线
// 其实不难找,在找递推公式时,前几项一写就知道了
// 时间 O(N)
// 空间 O(1)
2)(动态规划)硬算
public class Solution {
public int jumpFloorII(int target) {
int[] array =new int[100];
array[1] = 1;
for(int i=2; i<=target; ++i){
int sum = 0;
for(int j=1; j<=i-1; ++j)sum+=array[j];
array[i] = sum +1; //之前所有路径,再加上直接全部的1个跳法
}
return array[target];
}
}
//时间 O(N^2)
//空间 O(N)
10. 矩形覆盖
我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2n的大矩形,总共有多少种方法?
比如n=3时,23的矩形块有3种覆盖方法:

public class Solution {
public int rectCover(int target) {
int fi[] = new int[100];
for(int i= 0; i<=2; ++i)fi[i]=i;
for(int i=3; i<=target; ++i)fi[i]=fi[i-1]+fi[i-2];
return fi[target];
}
}
//(除了初始少许不一样,后面是斐波拉切)
// 找递推关系:分解情况==》最右边只可能为竖或横两种情况,这两种情况无交集,分别占用1个块块和2个块块
11. 二进制中1的个数【0x01按位与】
输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
方法一:按位与
public class Solution {
public int NumberOf1(int n) {
//可以直接拿int类型,当做二进制来进行位运算 (IDEA上亲测可用)
int count =0;
int mark = 0x01;
for(int i=0;i<32;++i){
//从最低位到最高位,一个个试 //或者用while(mark !=0)也可以
if((n & mark)!=0)++count;//不能是if(bit&n!=0),少了括号后,先计算n!=0(判断优先于按位运算)
mark<<=1;//mark中唯一的1,左移一位
}
return count;
}
}
//时间复杂度O(1)==>O(32)
//空间复杂度O(1)
//C++要3ms,Java要13ms
方法二:神奇方法(由于规模限制,所以并无明显优势)
public class Solution {
public int NumberOf1(int n) {
int count=0;
while(n!=0){
n=n&(n-1);//神奇的方法:补码&反码 //&的含义是只有两个都是1,结果才是1
count++;//跳过了补码中没有1的位,每一轮循环弹无虚发,都找到一个1
}
return count;
}
}
//时间复杂度O(1)==>O(16),输入n的补码中1的个数,平均为O(16)
//空间复杂度O(1)
//C++要3ms,Java要13ms //和上面一样时间,白优化了hhh
12. 数值的整数次方
给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
保证base和exponent不同时为0
public class Solution {
public double Power(double base, int exponent) {
if(exponent == 0)return 1;
double res = 1.0;
if(exponent <0){
base = 1.0/base;
exponent *=(-1);
}
for(int i=1; i<=exponent; ++i)res*=base;
return res;
}
}
//时间 O(exponent)
//空间 O(1)
13. 调整数组顺序使奇数位于偶数前面 【保证顺序:新开空间;只区分奇偶:双指针】
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
【思路】
1)若是保证顺序:需要新开辟空间
2)若是只需要区分奇偶数,就直接在原数组上用双指针 (类似快排:一个条件划分两块)
import java.util.*;
public class Solution {
public int[] reOrderArray (int[] array) {
int len = array.length;
//1个数组 + 1个下标:
int [] res = new int[len];
int k =0;
for(int i=0; i<=len-1; ++i){
if(array[i]%2 == 1)
res[k++] = array[i];
}
for(int i=0; i<=len-1; ++i){
if(array[i]%2 ==0)
res[k++] = array[i];
}
return res;
}
}
//在保证奇数偶数内部相对顺序的情况下,这种方法就是最优了,时间、空间都是O(n)
//书上的题目要求仅仅是将奇偶数分开,那么用类似快排的two pointer就行,时间O(n),空间是O(1)
14. 链表中倒数最后k个结点 【双指针,一个先走k步】
输入一个链表,输出一个链表,该输出链表包含原链表中从倒数第k个结点至尾节点的全部节点。
如果该链表长度小于k,请返回一个长度为 0 的链表。
1)标准的 双指针法
public class Solution {
public ListNode FindKthToTail (ListNode pHead, int k) {
ListNode pre = pHead;//head是第一个节点
ListNode res = pHead;
while(--k >= 0){
if(pre== null)return null;//倒数第k的k值大于链表总长度
pre=pre.next;
}
while(pre!=null){
pre=pre.next;
res=res.next;
}
return res;
}
}
//时间 O(N)
//空间 O(1)
2)朴素方法(可读性好,且性能相当)
public class Solution {
public ListNode FindKthToTail (ListNode pHead, int k) {
int len =0;
ListNode p = pHead;
while(p != null){
p = p.next;
++len;
}
if(len<k)return null;
p = pHead;
for(int i=1; i<=len-k; ++i){
p=p.next;
}
return p;
}
}
//【个人认为】前后指针的方法,其实和朴素的方法没有实质的性能差别。==>感觉只能炫技而已,华而不实
//时间上:两种方法的遍历都是 一个指针完整遍历N跳,另一个/另一次遍历N-k跳;
//空间上:朴素方法只要一个节点指针,前后节点法占用两个节点指针;
//总的来说,就是【一个指针遍历两次,和两个指针遍历一次】这样子,性能上,并没有两倍的时间差别
15. 反转链表 【三指针法】
输入一个链表,反转链表后,输出新链表的表头。
public class Solution {
public ListNode ReverseList(ListNode head) {
//【设置3个指针三连排:p1/p2/p3】//两个指针可以是pre/cur,但多了用1/2/3比较好(清晰)
//反转时候最怕的就是单链断掉,所以这里用几个节点指针来缓存“断链操作”处的信息
ListNode p1=null;//第一次使head指向null
ListNode p2=head;//【p2是head】
ListNode p3=null;
while(p2!=null){
//选p2是因为p2第一次作为head、正好去遍历全部单链节点
p3=p2.next;//p3的作用是在p2.next被翻转前,标记原始的p2.next
p2.next=p1;//反转操作
p1=p2;
p2=p3;
}
return p1;//最后p1为翻转后的head
}
}
//时间复杂度O(N),空间复杂度O(1)
16. 合并两个排序的链表 【虚拟头结点】
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
1)朴素方法
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
//没有虚拟头结点的坏处:要额外拎出来头部的过程,而不是统一
if(list1 == null)return list2;
if(list2 == null)return list1;
ListNode p0 = null;//标记头部,不能动
if(list1.val < list2.val){
p0=list1;
list1=list1.next;
}
else{
p0=list2;
list2=list2.next;
}
ListNode p=p0;
while(list1 != null && list2 != null){
//正式主体过程
if(list1.val < list2.val){
p.next=list1;
p=p.next;//【千万别漏了这个】
list1=list1.next;
}
else{
p.next=list2;
p=p.next;
list2=list2.next;//直接修改题目变量,可节约空间
}
}
if(list1==null)p.next=list2;
if(list2==null)p.next=list1;
return p0;
}
}
// 时间 O(N), 空间 O(1)
2)虚拟头结点:统一过程&精简代码
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
//建立虚拟头结点,可以统一过程&精简代码,同时减少或不用考虑头部的情况
//此代码和上面相比,只有函数里的第一行和最后一行是修改的
ListNode vHead = new ListNode(Integer.MIN_VALUE);//建立虚拟头结点 //建立的时候不能用null,必须实例化
ListNode p=vHead;
while(list1 != null && list2 != null){
if(list1.val < list2.val){
p.next=list1;
p=p.next;
list1=list1.next;
}
else{
p.next=list2;
p=p.next;
list2=list2.next;
}
}
if(list1==null)p.next=list2;
if(list2==null)p.next=list1;
return vHead.next;//虚拟头结点一直在那不动,返回它的下一个即为第一个真实节点
}
}
17. 树的子结构 【大树每一个节点node,和子树的root进行匹配; 注意:N*if + return(|| &&)】
输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
ps:大的思路不算难,但这里面细节比较多【见下方代码及注释】
public class Solution {
//分两步:
//[1]遍历root1树,尝试每一个节点
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
if(root1==null || root2==null)return false;//由题,root1或root2初试为null都会导致false
if(judge(root1,root2)==true)return true;//必须要有if判断 ==>只有true才返回、并结束题目任务;false时不能返回,并进行下方的详细判别
return HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2);//这里的关系是"或"
} //表示整个树的所有分支只要有一个末端分支满足即可。
//[2]针对某一节点node,判断是否与root2匹配
public boolean judge(TreeNode node, TreeNode root2){
if(root2==null)return true;//在前,因为有:node和root2都为null的情况,root2为空node不为空的情况(本题允许在匹配时,子树比原树下方短)
if(node==null)return false;//在后,相当与node==null&&root2!=null
if(node.val != root2.val)return false;//不相等直接结束,否则继续向下详细检查
return judge(node.left,root2.left) && judge(node.right,root2.right);//"与"的关系,表示子树所有分支全部都要满足。
}
}
//judge()函数复杂度为O(root2) //root2是B树(子树)
//HasSubtree()由于每个root1树的节点都要试一下,调用次数O(root1)
//==>时间复杂度 O(root1*root2)
leetcode变种版本:
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
例如:
给定的树 A:
3
/
4 5
/
1 2
给定的树 B:
4
/
1
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
if(B==null)return false;
return tryTree(A,B);
}
public boolean tryTree(TreeNode A, TreeNode B){
if(A!=null){
if(judge(A,B)==true)return true;
if(tryTree(A.left, B)==true || tryTree(A.right, B)==true)return true;//这里没有那么简洁,但是可读性好。
}
return false;
}
public boolean judge(TreeNode A, TreeNode B){
if(B!=null){
if(A==null || B.val != A.val)return false;
return (judge(A.left,B.left)==true)&&(judge(A.right,B.right)==true);
}
return true;
}
}
18. 二叉树的镜像 【借temp交换】
操作给定的二叉树,将其变换为源二叉树的镜像。
public class Solution {
//总体思想:递归遍历+交换
public TreeNode Mirror (TreeNode pRoot) {
if(pRoot!=null){
//递归终止条件:null时不再向下
TreeNode temp = pRoot.left;
pRoot.left = pRoot.right;
pRoot.right = temp;//经典的三步交换:当前节点 交换 左右子树
Mirror(pRoot.left);
Mirror(pRoot.right);//向下传递左右子树根节点,向上无传递
}
return pRoot;
}
}
//时间O(N)
//空间不是O(1)而是O(logN)==>先中后序dfs都占用logN层系统栈空间
19. 顺时针打印矩阵 【上下左右四界】
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> printMatrix(int [][] matrix) {
ArrayList<Integer> res = new ArrayList<Integer>();
//定义上下左右的边界,共需要4个变量
int up = 0;
int down = matrix.length-1;//这里的4个边界全部做到可以达到
int left = 0;
int right = matrix[0].length-1;
while(true){
//while中有4个类似的结构,但是不适合合并,因为合并也不会减少代码
if(up>down)break;//函数开头不用另外验matrix,因为while里面的判断 已经做得很完善了
for(int i = left;