JZ1:二维数组中的查找

https://leetcode-cn.com/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/solution/mian-shi-ti-04-er-wei-shu-zu-zhong-de-cha-zhao-zuo/
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
分析:矩阵是从左到右递增、从上到下递增的,这是一个有规律的矩阵,从特殊的地方开始找目标值。
剑指offer记录 - 图2
思路:
1.从左下角开始查找
2.从右上角开始查找
知识点:
代码实现(思路1):

  1. public class Solution {
  2. public boolean Find(int target, int [][] array) {
  3. if(array.length == 0 || array == null ){
  4. return false;
  5. }
  6. int x = 0,y = array[0].length-1;
  7. while( x < array.length && y >= 0 ){
  8. if(array[x][y] == target){
  9. return true;
  10. }else if(array[x][y] > target){
  11. y--;
  12. }else{
  13. x++;
  14. }
  15. }
  16. return false;
  17. }
  18. }

JZ2:替换空格

请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy
分析:题目就是题解
思路:
方法1:直接使用String类的方法replace()
方法2:使用StringBuffer的方法append(),一个一个将原始字符和替换后的字符加进去
知识点:
String:不可变,因为是final声明的字符数组
StringBuffer:可变,加锁,线程安全,速度慢
StringBuilder:可变,不加锁,线程不安全,速度快
(String实现字符串相加的底层原理是先把String变为StringBuffer,再往里面添加字符串)
代码实现:

  1. public class Solution {
  2. public String replaceSpace(StringBuffer str) {
  3. // return str.toString().replace(" ","%20"); //方式一
  4. StringBuffer result = new StringBuffer(); //方式二
  5. for(char ch : str.toString().toCharArray()){
  6. if(ch == ' ')
  7. result.append("%20");
  8. else
  9. result.append(ch);
  10. }
  11. return result.toString();
  12. }
  13. }

JZ3:从尾到头打印链表

输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
分析:
可以使用递归或者非递归两种方式求解
思路:
1.递归方式
2.非递归方式
知识点:
1.递归与非递归
2.ArrayList的扩容机制:
包括jdk1.8和jdk1.7的区别:
jdk1.7:
jdk1.8:
待补充……
ArrayList的好的特性:是有序的,且每个元素必须挨着插入,如果删除或者插入很可能要前后移动大量元素位置
3.ArrayList和LinkedList的区别:
ArrayList:查找快,增删慢
LinkedList:查找慢。增删快
4.链表ListNode如何实现以及使用的
待补充……
代码实现:

  1. /**
  2. * public class ListNode {
  3. * int val;
  4. * ListNode next = null;
  5. * ListNode(int val) {
  6. * this.val = val;
  7. * }
  8. * }
  9. */
  10. //非递归方式
  11. import java.util.ArrayList;
  12. public class Solution {
  13. public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
  14. ArrayList<Integer> list = new ArrayList<Integer>();
  15. ListNode temp = listNode;
  16. while(temp != null){
  17. list.add(0,temp.val);
  18. temp = temp.next;
  19. }
  20. return list;
  21. }
  22. }
  23. //递归方式
  24. import java.util.ArrayList;
  25. public class Solution {
  26. ArrayList<Integer> list = new ArrayList<Integer>();
  27. public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
  28. if(listNode != null){
  29. printListFromTailToHead(listNode.next);
  30. list.add(listNode.val);
  31. }
  32. return list;
  33. }
  34. }

此题补充力扣网站上的解法
方法一:递归法
解题思路:
利用递归: 先走至链表末端,回溯时依次将节点值加入列表 ,这样就可以实现链表值的倒序输出。
Java 算法流程:

  1. 递推阶段: 每次传入 head.next ,以 head == null(即走过链表尾部节点)为递归终止条件,此时直接返回。
  2. 回溯阶段: 层层回溯时,将当前节点值加入列表,即tmp.add(head.val)。
  3. 最终,将列表 tmp 转化为数组 res ,并返回即可。

复杂度分析:

  • 时间复杂度O(N): 遍历链表,递归 N 次。
  • 空间复杂度O(N): 系统递归需要使用 O(N) 的栈空间。

方法二:辅助栈法
解题思路:
链表特点: 只能从前至后访问每个节点。
题目要求: 倒序输出节点值。
这种 先入后出 的需求可以借助 栈 来实现。
算法流程:
入栈: 遍历链表,将各节点值 push 入栈。
出栈: 将各节点值 pop 出栈,存储于数组并返回。(PJava 新建一个数组,通过 popLast() 方法将各元素存入数组,实现倒序输出)。
复杂度分析:

  • 时间复杂度 O(N): 入栈和出栈共使用 O(N) 时间。
  • 空间复杂度 O(N): 辅助栈 stack 和数组 res 共使用 O(N) 的额外空间。

知识点:
1.LinkedList可以用来表示栈结构(Stack)
add(E e) 和 addLast(E e) :将元素添加到栈结构得最后
public E removeLast():返回最后一个元素,并且有返回值

  1. //方式1:递归法
  2. class Solution {
  3. ArrayList<Integer> list=new ArrayList<>();
  4. public int[] reversePrint(ListNode head) {
  5. recur(head);
  6. int[] res=new int[list.size()];
  7. for(int i=0;i<list.size();i++)
  8. res[i]=list.get(i);
  9. return res;
  10. }
  11. private void recur(ListNode node){
  12. if(node==null) return;
  13. recur(node.next);
  14. list.add(node.val);
  15. }
  16. }
  17. //方式2:辅助栈法
  18. class Solution {
  19. public int[] reversePrint(ListNode head) {
  20. LinkedList<Integer> stack = new LinkedList<>();
  21. while (head!=null){
  22. stack.add(head.val);
  23. head=head.next;
  24. }
  25. int[] res=new int[stack.size()];
  26. for(int i=0;i<res.length;i++){
  27. res[i]=stack.removeLast();
  28. }
  29. return res;
  30. }
  31. }
  32. //方式3:自己的解法
  33. class Solution {
  34. public int[] reversePrint(ListNode head) {
  35. ArrayList<Integer> list=new ArrayList<>();
  36. ListNode node=head;
  37. while(node!=null){
  38. list.add(0,node.val);
  39. node=node.next;
  40. }
  41. int[] res=new int[list.size()];
  42. for(int i=0;i<list.size();i++)
  43. res[i]=list.get(i);
  44. return res;
  45. }
  46. }

JZ4:重建二叉树

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
分析:
使用递归与二叉树特点来求解
image.png
思路:
前序遍历和中序遍历就能唯一重构二叉树;不断划分为左子树和右子树(前序遍历第一个为root节点,中序遍历能确定root节点的左右子树,不断递归求解)
知识点:
1.递归思想
2.二叉树的前序、中序、后序遍历和重构
代码实现:

  1. /**
  2. * Definition for binary tree
  3. * public class TreeNode {
  4. * int val;
  5. * TreeNode left;
  6. * TreeNode right;
  7. * TreeNode(int x) { val = x; }
  8. * }
  9. */
  10. public class Solution {
  11. public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
  12. TreeNode node = help(pre,in,0,pre.length-1,0,in.length-1);
  13. return node;
  14. }
  15. private TreeNode help(int[] pre,int[] in,int preleft,int preright,int inleft,int inright){
  16. if(preleft >= pre.length || inleft >= in.length || preleft > preright || inleft >inright){
  17. return null;
  18. }
  19. int rootValue = pre[preleft];
  20. TreeNode node = new TreeNode(rootValue);//根确定了,然后进行左右划分
  21. int count = inleft;
  22. while(rootValue != in[count]){
  23. count++;
  24. }
  25. count -= inleft;
  26. node.left = help(pre,in,preleft+1,preleft+count,inleft,inleft+count-1);
  27. node.right = help(pre,in,preleft+count+1,preright,inleft+count+1,inright);
  28. return node;
  29. }
  30. }

JZ5:用两个栈实现队列

用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
分析:读懂题目,利用栈和队列的特点来完成
思路:
栈是先进后出,用两个栈来实现队列先进先出(那么就同时只能有一个栈在进,一个栈在出才能实现)
知识点:
负责出栈的那个栈的元素只能是另一个栈放入的(并且是在负责出栈的那个栈为空的时候再入栈)
代码实现:

  1. import java.util.Stack;
  2. public class Solution {
  3. Stack<Integer> stack1 = new Stack<Integer>();
  4. Stack<Integer> stack2 = new Stack<Integer>();
  5. public void push(int node) {
  6. stack1.push(node);
  7. }
  8. public int pop() {
  9. if(stack2.size()<=0){
  10. while(stack1.size()!= 0){
  11. stack2.push(stack1.pop());
  12. }
  13. }
  14. return stack2.pop();
  15. }
  16. }

补充力扣网上的题解:
解题思路:

  • 栈无法实现队列功能: 栈底元素(对应队首元素)无法直接删除,需要将上方所有元素出栈。
  • 双栈可实现列表倒序: 设有含三个元素的栈 A=[1,2,3] 和空栈 B=[]。若循环执行 A 元素出栈并添加入栈 B ,直到栈 A 为空,则 A=[] , B=[3,2,1] ,即 栈 B 元素实现栈 A 元素倒序 。
  • 利用栈 B 删除队首元素: 倒序后,B 执行出栈则相当于删除了 A 的栈底元素,即对应队首元素。

image.png
函数设计:
题目只要求实现 加入队尾appendTail() 和 删除队首deleteHead() 两个函数的正常工作,因此我们可以设计栈 A 用于加入队尾操作,栈 B 用于将元素倒序,从而实现删除队首元素。
加入队尾 appendTail()函数: 将数字 val 加入栈 A 即可。
删除队首deleteHead()函数: 有以下三种情况。

  • 当栈 B 不为空: B中仍有已完成倒序的元素,因此直接返回 B 的栈顶元素。
  • 否则,当 A 为空: 即两个栈都为空,无元素,因此返回 −1 。
  • 否则: 将栈 A 元素全部转移至栈 B 中,实现元素倒序,并返回栈 B 的栈顶元素。

复杂度分析:
由于问题特殊,以下分析仅满足添加 N 个元素并删除 N 个元素,即栈初始和结束状态下都为空的情况。

  • 时间复杂度: appendTail()函数为 O(1) ;deleteHead() 函数在 N 次队首元素删除操作中总共需完成 N 个元素的倒序。
  • 空间复杂度 O(N) : 最差情况下,栈 A 和 B 共保存 N 个元素。
    1. class CQueue {
    2. LinkedList<Integer> A, B;
    3. public CQueue() {
    4. A = new LinkedList<Integer>();
    5. B = new LinkedList<Integer>();
    6. }
    7. public void appendTail(int value) {
    8. A.addLast(value);
    9. }
    10. public int deleteHead() {
    11. if(!B.isEmpty()) return B.removeLast();//当栈 B 不为空,直接返回 B 的栈顶元素。
    12. if(A.isEmpty()) return -1;//当 A 为空: 即两个栈都为空,无元素,因此返回 −1
    13. while(!A.isEmpty())//(B为空但A不为空的情况)
    14. B.addLast(A.removeLast());
    15. return B.removeLast();
    16. }
    17. }

    JZ6:旋转数组的最小数字

    把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
    分析:
    这个题要用到非递减数组,同时解题容易出现漏洞的也是非递减排序;同时用旋转数组的特性进行求解。一般做查找的题先想到可以使用二分法查找;非递减数组旋转后变成了两个非递减数组
    思路:
    1.将middle和左端点进行比较(左端点作为目标target)
    2.将middle和右端点进行比较(右端点作为目标target)
    知识点:
    1.有序列可以使用二分查找:头、尾、中间值、目标值(没有目标值把头尾端点作为目标值)
    2.首先对入参进行边界值的判断,学会抛异常
    3.从题目给的非递减数组和旋转数组的本身特性入手
    代码实现: ```java //思路1:以左端点为目标 class Solution { public int minArray(int[] numbers) {
    1. if(numbers.length == 0){
    2. throw new RuntimeException("numbers is empty");
    3. }
    4. int begin = 0;
    5. int end = numbers.length - 1;
    6. int middle = 0;
    7. while(numbers[begin]>=numbers[end]){
    8. if((end-begin)==1){
    9. return numbers[end];
    10. }
    11. middle=(begin+end)/2;
    12. if(numbers[begin]<numbers[middle]){
    13. begin=middle;
    14. }else if(numbers[begin]>numbers[middle]){
    15. end=middle;
    16. }else{
    17. if(begin<end)
    18. begin++;
    19. else
    20. break;
    21. }
    22. }
    23. return numbers[begin];
    } }

//思路2:以右端点为目标 import java.util.ArrayList; public class Solution { public int minNumberInRotateArray(int [] array) { if(array.length == 0){ throw new RuntimeException(“array is empty”); } int begin = 0; int end = array.length - 1; int middle = 0; while(array[begin]>=array[end]){ middle=(begin+end)/2; if(array[end]array[middle+1]){ return array[middle+1]; }
begin=middle+1; }else if(array[end]>array[middle]){ end=middle; }else{ end—; }
} return 0; } }

  1. <a name="aae028520a082413c037bd0379d9f95d"></a>
  2. ### JZ7:斐波那契数列
  3. 大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。<br />分析:<br />需要知道斐波那契数列的特点 f(n)=f(n-1)+f(n-2),说明只与前两项的和有关,只需关注前两项,从此处找到解题的突破口。<br />思路:<br />递归(很浪费时间)--->计划递归(仍浪费空间)--->动态规划(时间和空间上都做了优化)<br />1.递归:一个复杂问题不断分解为子问题,自顶向下分解,再自底向上返回去。需要重复分解很多次,时间复杂度高。<br />时间复杂度:O(n^2) <br />空间复杂度:O(1)<br />2.计划递归(常规):使用存储法,空间换时间的做法,把之前分解过的值都保存起来,解决重复分解浪费时间的问题,减少时间复杂度,但空间复杂度增加。<br />时间复杂度:O(n) <br />空间复杂度:O(n)<br />3.计划递归优化(常规优化):实际上我们只关心前两项,只需要存储下来前两项的值就可以计算出第三项的值,其余的值不需要存储,则可以使用两个变量来反复存储我们需要的前两项就可以,这个反复存储的过程相当于数组平行向右移动的过程,反复利用这两个变量,减少了空间复杂度,优化存储。<br />时间复杂度:O(n) <br />空间复杂度:O(1)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/8427696/1610692043336-2d7f1290-72ec-4281-b1df-d521d5f9c16d.png#height=228&id=XzhCe&margin=%5Bobject%20Object%5D&name=image.png&originHeight=228&originWidth=365&originalType=binary&ratio=1&size=9666&status=done&style=none&width=365)<br />知识点:<br />1.斐波那契数列特点:第三项的值等于前两项值的和(特点:只需关注前两项的值)<br />2.计划递归:用空间换时间<br />3.动态规划:计划递归的优化<br />4.从时间复杂度(减少计算次数)和空间复杂度(需要多少空间才给多少)来考虑优化<br />2.关于求余运算:(a+b)%p=(a%p+b%p)%p;<br />代码实现:
  4. ```java
  5. //思路1:递归
  6. public class Solution {
  7. public int Fibonacci(int n) {
  8. if(n<2){
  9. return n;
  10. }
  11. return Fibonacci(n-1)+Fibonacci(n-2);
  12. }
  13. }
  14. //思路2:计划递归
  15. public class Solution {
  16. public int Fibonacci(int n) {
  17. if(n==0){
  18. return 0;
  19. }else if(n==1){
  20. return 1;
  21. }else{
  22. int[] result=new int[n+1];
  23. result[0]=0;
  24. result[1]=1;
  25. for(int i=2;i<=n;i++){
  26. result[i] = result[i-1]+result[i-2];
  27. }
  28. return result[n];
  29. }
  30. }
  31. }
  32. //思路3:计划递归的优化
  33. public class Solution {
  34. public int Fibonacci(int n) {
  35. if(n<2){
  36. return n;
  37. }
  38. int sum=0;
  39. int two=0;
  40. int one=1;
  41. for(int i=2;i<=n;i++){
  42. sum=one+two;
  43. two=one;
  44. one=sum;
  45. }
  46. return sum;
  47. }
  48. }

image.png

JZ8:跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
分析:
状态 F(i),状态方程 F(i)=F(i-1)+F(i-2),最后跳的台阶个数是互斥的,即最后跳一级和最后跳两级
思路:
转换为斐波那契数列的求解,仅初始状态不同
知识点:
1.动态规划的运用
代码实现:

  1. class Solution {
  2. public int numWays(int n) {
  3. if(n==0){
  4. return 1;
  5. }else if(n==1){
  6. return 1;
  7. }else{
  8. int sum = 0;
  9. int two = 1;
  10. int one = 1;
  11. for(int i=2;i<=n;i++){
  12. sum=(two+one)%1000000007;
  13. two=one;
  14. one=sum;
  15. }
  16. return sum;
  17. }
  18. }
  19. }

JZ9:变态跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
分析:
使用动态规划或者排列组合解题
思路:
1.动态规划求解:确定状态为 F(i),确定状态转移方程 F(i)=2F(i-1),初始状态为 F(1)=1,返回值为 F(n)=2^(n-1)[相当于是一个幂运算或者1的移位操作]
状态转移方程分析:
F(i)=F(i-1)+F(i-2)+F(i-3)+…+1 此方程的项数不确定,不好用代码实现,则分析:
F(i-1)=F(i-2)+F(i-3)+F(i-4)+…+1,
则 F(i)=2
F(i-1) 项数是确定的,容易用代码实现
这是一个幂级运算或1的位移运算
2.排列组合求解:每个台阶跳与不跳都有两种选择,相当于是求一个排列组合。但最后一级台阶只有一种选择,那就是必须跳上去,从第1到第n-1级台阶都有2种选择,最后一级台阶只有1种选择:跳上去。
知识点:
1.动态规划状态、转移方程、初始状态、返回值的理解
代码实现:

  1. public class Solution {
  2. public int JumpFloorII(int target) {
  3. if(target==0)
  4. return 1;
  5. return (1<<(target-1))%1000000007;
  6. }
  7. }

JZ10:矩形覆盖

我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2n的大矩形,总共有多少种方法?比如n=3时,23的矩形块有3种覆盖方法。
分析:
分析状态 F(i),状态转移方程 F(i)=F(i-1)+F(i-2),初始状态 F(1)=1;F(2)=2,返回值是一个斐波那契数列的求解。
分析题目中:小矩形可以横着或者竖着,最后一步的状态转移操作可以有这两种方式,且这两种方式的最后一步是互斥不同的,没有重复。
思路:
使用动态规划解题
知识点:
转移方程:考虑通过一步的操作到当前状态的递推,只能一步操作。
代码实现:

  1. public class Solution {
  2. public int rectCover(int target) {
  3. if(target<3){
  4. return target;
  5. }else{
  6. int sum=0;
  7. int two=1;
  8. int one=2;
  9. for(int i=3;i<=target;i++){
  10. sum=(one+two)%1000000007;
  11. two=one;
  12. one=sum;
  13. }
  14. return sum;
  15. }
  16. }
  17. }

JZ11:二进制中1的个数

请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。
该题的另一种说法:(输入一个整数,输出该数32位二进制表示中1的个数。其中负数用补码表示。)
分析:
int数据类型在内存中占用了4个字节,也就是32位。int类型是有符号的,因此,32位并不会全部用来存储数据,使用最高位来存储符号,最高位是0,提示数据是正数,最高位是1,表示数据是负数,使用其他的31位来存储数据。
思路:
1.二进制,使用与运算与移位运算来解题
2.使用java中的方法 Integer.bitCount(int n);
3.巧妙运用 n&(n-1) 能够消除掉n中的倒数第一个1,来计算n里包含1的个数
知识点:
1.java中的左移右移和无符号右移
<<表示左移,不分正负数,低位补0; 
>>表示右移,如果该数为正,则高位补0,若为负数,则高位补1;
>>>表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位补0;
2.移位运算的高效性
3.原码、补码和反码
原码:是最简单的机器数表示法。用最高位表示符号位,‘1’表示负号,‘0’表示正号。其他位存放该数的二进制的绝对值。原码会出现+0和-0。
反码:正数的反码还是等于原码。负数的反码就是他的原码除符号位外,按位取反。
补码:正数的补码等于他的原码。负数的补码等于反码+1。
代码实现:

  1. //思路1
  2. public class Solution {
  3. public int NumberOf1(int n) {
  4. int count=0;
  5. while(n!=0){
  6. if((n&1)==1){
  7. count++;
  8. }
  9. n=n>>>1;
  10. }
  11. return count;
  12. }
  13. }
  14. //思路2
  15. public class Solution {
  16. public int NumberOf1(int n) {
  17. return Integer.bitCount(n);
  18. }
  19. }
  20. //思路3
  21. public class Solution {
  22. public int hammingWeight(int n) {
  23. int count=0;
  24. while(n!=0){//n!=0,说明至少有1个1,则先增加一次
  25. count++;
  26. n&=(n-1);//每与上一次则消灭一个1
  27. }
  28. return count;
  29. }
  30. }

JZ12:数值的整数次方

给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。保证base和exponent不同时为0。不得使用库函数,同时不需要考虑大数问题。
分析:
使用快速幂算法求解(快速幂是二分思想的一种,二分角度)
快速幂:快速计算底数的 n 次幂,时间复杂度 O(log2的n),与原来的O(n)有了很大的提升
思路:
将 n 次幂不断的二分,偶数则直接二分,奇数则二分后再把多余的那一项乘进结果里面
知识点:
1.b>>>=1; 比 b=b>>>1;节省内存
2.(n&1)==1 运算表示 n 换算成二进制表示最后一位为1,也表示 n 是个奇数,也表示 n 不是2的次幂
3.(n>>>1) 无符号右移可以表示向下除2取整
代码实现:

  1. class Solution {
  2. public double myPow(double x, int n) {
  3. long b=n;
  4. if(b<0){//负指数转为正指数计算
  5. b=-b;
  6. x=1/x;
  7. }
  8. double result=1;
  9. while(b!=0){
  10. if((b&1)==1){//当前幂为奇数,与运算判断奇偶
  11. result *= x;//当前幂为奇数,则需要将多余的那一项乘进结果里面
  12. }
  13. x *= x;//x被替换成x^2
  14. b>>>=1;//右移1相当于向下取整
  15. }
  16. return result;
  17. }
  18. }

JZ13:调整数组顺序使奇数位于偶数前面

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
分析:
分为需要考虑调整后数组里数的相对位置是否改变
思路:
1.不考虑相对位置:双指针法,头指针指向偶数,尾指针指向奇数,然后交换
2.考虑相对位置不变:找到了奇数,就往前插入
知识点:
1.排序算法
2.双指针算法
代码实现:

  1. //思路1:不考虑相对位置
  2. class Solution {
  3. public int[] exchange(int[] nums) {
  4. int head=0;//头指针
  5. int tail=nums.length-1;//尾指针
  6. int temp=0;
  7. while(head<tail){
  8. if((nums[head]&1)==0){//头指针指向了偶数
  9. if((nums[tail]&1)==1){//尾指针指向了奇数
  10. temp=nums[head];
  11. nums[head]=nums[tail];
  12. nums[tail]=temp;
  13. head++;
  14. tail--;
  15. continue;//交换了也直接跳过
  16. }
  17. tail--;//尾指针不是奇数直接跳过
  18. continue;
  19. }
  20. head++;//头指针不是偶数直接跳过
  21. continue;
  22. }
  23. return nums;
  24. }
  25. }
  26. //优化思路1
  27. class Solution {
  28. public int[] exchange(int[] nums) {
  29. int head=0;//头指针
  30. int tail=nums.length-1;//尾指针
  31. int temp=0;
  32. while(head<tail){
  33. while((head<tail)&&(nums[head]&1)==1) head++;
  34. while((head<tail)&&(nums[tail]&1)==0) tail--;
  35. temp=nums[head];
  36. nums[head]=nums[tail];
  37. nums[tail]=temp;
  38. head++;
  39. tail--;
  40. }
  41. return nums;
  42. }
  43. }
  44. //思路2:考虑相对位置改变
  45. public class Solution {
  46. public void reOrderArray(int [] array) {
  47. int i=0;//i表示已经将奇数放好的下一个位置,初始i=0表示一个奇数都还没有放好
  48. for(int j=0;j<array.length;j++){//j表示数组下标
  49. if((array[j]&1)==1){//奇数
  50. int temp = array[j];
  51. for(int k=j-1;k>=i;k--){//把从【i,j-1】的数都往后移动一个位置
  52. array[k+1]=array[k];//因为从前往后移动,所有倒着开始移动
  53. }
  54. array[i++]=temp;//把找到的这个奇数放到i的位置
  55. }
  56. }
  57. }
  58. }

JZ14:链表中倒数第k个节点

输入一个链表,输出该链表中倒数第k个结点。
分析:
image.png
image.png
思路:
使用同向双指针,两个指针保持固定距离 k-1 的平移,就能找到倒数第 k 个元素
知识点:
快慢双指针问题(滑动窗口)
代码实现:

  1. /*
  2. public class ListNode {
  3. int val;
  4. ListNode next = null;
  5. ListNode(int val) {
  6. this.val = val;
  7. }
  8. }*/
  9. public class Solution {//运用同向双指针,滑动窗口解题
  10. public ListNode FindKthToTail(ListNode head,int k) {
  11. if(head==null || k==0){
  12. return null;
  13. }
  14. ListNode front=head;
  15. ListNode behind=head;
  16. for(int i=1;i<k;i++){
  17. if(front.next == null)
  18. return null;//如果链表的长度还没有k长,则返回null
  19. front =front.next;
  20. }//结束循环时,front已经指向第k个元素,然后behind在第一个,两者之间差了k-1个元素
  21. while(front.next!=null){
  22. front=front.next;
  23. behind=behind.next;
  24. }//跳出这个循环时,则front到了末尾,behind到了倒数第k个
  25. return behind;//返回找到的倒数第k个元素
  26. }
  27. }
  28. //同一种答案的不同写法(中间差k个或者差k-1个元素)
  29. public class Solution {
  30. public ListNode FindKthToTail (ListNode pHead, int k) {
  31. if(k==0) return null;
  32. ListNode front=pHead;
  33. ListNode after=pHead;
  34. for(int i=0;i<k;i++){
  35. if(front==null) return null;
  36. front=front.next;
  37. }//循环跳出时指向第k+1个节点
  38. while(front!=null){
  39. front=front.next;
  40. after=after.next;
  41. }
  42. return after;
  43. }
  44. }

JZ15:反转链表

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
分析:
数组交换两个索引处元素的位置,需要三个变量,即:两个需要交换的元素和一个临时变量;
同样的,链表交换两个位置处的元素,需要3个指针,即:两个需要交换的指针和一个保存临时链表头的指针。
image.png
image.png
思路:
使用三个指针进行相邻两个位置元素的交换,然后交换一次(改变链表的 next 指向的元素),这三个指针向后平移一次;需要注意:原链表的头元素变成尾元素,则这个元素的 next 应该置空 null,原链表的尾元素要变成头元素,则这个元素的 next 应该从 null 变成原链表的倒数第2个元素;这样才完成了链表的反转操作。
知识点:
1.链表一般是指单向链表,单向链表只有 next 元素,没有 pre 元素,所以在使用链表的时候,一定要记得先把链表的头指针保存下来,再运用其它指针遍历链表,头指针如果不保存就回不到头部了。
比如:
ListNode front=head;//保存头指针
ListNode curPot=head;//用这个指针来遍历链表
2.使用链表特别需要注意当前元素的 next 什么时候会指向 null,即指针到了尾元素的时候
3.注意链表不要成了环形链表,即尾指针的 next 为 null
代码实现:

  1. /*
  2. public class ListNode {
  3. int val;
  4. ListNode next = null;
  5. ListNode(int val) {
  6. this.val = val;
  7. }
  8. }*/
  9. public class Solution {
  10. public ListNode ReverseList(ListNode head) {
  11. if(head==null){//说明链表为空,必须进行边界判断
  12. return null;
  13. }
  14. ListNode point_pre=head;
  15. ListNode point_cur=head.next;
  16. if(point_cur==null){//说明链表只有头元素
  17. return point_pre;
  18. }
  19. ListNode point_temp=point_cur.next;
  20. point_pre.next=null;//头元素要变为尾元素,则应把指向的下一个元素置空
  21. while(point_temp!=null){
  22. point_cur.next=point_pre;//反向重连
  23. //向后平移三个指针
  24. point_pre=point_cur;
  25. point_cur=point_temp;
  26. point_temp=point_temp.next;
  27. }//临时元素已经到了最后一个元素
  28. point_cur.next=point_pre;//最后一个元素的下一个应该指向前一个
  29. return point_cur;
  30. }
  31. }

JZ16:合并两个排序的链表

输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
分析:

image.png
思路:
1.创建一个新的链表,谁小则这个链表指向谁,并且把小的那个元素指针向后移,新链表同时也需要向后移
2.使用递归
知识点:
1.分离双指针的使用,遍历一个链表时,首先需要把头指针保存起来
2.递归知识点:
写递归代码,最重要的要明白递归函数的功能。可以不必关心递归函数的具体实现。
比如这个ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
函数功能:合并两个单链表,返回两个单链表头结点值小的那个节点。
如果知道了这个函数功能,那么接下来需要考虑2个问题:

  1. 1. 递归函数结束的条件是什么?
  2. 1. 递归函数一定是缩小递归区间的,那么下一步的递归区间是什么?

代码实现:

  1. class ListNode {//单向链表
  2. int val;
  3. ListNode next = null;
  4. ListNode(int val) {
  5. this.val = val;
  6. }
  7. }
  8. //思路1:双指针
  9. public class Solution {
  10. public ListNode Merge(ListNode list1,ListNode list2) {
  11. ListNode head1=list1;
  12. ListNode head2=list2;
  13. ListNode newHead=new ListNode(1);//新链表的头指针需要保存起来,哨兵
  14. ListNode temp=newHead;//移动头指针
  15. while(head2!=null&&head1!=null){
  16. if(head2.val>head1.val){//谁小则指向谁,并且后移
  17. temp.next=head1;
  18. head1=head1.next;
  19. }else{
  20. temp.next=head2;
  21. head2=head2.next;
  22. }
  23. temp=temp.next;//temp始终要指向新链表的末尾
  24. }
  25. //直接将剩余元素加到新链表的末尾
  26. if(head1!=null){
  27. temp.next=head1;
  28. }
  29. if(head2!=null){
  30. temp.next=head2;
  31. }
  32. return newHead.next;
  33. }
  34. }
  35. //思路2:递归
  36. public class Solution {
  37. public ListNode Merge(ListNode list1,ListNode list2) {
  38. if (list1 == null) {
  39. return list2;
  40. } else if (list2 == null) {
  41. return list1;
  42. }else{
  43. if(list1.val<list2.val){
  44. list1.next=Merge(list1.next, list2);
  45. return list1;
  46. }else {
  47. list2.next=Merge(list1, list2.next);
  48. return list2;
  49. }
  50. }
  51. }
  52. }

JZ17: 树的子结构

输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)。B是A的子结构, 即 A中有出现和B相同的结构和节点值。
分析:
若树 B 是树 A 的子结构,则子结构的根节点可能为树 A 的任意一个节点。因此,判断树 B是否是树 A 的子结构,需完成以下两步工作:
1.先序遍历树 A 中的每个节点 nA (对应函数 isSubStructure(A, B))
2.判断树 A 中 以 nA 为根节点的子树 是否包含树 B (对应函数 recur(A, B))image.png
recur(A, B) 函数
1.终止条件:
1.当节点 B 为空:说明树 B 已匹配完成(越过叶子节点),因此返回 true ;
2.当节点 A 为空:说明已经越过树 A 叶子节点,即匹配失败,返回 false ;
3.当节点 A 和 B 的值不同:说明匹配失败,返回 false ;
2.返回值:
1.判断 A 和 B 的左子节点是否相等,即 recur(A.left, B.left) ;
2.判断 A 和 B 的右子节点是否相等,即 recur(A.right, B.right) ;
isSubStructure(A, B) 函数:
1.特例处理: 当 树 A 为空 或 树 B 为空 时,直接返回 false ;
2.返回值: 若树 B 是树 A 的子结构,则必满足以下三种情况之一,因此用或 || 连接;
1.以 节点 A 为根节点的子树 包含树 B ,对应 recur(A, B);
2.树 B 是 树 A 左子树 的子结构,对应 isSubStructure(A.left, B);
3.树 B 是 树 A 右子树 的子结构,对应 isSubStructure(A.right, B);
以上 2. 3. 实质上是在对树 A 做先序遍历 。
思路:
1.先在A树中找到与B树根节点值相同的节点P
2.以P节点作为根节点,与B树做比较
B树有的子节点,A树必须要
B树没有的子节点,A树可以有
知识点:
递归的使用
代码实现:

  1. /**
  2. public class TreeNode {
  3. int val = 0;
  4. TreeNode left = null;
  5. TreeNode right = null;
  6. public TreeNode(int val) {
  7. this.val = val;
  8. }
  9. }
  10. */
  11. public class Solution {
  12. public boolean HasSubtree(TreeNode root1,TreeNode root2) {
  13. if(root1==null || root2==null){//待比较的两个根如果有一个为空,则不用比了
  14. return false;
  15. }
  16. if(isContain(root1,root2)){//判断是否满足是树子结构
  17. return true;
  18. }
  19. //把A树的左右子节点当做根节点继续向下查找
  20. return HasSubtree(root1.left,root2) || HasSubtree(root1.right,root2);
  21. }
  22. public boolean isContain(TreeNode node1, TreeNode node2){
  23. if(node1==null && node2!=null){
  24. return false;
  25. }
  26. if(node2==null){
  27. return true;
  28. }
  29. return node1.val==node2.val&&isContain(node1.left,node2.left)&&isContain(node1.right,node2.right);
  30. }
  31. }
  32. //优化内存占用
  33. class Solution {
  34. public boolean isSubStructure(TreeNode A, TreeNode B) {
  35. return (A!=null && B!=null) && (recur(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B));
  36. }
  37. boolean recur(TreeNode A, TreeNode B) {
  38. if(B == null)
  39. return true;
  40. if(A == null || A.val != B.val)
  41. return false;
  42. return recur(A.left, B.left) && recur(A.right, B.right);
  43. }
  44. }

