左神算法与数据结构——基础提升哈希函数和哈希表

P1哈希函数和哈希表

认识哈希函数和哈希表的实现

哈希函数概念

  1. 输入域无穷,输出域很大但有穷尽 out F(in)
  2. 相同的输入值返回相同的输出值,即哈希函数没有随机值
  3. 由于输入域无限,输出域有穷,则存在哈希碰撞,不同输入产生相同输出,但概率很小
  4. 具有离散性(将不同属性的点离散开)和均匀性
  5. 在这里插入图片描述

题目:有40亿个数的无符号整型数的集合,需要将其放入1G的内存中

在这里插入图片描述

1、B是Byte的缩写,B就是Byte,也就是字节(Byte)
2.b是bit的缩写,b就是bit,也就是比特位(bit)
8bit(比特位)= 1Byte(字节)
1024Byte(字节)= 1K(千字节)
1024K(千字节)= 1M(兆字节)
1024M = 1G
1024G = 1T
一个汉字占两个字节(Byte)
一个字符占一个字节(Byte)

在这里插入图片描述

利用哈希函数解决:先对每个数调用哈希函数,分别bi,再取模成mi,则有0~m-1个小文件,相当于对不同类型的数据进行了分类(近似均分),再对每个小文件利用哈希表求出次数最多的数,最后对不同文件求出次数最多的数

哈希表的实现

例子:利用哈希函数得到out,再取模得到响应值,利用单链表串在该值后面,链的长度均匀变长。但为了使得查找时时间复杂度未O(1),则需要使用到扩容机制。

在这里插入图片描述

扩容机制:当单链表达到k个时,其他链也基本到k个,直接扩容为原先两倍,扩容复杂度最差logN级别(链长为2就扩容)。需要重新计算哈希值哈希函数,每次扩容的代价是O(N),总的代价是O(NlogN),平均加入一个字符串的代价为O(logN),但是k可以取大于2,因此扩容代价可以近似认为是O(1)。理论上增删改查为O(logN),但实际情况接近O(1)

在这里插入图片描述

设计RandomPool结构

在这里插入图片描述

数据结构设计,使用哈希表

建立map1和map2,和size。

insert:在map1中分别加入key和key对应的位置,在map2中分别加入每个位置及其对应的key值

getRandom:随机生成0~25范围内的整形数字,查找该数字对应的key值

delete:为了保证getRandom时候不生成没有key的坐标值,因此不能直接删除而留下空白地址。由于size已知,因此可以将size位置的值赋给被删除元素,再将size–,则不会留下空白

在这里插入图片描述

template<class T>
class RandomPool {
private:
    unordered_map<T, int> keyIndexMap;// 存放key及其对应位置
    unordered_map<int, T> indexKeyMap;// 存放位置及其对应key
    int size;
public:
    RandomPool() {
        //this->keyIndexMap = new unordered_map<T, int>;
        //this->indexKeyMap = new unordered_map<int, T>;
        this->size = 0;
    }
    void insert(T key);
    void deleteKey(T key);
    T getRandom();
};

template<class T>
void RandomPool<T>::insert(T key)
{
    if (this->keyIndexMap.find(key) == this->keyIndexMap.end()) {
        this->keyIndexMap.insert({ key,this->size });
        this->indexKeyMap.insert({ this->size,key });
        this->size++;
    }
}

template<class T>
void RandomPool<T>::deleteKey(T key) {
    if (this->keyIndexMap[key] != this->keyIndexMap.end()) {// 找得到
        int deleteIndex = this->keyIndexMap[key];          // 被删key对应的位置     
        int lastIndex = --size;                            // 最后一位的位置
        T lastKey = this->indexKeyMap[lastIndex];          // 最后一位上key的值
        this->keyIndexMap.insert({ lastKey, deleteIndex });// 用最后一位的值覆盖被删key的位置
        this->indexKeyMap.insert({ deleteIndex, lastKey });// 用最后一位的值覆盖被删key的位置
        this->keyIndexMap.erase(key);                      // 抹去被删key
        this->indexKeyMap.erase(lastIndex);                  // 抹去最后一位key
    }
}

template<class T>
T RandomPool<T>::getRandom() {
    default_random_engine e;
    uniform_int_distribution<unsigned> u(0, this->size);
    return this->indexKeyMap[u(e)];
}

布隆过滤器

类似黑名单结构,例如有100亿个url不能访问,将其加入至一个集合中,用户需要查询urlx在不在这个集合里面。不需要对集合进行删除操纵,只需要查询和增加。

经典结构利用hashset则需要过多内存空间。

在这里插入图片描述

布隆过滤器:占用内存少,允许一定失误率(白可能误报成黑(布隆过滤器可以通过设计将这种失误率变得很低,但不可避免),黑不会被误报成白)(黑表示是黑名单的东西,白表示不是)

在这里插入图片描述

位图:每个位只占1个bit的数组

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 = i / 32;
	int bitIndex = i % 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)));
}


在这里插入图片描述

流程:准备k个哈希函数,从url1开始,分辨调用k个哈希函数,再模m生成k个值,将位数组中的空白描黑。urlx来到时,调用k个哈希函数再模m,将得到的值看是不是已经在位数组中描黑

在这里插入图片描述

