文章目录
1.两数之和(哈希表)
算法:
用哈希表unordered_map<int,int>存储元素下标
- 用指针i遍历整个nums数组,对于每个nums[i],在哈希表中查找是否存在对应的
target-nums[i]
,如果没有将i的下标存入哈希表- 时间复杂度是O(n)
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int n=nums.size();
unordered_map<int,int> hash;
for(int i=0;i<n;i++){
//因为数组中存在下标0,所以用count函数判断是否存在该哈希值
if(hash.count(target-nums[i]))return {hash[target-nums[i]],i};
hash[nums[i]]=i;
}
return {-1,-1};
}
};
187.重复的DNA序列(哈希表)
算法
- 遍历这个字符串,用哈希表存储每个长度为10的字符串的出现次数
- 统计所有出现次数>=2 的字符串
class Solution {
public:
vector<string> findRepeatedDnaSequences(string s) {
unordered_map<string,int> hash;
vector<string> res;
for(int i=0;i+10<=s.size();i++){ //注意边界是i+10<=s.size()
string c=s.substr(i,10);
if(hash[c]==1)res.push_back(c); //如果当前出现过一次,统计入res,不能写>=1,避免重复统计
hash[c]++;
}
return res;
}
};
706.设计哈希映射 **
- 邻接表法
- N取质数避免冲突
- find操作应当返回一迭代器
- 用数组链表模拟哈希表(邻接表法)Vector<list <pair<int,int>>>
class MyHashMap {
public:
int N=10007;
vector<list<pair<int,int>>> h;
/** Initialize your data structure here. */
MyHashMap() {
h=vector<list<pair<int,int>>> (N);
}
list <pair<int,int>> ::iterator find(int key){
int t=key % N;
for(auto it=h[t].begin();it!=h[t].end();it++)
if(it->first==key) return it;
return h[t].end();
}
/** value will always be non-negative. */
void put(int key, int value) {
auto it=find(key);
int t= key % N;
if(it==h[t].end())h[t].push_back({key,value}); //判断条件里不能写it->first 如果返回的it不存在就会报空指针错误
else it->second=value;
}
/** Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key */
int get(int key) {
auto it=find(key);
int t=key % N;
if(it==h[t].end())return -1;
else return it->second;
}
/** Removes the mapping of the specified value key if this map contains a mapping for the key */
void remove(int key) {
auto it=find(key);
int t= key % N;
if(it!=h[t].end())h[t].erase(it);
}
};
开放寻址法
- 开放寻址法的基本思想是这样的:如果当前位置已经被占,则顺次查看下一个位置,直到找到一个空位置为止。
- 对于每一个位置的状态初始化为-1,代表位置为空
- 对于find操作,我们从哈希表的hash_key[key%N]处开始逐空查找,直到找到值为key或者位置为空(-1)为止
- 对于remove操作,我们不能再将任意位置的值变回-1,而是赋予其他的值(本题为-2),如果赋予-1,那么就会把后面的值阻断,导致查询不到数据,这样就可能重复的插入key值
class MyHashMap {
public:
const static int N=20011; //必须初始化为静态变量,不然报错,不知道为啥
int hash_key[N],hash_value[N];
/** Initialize your data structure here. */
MyHashMap() {
memset(hash_key,-1,sizeof hash_key); //初始化为-1
}
int find(int key){
int t=key % N;
while(hash_key[t]!=-1&&hash_key[t]!=key){
if(++t==N)t=0; //如果找到头,则重头开始
}
return t;
}
/** value will always be non-negative. */
void put(int key, int value) {
int t=find(key);
hash_key[t]=key;
hash_value[t]=value;
}
/** Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key */
int get(int key) {
int t=find(key);
if(hash_key[t]!=-1)return hash_value[t];
else return -1;
}
/** Removes the mapping of the specified value key if this map contains a mapping for the key */
void remove(int key) {
int t=find(key);
if(hash_key[t]!=-1)hash_key[t]=-2;
}
};
652.寻找重复子树 ** (二次哈希)
- 用
前序遍历
的方式将二叉树转化成字符串,详见## Leetcode 297.二叉树的序列化和反序列化
这样每个子树都具有唯一确定的字符串如2,4,'#'
,我们枚举(dfs)每一个节点,对于该节点可以构造出唯一的一个字符串,并将字符串加入Unordered_map<string ,int> 当拥有个数等于2 时,保存为一个结果- 每个结点仅遍历一次,unordered_map 单次操作的时间复杂度为 O(1),一共有n个节点,总复杂度为O(n)
- 遍历结点中,可能要拷贝当前字符串到答案,拷贝(
string 的相加
)的时间复杂度为 O(n),故总时间复杂度为 O(n^2),相当于一个二重循环
//O(n^2)时间复杂度
class Solution {
public:
unordered_map<string ,int> hash;
vector<TreeNode* >res;
vector<TreeNode*> findDuplicateSubtrees(TreeNode* root) {
dfs(root);
return res;
}
string dfs(TreeNode* root){
if(!root)return "#";
string cur;
cur+=to_string(root->val)+',';
cur+=dfs(root->left)+','; //字符串的拷贝
cur+=dfs(root->right);
hash[cur]++;
if(hash[cur]==2)res.push_back(root); //个数等于2时,将该节点做根节点返回到res中
return cur;
}
};
优化算法
- 由于每一次复制字符串时会占用额外的O(n)的空间,我们将每一个字符串先映射为一个整数
- 例如"#“可以映射在哈希表
unordered_map<string ,int > hash
为1,而数字1在另一个哈希表unordered_map<int ,int> count
映射为”#"出现的次数 ,这样每次返回一个整数对应的字符串,可以将string 的复制操作降为 O ( 1 ) O(1) O(1)- 实际的映射只需要将dfs出的字符串按数字顺序映射即可
class Solution {
public:
unordered_map<string ,int> hash; //字符串映射为整数
unordered_map<int,int> count; //记录每个字符串的个数
int cnt=0;
vector<TreeNode* >res;
vector<TreeNode*> findDuplicateSubtrees(TreeNode* root) {
hash["#"]=++cnt; //空字符串先映射为1
dfs(root);
return res;
}
string dfs(TreeNode* root){
if(!root)return to_string(hash["#"]); //root为空返回"#"对应的整数(转化成string)
string cur;
cur+=to_string(root->val)+','+dfs(root->left)+','+dfs(root->right); //每次相加最多只加一个字符,时间复杂度降为O(1)
if(!hash.count(cur))hash[cur]=++cnt; //如果当前字符串还没有出现过,先在hash中添加他的一个映射
int t=hash[cur];
count[t]++; //字符串个数++
if(count[t]==2)res.push_back(root);
return to_string(hash[cur]);
}
};
560.和为k的子数组(前缀和+哈希)
正确解法是用前缀和+哈希,只想到一半
- 首先应想到统计数组中的前缀和…暴力想法是对于前缀和数组
sum
,我们枚举每个前缀和sum[i]
,
那么如果存在答案的话,一定有j属于[0,i-1]
,使得sum[i]-sum[j]=k
,- 换句话说只需要在
[0,i-1]
区间内找到满足条件的前缀和,且这些前缀和的值都等于sum[i]-k
- 这样我们使用一个哈希表对前缀和的个数进行标记,每遍历一个
sum[i]
都在哈希表中找一下符合条件的答案即可- 注意这道题的写法,并没有额外的统计出前缀和,写法比较精巧
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int,int> hash;
int res=0;
hash[0]=1; //前缀和为0的数据有一个
for(int i=0,sum=0;i<nums.size();i++){ //对于每个前缀和,求出相应的hash[sum-k]个数,顺便统计前缀和
sum+=nums[i];
res+=hash[sum-k];
hash[sum]++;
}
return res;
}
};
547.朋友圈(并查集)
此题考察并查集的基本操作
- 基础的并查集能解决的一类问题是不断将两个元素所在集合合并,并随时
询问两个元素是否在同一集合
。- 定义数组
p[i]
表示i
元素所在集合的根结点。初始时,所有元素所在集合的根结点就是自身。- 合并时,直接将两个集合的根结点合并,即修改 p 数组。
- 查询时,不断通过判断 i 是否等于 f(i) 的操作,若不相等则递归判断 f(f(i)),直到 i == f(i) 为止。
但以上做法会在一条链的情况下单次查询的时间复杂度退化至线性,故可以采用路径压缩优化,将复杂度降到近似常数。- 对于此题,每合并一个区域就减少一组关系(res),遍历完数组输出即可
class Solution {
public:
vector<int> p;
int find(int x){
if(p[x]!=x)p[x]=find(p[x]);
return p[x];
}
int findCircleNum(vector<vector<int>>& M) {
int n=M.size();
for(int i=0;i<n;i++)p.push_back(i);
int res=n;
for(int i=0;i<n;i++)
for(int j=0;j<i;j++){
if(M[i][j]==1&&find(i)!=find(j)){ //ij是朋友关系,如果不在一个连通块内,则合并该连通块,并将res-1
p[find(i)]=find(j);
res--;
}
}
return res;
}
};
684.冗余连接(并查集)
同样考察并查集的基本操作
首先题目给了一个具有N个节点的树,那么一共会连接N-1条边,此时该树多连接了一条边就会构成一个环,输出多余的这条边即可
找到这条边比较简单,即如果两条边不在一个连通块内,我们将他们合并,如果发现已经联通,那么说明要添加的那一条边就是多余的边,输出他即可
class Solution {
public:
vector<int> p;
int find(int x){
if(p[x]!=x)p[x]=find(p[x]);
return p[x];
}
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
int n=edges.size();
p=vector<int> (n+1);
for(int i=1;i<=n;i++)p[i]=i; //下标从1 开始
for(int i=0;i<n;i++){
int u=edges[i][0],v=edges[i][1];
if(find(u)!=find(v))p[find(u)]=find(v); //u,v没有被连接,将它们联通
else return {u,v}; //如果u,v已经被联通,说明他们是多余的边
}
return {-1,-1};
}
};
692.前K个高频单词(堆)
- 本题使用c++STL库函数
priority_queue
(优先队列/大根堆)来求解,考察优先队列的基本操作top()
返回堆顶元素O(1)pop()
弹出堆顶元素O(1)push()
加入元素并排序 O(logn)- 算法
- 首先使用哈希表对全部单词出现次数进行统计,随后将他们加入堆,(因为默认是大根堆,所以我们将元素数值取负数,让最小的数排在堆顶)并且维持堆中元素为k个,当第堆中元素大于k的时候让堆顶元素弹出即可
class Solution {
public:
vector<string> topKFrequent(vector<string>& words, int k) {
typedef pair<int,string> PIS;
priority_queue<PIS> heap;
unordered_map<string,int> hash;
for(auto w:words)hash[w]++;
for(auto it:hash){
PIS t(-it.second,it.first);
heap.push(t);
if(heap.size()>k)heap.pop();
}
vector<string> res(k);
for(int i=k-1; i>=0; i--){ //由于取负数排列,正序输出为从小到大,我们反序输出即可
res[i]=heap.top().second;
heap.pop();
}
return res;
}
};
295.数据流的中位数(对顶堆)
- 维护动态有序序列需要使用平衡树,这里我们直接用两个二叉堆实现
- 如图所示,我们使用一个大根堆,一个小根堆,大根堆的所有元素要小于小根堆的所有元素
- 在动态的维护过程中,我们始终保证小根堆的元素大于等于大根堆的元素(最多多一个元素)那么当维护过程结束时,如果共有偶数个数,中位数就是两个堆的顶点,如果是奇数个数,中位数就是小根堆的顶点
- 在插入过程中,始终应当满足
如果x大于大根堆顶点,就将其插入小根堆,否则将其插入大根堆
- 保证在维护过程中上面堆至少要大于等于下面堆的元素个数,
所以每次插入元素之后,都挪动一个元素到小根堆,最后判断如果大根堆的元素过少时,再向下移动元素到大根堆
- 向下挪动一个元素相当于小根堆少了一个而大根堆多了一个元素,如果元素个数是偶数的话,不可能出现个数不平衡的情况
class MedianFinder {
public:
priority_queue<int,vector<int>,greater<int> > up; //初始化为小根堆
priority_queue<int > down; //大根堆
/** initialize your data structure here. */
MedianFinder() {
}
void addNum(int num) {
if(down.empty()||num>=down.top())up.push(num); //大根堆为空或者插入元素大于大根堆顶,将元素插入到小根堆
else{
down.push(num); //否则将元素插入到大根堆,并且默认将大根堆元素向上转移一个
up.push(down.top());
down.pop();
}
if(up.size()>down.size()+1){ //如果上面小根堆元素过多,再将其转移到大根堆
down.push(up.top());
up.pop();
}
}
double findMedian() {
if(down.size()+up.size()&1)return up.top(); //奇数返回小根堆堆顶,偶数返回两个堆顶的平均数
else return (down.top()+up.top())/2.0;
}
};
352.将数据流变为多个不相交的区间
- 用
map
维护所有区间,为此我们需要map<int,int>L
可以从右端点索引到左端点map<int,int>R
可以从左端点索引到右端点- 注意用
unordered_map
是不行的,因为要调用lower_bound函数找到大于等于x的第一个位置,所以无序的哈希表是满足使用要求的- 由此,当我们插入一个区间
[x,x]
的时候,我们考察x左右两边距离为1的位置
,应该分成四种情况
- x的左右两边都存在,将三个区间合并
- x的左边区间存在,将左区间合并
- x的右边区间存在,将右区间合并
- x的左右两边都没有区间,将
[x,x]
插入
class SummaryRanges {
public:
map<int,int> L,R;
/** Initialize your data structure here. */
SummaryRanges() {
}
void addNum(int x) {
if(L.size()){
auto t=L.lower_bound(x); //返回L中第一个大于等于x的迭代器
if(t!=L.end()&&t->second<=x)return ; //如果x处于已有区间内,直接返回,此处只能用t->second,不能用t.second
}
int left=L.count(x-1),right=R.count(x+1); //找到x的左右两边是否有区间存在,分四种情况讨论
if(left&&right){ //左右都存在,合并三个区间
R[L[x-1]]=R[x+1];
L[R[x+1]]=L[x-1];
L.erase(x-1),R.erase(x+1); //结束后要分别删除L,R中原本对应的边界
}
else if(left){ //合并左边区间
R[L[x-1]]=x;
L[x]=L[x-1];
L.erase(x-1);
}
else if(right){ //合并右边区间
L[R[x+1]]=x;
R[x]=R[x+1];
R.erase(x+1);
}
else{ //直接插入[x,x]
L[x]=x;
R[x]=x;
}
}
vector<vector<int>> getIntervals() {
vector<vector<int>>res;
for(auto it:R)res.push_back({it.first,it.second});
return res;
}
};