JZ18:二叉树的镜像

请完成一个函数,输入一个二叉树,该函数输出它的镜像。
分析:
方法一:递归法:

  1. 终止条件: 当节点 root 为空时(即越过叶节点),则返回 null ;
  2. 递推工作:
    1. 初始化节点 tmp ,用于暂存 root 的左子节点;
    2. 开启递归 右子节点 mirrorTree(root.right),并将返回值作为 root 的 左子节点 。
    3. 开启递归 左子节点 mirrorTree(tmp),并将返回值作为 root 的 右子节点 。
  3. 返回值: 返回当前节点 root ;

Q: 为何需要暂存 root 的左子节点?
A: 在递归右子节点 “root.left = mirrorTree(root.right);” 执行完毕后, root.left 的值已经发生改变,此时递归左子节点 mirrorTree(root.left)则会出问题。
方法二:辅助栈(或队列)
利用栈(或队列)遍历树的所有节点node,并交换每个node的左/右子节点。算法流程:
1.特例处理:当root为空时,直接返回null ;
2初始化:栈(或队列),本文用栈,并加入根节点root 。
3.循环交换:当栈stack为空时跳出;
1.出栈:记为node ;
2.添加子节点:将node左和右子节点入栈;
3.交换:交换node的左/右子节点。
4.返回值:返回根节点root 。
思路:
1.递归法
根据二叉树镜像的定义,考虑递归遍历(dfs)二叉树,交换每个节点的左 / 右子节点,即可生成二叉树的镜像。
2.辅助栈
引入了栈数据结构(递归的实现原理也是栈,不过由系统自动完成),虽然这题使用辅助栈和递归的时间和空间复杂度都一样,但辅助栈占用的内存更多,使用的时间更长。
知识点:
1.关于递归,需要考虑2个问题:
1.递归函数结束的条件是什么?
2.递归函数一定是缩小递归区间的,那么下一步的递归区间是什么?
代码实现:

  1. //递归法(无返回值)
  2. class Solution {
  3. public TreeNode mirrorTree(TreeNode root) {
  4. if(root==null){//递归的终止条件
  5. return null;
  6. }
  7. TreeNode temp=root.left;
  8. root.left=root.right;
  9. root.right=temp;
  10. mirrorTree(root.left);//只有递归到 root.left==null 时,这句函数才结束
  11. mirrorTree(root.right);//只有递归到 root.right==null 时,这句函数才结束
  12. return root;//这句只会执行一次
  13. }
  14. }
  15. //递归法(有返回值)
  16. class Solution {
  17. public TreeNode mirrorTree(TreeNode root) {
  18. if(root == null)
  19. return null;
  20. TreeNode tmp = root.left;
  21. root.left = mirrorTree(root.right);
  22. root.right = mirrorTree(tmp);
  23. return root;
  24. }
  25. }
  26. //辅助栈
  27. class Solution {
  28. public TreeNode mirrorTree(TreeNode root) {
  29. if(root == null) //当 root 为空时,直接返回 null ;
  30. return null;
  31. Stack<TreeNode> stack = new Stack<>();
  32. stack.add(root);
  33. while(!stack.isEmpty()) {//当栈 stack 为空时跳出
  34. TreeNode node = stack.pop();
  35. if(node.left != null)
  36. stack.add(node.left);//左节点入栈
  37. if(node.right != null)
  38. stack.add(node.right);//右节点入栈
  39. TreeNode tmp = node.left;//交换左右节点
  40. node.left = node.right;
  41. node.right = tmp;
  42. }
  43. return root;
  44. }
  45. }

JZ19:顺时针打印矩阵

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
分析:
根据题目示例 matrix = [[1,2,3],[4,5,6],[7,8,9]] 的对应输出 [1,2,3,6,9,8,7,4,5] 可以发现,顺时针打印矩阵的顺序是 “从左向右、从上向下、从右向左、从下向上” 循环。因此,考虑设定矩阵的“左、上、右、下”四个边界,模拟以上矩阵遍历顺序。
image.png
思路:
1.空值处理: 当 matrix 为空时,直接返回空列表 [] 即可。
2.初始化: 矩阵 左、右、上、下 四个边界 l , r , t , b ,用于打印的结果列表 res 。
3。循环打印: “从左向右、从上向下、从右向左、从下向上” 四个方向循环,每个方向打印中做以下三件事 (各方向的具体信息见下表) ;
根据边界打印,即将元素按顺序添加至列表 res 尾部;
边界向内收缩 1(代表已被打印);
判断是否打印完毕(边界是否相遇),若打印完毕则跳出。
4.返回值: 返回 res 即可。
知识点:
给这个矩阵规划界限,并且不断缩小界限,打印完一行或一列就缩小一行或一列的界限;
注意判断打印完成的界限。
代码实现:

  1. import java.util.ArrayList;
  2. public class Solution {
  3. public ArrayList<Integer> printMatrix(int[][] matrix) {
  4. ArrayList<Integer> list=new ArrayList<Integer>();
  5. if(matrix.length == 0) return list;
  6. int left=0,right=matrix[0].length-1,top=0,button=matrix.length-1;
  7. while(true){
  8. for(int i=left;i<=right;i++) list.add(matrix[top][i]);
  9. if(++top>button) break;
  10. for(int i=top;i<=button;i++) list.add(matrix[i][right]);
  11. if(left>--right) break;
  12. for(int i=right;i>=left;i--) list.add(matrix[button][i]);
  13. if(top>--button) break;
  14. for(int i=button;i>=top;i--) list.add(matrix[i][left]);
  15. if(++left>right) break;
  16. }
  17. return list;
  18. }
  19. }

JZ20:包含min函数的栈

定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
分析:
普通栈的 push() 和 pop() 函数的复杂度为 O(1);而获取栈最小值 min() 函数需要遍历整个栈,复杂度为 O(N)
本题难点: 将 min() 函数复杂度降为 O(1) ,可通过建立辅助栈实现;
数据栈 A : 栈 A 用于存储所有元素,保证入栈 push() 函数、出栈 pop() 函数、获取栈顶 top() 函数的正常逻辑。
辅助栈 B : 栈 B 中存储栈 A 中所有 非严格降序 的元素,则栈 A 中的最小元素始终对应栈 B 的栈顶元素,即 min() 函数只需返回栈 B 的栈顶元素即可。
因此,只需设法维护好 栈 B 的元素,使其保持非严格降序,即可实现 min() 函数的 O(1)复杂度。
image.png
思路:
函数设计:
push(x) 函数: 重点为保持栈 B 的元素是非严格降序 的。
将 xx 压入栈 A (即 A.add(x) );
若 ① 栈 B 为空 或 ② xx 小于等于 栈 B 的栈顶元素,则将 xx 压入栈 B (即 B.add(x) )。
pop() 函数: 重点为保持栈 A, B 的 元素一致性 。
执行栈 A 出栈(即 A.pop() ),将出栈元素记为 y ;
若 y 等于栈 B 的栈顶元素,则执行栈 B 出栈(即 B.pop() )。
top() 函数: 直接返回栈 A 的栈顶元素即可,即返回 A.peek() 。
min() 函数: 直接返回栈 B 的栈顶元素即可,即返回 B.peek() 。
知识点:
1.关于栈的使用
Stack<> s1=new Stack<>();
s1.pop();//弹出栈顶元素并删除,使用一次则弹出一次
s1.peek();//查看栈顶元素不删除
s1.add(xxx);//添加一个元素到栈顶,但返回值是boolean类型,添加是否成功
s1.push(xxx);//添加一个元素到栈顶,但返回值是当前添加进去的值
代码实现:

  1. import java.util.Stack;
  2. public class Solution {
  3. Stack<Integer> stoStk;
  4. Stack<Integer> minStk;
  5. public Solution() {
  6. stoStk=new Stack<>();
  7. minStk=new Stack<>();
  8. }
  9. public void push(int node) {
  10. stoStk.add(node);
  11. if( minStk.empty() || minStk.peek()>=node)
  12. minStk.add(node);
  13. }
  14. public void pop() {
  15. if(stoStk.pop().equals(minStk.peek()))
  16. minStk.pop();
  17. }
  18. public int top() {
  19. return stoStk.peek();
  20. }
  21. public int min() {
  22. return minStk.peek();
  23. }
  24. }

JZ21: 栈的压入、弹出序列

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。
分析:
给定一个压入序列 pushed 和弹出序列 popped,则压入和 弹出操作的顺序(即排列)是唯一确定的。
考虑借用一个辅助栈 stack ,模拟 压入 / 弹出操作的排列。根据是否模拟成功,即可得到结果。
入栈操作: 按照压栈序列的顺序执行。
出栈操作: 每次入栈后,循环判断 “栈顶元素 == 弹出序列的当前元素” 是否成立,将符合弹出序列顺序的栈顶元素全部弹出。
由于题目规定 栈的所有数字均不相等 ,因此在循环入栈中,每个元素出栈的位置的可能性是唯一的(若有重复数字,则具有多个可出栈的位置)。因而,在遇到 “栈顶元素 == 弹出序列的当前元素” 就应立即执行出栈。
思路:
算法流程:
1.初始化:辅助栈stack,弹出序列的索引i;
2.遍历压栈序列:各元素记为num ;
1.元素num入栈;
⒉.循环出栈:若stack的栈顶元素=弹出序列元素popped[i],则执行出栈与i++;
3.返回值:若stack为空,则此弹出序列合法。
复杂度分析:
时间复杂度O(N):其中N为列表pushed的长度;每个元素最多入栈与出栈一次,即最多共2N次出入栈操作。
空间复杂度O(N):辅助栈stack最多同时存储N个元素。
知识点:
1.栈的先入先出的特点
2.合理运用模拟栈
代码实现:

  1. class Solution {
  2. public boolean validateStackSequences(int[] pushed, int[] popped) {
  3. Stack<Integer> stack=new Stack<Integer>();//根据是否模拟成功,即可得到结果。
  4. int i=0;
  5. for(int num:pushed){
  6. stack.add(num);
  7. while(!stack.empty()&&stack.peek()==popped[i]){
  8. stack.pop();
  9. i++;
  10. }
  11. }
  12. return stack.empty();//如果栈内元素不为空,则表示不是出栈顺序
  13. }
  14. }

JZ22-1:从上往下打印二叉树

从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。(从上到下打印二叉树 I )
分析:

  • 题目要求的二叉树的 从上至下 打印(即按层打印),又称为二叉树的 广度优先搜索(BFS)。
  • BFS 通常借助 队列 的先入先出特性来实现。

思路:
算法流程:
1.特例处理:当树的根节点为空,则直接返回空列表;
2.初始化:初始化印结果列表res,初始化包含根节点的队列queue = [root] ;
3.BFS循环:当队列queue为空时跳出;
1.出队:队首元素出队,记为node ;
2.打印:将node.val添加至列表res尾部;
3.添加子节点:若node的左(右)子节点不为空,则将左(右)子节点加入队列queue;
4.返回值:返回打印结果列表res 即可。
知识点:
1.ArrayList 想要变成数组的话,只能挨个遍历
2.队列的实现类可以使用 LinkedList : Queue queue = new LinkedList<>();
队列判断是否为空:queue.isEmpty()
3.树的话每个节点都需要 new TreeNode(xxx)
代码实现:

  1. import java.util.*;
  2. public class Solution {
  3. public int[] levelOrder(TreeNode root) {
  4. if(root == null)
  5. return new int[0];
  6. LinkedList<TreeNode> queue = new LinkedList<>();
  7. queue.add(root);
  8. ArrayList<Integer> ans = new ArrayList<>();
  9. while(!queue.isEmpty()) {
  10. TreeNode node = queue.poll();
  11. ans.add(node.val);
  12. if(node.left != null)
  13. queue.add(node.left);
  14. if(node.right != null)
  15. queue.add(node.right);
  16. }
  17. int[] res = new int[ans.size()];
  18. for(int i = 0; i < ans.size(); i++)
  19. res[i] = ans.get(i);
  20. return res;
  21. }
  22. }

JZ22-2:JZ60:把二叉树打印成多行

从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。(从上到下打印二叉树 II )
分析:
I. 按层打印: 题目要求的二叉树的 从上至下 打印(即按层打印),又称为二叉树的 广度优先搜索(BFS)。BFS 通常借助 队列 的先入先出特性来实现。
II. 每层打印到一行: 将本层全部节点打印到一行,并将下一层全部节点加入队列,以此类推,即可分为多行打印。
思路:
算法流程:
1.特例处理:当根节点为空,则返回空列表[];
2.初始化:初始化打印结果列表res,初始化包含根节点的队列queue = [root] ;
3.BFS循环:当队列queue 为空时跳出;
1.新建一个临时列表tmp,用于存储当前层打印结果;
2.当前层打印循环:循环次数为当前层节点数(即队列queue长度);
1.出队:队首元素出队,记为node ;
2.打印:将node.val添加至tmp尾部;
3.添加子节点:若node的左(右)子节点不为空,则将左(右)子节点加入队列queue
3.将当前层结果tmp添加入res 。
4.返回值:返回打印结果列表res 即可。
知识点:
1.使用 List 和 ArrayList 的时候时,嵌套多层泛型时候,泛型类型必须一模一样
例如:List> arrList=new ArrayList>();或者省略
List> arrList=new ArrayList<>();
2.for(int i=queue.size();i>0;i—){
这句代码是实现分层打印的精髓;把 i 的初始值设置为 queue.size();则只会初始进入 for 时的这一次;但是如果错误的把 i 的终止条件设置为 queue.size();则会导致终止条件随时在发生改变(因为 queue.size() 会在循环体内发生改变),则应该把 queue.size() 设置为 for 循环的初始化条件。
3.集合中有方法:Collections.reverse(集合对象);//反转
代码实现:

  1. class Solution {
  2. public List<List<Integer>> levelOrder(TreeNode root) {
  3. Queue<TreeNode> queue = new LinkedList<>();
  4. List<List<Integer>> res = new ArrayList<>();
  5. if(root != null) queue.add(root);
  6. while(!queue.isEmpty()) {
  7. List<Integer> tmp = new ArrayList<>();
  8. for(int i = queue.size(); i > 0; i--) {
  9. TreeNode node = queue.poll();
  10. tmp.add(node.val);
  11. if(node.left != null) queue.add(node.left);
  12. if(node.right != null) queue.add(node.right);
  13. }
  14. if(res.size() % 2 == 1) Collections.reverse(tmp);
  15. res.add(tmp);
  16. }
  17. return res;
  18. }
  19. }

JZ22-3:JZ59:按之字形顺序打印二叉树

请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。(从上到下打印二叉树 III )
分析:
思路:
方法一:层序遍历+双端队列
。利用双端队列的两端皆可添加元素的特性,设打印列表(双端队列)tmp,并规定:
。奇数层则添加至tmp尾部,
。偶数层则添加至tmp头部。
复杂度分析:
。时间复杂度 O(N) :N 为二叉树的节点数量,即 BFS 需循环 N 次,占用 O(N) ;双端队列的队首和队尾的添加和删除操作的时间复杂度均为 O(1) 。
。空间复杂度 O(N) : 最差情况下,即当树为满二叉树时,最多有 N/2 个树节点 同时 在 deque 中,使用 O(N) 大小的额外空间。
方法三:层序遍历+倒序
。此方法的优点是只用列表即可,无需其他数据结构。
。偶数层倒序:若res的长度为奇数,说明当前是偶数层,则对tmp执行倒序操作。
复杂度分析:
。时间复杂度O(N):N为二叉树的节点数量,即BFS需循环N次,占用O(N)。共完成少于N个节点的倒序操作,占用O(N)。
。空间复杂度O(N)︰最差情况下,即当树为满二叉树时,最多有N/2个树节点同时在queue 中,使用O(N)大小的额外空间。
知识点:
1.Java 中将链表 LinkedList 作为双端队列使用。有方法addFirst()与addLast()
addFirst():加入队列头部
addLast():加入队列尾部
代码实现:

  1. //方法一:层序遍历+双端队列
  2. class Solution {
  3. public List<List<Integer>> levelOrder(TreeNode root) {
  4. Queue<TreeNode> queue = new LinkedList<>();
  5. List<List<Integer>> res = new ArrayList<>();
  6. if(root != null) queue.add(root);
  7. while(!queue.isEmpty()) {
  8. LinkedList<Integer> tmp = new LinkedList<>();
  9. for(int i = queue.size(); i > 0; i--) {
  10. TreeNode node = queue.poll();
  11. if(res.size() % 2 == 0) tmp.addLast(node.val); // 奇数层 -> 队列尾部
  12. else tmp.addFirst(node.val); // 偶数层 -> 队列头部
  13. if(node.left != null) queue.add(node.left);
  14. if(node.right != null) queue.add(node.right);
  15. }
  16. res.add(tmp);
  17. }
  18. return res;
  19. }
  20. }
  21. //方法三:层序遍历+倒序
  22. import java.util.*;
  23. public class Solution {
  24. public List<List<Integer>> levelOrder(TreeNode root) {
  25. Queue<TreeNode> queue = new LinkedList<>();
  26. List<List<Integer>> res = new ArrayList<>();
  27. if(root != null) queue.add(root);
  28. while(!queue.isEmpty()) {
  29. List<Integer> tmp = new ArrayList<>();
  30. for(int i = queue.size(); i > 0; i--) {
  31. TreeNode node = queue.poll();
  32. tmp.add(node.val);
  33. if(node.left != null) queue.add(node.left);
  34. if(node.right != null) queue.add(node.right);
  35. }
  36. if(res.size() % 2 == 1) Collections.reverse(tmp);
  37. res.add(tmp);
  38. }
  39. return res;
  40. }
  41. public static void main(String[] args) {
  42. TreeNode tree1=new TreeNode(3);
  43. tree1.left=new TreeNode(9);
  44. TreeNode tree2=new TreeNode(20);
  45. tree1.right=tree2;
  46. tree2.left=new TreeNode(15);
  47. tree2.right=new TreeNode(7);
  48. List<List<Integer>> lists = new Solution().levelOrder(tree1);
  49. for (List<Integer> list : lists){
  50. System.out.println(list);
  51. }
  52. }
  53. }
  54. class TreeNode {
  55. int val;
  56. TreeNode left;
  57. TreeNode right;
  58. TreeNode(int x) {
  59. val = x;
  60. }
  61. }

JZ23:二叉搜索树的后序遍历路径

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则返回true,否则返回false。假设输入的数组的任意两个数字都互不相同。
分析:
后序遍历定义: [ 左子树 | 右子树 | 根节点 ] ,即遍历顺序为 “左、右、根” 。
二叉搜索树定义: 左子树中所有节点的值 <根节点的值;右子树中所有节点的值 > 根节点的值;其左、右子树也分别为二叉搜索树。
image.png
思路:
1.递归方式1:
2.递归方式2:根据二叉搜索树的定义,可以通过递归,判断所有子树的 正确性 (即其后序遍历是否满足二叉搜索树的定义) ,若所有子树都正确,则此序列为二叉搜索树的后序遍历。

  1. - 递归解析
  2. - 终止条件:当ij,说明此子树节点数量≤1,无需判别正确性,因此直接返回true ;
  3. - 递推工作:

1.划分左右子树:遍历后序遍历的 [i,j] 区间元素,寻找第一个大于根节点的节点,索引记为m。此时,可划分出左子树区间[i,m -1]、右子树区间[m,j-1]、根节点索引 j 。
2.判断是否为二叉搜索树:
左子树区间 [i, m-1] 内的所有节点都应右子树区间 [m,j-1] 内的所有节点都应>postorderlj]。实现方式为遍历,当遇到≤postorder[j]的节点则跳出;则可通过 p==j 判断是否为二叉搜索树。

  1. - 返回值:所有子树都需正确才可判定正确,因此使用与逻辑符&&连接。

1.p==j︰判断此树是否正确。
2.recur(i, m - 1):判断此树的左子树是否正确。
3.recur(m,j - 1):判断此树的右子树是否正确。
3.方法二:辅助单调栈
后序遍历倒序: [ 根节点 | 右子树 | 左子树 ] 。类似 先序遍历的镜像 ,即先序遍历为 “根、左、右” 的顺序,而后序遍历的倒序为 “根、右、左” 顺序。
image.pngimage.pngimage.png
image.png
知识点:

代码实现:

  1. //递归方式1:
  2. class Solution {
  3. public boolean verifyPostorder(int[] postorder) {
  4. if(postorder.length<1)
  5. return true;
  6. ArrayList<Integer> list= new ArrayList<>();
  7. for(int num:postorder){
  8. list.add(num);
  9. }
  10. return isConverse(list);
  11. }
  12. public boolean isConverse(ArrayList<Integer> list){
  13. if(list.size()<1)
  14. return true;
  15. int root=list.get(list.size()-1);
  16. int i=0;
  17. ArrayList<Integer> left=new ArrayList<Integer>();
  18. ArrayList<Integer> right=new ArrayList<Integer>();
  19. while(root>list.get(i)){
  20. left.add(list.get(i++));
  21. }
  22. while(root<list.get(i)){
  23. left.add(list.get(i++));
  24. }
  25. if(i<list.size()-1)
  26. return false;
  27. return isConverse(left)&&isConverse(right);
  28. }
  29. }
  30. //递归方式2
  31. class Solution {
  32. public boolean verifyPostorder(int[] postorder) {
  33. return recur(postorder, 0, postorder.length - 1);
  34. }
  35. boolean recur(int[] postorder, int i, int j) {
  36. if(i >= j) return true;//说明此节点已经没有子树了
  37. int p = i;
  38. while(postorder[p] < postorder[j]) p++;//找遍左子树
  39. int m = p;
  40. while(postorder[p] > postorder[j]) p++;//找遍右子树
  41. return p == j && recur(postorder, i, m - 1) && recur(postorder, m, j - 1);
  42. }
  43. }
  44. //方法二:辅助单调栈
  45. class Solution {
  46. public boolean verifyPostorder(int[] postorder) {
  47. Stack<Integer> stack = new Stack<>();
  48. int root = Integer.MAX_VALUE;
  49. for(int i = postorder.length - 1; i >= 0; i--) {
  50. if(postorder[i] > root) return false;
  51. while(!stack.isEmpty() && stack.peek() > postorder[i])
  52. root = stack.pop();
  53. stack.add(postorder[i]);
  54. }
  55. return true;
  56. }
  57. }

JZ24:二叉树中和为某一值的路径

输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。
分析:
本问题是典型的二叉树方案搜索问题,使用回溯法解决,其包含 先序遍历 + 路径记录 两部分。
先序遍历: 按照 “根、左、右” 的顺序,遍历树的所有节点。
路径记录: 在先序遍历中,记录从根节点到当前节点的路径。当路径为
① 根节点到叶节点形成的路径,且
② 各节点值的和等于目标值 sum 时,
将此路径加入结果列表。
思路:

算法流程:

pathSum(root, sum) 函数:

  1. - 初始化: 初始化结果列表 `res` ,初始化路径列表 `path`
  2. - 返回值: 返回 `res` 即可。

recur(root, tar)函数:

  1. - 递推参数: 当前节点 root ,当前目标值 tar
  2. - 终止条件: 若节点 root 为空,则直接结束本轮方法。
  3. - 递推工作:
  4. - 路径更新: 将当前节点值 root.val 加入路径 path
  5. - 目标值更新: tar = tar - root.val(即目标值 tar sum 减至 00 );
  6. - 路径记录: root 为叶节点 路径和等于目标值 ,则将此路径 path 加入 res
  7. - 先序遍历: 递归左 / 右子节点。
  8. - 路径恢复: 向上回溯前,需要将当前节点从路径 path 中删除,即执行 path.removeLast()

知识点:
1.LinkedList是List接口的实现类,
因为LinkedList中有removeLast()、removeFirst()等方法,所以本题中使用它。
2.ArrayList也是List接口的实现类。
因为ArrayList中有remove()等方法,所以本题中使用它。
3..值得注意的是:
记录路径时若直接执行 res.append(path) ,则是将 path 对象加入了 res ;后续 path 改变时, res 中的 path 对象也会随之改变。所以本题使用 res.add(new LinkedList(path)); 或者 path.remove(path.size()-1);
代码实现:

  1. class Solution {
  2. LinkedList<List<Integer>> res =new LinkedList<>();
  3. LinkedList<Integer> path=new LinkedList<>();
  4. public List<List<Integer>> pathSum(TreeNode root, int sum) {
  5. recur(root,sum);
  6. return res;
  7. }
  8. void recur(TreeNode root, int sum) {
  9. if(root==null) return;
  10. path.add(root.val);
  11. sum-=root.val;
  12. if(sum==0&&root.left==null&&root.right==null)
  13. res.add(new LinkedList(path));
  14. recur(root.left,sum);
  15. recur(root.right,sum);
  16. path.removeLast();
  17. }
  18. }

JZ25:复杂链表的复制

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
分析:
方法一:递归
递归遍历原链表的 next 节点和 random 节点,连接到新链表上并返回
方法二:哈希表
利用哈希表的查询特点,考虑构建原链表节点和新链表对应节点的键值对映射关系,再遍历构建新链表各节点的next和random引用指向即可。
方法三:拆分+拼接链表
考虑构建 原节点 1 -> 新节点 1 -> 原节点 2 -> 新节点 2 -> …… 的拼接链表,如此便可在访问原节点的 random 指向节点的同时找到新对应新节点的 random 指向节点,最后拆分出原链表和新链表。
image.png
思路:
2.方法二算法流程:
1.若头节点head为空节点,直接返回null ;
2.初始化:哈希表dic ,节点cur 指向头节点;
3.复制链表:
1.建立新节点,并向dic添加键值对(原cur 节点,新cur 节点);
2. cur 遍历至原链表下一节点;
4.构建新链表的引用指向:
1.构建新节点的next和random引用指向;
2. cur 遍历至原链表下一节点;
5.返回值:新链表的头节点dic[cur] ;
复杂度分析:
。时间复杂度O(N):两轮遍历链表,使用O(N)时间。
。空间复杂度O(N):哈希表dic 使用线性大小的额外空间。
3.拼接+拆分链表
image.png
知识点:
1.HashMap的key和value都可以存放null
2.链表的表头很重要,使用链表时要先把表头存起来,以及需要定义做遍历用的指针
代码实现:

  1. //方式1:递归
  2. class Solution {
  3. HashMap<Node,Node> map=new HashMap<Node,Node>();
  4. public Node copyRandomList(Node head) {
  5. if(head==null)
  6. return null;
  7. if(map.containsKey(head))
  8. return map.get(head);
  9. Node tempNode=new Node(head.val);
  10. map.put(head,tempNode);
  11. tempNode.next=copyRandomList(head.next);
  12. tempNode.random=copyRandomList(head.random);
  13. return tempNode;
  14. }
  15. }
  16. //方式2:哈希表
  17. class Solution {
  18. public Node copyRandomList(Node head) {
  19. if(head == null) return null;
  20. Node cur = head;
  21. Map<Node, Node> map = new HashMap<>();
  22. // 3. 复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射
  23. while(cur != null) {
  24. map.put(cur, new Node(cur.val));
  25. cur = cur.next;
  26. }
  27. cur = head;
  28. // 4. 构建新链表的 next 和 random 指向
  29. while(cur != null) {
  30. map.get(cur).next = map.get(cur.next);//构建next指向
  31. map.get(cur).random = map.get(cur.random);//构建random指向
  32. cur = cur.next;
  33. }
  34. // 5. 返回新链表的头节点
  35. return map.get(head);//这里的head是原链表和新链表的映射关系,这里head对应的键值就是新链表的head,所以会输出新链表
  36. }
  37. }
  38. //方式3:
  39. class Solution {
  40. public Node copyRandomList(Node head) {
  41. if(head == null) return null;
  42. Node cur = head;
  43. // 1. 复制各节点,并构建拼接链表
  44. while(cur != null) {
  45. Node tmp = new Node(cur.val);
  46. tmp.next = cur.next;
  47. cur.next = tmp;
  48. cur = tmp.next;
  49. }
  50. // 2. 构建各新节点的 random 指向
  51. cur = head;
  52. while(cur != null) {
  53. if(cur.random != null)
  54. cur.next.random = cur.random.next;
  55. cur = cur.next.next;
  56. }
  57. // 3. 拆分两链表:需要准备两个链表的表头和移动的指针
  58. cur = head.next;//移动的指针
  59. Node pre = head, res = head.next;//拆分出的两个链表的表头
  60. while(cur.next != null) {//拆分原链表是为了不改变输入链表,通常情况下函数“不改变输入变量”是约定俗成的
  61. pre.next = pre.next.next;
  62. cur.next = cur.next.next;
  63. pre = pre.next;
  64. cur = cur.next;
  65. }
  66. pre.next = null; // 单独处理原链表尾节点
  67. return res; // 返回新链表头节点
  68. }
  69. }

JZ26:二叉搜索树与双向链表

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
分析:
本文解法基于性质:二叉搜索树的中序遍历为 递增序列 。
将 二叉搜索树 转换成一个 “排序的循环双向链表” ,其中包含三个要素:

  1. - 排序链表: 节点应从小到大排序,因此应使用 中序遍历 “从小到大”访问树的节点;
  2. - 双向链表: 在构建相邻节点(设前驱节点 pre ,当前节点 cur )关系时,不仅应 pre.right = cur ,也应 cur.left = pre
  3. - 循环链表: 设链表头节点 head 和尾节点 tail ,则应构建 head.left = tail tail.right = head

中序遍历 为 “左、根、右” 顺序,递归实现代码如下:
// 打印中序遍历
void dfs(TreeNode root) {
if(root == null) return;
dfs(root.left); // 左
System.out.println(root.val); // 根
dfs(root.right); // 右
}
根据以上分析,考虑使用中序遍历访问树的各节点 cur ;并在访问每个节点时构建 cur 和前驱节点 pre 的引用指向;中序遍历完成后,最后构建头节点和尾节点的引用指向即可。
思路:
1.将中序遍历搜索二叉树的结果(是有序的,并且是升序的结果),存放到链表中,然后构建链表的指针即可。
2.在中序遍历的同时构建双向链表:
算法流程:
dfs(cur): 递归法中序遍历;
终止条件: 当节点 cur 为空,代表越过叶节点,直接返回;
递归左子树,即 dfs(cur.left) ;
构建链表:
当 pre 为空时: 代表正在访问链表头节点,记为head 。
当 pre不为空时: 修改双向节点引用,即 pre.right = cur, cur.left = pre ;
保存 curcur : 更新 pre = cur ,即节点 cur 是后继节点的 pre;
递归右子树,即 dfs(cur.right) ;
treeToDoublyList(root):
特例处理: 若节点 root 为空,则直接返回;
初始化: 空节点 pre;
转化为双向链表: 调用 dfs(root) ;
构建循环链表: 中序遍历完成后,head 指向头节点, pre指向尾节点,因此修改 head 和 pre的双向节点引用即可。
返回值: 返回链表的头节点 head 即可。
复杂度分析:
时间复杂度 O(N) : N 为二叉树的节点数,中序遍历需要访问所有节点。
空间复杂度 O(N) : 最差情况下,即树退化为链表时,递归深度达到 NN,系统使用 O(N) 栈空间。
知识点:
1.搜索二叉树的中序遍历结果是升序的
代码实现:

  1. //思路1:将中序遍历的结果存放到ArrayList中
  2. class Solution {
  3. public Node treeToDoublyList(Node root) {
  4. if(root==null) return null;
  5. ArrayList<Node> resList=new ArrayList<>();
  6. InOrder(root,resList);
  7. int len=resList.size()-1;
  8. resList.get(0).left=resList.get(len);
  9. for(int i=1;i<len+1;i++){
  10. if(root!=null){
  11. resList.get(i).left=resList.get(i-1);
  12. resList.get(i-1).right=resList.get(i);
  13. }
  14. }
  15. resList.get(len).right=resList.get(0);
  16. return resList.get(0);
  17. }
  18. void InOrder(Node root,ArrayList<Node> resList){
  19. if(root==null) return;
  20. InOrder(root.left,resList);
  21. resList.add(root);
  22. InOrder(root.right,resList);
  23. }
  24. }
  25. //思路2:
  26. class Solution {
  27. Node head, pre;
  28. public Node treeToDoublyList(Node root) {
  29. if(root==null) return null;
  30. dfs(root);//搜索二叉树的中序遍历,顺便构建了双向链表
  31. pre.right = head;
  32. head.left =pre;//进行头节点和尾节点的相互指向,头尾结点在中序遍历中由初始值null变更到其它值了
  33. return head;
  34. }
  35. public void dfs(Node cur){//cur是每次遍历到的当前结点,对当前结点进行操作
  36. if(cur==null) return;
  37. dfs(cur.left);
  38. //pre用于记录双向链表中位于cur左侧的节点,即上一次迭代中的cur
  39. //当pre==null时,cur左侧没有节点,即此时cur为双向链表中的头节点
  40. //反之,pre!=null时,cur左侧存在节点pre,需要进行pre.right=cur的操作。
  41. if(pre==null) head = cur;
  42. else pre.right = cur;//这是遍历到了当前结点,对当前结点进行的处理
  43. cur.left = pre; //也是对当前结点进行的处理,指明了当前结点的左子节点
  44. pre = cur;//pre指向当前的cur,把当前结点作为它的后继结点的前驱结点
  45. dfs(cur.right);//全部迭代完成后,pre指向双向链表中的尾节点
  46. }
  47. }

