C++笔记---哈希表

news2024/10/23 22:09:23

1. 哈希的概念

哈希(hash)又称散列,是一种组织数据的方式。从译名来看,有散乱排列的意思。

本质就是通过哈希函数把关键字Key跟存储位置建立一个映射关系,查找时通过这个哈希函数计算出Key存储的位置,进行快速查找

STL中的unordered_set和unordered_map的底层就是对哈希表的封装。

图书馆中的书就类似于使用哈希的方式进行存储的,当你在图书馆找书时,你通过想找的书的类型(key)判断出其所在的分区(哈希函数计算出地址),就可以很快地将搜索范围锁定到一块区域。

在图书馆找书本来是一件大海捞针似的事,但是由于书是按分区存放的(书的类型与存放地址产生了联系),我们就可以很迅速地找到想要的书。

在数据结构中,存储位置通常是指存储数据的数组的下标。

所以,支持存储到哈希表中的key一定是整形或可以发生隐式类型转换为整形。

如果需要存储的类型不支持隐式类型转换为整形,或者在转换时会丢失大量数据,我们就需要自己实现哈希函数。

下面介绍一点学习哈希的必备概念:

1.1 直接定址法

当关键字范围比较集中时,直接定址法是最简单高效的一种哈希方式,常常会在算法题中使用到这种思想。

比如一组关键字都在[0,99]之间,那么我们开一个100个数的数组,每个关键字的值直接就是存储位置的下标。再比如一组关键字值都在[a,z]的小写字母,那么我们开一个26个数的数组,每个关键字acsii码-a ascii码就是存储位置的下标。

也就是说直接定址法本质就是用关键字计算出一个绝对位置或者相对位置。

在计数排序中,这一方法就得到了应用,在下面这道oj题中也有妙用:. - 力扣(LeetCode)

class Solution {
public:
    int firstUniqChar(string s) {
        // 每个字⺟的ascii码-'a'的ascii码作为下标映射到count数组,数组中存储出现的次数
        int count[26] = {0};
        // 统计次数
        for(auto ch : s)
        {
            count[ch-'a']++;
        } 
        for(size_t i = 0; i < s.size(); ++i)
        {
            if(count[s[i]-'a'] == 1)
            return i;
        } 
        return -1;
    }
}

直接定址法的缺点也非常明显,当关键字的范围比较分散时,就很浪费内存甚至内存不够用。

假设我们只有数据范围是[0, 9999]的N个值,我们要映射到一个M个空间的数组中(一般情况下M >= N),那么就要借助哈希函数(hash function)hf,关键字key被放到数组的hf(key)位置。

这里要注意的是hf(key)计算出的值必须在[0, M)之间。

1.2 哈希冲突

这里存在的一个问题就是,两个不同的key可能会映射到同一个位置去,这种问题我们叫做哈希冲突,或者哈希碰撞。

理想情况是找出一个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的。

所以我们尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的方案。

1.3 负载因子

假设哈希表中已经映射存储了N个值,哈希表的大小为M,那么"负载因子 = N/M",负载因子在有些地方也翻译为载荷因子/装载因子等,他的英文为load factor。

负载因子越大,哈希冲突的概率越高,空间利用率越高;负载因子越小,哈希冲突的概率越低,空间利用率越低。

2. 哈希函数

一个好的哈希函数应该让样本空间中的关键字被等概率的均匀的散列分布到哈希表的M个空间中,以减少哈希冲突,实际中却很难做到,但我们要尽量往这个方向去考量设计。

2.1 除法散列法/除留余数法

除法散列法也叫做除留余数法,顾名思义,假设哈希表的大小为M,那么通过key除以M的余数作为映射位置的下标,也就是哈希函数为:hf(key) = key % M

当使用除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等,而尽量取素数。

假如M为2^{n},在32位环境下对key进行取模运算之后,相当于key的二进制的前32-n位被舍弃了,只保留了零头。这使得原本不相同的数只需要零头相同就能计算出相同的哈希值,大大增加了冲突概率。

