目录

之前做的时候解析全写代码里了,贴一下repo。之后再刷或再遇到的话还是摘出来好好总结一下吧。

repo:https://github.com/Silincee/LeetRinCode/tree/master/src/leetcode/editor/cn

题目 算法思想
剑指 Offer 03 数组中重复的数字  
剑指 Offer 04 二维数组中的查找  
剑指 Offer 05 替换空格  
剑指 Offer 06 从尾到头打印链表  
剑指 Offer 07 重建二叉树  
剑指 Offer 09 用两个栈实现队列  
剑指 Offer 10- I 斐波那契数列  
剑指 Offer 10- II 青蛙跳台阶问题  
剑指 Offer 11 旋转数组的最小数字  
剑指 Offer 12 矩阵中的路径  
剑指 Offer 13 机器人的运动范围  
剑指 Offer 14- I 剪绳子  
剑指 Offer 14- II 剪绳子 II  
剑指 Offer 15 二进制中1的个数  
剑指 Offer 16 数值的整数次方  
剑指 Offer 17 打印从1到最大的n位数  
剑指 Offer 18 删除链表的节点  
剑指 Offer 19 正则表达式匹配  
剑指 Offer 20 表示数值的字符串  
剑指 Offer 21 调整数组顺序使奇数位于偶数前面  
剑指 Offer 22 链表中倒数第k个节点  
剑指 Offer 24 反转链表  
剑指 Offer 25 合并两个排序的链表  
剑指 Offer 26 树的子结构 先序遍历 + 包含判断
剑指 Offer 27 二叉树的镜像  
剑指 Offer 28 对称的二叉树 递归
剑指 Offer 29 顺时针打印矩阵  
剑指 Offer 30 包含min函数的栈  
剑指 Offer 31 栈的压入、弹出序列  
剑指 Offer 32 - I 从上到下打印二叉树  
剑指 Offer 32 - II 从上到下打印二叉树 II  
剑指 Offer 32 - III 从上到下打印二叉树 III  
剑指 Offer 33 二叉搜索树的后序遍历序列  
剑指 Offer 34 二叉树中和为某一值的路径  
剑指 Offer 35 复杂链表的复制  
剑指 Offer 36 二叉搜索树与双向链表  
剑指 Offer 37 序列化二叉树  
剑指 Offer 38 字符串的排列 回溯/两两交换
剑指 Offer 39 数组中出现次数超过一半的数字  
剑指 Offer 40 最小的k个数  
剑指 Offer 41 数据流中的中位数  
剑指 Offer 42 连续子数组的最大和 动态规划
剑指 Offer 43 1~n 整数中 1 出现的次数  
剑指 Offer 44 数字序列中某一位的数字  
剑指 Offer 45 把数组排成最小的数 ⭐️ 自定义排序
剑指 Offer 46 把数字翻译成字符串  
剑指 Offer 47 礼物的最大价值  
剑指 Offer 48 最长不含重复字符的子字符串  
剑指 Offer 49 丑数  
剑指 Offer 50 第一个只出现一次的字符  
剑指 Offer 51 数组中的逆序对  
剑指 Offer 52 两个链表的第一个公共节点  
剑指 Offer 53 - I 在排序数组中查找数字 I  
剑指 Offer 53 - II 0~n-1中缺失的数字  
剑指 Offer 54 二叉搜索树的第k大节点  
剑指 Offer 55 - I 二叉树的深度  
剑指 Offer 55 - II 平衡二叉树  
剑指 Offer 56 - I 数组中数字出现的次数  
剑指 Offer 56 - II 数组中数字出现的次数 II  
剑指 Offer 57 和为s的两个数字  
剑指 Offer 57 - II 和为s的连续正数序列  
剑指 Offer 58 - I 翻转单词顺序  
剑指 Offer 58 - II 左旋转字符串  
剑指 Offer 59 - I 滑动窗口的最大值  
剑指 Offer 59 - II 队列的最大值  
剑指 Offer 60 n个骰子的点数  
剑指 Offer 61 扑克牌中的顺子  
剑指 Offer 62 圆圈中最后剩下的数字  
剑指 Offer 63 股票的最大利润  
剑指 Offer 64 求1+2+…+n  
剑指 Offer 65 不用加减乘除做加法 ⭐️ 位运算
剑指 Offer 66 构建乘积数组  
剑指 Offer 67 把字符串转换成整数  
剑指 Offer 68 - I 二叉搜索树的最近公共祖先  
剑指 Offer 68 - II 二叉树的最近公共祖先  

