【C++進階篇】哈希表的模擬實現(賦源碼)

這里寫目錄標題

  • 前言
  • 一. 開放地址法實現哈希表
    • 1.1 閉散列結構定義
    • 1.2 構造函數
    • 1.3 插入(線性探測)
      • 1.3.1 傳統寫法
      • 1.3.2 現代寫法
    • 1.4 查找
    • 1.5 刪除
  • 二. 鏈地址法實現哈希表(哈希桶)
    • 2.1 開散列結構定義
    • 2.2 構造函數
    • 2.3 插入
    • 2.4 查找
    • 2.5 刪除
  • 三. 最后

前言

哈希表的核心思想就是映射,通過將key值以某種算法映射成不大于哈希表長度的哈希值,從而實現存儲數據。上篇提到解決哈希沖突有 閉散列 和 開散列,本文將用這兩種理論思想實現哈希表。
代碼位置:哈希模擬的實現源碼

一. 開放地址法實現哈希表

1.1 閉散列結構定義

該閉散列哈希表使用vector數組,其中數組里面包含一個結構體,該結構存儲pair類型數據及當前位置的狀態,是否為空,不為空,或者有數據然后被刪除了的三個狀態,偽代碼如下:

//節點可能的三種狀態
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:
private:vector<HashData<K, V>> _tables;size_t _n = 0;//表中實際存儲的數據個數
};
  • 仿函數作用:

因為里面是數組,通過key值映射成哈希值后,哈希值不能超過哈希表的長度,需要取模,所以同時也要傳入仿函數,將任意類型數據不能取模轉換成整數,本來就可以進行取模運算的,就無需借助額外的仿函數,所以就可以給一個默認的仿函數,該仿函數給本來是整數去使用的。

//默認的仿函數
template<class K>
class HashFunc
{
public:size_t operator()(const K& key){return (size_t)key;}
};

1.2 構造函數

給vector數組開一定的空間,為了防止2的冪次方的數,底層給了一定合理的素數,下面給代碼即可。

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;
}HashTable()
{_tables.resize(__stl_next_prime(1));
}

1.3 插入(線性探測)

插入之前判斷該key值是否在存在,存在直接返回即可。對插入的key值映射成哈希值,然后進行探測,探測規則:當前位置為空,繼續向后探測,跳出循環后,將該位置插入數據并將該位置的狀態設置為存在,_n++(表示存儲的數據個數+1),這里有一個問題:擴容???

  • 問題:如何擴容

1.3.1 傳統寫法

創建一個數組(數組已經擴容后),將舊表中的vector數組中的數據重新進行映射成新開的數組(這里的一個優勢在于:原來在舊表中沖突的數據可能在新表中就不沖突了),也需進行探測,過程基本與插入過程一致,將舊數據完成探測之后,在新表與舊表進行交換。偽代碼如下:

bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))return false;//擴容if ((double)_n / (double)_tables.size() >= 0.7){// 獲取素數表里面比當前表大的下一個素數size_t newSize = __stl_next_prime(_tables.size() + 1);vector<HashData<K, V>> newTables(newSize);//遍歷舊表,將數據都映射到新表for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){size_t hash0 = _tables[i].kv.first % newSize;size_t i = 1;size_t hashi = hash0;while (newTables[hashi]._state == EXIST){//沖突探測hashi = (hash0 + i) % newSize;i++;}newTables[hashi]._kv = kv;newTables[hashi]._state = EXIST;//_n++;需不需要寫???思考一下}}_tables.swap(newTables);}Hash hs;size_t hash0 = hs(kv.first) % _tables.size();size_t i = 1;size_t hashi = hash0;while (_tables[hashi]._state == EXIST){//沖突探測hashi = (hash0 + i) % _tables.size();i++;}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_n++;return true;
}
  • 細節1:

重新尋找哈希值,需要對新的哈希表的長度進行取余,不要依然對舊表中的哈希表長度進行取余,否則會導致新表中新增加的長度沒有被使用,沖突概率未減少。

  • 細節2:

_n++需不需要寫???不需要,因為是將舊表中的數據重新映射至新表,數據并沒有增加。

注意:可以看出上述代碼有點冗余,特別是插入過程與重新建立映射關系的代碼很相似。
下面這種寫法很巧妙,且代碼量少。

1.3.2 現代寫法

思想:建立一個新表,然后給新表中的哈希表開一定足夠的空間,然后將舊表中的數據直接插入新表中即可,插入的過程同時也完成探測過程。很優雅的寫法,建議采用它。

  • 偽代碼:
bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))return false;//擴容if ((double)_n / (double)_tables.size() >= 0.7){size_t newSize = __stl_next_prime(_tables.size() + 1);HashTable<K, V, Hash> newHT;newHT._tables.resize(newSize);//遍歷舊表,將數據都映射到新表for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);//此行為已經將舊數據映射到了新表中,同時進行線性探測和沖突探測,可能原來在舊表中的數據沖突,在新表中它就不沖突了}}_tables.swap(newHT._tables);//將新表與舊表進行交換}Hash hs;size_t hash0 = hs(kv.first) % _tables.size();size_t i = 1;size_t hashi = hash0;while (_tables[hashi]._state == EXIST){//沖突探測hashi = (hash0 + i) % _tables.size();i++;}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_n++;return true;
}

該實現是一個功能完整的線性探測哈希表,適合學習用途。

1.4 查找

查找過程很簡單,首先對要查找的key值轉化成映射后的哈希值,對應位置狀態為!EXIST,進行查詢當前位置狀態不為空,且不為刪除直接返回該指針即可,否則繼續往后探測,當探測為空,說明不存在,直接返回nullptr即可。

  • 偽代碼:
HashData<K, V>* Find(const K& key)
{Hash hs;size_t hash0 = hs(key) % _tables.size();size_t hashi = hash0;size_t i = 1;while (_tables[hashi]._state != EMPTY){if (_tables[hashi]._state == EXIST&& _tables[hashi]._kv.first == key){return &_tables[hashi];}hashi = (hash0 + i) % _tables.size();++i;}return nullptr;
}

1.5 刪除

直接調用接口Find即可,接收返回值,判斷返回值是否為空,為空直接返回false;不為空將該位置的狀態設置為刪除即可,然后返回true即可。

  • 偽代碼:
bool Erase(const K& key)
{HashData<K, V>* ret = Find(key);if (ret == nullptr){return false;}else{--_n;ret->_state = DELETE;return true;}
}

標記刪除操作時間復雜度為 O(1),無數據遷移開銷。
聲明:上述做法了解一下即可,下面這個才是王炸,需重點關注及掌握。

二. 鏈地址法實現哈希表(哈希桶)

2.1 開散列結構定義

該開散列哈希表使用vector數組,其中數組里面包含一個哈希表節點,該節點存儲pair類型數據及下一個哈希表數據的指針。偽代碼如下:

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) {} // 構造函數
};template<class K, class V>
class HashTable {typedef HashNode<K, V> Node;vector<Node*> _tables;    // 哈希槽數組(存儲鏈表頭指針)size_t _n = 0;            // 當前元素總數
};

該代碼提供了鏈地址法哈希表的核心骨架。

2.2 構造函數

構造函數與上述閉散列一致。

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;
}HashTable()
{_tables.resize(__stl_next_prime(1), nullptr);
}

2.3 插入

創建一個數組,擴容時,將舊表中的節點挪動至新表中,需重新建立映射關系,也需要進行探測,過程與插入過程一致,將舊數據完成探測之后,在新表與舊表進行交換。偽代碼如下:
**插入數據時有個問題,**進行尾插還是頭插,兩者都可以,但頭插很簡單,尾插相對較復雜,需要找尾,當前桶挪動完畢,需要將當前桶只為nullptr,本文以頭插為示例。

bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))return false;if (_n == _tables.size()){size_t newSize = __stl_next_prime(_tables.size() + 1);vector<Node*> newtables(newSize, nullptr);//遍歷舊表,將舊表的節點挪動到新表for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t hashi = cur->_kv.first % newSize;cur->_next = newtables[hashi];newtables[hashi] = cur;}_tables[i] = nullptr;}_tables.swap(newtables);}size_t hashi = kv.first % _tables.size();Node* newnode = new Node(kv);//頭插,尾插也行,找尾麻煩,需要找尾newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;
}

2.4 查找

查找過程很簡單,將要查找的key值轉換成哈希值,然后對該桶進行遍歷,判斷桶中的數據key值是否與要查找的key值是否相等,相等返回該節點指針即可,找到空還未找到,返回nullptr即可。

  • 偽代碼如下:
Node* Find(const K& key)
{size_t hashi = key % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;
}

2.5 刪除

刪除需要前驅指針(默認為空)將刪除后的節點連接起來,刪除過程相對來說較復雜,也需要要刪除的key值轉換成哈希值,然后遍歷該桶,判斷桶中節點的數據key值與要查找的key值是否相等,相等進行刪除,返回true即可,里面有細節 -> :

  • 細節一:

當刪除頭結點時,頭結點沒有前驅指針,會對空指針進1行解引用,所以需要分情況討論。

  1. 前驅指針不為空,和為空兩種狀態。
  2. 不相等繼續往后找,當前桶找完還未找到,返回false即可。
  • 偽代碼:
		bool Erase(const K& key){size_t hashi = 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;}

