当前位置: 首页 > news >正文

哈希表以及哈希表的底层结构 --- 万字解说【c++11】

目录

哈希概念

哈希冲突

哈希函数

解决哈希冲突的常用方法

        闭散列

                版本1:闭散列(开放定址法)

        开散列

                版本2:开散列(开链法/哈希桶)

版本3:设计仿函数支持string


哈希概念

        顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较顺序查找时间复杂度为O(N),平衡树中为树的高度,即O($log_2 N$),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素
        
        如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素
当向该结构中:
  • 插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)。用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快

  • 示例:数据集合{1,7,6,4,5,9};
        哈希函数设置为:hash(key) = key % capacity;    capacity == 10 为存储元素底层空间总的大小。 (注: hash(1) = 1%10 = 1   hash(7) = 7%10 = 7......以此类推)


哈希冲突

按照上述哈希方式,向集合中插入元素77,会出现什么问题?
我们会发现不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞


哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理
哈希函数设计原则
哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
哈希函数计算出来的地址能均匀分布在整个空间中
常见哈希函数(这里只介绍两种,其他方法可以自行检索)

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

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
  • 示例:字符串中的第一个唯一字符
思路:哈希映射的思路,把26个字母映射存储在一个数组中,因为是字符,存在ascii码表中是 97 ~ 122,所以存储的过程中要减去 ‘a’ ;再验证数组中对应是位置是不是1即可。(数组空间是27,因为字符串后面会有\0的存在 )
class Solution {
public:int firstUniqChar(string s) {int count[27] = {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;}
};

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

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突  (其他方法: 平方取中法、 折叠法、 随机数法、 数学分析法)


解决哈希冲突的常用方法

        闭散列

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

那如何寻找下一个空位置?

1. 线性探测

比如上述哈希冲突中的场景,现在需要插入元素77,先通过哈希函数计算哈希地址,hashAddr为7,因此77理论上应该插在该位置,但是该位置已经放了值为7的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
  • 插入
通过哈希函数获取待插入元素在哈希表中的位置
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素
  • 删除
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。比如删除元素7,如果直接删除掉,77查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素
// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};
 

版本1:闭散列(开放定址法)

定义:

#pragma onceenum State
{EMPTY,EXIST,DELETE
};template<class K>
class hash_data
{
public:K _key; //数据State _state = EMPTY;   // 状态
};template<class K>
class hash_table
{
public://闭散列/开放定址法bool insert(const K& key){//映射起始位置size_t hashi = key % _tables.capacity(); //线性探测size_t i = 1;while(_tables[hashi]._state == EXIST){hashi += i;hashi %= _tables.size();i++;}_tables[hashi]._key = key;_tables[hashi]._state = EXIST;_count++;}private:vector<hash_data<K>> _tables;size_t _count = 0;   //记录存储的个数
};

  • 问题1:这里能使用capacity来确定位置嘛???是选择①还是②?

答案是②,因为熟悉vector的都知道,vector是用 size()来进行边界判断的,如果取模的值比size()要大,会报断言错误,所以这里不能 %capacity()。

  • 问题2:这里 %size() 就没有问题了吗?也不尽然,因为存在其他问题,
  • 如果capacity()满了怎么办?
  • 如果size()为0怎么办?
  • 如果size()变了那么映射关系是不是也需要改变???

所以这里需要补充一个相关知识:负载因子

负载因子也被称为载荷因子,表示的是存储量的百分比

散列表的载荷因子定义为: α =填入表中的元素个数 / 散列表的长度


        α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之,α越小,标明填入表中的元素越少,产生冲突的可能性就越小。实际上,散列表的平均查找长度是载荷因子α的函数,只是不同处理冲突的方法有不同的函数。


        对于开放定址法,荷载因子是特别重要因素,应严格限制在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中〈cachemissing)按照指数曲线上升。因此,一些采用开放定址法的hash库,如Java的系统库限制了荷载因子为0.75,超过此值将resize散列表。

所以当我们调用insert就需要扩容:这样可以吗?

		//负载因子超过0.7就扩容if (_count * 10 / _tables.size() >= 7){_tables.resize(_tables.capacity() * 2);}