题解

#剑指 Offer 03. 数组中重复的数字

  • 中等
  • 2021.04.13:

题目:

找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3 

分析:

题目说明尚未被充分使用,即 在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内 。 此说明含义:数组元素的 索引 和 值 是 一对多 的关系。

因此,可遍历数组并通过交换操作,使元素的 索引 与 值 一一对应(即 nums[i] =i )。因而,就能通过索引映射对应的值,起到与字典等价的作用。

Picture0.png

遍历中,第一次遇到数字 x 时,将其交换至索引 x 处;而当第二次遇到数字 x 时,一定有 nums[x] =x ,此时即可得到一组重复数字。

代码:

public int findRepeatNumber(int[] nums) {
  int i = 0;
  while(i < nums.length) {
    if(nums[i] == i) {
      i++;
      continue;
    }
    if(nums[nums[i]] == nums[i]) return nums[i];
    int tmp = nums[i];
    nums[i] = nums[tmp];
    nums[tmp] = tmp;
  }
  return -1;
}

#剑指 Offer 05. 替换空格

  • 简单
  • 2021.04.13:

题目:

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

示例 1:
输入:s = "We are happy."
输出:"We%20are%20happy."

分析:

Java 语言中,字符串都被设计成「不可变」的类型,即无法直接修改字符串的某一位字符,需要新建一个字符串实现。

遍历列表 s 中的每个字符 c :

  • 当 c 为空格时:向 res 后添加字符串 “%20” ;

  • 当 c不为空格时:向 res 后添加字符 c ;

  • 将列表 res 转化为字符串并返回。

代码:

public String replaceSpace(String s) {
  StringBuilder res = new StringBuilder();
  for(Character c : s.toCharArray())
  {
    if(c == ' ') res.append("%20");
    else res.append(c);
  }
  return res.toString();
}

剑指 Offer 26. 树的子结构

  • 中等
  • 2022.01.07:

题目:

输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
例如:
给定的树 A:

     3
    / \
   4   5
  / \
 1   2
给定的树 B:
   4 
  /
 1
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。

示例 1:
输入:A = [1,2,3], B = [3,1]
输出:false
示例 2:
输入:A = [3,4,5,1,2], B = [4,1]
输出:true

解析:

若树 B 是树 A 的子结构,则子结构的根节点可能为树 A 的任意一个节点。因此,判断树 B 是否是树 A 的子结构,需完成以下两步工作:

  1. 先序遍历A中的每个节点$n_a$,对应函数isSubStructure(A, B)
  2. 判断A中以$n_a$为根节点的子树是否包含树B,对应函数recur(A, B)

image-20220107212939622

代码:

//先序遍历A(recur就是对根节点的操作,相当于打印,然后依次操作左右子节点)
public boolean isSubStructure(TreeNode A, TreeNode B) {
  if(A==null || B==null) return false;// 当树A为空或树B为空时,直接返回 false;
  //若树 B 是树 A 的子结构,则必满足以下三种情况之一:
  //节点A为根节点的子树包含树B;树B是树A的子结构;树B是树A的子结构
  return recur(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
}

//判断A中以n_a为根节点的子树是否包含树B
boolean recur(TreeNode A, TreeNode B) {
  if(B == null) return true; //当节点 B 为空:说明树 B 已匹配完成(越过叶子节点)
  //当节点 A 为空:说明已经越过树 A 叶子节点,即匹配失败;当节点 A 和 B 的值不同:说明匹配失败
  if(A == null || A.val != B.val) return false; 
  //判断 A 和 B 的左子节点是否相等 && 判断 A 和 B 的右子节点是否相等
  return recur(A.left, B.left) && recur(A.right, B.right);
}

剑指 Offer 28. 对称的二叉树

  • 简单
  • 2021.04.13:

题目:

请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

    1
   / \
  2   2
 / \ / \
3  4 4  3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
    1
   / \
  2   2
   \   \
   3    

代码:

public boolean isSymmetric(TreeNode root) {
  return root == null ? true : recur(root.left, root.right);
}
boolean recur(TreeNode L, TreeNode R) {
  if(L == null && R == null) return true;
  if(L == null || R == null || L.val != R.val) return false;
  return recur(L.left, R.right) && recur(L.right, R.left);
}

#剑指 Offer 38. 字符串的排列

  • 中等
  • 2021.04.13:

题目:

输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

示例:
输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]

分析:

方法一:

如果没有重复字符,我们可以选择逐个添加的方式,回溯求解,在没有添加过的字符里面进行选择和添加。那么这一题不一样,字符会出现重复,回溯剪枝时需要注意,如果选择新字符后的串之前已经遍历过了,直接跳过,注意把新字符从选择列表中移除。

IMG_7676D34FDF79-1

⭐️ 方法二:

因此该题更适合用字符交换的方式求解【方法二】。字符串可以包含重复的字符,因此返回数组的长度我们不得而知,直接交换最终得到的字符串也会重复,要去重复可以用set容器来存储得到的结果,最后返回结果集的数组形式即可。

我们对字符串中的字符进行两两交换,可以得到所有结果,这种交换同样是做出选择的方式:

首先在第一个位置,选择所有可能的字符,这里面就有len种选择,然后在每一种选择里面将第一个位置固定,第二个位置又有len-1种选择,在每一种选择里面又将第二个位置固定,每一种的第三个位置又有len-2种选择,如此往复,直到最后一个位置。

加上该算法的部分导图,大家可以结合代码看一下:

image-20220103153629885

如何剪枝:当字符串存在重复字符时,排列方案中也存在重复的排列方案。为排除重复方案,需在固定某位字符时,保证 “每种字符只在此位固定一次” ,即遇到重复字符时不交换,直接跳过。

即在每一层记录一个已遍历过的字符Set。

代码:

// 方法一
private ArrayList<String> res = new ArrayList<>(); // 结果集
private Set<String> set = new HashSet<>(); // 保存已遍历过的前缀,用于剪枝

private List<String> permutation(String s) {
  backtrack(s.toCharArray(), new LinkedList<Integer>());
  String[] ans = new String[res.size()];
  for(int i=0;i<res.size();i++){
    ans[i] = res.get(i);
  }
  return ans;
}

// list : 当前路径,已遍历过的字符下标
private void backtrack(char[] arr, LinkedList<Integer> list) {
  // 回溯终止条件
  if (list.size() == arr.length) {
    res.add(getString(arr,list));
    return;
  }

  // 遍历可选列表
  for (int i = 0; i < arr.length; i++) {
    if (!list.contains(i)) {
      // 做选择
      list.add(i);
      // 判断是否要开启后续递归(当前串是否为遍历过的前缀)
      String temp = getString(arr, list);
      if (set.contains(temp)){
        list.removeLast();
        continue;
      }
      set.add(temp);
      
      backtrack(arr, list);
      
      // 撤回选择
      list.removeLast();
    }
  }
}

// 将已遍历过的字符转换为字符串
private String getString(char[] arr, LinkedList<Integer> list){
  StringBuilder builder = new StringBuilder();
  for (int i : list) {
    builder.append(arr[i]);
  }
  return builder.toString();
}

// 方法二 两两交换
private ArrayList<String> res = new ArrayList<>(); // 结果集

private List<String> permutation(String s) {
  backtrack(s.toCharArray(),  0);
  return res;
}

// index: 当前交换字符的下标
private void backtrack(char[] arr, int index){
  if (index == arr.length-1) {
    res.add(new String(arr));
    return;
  }

  // 为排除重复方案,需在固定某位字符时,保证 “每种字符只在此位固定一次” ,即遇到重复字符时不交换,直接跳过。
  HashSet<Character> set = new HashSet<>();
  // 选择列表:当前字符可与 自己和之后的字符进行交换
  for (int i = index; i < arr.length; i++) {
    if (set.contains(arr[i])) continue;
    set.add(arr[i]);

    swap(arr,index,i);
    backtrack(arr,index+1);
    swap(arr,index,i);
  }

}

private void  swap(char[] arr,int i,int j){
  char temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

#剑指 Offer 42. 连续子数组的最大和

  • 简单
  • 2022.02.11:

题目:

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。

示例1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

分析:

方法一:动态规划

image-20220211125833037

代码:

public int maxSubArray(int[] nums) {
  int[] dp = new int[2];
  int res = Integer.MIN_VALUE;
  for(int i = 0;i<nums.length;i++){
    dp[0] = dp[1];
    dp[1] = Math.max(dp[0]+nums[i],nums[i]);
    res = dp[1]>res?dp[1]:res;
  }
  return res;
}

#剑指 Offer 45 把数组排成最小的数

  • 中等
  • 2021.04.13:

题目:

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。


示例 1:
输入: [10,2]
输出: "102"
示例 2:
输入: [3,30,34,5,9]
输出: "3033459"

分析:

方法一:自定义字符串排序

Picture1.png

代码:

// 快排
Random random = new Random();
public String minNumber(int[] nums) {
  String[] strArr = new String[nums.length];
  for(int i=0;i<nums.length;i++){
    strArr[i] = String.valueOf(nums[i]);
  }
  // 自定义快速排序
  quickSort(strArr,0,nums.length-1);
  StringBuilder builder = new StringBuilder();
  for(String s:strArr){
    builder.append(s);
  }
  return builder.toString();

}

public void quickSort(String[] arr, int start, int end) {
  if (start >= end) return;

  int left = start;
  int right = end;
  // TODO 随机枢纽
  int i =random.nextInt(end - start + 1)+start;
  String temp = arr[left];
  arr[left]=arr[i];
  arr[i]=temp;

  String pivot = arr[left];

  while (left < right) { // 3, 30, 34, 5, 9
    while ((pivot + arr[right]).compareTo(arr[right] + pivot) <= 0 && left < right) {
      right--;
    }
    arr[left] = arr[right];

    while ((pivot+arr[left]).compareTo( arr[left]+pivot) >= 0 && left < right) {
      left++;
    }
    arr[right] = arr[left];
  }
  // 复原中枢点
  arr[left] = pivot;
  quickSort(arr, start, left - 1);
  quickSort(arr, left + 1, end);
}

// JDK 比较器
public String minNumber(int[] nums) {
  String[] strArr = new String[nums.length];
  for(int i=0;i<nums.length;i++){
    strArr[i] = String.valueOf(nums[i]);
  }

  Arrays.sort(strArr,(x,y)->(x+y).compareTo(y+x));
  StringBuilder builder = new StringBuilder();
  for(String s:strArr){
    builder.append(s);
  }
  return builder.toString();
}

#剑指Offer 65. 不用加减乘除做加法

  • 简单
  • 2021.03.20:

题目:

写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。

示例:
输入: a = 1, b = 1
输出: 2

分析:

方法一: 和 = 无进位和 + 进位

image-20210321131545164

代码:

public int add(int a, int b) { // a无进位和 b进位
  if (b == 0) { // 没有进位后退出递归
    return a;
  }

  // 转换成非进位和 + 进位
  return add(a ^ b, (a & b) << 1);
}