目录
[136. 只出现一次的数字](#136. 只出现一次的数字)
[118. 杨辉三角](#118. 杨辉三角)
[26. 删除有序数组中的重复项](#26. 删除有序数组中的重复项)
[137. 只出现一次的数字ll](#137. 只出现一次的数字ll)
[260. 只出现一次的数字 III](#260. 只出现一次的数字 III)
[17. 电话号码的字母组合](#17. 电话号码的字母组合)
[JZ39 数组中出现次数超过一半的数字](#JZ39 数组中出现次数超过一半的数字)
136. 只出现一次的数字
采用异或运算的思路
- 异或运算的特性是,相同的数异或结果为0,而任何数与0异或结果仍为原数。
- 通过在循环中不断进行异或运算,最终只出现一次的元素将会被保留在变量
v
中,而其他出现多次的元素则会被抵消掉。
cpp
class Solution {
public:
int singleNumber(vector<int>& nums) {
int v=0;
for(auto a:nums)
{
v^=a;
}
return v;
}
};
118. 杨辉三角
思路:找出杨辉三角的规律,发现每一行头尾都是1,中间第[j]个数等于上一行[j-1]+[j]。
cpp
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> v;
v.resize(numRows, vector<int>());
for (size_t i = 0; i < numRows; i++) {
v[i].resize(i + 1, 0);
v[i][0] = v[i][v[i].size() - 1] = 1;
}
for (size_t i = 0; i < v.size(); i++) {
for (size_t j = 0; j < v[i].size(); j++) {
if (v[i][j] == 0) {
v[i][j] = v[i - 1][j] + v[i - 1][j - 1];
}
}
}
return v;
}
};
- 首先,函数定义了一个二维整型数组
v
,用于存储生成的杨辉三角形。通过调用resize
函数,将数组的行数设置为numRows
,并将每一行的列数初始化为0。 - 接下来,使用两个嵌套的
for
循环来遍历数组v
中的每个元素。外层循环控制行数,内层循环控制列数。 - 在内层循环中,首先通过
v[i].resize(i+1,0)
将当前行的列数设置为i+1
,并将所有元素初始化为0。然后,将当前行的第一个元素和最后一个元素设置为1,即v[i][0]=v[i][v[i].size()-1]=1
。 - 接下来,通过判断当前元素是否为0,来确定是否需要计算该元素的值。如果当前元素为0,则表示该元素是由上方两个元素相加得到的。通过
v[i-1][j-1]+v[i-1][j]
计算出当前元素的值,并将结果赋值给v[i][j]
。 - 通过这样的循环嵌套,依次计算出杨辉三角形中每个位置的值。
- 最后,函数返回生成的杨辉三角形数组
v
。
26. 删除有序数组中的重复项
方法一
使用双指针方法,一个指针用于遍历数组并查找唯一的元素,另一个指针用于跟踪下一个不重复元素应该插入的位置。时间复杂度为O(N).
cpp
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if (nums.empty())
return 0;
int a = 0, b = 1;
while (b < nums.size()) {
if (nums[a] != nums[b]){
nums[++a] = nums[b];
}
++b;
}
return a + 1;
}
};
- 如果
nums
为空,则立即返回0。这是因为空数组没有元素可以移除。函数使用两个指针a
和b
(或者可以认为是索引)来遍历数组。a
指向要插入下一个唯一元素的位置,而b
遍历数组寻找不重复的元素。 - 当
b
小于nums.size()
时,继续遍历数组。 - 检查
nums[a]
和nums[b]
。如果它们不相等,这意味着nums[b]
是一个新的唯一元素,它应该被移到a
指向的下一个位置。 - 如果
nums[a]
和nums[b]
相等,这意味着nums[b]
是重复元素,因此不应移动a
。 - 当找到新的唯一元素时(即
nums[a]
和nums[b]
不相等),将nums[b]
的值复制到nums[a+1]
的位置。这样做是为了在数组的前部保留所有唯一的元素。 - 指针
a
递增以指向下一个可能的唯一元素的位置。 - 无论
nums[a]
和nums[b]
是否相等,b
都会递增,以便继续检查数组的下一个元素。 - 函数返回
a+1
,这是因为a
是最后一个唯一元素的索引,而数组长度是最后一个元素的索引加1。
方法二
思路: erase删除重复数据,不过会导致数据挪动,时间复杂度为O(N)
- 由于数据是排序好的,通过查询每一个数据的重复区间
- 将重复区间进行删除,只保留一个数据即可
cpp
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if (nums.size() == 1)
return 1;
vector<int>::iterator it = nums.begin();
vector<int>::iterator it1 = it + 1;
while (it != nums.end()) {
while (it1 != nums.end() && *it1 == *it){
it1++;
}
it = nums.erase(++it, it1);
it1 = it;
it1++;
}
return nums.size();
}
};
137. 只出现一次的数字ll
方法一: 我们可以使用哈希映射统计数组中每个元素的出现次数。对于哈希映射中的每个键值对,键表示一个元素,值表示其出现的次数。在统计完成后,我们遍历哈希映射即可找出只出现一次的元素。
cpp
class Solution {
public:
int singleNumber(vector<int>& nums) {
unordered_map<int, int> count;
for (auto a : nums) {
++count[a];
}
for (auto& pair : count) {
if (pair.second == 1) {
return pair.first;
}
}
return -1;
}
};
class Solution {
public:
int singleNumber(vector<int>& nums) {
unordered_map<int, int> map;
for (auto n : nums) {
++map[n];
}
int a = 0;
for (auto [n, o] : map) {
if (o == 1) {
a = n;
break;
}
}
return a;
}
};
-
创建一个无序映射(unordered_map)
count
,用于记录每个元素出现的次数。 -
遍历数组
nums
,对于数组中的每个元素a
,将其作为键,将其出现的次数加1。 -
遍历映射
count
中的每个键值对,对于每个键值对pair
,如果其值等于1,说明该键对应的元素只出现了一次,返回该键。 -
如果遍历完映射
count
后仍未找到只出现一次的元素,则返回-1。
**方法二:**只有一个数字出现一次,其余数字均出现3次,假设数组为{3,5,3,3} 通过分析可知: 3的二进制:0 0 0 0 0 0 1 1 5的二进制:0 0 0 0 0 1 0 1 3的二进制:0 0 0 0 0 0 1 1 3的二进制:0 0 0 0 0 0 1 1 0 0 0 0 0 1 3 4 二进制1的总数 对于出现3次的数字,各位出现的次数都是3的倍数,因此对统计的为1的比特总数%3 0 0 0 0 0 1 0 1 = 5
cpp
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for (int i = 0; i < 32; i++) {
int sum = 0;
for (auto n : nums) {
if (((n >> i) & 1) == 1)
sum++;
}
sum %= 3;
if (sum == 1)
ret |= 1 << i;
}
return ret;
}
};
260. 只出现一次的数字 III
方法一:哈希表
cpp
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
unordered_map<int, int> map;
for (auto n : nums) {
map[n]++;
}
vector<int> rec;
for (size_t i = 0; i < nums.size(); ++i) {
if (map[nums[i]] == 1) {
rec.push_back(nums[i]);
}
}
return rec;
}
};
这种方法虽然在代码中实现了,但是它不符合题目要求的线性时间和常数空间的要求。
- 使用哈希表来记录每个元素出现的次数。
- 遍历数组,将出现一次的元素添加到结果列表中返回。
这种方法的时间复杂度为O(n),空间复杂度也为O(n),因为需要额外的空间来存储哈希表。
方法二:位操作
cpp
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
int sum = 0;
for (int num : nums) {
sum ^= num;
}
int l = (sum == INT_MIN ? sum : sum & (-sum));
int num1 = 0, num2 = 0;
for (int num : nums) {
if (num & l)
num1 ^= num;
else
num2 ^= num;
}
return {num1, num2};
}
};
这是符合题目要求的方法,利用了XOR运算的性质来解决问题:
-
异或运算特性:相同为0,相异为1,无进位相加
- 任何数和0做异或运算,结果仍然是原来的数,即 a⊕0=aa⊕0=a。
- 任何数和其自身做异或运算,结果是0,即 a⊕a=0a⊕a=0。
- 异或运算满足交换律和结合律,即 a⊕b=b⊕aa⊕b=b⊕a 和 (a⊕b)⊕c=a⊕(b⊕c)(a⊕b)⊕c=a⊕(b⊕c)。
-
第一次异或:
- 我们遍历整个数组,将所有元素进行异或运算。由于数组中的大部分元素都是成对出现的,所以这些成对的元素相互抵消,最后得到的结果就是那两个只出现一次的数的异或结果。设这两个数为 xx 和 yy,那么我们得到的异或结果 sum=x⊕ysum=x⊕y。
-
寻找差异位:
- 因为 xx 和 yy 是不同的数,所以 sumsum 至少有一个比特位是1。我们需要找到这个比特位,这可以通过将 sumsum 和它的补码 sum⊕(−sum)sum⊕(−sum) 进行与运算得到。这个比特位就是 sumsum 中从最低位开始的第一个1。
- 另一种方法是直接使用 sumsum 本身,因为如果 sumsum 不是最小负数 INT_MIN,那么 sumsum 和它的补码的与运算结果就是 sumsum 的最低位1。如果 sumsum 是 INT_MIN,那么 sumsum 就已经是这个比特位了。
-
第二次异或:
- 利用找到的差异位,我们可以将数组中的所有元素分为两类:那些在这个比特位上为1的数,以及那些在这个比特位上为0的数。
- 对这两类数分别进行异或运算。因为每一对重复的数在这个比特位上的值是一样的,它们会在各自的类别中相互抵消,剩下的就是那个类别中只出现一次的那个数。
- 于是我们得到了 xx 和 yy。
这种方法的时间复杂度为O(n),并且只需要常数级别的额外空间,满足题目的要求。
方法三:排序后比较
cpp
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<int> res;
int i = 0;
for (; i < nums.size() - 1;) {
if (nums[i] == nums[i + 1]) {
i += 2;
} else {
res.push_back(nums[i]);
i += 1;
}
}
if (i < nums.size()) {
res.push_back(nums[i]);
}
return res;
}
};
这种方法也不完全符合题目要求的线性时间和常数空间的要求,但可以理解它的逻辑:
- 先对数组进行排序。
- 遍历排序后的数组,如果遇到相邻元素不相等的情况,则当前元素一定是只出现一次的元素。
- 最后处理数组尾部的元素。
这种方法的时间复杂度为O(n log n),因为排序通常需要O(n log n)的时间复杂度。空间复杂度取决于使用的排序算法,如果是原地排序则可以达到O(1),但大多数情况下会大于O(1)。
17. 电话号码的字母组合
回溯算法
cpp
class Solution {
public:
string num_str[10] = {"", "", "abc", "def", "ghi",
"jkl", "mno", "pqrs", "tuv", "wxyz"};
void combination(const string& digits, size_t di, string combStr,
vector<string>& strv) {
if (di == digits.size()) {
strv.push_back(combStr);
return;
}
int num = digits[di] - '0';
string str = num_str[num];
for (auto ch : str) {
combination(digits, di + 1, combStr + ch, strv);
}
}
vector<string> letterCombinations(string digits) {
vector<string> str;
if (digits.size() == 0)
return str;
combination(digits, 0, "", str);
return str;
}
};
-
初始化映射表:
- 创建了一个静态数组
num_str
,用于存储数字到字母的映射关系。例如,"2"
映射到"abc"
,"3"
映射到"def"
等等。注意索引0
和1
处的字符串为空,因为我们不考虑这两个数字对应的字母。
- 创建了一个静态数组
-
递归函数
combination
:- 这个函数接受四个参数:
digits
(输入的数字字符串)、di
(当前处理的数字索引)、combStr
(当前构建的字符串组合)以及strv
(用于存储所有可能组合的向量)。 - 如果当前处理的索引
di
已经等于digits
的长度,说明已经处理完所有的数字,此时将当前构建的字符串combStr
加入结果向量strv
。 - 否则,获取当前数字对应的字母字符串
str
,然后遍历这个字符串中的每一个字母,并递归调用combination
函数,同时增加索引di
并将当前字母加入到combStr
。
- 这个函数接受四个参数:
-
主函数
letterCombinations
:- 检查输入的
digits
字符串是否为空,如果为空则直接返回空的字符串向量。 - 调用
combination
函数开始递归生成所有可能的组合。 - 最终返回所有可能的字母组合。
- 检查输入的
示例步骤
假设 digits = "23"
,则有以下步骤:
- 开始时,
di=0
,combStr=""
,num=2
,对应的字母是"abc"
。 - 对于每个字母
a
,b
,c
,执行di+1
,即di=1
。 - 当
di=1
时,num=3
,对应的字母是"def"
。 - 对于每个字母
d
,e
,f
,执行di+1
,即di=2
。 - 当
di=2
时,已经到达字符串末尾,将当前的combStr
添加到结果向量中。
结果
对于 "23"
,结果向量将会包含 "ad"
, "ae"
, "af"
, "bd"
, "be"
, "bf"
, "cd"
, "ce"
, "cf"
。
这种方法的时间复杂度是 O(3^N * 4^M),其中 N 是数字2、3、4、5、6、8出现的次数,M 是数字7、9出现的次数,因为数字7和9对应4个字符,其它数字对应3个字符。空间复杂度主要取决于递归调用栈的深度,最坏情况下为 O(N)。
JZ39 数组中出现次数超过一半的数字
哈希表
cpp
class Solution {
public:
int MoreThanHalfNum_Solution(vector<int>& numbers) {
unordered_map<int, int> count;
int n=numbers.size();
for (auto a : numbers) {
++count[a];
if (count[a] > n / 2) {
return a;
}
}
return -1;
}
};