上述代码依旧有问题,因为存在capacity为0的情况,也存在size()没有改变的情况。所以需要优化:

		if (_tables.size() == 0 || _count * 10 / _tables.size() >= 7){size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;_tables.resize(newsize);}

        这样是否还有问题呢?依然存在,因为size()发生了改变,而未扩容之前插入的数据,%的是未改变的size(), 而接下来插入数据%是已经改变的size(),这样,同一张哈希表,映射的关系居然不同,肯定是错误的,所以,扩容是需要取到之前映射的值重新进行映射。如图:

        我们发现,表2是不对的,因为直接扩容之后,再次插入的值%都是20,而之前的值%都是10,直接的问题就是以后进行查找的话,是怎么都找不到的,因为查找要找到映射位置,也要%,那么谁知道%是多少呢?

所以扩容是需要像表3一样,取上面的值重新进行映射,而且我们发现,映射之后的存储位置是有些不同的。所以,哈希表扩容的代价是有些高的。

代码如下:

			//负载因子超过0.7就扩容if (_tables.size() == 0 || _count * 10 / _tables.size() >= 7){size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;//方法1.vector<hash_data<K>> newtables(newsize);for (auto& data : _tables){size_t hashi = key % _tables.size();if (data._state == EMPTY){//线性探测size_t i = 1;size_t index = hashi;while (_tables[index]._state == EXIST){index = index + i;index = index %  _tables.size();i++;}_tables[index]._key = key;_tables[index]._state = EXIST;_count++;}}_tables.swap(newtables);}

        这里的思路是复用,因为需要重新插入,所以重新走了一边线性探测,当然,也可以把线性探测封装成一个函数进行调用,不过这里介绍另一个逻辑,当然也是和复用相关:

                hash_table<K> newht;newht._tables.resize(newsize);for (auto& data : _tables){if (data._state == EMPTY){newht.insert(data._key);}}_tables.swap(newht._tables);

我们可以新建一个哈希对象,然后去调用insert,进行内部复用,然后交换指针即可。

现在我们就可以丰富一下这个类,如下:闭散列

                                                                                                              hash_table.h

