0%

1. 两数之和

1
2
3
4
5
6
7
8
9
10
std:unordered_map<int,int> map;
//key 存数字 value存index
for(int i = 0 ;i < nums.size(); i++){
auto iter= map.find(target-nums[i]);
if(iter!=map.end()){
return{iter->second,i};
}
map.insert(pair<int,int>(nums[i],i));
}
return {};
1
2
3
4
5
6
7
8
9
unordered_map<int,int> map;
for(int i = 0 ; i <nums.size();i++){
unordered_map<int,int>::iterator iter=map.find(target-nums[i]);
if(iter!=map.end()){
return {iter->second,i};
}
map.insert(pair<int,int>(nums[i],i));
}
return {};

Q: 在这边 map.insert(pair<int,int>(nums[i],i)); 可以换成 map.insert(unordered_map<int,int>(nums[i],i));吗

A: 不可以,因为 std::unordered_map 是一个关联容器,其中的元素是键值对(key-value pair),而 unordered_map<int,int>(nums[i], i) 是一个无序键值对,不能直接用来插入到 std::unordered_map 容器中。

在这个例子中,我们需要将每个元素的值 nums[i] 和下标 i 插入到 map 中,以便在后续查找过程中能够快速找到匹配的元素。因此,我们需要使用 std::pair<int, int> 类型来表示每个元素的键值对,然后将其插入到 map 中。

在代码中,map.insert(pair<int, int>(nums[i], i)); 表示将一个由 nums[i]i 组成的键值对插入到 map 中。这里使用了 std::pair<int, int> 类型来表示键值对,从而可以将其作为 map 的元素插入到容器中。

需要注意的是,C++ 中提供了很多种容器类型,每种容器类型都有其特定的用途和特性。在使用容器时,需要根据具体情况选择合适的容器类型,并了解容器类型的特性和使用方法,以提高代码的效率和可读性。

202. 快乐数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int getSum(int n){
int sum=0;
while(n){
sum+=(n%10)*(n%10);
n/=10;
}
return sum;
}
bool isHappy(int n){
unordered_set<int> set;
while(1){
int sum=getSum(n);
if(sum==1){
return true;
}
// 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false
if(set.find(sum)!=set.end()){
return false;
}
else{
set.insert(sum);
}
n=sum;
}
}

349. 两个数组的交集

set

1
2
3
4
5
6
7
8
9
10
11
12
vector<int> intersection(vector<int>& nums1,vector<int>& nums2){
unordered_set<int> result_set;//存放结果,给结果去重
unordered_set<int> nums_set(nums1.begin(),nums1.end());
for(int num: nums2){
//发现nums2的元素,在nums_set里又出现过
//nums_set.end() 表示 std::unordered_set 中最后一个元素的下一个位置的迭代器
if(nums_set.find(num)!=nums_set.end()){
result_set.insert(num);
}
}
return vector<int>(result_set.begin(),result.end());
}

数组

1
2
3
4
5
6
7
8
9
10
11
12
13
vector<int> intersection(vector<int>& nums1,vector<int>& nums2){
unordered_set<int> result_set; //save result
int hash[1005]={0};//默认0
for(int num:nums1){//出现过的字母在hash数组中做记录
hash[num]=1;
}
for(int num:num2){//nums2中出现的话,result记录
if(hash[num]==1){
result_set.inset(num);
}
}
return vector<int>(result_set.begin(),result_sey.end());
}

首先什么是 哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。

哈希表是根据关键码的值而直接进行访问的数据结构。

这么这官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。

哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:

哈希表1

那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。

例如要查询一个名字是否在这所学校里。

要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。

我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。

将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数

#哈希函数

哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。

哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。

哈希表2

如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?

此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。

此时问题又来了,哈希表我们刚刚说过,就是一个数组。

如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。

接下来哈希碰撞登场

#哈希碰撞

如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞

哈希表3

