C++數據結構——哈希表

前言:本篇文章將繼續進行C++數據結構的講解——哈希表。


目錄

一.哈希表概念

二.哈希函數

1.除留取余法

三.哈希沖突

1.閉散列

線性探測

?(1)插入

(2)刪除

?2.?開散列

開散列概念

四.閉散列哈希表

1.基本框架?

2.插入

3.尋找

4.刪除

5.數據類型問題

五.開散列哈希表

1.基本框架

2.插入

3.尋找

4.刪除

總結


一.哈希表概念

順序結構以及平衡樹中,元素關鍵碼與其存儲位置之間沒有對應的關系,因此在查找一個元素時,必須要經過關鍵碼的多次比較。順序查找時間復雜度為O(N),平衡樹中為樹的高度,即O(log_2 N),搜索的效率取決于搜索過程中元素的比較次數
理想的搜索方法:可以不經過任何比較,一次直接從表中得到要搜索的元素
如果構造一種存儲結構,通過某種函數(hashFunc)使元素的存儲位置與它的關鍵碼之間能夠建立一一映射的關系,那么在查找時通過該函數可以很快找到該元素。

比如說:要統計一個全是小寫英文字母的字符串中各個英文字母出現的個數,該怎么做???

很容易,因為小寫英文字母有26個,所以我們直接創建一個大小為26的int數組并將值全部初始化為0讓數組的每一位都代表一個小寫英文字母該字母每出現一次,就讓該怎么對于位置的值++,最終每個位置的值便是對應小寫英文字母的個數

該方式即為哈希(散列)方法,哈希方法中使用的轉換函數稱為哈希(散列)函數,構造出來的結構稱為哈希表(Hash Table)(或者稱散列表)?


二.哈希函數

有時候我們可能不能像上邊一樣有多少種數據就創建多大的哈希表,比如說雖然我們只有1,12,103,1004,10005四個數要統計個數,難道要創建一個10005大小的數組嗎????


1.除留取余法

實際上只需要創建大小為6的數組即可,此時我們可以使用哈希函數:除留取余法

找到一個合適的除數,能夠讓上述數字取余之后分別為不同的值,從而拉進各個數字之間的距離,創建更小的哈希表

比如說讓上述數字均取余上10,得到的就會是1,2,3,4,5,剛好對應數組各個位置


三.哈希沖突

雖然上述函數可以解決大部分問題,但不妨有些時候會出現像1,11,111,1111這樣的數字,它們%10之后得到的數字均為1,這樣就會導致不同的值映射到相同的位置,從而導致哈希沖突

?那么為了解決哈希沖突,有兩種常用的方法:閉散列和開散列


1.閉散列

閉散列:也叫開放定址法,當發生哈希沖突時,如果哈希表未被裝滿,說明在哈希表中必然還有空位置,那么可以把key存放到沖突位置中的“下一個” 空位置中去。那如何尋找下一個空位置呢?


線性探測

線性探測:從發生沖突的位置開始,依次向后探測,直到尋找到下一個空位置為止。?

?(1)插入

通過哈希函數獲取待插入元素在哈希表中的位置

如果該位置中沒有元素則直接插入新元素,如果該位置中有元素發生哈希沖突,使用線性探測找到下一個空位置,插入新元素

(2)刪除

采用閉散列處理哈希沖突時,不能隨便物理刪除哈希表中已有的元素若直接刪除元素會影響其他元素的搜索。比如刪除元素4,如果直接刪除掉,44查找起來可能會受影響。因此線性探測采用標記的偽刪除法來刪除一個元素。

哈希表的每個空間都給上一個標記
EMPTY此位置空, EXIST此位置已經有元素, DELETE元素已經刪除,通過枚舉實現:
enum State{EMPTY, EXIST, DELETE};??


?2.?開散列

開散列概念