#pragma once
#include <vector>
namespace dwr
{enum State{EMPTY,EXIST,DELETE};template<class K>struct hash_data{K _key; //数据State _state = EMPTY;   // 状态};template<class K>class hash_table{public://闭散列/开放定址法bool insert(const K& key){if ( _tables.size() != 0 && find(key)) //去重{return false;}//负载因子超过0.7就扩容if (_tables.size() == 0 || _count * 10 / _tables.size() >= 7){size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;//方法1.//vector<hash_data<K>> newtables(newsize);//for (auto& data : _tables)//{//	size_t hashi = key % _tables.size();//	if (data._state == EMPTY)//	{//		//线性探测//		size_t i = 1;//		size_t index = hashi;//		while (_tables[index]._state == EXIST)//		{//			index = index + i;//			index = index %  _tables.size();//			i++;//		}//		_tables[index]._key = key;//		_tables[index]._state = EXIST;//		_count++;//	}//}//_tables.swap(newtables);//方法2.hash_table<K> newht;newht._tables.resize(newsize);for (auto& data : _tables){if (data._state == EMPTY){newht.insert(data._key);}}_tables.swap(newht._tables);}//映射起始位置size_t hashi = key % _tables.size();//线性探测size_t i = 1;size_t index = hashi;while (_tables[index]._state == EXIST){index = index + i ;index = index %  _tables.size();i++;}_tables[index]._key = key;_tables[index]._state = EXIST;_count++;return true;}hash_data<K>* find(const K& key){//映射起始位置size_t hashi = key % _tables.size();//线性探测size_t i = 1;size_t index = hashi;while (_tables[index]._state != EMPTY){if (_tables[index]._state == EXIST&& _tables[index]._key== key){return& _tables[index];}index = index + i;index = index % _tables.size();++i;}return nullptr;		}bool erase(const K& key){hash_data<K>* ret = find(key);if (ret){//伪删除ret->_state = DELETE;_count--;}else{return false;}}private:vector< hash_data<K> > _tables;size_t _count = 0;   //记录存储的个数};}void test_hashtable1()
{int arr[] = { 3, 33, 2, 13,5, 12, 1002 };dwr::hash_table<int> ht;for (auto& kv : arr){  ht.insert(kv);}if (ht.find(13)){cout << "13在" << endl;}else{cout << "13不再" << endl;}ht.erase(13);if (ht.find(13)){cout << "13在" << endl;}else{cout << "13不再" << endl;}
}

但是其实上述代码有两个隐藏的问题,在find内:这里容易引起除0错误,因为_tables.size()可能为0,所以要加一个判断。

size_t hashi = key % _tables.size();

其二,这里:可能会出现死循环,因为假设哈希表内的存储情况只有EXIST和DELETE,那么就会出现,所以也需要优化

while (_tables[index]._state != EMPTY)

优化如下:

        hash_data<K>* find(const K& key){//加判断,防止除0错误if (_tables.size() == 0){return nullptr;}//映射起始位置size_t hashi = key % _tables.size();//线性探测size_t i = 1;size_t index = hashi;while (_tables[index]._state != EMPTY){if (_tables[index]._state == EXIST&& _tables[index]._key== key){return& _tables[index];}index = index + i;index = index % _tables.size();++i;//加个判断,防止死循环 - 如果相等,证明寻找一圈也未找到,因为hashi是起始位置if (index == hashi){break;}}return nullptr;		}
        线性探测优点:实现非常简单
        线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。如何缓解呢?
2. 二次探测
  •         线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:$H_i$ = ($H_0$ + $i^2$ )% m, 或者:$H_i$ = ($H_0$ - $i^2$ )% m。其中:i =1,2,3…, $H_0$是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
  •         研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
  • 因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。所以一般情况下,我们更偏向于使用开散列(哈希桶)来解决哈希冲突。


版本2:开散列(开链法/哈希桶)

开散列概念

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

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

定义:

	template<class K>struct hash_node{hash_node<K>* _next;K _key;hash_node(const K& key):_next(nullptr),_key(key){}};template<class K>class hash_table{typedef hash_node<K> _node;public://代码实现private:vector<_node*> _tables;size_t _count = 0; // 记录个数};

代码实现:如下图

		bool insert(const K& key){//计算映射起始位置size_t hashi = key % _tables.size();_node* newnode = new _node(key);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_count;return true;}

开散列增容

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

如图,这样扩容会导致后面数据的映射关系与扩容之前插入数据的映射关系不同,所以需要重新开辟空间进行扩容

但是进行重新插入的方法损耗代价也很高,有没有其他的方法进行扩容呢?

方法①:开辟新空间,创建7个节点,再重新插入到新空间,最后释放原来的7个节点(损耗较大)
方法②:开辟新空间,原节点重新计算位置,挪到到新空间(不用新建节点,不用释放原来节点)
       bool insert(const K& key){//负载因子等于1即进行扩容if (_tables.size() == 0 || _count == _tables.size()){size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;vector<_node*> newtables(newsize, nullptr); // 指针数组for (auto& cur : _tables){while (cur){_node* next = cur->_next;size_t hashi = key % _tables.size();  //重新计算映射位置cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}}_tables.swap(newtables);}//计算映射起始位置size_t hashi = key % _tables.size();_node* newnode = new _node(key);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_count;return true;}

其中,这一句代码表示或者可以更换为:因为他代表着数组中的数据位置
for (auto& cur : _tables)
				for (size_t i = 0; i < _tables.size(); ++i){_node* cur = _tables[i];//.....}
通过调试窗口,也可以确认以上程序:
现在可以尝试丰富一下这个类: 开散列
     hash_table.h
namespace dwq
{template<class K>struct hash_node{hash_node<K>* _next;K _key;hash_node(const K& key):_next(nullptr),_key(key){}};template<class K>class hash_table{typedef hash_node<K> _node;public:bool insert(const K& key){//负载因子等于1即进行扩容if (_tables.size() == 0 || _count == _tables.size()){size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;vector<_node*> newtables(newsize, nullptr); // 指针数组for (auto& cur : _tables){while (cur){_node* next = cur->_next;size_t hashi = key % _tables.size();  //重新计算映射位置cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}}_tables.swap(newtables);}//计算映射起始位置size_t hashi = key % _tables.size();_node* newnode = new _node(key);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_count;return true;}_node* find(const K& key){if (_tables.size() == 0)return nullptr;size_t hashi = key % _tables.size();_node* cur = _tables[hashi];while (cur){if (cur->_key == key){return cur;}cur = cur->_next;}return nullptr;}bool erase(const K& key){size_t hashi = key % _tables.size();_node* cur = _tables[hashi];_node* prev = nullptr;while (cur){if (cur->_key == key){//删除的情况分两种 : a.删除的是头节点   b.删除的是中间节点if (prev == nullptr){//prev为空,代表是是删除的是头节点_tables[hashi] = cur->_next;}else{//反之删除的是中间节点prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;}private:vector<_node*> _tables;size_t _count = 0; // 记录个数};
}void test_hashtable2()
{int arr[] = { 3, 33, 2, 13, 5, 12, 1002 };dwq::hash_table<int> ht;for (auto& kv : arr){ht.insert(kv);}if (ht.find(13)){cout << "13在" << endl;}else{cout << "13不再" << endl;}ht.erase(13);if (ht.find(13)){cout << "13在" << endl;}else{cout << "13不再" << endl;}}

需要注意的是这里的删除逻辑,因为哈希桶是vector 和forward_list 组成,所以需要注意节点的链接与释放。

并且,删除节点需要分情况讨论,看是为头节点还是中间链接的节点。如图:

开散列与闭散列比较

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

版本3:设计仿函数支持string

        事实上我们会发现,使用哈希表大多数映射的可能不是整型,而是字符串,此时,而上述的哈希桶是不支持字符串的,上述的哈希桶是只支持整型,并且如果是浮点型也是不支持的,如果传整型或浮点型会报错,如下:

所以这里需要设计一些仿函数:

template<class K>
struct hash_func
{size_t operator()(const K& key){return key;}
};
	//添加模板参数支持仿函数template<class K, class hash = hash_func<K>>class hash_table

 所有需要取模的位置都加上仿函数:

hash ht;
size_t hashi = ht(key) % _tables.size();

那这样,这里也就支持了浮点型与整型,那么string类型怎么办?

所有,这里需要新增仿函数:

template<class K>
struct hash_string
{size_t operator()(const string& s){return s[0];}
};

但是这个仿函数有问题,因为如果字符串为空或者这里字符串的首字母相同,就会报错,所有需要进一步优化:


struct hash_str
{size_t operator()(const string& s){size_t hash_count = 0;for (auto& ch : s){hash_count = hash_count + ch;}return hash_count;}
};

这样可以了吗?理论上大致ok,但是如果字符串相同,但是字符串的顺序不同,就会出现问题,如下:

这里库或者是网络给出了经典的一些哈希算法,这里采用BKDR哈希算法

struct hash_str
{size_t operator()(const string& s){size_t hash_count = 0;for (auto& ch : s){//BKDRHashhash_count += ch;hash_count *= 31;}return hash_count;}
};

可以看到,这里虽然结果相近,但是却不同。

写到这里是不是以为结束啦?不,还没有,因为这里需要显示去传参,不然是没办法使用的,有没有更好的办法?

hash_table<string, hash_str> ht;

答案是有的,使用模板特化  如下:

template<>
struct hash_func<string>
{size_t operator()(const string& s){size_t hash_count = 0;for (auto& ch : s){//BKDRHashhash_count += ch;hash_count *= 31;}return hash_count;}
};

效果:

附上源码:

                                                                                                            hash_table.h

//转换成整型进行比较template<class K>
struct hash_func
{size_t operator()(const K& key){return key;}
};template<>
struct hash_func<string>
{size_t operator()(const string& s){size_t hash_count = 0;for (auto& ch : s){//BKDRHashhash_count += ch;hash_count *= 31;}return hash_count;}
};namespace dwq
{template<class K>struct hash_node{hash_node<K>* _next;K _key;hash_node(const K& key):_next(nullptr),_key(key){}};//添加模板参数支持仿函数template<class K, class hash = hash_func<K>>class hash_table{typedef hash_node<K> _node;public:bool insert(const K& key){hash ht;//负载因子等于1即进行扩容if (_tables.size() == 0 || _count == _tables.size()){size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;vector<_node*> newtables(newsize, nullptr); // 指针数组for (auto& cur : _tables){while (cur){_node* next = cur->_next;size_t hashi = ht(key) % _tables.size();  //重新计算映射位置cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}}_tables.swap(newtables);}//计算映射起始位置size_t hashi = ht(key) % _tables.size();_node* newnode = new _node(key);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_count;return true;}_node* find(const K& key){hash ht;if (_tables.size() == 0)return nullptr;size_t hashi = ht(key) % _tables.size();_node* cur = _tables[hashi];while (cur){if (cur->_key == key){return cur;}cur = cur->_next;}return nullptr;}bool erase(const K& key){hash ht;size_t hashi = ht(key) % _tables.size();_node* cur = _tables[hashi];_node* prev = nullptr;while (cur){if (cur->_key == key){//删除的情况分两种 : a.删除的是头节点   b.删除的是中间节点if (prev == nullptr){//prev为空,代表是是删除的是头节点_tables[hashi] = cur->_next;}else{//反之删除的是中间节点prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;}private:vector<_node*> _tables;size_t _count = 0; // 记录个数};
}void test_hashtable2()
{int arr[] = { 3, 33, 2, 13, 5, 12, 1002 };dwq::hash_table<int> ht;for (auto& kv : arr){ht.insert(kv);}if (ht.find(13)){cout << "13在" << endl;}else{cout << "13不再" << endl;}ht.erase(13);if (ht.find(13)){cout << "13在" << endl;}else{cout << "13不再" << endl;}}void test_hashtable3()
{//dwq::hash_table<double> ht;//ht.insert(12.34);//dwq::hash_table<string> ht;//ht.insert("sort");//hash_str hashstr;//cout << hashstr("abcd") << endl;//cout << hashstr("acdb") << endl;//cout << hashstr("cdab") << endl;//cout << hashstr("bcad") << endl;//cout << hashstr("cat") << endl;//cout << hashstr("tca") << endl;dwq::hash_table<string> ht;ht.insert("string");ht.insert("left");ht.insert("right");ht.insert("");
}

关于涉及到的知识:

模板特化

vector

后面会跟一期使用哈希桶封装unordered_set or unordered_map, 底层结构就是上述的哈希桶。


以上仅代表个人观点,仅供参考。

相关文章:

  • 第十四届蓝桥杯JavaA组省赛真题 - 特殊日期
  • python函数参数中独立星号*的作用
  • 小狐狸JSON-RPC:钱包连接,断开连接,监听地址改变
  • es6的核心语法
  • OpenGL的MVP矩阵理解
  • 专业130+总分410+西南交通大学924信号与系统考研经验西南交大电子信息通信工程,真题,大纲,参考书。
  • 【概率基础】从概率角度去解释回归和分类的主要区别是什么?
  • 文本文件操作
  • 设计模式 —— 设计原则
  • 前端-包管理器
  • MR混合现实情景实训教学系统在军事演练课堂中的教学应用
  • Python+Django+Yolov5路面墙体桥梁裂缝特征检测识别html网页前后端
  • Java设计模式—备忘录模式(快照模式)
  • 【问题分析】InputDispatcher无焦点窗口ANR问题【Android 14】
  • 探索SOCKS5代理、代理IP、HTTP与网络安全
  • 【技术性】Search知识
  • 【跃迁之路】【733天】程序员高效学习方法论探索系列(实验阶段490-2019.2.23)...
  • Brief introduction of how to 'Call, Apply and Bind'
  • canvas实际项目操作,包含:线条,圆形,扇形,图片绘制,图片圆角遮罩,矩形,弧形文字...
  • Docker: 容器互访的三种方式
  • docker容器内的网络抓包
  • KMP算法及优化
  • MySQL数据库运维之数据恢复
  • nginx 负载服务器优化
  • php的插入排序,通过双层for循环
  • SQLServer之创建数据库快照
  • -- 查询加强-- 使用如何where子句进行筛选,% _ like的使用
  • 记一次和乔布斯合作最难忘的经历
  • 世界编程语言排行榜2008年06月(ActionScript 挺进20强)
  • 译自由幺半群
  • AI算硅基生命吗,为什么?
  • Java总结 - String - 这篇请使劲喷我
  • SAP CRM里Lead通过工作流自动创建Opportunity的原理讲解 ...
  • 从如何停掉 Promise 链说起
  • 东超科技获得千万级Pre-A轮融资,投资方为中科创星 ...
  • 数据库巡检项
  • #### go map 底层结构 ####
  • ###STL(标准模板库)
  • #我与Java虚拟机的故事#连载04:一本让自己没面子的书
  • (1)(1.19) TeraRanger One/EVO测距仪
  • (Redis使用系列) Springboot 整合Redisson 实现分布式锁 七
  • (附源码)springboot“微印象”在线打印预约系统 毕业设计 061642
  • (附源码)springboot宠物管理系统 毕业设计 121654
  • (循环依赖问题)学习spring的第九天
  • (一)Linux+Windows下安装ffmpeg
  • (已解决)什么是vue导航守卫
  • (转)h264中avc和flv数据的解析
  • (转)http-server应用
  • (轉貼) 蒼井そら挑戰筋肉擂台 (Misc)
  • .[hudsonL@cock.li].mkp勒索病毒数据怎么处理|数据解密恢复
  • .NET 除了用 Task 之外,如何自己写一个可以 await 的对象?
  • .NET 使用 ILMerge 合并多个程序集,避免引入额外的依赖
  • .NET 中 GetHashCode 的哈希值有多大概率会相同(哈希碰撞)
  • .NET/C# 使用 SpanT 为字符串处理提升性能
  • .NET的数据绑定