一般哈希碰撞有两种解决方法, 拉链法和线性探测法。

#拉链法

刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了

哈希表4

(数据规模是dataSize, 哈希表的大小为tableSize)

其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。

#线性探测法

使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。

例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:

哈希表5

其实关于哈希碰撞还有非常多的细节,感兴趣的同学可以再好好研究一下,这里我就不再赘述了。

#常见的三种哈希结构

当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。

  • 数组
  • set (集合)
  • map(映射)

这里数组就没啥可说的了,我们来看一下set。

在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:

集合底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
std::set红黑树有序O(log n)O(log n)
std::multiset红黑树有序O(logn)O(logn)
std::unordered_set哈希表无序O(1)O(1)

std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。

映射底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
std::map红黑树key有序key不可重复key不可修改O(logn)O(logn)
std::multimap红黑树key有序key可重复key不可修改O(log n)O(log n)
std::unordered_map哈希表key无序key不可重复key不可修改O(1)O(1)

std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。

当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。

那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。

其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。

虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,std::set、std::multiset 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。

这里在说一下,一些C++的经典书籍上 例如STL源码剖析,说到了hash_set hash_map,这个与unordered_set,unordered_map又有什么关系呢?

实际上功能都是一样一样的, 但是unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好,这就好比一个是官方认证的,hash_set,hash_map 是C++11标准之前民间高手自发造的轮子。

哈希表6

#总结

总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法

但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。

如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!

242. 有效的字母异位词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool isAnagram(string s,string t){
int record[26]={0};
for(int i = 0 ;i<s.size();i++){
// 并不需要记住字符a的ASCII,只要c求出一个相对数值就可以了
record[s[i]-'a']++;
}
for(int i =0 ;i <t.size();i++){
record[t[i]-'a']--;
}
for(int i = 0 ;i <26;i++){
if(record[i]!=0){
return flase;
}
}
return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26]={0};
for(int i=0;i<s.size();i++){
record[s[i]-'a']++;
}
for(int i = 0 ; i<t.size();i++){
record[t[i]-'a']--;
}
for(int i = 0 ; i <26;i++){
if(record[i]!=0)
return false;
}
return true;
}
};

丢人现眼的暴力(原来之前的这种思维题算是哈希法0.0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include<iostream>
using namespace std;
int main(){
string a,b;
cin>>a;
cin>>b;
int A[60000];
int B[60000];
for(int i = 0 ; i < a.size();i++){
A[a[i]]=A[a[i]]+1;
}

for(int i = 0 ; i < b.size();i++){
if(A[b[i]]==A[a[i]]){
A[a[i]]--;
}
}
int flag=0;
for(int i = 0 ; i <a.size();i++){
if(A[a[i]]!=0){
flag=1;
}

}
if (flag==0){
cout<<"niubi";
}
else{
cout<<"goushi";
}

return 0;
}

142. 环形链表 II

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ListNode* fast =head;
ListNode* slow=head;
while(fast!=NULL&&fast->next!=NULL){
slow=slow->next;
fast=fast->next->next;
if(slow==fast){
ListNode* index1=fast;
ListNode* index2=head;
while(index1!=index2){
index1=index1->next;
index2=index2->next;
}
return inedx2;
}
return NULL;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *fast=head;
ListNode *slow=head;
while(fast!=NULL&&fast->next!=NULL){
fast=fast->next->next;//走两部
slow=slow->next;//走一步
if(fast==slow){ //当快==慢时候(为什么不直返回slow呢?因为fast 不只走了一圈)
ListNode* index1=fast; //所以直接找它的地址再的地方
ListNode* index2=head; //从头数fast在哪
while(index1!=index2){
index1=index1->next;
index2=index2->next;
}
return index2;
}

}
return NULL;
}
};