使用素数作为哈希表的数组空间长度是为了利用素数的数学特性来优化哈希函数的性能,减少碰撞,并提高整体的数据分布均匀性。

尽管在某些情况下,非素数的哈希表也可以表现良好,尤其是当使用高质量的哈希函数时,但素数仍然是一个经典和广泛推荐的做法。

说到使用高质量哈希函数的非素数哈希表,这里就不得不提到Java中的HashMap。

Java的HashMap采用除法散列法时就是2的整数次幂做哈希表的大小M,这样玩的话,就不用取模,而可以直接位运算,相对而言位运算比模更高效一些。但是他不是单纯的去取模,比如M是2^16次方,本质是取后16位,那么用key' = key>>16,然后把key和key' 异或的结果作为哈希值。也就是说我们映射出的值还是在[0,M)范围内,但是尽量让key所有的位都参与计算,任何一位的不同都有可能导致哈希值的不同,这样映射出的哈希值就会更加多样,均匀一些。

2.2 乘法散列法

乘法散列法的思路就是:用一个和key相关的[0,1)之间的小数与M相乘,即可的到在[0,M)之间的与key相关的哈希值。

其中,与key相关的[0,1)之间的小数通过key与一个小数常数相乘取小数部分得到。

第⼀步:用关键字 key 乘上常数 A (0<A<1),并抽取出 k*A 的小数部分。

第⼆步:后再用M乘以k*A 的小数部分,再向下取整。

即,哈希函数为"hf(key) = floor(M × ((A × key)%1.0))"。

其中floor表示对表达式进行向下取整,A∈(0,1),这里最重要的是A的值应该如何设定,Knuth(算法界大佬)认为A = (\sqrt{5} - 1) / 2 = 0.6180339887... (黄金分割点)比较好。

乘法散列法对哈希表大小M是没有要求的。

2.3 全域散列法(简单了解)

• 如果存在一个恶意的对手,他针对我们提供的散列函数,特意构造出一个发生严重冲突的数据集,比如,让所有关键字全部落入同一个位置中。这种情况是可以存在的,只要散列函数是公开且确定的,就可以实现此攻击。解决方法自然是见招拆招,给散列函数增加随机性,攻击者就无法找出确定可以导致最坏情况的数据。这种方法叫做全域散列。

• hf(key) = ((a × key + b)%P )%M,P需要选一个足够大的质数,a可以随机选[1,P-1]之间的任意整数,b可以随机选[0,P-1]之间的任意整数,这些函数构成了一个P*(P-1)组全域散列函数组。假设P=17,M=6,a = 3, b = 4, 则h34(8) = ((3 × 8 + 4)%17)%6 = 5 。

• 需要注意的是每次初始化哈希表时,随机选取全域散列函数组中的一个散列函数使用,后续增删查改都固定使用这个散列函数,否则每次哈希都是随机选一个散列函数,那么插入是一个散列函数,查找又是另一个散列函数,就会导致找不到插入的key了。

3. 哈希冲突地解决方式

哈希冲突无法被避免,我们只能尝试去解决它,常见的解决方式有两种:开放定址法和链地址法。

3.1 开放定址法

开放定址法(Open Addressing)是一种处理哈希冲突的方法,当两个或更多的键通过哈希函数映射到同一个槽(哈希地址)时,开放定址法通过在哈希表中寻找下一个空槽来解决冲突。这种方法不使用额外的存储结构,而是在哈希表内部解决冲突。

开放定址法的基本思想是,如果一个键的哈希地址所在的槽已被占用,就按照预定的策略在哈希表中寻找下一个可用的槽。

这种策略通常包括线性探测、二次探测、双重散列等多种形式。在这些策略中,线性探测是最简单的一种,它按照固定的增量顺序检查后续的槽;二次探测则使用递增的平方数作为增量;双重散列使用两个或以上的散列函数来确定探测的步长。

