哈希 :哈希沖突、負載因子、哈希函數、哈希表、哈希桶

文章目錄

  • 哈希
  • 哈希(散列)函數
    • 常見的哈希函數
    • 字符串哈希函數
  • 哈希沖突
    • 閉散列(開放地址法)
    • 開散列(鏈地址法/拉鏈法)
    • 負載因子以及增容
      • 對于閉散列
    • 對于開散列結構
  • 具體實現
    • 哈希表(閉散列)
      • 創建
      • 插入
      • 查找
      • 刪除
      • 完整代碼
    • 哈希桶(開散列)
      • 插入
      • 查找
      • 刪除
      • 完整代碼


哈希

哈希(散列)是一種數據結構,通過散列算法將元素值轉換為散列值進行存儲。使得元素存儲的位置與元素本身建立起了映射關系,如果要查、改數據,就可以直接到對應的位置去,使得時間復雜度達到了 O(1) ,原因如下:

若結構中存在和 關鍵字K 相等的記錄,則必定在 f(K) 的存儲位置上。由此,不需比較便可直接取得所查記錄。

稱上面的對應關系 f散列函數(Hash function),按這個映射關系事先建立的表為散列表,這一映象過程稱為 散列造表散列 ,最終所得的存儲位置稱 散列地址


哈希(散列)函數

哈希函數用來建立元素與其存儲位置的映射關系。

對于哈希函數來說,必須具有以下特點:

  • 哈希函數的定義域必須包括需要存儲的全部關鍵碼,而如果散列表允許有 m 個地址時,其值域必須在 0m-1 之間。
  • 哈希函數計算出來的地址能均勻分布在整個空間中(防止產生密集的哈希沖突)

哈希沖突大量出現往往都是因為哈希函數設計的不夠合理,但是即使再優秀的哈希函數,也只能盡量減少哈希沖突的次數,無法避免哈希沖突。


常見的哈希函數

  1. 直接定址法(常見)
    哈希函數:Hash(Key) = A * Key + B
    這是最簡單的哈希函數,直接取關鍵字本身或者他的線性函數來作為散列地址。

  2. 除留余數法(常見)
    哈希函數 :Hash(key) = key % capacity
    幾乎是最常用的哈希函數,用一個數來對 key 取模,一般來說這個數都是容量。

  3. 平方取中法
    對關鍵字進行平方,然后取中間的幾位來作為地址。

  4. 折疊法
    折疊法是將關鍵字從左到右分割成位數相等的幾部分(最后一部分位數可以短些),然后將這幾部分疊加求和(去除進位),并按散列表表長,取后幾位作為散列地址。
    折疊法適合事先不需要知道關鍵字的分布,適合關鍵字位數比較多的情況.

  5. 隨機數法
    選擇一個隨機函數,取關鍵字的隨機函數值為它的哈希地址,即 H(key) = random(key),其中 random 為隨機數函數。通常應用于關鍵字長度不等時。

  6. 數學分析法
    分析一組數據,比如一組員工的出生年月日,這時我們發現出生年月日的前幾位數字大體相同,這樣的話,出現沖突的幾率就會很大,但是我們發現年月日的后幾位表示月份和具體日期的數字差別很大,如果用后面的數字來構成散列地址,則沖突的幾率會明顯降低。因此數字分析法就是找出數字的規律,盡可能利用這些數據來構造沖突幾率較低的散列地址。


字符串哈希函數

因為哈希函數的常用方法如直接定址、除留余數、平方取中等方法需要用的 key值整型,而大部分時候我們的 key 都是 string,由于無法對 string 進行算數運算,所以需要考慮新的方法。

常見的字符串哈希算法有 BKDSDBRS 等,這些算法大多通過一些公式來對字符串每一個 字符的 ascii值 或者 字符串的大小 進行計算,來推導出一個不容易產生沖突的 key值

例如 BKDHash

struct _Hash<std::string>
{const size_t& operator()(const std::string& key){// BKDR字符串哈希函數size_t hash = 0;for (size_t i = 0; i < key.size(); i++){hash *= 131;hash += key[i];}return hash;}
};

這里推薦兩篇文章,一篇具體對比各類字符串哈希函數的效率,一篇是實現:

  • 字符串Hash函數對比
  • 各種字符串Hash函數

哈希沖突

對不同的關鍵字可能得到同一散列地址,即 key1≠key2 ,而 f(key1)=f(key2) ,這種現象稱碰撞(哈希沖突)

哈希沖突使得多個數據映射的位置相同,但是每個位置又只能存儲一個數據,所以就需要通過某種方法來解決哈希沖突。

查找過程中,關鍵碼的比較次數,取決于產生沖突的多少,產生的沖突少,查找效率就高;產生的沖突多,查找效率就低。因此,影響產生沖突多少的因素,也就是影響查找效率的因素。影響產生沖突多少有以下三個因素:

  1. 散列函數是否均勻;
  2. 處理沖突的方法;
  3. 散列表的負載因子(裝填因子)。

第一點取決于上面講過的哈希函數,不再贅述,下面詳細講一下2、3點。

處理沖突的方法可分為兩類:開散列方法( open hashing,也稱為拉鏈法,separate chaining )閉散列方法( closed hashing,也稱為開地址方法,open addressing )。這兩種方法的不同之處在于:開散列法把發生沖突的關鍵碼存儲在散列表主表之外,而閉散列法把發生沖突的關鍵碼存儲在表中另一個槽內。


閉散列(開放地址法)

因為閉散列是順序的結構,所以可以通過遍歷哈希表,來將沖突的數據放到空的位置上。

  1. 線性探測
    線性探測即為從發生沖突的位置開始,依次向后探測,直到尋找到下一個空位置為止。
    這種方法實現起來極為簡單,但是效率也不高,因為如果同一位置產生了大量的哈希沖突,就會導致每次都在同一個位置進行探測,例如我在10這里連續沖突100次,此時所有探測的次數加起來就會高達100!

  2. 二次探測
    二次探測即為從發生沖突的位置開始,每次往后探測 ±k2(k<=m/2,m為散列表長) 個位置,如:12,-(12),22,-(22) 等。
    這樣的話就將每次探測的效率從 O(N) 提升到了 O(logN) ,即使有著大量的沖突堆積,也不會導致效率過低。

  3. 偽隨機數探測
    這種方法并不常見,實現方法是:創建一個偽隨機數序列,根據序列內容決定每次往后探測的長度。


開散列(鏈地址法/拉鏈法)

先用哈希函數計算每個數據的散列地址,把具有相同地址的元素歸于同一個集合之中,把該集合處理為一個鏈表,鏈表的頭節點存儲于哈希表之中。

在這里插入圖片描述

鏈地址法在每一個映射位置都建立起一個鏈表(數據過多時可能會轉為建立紅黑樹),將每次插入的數據都直接連接上這個鏈表,這樣就不會像閉散列一樣進行大量的探測,但是如果鏈表過長也會導致效率低下。


負載因子以及增容

哈希沖突出現的較為密集,往往代表著此時數據過多,而能夠映射的地址過少,而要想解決這個問題,就需要通過 負載因子(裝填因子) 的判斷來進行增容。

負載因子的大小 = 表中數據個數 / 表的容量(長度)

對于閉散列

對于閉散列來說,因為其是一種線性的結構,所以一旦負載因子過高,就很容易出現哈希沖突的堆積,所以當負載因子達到一定程度時就需要進行增容,并且增容后,為了保證映射關系,還需要將數據重新映射到新位置。

經過算法科學家的計算, 負載因子應當嚴格的控制在 0.7-0.8 以下,所以一旦負載因子到達這個范圍,就需要進行增容。

因為除留余數法等方法通常是按照表的容量來計算,所以科學家的計算,當對一個質數取模時,沖突的幾率會大大的降低,并且因為增容的區間一般是 1.5-2 倍,所以算法科學家列出了一個增容質數表,按照這樣的規律增容,沖突的幾率會大大的降低。

這也是 STLunordered_map/unordered_set 使用的增容方法。

//算法科學家總結出的一個增容質數表,按照這樣增容的效率更高const int PRIMECOUNT = 28;const size_t primeList[PRIMECOUNT] = {53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,1610612741ul, 3221225473ul, 4294967291ul};

hashmap 的負載因子為什么默認是 0.75

比如說當前的容器容量是 16,負載因子是 0.75,16*0.75=12,也就是說,當容量達到了 12 的時候就會進行擴容操作。而負載因子定義為 0.75 的原因是:

  • 當負載因子是 1.0 的時候,也就意味著,只有當散列地址全部填充了,才會發生擴容。意味著隨著數據增長,最后勢必會出現大量的沖突,底層的紅黑樹變得異常復雜。雖然空間利用率上去了,但是查詢時間效率降低了。
  • 負載因子是 0.5 的時候,這也就意味著,當數組中的元素達到了一半就開始擴容。雖然時間效率提升了,但是空間利用率降低了。 誠然,填充的元素少了,Hash沖突也會減少,那么底層的鏈表長度或者是紅黑樹的高度就會降低。查詢效率就會增加。但是,這時候空間利用率就會大大的降低,原本存儲 1M 的數據,現在就意味著需要 2M 的空間。

對于開散列結構

因為哈希桶是開散列的鏈式結構,發生了哈希沖突是直接在對應位置位置進行頭插,而桶的個數是固定的,而插入的數據會不斷增多,隨著數據的增多,就可能會導致某一個桶過重,使得效率過低。

所以最理想的情況,就是每個桶都有一個數據。這種情況下,如果往任何一個地方插入,都會產生哈希沖突,所以當數據個數與桶的個數相同時,也就是負載因子為 1 時就需要進行擴容。


具體實現

哈希表(閉散列)

創建

對于閉散列,我們需要通過狀態來記錄一個數據是否在表中,所以這里會使用枚舉來實現。

enum State
{EMPTY,//空EXITS,//存在DELETE,//已經刪除
};template<class T>
struct HashData
{HashData(const T& data = T(), const State& state = EMPTY): _data(data), _state(state){}T _data;State _state;
};

插入

插入的思路很簡單,計算出映射的地址后,開始遍歷判斷下面幾種狀態:

  • 如果映射位置已存在數據,并且值與當前數據不同,則說明產生沖突,繼續往后查找
  • 如果映射位置的數據與插入的數據相同,則說明此時數據已經插入過,此時就不需要再次插入
  • 如果映射位置的狀態為刪除或者空,則代表著此時表中沒有這個數據,在這個位置插入即可
bool Insert(const T& data)
{KeyOfT koft;//判斷此時是否需要增容//當裝填因子大于0.7時增容if (_size * 10 / _table.size() >= 7){//增容的大小按照別人算好的近似兩倍的素數來增,這樣效率更高,也可以直接2倍或者1.5倍。std::vector<HashData> newTable(getNextPrime(_size));for (size_t i = 0; i < _table.size(); i++){//將舊表中的數據全部重新映射到新表中if (_table[i]._state == EXITS){//如果產生沖突,則找到一個合適的位置size_t index = HashFunc(koft(_table[i]._data));while (newTable[i]._state == EXITS){i++;if (i == _table.size()){i = 0;}}newTable[i] = _table[i];}}//最后直接將數據進行交換即可,原來的數據會隨著函數棧幀一起銷毀_table.swap(newTable);}//用哈希函數計算出映射的位置size_t index = HashFunc(koft(data));//從那個位置開始探測, 如果該位置已經存在時,有兩種情況,一種是已經存在,一種是沖突,這里使用的是線性探測while (_table[index]._state == EXITS){//如果已經存在了,則說明不用插入if (koft(_table[index]._data) == koft(data)){return false;}else{index++;index = HashFunc(index);}}//如果走到這里,說明這個位置是空的或者已經被刪除的位置,可以在這里插入_table[index]._data = data;_table[index]._state = EXITS;_size++;return true;
}

查找

查找也分幾種情況

