剑指offer刷题记录

剑指offer

一、字符串

1、注意事项

2、例题

(1)168. Excel表列名称
  • 进展转换问题

    • 一般是0-25为26进制,但是题目中给出了1-26的映射关系,使用给出一个值转成字符表示的时候,每次取余的时候都要减一
    StringBuffer str = new StringBuffer(); // Java中StringBuffer带reverse()
    // C++中有这个reverse函数,不过要引#include
    for(num > 0){
        str += (num - 1) % 26 + "A";
        num = (num - 1) / 26;
    }
    str.reverse();
    

二、链表

1、注意事项

  • 创建单链表时要设置一个虚拟头节点,叫做哨兵(JZ18 删除链表的节点
  • 合理的利用数据结构可以解决链表中的问题(Stack、PriorityQueue、Set等)

2、例题

(1)JZ24 反转链表JZ22 链表中倒数最后k个结点
  • 双(快慢)指针(空间复杂度小)、栈
    • 倒数的k节点中,fast总要比slow快上k
(2)JZ25 合并两个排序的链表
  • 优先队列、比较法
(3)JZ52 两个链表的第一个公共结点

输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。

  • 双同速指针法:分别设置p1和p2指向两个链表,当p1走完链表1后让它从链表2开始,p2同理,这样它们走的长度就相等,如果有公共节点会相遇
    • 剑指offer刷题记录_第1张图片
(4)BM6 判断链表中是否有环

判断给定的链表中是否有环。如果有环则返回true,否则返回false

  • 快慢指针法:fast和slow从同一点开始,fast每次走两个,slow每次走一个,相遇就代表有环

    • 注意事项:while循环中的判断条件:fast != null && fast.next != null
  • 创建Set判断长度是否发生变化

  • 拓展JZ23 链表中环的入口结点

    • 按照BM6中的方法先判断有没有环,有的话(相遇)将fast再次放到头节点开始步长调整为1,再次相遇就是环的入口
(5)JZ35 复杂链表的复制

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。

  • 组合链表法:链表拼接、拆分
    • 先在源链表中添加,然后再拆分出来(当题目要求两个列表不能相同时,原链表也要恢复成原来的样子)

三、树

1、注意事项

  • 应该熟练掌握建树的操作(层次、前(后)序和中序)
  • 树的查找(遍历)和删除操作(腾讯音乐考过按层次删除)

2、例题

(1)JZ7 重建二叉树

给定节点数为 n 的二叉树的前序遍历和中序遍历结果,请重建出该二叉树并返回它的头结点。

  • 递归法建树

    • import java.util.*;
      public class Solution {
          public TreeNode reConstruct(int [] pre,int [] vin) {
              int n = pre.length;
              int m = vin.length;
              //每个遍历都不能为0
              if(n == 0 || m == 0) 
                  return null;
              //构建根节点
              TreeNode root = new TreeNode(pre[0]);
              for(int i = 0; i < vin.length; i++){
                  //找到中序遍历中的前序第一个元素
                  if(pre[0] == vin[i]){ 
                      //构建左子树
                      root.left = reConstruct(Arrays.copyOfRange(pre, 1, i + 1), Arrays.copyOfRange(vin, 0, i)); 
                      //构建右子树
                      root.right = reConstruct(Arrays.copyOfRange(pre, i + 1, pre.length), Arrays.copyOfRange(vin, i + 1, vin.length));
                      break;
                  }
              }
              return root;
          }
      }
      
(2)JZ26 树的子结构
  • 递归判断法
    • 找到节点,然后判断他俩的值、他俩的左节点和他俩的右节点
(3)JZ32 从上往下打印二叉树JZ27 二叉树的镜像
  • 层次遍历法
(4)JZ33 二叉搜索树的后序遍历序列
  • 分治/递归法
    • 先找到根节点,然后划分左右子树,判断是否左子树都小于根&&右子树大于根,然后递归判断左右子树
(5)JZ82、JZ34、JZ84 二叉树中和为某一值的路径
  • 深度优先搜素
    • JZ34中找所有符合要求的路径时,可以用一个栈来解决(由于Java中Collection类可以相互转换,类似list.add(new ArrayList(linkedList)),所以使用LinkedList代替栈)
(6)JZ36 二叉搜索树与双向链表
  • 借助栈来实现,将树按中序入栈,然后出栈修改指针(注意只有一个节点的情况)
(7)JZ8 二叉树的下一个结点

给定一个二叉树其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的next指针

  • 中序遍历返回右子树的第一个值
    • 右子树为空时,判断此节点是不是上一个节点的右节点
      • 是:判断上一个节点的上一个节点
        • 为空返回null
      • 否:返回上一个节点
(8)JZ68 二叉搜索树的最近公共祖先JZ86 在二叉树中找到两个节点的最近公共祖先
  • 二叉搜索树有顺序,要找p、q节点的最近公共祖先只需要找到一个val满足min(p,q)<=val&&val>=max(p,q)的节点就行了
  • 升级版——> JZ86,dfs+栈(保存路径)

四、栈&队列

1、注意事项

  • 灵活运用数据结构

2、例题

(1)JZ31 栈的压入、弹出序列
  • 模拟压栈和弹出的过程,当栈顶元素等于弹出序列时就弹出
(2)JZ9 用两个栈实现队列
  • 用stack1作为压入,stack2作为弹出。当进行入队时,stack1进行push;当出栈时,stack2若为空,stack1倒出来给stack2,若不为空,stack2直接pop
(3)JZ59 滑动窗口的最大值

给定一个长度为 n 的数组 nums 和滑动窗口的大小 size ,找出所有滑动窗口里数值的最大值。

如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。

要求:空间复杂度 O(n),时间复杂度 O(n)

  • 每次新加一个的时候都把前面比这个小的数换成这个数

五、搜索

1、注意事项

  • 熟练应用二分查找等技巧,二分查找,最好O(1),最坏O(logn)

2、例题

(1)JZ4 二维数组中的查找

在一个二维数组array中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

  • (推荐)线性表搜素,从左下角/右上角开始,因为从这两个角走可以区分值大小,O(M+N)
  • 二分查找,每一行找O(NlogM)(另一种复杂的找不会)
(2)JZ53 数字在升序数组中出现的次数

给定一个长度为 n 的非降序数组和一个非负数整数 k ,要求统计 k 在数组中出现的次数

要求:空间复杂度 O(1),时间复杂度 O(logn)

  • 二分法找到一个比k大0.5的,再找一个比k小0.5的
    • 用0.5的原因,因为都是整数,防止:array = [3],k=4的情况,bin(k-1)为0,bin(k+1)为1,而bin(k-0.5)为1,bin(k+0.5)为1
// 二分查找
public int bin(int [] array , double k){
        int low = 0;
        int high = array.length -1;
        while(low <= high){
           int mid = (high + low) / 2 ;
           if(array[mid] > k){
               high = mid - 1;
           }else if(array[mid] < k){
               low = mid + 1;
           }else return mid;
        }
        return low;
    }
(3)JZ11 旋转数组的最小数字

有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。

空间复杂度:O*(1) ,时间复杂度:O(logn)*

  • 二分查找
    • 虽然不是有序的,但找到mid后和左右的比较,可以确定就接着分,不能确定就挨个遍历
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-usR7x8ja-1659513458073)(https://uploadfiles.nowcoder.com/images/20211207/397721558_1638878411855/299059EFCD5648D6783E12C1C94BEF4F)]
  • 遍历
    • 用第一个和最后一个比较,如果小于,直接返回;否则,找到小于他的那个返回
(4)JZ38 字符串的排列

输入一个长度为 n 字符串,打印出该字符串中字符的所有排列,你可以以任意顺序返回这个字符串数组。

例如输入字符串ABC,则输出由字符A,B,C所能排列出来的所有字符串ABC,ACB,BAC,BCA,CBA和CAB。

  • 递归+回溯
    • 在递归中加入一个标志数组,循环时先将arr[i]设为1,传入递归函数,再重置为0;
    • 若传入与下一次有联系,可以用一个linkedlist(因为可以移除最后一个元素)作为前缀传入;
    • 若担心有重复,可以声明成set来保存全排列的数据
(5)JZ44 数字序列中某一位的数字

数字以 0123456789101112131415… 的格式作为一个字符序列,在这个序列中第 2 位(从下标 0 开始计算)是 2 ,第 10 位是 1 ,第 13 位是 1 ,以此类题,请你输出第 n 位对应的数字。

数据范围: 0 < n < 10^9

  • 计算区间的长度,从1开始的,1-9是9位,10-99是180位,……。找出n在的区间(用n每次比较减去小的那个区间值),找出对应区间,tmp等于用剩余的n_除以区间的位数,得到对应的值(tmp不为0且余数不为1加1) ,找到对应位置的值

六、动态规划

1、注意事项

  • 找规律

2、例题

(1)JZ42 连续子数组的最大和

输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,子数组最小长度为1。求所有子数组的和的最大值。

  • 经典动态规划,max(dp[i-1]+array[i],array[i])
(2)JZ85 连续子数组的最大和(二)

输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,找到一个具有最大和的连续子数组。

  • 在上面的基础上加几个字段来记录数组的起始和终止位置,begin,end用来保存最大最长的位置,begin_,end_用来保存每次的新数组
  • Arrays.copyOfRange(array,begin,end+1)来截断数组
(4)JZ71 跳台阶扩展问题

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶(n为正整数)总共有多少种跳法。

  • 跳台阶的拓展
    • 跳台阶,每一阶都等于前两节的和,JZ69 跳台阶
    • 这个可以从任意一个跳上来,用前面台阶的跳法之和+1
(5)JZ63 买卖股票的最好时机(一)

假设你有一个数组prices,长度为n,其中prices[i]是股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益

1.你可以买入一次股票和卖出一次股票,并非每天都可以买入或卖出一次,总共只能买入和卖出一次,且买入必须在卖出的前面的某一天

2.如果不能获取到任何利润,请返回0

3.假设买入卖出均无手续费

  • 声明一个min保存最小值,一个profit保存利润,遍历数组:当有值比min小的时候,min等于那个值;当有值大于min时,计算该值与min的差,比较是否大于当前profit;最后返回profit
(6)JZ47 礼物的最大价值

在一个m×n的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

如输入这样的一个二维数组,
[[1,3,1],
[1,5,1],
[4,2,1]]

那么路径 1→3→5→2→1 可以拿到最多价值的礼物,价值为12

  • 记录每个位置的值,为max(grid[i][j]+up,grid[i][j]+left),返回max
(7)JZ48 最长不含重复字符的子字符串

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

  • 滑动窗口+哈希表(hashmap)
    • 当遇到重复值时,比较与最大的长度,hash表清空从重复值的下一个开始
  • 动态规划+哈希表
    • 用hashmap,当出现重复时,当前位置 = 前一个位置 - 重复位置的长度
(8)JZ46 把数字翻译成字符串

有一种将字母编码成数字的方式:‘a’->1, ‘b->2’, … , ‘z->26’。

我们把一个字符串编码成一串数字,再考虑逆向编译成字符串。

由于没有分隔符,数字编码成字母可能有多种编译结果,例如 11 既可以看做是两个 ‘a’ 也可以看做是一个 ‘k’ 。但 10 只可能是 ‘j’ ,因为 0 不能编译成任何结果。

现在给一串数字,返回有多少种可能的译码结果

  • 动态规划
    • 判断这一个数字和前一个数字组合起来是不是小于26,小于的话dp[i]=dp[i-1] + dp[i-2](等于前i-1个组合+第i个 和 前i-2个组合+第i-1与i-2个)
      • 当i-2<0时,dp[i]=dp[i-1] + 1
      • 当第i个为0时,只有这一个数字和前一个数字组合起来小于26才有效,dp[i]=dp[i-1];当大于26时没有意义,返回0
      • 当i-1为0时,dp[i]=dp[i-1]

七、回溯

1、注意事项

  • 回溯一般和dfs结合,先建立相同的地图数组,然后找到一个点开始判断是否可行,可以就下一步,不行就返回并重置地图

2、例题

(1)JZ12 矩阵中的路径

请设计一个函数,用来判断在一个n乘m的矩阵中是否存在一条包含某长度为len的字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如

a b c e

s f c s

a d e e

矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

数据范围:0≤n,m≤20 ,1≤len≤25

  • 先找到开始的那个位置,然后开始dfs回溯

八、排序

1、注意事项

  • 注意各种排序算法的时间复杂度

    算法 时间复杂度 最好 最坏 稳定性 空间
    插入排序 O(n^2) O(n) O(n^2) 稳定
    冒泡排序 O(n^2) O(n) O(n^2) 稳定
    归并排序 O(nlogn) O(nlogn) O(nlogn) 稳定 O(n)
    堆排序 O(nlogn) O(nlogn) O(nlogn) 不稳定
    快速排序 O(nlogn) O(nlogn) O(n^2) 不稳定

2、例题

(1)JZ51 数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P mod 1000000007

要求:空间复杂度 O*(n),时间复杂度 O(nlogn)

  • 归并排序解决
(2)JZ40 最小的K个数

给定一个长度为 n 的可能有重复值的数组,找出其中不去重的最小的 k 个数。例如数组元素是4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4(任意顺序皆可)。

要求:空间复杂度 O*(*n) ,时间复杂度 O(nlogn)

  • 堆排序、归并排序、快速排序
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VR3UrkIS-1659513458073)(https://uploadfiles.nowcoder.com/images/20210722/397721558_1626945012109/6A105C4B5BE11C9FE59934C5B4E772BF)]

九、位运算

1、基础知识

  • 计算机中存储的都是补码

  • 正数的补码是他本身,负数的补码为反码加1(这里值我们看到的实际的负数,在计算机中其实本身就是存的补码)

  • ~按位取反,^异或,&与,|

    • 按位取反计算公式:~x = -x - 1
    • 注意,按位取反和反码的区别,正数也可以按位取反,但是正数的反码是他本身
      • 5 = 0000 0000 0000 0000 0000 0000 0000 0101
      • 5的反码 = 0000 0000 0000 0000 0000 0000 0000 0101
      • ~5 = 1111 1111 1111 1111 1111 1111 1111 1010
        • 将其视为某个数的补码,-1为反码:1111 1111 1111 1111 1111 1111 1111 1001
        • 取反得到源码: 1000 0000 0000 0000 0000 0000 0000 0110 = -6 (符号位不变)
        • 相当于套公式:~5 = -5 - 1
  • <<左移(乘以2), >>有符号右移(除2,左边补符号), >>>无符号右移(除2,左边补0)

  • 加减乘除

    • 加法:先通过异或不带进位的加,在通过与的方式添加进位(JZ65 不用加减乘除做加法

      • // x + y
        while(y>0){ // 是否有进位
            int tem = x ^ y; // 不进位的加
            y = (x & y) << 1; // y保存进位并左移一位
            x = tem; // x保存本次结果
        }
        
    • 减法:将被减数变为负数(被减数变为其相反数,即需要计算机存这个数相反数的补码,按位取反加一)

      • // x - y
        y = (~y) + 1;
        add(x,y);
        
    • 乘法:先转为正数,通过多次加法实现乘法的效果,再通过是否同号决定符号

    • 除法:同上,不停的减去被减数,直到减数小于被减数,记录次数

2、例题

(1)JZ15 二进制中1的个数

输入一个整数 n ,输出该数32位二进制表示中1的个数。其中负数用补码表示。

  • 用一个1不断的左移,测试每一位是不是1
  • 当n是负数时,请注意,计算机中本身就是存的负数的补码

十、模拟

1、注意事项

2、例题

(1)JZ29 顺时针打印矩阵

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵:

[[1,2,3,4],
[5,6,7,8],
[9,10,11,12],
[13,14,15,16]]

则依次打印出数字

[1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10]

数据范围:

0 <= matrix.length <= 100

0 <= matrix[i].length <= 100

  • 方向数组+边界判断

  • public ArrayList<Integer> printMatrix(int [][] matrix) {
        
            int n = matrix.length;
            if(n == 0) return new ArrayList<>();
            int m = matrix[0].length;
            int[][] dir = {{0,1},{1,0},{0,-1},{-1,0}}; // 右下左上
            ArrayList<Integer> ans = new ArrayList<>();
            int pos = 0;
            int i = 0;
            int j = 0;
            while(i  >= 0 && j>=0 && i < n && j < m && matrix[i][j] != -1 ){
                ans.add(matrix[i][j]);
                matrix[i][j] = -1;
                if( i + dir[pos][0] >= 0 && j+ dir[pos][1]>=0 && i+ dir[pos][0] < n && j+ dir[pos][1] < m && matrix[i+ dir[pos][0]][j+ dir[pos][1]] != -1){
                    i += dir[pos][0];
                    j += dir[pos][1];
                }else{
                    pos = (pos + 1) % 4;
                    i += dir[pos][0];
                    j += dir[pos][1];
                }
            }
            return ans;
       
        }
    

十一、其他算法

1、注意事项

  • 这些题大多是用一些技巧或者思想,需要记忆

2、例题

(1)JZ39 数组中出现次数超过一半的数字

给一个长度为 n 的数组,数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

例如输入一个长度为9的数组[1,2,3,2,2,2,5,4,2]。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。

数据范围:n≤50000,数组中元素的值 100000≤val≤10000

要求:空间复杂度:O*(1),时间复杂度O(*n)

  • 排序法,时间复杂度O(nlogn),空间O(1)
public int MoreThanHalfNum_Solution(int [] array) {
        Arrays.sort(array);
        int ans = array[0];
        int count = 1;
        for(int i = 1;i<array.length;i++){
            if(array[i] == array[i-1]){
                count++;
                if(array.length / 2 < count){
                    ans = array[i];
                }
            }else{
                count = 1;
            }
        }
        return ans;
    }

  • 候选法(投票法),时间复杂度O(n),空间O(1),太牛了
    • 众数一定大于数组的长度的一半
    • 如果两个数不相等,就消去这两个数,最坏情况下,每次消去一个众数和一个非众数,那么如果存在众数,最后留下的数肯定是众数。
public int MoreThanHalfNum_Solution(int [] array) {
        int cond = array[0];
        int num = 1;
        for(int i = 1;i<array.length;i++){
            if(num == 0){
                cond = array[i];
                num = 1;
            }else if(array[i]!=cond){
                num--;
            }else{
                num++;
            }
        }
        return cond;
    }

(2)JZ16 数值的整数次方

实现函数 double Power(double base, int exponent),求base的exponent次方。

注意:

1.保证base和exponent不同时为0。

2.不得使用库函数,同时不需要考虑大数问题

3.有特殊判题,不用考虑小数点后面0的位数。

数据范围: ∣base∣≤100 ,∣exponent∣≤100 ,保证最终结果一定满足 ∣val∣≤104
空间复杂度 O(1),时间复杂度 O(n)

  • 快速幂:当 y y y是偶数时 x y = x y / 2 ∗ x y / 2 x^y=x^{y/2}*x^{y/2} xy=xy/2xy/2,当 y y y是奇数时 x y = x y / 2 ∗ x y / 2 ∗ x x^y=x^{y/2}*x^{y/2}*x xy=xy/2xy/2x
    • 坑点:y可能是负数,记得转换
    • 时间复杂度:O(log2n),其中n为所求的次方数,快速幂相当于对求幂使用二分法
    • 空间复杂度:O(1),常数级变量,无额外辅助空间
public double Power(double base, int exponent) {
        if(exponent == 0) return 1;
        if(exponent < 0) {
            base = 1. / base;
            exponent = - exponent;
        }
        if(exponent % 2 == 0){
            return Power(base , exponent / 2) * Power(base , exponent / 2);
        }else{
            return Power(base , exponent / 2) * Power(base , exponent / 2) * base;
        }
  }

可能会更新

你可能感兴趣的:(笔试复习,链表,数据结构)