C++进阶—哈希/unordered系列关联式容器/底层结构(一篇文章学习哈希)

news2024/7/6 17:39:30

 

目录

0. 前言map/set和unordered_map/unordered_set

1. unordered系列关联式容器

1.1 unordered_map

1.1.2 unordered_map的接口说明

1. unordered_map的构造

2. unordered_map的容量

3. unordered_map的迭代器

4. unordered_map的元素访问

5. unordered_map的查询

6. unordered_map的修改操作

 7. unordered_map的桶操作

8.其他成员函数

1.2 unordered_set

1.3 在线OJ

2. 底层结构

2.1 哈希概念

2.2 哈希冲突

2.3 哈希函数

2.4 哈希冲突解决

2.4.1 闭散列

1. 线性探测

2. 线性探测的实现

3. 二次探测

2.4.2 开散列

1. 开散列概念

2. 开散列实现

3. 开散列增容

4. 开散列的思考

5. 开散列与闭散列比较

3.封装模拟实现unordered_map/unordered_set/迭代器

3.1 哈希表的改造

1. 模板参数列表的改造

2. 增加迭代器操作

3. 增加通过key获取value操作、及相同值判断仿函数

3.2 unordered_map封装实现

3.3 unordered_set封装实现

3.4 Test代码


0. 前言map/set和unordered_map/unordered_set

字典类型又被称为关联数组(associative array),关联数组和正常数组的使用方法是相似的,但其不同之处在于字典结构的下标不必是整数,而可以是任意类型。

        map和unordered_map这两种字典结构都是通过键值对(key-value)存储数据的,键(key)和值(value)的数据类型可以不同。但是字典中的key只能存在一个,即必须唯一(如果不唯一,则被称为multimap)。上述这点保证了值(value)可以直接通过键(key)来访问,这便是字典结构最为便捷之处。

内部实现机理

数据结构其实是两种类型最为根本的区别,其他的不同都是这种区别产生的结果。

  1. map是基于红黑树结构实现的。红黑树是一种平衡二叉查找树的变体结构,它的左右子树的高度差有可能会大于 1。所以红黑树不是严格意义上的平衡二叉树AVL,但对之进行平衡的代价相对于AVL较低, 其平均统计性能要强于AVL。红黑树具有自动排序的功能,因此它使得map也具有按键(key)排序的功能,因此在map中的元素排列都是有序的。在map中,红黑树的每个节点就代表一个元素,因此实现对map的增删改查,也就是相当于对红黑树的操作。对于这些操作的复杂度都为O(logn),复杂度即为红黑树的高度。

  2. unordered_map是基于哈希表(也叫散列表)实现的。散列表是根据关键码值而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。散列表使得unordered_map的插入和查询速度接近于O(1)(在没有冲突的情况下),但是其内部元素的排列顺序是无序的。

效率及其稳定性不同
这点实际上也是由底层的数据结构决定的。

        存储空间:unordered_map的散列空间会存在部分未被使用的位置,所以其内存效率不是100%的。而map的红黑树的内存效率接近于100%。
        查找性能的稳定性:map的查找类似于平衡二叉树的查找,其性能十分稳定。

        例如在1M数据中查找一个元素,需要多少次比较呢?20次。map的查找次数几乎与存储数据的分布与大小无关。而unordered_map依赖于散列表,如果哈希函数映射的关键码出现的冲突过多,则最坏时间复杂度可以达到是O(n)。因此unordered_map的查找次数是与存储数据的分布与大小有密切关系的,它的效率是不稳定的。

优缺点以及适用处
1. map
优点:

  1. 有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作。
  2. 红黑树,内部实现一个红黑树使得map的很多操作在$log_2 N$的时间复杂度下就可以实现,因此效率非常的高。
  3. map的各项性能较为稳定,与元素插入顺序无关。
  4. map支持范围查找。

缺点:

  1. 空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点,孩子节点以及红/黑性质,使得每一个节点都占用大量的空间。
  2. 对于单次查询时间较为敏感,必须保持查询性能的稳定性,比如实时应用等等。

适用处,对于那些有顺序要求的问题,用map会更高效一些


2. unordered_map
优点:

  1. 因为内部实现了哈希表,查询速度快,平均性能接近于常数时间O(1)

缺点:

  1. 元素无序 
  2. 哈希表的建立比较耗费时间,解决冲突new节点
  3. 查询性能不太稳定,最坏时间复杂度可达到O(n)

适用处,对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map


map和unordered_map并无好坏之分,它们都有各自应用的场景。它们之间的区别归根结底来源于使用的数据结构不同。
        在需要元素有序性或者对单次查询性能要求较为敏感时,请使用map,其余情况下应使用unordered_map。
        因此在需要使用字典结构进行算法编程的大部分情况下,都需要使用unordered_map而不是map。

拓展 :
c++ std中set与unordered_set区别和map与unordered_map区别类似:

        set 基于红黑树实现,红黑树具有自动排序的功能,因此 map 内部所有的数据,在任何时候,都是有序的。
        unordered_set 基于哈希表,数据插入和查找的时间复杂度很低,几乎是常数时间,而代价是消耗比较多的内存,无自动排序功能。底层实现上,使用一个下标范围比较大的数组来存储元素,形成很多的桶,利用 hash 函数对 key 进行映射到不同区域进行保存。