JZ27:字符串的排列

输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则按字典序打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。(你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。)
分析:
排列方案数量: 对于一个长度为n 的字符串(假设字符互不重复),其排列共有n×(n−1)×(n−2)…×2×1 种方案。
排列方案的生成方法: 根据字符串排列的特点,考虑深度优先搜索所有排列方案。即通过字符交换,先固定第 1 位字符( n 种情况)、再固定第2 位字符( n−1 种情况)、… 、最后固定第n 位字符(1 种情况)。
重复方案与剪枝: 当字符串存在重复字符时,排列方案中也存在重复方案。为排除重复方案,需在固定某位字符时,保证 “每种字符只在此位固定一次” ,即遇到重复字符时不交换,直接跳过。从 DFS 角度看,此操作称为 “剪枝” 。
思路:
1.递归+回溯
确定一个位置的值删除一个位置的值
2.递归+回溯
递归解析:递归终止条件、递归缩小的区间

  1. - 终止条件: x = len(c) - 1 时,代表所有位已固定(最后一位只有 1种情况),则将当前组合 c 转化为字符串并加入 res,并返回;
  2. - 递推参数: 当前固定位 x
  3. - 递推工作: 初始化一个 Set ,用于排除重复的字符;将第 x 位字符与 i∈[x,len(c)] 字符分别交换,并进入下层递归;
  4. - 剪枝: c[i] Set 中,代表其是重复字符,因此“剪枝”;
  5. - c[i] 加入 Set ,以便之后遇到重复字符时剪枝;
  6. - 固定字符: 将字符 c[i] c[x] 交换,即固定 c[i] 为当前位字符;
  7. - 开启下层递归: 调用 dfs(x + 1) ,即开始固定第 x + 1个字符;
  8. - 还原交换: 将字符 c[i] c[x] 交换(还原之前的交换);

复杂度分析:
时间复杂度 O(N!) : N 为字符串 s 的长度;时间复杂度和字符串排列的方案数成线性关系,方案数为 N×(N−1)×(N−2)…×2×1 ,因此复杂度为 O(N!)
空间复杂度 O(N^2): 全排列的递归深度为 N,系统累计使用栈空间大小为 O(N) ;递归中辅助 Set 累计存储的字符数量最多为 N+(N−1)+…+2+1=(N+1)N/2 ,即占用 O(N^2)的额外空间。
知识点:
1.Set集合:去重和比较方法

  • hashset 具有去重功能,去重且无序(不是按照输入顺序打印)
  • LinkedHashSet 有序的HashSet(按照存入集合的顺序打印)
  • TreeSet用来排序,输出结果自动去重排序

注意:
1.对象时不会自动去重,需要重写equals和hashcode方法,去重系统类的对象时不用重写
2. 当添加到Set的对象HashCode码不相同时,不会调用equals方法,对象直接存到Set集合中;
HashCode码相同时,才会调用equals方法 查看是否是同一个对象(是否重复),是—-则无法存入
3.TreeSet存入对象打印时,需要实现Comparable接口 或者 创建比较器
写TreeSet排序规则步骤:
1.实现Comparable接口
2.重写接口中的 comparableTo() 方法
compareTo返回值:返回0 只有一个元素;返回正数 打印的数据 正序输出(不会去重 需要写 compareTo方法);返回负数 打印的数据 倒序输出
3.编写你想要的排序规则
2.ArrayList(Collection<? extends E> c)
构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的,可以直接在构造器参数中加入其它类型的集合,转为ArrayLis集合
3.关于此题的理解:
思路1:
思路2:
关于n个字符的排列组合问题:按排列算法,第1个位置有n种放法,第2个位置有 n-1 种放法,一次类推,第 n 个位置只有唯一的一种放置方法,假设字符串为 str=abcd
第一个位置处的4 种放法,可以这样理解,
4 种放法= a与a进行交换+a与其它3个元素进行交换,则此交换可以在一个for循环(4次)中完成;
那么接下来是同样的,第二个位置处的放法有3个,
3种方法=当前元素与本身交换+当前元素与其余的2个元素交换,可以在一个for循环(3次)中完成;
以此类推。。。则可以把每个位置放置一遍,每个位置的放置使用递归完成
但是需要注意的点有:
1.递归操作的终止条件:递归到倒数第一个位置时就可以结束,因为最后一个位置的元素唯一确定
2.每次递归缩小的范围区间:每次递归都进行到下一个位置
3.每次交换完两个元素之后需要再交换回去,这样才能在原始顺序上进行和其它元素的交换(这个与方法一中确定一个元素后删除元素,在下次排列时需要添加回去是一样的)
4.使用HashSet来去重,每一层位置都设置一个set,确保在固定某个位置时,每个字符只会在这个位置处出现一次
5.for循环的是每个位置处能选择的字符,递归的是固定的位置
代码实现:

  1. //思路1:递归+回溯
  2. import java.util.*;
  3. class Solution {
  4. TreeSet<String> result;
  5. public ArrayList<String> Permutation(String str) {
  6. if(str==null || str.length()<1){//对入参进行边界判断
  7. return new ArrayList<>();
  8. }
  9. ArrayList<Character> charArr=new ArrayList<>();
  10. for(char ch:str.toCharArray()){//将字符串改为字符列表。方便操作
  11. charArr.add(ch);
  12. }
  13. result=new TreeSet<>();//要用的result的时候再分配内存
  14. Compose(charArr, new StringBuffer(), charArr.size(), 0);//传入一个new StringBuffer()是只构建一个buffer,无论如何递归,都用这一个
  15. return new ArrayList<>(result);
  16. }
  17. private void Compose(List<Character> arr, StringBuffer buffer, int len, int index){
  18. if(len==index){//递归终止条件
  19. result.add(buffer.toString());//将排列出的字符串加入到结果集合中
  20. return;
  21. }
  22. for(int i=0;i<arr.size();i++){
  23. char valStr=arr.get(i);
  24. buffer.append(valStr);//确定index处的字符
  25. arr.remove(i);//删除指定索引处的值
  26. Compose(arr, buffer, len, index+1);//确定index+1处的字符
  27. arr.add(i, valStr);//将删除的索引处的值重新加入链表,准备回溯
  28. buffer.deleteCharAt(buffer.length()-1);//删除buffer中最后一个元素,准备回溯
  29. }
  30. }
  31. public static void main(String[] args) {
  32. ArrayList<String> abcd = new Solution().Permutation("abcd");
  33. System.out.println(abcd.size());
  34. }
  35. }
  36. //思路2:递归+回溯
  37. class Solution {
  38. char[] charArr;
  39. List<String> resList=new ArrayList<>();
  40. public String[] permutation(String s) {
  41. charArr=s.toCharArray();//转为字符串数组,方便交换
  42. dfs(0);//从第一层开始遍历
  43. return resList.toArray(new String[resList.size()]);
  44. }
  45. private void dfs(int x){//X是当前正在固定位置的索引
  46. if(x==charArr.length-1){//递归结束条件(因为递归固定到最后一位时只有一种选择,所以不用交换))
  47. resList.add(String.valueOf(charArr));//将每次的排列结果加入到结果链表中
  48. return;//结束本次递归,准备回溯
  49. }
  50. TreeSet<Character> set=new TreeSet<>();//每一层位置固定时,都有一个HashSet
  51. for(int i=x;i<charArr.length;i++){//i代表固定位置x处时能有多少种选择,x是当前正在固定位置的索引
  52. if(set.contains(charArr[i])) continue;
  53. set.add(charArr[i]);//保证每种字符只在一个位置处出现一次
  54. swap(x,i);//交换当前正在固定位置x处的值为i处的值
  55. dfs(x+1);//固定下一个位置的值
  56. swap(x,i);//每固定好下一个位置的值,需要把上一个位置的值交换回来,方便上一个位置与另外的位置再次交换
  57. }
  58. }
  59. private void swap(int m,int n){//交换两个位置的值
  60. char temp=charArr[m];
  61. charArr[m]=charArr[n];
  62. charArr[n]=temp;
  63. }
  64. }

JZ28:数组中出现次数超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
分析:
解题思路:

本文将 “数组中出现次数超过一半的数字” 简称为 “众数” 。 需要注意的是,数学中众数的定义为 “数组中出现次数最多的数字” ,与本文定义不同。 摩尔投票法: 核心理念为 票数正负抵消 。此方法时间和空间复杂度分别为 O(N)和 O(1) ,为本题的最佳解法。 摩尔投票法: 设输入数组 nums 的众数为x ,数组长度为 n 。 推论一: 若记 众数 的票数为 +1 ,非众数 的票数为 −1 ,则一定有所有数字的 票数和 > 0 。 推论二: 若数组的前 a 个数字的 票数和 =0 ,则 数组剩余 (n-a) 个数字的 票数和一定仍 >0,即后 (n-a) 个数字的 众数仍为 x 。 根据以上推论,假设数组首个元素 n1为众数,遍历并统计票数。当发生 票数和 =0 时,剩余数组的众数一定不变 ,这是由于:n1=x : 抵消的所有数字,有一半是众数 x 。n1!=x : 抵消的所有数字,少于或等于一半是众数 x 。利用此特性,每轮假设发生票数和= 0都可以缩小剩余数组区间。当遍历完成时,最后- -轮假设 的数字即为众数。
image.png

思路:
算法流程:

  • 初始化: 票数统计 votes = 0 , 众数 x;
  • 循环: 遍历数组 nums 中的每个数字 num ;
    • 当 票数 votes 等于 0 ,则假设当前数字 num 是众数;
    • 当 num = x 时,票数 votes 自增 1 ;当 num != x 时,票数 votes 自减 1 ;
  • 返回值: 返回 x 即可;

拓展: 由于题目说明 给定的数组总是存在多数元素 ,因此本题不用考虑 数组不存在众数 的情况。若考虑,需要加入一个 “验证环节” ,遍历数组 nums 统计 x 的数量。若 x 的数量超过数组长度一半,则返回 x ;否则,返回未找到众数;
知识点:
1.摩尔投票法
代码实现:

  1. //思路1:力扣网(有条件:给定的数组总是存在多数元素)
  2. class Solution {
  3. public int majorityElement(int[] nums) {
  4. int x = 0, votes = 0;
  5. for(int num : nums){
  6. if(votes == 0) x = num;
  7. votes += num == x ? 1 : -1;
  8. }
  9. return x;
  10. }
  11. }
  12. //思路2:牛客网(没给条件:给定的数组总是存在多数元素)
  13. import java.util.*;
  14. public class Solution {
  15. public int MoreThanHalfNum_Solution(int [] array) {
  16. int votes=0,x=0;
  17. for(int num:array){
  18. if(votes==0)
  19. x=num;
  20. votes += (x==num)?1:-1;
  21. }
  22. int count=0;
  23. for(int num:array){// 验证 x 是否为众数
  24. if(x==num)
  25. count++;
  26. }
  27. return 2*count>array.length?x:0;
  28. }
  29. }

JZ29:最小的k个数

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。
分析:
大顶堆方式解答:
我们用一个大根堆实时维护数组的前 k 小值。首先将前 k 个数插入大根堆中,随后从第 k+1 个数开始遍历,如果当前遍历到的数比大根堆的堆顶的数要小,就把堆顶的数弹出,再插入当前遍历到的数。最后将大根堆里的数存入数组返回即可。
复杂度分析
时间复杂度:O(nlogk),其中 n 是数组 arr 的长度。由于大根堆实时维护前 k 小值,所以插入删除都是 O(logk) 的时间复杂度,最坏情况下数组里 n 个数都会插入,所以一共需要 O(nlogk) 的时间复杂度。
空间复杂度:O(k),因为大根堆里最多 k 个数。
思路:
1.大顶堆
2.快速排序(还没看懂)
知识点:
1.Arrays.sort() 排序的复杂度为 O(nlogn)
2.边界条件要仔细判断
3.java中使用PriorityQueue这个类实现大顶堆这种数据结构
PriorityQueue实现了这些接口: Serializable, Iterable, Collection, Queue

  1. PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>() {
  2. public int compare(Integer num1, Integer num2) {
  3. return num2 - num1;
  4. }
  5. });
  6. PriorityQueue<Integer> queue=new PriorityQueue<>((o1,o2) -> o2-o1);//java8的新特性:函数式表达式

代码实现:

  1. //思路1:大顶堆
  2. class Solution {
  3. public int[] getLeastNumbers(int[] arr, int k) {
  4. int[] res=new int[k];
  5. if(k==0)//边界判断(根据题目给出的限制,k<arr.length,只要k>=1,则数组长度只会更大)
  6. return res;
  7. PriorityQueue<Integer> queue=new PriorityQueue<>((o1,o2) -> o2-o1);//java中用PriorityQueue实现大顶堆(即每个根节点都比它的子节点要大)
  8. for(int i=0;i<k;i++){
  9. queue.add(arr[i]);//先将大顶堆填满
  10. }
  11. for(int i=k;i<arr.length;i++){//遍历数组中的每个数与大顶堆的顶值进行比较
  12. if(queue.size()>=k && queue.peek()>arr[i]){//一定要保证大顶堆里一直保持着当前情况下最小的4个值
  13. queue.poll();
  14. queue.add(arr[i]);
  15. }
  16. }
  17. for(int i=0;i<k;i++){//将4个最小值遍历输出
  18. res[i]=queue.poll();
  19. }
  20. return res;
  21. }
  22. }
  23. //方式2:快速排序的思想(还没看懂)
  24. class Solution {
  25. public int[] getLeastNumbers(int[] arr, int k) {
  26. randomizedSelected(arr, 0, arr.length - 1, k);
  27. int[] vec = new int[k];
  28. for (int i = 0; i < k; ++i) {
  29. vec[i] = arr[i];
  30. }
  31. return vec;
  32. }
  33. public void randomizedSelected(int[] arr, int l, int r, int k) {
  34. if (l >= r) {//递归结束条件
  35. return;
  36. }
  37. int pos = randomizedPartition(arr, l, r);
  38. int num = pos - l + 1;
  39. if (k == num) {
  40. return;
  41. } else if (k < num) {
  42. randomizedSelected(arr, l, pos - 1, k);
  43. } else {
  44. randomizedSelected(arr, pos + 1, r, k - num);
  45. }
  46. }
  47. // 基于随机的划分
  48. public int randomizedPartition(int[] nums, int l, int r) {
  49. int i = new Random().nextInt(r - l + 1) + l;//nextInt产生的值在 [0,r-l+1) 左闭右开之间
  50. swap(nums, r, i);//把pivot的值换到最右边的位置
  51. return partition(nums, l, r);
  52. }
  53. public int partition(int[] nums, int l, int r) {
  54. int pivot = nums[r];
  55. int i = l - 1;
  56. for (int j = l; j <= r - 1; ++j) {
  57. if (nums[j] <= pivot) {
  58. i = i + 1;
  59. swap(nums, i, j);
  60. }
  61. }
  62. swap(nums, i + 1, r);
  63. return i + 1;
  64. }
  65. private void swap(int[] nums, int i, int j) {
  66. int temp = nums[i];
  67. nums[i] = nums[j];
  68. nums[j] = temp;
  69. }
  70. public static void main(String[] args) {
  71. int[] arr={3,2,1};
  72. int[] numbers = new Solution().getLeastNumbers(arr, 2);
  73. for(int num:numbers){
  74. System.out.println(num);
  75. }
  76. }
  77. }

JZ30:连续子数组的最大和

输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为 O(n).
分析:
最大连续和:最大且连续
状态 F(i):由前 i 个元素构成的序列中的最大连续和
状态转移方程: F(i-1) 加上多出来的第 i 个元素如何利用;但问题是不能保证是否是从第 i 个元素结尾的。写不出来状态转移方程,因为状态定义不完备。
所以,修改状态的定义:
状态 F(i):以第 i 个元素结尾的子序列中的最大连续和;转移方程 F(i)=max{ F(i-1)+a[i],a[i] };初始状态 F(0)=a[0];返回值为 max{ F(n-1), F(n-2),…. F(0) }。因为 F(i) 只是代表当前状态所有子序列的最大连续和。
每一个状态与前一个状态的值和新增元素本身的值有关系;
每一个状态的子序列有:前一个状态的子序列加新增元素构成的序列,以及新增元素本身构成一个子序列;
所以每一个状态子序列和的最大值只会在当前元素本身和前一个状态子序列和最大值加上当前元素,这两者之间产生。
思路:
使用动态规划解题
知识点:
动态规划
代码实现:

  1. import java.util.Arrays;
  2. public class Solution {
  3. public int FindGreatestSumOfSubArray(int[] array) {
  4. int maxSum=array[0];
  5. int Fi1=array[0];
  6. for(int i=1;i<array.length;i++){
  7. Fi1=(Fi1+array[i]>array[i])?(Fi1+array[i]):array[i];//Fi1=Math.max(Fi1+array[i],array[i]);
  8. maxSum=(maxSum>Fi1)?maxSum:Fi1;//maxSum=Math.max(maxSum,Fi1);
  9. }
  10. return maxSum;
  11. }
  12. }

JZ31:整数中1出现的次数(从1到n整数中1出现的次数)

求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
分析:
将 1 ~ n 的个位、十位、百位、…的 1 出现次数相加,即为 1 出现的总次数。某位中 1 出现次数的计算方法:
根据当前位 cur 值的不同,分为以下三种情况:
1.当 cur=0 时: 此位 1 的出现次数只由高位 high 决定,计算公式为:high×digit
2.当 cur=1 时: 此位 1 的出现次数由高位 high 和低位 low 决定,计算公式为:high×digit+low+1
3.当 cur=2,3,⋯,9 时:此位 1 的出现次数只由高位 high 决定,计算公式为:(high+1)×digit
思路:
变量递推公式,设计按照 “个位、十位、…” 的顺序计算,则 high/cur/low/digit 应初始化为:
high = n / 10 //高位
cur = n % 10 //当前位
low = 0 //低位
digit = 1 //位因子
复杂度分析:
时间复杂度 O(logn) : 循环内的计算操作使用 O(1) 时间;循环次数为数字 n 的位数,即 logn,因此循环使用 O(logn) 时间。
空间复杂度 O(1) : 几个变量使用常数大小的额外空间。
知识点:

代码实现:

  1. public class Solution {
  2. public int NumberOf1Between1AndN_Solution(int n) {
  3. int digit=1,low=0,high=n/10,cur=n%10,res=0;
  4. while(high!=0 || cur!=0){//当 high 和 cur 同时为 0 时,说明已经越过最高位,因此跳出
  5. if(cur==0)
  6. res += high*digit;
  7. else if(cur==1)
  8. res += high*digit+low+1;
  9. else
  10. res += (high+1)*digit;
  11. low += cur*digit;//digit # 将 cur 加入 low ,组成下轮 low
  12. cur = high%10;//下轮 cur 是本轮 high 的最低位
  13. high /=10;//将本轮 high 最低位删除,得到下轮 high
  14. digit *=10;//位因子每轮 × 10
  15. }
  16. return res;
  17. }
  18. }

JZ32:把数组排成最小的数

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
示例 1
输入: [10,2]
输出: “102”
示例 2:
输入: [3,30,34,5,9]
输出: “3033459”
提示:
0 < nums.length <= 100
说明:
输出结果可能非常大,所以你需要返回一个字符串而不是整数
拼接起来的数字可能会有前导 0,最后结果不需要去掉前导 0
分析:
此题求拼接起来的 “最小数字” ,本质上是一个排序问题。
排序判断规则: 设 nums任意两数字的字符串格式 x和 y ,则
若拼接字符串 x+y>y+x ,则 x “大于” y ;
反之,若 x+y根据以上规则,套用任何排序方法对 nums 执行排序即可。
image.png
思路:
算法流程:

  1. 初始化: 字符串列表 strs ,保存各数字的字符串格式;
  2. 列表排序: 应用以上 “排序判断规则” ,对 strs 执行排序;
  3. 返回值: 拼接 strs 中的所有字符串,并返回。

复杂度分析:
时间复杂度 O(NlogN) : N 为最终返回值的字符数量( strs 列表的长度 ≤N );使用快排或内置函数的平均时间复杂度为 O(NlogN) ,最差为 O(N^2)
空间复杂度 O(N) : 字符串列表 strs 占用线性大小的额外空间。
知识点:
1.字符串大小(字典序)对比的实现方法:Java 中使用 A.compareTo(B) 表示从小到大进行排序
2.内置函数定义排序规则:Java 定义为 (x, y) -> (x + y).compareTo(y + x)

  1. Arrays.sort(T[] a, Comparator<? super T> c) //根据指定的比较器引发的顺序对指定的对象数组进行排序。
  2. Arrays.sort(strArr,new Comparator<String>(){
  3. @Override
  4. public int compare(String o1,String o2){
  5. return (o1+o2).compareTo(o2+o1);
  6. }
  7. });//排序字符串数组

3.快速排序算法的掌握:看方式四的解答
代码实现:

  1. //方式一:快速排序
  2. class Solution {
  3. public String minNumber(int[] nums) {
  4. String[] strArr=new String[nums.length];
  5. for(int i=0;i<nums.length;i++)//将整数数组转为字符串数组,方便排序
  6. strArr[i]=String.valueOf(nums[i]);
  7. fastSort(strArr,0,strArr.length-1);//排序字符串数组
  8. StringBuilder builder=new StringBuilder();
  9. for(String str:strArr)
  10. builder.append(str);//将字符串数组转为字符串
  11. return builder.toString();
  12. }
  13. void fastSort(String[] strArr,int left,int right){//快速排序
  14. if(left>=right) return;//快速排序的结束条件(细分到只有一个元素)
  15. int i=left,j=right;//定义高低位的两个指针,用于移动
  16. String temp=strArr[i];//将低位取出作为第一个基准位置
  17. while(i<j){//i==j是每趟快速排序的结束条件
  18. while((strArr[j]+strArr[left]).compareTo(strArr[left]+strArr[j])>=0 && i<j )
  19. j--;//从右往左找到小于基准的值才停止
  20. while((strArr[left]+strArr[i]).compareTo(strArr[i]+strArr[left])>=0 && i<j )
  21. i++;//从左往右找到大于基准的值才停止
  22. //接下来交换两个不同方向找到的值
  23. temp=strArr[i];
  24. strArr[i]=strArr[j];
  25. strArr[j]=temp;
  26. }
  27. strArr[i]=strArr[left];//把基准数据(最低位)赋给当前位置
  28. strArr[left]=temp;//这里在交换基准位和当前位的值
  29. fastSort(strArr,left,i-1);
  30. fastSort(strArr,i+1,right);
  31. }
  32. }
  33. //方式二:使用数组内置定制排序规则
  34. class Solution {
  35. public String minNumber(int[] nums) {
  36. String[] strArr=new String[nums.length];
  37. for(int i=0;i<nums.length;i++)//将整数数组转为字符串数组,方便排序
  38. strArr[i]=String.valueOf(nums[i]);
  39. Arrays.sort(strArr,new Comparator<String>(){
  40. @Override
  41. public int compare(String o1,String o2){
  42. return (o1+o2).compareTo(o2+o1);
  43. }
  44. });//排序字符串数组
  45. StringBuilder builder=new StringBuilder();
  46. for(String str:strArr)
  47. builder.append(str);//将字符串数组转为字符串
  48. return builder.toString();
  49. }
  50. }
  51. //方式三:使用lamda表达式
  52. class Solution {
  53. public String minNumber(int[] nums) {
  54. String[] strArr=new String[nums.length];
  55. for(int i=0;i<nums.length;i++)//将整数数组转为字符串数组,方便排序
  56. strArr[i]=String.valueOf(nums[i]);
  57. Arrays.sort(strArr,(o1,o2)-> (o1+o2).compareTo(o2+o1));//排序字符串数组
  58. StringBuilder builder=new StringBuilder();
  59. for(String str:strArr)
  60. builder.append(str);//将字符串数组转为字符串
  61. return builder.toString();
  62. }
  63. }
  64. //方式四:我的快速排序解法
  65. class Solution {
  66. public String minNumber(int[] nums) {
  67. String[] strs = new String[nums.length];
  68. for(int i = 0; i < nums.length; i++)
  69. strs[i] = String.valueOf(nums[i]);
  70. fastSort(strs, 0, strs.length - 1);
  71. StringBuilder res = new StringBuilder();
  72. for(String s : strs)
  73. res.append(s);
  74. return res.toString();
  75. }
  76. void fastSort(String[] strArr, int left, int right) {
  77. if(left>=right) return;
  78. int i=left,j=right;//定义高低位的两个指针,用于移动
  79. String temp=strArr[left];//将低位取出作为第一个基准位置
  80. while(i<j){//i==j是每趟快速排序的结束条件
  81. while((strArr[j]+temp).compareTo(temp+strArr[j])>=0 && i<j )
  82. j--;//从右往左找到小于基准的值才停止
  83. if(i<j)
  84. strArr[i++]=strArr[j];//然后填坑上一个位置
  85. while((temp+strArr[i]).compareTo(strArr[i]+temp)>=0 && i<j )
  86. i++;//从左往右找到大于基准的值才停止
  87. if(i<j)
  88. strArr[j--]=strArr[i];//继续填坑下一个位置
  89. }
  90. strArr[i]=temp;//将基准位置填到最后一个位置
  91. fastSort(strArr,left,i-1);
  92. fastSort(strArr,i+1,right);
  93. }
  94. }

JZ33:丑数

丑数:我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
示例:
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
分析:
要算出第N个丑数是多少, 需要先算出前N个丑数, 那么计算前N个丑数的思路, 其实不难, 难的是代码怎么写才不超时。
首先1是丑数, 那么1乘以2,3,5得到的乘积也肯定是丑数(根据丑数的定义可知), 也就是说每一个已知的丑数, 乘上2,3,5之后都会得到3个更大的丑数(可能有重复)
由于题目要求丑数的顺序是从小到大排序, 那么我们就把已知的丑数先写出来, 每个丑数占一行, 而和2,3,5的乘积的丑数就作为列, 放到同一行上, 如下所示
1 2 3 5 // 这一行里, 2,3,5分别是1的三个乘积
2 4 6 10 // 同理, 4,6,10是2的三个乘积
3 6 9 15 // ….
4 8 12 20
观察一下上面的矩阵可以轻松发现, 每一个数组都是从左往右递增, 从上往下递增, 我们可以设置三个指针, 分别指向第0行的1,2,3列(行列数从0开始算),之后只要把三个指针所在位置的元素做个对比, 取最小的那个元素, 就是下一个丑数了. 接着三个指针必须往下移动, 直到指向的元素都大于已知丑数才可以停下来, 再继续比较.
把丑数放入丑数数组中, 继续计算下一个丑数, 直到算出第N个丑数出来即可.
思路:
每后一个丑数都是前一个丑数乘以丑数2或3或5得到的,并且是按照从小到大的顺序排列的。
知识点:
代码实现:

  1. //思路1:丑数都是2,3,5其中一个数字的倍数;每次找到最小的那个数就是丑数
  2. class Solution {
  3. public int nthUglyNumber(int n) {
  4. if(n<=0) return 0;//边界判断
  5. int[] uglyArr=new int[n];
  6. uglyArr[0]=1;//第1个丑数是1
  7. int p2=0,p3=0,p5=0;//定义3个游标,分别表示乘2,3,5,然后全部指向0位置
  8. int uglyIndex=1;//表示下一个丑数在丑数数组中的位置
  9. while(uglyIndex<n){//当uglyIndex=n,就表示了最后一个丑数的位置是uglyIndex-1
  10. uglyArr[uglyIndex]=Math.min(Math.min(uglyArr[p2]*2,uglyArr[p3]*3),uglyArr[p5]*5);
  11. while(uglyArr[uglyIndex]>=uglyArr[p2]*2){
  12. p2+=1;
  13. }
  14. while(uglyArr[uglyIndex]>=uglyArr[p3]*3){
  15. p3+=1;
  16. }
  17. while(uglyArr[uglyIndex]>=uglyArr[p5]*5){
  18. p5+=1;
  19. }
  20. uglyIndex+=1;
  21. }
  22. return uglyArr[uglyIndex-1];
  23. }
  24. }

JZ34:第一个只出现一次的字符位置

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
分析:
思路:
方法一:哈希表
遍历字符串 s ,使用哈希表统计 “各字符数量是否>1 ”。
再遍历字符串 s ,在哈希表中找到首个 “数量为 1 的字符”,并返回。
image.png
算法流程:

  1. 1. 初始化: HashMap(Java)、,记为 dic
  2. 1. 字符统计: 遍历字符串 s 中的每个字符 c

若 dic 中 不包含 键(key) c :则向 dic 中添加键值对 (c, True) ,代表字符 c 的数量为 1 ;
若 dic 中 包含 键(key) c :则修改键 c 的键值对为 (c, False) ,代表字符 c 的数量 >1 。

  1. 1. 查找数量为 11 的字符: 遍历字符串 s 中的每个字符 c

若 dic中键 c 对应的值为 True :,则返回 c 。

  1. 1. 返回 ' ' ,代表字符串无数量为 11 的字符。

复杂度分析:

  1. - 时间复杂度 O(N): N 为字符串 s 的长度;需遍历 s 两轮,使用 O(N) HashMap 查找操作的复杂度为 O(1);
  2. - 空间复杂度 O(1) 由于题目指出 s 只包含小写字母,因此最多有 26 个不同字符,HashMap 存储需占用O(26)=O(1) 的额外空间。

方法二:有序哈希表
在哈希表的基础上,有序哈希表中的键值对是 按照插入顺序排序 的。基于此,可通过遍历有序哈希表,实现搜索首个 “数量为 1 的字符”。
哈希表是 去重 的,即哈希表中键值对数量 ≤ 字符串 s 的长度。因此,相比于方法一,方法二减少了第二轮遍历的循环次数。当字符串很长(重复字符很多)时,方法二则效率更高。
复杂度分析:
时间和空间复杂度均与 “方法一” 相同,而具体分析:
方法一 需遍历 s 两轮;
方法二 遍历 s 一轮,遍历 dic 一轮( dic 的长度不大于 26,所以在字符串很长时有序哈希表更有效)。
方式三:用数组
使用一个长度为26的整数型数组(对应于26个字母)存放每个字母出现的次数:int[ ] arr=new int[ ];
arr[0]对应a出现的次数。。。arr[1]对应b出现的次数。。。
最后查找哪个位置的值等于1,就返回对应的字符。
知识点:
1.在字符串长度较大、重复字符很多时,“有序哈希表” 解法理论上效率更高。
2.HashMap的方法:
map.put(K,V);//将某个键值对加入到哈希表中
map.containsKey(K);//是否包含了某个键
3.LinkedHashMap的方法:
Set> entry= map.entrySet();
关于Entry的使用方式:
entry.getKey()
entry.getValue()
代码实现:

  1. //方式一:哈希表
  2. class Solution {
  3. public char firstUniqChar(String s) {
  4. HashMap<Character,Boolean> dic=new HashMap<>();
  5. char[] charArr=s.toCharArray();
  6. for(char ch:charArr){
  7. dic.put(ch,!dic.containsKey(ch));
  8. }
  9. for(char ch:charArr){
  10. if(dic.get(ch)) return ch;
  11. }
  12. return ' ';
  13. }
  14. }
  15. //方式二:有序哈希表
  16. class Solution {
  17. public char firstUniqChar(String s) {
  18. Map<Character,Boolean> dic=new LinkedHashMap<>();
  19. char[] charArr=s.toCharArray();
  20. for(char ch:charArr){
  21. dic.put(ch,!dic.containsKey(ch));
  22. }
  23. for(Map.Entry<Character,Boolean> entry:dic.entrySet()){
  24. if(entry.getValue()) return entry.getKey();
  25. }
  26. return ' ';
  27. }
  28. }
  29. //方式3:数组
  30. class Solution {
  31. public char firstUniqChar(String s) {
  32. int[] arr=new int[26];//最多26个字母,数组有默认的初始值
  33. char[] charArr=s.toCharArray();
  34. for(char ch:charArr){
  35. arr[ch-'a']++;
  36. }
  37. for(char ch:charArr){
  38. if(arr[ch-'a']==1)
  39. return ch;
  40. }
  41. return ' ';
  42. }
  43. }
  44. //优化方式3:初始一个长度为128的数组
  45. class Solution {
  46. public char firstUniqChar(String s) {
  47. int[] arr=new int[128];//最多26个字母,数组有默认的初始值
  48. char[] charArr=s.toCharArray();
  49. for(char ch:charArr){
  50. arr[ch]++;
  51. }
  52. for(char ch:charArr){
  53. if(arr[ch]==1)
  54. return ch;
  55. }
  56. return ' ';
  57. }
  58. }

补充:题目升级为需要区分大小写
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).(从0开始计数)
知识点:
1.A的ASCII码是65,a的ASCII码是97
2.字符最大为127,则可以设置一个长度为128的数组来存数

  1. //方式1:使用数组
  2. public class Solution {
  3. public int FirstNotRepeatingChar(String str) {
  4. int[] arr=new int[128];
  5. char[] charArr=str.toCharArray();
  6. for(char ch:charArr){
  7. arr[ch]++;
  8. }
  9. for(int i=0;i<charArr.length;i++){
  10. if(arr[charArr[i]]==1)
  11. return i;
  12. }
  13. return -1;
  14. }
  15. }

