leetcode刷题笔记(剑指:哈希表标签)
文章目录
- leetcode刷题笔记(剑指:哈希表标签)
- [面试题 01.01 判定字符是否唯一(easy)](https://leetcode.cn/problems/is-unique-lcci/)
- [剑指 Offer 03:数组中重复的数字(easy)](https://leetcode.cn/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/)
- [剑指 Offer 07:重建二叉树(medium)](https://leetcode.cn/problems/zhong-jian-er-cha-shu-lcof/)
- [剑指 Offer 39:多数元素(easy)](https://leetcode.cn/problems/majority-element/)
- [剑指 Offer 35:复杂链表的复制(medium)](https://leetcode.cn/problems/fu-za-lian-biao-de-fu-zhi-lcof/)
- [剑指 Offer 48:最长不含重复字符的子字符串(medium)](https://leetcode.cn/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/) ————————记得看其他解法
- [剑指 Offer 49:丑数(medium)](https://leetcode.cn/problems/chou-shu-lcof/)
- [剑指 Offer 50:第一个只出现一次的字符(easy)](https://leetcode.cn/problems/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof/) ————————队列后面记得看
- [剑指 Offer 52:两个链表的第一个公共节点(easy)](https://leetcode.cn/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/)
- [剑指 Offer 53-2:0~n-1中缺失的数字(easy)](https://leetcode.cn/problems/que-shi-de-shu-zi-lcof/)
哈希只是种数据结构,算不上是什么思想算法应该,这里只是为了熟悉哈希的用法,要想起这个东西。
面试题 01.01 判定字符是否唯一(easy)
实现一个算法,确定一个字符串
s
的所有字符是否全都不同。
首先很容易想到使用哈希表的方法,此时时间和空间复杂度都为O(n)
class Solution {
public:
bool isUnique(string s) {
int n=s.size();
unordered_map<char,int> umap;
for(int i=0;i<n;++i){
if(umap[s[i]]) return false;
umap[s[i]]++;
}
return true;
}
};
采用哈希表是为了记录每个字符出现的次数,但会使用多余的空间。因此可以采用位运算,每一个bit位记录一个字符是否出现,从而不使用额外的数据结构。
class Solution {
public:
bool isUnique(string s) {
int n=s.size(),tmp=0,step;
for(int i=0;i<n;++i){
step=s[i]-'a';
if(tmp & 1<<step) return false;
tmp=tmp|(1<<step);
}
return true;
}
};
剑指 Offer 03:数组中重复的数字(easy)
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
和上一题基本上一样。首先想到哈希表。但本题不能采用位运算,因为上一题只有小写字母26个,而本题数据范围为2-10000,10000个bit位,会越界?。
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
int n=nums.size();
unordered_map<int,int> umap;
for(int i=0;i<n;++i){
if(umap[nums[i]]) return nums[i];
umap[nums[i]]++;
}
return -1;
}
};
注意题目的条件,他并不是任意的随机一个数组,而是长度为n,且范围在0~n-1。因此可以利用数组的特殊性进行解题优化。
如果没有重复或者缺失,每个元素都应该满足nums[i]=i,每个元素都有它应该在的一个位置。因此利用这个性质,可以依次遍历,把每个元素放在正确的位置上,如果在交换过程中,待交换位置元素已经放置正确,那说明交换位置的nums[i],和待交换位置的nums[nums[i]]相同,因此其为重复元素。
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
int n=nums.size();
for(int i=0;i<n;++i){
while(nums[i]!=i){
if(nums[i]==nums[nums[i]]) return nums[i];
//交换位置,当前于元素归位
int tmp_inx=nums[i];
nums[i]=nums[tmp_inx];
nums[tmp_inx]=tmp_inx;
}
}
return -1;
}
};
剑指 Offer 07:重建二叉树(medium)
输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。
假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
class Solution {
public:
unordered_map<int,int> umap;
int inx;
TreeNode* helper(vector<int>& preorder, vector<int>& inorder,int left,int right){
if(left>right) return nullptr;
int root_val=preorder[inx];
TreeNode* node=new TreeNode(root_val);
int root_idx_in=umap[root_val];
++inx;
node->left=helper(preorder,inorder,left,root_idx_in-1);
node->right=helper(preorder,inorder,root_idx_in+1,right);
return node;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
int n=preorder.size();
inx=0;
if(n==0) return nullptr;
for(int i=0;i<n;++i) umap[inorder[i]]=i; //存储中序遍历的元素对应索引
//对于前序遍历,第一个元素是根节点
//对于中序遍历,根节点的前后子数组分别对应左右子树
return helper(preorder,inorder,0,n-1);
}
};
剑指 Offer 39:多数元素(easy)
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
首先找多数元素,肯定可以想到hash计数
class Solution {
public:
int majorityElement(vector<int>& nums) {
unordered_map<int,int> umap;
int n=nums.size();
for(int i=0;i<n;++i){
umap[nums[i]]++;
if(umap[nums[i]]>n/2) return nums[i];
}
return -1;
}
};
考虑题目的特殊性,多数元素大于元素数量的一半。因此可以先排序,中间元素一定是多数元素。
class Solution {
public:
int majorityElement(vector<int>& nums) {
//排序,然后中间的
sort(nums.begin(),nums.end());
return nums[nums.size()/2];
}
};
对于本题,还可以采用投票法。将多数元素看为1,其他为-1,最后相加结果一定大于0。
class Solution {
public:
int majorityElement(vector<int>& nums) {
int sum=0,tmp;
for(int i=0;i<nums.size();++i){
if(sum==0){
sum++;
tmp=nums[i];
continue;
}
if(nums[i]==tmp) sum++;
else sum--;
}
return tmp;
}
};
剑指 Offer 35:复杂链表的复制(medium)
请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
采用哈希表建立各节点的映射关系。
/*
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
*/
class Solution {
public:
Node* copyRandomList(Node* head) {
if(head == nullptr) return nullptr;
Node* node=head;
unordered_map<Node*,Node*> umap;
while(node!=nullptr){
umap[node]=new Node(node->val);
node=node->next;
}
node=head;
while(node!=nullptr){
umap[node]->next=umap[node->next];
umap[node]->random=umap[node->random];
node=node->next;
}
return umap[head];
}
};
剑指 Offer 48:最长不含重复字符的子字符串(medium) ————————记得看其他解法
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
采用unordered_map记录出现的字符,value为字符对应的索引。
采用left记录子字符串的起始索引。当 当前字符s[i]出现过的时候,此时子字符串的长度为i-left,并更新left=umap[s[i]]+1。
此时,需要将left~umap[s[i]]范围内的字符对应value置为0,接着进行下一轮子字符串的遍历查询。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int n=s.size();
//哈希表记录已经出现的字符,key:字符 value:索引
//如果找到了,则将索引idx及之前的 字符的value记为0
int ans=0;
int left=0;
unordered_map<char,int> umap;
for(int i=0;i<n;++i){
if(i==0){
//i=0的时候,特殊处理,以免与未出现过的字符产生混淆
umap[s[i]]=n;
}
else{
if(umap[s[i]]==0){
umap[s[i]]=i;
}
else{
ans=max(ans,i-left);
int j;
if(umap[s[i]]==n) j=0;
else j=umap[s[i]];
//将出现过字符及之前的字符value置为0
for(int k=left;k<=j;++k){
umap[s[k]]=0;
}
umap[s[i]]=i;
left=j+1;
}
}
}
ans=max(ans,n-left);
return ans;
}
};
还有其他解法欸,后面再继续
剑指 Offer 49:丑数(medium)
我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
标签里加了哈希表,但是感觉用动态规划三指针更好懂。题解里给的哈希表标签是用来建堆时去重的。也去学习下吧!!!不要懒
动态规划做法:下一个丑数是通过前面的丑数*2 *3 *5 然后比较出来的最小的数得到的。每个丑数都需要 *2 *3 *5 ,但是无法保证他的位置。题解里使用了三指针n1,n2,n3分别代表 *2 *3 *5 后的数,而i1,i2,i3是dp数组中的索引,如果乘积后成为了新的丑数或者和新的丑数相等,索引就会+1,由此保证不会有乘积/丑数被遗漏。
class Solution {
public:
int nthUglyNumber(int n) {
//首先理解题意,丑数是可以通过只用2 3 5随便怎么乘积可以得到的数
vector<int>dp(n);
dp[0]=1;
int i1=0,i2=0,i3=0;
for(int i=1;i<n;++i){
int n1=dp[i1]*2;
int n2=dp[i2]*3;
int n3=dp[i3]*5;
dp[i]=min(n1,min(n2,n3));
if(dp[i]==n1) i1++;
if(dp[i]==n2) i2++;
if(dp[i]==n3) i3++;
}
return dp[n-1];
}
};
最小堆+哈希。关于优先队列,还有堆,其实还不太熟悉。相当于采用堆是把当前丑数都*2 *3 *5 ,然后存入堆中排序,是堆顶的时候取出,有点暴力那味。而动态规划是一个个的按顺序的找。也很明显这个复杂度高很多。
(25条消息) c++优先队列(priority_queue)用法详解_c++优先级队列_吕白_的博客-CSDN博客
class Solution {
public:
int nthUglyNumber(int n) {
vector<int> factors = {2, 3, 5};
unordered_set<long> seen;
priority_queue<long, vector<long>, greater<long>> heap; //小顶堆,升序排列的
seen.insert(1L); //1L就是1,但long类型,不然后面会溢出
heap.push(1L);
int ugly = 0;
for (int i = 0; i < n; i++) {
long curr = heap.top();
heap.pop(); //堆顶最小,弹出记录
ugly = (int)curr; //第i+1个丑数
for (int factor : factors) {
long next = curr * factor;
//没有出现过,所以插入堆和set中保存,priority_queue会自动排序,本质是个堆实现
if (!seen.count(next)) {
seen.insert(next);
heap.push(next);
}
}
}
return ugly;
}
};
//作者:LeetCode-Solution
//链接:https://leetcode.cn/problems/chou-shu-lcof/solution/chou-shu-by-leetcode-solution-0e5i/
//来源:力扣(LeetCode)
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
剑指 Offer 50:第一个只出现一次的字符(easy) ————————队列后面记得看
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
很快就能想到hash表计数啦。时间复杂度是O(n)
class Solution {
public:
char firstUniqChar(string s) {
int n=s.size();
unordered_map<char,int> umap;
for(int i=0;i<n;++i){
umap[s[i]]++;
}
for(int i=0;i<n;++i){
if(umap[s[i]]==1) return s[i];
}
return ' ';
}
};
题解里还提到了队列,平时做题基本上想不到用队列/栈之类的。
剑指 Offer 52:两个链表的第一个公共节点(easy)
输入两个链表,找出它们的第一个公共节点。
最开始能想到的,哈希表先存储一条链的节点,再遍历第二条,如果有一样的就是公共节点。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
//哈希表先存储一条链的节点,再遍历第二条,如果有一样的就是公共节点
ListNode *node1=headA,*node2=headB;
unordered_map<ListNode*,int> umap;
while(node1!=nullptr){
umap[node1]++;
node1=node1->next;
}
while(node2!=nullptr){
if(umap[node2]) return node2;
node2=node2->next;
}
return nullptr;
}
};
采用哈希会用到额外的空间,可以考虑双指针的方法。这个也好理解,反向来推的话,比如两个链表长度分别为len1,len2,两个指针node1,node2分别从两条链表起始开始,走到头后又从另一条链表开始,那么他们都走过的长度是len1+len2,相交节点到末尾距离令为tmp,是他们最后都为经历的,因此他们都会在len1+len2-tmp步长的时候相遇。再正向去想······感觉有点没解释清楚。
评论区真的——浪漫的嘞。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *node1=headA,*node2=headB;
while(node1!=node2){
if(node1!=nullptr) node1=node1->next;
else node1=headB;
if(node2!=nullptr) node2=node2->next;
else node2=headA;
}
return node2;
}
};
剑指 Offer 53-2:0~n-1中缺失的数字(easy)
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
给了用哈希表的标签,官方题解就是把nums中元素放到set里面,然后遍历0~n-1看缺哪个。。只能说很抽象没必要吧。还不如直接遍历。
第一反应就是遍历数组,因为已经有序,如果不缺的话nums[i]=i,如果不满足那么缺的就是i呀。然后考虑边界情况。这个应该算暴力法吧。
class Solution {
public:
int missingNumber(vector<int>& nums) {
//已经有序
int n=nums.size();
for(int i=0;i<n;++i){
if(nums[i]!=i) return i;
}
return n;
}
};
上面这个方法时间复杂度O(n)。因为已经有序,还可以考虑二分,降低复杂度。二分一个很重要的要判断边界问题,什么时候去等什么时候不取等!!!
class Solution {
public:
int missingNumber(vector<int>& nums) {
//采用二分实际上还是对nums[i]==i进行判断,只是不是全部遍历罢了
int left=0,right=nums.size()-1;
while(left<right){
int mid=(right-left)/2+left;
if(nums[mid]==mid) left=mid+1; //相等则一定不缺!!
else right=mid; //否则存在缺的可能
}
//考虑右边界
return nums[right] != right ? right : right + 1;
}
};
数学方法,就是0~n-1的和可求,原数组和也可求,两个相减就是缺的数,时间复杂度也为O(n)。没必要写。
位运算,两个一样的数异或为0,异或具有交换组合性质。因此可以将数组的数相异或,再和0~n-1异或。最后剩的就是差的那个。
class Solution {
public:
int missingNumber(vector<int>& nums) {
int n=nums.size();
int ans=0;
for(int i=0;i<n;++i){
ans^=nums[i];
}
for(int i=0;i<=n;++i){ //这里需要注意的是n-1的递增序列,它的可能最大数为n!!因此这里要取等
ans^=i;
}
return ans;
}
};