参考文章:(5条消息) 关于map与unordered_map使用的时间效率的思考探索(可能进一步拓展到C++ STL容器及其操作)_unordered_map时间复杂度_努力的耿耿的博客-CSDN博客

1. unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到$log_2 N$,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个 unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同

本文中只对unordered_map和unordered_set进行介绍, unordered_multimap和unordered_multiset可查看文档介绍。

1.1 unordered_map

unordered_map - C++ Reference (cplusplus.com)

  1. unordered_map是存储键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
  2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此 键关联。键和映射值的类型可能不同。
  3. 在内部,unordered_map没有对按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
  4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭 代方面效率较低。
  5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问 value。
  6. 它的迭代器至少是前向迭代器。

1.1.2 unordered_map的接口说明

1. unordered_map的构造

unordered_map::unordered_map - C++ Reference (cplusplus.com)
 

2. unordered_map的容量

 

3. unordered_map的迭代器

 

4. unordered_map的元素访问

注意:operator[ ] 函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶 中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中, 将key对应的value返回。

5. unordered_map的查询

注意:count函数返回哈希桶中关键码为key的键值对的个数,unordered_map中key是不能重复的,因此count函数的返回值最大为1。

6. unordered_map的修改操作

 7. unordered_map的桶操作

 注意:bucket_count 函数返回哈希桶中桶的个数,max_bucket_count返回桶的最大数目,bucket_size返回n号桶中有效元素的总个数,bucket返回元素key所在的桶号。

8.其他成员函数

 注意:load_factor返回负载因子,max_load_factor获取或设置最大负载因子,rehash设置桶的数量,reserve请求容量更改(公共成员函数)

1.2 unordered_set

  • unoredered_set是存储没有特定顺序的唯一元素的容器,它允许基于它们的值快速检索单个元素。
  • 在unordered_set中,元素的值同时也是唯一标识它的键。键是不可变的,因此,在容器中不能修改unordered_set中的元素,但是可以插入和删除它们。
  • 在内部,unordered_set中的元素没有按照任何特定的顺序排序,而是根据它们的散列值组织到bucket中,以便通过它们的值直接快速访问单个元素(平均时间复杂度为常数)。unordered_set容器在按键访问单个元素时比set容器快,尽管它们在通过其元素子集进行范围迭代时通常效率较低。容器中的迭代器至少是前向迭代器。
  • 其成员函数参考文档unordered_set - C++ Reference (cplusplus.com)

1.3 在线OJ

961. 在长度 2N 的数组中找出重复 N 次的元素 - 力扣(LeetCode)

 

class Solution {
public:
    int repeatedNTimes(vector<int>& nums) {
        //假设题目为找出数组中重复为N次的元素
        
        //解法1:暴力求解O(N^2)

        //解法2:使用map,时间复杂度为O(N)
        // map<int, int> numsCount;
        // for(auto e : nums){
        //     numsCount[e]++;
        // }
        // for(auto e : nums){
        //     if(numsCount[e] == nums.size() / 2){
        //         return e;
        //     }
        // }
        // return -1;

        //解法3:排序,前后指针,时间复杂度O(NlogN)
        std::sort(nums.begin(), nums.end());
        size_t prev = 0;
        size_t cur = 0;
        while (cur < nums.size()) {
            int count = 0;
            while (cur < nums.size() && nums[cur] == nums[prev]) {
                count++;
                if (count == nums.size() / 2) {
                    return nums[cur];
                }
                cur++;
            }
            prev = cur;
        }
        return nums[cur];

        //解法4,使用哈希O(N)
        size_t N = A.size()/2;
        // 用unordered_map统计每个元素出现的次数
        unordered_map<int, int> m;
        for(auto e : A)
            m[e]++;
        
        // 找出出现次数为N的元素
        for(auto& e : m)
        {
            if(e.second == N)
                return e.first;
        }

    }
};

349. 两个数组的交集 - 力扣(LeetCode)

 

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
         // 用unordered_set对nums1中的元素去重
        unordered_set<int> s1;
        for (auto e : nums1)
            s1.insert(e);
        // 用unordered_set对nums2中的元素去重
        unordered_set<int> s2;
        for (auto e : nums2)
            s2.insert(e);
        // 遍历s1,如果s1中某个元素在s2中出现过,即为交集
        vector<int> vRet;
        for (auto e : s1)
        {
            if (s2.find(e) != s2.end())
                vRet.push_back(e);
        }
        return vRet;
    }
};

350. 两个数组的交集 II - 力扣(LeetCode)

class Solution {
public:
    vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
        if (nums1.size() > nums2.size()) {
            return intersect(nums2, nums1);
        }
        unordered_map <int, int> m;
        for (int num : nums1) {
            ++m[num];
        }
        vector<int> intersection;
        for (int num : nums2) {
            if (m.count(num)) {
                intersection.push_back(num);
                --m[num];
                if (m[num] == 0) {
                    m.erase(num);
                }
            }
        }
        return intersection;
    }
};

217. 存在重复元素 - 力扣(LeetCode)

class Solution {
public:
    bool containsDuplicate(vector<int>& nums) {
        unordered_set<int> s;
        for (int x: nums) {
            if (s.find(x) != s.end()) {
                return true;
            }
            s.insert(x);
        }
        return false;
    }
};

884. 两句话中的不常见单词 - 力扣(LeetCode)

