【C++】哈希的应用

目录

[1. 位图](#1. 位图)

[1.1 概念](#1.1 概念)

[1.2 模拟实现](#1.2 模拟实现)

[1.3 位图的使用](#1.3 位图的使用)

[2. 布隆过滤器](#2. 布隆过滤器)

[2.1 应用场景](#2.1 应用场景)

[2.2 模拟实现](#2.2 模拟实现)

[2.3 误判率测试](#2.3 误判率测试)

[2.4 哈希切分](#2.4 哈希切分)


1. 位图

1.1 概念

概念:用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常用来判断某个数据存不存在

面试题【腾讯】:给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中

set、排序+二分查找都不行

1G = 10 亿+Byte

40 亿整数,160 亿 Byte,16G


开空间和数据个数无关,和类型范围有关
无符号整数范围在 0-42 亿,开 42亿(2^32) bit,标记对应值在不在

1Byte = 8bit 实际开 2^29Byte = 0.5G

1.2 模拟实现

是否要考虑大小端呢?

不需要,大小端是底层的事情,用户在使用层,不论大小端,1 都是 00 00 00 01


与 &:有 0 为 0,同时为 1 才为 1

或 | :有 1 为 1,同时为 0 才为 0

异或 ^:相同为 0,相异为 1

cpp 复制代码
// x映射的那个标记成1
void set(size_t x)
{
    size_t i = x / 32;
    size_t j = x % 32;

    _a[i] |= (1 << j); // 左移
}
cpp 复制代码
// x映射的那个标记成0
void reset(size_t x)
{
    size_t i = x / 32;
    size_t j = x % 32;

    _a[i] &= (~(1 << j));
}
cpp 复制代码
bool test(size_t x)
{
    size_t i = x / 32;
    size_t j = x % 32;

    return _a[i] & (1 << j);
}

要开空间,要比 40 位数,就开 40/32+1 个,最多浪费 4Byte

BitSet.h

cpp 复制代码
namespace qtw
{
	template<size_t N> // 非类型模板参数
	class bitset
	{
	public:
		bitset()
		{
			_a.resize(N / 32 + 1);
		}

		// x映射的那个标记成1
		void set(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;

			_a[i] |= (1 << j);
		}

		// x映射的那个标记成0
		void reset(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;

			_a[i] &= (~(1 << j));
		}

		bool test(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;

			return _a[i] & (1 << j);
		}
	private:
		vector<int> _a;
	};
}

Test.cpp

cpp 复制代码
int main()
{
	qtw::bitset<1000> bs;
	bs.set(1), bs.set(10), bs.set(100);

	cout << bs.test(1) << endl; // 1
	cout << bs.test(10) << endl; // 1
	cout << bs.test(100) << endl; // 1
	cout << bs.test(999) << endl<<endl; // 0

	bs.set(999), bs.reset(10);

	cout << bs.test(1) << endl; // 1
	cout << bs.test(10) << endl; // 0
	cout << bs.test(100) << endl; // 1
	cout << bs.test(999) << endl << endl; // 1

	//qtw::bitset<-1> bs1;
	//bit::bitset<0xffffffff> bs2;

	return 0;
}

库里面也实现了 bitset:set、reset、test 这三个接口用的多

1.3 位图的使用

  1. 快速查找某个数据是否在一个集合中
  2. 排序 + 去重
  3. 求两个集合的交集、并集等
  4. 操作系统中磁盘块标记

给定100亿个整数,设计算法找到只出现一次的整数

思路 1:一个位图,2 个比特位标识一个值,有 00 01 10 11 四种结果,这里只用前三种就行

分别对应:没有出现、出现 1 次、出现 2 次及以上

思路 2:两个位图

BitSet.h

cpp 复制代码
namespace qtw
{
	template<size_t N> // 非类型模板参数
	class bitset
	{
	public:
		bitset()
		{
			_a.resize(N / 32 + 1);
		}

		// x映射的那个标记成1
		void set(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;

			_a[i] |= (1 << j);
		}

		// x映射的那个标记成0
		void reset(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;

			_a[i] &= (~(1 << j));
		}

		bool test(size_t x)
		{
			size_t i = x / 32;
			size_t j = x % 32;

			return _a[i] & (1 << j);
		}
	private:
		vector<int> _a;
	};

	template<size_t N>
	class twobitset
	{
	public:
		void set(size_t x)
		{
			// 00 -> 01
			if (!_bs1.test(x) && !_bs2.test(x))
			{
				_bs2.set(x);
			} // 01 -> 10
			else if (!_bs1.test(x) && _bs2.test(x))
			{
				_bs1.set(x);
				_bs2.reset(x);
			}
			// 本身10代表出现2次及以上,就不变了
		}

		bool is_once(size_t x)
		{
			return !_bs1.test(x) && _bs2.test(x);
		}
	private:
		bitset<N> _bs1; // 自定义类型自己调用构造函数
		bitset<N> _bs2;
	};
}

Test.cpp

cpp 复制代码
int main()
{
	int a[] = { 1,2,3,3,4,4,4,4,4,2,3,6,3,1,5,5,8,9};
	qtw::twobitset<10> tbs;
	for (auto e : a)
	{
		tbs.set(e);
	}

	for (auto e : a)
	{
		if (tbs.is_once(e))
		{
			cout << e << " "; // 6 8 9
		}
	}
	cout << endl;
}

给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集

错误方法:一个文件所有值映射到一个位图、另一个文件判断在不在

比如,文件1:3 文件2:3 3 3

出来的交集有重复,要去重

正确方法:两个文件分别映射到两个位图,对应位置 &

对应位置都为 1,那么这个值就是交集

Test.cpp

cpp 复制代码
int main()
{
	int a1[] = { 1,2,3,3,4,4,4,4,4,2,3,6,3,1,5,5,8,9 };
	int a2[] = { 8,4,8,4,1,1,1,1 };

	qtw::bitset<10> bs1;
	qtw::bitset<10> bs2;

	// 去重
	for (auto e : a1) { bs1.set(e); }
	for (auto e : a2) { bs2.set(e); }

	for (int i = 0; i < 10; i++)
	{
		if (bs1.test(i) && bs2.test(i))
		{
			cout << i << " "; // 1 4 8
		}
	}
	cout << endl;
}

位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数

类似于问题 1,两个位图解决

00:没有出现 01:出现 1 次 10:出现 2 次 11:出现 3 次及以上

2. 布隆过滤器

上面是针对整形的办法,字符串呢?

数组不可能无限长,"哈哈"公司字符串变整形肯定和其他三个公司不一样,但取模后可能存在冲突导致误判

判断结果,在:可能误判 不在:一定准确

如何解决?

无法解决,哈希是把字符串挂到下面,但这里肯定挂不了,只能降低误判率


如何降低误判率(无法消除误判):布隆过滤器

我们取 3 个字符串哈希算法,每个字符串映射 3 个值

查找时,若 3 个映射值都能找到,则存在;有一个找不到,则不存在

判断结果,在:可能误判 (低概率) 不在:一定准确

2.1 应用场景

不需要精确的场景(允许误判)

eg:快速判断昵称是否注册过

需要精确的场景(不允许误判)

昵称在布隆过滤器没有找到(此结果一定为真),就直接返回

昵称在布隆过滤器找到(此结果可能为假),再去数据库里查找,再返回

降低数据库查询负载压力,提高效率

2.2 模拟实现

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素

一种支持删除的方法:多个位标识一个值,使用引用计数

将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计 数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作

缺陷:1. 无法确认元素是否真正在布隆过滤器中 2. 存在计数回绕

BloomFilter.h

cpp 复制代码
#pragma once
#include<bitset>
#include<string>

struct BKDRHash
{
    size_t operator()(const string& str)
    {
        size_t hash = 0;
        for (auto ch : str)
        {
            hash = hash * 131 + ch;
        }
        return hash;
    }
};

struct APHash
{
    size_t operator()(const string& str)
    {
        size_t hash = 0;
        for (size_t i = 0; i < str.size(); i++)
        {
            size_t ch = str[i];
            if ((i & 1) == 0)
            {
                hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
            }
            else
            {
                hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
            }
        }
        return hash;
    }
};

struct DJBHash
{
    size_t operator()(const string& str)
    {
        size_t hash = 5381;
        for (auto ch : str)
        {
            hash += (hash << 5) + ch;
        }
        return hash;
    }
};

template<size_t N, class K = string,
    class Hash1 = BKDRHash,
    class Hash2 = APHash,
    class Hash3 = DJBHash>
class BloomFilter
{
public:
    void Set(const K& key)
    {
        size_t hash1 = Hash1()(key) % N; // 匿名对象
        _bs.set(hash1);

        size_t hash2 = Hash2()(key) % N;
        _bs.set(hash2);

        size_t hash3 = Hash3()(key) % N;
        _bs.set(hash3);
    }

    bool Test(const K& key)
    {
        size_t hash1 = Hash1()(key) % N;
        if (_bs.test(hash1) == false)
            return false;

        size_t hash2 = Hash2()(key) % N;
        if (_bs.test(hash2) == false)
            return false;

        size_t hash3 = Hash3()(key) % N;
        if (_bs.test(hash3) == false)
            return false;

        return true;
    }

private:
    bitset<N> _bs;
};

Test.cpp

cpp 复制代码
#include<iostream>
using namespace std;
#include "BloomFilter.h"

void TestBloomFilter()
{
	BloomFilter<100> bf;
	bf.Set("孙悟空");
	bf.Set("猪八戒");
	bf.Set("牛魔王");
	bf.Set("二郎神");

	cout << bf.Test("孙悟空") << endl; // 1
	cout << bf.Test("猪八戒") << endl; // 1
	cout << bf.Test("沙悟净") << endl; // 0
}

int main()
{
	TestBloomFilter();
	return 0;
}

2.3 误判率测试

如何选择哈希函数个数和布隆过滤器长度

k:哈希函数个数 m:布隆过滤器长度 n:插入的元素个数 p:误报率

Test.cpp

cpp 复制代码
void TestBloomFilter2()
{
	srand(time(0));
	const size_t N = 100000;
	BloomFilter<N * 8> bf;

	std::vector<std::string> v1;
	//std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
	std::string url = "猪八戒";

	for (size_t i = 0; i < N; ++i)
	{
		v1.push_back(url + std::to_string(i));
	}

	for (auto& str : v1)
	{
		bf.Set(str);
	}

	// v2跟v1是相似字符串集(前缀一样),但是不一样
	std::vector<std::string> v2;
	for (size_t i = 0; i < N; ++i)
	{
		std::string urlstr = url;
		urlstr += std::to_string(9999999 + i);
		v2.push_back(urlstr);
	}

	size_t n2 = 0;
	for (auto& str : v2)
	{
		if (bf.Test(str)) // 误判
		{
			++n2;
		}
	}
	cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;

	// 不相似字符串集
	std::vector<std::string> v3;
	for (size_t i = 0; i < N; ++i)
	{
		string url = "zhihu.com";
		url += std::to_string(i + rand());
		v3.push_back(url);
	}

	size_t n3 = 0;
	for (auto& str : v3)
	{
		if (bf.Test(str))
		{
			++n3;
		}
	}
	cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
}

2.4 哈希切分

给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法

近似算法:把一个文件放到布隆过滤器,另一个文件判断在不在,在就是交集

精确算法:哈希切分

假设平均一个 query 是 30byte,100 亿 query 是 3000 亿 byte,是 300G

均等切分:假设把每个文件切成 1000 份,A 文件的 1000 份小文件,要和 B 文件的 1000 分小文件一一找,麻烦

哈希切分:i = Hash(query) % 1000 i 是多少,query 就进入第 i 个小文件

用同一个哈希算法,A 和 B 中相同的 query(交集) 一定会分别进入 Ai 和 Bi 编号相同的小文件

找交集,Ai 读出来放到一个 set,再依次读取 Bi 的 query,看在不在,在就是交集并且删掉。就可以找出 Ai 和 Bi 的交集

问题:平均切是 300M,但是我们是哈希切分,若冲突太多,会导致某个Ai文件太大,甚至超过1G,怎么办 ?

两个场景,比如 Ai 有 5G

  1. 4G 都是相同 query,1G 是不同的 query

  2. 大多数都是不同的 query

解决方案:

  1. 先把 Ai 的 query 读到一个 set,如果 set 的 insert 报错抛异常(bad alloc),那么就说明是大多数 query 都是不同的。如果能够全部读出来,insert 到 set 里面,那么说明 Ai 有大量相同的 query

  2. 如果抛异常,说明有大量冲突,再换一个哈希函数,再进行二次切分。


给一个超过 100G 大小的 log file,log 中存着 IP 地址,设计算法找到出现次数最多的 IP 地址?

给一个超过 100G 大小的日志文件,日志文件中存着 IP 地址,设计算法找到出现次数最多的 IP 地址?

设计算法找到出现次数最多的 k 个 IP 地址

相同 ip 一定进入了同一个小文件,用 map 去分别统计每个小文件中 ip 出现次数即可


位图的使用那里的第 2 个问题也可以用哈希切分直接取模,分成不同的文件......,太麻烦

本篇的分享就到这里了,感谢观看 ,如果对你有帮助,别忘了点赞+收藏+关注

小编会以自己学习过程中遇到的问题为素材,持续为您推送文章

相关推荐
点云SLAM1 小时前
Tracy Profiler 是目前 C++ 多线程程序实时性能分析工具
开发语言·c++·算法·slam·算法性能分析·win环境性能分析·实时性能分析工具
每天回答3个问题1 小时前
leetcodeHot100|148.排序链表
数据结构·c++·链表·ue4
We་ct1 小时前
LeetCode 17. 电话号码的字母组合:回溯算法入门实战
前端·算法·leetcode·typescript·深度优先·深度优先遍历
吃着火锅x唱着歌2 小时前
LeetCode 447.回旋镖的数量
算法·leetcode·职场和发展
承渊政道2 小时前
C++学习之旅【unordered_map和unordered_set的使⽤以及哈希表的实现】
c语言·c++·学习·哈希算法·散列表·hash-index
我能坚持多久2 小时前
【初阶数据结构08】——深入理解树与堆
数据结构·算法
未来之窗软件服务2 小时前
浏览器开发CEF(二十二)C#闪退处理——东方仙盟元婴期
开发语言·人工智能·c#·浏览器开发·仙盟创梦ide·东方仙盟
Trouvaille ~2 小时前
【贪心算法】专题(一):从局部到全局,数学证明下的最优决策
c++·算法·leetcode·面试·贪心算法·蓝桥杯·竞赛
小钻风33662 小时前
Java 8 流式编程
java·开发语言·windows