02.07. 链表相交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
ListNode* curA=headA;
ListNode* curB=headB;
int lenA=0,lenB=0;
while(curA!=NULL){
lenA++;//A长度
curA=curA->next;
}
while(curB!=NULL){
lenB++;//B长度
curB=curB->next;
}
curA=headA;
curB=headB;
//curA become max ,lenA is its length
if(lenB>lenA){
swap(lenA,lenB);
swap(curA,curB);
}
//求长度差
int gap= lenA-lenB;
while(gap--){
curA=curA->next;
}
//遍历curA和curB 遇到相同则直接返回
while(curA!=NULL){
if(curA==curB){
return curA;
}
curA=curA->next;
curB=curB->next;
}
return NULL;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA=headA;
ListNode* curB=headB;
int lenA=0,lenB=0;
while(curA!=NULL){
lenA++;
curA=curA->next;
}
while(curB!=NULL){
lenB++;
curB=curB->next;
}
//为什么还要再次赋值,因为上面next到了NULL,懂否,傻子
curA = headA;
curB = headB;
if(lenB>lenA){
swap(lenA,lenB);
swap(curA,curB);
}
int gap=lenA-lenB;
//移动
while(gap){
curA=curA->next;
gap--;
}
while(curA!=NULL){
if(curA==curB){
return curA;
}
curA=curA->next;
curB=curB->next;
}
return NULL;

}
};

19. 删除链表的倒数第 N 个结点

1682089665791

1682089701537

所以快慢指针只是减少了遍历一遍链表长度实质上就是用fast来代替长度罢了………………

还不如自己写一遍傻的暴力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead=new ListNode(0);
dummyHead->next=head;
ListNode* fast=dummyHead;
ListNode* slow=dummyHead;
while(n&&fast->next!=NULL){
fast=fast->next;
n--;
}
fast=fast->next;
while(fast!=NULL){
fast=fast->next;
slow=slow->next;
}
slow->next=slow->next->next;
return dummyHead->next;
}

};

暴力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead=new ListNode(0);
dummyHead->next=head;
int length=0;
while(head!=NULL){
length++;
head=head->next;
}
ListNode* cur=dummyHead;
for(int i = 1 ;i<=length-n;i++){
cur=cur->next;
}
cur->next=cur->next->next;
return dummyHead->next;
}

};

24. 两两交换链表中的节点

1682085503157

1682085672405

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode *dummyHead=new ListNode(0);
dummyHead->next=head;
ListNode *cur=dummyHead;
while(cur->next!=NULL&&cur->next->next!=NULL){
ListNode* temp=cur->next;
ListNode* temp1=cur->next->next->next;
cur->next=cur->next->next;
cur->next->next=temp;
cur->next->next->next=temp1; //顺着思路比较好毕竟已经改变了cur->next的值,直接顺下比较清楚

cur=cur->next->next;
}
return dummyHead->next;
}
};

206. 反转链表

示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

1
2
3
4
5
6
7
8
9
10
11
12
ListNode* reverseList(ListNode* head){
ListNode* temp; //保存cur的下一个结点
ListNode* cur=head;
ListNode* pre =NULL;
while(cur){
tmp=cur->next; //保存cur的下一个结点,因为接下来要改变cur->next
cur->next=pre; //反转
pre=cur; //更新pre
cur=temp; //再更新cur
}
return pre;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre=NULL;
ListNode* cur=head;
ListNode* tmp;
while(cur){
tmp=cur->next;
cur->next=pre;
pre=cur;
cur=tmp;
}
return pre;
}
};

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
ListNode* reverse(ListNode* pre,ListNode* cur){
if(cur == NULL) return pre;
ListNode* temp = cur->next;
cur->next = pre;
// 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
// pre = cur;
// cur = temp;
return reverse(cur,temp);
}
ListNode* reverseList(ListNode* head) {
// 和双指针法初始化是一样的逻辑
// ListNode* cur = head;
// ListNode* pre = NULL;
return reverse(NULL, head);
}

};