class Solution {
public:
    vector<string> uncommonFromSentences(string s1, string s2) {
        unordered_map<string, int> map;
        int n = s1.size();
        for(int i = 0;i<n;++i){
            int l = i;
            while(i<n && s1[i] != ' ') ++i;
            map[s1.substr(l,i-l)]++;
        }
         n = s2.size();
        for(int i = 0;i<n;++i){
            int l = i;
            while(i<n && s2[i] != ' ') ++i;
            map[s2.substr(l,i-l)]++;
        }

        vector<string> res;
        for(auto & itr:map){
            if(itr.second == 1) res.push_back(itr.first);
        }

        return res;
    }
};

2. 底层结构

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

2.1 哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即 O($log_2 N$),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

插入元素

        根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

搜索元素

        对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称 为哈希表(Hash Table)(或者称散列表)。

例如:数据集合{1,7,6,4,5,9};

哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

 用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快 问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?

2.2 哈希冲突

对于两个数据元素的关键字$k_i$和 $k_j$(i != j),有$k_i$ != $k_j$,但有:Hash($k_i$) == Hash($k_j$),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”

发生哈希冲突该如何处理呢?

2.3 哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。

哈希函数设计原则:

  1.         哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  2.         哈希函数计算出来的地址能均匀分布在整个空间中
  3.         哈希函数应该比较简单

1. 直接定址法--(常用)

        取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

        优点:简单、均匀

        缺点:需要事先知道关键字的分布情况

        使用场景:适合查找比较小且连续的情况

2. 除留余数法--(常用)

        设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数, 按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

3. 平方取中法--(了解)

        假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址

        平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

4. 折叠法--(了解)

        折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

        折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况

5. 随机数法--(了解)

        选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中 random为随机数函数。

        通常应用于关键字长度不等时采用此法

6. 数学分析法--(了解)

        设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定 相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散 列地址。例如:

假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是 相同 的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还 可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移 位、前两数与后两数叠加(如1234改成12+34=46)等方法。

        数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的 若干位分布较均匀的情况

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

2.4 哈希冲突解决

解决哈希冲突两种常见的方法是:闭散列和开散列

2.4.1 闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?

1. 线性探测

        比如2.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4, 因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

        线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

插入操作

        通过哈希函数获取待插入元素在哈希表中的位置

        如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素

 删除操作

        采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影 响。因此线性探测采用标记的伪删除法来删除一个元素

// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE}; 

2. 线性探测的实现

namespace Thb {
	enum State {
		EMPTY,
		EXIST,
		DELETE
	};
	template<class K, class V>
	class HashData {
	public:
		std::pair<K, V> _kv;
		State _state = EMPTY;
	};
	template<class K>
	struct HashFunc
	{
		size_t operator()(const K& key) {
			return (size_t)key;
		}
	};
	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable {
	public:
		bool Insert(const std::pair<K, V>& kv) {
			//去重
			if (Find(kv.first) != nullptr) {
				return false;
			}
			//控制负载因子——扩容
			if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7) {
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V, Hash> newHT;
				newHT._tables.resize(newsize);
				for (auto& e : _tables) {
					if (e._state == EXIST) {
						newHT.Insert(e._kv);
					}
				}
				_tables.swap(newHT._tables);
			}
			//线性探测
			//size_t hashi = hash(kv.first) % _tables.size();
			//while (_tables[hashi]._state == EXIST) {
			//	hashi++;
			//	hashi %= _tables.size();
			//}
			//_tables[hashi]._kv = kv;
			//_tables[hashi]._state = EXIST;
			//_size++;	
			
			//二次探测
			size_t start = hash(kv.first) % _tables.size();
			size_t i = 0;
			size_t hashi = start + i;
			while (_tables[hashi]._state == EXIST) {
				i++;
				hashi = start + i * i;
				hashi %= _tables.size();
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			_size++;
			return true;
		}
		HashData<K, V>* Find(const K& key) {
			if (_tables.size() == 0) {
				return nullptr;
			}
			size_t start = hash(key) % _tables.size();
			size_t i = 0;
			size_t hashi = start + i;
			while (_tables[hashi]._state != EMPTY) {
				if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key) {
					return &_tables[hashi];
				}
				i++;
				hashi = start + i * i;
				hashi %= _tables.size();
				if (hashi == start) {
					break;
				}
			}
			return nullptr;
		}
		bool Erase(const K& key) {
			auto fptr = Find(key);
			if (fptr) {
				fptr->_state = DELETE;
				_size--; 
				return true;
			}
			return false;
		}
		void Print() {
			for (auto& e : _tables) {
				if (e._state == EXIST) {
					std::cout << "[" << e._kv.first << ":" << e._kv.second << "]" << " ";
				}
			}
			std::cout << std::endl;
		}
	private:
		std::vector<HashData<K, V>> _tables;
		size_t _size = 0;
		Hash hash;
	};
}

思考:哈希表什么情况下进行扩容?如何扩容?

 

void CheckCapacity()
{
    if(_size * 10 / _ht.capacity() >= 7)
   {
        HashTable<K, V, HF> newHt(GetNextPrime(ht.capacity));
        for(size_t i = 0; i < _ht.capacity(); ++i)
       {
            if(_ht[i]._state == EXIST)
                newHt.Insert(_ht[i]._val);
       }
        
        Swap(newHt);
   }
}

线性探测优点:实现非常简单

线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同 关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降 低。如何缓解呢?

