【高阶数据结构】手撕哈希表(万字详解)

news2024/11/18 16:20:07

🌈欢迎来到数据结构专栏~~手撕哈希表


  • (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
  • 目前状态:大三非科班啃C++中
  • 🌍博客主页:张小姐的猫~江湖背景
  • 快上车🚘,握好方向盘跟我有一起打天下嘞!
  • 送给自己的一句鸡汤🤔:
  • 🔥真正的大师永远怀着一颗学徒的心
  • 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
  • 🎉🎉欢迎持续关注!
    请添加图片描述

请添加图片描述

文章目录

  • 🌈欢迎来到数据结构专栏~~手撕哈希表
    • unordered map/set和map/set区别
    • 一. 哈希概念
    • 二. 哈希冲突
    • 三. 哈希函数
    • 四. 如何解决哈希冲突?
      • 🌏闭散列 —— 开放定址法
      • 🌏开散列——链地址法(拉链法)
    • 五. 闭散列的实现
      • 🎨数据插入
      • 🎨数据查找
      • 🎨数据删除
      • 🎨仿函数
    • 六. 开散列的实现(哈希桶)
      • 💦数据插入
      • 💦数据查找
      • 💦数据删除
    • 哈希表的大小为什么建议是素数?
    • 实现方案
  • 📢写在最后

请添加图片描述

unordered map/set和map/set区别

  • map和set遍历是有序的,unordered map/set遍历是无序的!(讲到底层就了解了)

  • map和set是双向迭代器,unordered系列是单向迭代器

  • 基于上面的区别相比而言,map和set更强大,为什么还需要提供unordered系列?

    • 大量数据时,增删查改效率更优,尤其是查!

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

一. 哈希概念

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

而理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素

如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素

当向该结构中插入和删除时:

  • 插入元素: 根据待插入元素的关键码,用此函数计算出该元素的存储位置,并将元素存放到此位置。
  • 搜索元素: 对元素的关键码进行同样的计算,把求得的函数值当作元素的存储位置,在结构中按此位置取元素进行比较,若关键码相等,则搜索成功。

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

在这里插入图片描述
举个例子:
给定集合 {1, 7, 6, 4, 5000, 9000},将哈希函数设置为::hash(key) = key % capacity取模),其中capacity为存储元素空间的总大小。
若我们将该集合存储在 capacity 为10的哈希表中,则各元素存储位置对应如下:
在这里插入图片描述

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快

问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题? 哈希冲突!

二. 哈希冲突

也叫哈希碰撞:不同关键字通过相同哈希哈数计算出相同的哈希地址,比如在上述的例子中再插入44就会产生哈希冲突,因为44模10后,也等于4

在这里插入图片描述
面对这种问题该怎么样处理呢?

三. 哈希函数

不合理的哈希函数就是引发哈希冲突的重要原因,哈希函数设计的越精妙,产生哈希冲突的可能性越低!

哈希函数的设计遵从三大原则

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

常见的哈希函数有:

  1. 直接定址法(常用)
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况

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

优点:使用广泛,不受限制
缺点:需要解决哈希冲突,冲突越多,效率越低

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

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

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

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

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

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

  1. 数学分析法–(了解)
    设有n个d位数,每一位可能有r种不同的符号,这r中不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,而在某些位上分布不均匀,只有几种符号经常出现。此时,我们可根据哈希表的大小,选择其中各种符号分布均匀的若干位作为哈希地址
    例如:
    在这里插入图片描述

假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是相同的,那么我们可以选择后面的四位作为哈希地址。

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

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

四. 如何解决哈希冲突?

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

🌏闭散列 —— 开放定址法

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

  1. 线性探测
    当发生哈希冲突时,从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止
    请添加图片描述

Hi=(H0+i)%m  ( i = 1 , 2 , 3 , . . . )

H0:通过哈希函数对元素的关键码进行计算得到的位置
Hi:冲突元素通过线性探测后得到的存放位置
m:表的大小

例如,我们用除留余数法将序列{1, 6, 10, 1000, 11, 18, 7, 40}插入到表长为10的哈希表中,当发生哈希冲突时我们采用闭散列的线性探测找到下一个空位置进行插入,插入过程如下:

请添加图片描述

