1.哈希表基础 Hash Table
1.映射
映射的五大核心方法
1.1 Hash Table 的定义
哈希表是根据关键码的值而直接进行访问的数据结构。哈希表也可以被称作是散列表
哈希表的底层实现是一个数组,哈希表是一个特殊的数组
1.1.1 Hash Tabel 的映射
在一个字典中,有了 Key 和 value 的值可以形成一个映射,可以先考虑一种有限制的设置,在这个设置中有 n 个元组,想要将这 n 个映射,就要定义一个比元组个数还要大的映射表用来表示这个映射,如下图所示:
在上图的情况下,将键值 k 对应的值映射在索引值也为 k 的位置上。但是这样存储的缺点就是:①需要定义的 N 要大于 n ,造成空间资源的浪费 ②在这种情况下想完成 O(1) 复杂度的搜索就要求键值必须是整数③有多个键值映射到相同的索引
当有多个键值映射到相同索引时就需要用桶数组
1.1.2 哈希函数定义
哈希函数 h 的目标是把每个键 k 映射到 [0,N-1] 区间内的整数,其中 N 是哈希表桶数组的容量而不用键 k 做索引,最后会在桶A[h(k)] 中存储元组(k,v),哈希函数由两个部分组成:①哈希码 ②压缩函数
1.1.3 发生冲突
如果有两个或者更多的键具有相同的哈希值,那么这两个不同的元组会映射到同一个桶中,在这种情况下就是发生了一次冲突
1.2 Hash Table 的使用场景
1.Python 中 Dict,Set 的构建就是用的数据结构 Hash Table(散列表) ,因为 Hash Table 可以实现映射
2.hash table 还可以快速判断一个元素是否出现在集合里
枚举方法时间复杂度:O(N)
哈希表时间复杂度:O(1)
因为哈希表有一个哈希函数,哈希函数可以将所有的元素都映射到一张表中,表明该元素存储在内存中的什么位置。在找某一个元素的时候根据哈希函数直接找到关键词在哈希表中的位置就可以一步定位到内存
1.3 Hash Table 会出现的问题
1.3.1index > hash table size
解决方法:最后会做一个取模的操作
1.3.2哈希碰撞
多个元素的索引都映射到了相同的位置
1.4 哈希碰撞的解决方法
一般有两种解决方法:拉链法和线性探测法
1.4.1拉链法
如果发生了冲突,冲突的元素都放在链表中,然后通过索引找到想要找的对象
拉链法应该选择适当的哈希表的大小,这样可以避免后面接的链表过长而导致查找时间也变长
1.4.2 线性探测法
线性探测法一定要保证哈希表的表长发大于等于数据的个数,这样才能保证当发生冲突的时候可以有多余的空位存放冲突元素
1.5 Hash Table 的实现方式
1.数组:判断某元素是否存在并计数,要知道要保存的数据的长度
2.set :判断某元素是否存在,
3.Dict:判断某元素是否存在并记录元素位置
1.6 关于容器常用的几种函数
1.6.1 关联性容器的常用函数
找到排序容器的最大值和最小值,这里得到的是 iterator ,要想取到值还要进行 * 处理
(1)最小值
set.begin();
(2)最大值
set.rbegin();
(3)寻找某个值
最后返回的是 iterator
st.find(nums[i])
2.LeecCode 相关题目
常用数据结构:
- 数组
- Set(集合)
- map(映射)
- set 和 map 的区别在于:map 是有 key 值的,set 没有
- set 的 value 值和 map 的 key 值是使用相应数据结构存储的,所以他们都不可以修改,只能增加和删除
- 和底层使用哈希表和使用红黑树的区别就在于哈希表是无序的,红黑树是有序的
- 能不使用 set 就不适用 set :因为 set 占的空间比数组大,并且速度比数组慢,因为要将每个值都映射到 hashmap 上
关于 key 值的查找,count 经常用于查找 map 或者 set 中该 key 值的元素有几个。但是 map,set key 值都是不能重复的,所以 count 就用于查找该值是否在 map 中
2.1_242 有效的字母异位词
2.1.1 算法描述
异构词:只需要关注有哪些字母出现过,这些字母出现了几次
2.1.2 Python&C++ 代码实现
1.Python 代码实现
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
# define hash table(list--Dict)
count_s = [0]*26
for c in s:
count_s[ord(c)-ord('a')]+=1
for c in t:
count_s[ord(c)-ord('a')]-=1
for i in range(len(count_s)):
if count_s[i]!=0:
return False
return True
2.C++ 代码实现
class Solution {
public:
bool isAnagram(string s, string t) {
int count[26]={0}; // 数组即可
for(char c:s){
count[c-'a']++;
}
for(char c:t){
count[c-'a']--;
}
// 判断是否每个数都为 0
for(int c:count){
if(c!=0) return false;
}
return true;
}
};
2.1.3时空复杂度
时间复杂度:O(N)
空间复杂度:O(1) 因为 Dict 是一个常数
2.1.4相关技巧
字母个数计数器:
count_s = [0]*26
for c in s:
count_s[ord(c)-ord('a')]+=1 # get the ASCII to index
2.2_349两个数组的交集
2.2.1 算法描述
输出结果答案中的值是唯一的,并且可以不考虑输出结果的顺序
首先可以如上一道题,使用数组做哈希的题目,实现一个计数器。
但是因为并不知道计数器中需要保存多少个数值,如果数值之间跨度比较大,数值比较少那么就会造成一种浪费
所以这里使用 set 最为计数器的存储结构
2.2.2 python&C++ 代码
1.C++ 代码实现
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
set<int>set1(nums1.begin(),nums1.end());
set<int>res;
for(int num:nums2){
if(set1.find(num)!=set1.end()) res.insert(num);
}
return vector<int>(res.begin(),res.end());
}
};
易错点:
2.Python 代码实现
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
res_set = set()
set1 = set(nums1)
for i in nums2:
if i in set1:
res_set.add(i)
return list(res_set)
2.2.3 时空复杂度
时间复杂度:O(N)
空间复杂度:O(N)
2.3_202 快乐数
2.3.1 算法描述
这道题的关键在于两个地方:
①什么时候停止循环
这里肯定不能用 for 循环,因为不知道会判断几次,只能用 while
停止 while 循环条件:当 sums 重复出现的时候就要停止循环,所以要用 set 进行保存
②求各个位数之和
这个总结在本题技巧部分
2.3.2 C++&Python 代码实现
1.C++ 代码实现
class Solution {
public:
int getpos(int a){
int sums = 0;
while(a!=0){
sums+=(a%10)*(a%10);
a/=10;
}
return sums;
}
bool isHappy(int n) {
int val = getpos(n);
unordered_set<int>res;
while(1){
int val = getpos(n);
if(val==1) return true;
if(res.find(val)!=res.end()){ // val 在 set 中
return false;
}else{
res.insert(val);
n = val;
continue;
}
}
}
};
2.Python 代码实现
class Solution:
def isHappy(self, n: int) -> bool:
set_sums = set()
while 1:
sums = self.getSum(n)
if sums == 1:
return True
# if sums in set ,return False
elif sums in set_sums:
return False
else:
set_sums.add(sums)
n = sums
# get the every element of n
def getSum(self, n):
sums = 0
while n > 0:
sums += (n%10) * (n%10)
n //= 10
return sums
2.3.3 时空复杂度
时间复杂度:O()
2.3.4 技巧:得到一个数的每一位上的值
res = list()
def getSum(n):
sums = 0
while n > 0:
res.append(n%10)
n //= 10
2.4_1 两数之和
2.4.1 算法描述
1.C++ 算法描述
因为有 index 的参与所以需要保存数组下标。
然后判断 target-nums[i] 是否也保存在 map ,如果保存了就返回其 index ,如果没有找到就接着往下找
2.Python 算法描述
一共有三种数据结构可以选择:数组,Set,Dict
本题要是用的数据结构是 Dict
①不知道保存数据的长短,所以不能使用数组
②既要保存 value ,又要保存 index 所以不能用 Set
③所以选择 Dict
2.4.2 C++&Python 代码实现
1.C++ map 代码实现
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
// 定义一个 map
unordered_map<int,int> mp; // (value index)
// 遍历 nums
for(int i =0;i<nums.size();i++){
// 判断其被减数是否在 map 中
auto iter = mp.find(target-nums[i]);
if(iter!=mp.end()){
return {iter->second,i};
}
// 不在的话就要插入
mp.insert(pair<int,int>(nums[i],i));
}
return vector<int>(-1,-1);
}
};
2.Python 代码实现
方法1:Dict
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
dict_nums = defaultdict(int)
res = []
for i,v in enumerate(nums):
dict_nums[v] = i # key-->value in nums ; value-->index in nums
for i in range(len(nums)):
element = target-nums[i] # get the value
j = dict_nums[element] # find the whether in dict or not
if j and j!=i:
return [i,j]
方法2:双指针
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
for i in range(len(nums)):
sums = 0
for j in range(i+1,len(nums),1):
if((nums[i]+nums[j])==target):
return [i,j]
return None
2.4.3 时空复杂度
时间复杂度:O(N)
空间复杂度:O(N)
2.5_454四数相加2
2.5.1 算法描述
1.C++ 算法描述
本题要求出现的次数所以在设置 Map 时设置的 Map 的值就不能是 index 。而是记录某种组合出现的次数
本来应该是有一个 四重 for 循环去类似于排列的方法进行一一计算,但是可以使用 Map 的方式减少 for 循环的次数
将 nums1 和 nums2 两两组合计算,这样就是一个双重 for 循环。
因为这里允许重复,所以比方说如果 n1+n2 == a 出现了 5 次,但是 n3+n4 只出现1 次,最后的结果还是 5 ,不是 1 。所以在求次数的时候可以看到,加的是总的出现次数
count+=map[0-(c+d)];
2.Python 算法描述
本来想用 list 实现,但是发现 list 只能统计一个数是否出现过,无法判断出现次数
要求次数,并且不知道要保存多少个数值。所以使用 Dict 的数据结构实现 HashTable
这里两两为一个大组进行判断,这样就可以转换成两个大组之间的计算
nums1,nums2 并成一个大组;nums3,nums4并成一个大组
Dict 中:key --> 两个元素相加的值 value:前面相加的结果一共出现几次
本题和下面的四数之和不太一样,这个题只需要返回次数即可,不需要知道具体的值,所以是可以重复的
2.5.2 C++&Python 代码实现
1.C++ 代码实现
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
// 1. 定义一个 map ,key 值是 a+b 有可能出现的所有值
std::unordered_map<int,int> map ; // key:累加和 ; value: 该值出现的次数
int count = 0;
// 将 AB 的累加放到第一个数组
for(int a:nums1){
for(int b:nums2){
map[a+b]++;
}
}
// 减去 CD 的 for 循环
for(int c:nums3){
for(int d:nums4){
if(map.find(0-(c+d))!=map.end()){ // 可以进行相减
count+=map[0-(c+d)];
}
}
}
return count;
}
};
2.Python 代码实现
class Solution:
def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
hashmap = defaultdict(int) # 定义一个 map ,key-->可能出现的两数之和 value-->两数之和出现的次数,两两进行组合
for n1 in nums1: # 两两组合
for n2 in nums2:
if n1+n2 in hashmap:
hashmap[n1+n2] +=1
else:
hashmap[n1+n2] =1
count = 0
for n3 in nums3:
for n4 in nums4:
key = -n3-n4 # 取负
if key in hashmap:
count+=hashmap[key] # 可以是重复的,value 有几组就算几组
return count
2.5.3 时空复杂度
时间复杂度:O(N²)
空间复杂度:O(N)
2.6_383赎金信
2.6.1 算法描述
本题有一个将 ransomNote 的每个字符从 magazine 中的查找操作,所以想到使用 hash table 。
实现 hash table 有三种方法:①数组 ②set ③Dict
因为这里需要对字母出现的个数进行记录并且字母的个数有限,所以使用数组的方式对 字母和每个字母出现的个数进行记录
2.6.2 C++ & Python 代码实现
1.C++ 代码实现
数组实现字母的 map
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
// 出现的次数还要匹配
// magazine 的范围更广
int res[26] = {0};
// 将 mag 的字符放到 map 中
for(char c:magazine){
res[c-'a']++;
}
// 遍历 ran
for(char c : ransomNote){
if(--res[c-'a']<0){ // 用完了
return false;
}else continue;
}
return true;
}
};
方法1:list 实现
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
# record the str lenth of the letter
m_list = [0]*26
for c in (magazine):
m_list[ord(c)-ord('a')]+=1
for c in (ransomNote):
letter_index = ord(c)-ord('a')
m_list[letter_index]-=1
if m_list[letter_index]<0:
return False
return True
方法2:Dict 实现
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
zimu_dict = defaultdict(int)
for c in magazine:
if c in zimu_dict:
zimu_dict[c]+=1
else:
zimu_dict[c]=1
for c in ransomNote:
if c in zimu_dict:
zimu_dict[c]-=1
if zimu_dict[c]<0:
return False
else:
return False
return True
2.6.3 时空复杂度
时间复杂度: O(N)
空间复杂度:O(N)
2.7_15三数之和
2.7.1算法描述
1.什么时候可以对 nums 进行排序
这里我们返回的是 nums 中的值而不是 nums 中某几个元素的 index ,所以我们可以将 nums 进行排序然后再加以判断。
如果题目中要求返回 nums 的 index ,则无法打乱 nums ,或者先将 nums 的下标进行保存
2.步骤:
如果使用 for 循环的话需要使用三重 for 循环,但是也可以使用滑动窗口实现
需要对 nums 中的每一个元素进行判断。假设说现在判断 -4 这个元素
cur = -4 在这一轮循环中这个值就固定了,就要计算 cur 和它滑动窗口中的和
left 为 cur 右边第一个元素,right 为最后一个元素
将 cur ,left,right 的值相加,如果 sums <0 ,left 向右移动,如果 sums >0 right 向左移动
当 sums==0 时:
这个时候 cur 还没有判断完,滑动窗口中满足 sums 的 left 和 right 可能不止一个,所以还要继续移动 left ,right ;与此同时避免 left ,right 相邻重复的情况
3.在进行滑动窗口计算时先要判断:
①这个 cur 在刚才是否有判断过
②cur 的值是否>0,因为 nums 是顺序排序的,如果 cur>0 则后面不用再判断
4.技巧
三数之和可以使用固定一个值然后再滑动窗口的方式求和,减少 for 循环
易错点:
left 和 right 的去重操作
(1)在 i ,left,right 的值不满足taget 的时候是不用进行去重操作的,因为 nums[left],nums[right] 的值不会被放入 path 中,所以不用去重
(2)当 sums 的值等于 target 时就要进行去重操作,去重到下一个数和当前数不相等为止。去重的方式有很多
2.7.2 C++ & Python 代码实现
1.C++ 代码实现
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(),nums.end());
for(int i =0;i<nums.size();i++){
if(nums[i]>0) break; // 因为顺序了,如果 i 指向的元素>0 则不用接着判断了
if(i>0&&nums[i]==nums[i-1]) continue; // i 元素也要去重
int left = i+1;
int right = nums.size()-1;
while(left<right){
int sums = nums[left]+nums[right]+nums[i];
if(sums==0){
res.push_back(vector<int>{nums[i],nums[left],nums[right]});
// left,right 指向的元素不能立刻移动,也要去重
while(left<right&&nums[right]==nums[right-1]) right--;
while(left<right&&nums[left]==nums[left+1]) left++;
// 通过上一步 while 循环指向的还是重复值所以继续进行下一步收缩
right--;
left++;
}else if(sums>0) right--;
else left++;
}
}
return res;
}
};
2.Python 代码实现
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
res = []
n = len(nums)
nums = sorted(nums)
for i in range(n): # 逐个遍历 nums 中的每个元素
left = i+1
right = n-1
if nums[i]>0: # nums 已经排序,右边的都是比它大的。left right 都是从它右边开始判断
break
if i>=1 and nums[i]==nums[i-1]:# 为了防止出现结果一样的情况
continue
while left<right:
sums = nums[i]+nums[left]+nums[right]
if sums<0:
left+=1
elif sums>0:
right-=1
else:
res.append([nums[i],nums[left],nums[right]])
# 这时候 cur 的判断还没有结束,后面也许还有能满足条件 left 和 right
# left 和 right 也要跳过相等的值
while left<right and nums[left] == nums[left+1]:
left+=1
while left<right and nums[right] == nums[right-1]:
right-=1
left+=1
right-=1
return res
错误代码
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
if n<3:
return []
res = set()
for i in range(0,n-2,1):
for j in range(i+1,n-1,1):
for k in range(j+1,n,1):
if (nums[i]+nums[j]+nums[k])==0:
res.add((nums[i],nums[j],nums[k]))
return list(res)
这里不可以使用三重 for 循环计算值与之之间的和,然后保存到 Set 中,因为会出现下面重复的输出。而去重操作比较费时所以不建议这个方法
2.7.3 时空复杂度
时间复杂度:O(N²)
空间复杂度:O(N)
2.8_18 四数之和
2.8.1 算法描述
三数之和的解法是一层 for 循环中嵌套一个 while 循环,for 循环固定 nums[i] ,while 循环移动 left ,right
四数之和的解法是两层 for 循环,一层固定 nums[i] ,另一层固定 nums[j],在这两者都固定的情况下移动 left 和 right 的位置
如果有五个数组的话那就使用 3 层 for 循环,几层 for 循环就要有几个数组,然后减掉滑动窗口的两个指针即可
如何避免有重复的值产生:
i,j,left,right 都要判断它们的后一个值是否与当前值相同,如果相同的话就要 continue ,因为在这种情况下产生的结果是一样的
同样 left,right 指针也要再加一个判断,防止下一步还是指向相同的位置
易错点:
1.这里的 target 是任意值不能通过 nums[i]>target 就进行剪枝,所以这里只进行去重操作
2.这里对 j 进行去重时因为 j 是从 i+1 开始的,在判断 nums[j-1] 和 nums[j] 是否相等时 ,j 的范围是 j>i+1
3.这里不能使用 sums = nums[i]+nums[j]+nums[left]+nums[right] 会造成溢出
2.8.2 代码实现
1.C++ 代码实现
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i++) {
// 这种剪枝是错误的,这道题目target 是任意值
// if (nums[k] > target) {
// return result;
// }
// 去重
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
for (int j = i + 1; j < nums.size(); j++) {
// j 是从 i 开始的不是从 0 开始的
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
int left = j + 1;
int right = nums.size() - 1;
while (right > left) {
// nums[k] + nums[i] + nums[left] + nums[right] > target 会溢出
if (nums[i] + nums[j] > target - (nums[left] + nums[right])) {
right--;
// nums[k] + nums[i] + nums[left] + nums[right] < target 会溢出
} else if (nums[i] + nums[j] < target - (nums[left] + nums[right])) {
left++;
} else {
result.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right]});
// 去重逻辑应该放在找到一个四元组之后
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
}
return result;
}
};
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
nums = sorted(nums)
n = len(nums)
res = []
for i in range(n):
if i>0 and nums[i]==nums[i-1]:
continue
for j in range(i+1,n):
if j>i+1 and nums[j]==nums[j-1]: # 防止 j 有重复的情况
continue
left = j+1
right = n-1
while left<right:
if nums[i]+nums[j]+nums[left]+nums[right]>target:
right-=1
elif nums[i]+nums[j]+nums[left]+nums[right]<target:
left+=1
else:
res.append([nums[i],nums[j],nums[left],nums[right]])
# 避免 left 和 right 有重复值的情况下,接着寻找下一组解
while left<right and nums[left]==nums[left+1]:left+=1
while left<right and nums[right]==nums[right-1]:right-=1
left+=1
right-=1
return res
2.8.3时空复杂度
时间复杂度:O(N^3) ; 暴力法:O(N^4)
空间复杂度:O(N)
2.8.4 N数之和总结
①如果想求每两个数之间的和肯定需要进行 for 循环嵌套,两个 for 的原因是,第一个 for 循环中的数按着不动,第二个 for 循环在另一个数组或者本书组剩余值上滑动,依次求两个数之间的和
一般来讲双重 for 循环可以用双指针代替,但是可以像1.两数求和一样,使用的一个 Dict 代替
算法整体结构:
如果是求 N 数之和肯定有 N-2 个 for 循环进行嵌套,-2 是因为 left 指针和 right 指针
如何避免结果重复:
在对当前 cur 进行计算的时候在上一步这个是否有被计算过
与此同时在 left +1 ,right-1 的时候不要让 left 和 right 移动到和上一步一模一样的值上面
如何得到全部结果:
当 nums 的和进行相加等于 target 的时候,此时 left ,right 在 ij 的当前值上还要继续向前移动,才能保证当前 cur 结果的完整性
时间复杂度:
如果是暴力解法的,时间复杂度是 O(N^N),非暴力解法因为使用了双指针,所以将时间复杂度 ^(N) 变成了 ^(N-1)
3.其他题目
3.1_49字母异位词分组
哈希表
3.1.1 算法描述
异构词是具有相同字母构成但是字母顺序不同的单词。在往常使用 hash 对 a 单词中字母出现的次数进行映射。
这里使用的是对单词 a 的结构当做 value 值进行映射,如果结构一样就保存到 res 中。避免了一个个数字母个数的麻烦
3.1.2 C++ 代码实现
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<vector<string>>res; // 存放结果的数据结构
unordered_map<string,vector<string>> sig; // 对单词进行记录
for(int i =0;i<strs.size();i++){
string s = strs[i];
sort(s.begin(),s.end()); // 对一个单词进行排序
sig[s].push_back
(strs[i]); // 将具有相同结构的字符串当做 key 值放入
}
for(auto x:sig){x
res.push_back(x.second); // 将所有的 key 值放入
}
return res;
}
};
3.1.3 时空复杂度
3.2_128最长连续序列
3.2.1 算法描述
朴素的做法是:枚举nums中的每一个数x,并以x起点,在nums数组中查询后面连续的数 x + 1,x + 2,,,x + cur 是否存在。
假设查询到了 x + cur ,那么以 x 为起点的最长连续子序列长度即为 cur 。
向 1 ,100 ,200 都是起点
但是如果 x-1 也存在数组中那么对于 x 来说,x 肯定不是起点,所以如果 x 有 x-1 存在于数组中的话那 x 可以直接跳过,子算起点的子序列长度
因为要判断 x+1 ,x+2 …是否存在于数组中,这里使用 hash 的方式是快,并且在插入时最节约的
3.2.2 代码实现
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
// 使用 unordered_set 来解决
unordered_set<int> st(nums.begin(),nums.end());
int res = 0;
for(int i = 0;i<nums.size();i++){
if(!st.count(nums[i]-1)){
// 以该值为起点时的最长递增序列
int cur = 1;
while(st.count(nums[i]+cur)){
cur++;
}
res = max(res,cur);
}
}
return res;
}
};
3.2.3 时空复杂度
时间复杂度:O(N)
空间复杂度:O(N)
3.3_41. 缺失的第一个正数
3.3.1 算法描述
1.情况 1
本题是使用原地 hash 的算法做到时间复杂度为 O(N) 空间复杂度为 O(1)
在一般情况下排序的时间复杂度都是 O(nlogn) 但是有在一种情况下可以将时间复杂度降为 O(N),如:
对 [0,1,2,5,3,4] 使用 O(1) 进行排序
这种方法的大体思路就是将 index = i 的位置放置 i 这个值,这里使用一个 for 循环遍历整个数组
S1:看 nums[0] 是 0 不–> 是–> 不做任何操作
S2:看 nums[1],nums[2] 都是 1,2 所以不做任何操作
S3:看 nums[3] 是 3 不,不是–> nums[3] 是 5 ,所以物归原主,将 nums[3] 的 5 放在 nums[nums[3]] 的位置
S4:看 nums[4] 是 4 不,不是–> 将 nums[4] 放在它本应该在的位置 nums[nums[4]] 上
S5:。。。。。。
也就是在 for 循环遍历的时候首先判断 nums[i] 上是 i 嘛,如果不是那么就要将 nums[i] 上的值放在它本身的位置上也就是 nums[nums[i]] 上
2.情况 2:循环交换
但是我们并不能保证本题给的数组就是 [1,n] 所有数都存在,但是方法不变
[2,4,3,5,2] 这个题是没有 1 的而且还存在重复的数
排序过程是:
[2,4,3,5,2]–>[4,2,3,5,2]–>[5,2,3,4,2]–>[2,2,3,4,5]
为了防止两个 [2,2] 会一直循环,也就是形成了一个环路,也就是代表了 nums[i] 该放的位置和 nums[i] 是一样的
nums[i] != nums[nums[i]]
注意本题最后得到的 nums 如果不是 情况 1,那么我们得到的 nums 不一定是有序的,它只能看出 index = i 时 i 是否在那个位置上,如果那 i 这个值不在 index = i 上,说明 i 是确实的,因为我们是根据 index 定位值的,而且本题求最小正数,缺失的 i 就是最后答案
通过上面的描述我们所要做的就是一直交换,直到将 nums[i] 放在正确的 nums[nums[i]] 中,但不是所有的值都能交换,可以交换的情况如下
1.同理 nums[i]>n 的时候也是没有办法交换的,就像上面例子中的 100 一样
2.nums[i] 是负数,因为这里要将 nums[i] 放到 index 为 i 的地方,这里 nums[i] 都是负数了他也没有地方放,所以就不用交换
3.i 位置上的值不为 i
4.永久循环,就像上面的例子其中 [2,2] 这两个数会不断的交换,永无止境
每一次 while 循环都会有一个元素被放到正确的位置上
[0,1,2,5,3,4]
当 for 循环遍历到 5 时发现 i!=nums[i],然后交换位置
3.3.2 代码实现
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
// 每一次将一个数放在合适的位置
nums.insert(nums.begin(),0);
int n = nums.size();
for(int i = 0;i<nums.size();i++){
// 判断该数是否可以放在合适的位置或者其他的数是否可以放在他的位置
while(i!=nums[i]&&nums[i]>=0&&nums[i]<n&&nums[i]!=nums[nums[i]]) {
swap(nums[i],nums[nums[i]]);
}
}
// 从前向后遍历
for(int i = 1;i<n;i++) if(i!=nums[i]) return i;
return n;
}
};