代碼正確處理了鏈表節點的刪除,維護了計數器_n,但未處理重復鍵(若允許存在)。

三. 最后

本文詳述了哈希表的兩種實現方式:開放地址法(閉散列)與鏈地址法(開散列)。開放地址法通過線性探測解決沖突,采用標記刪除優化性能,擴容時重建哈希表;鏈地址法以鏈表處理沖突,頭插法簡化操作,擴容時重新映射所有節點。兩者均使用素數表優化哈希分布,核心操作包括插入、查找、刪除,適用于不同場景,是高效鍵值存儲的關鍵技術。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/907270.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/907270.shtml
英文地址,請注明出處:http://en.pswp.cn/news/907270.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

07-后端Web實戰(部門管理)

5. 修改部門 對于任何業務的修改功能來說&#xff0c;一般都會分為兩步進行&#xff1a;查詢回顯、修改數據。 5.1 查詢回顯 5.1.1 需求 當我們點擊 "編輯" 的時候&#xff0c;需要根據ID查詢部門數據&#xff0c;然后用于頁面回顯展示。 5.1.2 接口描述 參照參照…

深度解析項目集方向或目標根本性轉變的應對策略 —— 項目集管理實戰指南

在復雜多變的商業環境中&#xff0c;項目集管理面臨著重重挑戰&#xff0c;而項目集方向或目標的根本性轉變無疑是其中最具沖擊力的問題之一。本文將深度剖析這一難題&#xff0c;為項目集管理從業者提供實用、新穎且富有價值的應對策略&#xff0c;助力大家在項目集管理的復雜…

JAVA面試復習知識點

面試中遇到的題目&#xff0c;記錄復習&#xff08;持續更新&#xff09; Java基礎 1.String的最大長度 https://www.cnblogs.com/wupeixuan/p/12187756.html 2.集合 Collection接口的實現&#xff1a; List接口&#xff1a;ArraryList、LinkedList、Vector Set接口&#xff1a…

【燒腦算法】定長滑動窗口:算法題中的“窗口”智慧

目錄 引言 定長滑動窗口習題剖析 3439. 重新安排會議得到最多空余時間 I 2134. 最少交換次數來組合所有的 1 II 1297. 子串的最大出現次數 2653. 滑動子數組的美麗值 567. 字符串的排列 438. 找到字符串中所有字母異位詞 30. 串聯所有單詞的子串 220. 存在重復元素 II…

JWT安全:接收無簽名令牌.【簽名算法設置為none繞過驗證】

JWT安全&#xff1a;假密鑰【簽名隨便寫實現越權繞過.】 JSON Web 令牌 (JWT)是一種在系統之間發送加密簽名 JSON 數據的標準化格式。理論上&#xff0c;它們可以包含任何類型的數據&#xff0c;但最常用于在身份驗證、會話處理和訪問控制機制中發送有關用戶的信息(“聲明”)。…

XGBoost與SHAP深度解析:從算法原理到實戰價值

在機器學習領域&#xff0c;XGBoost以其卓越的性能長期占據Kaggle競賽和工業界的主流地位&#xff0c;而SHAP&#xff08;SHapley Additive exPlanations&#xff09;則成為模型可解釋性的標桿工具。本文將深度解析兩者的技術內核&#xff0c;并通過實戰案例揭示其結合應用的實…

Java SE Cloneable接口和深/淺拷貝

Java為我們提供了各種各樣功能的接口&#xff0c;Clonable接口就是其中之一。 它通常配合Object類的 clone方法使用。這個方法可以為我們創建一個對象的拷貝&#xff0c;即復制一個對象。在進入本文的主要內容之前&#xff0c;先來對訪問限定符 protected進行一個解剖。 1.再…

Python學習(3) ----- Python的函數定義及其使用

Python 中函數是組織好的、可重復使用的代碼塊&#xff0c;用于實現單一或相關聯的功能。下面是函數定義和使用的完整說明&#xff1a; &#x1f4cc; 一、函數定義語法 def 函數名(參數1, 參數2默認值, *args, **kwargs):"""函數說明文檔"""函…

vue2使用el-tree實現兩棵樹間節點的拖拽復制

原文鏈接&#xff1a;兩棵el-tree的節點跨樹拖拽實現 參照這篇文章&#xff0c;把它做成組件&#xff0c;新增左側樹&#xff08;可拖出&#xff09;被拖節點變灰提示&#xff1b; 拖拽中&#xff1a; 拖拽后&#xff1a; TreeDragComponent.vue <template><!-- …