通过上图可以看出:随着哈希表中数据的增多,产生哈希冲突的可能性也随着增加,最后在40进行插入的时候更是连续出现了四次哈希冲突(踩踏效应

我们将数据插入到有限的空间中,随着数据的增多,冲突的概率越发越多,冲突多的时候插入的数据,在查找时候效率也会随之低下,为此引入了负载因子:

负载因子 = 表中有效数据个数 / 空间的大小

  • 负载因子越大,产生的概率就越多,增删查改的效率越低
  • 负载因子越小,产生的概率就越少,增删查改的效率越高,但是越小也意味着空间利用率越低,此时大量空间可能被浪费

如果我们把哈希表增大变成20,可以发现在插入相同数据时,产生的冲突会少

请添加图片描述

因此我们在闭散列(开放定址法)对负载因子的标准定在了 0.7~0.8,一旦大于 0.8 会导致查表时缓存未命中率呈曲线上升;这就是为什么有些哈希库都有规定的负载因子,Java 的系统库就将负载因子定成了 0.75,超过 0.75 就会自动扩容

😎作总结:

线性探测的优点:实现非常简单
线性探测的缺点:一旦发生冲突,所有的冲突连在一起,容易产生数据“堆积”,即不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要多次比较(踩踏效应),导致搜索效率降低。

  1. 二次探索
    线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:以2的i次方进行探测

Hi=(H0+i*i)%m  ( i = 1 , 2 , 3 , . . . )

H0:通过哈希函数对元素的关键码进行计算得到的位置。
Hi:冲突元素通过二次探测后得到的存放位置。
m:表的大小

接下来举个例子:

请添加图片描述

但是二次探测没有从本质上解决问题,还是占用式的占用别人位置

🌏开散列——链地址法(拉链法)

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

例如,我们用除留余数法将序列{1, 6, 15, 60, 88, 7, 40, 5, 10}插入到表长为10的哈希表中,当发生哈希冲突时我们采用开散列的形式,将哈希地址相同的元素都链接到同一个哈希桶下,插入过程如下:
请添加图片描述
相比于比散列的报复式占用其他人的位置(小仙女行为)来说,开散列就好得多了,用的是一种乐观的方式,我挂在这个节点的下面

与闭散列不同的是,这种将相同哈希地址的元素通过单链表链接起来,然后将链表的头结点存储在哈希表中的方式,不会影响与自己哈希地址不同的元素的增删查改的效率,因此开散列的负载因子相比闭散列而言,可以稍微大一点

  • 开散列的哈希桶,负载因子可以超过1,一般建议控制在[0.0, 1.0]之间

为什么开散列在实际中,更加实用呢?

  • 哈希桶的负载因子可以更大,空间利用率高
  • 哈希桶在极端情况下还有可用的解决方案

哈希桶的极端情况就是:所以元素全部都挂在一个节点下面,此时的效率为O(N)

在这里插入图片描述

一个桶中如果元素过多的话,可以考虑用红黑树结构代替,并将红黑树的根结点存储在哈希表中

在这里插入图片描述

这样一来就算是有十亿个数,都只要在这个桶里查找30次,这就是桶里种树

在这里插入图片描述

五. 闭散列的实现

在闭散列的哈希表中,每个位置不仅仅要存放数据之外,还要存储当前节点的状态,三大状态如下:

  • EMPTY(空位置)
  • EXIST(已经存放数据了)
  • DELETE(原本有数据,但被删除)

对此我们可以用枚举实现:

//枚举出三种状态
enum State
{
	EXIST,
	EMPTY,
	DELETE
};

那么状态的存在意义是什么?

💢举个例子:当我们需要在哈希表中查找一个数据40,这个数据我用哈希函数算出来他的位置是 0 ,但是我们不知道是不是存在哈希冲突,如果冲突就会向后偏移,我们就需要从 0 这个位置开始向后遍历,但是万万不能遍历完整个哈希表,这样就失去了哈希原本的意义

  • 通过除留余数法得知元素在哈希表中的地址0
  • 从0下标开始向后进行查找,若找到了40则说明存在,找到空位置判定为不存在即可

但是这样真的行得通吗?如果我是先删除了一个值1000,空出的空位在40之前,查找遇到空就停止了,此时并没有找到元素40,但是元素40却在哈希表中存在。

在这里插入图片描述

因此我们必须要给哈希表中的每个节点设置一个状态,有三种可能:当哈希表中的一个元素被删除后,我们不应该简单的将该位置的状态设置为EMPTY,而是应该将该位置的状态设置为DELETE

这样一来在查找的时候,遇到节点是EXIST或者DELETE的都要继续往后找,直到遇到空为止;而当我们插入元素的时候,可以将元素插入到状态为EMPTY或是DELETE的位置

所以节点的数据不仅仅要包括数据,还有包括当前的状态

//哈希节点存储结构
template<class K, class V>
struct HashNode
{
	pair<K, V> _kv;
	State _state = EMPTY; //状态
};

为了要在插入的时候算好负载因子,我们还要记录下哈希表中的有效数据,数据过多时进行扩容

templete<class K, class V>
class HashTable
{
public:
	//...
	
private:
	vector<HashData<K, V>> _tables;//哈希表
	size_t _size;//存储多少个有效数据
};

🎨数据插入

步骤如下:

1.查找该键值对是否存在,存在则插入失败
2.判断是否需要扩容:哈希表为空 & 负载因子过大 都需要扩容
3. 插入键值对
4. 有效元素个数++

其中扩容方式如下:

  • 如果是哈希表为空:就将哈希表的初始大小增大为10
  • 如果是负载因子大于0.7: 先要创建一个新的哈希表(大小是原来的两倍),遍历原哈希表,旧表的数据映射到新表(此处复用插入),最后两个哈希表互换。

此处要注意:是将 旧表的数据重新映射到新表,而不是直接把原有的数据原封不动的搬下来,要重新计算在新表的位置,再插入

产生了哈希冲突,就会出现踩踏事件,不断往后挪,又因为每次插入的时候会判断负载因子,超出了就会扩容,所以哈希表不会被装满!

bool Insert(const pair<K, V>& kv)
{
	//数据冗余
	if (Find(kv.first))
		return false;

	//负载因子到了就扩容
	if (_tables.size() == 0 || 10 * _size / _tables.size() > 7)//扩容
	{
		size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;

		//创建新的哈希表,大小设置为原哈希表的2倍
		HashTable<K, V> newHT;
		newHT._tables.resize(newSize);
		//旧表的数据映射到新表
		for (auto e : _tables)
		{
			if (e._state == EXIST)
			{
				newHT.Insert(e._kv);//复用插入,因为已经有一个开好了的两倍内存的哈希表
			}
		}

		_tables.swap(newHT._tables);//局部对象出作用域 析构
	}

	//注意不能是capacity,size存的是有效字符个数,capacity是能存有效字符的容量
	size_t hashi = kv.first % _tables.size();
	//线性探测
	while (_tables[hashi]._state == EXIST)
	{
		++hashi;
		hashi %= _tables.size();
	}
	
	/*Hash hash;
	size_t start = hash(kv.first) % _tables.size();
	size_t i = 0;
	size_t hashi = start;
	//二次探测
	while (_tables[hashi]._state == EXIST)
	{
		++i;
		hashi = start + i*i;
		hashi %= _tables.size();
	}*/

	//数据插入
	_tables[hashi]._kv = kv;
	_tables[hashi]._state = EXIST;
	++_size;

	return true;
}

🎨数据查找

步骤如下:

  1. 先判断哈希表大小是否,如果为零查找失败!
  2. 通过除留余数法算出对应的哈希地址
  3. 从哈希地址开始向后线性探测,直到遇到 EMPTY 位置还没找到则查找失败,如果遇到状态是DELETE的话,也要继续往后探测,因为该值已经被删掉了

💢注意:key相同的前提是状态不能是:删除。必须找到的是位置状态为 EXISTkey 值匹配,才算查找成功(不然找到的数据相同的,确实被删除了的)

HashData<K, V>* Find(const K& key)
{
	//如果是空表就直接返回空
	if (_tables.size() == 0)
		return nullptr;

	size_t start = key % _tables.size();
	size_t hashi = start;
	while (_tables[hashi]._state != EMPTY)//不等于空 == 存在和删除都要继续找
	{
		//key相同的前提是状态不能是:删除
		if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
		{
			return &_tables[hashi];
		}

		hashi++;
		hashi %= _tables.size();

		if (hashi == start)//极端判断:兜兜转转一圈遇到了
		{
			break;
		}
	}
	return nullptr;
}

🎨数据删除

删除的步骤比较简单:修改状态 —— 减少元素个数

  1. 检查哈希表中是否存在该元素
  2. 如果存在,把其状态改成DELETE即可
  3. 哈希的有效元素个数-1

注意:我们这里是伪删除:只是修改了数据的状态变成DELETE,并没有把数据真正的删掉了,因为插入时候的数据可以覆盖原有的 —— 数据覆盖

bool Erase(const K& key)
{
	HashData<K, V>* ret = Find(key);
	if (ret)   //找到了
	{
		ret->_state = DELETE; //状态改成删除
		--_size; //有效元素个数-1
		return true;
	}
	else
	{
		return false;
	}
}

🎨仿函数

如果我们统计的是字符串的出现次数呢?kv.first还能取模吗?怎么样转化string呢 —— 其实大佬早就帮我们想到了

在这里插入图片描述

仿函数转化成一个可以取模的值

  • 将key数据强制类型转换成size_t,如果key是string类型的就走string类型的特化版本
  • 这样就可以不用显示的传属于哪个Hashfunc

涉及到了BKDR算法,因为ascll码值单纯地加起来,可能会出现相同现象,大佬推算出了这个算法

特化:符合string类型的优先走string类型

template<class K>
struct Hashfunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

//特化版本
template<>
struct Hashfunc<string>
{
	//BKDR算法
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (auto ch : key)
		{
			val *= 131;//为什么是131?经过了
			val += ch;
		}
		return val;
	}
};

六. 开散列的实现(哈希桶)

在开散列的哈希表中,哈希表的每个位置存储的实际上是某个单链表的头结点,即每个哈希桶中存储的数据实际上是一个结点类型,该结点类型除了存储所给数据之外,还需要存储一个结点指针用于指向下一个结点:next

template<class K, class V>
struct HashNode
{
	pair<K, V> _kv;
	HashNode<K, V>* _next;

	//构造函数
	HashNode(const pair<K, V>& kv)
		:_kv(kv)
		,_next(nullptr)
	{}
};

为了使代码更有观赏感,对节点的类型进行typedef

typedef HashNode<K, V> Node;

这里与闭散列不同的是,不用给每个节点设置状态,因为将哈希地址相同的元素都放到了同一个哈希桶中了,不用再所谓的遍历找下一个空位置

当然了哈希桶也是要进行扩容的,在插入数据时也需要根据负载因子判断是否需要增容,所以我们也应该时刻存储整个哈希表中的有效元素个数,当负载因子过大时就应该进行哈希表的增容

template<class K, class V>
struct HashTable
{
	typedef HashNode<K, V> Node;
public:
	//...
	 
private:
	vector<Node*> _table;
	size_t _size; //存储的有效数据个数
};

💦数据插入

步骤如下:

  1. 去重,如果有相同的值在哈希表中,则插入失败
  2. 判断是否需要扩容:哈希表为0、负载因子过大
  3. 插入数据
  4. 有效元素个数++

其中哈希表中的调整方式

  • 若哈希表的大小为0,则将哈希表的初始大小设置为10
  • 如果哈希表中负载因子等于1,则先创建一个新的表,遍历旧表,把节点都统统转移到新表上,最后交换两个表

注意:此处我们没有复用插入,是因为我们可以使用原本节点来对新的哈希表进行复制,这样就可以节省了新哈希表中的插入的节点了

bool Insert(const pair<K, V>& kv)
{
	//去重
	if (Find(kv.first))
		return false;

	//负载因子到1就扩容
	if (_size == _table.size())
	{
		size_t newsize = _table.size == 0 ? 10 : 2 * _table.size();
		vector<Node*> newTable;
		newTable.resize(newsize);

		//旧表节点移动映射到新表中
		for (size_t i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			while (cur)
			{
				Node* next = cur->_next; //记录cur的下一个节点

				size_t hashi = cur->_kv.first % newTable.size();//计算哈希地址
				//头插
				cur->_next = newTable[hashi];
				newTable[hashi] = cur;

				cur = next;
			}
			_table[i] = nullptr;//原桶取完后置空
		}
		//交换
		_table.swap(newTable);
	}

	size_t hashi = kv.first % _table.size();
	//头插
	Node* newnode = new Node(kv);
	newnode->_next = _table[hashi]; // _table[hashi]指向的就是第一个结点
	_table[hashi] = newnode;

	++_size;
	return true;
}