  • 如果映射的位置為空,則說明查找失敗。
  • 如果映射的位置的數據不同,則說明產生沖突,繼續向后查找
  • 如果映射的位置的數據相同,如果狀態為刪除,則說明數據已經刪除,查找失敗;而如果數據為存在,則說明查找成功。
HashData* Find(const K& key)
{KeyOfT koft;size_t index = HashFunc(key);//遍歷,如果查找的位置為空,則說明查找失敗while (_table[index]._state != EMPTY){//此時判斷這個位置的數據是否相同,如果不同則說明出現哈希沖突,繼續往后查找if (koft(_table[index]._data) == key){//此時有兩個狀態,一種是數據已經被刪除,一種是數據存在。if (_table[index]._state == EXITS){return &_table[index];}else if (_table[index]._state == DELETE){return nullptr;}}index++;//如果index越界,則歸零if (index == _table.size()){index = 0;}}return nullptr;
}

刪除

直接遍歷查找數據,如果找不到則說明已經被刪除,如果找到了則直接將狀態改為刪除即可。

bool Erase(const K& key)
{HashData* del = Find(key);//如果找不到則說明已經被刪除if (del == nullptr){return false;}else{//找到了則直接更改狀態即可del->_state = DELETE;_size--;return true;}
}

完整代碼

#pragma once
#include<vector>namespace lee
{//算法科學家總結出的一個增容質數表,按照這樣增容的效率更高const int PRIMECOUNT = 28;const size_t primeList[PRIMECOUNT] = {53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,1610612741ul, 3221225473ul, 4294967291ul};enum State{EMPTY,EXITS,DELETE,};template<class T>struct HashData{HashData(const T& data = T(), const State& state = EMPTY): _data(data), _state(state){}T _data;State _state;};template<class K, class T, class KeyOfT>class HashTable{public:typedef HashData<T> HashData;HashTable(size_t capacity = 10): _table(capacity), _size(0){}size_t getNextPrime(size_t num){size_t i = 0;for (i = 0; i < PRIMECOUNT; i++){//返回比那個數大的下一個質數 if (primeList[i] > num){return primeList[i];}}//如果比所有都大,還是返回最后一個,因為最后一個已經是32位最大容量return primeList[PRIMECOUNT - 1];}//除留余數法size_t HashFunc(const K& key){return key % _table.size();}bool Insert(const T& data){KeyOfT koft;//判斷此時是否需要增容//當裝填因子大于0.7時增容if (_size * 10 / _table.size() >= 7){//增容的大小按照別人算好的近似兩倍的素數來增,這樣效率更高,也可以直接2倍或者1.5倍。std::vector<HashData> newTable(getNextPrime(_size));for (size_t i = 0; i < _table.size(); i++){//將舊表中的數據全部重新映射到新表中if (_table[i]._state == EXITS){//如果產生沖突,則找到一個合適的位置size_t index = HashFunc(koft(_table[i]._data));while (newTable[i]._state == EXITS){i++;if (i == _table.size()){i = 0;}}newTable[i] = _table[i];}}//最后直接將數據進行交換即可,原來的數據會隨著函數棧幀一起銷毀_table.swap(newTable);}//用哈希函數計算出映射的位置size_t index = HashFunc(koft(data));//從那個位置開始探測, 如果該位置已經存在時,有兩種情況,一種是已經存在,一種是沖突,這里使用的是線性探測while (_table[index]._state == EXITS){//如果已經存在了,則說明不用插入if (koft(_table[index]._data) == koft(data)){return false;}else{index++;index = HashFunc(index);}}//如果走到這里,說明這個位置是空的或者已經被刪除的位置,可以在這里插入_table[index]._data = data;_table[index]._state = EXITS;_size++;return true;}HashData* Find(const K& key){KeyOfT koft;size_t index = HashFunc(key);//遍歷,如果查找的位置為空,則說明查找失敗while (_table[index]._state != EMPTY){//此時判斷這個位置的數據是否相同,如果不同則說明出現哈希沖突,繼續往后查找if (koft(_table[index]._data) == key){//此時有兩個狀態,一種是數據已經被刪除,一種是數據存在。if (_table[index]._state == EXITS){return &_table[index];}else if (_table[index]._state == DELETE){return nullptr;}}index++;//如果index越界,則歸零if (index == _table.size()){index = 0;}}return nullptr;}bool Erase(const K& key){HashData* del = Find(key);//如果找不到則說明已經被刪除if (del == nullptr){return false;}else{//找到了則直接更改狀態即可del->_state = DELETE;_size--;return true;}}private:std::vector<HashData> _table;size_t _size;};
};

哈希桶(開散列)

開散列也叫哈希桶,桶為每一個映射的位置,桶一般用鏈表或者紅黑樹實現(這里我用的是鏈表)。當我們通過映射的地址,找到存放數據的桶,再對桶進行插入或者刪除操作即可。

插入

通過計算映射位置找到對應的桶,再判斷數據是否存在后將數據頭插進去即可(也可以尾插)

bool Insert(const T& data)
{KeyofT koft;/*因為哈希桶是開散列的鏈式結構,發生了哈希沖突是直接在對應位置位置進行頭插,而桶的個數是固定的,而插入的數據會不斷增多,隨著數據的增多,就可能會導致某一個桶過重,使得效率過低。所以最理想的情況,就是每個桶都有一個數據。這種情況下,如果往任何一個地方插入,都會產生哈希沖突,所以當數據個數與桶的個數相同時,也就是負載因子為1時就需要進行擴容。*/if (_size == _table.size()){//按照素數表來增容size_t newSize = getNextPrime(_table.size());size_t oldSize = _table.size();std::vector<Node*> newTable(newSize);_table.resize(newSize);//接著將數據重新映射過去for (size_t i = 0; i < oldSize; i++){Node* cur = _table[i];while (cur){//重新計算映射的位置size_t pos = HashFunc(koft(cur->_data));//找到位置后頭插進對應位置Node* next = cur->_next;cur->_next = newTable[pos];newTable[pos] = cur;cur = next;}//原數據置空_table[i] == nullptr;}//直接和新表交換,交換過去的舊表會和函數棧幀一塊銷毀。_table.swap(newTable);}size_t pos = HashFunc(koft(data));Node* cur = _table[pos];//因為哈希桶key值唯一,如果已經在桶中則返回falsewhile (cur){if (koft(cur->_data) == koft(data)){return false;}else{cur = cur->_next;}}//檢查完成,此時開始插入,這里選擇的是頭插,這樣就可以減少數據遍歷的次數。Node* newNode = new Node(data);newNode->_next = _table[pos];_table[pos] = newNode;++_size;return true;
}

查找

直接根據映射的位置到桶中查找數據即可

Node* Find(const K& key)
{KeyofT koft;size_t pos = HashFunc(key);Node* cur = _table[pos];while (cur){if (koft(cur->_data) == key){return cur;}else{cur = cur->_next;}}return nullptr;
}

刪除

bool Erase(const K& key)
{KeyofT koft;size_t pos = HashFunc(key);Node* cur = _table[pos];Node* prev = nullptr;while (cur){if (koft(cur->_data) == key){//如果要刪除的是第一個節點,就讓下一個節點成為新的頭節點,否則直接刪除。if (prev == nullptr){_table[pos] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_size;return true;}else{prev = cur;cur = cur->_next;}}return false;
}

完整代碼

#pragma once
#include<vector>
#include<string>namespace lee
{//算法科學家總結出的一個增容質數表,按照這樣增容的效率更高const int PRIMECOUNT = 28;const size_t primeList[PRIMECOUNT] = {53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,1610612741ul, 3221225473ul, 4294967291ul};/*因為哈希函數的常用方法如直接定地、除留余數、平方取中等方法需要用的key值為整型,而大部分時候我們的key都是string,或者某些自定義類型,這個時候就可以提供一個仿函數的接口給外部,讓他自己處理如何將key轉換成我們需要的整型*/template<class K>struct Hash{const K& operator()(const K& key){return key;}};template<>struct Hash<std::string>{const size_t & operator()(const std::string& key){//BKDR字符串哈希函數size_t hash = 0;for (size_t i = 0; i < key.size(); i++){hash *= 131;hash += key[i];}return hash;}};template<class T>struct HashNode{HashNode(const T& data = T()): _data(data), _next(nullptr){}T _data;HashNode<T>* _next;};template<class K, class T, class KeyofT, class Hash = Hash<K>>class HashBucket{public:typedef HashNode<T> Node;HashBucket(size_t capacity = 10): _table(capacity), _size(0){}~HashBucket(){Clear();}size_t getNextPrime(size_t num){size_t i = 0;for (i = 0; i < PRIMECOUNT; i++){//返回比那個數大的下一個質數 if (primeList[i] > num){return primeList[i];}}//如果比所有都大,還是返回最后一個,因為最后一個已經是32位最大容量return primeList[PRIMECOUNT - 1];}size_t HashFunc(const K& key){Hash hash;return hash(key) % _table.size();}bool Insert(const T& data){KeyofT koft;/*因為哈希桶是開散列的鏈式結構,發生了哈希沖突是直接在對應位置位置進行頭插,而桶的個數是固定的,而插入的數據會不斷增多,隨著數據的增多,就可能會導致某一個桶過重,使得效率過低。所以最理想的情況,就是每個桶都有一個數據。這種情況下,如果往任何一個地方插入,都會產生哈希沖突,所以當數據個數與桶的個數相同時,也就是負載因子為1時就需要進行擴容。*/if (_size == _table.size()){//按照素數表來增容size_t newSize = getNextPrime(_table.size());size_t oldSize = _table.size();std::vector<Node*> newTable(newSize);_table.resize(newSize);//接著將數據重新映射過去for (size_t i = 0; i < oldSize; i++){Node* cur = _table[i];while (cur){//重新計算映射的位置size_t pos = HashFunc(koft(cur->_data));//找到位置后頭插進對應位置Node* next = cur->_next;cur->_next = newTable[pos];newTable[pos] = cur;cur = next;}//原數據置空_table[i] == nullptr;}//直接和新表交換,交換過去的舊表會和函數棧幀一塊銷毀。_table.swap(newTable);}size_t pos = HashFunc(koft(data));Node* cur = _table[pos];//因為哈希桶key值唯一,如果已經在桶中則返回falsewhile (cur){if (koft(cur->_data) == koft(data)){return false;}else{cur = cur->_next;}}//檢查完成,此時開始插入,這里選擇的是頭插,這樣就可以減少數據遍歷的次數。Node* newNode = new Node(data);newNode->_next = _table[pos];_table[pos] = newNode;++_size;return true;}bool Erase(const K& key){KeyofT koft;size_t pos = HashFunc(key);Node* cur = _table[pos];Node* prev = nullptr;while (cur){if (koft(cur->_data) == key){//如果要刪除的是第一個節點,就讓下一個節點成為新的頭節點,否則直接刪除。if (prev == nullptr){_table[pos] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_size;return true;}else{prev = cur;cur = cur->_next;}}return false;}Node* Find(const K& key){KeyofT koft;size_t pos = HashFunc(key);Node* cur = _table[pos];while (cur){if (koft(cur->_data) == key){return cur;}else{cur = cur->_next;}}return nullptr;}void Clear(){//刪除所有節點for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_table[i] = nullptr;}}private:std::vector<Node*> _table;size_t _size;};
};

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

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

相關文章

C++ 泛型編程(一):模板基礎:函數模板、類模板、模板推演成函數的機制、模板實例化、模板匹配規則

文章目錄泛型編程函數模板函數模板實例化隱式實例化顯式實例化函數模板的匹配規則類模板類模板的實例化泛型編程 泛型編程旨在削減重復工作&#xff0c;如&#xff1a; 將一個函數多次重載不如將他寫成泛型。 void Swap(int& left, int& right) {int temp left;lef…

你真的了解靜態變量、常量的存儲位置嗎?

文章目錄引言C對內存的劃分如何落實在Linux上自由存儲區和堆之間的問題棧常量區靜態存儲區靜態局部變量靜態局部變量、靜態全局變量、全局變量的異同macOS系統的測試結果總結引言 在動態內存的博客中&#xff0c;我提到&#xff1a; 在Linux 內存管理的博客中&#xff0c;我提…

C++ 泛型編程(二):非類型模板參數,模板特化,模板的分離編譯

文章目錄非類型模板參數函數模板的特化類模板的特化全特化偏特化部分參數特化參數修飾特化模板分離編譯解決方法非類型模板參數 模板的參數分為兩種&#xff1a; 類型參數&#xff1a; 則是我們通常使用的方式&#xff0c;就是在模板的參數列表中在 class 后面加上參數的類型…

Java操作——獲取文件擴展名,去掉文件擴展名

昨天收郵件&#xff0c;得知要參加一個產品部的會議&#xff0c;猜想&#xff0c;也許是因為我做的這個產品demo問題。于是昨天忙活到凌晨3點半&#xff0c;結果早上一來才知道又被調戲了。發郵件的MM把郵件誤發給我了。悲催啊有木有&#xff0c;困啊有木有&#xff01;自己還是…

數據結構 | B樹、B+樹、B*樹

文章目錄搜索結構B樹B樹的插入B樹的遍歷B樹的性能B樹B樹的插入B樹的遍歷B*樹B*樹的插入總結搜索結構 如果我們有大量的數據需要永久存儲&#xff0c;就需要存儲到硬盤之中。但是硬盤的訪問速度遠遠小于內存&#xff0c;并且由于數據量過大&#xff0c;無法一次性加載到內存中。…

MySQL 索引 :哈希索引、B+樹索引、全文索引

文章目錄索引引言常見的索引哈希索引自適應哈希索引B樹索引聚集索引非聚集索引使用方法聯合索引最左前綴匹配規則覆蓋索引全文索引使用方法索引 引言 為什么需要索引&#xff1f; 倘若不使用索引&#xff0c;查找數據時&#xff0c;MySQL必須遍歷整個表。而表越大&#xff0c;…

服裝店怎么引流和吸引顧客 服裝店鋪收銀系統來配合

實體店的同城引流和經營是實體經濟的一個重要的一環&#xff0c;今天我們來分享服裝行業的實體店鋪怎么引流和吸引、留住顧客&#xff0c;并實現復購。大家點個收藏&#xff0c;不然劃走就再也找不到了&#xff0c;另外可以點個關注&#xff0c;下次有新的更好的招&#xff0c;…

約瑟夫環(丟手絹問題)

文章目錄問題描述思路代碼實現問題描述 有 1~N 個數字&#xff0c;從 1~m 依次報數&#xff0c;數到 m 的數字要被刪掉&#xff0c;求最后剩下的數字是&#xff1f; 思路 第一次報數第二次報數1n-m12n-m2……m-2n-2m-1n-1m被刪掉了m11m22……n-1n-1-mnn-m 通過上面的表格&…

MySQL 鎖的相關知識 | lock與latch、鎖的類型、簡談MVCC、鎖算法、死鎖、鎖升級

文章目錄lock與latch鎖的類型MVCC一致性非鎖定讀&#xff08;快照讀&#xff09;一致性鎖定讀&#xff08;當前讀&#xff09;鎖算法死鎖鎖升級lock與latch 在了解數據庫鎖之前&#xff0c;首先就要區分開 lock 和 latch。在數據庫中&#xff0c;lock 和 latch 雖然都是鎖&…

Hibernate使用原生SQL適應復雜數據查詢

HQL盡管容易使用&#xff0c;但是在一些復雜的數據操作上功能有限。特別是在實現復雜的報表統計與計算&#xff0c;以及多表連接查詢上往往無能為力&#xff0c;這時可以使用SQL&#xff08;Native SQL&#xff09;實現HQL無法完成的任務。 1、使用SQL查詢 使用SQL查詢可以通過…

MySQL 存儲引擎 | MyISAM 與 InnoDB

文章目錄概念innodb引擎的4大特性索引結構InnoDBMyISAM區別表級鎖和行級鎖概念 MyISAM 是 MySQL 的默認數據庫引擎&#xff08;5.5版之前&#xff09;&#xff0c;但因為不支持事務處理而被 InnoDB 替代。 然而事物都是有兩面性的&#xff0c;InnoDB 支持事務處理也會帶來一些…

MySQL 事務 | ACID、四種隔離級別、并發帶來的隔離問題、事務的使用與實現

文章目錄事務ACID并發帶來的隔離問題幻讀&#xff08;虛讀&#xff09;不可重復讀臟讀丟失更新隔離級別Read Uncommitted (讀未提交)Read Committed (讀已提交)Repeatable Read (可重復讀)Serializable (可串行化)事務的使用事務的實現Redoundo事務 事務指邏輯上的一組操作。 …

MySQL 備份與主從復制

文章目錄備份主從復制主從復制的作用備份 根據備份方法的不同&#xff0c;備份可劃分為以下幾種類型&#xff1a; 熱備(Hot Backup) &#xff1a; 熱備指的是在數據庫運行的時候直接備份&#xff0c;并且對正在運行的數據庫毫無影響&#xff0c;這種方法在 MySQL 官方手冊中又…

C++ 流的操作 | 初識IO類、文件流、string流的使用

文章目錄前言IO頭文件iostreamfstreamsstream流的使用不能拷貝或對 IO對象 賦值條件狀態與 iostate 類型輸出緩沖區文件流fstream類型文件模式文件光標函數tellg() / tellp()seekg() / seekp()向文件存儲內容/讀取文件內容string流istringstreamostringstream前言 我們在使用 …

Hibernate 更新部分更改的字段 hibernate update

Hibernate 中如果直接使用 Session.update(Object o);或則是Session.updateOrUpdate(Object o); 會把這個表中的所有字段更新一遍。 如&#xff1a; ExperClass4k e new ExperClass4k(); e.setTime(time); e.setQ_num(q_num); e.setK(k); if (str "finch_fix")…

C++ 類的行為 | 行為像值的類、行為像指針的類、swap函數處理自賦值

文章目錄概念行為像值的類行為像指針的類概念引用計數動態內存實現計數器類的swap概念swap實現自賦值概念 行為像值的類和行為像指針的類這兩種說法其實蠻拗口的&#xff0c;這也算是 《CPrimer》 翻譯的缺點之一吧。。。 其實兩者的意思分別是&#xff1a; 行為像值的類&am…

C++ 右值引用 | 左值、右值、move、移動語義、引用限定符

文章目錄C11為什么引入右值&#xff1f;區分左值引用、右值引用move移動語義移動構造函數移動賦值運算符合成的移動操作小結引用限定符規定this是左值or右值引用限定符與重載C11為什么引入右值&#xff1f; C11引入了一個擴展內存的方法——移動而非拷貝&#xff0c;移動較之拷…

且談關于最近軟件測試的面試

前段時間有新的產品需要招人&#xff0c;安排和參加了好幾次面試&#xff0c;下面就談談具體的面試問題&#xff0c;在面試他人的同時也面試自己。 面試問題是參與面試同事各自設計的&#xff0c;我也不清楚其他同事的題目&#xff0c;就談談自己設計的其中2道題。 過去面試總是…

C++ 多態 | 虛函數、抽象類、虛函數表

文章目錄多態虛函數重寫重定義&#xff08;參數不同&#xff09;協變&#xff08;返回值不同&#xff09;析構函數重寫&#xff08;函數名不同&#xff09;final和override重載、重寫、重定義抽象類多態的原理虛函數常見問題解析虛函數表多態 一種事物&#xff0c;多種形態。換…

C++ 運算符重載(一) | 輸入/輸出,相等/不等,復合賦值,下標,自增/自減,成員訪問運算符

文章目錄輸出運算符<<輸入運算符>>相等/不等運算符復合賦值運算符下標運算符自增/自減運算符成員訪問運算符輸出運算符<< 通常情況下&#xff0c;輸出運算符的第一個形參是一個 非常量ostream對象的引用 。之所以 ostream 是非常量是因為向流寫入內容會改變…