赞
踩
哈希表的长度最好是素数表中的数,哈希表扩容的时候也是从哈希表中取得,扩容后需要把原来表中的数据重新哈希,存入新的哈希表
装载因子:当表的使用量 超过 总容量 * 装载因子 时,需要进行扩容,使用哈希表的 均摊时间复杂度是O(1)
enum State { UNUSE, // 从未被使用 USING, // 正在使用 DELETE // 使用后被删除 }; struct Node { Node(int val = 0, State state = UNUSE) :val_(val) , state_(state) {} int val_; State state_; // 节点的状态 }; class HashTable { public: HashTable(int cap = primes_[0], double load_factor = 0.75) : load_factor_(load_factor) , prime_idx(0) , size_(0) { // 如果用户传入的cap不符合素数表,需要重新指定cap_ if (cap != primes_[0]) { for (; prime_idx < PRIMES_SIZE; prime_idx++) { if (primes_[prime_idx] >= cap) { break; } } // 用户传入的cap过大,不合法,重新指定cap_ if (prime_idx == PRIMES_SIZE) { prime_idx--; } } cap_ = primes_[prime_idx]; table_ = new Node[cap_]; } ~HashTable() { delete[] table_; table_ = nullptr; } public: // 可以插入重复的val bool insert(int val) { cout << "哈希表使用因子:" << size_ * 1.0 / cap_ << endl; if (size_ >= load_factor_ * cap_) { cout << "当前使用:" << size_ << " 哈希表总容量:" << cap_ << " 需要扩容" << endl; expand(); cout << "扩容完成,当前容量:"<<cap_ << endl; } int start = val % cap_; int idx = start; do { if (table_[idx].state_ != UNUSE) { table_[idx].state_ = USING; table_[idx].val_ = val; size_++; return true; } idx = (idx + 1) % cap_; } while (idx != start); return false; } // 删除所有val bool erase(int val) { int start = val % cap_; int idx = start; do { // 在用 && 找到 if (table_[idx].state_ == USING && table_[idx].val_ == val) { table_[idx].state_ = DELETE; size_--; } idx = (idx + 1) % cap_; } while (table_[idx].state_ != UNUSE && idx != start); // 若碰到了UNUSE,则说明后面肯定没有自己要找的val了 return true; } // 找到val返回对应的key,找不到返回-1 int find(int val) { int start = val % cap_; int idx = start; do { // 在用 && 找到 if (table_[idx].state_ == USING && table_[idx].val_ == val) { return idx; } idx = (idx + 1) % cap_; } while (table_[idx].state_ != UNUSE && idx != start); // 若碰到了UNUSE,则说明后面肯定没有自己要找的val了 return -1; } private: void expand() { prime_idx++; if (prime_idx == PRIMES_SIZE) { throw "current hashtable is too large, can't expand anymore!"; } Node* tmp = new Node[primes_[prime_idx]]; for (int i = 0; i < cap_; i++) { if (table_[i].state_ == USING) { // 取出旧表中的元素,在新表中重新哈希 int start = table_[i].val_ % primes_[prime_idx]; int idx = start; do { if (tmp[idx].state_ != USING) { tmp[idx].state_ = USING; tmp[idx].val_ = table_[i].val_; break; // 新表中插入一个元素后,就可以跳出查找位置的while循环 } idx = (idx + 1) % primes_[prime_idx]; } while (idx != start); } } cap_ = primes_[prime_idx]; delete[] table_; table_ = tmp; } private: Node* table_; int cap_; // 哈希表的容量 int size_; // 已经使用的空间 double load_factor_; // 装载因子 static const int PRIMES_SIZE = 10; static const int primes_[PRIMES_SIZE]; int prime_idx; // 当前哈希表使用的素数 }; const int HashTable::primes_[PRIMES_SIZE] = { 3,7,23,47,251,443,911,1471,42773 }; int main() { HashTable h_table; h_table.insert(21); h_table.insert(32); h_table.insert(14); h_table.insert(15); h_table.insert(22); cout << h_table.find(21) << endl; h_table.erase(21); cout << h_table.find(21) << endl; return 0; }
缺点:
若使用线性探测哈希表,要锁住整个数组。使用链式哈希表,则可以只使用分段的锁(锁住一个桶即可),既保证了线程安全,又有一定的并发量,提高了效率
优化空间:
class HashTable { public: // size用于初始化vector的大小 HashTable(int size = primes_[0], double load_factor=0.75) : load_factor_(load_factor) , prime_idx(0) , use_num(0) { if (size != primes_[0]) { for (; prime_idx < PRIMES_SIZE; prime_idx++) { if (primes_[prime_idx] >= size) { break; } } if (prime_idx == PRIMES_SIZE) { prime_idx--; } } // 开辟空间,并且创建元素:table_.size() == primes_[prime_idx] && table_.capacity == primes_[prime_idx] table_.resize(primes_[prime_idx]); } // 实现为不可重复插入 bool insert(int val) { bool inserted = false; double factor = use_num * 1.0 / table_.size(); cout << "当前装载因子:" << factor << endl; if (factor >= load_factor_) { cout <<" 需要扩容" << endl; expand(); cout << "扩容完成,当前容量:"<<table_.size() << endl; } int idx = val % table_.size(); if (table_[idx].empty()) { use_num++; table_[idx].emplace_front(val); // 链表头插 inserted = true; } else { auto iter = ::find(table_[idx].begin(), table_[idx].end(), val); if (iter == table_[idx].end()) { // 当前插入的值不存在,则插入链表 table_[idx].emplace_front(val); // 链表头插 inserted = true; } } return inserted; } // 删除元素 bool erase(int val) { int idx = val % table_.size(); // 如果链表节点过长,说明散列函数有问题,如果散列结果很离散,链表不会很长 auto iter = ::find(table_[idx].begin(), table_[idx].end(), val); if (iter != table_[idx].end()) { table_[idx].erase(iter); if (table_[idx].empty()) { use_num--; // 删完后当前链表为空,则调整vector已经使用的个数 } return true; } return false; } bool find(int val) { int idx = val % table_.size(); auto iter = ::find(table_[idx].begin(), table_[idx].end(), val); return iter != table_[idx].end(); } private: void expand() { if (prime_idx + 1 == PRIMES_SIZE) { throw "hash table can't expand anymore!"; } vector<list<int>> tmp; // 资源交换过来后,局部变量出作用域自动析构 table_.swap(tmp); // 若使用相同的容器适配器,只是交换成员变量 table_.resize(primes_[++prime_idx]); // 遍历旧表中的元素,然后重新哈希 for (list<int> l : tmp) { for (int val : l) { int idx = val % table_.size(); if (table_[idx].empty()) { use_num++; } table_[idx].emplace_front(val); // 链表头插 } } } private: vector<list<int>> table_; double load_factor_; // 装载因子 int use_num; static const int PRIMES_SIZE = 10; static const int primes_[PRIMES_SIZE]; int prime_idx; // 当前哈希表使用的素数 }; const int HashTable::primes_[PRIMES_SIZE] = { 3,7,23,47,251,443,911,1471,42773 };
把一个文件的ip放在一个哈希表,然后遍历另一个文件的同时在哈希表中查找,找到则输出
1亿 约等于 100M
100M * 4B = 400MB数据 ==> 放入链式哈希表后,还需要增加400MB的地址域,一共800MB
将两个大文件分成足够多的小文件,使得哈希过后将数字存入每个小文件的数据不至于超过100MB,这样相同的数必然会被哈希到下标相同的小文件中。
把某个a小文件全部读入内存中的哈希表,然后从下标相同的b文件中挨个读取数据,然后在哈希表中查找,找到则在两个文件中重复出现
现在有1亿个数据,最大值为1亿,每个数字4B,限制内存100MB
若使用哈希表,需要使用100M * 4B * 2 = 800MB
使用位图,则只需要使用100000000bit,(100000000/8+1)B = 12500000B = 12.5MB
位操作: |
是赋值的,&
是求值的
template<typename T> void show_repeat(const vector<int>& vec, T bit_map_type = char()) { int max_val = *max_element(vec.begin(), vec.end()); char* bitmap = new char[max_val / (sizeof(char) * 8) + 1](); unique_ptr<char> ptr(bitmap); for (int v : vec) { int index = v / (sizeof(T) * 8); int offset = v % (sizeof(T) * 8); // &用于取出某个bit,|用于对某个bit赋值 if ((bitmap[index] & (1 << offset)) == 0) { bitmap[index] |= (1 << offset); } else { cout << v << "重复" << endl; } } } int main() { vector<int> vec = {42,3,45,423,15,1,23,1,3412,45,12,45,2,65,412,5,4}; show_repeat(vec, int()); return 0; }
位图适用场景: 数据数量和数据的最大值相当的时候适用,不适用于数据很少,但是最大值很大的情况,因为要按照最大值开辟内存空间
增加元素: 把key用k个哈希函数计算后,得到一组下标,将Bloom Filter中对应的bit置为1
搜索元素: 把key用k个哈希函数计算后,得到一组下标,若Bloom Filter中对应的bit全为1,那这个key有可能存在,若对应的bit不全为1,则key肯定不在。Bloom Filter存在一定的误判率,但随着其长度和哈希函数的增加,误判率会大大降低
删除元素: Bloom Filter不提供删除操作,若key1和key2有公用的bit,删除key1对应的bit后,那也改变了key2对应的bit,再查找key2的时候,也就查不到key2了
一些应用场景
优点: 结合了哈希表的效率高和位图的省内存的优点,但是会存在一定的误判率,但随着其长度和哈希函数的增加,误判率会大大降低
#include<iostream> #include<vector> #include<string> #include"stringhash.h" using namespace std; class BloomFilter { public: BloomFilter(int bit_cap = 1471) : bit_cap_(bit_cap) , type_size_(sizeof(char)) { bit_map_.resize(bit_cap_ / (type_size_ * 8) + 1); } // 往Bloom Filter中插入key void set_bit(const char* key) { int idx1 = BKDRHash(key) % bit_cap_; int idx2 = RSHash(key) % bit_cap_; int idx3 = APHash(key) % bit_cap_; int index = 0; int offset = 0; index = idx1 / (type_size_ * 8); offset = idx1 % (type_size_ * 8); bit_map_[index] |= (1 << offset); index = idx2 / (type_size_ * 8); offset = idx2 % (type_size_ * 8); bit_map_[index] |= (1 << offset); index = idx3 / (type_size_ * 8); offset = idx3 % (type_size_ * 8); bit_map_[index] |= (1 << offset); } // 查询key是否存在于Bloom Filter bool get_bit(const char* key) { int idx1 = BKDRHash(key) % bit_cap_; int idx2 = RSHash(key) % bit_cap_; int idx3 = APHash(key) % bit_cap_; int index = 0; int offset = 0; index = idx1 / (type_size_ * 8); offset = idx1 % (type_size_ * 8); if ((bit_map_[index] & (1 << offset)) == 0) { return false; } index = idx2 / (type_size_ * 8); offset = idx2 % (type_size_ * 8); if ((bit_map_[index] & (1 << offset)) == 0) { return false; } index = idx3 / (type_size_ * 8); offset = idx3 % (type_size_ * 8); if ((bit_map_[index] & (1 << offset)) == 0) { return false; } return true; } private: int bit_cap_; int type_size_; vector<char> bit_map_; }; class BlackList { public: void add(string url) { bloom_filter_.set_bit(url.c_str()); } bool query(string url) { return bloom_filter_.get_bit(url.c_str()); } private: BloomFilter bloom_filter_; }; int main() { BlackList b_list; b_list.add("www.baidu.com"); b_list.add("www.alibaba.com"); b_list.add("www.tencent.com"); cout << b_list.query("www.baidu.com") << endl; cout << b_list.query("www.alibaba.com") << endl; cout << b_list.query("www.tencent.com") << endl; cout << b_list.query("www.bytedance.com") << endl; return 0; }
这里总结一下Bloom Filter的注意事项:
Bloom Filter增加元素的过程: 把元素的值通过k个哈希函数进行计算,得到k个值,然后把k当作位数组的下标,在位数组中把相应k个值修改成1。
Bloom Filter查询元素的过程: 把元素的值通过k个哈希函数进行计算,得到k个值,然后把k当作位数组的下标,看看相应位数组下标标识的值是否全部是1,如果有一个为0,表示元素不存在(判断不存在绝对正确);如果都为1,表示元素存在(判断存在有错误率)。
所以用Bloom Filter需要少量的内存就可以判断元素是否存在集合当中,当需要查找同时在a、b两个大文件中都出现的元素时,我们可以用a文件的数据构建Bloom Filter的位数组中的状态值,然后再读取b文件的数据进行布隆过滤的查找操作就可以了
参考:https://blog.csdn.net/QIANGWEIYUAN/article/details/88815772
求最小的K个元素,用大根堆:堆中加入小的,淘汰大的
求最大的K个元素,用小根堆:堆中加入大的,淘汰小的
时间复杂度: O(logK)*O(n) = O(n)
手写堆,实现求最大的k个数
求出最大的K个元素,使用小根堆
把数组前K个元素建立一个小根堆,从前K个元素第一个内部节点(K-1)/2开始建堆
建堆完成后,开始遍历数组的其他元素
若当前元素大于堆顶元素,则淘汰堆顶元素,将当前元素放在堆顶,然后对堆顶元素调堆
若当前元素小于堆顶元素,则直接遍历下一个元素
// 在[0,size-1]的范围内调堆,使下标为cur的元素满足堆性质 void sift_down(int arr[], int cur, int size) { int val = arr[cur]; int n = size - 1; while (cur <= (size - 2) / 2) { int min_child = 2 * cur + 1; // 若满足while循环条件,则必然存在左孩子 if (2 * cur + 2 <= n) { min_child = arr[min_child] < arr[2 * cur + 2] ? min_child : 2 * cur + 2; } if (arr[min_child] < val) { arr[cur] = arr[min_child]; cur = min_child; } else { break; } } arr[cur] = val; } int main() { int arr[10] = { 11,54,62,45,27,98,7,625,37,59 }; int k = 5; for (int i = (k - 1 - 1) / 2; i >= 0; i--) { sift_down(arr, i, k); // 下标为 0 ~ k-1 的前k个元素建小顶堆 } int n = sizeof(arr) / sizeof(arr[0]); for (int i = k; i < n; i++) { if (arr[i] > arr[0]) { arr[0] = arr[i]; // 大于堆顶元素,则放在堆顶,然后调堆 sift_down(arr, 0, k); } else { continue; // 比小顶堆的堆顶元素还小,一定不是前k大的数,跳过 } } for (int i = 0; i < k; i++) { cout << arr[i] << " "; // 数组前k个元素就是最大的k个 } return 0; }
使用内置的优先级队列和哈希表实现统计出现次数最少的k个数字
int main() { int k = 5; vector<int> vec; srand(time(nullptr)); for (int i = 0; i < 1000; i++) { vec.push_back(rand() % 100); } // 统计元素出现的个数 unordered_map<int, int> mp; for (int key : vec) { mp[key]++; } using Type = pair<int, int>; using Comp = function<bool(Type&, Type&)>; priority_queue<Type, vector<Type>, Comp> max_heap( [](Type& a, Type& b)->bool { return a.second < b.second; // 大根堆 } ); auto iter = mp.begin(); for (int i = 0; i < k; i++, iter++) { max_heap.push(*iter); } for (; iter != mp.end(); iter++) { if (iter->second < max_heap.top().second) { max_heap.pop(); max_heap.push(*iter); } } while (!max_heap.empty()) { cout <<"value:"<< max_heap.top().first << " times:" << max_heap.top().second << endl; max_heap.pop(); } return 0; }
int partition(int arr[], int begin, int end, int k) { int l = begin; int r = end; int val = arr[l]; while (l < r) { while (l < r && arr[r] > val) r--; if (l < r) { arr[l] = arr[r]; l++; } while (l < r && arr[l] < val) l++; if (l < r) { arr[r] = arr[l]; r--; } } arr[l] = val; return l; } // 递归函数作用:能把[begin, end]区间内,前topk的元素放在[0, k] void select_topk(int arr[], int begin, int end, int k) { int pos = partition(arr, begin, end, k); if (pos == k - 1) { return; } else if (pos < k - 1) { select_topk(arr, pos + 1, end, k); } else { select_topk(arr, begin, pos - 1, k); } }
最好时间复杂度: 每次基准数都在中间,二叉树很平衡,n+n/2+…+1 = O(n)
最坏时间复杂度: 数据本就有序,基准数每次都在最边上,二叉树退化为链表,O(n^2)
经过哈希算法hash(ip:port)%N
,同一客户的请求都会被映射到相同的服务器上,这有效解决了会话共享 的问题,这时候就不需要加入redis缓存服务器去记录客户的登录状态,避免引用外部模块过多导致系统不稳定的问题。
但这种方法也有问题,比如当某个服务器宕机了(或增加服务器),这时候N
的值就发生改变,导致相同客户的请求就不一定还会转发到以前那台服务器上,这会发生身份认证的问题。
我们想要的是 同一 ip:port 的请求,永远被映射到同一台server上处理
客户通常发请求查询信息的时候,是现在缓存服务器上查找,查找不到再到DB查找,然后把数据从DB转移到缓存服务器。
若此时有一台服务器宕机了,同理在2号缓存服务器中的信息再次被查询时,这个请求很有可能会被转发到1号服务器,此时无法在1号缓存服务器中找到信息,就会去DB中查找,此时大量的磁盘IO会严重影响server的响应速度。更严重来讲,一台服务器的负载太高,可能会影响整个系统的运转,甚至崩溃
若此时增加了一台服务器,N
发生改变。举个极端的例子,此时所有的请求都被转发到新的服务器中,新的服务器无法查询到相应信息,同理会产生大量的磁盘IO,甚至服务器崩溃
我们的理想情况是:
为解决普通哈希算法导致的以上问题,提出一致性哈希算法(分布式系统负载均衡的首选算法)
一个良好的分布式哈希方案应该具有良好的单调性,即服务节点数量的改变不会造成大量哈希的重新定位
算法过程
容错性分析
为了达到良好的负载均衡,经过一致性哈希算法后的 server最好在哈希环上分散开,所以一般我们要在哈希环上加入物理节点的虚拟节点。加入物理节点的虚拟节点可防止由于物理节点过少,导致哈希处理后几台服务器挤在一起,从而导致某一台主机的负载过多,而其他空闲的情况
类设计如下:
#include<iostream> #include<set> #include<list> #include<map> #include<string> #include"md5.h" using namespace std; using uint = unsigned int; class PhysicalHost; class VirtualHost { public: VirtualHost(string ip, PhysicalHost* phy_host_ptr) : ip_(ip) , phy_host_ptr_(phy_host_ptr) { md5_ = getMD5(ip_.c_str()); } // 虚拟节点存放到set的时候,需要排序,默认less,需要提供operator< bool operator<(const VirtualHost& vir_host) const { // 根据md5值排序 return md5_ < vir_host.md5_; } // 删除哈希环上的虚拟节点时,需要查找,重载operator== bool operator==(const VirtualHost& vir_host) const { return ip_ == vir_host.ip_; } const uint get_md5() const { return md5_; } const PhysicalHost* get_phy_host() const { return phy_host_ptr_; } private: string ip_; // 虚拟节点记录的ip信息 uint md5_; // 根据物理节点的ip计算的ip值得到的MD5,这是32位加密串运算得到的uint PhysicalHost* phy_host_ptr_; // 指向实际的物理节点 }; class PhysicalHost { public: // 物理节点的ip,创建虚拟节点的个数 PhysicalHost(string ip, int v_number) : ip_(ip) { for (int i = 0; i < v_number; i++) { // 虚拟节点需要记录物理主机的ip以及对应的物理节点 virtual_hosts_.push_back(VirtualHost(ip_ + "#" + ::to_string(i), this)); } } const string get_ip() const{ return ip_; } const list<VirtualHost>& get_virtual_hosts() const { return virtual_hosts_; } private: string ip_; list<VirtualHost> virtual_hosts_; // 双向循环链表 }; class ConsistentHash { public: // 添加物理主机的虚拟节点到一致性哈希环 void add_host(PhysicalHost& phy_host) { auto vir_list = phy_host.get_virtual_hosts(); for (auto vir_host : vir_list) { hash_circle_.insert(vir_host); } } // 删除哈希环物理节点所有的虚拟节点 void del_host(PhysicalHost& phy_host) { auto vir_list = phy_host.get_virtual_hosts(); for (auto vir_host : vir_list) { // 红黑树查找,O(log2n) auto iter = hash_circle_.find(vir_host); if (iter != hash_circle_.end()) { hash_circle_.erase(iter); } } } // 根据客户的ip,计算其对应的虚拟主机,然后根据虚拟主机返回真是的物理主机的ip string get_client_host(string client_ip) const{ uint client_md5 = getMD5(client_ip.c_str()); // 找第一个比客户ip的md5大的虚拟主机 for (VirtualHost vir_host : hash_circle_) { if (vir_host.get_md5() > client_md5) { return vir_host.get_phy_host()->get_ip(); } } // 客户的ip得到的md5过大,无法找到更大的md5,那直接分配第一个虚拟节点 return hash_circle_.begin()->get_phy_host()->get_ip(); } private: // 由于需要顺时针查找,需要排序,所以一致性哈希算法底层用到红黑树 set<VirtualHost> hash_circle_; }; void show_consistent_hash(const ConsistentHash& hash_circle) { list<string> client_ip_list{ "192.168.1.100", "192.168.1.101", "192.168.1.102", "192.168.1.103", "192.168.1.104", "192.168.1.105", "192.168.1.106", "192.168.1.107", "192.168.1.108", "192.168.1.109", "192.168.1.110", "192.168.1.111", "192.168.1.112", "192.168.1.113", }; // 物理服务器ip 所服务的客户端ip map<string, list<string>> ip_map; for (string client_ip : client_ip_list) { // 根据客户端的ip,计算哈希环上的虚拟主机,从而拿到对应物理主机的ip string phy_host_ip = hash_circle.get_client_host(client_ip); ip_map[phy_host_ip].push_back(client_ip); } for (auto pair : ip_map) { cout << "server ip :" << pair.first << endl; cout << "该服务器服务的客户端有" << pair.second.size() << "个" << endl; cout << "client ip :" << endl; for (string client_ip : pair.second) { cout << client_ip << endl; } cout << "-----------------------------" << endl; } } int main() { PhysicalHost phy_host1("10.117.121.66", 200); PhysicalHost phy_host2("10.117.121.67", 200); PhysicalHost phy_host3("10.117.121.68", 200); ConsistentHash hash_circle; hash_circle.add_host(phy_host1); hash_circle.add_host(phy_host2); hash_circle.add_host(phy_host3); show_consistent_hash(hash_circle); hash_circle.del_host(phy_host2); cout << "*********主机2宕机*********" << endl; show_consistent_hash(hash_circle); return 0; }
server ip :10.117.121.66 该服务器服务的客户端有3个 client ip : 192.168.1.105 192.168.1.109 192.168.1.112 ----------------------------- server ip :10.117.121.67 该服务器服务的客户端有5个 client ip : 192.168.1.100 192.168.1.102 192.168.1.103 192.168.1.107 192.168.1.110 ----------------------------- server ip :10.117.121.68 该服务器服务的客户端有6个 client ip : 192.168.1.101 192.168.1.104 192.168.1.106 192.168.1.108 192.168.1.111 192.168.1.113 ----------------------------- *********主机2宕机********* server ip :10.117.121.66 该服务器服务的客户端有4个 client ip : 192.168.1.105 192.168.1.109 192.168.1.110 192.168.1.112 ----------------------------- server ip :10.117.121.68 该服务器服务的客户端有10个 client ip : 192.168.1.100 192.168.1.101 192.168.1.102 192.168.1.103 192.168.1.104 192.168.1.106 192.168.1.107 192.168.1.108 192.168.1.111 192.168.1.113 -----------------------------
该代码需要md5源码才可运行,需要可以私信
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。