开放定址法的关键在于处理删除操作时的特殊情况。由于开放定址法中的槽可能会被多次探测,简单地删除一个元素可能会导致后续插入或查找操作中断开,因此通常需要设置一个特殊的标志来指示一个槽虽然被标记为删除,但仍然参与探测过程。

就好比我们通过书的类型计算出的地址是某个分区的首位,但此处已经放上了书,那么我们就可以在其后的位置(该区的下一个空位)放下这本书。在找书时依然按照相同的策略,也可以很快找到书,且冲突被化解了。

但是在依次查找书时,不能因为遇到了空槽就认为要查找的书没有被存放,而不继续向下寻找,因为空槽的位置可能在存书时存在,而在取书之前先被别人取走了。 

3.1.1 线性探测

从发生冲突的位置开始,依次线性向后探测,直到寻找到下一个没有存储数据的位置为止,如果走到哈希表尾,则回绕到哈希表头的位置。

hf(key) = hash0 = key % M ,hash0位置冲突了,则线性探测公式为:hc(key, i) = hashi = (hash0 + i) % M, i = {1, 2, 3, ..., M - 1} ,因为负载因子小于1,则最多探测M-1次,一定能找到一个存储key的位置。

线性探测的比较简单且容易实现,线性探测的问题假设,hash0位置连续冲突,hash0,hash1,hash2位置已经存储数据了,后续映射到hash0,hash1,hash2,hash3的值都会争夺hash3位置,这种现象叫做群集/堆积。下面的二次探测可以一定程度改善这个问题。

下面演示 {19,30,5,36,13,20,21,12} 等这一组值映射到M=11的表中。

h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) =10,h(12) = 1

3.1.2 二次探测 

从发生冲突的位置开始,依次左右按二次方跳跃式探测,直到寻找到下一个没有存储数据的位置为止,如果往右走到哈希表尾,则回绕到哈希表头的位置;如果往左走到哈希表头,则回绕到哈希表尾的位置。

hf(key) = hash0 = key % M , hash0位置冲突了,则二次探测公式为:hc(key, i) = hashi = (hash0 ± i2) % M, i = {1, 2, 3, ..., M/2}

二次探测当 hashi = (hash0 - i2)%M 时,当hashi<0时,需要hashi += M

下面演示 {19,30,52,63,11,22} 等这一组值映射到M=11的表中。

 h(19) = 8, h(30) = 8, h(52) = 8, h(63) = 8, h(11) = 0, h(22) = 0

 3.1.3 双重散列

第一个哈希函数计算出的值发生冲突,使用第二个哈希函数计算出一个跟key相关的偏移量值,不断往后探测,直到寻找到下一个没有存储数据的位置为止。

h1(key) = hash0 = key % M, hash0位置冲突了,则双重探测公式为:

hc(key, i) = hashi = (hash0 + i ∗ h2(key)) % M, i = {1, 2, 3, ..., M}

要求h2(key) < M 且h2(key)和M互为质数,有两种简单的取值方法:

1、当M为2整数幂时,h2(key) 从[0,M-1]任选一个奇数;

2、当M为质数时, h2(key) = key % (M - 1) + 1

保证h2(key)与M互质是因为,假如偏移量刚好是散列表大小的因数,那么在每次寻址到末尾回到开头时都会是同一个位置,导致形成一个循环,只有固定的几个地址能被寻到。

举例来说,若初始探查位置为1,偏移量为3,整个散列表大小为12,那么所能寻址的位置为{1, 4, 7, 10},寻址个数为12/gcd(12, 3) = 4

但如果h2(key)与M互质的话,每次回到开头时都不会与上一轮重复。

例如将偏移量改为5,那么所能寻址的位置为{1, 6, 11, 4, 9, 2, 7, 0, 5, 10, 3, 8},每一个位置都有可能寻到。