JZ35:数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
对于50%的数据,size≤10^4
对于75%的数据,size≤10^5
对于100%的数据,size≤2*10^5
输入描述:题目保证输入的数组中没有的相同的数字
分析:
思路1:归并排序求逆序对
求逆序对和归并排序又有什么关系呢?关键就在于「归并」当中「并」的过程。
复杂度分析

  1. - 时间复杂度:同归并排序 O(nlogn)。
  2. - 空间复杂度:同归并排序 O(n),因为归并排序需要用到一个临时数组。

思路2:
思路:
知识点:
1.关于归并排序知识点
「归并排序」是分治思想的典型应用,它包含这样三个步骤:

  1. 1. 分解: 待排序的区间为[l,r],令 m = (l+r)/2;我们把 [l, r]分成 [l, m] [m + 1, r]
  2. 1. 解决: 使用归并排序递归地排序两个子序列
  3. 1. 合并: 把两个已经排好序的子序列 [l, m] [m + 1, r] 合并起来

在待排序序列长度为 1 的时候,递归开始「回升」,因为我们默认长度为 1 的序列是排好序的。

补充知识点
1.System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
将数组从指定的源数组(从指定位置开始)复制到目标数组的指定位置
代码实现:

  1. //方式一:归并
  2. class Solution {
  3. public int reversePairs(int[] nums) {
  4. int len=nums.length;
  5. if(len<2) return 0;
  6. int[] copyNums=new int[len];
  7. for(int i=0;i<len;i++)
  8. copyNums[i]=nums[i];//拷贝题目给的原始数组,尽量不修改原始数组
  9. int[] temp=new int[len];//临时数组只创建一个就够用
  10. return mergeSort(copyNums,0,len-1,temp);
  11. }
  12. private int mergeSort(int[] nums,int first,int last,int[] temp){
  13. if(first==last) return 0;//递归分解截止条件:细分到一个元素
  14. int mid=first+(last-first)/2;//防止造成(first+last)造成整数越界
  15. int leftCount=mergeSort(nums,first,mid,temp);//左递归分解
  16. int rightCount=mergeSort(nums,mid+1,last,temp);//右递归分解
  17. if (nums[mid] <= nums[mid + 1])//已经是两个有序数列,不需要再继续向下归并排序
  18. return leftCount + rightCount;
  19. int mergeCount=mergeArray(nums,first,mid,last,temp);//合并数列
  20. return leftCount+rightCount+mergeCount;
  21. }
  22. //合并数列中:计算逆序对的个数
  23. private int mergeArray(int[] nums,int left,int mid,int right,int[] temp){
  24. int i=left,j=mid+1;
  25. int k=0;
  26. int count=0;
  27. while(i<=mid && j<=right){
  28. if(nums[i]<=nums[j]){
  29. temp[k++]=nums[i++];
  30. }else{
  31. temp[k++]=nums[j++];
  32. count+=(mid-i+1);
  33. }
  34. }
  35. while(i<=mid)
  36. temp[k++]=nums[i++];
  37. while(j<=right)
  38. temp[k++]=nums[j++];
  39. for(i=0;i<k;i++)
  40. nums[left+i]=temp[i];
  41. return count;
  42. }
  43. }
  44. //方式2:修改方式1(让count为全局变量,并且进行了取模%1000000007)
  45. import java.util.*;
  46. public class Solution {
  47. public int InversePairs(int [] array) {
  48. int len=array.length;
  49. if(len<2) return 0;
  50. int[] copyNums=new int[len];
  51. for(int i=0;i<len;i++){
  52. copyNums[i]=array[i];
  53. }
  54. int[] temp=new int[len];
  55. int[] count=new int[1];
  56. mergeSort(copyNums,0,len-1,temp,count);
  57. return count[0];
  58. }
  59. private void mergeSort(int[] nums,int first,int last,int[] temp,int[] count){
  60. if(first==last) return;
  61. int mid=first+(last-first)/2;
  62. mergeSort(nums,first,mid,temp,count);
  63. mergeSort(nums,mid+1,last,temp,count);
  64. if (nums[mid] <= nums[mid + 1])
  65. return;
  66. mergeArray(nums,first,mid,last,temp,count);
  67. }
  68. private void mergeArray(int[] nums,int left,int mid,int right,int[] temp,int[] count){
  69. int i=left,j=mid+1;
  70. int k=0;
  71. while(i<=mid && j<=right){
  72. if(nums[i]<=nums[j]){
  73. temp[k++]=nums[i++];
  74. }else{
  75. temp[k++]=nums[j++];
  76. count[0]=(count[0]+(mid-i+1))%1000000007;
  77. }
  78. }
  79. while(i<=mid)
  80. temp[k++]=nums[i++];
  81. while(j<=right)
  82. temp[k++]=nums[j++];
  83. for(i=0;i<k;i++)
  84. nums[left+i]=temp[i];
  85. }
  86. }

JZ36: 两个链表的第一个公共结点

输入两个链表,找出它们的第一个公共结点。
分析:
题目抽象:给定两个单链表A和B,假设一定含有公共结点,返回第一个公共结点的指针。
思路:
如何让本来长度不相等的变为相等的?可以让a+b作为链表A的新长度,b+a作为链表B的新长度。长度一致,就可以用上述的双指针解法了。
一般分析:使用两个指针 node1,node2 分别指向两个链表 headA,headB 的头结点,然后同时分别逐结点遍历,当 node1 到达链表 headA 的末尾时,重新定位到链表 headB 的头结点;当 node2 到达链表 headB 的末尾时,重新定位到链表 headA 的头结点。这样,当它们相遇时,所指向的结点就是第一个公共结点。
浪漫分析:两个链表长度分别为L1+C、L2+C, C为公共部分的长度,按照楼主的做法: 第一个人走了L1+C步后,回到第二个人起点走L2步;第2个人走了L2+C步后,回到第一个人起点走L1步。 当两个人走的步数都为L1+L2+C时就两个家伙就相爱了。
复杂度分析:

  • 时间复杂度:O(m+n), m,n分别为链表A,B的长度,最坏情况下,公共结点为最后一个,需要遍历m+n个结点
  • 空间复杂度:O(1)

知识点:
1.使用链表注意保存好头结点
代码实现:

  1. /**
  2. * Definition for singly-linked list.
  3. * public class ListNode {
  4. * int val;
  5. * ListNode next;
  6. * ListNode(int x) {
  7. * val = x;
  8. * next = null;
  9. * }
  10. * }
  11. */
  12. public class Solution {
  13. public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
  14. ListNode hA=headA;
  15. ListNode hB=headB;
  16. while(hA!=hB){
  17. hA=(hA==null)?headB:hA.next;
  18. hB=(hB==null)?headA:hB.next;
  19. }
  20. return hA;
  21. }
  22. }

JZ37:数字在排序数组中出现的次数

统计一个数字在排序数组中出现的次数。
分析:
排序数组中的搜索问题,首先想到 二分法 解决。
思路:
排序数组 nums 中的所有数字 target 形成一个窗口,记窗口的 左 / 右边界 索引分别为 left 和 right ,分别对应窗口左边 / 右边的首个元素。
本题要求统计数字 target 的出现次数,可转化为:使用二分法分别找到 左边界 left 和 右边界 right ,易得数字 target 的数量为 right−left−1 。
算法解析:

  1. 初始化: 左边界 i=0 ,右边界 j=len(nums)−1 。
  2. 循环二分: 当闭区间 [i, j] 无元素时跳出;
    1. 计算中点 m = i+ (j - i) / 2(向下取整);
    2. 若 nums[m] < target,则 target 在闭区间 [m + 1, j] 中,因此执行 i = m + 1;
    3. 若 nums[m] > target ,则 target 在闭区间 [i, m - 1] 中,因此执行 j = m - 1;
    4. 若 nums[m] = target ,则右边界 right 在闭区间 [m+1, j] 中;左边界 left 在闭区间 [i, m-1] 中。因此分为以下两种情况:
      1. 若查找 右边界 right ,则执行 i = m + 1 ;(跳出时 ii 指向右边界)
      2. 若查找 左边界 left ,则执行 j = m - 1 ;(跳出时 jj 指向左边界)
  3. 返回值: 应用两次二分,分别查找 right 和 left ,最终返回 right - left - 1 即可。

效率优化1:
以下优化基于:查找完右边界 right = i 后,则 nums[j] 指向最右边的 target (若存在)。查找完右边界后,可用 nums[j] == target 判断数组中是否包含 target ,若不包含则直接提前返回 0 ,无需后续查找左边界。查找完右边界后,左边界 left 一定在闭区间 [0, j] 中,因此直接从此区间开始二分查找即可。
效率优化2:
以上代码显得比较臃肿(两轮二分查找代码冗余)。为简化代码,可将二分查找右边界 right 的代码 封装至函数 helper() 。由于数组 nums 中元素都为整数,因此可以分别二分查找 target 和 target - 1 的右边界,将两结果相减并返回即可。本质上看, helper() 函数旨在查找数字 target 在数组 nums 中的 插入点 ,且若数组中存在值相同的元素,则插入到这些元素的右边。
image.png
知识点:
代码实现:

  1. //方式一:查找某个数的边界,再来计算次数的次数
  2. class Solution {
  3. public int search(int[] nums, int target) {
  4. int[] res=new int[2];
  5. res[0]=searchLeft(nums,target);
  6. res[1]=searchRight(nums,target);
  7. if(res[0]!=-1)
  8. return res[1]-res[0]+1;
  9. else
  10. return 0;
  11. }
  12. private int searchLeft(int[] array,int k){
  13. int left=0;
  14. int right=array.length-1;
  15. int mid=0;
  16. while(left<=right){
  17. mid=left+(right-left)/2;
  18. if(array[mid]>k){
  19. right=mid-1;
  20. }else if(array[mid]<k){
  21. left=mid+1;
  22. }else if(mid==0 || array[mid-1]!=k){
  23. return mid;
  24. }else{
  25. right=mid-1;
  26. }
  27. }
  28. return -1;
  29. }
  30. private int searchRight(int[] array,int k){
  31. int left=0;
  32. int right=array.length-1;
  33. int mid=0;
  34. while(left<=right){
  35. mid=left+(right-left)/2;
  36. if(array[mid]>k){
  37. right=mid-1;
  38. }else if(array[mid]<k){
  39. left=mid+1;
  40. }else if(mid==array.length-1 || array[mid+1]!=k){
  41. return mid;
  42. }else{
  43. left=mid+1;
  44. }
  45. }
  46. return -1;
  47. }
  48. }
  49. //方式2:仍查找某个数的上下边界所在位置
  50. public class Solution {
  51. public int GetNumberOfK(int [] array , int k) {
  52. int left=0;
  53. int right=array.length-1;
  54. int mid=0;
  55. while(left<=right){// 搜索右边界 left
  56. mid=left+(right-left)/2;
  57. if(array[mid]<=k){//不断往右边寻找
  58. left=mid+1;
  59. }else{
  60. right=mid-1;
  61. }
  62. }
  63. int resRight=left;//指向数组中最右一个目标元素的后一个位置
  64. if(right>=0&&array[right]!=k) return 0; // 若数组中无 target ,则提前返回,不再查找左边界
  65. left=0;
  66. right=array.length-1;
  67. while(left<=right){ // 搜索左边界 right
  68. mid=left+(right-left)/2;
  69. if(array[mid]<k){//不断往左边寻找
  70. left=mid+1;
  71. }else{
  72. right=mid-1;
  73. }
  74. }
  75. int resLeft=right;//指向数组中最左一个目标元素的前一个位置
  76. return resRight-resLeft-1;
  77. }
  78. }
  79. //方式3:优化方式2
  80. class Solution {
  81. public int search(int[] nums, int target) {
  82. return helper(nums,target)-helper(nums,target-1);
  83. }
  84. private int helper(int[] nums,int k){
  85. int left=0;
  86. int right=nums.length-1;
  87. int mid=0;
  88. while(left<=right){
  89. mid=left+(right-left)/2;
  90. if(nums[mid]>k)
  91. right=mid-1;
  92. else
  93. left=mid+1;//要把数设置在最右边,则把等号放在这个判断条件里
  94. }
  95. return left;
  96. }
  97. }

JZ38:二叉树的深度

输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
分析:
树的遍历方式总体分为两类:深度优先搜索(DFS)、广度优先搜索(BFS);
常见的 DFS : 先序遍历、中序遍历、后序遍历;
常见的 BFS : 层序遍历(即按层遍历)。
求树的深度需要遍历树的所有节点,本文将介绍基于 后序遍历(DFS) 和 层序遍历(BFS) 的两种解法。
思路:
方法一:后序遍历(DFS)
树的后序遍历 / 深度优先搜索往往利用 递归 或 栈 实现,本文使用递归实现。
关键点: 此树的深度和其左(右)子树的深度之间的关系。显然,此树的深度 等于 左子树的深度 与 右子树的深度 中的 最大值 +1。
image.png
算法解析:
终止条件: 当 root 为空,说明已越过叶节点,因此返回 深度 0 。
递推工作: 本质上是对树做后序遍历。
计算节点 root 的 左子树的深度 ,即调用 maxDepth(root.left);
计算节点 root 的 右子树的深度 ,即调用 maxDepth(root.right);
返回值: 返回 此树的深度 ,即 max(maxDepth(root.left), maxDepth(root.right)) + 1。
复杂度分析:

  1. - 时间复杂度 O(N) N 为树的节点数量,计算树的深度需要遍历所有节点。
  2. - 空间复杂度 O(N) 最差情况下(当树退化为链表时),递归深度可达到 N

方法二:层序遍历(BFS)
树的层序遍历 / 广度优先搜索往往利用 队列 实现。
关键点: 每遍历一层,则计数器 +1 ,直到遍历完成,则可得到树的深度。
算法解析:

  1. 1. 特例处理: root 为空,直接返回 深度 0
  2. 1. 初始化: 队列 queue (加入根节点 root ),计数器 res = 0
  3. 1. 循环遍历: queue 为空时跳出。
  4. 1. 初始化一个空列表 tmp ,用于临时存储下一层节点;
  5. 1. 遍历队列: 遍历 queue 中的各节点 node ,并将其左子节点和右子节点加入 tmp
  6. 1. 更新队列: 执行 queue = tmp ,将下一层节点赋值给 queue
  7. 1. 统计层数: 执行 res += 1 ,代表层数加 1
  8. 4. 返回值: 返回 res 即可。

复杂度分析:

  1. - 时间复杂度 O(N) N 为树的节点数量,计算树的深度需要遍历所有节点。
  2. - 空间复杂度 O(N) 最差情况下(当树平衡时),队列 queue 同时存储 N/2 个节点。

知识点:
代码实现:

  1. //方式1:后序遍历(左-右-根)【递归】
  2. class Solution {
  3. public int maxDepth(TreeNode root) {
  4. if(root==null) return 0;
  5. return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
  6. }
  7. }
  8. //方式2:优化方式1
  9. class Solution {
  10. public int maxDepth(TreeNode root) {
  11. if(root==null) return 0;
  12. int left=maxDepth(root.left);
  13. int right=maxDepth(root.right);
  14. return Math.max(left+1,right+1);
  15. }
  16. }
  17. //方式3:层级搜索
  18. class Solution {
  19. public int maxDepth(TreeNode root) {
  20. if(root==null) return 0;
  21. List<TreeNode> queue=new LinkedList<>();
  22. queue.add(root);
  23. List<TreeNode> temp;
  24. int count=0;
  25. while(!queue.isEmpty()){
  26. temp=new LinkedList<>();
  27. for(TreeNode node:queue){
  28. if(node.left!=null) temp.add(node.left);
  29. if(node.right!=null) temp.add(node.right);
  30. }
  31. queue=temp;
  32. count++;
  33. }
  34. return count;
  35. }
  36. }
  37. //方式4:优化方式3(不需要每次都new一个新的链表出来)
  38. class Solution {
  39. public int maxDepth(TreeNode root) {
  40. if(root==null) return 0;
  41. int count=0;
  42. LinkedList<TreeNode> queue=new LinkedList<>();
  43. queue.add(root);
  44. while(!queue.isEmpty()){
  45. for(int i=queue.size();i>0;i--){
  46. TreeNode node=queue.poll();
  47. if(node.left!=null) queue.add(node.left);
  48. if(node.right!=null) queue.add(node.right);
  49. }
  50. count++;
  51. }
  52. return count;
  53. }
  54. }

JZ39:平衡二叉树

输入一棵二叉树,判断该二叉树是否是平衡二叉树。在这里,我们只需要考虑其平衡性,不需要考虑其是不是排序二叉树
平衡二叉树具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
分析:
任意树的深度 等于 左子树的深度右子树的深度 中的 最大值 +1 。
思路:
方法一:后序遍历 + 剪枝 (从底至顶)此方法为本题的最优解法,但剪枝的方法不易第一时间想到。
思路:对二叉树做后序遍历,从底至顶返回子树深度,若判定某子树不是平衡树则 “剪枝” ,直接向上返回。
算法流程:
recur(root) 函数:
返回值:

  1. 当节点root 左 / 右子树的深度差 ≤1 :则返回当前子树的深度,即节点 root 的左 / 右子树的深度最大值 +1 ( max(left, right) + 1 );
  2. 当节点root 左 / 右子树的深度差 >=2 :则返回 −1 ,代表 此子树不是平衡树 。

终止条件:

  1. 当 root 为空:说明越过叶节点,因此返回高度 0 ;
  2. 当左(右)子树深度为 −1 :代表此树的 左(右)子树 不是平衡树,因此剪枝,直接返回 −1 ;

isBalanced(root) 函数:
返回值: 若 recur(root) != -1 ,则说明此树平衡,返回 true ; 否则返回 false 。
复杂度分析:

  • 时间复杂度 O(N): N 为树的节点数;最差情况下,需要递归遍历树的所有节点。
  • 空间复杂度 O(N): 最差情况下(树退化为链表时),系统递归需要使用 O(N) 的栈空间。

方法二:先序遍历 + 判断深度 (从顶至底)此方法容易想到,但会产生大量重复计算,时间复杂度较高。
思路:是构造一个获取当前子树的深度的函数 depth(root),通过比较某子树的左右子树的深度差 abs(depth(root.left) - depth(root.right)) <= 1 是否成立,来判断某子树是否是二叉平衡树。若所有子树都平衡,则此树平衡。
算法流程:
isBalanced(root) 函数: 判断树 root 是否平衡

  1. 特例处理: 若树根节点 root 为空,则直接返回 true ;
  2. 返回值: 所有子树都需要满足平衡树性质,因此以下三者使用与逻辑 && 连接;
    • abs(self.depth(root.left) - self.depth(root.right)) <= 1 :判断 当前子树 是否是平衡树;
    • self.isBalanced(root.left) : 先序遍历递归,判断 当前子树的左子树 是否是平衡树;
    • self.isBalanced(root.right) : 先序遍历递归,判断 当前子树的右子树 是否是平衡树;

depth(root) 函数: 计算树 root 的深度

  1. 终止条件: 当 root 为空,即越过叶子节点,则返回高度 0 ;
  2. 返回值: 返回左 / 右子树的深度的最大值 +1 。

复杂度分析:

  • 总体时间复杂度 :每层执行复杂度 × 层数复杂度 = O(N×logN) 。
  • 空间复杂度 O(N): 最差情况下(树退化为链表时),系统递归需要使用 O(N) 的栈空间。

知识点:
代码实现:

  1. //方法一:后序遍历 + 剪枝 (从底至顶)
  2. class Solution {
  3. public boolean isBalanced(TreeNode root) {
  4. return dsf(root)!=-1;
  5. }
  6. private int dsf(TreeNode node){
  7. if(node==null) return 0;
  8. int left=dsf(node.left);
  9. if(left==-1) return -1;
  10. int right=dsf(node.right);
  11. if(right==-1) return -1;
  12. return Math.abs(left-right)>1?-1:Math.max(left,right)+1;
  13. }
  14. }
  15. //方法二:先序遍历 + 判断深度 (从顶至底)
  16. class Solution {
  17. public boolean isBalanced(TreeNode root) {
  18. if (root == null) return true;
  19. return Math.abs(depth(root.left) - depth(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
  20. }
  21. private int depth(TreeNode root) {
  22. if (root == null) return 0;
  23. return Math.max(depth(root.left), depth(root.right)) + 1;
  24. }
  25. }

JZ40:数组中只出现一次的数字

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
分析:
题目要求时间复杂度 O(N) ,空间复杂度 O(1) ,因此首先排除 暴力法 和 哈希表统计法 。
简化问题: 一个整型数组 nums 里除 一个 数字之外,其他数字都出现了两次。设整型数组 nums 中出现一次的数字为 x ,出现两次的数字为 a,a,b,b,… ,即:nums=[a,a,b,b,…,x]
异或运算有个重要的性质,两个相同数字异或为 0 ,即对于任意整数 a 有 a⊕a=0 。因此,若将 nums 中所有数字执行异或运算,留下的结果则为 出现一次的数字 x ,即:a⊕a⊕b⊕b⊕…⊕x=0⊕0⊕…⊕x=x
异或运算满足交换律a⊕b=b⊕a ,即以上运算结果与 nums 的元素顺序无关。代码如下:

  1. public int[] singleNumber(int[] nums) {
  2. int x = 0;
  3. for(int num : nums) // 1. 遍历 nums 执行异或运算
  4. x ^= num;
  5. return x; // 2. 返回出现一次的数字 x
  6. }
  7. //0与任何数字num异或,结果仍是num

本题难点: 数组 nums 有 两个 只出现一次的数字,因此无法通过异或直接得到这两个数字。
思路:
设两个只出现一次的数字为 x , y ,由于 x 不等于 y ,则 x 和 y 二进制至少有一位不同(即分别为 0 和 1 ),根据此位可以将 nums 拆分为分别包含 x 和 y 的两个子数组。易知两子数组都满足 「除一个数字之外,其他数字都出现了两次」。因此,仿照以上简化问题的思路,分别对两子数组遍历执行异或操作,即可得到两个只出现一次的数字 x, y 。
算法流程:
1.遍历 nums 执行异或:

  • 设整型数组nums=[a,a,b,b,…,x,y] ,对 nums 中所有数字执行异或,得到的结果为 x⊕y ,即:a⊕a⊕b⊕b⊕…⊕x⊕y = 0⊕0⊕…⊕x⊕y = x⊕y

2.循环左移计算 m :

  • 根据异或运算定义,若整数 x⊕y 某二进制位为 1 ,则 x 和 y 的此二进制位一定不同。找到 x⊕y 某为 1 的二进制位,即可将数组 nums 拆分为上述的两个子数组。根据与运算特点,可知对于任意整数 a 有:
    • 若 a&0001=1 ,则 a 的第一位为 1 ;
    • 若 a&0010=1 ,则 a 的第二位为 1 ;
    • 以此类推……
  • 因此,初始化一个辅助变量 m=1 ,通过与运算从右向左循环判断,可 获取整数 x⊕y 首位 1 ,记录于 m 中,代码如下:

    1. while(z & m == 0) // m 循环左移一位,直到 z & m != 0
    2. m <<= 1

    3.拆分 nums 为两个子数组:
    4.分别遍历两个子数组执行异或:

  • 通过遍历判断 nums 中各数字和 m 做与运算的结果,可将数组拆分为两个子数组,并分别对两个子数组遍历求异或,则可得到两个只出现一次的数字,代码如下:

    1. for(int num: nums) {
    2. if((num & m) != 0) x ^= num; // 若 num & m != 0 , 划分至子数组 1 ,执行遍历异或
    3. else y ^= num; // 若 num & m == 0 , 划分至子数组 2 ,执行遍历异或
    4. }
    5. return new int[] {x, y}; // 遍历异或完毕,返回只出现一次的数字 x 和 y

    5.返回值:

  • 返回只出现一次的数字 x, y 即可。

设 nums=[3,3,4,4,1,6] ,以上计算流程如下图所示。
image.png
知识点:
代码实现:

  1. class Solution {
  2. public int[] singleNumbers(int[] nums) {
  3. int m=1,n=0,x=0,y=0;
  4. for(int num:nums){
  5. n^=num;//计算出异或 x^y 的结果
  6. }
  7. while((n&m)==0) m<<=1;//找出 x^y 中第一个为1的位(异或出的结果至少含1个1,说明x与y不相同)
  8. for(int num:nums){
  9. if((num&m)!=0) x^=num;//用 num&m 把整个数组分为两组(用得就是 x与y至少有1位上的数不相同)
  10. else y^=num;
  11. }
  12. return new int[]{x,y};
  13. }
  14. }

JZ41:和为S的连续正数序列

分析:
方法一:求和公式
设连续正整数序列的左边界 i 和右边界 j ,则此序列的 元素和 target 等于元素平均值 剑指offer记录 - 图27 乘以元素数量剑指offer记录 - 图28 ,即:剑指offer记录 - 图29,观察发现,当确定元素和 target 与 左边界 i 时,可通过解一元二次方程 ,直接计算出右边界 j ,根据一元二次方程求根公式,由于j>i 恒成立,因此直接舍去必为负数的解 ,即 j 的唯一解求取公式为:
剑指offer记录 - 图30
因此,通过从小到大遍历左边界 i 来计算 以 i 为起始数字的连续正整数序列 。每轮中,由以上公式计算得到右边界 j ,当 j 为 整数 时符合题目的 连续正整数数列 要求,此时记录结果即可。
当target=9 时,以上求解流程如下图所示:
image.png
复杂度分析:

  • 时间复杂度 (N) : 其中 N=target ;连续整数序列至少有两个数字,而 i<j 恒成立,因此至多循环 剑指offer记录 - 图32次,使用 O(N) 时间;循环内,计算 j 使用 O(1) 时间;当 i=1 时,达到最大序列长度 剑指offer记录 - 图33 ,考虑到解的稀疏性,将列表构建时间简化考虑为 O(1) ;
  • 空间复杂度O(1) : 变量 i , j 使用常数大小的额外空间。

方法二:滑动窗口(双指针)
设连续正整数序列的左边界 i 和右边界 j ,则可构建滑动窗口从左向右滑动。循环中,每轮判断滑动窗口内元素的和 与 目标值 target 的大小关系,若相等则记录结果,若大于 target 则移动左边界 i (以减小窗口内的元素和),若小于 target 则移动右边界 j (以增大窗口内的元素和)。
算法流程:
1.初始化: 左边界 i=1 ,右边界 j=2 ,元素和 s=3 ,结果列表 res ;
2.循环: 当 i≥j 时跳出;
当 s>target 时: 向右移动左边界 i=i+1 ,并更新元素和 s ;
当 s当 s=target 时: 记录连续整数序列,并向右移动左边界 i=i+1 ;
3.返回值: 返回结果列表 res ;
复杂度分析

  • 时间复杂度 O(N) : 其中 N=target ;连续整数序列至少有两个数字,而 i<j 恒成立,因此至多循环 target 次( i , j 都移动到 剑指offer记录 - 图34 ),使用 O(N) 时间;当 i=1 时,达到最大序列长度剑指offer记录 - 图35,考虑到解的稀疏性,将列表构建时间简化考虑为 O(1) ;
  • 空间复杂度 O(1) : 变量 i , j , s 使用常数大小的额外空间。

当 target=9 时,以上求解流程如下图所示:观察本文的算法流程发现,当 s=targets>target 的移动边界操作相同,因此可以合并代码。
image.png
思路:
知识点:
代码实现:

  1. //方式一:求和公式
  2. class Solution {
  3. public int[][] findContinuousSequence(int target) {
  4. int i=1;
  5. double j=2.0;//初始值
  6. List<int[]> resList=new ArrayList<>();
  7. while(i<j){
  8. //这里要用(long)i*i;而不是(long)(i*i),这样可能已经超过 int 类型取值范围
  9. j= ( -1+Math.sqrt(1+4*(2*target+(long)i*i-i)) )/2.0;//求根公式
  10. if(j==(int)j){//求出的根是整数
  11. int[] resArr=new int[(int)j-i+1];
  12. for(int k=i;k<=(int)j;k++)
  13. resArr[k-i]=k;
  14. resList.add(resArr);
  15. }
  16. i++;
  17. }
  18. return resList.toArray(new int[0][]);//i==j时,跳出循环,返回值
  19. }
  20. }
  21. //方法二:滑动窗口(双指针)
  22. class Solution {
  23. public int[][] findContinuousSequence(int target) {
  24. int i=1;
  25. int j=2;
  26. int res=3;
  27. List<int[]> resList=new ArrayList<>();
  28. while(i<j){//滑动窗口的左右边界
  29. if(target==res){
  30. int[] resArr=new int[(int)j-i+1];
  31. for(int k=i;k<=(int)j;k++)
  32. resArr[k-i]=k;
  33. resList.add(resArr);
  34. }
  35. if(res>=target){//可以合并大于等于对滑动窗口所做的操作
  36. res-=i;
  37. i++;
  38. }else{
  39. j++;
  40. res+=j;
  41. }
  42. }
  43. return resList.toArray(new int[0][]);
  44. }
  45. }

补充知识点:集合转数组的toArray()和toArray(T[] a)方法

  1. 1ArrayListtoArray
  2.   ArrayList提供了一个将List转为数组的一个非常方便的方法toArraytoArray有两个重载的方法:
  3.   1list.toArray(); public Object[] toArray()
  4.   2list.toArray(T[] a); public <T> T[] toArray(T[] a)
  5.   对于第一个重载方法,是将list直接转为Object[] 数组;
  6.   第二种方法是将list转化为你所需要类型的数组,当然我们用的时候会转化为与list内容相同的类型。
  7. 注意:不能将Object[] 转化为String[],转化的话只能是取出每一个元素再转化。
  8. java中的强制类型转换只是针对单个对象的,想要偷懒将整个数组转换成另外一种类型的数组是不行的,
  9. 这和数组初始化时需要一个个来也是类似的。
  10. 总结:
  11. 1、集合转数组用方法,比如:list.toArray(new String[list.size()]);
  12. 2、关于toArray()与toArray(T[] a]的对比
  13. List接口的toArray()方法就是直接调用Arrays.copyOf(elementData, size),将list中的元素对象的引用装在一个新的生成数组中。
  14. List接口的toArray(T[] a)方法会返回指定类型(必须为list元素类型的父类或本身)的数组对象,
  15. 如果a.length小于list元素个数就直接调用ArrayscopyOf()方法进行拷贝并且返回新数组对象,新数组中也是装的list元素对象的引用,
  16. 否则先调用System.arraycopy()将list元素对象的引用装在a数组中,如果a数组还有剩余的空间,则在a[size]放置一个nullsize就是list中元素的个数,这个null值可以使得toArray(T[] a)方法调用者可以判断null后面已经没有list元素了。

JZ42:和为S的两个数字

输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。返回值描述:对应每个测试案例,输出两个数,小的先输出。
分析:
最外层的乘积最小,别被题目误导!!!
思路:
知识点:
1.使用 new ArrayList<>(Arrays.asList(new Integer[]{array[i],array[j]})); 外面包裹一层new ArrayList<>();
代码实现:

  1. import java.util.ArrayList;
  2. import java.util.Arrays;
  3. public class Solution {
  4. public static ArrayList<Integer> FindNumbersWithSum(int [] array, int sum) {
  5. int i=0,j=array.length-1;
  6. while(i<j){
  7. int resSum=array[i]+array[j];
  8. if(sum>resSum){
  9. i++;
  10. }else if(sum<resSum){
  11. j--;
  12. }else{
  13. return new ArrayList<>(Arrays.asList(new Integer[]{array[i],array[j]}));
  14. }
  15. }
  16. return new ArrayList<>();
  17. }
  18. }

补充 1:JZ42题的简化版本

  1. 题目:和为s的两个数字
  2. 输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s
  3. 如果有多对数字的和等于s,则输出任意一对即可。
  4. 思路:
  5. 利用 HashMap 可以通过遍历数组找到数字组合,时间和空间复杂度均为 O(N)
  6. 注意本题的 nums 排序数组 ,因此可使用 双指针法 将空间复杂度降低至 O(1)
  7. 算法流程:
  8. 1、初始化: 双指针 i , j 分别指向数组 nums 的左右两端 (俗称对撞双指针)。
  9. 2、循环搜索: 当双指针相遇时跳出;
  10. 计算和s=nums[i]+nums[j]
  11. s>target ,则指针 j 向左移动,即执行 j=j1
  12. s<target ,则指针 i 向右移动,即执行 i=i+1
  13. s=target ,立即返回数组 [nums[i],nums[j]]
  14. 3、返回空数组,代表无和为 target 的数字组合。
  15. 复杂度分析:
  16. 时间复杂度 O(N) N 为数组 nums 的长度;双指针共同线性遍历整个数组。
  17. 空间复杂度 O(1) 变量 i, j 使用常数大小的额外空间。
  18. 代码:
  19. class Solution {
  20. public int[] twoSum(int[] nums, int target) {
  21. int i = 0, j = nums.length - 1;
  22. while(i < j) {
  23. int s = nums[i] + nums[j];
  24. if(s < target) i++;
  25. else if(s > target) j--;
  26. else return new int[] { nums[i], nums[j] };
  27. }
  28. return new int[0];
  29. }
  30. }

补充2:工具类Arrays.asList()方法把数组转换成集合

  1. 总结:
  2. 1Arrays.asList()不要乱用,底层其实还是数组。
  3. 2、如果使用了Arrays.asList()的话,最好不要使用其集合的操作方法。
  4. 3List list = new ArrayList<>(Arrays.asList("a", "b", "c"))可以在外面这样包一层真正的ArrayList

JZ43:左旋转字符串

左旋转字符串:字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。比如,输入字符串”abcdefg”和数字2,该函数将返回左旋转两位得到的结果”cdefgab”。
分析:
本题做法较多,本文主要介绍 “字符串切片”,“列表遍历拼接”,“字符串遍历拼接” 三种方法。
由于本题的多解法涉及到了 字符串为不可变对象 的相关概念,导致效率区别较大。
思路:
方法一:字符串切片,应用字符串切片函数,可方便实现左旋转字符串。
获取字符串 s[n:] 切片和 s[:n] 切片,使用 “+” 运算符拼接并返回即可。
复杂度分析:
时间复杂度 O(N) : 其中 N 为字符串 s 的长度,字符串切片函数为线性时间复杂度;
空间复杂度 O(N) : 两个字符串切片的总长度为 N 。
方法二:列表遍历拼接,若面试规定不允许使用 切片函数 ,则使用此方法。
算法流程:

  1. 1. 新建一个StringBuilder(Java) ,记为 res
  2. 1. 先向 res 添加 “第 n+1 位至末位的字符”
  3. 1. 再向 res 添加 “首位至第 n 位的字符”
  4. 1. res 转化为字符串并返回。

复杂度分析:
时间复杂度 O(N) : 线性遍历 s 并添加,使用线性时间;
空间复杂度 O(N) : 新建的辅助 res 使用 O(N) 大小的额外空间。
方法三:字符串遍历拼接
若规定 Java 只能用 String ,则使用此方法。此方法与 方法二 思路一致,区别是使用字符串代替列表。
复杂度分析:
时间复杂度 O(N) : 线性遍历 s 并添加,使用线性时间;
空间复杂度O(N) : 假设循环过程中内存会被及时回收,内存中至少同时存在长度为 N 和 N−1 的两个字符串(新建长度为 N 的 res 需要使用前一个长度 N−1 的 res ),因此至少使用 O(N) 的额外空间。
效率比较:
方法一,无冗余操作,效率最高。方法三,数据量较大时效率低下。
代码实现:

  1. //方式一:字符串切片(Java的API)
  2. class Solution {
  3. public String reverseLeftWords(String s, int n) {
  4. if(n>s.length()) return "";
  5. return s.substring(n,s.length())+s.substring(0,n);
  6. //或者 return s.substring(n)+s.substring(0,n);
  7. }
  8. }
  9. //方式二:列表遍历拼接(StringBuilder)
  10. class Solution {
  11. public String reverseLeftWords(String s, int n) {
  12. if(n>s.length()) return "";
  13. StringBuilder builder=new StringBuilder();
  14. for(int k=n;k<n+s.length();k++)
  15. builder.append(s.charAt(k%s.length()));
  16. return builder.toString();
  17. }
  18. }
  19. //方式三:字符串遍历拼接(String)
  20. class Solution {
  21. public String reverseLeftWords(String s, int n) {
  22. if(n>s.length()) return "";
  23. String res="";
  24. for(int k=n;k<n+s.length();k++)
  25. res+=s.charAt(k%s.length());
  26. return res;
  27. }
  28. }

JZ44:翻转单词顺序列

翻转单词顺序列:牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?
分析:
思路:
方法一:双指针
算法解析:

  1. 倒序遍历字符串 s ,记录单词左右索引边界 i , j ;
  2. 每确定一个单词的边界,则将其添加至单词列表 res ;
  3. 最终,将单词列表拼接为字符串,并返回即可。

复杂度分析:
时间复杂度 O(N) : 其中 N 为字符串 s 的长度,线性遍历字符串。
空间复杂度 O(N) : 新建的 StringBuilder中的字符串总长度 ≤N ,占用 O(N) 大小的额外空间。
方法二:分割 + 倒序
利用 “字符串分割”、“列表倒序” 的内置函数 (面试时不建议使用) ,可简便地实现本题的字符串翻转要求。
算法解析:以空格为分割符完成字符串分割后,若两单词间有 x>1 个空格,则在单词列表 strs 中,此两单词间会多出 x−1 个 “空单词” (即 “” )。解决方法:倒序遍历单词列表,并将单词逐个添加至 StringBuilder ,遇到空单词时跳过。

image.png

复杂度分析:

  1. - 时间复杂度 O(N) 总体为线性时间复杂度,各函数时间复杂度和参考资料链接如下。

split() 方法: 为 O(N) ;
trim() 和 strip() 方法: 最差情况下(当字符串全为空格时),为 O(N) ;
join() 方法: 为 O(N) ;
reverse() 方法: 为O(N) ;

  1. - 空间复杂度 O(N) 单词列表 strs 占用线性大小的额外空间。

知识点:
1.关于String的方法
public String trim() :去除字符串序列的首尾空格
public String[] split(String regex):将字符串拆分为给定表达式的字符串数组。
public String substring(int beginIndex, int endIndex):截取指定区间的字符串,左开右闭
public String substring(int beginIndex):截取指定位置开始到结束的字符串
public char charAt(int index):返回字符串索引位置处的单个字符
2.关于StringBuilder的方法
public StringBuffer append(Xxx xx):追加到末尾
代码实现:

  1. //方式一:双指针法(从尾到头挨个取出单词)
  2. class Solution {
  3. public String reverseWords(String s) {
  4. s=s.trim();//删除首尾空格
  5. int j=s.length()-1,i=j;
  6. StringBuilder builder=new StringBuilder();
  7. while(i>=0){
  8. while(i>=0&&s.charAt(i)!=' ') i--;// 搜索首个空格
  9. builder.append(s.substring(i+1,j+1)+" "); // 添加单词
  10. while(i>=0&&s.charAt(i)==' ') i--;// 跳过单词间空格
  11. j=i;// j 指向下个单词的尾字符
  12. }
  13. return builder.toString().trim();// 转化为字符串并返回
  14. }
  15. }
  16. //方式二:分割+倒序
  17. class Solution {
  18. public String reverseWords(String s) {
  19. String[] strs=s.trim().split(" "); // 删除首尾空格,分割字符串
  20. StringBuilder res=new StringBuilder();
  21. for(int i=strs.length-1;i>=0;i--) { // 倒序遍历单词列表
  22. if(strs[i].equals("")) continue; // 遇到空单词则跳过
  23. res.append(strs[i] + " "); // 将单词拼接至 StringBuilder
  24. }
  25. return res.toString().trim(); // 转化为字符串,删除尾部空格,并返回
  26. }
  27. }

JZ45:扑克牌顺子

扑克牌中的顺子:LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张^_^)…他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子…..LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。
分析:
根据题意,此 5 张牌是顺子的 充分条件 如下:

  1. 除大小王外,所有牌 无重复
  2. 设此 5 张牌中最大的牌为 max ,最小的牌为 min (大小王除外),则需满足:max−min<5

因而,可将问题转化为:此 5 张牌是否满足以上两个条件。
image.png
思路:
方法一:排序 + 遍历

  1. 先对数组执行排序。Arrays.sort(int[] nums)
  2. 判别重复: 排序数组中的相同元素位置相邻,通过遍历数组判断 nums[i]=nums[i+1] 是否成立来判重。
  3. 获取最大 / 最小的牌: 排序后,数组末位元素 nums[4] 为最大牌;元素 nums[joker] 为最小牌,其中joker 为大小王的数量。

复杂度分析:

  • 时间复杂度O(NlogN)=O(5log5)=O(1) : 其中 N 为 nums 长度;数组排序使用 O(NlogN) 时间。
  • 空间复杂度 O(1) : 变量 joker 使用 O(1) 大小的额外空间。

方法二: 集合 Set + 遍历

  1. 遍历五张牌,遇到大小王(即 0 )直接跳过。
  2. 判别重复: 利用 Set 实现遍历判重, Set 的查找方法的时间复杂度为 O(1) ;
  3. 获取最大 / 最小的牌: 借助辅助变量 max 和 min ,遍历统计即可。

复杂度分析:

  • 时间复杂度 O(N)=O(5)=O(1) : 其中 N 为 nums 长度 ;遍历数组使用 O(N) 时间。
  • 空间复杂度 O(N)=O(5)=O(1) : 用于判重的辅助 Set 使用 O(N) 额外空间。

知识点:
1.Arrays.sort()使用了归并排序算法,时间复杂度为O(nlogn)
代码实现:

  1. //方式一:排序+遍历
  2. class Solution {
  3. public boolean isStraight(int[] nums) {
  4. if(nums==null || nums.length==0) return false;//越界判断
  5. Arrays.sort(nums);//数组排序
  6. int count=0;
  7. for(int i=0;i<4;i++){
  8. if(nums[i]==0) count++; //统计大小王数量
  9. else if(nums[i]==nums[i+1])
  10. return false;//如果有重复,则直接返回
  11. }
  12. return nums[4]-nums[count]<5;// 最大牌-最小牌<5则可构成顺子
  13. }
  14. }
  15. //方式二:集合Set+遍历
  16. class Solution {
  17. public boolean isStraight(int[] nums) {
  18. if(nums==null || nums.length==0) return false;
  19. HashSet<Integer> repeat=new HashSet<>();
  20. int max=0,min=14;
  21. for(int num:nums){
  22. if(num==0) continue; //跳过大小王
  23. max=Math.max(max,num); //最大牌
  24. min=Math.min(min,num); //最小牌
  25. if(repeat.contains(num)) return false; // 若有重复,提前返回 false
  26. repeat.add(num); //添加此牌至Set
  27. }
  28. return max-min<5; //最大牌-最小牌<5则可构成顺子
  29. }
  30. }

JZ46:孩子们的游戏(圆圈中最后剩下的数)

孩子们的游戏:每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0…m-1报数….这样下去….直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1) 。如果没有小朋友,请返回-1
分析:
模拟整个删除过程最直观,即构建一个长度为 n 的链表,各节点值为对应的顺序索引;每轮删除第 m 个节点,直至链表长度为 1 时结束,返回最后剩余节点的值即可。模拟法需要循环删除 n−1 轮,每轮在链表中寻找删除节点需要 m 次访问操作(链表线性遍历),因此总体时间复杂度为 O(nm) 。题目给定的 m,n 取值范围如下所示,观察可知此时间复杂度是不可接受的。
1≤n≤10^5
1≤m≤10^6
实际上,本题是著名的 “约瑟夫环” 问题,可使用 动态规划 解决。
思路:
输入 n,m ,记此约瑟夫环问题为 「n,m 问题」 ,设解(即最后留下的数字)为 f(n) ,则有:
「n,m 问题」:数字环为 0,1,2,…,n−1 ,解为 f(n) ;
「n−1,m 问题」:数字环为 0,1,2,…,n−2 ,解为 f(n−1) ;
以此类推……
请注意,数字环是 首尾相接 的,为方便行文,本文使用列表形式表示。
对于「n,m 问题」,首轮删除环中第 m 个数字后,得到一个长度为 n−1 的数字环。由于有可能 m>n ,因此删除的数字为 m%n−1 ,删除后的数字环从下个数字(即 m%n )开始,设 t=m%n ,可得数字环:
t,t+1,t+2,…,0,1,…,t−3,t−2
删除一轮后的数字环也变为一个「n−1,m 问题」,观察以下数字编号对应关系:
「n-1, m 问题」 「n, m 问题」删除后
0 → t+0
1 → t+1
… → …
n−2 → t−2
设「n−1,m 问题」某数字为 x ,则可得递推关系:
x → (x+t)%n
换而言之,若已知「n−1,m 问题」的解 f(n−1) ,则可通过以上公式计算得到「n,m 问题」的解 f(n) ,即:
f(n)=(f(n−1)+t)%n=(f(n−1)+m%n)%n=(f(n−1)+m)%n
f(n) 可由 f(n−1) 得到,f(n−1) 可由 f(n−2) 得到,……,f(2) 可由 f(1) 得到;若给定f(1) 的值,就可以递推至任意 f(n) 。而「1,m 问题」的解 f(1)=0 恒成立,即无论 m 为何值,长度为 1 的数字环留下的是一定是数字 0 。
n 与 f(n) 里面的 n 是同一个同样大
以上数学推导本质是得出动态规划的 转移方程初始状态
动态规划解析:

  1. 状态定义: 设「i,m 问题」的解为 dp[i] ;
  2. 状态转移方程: 通过以下公式可从 dp[i−1] 递推得到 dp[i] ;

