1. 程式人生 > >資料結構第十一節(散列表)

資料結構第十一節(散列表)

#散列表 ##什麼是散列表 散列表(Hash table,也叫雜湊表),是根據鍵(Key)而直接訪問在記憶體儲存位置的資料結構。也就是說,它通過計算一個關於鍵值的函式,將所需查詢的資料對映到表中一個位置來訪問記錄,這加快了查詢速度。這個對映函式稱做雜湊函式,存放記錄的陣列稱做散列表。 舉一個簡單的例子,假設我們有5個數字,他們的個位都不相同,如何把這5個數字儲存起來,而且在查詢這個數字時超級快呢。在這個簡單的例子中由於它們的個位數都不相同,故我們對這些數字對10求餘取個位數,放在對應下標的的數組裡。({54,76,31,92,109}這個集合,我們將54儲存到array[4],將76儲存到array[6],將31儲存到array[1],將92儲存到array[2],將54儲存到array[9]) 上面是一個極其簡單和特殊的例子,在正常情況下我們很難遇到上面的簡單情況,多數的情況,會出現衝突。對兩個不同的數,如果計算到了同樣的鍵值,該如何處理? 所以現在構造一個好用的散列表,最重要的是做好以下兩件事情: 1. 設計一個"好"的雜湊函式來計算Key值。(好的雜湊函式應儘可能避免衝突的出現,而且計算時應儘可能簡潔快速) 2. 出現了衝突時又該如何調整插入元素。 ##散列表的實現 ###雜湊函式的實現 雜湊函式的實現主要有以下幾種方法: 1. 直接定址法:取關鍵字或關鍵字的某個線性函式值為雜湊地址。即$hash(k)=k$或$hash(k)=a*k+b$,其中$a$,$b$為常數(這種雜湊函式叫做自身函式) 2. 數字分析法:假設關鍵字是以r為基的數,並且雜湊表中可能出現的關鍵字都是事先知道的,則可取關鍵字的若干數位組成雜湊地址。 3. 平方取中法:取關鍵字平方後的中間幾位為雜湊地址。通常在選定雜湊函式時不一定能知道關鍵字的全部情況,取其中的哪幾位也不一定合適,而一個數平方後的中間幾位數和數的每一位都相關,由此使隨機分佈的關鍵字得到的雜湊地址也是隨機的。取的位數由表長決定。 4. 摺疊法:將關鍵字分割成位數相同的幾部分(最後一部分的位數可以不同), 然後取這幾部分的疊加和(捨去進位)作為雜湊地址。 5. 隨機數法 6. 除留餘數法:取關鍵字被某個不大於散列表表長m的數p除後所得的餘數為雜湊地址。即$hash(k)=k$ $mod$ $p$。不僅可以對關鍵字直接取模,也可在摺疊法、平方取中法等運算之後取模。對p的選擇很重要,一般取素數或m,若p選擇不好,容易產生衝突。 以上方法精是對數字型別的操作,對字串型別的資料,可以選擇通過相加或者進位轉化成數字後,再執行上面的計算方法。 ###散列表解決衝突的方法 解決衝突主要有兩種方式,一種是鏈地址法,另一種為開放地址法。 對於第1種方法,是將有共同鍵值的元素串成一條連結串列的思路。而第2種方法,如果該位置已經有元素了,則換一個地方,當然為了查詢時還能找得到他,肯定是不可以隨便放的,需要按照指定的增長序列{$k_1,k_2,k_3,k_4...k_n$},依次探查離自己距離自己$k_1,k_2,k_3,k_4...k_n$的地方是否有空。 增量序列的不同,提供了不同的解決衝突方法。常用的方法有三種,分別為線性探測和平方探測和雙雜湊。 使用線性探測時,如果出現衝突那就向後錯一位,看是否有空,直到找到空的(有點像上廁所找坑);使用平方探測時,每次向前向後$d_i^2$位。使用這2種方法容易會造成聚集。聚集(Cluster,也翻譯做“堆積”)的意思是,在函式地址的表中,雜湊函式的結果不均勻地佔據表的單元,形成區塊,造成線性探測產生一次聚集(primary clustering)和平方探測的二次聚集(secondary clustering),雜湊到區塊中的任何關鍵字需要查詢多次試選單元才能插入表中,解決衝突,造成時間浪費。對於開放定址法,聚集會造成效能的災難性損失,是必須避免的。總體來說使用平方探測要比使用線性探測出現聚集的概率小。 使用雙雜湊時,如果出現衝突,就用當前的鍵值作為引數用另一個雜湊函式計算鍵值,直到找到空位。 ###雜湊表程式碼實現 ```cpp #include #include #include #define null -99999 #define notFound -1 using namespace std; typedef struct CellNode* Cell; struct CellNode { int Data; }; //雜湊表結構體 typedef struct HashTlNode* HashTable; struct HashTlNode { int TableSize; Cell Table; }; //判斷一個數是否為素數 bool isPrime(int n) { if (n < 2) { return false; } else if (n == 2 || n == 3) { return true; } else if(n%6==1||n%6==5){ double p = sqrt(n); for (int i = 2; i <= p; i++) { if (n % i == 0) { return false; } } return true; } else { return false; } } //求一個距離最小的素數 int NextPrime(int n) { //如果N已經是素數,那麼他填滿了,在查詢不存在的數時一定會死環 //所以我們去找下一個素數 if (isPrime(n)) { n++; } //必須是素數,而且可以寫成4k+3的形式,這樣保證平方探測時不會出現死環。 while (!(isPrime(n)&&n%4==3)) { n++; } return n; } //建立空的雜湊表 HashTable CreatHashTable(int TableSize) { HashTable H = (HashTable)malloc(sizeof(struct HashTlNode)); H->TableSize = NextPrime(TableSize); H->
Table = (Cell)malloc(sizeof(struct CellNode) * H->TableSize); for (int i = 0; i < H->TableSize; i++) { H->Table[i].Data = null; } return H; } //雜湊函式 int HashFunction(int key, HashTable H) { return key % H->TableSize; } //查詢 int find(HashTable H,int Key) { int currentPos, NewPos; int Cnum = 0; currentPos = NewPos = HashFunction(Key, H); while (H->Table[NewPos].Data!=null&& H->Table[NewPos].Data != Key) { if (++Cnum % 2 != 0) { NewPos = currentPos + (Cnum + 1) * (Cnum + 1) / 4; while (NewPos>=H->TableSize) { NewPos -= H->TableSize; } } else { NewPos = currentPos - Cnum * Cnum / 4; while (NewPos <0) { NewPos += H->TableSize; } } } if (H->Table[NewPos].Data == null) { NewPos = notFound; } return NewPos; } //插入 void Insert(int key,HashTable H) { int currentPos, NewPos; int Cnum = 0; currentPos = NewPos = HashFunction(key, H); while (H->Table[NewPos].Data != null) { if (++Cnum % 2 != 0) { NewPos = currentPos + (Cnum + 1) * (Cnum + 1) / 4; while (NewPos >= H->TableSize) { NewPos -= H->TableSize; } } else { NewPos = currentPos - Cnum * Cnum / 4; while (NewPos < 0) { NewPos += H->TableSize; } } } H->Table[NewPos].Data = key; } int main() { int n,t,array[100]; scanf_s("%d", &n); HashTable H = CreatHashTable(n); for (int i = 0; i < n; i++) { scanf_s("%d", &t); array[i] = t; Insert(t, H); } printf("The hashtable size is %d\n",H->TableSize); for (int i = 0; i < n; i++) { int p = find(H, array[i]); if (p != notFound) { printf("%d in the %d\n", array[i], p); } else { printf("%d not found\n", array[i]); } } while(scanf_s("%d", &t)!=EOF) { int p = find(H, t); if (p != notFound) { printf("%d in the %d\n", t, p); } else { printf("%d not found\n", t); } } return 0; } ``` ##散列表的效能分析 平均查詢長度(ASL)用來度量散列表查詢效率:成功、不成功 影響產生衝突多少有以下三個因素: (1)雜湊函式是否均勻; (2)處理衝突的方法; (3)散列表的裝填因子α。 根據上面的性質我們知道,裝填因子α越大,尋找一個元素越困難,如果徹底排滿的話,又不去判斷是否回到頭,就一定會陷入死迴圈。 ##課後練習(4個小題) ###11-雜湊1 電話聊天狂人 (25point(s)) 給定大量手機使用者通話記錄,找出其中通話次數最多的聊天狂人。 輸入格式: 輸入首先給出正整數N(≤10^5​),為通話記錄條數。隨後N行,每行給出一條通話記錄。簡單起見,這裡只列出撥出方和接收方的11位數字構成的手機號碼,其中以空格分隔。 **輸出格式:** 在一行中給出聊天狂人的手機號碼及其通話次數,其間以空格分隔。如果這樣的人不唯一,則輸出狂人中最小的號碼及其通話次數,並且附加給出並列狂人的人數。 **輸入樣例:** >4 13005711862 13588625832 13505711862 13088625832 13588625832 18087925832 15005713862 13588625832 **輸出樣例:** >13588625832 3 **題解:** 提取號碼後六位做雜湊函式,雜湊表開到10^6,雖然空間浪費有點狠,但是時間可以到100ms左右,穩定過。只取4位,emm。。。小概率壓線過。 ```cpp #include #include #include #include #include #define notFound -1 typedef struct CellNode* Cell; struct CellNode { char Data[12]; int isEmpty; int time; }; //雜湊表結構體 typedef struct HashTlNode* HashTable; struct HashTlNode { int TableSize; Cell Table; }; //判斷一個數是否為素數 bool isPrime(int n) { if (n < 2) { return false; } else if (n == 2 || n == 3) { return true; } else if (n % 6 == 1 || n % 6 == 5) { double p = sqrt(n); for (int i = 2; i <= p; i++) { if (n % i == 0) { return false; } } return true; } else { return false; } } //求一個距離最小的素數 int NextPrime(int n) { //如果N已經是素數,那麼他填滿了,在查詢不存在的數時一定會死環 //所以我們去找下一個素數 if (isPrime(n)) { n++; } //必須是素數,而且可以寫成4k+3的形式,這樣保證平方探測時不會出現死環。 while (!(isPrime(n) && n % 4 == 3)) { n++; } return n; } //建立空的雜湊表 HashTable CreatHashTable(int TableSize) { HashTable H = (HashTable)malloc(sizeof(struct HashTlNode)); H->
TableSize = NextPrime(TableSize); H->Table = (Cell)malloc(sizeof(struct CellNode) * H->TableSize); for (int i = 0; i < H->TableSize; i++) { H->Table[i].isEmpty = 1; } return H; } //雜湊函式 int HashFunction(char key[], HashTable H) { char result[7]; strncpy(result, key + 5, 7); return atoi(result); } //插入 void Insert(char key[], HashTable H) { int currentPos, NewPos; int Cnum = 0; currentPos = NewPos = HashFunction(key, H); //迴圈結束條件,找到元素或者找不到(找到了未被使用的空間)。 while (H->Table[NewPos].isEmpty != 1) { //判斷是否相同,相同直接跳出 if (strcmp(key,H->Table[NewPos].Data)==0) { break; } else if (++Cnum % 2 != 0) { NewPos = currentPos + (Cnum + 1) * (Cnum + 1) / 4; while (NewPos >= H->TableSize) { NewPos -= H->TableSize; } } else { NewPos = currentPos - Cnum * Cnum / 4; while (NewPos < 0) { NewPos += H->TableSize; } } } //沒有找到,建立新的 if (H->Table[NewPos].isEmpty==1) { H->Table[NewPos].isEmpty = 0; strcpy(H->Table[NewPos].Data, key); H->Table[NewPos].time = 1; } else { H->Table[NewPos].time++; } } //find 電話聊天狂人 void find(HashTable H) { int count = 1; int max = 0; for (int i = 1; i < H->TableSize; i++) { if (H->Table[i].isEmpty != 1) { if (H->Table[i].time > (H->Table[max].time)) { max = i; count = 1; } else if(H->Table[i].time == (H->Table[max].time)){ if (strcmp(H->Table[i].Data, H->Table[max].Data)<0) { max = i; } count++; } } } printf("%s %d", H->Table[max].Data, H->Table[max].time); if (count == 1) { printf("\n"); } else { printf(" %d\n",count); } } int main() { int n; char phone[12]; scanf("%d", &n); HashTable H = CreatHashTable(1000000); for (int i = 0; i < n; i++) { scanf("%s", phone); Insert(phone, H); scanf("%s", phone); Insert(phone, H); } find(H); return 0; } ``` ###11-雜湊2 Hashing (25point(s)) The task of this problem is simple: insert a sequence of distinct positive integers into a hash table, and output the positions of the input numbers. The hash function is defined to be H(key)=key%TSize where TSize is the maximum size of the hash table. Quadratic probing (with positive increments only) is used to solve the collisions. Note that the table size is better to be prime. If the maximum size given by the user is not prime, you must re-define the table size to be the smallest prime number which is larger than the size given by the user. Input Specification: Each input file contains one test case. For each case, the first line contains two positive numbers: MSize (≤10^4) and N (≤MSize) which are the user-defined table size and the number of input numbers, respectively. Then N distinct positive integers are given in the next line. All the numbers in a line are separated by a space. **Output Specification:** For each test case, print the corresponding positions (index starts from 0) of the input numbers in one line. All the numbers in a line are separated by a space, and there must be no extra space at the end of the line. In case it is impossible to insert the number, print "-" instead. **Sample Input:** >4 4 10 6 4 15 **Sample Output:** >0 1 4 - **題解:** 題目的意思是讓做雜湊插入,用僅正向平方探測解決衝突,如果解決不了,就是出現了環,就要輸出-,否則輸出他的位置。題目的難點在於如何判斷環的出現。根據數學可以證明,平方探測的次數達到整個表的一半時,就說明無法插入了,如何得來呢:假設現在進行到了第n次(n $(size^2+2*m*size+m^2)$ $mod$ $size$ => $m^2$ $mod$ $size$。因為前兩項中包含size,故對他們求於一定為0。到現在我們證明了,對於數m,n(m + n = size 且 m,n < size),它們對應的平方探測值相同。也就是,平方探測值關於size/2對稱。 ```cpp #include #include #include #include #define null -99999 #define notFound -1 using namespace std; typedef struct CellNode* Cell; struct CellNode { int Data; }; //雜湊表結構體 typedef struct HashTlNode* HashTable; struct HashTlNode { int TableSize; Cell Table; }; //判斷一個數是否為素數 bool isPrime(int n) { if (n < 2) { return false; } else if (n == 2 || n == 3) { return true; } else if (n % 6 == 1 || n % 6 == 5) { double p = sqrt(n); for (int i = 2; i <= p; i++) { if (n % i == 0) { return false; } } return true; } else { return false; } } //求一個距離最小的素數 int NextPrime(int n) { while (!isPrime(n)) { n++; } return n; } //建立空的雜湊表 HashTable CreatHashTable(int TableSize) { HashTable H = (HashTable)malloc(sizeof(struct HashTlNode)); H->
TableSize = NextPrime(TableSize); H->Table = (Cell)malloc(sizeof(struct CellNode) * H->TableSize); for (int i = 0; i < H->TableSize; i++) { H->Table[i].Data = null; } return H; } //雜湊函式 int HashFunction(int key, HashTable H) { return key % H->TableSize; } //插入 void Insert(int key, HashTable H) { int currentPos, NewPos; int Cnum = 0,end =H->TableSize; currentPos = NewPos = HashFunction(key, H); while (H->Table[NewPos].Data != null) { Cnum++; NewPos = (currentPos + (Cnum) * (Cnum))% H->TableSize; if (Cnum == end) { printf("-"); return; } } H->Table[NewPos].Data = key; printf("%d", NewPos); } int main() { int n, t; scanf("%d %d", &t,&n); HashTable H = CreatHashTable(t); scanf("%d", &t); Insert(t, H); for (int i = 1; i < n; i++) { printf(" "); scanf("%d", &t); Insert(t, H); } printf("\n"); return 0; } ``` ###11-雜湊3 QQ帳戶的申請與登陸 (25point(s)) 實現QQ新帳戶申請和老帳戶登陸的簡化版功能。最大挑戰是:據說現在的QQ號碼已經有10位數了。 輸入格式: 輸入首先給出一個正整數N(≤10^​5),隨後給出N行指令。每行指令的格式為:“命令符(空格)QQ號碼(空格)密碼”。其中命令符為“N”(代表New)時表示要新申請一個QQ號,後面是新帳戶的號碼和密碼;命令符為“L”(代表Login)時表示是老帳戶登陸,後面是登陸資訊。QQ號碼為一個不超過10位、但大於1000(據說QQ老總的號碼是1001)的整數。密碼為不小於6位、不超過16位、且不包含空格的字串。 **輸出格式:** 針對每條指令,給出相應的資訊: 1)若新申請帳戶成功,則輸出“New: OK”; 2)若新申請的號碼已經存在,則輸出“ERROR: Exist”; 3)若老帳戶登陸成功,則輸出“Login: OK”; 4)若老帳戶QQ號碼不存在,則輸出“ERROR: Not Exist”; 5)若老帳戶密碼錯誤,則輸出“ERROR: Wrong PW”。 **輸入樣例:** >5 L 1234567890 [email protected] N 1234567890 [email protected] N 1234567890 [email protected] L 1234567890 myQQ@qq L 1234567890 [email protected] **輸出樣例:** >ERROR: Not Exist New: OK ERROR: Exist ERROR: Wrong PW Login: OK **題解:** 一道簡單的模擬題,我這裡的雜湊函式是取QQ號的前6位,開了一個10的6次方大小的散列表,雖然有些浪費,但是速度湊合。 ```cpp #include #include #include #include #include #define _CRT_SECURE_NO_WARNINGS using namespace std; typedef struct CellNode* Cell; struct CellNode { char Data[11]; int isEmpty; char passw[17]; }; //雜湊表結構體 typedef struct HashTlNode* HashTable; struct HashTlNode { int TableSize; Cell Table; }; //判斷一個數是否為素數 bool isPrime(int n) { if (n < 2) { return false; } else if (n == 2 || n == 3) { return true; } else if (n % 6 == 1 || n % 6 == 5) { double p = sqrt(n); for (int i = 2; i <= p; i++) { if (n % i == 0) { return false; } } return true; } else { return false; } } //求一個距離最小的素數 int NextPrime(int n) { //如果N已經是素數,那麼他填滿了,在查詢不存在的數時一定會死環 //所以我們去找下一個素數 if (isPrime(n)) { n++; } //必須是素數,而且可以寫成4k+3的形式,這樣保證平方探測時不會出現死環。 while (!(isPrime(n) && n % 4 == 3)) { n++; } return n; } //建立空的雜湊表 HashTable CreatHashTable(int TableSize) { HashTable H = (HashTable)malloc(sizeof(struct HashTlNode)); H->TableSize = NextPrime(TableSize); H->Table = (Cell)malloc(sizeof(struct CellNode) * H->TableSize); for (int i = 0; i < H->TableSize; i++) { H->Table[i].isEmpty = 1; } return H; } //雜湊函式 int HashFunction(char key[], HashTable H) { char result[7]; strncpy(result, key, 6); return atoi(result); } //插入 void Insert(char key[], char pass[], HashTable H) { int currentPos, NewPos; int Cnum = 0; currentPos = NewPos = HashFunction(key, H); //迴圈結束條件,找到元素或者找不到(找到了未被使用的空間)。 while (H->Table[NewPos].isEmpty != 1) { //判斷是否相同,相同直接跳出 if (strcmp(key, H->Table[NewPos].Data) == 0) { break; } else if (++Cnum % 2 != 0) { NewPos = currentPos + (Cnum + 1) * (Cnum + 1) / 4; while (NewPos >= H->TableSize) { NewPos -= H->TableSize; } } else { NewPos = currentPos - Cnum * Cnum / 4; while (NewPos < 0) { NewPos += H->TableSize; } } } //沒有找到,建立新的 if (H->Table[NewPos].isEmpty == 1) { H->Table[NewPos].isEmpty = 0; strcpy(H->Table[NewPos].Data, key); strcpy(H->Table[NewPos].passw, pass); printf("New: OK\n"); } else { printf("ERROR: Exist\n"); } } int stringcmp(const char pass1[], const char pass2[]) { for (int i = 0; i < max(strlen(pass1),strlen(pass2)); i++) { if (pass1[i] != pass2[i]) { return 0; } } return 1; } //登陸 void Login(char key[], char pass[], HashTable H) { int currentPos, NewPos; int Cnum = 0; currentPos = NewPos = HashFunction(key, H); //迴圈結束條件,找到元素或者找不到(找到了未被使用的空間)。 while (H->Table[NewPos].isEmpty != 1) { //判斷是否相同,相同直接跳出 if (strcmp(key, H->Table[NewPos].Data) == 0) { break; } else if (++Cnum % 2 != 0) { NewPos = currentPos + (Cnum + 1) * (Cnum + 1) / 4; while (NewPos >= H->TableSize) { NewPos -= H->TableSize; } } else { NewPos = currentPos - Cnum * Cnum / 4; while (NewPos < 0) { NewPos += H->TableSize; } } } //沒有找到 if (H->Table[NewPos].isEmpty == 1) { printf("ERROR: Not Exist\n"); } else { if (stringcmp(H->Table[NewPos].passw,pass)) { printf("Login: OK\n"); } else { printf("ERROR: Wrong PW\n"); } } } int main() { int n; char id[11]; char pas[17]; char op = ' '; scanf("%d", &n); HashTable H = CreatHashTable(1000000); for (int i = 0; i < n; i++) { scanf("\n%c", &op); scanf("%s", id); scanf("%s", pas); if (op == 'L') { Login(id, pas, H); int p = 0; } else if (op == 'N') { Insert(id, pas, H); int p = 0; } } return 0; } ``` ###11-雜湊4 Hashing - Hard Version (30point(s)) Given a hash table of size N, we can define a hash function H(x)=x%N. Suppose that the linear probing is used to solve collisions, we can easily obtain the status of the hash table with a given sequence of input numbers. However, now you are asked to solve the reversed problem: reconstruct the input sequence from the given status of the hash table. Whenever there are multiple choices, the smallest number is always taken. **Input Specification:** Each input file contains one test case. For each test case, the first line contains a positive integer N (≤1000), which is the size of the hash table. The next line contains N integers, separated by a space. A negative integer represents an empty cell in the hash table. It is guaranteed that all the non-negative integers are distinct in the table. **Output Specification:** For each test case, print a line that contains the input sequence, with the numbers separated by a space. Notice that there must be no extra space at the end of each line. **Sample Input:** >11 33 1 13 12 34 38 27 22 32 -1 21 **Sample Output:** >1 13 12 21 33 34 38 27 22 32 **題解:** 題目的大概意思是,先讀入一個 n,然後傳給你一個大小為N的散列表,如果散列表中的元素為負值,說明該位置是空的,而且該散列表在建立時,用的是散列表大小求餘的雜湊函式,解決衝突的方式為線性探索。 要求輸出散列表的插入順序,數字小的優先靠前。 整個題目可以用一個拓撲排序,再加上一個優先佇列(最小堆)來解決,不過優先佇列比較麻煩而且該題資料量不大,就直接線性掃描了。對於一個位置是否對另一個位置會造成影響(也就是說這個位置的存在,導致了元素衝突),可以通過下面的方式來判別,在初始時我們有計算過它的初始偏移量p = i - t % n, p大於雜湊表尺寸時,p-=n。在我們輸出一個元素後,迴圈遍歷計算該元素到其他所有元素的距離,如果這個距離小於等於初始偏移量,就將該位置的元素偏移量減1。 ```cpp #include #include #include #include #include #define INFINITY 999999 #define _CRT_SECURE_NO_WARNINGS using namespace std; typedef struct CellNode Cell; struct CellNode { int Data; int pos; int firstpos; }; int main() { int n, t, invalid = 0; scanf("%d", &n); vector v(n); for (int i = 0; i < n; i++) { scanf("%d", &t); v[i].Data = t; if (t < 0) { v[i].firstpos = -1; v[i].pos = -1; invalid++; } else { int p = i - t % n; if (p < 0) { p += n; } v[i].firstpos = p; v[i].pos = p; } } //判斷是否為第一個輸出,是前面不輸出空格 bool isf = true; for (int i = 0; i < n-invalid; i++) { int p=0, minnum = INFINITY; //掃描全序列,找偏移為0且最小的點。 for (int j = 0; j < n; j++) { if (v[j].pos == 0 && v[j].Data < minnum) { minnum = v[j].Data; p = j; } } //輸出且刪除該點 if (isf) { printf("%d", minnum); isf = false; } else { printf(" %d", minnum); } //因為p點而偏移的數,偏移量減去1 for (int j = 0; j < n; j++) { int dis = j - p; if (dis < 0) { dis += n; } //因為p點而偏移 if (v[j].firstpos >= dis) { v[j].pos--; } } v[p].pos = -1; } printf("\n"); return 0