哈希及其模拟实现

1.哈希的概念

顺序结构以及平衡树中,元素的关键码与其存储位置之间没有对应的关系。因此,在查找一个元素时,必须要经过关键码的多次比较。顺序查找的时间复杂度为O(N),平衡树中为树的高度,即O(log_2 N),搜索的效率取决于搜索过程中元素的比较次数。理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(HashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

1.1 哈希思想

哈希(Hash)是一种映射思想,规定存在一个唯一的哈希值键值之间建立映射关系,由这种思想而构成的数据结构称为哈希表

哈希表中查找数据的时间复杂度可以做到O(1),这是非常高效的,比AVL树还要快。

哈希表中插入和查找数据的具体操作如下:
插入数据:根据当前待插入的元素的键值,计算出哈希值,并存入到相应的位置中。
查找元素:根据待查找元素的键值,计算出哈希值,判断对应的位置中存储的值是否与键值相等。

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

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

显然,这个哈希表并没有把所有的位置都填满,数据分布无序且分散。因此,哈希表又称为散列表

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

1.2 哈希冲突

对于两个数据元素的关键字Ki和Kj(i!=j),有Ki!=Kj,但有Hash(Ki)==Hash(Kj),即不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或者哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。发生哈希冲突该如何处理呢?
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:
1、哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表运允许有m个地址时,其值域必须在0到m-1之间;
2、哈希函数计算出来的地址能均匀分布在整个空间中;
3、哈希函数应该比较简单。

1.3 常见的哈希函数

1.3.1 直接定址法--(常用)

取关键字的某个线性函数为单列地址:Hash(Key)=A*Key+B。
优点:简单、均匀;
缺点:需要事先知道关键字的分布情况;
使用场景:适合找比较小且连续的情况。

1.3.2 除留余数法--(常用)

设散列表中允许的地址数位m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key)key%p(p<=m),将关键码转换成哈希地址。
除了以上两种常见的哈希函数之外,还有平方取中法、折叠法、随机数法、数学分析法

2.哈希冲突

哈希冲突(哈希碰撞)是面试中的常客,可用通过一个哈希冲突间接引出哈希中的其他知识点。

2.1哈希冲突产生的原因

哈希值键值通过哈希函数计算得出的位置标识符,难以避免重复的问题。比如在下面的哈希表中,存入元素20,哈希值HashI=20%8=4,此时4位置中并没有元素,可以直接存入。

但是如果继续插入元素36,哈希值HashI=36%8=4,此时,4位置处已经有了元素20了,无法继续存入,此时就发生了哈希冲突。

不同的哈希函数引发哈希冲突的概率不同,但最终都会面临哈希冲突这个问题,因此需要一些方法来解决哈希冲突。

2.2 哈希冲突的解决方法

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

2.2.1 闭散列解决哈希冲突

闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个”空位置中去。那如何寻找下一个空位置呢?有两种常见的方法,1、线性探测;2、二次探测。

2.2.1.1 线性探测

如下的场景中,现在需要插入元素36,先通过哈希函数计算哈希地址,hashAddr为4。因此,36理论上应该插在该位置,但是该位置已经存放了值为20的元素,即发生哈希冲突。此时,继续向后探测、找到可用的位置。如下图所示,将36存入位置0处。

规定:当哈希表中存储的数据量与哈希表的容量比值(负载因子)过大时,扩大哈希表的容量,并重新进行映射。
线性探测:因为有负载因子的存在,所以哈希表一定是有剩余空间的。当发生哈希冲突时,从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

  1. 插入
    (1)通过哈希函数获取待插入元素在哈希表中的位置。
    (2)如果该位置没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。
  2. 2、删除
    采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已经有的元素,若直接删除元素会影响其他元素的搜索。比如直接删除元素4,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

负载因子=存储的有效数据个数/空间的大小
负载因子越大,冲突的概率越高,增删查改的效率越低;
负载因子越小,冲突的概率越低,增删查改的效率越高,但是空间利用率低,浪费多。
负载因子<1,就能保证发生哈希冲突时一定能找到空位置。

2.2.1.2 线性探测的代码实现

存储数据结构的定义:闭散列中存储的数据至少包含两个信息:键值对、状态表示。键值对既可以是K也可以是K/V,我们这里实现的是K/V

