本文屬於系列文章【資料結構和演算法:簡單方法】
- 【資料結構之順序表】用圖和程式碼讓你搞懂順序結構線性表
- 【資料結構之連結串列】看完這篇文章我終於搞懂連結串列了
- 【資料結構之棧】用詳細圖文把「棧」搞明白(原理篇)
- 【資料結構之佇列】詳細圖解!在學習佇列?看這一篇就夠了!
- 【資料結構之連結串列】詳細圖文教你花樣玩連結串列
- 【資料結構之二叉樹】一文看懂二叉樹的概念和原理
- 【資料結構之二叉樹】二叉樹的建立及遍歷實現
- 【資料結構之線索二叉樹】線索二叉樹的原理及建立
- 【資料結構之二叉堆】二叉堆的原理及操作
1. 何為查詢?
我們平常做很多事情,都會涉及到大量的增刪改查操作。比如一個使用者管理系統,會涉及使用者註冊(增)、使用者登出(刪)、修改使用者資訊(改)、查詢使用者(查),其中“刪”和“改”要依賴“查”操作。
下面重點來介紹一下查詢這個重要的操作。
現給你一個點名冊,讓你查詢一個學生。我們的做法是:根據這個學生的姓名或者學號,在點名冊中一個個的比對,直到找到一個學號或姓名符合條件的學生為止,否則就可以說點名冊中沒有該學生。
學號 | 姓名 | 專業 |
---|---|---|
101 | 張三 | 鍵盤清潔與維修 |
102 | 李四 | 母豬產後護理與保養 |
103 | 李四 | 計算機開關機專業操作員 |
點名冊是一個集合,也可稱之為查詢表,其中有大量同一型別的元素,也可稱之為記錄——學生。學生中可能有重名的,但不會有重學號的,也即,一個學號唯一對應一個學生,一個姓名可能對應多個學生。如果我們根據學號找,只要點名冊中有,那麼就可以找到唯一一個符合條件的學生。如果我們根據姓名找,那麼我們就可能找到多個符合條件的學生。
像學號和姓名這種可以標識一個學生的值,我們稱之為關鍵字,學號這種唯一標識一個元素的值為主關鍵字,姓名這種可能標識若干元素的值為次關鍵字。當集合中的元素只有一個資料項時,其關鍵字即為該資料元素的值。
比如陣列[1, 2, 3, 4, 5, 6, 7, 8, 9],其元素只有一個資料項,關鍵字即元素值本身;而點名冊中的元素——學生,卻有三個資料項——學號、姓名、專業,其中學號、姓名為關鍵字。
如果你學過資料庫,那麼以上概念很容易理解。
所謂查詢,通俗點說就是在一大群元素(集合 / 查詢表)中,依照某個查詢依據,找一個特定的、符合要求的元素(記錄)。
-
如果找到了,即查詢成功,返回元素的資訊;
-
如果找遍所有元素還沒找到,說明這群元素中沒有符合要求的元素,即查詢失敗,返回一個可以明顯標記失敗的值,比如“空記錄”或“空指標”。
所謂查詢依據,就是給定一個目標值,比較該目標值和關鍵字是否相等。這就要求目標值和關鍵字的型別要相同。
2. 順序查詢(Sequential Search)
順序查詢是我們最容易想到的查詢方式,上面的點名冊例子中,查詢一個學生就是用的就是順序查詢。
順序查詢思想:
從集合中的第一個元素開始至最後一個元素,逐個比較其關鍵字和目標值。
- 若某個關鍵字和目標值相等,則查詢成功,返回所查元素的資訊;
- 若沒有一個關鍵字和目標值相等,則查詢失敗,返回失敗標識值。
比如,給定一個陣列[11, 8, 4, 6, 9, 1, 16, 22, 14, 10],給定目標值 key
,若找到,則返回其陣列下標;否則,返回 -1:
只需從下標 0 開始遍歷整個陣列進行比較即可:
/**
* @description: 從頭到尾遍歷整個陣列,查詢目標值 key,返回其下標 index
* @param {int} *array 陣列 為了說明問題簡單,這裡的陣列元素不重複
* @param {int} length 陣列長度
* @param {int} key 目標值
* @return {int} 如果找到,返回目標值下標;否則返回 -1
*/
int sequential_search(int *array, int length, int key)
{
for (int index = 0; index < length; index++) {
if (array[index] == key) {
return index;
}
}
return -1;
}
以上程式碼存在可優化的地方,因為每次比較之前要判斷陣列是否越界:index < length
,增加哨兵則可以避免這一步比較。
所謂哨兵,是一種形象的說法,將其放在陣列頭或尾,用來標記結束,當遍歷到“哨兵”時,就說明陣列中沒有目標值,查詢失敗。
為此,我們要特意在陣列中留出一個位置給“哨兵”,並且把哨兵的值設定為目標值:
像這樣,從另一側往“哨兵”一側遍歷。如果陣列中有目標值,則一定能找到;如果陣列中沒有目標值,那麼就會遍歷至“哨兵”而停下,因為“哨兵”的值就是目標值,所以返回下標為 0 時,意味著查詢失敗。
/**
* @description: 順序查詢改進,增加哨兵
* @param {int} *array array[0] 不存放資料元素,充當哨兵
* @param {int} length 陣列長度
* @param {int} key 目標值
* @return {int} 返回0,即哨兵下標,則查詢失敗;否則成功
*/
int sequential_search_pro(int *array, int length, int key)
{
array[0] = key; // 哨兵
int index = length - 1;
while (array[index] != key) {
index--;
}
return index;
}
在一側放置“哨兵”的做法避免了每次遍歷進行的陣列越界檢查,這樣能提高效率。你可能會問就一句比較能提高多少效率?蚊子腿再小也是肉,而且當資料量很多時,這些“蚊子腿”就會積累成“大象腿”了。
以上就是順序查詢的基本內容,雖然加了“哨兵”可以小小地優化一下,但當資料量極大時,仍然改變不了這種查詢方法效率低下的事實。
因為我們是從一頭到另一頭“順序遍歷”,所以有時候可能目標值就在第一個位置,只查詢一次就找到了,彷彿是天選之子;但有時候可能目標值在最後一個位置,那就需要把所有元素都查詢一遍才行,當有千萬、億條資料時,這種就太可怕了。
當然,優點也有:演算法簡單好理解、適合資料量小的情況使用(使用時儘量把常用資料排在前面,這樣可以提高效率)。
3. 二分查詢(Binary Search)
回到上面舉得那個點名冊的例子,那樣一個個地找學號或姓名實在是太慢了,有沒有什麼更快的方法呢?
其實,在日常生活中的點名冊更多的是已排序的,比如按姓氏首字母、按學號大小排序,這樣一來,在找名字或找學號的時候就能有一個大致的區域了,而不必從頭到尾一個個地找。
所以,排好序的集合是便於查詢的。下面介紹一種利用已排序的查詢——二分查詢(或折半查詢)。
所謂二分查詢,關鍵在“二分”“折半”上,顧名思義,不斷將集合進行二分(折半)拆分,以此將集合拆分幾個區域,然後在某個區域中查詢。前提條件是集合中的元素是有序的,元素必須採用順序表(陣列)儲存。
二分查詢思想:
在有序順序表中,取中間元素,將有序順序表分為左半區和右半區,比較中間元素的關鍵字和目標值 key
是否相等:
- 如果相等,則查詢成功,返回中間元素的資訊;
- 如果不相等,說明目標值
key
在左半區或右半區:- 若目標值
key
小於中間元素的關鍵字,則來到左半區; - 若目標值
key
大於中間元素的關鍵字,則來到右半區;
- 若目標值
不斷在各半區中重複上述過程,直到查詢成功;否則,則集合中無目標值,查詢失敗。
下面結合例項,看一下具體過程。
這是一個有序的陣列,我們要查詢 33:
要想將陣列分為左右半區,需要三個標緻:最左標誌位 left
、最右標誌位 right
和中間標誌位 mid
。其中 mid = (left + right) / 2
,確定了 mid
的值之後,與目標值 key
進行比較:
中間值 28 小於目標值key
,說明目標值在右半區,所以更新三個標誌位,進入右半區,然後繼續比較:
中間值 39 大於目標值key
,更新三個標誌位,進入左半區:
中間值 30 小於目標值key
,更新三個標誌位,進入右半區:
中間值 33 等於目標值key
,返回其下標,即 mid
。
具體程式碼如下:
/**
* @description: 二分查詢
* @param {int} *array 有序陣列
* @param {int} length 陣列長度
* @param {int} key 目標值,和關鍵字比較
* @return {int} 返回目標值下標;若查詢失敗,則返回 -1
*/
int binary_search(int *array, int length, int key)
{
int left, mid, right;
left = 0;
right = length - 1;
while (left <= right) {
mid = (left + right) / 2; // 中間下標
if (key < array[mid]) { // key 比中間值小
right = mid - 1; // 更新最右下標,進入左半區
} else if (key > array[mid]) { // key 比中間值大
left = mid + 1; // 更新最左下標,進入右半區
} else {
return mid; // key 等於中間值,返回其下標
}
}
return -1; //未找到,返回 -1
}
二分查詢的精髓在於中間標誌位 mid
把有序順序表一分為二,通過比較中間值和目標值的大小關係,能夠篩選掉一半的資料,相當於減少了一半的工作量。
上例只比較了四次,就找到了目標值,如果使用順序查詢,則需要八次。
可以看出,二分查詢的效率相較於順序查詢有了很大提高,但美中不足的是二分查詢必須要求元素有序。在元素的有序狀態不變化或不經常變化的情景下,二分查詢非常合適;但是如果涉及到頻繁的插入和刪除操作,就意味著元素的有序狀態會被頻繁破壞,這樣一來,我們就不得不花精力去維護元素的有序狀態,自然又會降低效率,所以要根據實際情況靈活取捨。
以上就是順序查詢和二分查詢的內容。
如有錯誤,還請指正。
如果覺得寫的不錯,可以點個贊和關注。後續會有更多資料結構和演算法相關文章。