3. 二次探测

        线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位 置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法 为:$H_i$ = ($H_0$ + $i^2$ )% m, 或者:$H_i$ = ($H_0$ - $i^2$ )% m。其中:i = 1,2,3…, $H_0$是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表 的大小。

对于2.1中如果要插入44,产生冲突,使用解决后的情况为:

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任 何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在 搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出 必须考虑增容。

因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

2.4.2 开散列

1. 开散列概念

        开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地 址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链 接起来,各链表的头结点存储在哈希表中。

 从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素

2. 开散列实现

namespace HashBucket {
	template<class K>
	struct HashFunc
	{
		size_t operator()(const K& key) {
			return (size_t)key;
		}
	};
	template<class K, typename V>
	class HashBucketNode {
	public:
		HashBucketNode(const std::pair<K, V>& kv)
			:_pNext(nullptr)
			,_kv(kv)
		{}

		HashBucketNode<K, V>* _pNext;
		std::pair<K, V> _kv;
	};
	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable {
		typedef HashBucketNode<K, V> Node;
	public:
		~HashTable()
		{
			for (size_t i = 0; i < _tables.size(); ++i) {
				Node* cur = _tables[i];
				while (cur) {
					_tables[i] = cur->_pNext;
					delete cur;
					cur = _tables[i];
				}
			}
			_size = 0;
		}
		inline size_t __stl_next_prime(size_t n) {
			static const size_t __stl_num_prime = 28;
			static const size_t __stl_prime_list[__stl_num_prime] = {
				53, 97, 193, 389, 769,
				1543, 3079, 6151, 12289, 24593,
				49157, 98317, 196613, 393241, 786433,
				1572869, 3145739, 6291469, 12582917, 24165843,
				50331653, 100663319, 201326611, 402653189, 805306457,
				1610612741, 3221225473, 4294967291
			};
			for (size_t i = 0; i < __stl_num_prime; ++i) {
				if (__stl_prime_list[i] > n) {
					return __stl_prime_list[i];
				}
			}
			return -1;
		}
		bool Insert(const std::pair<K, V>& kv) {
			//去重
			if (Find(kv.first)) {
				return false;
			}
			//扩容——负载因子到1就扩容
			if (_size == _tables.size()) {
				std::vector<Node*> newTables;
				/*size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				newTables.resize(newsize , nullptr);*/
				newTables.resize(__stl_next_prime(_tables.size()), nullptr);
				for (size_t i = 0; i < _tables.size(); ++i) {
					Node* first = _tables[i];
					while (first) {
						_tables[i] = first->_pNext;
						size_t hashi = hash(first->_kv.first) % newTables.size();
						first->_pNext = newTables[hashi];
						newTables[hashi] = first;  
						first = _tables[i];
					}
				}
				_tables.swap(newTables);
			}

			//探测位置
			size_t hashi = hash(kv.first) % _tables.size();
			//单链表头插,尾插需要遍历找尾
			Node* newNode = new Node(kv);
			newNode->_pNext = _tables[hashi];
			_tables[hashi] = newNode;
			++_size;
			return true;
		}
		bool Erase(const K& key) {
			Node* delnode = Find(key);
			if (delnode == nullptr) {
				return false;
			}
			size_t hashi = hash(key) % _tables.size();
			Node* prev = _tables[hashi];
			if (prev->_kv.first == key) {
				_tables[hashi] = delnode->_pNext;
			}
			else {
				while (prev && prev->_pNext != delnode) {
					prev = prev->_pNext;
				}
				assert(prev != nullptr);
				prev->_pNext = delnode->_pNext;
			}
			delete delnode;
			--_size;
			return true;
		}
		Node* Find(const K& key) {
			if (_tables.size() == 0) {
				return nullptr;
			}
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur && cur->_kv.first != key) {
				cur = cur->_pNext;
			}
			return cur;
		}
		size_t size() {
			return _size;
		}
		bool empty() {
			return _size == 0;
		}
		//表的长度
		size_t TableSize() {
			return _tables.size();
		}
		//桶的个数
		size_t BucketNum() {
			size_t count = 0;
			for (auto& e : _tables) {
				if (e)
					count++;
			}
			return count;
		}
		//最大的桶长度
		size_t MaxBucketLenth() {
			size_t max = 0;
			size_t i = 0;
			for (; i < _tables.size(); ++i) {
				Node* cur = _tables[i];
				size_t size = 0;
				while (cur) {
					size++;
					cur = cur->_pNext;
				}
				if (max < size) {
					max = size;
				}
			}
			printf("[%zu]号桶最长==》 %zu\n",i, max);
			return max;
		}
		void Print() {
			for (size_t i = 0; i < _tables.size(); ++i) {
				Node* cur = _tables[i];
				while (cur) {
					std::cout << "[" << cur->_kv.first << "]" << cur->_kv.second << " ";
					cur = cur->_pNext;
				}
			}
			std::cout << std::endl;
		}
	private:
		std::vector<Node*> _tables;
		size_t _size = 0;
		Hash hash;
	};
}

3. 开散列增容

        桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可 能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希 表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。

4. 开散列的思考

1. 只能存储key为整形的元素,其他类型怎么解决?上述代码已解决

// 哈希函数采用处理余数法,被模的key必须要为整形才可以处理
// 此处提供将key转化为整形的方法
// 整形数据不需要转化
    template<class K>
	struct HashFunc
	{
		size_t operator()(const K& key) {
			return (size_t)key;
		}
	};

