1. 程式人生 > >6.1 查詢演算法概述

6.1 查詢演算法概述

一、基本概念

查詢是在大量的資訊中尋找一個特定的資訊元素,在計算機應用中,查詢是常用的基本運算。

被查詢的物件是由一組記錄組成的表或檔案,而每個記錄則由若干個資料項組成,並假設每個記錄都有一個能唯一標識該記錄的關鍵字。在這種條件下,查詢的定義是:給定一個值k,在含有n個記錄的表中找出關鍵字等於k的記錄。若找到,則查詢成功,返回該記錄的資訊或該記錄在表中的位置,否則查詢失敗,返回相關的指示資訊。

採用何種查詢方法首先取決於使用哪種資料結構來表示“表”,即表中記錄的組織方式。為了提高查詢速度,常常用某些特殊的資料結構來組織表,或對錶事先進行諸如排序這樣的運算。

若在查詢的同時對錶做修改運算(如插入和刪除),則相應的表稱為動態查詢表

,否則稱之為靜態查詢表

查詢也分內查詢外查詢之分。若整個查詢過程都在記憶體進行,則稱之為內查詢;若查詢過程中需要訪問外存,則稱之為外查詢。

查詢的效率:

平均查詢長度(Average Search Length,ASL):需和指定key進行比較的關鍵字的個數的期望值,稱為查詢演算法在查詢成功時的平均查詢長度。

  對於含有n個數據元素的查詢表,查詢成功的平均查詢長度為:ASL = Pi*Ci的和。
  Pi:查詢表中第i個數據元素的概率。
  Ci:找到第i個數據元素時已經比較過的次數。

二、查詢演算法的分類

先介紹查詢演算法的分類,建立查詢演算法的理論框架,再學習起來思路會比較清晰。

參考部落格:七大查詢演算法

但是,該部落格沒有建立清晰的查詢演算法分類,但是對主要的查詢演算法的基本思想和複雜度進行了介紹,並有C++原始碼,可以學習一個。

如下是教科書上對查詢演算法的分類及其代表性演算法,本章及後文再介紹各個演算法時,相信大家會有比較明確的概念了。查詢演算法的大體框架如下,下文是對樹表之外的演算法進行介紹~

查詢演算法分類
演算法分類 主要演算法 說明
線性表查詢/靜態查詢表 順序查詢、二分查詢和分塊查詢 下面會對其中的演算法進行介紹,演算法思想都很簡單。
樹表查詢/動態查詢表 二叉搜尋樹、平衡二叉樹、B-樹、B+樹 詳情請關注資料結構之樹中對樹表查詢的介紹。
雜湊表查詢/雜湊查詢 就是雜湊演算法 下面也會介紹雜湊的基本原理。


三、線性表查詢

線性表有順序和鏈式兩種儲存結構,此處只討論順序表的查詢,連結串列的相關問題可參考第一章。

1. 順序查詢

順序查詢也稱為線形查詢,從資料結構線形表的一端開始,順序掃描,依次將掃描到的結點關鍵字與給定值k相比較,若相等則表示查詢成功;若掃描結束仍沒有找到關鍵字等於k的結點,表示查詢失敗。

複雜度分析: 

查詢成功時的平均查詢長度為:(假設每個資料元素的概率相等) ASL = 1/n(1+2+3+…+n) = (n+1)/2 ;
當查詢不成功時,需要n+1次比較,時間複雜度為O(n);

所以,順序查詢的時間複雜度為O(n)

傻瓜式的查詢,n較大時不建議使用,效率低。其優點是對錶的結構無任何要求。

2. 二分查詢

這是這一節的重點,後面會有相關的數道題目對這一方法進行全面解讀。

元素必須是有序的,如果是無序的則要先進行排序操作。

基本思想:也稱為是折半查詢,屬於有序查詢演算法。用給定值k先與中間結點的關鍵字比較,中間結點把線形表分成兩個子表,若相等則查詢成功;若不相等,再根據k與該中間結點關鍵字的比較結果確定下一步查詢哪個子表,這樣遞迴進行,直到查詢到或查詢結束髮現表中沒有這樣的結點。二分查詢的過程可以用二叉樹來描述。

複雜度分析:最壞情況下,關鍵詞比較次數為log2(n+1),且期望時間複雜度為O(log2n)

