剑指 Offer 专项突击版刷题笔记
第 1 天 整数
剑指 Offer II 001. 整数除法
一开始我是直接暴力减法,力扣给出的解答错误,但是没说具体哪错了
class Solution {
private:
int sign(int x) {
if (x > 0) return 1;
else if (x < 0) return -1;
else return 0;
}
public:
int divide(int a, int b) {
if (b == 0) return 0;
// a = 15 b = 2 输出 7 余 1
// a = 7 b = -3 输出 -2 余 1
// a = -10 b = -3 输出 3 余 -1
// a = -10 b = 3 输出 -3 余 1
// 个人感觉规律应该是
// 一直减,直到 abs(a) < abs(b)
int ans_sign = sign(a) * sign(b);
a = abs(a);
b = abs(b);
int ans = 0;
while (a >= b) {
a -= b;
++ans;
}
return ans_sign * ans;
}
};
然后我就在每次尝试的时候都是用 b , b ∗ 2 , b ∗ 4 b,b*2,b*4 b,b∗2,b∗4 这样去尝试减的
这样的时间复杂度应该能到 l o g 2 n log_2n log2n
class Solution {
private:
int sign(int x) {
if (x > 0) return 1;
else if (x < 0) return -1;
else return 0;
}
public:
int divide(int a, int b) {
if (b == 0) return 0;
// a = 15 b = 2 输出 7 余 1
// a = 7 b = -3 输出 -2 余 1
// a = -10 b = -3 输出 3 余 -1
// a = -10 b = 3 输出 -3 余 1
// 个人感觉规律应该是
// 一直减,直到 abs(a) < abs(b)
int ans_sign = sign(a) * sign(b);
a = abs(a);
b = abs(b);
int ans = 0;
while (a >= b) {
for (int i = 0; i < 32; ++i) {
if (a >= (b << i)) continue;
else if (i > 0) {
a -= (b << (i - 1));
ans += (1 << (i - 1));
break;
}
}
}
return ans_sign * ans;
}
};
报错:
输入:
-2147483648
-1
输出:
0
预期结果:
2147483647
因为这个算例,我就改成了,在对 a 取绝对值的时候,如果 a 是 INT_MIN,那么转换成一个 INT_MAX 和一个 res
class Solution {
private:
int sign(int x) {
if (x > 0) return 1;
else if (x < 0) return -1;
else return 0;
}
public:
int divide(int a, int b) {
if (b == 0) return 0;
// a = 15 b = 2 输出 7 余 1
// a = 7 b = -3 输出 -2 余 1
// a = -10 b = -3 输出 3 余 -1
// a = -10 b = 3 输出 -3 余 1
// 个人感觉规律应该是
// 一直减,直到 abs(a) < abs(b)
int ans_sign = sign(a) * sign(b);
// -2147483648 经过 abs 之后还是 -2147483648
int res = 0;
if (a == INT_MIN) {
a = INT_MAX;
res = 1;
}
else {
a = abs(a);
}
// 什么东西被 -2147483648 除都等于 0
if (b == INT_MIN) {
return 0;
}
else {
b = abs(b);
}
int ans = 0;
int tmp = 0;
while (a >= b) {
// 可能需要 a -= 1 << 31
// 因此需要 i 能取到 32
// 但是问题是 1 << 32 == 0
// 1 << 31 == -2147483648
// 对于 1 是如此,对于其他数更加不知道左移多少位的时候会变负
// 所以不强求取到 b * 2^31,而是额外要求左移之后不能为负
for (int i = 0; i <= 32; ++i) {
if ((b << i) > 0 && a >= (b << i)) continue;
else if (i > 0) {
a -= (b << (i - 1));
ans += (1 << (i - 1));
break;
}
}
// 把 INT_MIN 转换到正数丢失的部分加回来
if (res > 0 && a <= INT_MAX - res) {
a += res;
res = 0;
}
}
// 如果除法结果溢出
if (ans < 0) return INT_MAX;
return ans_sign * ans;
}
};
之后发现力扣是不允许在计算的时候溢出的
于是改成在计算之前判断
class Solution {
private:
int sign(int x) {
if (x > 0) return 1;
else if (x < 0) return -1;
else return 0;
}
public:
int divide(int a, int b) {
if (b == 0) return 0;
// a = 15 b = 2 输出 7 余 1
// a = 7 b = -3 输出 -2 余 1
// a = -10 b = -3 输出 3 余 -1
// a = -10 b = 3 输出 -3 余 1
// 个人感觉规律应该是
// 一直减,直到 abs(a) < abs(b)
int ans_sign = sign(a) * sign(b);
// -2147483648 经过 abs 之后还是 -2147483648
int res = 0;
if (a == INT_MIN) {
a = INT_MAX;
res = 1;
}
else {
a = abs(a);
}
// 什么东西被 -2147483648 除都等于 0
if (b == INT_MIN) {
return 0;
}
else {
b = abs(b);
}
int ans = 0;
int tmp = 0;
while (a >= b) {
// 可能需要 a -= 1 << 31
// 因此需要 i 能取到 32
// 但是问题是 1 << 32 == 0
// 1 << 31 == -2147483648
// 对于 1 是如此,对于其他数更加不知道左移多少位的时候会变负
// 所以不强求取到 b * 2^31,而是额外要求左移之后不能为负
for (int i = 0; i <= 32; ++i) {
if ((b << i) > 0 && a >= (b << i)) continue;
else if (i > 0) {
a -= (b << (i - 1));
// 在这里可能溢出
if (ans > INT_MAX - (1 << (i - 1))) return INT_MAX;
else ans += (1 << (i - 1));
break;
}
}
// 把 INT_MIN 转换到正数丢失的部分加回来
if (res > 0 && a <= INT_MAX - res) {
a += res;
res = 0;
}
}
return ans_sign * ans;
}
};
但是不知道为什么他还是会提示未知错误
之后有提示了
输入:
-2147483648
1
输出:
2147483647
预期结果:
-2147483648
这些特殊情况……我觉得我的考虑已经很好了……
于是我单独取出来
class Solution {
private:
int sign(int x) {
if (x > 0) return 1;
else if (x < 0) return -1;
else return 0;
}
public:
int divide(int a, int b) {
if (b == 0) return 0;
// a = 15 b = 2 输出 7 余 1
// a = 7 b = -3 输出 -2 余 1
// a = -10 b = -3 输出 3 余 -1
// a = -10 b = 3 输出 -3 余 1
// 个人感觉规律应该是
// 一直减,直到 abs(a) < abs(b)
int ans_sign = sign(a) * sign(b);
// 特殊情况
if (a == INT_MIN && b == 1) return INT_MIN;
if (a == INT_MIN && b == INT_MIN) return 1;
// -2147483648 经过 abs 之后还是 -2147483648
int res = 0;
if (a == INT_MIN) {
a = INT_MAX;
res = 1;
}
else {
a = abs(a);
}
// 什么东西被 -2147483648 除都等于 0
if (b == INT_MIN) {
return 0;
}
else {
b = abs(b);
}
int ans = 0;
int tmp = 0;
while (a >= b) {
// 可能需要 a -= 1 << 31
// 因此需要 i 能取到 32
// 但是问题是 1 << 32 == 0
// 1 << 31 == -2147483648
// 对于 1 是如此,对于其他数更加不知道左移多少位的时候会变负
// 所以不强求取到 b * 2^31,而是额外要求左移之后不能为负
for (int i = 0; i <= 32; ++i) {
if ((b << i) > 0 && a >= (b << i)) continue;
else if (i > 0) {
a -= (b << (i - 1));
// 在这里可能溢出
if (ans > INT_MAX - (1 << (i - 1))) return INT_MAX;
else ans += (1 << (i - 1));
break;
}
}
// 把 INT_MIN 转换到正数丢失的部分加回来
if (res > 0 && a <= INT_MAX - res) {
a += res;
res = 0;
}
}
return ans_sign * ans;
}
};
这样就通过了
剑指 Offer II 002. 二进制加法
就是做一个加法器,很简单
class Solution {
public:
string addBinary(string a, string b) {
string ans;
int p1 = a.size()-1, p2 = b.size()-1;
int tmp1 = 0, tmp2 = 0, add = 0, cin = 0;
while (p1 >= 0 && p2 >= 0) {
tmp1 = a[p1] - '0';
tmp2 = b[p2] - '0';
add = tmp1 + tmp2 + cin;
ans.insert(0, 1, '0' + (add & 1));
cin = (add >> 1) & 1;
--p1;
--p2;
}
while (p1 >= 0) {
add = a[p1] - '0' + cin;
ans.insert(0, 1, '0' + (add & 1));
cin = (add >> 1) & 1;
--p1;
}
while (p2 >= 0) {
add = b[p2] - '0' + cin;
ans.insert(0, 1, '0' + (add & 1));
cin = (add >> 1) & 1;
--p2;
}
if(cin == 1) ans.insert(0, 1, '1');
return ans;
}
};
剑指 Offer II 003. 前 n 个数字二进制中 1 的个数
给定一个非负整数 n ,请计算 0 到 n 之间的每个数字的二进制表示中 1 的个数,并输出一个数组。
示例 1:
输入: n = 2
输出: [0,1,1]
解释:
0 --> 0
1 --> 1
2 --> 10
示例 2:
输入: n = 5
输出: [0,1,1,2,1,2]
解释:
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/w3tCBm
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
暴力解法
暴力解法就是对每一个数字都看 32 个位
Brian Kernighan 算法
Brian Kernighan 算法的核心:
x = x & (x - 1) 会将 x 的二进制形式中的最后一个 1 变成 0
这很容易理解,因为 -1 就是要把最后一个 1 拆开
而拆出来的新的 1 出现在低位,原来的 x 在这些低位上一定是 0,因为我在 -1 的时候用的是最后一个 1,这就意味着最后一个 1 后面全是 0
所以 x & (x - 1) 就会把后面被拆开的新出现的 1 给消掉,但是前面仍然保持原状
然后一直消,消到 x == 0 为止,那么这个时候消去的次数就是原始 x 中 1 的个数
class Solution {
public:
vector<int> countBits(int n) {
vector<int> ans;
int count = 0;
int tmp = 0;
for(int i = 0; i <= n; ++i){
count = 0;
tmp = i;
while(tmp != 0){
tmp = tmp & (tmp - 1);
++count;
}
ans.push_back(count);
}
return ans;
}
};
方法二:动态规划——最高有效位
假设我已经知道 B 的二进制形式有 n 个 1,然后假设 A 的二进制形式只比 B 的二进制形式多一个 1,且 A > B,那么就可以知道 A 的二进制形式中的 1 的数量 = B 的二进制形式中的 1 的数量 + 1
这个……有点看上去有点多余的想法
就,他是想强调,会存在 A > B 满足 A = 0…001B 的这个形式
那么怎么找这两个数呢?可以从已经有的一个数中拆分
比如我已知 x = 0001 0010,那么我拆成 x = y + z
y = 0001 0000
z = 0000 0010
那么得到的就是 dp[x] = dp[z] + 1
这就是状态转移方程了
但是如果要上手写就有一个问题,我该怎么拆分?
因为我们直觉上就可以看出来,只要我能够拆分,这道题就写完了:x 拆分成的 y 和 z,z 的 dp 值一定是我算过的,所以我们这样的递归思路是很清晰的
问题就是我们拆 y+z 的时候,我们直接看的最高位的 1 得到的 y
但是给定任意一个数 x,你有没有办法快速,直接得到它的最高位的 1?
似乎没有,大概
所以他这里就是要保存一个最高位的 1 的位置信息,如果我们一开始知道这个位置在 0,之后他每一步移动的规律我们也知道的话,那么我们就可以根据这个保存的值知道每一步的最高位的 1 的位置
那么我们一开始,最高位的 1 的位置是 0,它的变化规律我们知道吗?是可以的,变化规律就是,类似 Brian Kernighan 算法,如果 x & (x-1) == 0,说明 x 只有一位是 1,那么什么时候 x 只有一位是 1,在
1
10
100
1000
…
的时候
这些时候正好是最高位的 1 的位置前进的时候
所以综上所述,我们就可以对于任意一个 x,知道它的最高位的 1 的位置,然后使用这个信息把 x 拆成 y + z,然后就得到状态转移方程 dp[x] = d[z] + 1,其中 dp[z] 已知
class Solution {
public:
vector<int> countBits(int n) {
vector<int> dp(n+1, 0);
int high_bit = 0;
// 被拆分出来的,去掉最高位的 1 之后剩下的数
int tmp = 0;
for(int i = 1; i <= n; ++i){
if((i & (i - 1)) == 0){
++high_bit;
dp[i] = 1;
continue;
}
tmp = i - (1 << (high_bit-1));
dp[i] = dp[tmp] + 1;
}
return dp;
}
};
方法三:动态规划——最低有效位
方法四:动态规划——最低设置位
知道了第二个方法,那么其他方法在根本的思路上是类似的
找到一个运算,使得 x > f(x), x 的二进制中 1 的数量 = f(x) 的二进制中 1 的数量 + c,c 为跟 f 有关的已知数
那么状态转移方程就是 dp[x] = dp[f(x)] + c
比如 方法三:动态规划——最低有效位
就是对 x 除 2,那么如果 x 是偶数,dp[x] = dp[x/2],如果 x 是奇数,那么 x/2 就相当于 x 左移一位,把奇数二进制形式中最低位的 1 移走了,所以 dp[x] = dp[x/2] + 1
综合起来就是 dp[x] = dp[x >> 1] + x&1
而 方法四:动态规划——最低设置位 就是 Brian Kernighan 算法 取 y = x & (x-1)
那么 y 就是 x 去掉最低的 1 的结果
所以 dp[x] = dp[y] + 1
也就是 dp[x] = dp[x & (x-1)] + 1
第 2 天 整数
剑指 Offer II 004. 只出现一次的数字
给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
示例 1:
输入:nums = [2,2,3,2]
输出:3
示例 2:
输入:nums = [0,1,0,1,0,1,100]
输出:100
一开始我就自然地用 map 做了
class Solution {
public:
int singleNumber(vector<int>& nums) {
// 如果是某个元素只出现一次,其他每个元素都恰出现两次
// 那么还有可以用一个 set,遍历一遍数组,没出现的添加进去,出现了的就删除
// 那么如果是三次的话,或许可以用一个 map
// 出现了三次的就删除,其他时候不删
map<int, int> my_map;
for(int num : nums){
if(my_map.find(num) == my_map.end()){
my_map.insert({num, 1});
}
else{
++my_map[num];
if(my_map[num] == 3){
my_map.erase(num);
}
}
}
return my_map.begin()->first;
}
};
但是它提示还有一个进阶要求
进阶:你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
我依稀记得以前的题好像是用异或来做的
异或的性质
https://blog.youkuaiyun.com/xzerui/article/details/105923129
异或是对于参与运算的两个数的每个二进制位,同值取0,异值取1。
简单理解就是不进位加法,如1+1=0,,0+0=0,1+0=1。(这点很重要)
性质:
-
结合律
(a XOR b) XOR c = a XOR (b XOR c)
-
一个数对自己求异或得 0,对 0 求异或得到它本身
a XOR a = 0, a XOR 0 = a
这个很好理解,因为异或是不进位加法,所以如果自己加自己,只存在 1 和 1 相加,0 和 0 相加这两种情况,其中 0 和 0 相加自然得 0,1 和 1 相加本来是有进位的,但是由于异或是不进位加法,所以 1 和 1 相加也得 0,并且没有进位,对其他位没有产生影响,所以全都得 0,结果就是 0
同理,一个数加上 0 等于它本身
-
自反性
a XOR b XOR b = a
应用:
-
交换两个元素不需要中间变量
A=A XOR B (a XOR b)
B=B XOR A (b XOR a XOR b = a)
A=A XOR B (a XOR b XOR a = b)
-
找出唯一成对的那个数
情景描述:1-n 这 n 个数中,只有唯一的一个元素值重复,其它均只出现 一次。每个数组元素只能访问一次,设计一个算法,不用辅助存储空间,将其找出来.
解法:将所有的数全部异或
(1^2^…^1000^p,p为重复的数)
,得到的结果与(1^2^3^…^1000)
的结果进行异或,得到的结果就是重复数。 即(1^2^...^1000^p)^(1^2^3^...^1000)
,由于有结合律、交换律存在,可以这么调整为(1^1^2^2^...^1000^1000^p)
-
找出出现奇数次的数
情景描述:一个数组存放若干整数,一个数出现奇数次,其余数均出现偶数次,找出这个出现奇数次的数.
解法:结果=
a[0]^a[1]^...^a[n-1]
偶数次出现的数异或之后 = 0,奇数次出现的数异或之后 = 它本身
但是这题一个数出现一次,其他数都出现三次,感觉好像用不了奇偶来区分?
方法二:依次确定每一个二进制位
对每一个数字都转化为二进制
然后设置一个 int array[32]
表示每一位上 1 出现的次数
如果有四个数:
0010 1001
1001 0000
1001 0000
1001 0000
那么 array 写为:
3013 1001
在累加的每一步 %3,那么就得到
0010 1001
于是这种方法就是,遍历 nums,同时用一个 32 个元素的 int 数组,来记录每一个元素的 32 个数位上的 1 在遍历过程中出现的次数之和 % 3
得到的就是唯一出现的那个数字
class Solution {
public:
int singleNumber(vector<int>& nums) {
int array[32];
for(int i = 0; i < 32; ++i){
array[i] = 0;
}
for(int num : nums){
for(int i = 0; i < 32; ++i){
array[i] = (array[i] + (num & 1)) % 3;
num = num >> 1;
}
}
int ans = 0;
for(int i = 0; i < 32; ++i){
ans = ans << 1;
ans += array[31 - i];
}
return ans;
}
};
方法三:数字电路设计
在方法二中,统计每一个数位上 1 出现的次数是挨个统计的
这样就会有一个问题是,这样没有办法并行计算
为什么要并行计算?我一开始也完全没有这个意识,其实这个是从它的位运算来的
位运算本身就是可以并行的,所以位运算相关的计算就有这个并行的基础
对于每一位来说,有三种可能,0, 1, 2 所以要用两个二进制位来表示
假设 x 为某一个数位上的统计量,input 为 nums 每一个元素当前数位上的值,x’ 为统计过这个元素之后的统计量的值
x input x'
00 0 00
01 0 01
10 0 10
00 1 01
01 1 10
10 1 00
之前我也写过这样的题
x2 x1 input x2' x1'
0 0 0 0 0
0 1 0 0 1
1 0 0 1 0
0 0 1 0 1
0 1 1 1 0
1 0 1 0 0
把 x 拆分成 x2 和 x1 之后,x2 和 x1 之间的更新规则是必然会有一部分回互相依赖的
根据真值表先写出每一个变量的更新逻辑
主要看这个表
x2 x1 input x1'
0 0 0 0
0 1 0 1
1 0 0 0
0 0 1 1
0 1 1 0
1 0 1 0
得到
if(input == 0)
x1' = x1
else{
if(x2 == 0)
x1' = x1^input
else
x1' = 0
}
写成 x1 = (x1 & (~input)) | ((x1^input)&(~x2))
然后更新了 x1 之后,我再看 x2 的更新
x2 input x2' x1'
0 0 0 0
0 0 0 1
1 0 1 0
0 1 0 1
0 1 1 0
1 1 0 0
写逻辑
if(input == 0)
x2' = x2
else{
if(x1' == 0)
x2' = x2^input
else
x2' = 0
}
那么跟上面是一样的 x2 = (x2 & (~input)) | ((x2^input)&(~x1))
最终的答案是 x1 一定是 0 或 1
然后就过了
class Solution {
public:
int singleNumber(vector<int>& nums) {
// 如果是某个元素只出现一次,其他每个元素都恰出现两次
// 那么还有可以用一个 set,遍历一遍数组,没出现的添加进去,出现了的就删除
// 那么如果是三次的话,或许可以用一个 map
// 出现了三次的就删除,其他时候不删
int x1 = 0, x2 = 0;
for(int num : nums){
x1 = (x1 & (~num)) | ((x1 ^ num) & (~x2));
x2 = (x2 & (~num)) | ((x2 ^ num) & (~x1));
}
return x1;
}
};
剑指 Offer II 005. 单词长度的最大乘积
给定一个字符串数组 words,请计算当两个字符串 words[i] 和 words[j] 不包含相同字符时,它们长度的乘积的最大值。假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回 0。
示例 1:
输入: words = [“abcw”,“baz”,“foo”,“bar”,“fxyz”,“abcdef”]
输出: 16
解释: 这两个单词为 “abcw”, “fxyz”。它们不包含相同字符,且长度的乘积最大。
示例 2:
输入: words = [“a”,“ab”,“abc”,“d”,“cd”,“bcd”,“abcd”]
输出: 4
解释: 这两个单词为 “ab”, “cd”。
示例 3:
输入: words = [“a”,“aa”,“aaa”,“aaaa”]
输出: 0
解释: 不存在这样的两个单词。
提示:
2 <= words.length <= 1000
1 <= words[i].length <= 1000
words[i] 仅包含小写字母
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/aseY1I
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
我实在是想不到什么低于 o(n^2) 的方法了……
我感觉这种在一个数组中要找到满足一定条件的一对数,那么至少要将整个数组扫一遍,所以时间复杂度最少也是 o(n)?
除非有那种跳着看的可以 o(logn)
于是我之后就把每一个单词表示为一个 26 位的二进制数,单词具有某一个字母,嵌入的 int 在字母对应的序号的位数上的值就为 1
class Solution {
public:
int maxProduct(vector<string>& words) {
// 单词嵌入表示为一个 26 位的二进制数
vector<int> words_embedding(words.size(), 0);
for (int i = 0; i < words.size(); ++i) {
for (int j = 0; j < words[i].size(); ++j) {
// 如果嵌入 int 的每一个位上都为 1,提前终止
if (words_embedding[i] == 0x3FFFFFF) break;
// nums[i] 的某一位字母对应的序号嵌入到 int 对应序号的位置上,赋为 1
words_embedding[i] |= (1 << (int)(words[i][j] - 'a'));
}
}
int max_prod = 0;
for (int i = 0; i < words.size(); ++i) {
for (int j = i + 1; j < words.size(); ++j) {
// 如果两个嵌入 int 的与运算为 0,说明两者代表的单词的字母之间没有重复
if ((words_embedding[i] & words_embedding[j]) == 0) {
if (words[i].size() * words[j].size() > max_prod) {
max_prod = words[i].size() * words[j].size();
}
}
}
}
return max_prod;
}
};
位掩码
之后发现别人把这个叫做位掩码……合理
然后我还看到别人可以是,因为我们只需要长度最大的和,所以我们可以在创建位掩码的时候,对于位掩码相同的单词,我们人为地记录对于某一个位掩码长度最大的情况
例如对于 “a” 和 “aa”,我们得到的位掩码是一样的 0x000 0001,那么我们对于这个位掩码记录最大长度为 2
这就需要创建一个哈希表,建立位掩码到最大长度的映射
所有单词的位掩码都创建完了之后,遍历这个哈希表,遍历次数虽然还是 o ( n 2 ) o(n^2) o(n2),但是比遍历 words 好了一点
剑指 Offer II 006. 排序数组中两个数字之和
给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。
函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 0 开始计数 ,所以答案数组应当满足 0 <= answer[0] < answer[1] < numbers.length 。
假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。
示例 1:
输入:numbers = [1,2,4,6,10], target = 8
输出:[1,3]
解释:2 与 6 之和等于目标数 8 。因此 index1 = 1, index2 = 3 。
示例 2:
输入:numbers = [2,3,4], target = 6
输出:[0,2]
示例 3:
输入:numbers = [-1,0], target = -1
输出:[0,1]
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/kLl5u1
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
一开始本来想利用一下他这个已排序的性质的,比如用一下二分之类的
class Solution {
private:
int find(vector<int>& numbers, int target, int left, int right){
if(left > right) return -1;
int middle = left + (right - left) >> 1;
if(numbers[middle] == target) return middle;
int tmp = 0;
tmp = find(numbers, target, left, middle - 1);
if(tmp != -1) return tmp;
tmp = find(numbers, target, middle + 1, right);
if(tmp != -1) return tmp;
}
public:
vector<int> twoSum(vector<int>& numbers, int target) {
// 因为原数组有序,所以直接二分查找?
int right = find(numbers, target, 0, numbers.size()-1);
}
};
但是之后我发现我不知道怎么用……
比如这里,我可能原数组 numbers 里面没有 target 的
写的暴力查找又超时了
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
// 因为原数组有序,所以直接二分查找?
int left = 0, right = 0;
// numbers 所有数字 + 1000,target + 2000 使得 numbers 最小值为 0
// 找最右边
for(int i = numbers.size()-1; i >= 0; --i){
// if(numbers[i] + 1000 < target + 2000){
if(numbers[i] < target + 1000){
right = i;
break;
}
}
// 找最左边
for(int i = 0; i < numbers.size(); ++i){
// if(numbers[i] + 1000 >= target + 2000 - numbers[right] - 1000){
if(numbers[i] >= target - numbers[right]){
left = i;
break;
}
}
for(int i = left; i <= right; ++i){
for(int j = right; j > i; --j){
if(numbers[i] + numbers[j] == target){
return vector<int>({i,j});
}
}
}
return vector<int>({0,0});
}
};
之后才看到别人是怎么利用这个规律的……
我之前以为二分查找不一定能找到一个数,确实……但是问题是别人就是说一定会有这个数,那你为什么不固定左边的数,去二分查找右边的数……感觉我这个思维还是没有转过来
但是我自己写的二分还是超时了……
class Solution {
private:
int find(vector<int>& numbers, int target, int left, int right){
if(left > right) return -1;
int middle = left + ((right - left) >> 1);
if(numbers[middle] == target) return middle;
int tmp = 0;
tmp = find(numbers, target, left, middle - 1);
if(tmp != -1) return tmp;
tmp = find(numbers, target, middle + 1, right);
if(tmp != -1) return tmp;
return -1;
}
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int idx = 0;
for(int i = 0; i < numbers.size(); ++i){
idx = find(numbers, target - numbers[i], i + 1, numbers.size() - 1);
if(idx != -1){
return vector<int>({i, idx});
}
}
return vector<int>({0,0});
}
};
出错的例子就是一个很多重复数字的
之后发现是我的二分写错了……我写成两个子区间都进入的递归了……
就,这个东西,很容易写惯成两个子区间都进入啊
实际上二分的精髓就是通过区间端点的判断,结合数组的性质或者题目的要求,就可以从两个子区间中选出唯一确定的一个子区间递归,而不用两个都进入
之前我也是知道这个事情的……但是几周之后就忘了……
class Solution {
private:
inline int find(vector<int>& numbers, int target, int left, int right){
if(left > right) return -1;
int middle = left + ((right - left) >> 1);
if(numbers[middle] == target) return middle;
if(numbers[middle] > target){
return find(numbers, target, left, middle - 1);
}
else{
return find(numbers, target, middle + 1, right);
}
}
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int idx = 0;
for(int i = 0; i < numbers.size(); ++i){
idx = find(numbers, target - numbers[i], i + 1, numbers.size() - 1);
if(idx != -1){
return vector<int>({i, idx});
}
}
return vector<int>({0,0});
}
};
双指针解和为确定值的数对
官方这个解析太 nb 了
我完全没有想过这种,左右分别缩小,只要根据当前和与目标值之间的比较结果就能知道移动谁的这种感觉
这种感觉就像是,嗯,梯度下降,的感觉
就,有一种,哪里凸出来了就戳哪里的感觉
问题是怎么获得这种哪里凸出来了的在题目中的表现,然后你又怎么知道戳哪里这种对应的解决关系……就很神奇
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int left = 0, right = numbers.size() - 1;
int sum = -1001;
while(sum != target){
sum = numbers[left] + numbers[right];
if(sum < target) ++left;
else if(sum > target) --right;
}
return vector<int>({left, right});
}
};
第 3 天 数组
剑指 Offer II 007. 数组中和为 0 的三个数
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/1fGaJU
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
一开始直接暴力枚举前两个数的所有的可能的情况
为了加快一点搜索第三个数的效率,我还使用了二分
每一个三元组中,三个元素之间是可能有重复数字的,所以我没有对 nums 去重
为了对三元组去重,我使用的方法是,在双指针遍历的过程中,对于某一个指针,如果该指针移动到的数字与上一次指向的数字相同,那么就跳过这个重复的数字
为什么可以跳过呢?因为当指针第二次指向重复的数字时,其实他面临的情况是与第一次指向该数字时的情况是一样的
我使用某指针之后的数组的分布来判断一个情况,所以就,当指针连续指向重复的数字时,显然, 重复数字之后的数组分布是一样的,所以我可以跳过
class Solution {
private:
inline int find(vector<int>& numbers, int target, int left, int right){
if(left > right) return -1;
int middle = left + ((right - left) >> 1);
if(numbers[middle] == target) return middle;
if(numbers[middle] > target){
return find(numbers, target, left, middle - 1);
}
else{
return find(numbers, target, middle + 1, right);
}
}
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
if(nums.size() < 3) return ans;
sort(nums.begin(), nums.end());
int target = 0;
int last_num_1 = INT_MIN, last_num_2 = INT_MIN;
for(int i = 0; i < nums.size() - 2; ++i){
if(nums[i] == last_num_1) continue;
last_num_1 = nums[i];
for(int j = i + 1; j < nums.size() - 1; ++j){
if(nums[j] == last_num_2) continue;
last_num_2 = nums[j];
target = -(nums[i] + nums[j]);
if(find(nums, target, j + 1, nums.size() - 1) != -1){
ans.push_back(vector<int>({ nums[i], nums[j], target }));
}
}
}
return ans;
}
};
虽然过了,因为时间复杂度是 o ( n 2 log n ) o(n^2\log{n}) o(n2logn) 但是效率惨不忍睹
执行用时:420 ms, 在所有 C++ 提交中击败了5.02% 的用户
内存消耗:19.5 MB, 在所有 C++ 提交中击败了55.79% 的用户
但是我觉得,这个真的没有办法了吧……因为你有三个独立的数,只能固定其中两个,去找另外一个啊……
回想上一个题,双指针,判断双指针指向的数的和,这个和如果较大的话就说明右指针该往里缩,这个和如果较小的话,就说明左指针该往里缩
感觉我好像有点懂了……那我现在是三指针嘛,那我可以固定一个指针,然后剩下的两个指针就化归为这个问题了,可以用往里缩的方法
感觉这样的时间复杂度为 o ( n 2 ) o(n^2) o(n2),应该更好一点
于是我写成
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
if(nums.size() < 3) return ans;
sort(nums.begin(), nums.end());
int p1 = 0, p2 = 0;
for(int i = 0; i < nums.size() - 2; ++i){
p1 = i + 1;
p2 = nums.size() - 1;
while(p1 < p2){
if(nums[i] + nums[p1] + nums[p2] > 0) --p2;
else if(nums[i] + nums[p1] + nums[p2] < 0) ++p1;
else {
ans.push_back(vector<int>({ nums[i], nums[p1], nums[p2] }));
break;
}
}
}
return ans;
}
};
但是对于特殊例子这个会错……
输入:
[0,0,0,0]
输出:
[[0,0,0],[0,0,0]]
预期结果:
[[0,0,0]]
我这个代码完全没有去重……我也知道
我本来是为了去重,我会在我固定的这个指针这里跳过
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
if(nums.size() < 3) return ans;
sort(nums.begin(), nums.end());
int last_num_1 = INT_MIN;
int p1 = 0, p2 = 0;
for(int i = 0; i < nums.size() - 2; ++i){
if(nums[i] == last_num_1) continue;
last_num_1 = nums[i];
p1 = i + 1;
p2 = nums.size() - 1;
while(p1 < p2){
if(nums[i] + nums[p1] + nums[p2] > 0) --p2;
else if(nums[i] + nums[p1] + nums[p2] < 0) ++p1;
else {
ans.push_back(vector<int>({ nums[i], nums[p1], nums[p2] }));
break;
}
}
}
return ans;
}
};
但是后面发现我不能在第一个指针这里跳过
比如 -1 0 1 2 -1 -4
排序之后是 -4 -1 -1 0 1 2
然后一开始是指向
-4 -1 -1 0 1 2
i p1 p2
之后本来正确的是
-4 -1 -1 0 1 2
i p1 p2
但是我这么去重就会跳过这种情况,直接到
-4 -1 -1 0 1 2
i p1 p2
了
于是之后我写了判断两个指针是否和上一次的两个指针重复的
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
if(nums.size() < 3) return ans;
sort(nums.begin(), nums.end());
int last_num_1 = INT_MIN, last_num_2 = INT_MIN;
int p1 = 0, p2 = 0;
for(int i = 0; i < nums.size() - 2; ++i){
p1 = i + 1;
p2 = nums.size() - 1;
while(p1 < p2){
if((nums[i] == last_num_1 && nums[p1] == last_num_2) ||
(nums[i] == last_num_2 && nums[p1] == last_num_1))
break;
if(nums[i] + nums[p1] + nums[p2] > 0) --p2;
else if(nums[i] + nums[p1] + nums[p2] < 0) ++p1;
else {
ans.push_back(vector<int>({ nums[i], nums[p1], nums[p2] }));
break;
}
}
last_num_1 = nums[i];
last_num_2 = nums[i + 1];
}
return ans;
}
};
但是还是有错
输入:
[1,-1,-1,0]
输出:
[[-1,0,1],[-1,0,1]]
预期结果:
[[-1,0,1]]
排序后是
最开始
-1 -1 0 1
i p1 p2
发现一个三元组
-1 -1 0 1
i p1 p2
第二步
-1 -1 0 1
i p1 p2
于是我看这个规律,似乎重复是因为最后一组 (i, p1) 与新的一组 (i, p1) 重复了
于是我改成
last_num_1 = nums[i];
last_num_2 = nums[p1];
得到的又有问题
输入:
[-2,0,1,1,2]
输出:
[[-2,0,2]]
预期结果:
[[-2,0,2],[-2,1,1]]
这个是因为我在某一次固定第一个指针的时候,只取了一个三元组……感觉确实有问题
再看了官方的不重合的解释
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
if(nums.size() < 3) return ans;
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size() - 2; ++i){
if(!(i == 0 || nums[i - 1] != nums[i])) continue;
for(int j = i + 1; j < nums.size() - 1; ++j){
if(!(j == i + 1 || nums[j - 1] != nums[j])) continue;
for(int k = nums.size() - 1; k > j; --k){
if(nums[i] + nums[j] + nums[k] == 0){
ans.push_back(vector<int>({ nums[i], nums[j], nums[k] }));
break;
}
}
}
}
return ans;
}
};
一开始这样是 o ( N 3 ) o(N^3) o(N3),因为每一个第二层循环都会引起内层循环从最右边找一遍
之后可以法线可以优化的地方……
就是比如某一次查找成功是
a1 a2 a3 a4 a5 a6 a7 a8 a9
i j k
那么当 j 前进一步时,k 不用再从数组末尾开始找,而是从上一次结束的地方开始
也就是 j 前进一步后变成
a1 a2 a3 a4 a5 a6 a7 a8 a9
i j k
然后 k 再向左移动
为什么呢……?因为 j 前进了一步之后,能够就相当于 nums[i] + nums[j]
相比于上一次变大了,那么要求的 nums[k]
势必要比上一次小,所以一定在上一次的 k 的左边
这样,第二层和第三层循环就变为了一个 o ( n ) o(n) o(n) 的双指针问题,这样总的时间复杂度就是 o ( n 2 ) o(n^2) o(n2) 了
但是这样还是错的啊……
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
if(nums.size() < 3) return ans;
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size() - 2; ++i){
// skip repeat num
if(i == 0 || nums[i - 1] != nums[i]){
// double pointer problem
for(int j = i + 1, k = nums.size() - 1; j < nums.size() - 1; ++j){
// skip repeat num
if(j == i + 1 || nums[j - 1] != nums[j]){
while(k > j){
if(nums[i] + nums[j] + nums[k] == 0){
ans.push_back(vector<int>({ nums[i], nums[j], nums[k] }));
break;
}
--k;
}
}
}
}
}
return ans;
}
};
输入:
[1,-1,-1,0]
输出:
[]
预期结果:
[[-1,0,1]]
这是因为我不断留着 k 不向右,一直向左,然后就,嗯,会导致错过一些东西
比如这里,我的程序会这么运行
-1 -1 0 1
i j k
i j k
i jk
直到这里,就会错过 i = 1 这一步,因为前两个元素都是一样的,所以会跳过
或者说是会跳过这个情况
-1 -1 0 1
i j k
这就要求我,我不能一直固定 j 然后一直让 k 主动向左动……
但是这思路就跟我之前写的双指针没有啥区别了
那么我之前就是这么写的,但是不对
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
if(nums.size() < 3) return ans;
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size() - 2; ++i){
// skip repeat num
if(i == 0 || nums[i - 1] != nums[i]){
// double pointer problem
for(int j = i + 1, k = nums.size() - 1; j < nums.size() - 1; ++j){
// skip repeat num
if(j == i + 1 || nums[j - 1] != nums[j]){
while(k > j){
if(nums[i] + nums[j] + nums[k] > 0) --k;
else if(nums[i] + nums[j] + nums[k] < 0) ++j;
else{
ans.push_back(vector<int>({ nums[i], nums[j], nums[k] }));
++j;
}
}
}
}
}
}
return ans;
}
};
输入:
[0,0,0,0]
输出:
[[0,0,0],[0,0,0]]
预期结果:
[[0,0,0]]
或者说我换一个方式写去重,还是错
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
if(nums.size() < 3) return ans;
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size() - 2; ++i){
// skip repeat num
if(i > 0 && nums[i - 1] == nums[i]){
++i;
continue;
}
// double pointer problem
for(int j = i + 1, k = nums.size() - 1; j < nums.size() - 1; ++j){
while(k > j){
// skip repeat num
if(j > i + 1 && nums[j - 1] == nums[j]){
++j;
continue;
}
if(nums[i] + nums[j] + nums[k] > 0) --k;
else if(nums[i] + nums[j] + nums[k] < 0) ++j;
else{
ans.push_back(vector<int>({ nums[i], nums[j], nums[k] }));
++j;
}
}
}
}
return ans;
}
};
输入:
[34,55,79,28,46,33,2,48,31,-3,84,71,52,-3,93,15,21,-43,57,-6,86,56,94,74,83,-14,28,-66,46,-49,62,-11,43,65,77,12,47,61,26,1,13,29,55,-82,76,26,15,-29,36,-29,10,-70,69,17,49]
输出:
[[-82,-11,93],[-82,13,69],[-82,17,65],[-82,21,61],[-82,26,56],[-82,33,49],[-82,34,48],[-82,36,46],[-70,-14,84],[-70,-6,76],[-70,1,69],[-70,13,57],[-70,15,55],[-70,21,49],[-70,34,36],[-66,-11,77],[-66,-3,69],[-66,1,65],[-66,10,56],[-66,17,49],[-49,-6,55],[-49,-3,52],[-49,1,48],[-49,2,47],[-49,13,36],[-49,15,34],[-49,21,28],[-43,-14,57],[-43,-6,49],[-43,-3,46],[-43,10,33],[-43,12,31],[-43,15,28],[-43,17,26],[-29,-14,43],[-29,1,28],[-29,12,17],[-11,-6,17],[-11,1,10],[-3,1,2]]
预期结果:
[[-82,-11,93],[-82,13,69],[-82,17,65],[-82,21,61],[-82,26,56],[-82,33,49],[-82,34,48],[-82,36,46],[-70,-14,84],[-70,-6,76],[-70,1,69],[-70,13,57],[-70,15,55],[-70,21,49],[-70,34,36],[-66,-11,77],[-66,-3,69],[-66,1,65],[-66,10,56],[-66,17,49],[-49,-6,55],[-49,-3,52],[-49,1,48],[-49,2,47],[-49,13,36],[-49,15,34],[-49,21,28],[-43,-14,57],[-43,-6,49],[-43,-3,46],[-43,10,33],[-43,12,31],[-43,15,28],[-43,17,26],[-29,-14,43],[-29,1,28],[-29,12,17],[-14,-3,17],[-14,1,13],[-14,2,12],[-11,-6,17],[-11,1,10],[-3,1,2]]
之后看了官方题解
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
if(nums.size() < 3) return ans;
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size() - 2; ++i){
// skip repeat num
if(i > 0 && nums[i - 1] == nums[i]) continue;
// double pointer problem
for(int j = i + 1, k = nums.size() - 1; j < k; ++j){
// skip repeat num
if(j > i + 1 && nums[j - 1] == nums[j]) continue;
while(j < k && nums[i] + nums[j] + nums[k] > 0) --k;
if(j == k) break;
if(nums[i] + nums[j] + nums[k] == 0)
ans.push_back(vector<int>({ nums[i], nums[j], nums[k] }));
}
}
return ans;
}
};
他这里就是,确实是双重循环,需要 i 和 j 步进的,但是这个 k 的移动有讲究的,不是说 nums[i] + nums[j] + nums[k] != 0
就可以往左移,而是只有 nums[i] + nums[j] + nums[k] > 0
的时候才能往左移,这样才能保证在 j
移动时 k
留在正确的位置
同时 j
, k
移动时也要注意 j
和 k
之间的大小顺序,j == k
时则退出 j
的循环