剑指offer
目录
之前做的时候解析全写代码里了,贴一下repo。之后再刷或再遇到的话还是摘出来好好总结一下吧。
repo:https://github.com/Silincee/LeetRinCode/tree/master/src/leetcode/editor/cn
题解
#剑指 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 )。因而,就能通过索引映射对应的值,起到与字典等价的作用。
遍历中,第一次遇到数字 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 的子结构,需完成以下两步工作:
- 先序遍历A中的每个节点$n_a$,对应函数
isSubStructure(A, B)
- 判断A中以$n_a$为根节点的子树是否包含树B,对应函数
recur(A, B)
代码:
//先序遍历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"]
分析:
方法一:
如果没有重复字符,我们可以选择逐个添加的方式,回溯求解,在没有添加过的字符里面进行选择和添加。那么这一题不一样,字符会出现重复,回溯剪枝时需要注意,如果选择新字符后的串之前已经遍历过了,直接跳过,注意把新字符从选择列表中移除。
⭐️ 方法二:
因此该题更适合用字符交换的方式求解【方法二】。字符串可以包含重复的字符,因此返回数组的长度我们不得而知,直接交换最终得到的字符串也会重复,要去重复可以用set容器来存储得到的结果,最后返回结果集的数组形式即可。
我们对字符串中的字符进行两两交换,可以得到所有结果,这种交换同样是做出选择的方式:
首先在第一个位置,选择所有可能的字符,这里面就有len种选择,然后在每一种选择里面将第一个位置固定,第二个位置又有len-1种选择,在每一种选择里面又将第二个位置固定,每一种的第三个位置又有len-2种选择,如此往复,直到最后一个位置。
加上该算法的部分导图,大家可以结合代码看一下:
如何剪枝:当字符串存在重复字符时,排列方案中也存在重复的排列方案。为排除重复方案,需在固定某位字符时,保证 “每种字符只在此位固定一次” ,即遇到重复字符时不交换,直接跳过。
即在每一层记录一个已遍历过的字符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。
分析:
方法一:动态规划
代码:
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"
分析:
方法一:自定义字符串排序
代码:
// 快排
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
分析:
方法一: 和 = 无进位和 + 进位
代码:
public int add(int a, int b) { // a无进位和 b进位
if (b == 0) { // 没有进位后退出递归
return a;
}
// 转换成非进位和 + 进位
return add(a ^ b, (a & b) << 1);
}
既已览卷至此,何不品评一二: