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

C++数据结构 : 哈希表的实现

C++数据结构 : 哈希表的实现

目录

  • C++数据结构 : 哈希表的实现
    • 引言
    • 1. 哈希概念
      • 1.1 直接定址法
      • 1.2 哈希冲突
      • 1.3 负载因子
    • 2. 哈希函数
      • 2.1 除法散列法/除留余数法
      • 2.2 乘法散列法(了解)
      • 2.3 全域散列法(了解)
    • 3. 处理哈希冲突
      • 3.1 开放定址法
        • 3.1.1 线性探测
        • 3.1.2 二次探测
        • 3.1.3 双重探测(了解)
        • 3.1.4 开放地址码代码实现
      • 3.2 链地址法(重点)

引言

哈希表(Hash Table)是一种高效的数据结构,它通过哈希函数将键(Key)映射到存储位置,从而实现平均时间复杂度为O(1)的快速查找、插入和删除操作。本文将详细介绍哈希表的核心概念,包括哈希函数的设计、哈希冲突的解决方法(开放定址法和链地址法),并通过C++代码实现一个完整的哈希表。无论你是初学者还是希望深入理解哈希表内部机制的开发者,本文都将为你提供清晰的理论指导和实用的代码示例。


1. 哈希概念

**哈希(Hash)**又称散列,是一种组织数据的方式。从译名来看,有散乱排列的意思。本质就是通过哈希函数把关键字 Key 跟存储位置建立一个映射关系,查找时通过这个哈希函数计算出 Key 存储的位置,进行快速查找。

1.1 直接定址法

直接定址法在关键字范围较集中时是非常简单高效的方法。例如,若一组关键字都在 [0,99] 之间,可直接开辟一个长度为 100 的数组,关键字的值即为存储位置的下标;若关键字均为 [a,z] 的小写字母,则可开辟长度为 26 的数组,通过 ASCII码 - 'a'的ASCII码 计算存储位置的下标。直接定址法的本质是利用关键字计算出一个绝对位置或相对位置。

代码示例:字符串中的第一个唯一字符

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;}
};

1.2 哈希冲突

  • 直接定址法的缺点非常明显,当关键字的范围比较分散时,会非常浪费内存甚至导致内存不够用。假设我们只有数据范围是[0, 9999]的N个值,要映射到一个M个空间的数组中(一般情况下M ≥ N),就需要借助哈希函数(hash function)hf,将关键字key放到数组的h(key)位置。需要注意的是,h(key)计算出的值必须在[0, M)之间。
  • 这里存在的一个问题是,两个不同的key可能会映射到同一个位置,这种情况称为哈希冲突哈希碰撞。理想情况下,我们希望找到一个优秀的哈希函数来避免冲突,但在实际应用中,冲突是不可避免的。因此,我们应尽量设计高效的哈希函数以减少冲突次数,同时也要制定合理的冲突解决方案。

1.3 负载因子

负载因子(有些地方也翻译为载荷因子/装载因子等,英文为 load factor)是指哈希表中已存储的N个值与哈希表大小M的比值。负载因子越大,哈希冲突的概率越高,空间利用率越高;负载因子越小,哈希冲突的概率越低,空间利用率越低。


2. 哈希函数

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

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

  • 除法散列法(除留余数法)通过哈希表大小 ( M ) 计算键 ( key ) 的映射位置,哈希函数为 ( h(key) = key % M )。使用时应避免 ( M ) 为 2 或 10 的幂次,否则会导致仅保留 ( key ) 的低位,引发冲突。例如:若 ( M=16 ),则 ( 63 )(二进制 00111111)和 ( 31 )(二进制 00011111)的哈希值均为 15;若 ( M=100 ),则 ( 112 ) 和 ( 12312 ) 的哈希值均为 12。
  • 实践中建议 ( M ) 取远离 2 的整数次幂的质数,以减少冲突。但 Java 的 HashMap 采用 ( M ) 为 2 的幂次(如 ( 2^{16} )),通过位运算(右移+异或)优化效率,使所有位参与计算,确保哈希值分布均匀。尽管与经典理论不同,这种方法通过调整计算方式仍能达到较好的效果。

2.2 乘法散列法(了解)

  • 乘法散列法对哈希表大小M没有要求,其核心思路分为两步:第一步是用关键字K乘上常数A(0 < A < 1),并提取出k*A的小数部分;第二步是用M乘以该小数部分,再向下取整。
  • 具体公式为:h(key) = floor(M × ((A × key)%1.0)),其中,floor表示向下取整,A∈(0,1)。A的取值至关重要,Knuth建议采用黄金分割点(5−12≈0.618033988725−1≈0.6180339887)效果较好。
  • 示例:乘法散列法对哈希表大小M是没有要求的,假设M为1024,key为1234,A = 0.6180339887, A*key= 762.6539420558,取小数部分为0.6539420558, M×((A×key)%1.0) = 0.6539420558*1024 =669.6366651392,那么h(1234) = 669。

2.3 全域散列法(了解)

  • 如果存在一个恶意的对手,他针对我们提供的散列函数,特意构造出一个发生严重冲突的数据集,比如让所有关键字全部落入同一个位置中,这种情况是可以存在的,只要散列函数是公开且确定的,就可以实现此攻击。解决方法自然是见招拆招,给散列函数增加随机性,攻击者就无法找出确定可以导致最坏情况的数据,这种方法叫做全域散列。
  • 全域散列的实现需要选一个足够大的质数P,a可以随机选[1,P-1]之间的任意整数,b可以随机选[0,P-1]之间的任意整数,这些函数构成了一个P*(P-1)组全域散列函数组。例如,假设P=17,M=6,a=3,b=4,则散列函数计算方式为:hab(key) = ((a × key + b) % P) % M,具体示例h34(8) = ((3 × 8 + 4) % 17) % 6 = 5。
  • 需要注意的是,每次初始化哈希表时,随机选取全域散列函数组中的一个散列函数使用,后续增删查改都固定使用这个散列函数,否则如果每次操作都随机选择散列函数,会导致插入和查找使用不同的函数,从而无法正确找到已插入的key。

3. 处理哈希冲突

实践中哈希表⼀般还是选择除法散列法作为哈希函数,当然哈希表无论选择什么哈希函数也避免不了冲突,那么插入数据时,如何解决冲突呢?主要有两种两种方法,开放定址法和链地址法。

3.1 开放定址法

在开放定址法中所有的元素都放到哈希表里,当一个关键字key用哈希函数计算出的位置冲突了,则按照某种规则找到一个没有存储数据的位置进行存储,开放定址法中负载因子一定是小于1的。这里的规则有三种:线性探测、二次探测、双重探测。

3.1.1 线性探测
  • 从发生冲突的位置开始,依次线性向后探测,直到寻找到下一个没有存储数据的位置为止,如果走到哈希表尾,则回绕到哈希表头的位置。
  • hash0 位置冲突,则线性探测公式为 h(key) = hash0 = key % Mhc(key,i) = hashi = (hash0 + i) % M(其中 i = {1, 2, 3, ..., M − 1}),因为负载因子小于1,则最多探测 M-1 次,一定能找到一个存储 key 的位置。
  • 线性探测比较简单且容易实现,但假设 hash0 位置连续冲突,hash0hash1hash2 位置已存储数据,后续映射到 hash0hash1hash2hash3 的值都会争夺 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 二次探测
  • 二次探测冲突解决方法:从发生冲突的位置开始,依次左右按二次方跳跃式探测,直到寻找到下一个没有存储数据的位置为止;如果往右走到哈希表尾,则回绕到哈希表头的位置;如果往左走到哈希表头,则回绕到哈希表尾的位置。
  • 哈希函数计算方式为:h(key) = hash0 = key % M,若hash0位置冲突,则二次探测公式为:hc(key,i) = hashi = (hash0 ± i²) % M,其中i = {1, 2, 3, ..., M/2}
  • 需要注意的是,当hashi = (hash0 − i²) % Mhashi < 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)=(hash0+i*h2(key))%M(i=1,2,3,…,M)。
  • 要求h2(key)与M互质,两种简单取值方法为:①当M为2的整数幂时,从[0,M-1]任选一个奇数;②当M为质数时,h2(key)=key%(M-1)+1。保持互质是为了确保探测能覆盖整个散列表,否则最大公约数p>1时,可寻址位置将减少为M/p个。例如初始位置1、偏移量3、表大小12时,只能访问{1,4,7,10}四个位置。
  • 示例:将{19,30,52,74}插入M=11的表,设h2(key)=key%10+1

在这里插入图片描述


3.1.4 开放地址码代码实现
  • 开放定址法的哈希表结构

    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 HashTable
    {
    private:vector<HashData<K, V>> _tables;size_t _n = 0; // 表中存储数据个数
    };
    

    需要注意的是,这里需要给每个存储值的位置加一个状态标识(如 {EXIST, EMPTY, DELETE}),否则删除某些值后会影响后续冲突值的查找。例如,删除 30 可能导致查找 20 失败;但如果为每个位置添加状态标识,删除 30 时只需将其状态改为 DELETE 而非直接删除值,这样在查找 20 时遇到 EMPTY 状态才会终止,从而正确找到 20

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

    在这里插入图片描述

    在这里插入图片描述

  • 扩容

    inline unsigned long __stl_next_prime(unsigned long n)
    {// Note: assumes long is at least 32 bits.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};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;
    }
    
  • key不能取模的问题

    当key是string/Date等类型时,key不能直接取模,此时需要为HashTable增加一个仿函数。该仿函数需支持将key转换为可进行取模运算的整型值。若key本身可转换为整型且不易产生冲突,则使用默认参数即可;若key无法直接转换为整型,则需要自定义实现该仿函数。实现仿函数时需确保key的每个值都参与计算,尽量使不同key转换出的整型值不同。由于string作为哈希表key的情况非常常见,可以考虑对string类型进行特化处理。

    template<class K>
    struct HashFunc
    {size_t operator()(const K& key){return (size_t)key;}
    };// 特化
    template<>
    struct HashFunc<string>
    {
    // 字符串转换成整形,可以把字符ascii码相加即可
    // 但是直接相加的话,类似"abcd"和"bcad"这样的字符串计算出是相同的
    // 这⾥我们使⽤BKDR哈希的思路,⽤上次的计算结果去乘以⼀个质数,这个质数⼀般去31, 131等效果会⽐较好size_t operator()(const string& key){size_t hash = 0;for (auto e : key){hash *= 131;hash += e;}return hash;}
    };
    template<class K, class V, class Hash = HashFunc<K>>
    class HashTable
    {
    public:
    private:vector<HashData<K, V>> _tables;size_t _n = 0; // 表中存储数据个数
    };    
    
  • 完整代码实现

    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 HashTable{public:// 获取下一个质数(用于哈希表扩容)inline unsigned long __stl_next_prime(unsigned long n){// 注意:假设long至少32位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};const unsigned long* first = __stl_prime_list;  // 质数列表起始位置const unsigned long* last = __stl_prime_list + __stl_num_primes;  // 结束位置// 使用lower_bound找到第一个不小于n的质数const unsigned long* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;  // 如果n大于所有质数,返回最大的}// 构造函数HashTable(){// 初始化为第一个质数大小的表_tables.resize(__stl_next_prime(0));}// 插入键值对bool Insert(const pair<K, V>& kv){// 如果键已存在,插入失败if (Find(kv.first))return false;// 负载因子大于0.7就扩容(_n*10/_tables.size() >=7 相当于 _n/_tables.size() >=0.7)if (_n * 10 / _tables.size() >= 7){// 创建新的哈希表HashTable<K, V, Hash> newHT;// 扩容到下一个质数大小newHT._tables.resize(__stl_next_prime(_tables.size()+1));// 将旧表中的有效数据重新插入到新表中for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);}}// 交换新旧表(现代写法,利用RAII)_tables.swap(newHT._tables);}Hash hash;  // 哈希函数对象size_t hash0 = hash(kv.first) % _tables.size();  // 计算初始哈希位置size_t hashi = hash0;size_t i = 1;  // 探测步长// 线性探测寻找空位置while (_tables[hashi]._state == EXIST){// 线性探测:每次加1hashi = (hash0 + i) % _tables.size();// 如果是二次探测,可以改为 +- i^2++i;}// 找到空位置,插入数据_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;  // 增加元素计数return true;}// 查找键HashData<K, V>* Find(const K& key){Hash hash;  // 哈希函数对象size_t hash0 = hash(key) % _tables.size();  // 计算初始哈希位置size_t hashi = hash0;size_t i = 1;  // 探测步长// 线性探测查找while (_tables[hashi]._state != EMPTY){// 如果位置状态为EXIST且键匹配,返回该位置if (_tables[hashi]._state == EXIST&& _tables[hashi]._kv.first == key){return &_tables[hashi];}// 线性探测:每次加1hashi = (hash0 + i) % _tables.size();++i;}return nullptr;  // 未找到}// 删除键bool Erase(const K& key){// 先查找键是否存在HashData<K, V>* ret = Find(key);if (ret == nullptr){return false;  // 键不存在,删除失败}else{// 标记为DELETE状态(伪删除)ret->_state = DELETE;--_n;  // 减少元素计数return true;}}private:vector<HashData<K, V>> _tables;  // 哈希表存储数组size_t _n = 0;  // 表中存储的有效数据个数};
    }
    

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

      在这里插入图片描述

  • 扩容

    • 开放定址法的负载因子必须小于1,而链地址法的负载因子没有限制,可以大于1。负载因子越大,哈希冲突的概率越高,但空间利用率也越高;负载因子越小,哈希冲突的概率越低,但空间利用率也越低。STL中 unordered_xxx 的最大负载因子基本控制在1,超过1时就会触发扩容,我们在后续实现中也采用这种方式。
    • 对于极端场景,如果某个桶的链长度过长,可以考虑使用全域散列法来避免被针对性攻击。但即使采用全域散列法,若偶然出现某个桶长度过长导致查找效率下降,Java8的 HashMap 会在桶长度超过阈值(如8)时将链表转换为红黑树以提升性能。不过在实际应用中,通过合理扩容可以大幅降低单个桶过长的概率,因此在后续实现中我们暂不引入这种复杂机制,但了解这种极端场景的解决思路仍很有价值。
  • 链地址法代码实现

    namespace hash_bucket 
    {/* 哈希节点模板类* 模板参数:*   K - 键(key)类型*   V - 值(value)类型* 实现原理:*   每个节点存储一个键值对,并通过_next指针形成链表*   这是解决哈希冲突的链式存储结构基础*/template<class K, class V>struct HashNode{pair<K, V> _kv;        // 存储实际的键值对数据HashNode<K, V>* _next; // 指向下一个节点的指针(用于解决哈希冲突的链表结构)/* 构造函数* 参数:kv - 要存储的键值对* 功能:初始化节点的键值对和next指针*/HashNode(const pair<K, V>& kv):_kv(kv)          // 使用初始化列表设置键值对,_next(nullptr)   // 新节点的next指针默认为nullptr{}};/* 哈希表模板类* 模板参数:*   K - 键类型*   V - 值类型  *   Hash - 哈希函数类型(默认为HashFunc<K>)* 实现特点:*   1. 使用vector存储桶(buckets)*   2. 链地址法解决冲突*   3. 自动扩容机制*/template<class K, class V, class Hash = HashFunc<K>>class HashTable{private:typedef HashNode<K, V> Node; // 节点类型别名,简化代码/* 获取下一个合适大小的素数(用于哈希表扩容)* 参数:n - 当前大小* 返回值:不小于n的最小素数,如果n超过最大值则返回最大素数* 设计原理:*   1. 使用素数作为表大小可以减少哈希冲突*   2. 预定义一组素数,扩容时选择合适的大小*/inline unsigned long __stl_next_prime(unsigned long n){// 预定义的素数列表(来自STL的实现)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};// 使用二分查找找到第一个不小于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;}public:/* 构造函数* 功能:初始化哈希表,使用第一个素数(53)作为初始大小* 设计考虑:*   1. 初始大小不宜过小,避免频繁扩容*   2. 使用素数有利于哈希分布*/HashTable(){_tables.resize(__stl_next_prime(0), nullptr); // 0会返回第一个素数53}/* 析构函数* 功能:释放哈希表中所有节点* 实现细节:*   1. 遍历每个桶*   2. 遍历每个桶中的链表*   3. 逐个删除节点*/~HashTable(){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; // 将桶指针置空}}/* 插入键值对* 参数:kv - 要插入的键值对* 返回值:插入是否成功* 实现步骤:*   1. 检查是否需要扩容*   2. 计算哈希值确定桶位置*   3. 头插法插入新节点* 扩容策略:*   负载因子(元素数/桶数)达到1时扩容*/bool Insert(const pair<K, V>& kv){Hash hs; // 哈希函数对象size_t hashi = hs(kv.first) % _tables.size(); // 计算哈希值// 检查是否需要扩容(负载因子=1)if (_n == _tables.size()){// 创建新表,大小为下一个素数vector<Node*> newtables(__stl_next_prime(_tables.size()+1), nullptr);// 遍历旧表所有桶for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i]; // 当前桶的头节点while (cur) // 遍历链表{Node* next = cur->_next; // 保存下一个节点// 重新计算节点在新表中的位置size_t newHashi = hs(cur->_kv.first) % newtables.size();// 头插法将节点插入新表cur->_next = newtables[newHashi];newtables[newHashi] = cur;cur = next; // 处理下一个节点}_tables[i] = nullptr; // 清空旧桶}_tables.swap(newtables); // 交换新旧表}// 创建新节点并头插到对应桶中Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n; // 更新元素计数return true;}/* 查找键值对* 参数:key - 要查找的键* 返回值:找到的节点指针,未找到返回nullptr* 实现步骤:*   1. 计算哈希值确定桶位置*   2. 遍历链表查找键*/Node* Find(const K& key){Hash hs;size_t hashi = hs(key) % _tables.size(); // 计算哈希值Node* cur = _tables[hashi]; // 获取对应桶的头节点while (cur) // 遍历链表{if (cur->_kv.first == key) // 找到键{return cur; // 返回节点指针}cur = cur->_next; // 检查下一个节点}return nullptr; // 未找到}/* 删除键值对  * 参数:key - 要删除的键* 返回值:是否删除成功* 实现步骤:*   1. 计算哈希值确定桶位置*   2. 遍历链表查找键*   3. 维护链表结构并删除节点*/bool Erase(const K& key){Hash hs;size_t hashi = hs(key) % _tables.size(); // 计算哈希值Node* prev = nullptr; // 前驱节点指针Node* cur = _tables[hashi]; // 当前节点指针while (cur) // 遍历链表{if (cur->_kv.first == key) // 找到要删除的键{if (prev == nullptr) // 删除的是头节点{_tables[hashi] = cur->_next; // 更新桶的头指针}else // 删除的是中间或尾部节点{prev->_next = cur->_next; // 跳过当前节点}delete cur; // 释放节点内存--_n;      // 更新元素计数return true; // 删除成功}prev = cur;       // 更新前驱节点cur = cur->_next; // 移动到下一个节点}return false; // 未找到要删除的键}private:vector<Node*> _tables; // 哈希桶数组(每个元素是一个链表头指针)size_t _n = 0;        // 表中存储的元素个数(用于计算负载因子)};
    }
    

相关文章:

C++数据结构 : 哈希表的实现

C数据结构 &#xff1a; 哈希表的实现 目录 C数据结构 &#xff1a; 哈希表的实现引言1. 哈希概念1.1 直接定址法1.2 哈希冲突1.3 负载因子 2. 哈希函数2.1 除法散列法/除留余数法2.2 乘法散列法&#xff08;了解&#xff09;2.3 全域散列法&#xff08;了解&#xff09; 3. 处…...

抖音电商客户端一面面经

抖音电商客户端一面面经 时间&#xff1a; 25.05.30 岗位&#xff1a; 抖音电商客户端开发工程师 形式&#xff1a; 技术一面 刚刚结束了字节跳动抖音电商客户端开发工程师岗位的技术一面&#xff0c;整体感觉考察范围非常全面&#xff0c;涵盖了基础、项目、算法、系统设计等…...

JavaScript 在 AcroForm 中的广泛应用

在Adobe表单(特别是SAP Interactive Forms by Adobe)中使用JavaScript的各种技巧和方法,下面这些代码片段可以帮助开发者更高效地处理表单逻辑和交互。 1. 获取数据内容 从上下文结构中获取数据 var LV_DATA = xfa.resolveNode("$record.IM_TEST.FIELDNAME").val…...

Socket编程之TCP套件字

基于的TCP套件字编程流程 1. Socket套接字 Socket是一个编程接口&#xff08;网络编程接口&#xff09;&#xff0c;是一种特殊的文件描述符&#xff08;write/read&#xff09;。Socket并不 仅限于TCP/IP Socket独立于具体协议的编程接口&#xff0c;这个接口位于TCP/IP四层…...

AD9268、AD9643调试过程中遇到的问题

Ad9268芯片 AD9268是一款双通道、16位、80 MSPS/105 MSPS/125 MSPS模数转换器(ADC)。AD9268旨在支持要求高性能、低成本、小尺寸和多功能的通信应用。双通道ADC内核采用多级差分流水线架构&#xff0c;集成输出纠错逻辑。每个ADC都具有宽带宽、差分采样保持模拟输入放大器&…...

Java-File类基本方法使用指南

Java-File类基本方法使用指南 一、File类基础概念1.1 什么是File类1.2 File类的构造函数 二、文件和目录的创建与删除2.1 创建文件 - createNewFile()2.2 创建目录 - mkdir() 和 mkdirs()2.3 删除文件或目录 - delete() 三、文件和目录的查询与判断3.1 存在性判断 - exists()3.…...

Python爬虫实战:研究PyQuery库相关技术

1. 引言 1.1 研究背景与意义 随着互联网的快速发展,网络上的数据量呈爆炸式增长。如何高效地从海量的网页数据中提取有价值的信息,成为当前信息技术领域的一个重要研究方向。网络爬虫作为一种自动获取网页内容的程序,能够按照一定的规则,自动地抓取万维网信息,在搜索引擎…...

第九篇:MySQL 安全加固与访问控制策略实战

数据库的安全不仅仅是防止外部入侵&#xff0c;更包括合理配置账户权限、日志审计、网络加密、配置加固等。本文将系统性梳理 MySQL 的安全机制与实战加固方法&#xff0c;助你构建安全可靠的数据库运行环境。 一、数据库安全风险面 数据库常面临的威胁&#xff1a; 弱口令或默…...

神经网络-Day40

目录 单通道图片的规范写法图像任务中的张量形状NLP任务中的张量形状1. **Flatten操作**2. **view/reshape操作** 总结彩色图片的规范写法 图像数据的格式以及模型定义的过程&#xff0c;和之前结构化数据的略有不同&#xff0c;主要差异体现在2处 模型定义的时候需要展平图像由…...

WindowServer2022下docker方式安装dify步骤

WindowServer2022下docker方式安装dify步骤&#xff08;稳定后考虑部署至linux中&#xff09; 教程&#xff1a;https://blog.csdn.net/qq_49035156/article/details/143264534 0、资源要求 ---windows&#xff1a;8核CPU、16G内存、200G500G存储 ---10.21.31.122/administra…...

Java五种方法批量处理List元素全解

Java:如何优雅批量处理List中的每个元素 一、场景分析&#xff1a;为什么需要批量处理List&#xff1f;二、核心方法&#xff1a;五种实现方式对比2.1 普通for循环&#xff08;最直接的方式&#xff09;代码示例&#xff1a;优缺点&#xff1a; 2.2 Java 8 replaceAll&#xff…...

springboot文件上传下载

基于ResponseEntity的下载响应 SpringBoot中&#xff0c;ResponseEntity类型可以精确控制HTTP响应&#xff0c;为文件下载提供完善的HTTP头信息。 RestController RequestMapping("/api/download") public class FileDownloadController {GetMapping("/file/{…...

webpack CDN打包优化

CDN网络分发服务 请求资源时最近的服务器将缓存内容交给用户 体积较大且变动不多的文件存在CDN文件中 react react-dom资源 // 添加自定义对于webpack的配置const path require(path) const { whenProd, getPlugin, pluginByName } require(craco/craco)module.exports {//…...

ARM内核一览

经常看介绍某某牛批芯片用的又是ARM什么核&#xff0c;看的云里雾里&#xff0c;所以简单整理整理。&#xff08;内容来自官网和GPT&#xff09; 1 ARM 内核总体分类 系列特点应用场景Cortex-M超低功耗、低成本、实时性嵌入式系统、微控制器、IoTCortex-R高可靠性、硬实时汽车…...

Rust 和 Python 如何混合使用

Rust 与 Python 可以通过多种方式混合使用&#xff0c;如 FFI 接口、PyO3 库、CFFI、CPython API、wasm 模块嵌入等。这种混合开发模式可结合 Rust 的性能优势与 Python 的开发效率。其中&#xff0c;PyO3 是目前最受欢迎的桥接工具&#xff0c;它允许使用 Rust 编写 Python 扩…...

台式电脑CPU天梯图_2025年台式电脑CPU天梯图

CPU的选择绝对是重中之重,它关乎了一台电脑性能好坏。相信不少用户,在挑选CPU的时候不知道谁强谁弱,尤其是intel和AMD两款CPU之间。下面通过2025年台式电脑CPU天梯图来了解下这两款cpu. 2025年台式电脑CPU天梯图 2025年台式电脑CPU天梯图包含了老旧型号以及12代、13代、14代…...

2025年渗透测试面试题总结-匿名[校招]安全服务工程师(题目+回答)

安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 匿名[校招]安全服务工程师 一面问题与完整回答 1. 学校、专业、成绩与排名 2. 学习安全时长 3. 当前学习…...

Deseq2:MAG相对丰度差异检验

首先使用代码将contigs和MAG联系起来 https://github.com/MrOlm/drep/blob/master/helper_scripts/parse_stb.py ~/parse_stb.py --reverse -f ~/bin_dir/* -o ~/bin_dir/genomes.stb # 查看第一列的contigs有没有重复&#xff08;重复的话会影响后续比对&#xff09; awk {p…...

CTFHub-RCE 命令注入-过滤目录分隔符

观察源代码 代码里面可以发现过滤了目录分隔符\和/ 判断是Windows还是Linux 源代码中有 ping -c 4 说明是Linux 查看有哪些文件 127.0.0.1|ls 打开flag文件 发现存在一个flag_is_here的文件夹&#xff0c;我们需要打开这个文件夹找到目标文件我们尝试分步&#xff0c;先利…...

从零开始的数据结构教程(七) 回溯算法

&#x1f504; 标题一&#xff1a;回溯核心思想——走迷宫时的“穷举回头”策略 回溯算法 (Backtracking) 是一种通过探索所有可能的候选解来找出所有的解或某些解的算法。它就像你在一个复杂的迷宫中寻找出路&#xff1a;当你遇到一个岔路口时&#xff0c;你会选择一条路继续…...

CentOS-stream-9 Zabbix的安装与配置

一、Web环境搭建部署Zabbix时&#xff0c;选择合适的MariaDB、PHP和Nginx版本非常重要&#xff0c;以确保兼容性和最佳性能。以下是建议版本&#xff1a;Zabbix 6.4 MariaDB&#xff1a;官方文档推荐使用MariaDB 10.3或更高版本。对于CentOS Stream 9&#xff0c;建议使用Maria…...

开源是什么?我们为什么要开源?

本片为故事类文章推荐听音频哦 软件自由运动的背景 梦开始的地方 20世纪70年代&#xff0c;软件行业处于早期发展阶段&#xff0c;软件通常与硬件捆绑销售&#xff0c;用户对软件的使用、修改和分发权利非常有限。随着计算机技术的发展和互联网的普及&#xff0c;越来越多的开…...

【unity游戏开发——编辑器扩展】EditorApplication公共类处理编辑器生命周期事件、播放模式控制以及各种编辑器状态查询

注意&#xff1a;考虑到编辑器扩展的内容比较多&#xff0c;我将编辑器扩展的内容分开&#xff0c;并全部整合放在【unity游戏开发——编辑器扩展】专栏里&#xff0c;感兴趣的小伙伴可以前往逐一查看学习。 文章目录 前言一、监听编辑器事件1、常用编辑器事件2、示例监听播放模…...

elasticsearch低频字段优化

在Elasticsearch中&#xff0c;通过设置"index": false关闭低频字段的倒排索引构建是常见的优化手段&#xff0c;以下是关键要点&#xff1a; 一、核心机制 ‌倒排索引禁用‌ 设置index: false后&#xff0c;字段不会生成倒排索引&#xff0c;无法通过常规查…...

React---day3

React 2.5 jsx的本质 jsx 仅仅只是 React.createElement(component, props, …children) 函数的语法糖。所有的jsx最终都会被转换成React.createElement的函数调用。 createElement需要传递三个参数&#xff1a; 参数一&#xff1a;type 当前ReactElement的类型&#xff1b;…...

PyCharm接入DeepSeek,实现高效AI编程

介绍本土AI工具DeepSeek如何结合PyCharm同样实现该功能。 一 DeepSeek API申请 首先进入DeepSeek官网&#xff1a;DeepSeek 官网 接着点击右上角的 “API 开放平台“ 然后点击API keys 创建好的API key&#xff0c;记得复制保存好 二 pycharm 接入deepseek 首先打开PyCh…...

前端面经 get和post区别

get获取数据 post提交资源&#xff0c;引起服务器状态变化或者副作用 区别 1get会比post更不安全 get参数写在url中 post在请求体内 2get报文 head和body一起发 响应200 post报文 先发head 100 再发 body 200 3 get请求url有长度限制 4 默认缓存get 请求...

CTFSHOW-WEB-36D杯

给你shell 这道题对我这个新手还是有难度的&#xff0c;花了不少时间。首先f12看源码&#xff0c;看到?view_source&#xff0c;点进去看源码 <?php //Its no need to use scanner. Of course if you want, but u will find nothing. error_reporting(0); include "…...

MySQL connection close 后, mysql server上的行为是什么

本文着重讲述的是通过 msql client 连接到 mysql server &#xff0c;发起 update 、 select 操作(由于数据量非常大&#xff0c;所以 update、select 操作都很耗时&#xff0c;即在结果返回前我们有足够的时间执行一些操作) 。 在客户端分别尝试执行 ctrl C 结束关闭 mysql c…...

RabbitMQ vs MQTT:深入比较与最新发展

RabbitMQ vs MQTT&#xff1a;深入比较与最新发展 引言 在消息队列和物联网&#xff08;IoT&#xff09;通信领域&#xff0c;RabbitMQ 和 MQTT 是两种备受瞩目的技术&#xff0c;各自针对不同的需求和场景提供了强大的解决方案。随着 2025 年的到来&#xff0c;这两项技术都…...