🔥个人主页:Quitecoder
🔥专栏:Leetcode刷题
1.只出现一次的数字
这道题很简单,我们只需要遍历一遍数组,利用异或操作的性质(一个数与自身异或结果为0,任何数与0异或还是其本身)
class Solution {
public:
int singleNumber(vector<int>& nums) {
int value =0;
for(auto v:nums)
{
value^=v;
}
return value;
}
};
2.杨辉三角
这道题我们需要构造二维数组,典型的vector的嵌套使用
首先,我们先构建二维数组,开辟行数大小:
vector<vector<int>> v(numRows);
接着对每一行进行开辟空间,并将两端初始化为1
for(int i=0;i<numRows;i++)
{
v[i].resize(i+1);
v[i][0]=1;v[i][i]=1;
}
注意,resize是会进行初始化的,我们没有传值,默认为零
所以我们只需要遍历一遍,遍历到的位置为0,进行相加操作
完整代码如下:
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> v(numRows);
for(int i=0;i<numRows;i++)
{
v[i].resize(i+1);
v[i][0]=1;v[i][i]=1;
}
for(int i=0;i<numRows;i++)
{
for(int j=0;j<i;j++)
{
if(v[i][j]==0)
{
v[i][j]=v[i-1][j]+v[i-1][j-1];
}
}
}
return v;
}
};
3.删除有序数组中的重复项
这题是一道简单的双指针思路的题,由于已经排序好,我们只需要设置两个索引,一个向后遍历,若与前面的索引指向值不相同,则对前面的值进行修改
lass Solution {
public:
int removeDuplicates(vector<int>& nums) {
if (nums.size() == 0) {
return 0;
}
int slow = 0;
for (int fast = 1; fast < nums.size(); fast++) {
if (nums[fast] != nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1;
}
};
完成了值的覆盖过程
4.只出现一次的数字II
这个问题的解决方案基于位操作和有限状态自动机的原理。我们要处理的数字是32位整数,因此,我们需要考虑每一位相加后的结果。由于除了一个数字以外,其它数字都出现了三次,我们可以构造一个数字的每一位相加后,模3的结果就是这个只出现一次的数字的相应位
思路如下:
使用两个整数变量ones
和twos
。ones
将会记录每个位只出现一次的情况,而twos
将会记录每个位出现两次的情况
对于每个数字num
及其每一位,我们更新ones
和twos
:
-
在第
i
个位置上,如果ones
里的位是1,则表示num
要么是第一次遇到i
位为1,要么是第四次。如果是第四次,我们已经在twos
里记录了两次,所以这次应该把ones
里的该位清零,否则保持不变 -
同理,如果
twos
里的位是1,则是第二次遇到i
位为1或者是第五次。如果是第五次,我们既要在ones
里面加1,同时也要在twos
里面清零该位,否则保持不变 -
由于我们只需要考虑每个位上1出现的次数,所以任何时候位上的1出现3次,我们都应该清零
最后,ones
保留的就是每位上出现一次的结果,而twos
将会是0。
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ones = 0, twos = 0;
for (int num : nums) {
ones = (ones ^ num) & ~twos;
twos = (twos ^ num) & ~ones;
}
return ones;
}
};
当我们讨论处理出现三次的数字和一个只出现一次的数字时,ones
和 twos
的位操作确实是难以理解的 ,分解这两行代码:
对于每一个新的数字 num
,我们用 ones
和 twos
来跟踪彼此独立的状态:
ones = (ones ^ num) & ~twos;
这里,我们正更新 ones
以包含出现一次的位。让我们分解这行代码:
-
ones ^ num
:这个按位异或操作背后的思想是:当前的ones
表示上一步迭代中已经出现一次的位。当我们再次看到这些位时(即num
中的对应位也是1),我们希望重置ones
中的那些位(因为出现一次变成了两次)。对于num
中新出现的1,ones
中还没有记录,这将被加进ones
-
& ~twos
:接下来的按位与操作与~twos
结合表示:我们删除twos
中已经出现两次的位。~twos
是对twos
取反,意味着取出twos
中为0的位。只有那些在twos
中没有记录(即还没达到两次)的1才应该加入ones
。即使刚才ones ^ num
把某些位变成了1,若那些位在twos
中已经出现过两次,我们必须确保它们在ones
中不变成1
结合二者,ones
在每次迭代结束时仅保留那些恰好出现一次的位。如果某位在 ones
中变成了1但已经在 twos
中出现过,我们需要重置 ones
中的那位为0
twos = (twos ^ num) & ~ones;
接着我们更新 twos
来反映那些已经看到两次的位:
-
twos ^ num
:与更新ones
类似,我们对于每个新来的num
,我们都会用按位异或更新twos
。如果在twos
中的位是1,且对应的num
中的位也是1,那么它们会重置为0,因为现在这个位出现了第三次,而我们的目标是找到出现了一次和两次的位。如果出现的是一个新的1(即num
中的1,而twos
中并没有记录),twos
就会记录它。这会出现加到三的情况,我们随后会处理。 -
& ~ones
:这个按位与操作保证如果在ones
中有1(意味着这个位已经出现了一次),我们不会在twos
中加入该位。如果某个位同时在ones
和twos
中出现,这意味着这个位出现了3次,并且最终会被忽略。
通过 & ~ones
,我们确保了一个位仅仅当它在 num
中为1且在 ones
中尚未出现(即 ones
中为0)时,才会被加入 twos
。
总结来说,这两步操作是相互独立并且排他的:它们保证一个位在 ones
或 twos
中出现,但不会同时出现。我们在整体数组中使用循环来考虑每个数字的影响。最终,由于所有出现三次的数字在这两个变量中都被消去,ones
会留下那个出现一次的唯一位
5.只出现一次的数字III
此类问题可以通过位运算(异或操作)来解决。首先,我们可以通过对所有数组元素执行异或操作来找出两个只出现一次的元素的异或结果。因为异或操作具有交换律和结合律,同时一个数字和自己进行异或会变成0,所以最终剩下的结果就是那两个只出现一次的数字的异或结果
这个结果中至少有一个位是1(否则这两个数相同),我们可以找到这个数中的任何一个为1的位,用它来把原数组分成两组,一组在该位上是1,一组在该位上是0。这样每组就包含了一个只出现一次的数字和一些成对出现的数字。然后再对这两个组分别进行异或操作,即可得到这两个只出现一次的数字。
下面是这个算法实现:
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
// 第一步,对所有元素进行异或,最终的结果就是两个只出现一次数的异或结果
int diff = 0;
for (int num : nums) {
diff ^= num;
}
// 找到diff中任何为1的位,可以使用diff & -diff快速找到
// 这个操作可以隔离出diff最右端的1
unsigned int diff_unsigned = diff;
diff_unsigned &= -diff_unsigned;
// 使用找到的这一位将数组中的数字分成两组
vector<int> results(2, 0); // 最终结果
for (int num : nums) {
if ((num & diff_unsigned) == 0) {
// 第一组,与diff_unsigned对应位为0
results[0] ^= num;
} else {
// 第二组,与diff_unsigned对应位为1
results[1] ^= num;
}
}
return results;
}
};
在这个代码中:
diff_unsigned
最终会被设置为两个目标数字的异或结果。diff_unsigned &= -diff_unsigned;
的结果是取出diff_unsigned
最右边的1位,也就是两个只出现一次的数在这一位上不同的地方。- 然后我们通过判断这一位是否为1来将全部数字分为两组,并再次分别对它们进行异或操作,以此找到两个只出现一次的数。
这条语句 diff_unsigned &= -diff_unsigned;
是一种计算机用来找到一个数字中最右边的1的位,并且保持所有其他位为0的技巧。为了更好地理解这个技巧,我们需要先了解计算机中的数字表示——特别是补码表示法,因为这个技巧与负数的二进制表示相关
在补码表示中,一个负数是通过取其正值的二进制表示的反码(每个位取反)然后加1得到的。例如,假设我们有一个4位的系统:
正数 2 的二进制表示: 0010
反码 (invert): 1101
加1得到负数 -2: 1110
观察发现,从正数2的二进制表示到负数-2的表示,最右边的1以及之前的所有0都保持不变,而最右边的1之后的所有位都翻转了。这给了我们一种找到最右边的1的方法。现在,如果我们对2和-2执行按位与操作:
正数 2: 0010
负数 -2: 1110
按位与: 0010
按位与操作的结果就是只有最右边的1保留了下来,其它所有位都变成了0。换句话说,diff_unsigned &= -diff_unsigned;
将结果的所有位都置为0,除了最右边的1所在的位。
在解决问题时,我们首先会通过对所有数字进行异或得到 diff
,这代表了两个只出现一次的数字的差异。
diff 变量首先被转换成一个无符号整数 diff_unsigned,然后对它进行取负和按位与操作,以避免未定义行为。这样就保证了即使 diff 的最高有效位是1,我们也不会超出无符号整型的范围
然后使用 diff_unsigned &= -diff_unsigned;
来保留最右边的1,这是两个独特数字在二进制表示中第一个不同的位。
通过这个位的差异,我们可以将所有的数字分成两组来进一步操作,每组包含一个只出现一次的数字以及成对出现的数字。这个1所在的位将用于分辨哪些数字在该位为0或1 —— 这正是对数组进行划分的依据
6.电话号码的字母组合
这个问题可以通过回溯法解决,这是一种通过穷举所有可能的解来找到全部解的算法。基本思想是从左到右遍历数字字符串,对于每个数字,向当前的字母组合中添加对应的每个字母,然后对剩余的字符串重复这个过程。
下面是递归解决实现:
class Solution {
public:
vector<string> letterCombinations(string digits) {
if (digits.empty()) return {}; // 如果输入为空,直接返回空数组
vector<string> mappings = { // 数字到字母的映射
"", "", "abc", "def", // '0','1','2',...
"ghi", "jkl", "mno",
"pqrs", "tuv", "wxyz"
};
vector<string> result;
string current;
backtrack(result, digits, 0, current, mappings);
return result;
}
private:
void backtrack(vector<string>& result, const string& digits,
int index, string& current, const vector<string>& mappings) {
if (index == digits.length()) { // 如果到达了数字字符串的末尾,就添加当前的字母组合到结果中
result.push_back(current);
return;
}
string letters = mappings[digits[index] - '0']; // 获取当前数字对应的所有字母
for (char letter : letters) { // 遍历这些字母
current.push_back(letter); // 添加当前的字母
backtrack(result, digits, index + 1, current, mappings); // 继续处理下一个数字
current.pop_back(); // 回溯,移除当前字母,以便尝试下一个字母
}
}
};
这段代码定义了一个辅助函数 backtrack
,用来递归寻找所有可能的字母组合。我们维护一个 current
字符串,它保存当前的部分组合。函数的工作流程是这样的:
-
确定终止条件:如果
current
的长度与输入数字字符串的长度相同,说明当前递归路径已经走到头,我们找到了一个完整的字母组合,将其添加到结果中。 -
确定递归逻辑:从
mappings
数组中获取当前处理的数字对应的所有可能字母,然后逐一向current
添加每个字母,并递归地调用自己处理下一个数字。 -
回溯处理:每次递归调用完成后,需要将之前添加的字母移除,以便对当前位置尝试不同的字母。