@html
查找是各類軟件系統中常常用到的操做。查找的效率很是重要,大型的系統尤爲如此。java
首先來看一些查找的基本概念和術語。算法
查找表
查找表是由同一類型的數據元素(或記錄)構成的集合。因爲 「集合」 中的數據元素之間存在着徹底鬆散的關係,所以查找表是一種很是靈活的數據結構,能夠利用其餘的數據結構來實現,例如線性表、樹表及散列表等。數組
關鍵字
關鍵字是數據元素(或記錄) 中某個數據項的值,用它能夠標識一個數據元素(或記錄)。若此關鍵字 能夠惟一地標識一個記錄,則稱此關鍵字爲主關鍵字(對不一樣的記錄,其主關鍵字均不一樣)。反之,稱用以識別若千記錄的關鍵字爲次關鍵字。當數據元素只有一個數據項時,其關鍵字即爲該數據元素的值。數據結構
查找
查找是指根據給定的某個值,在查找表中肯定一個其關鍵字等千給定值的記錄或數據元素。若表中存在這樣的一個記錄, 則稱查找成功,此時查找的結果可給出整個記錄的信息,或指示該記錄在查找表中的位置;若表中不存在關鍵字等於給定值的記錄,則稱查找不成功,此時查找的結果可給出一個 「空」 記錄或 「空」 指針。dom
動態查找表和靜態查找表
若在查找的同時對錶作修改操做(如插入和刪除),則相應的表稱之爲動態查找表,不然稱之爲靜態查找表。換句話說,動態查找表的表結構自己是在查找過程當中動態生成的,即在建立表時,對千給定值, 若表中存在其關鍵字等於給定值的記錄, 則查找成功返回;不然插入關鍵字等千給定值的記錄。函數
平均查找長度
爲肯定記錄在查找表中的位置,需和給定值進行比較的關鍵字個數的指望值,稱爲查找算法,在查找成功時的平均查找長度(Average Search Length, ASL)。post
在查找表的組織方式中,線性表是最簡單的一種。性能
在表的組織方式中,線性表是最簡單的一種。而順序查找是線性表查找中最簡單的一種。學習
順序查找的基本思想:從表的一端開始,順序掃描線性表,依次掃描到的結點關鍵字和給定的K值相比較,若當前掃描到的結點關鍵字與 K相等,則查找成功;若掃描結束後,仍未找到關鍵字等於 K的結點,則查找失敗。
順序査找方法既適用於線性表的順序存儲結構,也適用於線性表的鏈式存儲結構。
/** * 順序查找 * @param data * @param key * @return */ public int linearSearch(int[] data,int key){ //遍歷查找 for (int i=0;i<data.length;i++){ //查找到 if (key==data[i]){ return i; } } //沒有查找到 return -1; }
成功時的順序査找的平均査找長度爲:
在數據量爲n的狀況下,線性表的平均查找長度:
(n+……+2+1)/n=(n+1)/2
順序查找須要從頭開始不斷地按順序檢查數據,所以在數據量大且目標數據靠後,
或者目標數據不存在時,比較的次數就會更多,也更爲耗時。若數據量爲 n,線性查找的時間複雜度便爲 O(n)。
因此雖然順序查找比較簡單,且對錶的結構無任何要求,可是查詢效率較低,因此當n較大時不宜採用順序査找。
二分查找也叫折半查找。
二分查找是一種效率相對較高的線性表查找方式,它要求被查找的線性表是有序的。
二分查找的基本思想是:先肯定待查找記錄所在的範圍(區間),而後逐步縮小範圍直到找到或找不到該記錄爲止。
具體步驟以下:
/** * 二分查找-非遞歸 * @param data * @param key * @return */ public int binarySearch(int[] data,int key){ int mid; //置查找區間初值 int left=0; int right=data.length-1; while (left<right){ mid=(left+right)/2; if (key==data[mid]){ //找到待查元素 return mid; }else if (key>data[mid]){ //在右子表查找 left=mid+1; mid=(left+right)/2; }else{ //在左子表查找 right=mid-1; mid=(left+right)/1; } } return -1; //沒有查找到待查元素 }
/** * 二分查找-遞歸 * * @param data * @param left * @param right * @param key * @return */ public int binarySearchRescu(int[] data, int left, int right, int key) { if (left > right) { return -1; } int mid = (left + right) / 2; if (key == data[mid]) { return mid; } else if (key > data[mid]) { //在右子表查找 return binarySearchRescu(data, mid + 1, right, key); } else { return binarySearchRescu(data, left, mid - 1, key); } }
折半查找的時間複雜度爲 O(log2n), 折半查找的效率比順序查找高,但折半查找只適用千有序表,且限於順序存儲結構。
折半查找的優勢是:比較次數少,查找效率高。其缺點是:對錶結構要求高,只能用於順序存儲的有序表。
若是對無序表進行二分查找,查找前須要排序,而排序自己是一種費時的運算。同時爲了保持順序表的有序性,對有序表進行插入和刪除時,平均比較和移動表中一半元素,這也是一種費時的運算。所以,折半查找不適用於數據元素常常變更的線性表。
分塊查找又稱索引順序查找。它是把順序查找和二分査找相結合的一種查找方法,即把線性表分紅若干塊,塊和塊之間有序,但每一塊內的結點能夠無序。
分塊查找的基本思想是:肯定被査找的結點所在的塊(採用二分查找法)後,對該塊中的結點採用順序查找。
分塊査找介於順序和二分查找之間,其優勢是:在表中插入或刪除一個記錄時,只要找到該記錄所屬的塊,就在該塊內進行插入和刪除運算。分塊査找的主要代價是增長一個輔助數組的存儲空間和將初始表分塊排序的運算
在重學數據結構(6、樹和二叉樹) 裏,對大量的輸進行了詳細的描述和實現,因此針對樹表的查找,下面只是是作一些簡單的描述。
當用線性表做爲表的組織形式時,能夠用 3 種查找法。其中以二分査找效率最高。但因爲二分査找要求表中結點按關鍵字有序,且不能用鏈表做存儲結構,所以,當表的插入或刪除操做頻繁時,爲維護表的有序性,勢必要移動表中不少結點。這種由移動結點引發的額外時間開銷,就會抵消二分査找的優勢。也就是說,二分查找只適用於靜態查找表。若要對動態查找表進行高效率的查找,最好使用二叉排序樹。
二叉排序樹又稱爲是二叉查找樹或二叉搜索樹。二叉排序樹是具備下列性質的二叉樹:
由 BST性質可得:
(1) 二叉排序樹中任一結點x,其左(右)子樹中任一結點y若存在)的關鍵字必小(大)於 x 的關鍵字。
(2) 二叉排序樹中,各結點關鍵字是唯一的。
須要注意的是在實際應用中,不能保證被查找的數據集中各元素的關鍵字互不相同,因此可將二叉排序樹定義中 BST 性質⑴ 裏的「小於」改成「大於等於」,或將 BST性質(2)裏的「大於」改成「小於等於」,甚至可同時修改這兩個性質。
(3) 按中序遍歷該樹所獲得的中序序列是一個遞增有序序列。
二叉樹雖然利於查找,可是存在一個最壞的狀況——二叉樹退化爲線性表。
因此就須要就須要引入一些特性來使二叉樹保持平衡。
例如最先提出的平衡二叉樹AVL樹,是具備以下特性的二叉樹:
B樹也是一種平衡查找樹,不過不是二叉樹。
B樹也稱B-樹,它是一種多路平衡查找樹。
一棵m階的B樹定義以下:
B+樹是B樹的變體,也是一種多路搜索樹。
B+樹·和B樹有一些共同的特性:
B+樹和B樹也有一些不同的地方:
在前面學習了關於線性表、樹表的查找,這類查找方法都是以關鍵字的比較爲基礎的。
在查找過程當中只考慮各元素關鍵字之間的相對大小,記錄在存儲結構中的位置和其關鍵字無直接關係, 其查找時間與表的長度有關,特別是當結點個數不少時,查找時要大量地與無效結點的關鍵字進行比較,導致查找速度很慢。
若是能在元素的存儲位置和其關鍵字之間創建某種直接關係,那麼在進行查找時,就無需作比較或作不多次的比較,按照這種關係直接由關鍵字找到相應的記錄。這就是散列查找法 (HashSearch)的思想,它經過對元素的關鍵字值進行某種運算,直接求出元素的地址, 即便用關鍵字到地址的直接轉換方法,而不須要反覆比較。所以,散列查找法又叫雜湊法或散列法。
下面來看一些散列查找法的術語:
構造散列函數的方法不少,通常來講,應根據具體問題選用不一樣的散列函數,一般要考慮如下因素:
構造一個 「好」 的散列函數應遵循如下兩條原則:
下面介紹構造散列函數的幾種經常使用方法。
若是事先知道關鍵字集合, 且每一個關鍵字的位數比散列表的地址碼位數多,每一個關鍵字由n位數組成,如K1…Kn , 則能夠從關鍵字中提取數字分佈比較均勻的若干位做爲散列地址。
例如,有80個記錄,其關鍵字爲8位十進制數。假設散列表的表長爲100, 則可取兩位十進制數組成散列地址,選取的原則是分析這80個關鍵字,使獲得的散列地址盡最避免產生衝突。
假設這80個關鍵字中的一部分以下所列:
對關鍵字全體的分析中能夠發現:第一、2位都是"8 、1", 第3位只可能取3或4, 第4位可能取二、 5或 7,所以這 4 位都不可取。由千中間的 4 位可當作是近乎隨機的,所以可取其中任意兩位,或取其中兩位與另外兩位的疊加求和後捨去進位做爲散列地址。
數字分析法的適用狀況:事先必須明確知道全部的關鍵字每一位上各類數字的分佈狀況。
在實際應用中,例如,同一出版社出版的全部圖書,其ISBN號的前幾位都是相同的,所以,若數據表只包含同一出版社的圖書,構造散列函數時能夠利用這種數字分析排除ISBN號的前幾位數字。
一般在選定散列函數時不必定能知道關鍵字的所有狀況,取其中哪幾位也不必定合適,而一個數平方後的中間幾位數和數的每一位都相關,若是取關鍵字平方後的中間幾位或其組合做爲散列地址,則使隨機分佈的關鍵字獲得的散列地址也是隨機的,具體所取的位數由表長決定。平方取中法是一種較經常使用的構造散列函數的方法。
平方取中法的適用狀況:不能事先了解關鍵字的全部狀況,或難千直接從關鍵字中找到取值較分散的幾位。
將關鍵字分割成位數相同的幾部分(最後一部分的位數能夠不一樣),而後取這幾部分的疊加和(捨去進位) 做爲散列地址,這種方法稱爲摺疊法。根據數位疊加的方式,能夠把摺疊法分爲移位疊加和邊界疊加兩種。移位疊加是將分割後每一部分的最低位對齊,而後相加;邊界疊加是將兩相鄰的部分沿邊界來回摺疊,而後對齊相加。
例如,當散列表長爲 1000 時,關鍵字key = 45387765213, 從左到右按 3 位數一段分割,能夠獲得 4 個部分:453 、 877 、 652 、 13。分別採用移位疊加和邊界疊加,求得散列地址爲 995 和914, 以下圖 所示。
摺疊法的適用狀況:適合於散列地址的位數較少,而關鍵字的位數較多,且難千直接從關鍵字中找到取值較分散的幾位。
假設散列表表長爲m, 選擇一個不大千m的數p, 用p去除關鍵字,除後所得餘數爲散列地址,即
這個方法的關鍵是選取適當的p, 通常狀況下,能夠選p爲小於表長的最大質數。例如,表長m= 100 , 可取p= 97 。
除留餘數法計算簡單,適用範圍很是廣,是最經常使用的構造散列函數的方法。它不只能夠對關鍵字直接取模,也可在摺疊、平方取中等運算以後取模,這樣可以保證散列地址必定落在散列表的地址空間中。
選取一個隨機函數,取關鍵字的隨機函數的值爲散列地址。
選擇一個 「好」 的散列函數能夠在必定程度上減小衝突,但在實際應用中,很難徹底避免發生衝突,因此選擇一個有效的處理衝突的方法是散列法的另外一個關鍵問題。建立散列表和查找散列表都會遇到衝突,兩種狀況下處理衝突的方法應該一致。
處理衝突的方法與散列表自己的組織形式有關。按組織形式的不一樣,一般分兩大類:開放地址法和鏈地址法。
開放地址法的基本思想是:把記錄都存儲在散列表數組中,當某一記錄關鍵字 key的初始散列地址H0=H(key)發生衝突時,以H0爲基礎 ,採起合適方法計算獲得另外一個地址H1, 若是H1仍然發生衝突,以H1爲基礎再求下一個地址H2,若H2仍然衝突,再求得H3。依次類推,直至Hk不發生衝突爲止,則Hk爲該記錄在表中的散列地址。
這種方法在尋找 「下一個 「 空的散列地址時,原來的數組空間對全部的元素都是開放的,因此稱爲開放地址法。一般把尋找 「下一個 「 空位的過程稱爲探測,上述方法可用以下公式表示:
這種探測方法能夠將散列表假想成一個循環表,發生衝突時,從衝突地址的下一單元順序尋找空單元,若是到最後 一個位置也沒找到空單元,則回到表頭開始繼續查找,直到找到一個空位,就把此元素放入此空位中。若是找不到空位,則說明散列表已滿,須要進行溢出處理。
例如,散列表的長度爲 11, 散列函數 H(key) = key%11, 假設表中已填有關鍵字分別爲 17 、60 、 29 的記錄,如圖 11 (a) 所示。現有第四個記錄,其關鍵字爲 38, 由散列函數獲得散列地址爲 5, 產生衝突。
若用線性探測法處理時,獲得下一個地址 6, 仍衝突;再求下一個地址 7, 仍衝突;直到散列地址爲 8 的位置爲 「空」 時爲止,處理衝突的過程結束,38 填入散列表中序號爲 8 的位置,如圖11 (b) 所示。
若用二次探測法,散列地址 5 衝突後,獲得下一個地址 6, 仍衝突;再求得下一個地址 4 , 無衝突, 38 填入序號爲 4 的位置,如圖 11 (C), 所示。
若用僞隨機探測法,假設產生的僞隨機數爲 9, 則計算下一個散列地址爲(5+9)%11 = 3, 因此38 填入序號爲 3 的位置,如圖 11 (d) 所示。
從上述線性探測法處理的過程當中能夠看到一個現象:當表中 i, i+1, i+2 位置上已填有記錄時,下一個散列地址爲i、i+ I 、i+2和i+3 的記錄都將填入i+3 的位置,這種在處理衝突過程當中發生的兩個第一個散列地址不一樣的記錄爭奪同一個後繼散列地址的現象稱做 「二次彙集"(或稱做 「堆積"),即在處理同義詞的衝突過程當中又添加了非同義詞的衝突。
能夠看出,上述三種處理方法各有優缺點。
線性探測法的優勢是:只要散列表未填滿,總能找到一個不發生衝突的地址。缺點是:會產生 」二次彙集「 現象。
而二次探測法和僞隨機探測法的優勢是:能夠避免 「二次彙集「 現象。缺點也很顯然:不能保證必定找到不發生衝突的地址。
鏈地址法的基本思想是:把具備相同散列地址的記錄放在同一個單鏈表中,稱爲同義詞鏈表。有 m 個散列地址就有 m 個單鏈表,同時用數組 HT[0…m-1]存放各個鏈表的頭指針,凡是散列地址爲 i 的記錄都以結點方式插入到以 HT[i]爲頭結點的單鏈表中。
該方法的基本思想就是選擇足夠大的M,使得全部的鏈表都儘量的短小,以保證查找的效率。對採用鏈地址法的哈希實現的查找分爲兩步,首先是根據散列值找到等一應的鏈表,而後沿着鏈表順序找到相應的鍵。
散列表上的運算有查找、插入和刪除。其中主要是查找,這是由於散列表主要用於快速查找,且插入和刪除均要用到査找操做。
接下來創建一個簡單的散列表,其散列函數採用上述的除留餘數法,處理衝突的方法採用開放定址法下的線性探測法。
public class HashTable { int[] elem; int count; private static final int Nullkey = -32768; public HashTable(int count) { this.count = count; elem = new int[count]; for (int i = 0; i < count; i++) { elem[i] = Nullkey; // 表明位置爲空 } } /* * 散列函數 */ public int hash(int key) { return key % count; // 除留餘數法 } /* * 插入操做 */ public void insert(int key) { int addr = hash(key); // 求散列地址 while (elem[addr] != Nullkey) { // 位置非空,有衝突 addr = (addr + 1) % count; // 開放地址法的線性探測 } elem[addr] = key; } /* * 查找操做 */ public boolean search(int key) { int addr = hash(key); // 求散列地址 while (elem[addr] != key) { addr = (addr + 1) % count; // 開放地址法的線性探測 if (addr == hash(key) || elem[addr] == Nullkey) { // 循環回到原點或者到了空地址 System.out.println("要查找的記錄不存在!"); return false; } } System.out.println("存在記錄:" + key + ",位置爲:" + addr); return true; } public static void main(String[] args) { int[] arr = {12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34}; HashTable aTable = new HashTable(arr.length); for (int a : arr) { aTable.insert(a); } for (int a : arr) { aTable.search(a); } } }
α標誌散列表的裝滿程度。直觀地看,α越小,發生衝突的可能性就越小;反之,α越大,表中已填入的記錄越多,再填記錄時,發生衝突的可能性就越大,則查找時,給定值需與之進行比較的關鍵字的個數也就越多。
查找是數據處理中常用的一種操做。 這裏主要介紹了對查找表的查找 , 查找表實際上僅僅是一個集合,爲了提升查找效率,將查找表組織成不一樣的數據結構,主要包括3種不一樣結構的查找表:線性表、 樹表和散列表。
* 二叉排序樹的查找過程與折半查找過程相似
* 二叉排序樹在形態均勻時性能最好,而形態爲單支樹時其查找性能則退化爲與順序查找相同,所以,二叉排序樹最好是一棵平衡二叉樹。 平衡二叉樹的平衡調整方法就是確保二叉排序樹在任何狀況下的深度均爲 O(log2n),平衡調整方法分爲4種: LL型、RR型、LR型和RL型。
* B-樹是一種平衡的多叉查找樹,是一種在外存文件系統中經常使用的動態索引技術。 在 B-樹上進行查找的過程和二叉排序樹相似,是一個順指針查找結點和在結點內的關鍵字中查找交叉進行的過程。 爲了確保B-樹的定義,在B-樹中插入一個關鍵字,可能產生結點的 「分裂", 而刪除一個關鍵字,可能產生結點的 「合併"。
* B+樹是一種B-樹的變型樹,更適合作文件系統的索引。 在B+樹上進行隨機查找、 插入和刪除的過程基本上與B-樹相似,但具體實現細節又有所區別。
散列查找法主要研究兩方面的問題:如何構造散列函數,以及如何處理衝突。
* 構造散列函數的方法不少,除留餘數法是最經常使用的構造散列函數的方法。它不只能夠對關鍵字直接取模, 也可在摺疊、平方取中等運算以後取模。
* 處理衝突的方法一般分爲兩大類:開放地址法和鏈地址法,兩者之間的差異相似千順序表和單鏈表的差異。
上一篇:重學數據結構(7、圖)
本博客爲學習筆記,參考資料以下!
水平有限,不免錯漏,歡迎指正!
參考:
【1】:鄧俊輝 編著. 《數據結構與算法》
【2】:王世民 等編著 . 《數據結構與算法分析》
【3】: Michael T. Goodrich 等編著.《Data-Structures-and-Algorithms-in-Java-6th-Edition》
【4】:嚴蔚敏、吳偉民 編著 . 《數據結構》
【5】:程傑 編著 . 《大話數據結構》
【6】:圖解:如何理解與實現散列表
【7】:算法圖解之散列表
【8】:數據結構與算法-Day17-哈希(散列)表
【9】:Java數據結構與算法解析(十二)——散列表
【10】:【Java】 大話數據結構(13) 查找算法(4) (散列表(哈希表))