💦数据查找

步骤如下:

  • 还是先判断哈希表是否为0,为0则查找失败
  • 计算出对应哈希表中的地址
  • 通过哈希地址找到了节点中的单链表,遍历单链表即可
//查找
Node* Find(const K& key)
{
	 if(_table.size() == nullptr)//哈希表为0,没得找
	 {
		 return nullptr;
	 }

	 size_t hashi = kv.first % _table.size();//招牌先算出哈希地址
	 Node* cur = _table[hashi];
	 while (cur)//直到桶为空
	 {
		 if (cur->_kv.first == key)
		 {
			 return true;
		 }

		 cur = cur->_next;
	 }
	 return nullptr;//遍历完桶,都没找到,返回空
}

💦数据删除

  1. 通过哈希函数计算出对应的哈希桶编号
  2. 遍历哈希桶,寻找待删除节点
  3. 删除节点:头删 or 中间删
  4. 有效元素个数-1

在这里插入图片描述

注意: 这里我们不调用find函数,因为是单链表,我们还要自己去找prev,所以干脆我们自己去查找好了

bool Erase(const K& key)
{
	if(_table.size() == 0)
	{
		return nullptr;
	}

	//1、通过哈希函数计算出对应的哈希桶编号hashi
	size_t hashi = key % _table.size();
	Node* prev = nullptr;
	Node* cur = _table[hashi];
	while (cur)
	{
		if (cur->_kv.first == key)
		{
			//头删
			if (prev == nullptr)
			{
				_table[hashi] = cur->_next;//将第一个结点从该哈希桶中移除
				delete cur;
			}
			else //中间删除
			{
				prev->_next = cur->_next;//将该结点从哈希桶中移除
				delete cur;
			}
			--_size;
			return true;
		}

		prev = cur;
		cur = cur->_next;
	}

	return false;
}

哈希表的大小为什么建议是素数?

其实哈希表在使用除留余数法时,为了减少哈希冲突的次数,很多地方都使用了素数来规定哈希表的大小

下面用合数(非素数)10和素数11来进行说明。

合数10的因子有:1,2,5,10。
素数11的因子有:1,11。

我们选取下面这五个序列:

间隔为1的序列:s1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
间隔为2的序列:s2 = {2, 4, 6, 8,10, 12, 14, 16, 18, 20}
间隔为5的序列:s3 = {5, 10, 15, 20, 25, 30, 35, 40,45, 50}
间隔为10的序列:s4 = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100}
间隔为11的序列:s5 = {11, 22, 33, 44, 55, 66, 77, 88, 99, 110}

对这几个序列分别放进哈希表,分别观察,不难得出他们的规律:

  1. 如果一个序列中,每个元素之间的间隔为1,那么不管哈希表的大小为几,该序列插入哈希表后都是均匀分布的
  2. 如果一个序列中,每个元素之间的间隔刚好是哈希表大小或哈希表的倍数,他们将全部产生冲突
  3. 如果一个序列中,序列的间隔恰好是哈希表大小的因子,那么哈希表的分布就会产生间隔,反之则不会。

综上所述,某个随机序列当中,每个元素之间的间隔是不定的,为了尽量减少冲突,我们就需要让哈希表的大小的因子最少,此时素数就可以视为最佳方案

实现方案

很明显如果还是采用传统的 2 倍扩容就会不符合素数大小的要求,所以我们不妨直接将素数大小存储在数组里,我们规定下面这个数组即可,其中元素近似 2 倍增长

const size_t primeList[PRIMECOUNT] =
{
	53ul, 97ul, 193ul, 389ul, 769ul,
	1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
	49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
	1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
	50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
	1610612741ul, 3221225473ul, 4294967291ul
};

在扩容时直接求取下一个素数即可:

size_t GetNextPrime(size_t prime)
{
	const int PRIMECOUNT = 28;
	size_t i = 0;
	for (i = 0; i < PRIMECOUNT; i++)
	{
		if (primeList[i] > prime)
			return primeList[i];
	}
	return primeList[i];
}

📢写在最后

我想走出浪浪山

在这里插入图片描述

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

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

相关文章

【jqgrid篇】jqgrid.setCell 改变单元格的值 改变单元格的样式设置单元格属性

setCellrowid,colname, data, class, propertiesjqGrid对象 改变单元格的值。rowid&#xff1a;当前行id&#xff1b;colname&#xff1a;列名称&#xff0c;也可以是列的位置索引&#xff0c;从0开始&#xff1b;data&#xff1a;改变单元格的内容&#xff0c;如果为空则不更 …

将GO、Pathway富集结果整合在一张高颜值圆圈图上

富集分析是生物医学论文中非常常见的一类分析&#xff0c;例如GO富集分析&#xff0c;Pathway富集分析等。其结果一般包括以下几个要素&#xff1a;1&#xff0c;名字&#xff08;GO term或者KEGG description&#xff09;&#xff1b;2&#xff0c;该名字所包含的基因数目&…

400G数据中心短距离传输方案:400G QSFP-DD SR8光模块

随着更快、更高可靠性的网络需求增加&#xff0c;400G将是下一代骨干网升级和新建设的方向。400G光模块在构建400G网络系统中起着至关重要的作用。前面我们为大家介绍了短距离单模应用的400G QSFP-DD DR4光模块&#xff0c;本期文章&#xff0c;我们一起来了解一下短距离多模光…

自定义类型:结构体,枚举,联合(2)

TIPS 1. 类型的定义可以考虑放在头文件里头。 2. 一个汉字存储的时候占两个字节空间 3. 关于结构体变量初始化的一些细节 4. 关于结构体内存对齐的补充 1. 2. S1和S2类型的成员一模一样&#xff0c;但是S1和S2所占空间的大小有了一些区别。 3. 这两个结构体类型成员都…

【Linux】六、Linux 基础IO(一)|重谈文件|C语言文件操作|操作系统文件操作(系统文件I/O)|文件描述符

目录 一、重谈文件 二、C语言文件操作 2.1 重谈C语言文件操作 2.2 补充细节 三、操作系统文件操作&#xff08;系统文件I/O&#xff09; 3.1 文件相关系统调用&#xff1a;close 3.2 文件相关系统调用&#xff1a;open 3.2.1 open 的第二个参数 flags 3.2.2 open 的第…

解决跨微服务调用token共享问题

场景描述 使用jeecg搭建SpringCloud微服务系统模块&#xff0c;各个系统模块单独创建了拦截器进行权限校验。结果发现跨微服务调用存在鉴权失败问题。不能正常跨微服务调用。 原因描述 单个微服务鉴权拦截器。 package org.jeecg.modules.taxation.inerceptor;import org.s…

【MySQL】MySQL单表操作

序号系列文章2【MySQL】MySQL基本操作详解3【MySQL】MySQL基本数据类型4【MySQL】MySQL表的七大约束5【MySQL】字符集与校对集详解文章目录MySQL单表操作1&#xff0c;数据操作1.1&#xff0c;复制表结构和数据1.2&#xff0c;解决主键冲突1.3&#xff0c;清空数据1.4&#xff…

二叉树详解(概念+遍历实现)

一、基本概念 1.最左孩子结点&#xff1a;一个结点的孩子结点中位于最左边的孩子结点。例如&#xff0c;A——B&#xff0c;B——E&#xff1b; 2.树的高度&#xff1a;树的最高层数&#xff1b; 3.路径长度&#xff1a;树中的任意两个顶点之间都存在唯一的一条路径。一条路径所…

我们这样做容器分层性能测试

前言目前闲鱼不少业务正在从H5/Weex升级到Kun&#xff08;基于W3C标准&Flutter打造的混合高性能终端容器&#xff09;&#xff0c;从测试角度来看&#xff0c;我们希望这种升级迭代对于用户体验是正向的&#xff0c;所以用好性能测试这把标准尺就显得格外重要。早期做性能保…

有什么比较好用的低代码开发平台?

