查詢及其應用C語言實現(資料結構複習最全筆記)
所謂查詢(Search)又稱檢索,就是在一個數據元素集合中尋找滿足某種條件的資料元素。查詢在計算機資料處理中是經常使用的操作。查詢演算法的效率高低直接關係到應用系統的效能。查詢的方法很多,本章將介紹一些常用的查詢演算法,主要有:線性表的查詢、樹表的查詢和散列表的查詢,並對有關的演算法進行效能分析和對比
一.基本概念
1.資料表
就是指資料元素的有限集合。例如,為統計職工工作業績,建立一個包括:職工編號、職工姓名、業績等資訊的表格。這個表格中的每一個職工的資訊就是一個數據元素。對此表格可以根據職工編號查詢職工的業績等資訊;也可以根據職工的姓名查詢職工的業績等資訊。
2.關鍵字
資料表中資料元素一般有多個屬性域(欄位),即由多個數據成員組成,其中有一個屬性域可用來區分元素,作為查詢或排序的依據,該域即為關鍵字。每個資料表用哪個屬性域作為關鍵字,要視具體的應用需要而定。即使是同一個表,在解決不同問題的場合也可能取不同的域做關鍵字。如果在資料表中各個元素的關鍵字互不相同,這種關鍵字即主關鍵字。
3.查詢
查詢(Search)是資料處理中最常用的一種運算。最常見的一種方式是事先給定一個值,在資料表中找到其關鍵字等於給定值的資料元素。查詢的結果通常有兩種可能:一種可能是查詢成功,即找到關鍵字等於給定值的資料元素,這時作為查詢結果,可報告該資料元素在資料表中的位置,還可進一步給出該資料元素的具體資訊,後者在資料庫技術中叫做檢索;另一種可能是查詢不成功(查詢失敗),即資料表中找不到其關鍵字等於給定值的資料元素,此時查詢的結果可給出一個“空”記錄或“空”指標。
4.靜態查詢表和動態查詢表
資料表的組織有兩種不同方式。其一,資料表的結構固定不變,當查詢失敗時,作為查詢結果只報告一些資訊,如失敗標誌、失敗位置等,這類資料表稱為靜態查詢表;其二,資料表的結構在插入或刪除資料元素過程中會得到調整,當查詢失敗時,則把給定值的資料元素插入到資料表中,這類組織方式稱為動態查詢表。相比較而言,靜態查詢表的結構較為簡單,操作較為方便,但查詢的效率較低,而且需要考慮表的溢位問題。
其效率(查詢次數)不超過【log2n】+1(以2為底n的對數)
其二分查詢的過程類似一棵判斷樹
所謂判定樹,給出度孃的定義:判定樹指的是在處理活動的詳細分析中,用樹 形邏輯圖對單個功能與活動的一種詳細分析方法。用判定樹進行功能和活動的判定,也較為簡單明瞭,因而廣泛地用於資訊的系統分析和企業經營決策過程中。判定樹又稱決策樹,是由國內學者鐘鳴等人於1992年在《計算機研究與發展》第1期“示例學習的抽象通道模型及其應用”一文中首次使用,適合描述問題處理中具有多個判斷,而且每個決策與若干條件有關。 [1]
5.查詢的效率
查詢是經常使用的一種運算,因此,查詢的時間複雜度是人們關心的一個重要因素。查詢的時間複雜度一般用平均查詢長度(ASL)來衡量。平均查詢長度是指在資料表中查詢各資料元素所需進行的關鍵字比較次數的期望值,其數學定義為:
ASL=∑m=0nPi⋅Ci
ASL=∑m=0nPi⋅Ci
其中,PiPi表示待查詢資料元素在資料表中出現的概率,CiCi表示查詢此資料元素所需進行關鍵字的比較次數。
6.裝載因子
設資料表的長度為m,表中資料元素個數為n,則資料表的裝載因子α=n/m
二.靜態查詢
<1>有序表與順序表
【1】查詢概論
查詢表是由同一型別是資料元素(或記錄)構成的集合。
關鍵字是資料元素中某個資料項的值,又稱為鍵值。
若此關鍵字可以唯一標識一個記錄,則稱此關鍵字為主關鍵字。
查詢就是根據給定的某個值,在查詢表中確定一個其關鍵字等於給定值的資料元素(或記錄)。
查詢分為兩類:靜態查詢表和動態查詢表。
靜態查詢表:只作查詢操作的查詢表。主要操作:
(1)查詢某個“特定的”資料元素是否在查詢表中。
(2)檢索某個“特定的”資料元素和各種屬性。
動態查詢表:在查詢過程中同時插入查詢表中不存在的資料元素,或者從查詢表中刪除已經已經存在的某個資料元素。 主要操作:
(1)查詢時插入資料元素。
(2)查詢時刪除資料元素。
好吧!兩者的區別: 靜態查詢表只負責查詢任務,返回查詢結果。
而動態查詢表不僅僅負責查詢,而且當它發現查詢不存在時會在表中插入元素(那也就意味著第二次肯定可以查詢成功)
【2】順序表查詢
順序表查詢又稱為線性查詢,是最基本的查詢技術。 它的查詢思路是:
逐個遍歷記錄,用記錄的關鍵字和給定的值比較:
若相等,則查詢成功,找到所查記錄; 反之,則查詢不成功。
順序表查詢演算法程式碼如下:
對於這種查詢演算法,查詢成功最好就是第一個位置找到,時間複雜度為O(1)。
最壞情況是最後一個位置才找到,需要n次比較,時間複雜度為O(n) 顯然,n越大,效率越低下。
【3】有序表查詢
所謂有序表,是指線性表的資料有序排列。
(1)折半查詢(即二分查詢)
關於這個演算法前文已經說過,在此不做贅述,程式碼如下:
#include <iostream>
using namespace std;
// 折半查詢演算法(二分查詢)
int Binary_Search(int* a,int n,int key)
{
int low = 1, high = n, mid = 0; // 初始化
while (low <= high) // 注意理解這裡還有等於條件
{
mid = (low + high)/2; // 折半
if (key < a[mid])
high = mid -1; // 最高小標調整到中位小一位
else if (key > a[mid])
low = mid + 1; // 最低下標調整到中位大一位
else
return mid; // 相等說明即是
}
return 0;
}
void main ()
{
int a[11] = {0,9,23,45,65,88,90,96,100,124,210};
int n = Binary_Search(a,10, 9);
if (n != 0)
cout << "Yes:" << n << endl;
else
cout << "No:" << endl;
}
折半查詢演算法的時間複雜度為O(logn)。
(2)插值查詢
考慮一個問題:為什麼是折半?而不是折四分之一或者更多呢? 好吧,且看分解:
(3)斐波那契查詢(不是必會)
斐波那契查詢利用了黃金分割原理來實現。 如何利用斐波那契數列作為分割呢?
為了理清這個查詢演算法,首先需要一個斐波那契數列,如下圖所示:
查詢演算法如下描述:
注意閱讀以下詳解之前,請先編譯並執行第四部分的例項程式碼,結合程式碼再理解演算法。
首先要明確一點:
如果一個有序表的元素個數為n,並且n正好是某個斐波那契數-1,即n == F[k]-1時,才能用斐波那契查詢法。
1. 如果有序表的元素個數n不等於某個斐波那契數-1,即n != F[k]-1,如何處理呢?
這時必須要將有序表的元素個數擴充套件到比n大的第一個斐波那契數-1的個數才符合演算法的查詢條件。
通俗點講,也就是為了使用斐波那契查詢法,那麼要求所查詢順序表的元素個數n必須滿足n == F[k]-1這樣的條件才可以。
因為查詢表為從小到大的順序表,所以如果資料元素個數不滿足要求,只有在表末用順序表的最大值補滿。
程式碼中第9-10行的作用恰是如此。
2. 對於二分查詢,分割點是從mid= (low+high)/2開始。
而對於斐波那契查詢,分割是從mid = low + F[k-1] - 1開始的。 為什麼如此計算?
用例項驗證,比如本例中: 第一次進入查詢迴圈時,陣列元素個數準確說應該是12(包括隨後補滿的元素)
而黃金分割點比例為0.618,那麼12*0.618=7.416,此值對應12個元素應該為a[8]
觀察程式執行第一次mid=1+F[7-1]-1=8,正是此原理所體現。
key=59,a[8]=73,顯然key<a[8],可知low=1,high=7,k=7-1=6
注意此條件意思即為7個數據元素,正好滿足F[6]-1=7的再次查詢客觀要求
而同理,黃金分割點比例為0.618,那麼7*0.618=4.326,此值對應7個元素應該為a[5]
再看第二次進入迴圈mid=1+F[6-1]-1=5,正是此原理所體現。
key=59,a[5]=47,顯然key>a[5],可知low=6,high=7,k=6-2=4
注意此條件意思即為2個數據元素,正好滿足F[4]-1=2的再次查詢客觀要求
而同理黃金分割點比例為0.618,那麼2*0.618=1.236,此值對應2個元素中的第二個即為a[7]
key=59,a[7]=62,顯然key<a[7],可知low=6,high=6,k=4-1=3
同理mid=6+F[3-1]-1=6。此時a[6]=59=key。 即查詢成功。
3. 注意緊接著下面一句程式碼可以改寫為:
return (mid <= n) ? mid : n;
當然這樣寫也沒有功能錯誤,但是細細琢磨還是有邏輯問題:
mid == n時,返回為n; mid > n時返回也是n。
那麼到底n屬於那種情況下的返回值呢?是否有違背if的本質!
竊以為寫成if(mid < n)會合理些。
另外,許多資料對於這步判斷描述如下:
return (mid <= high) ? mid : n;
其實分析至此,我認為這種寫法從程式碼邏輯而言更為合理。
4. 通過上面知道:陣列a現在的元素個數為F[k]-1個,即陣列長為F[k]-1。
mid把陣列分成了左右兩部分,左邊的長度為:F[k-1]-1
那麼右邊的長度就為(陣列長-左邊的長度-1): (F[k]-1)-(F[k-1]-1)= F[k]-F[k-1]-1 = F[k-2] - 1
5. 斐波那契查詢的核心是:
a: 當key == a[mid]時,查詢成功;
b: 當key<a[mid]時,新的查詢範圍是第low個到第mid-1個,此時範圍個數為F[k-1] - 1個,
即陣列左邊的長度,所以要在[low, F[k - 1] - 1]範圍內查詢;
c: 當key>a[mid]時,新的查詢範圍是第mid+1個到第high個,此時範圍個數為F[k-2] - 1個,
即陣列右邊的長度,所以要在[F[k - 2] - 1]範圍內查詢。
關於斐波那契查詢, 如果要查詢的記錄在右側,則左側的資料都不用再判斷了,不斷反覆進行下去。
對處於中間的大部分資料,其工作效率要高一些。
所以儘管斐波那契查詢的時間複雜度也為O(logn),但就平均效能來說,斐波那契查詢要優於折半查詢。
可惜如果是最壞的情況,比如這裡key=1,那麼始終都處於左側在查詢,則查詢效率低於折半查詢。
還有關鍵一點:折半查詢是進行加法與除法運算的(mid=(low+high)/2)
插值查詢則進行更復雜的四則運算(mid = low + (high - low) * ((key - a[low]) / (a[high] - a[low])))
而斐波那契查詢只進行最簡單的加減法運算(mid = low + F[k-1]-1)
在海量資料的查詢過程中,這種細微的差別可能會影響最終的效率。
【4】斐波那契演算法程式碼實現
例項演算法程式碼如下:
#include <iostream>
#include <assert.h>
using namespace std;
#define MAXSIZE 11
// 斐波那契非遞迴
void Fibonacci(int *f)
{
f[0] = 0;
f[1] = 1;
for (int i = 2; i < MAXSIZE; ++i)
{
f[i] = f[i-1] + f[i-2];
}
}
// 斐波那契數列
/*---------------------------------------------------------------------------------
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
----------------------------------------------------------------------------------
| 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89 | 144 |
-----------------------------------------------------------------------------------*/
// 斐波那契數列查詢
int Fibonacci_Search(int *a, int n, int key)
{
int low = 1; // 定義最低下標為記錄首位
int high = n; // 定義最高下標為記錄末位(一般輸入的引數n必須是陣列的個數減一)
int F[MAXSIZE];
Fibonacci(F); // 確定斐波那契數列
int k = 0, mid = 0;
// 查詢n在斐波那契數列中的位置,為什麼是F[k]-1,而不是F[k]?
while (n > F[k]-1)
{
k++;
}
// 將不滿的數值補全
for (int i = n; i < F[k]-1; ++i)
{
a[i] = a[high];
}
// 查詢過程
while (low <= high)
{
mid = low + F[k-1] - 1; // 為什麼是當前分割的下標?
if (key < a[mid]) // 查詢記錄小於當前分割記錄
{
high = mid - 1;
k = k - 1; // 注意:思考這裡為什麼減一位?
}
else if (key > a[mid]) // 查詢記錄大於當前分割記錄
{
low = mid + 1;
k = k - 2; // 注意:思考這裡為什麼減兩位?
}
else
{
return (mid <= high) ? mid : n; // 若相等則說明mid即為查詢到的位置; 若mid > n 說明是補全數值,返回n
}
}
return -1;
}
void main()
{
int a[MAXSIZE] = {0,1,16,24,35,47,59,62,73,88,99};
int k = 0;
cout << "請輸入要查詢的數字:" << endl;
cin >> k;
int pos = Fibonacci_Search(a, MAXSIZE-1, k);
if (pos != -1)
cout << "在陣列的第"<< pos+1 <<"個位置找到元素:" << k;
else
cout << "未在陣列中找到元素:" << k;
}
若結合以上相關分析深入理解程式碼。
<2>線性索引
【1】線性索引
索引就是把一個關鍵字與它對應的記錄相關聯的的過程。
索引是為檢索而存在的。
一個索引由若干個索引項構成,每個索引項至少應包含關鍵字和其對應的記錄在儲存器中的位置等資訊。
索引技術是組織大型資料庫以及磁碟檔案的一種重要技術。
索引按照結構可以分為線性索引,樹形索引和多級索引。
所謂線性索引就是將索引項集合組織為線性結構,也稱為索引表。
重點了解三種線性索引:稠密索引,分塊索引和倒排索引。
【2】稠密索引
稠密索引是指線上性索引中,將資料集中的每個記錄對應一個索引項。
所下圖所示:
對於稠密索引這個索引表而言,索引項一定是按照關鍵碼有序的排列。
為什麼要這樣做呢?
索引項有序也就意味著,我們要查詢關鍵字時,可以用折半,插值及斐波那契等有序查詢演算法。
比如要查詢關鍵字18的記錄,如果直接從右側的資料表中查詢,那隻能順序查詢。
需要查詢6次才可以看到結果!!
而如果是從側的索引表中查詢,只需兩次折半查詢就可以得到18對應的指標。對應找到結果。
好吧!以上顯然是稠密所以優點。
如果資料集非常大,比如上億,那也就意味著索引也得同樣的資料集長度規模。
對於記憶體有限的計算機來說,可能就需要反覆去訪問磁碟,查詢效能大大下降。
稠密索引檔案的每個記錄都有一個索引項,記錄在資料區存放是任意的,但索引是按序的,這種索引稱為稠密索引。
稠密索引檔案的索引查詢、更新都較方便,但由於索引項多,佔用空間較大。
【3】分塊索引(必會)
注意理解:稠密索引是因為索引項和資料集的記錄個數相同,所以空間代價很大。
如何減少索引項的個數呢?
我們可以對資料集進行分塊,使其分塊有序,然後再對每一塊建立一個索引項(類似於圖書館的分塊)。
分塊有序是把資料集的記錄分成了若干塊,並且這些塊需要滿足兩個條件:
(1)塊內無序
每一塊內的記錄不要求有序
(2)塊間有序
比如要求第二塊所以記錄的關鍵字均要大於第一塊中所有記錄的關鍵字,第三塊要大於第二塊。
只有塊間有序才有可能在查詢時帶來效率。
對於分塊有序的資料集,將每塊對應一個索引項,這種索引方法叫做分塊索引。
分塊索引的索引項結構分為三個資料項:
a: 最大關鍵碼--儲存每一塊中的最大關鍵字。
b: 儲存每一塊中記錄的個數以便於迴圈時使用。
c: 用於指向塊首資料元素的指標,便於開始對這一塊中記錄進行遍歷。
如下圖所示:
在分塊索引表中查詢,可以分為兩步:
a: 在分塊索引表中查詢要查的關鍵字所在塊。
由於分塊索引表是塊間有序的,因此很容易利用折半插值等演算法得到結果。
b:根據塊首指標找到相應的塊,並在塊中順序查詢關鍵碼。
因為塊中可以是無序的,因此只能順序查詢。
【4】倒排索引
關於倒排索引,最好再參見一下這幾篇文章:
(1)http://www.cnblogs.com/fly1988happy/archive/2012/04/01/2429000.html
(2)https://blog.csdn.net/hguisu/article/details/7962350
三.動態查詢
<1>二叉搜尋樹(排序)
詳見這篇部落格:https://blog.csdn.net/weixin_42110638/article/details/83963764
<2>平衡二叉樹
詳見這篇部落格:https://blog.csdn.net/weixin_42110638/article/details/83963954
<3>多路查詢樹
這個用的不多,暫時沒空總結了。。。
四.散列表查詢(雜湊表)概述
這一部分的知識難點較大,但也不是重點,我就簡單總結一下
1.散列表
2.散列表的構造方法
3.衝突處理方法
4.散列表的效能分析