因此,m越大白->黑的可能性就越小,根据样本量和m定k

在这里插入图片描述

布隆过滤器公式计算

  1. set类型:n = 样本量, p = 失误率
  2. 失误率
  3. 注:单样本大小无关,保证哈希函数能接收就行

在这里插入图片描述

公式如上

一致性哈希

逻辑服务器对数据分布无要求,但数据服务器如何组织需要讨论。

可以利用哈希函数,再模上数据服务器个数,得到存到哪个数据服务器中,这样可以有效保证了每个数据服务器存储数据量的平均性。为了保证数据服务器的负载均衡,哈希函数的key应该选择高频、中频、低频较为均匀的数据类型

在这里插入图片描述

逻辑端可以自由增加机器,但数据服务器时,经典方法需要改变模值,数据迁移的代价是全量的。

因此需要用到一致性哈希。将哈希的输出值域想象成一个环,设有3台机器,则有对应的三个哈希值。找到最近的哪个大于等于输入信息对应的哈希值的机器(顺时针最近的那台),如果没有则为机器1.

在这里插入图片描述

易知,如果增加机器/减少机器,数据迁移的代价很小。

  • 问题1:机器不能做到将数据均分

  • 问题2:增加/减少机器后,即使原先负载均衡,加入后也不均衡

解决方法:虚拟节点计数,给每个机器分配1000个字符串,让这1000个字符串去抢环。通过设置不同比例的字符串抢环,若增加机器,则从已有机器中抢数据,还能管理负载,即给稍微弱点的机器分配少点的节点

在这里插入图片描述

岛问题

在这里插入图片描述

初级做法:利用递归

void infect(vector<vector<int>>& map, int i, int j) {
    int row = map.size();
    int col = map[0].size();
    if (i < 0 || i >= row || j < 0 || j >= col || map[i][j] != 1) {
        return;
    }
    map[i][j] = 2;
    infect(map, i + 1, j);
    infect(map, i - 1, j);
    infect(map, i, j + 1);
    infect(map, i, j - 1);
}
int countIslands(vector<vector<int>>& map) {
    int row = map.size();
    int col = map[0].size();
    int res = 0;
    for (int i = 0; i < row; i++) {
        for (int j = 0; j < col; j++) {
            if (map[i][j] == 1) {
                res++;
                infect(map, i, j);
            }
        }
    }
    return res;
}

利用并查集

在这里插入图片描述

例如,分配给两个CPU。左侧CPU除了需要进行感染外,需要记录右侧边界的感染源,右侧CPU需要记录左侧边界的感染源。在感染结束后,若查找到两个2不是一个集合,则合并集合并使数量减少一(由于本来属于一个集合,结果当作两个集合在计数了),反之不用。
在这里插入图片描述

多CPU同理

并查集

功能:1. 查两个集合是不是有包含关系,2.将两个集合合并

经典方法:若利用链表,其union很快O(1),但查找需要O(N);若利用哈希表,其查找很快O(1),但合并需要O(N)

实现:利用向上指的图的方式

  1. 初始化时,节点的指针指向自己
  2. 合并时,将节点少的顶部节点连接到节点多的顶部节点
  3. 查找时,不断找到最顶端节点,比较两者最顶端节点,若相同,则包含关系

在这里插入图片描述

优化

若链过长,需要进行扁平化操作。例:找Y的顶端节点,经过X,经过b,到达a,在返回a之前需要将Y、X、a、b的指向改为a

在这里插入图片描述

class Element {// 先将点进行封装
public:
    int val;
    Element(int x) : val(x) {}
};

class UnionFindSet {
private:
    unordered_map<int, Element*> elementmap;
    unordered_map<Element*, Element*> fathermap;
    unordered_map<Element*, int> rankmap;
	
    Element* findHead(Element* ele) {// 找到最顶端节点并进行扁平化优化
        stack<Element*>st;
        while (fathermap.at(ele) != ele) {
            st.push(ele);
            ele = fathermap[ele];
        }
        while (!st.empty()) {
            Element* temp = st.top();
            fathermap.insert({ temp, ele });
            st.pop();
        }
        return ele;
    }
public:
    UnionFindSet(vector<int> nums) {
        for (int i : nums) {
            Element* element = new Element(i);
            this->elementmap.insert({ i, element });
            this->fathermap.insert({ element ,element });
            this->rankmap.insert({ element ,1 });
        }
    }

    bool isSameSet(int a, int b) {
        if (elementmap.find(a) != elementmap.end() && elementmap.find(b) != elementmap.end()) {
            return findHead(elementmap.at(a)) == findHead(elementmap.at(b));
        }
        return false;
    }

    void Union(int a, int b) {
        if (elementmap.find(a) != elementmap.end() && elementmap.find(b) != elementmap.end()) {
            Element* ae = elementmap[a];
            Element* be = elementmap[b];
            Element* ah = findHead(ae);
            Element* bh = findHead(be);
            if (ah != bh) {
                Element* big = rankmap[ah] >= rankmap[bh] ? ah : bh;
                Element* small = rankmap[ah] >= rankmap[bh] ? bh : ah;
                fathermap.insert({ small, big });
                rankmap.insert({big, rankmap[big] + rankmap[small]});
                rankmap.erase(small);
            }
        }
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值