注:折半查詢的前提條件是需要有序表順序儲存,對於靜態查詢表,一次排序後不再變化,折半查詢能得到不錯的效率。但對於需要頻繁執行插入或刪除操作的資料集來說,維護有序的排序會帶來不小的工作量,那就不建議使用。——《大話資料結構》

 

3. 插值查詢以及斐波那契查詢

二分查詢、插值查詢以及斐波那契查詢都可以歸為一類——插值查詢。插值查詢和斐波那契查詢是在二分查詢的基礎上的優化查詢演算法。

插值查詢

基本思想:基於二分查詢演算法,將查詢點的選擇改進為自適應選擇,可以提高查詢效率。當然,差值查詢也屬於有序查詢。

折半查詢這種查詢方式,不是自適應的(也就是說是傻瓜式的)。二分查詢中查詢點計算如下:

  mid=(low+high)/2, 即mid=low+1/2*(high-low);

通過類比,我們可以將查詢的點改進為如下:

  mid=low+(key-a[low])/(a[high]-a[low])*(high-low),

  也就是將上述的比例引數1/2改進為自適應的,根據關鍵字在整個有序表中所處的位置,讓mid值的變化更靠近關鍵字key,這樣也就間接地減少了比較次數。

注:對於表長較大,而關鍵字分佈又比較均勻的查詢表來說,插值查詢演算法的平均效能比折半查詢要好的多。反之,陣列中如果分佈非常不均勻,那麼插值查詢未必是很合適的選擇。

複雜度分析:查詢成功或者失敗的時間複雜度均為O(log2(log2n))。

斐波那契查詢

基本思想:也是二分查詢的一種提升演算法,通過運用黃金比例的概念在數列中選擇查詢點進行查詢,提高查詢效率。同樣地,斐波那契查詢也屬於一種有序查詢演算法。

斐波那契序列的特點對有序表進行分割的,要求開始表中記錄的個數為某個斐波那契數小1,即n=F(k)-1。(注意這是對元素總數的強制性要求)

開始將k值與第F(k-1)位置的記錄進行比較(即mid=low+F(k-1)-1)而不是二分查詢的(mid=low+high)/2),比較結果分為三種

  1)相等,mid位置的元素即為所求

  2)>,low=mid+1,k-=2;

  說明:low=mid+1說明待查詢的元素在[mid+1,high]範圍內,k-=2 說明範圍[mid+1,high]內的元素個數為n-(F(k-1))= Fk-1-F(k-1)=Fk-F(k-1)-1=F(k-2)-1個,所以可以遞迴的應用斐波那契查詢。

  3)<,high=mid-1,k-=1。

  說明:low=mid+1說明待查詢的元素在[low,mid-1]範圍內,k-=1 說明範圍[low,mid-1]內的元素個數為F(k-1)-1個,所以可以遞迴 的應用斐波那契查詢。

複雜度分析:最壞情況下,時間複雜度為O(log2n),且其期望複雜度也為O(log2n)。

4. 分塊查詢

分塊查詢又稱索引順序查詢,它是順序查詢的一種改進方法。
演算法思想:將n個數據元素"按塊有序"劃分為m塊(m ≤ n)。每一塊中的結點不必有序,但塊與塊之間必須"按塊有序";即第1塊中任一元素的關鍵字都必須小於第2塊中任一元素的關鍵字;而第2塊中任一元素又都必須小於第3塊中的任一元素,……
演算法流程:
  step1 先選取各塊中的最大關鍵字構成一個索引表;
  step2 查詢分兩個部分:先對索引表進行二分查詢或順序查詢,以確定待查記錄在哪一塊中;然後,在已確定的塊中用順序法進行查詢。

 

四、雜湊表查詢

雜湊表(Hash table)又稱散列表,是除順序表、連結串列和索引表之外的又一種儲存線性表的儲存結構。

演算法思想:如果所有的鍵都是整數,那麼就可以使用一個簡單的無序陣列來實現:將鍵作為索引,值即為其對應的值,這樣就可以快速訪問任意鍵的值。這是對於簡單的鍵的情況,我們將其擴充套件到可以處理更加複雜的型別的鍵。

