資料結構與演算法之字典與集合
字典的解釋
字典:就好像我們所知的新華字典、中英文字典等等,字典這個詞的含義就是為字詞提供解釋,在資料結構中的字典給我們提供了什麼的解釋呢?我們去百度去搜索一下:
可以發現,我們在百度輸入框中輸入一段搜尋文字,就能得到對應的結果。我們可以稱百度是一個巨大的“字典”,它能給我們帶來一段關鍵字的解釋。在整個過程中,是(關鍵字->解釋)這樣的一個對映關係,這就是字典的基礎結構,字典也稱為(查詢表、對映或關聯表)常見的用法是(key->value)。在Python語言中dict就是一個典型的字典結構,Java語言中Map結構也是一個字典結構,可以通過Python中的dict或Java中的Map來學習和使用字典結構。
為什麼需要字典?
在我們日常生活中,字典關係處處可見,比如上學時給每個同學的學號,工作時給每位同事的工號等,如果沒有這樣的一個關係對映,那麼在查詢時將會消耗多餘時間,而這是難接受的一種現象。在計算機中,資料的儲存與檢索操作很頻繁,迭代方式效率低,所以想到能不能在檢索資料結構時,所提供的關鍵字(或關鍵碼)作為下標,然後所需要得到的資訊作為儲存內容,(關鍵碼->儲存內容)這樣的下標對映如果存在,那麼我們通過關鍵碼去拿資訊將會是常量時間。
字典分類
靜態字典:只用建立一次,建立之後,字典內容和結構都不再變化,主要操作是檢索。
動態字典:在初始建立之後,字典的內容和結構將一直處於變動之中。除了檢索操作之外,重要的還有資料項的插入和刪除。
字典怎麼實現?
從結構上來,字典就是key-value結構的彙集,可以使用Python中的一個tuple(二元組)來定義字典中元素的結構,或者定義一個結點類,裡面有key或value兩個屬性來定義字典中元素的結構。字典可以通過其他一些資料結構作為基礎來實現。比如順序表。
順序表實現字典
實現程式碼:略
實現總結:實現比較簡單,就是一個數組,數組裡的每個元素是一個二元組,[(key1, value1),(key2, value2)...],類似此種結構,檢索時,只能通過給定的關鍵字來遍歷陣列然後對比key來查詢元素,刪除元素時也需要先遍歷再刪除並移動元素。檢索、刪除操作效率不高,但實現簡單。
順序表實現字典2(有序順序表和二分查詢)
以上通過順序表來實現字典時,檢索、刪除不高的原因在於字典中的二元組儲存是亂的,如果二元組按順序儲存,那麼當我們需要定位某個元素(關鍵碼)時,就可以通過二分查詢的演算法來快速定位。
實現程式碼:略
實現總結:通過有序的順序表來實現字典,在檢索時速度快,為O(logn)的時間複雜度。對於插入、刪除元素時,檢索到要插入位置與刪除元素的位置時是O(logn)時間,但是插入元素後或刪除元素後都要維護字典原有的結構,都需要去移動元素,所以時間還是O(n)。
順序表實現字典適應場景:字典規模比較小,且不常進行動態變化的一些字典。
散列表實現字典
雜湊:將關鍵字key轉換為儲存資料的地址(下標)的方式叫做雜湊。此種轉換函式稱為雜湊函式或雜湊(hash)函式。
前面提過,最快能訪問到關鍵字對應的資訊的方法就是將關鍵字作為地址下標,那麼我們在獲取資料的時候就是O(1)時間。而關鍵字作為地址下標的操作稱之為雜湊。
雜湊技術的實現:我們將雜湊函式記做h(),對於任何一個key都有對應的下標index=h(key),而對應的下標是我們已經選定的一塊儲存區域地址,比如0-19,對於大資料量的key,我們都需要將h(key)對映到(0-19)這個範圍內。這是一種大範圍到小範圍的對映,肯定會產生不同的key對映到同一個下標的情況,這種情況稱之為雜湊衝突:當一個散列表中負載因子越大,發生雜湊衝突的機率也越大,而且衝突具有不知何時發生的不確定性與隨時都可能發生的確定性。所以必須要有處理雜湊衝突的方法。
雜湊函式的設計:雜湊函式的選擇好壞決定於雜湊衝突發生的機率,雜湊函式選擇需要遵循以下幾點:
1、函式能將關鍵碼對映到儲存地址區間(值域index)中儘可能大的部分,也就是說雜湊函式最好能將儲存地址區間index裡的每個下標都能一一對映到,避免有的下標對映不上,浪費。
2、儲存地址區間中的雜湊值應該均勻分佈。
3、函式簡單
對於整數的關鍵碼若干雜湊方法:數字分析法(就是選取整數關鍵碼的某些位置的數字作為下標),摺疊法(將數字分為幾段,並將它們進行運算,運算的結果去掉進位等)、中平方法(求出關鍵碼的平方,然後取出中間的幾位作為雜湊值)
常用雜湊函式:
1、除餘法:將整數值除於儲存地址區間index的範圍值,得到的餘數作為雜湊地址,比如一段整數值為5,7,11,44,將它們除以index範圍值為10(index=[0-9]),得到的雜湊地址為5/10=5,7/10=7,11/10=1,44/10=4 => 5,7,1,4
2、基數轉換法:將關鍵碼看做r進位制的數,然後將關鍵碼轉換為十進位制或二進位制
3、非整數的轉換:先轉換成整數,然後使用基數轉換法或除餘法進行得到雜湊地址
衝突消解機制:
衝突的內消解:開地址技術:
內消解:在儲存區的內部解決衝突問題。
開地址法:基本思想是:當遇到衝突時,為所衝突的元素再查詢一個合適的位置,查詢的過程稱之為探查方式。
探查方式分為兩種:
1、線性探查:當遇到衝突時,為衝突元素查詢合適位置時,是一步步向前後向後探測,每次探測距離是線性的。
2、雙雜湊探查:當遇到衝突時,為衝突元素查詢合適位置時,再進行一次雜湊來確定新的位置。
衝突的外消解:鏈地址技術
外消解的方法有兩種:
1、溢位區方法:另外設定一個溢位區,當發生衝突時將元素儲存在溢位區,在溢位區裡面的元素順序儲存,當在散列表中找不到元素時,就到溢位區中查詢,但是一旦溢位區擴大,字典的效能將趨向於線性。
2、桶雜湊(鏈地址法、拉鍊法):就是在散列表中不直接儲存關鍵碼對應的資料,而是儲存了另一個連結串列結構,連結串列結構是另一個表,裡面存的是衝突關鍵碼的所有結點。桶雜湊可用於大型字典,用於組織大量的資料,包括外存檔案等。
集合的解釋
集合:個體的彙集。在數學中集合是一個很重要的概念,它有三種特性:
1、確定性:集合中的元素是明確的
2、互異性:集合中任何兩個元素都是不相同的。
3、無序性
集合有三種重要的運算操作應用很廣:
求並集:比如兩個集合S與T,S與T的並集中的元素是S的元素或者是T的元素。
求交集:比如兩個集合S與T,S與T的交集中的元素既是S的元素,也是T的元素。
求差集:比如兩個集合S與T,S與T的差集中的元素表示僅屬於S不屬於T的元素,T與S的差集表示僅屬於T不屬於S的元素。
集合的實現
集合是一種彙集的資料結構,可以使用一些彙集型的資料結構比如順序表、連結串列或字典。
簡單順序表實現集合
總結:一些基本的操作,比如判斷元素是否在集合中,往集合插入元素(保證唯一性),刪除某個元素都是O(n)時間,重要操作,求並交差集為O(m*n)時間。效能上較差。
排序順序表實現集合
總結:相較於簡單順序表,檢索方面的效能有所提升,為O(logn)時間,重要操作,求並交差集為O(m+n)時間。效能上有較大提升
散列表實現集合
效能:在散列表還沒有較大沖突的情況下,各種操作都很高效。求並差交集也有O(m+n)的高效時間。
為什麼用散列表實現集合比較好?
相比最高效,將集合中的元素作為散列表中的關鍵碼來使用,並通過雜湊函式進行轉換為地址下標,就能通過常量時間來訪問集合元素了。
位向量實現集合
有一個集合的總集U,比如U={a,b,c,d,e,f,g,h},我們所使用的任何一個集合Si都是屬於U的子集,比如S1={a,b,c},那麼S1的位向量表示則為S=11100000,S2={b,c,d},則S2=01110000,等等情況,1和0表示的含義在於,1代表此集合S元素在U中,0代表不在U中。
位向量使用集合時,操作代價基於集合U的大小度量,因為無論是空集,它的表示長度還是8位,空集=00000000。在需要處理的是U的一些子集時,且U的規模不是很大的情況下這種實現方式比較適用。
擴充套件:
平均檢索長度:在一次完整檢索中比較關鍵碼的的平均次數,通常稱為平均檢索長度(ASL)。
索引:字典是兩種功能的統一,1、作為一種資料儲存結構,支援在字典裡儲存一批資料項。2、提供支援資料檢索功能,設法維護從關鍵碼找到相關資料的聯絡資訊。第二點也稱之為索引,其存在的目的就是為檢索服務。索引所做的事就是要實現從關鍵碼到資料儲存位置的對映。
負載因子:散列表中當時的實際資料項樹/散列表的基本儲存區能容納的元素個數,比如散列表所儲存的元素為7個,散列表儲存區能存下10個元素,那麼負載因子就是7/10=0.7。
總結:
1、字典是一種常用的資料結構,字典的基礎結構就是key-value的對映,因此字典也被稱為對映。
2、用雜湊技術來實現字典,可以將關鍵碼轉換為地址下標,將訪問時間達到常量級別。
3、用雜湊實現字典時,雜湊是大範圍到小範圍的對映,所以會產生雜湊衝突,雜湊衝突的方式有內消解與外消解方法,內消解是開地址法,在發生衝突時,為所衝突元素再找到一個插入位置;外消解法包括溢位表與桶雜湊法,在發生衝突時,將元素不直接儲存在已衝突的散列表中。
4、集合就是一批元素的彙集,使用雜湊技術來實現集合是最高效的。在特定場景下,使用位向量方式使用集合也是一種高效的手段。
5、基於雜湊實現的字典雖然效率高,但是沒有確定性的效率保證,因為一旦資料量增大,雜湊字典的效率逐步下降。所以對於實現字典還有更多可探究的方式,樹形結構來實現字典是其中一種,可以參考這篇博文。二叉排序樹、平衡二叉樹、多分支排序樹、B樹、B+樹