dp[i]=(dp[i−1]+m)%i

  1. 初始状态:「1,m 问题」的解恒为0 ,即 dp[1]=0 ;
  2. 返回值: 返回「n,m 问题」的解 dp[n] ;

复杂度分析:

  • 时间复杂度 O(n) : 状态转移循环 n−1 次使用 O(n) 时间,状态转移方程计算使用 O(1) 时间;
  • 空间复杂度 O(1) : 使用常数大小的额外空间;

知识点:
代码实现:

  1. class Solution {
  2. public int lastRemaining(int n, int m) {
  3. if(m==0||n==0) return -1;
  4. int x=0;//初始条件,初始的编号肯定为1
  5. for(int k=2;k<=n;k++){
  6. x=(x+m)%k;
  7. }
  8. return x;
  9. }
  10. }

JZ47:求1+2+3+…+n

求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
分析:
本题在简单问题上做了许多限制,需要使用排除法一步步导向答案。1+2+…+(n−1)+n 的计算方法主要有三种:平均计算、迭代、递归。
方法一: 平均计算
问题: 此计算必须使用 乘除法 ,因此本方法不可取,直接排除。

  1. public int sumNums(int n) {
  2. return (1 + n) * n / 2;
  3. }

方法二: 迭代
问题: 循环必须使用 while 或 for ,因此本方法不可取,直接排除。

  1. public int sumNums(int n) {
  2. int res = 0;
  3. for(int i = 1; i <= n; i++)
  4. res += i;
  5. return res;
  6. }

方法三: 递归
问题: 终止条件需要使用 if ,因此本方法不可取。
思考: 除了 if 和 switch 等判断语句外,是否有其他方法可用来终止递归?

  1. public int sumNums(int n) {
  2. if(n == 1) return 1;
  3. n += sumNums(n - 1);
  4. return n;
  5. }

思路:
逻辑运算符的短路效应:
常见的逻辑运算符有三种,即 “与&& “,”或 ∣∣ “,”非 ! “ ;而其有重要的短路效应,如下所示:

  1. if(A && B) // 若 A 为 false ,则 B 的判断不会执行(即短路),直接判定 A && B 为 false
  2. if(A || B) // 若 A 为 true ,则 B 的判断不会执行(即短路),直接判定 A || B 为 true
  3. 本题需要实现 “当 n = 1n=1 时终止递归” 的需求,可通过短路效应实现。
  4. n > 1 && sumNums(n - 1) // 当 n = 1 时 n > 1 不成立 ,此时 “短路” ,终止后续递归

image.png
复杂度分析:

  • 时间复杂度 O(n) : 计算 n+(n−1)+…+2+1 需要开启 n 个递归函数。
  • 空间复杂度 O(n) : 递归深度达到 n ,系统使用 O(n) 大小的额外空间。

知识点:
代码实现:

  1. class Solution {
  2. public int sumNums(int n) {
  3. boolean x= n>1 && (n+=sumNums(n-1))>0;
  4. return n;
  5. }
  6. }
  7. //注释版代码
  8. class Solution {
  9. int res = 0;
  10. public int sumNums(int n) {
  11. boolean tmp=(n>1)&&(sumNums(n-1)>0);//tmp值用不到,只是为了使后面sumNums(n-1)语句可以被执行;
  12. //第二个判断条件:sumNums(n-1)>0的判断条件也可改为<0;==0;等等,改成任意判断条件都可以
  13. //第二个判断条件只是为了进入迭代,且由于sumNums返回int,必须任意加一个判断转为boolean;
  14. res+=n;
  15. return res;
  16. }
  17. }

JZ48:不用加减乘除做加法

不用加减乘除做加法:写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“”、“/” 四则运算符号。
分析:
本题考察对位运算的灵活使用,即使用位运算实现加法。
设两数字的二进制形式 a,b ,其求和 s=a+b ,a(i) 代表 a 的二进制第 i 位,则分为以下四种情况:
image.png
观察发现,无进位和 与 异或运算 规律相同*进位 和 与运算 规律相同(并需左移一位
)。因此,无进位和 n 与进位 c 的计算公式如下;
n=a⊕b 非进位和:异或运算
c=a&b<<1 进位:与运算+左移一位
(和 s )=(非进位和 n )+(进位 c )。即可将 s=a+b 转化为:
s=a+b⇒s=n+c
循环求 n 和 c ,直至进位 c=0 ;此时 s=n ,返回 n 即可。

  1. Q 若数字 aa bb 中有负数,则变成了减法,如何处理?
  2. A 在计算机系统中,数值一律用 补码 来表示和存储。补码的优势: 加法、减法可以统一处理(CPU只有加法器)。
  3. 因此,以上方法 同时适用于正数和负数的加法

复杂度分析:

  • 时间复杂度 O(1) : 最差情况下(例如 a= 0x7fffffff ,b=1 时),需循环 32 次,使用 O(1) 时间;每轮中的常数次位操作使用 O(1) 时间。
  • 空间复杂度 O(1) : 使用常数大小的额外空间。

思路:
知识点:
代码实现:

  1. //方式1:循环
  2. class Solution {
  3. public int add(int a, int b) {
  4. while(b!=0){// 当进位为 0 时跳出
  5. int c= (a&b)<<1;// c = 进位
  6. a^=b; // a = 非进位和
  7. b=c;// b = 进位
  8. }
  9. return a;
  10. }
  11. }//方式2:递归
  12. class Solution {
  13. public int add(int a, int b) {
  14. if(b==0) return a;//把a+b转换成非进位和+进位;一直转换直到第二个加数变成0
  15. return add(a^b,(a&b)<<1);
  16. }
  17. }

JZ49:把字符串转换成整数

写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。
首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。//考虑使用trim()
当我们寻找到的第一个非空字符为正或者负号时,将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。//符号位
该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。
注意:
假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。
在任何情况下,若函数不能进行有效的转换时,请返回 0。
说明:
假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−2^31, 2^31 − 1]。如果数值超过这个范围,请返回 INT_MAX (2^31 − 1) 或 INT_MIN (−2^31) 。
分析:
根据题意,有以下四种字符需要考虑

  1. 首部空格: 删除之即可;
  2. 符号位: 三种情况,即 ‘’+’’ , ‘’−’’ , ‘’无符号” ;新建一个变量保存符号位,返回前判断正负即可。
  3. 非数字字符: 遇到首个非数字的字符时,应立即返回。
  4. 数字字符:

字符转数字: “此数字的 ASCII 码” 与 “ 0 的 ASCII 码” 相减即可;
数字拼接: 若从左向右遍历数字,设当前位字符为 c ,当前位数字为 x ,数字结果为 res ,
则数字拼接公式为:
res=10×res+x
x=ascii(c)−ascii(‘0’)
数字越界处理:
题目要求返回的数值范围应在 [-2^31, 2^31 - 1],因此需要考虑数字越界问题。而由于题目指出 环境只能存储 32 位大小的有符号整数 ,因此判断数字越界时,要始终保持 res 在 int 类型的取值范围内。
在每轮数字拼接前,判断 res 在此轮拼接后是否超过 2147483647 ,若超过则加上符号位直接返回。
设数字拼接边界 bndry=2147483647/10,则以下两种情况越界:
情况一:res>bndry,执行拼接 10×res ≥ 2147483650 越界
情况二:res>bndry && x>7 ,拼接后是 2147483648 或 2147483649 越界
复杂度分析:
时间复杂度 O(N) : 其中 N 为字符串长度,线性遍历字符串占用 O(N) 时间。
空间复杂度 O(N) : 删除首尾空格后需建立新字符串,最差情况下占用 O(N) 额外空间。
思路:
知识点:
代码实现:

  1. //方式一:使用trim(String str)去除字符串前后空格的空间复杂度为O(n)
  2. class Solution {
  3. public int strToInt(String str) {
  4. char[] c=str.trim().toCharArray();//去掉字符串的前后空格,并转为字符数组
  5. if(c.length==0) return 0;//如果是空串返回0
  6. int sign=1,i=1;//sign是符号位; i是数字开始的索引位置
  7. int res=0,bandary=Integer.MAX_VALUE/10;//bandary是越界界限
  8. if(c[0]=='-') //if-else语句只可能选择一个分支进入
  9. sign=-1;
  10. else if(c[0]!='+')//是+或-都是从索引1开始;是无符号则索引从0开始
  11. i=0;
  12. for(int j=i;j<c.length;j++){
  13. if(c[j]>'9' || c[j]<'0') break;//遇到非数字字符,直接返回
  14. if(res>bandary || (res==bandary&&c[j]>'7')) //越界情况
  15. return sign==1?Integer.MAX_VALUE:Integer.MIN_VALUE;
  16. res=res*10+ (c[j]-'0');//当前数字的大小
  17. }
  18. return res*sign;//最后的结果
  19. }
  20. }
  21. //方式二:从头开始遍历字符串,不使用trim()函数,可以将空间复杂度降低至 O(1)
  22. class Solution {
  23. public int strToInt(String str) {
  24. int res=0,bandary=Integer.MAX_VALUE/10;
  25. int i=0,sign=1,len=str.length();
  26. if(len==0) return 0;
  27. while(str.charAt(i)==' '){//处理空串的情况 或者 字符串的数字前面有空格的情况
  28. if(++i==len) return 0;//则 i 的位置相应的改变
  29. }
  30. if(str.charAt(i)=='-') sign=-1;//符号位,对负号做出相应的处理
  31. if(str.charAt(i)=='-'||str.charAt(i)=='+') i++;//符号位再次移动i,让i指向第一个数字的位置处,必须使用i++
  32. for(int j=i;j<len;j++){
  33. if(str.charAt(j)>'9'||str.charAt(j)<'0') break;//遇到非数字则跳出,然后返回
  34. if(res>bandary||(res==bandary&&str.charAt(j)>'7')) //越界情况的相应处理
  35. return sign==1?Integer.MAX_VALUE:Integer.MIN_VALUE;
  36. res=res*10+(str.charAt(j)-'0');
  37. }
  38. return res*sign;
  39. }
  40. }

JZ50:数组中重复的数字

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
分析:
方法一:哈希表 / Set
利用数据结构特点,容易想到使用哈希表(Set)记录数组的各个数字,当查找到重复数字则直接返回。
算法流程

  1. 初始化: 新建 HashSet ,记为 dic ;
  2. 遍历数组 nums 中的每个数字 num :
    1. 当 num 在 dic 中,说明重复,直接返回 num ;
    2. 将 num 添加至 dic 中;
  3. 返回 −1 。本题中一定有重复数字,因此这里返回多少都可以。

复杂度分析:
时间复杂度 O(N) : 遍历数组使用 O(N) ,HashSet 添加与查找元素皆为 O(1) 。
空间复杂度 O(N) : HashSet 占用 O(N) 大小的额外空间。
方法二:原地交换
题目说明尚未被充分使用,即 在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内 。 此说明含义:数组元素的 索引 和 值 是 一对多 的关系。
因此,可遍历数组并通过交换操作,使元素的 索引 与 值 一一对应(即 nums[i]=i )。因而,就能通过索引映射对应的值,起到与字典等价的作用。
遍历中,第一次遇到数字 x 时,将其交换至索引 x 处;而当第二次遇到数字 x 时,一定有 nums[x] = x,此时即可得到一组重复数字。
算法流程:
遍历数组 nums ,设索引初始值为 i=0 :

  1. 1. nums[i]=i 说明此数字已在对应索引位置,无需交换,因此跳过;
  2. 1. nums[nums[i]]=nums[i] 代表索引 nums[i] 处和索引 i 处的元素值都为nums[i] ,即找到一组重复值,返回此值 nums[i]
  3. 1. 否则: 交换索引为i nums[i] 的元素值,将此数字交换至对应索引位置。

若遍历完毕尚未返回,则返回−1 。
复杂度分析:

  1. - 时间复杂度 O(N) 遍历数组使用 O(N) ,每轮遍历的判断和交换操作使用 O(1)
  2. - 空间复杂度 O(1) 使用常数复杂度的额外空间。

思路:
知识点:
代码实现:

  1. //方式1:HashSet
  2. class Solution {
  3. public int findRepeatNumber(int[] nums) {
  4. HashSet<Integer> set=new HashSet<>();
  5. for(int num:nums){
  6. if(set.contains(num)) return num;
  7. set.add(num);
  8. }
  9. return -1;
  10. }
  11. }
  12. //方式2:利用题目给出的索引值与值是多对一的关系来解题
  13. class Solution {
  14. public int findRepeatNumber(int[] nums) {
  15. int i=0,len=nums.length;
  16. while(i<len){//循环数组
  17. if(nums[i]==i){//如果索引值=值,则判断下一索引处
  18. i++;
  19. continue;
  20. }
  21. if(nums[i]==nums[nums[i]]) return nums[i];//判断出已经存在一个对应值
  22. int temp=nums[i];//交换两个位置处的值
  23. nums[i]=nums[temp];//这里必须使用nums[temp],不然会导致值丢失
  24. nums[temp]=temp;
  25. }
  26. return -1;
  27. }
  28. }
  29. //方式2的另一种写法(仅仅是换了循环的写法)
  30. class Solution {
  31. public int findRepeatNumber(int[] nums) {
  32. for(int i=0;i<nums.length;i++){
  33. if(nums[i]==i) continue;
  34. else{
  35. if(nums[i]==nums[nums[i]]) return nums[i];
  36. else{
  37. int temp=nums[i];
  38. nums[i]=nums[temp];
  39. nums[temp]=temp;
  40. }
  41. }
  42. }
  43. return -1;
  44. }
  45. }

补充:牛客网上的题目为
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中第一个重复的数字。 例如,如果输入长度为7的数组[2,3,1,0,2,5,3],那么对应的输出是第一个重复的数字2。没有重复的数字返回-1。

  1. 目前只想到HashSet的解法!

JZ51:构建乘积数组

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。
分析:
本题的难点在于 不能使用除法 ,即需要 只用乘法 生成数组 B 。根据题目对 B[i] 的定义,可列表格,如下图所示。根据表格的主对角线(全为 1 ),可将表格分为 上三角 和 下三角 两部分。分别迭代计算下三角和上三角两部分的乘积,即可 不使用除法 就获得结果。
image.png
思路:
算法流程:

  1. 初始化:数组 B ,其中 B[0]=1 ;辅助变量 tmp=1 ;
  2. 计算 B[i] 的 下三角 各元素的乘积,直接乘入 B[i] ;
  3. 计算 B[i] 的 上三角 各元素的乘积,记为 tmp ,并乘入 B[i] ;
  4. 返回 BB 。

复杂度分析

  • 时间复杂度 O(N) : 其中 N 为数组长度,两轮遍历数组 a ,使用 O(N) 时间。
  • 空间复杂度 O(1) : 变量 tmp 使用常数大小额外空间(数组 b 作为返回值,不计入复杂度考虑)。

知识点:
代码实现:

  1. class Solution {
  2. public int[] constructArr(int[] a) {
  3. if(a.length==0) return new int[0];//返回空数组 []
  4. int[] b=new int[a.length];
  5. b[0]=1;//设b[0]的初始值,方便后面迭代
  6. for(int i=1;i<a.length;i++){//i是b数组的下标(从上往下,方便后面的计算利用前面的值)
  7. b[i]=b[i-1]*a[i-1];//下三角矩阵的计算
  8. }
  9. int temp=1;
  10. for(int i=a.length-2;i>=0;i--){//i也是b数组的下标(从下往上,方便后面的计算利用前面的值)
  11. temp*=a[i+1];//上三角矩阵的计算
  12. b[i]*=temp;
  13. }
  14. return b;
  15. }
  16. }

JZ52:正则表达式匹配

请实现一个函数用来匹配包括’.’和’‘的正则表达式。模式中的字符’.’表示任意一个字符,而’‘表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串”aaa”与模式”a.a”和”abaca”匹配,但是与”aa.a”和”aba”均不匹配。
分析:
用有限状态转移来解决这道问题
设 s 的长度为 n , p 的长度为 m ;将 s 的第 i 个字符记为 剑指offer记录 - 图42,p 的第 j 个字符记为 剑指offer记录 - 图43,将 s 的前 i 个字符组成的子字符串记为 s[:i] , 同理将 p 的前 j 个字符组成的子字符串记为 p[:j] 。因此,本题可转化为求 s[:n] 是否能和 p[:m] 匹配。
总体思路是从 s[:1] 和 p[:1] 是否能匹配开始判断,每轮添加一个字符并判断是否能匹配,直至添加完整个字符串 s 和 p 。展开来看,假设 s[:i] 与 p[:j] 可以匹配,那么下一状态有两种:
添加一个字符 剑指offer记录 - 图44 后是否能匹配?
添加字符 剑指offer记录 - 图45后是否能匹配?
因此,本题的状态共有 m×n 种,应定义状态矩阵 dp ,dp[i][j] 代表 s[:i] 与 p[:j] 是否可以匹配。做好状态定义,接下来就是根据 「普通字符」 , 「.」 , 「
」三种字符的功能定义,分析出动态规划的转移方程。
思路:
动态规划解析:
状态定义: 设动态规划矩阵 dp , dp[i][j] 代表字符串 s 的前 i 个字符和 p 的前 j 个字符能否匹配。
转移方程: 需要注意,由于 dp[0][0] 代表的是空字符的状态, 因此 dp[i][j] 对应的添加字符是 s[i - 1] 和 p[j - 1] 。

  • 当 p[j - 1] = ‘*’ 时, dp[i][j] 在当以下任一情况为 true 时等于 true :
    1. - dp[i][j - 2]: 即将字符组合 p[j - 2] * 看作出现 0 次时,能否匹配;
    2. - dp[i - 1][j] s[i - 1] = p[j - 2]: 即让字符 p[j - 2] 多出现 1 次时,能否匹配;
    3. - dp[i - 1][j] p[j - 2] = '.': 即让字符 '.' 多出现 1 次时,能否匹配;
  • 当 p[j - 1] != ‘*’ 时, dp[i][j] 在当以下任一情况为 true 时等于 true :
    1. - dp[i - 1][j - 1] s[i - 1] = p[j - 1]: 即让字符 p[j - 1] 多出现一次时,能否匹配;
    2. - dp[i - 1][j - 1] p[j - 1] = '.' 即将字符 . 看作字符 s[i - 1] 时,能否匹配;

初始化: 需要先初始化 dp 矩阵首行,以避免状态转移时索引越界。

  • dp[0][0] = true: 代表两个空字符串能够匹配。
  • dp[0][j] = dp[0][j - 2] 且 p[j - 1] = ‘‘: 首行 s 为空字符串,因此当 p 的偶数位为 时才能够匹配(即让 p 的奇数位出现 0 次,保持 p 是空字符串)。因此,循环遍历字符串 p ,步长为 2(即只看偶数位)。

返回值: dp 矩阵右下角字符,代表字符串 s 和 p 能否匹配。
image.png
复杂度分析:

  • 时间复杂度 O(MN) : 其中 M,N 分别为 s 和 p 的长度,状态转移需遍历整个 dp 矩阵。
  • 空间复杂度 O(MN) : 状态矩阵 dp 使用 O(MN) 的额外空间。

知识点:
代码实现:

  1. class Solution {
  2. public boolean isMatch(String s, String p) {
  3. int m=s.length()+1,n=p.length()+1;
  4. boolean[][] dp=new boolean[m][n];
  5. dp[0][0]=true;
  6. for(int i=2;i<n;i+=2){
  7. dp[0][i]=dp[0][i-2]&&p.charAt(i-1)=='*';//判断矩阵首行的比较结果,同样首列的结果也是固定的
  8. }
  9. //因为*前面至少要有一个数,所以在p串中至少在第二位,所以 j-2 不会越界,同样的, i-1 也不会越界
  10. for(int i=1;i<m;i++){
  11. for(int j=1;j<n;j++){
  12. dp[i][j]=p.charAt(j-1)=='*'?
  13. (dp[i][j-2])||(dp[i-1][j]&&s.charAt(i-1)==p.charAt(j-2))||(dp[i-1][j]&&p.charAt(j-2)=='.'):
  14. (dp[i-1][j-1]&&s.charAt(i-1)==p.charAt(j-1))||(dp[i-1][j-1]&&p.charAt(j-1)=='.');
  15. }
  16. }
  17. return dp[m-1][n-1];
  18. }
  19. }

JZ53:表示数值的字符串

请实现一个函数用来判断字符串是否表示数值。例如,字符”+100”、”5e2”、”-123”、”3.1416”、”-1E-16”、”0123”都表示数值,但”12e”、”1a3.14”、”1.2.3”、”+-5”及”12e+5.4”都不是。
分析:
本题使用有限状态自动机。根据字符类型合法数值的特点先定义状态,再画出状态转移图,最后编写代码即可。
字符类型:
空格 「 」、数字「 0—9 」 、正负号 「 +− 」 、小数点 「 . 」 、幂符号 「 e或E 」 。
状态定义:
按照字符串从左到右的顺序,定义以下 9 种状态。
0.开始的空格
1.幂符号前的正负号
2.小数点前的数字
3.小数点、小数点后的数字( 包含小数点 + 小数点后的数字 )
4.当小数点前为空格或符号时,小数点
5.幂符号
6.幂符号后的正负号
7.幂符号后的数字
8.结尾的空格
结束状态:
合法的结束状态有 2, 3, 7, 8 。
image.png
思路:
算法流程:

  1. 初始化:
    1. 状态转移表 states : 设 states[i] ,其中 i 为所处状态, states[i] 使用哈希表存储可转移至的状态。键值对 (key,value) 含义:若输入 key ,则可从状态 i 转移至状态 value 。
    2. 当前状态 p : 起始状态初始化为 p=0 。
  2. 状态转移循环: 遍历字符串 s 的每个字符 c 。
    1. 记录字符类型 t : 分为四种情况。
      • 当 c 为正负号时,执行 t = ‘s’ ;
      • 当 c 为数字时,执行 t = ‘d’ ;
      • 当 c 为 e , E 时,执行 t = ‘e’ ;
      • 当 c 为 . 和 空格 时,执行 t = c (即用字符本身表示字符类型);
      • 否则,执行 t = ‘?’ ,代表为不属于判断范围的非法字符,后续直接返回 false 。
    2. 终止条件: 若字符类型 t 不在哈希表 states[p] 中,说明无法转移至下一状态,因此直接返回 False 。
    3. 状态转移: 状态 p 转移至 states[p][t] 。
  3. 返回值: 跳出循环后,若状态 p∈2,3,7,8 ,说明结尾合法,返回 True ,否则返回 False 。