采用仿函数,通过类模板参数传递,当数据类型为其它类型时,只需要重写仿函数,通过类模板传递,或者使用类特化!

测试代码:

namespace Thb{
	template<>
	class HashFunc<std::string>
	{
	public:
		size_t operator()(const std::string& key) {
			size_t val = 0;
			for (auto e : key) {
				val *= 131;
				val += e;
			}
			return val;
		}
	};
}
void TestHashTable2() {
	std::string str[] = { "苹果", "西瓜","苹果" , "西瓜" ,"苹果", "苹果", "西瓜" ,"苹果","香蕉","苹果", "香蕉" };
	Thb::HashTable<std::string, int> ht;
	for (auto& e : str) {
		if (ht.Find(e) == nullptr) {
			ht.Insert(std::make_pair(e, 1));
		}
		else {
			ht.Find(e)->_kv.second++;
		}
	}
	ht.Print();
}
void TestHashBucket2() {
	std::string str[] = { "苹果", "西瓜","苹果" , "西瓜" ,"苹果", "苹果", "西瓜" ,"苹果","香蕉","苹果", "香蕉" };
	HashBucket::HashTable<std::string, int, Thb::HashFunc<std::string>> ht;
	for (auto& e : str) {
		if (ht.Find(e) == nullptr) {
			ht.Insert(std::make_pair(e, 1));
		}
		else {
			ht.Find(e)->_kv.second++;
		}
	}
	ht.Print();
}

2. 除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?上述代码已解决

根据PG版STL源代码可发现其扩容,使用了素数,为什么使用素数

参考文章:哈希容量大小为什么最好为素数

各种字符串Hash函数 - clq - 博客园

5. 开散列与闭散列比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上: 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

3.封装模拟实现unordered_map/unordered_set/迭代器

3.1 哈希表的改造

1. 模板参数列表的改造

// K:关键码类型
// T: 不同容器T的类型不同,如果是unordered_map,V代表一个键值对,如果是
unordered_set,T 为 K
// ExtractKey: 因为T的类型不同,通过value取key的方式就不同,详细见
unordered_map/set的实现
// Hash: 哈希函数仿函数对象类型,哈希函数使用除留余数法,需要将Key转换为整形数字才能
取模
//Pred: 传递仿函数,查找时比较规则,如果key的类型为指针,其内部比较的是地址,因此需要实现成仿函数,便于用户自行根据使用实现特化或模板参数传递,泛型编程
    template<class K, class T, class Hash, class ExtractKey, class Pred>
	class HashTable

2. 增加迭代器操作

//前置声明——编译器只会向上找
	template<class T>
	class HashBucketNode;

	template<class K, class T, class Hash, class ExtractKey, class Pred>
	class HashTable;

	// 注意:因为哈希桶在底层是单链表结构,所以哈希桶的迭代器不需要--操作
	//在哈希桶的迭代器类中需要用到HashBucketNode、HashTable本身,因此根据编译器查找规则,需要进行前置声明
	//因为需要访问HashTable的内部成员变量以便于找到需要的桶,在进行桶遍历
	//因此定义为友元,或者实现GetTables()公共成员函数
	template<class K, class T, class Ref, class Ptr, class Hash, class ExtractKey, class Pred>
	class __Hash_Iterator {
	public:
		typedef HashBucketNode<T> Node;
		typedef __Hash_Iterator<K, T, Ref, Ptr, Hash, ExtractKey, Pred> Self;
		typedef HashTable<K, T, Hash, ExtractKey, Pred> HTable;

		Node* _node;
		HTable* _pht;

		
		__Hash_Iterator(Node* node, HTable* pht)
			:_node(node)
			,_pht(pht)
		{}

		Ref operator*() {
			return _node->_data;
		}
		Ptr operator->() {
			return &_node->_data;
		}
		Self& operator++() {
			if (_node->_pNext) {
				_node = _node->_pNext;
			}
			else {
				ExtractKey tokey;
				Hash hash;
				size_t index = hash(tokey(_node->_data)) % _pht->_tables.size();
				++index;
				for (; index < _pht->_tables.size(); ++index) {
					if (_pht->_tables[index]) {
						_node = _pht->_tables[index];
						break;
					}
				}
				if (index == _pht->_tables.size()) {
					_node = nullptr;
				}
			}
			return *this;
		}
		bool operator!=(const Self& s)const {
			return _node != s._node;
		}

		bool operator==(Self& s) const {
			return _node == s._node;
		}
	};

3. 增加通过key获取value操作、及相同值判断仿函数