開散列法又叫鏈地址法(開鏈法),首先對關鍵碼集合用散列函數計算散列地址,具有相同地址的關鍵碼歸于同一子集合,每一個子集合稱為一個桶,各個桶中的元素通過一個單鏈表鏈接起來,各鏈表的頭結點存儲在哈希表中

從上圖可以看出,開散列中每個桶中放的都是發生哈希沖突的元素。?由于其形狀很像一個桶,所以開散列哈希表又叫哈希桶


四.閉散列哈希表

1.基本框架?

?下面來看代碼,我們先給出哈希表的基本框架:

namespace close_address
{enum State{EMPTY,EXIST,DELETE};template<class K,class V>struct HashData{pair<K, V> _kv;State _state = EMPTY;};template<class K, class V>class HashTable{public:private:vector<HashData<K, V>> _tables;size_t _n = 0;};
}

這里的_n用來記錄哈希表中數據的個數。?


2.插入

	bool Insert(const pair<K, V> kv){if (Find(kv.first))return false;size_t hashi = kv.first % _tables.size();//線性探測while (_tables[hashi]._state == EXIST){++hashi;hashi %= _tables.size();}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}

插入步驟還是容易實現的,值得注意的是,哈希表也不允許相同數據存在,所以需要提前判斷,這里直接調用后邊的Find()函數來判斷

除此之外還有一個非常關鍵的點在于——擴容

?因為雖然我們的哈希表是用vector作為底層,但是實際上填入數據時并一定是挨著存放的,所以我們需要在插入之前,提前創造空間

?那么哈希表也要等數據存滿的時候才擴容嗎???并不是。

對于散列表,存在一個荷載因子的定義:

α = 填入表中的元素 / 散列表的長度

當表中的元素足夠多但并未滿時,此時如果繼續插入數據,發生哈希沖突的可能性就會極大,導致插入時間變長。所以這里我們規定,當荷載因子的值 >= 0.7時就進行擴容

	HashTable(){_tables.resize(10);}

在擴容之前,應該給表一個初始的大小,這里通過構造函數使哈希表的初始長度為10

		//擴容if (_n * 10 / _tables.size() >= 7){HashTable<K, V> newHT;newHT.tables.resize(_tables.size() * 2);for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);}}_tables.swap(newHT._tables);}

隨后進行擴容判斷,這里有一個細節,?因為荷載因子是一個浮點數,如果直接進行比較,我們還需進行類型轉換,所以我們直接讓哈希表數據個數乘10再來進行計算。

判斷成立則進行擴容,注意,哈希表的擴容并非像順序表那樣進行復制粘貼,因為哈希表擴容,代表著哈希函數的分母發生變化,所以其對應的取余后的下標位置也會發生變化

這里我們通過新建一個二倍大小的哈希表遍歷原表數據并在新表中調用插入函數進行插入,最后在通過交換即可。


3.尋找

	//尋找HashData<K, V>* Find(const K& key){size_t hashi = key % _tables.size();//線性探測while (_tables[hashi]._state == EXIST &&_tables[hashi]._state != EMPTY){if (_tables[hashi]._kv.first == key){return &_tables[hashi];}++hashi;hashi %= _tables.size();}return nullptr;}

尋找就比較簡單了, 通過要尋找的值計算出其在哈希表中的下標判斷是否存在且是否為空存在且不為空則進行判斷,相等返回如果不相等,因為可能存在哈希沖突,所以循環往后遍歷,直至遇到空位置仍然沒有找到,說明其不在哈希表中,返回nullptr

這里有一個細節,返回值為哈希表中對應位置的地址,這是為后邊的刪除做伏筆。


4.刪除

	//刪除bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret == nullptr){return false;}else{ret->_state = DELETE;--_n;return true;}}

刪除我們直接借用Find函數去找到對應位置,無需在通過哈希函數去尋找。

如果要刪除的元素不存在,則返回失敗,反之,將其位置的標記改為DELETE,即偽刪除

下面來理解一下偽刪除

因為我們在尋找和插入時的判斷條件均為標記位EMPTY,所以刪除時只需將該位置的標記改為DELETE,這樣就不會影響該位置對應的沖突位的尋找以及新的插入了


5.數據類型問題

我們上述的哈希表實現能夠采用哈希函數進行取余操作的前提是數據為int類型,那如果是其他類型的數據,不能進行取余操作,又該如何使用哈希函數呢???

這里的方法是,通過建立仿函數將其他類型轉換成int類型

struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};

?此外,有一個特例,string類型不能直接轉換為int類型,所以想要滿足string類型,我們需要為其單獨創造一個仿函數

struct HashStringFunc
{size_t operator()(const string& s){size_t hash = 0;for (auto e : s){hash += e;}return hash;}
};

思想為,將string類型的每個字符的ascll碼值加起來作為其int類型的數據

隨后需要在模板中進行添加:

template<class K, class V,class Hash = HashFunc<K>>

這里采用了缺省參數默認情況下為其他類型,當為string類型時,在傳入獨有的模版。?


五.開散列哈希表

1.基本框架

namespace hash_bucket
{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;public:HashTable(){_tables.resize(10, nullptr);_n = 0;}private:vector<Node*> _tables;//指針數組size_t _n;};
}

不同于閉散列的哈希表vector中存放的是數據本身,哈希桶中vector存放的為節點指針


2.插入

因為哈希桶可能會在一個位置下面插入很多的數據如果采用尾插,就必須找到尾結點才能進行插入,效率會很低,所以我們采用頭插的方式:

		//插入bool Insert(const pair<K, V>& kv){//判斷是否存在if (Find(kv.first))return false;//擴容//......size_t hashi = kv.first % _tables.size();Node* newnode = new Node(kv);//頭插newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;}

創造一個新節點,讓新節點去指向原來的頭結點,再讓新節點成為頭結點

下面我們來關注擴容問題,雖然哈希桶是通過鏈表的方式進行插入,原則上不用進行擴容就可以滿足所有數據的存放。但是如果數據過大,會導致每個桶中的數據量過于龐大導致尋找操作的效率大大降低。所以規定,當數據個數等于哈希表的大小,即荷載因子α為1時,進行擴容

那么哈希桶的擴容又該如何進行呢???

因為是節點的緣故,如果像閉散列那樣復用插入去擴容,那樣就等于同一個節點又創建了一份,而原節點最后還需要進行銷毀,這樣未免過于麻煩和浪費時間。

所以更好一點的做法是,新建立一個哈希桶,遍歷原桶的節點,讓其按照哈希函數去直接指向新桶,最后再將兩個桶進行交換

			//擴容if (_n == _tables.size()){vector<Node*> newtables; (_tables.size() * 2, nullptr);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t hashi = kv.first % newtables.size();cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}_tables[i] = nullpter;}_tables.swap(newtables);}

3.尋找

		//尋找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;}

尋找操作較為簡單,就不在過多分享,注意返回值為節點類型


4.刪除

鏈表的刪除無非就三種情況,刪除的是頭結點,或者是中間節點,尾結點。其中尾結點可以和中間節點共用一種方式

那么如果刪除中間節點,就必須提前記錄該節點的前一個節點

此外就是頭結點,提前定義一個prev,并置空如果cur不為頭結點,就讓prev繼承cur,cur在往后,所以如果首次循環prev就為空,說明要刪除的節點即為頭結點。?

		bool Erase(const K& key){size_t hashi = key % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == key){//刪除的是第一個節點if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;}else{prev = cur;cur = cur->_next;}}return false;}

最后我們仍需使用仿函數來解決數據類型的問題,因方法與閉散列完全相同,這里不再重復


總結

關于哈希表就分享這么多,喜歡本篇文章記得一鍵三連,我們下期再見!

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

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

相關文章

場內期權怎么開戶?傭金手續費最低是多少?

今天期權懂帶你了解場內期權怎么開戶&#xff1f;傭金手續費最低是多少&#xff1f;我國的首個場內期權是50ETF期權&#xff0c;隨著投資者對期權產品日漸熟悉&#xff0c;投資者參與數量與交易量穩步增長。 場內期權怎么開戶&#xff1f; 滿足資金要求&#xff1a;根據監管要…

自動打卡腳本

奕輔導自動打卡腳本 打卡腳本&#xff0c;使用前需手動打卡一次并需要抓包&#xff0c;在其中找到相關的token。 # -*- encoding:utf-8 -*-import requests import jsonpunch_in_data {"questionnairePublishEntityId": "1001640744275339000980000000001&qu…

MyBatis:Parameter Maps collection does not contain value for 報錯解決收錄

MyBatis&#xff1a;Parameter Maps collection does not contain value for 報錯問題解決收錄 1.報錯收錄 后端測試時偶然遇到的用mybatis生成好的mapper文件&#xff0c;報Result Maps collection does not contain value…的錯誤 2.報錯分析 java.lang.ILledalAraumentEx…

必應bing國內廣告開戶首充和開戶費是多少?

微軟必應Bing作為國內領先的搜索引擎之一&#xff0c;其廣告平臺憑借其精準的投放、高效的數據分析和廣泛的用戶覆蓋&#xff0c;已成為眾多企業的首選。 根據最新政策&#xff0c;2024年必應Bing國內廣告開戶預充值金額設定為1萬元人民幣起。這一調整旨在確保廣告主在賬戶初始…

【嵌入式DIY實例】-OLED顯示DHT22傳感器數據

OLED顯示DHT22傳感器數據 1、應用實例介紹 本次實例將演示如何在OLED中顯示DHT22溫度濕度傳感器的數據。實例主要分兩步來完成: DHT22傳感器驅動,采集溫度和濕度OLED驅動,顯示采集到的溫度值和濕度值。在前面的文章中,對OLED的應用和驅動做了介紹,請參考: ESP8266-Ardu…

1.Nginx上配置 HTTPS

1.安裝 Nginx&#xff1a; 如果還沒有安裝 Nginx&#xff0c;可以使用包管理工具安裝。例如&#xff0c;在 Ubuntu 上&#xff1a; sudo apt update sudo apt install nginx2.上傳證書和私鑰文件&#xff1a; 將你的證書文件和私鑰文件上傳到服務器上的某個目錄&#xff0c;…

VBA宏指令寫的方法突然不能用了

背景:項目組有個自動化測試項目,實在excel中利用VBA開發的;時間比較久遠,我前面的哥們走后,這個軟件一直在用,最近系統不知道是不是更新的緣故;有些代碼除了問題; 先上源碼: Dim Conn As Object, Rst As Object Dim sqlStr$ Dim str_start_SN$, str2$ str_start_SN …

python 線性回歸模型

教材鏈接-3.2. 線性回歸的從零開始實現 c實現 該博客僅用于記錄一下自己的代碼&#xff0c;可與c實現作為對照 from d2l import torch as d2l import torch import random # nn是神經網絡的縮寫 from torch import nn from torch.utils import data# 加載訓練數據 # 加載訓…

什么是網關,網關有哪些作用?

網關(Gateway)是在計算機網絡中用于連接兩個獨立的網絡的設備&#xff0c;它能夠在兩個不同協議的網絡之間傳遞數據。在互聯網中&#xff0c;網關是一個可以連接不同協議的網絡的設備&#xff0c;比如說可以連接局域網和互聯網&#xff0c;它可以把局域網 的內部網絡地址轉換成…

論文閱讀--GLIP

把detection和phrase ground(對于給定的sentence&#xff0c;要定位其中提到的全部物體)這兩個任務合起來變成統一框架&#xff0c;從而擴展數據來源&#xff0c;因為文本圖像對的數據還是很好收集的 目標檢測的loss是分類loss定位loss&#xff0c;它與phrase ground的定位los…

爬蟲學習--11.MySQL數據庫的基本操作(上)

MySQL數據庫的基本操作 創建數據庫 我們可以在登陸 MySQL 服務后&#xff0c;使用命令創建數據庫&#xff0c;語法如下: CREATE DATABASE 數據庫名; 顯示所有的數據庫 show databases; 刪除數據庫 使用普通用戶登陸 MySQL 服務器&#xff0c;你可能需要特定的權限來創建或者刪…

Docker部署Minio小記

概述 因為工作需要搭建對象存儲的測試環境&#xff0c;故而使用Docker部署Minio&#xff0c;測試通過博文記錄用以備忘 步驟 拉取鏡像 docker pull minio/minio啟動容器 docker run -p 9000:9000 -p 9090:9090 \--name minio \-d --restartalways \-e "MINIO_ACCESS_K…

內臟油脂是什么?如何減掉?

真想減的人&#xff0c;減胖是很容易的&#xff0c;但想要形體美又健康&#xff0c;還是得從減內臟油脂開始&#xff0c;那么&#xff0c;問題來了&#xff0c;什么是內臟油脂&#xff1f; 油脂它分部于身體的各個角落&#xff0c;四肢、腹部、腰、臀部、臉、脖子...等&#xf…

VUE3+TS+elementplus創建table,純前端的table

一、前言 開始學習前端&#xff0c;直接從VUE3開始&#xff0c;從簡單的創建表格開始。因為自己不是專業的程序員&#xff0c;編程主要是為了輔助自己的工作&#xff0c;提高工作效率&#xff0c;VUE的基礎知識并不牢固&#xff0c;主要是為了快速上手&#xff0c;能夠做出一些…

Kubernetes中 Requests 和 Limits 的初步理解

1 靈魂拷問 我們在使用 Kubernetes 時是否遇到以下情況&#xff1a; 你會不會部署負載的時候將 CPU requests/limits 設置得過低或過高&#xff1f;你會不會部署負載的時候將 內存 requests/limits 設置得過低或過高&#xff1f;又或者你根本不設置 requests/limits&#xff…

SVN創建項目分支

目錄 背景調整目錄結構常規目錄結構當前現狀目標 調整SVN目錄調整目錄結構創建項目分支 效果展示 背景 當前自己本地做項目的時候發現對SVN創建項目不規范&#xff0c;沒有什么目錄結構&#xff0c;趁著創建目錄分支的契機&#xff0c;順便調整下SVN服務器上的目錄結構 調整目…

Stable Diffusion WebUI使用inpaint anything插件實現圖片局部重繪

Inpaint Anything是一個強大的圖像處理工具,它結合了SAM(Segment Anything Model)、圖像修補模型(如LaMa)和AIGC模型(如Stable Diffusion)等先進技術,以實現圖像中物體的移除、內容的填補以及場景的替換。無論是對圖像中的任何元素進行編輯,還是對圖像整體進行場景轉換…

【Vue】Vue2使用ElementUI

目錄 Element UI介紹特點Vue2使用Element安裝引入ElementUI組件庫 使用ElementUI用戶注冊列表展示其他 mint-ui介紹特點安裝組件引入組件Mint-ui相關組件 Element UI 介紹 官網(基于 Vue 2.x ):https://element.eleme.cn/#/zh-CN ElementUI 是一個基于 Vue.js 的桌面端組件庫…

Vue文本溢出如何自動換行

css新增 word-break: break-all; word-wrap: break-word;

【Linux系統】文件與基礎IO

本篇博客整理了文件與文件系統、文件與IO的相關知識&#xff0c;借由庫函數、系統調用、硬件之間的交互、操作系統管理文件的手段等&#xff0c;旨在讓讀者更深刻地理解“Linux下一切皆文件”。 【Tips】文件的基本認識 文件 內容 屬性。文件在創建時就有基本屬性&#xff0…