复杂度分析:

  • 时间复杂度 O(N) : 其中 N 为字符串 s 的长度,判断需遍历字符串,每轮状态转移的使用 O(1) 时间。
  • 空间复杂度 O(1) : states 和 p 使用常数大小的额外空间。

我的状态转移定义:
0.开始的空格
1.幂符号之前的正负号
2.小数点前的数字
3.小数点前有数字的小数点
4.小数点后的数字
5.小数点前无数字(为空格或者符号)的小数点
6.幂符号
7.幂符号后的正负号
8.幂符号后的数字
9.结尾的空格
知识点:
1.new HashMap<>(){{put(“s”,7);put(“d”, 8);}}, 是map.put(“s”,7);的文艺写法
代码实现:

  1. //方式1:K神的状态转移(特殊的是:状态3是包含两个字符了类型的( 包含小数点 + 小数点后的数字 ))
  2. class Solution {
  3. public boolean isNumber(String s) {
  4. Map[] states = {
  5. new HashMap<>() {{ put(' ', 0); put('s', 1); put('d', 2); put('.', 4); }}, // 0.
  6. new HashMap<>() {{ put('d', 2); put('.', 4); }}, // 1.
  7. new HashMap<>() {{ put('d', 2); put('.', 3); put('e', 5); put(' ', 8); }}, // 2.
  8. new HashMap<>() {{ put('d', 3); put('e', 5); put(' ', 8); }}, // 3.
  9. new HashMap<>() {{ put('d', 3); }}, // 4.
  10. new HashMap<>() {{ put('s', 6); put('d', 7); }}, // 5.
  11. new HashMap<>() {{ put('d', 7); }}, // 6.
  12. new HashMap<>() {{ put('d', 7); put(' ', 8); }}, // 7.
  13. new HashMap<>() {{ put(' ', 8); }} // 8.
  14. };
  15. int p = 0;
  16. char t;
  17. for(char c : s.toCharArray()) {
  18. if(c >= '0' && c <= '9') t = 'd';
  19. else if(c == '+' || c == '-') t = 's';
  20. else if(c == 'e' || c == 'E') t = 'e';
  21. else if(c == '.' || c == ' ') t = c;
  22. else t = '?';
  23. if(!states[p].containsKey(t)) return false;
  24. p = (int)states[p].get(t);
  25. }
  26. return p == 2 || p == 3 || p == 7 || p == 8;
  27. }
  28. }
  29. //方式2:我自己的状态转移
  30. class Solution {
  31. public boolean isNumber(String s) {
  32. Map[] status=new Map[]{
  33. new HashMap<String,Integer>(){{put("null",0);put("s", 1);put("d", 2);put(".", 5);}},//0
  34. new HashMap<String,Integer>(){{put("d",2);put(".", 5);}},//1
  35. new HashMap<String,Integer>(){{put("d", 2);put(".",3);put("e", 6);put("null",9);}},//2
  36. new HashMap<String,Integer>(){{put("d", 4);put("e", 6);put("null",9);}},//3
  37. new HashMap<String,Integer>(){{put("d", 4);put("e", 6);put("null",9);}},//4
  38. new HashMap<String,Integer>(){{put("d", 4);}},//5
  39. new HashMap<String,Integer>(){{put("s",7);put("d", 8);}},//6
  40. new HashMap<String,Integer>(){{put("d", 8);}},//7
  41. new HashMap<String,Integer>(){{put("d", 8);put("null", 9);}},//8
  42. new HashMap<String,Integer>(){{put("null", 9);}},//9
  43. };
  44. int p=0;//初始状态
  45. String t="";
  46. for(char ch:s.toCharArray()){
  47. if(ch>='0'&&ch<='9') t="d";
  48. else if(ch=='+'||ch=='-') t="s";
  49. else if(ch=='.') t=".";
  50. else if(ch=='e'||ch=='E') t="e";
  51. else if(ch==' ') t="null";
  52. else t="?";
  53. if(!status[p].containsKey(t)) return false;
  54. p=(int)status[p].get(t);
  55. }
  56. return p==2||p==3||p==4||p==8||p==9;
  57. }
  58. }

JZ54:字符流中第一个不重复的字符

字符流中第一个不重复的字符:请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符”go”时,第一个只出现一次的字符是”g”。当从该字符流中读出前六个字符”google”时,第一个只出现一次的字符是”l”。
分析:
方法:哈希+队列
针对题目的描述,我们先提出两个问题?对于一道题,如果没有思路,就要针对题目给自己问问题。然后针对问题,来考虑需要什么样的算法或者数据结构。
Q1. 给定一个字符串(只不过这里的字符串是可变的),如果快速判断一个字符是否存在于字符串中,如果存在,也就是重复?
Q2. 这里先不考虑重复,如果快速返回第一个字符?有没有感觉有点像先来先服务?
A1:对于“重复问题”,惯性思维应该想到哈希或者set。对于“字符串问题”,大多会用到哈希。因此一结合,应该可以想到,判断一个字符是否重复,可以选择用哈希,在java中,可以选择用HashMap
A2:对于字符流,源源不断的往池子中添加字符,然后还要返回第一个满足什么条件的字符,显然涉及到了“顺序”,也就是先来的先服务,这种先进先出的数据结构不就是队列。因此,这里可以用队列。
假如你已经知道了要用HashMap 和 Queue这两个数据结构,你可以试着自己想一想,接下来的算法过程是怎么样的?
算法过程:

  1. 初始化 HashMap 和队列 LinkedList

    HashMap map=new HashMap<>();
    LinkedList queue=new LinkedList<>();

  2. 对于Insert(char ch)操作, 如果ch是第一次出现,则添加到 queue 中,如果不是第一次出现,没必要添加到q中;然后在mp中更新一下ch是否出现过,之后要根据布尔值来判断是否重复。

  3. 对于FirstAppearingOnce()操作,我们直接判断 queue 的头部,然后在 map 中检查一下,是否重复,如果没有重复,那就是我们想要的数据。否则,如果重复了,那就应该移除头部,然后判断下一个头部是否满足要求。

复杂度分析:
时间复杂度:对于Insert(char ch)操作,为O(1), 对于FirstAppearingOnce()操作,为O(N),因为最坏情况下,队列中存入一半的重复数据, 比如“abcdabcd”,队列会存入“abcd”,并且弹出的时候都是重复的。
空间复杂度:O(N),用了LinkedList和HashMap两种数据结构,占用空间为O(N)
思路:
知识点:
1.关于 LinkedList 中的方法:
public E peek():list(队列)中的第一个元素,只查看不删除
public E poll():取出并删除队列的第一个元素
public E remove():删除队列第一个元素
代码实现:

  1. //方式一:哈希+队列
  2. public class Solution {
  3. LinkedList<Character> queue=new LinkedList<>();//记录单个字符
  4. HashMap<Character,Boolean> map=new HashMap<>();//记录字符是否重复出现
  5. public void Insert(char ch){
  6. if(!map.containsKey(ch)){
  7. queue.add(ch);
  8. }
  9. map.put(ch,map.containsKey(ch));
  10. }
  11. public char FirstAppearingOnce(){
  12. while(queue.peek()!=null){
  13. char ch=queue.peek();//这里不能用poll();因为在查看的同时会删掉峰值,而此题是流动的字符串,不能把之前出现过的单个字符删除掉
  14. if(map.get(ch)==false)
  15. return ch;
  16. else
  17. queue.remove();
  18. }
  19. return '#';
  20. }
  21. }
  22. //使用带插入顺序的哈希Map:LinkedHashMap (时间复杂度和空间复杂度不变)
  23. public class Solution {
  24. LinkedHashMap<Character,Boolean> map=new LinkedHashMap<>();
  25. public void Insert(char ch){
  26. map.put(ch,map.containsKey(ch));
  27. }
  28. public char FirstAppearingOnce(){
  29. for(Map.Entry<Character,Boolean> entry:map.entrySet()){
  30. if(entry.getValue()==false)
  31. return entry.getKey();
  32. }
  33. return '#';
  34. }
  35. }

JZ55:链表中环的入口节点

链表中环的入口节点:给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
分析:
思路:
方法一:哈希法

  1. 遍历单链表的每个结点
  2. 如果当前结点地址没有出现在set中,则存入set中
  3. 否则,出现在set中,则当前结点就是环的入口结点
  4. 整个单链表遍历完,若没出现在set中,则不存在环代码

复杂度分析:

  • 时间复杂度:O(n),while循环的时间复杂度就为O(N)
  • 空间复杂度:O(n),ArrayList的空间复杂度为O(N);最坏情况下,单链表的所有结点都在存入list中

方法二:双指针法
若不用辅助结构set,该怎么做呢?这里画了一张图

  1. 初始化:快指针fast指向头结点, 慢指针slow指向头结点
  2. 让fast一次走两步, slow一次走一步,第一次相遇在p处,停止
  3. 然后让fast指向头结点,slow原地不动,然后fast,slow每次走一步,当再次相遇,就是入口结点。

剑指offer记录 - 图48
在第一次相遇时,low指针走过的路为 a+b ;fast 指针走过的路为 a+b+n(b+c); (b+c)为环的长度Y,
fast指针的速度是slow指针速度的2倍,可以知道
2(a+b)=a+b+n(b+c)
n=1;a=c
n=2;a=b+2c=1Y+c
n=3;a=2b+3c=2Y+c
n=4;a=3b+4c=3Y+c
则可以让 fast 走 a 长度;low走过多个整圈数(多个整个Y)+c ;则可以相遇在环开始的地方
复杂度分析:
时间复杂度:O(n),while循环的时间复杂度为O(N)
空间复杂度:O(1),只用了几个ListNode类型的变量(即快慢指针)
知识点:
代码实现:

  1. //方式1:哈希法
  2. import java.util.*;
  3. public class Solution {
  4. public ListNode EntryNodeOfLoop(ListNode pHead) {
  5. ListNode ph=pHead;
  6. ArrayList<ListNode> nodeList=new ArrayList<>();
  7. while(ph!=null){
  8. if(!nodeList.contains(ph)){
  9. nodeList.add(ph);
  10. ph=ph.next;
  11. }else{
  12. return ph;
  13. }
  14. }
  15. return null;
  16. }
  17. }
  18. //方式2:双指针(快慢指针的用法)
  19. import java.util.*;
  20. public class Solution {
  21. public ListNode EntryNodeOfLoop(ListNode pHead) {
  22. ListNode fast=pHead;//快指针
  23. ListNode slow=pHead;//慢指针
  24. while(fast!=null&&fast.next!=null){//如果是单向链表就跳出
  25. fast=fast.next.next;//此处不是空指针
  26. slow=slow.next;
  27. if(fast==slow) break;//有环形链表在此刻跳出
  28. }
  29. if(fast==null||fast.next==null) return null;//单向链表无环形,对用while循环的结束条件
  30. fast=pHead;//快指针重新指向头节点
  31. while(fast!=slow){//快慢指针再次相遇就是环形链表的头结点
  32. fast=fast.next;
  33. slow=slow.next;
  34. }
  35. return fast;
  36. }
  37. }

JZ56:删除链表中重复的节点

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
分析:
方法一:使用set,暴力解法
根据题意,显然如果能够知道重复的值是什么,然后再遍历一次单链表,删除重复值即可。
找重复值的具体步骤:
1.初始化:set st
2. 遍历单链表相邻两个元素,如果相等,就加入到set当中
3. 直到单链表遍历完

删除重复值的具体步骤:
1.初始化:pre指针指向重复值的前一个节点,cur指向当前遍历的节点值
2.遍历单链表当前元素,然后再set中检查,如果是重复值,就删除,pre->next = cur->next
3. 否则,不是重复值,pre = pre->next, cur = cur->next
4. 知道单链表遍历完
复杂度分析:

  • 时间复杂度O(N):遍历了2次单链表
  • 空间复杂度O(N):HashSet的空间复杂度为O(N)

方法二:直接删除法
根据方法一,可以优化,在遍历单链表的时候,检查当前节点与下一点是否为相同值,如果相同,继续查找祥同值的最大长度,然后指针改变指向。
复杂度分析:

  • 时间复杂度O(N):循环的时间复杂度
  • 空间复杂度O(1):只是变量用到了额外的空间

思路:
知识点:
代码实现:

  1. //方法一:使用set,暴力解法
  2. import java.util.*;
  3. public class Solution {
  4. public ListNode deleteDuplication(ListNode pHead) {
  5. if(pHead==null) return null;
  6. ListNode pre=pHead;//前一个指针
  7. ListNode cur=pHead.next;//当前指针
  8. HashSet<Integer> set=new HashSet<>();
  9. while(cur!=null){//在set中存下所有重复的节点的值
  10. if(pre.val==cur.val){
  11. set.add(pre.val);
  12. }
  13. pre=pre.next;
  14. cur=cur.next;
  15. }
  16. ListNode newHead=new ListNode(-1);//设置哨兵,方便删除链表前两个节点就重复的情况
  17. newHead.next=pHead;
  18. pre=newHead;
  19. cur=pHead;
  20. while(cur!=null){
  21. if(set.contains(cur.val)){
  22. cur=cur.next;//每次跳过是重复值的一个节点
  23. pre.next=cur;
  24. // pre.next=cur.next;//此处不能这样用,会造成死循环;因为这样cur指针就从未移动过,所以死循环
  25. }else{
  26. pre=pre.next;
  27. cur=cur.next;
  28. }
  29. }
  30. return newHead.next;
  31. }
  32. }
  33. //方法二:直接删除法
  34. public class Solution {
  35. public ListNode deleteDuplication(ListNode pHead) {
  36. if(pHead==null) return null;
  37. ListNode newHead=new ListNode(-1);//设置哨兵,解决原链表前几个节点就有重复值的情况
  38. newHead.next=pHead;//把加入了哨兵节点的新链表连接起来
  39. ListNode pre=newHead;//设置两个指针比较节点值
  40. ListNode cur=pHead;
  41. while(cur!=null){
  42. if(cur.next!=null&&cur.val==cur.next.val){
  43. cur=cur.next;
  44. while(cur.next!=null&&cur.val==cur.next.val){
  45. cur=cur.next;
  46. }//这个循环跳出时,cur正指向一串重复值的最后一个节点
  47. cur=cur.next;//把cur移动到最后一个重复值的后一个节点那去
  48. pre.next=cur;//去掉重复的那串节点
  49. }else{
  50. pre=pre.next;
  51. cur=cur.next;
  52. }
  53. }
  54. return newHead.next;
  55. }
  56. }

JZ57:二叉树的下一个节点

给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
分析:
Q1:首先问你一个问题,如果这道题出现在笔试题中,你会用什么方法做?如果出现在面试题中呢?
A1:我想你肯定有点疑惑,同一道题为什么还分出现在笔试题中还是面试题中呢?很显然,笔试题中只要能过就好,设计的算法丑点,慢点也无所畏,不一定需要最优解法,当然前提是能够通过。而面试中就不一样了,显然面试官希望听到最优解法。
思路:
方法一:暴力解法
如果在笔试题中看到这道题,直接模拟题意就好了。题意需要找到某个结点中序遍历的下一个结点,那我们的做法很显然可以这样:

  1. 根据给出的结点求出整棵树的根节点 root
  2. 根据根节点递归求出树的中序遍历,存入列表 list 中
  3. 在 list 中查找当前结点,则当前结点的下一结点即为所求。

虽然有点暴力,但是时间复杂度也是线性的,第一步:最坏为O(N), N为整棵树结点的个数。第二步:O(N), 第三步:最坏为O(N),
所以整的时间复杂度:3O(N);时间复杂度还可以接受,关键是思路好想并且每一步的代码都很简单。
*复杂度分析:

  • 时间复杂度O(N):两次循环和中序遍历的时间复杂度均为O(N)
  • 空间复杂度O(N):用到了 ArrayList 这个数据结构,占用额外空间 O(N)

方法二:最优解法
但是,如果在面试中,方法一肯定上不了台面。但是最优解法该怎么去想呢?想不出来就画图分析,举个中序遍历的图:如下:
剑指offer记录 - 图49
红色数字是中序遍历的顺序。接下来,我们就假设,如果当前结点分别是1,2,3,4,5,6,7,下一结点看有什么规律没?
1 => 2 // 显然下一结点是 1 的父亲结点
2 => 3 // 下一节点是当前结点右孩子的左孩子结点,其实你也应该想到了,应该是一直到左孩子为空的那个结点
3 => 4 // 跟 2 的情况相似,当前结点右孩子结点的左孩子为空的那个结点
4 => 5 // 5 是父亲结点 3 的父亲结点,发现和1有点像,因为 1,3,同样是父亲结点的左孩子
5 => 6 // 跟 4=>5 一样的道理
6 => 7 // 跟 3=>4 一样的道理
7 => null // 因为属于最尾结点
此时,可以总结一下:
[1] 是一类:特点:当前结点是父亲结点的左孩子
[2 3 6] 是一类,特点:当前结点右孩子结点,那么下一节点就是:右孩子结点的最左孩子结点;如果右孩子结点没有左孩子就是自己
[4 5]是一类,特点:当前结点为父亲结点的右孩子结点,本质还是[1]那一类
[7]是一类,特点:最尾结点
当遇到不会的题,可以根据题意画图,分析,分析方法是关键。
可以把中序下一结点归为几种类型:

  1. 有右子树,下一结点是右子树中的最左结点,例如 2,下一结点是 3
  2. 无右子树,且结点是该结点父结点的左子树,则下一结点是该结点的父结点,例如 1,下一结点是 2
  3. 无右子树,且结点是该结点父结点的右子树,则我们一直沿着父结点追朔,直到找到某个结点是其父结点的左子树,如果存在这样的结点,那么这个结点的父结点就是我们要找的下一结点。例如 4,下一结点是 5;
  4. 没有下一节点的情况:例如 7,并没有符合情况的结点,所以 7 没有下一结点

复杂度分析:

  • 时间复杂度O(N):最坏情况下为O(N)
  • 空间复杂度O(1):

知识点:
代码实现:

  1. //方式1:
  2. import java.util.*;
  3. //中序遍历:左——中——右
  4. public class Solution {
  5. public TreeLinkNode GetNext(TreeLinkNode pNode) {
  6. TreeLinkNode root=null;
  7. TreeLinkNode temp=pNode;
  8. while(temp!=null){//利用当前节点找到根节点
  9. root=temp;
  10. temp=temp.next;
  11. }
  12. ArrayList<TreeLinkNode> res=new ArrayList<>();//存放所有节点的链表
  13. middleReverse(root,res);
  14. int len=res.size();
  15. for(int i=0;i<len;i++){
  16. if(res.get(i)==pNode&&(i+1)!=len){//找到和给定节点相等的节点(并且不是中序遍历的最后一个节点)
  17. return res.get(i+1);
  18. }
  19. }
  20. return null;
  21. }
  22. //用中序遍历出所有节点,并存储在list列表中
  23. private void middleReverse(TreeLinkNode node,ArrayList<TreeLinkNode> res){
  24. if(node==null) return;
  25. middleReverse(node.left,res);
  26. res.add(node);
  27. middleReverse(node.right,res);
  28. }
  29. }
  30. //方式2:最优解法
  31. public class Solution {
  32. public TreeLinkNode GetNext(TreeLinkNode pNode) {
  33. if(pNode==null) return null;//当前节点为null,则直接返回
  34. TreeLinkNode pN=pNode;//不改动给的参数
  35. if(pN.right!=null){//2、3、6类型(特点:当前结点右孩子结点,那么下一节点就是:右孩子结点的最左孩子结点,如果右孩子结点没有左孩子就是自己)
  36. pN=pN.right;
  37. while(pN.left!=null){
  38. pN=pN.left;
  39. }//跳出循环时pN.left==null(已经找到了左节点是null的那个节点)
  40. return pN;
  41. }
  42. while(pN.next!=null){//1、4、5类型(当前结点为父亲结点的左孩子或者右孩子结点)
  43. TreeLinkNode root=pN.next;
  44. if(root.left==pN){
  45. return root;
  46. }
  47. pN=pN.next;
  48. }
  49. return null;//7类型
  50. }
  51. }
  52. //方式3:方式2的另一种写法(把所有节点总结为4种情况求解)
  53. public class Solution {
  54. public TreeLinkNode GetNext(TreeLinkNode pNode) {
  55. if(pNode==null) return null;//当前节点为null,则直接返回
  56. TreeLinkNode pN=pNode;//不改动给的参数
  57. //1、有右子树,下一结点是右子树中的最左结点
  58. if(pN.right!=null){
  59. pN=pN.right;
  60. while(pN.left!=null){
  61. pN=pN.left;
  62. }//跳出循环时pN.left=-null
  63. return pN;
  64. }
  65. //2、无右子树,且结点是该结点父结点的左子树
  66. if(pN.next!=null&&pN.next.left==pN){
  67. return pN.next;
  68. }
  69. //3、无右子树,且结点是该结点父结点的右子树
  70. if(pN.next!=null&&pN.next.right==pN){
  71. TreeLinkNode root=pN.next;//当前节点的父节点
  72. while(root.next!=null&&root.next.right==root){
  73. root=root.next;
  74. }//跳出循环时找到了 root.next.left==root,找到某个结点是其父结点的左子树
  75. return root.next;
  76. }
  77. return null;//7类型
  78. }
  79. }

JZ58:对称的二叉树

请实现一个函数,用来判断一棵二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。
分析:
对称二叉树定义: 对于树中 任意两个对称节点L 和 R ,一定有:
L.val=R.val :即此两对称节点值相等。
L.left.val=R.right.val :即 L 的 左子节点 和 R 的 右子节点 对称;
L.right.val=R.left.val :即 L 的 右子节点 和 R 的 左子节点 对称。
根据以上规律,考虑从顶至底递归,判断每对节点是否对称,从而判断树是否为对称二叉树。
image.png
思路:
算法流程:
isSymmetric(root) :

  1. 1. 特例处理: 若根节点 root 为空,则直接返回 true
  2. 1. 返回值: recur(root.left, root.right) ;

recur(L, R) :
终止条件:

  1. - L R 同时越过叶节点: 此树从顶至底的节点都对称,因此返回 true
  2. - L R 中只有一个越过叶节点: 此树不对称,因此返回 false
  3. - 当节点 L != 节点 R 值: 此树不对称,因此返回 false

递推工作:
判断两节点 L.left 和 R.right 是否对称,即 recur(L.left, R.right) ;
判断两节点 L.right 和 R.left 是否对称,即 recur(L.right, R.left) ;
返回值:
两对节点都对称时,才是对称树,因此用与逻辑符 && 连接。
复杂度分析:

  • 时间复杂度 O(N) : 其中 N 为二叉树的节点数量,每次执行 recur() 可以判断一对节点是否对称,因此最多调用 N/2 次 recur(L, R) 方法。
  • 空间复杂度 O(N) : 最差情况下,二叉树退化为链表,系统使用 O(N) 大小的栈空间(函数的递归深度)。

知识点:
代码实现:

  1. class Solution {
  2. public boolean isSymmetric(TreeNode root) {
  3. return root == null ? true : recur(root.left, root.right);
  4. }
  5. private boolean recur(TreeNode node1,TreeNode node2){
  6. if(node1==null&&node2==null) return true;
  7. if(node1==null||node2==null||node1.val!=node2.val) return false;
  8. return recur(node1.left,node2.right)&&recur(node1.right,node2.left);
  9. }
  10. }

JZ59:JZ22-3:按之字形顺序打印二叉树

分析:
思路:
知识点:
代码实现:

  1. //方法一:层序遍历 + 队列
  2. public class Solution {
  3. public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
  4. LinkedList<TreeNode> nodeQueue=new LinkedList<>();//队列,存放每层的节点
  5. ArrayList<ArrayList<Integer>> res=new ArrayList<>();//结果
  6. if(pRoot!=null) nodeQueue.add(pRoot);//先把头结点加进队列
  7. while(!nodeQueue.isEmpty()){//队列为空则循环结束
  8. int len=nodeQueue.size();//根据队列里的元素来决定循环进行多少次的取值操作
  9. ArrayList<Integer> valList=new ArrayList<>();
  10. for(int i=0;i<len;i++){//取值的同时将下一层的节点加进来
  11. TreeNode node=nodeQueue.poll();
  12. if(res.size()%2==0) valList.add(node.val);//用res的长度来判断当前所处层数
  13. else valList.add(0,node.val);
  14. if(node.left!=null) nodeQueue.add(node.left);
  15. if(node.right!=null) nodeQueue.add(node.right);
  16. }
  17. res.add(valList);
  18. }
  19. return res;
  20. }
  21. }
  22. //方法三:层序遍历 + 倒序
  23. public class Solution {
  24. public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
  25. LinkedList<TreeNode> nodeQueue=new LinkedList<>();//队列,存放每层的节点
  26. ArrayList<ArrayList<Integer>> res=new ArrayList<>();//结果
  27. if(pRoot!=null) nodeQueue.add(pRoot);//先把头结点加进队列
  28. while(!nodeQueue.isEmpty()){//队列为空则循环结束
  29. int len=nodeQueue.size();//根据队列里的元素来决定循环进行多少次的取值操作
  30. ArrayList<Integer> valList=new ArrayList<>();
  31. for(int i=0;i<len;i++){//取值的同时将下一层的节点加进来
  32. TreeNode node=nodeQueue.poll();
  33. valList.add(node.val);
  34. if(node.left!=null) nodeQueue.add(node.left);
  35. if(node.right!=null) nodeQueue.add(node.right);
  36. }
  37. if(res.size()%2==1) Collections.reverse(valList);//仅仅是在奇数层做了翻转操作
  38. res.add(valList);
  39. }
  40. return res;
  41. }
  42. }

JZ60:JZ22-2:把二叉树打印成多行

分析:
思路:
知识点:
代码实现:

  1. //层序遍历+队列
  2. public class Solution {
  3. ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
  4. LinkedList<TreeNode> queue=new LinkedList<>();
  5. ArrayList<ArrayList<Integer>> res=new ArrayList<>();
  6. if(pRoot!=null) queue.add(pRoot);
  7. while(!queue.isEmpty()){
  8. ArrayList<Integer> temp=new ArrayList<>();
  9. for (int i=queue.size()-1;i>=0;i--){
  10. TreeNode node = queue.poll();
  11. temp.add(node.val);
  12. if(node.left!=null) queue.add(node.left);
  13. if(node.right!=null) queue.add(node.right);
  14. }
  15. res.add(temp);
  16. }
  17. return res;
  18. }
  19. }

JZ61:序列化二叉树

请实现两个函数,分别用来序列化和反序列化二叉树。
示例,你可以将以下二叉树:
1
/ \
2 3
/ \
4 5
序列化为 “[1,2,3,null,null,4,5]”
分析:
通常使用的前序、中序、后序、层序遍历记录的二叉树的信息不完整,即唯一的输出序列可能对应着多种二叉树可能性。题目要求的 序列化 和 反序列化 是 可逆操作 。因此,序列化的字符串应携带 完整的二叉树信息 。
观察题目示例,序列化的字符串实际上是二叉树的 “层序遍历”(BFS)结果,本文也采用层序遍历。
为完整表示二叉树,考虑将叶节点下的 null 也记录。在此基础上,对于列表中任意某节点 node ,其左子节点 node.left 和右子节点 node.right 在序列中的位置都是 唯一确定 的。如下图所示:
image.png
上图规律可总结为下表:
node.val node 的索引 node.left 的索引 node.right 的索引
1 0 1 2
2 1 3 4
3 2 5 6
4 5 7 8
5 6 9 10
设 m 为列表区间 [0,n] 中的 null 节点个数,则可总结出根节点、左子节点、右子节点的列表索引的递推公式:
node.val node 的列表索引 node.left 的列表索引 node.right 的列表索引
!=null n 2(n−m)+1 2(n−m)+2
== null nn 无 无
序列化 使用层序遍历实现。反序列化 通过以上递推公式反推各节点在序列中的索引,进而实现。
思路:
序列化 Serialize :借助队列,对二叉树做层序遍历,并将越过叶节点的 null 也打印出来。
算法流程:

  1. 特例处理: 若 root 为空,则直接返回空列表 “[]” ;
  2. 初始化: 队列 queue (包含根节点 root );序列化列表 res ;
  3. 层序遍历: 当 queue 为空时跳出;
    1. 节点出队,记为 node ;
    2. 若 node 不为空:① 打印字符串 node.val ,② 将左、右子节点加入 queue ;
    3. 否则(若 node 为空):打印字符串 “null” ;
  4. 返回值: 拼接列表,用 ‘,’ 隔开,首尾添加中括号;

复杂度分析:

  • 时间复杂度 O(N) : N 为二叉树的节点数,层序遍历需要访问所有节点,最差情况下需要访问 N+1 个 null ,总体复杂度为 O(2N+1)=O(N) 。
  • 空间复杂度 O(N) : 最差情况下,队列 queue 同时存储 剑指offer记录 - 图52个节点(或 N+1 个 null ),使用 O(N) ;列表 res 使用 O(N) 。

反序列化 Deserialize :基于本文开始推出的 node , node.left , node.right 在序列化列表中的位置关系,可实现反序列化。
利用队列按层构建二叉树,借助一个指针 i 指向节点 node 的左、右子节点,每构建一个 node 的左、右子节点,指针 i 就向右移动 1 位。
算法流程:

  1. 特例处理: 若 data 为空( data==”[]” ),直接返回 null ;
  2. 初始化: 序列化列表 vals (先去掉首尾中括号,再用逗号隔开),指针 i = 1 ,根节点 root (值为 vals[0] ),队列 queue(包含 root );
    1. 按层构建: 当 queue 为空时跳出;
    2. 节点出队,记为 node ;
    3. 构建 node 的左子节点:node.left 的值为 vals[i] ,并将 node.left 入队;
    4. 执行 i += 1 ;
    5. 构建 node 的右子节点:node.left 的值为 vals[i] ,并将 node.left 入队;
    6. 执行 i += 1 ;
  3. 返回值: 返回根节点 root 即可;

复杂度分析:

  • 时间复杂度 O(N) : N 为二叉树的节点数,按层构建二叉树需要遍历整个 vals ,其长度最大为 2N+1 。
  • 空间复杂度 O(N) : 最差情况下,队列 queue 同时存储剑指offer记录 - 图53个节点,因此使用 O(N) 额外空间。

知识点:
1.String的方法
public String substring(int beginIndex, int endIndex):截取指定区间的字符串,左开右闭
2.StringBuilder的方法
public StringBuffer deleteCharAt(int index):删除指定位置的字符
3.equals()的用法,判断是否相等
代码实现:

  1. public class Codec {
  2. public String serialize(TreeNode root) {//序列化二叉树
  3. if(root==null) return "[]";
  4. StringBuilder builder=new StringBuilder();
  5. builder.append("[");
  6. LinkedList<TreeNode> queue=new LinkedList<>();
  7. queue.add(root);
  8. while(!queue.isEmpty()){
  9. TreeNode node=queue.poll();
  10. if(node!=null){
  11. builder.append(node.val+",");
  12. queue.add(node.left);
  13. queue.add(node.right);
  14. }else{
  15. builder.append("null,");
  16. }
  17. }
  18. builder.deleteCharAt(builder.length()-1);
  19. builder.append("]");
  20. return builder.toString();
  21. }
  22. public TreeNode deserialize(String data) {//反序列化二叉树
  23. if(data.trim()=="[]") return null;
  24. String[] nodeVals=data.substring(1,data.length()-1).trim().split(",");
  25. TreeNode root=new TreeNode(Integer.parseInt(nodeVals[0].trim()));
  26. LinkedList<TreeNode> queue=new LinkedList<>();
  27. queue.add(root);
  28. int i=1;
  29. while(!queue.isEmpty()){
  30. TreeNode node=queue.poll();
  31. if(!"null".equals(nodeVals[i].trim())){
  32. node.left=new TreeNode(Integer.parseInt(nodeVals[i].trim()));
  33. queue.add(node.left);
  34. }
  35. i++;
  36. if(!"null".equals(nodeVals[i].trim())){
  37. node.right=new TreeNode(Integer.parseInt(nodeVals[i].trim()));
  38. queue.add(node.right);
  39. }
  40. i++;
  41. }
  42. return root;
  43. }
  44. }

补充:用队列来层级遍历二叉树bsf的代码

  1. ArrayList<Integer> bsf(TreeNode root){
  2. LinkedList<TreeNode> queue=new LinkedList<>();
  3. ArrayList<Integer> res=new ArrayList<>();
  4. queue.add(root);
  5. while (!queue.isEmpty()){
  6. TreeNode node = queue.poll();
  7. if(node!=null){
  8. res.add(node.val);
  9. queue.add(node.left);
  10. queue.add(node.right);
  11. }
  12. }
  13. return res;
  14. }

JZ62:二叉搜索树的第k个节点

给定一棵二叉搜索树,请找出其中第k大的节点。(二叉搜索树的第k大节点)
分析:
本文解法基于此性质:二叉搜索树的中序遍历为 递增序列 。
根据以上性质,易得二叉搜索树的 中序遍历倒序 为 递减序列 。因此,求 “二叉搜索树第 k 大的节点” 可转化为求 “此树的中序遍历倒序的第 k 个节点”。
中序遍历 为 “左、根、右” 顺序,递归法代码如下:

  1. // 打印中序遍历
  2. void dfs(TreeNode root) {
  3. if(root == null) return;
  4. dfs(root.left); // 左
  5. System.out.println(root.val); // 根
  6. dfs(root.right); // 右
  7. }