template<class T>
	class HashBucketNode {
	public:
		HashBucketNode(const T& data)
			:_pNext(nullptr)
			,_data(data)
		{}

		HashBucketNode<T>* _pNext;
		T _data;
	};

	template<class K, class T, class Hash, class ExtractKey, class Pred>
	class HashTable {
		typedef HashBucketNode<T> Node;
		
		//模板的友元
		template<class K, class T, class Ref, class Ptr, class Hash, class ExtractKey, class Pred>
		friend class __Hash_Iterator;

	public:

		typedef __Hash_Iterator<K, T, T&, T*, Hash, ExtractKey, Pred> iterator;


		iterator begin() {
			for (size_t i = 0; i < _tables.size(); ++i) {
				if (_tables[i] != nullptr) {
					return iterator(_tables[i], this);
				}
			}
			return end();
		}
		iterator end() {
			return iterator(nullptr, this);
		}

		~HashTable()
		{
			for (size_t i = 0; i < _tables.size(); ++i) {
				Node* cur = _tables[i];
				while (cur) {
					_tables[i] = cur->_pNext;
					delete cur;
					cur = _tables[i];
				}
			}
			_size = 0;
		}
		inline size_t __stl_next_prime(size_t n) {
			static const size_t __stl_num_prime = 28;
			static const size_t __stl_prime_list[__stl_num_prime] = {
				53, 97, 193, 389, 769,
				1543, 3079, 6151, 12289, 24593,
				49157, 98317, 196613, 393241, 786433,
				1572869, 3145739, 6291469, 12582917, 24165843,
				50331653, 100663319, 201326611, 402653189, 805306457,
				1610612741, 3221225473, 4294967291
			};
			for (size_t i = 0; i < __stl_num_prime; ++i) {
				if (__stl_prime_list[i] > n) {
					return __stl_prime_list[i];
				}
			}
			return -1;
		}

		std::pair<iterator,bool> Insert(const T& data) {
			Hash hash;
			ExtractKey tokey;
			iterator iter = Find(tokey(data));
			//去重
			if (iter != end()) {
				return std::make_pair(iter, false);
			}
			//扩容——负载因子可能为0.5-1,但是其桶最大长度不会超过3
			if (_size == _tables.size()) {
				std::vector<Node*> newTables;
				/*size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				newTables.resize(newsize , nullptr);*/
				newTables.resize(__stl_next_prime(_tables.size()), nullptr);
				for (size_t i = 0; i < _tables.size(); ++i) {
					Node* first = _tables[i];
					while (first) {
						_tables[i] = first->_pNext;
						size_t hashi = hash(tokey(first->_data)) % newTables.size();
						first->_pNext = newTables[hashi];
						newTables[hashi] = first;  
						first = _tables[i];
					}
				}
				_tables.swap(newTables);
			}

			//探测位置
			size_t hashi = hash(tokey(data)) % _tables.size();
			//单链表头插,尾插需要遍历找尾
			Node* newNode = new Node(data);
			newNode->_pNext = _tables[hashi];
			_tables[hashi] = newNode;
			++_size;
			return std::make_pair(iterator(newNode, this), true);
		}
		bool Erase(const K& key) {
			Hash hash;
			ExtractKey tokey;
			Pred equalto;
			Node* delnode = Find(key);
			if (delnode == nullptr) {
				return false;
			}
			size_t hashi = hash(key) % _tables.size();
			Node* prev = _tables[hashi];
			if (equalto(tokey(prev->_data), key)) {
				_tables[hashi] = delnode->_pNext;
			}
			else {
				while (prev && prev->_pNext != delnode) {
					prev = prev->_pNext;
				}
				assert(prev != nullptr);
				prev->_pNext = delnode->_pNext;
			}
			delete delnode;
			--_size;
			return true;
		}
		iterator Find(const K& key) {
			Hash hash;
			ExtractKey tokey;
			Pred equalto;
			if (_tables.size() == 0) {
				return end();
			}
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur) {
				if (equalto(tokey(cur->_data), key)) {
					return iterator(cur, this);
				}
				cur = cur->_pNext;
			}
			return end();
		}
		size_t size() {
			return _size;
		}
		bool empty() {
			return _size == 0;
		}
		//表的长度
		size_t TableSize() {
			return _tables.size();
		}
		//桶的个数
		size_t BucketNum() {
			size_t count = 0;
			for (auto& e : _tables) {
				if (e)
					count++;
			}
			return count;
		}
		//最大的桶长度
		size_t MaxBucketLenth() {
			size_t max = 0;
			size_t i = 0;
			for (; i < _tables.size(); ++i) {
				Node* cur = _tables[i];
				size_t size = 0;
				while (cur) {
					size++;
					cur = cur->_pNext;
				}
				if (max < size) {
					max = size;
				}
			}
			printf("[%zu]号桶最长==》 %zu\n",i, max);
			return max;
		}

	private:
		std::vector<Node*> _tables;
		size_t _size;
	};
}

3.2 unordered_map封装实现

#pragma once
#include"HashTable.h"

namespace HashBucket {

	template<class K, class T, class Hash = HashFunc<K>, class Pred = euqal_to<K>>
	class unordered_map {
		typedef HashBucketNode<std::pair<K, T>> Node;

		class map_KeyOfValue {
		public:
			const K& operator()(const std::pair<K, T>& kv) {
				return kv.first;
			}
		};
	public:
		typedef typename HashTable<K, std::pair<K, T>, Hash, map_KeyOfValue, Pred>::iterator iterator;


		iterator begin() {
			return _hb.begin();
		}
		iterator end() {
			return _hb.end();
		}
		std::pair<iterator, bool> insert(const std::pair<K, T>& _kv) {
			return _hb.Insert(_kv);
		}
		bool erase(const K& key) {
			return _hb.Erase(key);
		}
		iterator find(const K& key) {
			return _hb.Find(key);
		}
		T& operator[](const K& key) {
			return (_hb.Insert(std::make_pair(key, T())).first)->second;
		}
	private:
		HashTable<K, std::pair<K, T>, Hash, map_KeyOfValue, Pred> _hb;
	};
}