下面演示 {19,30,52} 等这一组值映射到M=11的表中,设 h2(key) = key%10 + 1

3.1.4 线性探测哈希表代码实现示例
static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
  53,         97,         193,       389,       769,
  1543,       3079,       6151,      12289,     24593,
  49157,      98317,      196613,    393241,    786433,
  1572869,    3145739,    6291469,   12582917,  25165843,
  50331653,   100663319,  201326611, 402653189, 805306457,
  1610612741, 3221225473, 4294967291
};

inline unsigned long __stl_next_prime(unsigned long n)
{
	const unsigned long* first = __stl_prime_list;
	const unsigned long* last = __stl_prime_list + __stl_num_primes;
	const unsigned long* pos = lower_bound(first, last, n);
	return pos == last ? *(last - 1) : *pos;
}

// 哈希函数采用除留余数法
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

// 哈希表中支持字符串的操作
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto e : key)
		{
			hash *= 131;
			hash += e;
		}

		return hash;
	}
};

template<class K>
struct Equal
{
	bool operator()(const K& key1, const K& key2)
	{
		return key1 == key2;
	}
};

// 以下采用开放定址法,即线性探测解决冲突
namespace open_address
{
	enum State
	{
		EXIST,
		EMPTY,
		DELETE
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

	template<class K, class V, class Hash = HashFunc<K>, class Compare = Equal<K>>
	class HashTable
	{
	public:
		HashTable()
			:_tables(__stl_next_prime(0))
			, _n(0)
		{}

		HashTable(const HashTable<K, V, Hash, Compare>& hashtable)
			: _tables(hashtable._tables)
			, _n(hashtable._n)
		{}

		bool Insert(const pair<K, V>& kv)
		{
			// 扩容
			if (_n == _tables.size())
			{
				HashTable<K, V, Hash> newHashTable;
				newHashTable._tables.resize(__stl_next_prime(_n + 1));
				for (size_t i = 0; i < _n; i++)
				{
					if (_tables[i]._state == EXIST)
						newHashTable.Insert(_tables[i]._kv);
				}
				swap(_tables, newHashTable._tables);
			}

			size_t pos = _hash(kv.first) % _tables.size();
			while (_tables[pos]._state == EXIST)
			{
				if (_com(_tables[pos]._kv.first, kv.first))
					return false;
				pos = (pos + 1) % _tables.size();
			}
			_tables[pos]._kv = kv;
			_tables[pos]._state = EXIST;
			++_n;
			return true;
		}

		HashData<K, V>* Find(const K& key)
		{
			size_t pos = _hash(key) % _tables.size();
			while (_tables[pos]._state != EMPTY)
			{
				if (_com(_tables[pos]._kv.first, key) && _tables[pos]._state != DELETE)
					return &_tables[pos];
				pos = (pos + 1) % _tables.size();
			}
			return nullptr;
		}

		bool Erase(const K& key)
		{
			if (HashData<K, V>* p = Find(key))
			{
				(*p)._state = DELETE;
				--_n;
			}
			return false;
		}

	private:
		vector<HashData<K, V>> _tables;
		size_t _n = 0;  // 表中存储数据个数
		Hash _hash;
		Compare _com;
	};
}

在上面的代码中,M的取值采取了STL中的实现方式,即在一个素数表中取值。

注意:扩容必须要将原表的数据重新插入新表中,因为M发生了变化,哈希函数计算出的地址也会发生变化。 

 3.2 链地址法

开放定址法中所有的元素都放到哈希表里,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储一个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成一个链表,挂在哈希表这个位置下面,链地址法也叫做拉链法或者哈希桶。

下面演示 {19,30,5,36,13,20,21,12,24,96} 等这一组值映射到M=11的表中。

h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) = 10,h(12) = 1, h(24) = 2, h(96) = 88

相比于开放地址法,链地址法不仅更加直观而且更加高效,实际当中的应用更多。