(1)状态

区分哈希表的一个位置有没有数据,如果用两种状态标识,在(1)或不在(0),那么就会带来两个问题:
1、0表示不在,那么如何存储数据0呢?
2、如果数据发生冲突,当前位置和后面位置都存放的是冲突数据,假如当前位置的数据被删除了,那么查找key时发现当前位置状态为不在时,那么就不会再向后查找了。
因此,要用3个状态位分别表示空、已存在、已删除,用枚举表示状态位。
空EMPTY--初始状态;
存在EXITS--插入数据后的状态;
删除DELETE--删除数据后的状态。
其实也可以分为可用/不可用两种状态,细分出EMPTY与DELETE是为了在进行探测时,提高效率。
探测时,如果探测到空,就不必再往后继续探测了,因为后面必然不存在我们想要找的数据。如果存在,这里就不会为空了。
所以闭散列中的存储数据结构可以用一个结构体定义。

//枚举表示某位置的状态
enum State
{EMPTY,EXITS,DELETE,
};
(2)定义HashData

哈希数据应包含两个成员:数据状态

//定义结构体存储数据
template<class T>
struct HashData
{T _data;State _state;
};
(3)哈希表

哈希表包含两个成员:哈希数据、存储的有效数据的个数,模板有3个参数K,T,KeyOfT。
①由于不知道key是K还是pair,所以需要定义两个模板参数K、T来包含key是K或者pair的两种情况;
②由于不知key的数据类型是int还是string、pair、struct,计算key的映射位置时需要取模,但是只能对int类型进行取模,string、pair、struct无法直接取模,KeyOfT作为仿函数,它的实例可用分别应对这些类型的取模。

//定义一个哈希表类
template<class K,class T,class KeyOfT>
class MyHashTable
{typedef HashTable<T> HashData;
public://...private:vector<HashData> _tables;//底层顺序表,用于存储数据size_t _num = 0;//保存的有效数据的个数
};
(4)查找函数

①无论传给哈希表的数据是K还是pair,查找时,都需要用K做key来进行查找;
②计算元素位置;
③如果当前位置元素为key,那么就返回该元素,否则可能发生了冲突,继续向后探测。

//1、查找数据
HashData* Find(const K& key)
{if (_tables.size() == 0)return nullptr;KeyOfT keyoft;size_t hashI = key % _tables.size();//取余查找位置//线性探测size_t i = 1;size_t index = hashI;while (_tables[index]._state != EMPTY){if (keyoft(_tables[index]._data) == key&& _tables[index]._state == EXITS){return &_tables[index];}//如果哈希表中的值与key值不相等,或者与key值相等,但是_state为DELETE,//则继续往后查找index = hashI + i;//线性查找  //index=hashI+i*i;//二次查找index %= _tables.size();//取余防止越界++i;//如果已经查找一圈,那么说明全是存在+删除if (index == hashI){break;}}return nullptr;
}
(5)插入函数

①根据额键值计算出下标(哈希值);
②线性探测,找到可用的位置[空/删除];
③插入数据,有效数据量+1。

//2、插入一个数据
bool Insert(const T& data)
{KeyOfT keyoft;if (Find(keyoft(data)))return false;//扩容//假如负载因子大于或者等于7时,以及哈希表的容量为0时,则需要进行扩容操作。//此时,在计算负载因子时应注意哈希表容量的大小不能为0。//取余查找位置,计算data中的key值在表中映射的位置size_t hashI = keyoft(data) % _tables.size();//线性探测写法size_t i = 1;size_t index = hashI;//状态为空就插入节点while (_tables[index]._state == EXITS){index = hashI + i;index %= _tables.size();//取余防止越界++i;}_tables[index]._data = data;_tables[index]._state = EXITS;_num++;return true;
}

如果想降低踩踏发生的概率,可用将线性探测改为二次探测

HashI += (i * i);	//二次探测

以上就是插入操作的具体实现,此时面临着一个棘手的问题:扩容问题。
当数据第一次插入或负载因子达到临界值时,需要进行扩容(因为vector<HashData> _tables一开始为空,所以第一次插入数据时也要扩容)。
当空间扩容后,容量发生了改变,这就意味着数据与下标的映射关系也发生了改变,需要更新数据,即重新进行线性探测。
扩容可分为传统写法和现代写法。
传统写法:

//2、插入一个数据
bool Insert(const T& data)
{KeyOfT keyoft;if (Find(keyoft(data)))return false;//假如负载因子大于或者等于7时,以及哈希表的容量为0时,则需要进行扩容操作。//此时,在计算负载因子时应注意哈希表容量的大小不能为0。if (_tables.size() == 0 || _num * 10 / _tables.size() >= 7){//1、开辟是原旧表2倍大小的新表(不一定是2倍);//2、遍历旧表的数据,重新计算原旧表数据在新表中的位置;//3、释放旧表//1、写法1:传统的开空间vector<HashData> newtables;//开辟一个新的vector顺序表size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;newtables.resize(newsize);//重新设置newtables的容量大小for (size_t i = 0; i < _tables.size(); ++i){if (_tables[i]._state == EXITS){//计算原旧表数据在新表中的位置,并处理冲突size_t index = keyoft(_tables[i]._data) % newtables.size();while (newtables[index]._state == EXITS){++index;if (index == newtables.size()){index = 0;}}newtables[index] = _tables[i];}}_tables.swap(newtables);}//插入//...
}

传统写法的思路:创建一个容量足够的信标,将原表中的数据映射至新表中,映射完成后,交换新表和原表,目的是为了更新当前哈希表对象中的表。
关于平衡因子的控制:根据别人的试验结果,哈希表中存储的有效数据量超过哈希表容器的70%时,推荐进行扩容。假设容量为M,存储的额有效数据量为N,两者比率N/M==0.7时进行扩容。因为我们这里的容量和数据量都是整数,所以在等式两边*10,可得N*10/M==7。

传统写法比较繁琐,下面来看看现代写法:

//2、插入一个数据
bool Insert(const T& data)
{KeyOfT keyoft;if (Find(keyoft(data)))return false;//假如负载因子大于或者等于7时,以及哈希表的容量为0时,则需要进行扩容操作。//此时,在计算负载因子时应注意哈希表容量的大小不能为0。if (_tables.size() == 0 || _num * 10 / _tables.size() >= 7){//1、开辟是原旧表2倍大小的新表(不一定是2倍);//2、遍历旧表的数据,重新计算原旧表数据在新表中的位置;//3、释放旧表//写法2:扩容的现代写法MyHashTable<K, T, KeyOfT> newht;size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;newht._tables.resize(newsize);for (size_t i = 0; i < _tables.size(); ++i){if (_tables[i]._state == EXITS){newht.Insert(_tables[i]._data);}}_tables.swap(newht._tables);
}//插入//...
}

其实传统写法中的插入部分逻辑与Insert中的插入操作重复了,因此我们可以借助现代思想,创建一个容量足够的哈希表,将原表中的数据遍历插入新的哈希表中,插入的过程调用Insert,代码很简洁,并且不容易出错。
细节:需不需要将当前对象中的有效数据量_n进行更新?
答案是不需要,往新的哈希表中插入_n个数据,意味着无论是新的哈希表还是当前对象,他们的有效数据量都是一致的,因此不需要更新。

(6)删除

删除操作就十分简单了,获取待删除数据,将其中的状态改为删除--DELETE即可,不必清空数据,只要访问不到就行了。当然,有效数据量-1。

//3、删除一个元素
bool Erase(const T& key)
{HashData* ret = Find(key);if (ret){ret->_state = DELETE;--_num;return true;}else{return false;}
}

像这种线性探测(暴力探测)可用解决哈希冲突的问题,但是会带来新的问题:踩踏。
踩踏:元素的存储位置被别的元素占用了,于是该元素也只能被迫线性探测,引发连锁反应,插入、查找等操作都会越来越慢。哈希冲突越多、效率越低。
优化方案:二次探测,每次向后探测i^2步,尽量减少踩踏。尽管如此,闭散列的实际效果不尽人意。闭散列的实战价值不大,因此只做简单了解即可,真正的重点是开散列。

2.2.2 开散列解决哈希冲突

所谓开散列就是在 原存储位置处 带上一个单链表,如果发生哈希冲突,就将冲突的值依次挂载即可,因此也叫做链地址法、开链法、哈希桶。
开散列中不需要负载因子,开散列的每个桶中存放的都是哈希冲突的元素,如果每个位置都被存满了,直接扩容就好了,当然扩容后也需要重新建立映射关系。
 