3.3 unordered_set封装实现

#pragma once
#include"HashTable.h"

namespace HashBucket {

	template<class K>
	class euqal_to {
	public:
		bool operator()(const K& key1, const K& key2) {
			return key1 == key2;
		}
	};

	template<class K, class Hash = HashFunc<K>, class Pred = euqal_to<K>>
	class unordered_set {
		typedef HashBucketNode<K> Node;

		class set_KeyOfVal {
		public:
			const K& operator()(const K& key) {
				return key;
			}
		};
	public:
		typedef typename HashTable<K, K, Hash, set_KeyOfVal, Pred>::iterator iterator;


		iterator begin() {
			return _hb.begin();
		}
		iterator end() {
			return _hb.end();
		}

		std::pair<iterator, bool> insert(const K& _kv) {
			return _hb.Insert(_kv);
		}
		bool erase(const K& key) {
			return _hb.Erase(key);
		}
		iterator find(const K& key) {
			return _hb.Find(key);
		}

	private:
		HashTable<K, K, Hash, set_KeyOfVal, Pred> _hb;
	};
}

3.4 Test代码

namespace HashBucket{
	template<>
	class HashFunc<std::string>
	{
	public:
		size_t operator()(const std::string& key) {
			size_t val = 0;
			for (auto e : key) {
				val *= 131;
				val += e;
			}
			return val;
		}
	};
}
void TestSetIterator() {
	HashBucket::unordered_set<int> hm;
	int arr[] = { 1, 11, 4, 15, 26, 7, 14, 9 ,17, 19, 20 };
	for (auto& e : arr) {
		hm.insert(e);
	}
	HashBucket::unordered_set<int>::iterator it = hm.begin();
	while (it != hm.end()) {
		std::cout << *it << " ";
		++it;
	}
	std::cout << std::endl;
}
void TestMapIterator() {
	HashBucket::unordered_map<std::string, std::string> dict;
	dict.insert(std::make_pair("InsertSort", "插入排序"));
	dict.insert(std::make_pair("ShellSort", "希尔排序"));
	dict.insert(std::make_pair("QuickSort", "快速排序"));
	dict.insert(std::make_pair("BubleSort", "冒泡排序"));

	auto it = dict.begin();
	while (it != dict.end()) {
		std::cout << it->first << " ";
		++it;
	}
	std::cout << std::endl;

	dict["ShellSort"] = "xier排序";
	dict["nihaoa"];
	auto it1 = dict.begin();
	while (it1 != dict.end()) {
		std::cout << it1->first << " " << it1->second << "\n";
		++it1;
	}
	std::cout << std::endl;
}
int main() {
	TestMapIterator(); TestHashBucket2();
	return 0;
}


 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/757809.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring原码学习第一篇:Spring源码概述

1、Spring获取对象的过程 2、Spring源码概述图&#xff1a; 2、一些重要的接口 BeanDefinition中实现的方法&#xff0c;把xml中定义的对象封装为一个对象&#xff0c;方便后面处理 4、BeandefinitionReader BeandefinitionReader作为一个抽象层来处理配置文件&#xff0c;定…

Autosar通信实战系列01-CanSM模块功能及配置开发详解

本文框架 前言1. CanSM General配置2. 通道详细配置解析2.1 添加一路CanSMController2.2 CanSMController配置2.3 关联DemEvent配置前言 在本系列笔者将结合工作中对通信实战部分的应用经验进一步介绍常用,包括但不限于通信各模块的开发教程,代码逻辑分析,调测试方法及典型…

【Maven二】——maven仓库

系列文章目录 Maven之POM介绍 maven命令上传jar包到nexus maven仓库 系列文章目录前言一、什么是maven仓库&#xff1f;二、仓库的分类建立私服的优势 三、远程仓库的配置四、远程仓库的认证部署至远程仓库五、快照版本&why六、从仓库解析依赖的机制七、镜像总结 前言 由…

习题—实用类

目录 1.xxx 1.包装类及其构造方法 import java.util.List; import java.util.ArrayList; import java.util.Scanner;//包装类及其构造方法 public class Test {public static void main(String[] args) {// 装箱&#xff1a;基本类型转换为包装类的对象 // 拆箱&#xff1a…

go mod verdor简明介绍

Go 语言在 go 1.6 版本以后编译 go 代码会优先从 vendor 目录先寻找依赖包&#xff0c;它具有以下优点&#xff1a; 复制依赖&#xff1a;go mod vendor 会把程序所依赖的所有包复制到项目目录下的vendor 文件夹中&#xff0c;所以即使这些依赖包在外部源&#xff08;如 GitHu…

「C/C++」C/C++宏定义#define

✨博客主页&#xff1a;何曾参静谧的博客 &#x1f4cc;文章专栏&#xff1a;「C/C」C/C程序设计 目录 术语说明宏定义 #define定义常量定义函数定义代码块常用标识符用宏包含头文件 术语说明 定义宏是一种预处理器指令&#xff0c;它可以将一些代码片段或者常量直接替换为另一…

刘二大人Pytorch课程笔记

Lecture01. Overview 没啥好记的&#xff0c;理解就好 人工智能和机器学习等的关系&#xff1a; 正向传播 正向传播本质上是按照输入层到输出层的顺序&#xff0c;求解并保存网络中的中间变量本身。 反向传播 反向传播本质上是按照输出层到输入层的顺序&#xff0c;求解并…