中序遍历的倒序 为 “右、根、左” 顺序,递归法代码如下:

  1. // 打印中序遍历倒序
  2. void dfs(TreeNode root) {
  3. if(root == null) return;
  4. dfs(root.right); // 右
  5. System.out.println(root.val); // 根
  6. dfs(root.left); // 左
  7. }

思路:
为求第 k 个节点,需要实现以下 三项工作

  1. 递归遍历时计数,统计当前节点的序号;
  2. 递归到第 k 个节点时,应记录结果 res ;
  3. 记录结果后,后续的遍历即失去意义,应提前终止(即返回)。

递归解析:

  1. 终止条件: 当节点 root 为空(越过叶节点),则直接返回;
  2. 递归右子树: 即 dfs(root.right) ;
  3. 三项工作:
    1. 提前返回: 若 k=0 ,代表已找到目标节点,无需继续遍历,因此直接返回;
    2. 统计序号: 执行 k=k−1 (即从 k 减至 0 );
    3. 记录结果: 若 k=0 ,代表当前节点为第 k 大的节点,因此记录 res=root.val ;
  4. 递归左子树: 即 dfs(root.left) ;

复杂度分析:

  • 时间复杂度 O(N) : 当树退化为链表时(全部为右子节点),无论 k 的值大小,递归深度都为 N ,占用 O(N) 时间。
  • 空间复杂度 O(N) : 当树退化为链表时(全部为右子节点),系统使用 O(N) 大小的栈空间(递归深度)。

知识点:
代码实现:

  1. //方式1:高效率解法 按中序遍历的倒序 右中左来进行遍历
  2. class Solution {
  3. int k,res;//形参k不能随着dfs的迭代而不断变化,为了记录迭代进程和结果,引入类变量k和res
  4. public int kthLargest(TreeNode root, int k) {
  5. this.k=k;//利用形参值k对类变量k进行初始化
  6. dsf(root);
  7. return res;//如果没有找到,则会返回1
  8. }
  9. private void dsf(TreeNode node){
  10. if(node==null) return;//当root为空或者已经找到了res时,直接返回
  11. dsf(node.right);//先右
  12. k--;//每遍历一个节点就减1
  13. if(k==0) {//如果k==0,则表示找到了这个值
  14. res=node.val;
  15. return;
  16. }
  17. dsf(node.left);//再左
  18. }
  19. }
  20. //方式2:低效率解法(增加了占用的空间和时间)
  21. class Solution {
  22. public int kthLargest(TreeNode root, int k) {
  23. if(root==null) return -1;
  24. ArrayList<Integer> resList=new ArrayList<>();
  25. middleReverse(root,resList);
  26. if(k>resList.size()|| k<1) return -1;
  27. return resList.get(k-1);
  28. }
  29. private void middleReverse(TreeNode node,ArrayList<Integer> resList){
  30. if(node==null) return;
  31. middleReverse(node.right,resList);
  32. resList.add(node.val);
  33. middleReverse(node.left,resList);
  34. }
  35. }

补充牛客上的题:给定一棵二叉搜索树,找出其中的第k小的TreeNode结点。(二叉搜索树的第K个节点)

  1. //解法:可以按中序遍历的顺序 左中右来进行遍历
  2. public class Solution {
  3. int k;
  4. TreeNode res;
  5. TreeNode KthNode(TreeNode pRoot, int k) {
  6. this.k=k;
  7. dsf(pRoot);
  8. return res;
  9. }
  10. private void dsf(TreeNode node){
  11. if(node==null) return;
  12. dsf(node.left);
  13. k--;
  14. if(k==0){
  15. res=node;
  16. return;
  17. }
  18. dsf(node.right);
  19. }
  20. }

JZ63:数据流中的中位数

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
分析:
给定一长度为 N 的无序数组,其中位数的计算方法:首先对数组执行排序(使用 O(NlogN) 时间),然后返回中间元素即可(使用 O(1) 时间)。
针对本题,根据以上思路,可以将数据流保存在一个列表中,并在添加元素时 保持数组有序 。此方法的时间复杂度为 O(N) ,其中包括: 查找元素插入位置 O(logN) (二分查找)、向数组某位置插入元素 O(N) (插入位置之后的元素都需要向后移动一位)。
借助 堆 可进一步优化时间复杂度。
建立一个 小顶堆 A 和 大顶堆 B ,各保存列表的一半元素,且规定:
A 保存 较大 的一半,长度为 剑指offer记录 - 图54( N 为偶数)或 剑指offer记录 - 图55 ( N 为奇数);
B 保存 较小 的一半,长度为剑指offer记录 - 图56( N 为偶数)或 剑指offer记录 - 图57 ( N 为奇数);
随后,中位数可仅根据 A,B 的堆顶元素计算得到。
image.png
思路:
算法流程:
设元素总数为 N=m+n ,其中 m 和 n 分别为 A 和 B 中的元素个数。
addNum(num) 函数:
情况1:当 m=n(即 N 为 偶数):需向 A 添加一个元素。实现方法:将新元素 num 插入至 B ,再将 B 堆顶元素插入至 A ;
情况2:当 m !=n(即 N 为 奇数,即m=n+1):需向 B 添加一个元素。实现方法:将新元素 num 插入至 A ,再将 A 堆顶元素插入至 B ;
假设插入数字 num 遇到情况 1:由于 num 可能属于 “较小的一半” (即属于 B ),因此不能将 num直接插入至 A 。而应先将 num 插入至 B ,再将 B 堆顶元素插入至 A 。这样就可以始终保持 A 保存较大一半、 B 保存较小一半。
findMedian() 函数:
当 m=n( N 为 偶数):则中位数为 ( A 的堆顶元素 + B 的堆顶元素 )/2。
当 m !=n(即 N 为 奇数,即m=n+1):则中位数为 A 的堆顶元素。
复杂度分析:

  • 时间复杂度:

查找中位数 O(1) : 获取堆顶元素使用 O(1) 时间;
添加数字 O(logN) : 堆的插入和弹出操作使用 O(logN) 时间。

  • 空间复杂度 O(N) : 其中 N 为数据流中的元素数量,小顶堆 A 和大顶堆 B 最多同时保存 N 个元素。

知识点:
代码实现:

  1. class MedianFinder {
  2. PriorityQueue<Integer> A;
  3. PriorityQueue<Integer> B;
  4. public MedianFinder() {
  5. A=new PriorityQueue<>((o1, o2)-> (o2-o1));//大顶堆的lamda表达式
  6. B=new PriorityQueue<>((o1,o2)-> (o1-o2));//小顶堆的表达式
  7. }
  8. public void addNum(int num) {//平衡两个堆
  9. if(A.size()==B.size()){
  10. B.add(num);
  11. A.add(B.poll());
  12. }else{
  13. A.add(num);
  14. B.add(A.poll());
  15. }
  16. }
  17. public double findMedian() {
  18. return A.size()==B.size()?(A.peek()+B.peek())/2.0:A.peek()*1.0;
  19. }
  20. }

JZ64:滑动窗口的最大值

分析:
设窗口区间为 [i,j] ,最大值为剑指offer记录 - 图59。当窗口向前移动一格,则区间变为 [i+1,j+1] ,即添加了 nums[j+1] ,删除了 nums[i] 。若只向窗口 [i,j] 右边添加数字 nums[j+1] ,则新窗口最大值可以 通过一次对比 使用 O(1) 时间得到,即:剑指offer记录 - 图60=max(剑指offer记录 - 图61 ,nums[j+1]);但是删除的 nums[i] 可能恰好是窗口内唯一的最大值 剑指offer记录 - 图62,因此不能通过以上方法计算 剑指offer记录 - 图63,而必须使用 O(j−i) 时间, 遍历整个窗口区间 获取最大值,即:
剑指offer记录 - 图64=max(nums(i+1),⋯,num(j+1))
根据以上分析,可得 暴力法 的时间复杂度为 O((n−k+1)k)≈O(nk) 。(设数组 nums 的长度为 n ,则共有 (n−k+1) 个窗口;获取每个窗口最大值需线性遍历,时间复杂度为 O(k) 。)
本题难点: 如何在每次窗口滑动后,将 “获取窗口内最大值” 的时间复杂度从 O(k)O(k) 降低至 O(1)O(1) 。
回忆 :使用 单调栈 实现了随意入栈出栈情况下的O(1) 时间获取 “栈内最小值” 。本题同理,不同点在于 “出栈操作” 删除的是 “列表尾部元素” ,而 “窗口滑动” 删除的是 “列表首部元素”
窗口对应的数据结构为 双端队列 ,本题使用 单调队列 即可解决以上问题。遍历数组时,每轮保证单调队列 deque :

  1. deque 内 仅包含窗口内的元素 ⇒ 每轮窗口滑动移除了元素 nums[i−1] ,需将 deque 内的对应元素一起删除。
  2. deque 内的元素 非严格递减 ⇒ 每轮窗口滑动添加了元素 nums[j+1] ,需将 deque 内所有 <nums[j+1] 的元素删除。

思路:
算法流程:

  1. 初始化: 双端队列 deque ,结果列表 res ,数组长度 n ;
  2. 滑动窗口: 左边界范围 i∈[1−k,n+1−k] ,右边界范围 j∈[0,n−1] ;
    1. 若 i>0 且 队首元素 deque[0] == 被删除元素 nums[i−1] :则队首元素出队;
    2. 删除 deque 内所有 <nums[j] 的元素,以保持 deque 递减;
    3. 将 nums[j] 添加至 deque 尾部;
    4. 若已形成窗口(即 i≥0 ):将窗口最大值(即队首元素 deque[0] )添加至列表 res 。
  3. 返回值: 返回结果列表 res 。

复杂度分析:

  • 时间复杂度 O(n) : 其中 n 为数组 nums 长度;线性遍历 nums 占用 O(N) ;每个元素最多仅入队和出队一次,因此单调队列 deque 占用 O(2N) 。
  • 空间复杂度 O(k) : 双端队列 deque 中最多同时存储k 个元素(即窗口大小)。

可以将 “未形成窗口” 和 “形成窗口后” 两个阶段拆分到两个循环里实现。代码虽变长,但减少了冗余的判断操作。
知识点:
代码实现:

  1. //方式一:双端队列+滑动窗口
  2. class Solution {
  3. public int[] maxSlidingWindow(int[] nums, int k) {
  4. if(nums.length==0||k==0||nums.length<k) return new int[0];
  5. int[] res=new int[nums.length-k+1];
  6. Deque<Integer> deque=new LinkedList<>();
  7. for(int i=0;i<k;i++){//形成滑动窗口之前
  8. while(!deque.isEmpty()&&deque.peekLast()<nums[i])
  9. deque.removeLast();//保持deque递减
  10. deque.addLast(nums[i]);
  11. }
  12. res[0]=deque.peekFirst();
  13. for(int i=k;i<nums.length;i++){
  14. if(deque.peekFirst()==nums[i-k])
  15. deque.removeFirst();//删除deque中对应的num[i-k]
  16. while(!deque.isEmpty()&&deque.peekLast()<nums[i])
  17. deque.removeLast();//保持deque递减
  18. deque.addLast(nums[i]);
  19. res[i-k+1]=deque.peekFirst();//记录窗口最大值
  20. }
  21. return res;
  22. }
  23. }

JZ65:矩阵中的路径

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 剑指offer记录 - 图65矩阵中包含一条字符串”bcced”的路径,但是矩阵中不包含”abcb”路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
分析:
本问题是典型的矩阵搜索问题,可使用 深度优先搜索(DFS)+ 剪枝 解决。
深度优先搜索: 可以理解为暴力法遍历矩阵中所有字符串可能性。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
剪枝: 在搜索中,遇到 这条路不可能和目标字符串匹配成功 的情况(例如:此矩阵元素和目标字符不同、此元素已被访问),则应立即返回,称之为 可行性剪枝 。
image.png
思路:
DFS 解析:

  1. 递归参数: 当前元素在矩阵 board 中的行列索引 i 和 j ,当前目标字符在 word 中的索引 k 。
  2. 终止条件:
    1. 返回 false : (1) 行或列索引越界 或 (2) 当前矩阵元素与目标字符不同 或 (3) 当前矩阵元素已访问过 ( (3) 可合并至 (2) ) 。
    2. 返回 true : k = len(word) - 1 ,即字符串 word 已全部匹配。
  3. 递推工作:
    1. 标记当前矩阵元素:将 board[i][j] 修改为 空字符 ‘\0’ ,代表此元素已访问过,防止之后搜索重复访问。
    2. 搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,使用 或 连接 (代表只需找到一条可行路径就直接返回,不再做后续 DFS ),并记录结果至 res 。
    3. 还原当前矩阵元素: 将 board[i][j] 元素还原至初始值,即 word[k] 。
  4. 返回值: 返回布尔量 res ,代表是否搜索到目标字符串。

特殊的:使用空字符(Python: ‘’ , Java/C++: ‘\0’ )做标记是为了防止标记字符与矩阵原有字符重复。当存在重复时,此算法会将矩阵原有字符认作标记字符,从而出现错误。
复杂度分析:M,N 分别为矩阵行列大小, K 为字符串 word 长度。

  • 时间复杂度 O(3^K*MN) : 最差情况下,需要遍历矩阵中长度为 K 字符串的所有方案,时间复杂度为 O(3^K);矩阵中共有 MN 个起点,时间复杂度为 O(MN) 。

方案数计算: 设字符串长度为 K ,搜索中每个字符有上、下、左、右四个方向可以选择,舍弃回头(上个字符)的方向,剩下 3 种选择,因此方案数的复杂度为 O(3^K)。

  • 空间复杂度 O(K) : 搜索过程中的递归深度不超过 K ,因此系统因函数调用累计使用的栈空间占用 O(K) (因为函数返回后,系统调用的栈空间会释放)。最坏情况下 K=MN ,递归深度为 MN ,此时系统栈使用 O(MN) 的额外空间。

image.png
知识点:
代码实现:

  1. //方式一:深度递归+剪枝
  2. class Solution {
  3. public boolean exist(char[][] board, String word) {
  4. char[] wordChar = word.toCharArray();//字符串转为字符数组方便递归依次比较每个字符
  5. for(int i=0;i<board.length;i++){//矩阵中的每一个点都可能是递归的起点,所以要走 MN 次,即为 O(MN)
  6. for(int j=0;j<board[0].length;j++){
  7. if(dsf(board, wordChar, i, j,0))//i,j是矩阵深度递归的起点,k是字符数组的比较起点
  8. return true;//如果某条路递归成功就返回
  9. //此处不能写成 return dsf(board, wordChar, i, j,0);
  10. //这样会导致从某个点开始的递归不成功就结束了整个exist()函数,不会进行下一次的循环
  11. }
  12. }
  13. return false;//走到这里就是一次都没有递归成功,则返回false
  14. }
  15. //我们自己设定dsf的遍历顺序为:下-上-右-左
  16. private boolean dsf(char[][] board, char[] wordChar,int i,int j,int k){
  17. //剪枝的情况(3种):数组越界、相比较的值不等、已经走过了某个位置
  18. //其中 board[i][j]!=wordChar[k]这句话合并了后两种情况,因为走过的值被设置为 '/0'为空字符
  19. if(i<0||i>board.length-1||j<0||j>board[0].length-1||board[i][j]!=wordChar[k]) return false;
  20. if(k==wordChar.length-1) return true;//递归结束条件
  21. board[i][j]='\0';//走过的地方标记为空字符 '/0' ,保证在当前匹配方案中不要走回头路。
  22. boolean res=dsf(board, wordChar, i+1, j, k+1)||dsf(board, wordChar,i-1 , j, k+1)||
  23. dsf(board, wordChar, i, j+1, k+1)||dsf(board, wordChar, i, j-1, k+1);//任何一种情况成立都可以
  24. board[i][j]=wordChar[k];//回溯,把修改为空字符的位置还原为原始值
  25. return res;
  26. }
  27. }

补充知识点

  1. 0 '0' "0" '\0' 区别
  2. '0' 代表 字符0 ,对应ASCII码值为 0x30 (也就是十进制 48)
  3. '\0' 代表 空字符(转义字符)【输出为空】, 对应ASCII码值为 0x00(也就是十进制 0), 用作字符串结束符
  4. 0 代表 数字0 若把 数字0 赋值给 某个字符,对应ASCII码值为 0x00(也就是十进制0)
  5. "0" 代表 一个字符串, 字符串中含有 2个字符,分别是'0''\0'

JZ66:机器人的运动范围

地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
分析:
本题与 矩阵中的路径 类似,是典型的搜索 & 回溯问题。在介绍回溯算法算法前,为提升计算效率,首先讲述两项前置工作: 数位之和计算 、 可达解分析
数位之和计算:
设一数字 x ,向下取整除法符号 / ,求余符号 % ,则有:
x%10 :得到 x 的个位数字;
x/10 : 令 x 的十进制数向右移动一位,即删除个位数字。
因此,可通过循环求得数位和 s ,数位和计算的封装函数如下所示:

  1. public int sums(int x){
  2. int s = 0;
  3. while(x != 0) {
  4. s += x % 10;
  5. x = x / 10;
  6. }
  7. return s;
  8. }

由于机器人每次只能移动一格(即只能从x 运动至 x±1),因此每次只需计算 x 到 x±1 的数位和增量。本题说明 1≤n,m≤100 ,以下公式仅在此范围适用。
数位和增量公式: 设 x 的数位和为 剑指offer记录 - 图68 ,x+1 的数位和为 剑指offer记录 - 图69
当 (x+1)%10=0 时:剑指offer记录 - 图70,例如 19,20 的数位和分别为 10,2 ;
当 (x+1)%10!=0 时:剑指offer记录 - 图71 ,例如 1,2 的数位和分别为 1,2 。
以下代码为增量公式的三元表达式写法,将整合入最终代码中: 剑指offer记录 - 图72
可达解分析:
根据数位和增量公式得知,数位和每逢 进位 突变一次。根据此特点,矩阵中 满足数位和的解 构成的几何形状形如多个 等腰直角三角形 ,每个三角形的直角顶点位于 0,10,20,… 等数位和突变的矩阵索引处 。
三角形内的解虽然都满足数位和要求,但由于机器人每步只能走一个单元格,而三角形间不一定是连通的,因此机器人不一定能到达,称之为 不可达解 ;同理,可到达的解称为 可达解 (本题求此解) 。
根据可达解的结构和连通性,易推出机器人可 仅通过向右和向下移动,访问所有可达解 。

  1. 三角形内部: 全部连通,易证;
  2. 两三角形连通处: 若某三角形内的解为可达解,则必与其左边或上边的三角形连通(即相交),即机器人必可从左边或上边走进此三角形。

image.png
思路:
方法一:深度优先遍历 DFS
深度优先搜索: 可以理解为暴力法模拟机器人在矩阵中的所有路径。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
剪枝: 在搜索中,遇到数位和超出目标值、此元素已访问,则应立即返回,称之为 可行性剪枝 。
算法解析

  1. 递归参数: 当前元素在矩阵中的行列索引 i 和 j ,两者的数位和 si, sj 。
  2. 终止条件: 当 ① 行列索引越界 或 ② 数位和超出目标值 k 或 ③ 当前元素已访问过 时,返回 0 ,代表不计入可达解。
  3. 递推工作:
    1. 标记当前单元格 :将索引 (i, j) 存入 visited 数组中,代表此单元格已被访问过。
    2. 搜索下一单元格: 计算当前元素的 下、右 两个方向元素的数位和,并开启下层递归 。
  4. : 返回 1+右方搜索的可达解总数 +下方搜索的可达解总数,代表从本单元格递归搜索的可达解总数。

复杂度分析:设矩阵行列数分别为 M,N 。

  1. 时间复杂度 O(MN) : 最差情况下,机器人遍历矩阵所有单元格,此时时间复杂度为 O(MN) 。
  2. 空间复杂度 O(MN) : 最差情况下,visited 内存储矩阵所有单元格的索引,使用 O(MN) 的额外空间。

方法二:广度优先遍历 BFS
BFS/DFS : 两者目标都是遍历整个矩阵,不同点在于搜索顺序不同。DFS 是朝一个方向走到底,再回退,以此类推;BFS 则是按照“平推”的方式向前搜索。
BFS 实现: 通常利用队列实现广度优先遍历。
算法解析:

  1. 初始化: 将机器人初始点 (0,0) 加入队列 queue ;
  2. 迭代终止条件: queue 为空。代表已遍历完所有可达解。
  3. 迭代工作:
    1. 单元格出队: 将队首单元格的 索引、数位和 弹出,作为当前搜索单元格。
    2. 判断是否跳过: 若 ① 行列索引越界 或 ② 数位和超出目标值 k 或 ③ 当前元素已访问过 时,执行 continue 。
    3. 标记当前单元格 :将单元格索引 (i, j) 存入 visited 数组中,代表此单元格 已被访问过 。
    4. 单元格入队: 将当前元素的 下方、右方 单元格的 索引、数位和 加入 queue 。
  4. 返回值: visited 数组的长度 len(visited) ,即可达解的数量。

复杂度分析:设矩阵行列数分别为 M,N 。

  • 时间复杂度 O(MN) : 最差情况下,机器人遍历矩阵所有单元格,此时时间复杂度为 O(MN) 。
  • 空间复杂度 O(MN) : 最差情况下, visited 内存储矩阵所有单元格的索引,使用 O(MN) 的额外空间。

知识点:
代码实现:

  1. //方式1:深度优先遍历
  2. class Solution {
  3. int m,n,k;//用类变量来传递值
  4. boolean[][] visited;//用类变量记录状态,是否走过此位置
  5. public int movingCount(int m, int n, int k) {
  6. this.m=m;this.n=n;this.k=k;
  7. visited=new boolean[m][n];
  8. return dsf(0, 0, 0, 0);
  9. }
  10. private int dsf(int i,int j,int si,int sj){
  11. if(i>=m||j>=n||si+sj>k||visited[i][j]) return 0;
  12. visited[i][j]=true;
  13. return 1+dsf(i+1,j,(i+1)%10==0?si-8:si+1,sj)+dsf(i,j+1,si,(j+1)%10==0?sj-8:sj+1);
  14. }
  15. }
  16. //广度优先遍历(队列实现)
  17. class Solution {
  18. public int movingCount(int m, int n, int k) {
  19. LinkedList<int[]> queue=new LinkedList<>();
  20. queue.add(new int[]{0,0,0,0});
  21. boolean[][] visited=new boolean[m][n];
  22. int res=0;
  23. while (!queue.isEmpty()){
  24. int[] temp = queue.poll();
  25. int i=temp[0],j=temp[1],si=temp[2],sj=temp[3];
  26. if(i>=m||j>=n||si+sj>k||visited[i][j]) continue;
  27. visited[i][j]=true;
  28. res++;
  29. queue.add(new int[]{i+1,j,(i+1)%10==0?si-8:si+1,sj});
  30. queue.add(new int[]{i,j+1,si,(j+1)%10==0?sj-8:sj+1});
  31. }
  32. return res;
  33. }
  34. }

JZ67:剪绳子

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
分析:
设将长度为 n 的绳子切为 a 段:
剑指offer记录 - 图74
本题等价于求解:
剑指offer记录 - 图75
以下数学推导总体分为两步:① 当所有绳段长度相等时,乘积最大。② 最优的绳段长度为 3 。
*数学推导:

以下公式为“算术几何均值不等式” ,等号当且仅当 剑指offer记录 - 图76 时成立。
剑指offer记录 - 图77
推论一: 将绳子 以相等的长度等分为多段 ,得到的乘积最大。
设将绳子按照 x 长度等分为 a 段,即 剑指offer记录 - 图78,则乘积为 剑指offer记录 - 图79。观察以下公式,由于 n 为常数,因此当 剑指offer记录 - 图80取最大值时, 乘积达到最大值。
剑指offer记录 - 图81
根据分析,可将问题转化为求剑指offer记录 - 图82 的极大值,因此对 x 求导数。
剑指offer记录 - 图83
剑指offer记录 - 图84 ,则 剑指offer记录 - 图85 ,易得驻点为剑指offer记录 - 图86;根据以下公式,可知剑指offer记录 - 图87 为极大值点。
剑指offer记录 - 图88
由于切分长度 x 必须为整数,最接近 e 的整数为 2 或 3 。如下式所示,代入 x=2 和 x=3 ,得出 x=3 时,乘积达到最大。
剑指offer记录 - 图89
口算对比方法:给两数字同时取 66 次方,再对比。
剑指offer记录 - 图90

推论二: 尽可能将绳子以长度 3 等分为多段时,乘积最大。
切分规则:
最优: 3 。把绳子尽可能切为多个长度为 3 的片段,留下的最后一段绳子的长度可能为 0,1,2 三种情况。
次优: 2 。若最后一段绳子长度为 2 ;则保留,不再拆为 1+1 。
最差: 1 。若最后一段绳子长度为 1 ;则应把一份 3+1 替换为 2+2,因为 2×2>3×1。
思路:
方法一:数学推导
算法流程

  1. 当 n≤3 时,按照规则应不切分,但由于题目要求必须剪成 m>1 段,因此必须剪出一段长度为 1 的绳子,即返回 n−1 。
  2. 当 n>3 时,求 n 除以 3 的 整数部分 a 和 余数部分 b (即 n=3a+b ),并分为以下三种情况(设求余操作符号为 “%” )::
    1. 当 b=0 时,直接返回 剑指offer记录 - 图91%1000000007 ;
    2. 当 b=1 时,要将一个 1+3 转换为 2+2,因此返回 剑指offer记录 - 图92%1000000007;
    3. 当 b=2 时,返回 剑指offer记录 - 图93%1000000007

复杂度分析:
时间复杂度 O(1) : 仅有求整、求余、次方运算。
求整和求余运算:资料提到不超过机器数的整数可以看作是 O(1) ;
幂运算:查阅资料,提到浮点取幂为 O(1) 。
空间复杂度 O(1) : 变量 a 和 b 使用常数大小额外空间。
方式二:自己的解法
复杂度分析:
时间复杂度 O(N) : 使用长度为n+1的数组数存储每个n划分后的最大乘积。
空间复杂度 O(N) : 遍历从3到n。
在n特别大时间和空间复杂度太高,不可取!!!
知识点:
Java 代码: 根据二分法计算原理,至少要保证变量 x 和 rem 可以正确存储 剑指offer记录 - 图94
,而剑指offer记录 - 图95,因此我们选取 long 类型。
代码实现:

  1. //方式一:(快速幂求余)
  2. class Solution {
  3. public int cuttingRope(int n) {
  4. if(n<4) return n-1;
  5. int b=n%3,p=1000000007;
  6. long res=1,x=3;
  7. for(int i=n/3-1;i>0;i/=2){
  8. //如果是奇数,只有最开始和最后会进入这个if
  9. //如果是偶数,只要最后会进入这个if
  10. if(i%2==1) res=res*x%p;
  11. x=x*x%p;
  12. }
  13. if(b==0) return (int)(res*3%p);
  14. else if(b==1) return (int)(res*4%p);
  15. else return (int)(res*6%p);
  16. }
  17. }
  18. //方式2:循环求余
  19. class Solution {
  20. public int cuttingRope(int n) {
  21. if(n<4) return n-1;
  22. long res=1;
  23. int temp=n/3,p=1000000007;
  24. while(temp>1){
  25. res=res*3%p;
  26. temp--;
  27. }
  28. if(n%3==0) res=res*3%p;
  29. else if(n%3==1) res=res*4%p;
  30. else res=res*6%p;
  31. return (int)res;
  32. }
  33. }