哈希桶的叫法实际上十分形象,每一个链表就是一个桶,我只需要知道把数据存到那个桶和到那个桶中去寻找需要的数据即可。

这种解决冲突的方式就与图书馆的例子更加贴合了,一个分区就是一个桶,我们在图书馆中找书,能锁定的就只是一个分区而已,但这无疑大大提高了我们找书的效率。

3.2.1 扩容时机

开放定址法中,负载因子一定小于1,表满就扩容,这是必然的。

但是在链地址法中,链表中可以存储无数的数据,负载因子没有限制。

负载因子越大,哈希冲突的概率越高,空间利用率越高;负载因子越小,哈希冲突的概率越低,空间利用率越低;stl中unordered_xxx的最大负载因子基本控制在1,大于1就扩容,我们下面实现也使用这个方式。

3.2.2 极端场景

如果极端场景下,某个桶特别长,查找效率很低怎么办?这里在Java8的HashMap中当桶的长度超过一定阀值(8)时就把链表转换成红黑树。一般情况下,不断扩容,单个桶很长的场景还是比较少的,下面我们实现就不搞这么复杂了,这个解决极端场景的思路,大家了解一下即可。

3.2.3 链地址哈希表代码实现示例
// 以下采用链地址法
namespace linked_address
{
	template<class K, class V>
	struct HashNode
	{
		pair<K, V> _kv;
		HashNode* _next;
		HashNode(const pair<K, V>& kv)
			:_kv(kv)
			, _next(nullptr)
		{}
	};

	template<class K, class V, class Hash = HashFunc<K>, class Compare = Equal<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
		typedef HashTable<K, V, Hash, Compare> Self;
	public:
		HashTable()
			:_tables(__stl_next_prime(0))
			, _n(0)
		{}

		HashTable(const Self& hashtable)
			:HashTable()
		{
			for (auto cur : hashtable._tables)
			{
				while (cur)
				{
					Insert(cur->_kv);
					cur = cur->_next;
				}
			}
		}

		Self& operator=(Self hashtable)
		{
			swap(_tables, hashtable._tables);
			swap(_n, hashtable._n);
			return *this;
		}

		~HashTable()
		{
			for (auto cur : _tables)
			{
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
			}
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;
			// 负载因子等于1,扩容
			if (_n == _tables.size())
			{
				vector<Node*> newTables(__stl_next_prime(_n + 1));
				// 原结点依次插入新表
				for (size_t i = 0; i < _n; i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t pos = _hash(cur->_kv.first) % _tables.size();
						cur->_next = newTables[pos];
						newTables[pos] = cur;
						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newTables);
			}

			size_t pos = _hash(kv.first) % _tables.size();
			Node* newnode = new Node(kv);
			newnode->_next = _tables[pos];
			_tables[pos] = newnode;
			++_n;
			return false;
		}

		Node* Find(const K& key)
		{
			size_t pos = _hash(key) % _tables.size();
			Node* cur = _tables[pos];
			while (cur && !_com(cur->_kv.first, key)) { cur = cur->_next; }
			return cur;
		}

		bool Erase(const K& key)
		{
			size_t pos = _hash(key) % _tables.size();
			if (_tables[pos] == nullptr)
				return false;
			Node* del = nullptr;
			if (_com(_tables[pos]->_kv.first, key))
			{
				del = _tables[pos];
				_tables[pos] = _tables[pos]->_next;
			}
			else
			{
				Node* parent = _tables[pos];
				while (parent->_next && !_com(parent->_next->_kv.first, key))
				{
					parent = parent->_next;
				}

				if (parent->_next == nullptr)
					return false;

				del = parent->_next;
				parent->_next = parent->_next->_next;
			}
			delete del;
			--_n;
			return true;
		}

	private:
		vector<Node*> _tables;
		size_t _n = 0;  // 表中存储数据个数
		Hash _hash;
		Compare _com;
	};
}

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

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