LangChain 联合创始人下场揭秘:如何用 LangChain 和向量数据库搞定语义搜索?

近期&#xff0c;关于 ChatGPT 的访问量有所下降的消息引发激烈讨论&#xff0c;不过这并不意味着开发者对于 AIGC 的热情有所减弱&#xff0c;例如素有【2023 最潮大语言模型 Web 开发框架】之称的大网红 LangChain 的热度就只增不减。 原因在于 LangChain 作为大模型能力“B2…

1快速入门MyBatis

开发前的准备 准备数据库表&#xff1a;汽⻋表t_car 确定表中的字段以及字段的数据类型 guide_price是decimal类型&#xff0c;专⻔为财务数据准备的类型produce_time可以用char类型 , 格式’2022-10-11’ 使用navicat for mysql⼯具向t_car表中插⼊两条数据 配置IDEA中ma…

【C++修炼之路】vector 模拟实现

&#x1f451;作者主页&#xff1a;安 度 因 &#x1f3e0;学习社区&#xff1a;StackFrame &#x1f4d6;专栏链接&#xff1a;C修炼之路 文章目录 一、读源码二、成员变量三、默认成员函数1、构造2、析构3、拷贝构造4、赋值重载 四、访问1、[ ] 重载2、迭代器 五、容量1、cap…

Profibus DP主站转Modbus TCP网关profibus从站地址范围

远创智控YC-DPM-TCP网关。这款产品在Profibus总线侧实现了主站功能&#xff0c;在以太网侧实现了ModbusTcp服务器功能&#xff0c;为我们的工业自动化网络带来了全新的可能。 远创智控YC-DPM-TCP网关是如何实现这些功能的呢&#xff1f;首先&#xff0c;让我们来看看它的Profib…

Oracle解析JSON字符串

Oracle解析JSON字符串 假设某个字段存储的JSON字符串&#xff0c;我们不想查出来后通过一些常见的编程语言处理&#xff08;JSON.parse()或者是JSONObject.parseObject()等&#xff09;&#xff0c;想直接在数据库上处理&#xff0c;又该如何书写呢&#xff1f; 其实在ORACLE中…

算法06-搜索算法-广度优先搜索

文章目录 参考&#xff1a;总结大纲要求搜索算法-广度优先搜索迷宫问题问题迷宫的存储迷宫的移动搜索方式代码实现 图的广度优先遍历题目描述用邻接矩阵表示图 搜索算法-广度优先搜索 参考&#xff1a; 【算法设计】用C类和队列实现图搜索的广度优先遍历算法 C/C 之 广度优先…

梯度下降(Gradient Descent)

基本思想 梯度下降是一个用来求函数最小值的算法&#xff0c;本次&#xff0c;我们将使用梯度下降算法来求出代价函数的最小值。 梯度下降背后的思想是&#xff1a;开始时我们随机选择一个参数的组合&#xff0c;计算代价函数&#xff0c;然后我们寻找下一个能让代价函数值下降…

Linux:squid透明代理

在传统代理上进行修改并添加网卡 这次不使用手动代理&#xff0c;而是把网关搞成代理 在下面这个链接里的文章实验下进行修改 Linux&#xff1a;squid传统代理_鲍海超-GNUBHCkalitarro的博客-CSDN博客 完成以后不用再win10上去配置&#xff0c;代理的那一步&#xff0c;然后…

Python(十二)常见的数据类型

❤️ 专栏简介&#xff1a;本专栏记录了我个人从零开始学习Python编程的过程。在这个专栏中&#xff0c;我将分享我在学习Python的过程中的学习笔记、学习路线以及各个知识点。 ☀️ 专栏适用人群 &#xff1a;本专栏适用于希望学习Python编程的初学者和有一定编程基础的人。无…

TabLayout+ViewPager实现滚动页面

目录 一、TabLayout介绍 二、TabLayout的常用属性和方法 常用属性&#xff1a; 常用方法&#xff1a; 三、适配器介绍 &#xff08;一&#xff09;、PagerAdapter介绍&#xff1a; &#xff08;二&#xff09;、FragmentPagerAdapter介绍&#xff1a; &#xff08;三&am…

习题 1.26

我们先来看看题目要求&#xff0c;题目住说将 square 调用换成了&#xff08;* x x),结果导致执行时间变慢。 根据以前学过的内容&#xff0c;我们知道 在做显示乘法的时候&#xff0c;是直接进行计算的&#xff0c;而在做函数调用的时候&#xff0c;是先进行表达式展开的&…

【MySQL】常见函数使用(二)

&#x1f697;MySQL学习第二站~ &#x1f6a9;本文已收录至专栏&#xff1a;数据库学习之旅 ❤️文末附全文思维导图&#xff0c;感谢各位点赞收藏支持~ 就如同许多编程语言中的API一样&#xff0c;MySQL中的函数同样是官方给我们封装好的&#xff0c;可以直接调用的一段代码。…

ZooKeeper ZAB

文章首发地址 在接收到一个写请求操作后&#xff0c;追随者会将请求转发给群首&#xff0c;群首将探索性地执行该请求&#xff0c;并将执行结果以事务的方式对状态更新进行广播。一个事务中包含服务器需要执行变更的确切操作&#xff0c;当事务提交时&#xff0c;服务器就会将这…