补充大数求余的知识点
大数求余解法
大数越界: 当 a 增大时,最后返回的 剑指offer记录 - 图96 大小以指数级别增长,可能超出 int32 甚至 int64 的取值范围,导致返回值错误。
大数求余问题: 在仅使用 int32 类型存储的前提下,正确计算 剑指offer记录 - 图97 对 p 求余(即 剑指offer记录 - 图98 )的值。
解决方案: 循环求余 、 快速幂求余 ,其中后者的时间复杂度更低,两种方法均基于以下求余运算规则推出:剑指offer记录 - 图99
1. 循环求余:
根据求余运算性质推出(∵ 本题中 x

解析: 利用此公式,可通过循环操作依次求 剑指offer记录 - 图100对 p 的余数,保证每轮中间值 rem 都在 int32 取值范围中。封装方法代码如下所示。
时间复杂度 O(N) : 其中 N=a ,即循环的线性复杂度。

  1. private int remainder1(int x,int a,int p){
  2. long rem=1;
  3. for(int i=1;i<=a;i++){
  4. rem=(rem*x)%p;
  5. }
  6. return (int)rem;
  7. }

2. 快速幂求余:
根据求余运算性质可推出:
剑指offer记录 - 图101
当 a 为奇数时 a/2 不是整数,因此分为以下两种情况( ‘’//‘’ 代表向下取整的除法):
剑指offer记录 - 图102
解析: 利用以上公式,可通过循环操作每次把指数 a 问题降低至指数 a/2 问题,只需循环 剑指offer记录 - 图103次,因此可将复杂度降低至对数级别。封装方法代码如下所示。

  1. private int remainder2(long x,int a,int p){
  2. //x是基本指数,a是多少个指数,p是余数
  3. long rem=1;
  4. while (a>0){
  5. if((a&1)==1) rem=(rem*x)%p;
  6. x=x^2%p;
  7. a>>>=2;
  8. }
  9. return (int)rem;
  10. }

复杂度分析:以下为二分求余法的复杂度。

  • 时间复杂度 剑指offer记录 - 图104 : 其中 N=a ,二分法为对数级别复杂度,每轮仅有求整、求余、次方运算。

求整 和 求余运算:资料提到不超过机器数的整数可以看作是 O(1) ;
幂运算:查阅资料,提到浮点取幂为 O(1) 。

  • 空间复杂度 O(1) : 变量 a, b, p, x, rem 使用常数大小额外空间。

    力扣JZ17:打印从1到最大的n位数

    输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
    示例 1:
    输入: n = 1
    输出: [1,2,3,4,5,6,7,8,9]
    分析:
    题目要求打印 “从 1 至最大的 n 位数的列表” ,因此需考虑以下两个问题:
    最大的 n 位数(记为 end )和位数 n 的关系: 例如最大的 1 位数是 9 ,最大的 2 位数是 99 ,最大的 3 位数是 999 。则可推出公式:end = 10^n - 1
    大数越界问题: 当 n 较大时,end 会超出 int32 整型的取值范围,超出取值范围的数字无法正常存储。但由于本题要求返回 int 类型数组,相当于默认所有数字都在 int32 整型取值范围内,因此不考虑大数越界问题。
    因此,只需定义区间 [1, 10^n - 1]和步长 1 ,通过 for 循环生成结果列表 res 并返回即可。
    复杂度分析:
    时间复杂度 O(10^n): 生成长度为 10^n的列表需使用 O(10^n) 时间。
    空间复杂度 O(1) : 建立列表需使用 O(1) 大小的额外空间(列表作为返回结果,不计入额外空间 )
    1. class Solution {
    2. public int[] printNumbers(int n) {
    3. int end = (int)Math.pow(10, n) - 1;
    4. int[] res = new int[end];
    5. for(int i = 0; i < end; i++)
    6. res[i] = i + 1;
    7. return res;
    8. }
    9. }
    大数打印解法:
    实际上,本题的主要考点是大数越界情况下的打印。需要解决以下三个问题:
    1. 表示大数的变量类型:
    无论是 short / int / long … 任意变量类型,数字的取值范围都是有限的。因此,大数的表示应用字符串 String 类型。
    2. 生成数字的字符串集:
    使用 int 类型时,每轮可通过 +1 生成下个数字,而此方法无法应用至 String 类型。并且, String 类型的数字的进位操作效率较低,例如 “9999” 至 “10000” 需要从个位到千位循环判断,进位 4 次。
    观察可知,生成的列表实际上是 n 位 0 - 9 的 全排列 ,因此可避开进位操作,通过递归生成数字的 String 列表。
    3. 递归生成全排列:
    基于分治算法的思想,先固定高位,向低位递归,当个位已被固定时,添加数字的字符串。例如当 n=2 时(数字范围 1−99 ),固定十位为 0 - 9 ,按顺序依次开启递归,固定个位 0 - 9 ,终止递归并添加数字字符串。
    image.png
    根据以上方法,可初步编写全排列代码:
    1. StringBuilder res;
    2. int n;
    3. char[] num,loop={'0','1','2','3','4','5','6','7','8','9'};
    4. public String printNumbers(int n) {
    5. this.n=n;
    6. res=new StringBuilder(); //数字字符串集
    7. num=new char[n];//定义长度为 n 的字符列表
    8. dsf(0);//开启全排列递归
    9. res.deleteCharAt(res.length()-1);//去掉最后一个逗号
    10. return res.toString();//转化为字符串并返回
    11. }
    12. private void dsf(int x){
    13. if(x==n){//终止条件:已固定完所有位
    14. //拼接 num 并添加至 res 尾部,使用逗号隔开
    15. res.append(String.valueOf(num)+",");//String.valueOf(num) 返回字符数组的字符串表示形式
    16. return;//结束当前所在的一个方法
    17. }
    18. for(char ch:loop){//遍历 ‘0‘ - ’9‘
    19. num[x]=ch;//固定第 x 位为 ch
    20. dsf(x+1);//开启固定第 x + 1 位
    21. }
    22. }
    在此方法下,各数字字符串被逗号隔开,共同组成长字符串。返回的数字集字符串如下所示: ``` 输入:n = 1 输出:”0,1,2,3,4,5,6,7,8,9”

输入:n = 2 输出:”00,01,02,…,10,11,12,…,97,98,99”

输入:n = 3 输出:”000,001,002,…,100,101,102,…,997,998,999”

  1. 观察可知,当前的生成方法仍有以下问题:
  2. 1. 诸如 00,01,02,⋯ 应显示为 0,1,2,⋯ ,即应 删除高位多余的 00 ;
  3. 1. 此方法从 0 开始生成,而题目要求 列表从 1 开始
  4. 以上两个问题的解决方法如下:<br />**1. 删除高位多余的 0 :**<br />字符串左边界定义: 声明变量 start 规定字符串的左边界,以保证添加的数字字符串 num[start:] 中无高位多余的 0 。例如当 n=2 时,19 start=1 1099 start=0 。<br />左边界 start 变化规律: 观察可知,当输出数字的所有位都是 9 时,则下个数字需要向更高位进 1 ,此时左边界 start 需要减 1 (即高位多余的 0 减少一个)。例如当 n=3 (数字范围 1999 )时,左边界 start 需要减 1 的情况有: "009" 进位至 "010" "099" 进位至 "100" 。设数字各位中 9 的数量为 nine ,所有位都为 99 的判断条件可用以下公式表示:<br />nstart=nine<br />统计 nine 的方法: 固定第 x 位时,当 i=9 则执行 nine=nine+1 ,并在回溯前恢复 nine=nine1 。<br />**2. 列表从 1 开始:**<br />在以上方法的基础上,添加数字字符串前判断其是否为 "0" ,若为 "0" 则直接跳过。<br />**复杂度分析:**
  5. - 时间复杂度 O(10^n) 递归的生成的排列的数量为 10^n
  6. - 空间复杂度 O(10^n): 结果列表 res 的长度为 10^n - 1,各数字字符串的长度区间为 1,2,...,n ,因此占用 O(10^n) 大小的额外空间。
  7. **代码:**<br />为 正确表示大数 ,以下代码的返回值为数字字符串集拼接而成的长字符串。
  8. ```java
  9. StringBuilder res;
  10. int n,start,nine;//start 是首个不为 0 的位置;nine 是统计字符串中 9 的个数
  11. char[] num, loop = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
  12. public String printNumbers(int n) {
  13. this.n=n;
  14. res=new StringBuilder();
  15. num=new char[n];
  16. start=n-1;//对于高位有 0 的整数,截取的开始位置
  17. dfs(0);
  18. res.deleteCharAt(res.length()-1);//去除最后一个逗号
  19. return res.toString();
  20. }
  21. void dfs(int x) {
  22. if(x==n){
  23. String s =String.valueOf(num).substring(start);//从首个不为 0 的位置开始截取
  24. if(!"0".equals(s))
  25. res.append(s+",");//这个数不是 0 才加入结果字符串(解决列表需要从 1 开始的问题)
  26. if(n==start+nine) start--;//整个字符串全都是 9 ,则 start 需要往前移 1 个位置
  27. return;//结束当前所在的一个方法
  28. }
  29. for(char ch:loop){
  30. num[x]=ch;
  31. if(ch=='9') nine++;//统计本次字符串中 9 会出现的个数
  32. dfs(x+1);
  33. }
  34. nine--;//回溯,需要让统计的 9 的个数回复原始状态
  35. }

本题要求输出 int 类型数组。为 运行通过 ,可在添加数字字符串 ss 前,将其转化为 int 类型。代码如下所示:

  1. int[] res;//改用数组返回
  2. int n,start,nine,count;//start 是首个不为 0 的位置;nine 是统计字符串中 9 的个数
  3. char[] num, loop = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
  4. public int[] printNumbers(int n) {
  5. this.n=n;
  6. res=new int[(int)Math.pow(10, n)-1];
  7. num=new char[n];
  8. start=n-1;//对于高位有 0 的整数,截取的开始位置
  9. dfs(0);
  10. return res;
  11. }
  12. void dfs(int x) {
  13. if(x==n){
  14. String s =String.valueOf(num).substring(start);//从首个不为 0 的位置开始截取
  15. if(!"0".equals(s))
  16. res[count++]=Integer.parseInt(s);//count用来计数
  17. if(n==start+nine) start--;//整个字符串全都是 9 ,则 start 需要往前移 1 个位置
  18. return;//结束当前所在的一个方法
  19. }
  20. for(char ch:loop){
  21. num[x]=ch;
  22. if(ch=='9') nine++;//统计本次字符串中 9 会出现的个数
  23. dfs(x+1);
  24. }
  25. nine--;//回溯,需要让统计的 9 的个数回复原始状态
  26. }

力扣JZ18:删除链表的节点

分析:
本题删除值为 val 的节点分需为两步:定位节点、修改引用。
定位节点: 遍历链表,直到 head.val == val 时跳出,即可定位目标节点。
修改引用: 设节点 cur 的前驱节点为 pre ,后继节点为 cur.next ;则执行 pre.next = cur.next ,即可实现删除 cur 节点。
image.png
思路:
算法流程:

  1. 特例处理: 当应删除头节点 head 时,直接返回 head.next 即可。
  2. 初始化: pre = head , cur = head.next 。
  3. 定位节点: 当 cur 为空 或 cur 节点值等于 val 时跳出。

    //当cur不为节点值时执行下面的操作

    • 保存当前节点索引,即 pre = cur 。
    • 遍历下一节点,即 cur = cur.next 。
  4. 删除节点: 若 cur 指向某节点,则执行 pre.next = cur.next ;若 cur 指向 nullnull ,代表链表中不包含值为 val 的节点。
  5. 返回值: 返回链表头部节点 head 即可。

知识点:
代码实现:

  1. //方式1:k神解法
  2. class Solution {
  3. public ListNode deleteNode(ListNode head, int val) {
  4. if(head.val==val) return head.next;
  5. ListNode pre=head;
  6. ListNode cur=head.next;
  7. while(cur!=null&&cur.val!=val){
  8. pre=cur;
  9. cur=cur.next;
  10. }
  11. if(cur!=null)
  12. pre.next=cur.next;
  13. return head;
  14. }
  15. }
  16. //方式2:我的笨拙解法,设置了哨兵(方便操作头结点的值)
  17. class Solution {
  18. public ListNode deleteNode(ListNode head, int val) {
  19. ListNode newHead=new ListNode(-1);
  20. newHead.next=head;
  21. ListNode node1=newHead;
  22. ListNode node2=newHead.next;
  23. while(node2!=null){
  24. if(node2.val==val){
  25. node2=node2.next;
  26. node1.next=node2;
  27. return newHead.next;
  28. }else{
  29. node2=node2.next;
  30. node1=node1.next;
  31. }
  32. }
  33. return newHead.next;
  34. }
  35. }

力扣JZ44:数字序列中某一位的数字

数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。请写一个函数,求任意第n位对应的数字。
分析:
将 101112⋯ 中的每一位称为 数位 ,记为 n ;
将 10,11,12,⋯ 称为 数字 ,记为 num ;
数字 10 是一个两位数,称此数字的 位数 为 2 ,记为 digit ;
每 digit 位数的起始数字(即:1,10,100,⋯),记为 start 。
image.png
观察上表,可推出各 digit 下的数位数量 count 的计算公式:
count=9×start×digit
根据以上分析,可将求解分为三步:

  1. 确定 n 所在 数字 的 位数 ,记为 digit ;
  2. 确定 n 所在的 数字 ,记为 num ;
  3. 确定 n 是 num 中的哪一数位,并返回结果。

  4. 确定所求数位的所在数字的位数
    如下图所示,循环执行 n 减去 一位数、两位数、… 的数位数量 count ,直至 n≤count 时跳出。
    由于 n 已经减去了一位数、两位数、…、(digit−1) 位数 的 数位数量 count ,因而此时的 n 是从起始数字 start 开始计数的(在start处从1开始)。
    结论: 所求数位
    ① 在某个 digit 位数中;
    ② 为从数字 start 开始的第 n 个数位。

    1. private void find(int n){
    2. int digit=1,start=1,count=9;//设置数位 digit,开始数字 start,总位数 count的初始值
    3. while (n>count){
    4. n=n-count;
    5. digit=digit+1;//数位增加 1,2,3,...
    6. start=start*10;//开始数字 1,10,100,...
    7. count=9*digit*start;//数位 9,180,2700 <<<=== 9=(9-1+1)
    8. }
    9. }

    image.png
    2. 确定所求数位所在的数字
    如下图所示,所求数位 在从数字 start 开始的第 [(n−1)/digit] 个 数字 中( start 为第 0 个数字)。
    num = start + (n - 1) /digit
    结论: 所求数位在数字 num 中。
    image.png
    3. 确定所求数位在 num 的哪一数位
    如下图所示,所求数位为数字 num 的第 (n−1)%digit 位( 每个数字的首个数位为第 0 位)。
    s = str(num) //; 转化为 String
    res = int(s[(n - 1) % digit]) // 获得 num 的 第 (n - 1) % digit 个数位,并转化为 int
    结论: 所求数位是 res 。
    image.png
    复杂度分析:

  • 时间复杂度 O(logn) : 所求数位 n 对应数字 num 的位数 digit 最大为 O(logn) ;第一步最多循环 O(logn) 次;第三步中将 num 转化为字符串使用 O(logn) 时间;因此总体为 O(logn) 。
  • 空间复杂度 O(logn) : 将数字 num 转化为字符串 str(num) ,占用 O(logn) 的额外空间。

知识点:
1.Long中将long类型的数据转为字符串类型:public static String toString(long i) // Long.toString(num)
同样的,Integer 中也有 toString(long num) 这个方法
代码实现:

  1. class Solution {
  2. public int findNthDigit(int n) {
  3. int digit=1;//数位 digit
  4. long start=1;//开始数字 start
  5. long count=9;//总位数 count
  6. while (n>count){
  7. n-=count;//或者为 n=(int)(n-count);
  8. digit=digit+1;//数位增加 1,2,3,...
  9. start=start*10;//开始数字 1,10,100,...
  10. count=9*digit*start;//数位 9,180,2700 <<<=== 9=(9-1+1)
  11. }//找到了所在的数位 digit 和 开始数字 start
  12. long num=start+(n-1)/digit;//找到了所在的数字
  13. // return String.valueOf(num).charAt((n-1)%digit)-'0';//找所在数字的 数位
  14. return Long.toString(num).charAt((n-1)%digit)-'0'; //找所在数字的 数位
  15. }
  16. }

力扣JZ46:把数字翻译成字符串

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
分析:
根据题意,可按照下图的思路,总结出 “递推公式” (即转移方程)。因此,此题可用动态规划解决,以下按照流程解题。
image.png
动态规划解析:
记数字num 第 i 位数字为 剑指offer记录 - 图112 ,数字 num 的位数为 n ;
例如: num=12258 的 n=5 , 剑指offer记录 - 图113
状态定义: 设动态规划列表 dp ,dp[i] 代表以剑指offer记录 - 图114为结尾的数字的翻译方案数量。
转移方程: 若剑指offer记录 - 图115剑指offer记录 - 图116 组成的两位数字可以被翻译,则 dp[i]=dp[i−1]+dp[i−2] ;否则 dp[i]=dp[i−1] 。
可被翻译的两位数区间:当 剑指offer记录 - 图117 时,组成的两位数是无法被翻译的(例如 00,01,02,⋯ ),因此区间为 [10,25] 。
剑指offer记录 - 图118
初始状态:dp[0]=dp[1]=1 ,即 “无数字” 和 “第 1 位数字” 的翻译方法数量均为 1 ;
返回值: dp[n] ,即此数字的翻译方案数量。
Q: 无数字情况 dp[0]=1 从何而来?
A: 当 num 第 1,2 位的组成的数字 ∈[10,25] 时,显然应有 2 种翻译方法,即 dp[2]=dp[1]+dp[0]=2 ,而显然 dp[1]=1 ,因此推出 dp[0]=1 。
思路:
方法一:字符串遍历
为方便获取数字的各位剑指offer记录 - 图119 ,考虑先将数字 num 转化为字符串 s ,通过遍历 s 实现动态规划。
通过字符串切片 s[i−2:i] 获取数字组合 剑指offer记录 - 图120 ,通过对比字符串 ASCII 码判断字符串对应的数字区间。
空间使用优化: 由于 dp[i] 只与 dp[i−1] 和 dp[i−2]有关,因此可使用两个变量 a,b 分别记录 dp[i-1],dp[i−2] ,两变量交替前进即可。此方法可省去 dp 列表使用的 O(N) 的额外空间。
复杂度分析:

  • 时间复杂度 O(N) : N 为字符串 s 的长度(即数字 num 的位数 log(num) ),其决定了循环次数。
  • 空间复杂度 O(N) : 字符串 s 使用 O(N) 大小的额外空间。

image.png
补充:此题的动态规划计算是 对称的 ,即 从左向右 遍历(从第 dp[2] 计算至 dp[n] )和 从右向左 遍历(从第 dp[n−2] 计算至 dp[0] )所得方案数一致。从右向左遍历的代码如下所示。
方法二:数字求余
上述方法虽然已经节省了 dp 列表的空间占用,但字符串 s 仍使用了 O(N) 大小的额外空间。
空间复杂度优化:
利用求余运算 num%10 和求整运算 num/10 ,可获取数字 num 的各位数字(获取顺序为个位、十位、百位…)。因此,可通过 求余 和 求整 运算实现 从右向左 的遍历计算。而根据上述动态规划 “对称性” ,可知从右向左的计算是正确的。
自此,字符串 s 的空间占用也被省去,空间复杂度从 O(N) 降至 O(1) 。
复杂度分析

  • 时间复杂度 O(N) : N 为字符串 s 的长度(即数字 num 的位数 log(num) ),其决定了循环次数。
  • 空间复杂度 O(1) : 几个变量使用常数大小的额外空间。

知识点:
1.String的方法:
public int compareTo(String anotherString):可以用来比较两个字符串数字的大小
代码实现:

  1. //方法1:字符串遍历(从左往右)
  2. public int translateNum1(int num) {
  3. String s = Integer.toString(num);
  4. int a=1,b=1;//a在后,b在前
  5. for(int i=2;i<=s.length();i++){
  6. String cutNum = s.substring(i - 2, i);//substring()截取字符串是左闭右开
  7. int temp=cutNum.compareTo("10")>=0&&cutNum.compareTo("25")<=0?a+b:a;//f(i)=f(i-1)+f(i-2);
  8. b=a;
  9. a=temp;
  10. }
  11. return a;
  12. }
  13. //方法2:字符串遍历(从右往左)
  14. public int translateNum2(int num) {
  15. String s = Integer.toString(num);
  16. int a=1,b=1;//a在后,b在前
  17. for(int i=s.length()-2;i>=0;i--){
  18. String cutNum = s.substring(i, i+2);//substring()截取字符串是左闭右开
  19. int temp=cutNum.compareTo("10")>=0&&cutNum.compareTo("25")<=0?a+b:a;//f(i)=f(i-1)+f(i-2);
  20. b=a;
  21. a=temp;
  22. }
  23. return a;
  24. }
  25. //方法3:数字求余
  26. public int translateNum3(int num) {
  27. int a=1,b=1,x,y=num%10;//a在后,b在前
  28. while (num!=0){
  29. num/=10;
  30. x=num%10;//y为最后一位,x为y前一位
  31. int subNum=10*x+y;
  32. int temp=(subNum-10)>=0&&(subNum-25)<=0?a+b:a;
  33. b=a;
  34. a=temp;
  35. y=x;
  36. }
  37. return a;
  38. }

力扣JZ 47.:物的最大价值

在一个 mn 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
分析:
题目说明:从棋盘的左上角开始拿格子里的礼物,并每次 向右 或者 向下 移动一格、直到到达棋盘的右下角。根据题目说明,易得某单元格只可能从上边单元格或左边单元格到达
设 f(i,j) 为从棋盘左上角走至单元格 (i,j) 的礼物最大累计价值,易得到以下递推关系:f(i,j) 等于 f(i,j−1) 和 f(i−1,j) 中的较大值加上当前单元格礼物价值 grid(i,j) 。
f(i,j)=max[f(i,j−1),f(i−1,j)]+grid(i,j)
因此,可用动态规划解决此问题,以上公式便为转移方程。
image.png
动态规划解析:
状态定义: 设动态规划矩阵 dp ,dp(i,j) 代表从棋盘的左上角开始,到达单元格 (i,j) 时能拿到礼物的最大累计价值。
转移方程
剑指offer记录 - 图123剑指offer记录 - 图124 时,为起始元素;
剑指offer记录 - 图125剑指offer记录 - 图126 时,为矩阵第一行元素,只可从左边到达;
剑指offer记录 - 图127剑指offer记录 - 图128 时,为矩阵第一列元素,只可从上边到达;
剑指offer记录 - 图129剑指offer记录 - 图130 时,可从左边或上边到达;
剑指offer记录 - 图131
初始状态: dp[0][0]=grid[0][0] ,即到达单元格 (0,0) 时能拿到礼物的最大累计价值为 grid[0][0] ;
返回值: dp[m−1][n−1] ,m,n 分别为矩阵的行高和列宽,即返回 dp 矩阵右下角元素。
空间复杂度优化:
由于 dp[i][j] 只与 dp[i−1][j] , dp[i][j−1] , grid[i][j] 有关系,因此可以将原矩阵 grid 用作 dp 矩阵,即直接在 grid 上修改即可。应用此方法可省去 dp 矩阵使用的额外空间,因此空间复杂度从 O(MN) 降至 O(1) 。
*复杂度分析:

  • 时间复杂度 O(MN) : M,N 分别为矩阵行高、列宽;动态规划需遍历整个 grid 矩阵,使用 O(MN) 时间。
  • 空间复杂度 O(1) : 原地修改使用常数大小的额外空间。

方法二:优化方法一
以上代码逻辑清晰,和转移方程直接对应,但仍可提升效率:当 grid 矩阵很大时, i=0 或 j=0 的情况仅占极少数,相当循环每轮都冗余了一次判断。因此,可先初始化矩阵第一行和第一列,再开始遍历递推。
代码实现:

  1. //方式1:动态规划
  2. class Solution {
  3. public int maxValue(int[][] grid) {
  4. int m=grid.length,n=grid[0].length;
  5. for(int i=0;i<m;i++){
  6. for(int j=0;j<n;j++){
  7. if(i==0&&j==0) continue;
  8. else if(i==0) grid[i][j]+=grid[0][j-1];
  9. else if(j==0) grid[i][j]+=grid[i-1][0];
  10. else grid[i][j]+=Math.max(grid[i-1][j],grid[i][j-1]);
  11. }
  12. }
  13. return grid[m-1][n-1];
  14. }
  15. }
  16. //方式2:动态规划,优化1中的时间复杂度
  17. public int maxValue(int[][] grid) {
  18. int m=grid.length,n=grid[0].length;
  19. for(int i=1;i<m;i++)
  20. grid[i][0]+=grid[i-1][0];
  21. for(int j=1;j<n;j++)
  22. grid[0][j]+=grid[0][j-1];
  23. for(int i=1;i<m;i++){
  24. for(int j=1;j<n;j++){
  25. grid[i][j]+=Math.max(grid[i-1][j],grid[i][j-1]);
  26. }
  27. }
  28. return grid[m-1][n-1];
  29. }

力扣JZ 48: 最长不含重复字符的子字符串

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
分析:
长度为 N 的字符串共有 剑指offer记录 - 图132 个子字符串(复杂度为 )剑指offer记录 - 图133,判断长度为 N 的字符串是否有重复字符的复杂度为 O(N) ,因此本题使用暴力法解决的复杂度为 剑指offer记录 - 图134 。考虑使用动态规划O(N)时间复杂度。
动态规划解析:
状态定义: 设动态规划列表 dp ,dp[j] 代表以字符 s[j] 为结尾的 “最长不重复子字符串” 的长度。
转移方程: 固定右边界 j ,设字符 s[j] 左边距离最近的相同字符为 s[i] ,即 s[i]=s[j] 。

  1. 当 i<0 ,即 s[j] 左边无相同字符,则 dp[j]=dp[j−1]+1 ;
  2. 当 dp[j−1]<j−i ,说明字符 s[i] 在子字符串 dp[j−1] 区间之外 ,则 dp[j]=dp[j−1]+1 ;
  3. 当 dp[j−1]≥j−i ,说明字符 s[i] 在子字符串 dp[j−1] 区间之中 ,则 dp[j] 的左边界由 s[i] 决定,即 dp[j]=j−i ;

当 i<0 时,由于 dp[j−1]≤j 恒成立,因而 dp[j−1] 剑指offer记录 - 图135
返回值: max(dp) ,即全局的 “最长不重复子字符串” 的长度。
image.png
空间复杂度优化:
由于返回值是取 dp 列表最大值,因此可借助变量 tmp 存储 dp[j] ,变量 res 每轮更新最大值即可。此优化可节省 dp 列表使用的 O(N) 大小的额外空间。
观察转移方程,可知问题为:每轮遍历字符 s[j] 时,如何计算索引 i ?
以下介绍 哈希表 , 线性遍历 两种方法。
方法一:动态规划 + 哈希表
哈希表统计: 遍历字符串 s 时,使用哈希表(记为 dic )统计 各字符最后一次出现的索引位置 。
左边界 i 获取方式: 遍历到 s[j] 时,可通过访问哈希表 dic[s[j]] 获取最近的相同字符的索引 i 。
复杂度分析:
时间复杂度 O(N) : 其中 N 为字符串长度,动态规划需遍历计算 dp 列表。
空间复杂度 O(1) : 字符的 ASCII 码范围为 0 ~ 127 ,哈希表 dic 最多使用 O(128)=O(1) 大小的额外空间。
方法二: 动态规划 + 线性遍历
左边界 i 获取方式: 遍历到 s[j] 时,初始化索引i=j−1 ,向左遍历搜索第一个满足 s[i]=s[j] 的字符即可 。
复杂度分析:
时间复杂度 O(N^2) : 其中 N 为字符串长度,动态规划需遍历计算 dp 列表,占用 O(N) ;每轮计算 dp[j] 时搜索 i 需要遍历 j 个字符,占用 O(N) 。
空间复杂度 O(1) : 几个变量使用常数大小的额外空间。
方法三:双指针 + 哈希表
本质上与方法一类似,不同点在于左边界 i 的定义。

  1. 哈希表 dic 统计: 指针 j 遍历字符 s ,哈希表统计字符 s[j] 最后一次出现的索引 。
  2. 更新左指针 i : 根据上轮左指针 i 和 dic[s[j]] ,每轮更新左边界 i ,保证区间 [i+1,j] 内无重复字符且最大

i=max(dic[s[j]],i)

  1. 更新结果 res : 取上轮 resres 和本轮双指针区间 [i+1,j] 的宽度(即 j−i )中的最大值。

res=max(res,j−i)
复杂度分析:
时间复杂度 O(N) : 其中 N 为字符串长度,动态规划需遍历计算 dp 列表。
空间复杂度 O(1) : 字符的 ASCII 码范围为 0 ~ 127 ,哈希表 dic 最多使用 O(128)=O(1) 大小的额外空间。
知识点:
1.子串:串中任意个连续的字符组成的子序列称为该串的子串:n(n+1)/2
解析:
包含1个字符的子串共n个
包含2个字符的子串共n-1个
包含3个字符的子串共n-2个
包含4个字符的子串共n-3个
…….
包含n个字符的子串共1个
综上所述:子串个数共:1+2+3+。。。+n = n(n+1)/2
知识点:
1.代码:getOrDefault(key,default) , 代表当哈希表包含键 key 时返回对应 value ,不包含时返回默认值 default 。
代码实现:

  1. //方法一:动态规划 + 哈希表
  2. class Solution {
  3. public int lengthOfLongestSubstring(String s) {
  4. int res=0,temp=0;
  5. HashMap<Character,Integer> map=new HashMap<>();
  6. for(int j=0;j<s.length();j++){
  7. //这里还有个巧妙之处,就是把 -1 位置假设看做在字符串中第一次出现的字符(方便计算出第一次出现字符的 j-i 的值)
  8. int i = map.getOrDefault(s.charAt(j), -1);//看map中是否有 s[i] 的存在
  9. map.put(s.charAt(j),j);//把 s[i] 的最新位置存到 map 中
  10. temp=temp<j-i?temp+1:j-i;//这里计算的是以当前字符,即 s[j] 结尾的最长不重复字符串,要求这个不重复子串必须是包含s[j]字符的
  11. res=temp>res?temp:res;//找出以字符串中每个字符结尾的最长不重复字符串
  12. }
  13. return res;
  14. }
  15. }
  16. //方法二: 动态规划 + 线性遍历
  17. class Solution {
  18. public int lengthOfLongestSubstring(String s) {
  19. int res=0,temp=0;
  20. HashMap<Character,Integer> map=new HashMap<>();
  21. for(int j=0;j<s.length();j++){
  22. int i=j-1;
  23. while (i>=0&&s.charAt(i)!=s.charAt(j)) i--;//跳出时要么找到了 s[i]==s[j] ;要么是 i=-1了
  24. map.put(s.charAt(j),j);//把 s[i] 的最新位置存到map照中
  25. temp=temp<j-i?temp+1:j-i;
  26. res=temp>res?temp:res;
  27. }
  28. return res;
  29. }
  30. }
  31. //方法三:双指针 + 哈希表
  32. class Solution {
  33. public int lengthOfLongestSubstring(String s) {
  34. int res=0,i=-1;
  35. HashMap<Character,Integer> map=new HashMap<>();
  36. for(int j=0;j<s.length();j++){
  37. if(map.containsKey(s.charAt(j)))//如果一直不进入这个if,则i=-1
  38. i=Math.max(i, map.get(s.charAt(j)));//进入了if,则每次更新i,让 i+1 到 j 之间无重复
  39. map.put(s.charAt(j),j);//把 s[j] 的最新位置存到map照中
  40. res=Math.max(res, j-i);
  41. }
  42. return res;
  43. }
  44. }

力扣JZ53 - II:0~n-1中缺失的数字

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
分析:
排序数组中的搜索问题,首先想到 二分法 解决。
根据题意,数组可以按照以下规则划分为两部分。
左子数组:nums[i]=i ;
右子数组:nums[i] 剑指offer记录 - 图137i ;
缺失的数字等于 “右子数组的首位元素” 对应的索引;因此考虑使用二分法查找 “右子数组的首位元素”
image.png
思路:
算法解析:
初始化: 左边界 i=0 ,右边界 j=len(nums)−1 ;代表闭区间 [i,j] 。
循环二分: 当 i≤j 时循环 (即当闭区间 [i,j] 为空时跳出);计算中点 m=(i+j)/2 ,其中 “/“ 为向下取整除法;若 nums[m]=m ,则 “右子数组的首位元素” 一定在闭区间 [m+1,j] 中,因此执行 i=m+1;
若 nums[m] 剑指offer记录 - 图139m ,则 “左子数组的末位元素” 一定在闭区间 [i,m−1] 中,因此执行 j=m−1;
返回值: 跳出时,变量 i 和 j 分别指向 “右子数组的首位元素” 和 “左子数组的末位元素” 。因此返回 i 即可。

复杂度分析:
  • 时间复杂度 O(log N): 二分法为对数级别复杂度。
  • 空间复杂度 O(1): 几个变量使用常数大小的额外空间。

    1. //方式1:
    2. class Solution {
    3. public int missingNumber(int[] nums) {
    4. int i=0,j=nums.length-1;
    5. while(i<=j){
    6. int m=i+(j-i)/2;
    7. if(nums[m]==m) i=m+1;
    8. else j=m-1;
    9. }
    10. return i;
    11. }
    12. }
    13. //方式2:我的想法
    14. class Solution {
    15. public int missingNumber(int[] nums) {
    16. int[] res=new int[nums.length+1];//空间O(n) ,时间O(n)
    17. for(int num:nums){
    18. res[num]=num;
    19. }
    20. for(int i=0;i<res.length;i++){
    21. if(res[i]!=i)
    22. return i;
    23. }
    24. return 0;
    25. }
    26. }

    JZ力扣56 - II. 数组中数字出现的次数 II

    在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
    分析:
    如下图所示,考虑数字的二进制形式,对于出现三次的数字,各 二进制位 出现的次数都是 3 的倍数。
    因此,统计所有数字的各二进制位中 1 的出现次数,并对 3 求余,结果则为只出现一次的数字
    image.png
    分析:
    知识点:
    代码实现:

    力扣JZ59 - II. 队列的最大值

    请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_valuepush_backpop_front均摊时间复杂度都是O(1)。
    若队列为空,pop_frontmax_value 需要返回 -1
    示例 1:
    输入:
    [“MaxQueue”,”push_back”,”push_back”,”max_value”,”pop_front”,”max_value”]
    [[],[1],[2],[],[],[]]
    输出: [null,null,null,2,1,2]
    分析:
    对于普通队列,入队 push_back() 和出队 pop_front() 的时间复杂度均为 O(1) ;本题难点为实现查找最大值 max_value() 的O(1) 时间复杂度。
    假设队列中存储 N 个元素,从中获取最大值需要遍历队列,时间复杂度为 O(N) ,单从算法上无优化空间。
    如下图所示,最直观的想法是 维护一个最大值变量 ,在元素入队时更新此变量即可;但当最大值出队后,并无法确定下一个 次最大值 ,因此不可行。
    image.png
    考虑利用 数据结构 来实现,即经常使用的 “空间换时间” 。如下图所示,考虑构建一个递减列表来保存队列 所有递减的元素 ,递减链表随着入队和出队操作实时更新,这样队列最大元素就始终对应递减列表的首元素,实现了获取最大值 O(1) 时间复杂度。
    image.png
    为了实现此递减列表,需要使用 双向队列 ,假设队列已经有若干元素:
    当执行入队 push_back() 时: 若入队一个比队列某些元素更大的数字 x ,则为了保持此列表递减,需要将双向队列 尾部所有小于 x 的元素 弹出。
    当执行出队 pop_front() 时: 若出队的元素是最大元素,则 双向队列 需要同时 将首元素出队 ,以保持队列和双向队列的元素一致性。
    使用双向队列原因:维护递减列表需要元素队首弹出、队尾插入、队尾弹出操作皆为 O(1) 时间复杂度。
    思路:
    函数设计:
    初始化队列 queue ,双向队列 deque ;
    最大值 max_value() :

    • 当双向队列 deque 为空,则返回−1 ;
    • 否则,返回 deque 首元素;

入队 push_back() :

  • 将元素 value 入队 queue ;
  • 将双向队列deque中队尾 所有 小于 value 的元素弹出(以保持 deque 非单调递减),并将元素 value 入队 deque ;

出队 pop_front() :(本题中只是为了设计max_value()这个函数,其余的入队和出队函数的功能并没有改变)

  • 若队列 queue 为空,则直接返回 −1 ;
  • 否则,将 queue 首元素出队;
  • 若 deque 首元素和 queue 首元素 相等 ,则将 deque 首元素出队(以保持两队列 元素一致 ) ;

设计双向队列为 单调不增 的原因:若队列 queue 中存在两个 值相同的最大元素 ,此时 queue 和 deque 同时弹出一个最大元素,而 queue 中还有一个此最大元素;即采用单调递减将导致两队列中的元素不一致。
复杂度分析:

  • 时间复杂度 O(1) : max_value(), push_back(), pop_front() 方法的均摊时间复杂度均为 O(1) ;
  • 空间复杂度 O(N) : 当元素个数为 NN 时,最差情况下deque 中保存 N 个元素,使用 O(N) 的额外空间;

知识点:
1.如果是对象来比较,使用equals;如果只是单纯的int型数字,才可以使用==来比较
2.Arrays.sort(数组):时间复杂度为O(nlogn)
3.关于LinkedList作为双向队列 Deque的方法

限定容量非友好方法(抛出异常) 限定容量友好方法(返回null或false) Deque的堆栈方法,可以作为栈
boolean add(E e); boolean offer(E e); void push(E e);
void addLast(E e); boolean offerLast(E e); E pop();
void addFirst(E e); boolean offerFirst(E e);
E getFirst(); E peek(); / E peekFirst();
E getLast(); E peekLast();
E remove(); E poll();
E removeFirst(); E pollFirst();
E removeLast(); E pollLast();

代码实现:

  1. class MaxQueue {
  2. LinkedList<Integer> queue;
  3. LinkedList<Integer> deque;
  4. public MaxQueue() {
  5. queue=new LinkedList<>();//原来的队列
  6. deque=new LinkedList<>();//用来实现O(1)的max_value()找最大值的双端队列
  7. }
  8. public int max_value() {
  9. return deque.isEmpty()?-1:deque.peekFirst();
  10. }
  11. public void push_back(int value) {
  12. queue.offerLast(value);
  13. while (!deque.isEmpty()&&deque.peekLast()<value)//如果是 deque.peekLast()==value,则应该保留,不能弹出,因为可能是最大值(设计双向队列为 单调不增 的原因:若队列 queue 中存在两个 值相同的最大元素 ,此时 queue 和 deque 同时弹出一个最大元素,而 queue 中还有一个此最大元素;即采用单调递减将导致两队列中的元素不一致。)
  14. deque.pollLast();
  15. deque.offerLast(value);
  16. }
  17. public int pop_front() {
  18. if(queue.isEmpty()) return -1;
  19. if(queue.peekFirst().equals(deque.peekFirst()))//queue 里面保存的是 Integer 而非 int ,peek() 返回的是 Integer 类型,没有自动拆箱,因此需要用 equals() 来比
  20. deque.pollFirst();
  21. return queue.pollFirst();
  22. }
  23. }