开散列中进行查找时,需要先根据哈希值找到对应的位置,并在单链表中进行遍历。一般情况下,单链表的长度不会太长,因为扩容后,整体长度会降低。
如果单链表真的过长了(几十个节点),我们还可以将其转为红黑树,此时效率依旧非常高。

2.2.2.1 哈希仿函数

在闭散列中,已经实现了Hash仿函数,用来获取哈希表中的元素key,方便后续计算映射位置。

//仿函数
template<class K>
struct _Hash
{const K& operator()(const K& key){return key;}
};

模板特化:string元素使用频率较高,进行模板特化。

//特化,针对某些类型进行特殊处理
template<>
struct _Hash<string>
{size_t operator()(const string& key){//BKDR Hashsize_t hash = 0;for (size_t i = 0; i < key.size(); ++i){hash *= 131;hash += key[i];}return hash;}
};
2.2.2.2 存储节点结构的定义

哈希桶中需要维护一个单链表,因此需要一个_next指针指向下一个节点。

template<class T>
struct HashNode
{T _data;HashNode<T>* _next;HashNode(const T& data):_data(data),_next(nullptr){}
};

因为引入了单链表,所以哈希桶的存储类型也变成了节点指针

template<class K,class T,class KeyOfT,class Hash=_Hash<K>>
class MyHashTable
{typedef HashNode<T> Node;
public://...private:vector<Node*> _tables;size_t _num;//记录表中存储的数据的个数
};
2.2.2.3 析构函数

因为有单链表,所以在对象析构时,需要手动遍历其中的节点,将其释放,避免内存泄漏。

~MyHashTable()
{Clear();
}void Clear()
{for (size_t i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}
}

为什么哈希表(闭散列)中不需要些析构函数?
因为在闭散列中,表中存储的数据不涉及自定义类型的动态内存管理,并且vector在对象调用默认析构时,会被调用其析构函数,释放其中的内存。

2.2.2.4 查找

先计算key在哈希表中的位置,然后在该位置的哈希桶中遍历查找。

Node* Find(const K& key)
{if (_tables.size() == 0)return nullptr;KeyOfT keyoft;size_t index = HashFunc(key) % _tables.size();Node* cur = _tables[index];while (cur){if (keyoft(cur->_data) == key){return cur;}else{cur = cur->_next;}}return nullptr;
}
2.2.2.5 插入

在进行数据插入时,既可以尾插,也可以头插,因为桶中的存储顺序没有要求。为了操作简单,我们选择头插。同样的,哈希桶在扩容时,也有传统写法和现代写法,这里采用传统写法。

bool Insert(const T& data)
{KeyOfT keyoft;if (Find(data) != nullptr)return false;//如果负载因子等于1,则增容,避免大量的哈希冲突if (_tables.size() == _num){//使用传统的增容写法vector<Node*> newtables;size_t newsize = _tables.size() == 0 ? 10 : 2 * _tables.size();newtables.resize(newsize);for (size_t i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];while (cur){//整个过程为单链表的头插//将旧表中的节点取下来,并重新计算在新表中的位置,并插入到新表中Node* next = cur->_next;//计算在新表中的位置size_t index = HashFunc(keyoft(cur->_data)) % newtables.size();cur->_next = newtables[index];newtables[index] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtables);}//计算数据在表中映射的位置size_t index = HashFunc(keyoft(data)) % _tables.size();//1、先检查这个值子在不在表中Node* cur = _tables[index];while (cur){if (keyoft(cur->_data) == keyoft(data)){return false;}else{cur = cur->_next;}}//2、头插到挂的链表中(尾插也可以)Node* newnode = new Node(data);newnode->_next = _tables[index];_tables[index] = newnode;++_num;return true;
}

这里的传统写法有一个很巧妙的地方:节点回收,既然旧表中节点不用了,那么我可用直接把其中的节点转移至新表中,这样可用提高效率。

2.2.2.6 删除

①计算key在表中的位置;
②要删除的数据是不是该位置的第一个哈希桶,如果是,那就让哈希表的第一个节点变成第二个桶,否则让这个桶的前一个桶指向这个桶的下一个桶。

bool Erase(const K& key)
{KeyOfT keyoft;size_t index = HashFunc(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[index];while (cur){if (keyoft(cur->_data) == key){if (prev == nullptr){//表示要删除的值在第一个节点_tables[index] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;
}

完整代码可用参考:哈希及其模拟实现

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

k8s,声明式API对象理解

命令式API 比如&#xff1a; 先kubectl create&#xff0c;再replace的操作&#xff0c;我们称为命令式配置文件操作 kubectl replace的执行过程&#xff0c;是使用新的YAML文件中的API对象&#xff0c;替换原有的API对象&#xff1b;而kubectl apply&#xff0c;则是执行了一…

开源ISP介绍(1)——开源ISP的Vivado框架搭建

开源github链接&#xff1a;bxinquan/zynq_cam_isp_demo: 基于verilog实现了ISP图像处理IP 国内Gitee链接&#xff1a;zynq_cam_isp: 开源ISP项目 基于以上开源链接移植项目到正点原子领航者Zynq7020开发板&#xff0c;并对该项目的Vivddo工程进行架构详解&#xff0c;后续会…

[Redis#13] cpp-redis接口 | set | hash |zset

目录 Set 1. Sadd 和 Smembers 2. Sismember 3. Scard 4. Spop 5. Sinter 6. Sinter store Hash 1. Hset 和 Hget 2. Hexists 3. Hdel 4. Hkeys 和 Hvals 5. Hmget 和 Hmset Zset 1. Zadd 和 Zrange 2. Zcard 3. Zrem 4. Zscore cpp-redis 的学习 主要关注于…

GEOBench-VLM:专为地理空间任务设计的视觉-语言模型基准测试数据集

2024-11-29 ,由穆罕默德本扎耶德人工智能大学等机构创建了GEOBench-VLM数据集&#xff0c;目的评估视觉-语言模型&#xff08;VLM&#xff09;在地理空间任务中的表现。该数据集的推出填补了现有基准测试在地理空间应用中的空白&#xff0c;提供了超过10,000个经过人工验证的指…

设计模式 更新ing

设计模式 1、六大原则1.1 单一设计原则 SRP1.2 开闭原则1.3 里氏替换原则1.4 迪米特法则1.5 接口隔离原则1.6 依赖倒置原则 2、工厂模式 1、六大原则 1.1 单一设计原则 SRP 一个类应该只有一个变化的原因 比如一个视频软件&#xff0c;区分不同的用户级别 包括访客&#xff0…

nlp培训重点

1. SGD梯度下降公式 当梯度大于0时&#xff0c;变小&#xff0c;往左边找梯度接近0的值。 当梯度小于0时&#xff0c;减去一个负数会变大&#xff0c;往右边找梯度接近0的值&#xff0c;此时梯度从负数到0上升 2.Adam优化器实现原理 #coding:utf8import torch import torch.n…

电脑关机的趣味小游戏——system函数、strcmp函数、goto语句的使用

文章目录 前言一. system函数1.1 system函数清理屏幕1.2 system函数暂停运行1.3 system函数电脑关机、重启 二、strcmp函数三、goto语句四、电脑关机小游戏4.1. 程序要求4.2. 游戏代码 总结 前言 今天我们写一点稍微有趣的代码&#xff0c;比如写一个小程序使电脑关机&#xf…

基础入门-Web应用OSS存储负载均衡CDN加速反向代理WAF防护部署影响

知识点&#xff1a; 1、基础入门-Web应用-防护产品-WAF保护 2、基础入门-Web应用-加速服务-CDN节点 3、基础入门-Web应用-文件托管-OSS存储 4、基础入门-Web应用-通讯服务-反向代理 5、基础入门-Web应用-运维安全-负载均衡 一、演示案例-Web-拓展架构-WAF保护-拦截攻击 原理&a…

Milvus×OPPO:如何构建更懂你的大模型助手

01. 背景 AI业务快速增长下传统关系型数据库无法满足需求。 2024年恰逢OPPO品牌20周年&#xff0c;OPPO也宣布正式进入AI手机的时代。超千万用户开始通过例如通话摘要、新小布助手、小布照相馆等搭载在OPPO手机上的应用体验AI能力。 与传统的应用不同的是&#xff0c;在AI驱动的…

002-日志增强版

日志增强版 一、需求二、引入依赖三、配置日志处理切面四、配置RequestWrapper五、效果展示 一、需求 需要打印请求参数和返回参数 二、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop<…

Spire.PDF for .NET【页面设置】演示:旋放大 PDF 边距而不改变页面大小

PDF 页边距是正文内容和页面边缘之间的空白。与 Word 不同&#xff0c;PDF 文档中的页边距不易修改&#xff0c;因为 Adobe 不提供任何功能供用户自由操作页边距。但是&#xff0c;您可以更改页面缩放比例&#xff08;放大/压缩内容&#xff09;或裁剪页面以获得合适的页边距。…

服务器数据恢复—EVA存储硬盘磁头和盘片损坏离线的数据恢复案例

服务器存储数据恢复环境&故障&#xff1a; 一台HP EVA存储中有23块硬盘&#xff0c;挂接到一台windows server操作系统的服务器。 EVA存储上有三个硬盘指示灯亮黄灯&#xff0c;此刻存储还能正常使用。管理员在更换硬盘的过程中&#xff0c;又出现一块硬盘对应的指示灯亮黄…

探索仓颉编程语言:官网上线,在线体验与版本下载全面启航

文章目录 每日一句正能量前言什么是仓颉编程语言仓颉编程语言的来历如何使用仓颉编程语言在线版本版本下载后记 每日一句正能量 当你被孤独感驱使着去寻找远离孤独的方法时&#xff0c;会处于一种非常可怕的状态。因为无法和自己相处的人也很难和别人相处&#xff0c;无法和别人…

idea 自动导包,并且禁止自动导 *(java.io.*)

自动导包配置 进入 idea 设置&#xff0c;可以按下图所示寻找位置&#xff0c;也可以直接输入 auto import 快速定位到配置。 Add unambiguous imports on the fly&#xff1a;自动帮我们优化导入的包Optimize imports on the fly&#xff1a;自动去掉一些没有用到的包 禁止导…

【时时三省】(C语言基础)结构体的自引用

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 结构体的自引用 在结构中包含一个类型为该结构体本身的成员是否可以呢&#xff1f; 在struct B里面包含了一个结构体struct A叫sa 结构体类型里面是可以包含另一个结构体类型变量作为它的成…

GoReplay开源工具使用教程

目录 一、GoReplay环境搭建 1、Mac、Linux安装GoReplay环境 二、GoReplay录制与重播 1、搭建练习接口 2、录制命令 3、重播命令 三、GoReplay单个命令 1、常用命令 2、其他命令 3、命令示例 4、性能测试 5、正则表达式 四、gorepaly组合命令 1、组合命令实例 2、…

宏海科技募资额有所缩减,最大销售和采购都重度依赖美的集团

《港湾商业观察》施子夫 11月29日&#xff0c;北交所上市审核委员会将召开2024年第24次上市委审议会议&#xff0c;届时将审议武汉宏海科技股份有限公司&#xff08;以下简称&#xff0c;宏海科技&#xff09;的首发上会事项。 在上会之前&#xff0c;宏海科技共收到北交所下…

驱动篇的开端

准备 在做之后的动作前&#xff0c;因为win7及其以上的版本默认是不支持DbgPrint&#xff08;大家暂时理解为内核版的printf&#xff09;的打印&#xff0c;所以&#xff0c;为了方便我们的调试&#xff0c;我们先要修改一下注册表 创建一个reg文件然后运行 Windows Registr…

渗透测试--Windows凭证收集

在渗透测试过程中&#xff0c;我们终究会遇到攻陷了某台加域Windows主机的情况。而这种情况下&#xff0c;我们很需要搜集当前主机的凭证信息。为进一步利用这些相互信任的凭证来进行横向打下基础。 在凭证收集中&#xff0c;我们主要有以下场景&#xff1a; 1.lsass.exe的DMP文…

PH热榜 | 2024-12-03

1. Vela OS 标语&#xff1a;利用人工智能&#xff0c;创业投资精准度提升十倍。 介绍&#xff1a;Vela操作系统是一个原生AI操作系统&#xff0c;它能让创业投资者的预测准确率比传统风投公司高出十倍。借助一群AI智能体&#xff0c;投资者可以发现蓬勃发展的市场、公司和人…