国内有特色的低代码快速开发平台产品有哪些&#xff1f;这篇就来介绍下目前市面上主要的几家零代码开发平台&#xff01; 简道云、明道云、IVX这几家目前是无代码赛道的明星选手&#xff0c;在市场综合表现上名列前茅。宜创、红圈营销虽也极具潜力&#xff0c;但在市场表现力上…

Java开发技术之成为高级java工程师必须学习的三个技术

所谓的Java高级程序员往往是经验和能力的结合&#xff0c;并不是说掌握了哪几个技术就是高级程序员了&#xff0c;能否把掌握的知识运用到实际的项目中&#xff0c;并且解决了具体的问题&#xff0c;这个才是衡量一个Java程序员的标准。 那么对于一名Java程序员来说&#xff0…

Java项目:房屋租赁系统设计和实现(java+ssm+mysql+spring+jsp)

源码获取&#xff1a;博客首页 "资源" 里下载&#xff01; 主要功能描述&#xff1a; 1.登录管理&#xff1a;主要有管理员登录和租客登录 2.房源列表以及添加房源功能&#xff1a; 3.租赁合同管理以及在租房源和已退租房源信息管理: 4.看房申请和退租申请管理&a…

【 java 集合】HashMap源码分析

&#x1f4cb; 个人简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是阿牛&#xff0c;全栈领域优质创作者。&#x1f61c;&#x1f4dd; 个人主页&#xff1a;馆主阿牛&#x1f525;&#x1f389; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4d…

python基础篇之列表(增删改查)

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a;lqj_本人的博客_CSDN博客-微信小程序,前端,vue领域博主lqj_本人擅长微信小程序,前端,vue,等方面的知识https://blog.csdn.net/lbcyllqj?spm1000.2115.3001.5343 哔哩哔哩欢迎关注&…

excel数据统计:三个公式提高统计工作效率

善于在工作中使用函数、公式可以提高工作效率&#xff0c;结合近期学员们遇到的问题&#xff0c;老菜鸟总结了三个非常实用的公式&#xff0c;每个公式都可以解决一类问题。学会这三个公式套路&#xff0c;就能解决日常遇到的很多麻烦事。第一类问题&#xff1a;对指定时间段的…

通过nvm 控制node的常见命令

通过nvm 控制node查看本电脑安装的node版本号切换到对应的node版本号可以查看nvm的全部命令查看node可安装的全部版本号下载对应node的版本查看本电脑安装的node版本号 nvm ls 查看本电脑安装的node版本号 切换到对应的node版本号 nvm use 版本号 切换到对应的node版本号 注意…

基于Python + Django 的密码自助平台项目(完整代码)

场景说明&#xff1a;因为本公司 AD 是早期已经在用&#xff0c;用户的个人信息不是十分全面&#xff0c;例如:用户手机号。 钉钉是后来才开始使用&#xff0c;钉钉默认是使用手机号登录。 用户自行重置密码时如果通过手机号来进行钉钉与 AD 之间的验证就行不通了。逻辑&#x…

Linux驱动开发基础_在设备树中指定中断以及在代码中获得中断

目录 1 设备树里中断节点的语法 1.1 设备树里的中断控制器 1.2 设备树里使用中断 2 设备树里中断节点的示例 3 在代码中获得中断 3.1 对于 platform_device 3.2 对于 I2C 设备、SPI 设备 3.3 调用 of_irq_get 获得中断号 3.4 对于 GPIO 1 设备树里中断节点的语法…

OVN实验----L3互通

概述 物理拓扑 如上一个实验OVN实验----L2互通 逻辑拓扑 按照上个实验OVN实验----L2互通 的操作方式&#xff0c;再配置一组容器blue&#xff0c;网段192.168.2.0/24 配置完成后可以在central上ovn-sbctl show看到如下4个绑定接口 此时&#xff0c;red和blue两个网段内是可…

EasyTrans,一个注解搞定数据翻译,减少30%SQL代码量

介绍easy trans适用于3种场景1 有userId/idCardNo(身份证号码-唯一键场景) 需要 userName&#xff0c;无需联表查询。2 有gender code 0 需要 男。3 枚举指定属性给前端亮点1 缓存支持2 跨微服务翻译支持(User和Order 是2个不同微服务&#xff0c;order里面有userId 需要userNa…