基本思路:1)以儲存物件的關鍵字k為自變數,通過給定的雜湊函式,把k對映到一個連續的記憶體單元的地址(或下標),並存儲在此;2)根據選擇的衝突處理方法解決地址衝突;常見的解決衝突的方法:拉鍊法和線性探測法。3)在雜湊表的基礎上執行雜湊查詢,只需要將待查詢的關鍵字k執行同樣的雜湊函式得到雜湊地址,查詢該記憶體地址的關鍵字是否為k(需注意衝突問題)。

雜湊表是一個在時間和空間上做出權衡的經典例子。如果沒有記憶體限制,那麼可以直接將鍵作為陣列的索引。那麼所有的查詢時間複雜度為O(1);如果沒有時間限制,那麼我們可以使用無序陣列並進行順序查詢,這樣只需要很少的記憶體。雜湊表使用了適度的時間和空間來在這兩個極端之間找到了平衡。只需要調整雜湊函式演算法即可在時間和空間上做出取捨。

在初等水平上,對雜湊資料結構的理解到此即可。雜湊演算法的諸多題目在各個章節都會涉及,本章也不單獨列出題目。

如何深入理解雜湊演算法,需要從以下幾個方面繼續學習:

1. 雜湊函式的選擇:

關鍵字為整數、字串等情況時函式的選擇;

如何構造雜湊函式使得到的雜湊值更雜湊(避免地址衝突)。

2. 雜湊衝突的解決方法

雜湊衝突很難避免,主要與三個因素有關:

一是裝填因子α,即雜湊表中已存入的記錄數n與雜湊地址空間大小m的比值。

二是所採用的雜湊函式。雜湊函式選擇適當,可使雜湊地址儘可能均勻分佈在雜湊地址空間上,從而減少衝突的發生。

三是解決衝突的雜湊衝突函式。

解決衝突的方法主要分為開放定址法拉鍊法,可以參見:淺談演算法和資料結構: 十一 雜湊表中的介紹,襠燃看教材上的也可。

開放定址法,以傳送衝突的雜湊地址為自變數,通過某種雜湊衝突函式得到一個新的空閒雜湊地址,常見方法有線性探索法、平方探索法等,反正就是在其他空位置找一個,在查詢時,當前位置沒找到的話,還要按照雜湊衝突函式繼續探索,憑感覺覺得並不靠譜,其實只要到某個地址為空就可。

拉鍊法,把地址衝突的儲存物件,用單鏈表連線起來,此時雜湊表每個單元儲存的是單鏈表的頭指標,由於單鏈表可以插入任意個,理論上都不需要擴容==,一般可把填充因子設定的較高。

相比之下,拉鍊法處理衝突更簡單,平均查詢長度較短;填充因子較大,減少浪費,連結串列空間動態申請,更為靈活。更顯著的優點是,雜湊表元素的刪除,連結串列中元素的刪除很簡單,而開放定址法元素如何刪除呢?可思考一下。。由於查詢時要到某個地址為空,刪除時,空地址是查詢失敗,我們的做法一般是做一個刪除的標記(也不可能按雜湊衝突函式去向前遞補,那樣會越來越亂),因此開放定址法在元素頻繁新增刪除時,並不適合。

3. 雜湊表的操作:如查詢,插入等,具體是如何實現的

理解Java 的資料結構中HashMap 與HashTable的區別,這是一個經典的問題。深刻理解二者的區別和原理,基本上就對上述三個方面搞明白了。參考部落格如下:

HashMap和Hashtable的區別

可以認真學習一下二者的雜湊值計算方法,以及在陣列加連結串列的實現原理之上(1.8之後使用了紅黑樹,有興趣的再深入瞭解),元素的插入、查詢方法的實現。

還有resize方法,即hashmap的擴容,具體過程為:先建立一個容量為table.length*2的新table,修改臨界值,然後把table裡面元素計算hash值並使用hash與table.length*2重新計算index放入到新的table裡面;注意擴容時用每個元素的hash值全部重新計算index。hashmap與hashtable的初始容量,填充因子和擴容,都是要記住的。


介紹了樹表以外的其他查詢演算法,二分查詢的具體案例看下一節;雜湊查詢的原理也是這一節的關鍵,具體案例可以看4.4節的幾道LeetCode題目~