相关文章

推荐IDE中实用AI编程插件,目前无限次使用

插件介绍 一款字节跳动推出的“基于豆包大模型的智能开发工具” 以vscode介绍【pycharm等都可以啊】&#xff0c;这个插件提供智能补全、智能预测、智能问答等能力&#xff0c;节省开发时间 直接在IDE中使用&#xff0c;就不用在网页中来回切换了 感觉还可以&#xff0c;响应速…

Excel表格如何修改“打开密码”,简单几步,轻松搞定

在保护Excel文件的安全性时&#xff0c;设置打开密码是常见且有效的方式。然而&#xff0c;有时我们需要修改已经设置的打开密码&#xff0c;以确保文件安全性或更新密码信息。今天小编来分享一下修改Excel文件打开密码的方法&#xff0c;操作简单&#xff0c;一起来看看吧&…

设置OpenAI API的环境变量

获取openai API 密钥 https://platform.openai.com/api-keys 设置环境变量 为什么不在代码中直接写入&#xff0c;而是设置环境变量&#xff1f; 安全性&#xff1a;将 API 密钥存储在环境变量中&#xff0c;而不是直接写在代码中&#xff0c;可以降低泄露密钥的风险。易于…

第二期:第15节,beep 大海

首先是 代码的编写&#xff1a; 里面已经有了解释了。 1 /*2 * main.c3 *4 * Created on: 2023-3-85 * Author: pengdan6 */7 #include "exynos_4412.h"89 void delay_ms(unsigned int num)10 {11 int i,j;12 for(inum; i>0;i--)13 …

『完整代码』坐骑召唤

创建一个按钮 作为召唤/消失坐骑的开关 将预制体放入指定文件夹 命名为Mount01 创建脚本并编写&#xff1a;CallMount.cs using UnityEngine; using UnityEngine.UI; public class CallMount : MonoBehaviour{public Button callBtn;GameObject mountPrefab;GameObject mountIn…