智變與重構:AI 賦能基礎教育教學的范式轉型研究報告

一、研究背景與核心價值 &#xff08;一&#xff09;技術驅動下的教育轉型浪潮 在全球數字化轉型加速的背景下&#xff0c;人工智能作為核心技術力量&#xff0c;正重塑基礎教育生態。據《人工智能賦能未來教育研究報告》指出&#xff0c;我國教育數字化戰略行動已推動超 70…

Go語言中Print、Printf和Println的區別及使用場景詳解

在Go語言的fmt包中&#xff0c;Print、Printf和Println是三個基礎但功能各異的輸出函數。本文將從多個維度進行詳細對比分析&#xff0c;并給出具體的使用建議。 1. 核心區別深度解析 1.1. 函數簽名與基本行為 func Print(a ...interface{}) (n int, err error) func Printf…

高端制造行業 VMware 替代案例合集:10+ 頭部新能源、汽車、半導體制造商以國產虛擬化支持 MES、PLM 等核心應用系統

在“中國制造 2025”政策的推動下&#xff0c;國內的新能源、汽車制造、半導體、高端裝備等高端制造產業迎來了蓬勃發展&#xff0c;成為全球制造業版圖中舉足輕重的力量。訂單數量的激增與國產化轉型的趨勢&#xff0c;也為高端制造企業的 IT 基礎設施帶來了新的挑戰&#xff…

Spring Ai | 從零帶你一起走進AI項目(中英)

目錄 Thinking Study question pox.xml Maven Gradle Configure API Key Use the AI Client Question Thinking 讓數據變得更加貼近用戶的想法 Study question null pox.xml 添加依賴 Maven <dependencies><dependency><groupId>org.springfram…

LiveGBS作為下級平臺GB28181國標級聯2016|2022對接海康大華宇視華為政務公安內網等GB28181國標平臺查看級聯狀態及會話

LiveGBS作為下級平臺GB28181國標級聯2016|2022對接海康大華宇視華為政務公安內網等GB28181國標平臺查看級聯狀態及會話 1、GB/T28181級聯概述2、搭建GB28181國標流媒體平臺3、獲取上級平臺接入信息3.1、向下級提供信息3.2、上級國標平臺添加下級域3.3、接入LiveGBS示例 4、配置…

卸載 Office PLUS

Office PLUS作為微軟官方推出的智能辦公提效工具&#xff0c;自2015年問世以來&#xff0c;憑借其豐富的模板資源和便捷的智能功能&#xff0c;迅速贏得了廣大職場人士和學生的青睞。本文將全面介紹Office PLUS的發展歷程、核心功能、可能帶來的使用問題&#xff0c;以及如何徹…

影響沉金價格的因素如何體現在多層電路板制造上?

隨著科技的不斷發展&#xff0c;電子產品越來越普及&#xff0c;對電路板的需求也越來越大。多層電路板作為電子產品的核心部件&#xff0c;其性能和質量直接影響到整個產品的穩定性和可靠性。在多層電路板的生產過程中&#xff0c;沉金工藝是一種常用的表面處理方法&#xff0…

擴展摩爾投票法:找出出現次數超過 n/3 的元素

文章目錄 問題描述關鍵洞察算法原理Java 實現算法演示投票階段驗證階段 復雜度分析算法關鍵點通用化公式實際應用場景邊界情況處理總結 標簽&#xff1a;LeetCode 169, 摩爾投票法, 多數元素, 算法擴展, 數組處理 在解決多數元素問題時&#xff0c;我們學習了經典的摩爾投票法處…

Git:現代軟件開發的基石——原理、實踐與行業智慧·優雅草卓伊凡

Git&#xff1a;現代軟件開發的基石——原理、實踐與行業智慧優雅草卓伊凡 一、Git的本質與核心原理 1. 技術定義 Git是一個分布式版本控制系統&#xff08;DVCS&#xff09;&#xff0c;由Linus Torvalds在2005年為管理Linux內核開發而創建。其核心是通過快照&#xff08;Sna…

程序人生-hello’s P2P

計算機系統 大作業 題 目 程序人生-hello’s P2P 專 業 計算機與電子通信類 學   號 2023111990 班   級 23L0514 學 生 袁騁 指 導 教 師 史…

Java設計模式之設計原則

Java設計模式 Java設計模式主要原則是開閉原則&#xff0c;即對擴展開放&#xff0c;對修改關閉。由此衍生出5大原則&#xff1a;單一職責原則&#xff0c;里式替換原則&#xff0c;迪米特原則&#xff0c;接口隔離職責&#xff0c;依賴倒置原則。1、開閉原則 開閉原則&#x…