代码随想录第六天 | Leecode 242.有效的字母异位词、 349. 两个数组的交集、202. 快乐数、1. 两数之和
昨天第五天是周日休息一天,今天第六天开始哈希表部分题目。
Leecode 242.有效的字母异位词
题目描述
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的 字母异位词。
-
示例 1:
- 输入:
s = "anagram",t = "nagaram" - 输出:
true
- 输入:
-
示例 2:
- 输入:
s = "rat",t = "car" - 输出:
false
- 输入:
-
提示:
1 <= s.length,t.length <= 5 * 104s和t仅包含小写字母
解法1 暴力法
两个字符串相当于是两个无序数组,要比较其中出现的各字符是否相同。那么可以考虑将两个字符串中的字符进行排序,随后逐位比较是否相同即可。那么可以写出如下算法结构:
- 首先比较两个字符串长度是否相同,如果不相同则直接输出false,相同再继续后续步骤
- 将两个字符串分别进行排序,这里可以使用不同的排序算法,时间复杂度在\(O(nlogn)\)到\(O(n^2)\),如果使用
sort快排,时间复杂度在\(O(nlogn)\) - 随后返回两个字符串判断是否相等的布尔结果
那么根据上面算法思想,可以得到下面代码:
class Solution {
public:
bool isAnagram(string s, string t) {
// 边界检查:长度不同直接返回false
if (s.size() != t.size()) return false;
// 排序两个字符串
sort(s.begin(), s.end());
sort(t.begin(), t.end());
// 比较是否相同
return s == t;
}
};
上面算法的时间复杂度为\(O(nlogn)\),主要的时间复杂度消耗在于进行了一次排序。为此我们考虑能否不排序就能直接比较两个字符串。
解法2 哈希表
哈希表的存和取操作的时间复杂度都是\(O(1)\),而本题中,字符串中每个字母在字母表中的序号本身就是一个非常好的哈希函数。即每一个字母都可以对应到0-25中的一个数字。而本题只需要比较两个字符串中每个字母出现的次数即可,故采用哈希表可以设计算法思路如下:
- 建立长度为26的int型哈希数组,每一个位置对应一个字母
- 遍历第一个数组,每经过一个字母则在哈希数组中将其对应的位置+1
- 遍历第二个数组,每经过一个字母则在哈希数组中将其对应的位置-1
- 最后逐位判断数组中每一个位置是否都为0,如果是则输出true,有一位不为0则输出false
根据上面算法可以写出代码如下:
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0}; // 初始化哈希数组
for(int i = 0; i < s.size(); i++ ){
record[(int)(s[i] - 'a')%26]++; // 统计第一个字符串中各字母出现数量
}
for(int i = 0; i < t.size(); i++){
record[(int)(t[i] - 'a')%26]--; // 统计第二个字符串中各字母出现数量
}
for(int i = 0; i < 26; i++){
if(record[i] != 0) return false; // 逐位判断两个字符串中的字母数量是否相等,有一位不相等则输出false
}
return true; // 所有字母数量相等则输出true
}
};
可以看出上面算法的时间复杂度为\(O(n)\),比暴力法的时间复杂度更低。
Leecode 349. 两个数组的交集
题目描述
给定两个数组 nums1 和 nums2 ,返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
-
示例 1:
- 输入:
nums1 = [1,2,2,1],nums2 = [2,2] - 输出:
[2]
- 输入:
-
示例 2:
- 输入:
nums1 = [4,9,5],nums2 = [9,4,9,8,4] - 输出:
[9,4] - 解释:
[4,9]也是可通过的
- 输入:
-
提示:
1 <= nums1.length,nums2.length <= 10000 <= nums1[i],nums2[i] <= 1000
解法1 哈希数组
由于提示中已经告诉给定的两个数组中的数的大小都不大于1000,即可能出现的值只有从0到1000的1001个值。因此可以用一个长度为1001的数组来表示每个数是否出现。故我们可以得到下面的代码:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
bool record1[1001] = {0}; // 初始化哈希数组1,用于记录num1中出现的数字,使用布尔型可以占用更少的空间
bool record2[1001] = {0}; // 初始化哈希数组2,用于记录num2中出现的数字
vector<int> record3 = {}; // 结果数组,最后存放交集数字
for(int i = 0; i < nums1.size(); i++){
if(!record1[nums1[i]]) record1[nums1[i]] = true; // 逐位比较数组num1,将所有出现过的数字进行记录
}
for(int i = 0; i < nums2.size(); i++){
if(!record2[nums2[i]]) record2[nums2[i]] = true; // 逐位比较数组num2,将所有出现过的数字进行记录
}
for(int i = 0; i < 1001; i++){
if(record1[i] && record2[i]){ // 将出现在num1和num2中的数字全部push到数组record3中,即可得到输出的交集
record3.push_back(i);
}
}
return record3;
}
};
上面算法的时间复杂度是\(O(n+m)\),而使用的哈希数组是一个定长的数组,是由于题目已经告诉出现的数字的范围在0-1000,因此使用一个长度为1001的数组就能当做哈希数组来记录所有出现过的数字。但是,使用这种方法只能解决本题的有限范围的情形,如果没有给定数字出现的范围,那么再使用这个方法就不行了。为此我们考虑如果位置数字可能范围未知的情况。
解法2 使用unorderd_set方法
当本题没有给出数字范围的时候,采用上面所述的数组方法就不再适用,考虑适用set来实现算法:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set;
unordered_set<int> set1(nums1.begin(),nums1.end()); // 初始化set,使用nums1中的int数据,这里表示从头到尾拷贝初始化
for(int num: nums2){ // for循环,相当于是容器nums2中的每一个数据都进行一遍循环
if(set1.find(num) != set1.end()) { // find会输出返回找到该数据的指针,但是如果没有找到,会返回该容器的最后一个地址,即end
result_set.insert(num); // 插入到结果set中
}
}
return vector<int>(result_set.begin(),result_set.end()); // 最后还需要将set容器转换为vector进行输出
}
};
对于上面代码实现,需要熟悉容器unordered_set和vector的使用。其中有一些方法我还是第一次见,为此这里特别整理一下:
C++中一些还不太熟悉的用法
begin()和end()迭代器
在上面代码中,初始化集合set1使用了语句:
unordered_set<int> set1(nums1.begin(),nums1.end());
其中unordered_set<int>声明数据类型为集合,集合中元素为整型;set1为变量名;(nums1.begin(),nums1.end())返回的是“迭代器”,相当于是一个指针;将begin理解为容器第一个元素的指针,end理解为最后一个元素的下一个指针;使用(nums1.begin(),nums1.end())初始化表示一个左闭右开的区间,将原本nums1容器中这个区间内的数据拷贝到新容器中来进行初始化。
for(int num: nums2)循环体
上面代码还使用了一行:
for(int num: nums2){}
来进行循环操作,这个for循环和往常使用的for循环不太一样。相当于使用变量num来逐一表示容器nums2中存放的数据,并在循环体中执行后续的操作。
find()查找返回迭代器
代码循环体中进行判断使用了一行:
if(set1.find(num) != set1.end()){}
其中用到了一个find()方法,相当于查找一个数据,如果找到了就返回该数据所在迭代器(可以理解为指针);而如果没有找到,则会返回end()迭代器,用于表示“不存在”。
Leecode 202.快乐数
题目描述
编写一个算法来判断一个数 n 是不是快乐数。
-「快乐数」 定义为:
-
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
-
然后重复这个过程直到这个数变为
1,也可能是 无限循环 但始终变不到1。 -
如果这个过程 结果为
1,那么这个数就是快乐数。 -
如果
n是 快乐数 就返回true;不是,则返回false。 -
示例 1:
- 输入:n = 19
- 输出:true
-
解释:
- 12 + 92 = 82
- 82 + 22 = 68
- 62 + 82 = 100
- 12 + 02 + 02 = 1
-
示例 2:
- 输入:n = 2
- 输出:false
使用集合进行判断
对于快乐数,相当于是从一个整数映射到另一个整数,这个链式计算过程中有可能形成环;也有可能到1之后后续全是1。由此我们可以知道,当迭代过程中,如果出现重复的数则说明此时已经成环,则一定不是快乐数;而如果出现1,则是快乐数。否则只需一直迭代下去即可。
为此可以使用unordered_set容器实现如下:
class Solution {
public:
int getSum(int n){ // 按位平方并求和
int sum = 0;
while(n){
int t = n%10; // 取出最右一位
sum += t*t; // 平方并求和
n /= 10; // 删去最右一位
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> nums; // 新建空集合
while(1){ // 无限循环
if(nums.find(n) != nums.end()) return false; // 如果数字出现重复,则说明不是快乐数
if( n == 1) return true; // 如果某时刻返回1,则是快乐数
nums.insert(n); // 计算出没有重复也不是1的时候,存入到集合中
n = getSum(n); // 按位平方求和,变成新的数
}
}
};
Leecode 1. 两数之和
题目描述
给定一个整数数组 nums· 和一个整数目标值 target,请你在该数组中找出 和为目标值 target` 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。
- 示例 1:
- 输入:nums = [2,7,11,15], target = 9
- 输出:[0,1]
- 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
- 示例 2:
- 输入:nums = [3,2,4], target = 6
- 输出:[1,2]
- 示例 3:
- 输入:nums = [3,3], target = 6
- 输出:[0,1]
解法1 暴力搜索
这题是Leecode经典第一题了,其实之前就刷过,这里还是给出我第一次做的时候想到的暴力法。用类似于冒泡排序的结构,逐一比较数组中任意两两元素之和是否与target相等,如果相等就将其放入数组中进行返回。具体代码如下:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
vector<int> result; // 新建空的vector容器
for(int i = 0; i < nums.size()-1; i++){ // 左指针i可取值从0到size-2
for(int j = i + 1; j < nums.size(); j++){ // 右指针可取值从1到size-1
if(nums[i] + nums[j] == target) { // 每一种可能都计算求和是否等于target
result.push_back(i); // 如果满足则将对应的i和j存入vector容器中进行返回
result.push_back(j);
}
}
}
return result;
}
};
上面算法时间复杂度为\(O(n^2)\),算是一个时间复杂度较大的算法。接下来我们考虑使用哈希来解决这道题目。
解法2 哈希
使用哈希的方法需要了解unordered_map容器的用法,首先需要明确,该容器中存放的数据为键值对。因此在定义map时,需要分别指定键和值的数据类型。而在本题中,使用的键值对的数据类型为<int,int>,即键和值的数据类型都是整型变量,分别用于表示数组中的数,及其所在位置。通过遍历数组将这些个键值对存放在map中,可以根据一个数值,在\(O(1)\)的时间内返回它存放于数组中的所在位置序号。
- 在有了这样存放了键值对的map容器之后,我们可以快速查找每一个数关于target的补数是否已经存放进入map中,
- 如果已经存放补数,那么可以直接根据这个值找到所在的位置序号,将其和当前数的序号一起return;
- 如果没有存放补数,那么将当前数值和他所在数组中的序号组成键值对,存放到map中;等待后续可能被其余数找到。
故可以得到下面代码:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> map; // 创建map键值对容器
for(int i = 0; i < nums.size(); i++){
if(map.find(target - nums[i]) != map.end()) { // 如果当前数的补数的键值对已经存放在map中了
int j = map.find(target - nums[i])->second; // 则取出补数所在序号
return vector<int> {j,i}; // 返回当前数序号,与其补数序号组成的vector
}
map.insert(pair<int,int>(nums[i],i)); // 如果当前数的补数不再map中,则将当前数及其序号组成键值对进行存放
}
return nums;
}
};
根据这样的算法,每一个数都可以经过\(O(1)\)时间复杂度的运算,来判断在它之前是否有他的补数,如果有也能在\(O(1)\)时间内取出补数的序号。同时只需要对这\(n\)个数据都进行这样的一次操作就能完成整个算法,故可以知道这个算法的时间复杂度为\(O(n)\).
今日总结
学习了哈希表的算法,包括使用数组来实现,以及标准库中unordered_set和unordered_map容器的使用。
今天感觉学了好多东西,收获满满的一天。
浙公网安备 33010602011771号