[项目详解][boost搜索引擎#1] 概述 | 去标签 | 数据清洗 | scp

目录 一、前言 二、项目的相关背景 三、搜索引擎的宏观原理 四、搜索引擎技术栈和项目环境 五、正排索引 VS 倒排索引--原理 正排索引 分词 倒排索引 六、编写数据去除标签和数据清洗模块 Parser 1.数据准备 parser 编码 1.枚举文件 EnumFile 2.去标签ParseHtml(…

使用Vscode配置ftp连接远程服务器(上传本地文件)

1.安装插件 扩展商店搜sftp,点击进行安装。 2.配置json文件 crtl+shift+p 输入ftp配置命令 sftp:config {"name": "My Server", //设置名字"host": "localhost"</

android app执行shell命令视频课程补充android 10/11适配-千里马android

(https://blog.csdn.net/learnframework/article/details/120103471) https://blog.csdn.net/learnframework/article/details/120103471 hi&#xff0c;有学员在学习跨进程通信专题课程时候&#xff0c;在实战app执行一个shell命令的项目时候&#xff0c;对课程本身的android …

JVM、字节码文件介绍

目录 初识JVM 什么是JVM JVM的三大核心功能 JVM的组成 字节码文件的组成 基础信息 Magic魔数 主副版本号 其它基础信息 常量池 字段 方法 属性 字节码常用工具 javap jclasslib插件 阿里Arthas 初识JVM 什么是JVM JVM的三大核心功能 1. 解释和运行虚拟机指…

js实现点击随机点名效果

代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </he…

uniapp+vue3+uview-plus修改默认样式

最近使用uniappvue3uview-plus开发微信小程序中&#xff0c;使用uview-plus自定义底部导航栏tabbar时&#xff0c;遇到修改默认样式不生效问题 使用传统的 ::v-deep、:deep、::v-deep&#xff0c;或者style标签中去掉scoped也是无效的&#xff0c;有好的方案欢迎交流&#xff…

深入剖析 C 与 C++ 动态内存管理之术

亲爱的读者朋友们&#x1f603;&#xff0c;此文开启知识盛宴与思想碰撞&#x1f389;。 快来参与讨论&#x1f4ac;&#xff0c;点赞&#x1f44d;、收藏⭐、分享&#x1f970;&#xff0c;共创活力社区。 &#x1f525;&#x1f525;&#x1f525;【C】进阶&#xff1a;类相关…

Java 二分查找算法详解及通用实现模板案例示范

1. 引言 二分查找&#xff08;Binary Search&#xff09;是一种常见的搜索算法&#xff0c;专门用于在有序数组或列表中查找元素的位置。它通过每次将搜索空间缩小一半&#xff0c;从而极大地提高了查找效率。相比于线性查找算法&#xff0c;二分查找的时间复杂度为 O(log n)&…

利用Docker搭建一套Mycat2+MySQL8一主一从、读写分离的最简单集群(保姆教程)

文章目录 1、Mycat介绍1.1、mycat简介1.2、mycat重要概念1.3、Mycat1.x与Mycat2功能对比1.2、主从复制原理 2、前提准备3、集群规划4、安装和配置mysql主从复制4.1、master节点安装mysql8容器4.2、slave节点安装mysql8容器4.2、配置主从复制4.3、测试主从复制配置 5、安装mycat…

【TDA】持续同调的矢量化方法

Topological deep learning: a review of an emerging paradigm 持续同调与深度学习协同下的3D点云分类_苏潇 Applications of Topology to Data Analysis Computational Topology: An Introduction 持续同调对于特征的方式有条形码PB和持续图表PD两种形式,它们包含了持续同调的…

Qt开发技巧(十八):新窗口控件用智能指针,将一些配置类变量封装起来,Qt窗体的Z序叠放,子窗体的释放,Qt中的事件发送,Qt的全局头文件

继续讲一些Qt开发中的技巧操作&#xff1a; 1.新窗口控件用智能指针 通过对Qt自带Examples的源码研究你会发现&#xff0c;越往后的版本&#xff0c;越喜欢用智能指针QScopedPointer来定义对象&#xff0c;这样有个好处就是用的地方只管new就行&#xff0c;一直new下去&#xf…

2025 年最佳的 Retool 开源替代方案

自 2017 年推出以来&#xff0c;Retool 已迅速成为开发者的热门选择。 Retool 的出现&#xff0c;填补了当时企业在快速构建内部工具上的空白。传统的应用开发往往需要耗费大量时间和资源&#xff0c;尤其是对于定制的内部业务应用。而 Retool 提供了一个灵活的平台&#xff0…

element设置时间和日期框早于现在的时间和日期禁用

效果: 今日此时此刻之前的日期、时间禁止选用&#xff0c;切换日期和时间为“2024-10-19 00:00:00"&#xff0c;再切换为”2024-10-18 00:00:00"时&#xff0c; 会给form.time默认赋值为今日此时此刻&#xff08;日期时间少于今日此时此刻则重新赋值&#xff09; 安…

datax连接池泄漏问题排查及解决

1、问题描述 频繁调用datax服务&#xff08;从oracle同步到mysql&#xff09;出现报错&#xff0c;获取不到连接 oracle读取时报错信息 "errorMessage": "Code:[DBUtilErrorCode-10], Description:[连接数据库失败. 请检查您的 账号、密码、数据库名称、IP、…

print_hex_dump调试内核,嘎嘎香

本文首发于我的公众号 皮塞作坊 专注于干货分享&#xff0c;号欢迎大家关注,二维码文末可以扫。 公众号: 使用print_hex_dump调试内核/驱动&#xff0c;太香了 最近在验证芯片功能的过程中发现了一个好用的内核调试接口&#xff0c;print_hex_dump&#xff0c;除了直接打印16…