刷题笔记(剑指offer-67题,Java版)

文章目录

  • 点击下方目录,可跳转查看:
  • 《剑指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时,2
3的矩形块有3种覆盖方法:

刷题笔记(剑指offer-67题,Java版)_第1张图片
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

你可能感兴趣的:(算法,java,数据结构)