1.哈希函数
(1)输入范围无限;输出范围有限(MD5:0-pow(2,64)-1)
(2)相同的输入有相同的输出(不随机)
(3)不同的输入可能有相同的输出(哈希碰撞)
(4)离散性:将不同的输入(可能很相近)离散到输出域的不同位置上;均匀性:输出均匀分布在输出域上的。离散性/均匀性越好,就认为哈希函数越好
输入(不均匀)-哈希f->输出(均匀)-模m->输出(均匀):
问题:
传统利用哈希表:
利用哈希表的问题:哈希表存储的数据量之和不同的数的个数有关,对于相同的数将存储在一个pair中,如果不同的数很多,占用的内存就会很大
利用哈希函数解决:先对每个数调用哈希函数,然后再取模,相当于对不同的数进行了分类(且近似是均匀分类),对每个小文件利用哈希表分别求出出现次数最多的数,最后不同的文件求出出现次数最多的数
2.哈希表
符串调用哈希函数,然后再取模,将模相同的的字符串存放在同一个单向链表中。当某一链表(各链表长度近似相等,因为哈希函数的均匀性)的长度超过某一值触发扩容机制,每次容量翻倍,在重新计算哈希值,存储在新的容量中
扩容的代价:加入N个字符串最多经过logN次扩容(假设链表长度为k=2触发扩容),每次扩容的代价是O(N),总的代价是O(NlogN),平均加入一个字符串的代价为O(logN),但是k可以取大于2,因此扩容代价可以近似认为是O(1)。
对于一些虚拟机语言(C++不是),比如Java可以使用离线扩容技术进一步缩减使用代价
3.设计RandomPool结构
简单使用哈希表:map1(str–>index)、map2(index–>str)、size;在没有随机删除行为时,随机生成一个0-size-1的数作为index,随机返回;对于有删除行为时,需要保证0-size-1中中间时连续的:每次删除一个数时,用最有一个数取填补之前删掉的位置,size–。
代码实现:
class RandomPool {
private:
unordered_map<string, int>keyIndex;
unordered_map<int, string>indexKey;
int size;
public:
RandomPool() {
this->size = 0;
}
void insert(string s) {
if (this->keyIndex.find(s) == this->keyIndex.end()) {
this->keyIndex.insert({ s,this->size });
this->indexKey.insert({ this->size,s });
this->size++;
}
}
void deleteKey(string s) {
if (this->keyIndex.find(s) != this->keyIndex.end()) {
int index = this->keyIndex[s];//删除的下标
this->keyIndex[indexKey[size - 1]] = index;//将最后一项的pair移到index(覆盖)
this->indexKey[index] = indexKey[size - 1];
this->indexKey.erase(size - 1);//删除重复
this->keyIndex.erase(s);
this->size--;
}
}
string getRandStr() {
if (this->size == 0) {
return;
}
srand((unsigned)time(NULL));
return this->indexKey[rand() % this->size];
}
};
4.布隆过滤器
解决类似黑名单问题:100亿个url不能访问,将其加入到一个集合中,每次用户访问要先查给出的url是否在这个集合中,对这个集合不需要删除操作。只有两个操作:将url加入集合;查询url是否在集合中。
如果使用hashset,则占用内存空间非常大。
使用布隆过滤器:允许一定程度的失误率(白可能误报成黑(布隆过滤器可以通过设计将这种失误率变得很低,但不可避免),黑不会被误报成白)
位组bit arr:每个元素占用一个bit的空间,拿基础类型的数(int32位)拼成位组。
代码实现:
void birArr() {//位组
int a = 0;//a 32bit
vector<int>arr(10);//32bit*10-->320bit
//arr[0] int 0-31
//arr[1] int 32-63
//arr[2] int 64-95
int i = 178;//想取得第178个bit的状态
int numIndex = 178 / 32;
int bitIndex = 178 % 32;
//获取状态0 / 1
int bit = ((arr[i / 32] >> (i % 32)) & 1);
int s = ((arr[numIndex] >> bitIndex) & 1);
//修改状态为1
arr[numIndex] = (arr[numIndex] | (1 << bitIndex));
//修改状态为0
arr[numIndex] = (arr[numIndex] & (~(1 << bitIndex)));
}
布隆过滤器就是个大位组:url调用k个哈希函数,得到的k个哈希值然后取模,将这k个位置的位组取1。每次需要查询时求给出的url的k个哈希值,找到对应位置,如果全为1,则认为在黑名单,否则认为不在。
有两个参量需要确定:位组的长度m和哈希函数个数k取几个。样本量n和可以允许的误报率–>确定m–>确定k
设计布隆过滤器的特征就是是否类似黑名单,然后问一句面试官是否允许失误率,然后问失误率多少。设计布隆过滤器的三个公式(只和样本量和失误率有关,与样本的大小无关):
5.一致性哈希
讨论数据服务器怎么组织的问题(分布式)。逻辑服务器对各个数据没区别。
经典数据服务器时求哈希值再取模使之分配到不同的数据服务器上考虑负载均衡的问题:选择hashkey要选择高中低频都有数量的key作划分。如果想要扩充数据服务器的数量,经典算法的需要重新计算哈希值,重新取模,代价是全量的—>一致性哈希
将哈希值的域想象成环,算机器的哈希值,在环中找出相应的位置;当来了数据之后,算数据的哈希值,在环上顺时针找最近机器的哈希值
如果增加机器/减少机器,数据迁移的代价很小。存在的问题(负载均衡):机器的哈希值可能在环上分布不均衡;即是均衡新加入机器之后也可能导致不均衡。利用虚拟节点技术解决:给每个机器分配1000个字符串,让这1000个字符串去抢环。通过设置不同比例的字符串抢环,还能管理负载
6.岛问题
遍历每个元素,元素为1调用依次感染(infect)过程,使与之相连的元素全部置为2,岛屿数+1。时间复杂度:O(M*N)
代码实现:
void infect(vector<vector<int>>& arr, int i, int j, int m, int n) {
if (i < 0 || i >= m || j < 0 || j >= n || arr[i][j] != 1) {
return;
}
arr[i][j] = 2;
infect(arr, i + 1, j, m, n);
infect(arr, i - 1, j, m, n);
infect(arr, i, j + 1, m, n);
infect(arr, i, j - 1, m, n);
}
int countIslands(vector<vector<int>>& arr) {
if (arr.size() == 0 || arr[0].size() == 0) {
return 0;
}
int res = 0;
for (int i = 0; i < arr.size(); i++) {
for (int j = 0; j < arr[0].size(); j++) {
if (arr[i][j] == 1) {
res++;
infect(arr, i, j, arr.size(), arr[0].size());
}
}
}
return res;
}
进阶:面试可能遇到并行算法的题目,只需要把过程讲清楚。给的数组非常大,如何将数组分成小的数组分别计算岛屿的数量,然后再合并–>并查集
7.并查集:
查询两个集合是否使同一个集合;合并两个集合。时间复杂度:O(1)。首先将所有元素单独成为一个集合,用一个向上的指针,用最上面的元素代表整个集合。如果findHead的调用达到O(N)以上的水平,单次findHead的代价使O(1)
代码实现:
class Element {
public:
int val;
Element(int val) {
this->val = val;
}
};
class UnionFindSet {
private:
unordered_map<int, Element*>elementMap;//key:样本 value:样本对应的元素
unordered_map<Element*, Element*>fatherMap;//key:元素 value:元素的父元素
unordered_map<Element*, int>sizeMap;key:集合的代表元素 value:该集合的大小
Element* findHead(Element* E) {//个人认为比左神给的代码好
if (fatherMap[E] != E) {
fatherMap[E] = findHead(fatherMap[E]);
}
return fatherMap[E];
}
public:
UnionFindSet(vector<int>& arr) {
for (int ele : arr) {
Element* Ele = new Element(ele);
elementMap.insert({ ele,Ele });
//fatherMap[Ele] = Ele;
//fatherMap.insert(make_pair(Ele, Ele));
fatherMap.insert({Ele, Ele});
sizeMap.insert({ Ele, 1 });
}
}
bool isSameSet(int a, int b) {
if (elementMap.find(a) != elementMap.end() && elementMap.find(b) != elementMap.end()) {
return findHead(elementMap[a]) == findHead(elementMap[b]);
}
return false;
}
void unionSet(int a, int b) {
if (elementMap.find(a) != elementMap.end() && elementMap.find(b) != elementMap.end()) {
Element* aF = findHead(elementMap[a]);
Element* bF = findHead(elementMap[b]);
if (aF != bF) {
if (sizeMap[aF] > sizeMap[bF]) {
fatherMap[bF] = aF;
sizeMap[aF] += sizeMap[bF];
sizeMap.erase(bF);
}
else {
fatherMap[aF] = bF;
sizeMap[bF] += sizeMap[aF];
sizeMap.erase(aF);
}
}
}
}
};