《大話數據結構》筆記——第8章 查找(一)

8.1 開場白

8.2 查找概述

只要你打開電腦,就會涉及到查找技術。如炒股軟件中查股票信息、硬盤文件中找照片、在光盤中搜 DVD ,甚至玩遊戲時在內存中查找攻擊力、魅力值等數據修改用來作弊等,都要涉及到查找。當然,在互聯網上査找信息就更加是家常便飯。所有這些需要被查的數據所在的集合,我們給它一個統稱叫查找表。

査找表(Search Table) 是由同一類型的數據元素(或記錄)構成的集合。例如圖 8-2-1 就是一個查找表。

關鍵字( Key )是數據元素中某個數據項的值,又稱爲鍵值,用它可以標識一個數據元素。也可以標識一個記錄的某個數據項(字段),我們稱爲關鍵碼,如下圖中(1)和(2)所示。

若此關鍵字可以唯一地標識一個記錄,則稱此關鍵字爲主關鍵字( Primary Key )。 注意這也就意味着,對不同的記錄,其主關鍵字均不相同。主關鍵字所在的數據項稱爲主關鍵碼,如下圖中(3)和(4)所示。

那麼對於那些可以識別多個數據元素(或記錄)的關鍵字,我們稱爲次關鍵字( SecondaryKey ),如下圖中(5)所示。次關鍵字也可以理解爲是不以唯一標識一個數據元素(或記錄)的關鍵字,它對應的數據項就是次關鍵碼

在這裏插入圖片描述

查找( Searching )就是根據給定的某個值,在查找表中確定一個其關鍵字等於給定值的數據元素(或記錄)

若表中存在這樣的一個記錄,則稱查找是成功的,此時查找的結果給出整個記錄的信息,或指示該記錄在查找表中的位置。比如上圖所示,如果我們查找主關鍵碼 “代碼” 的主關鍵字爲 “sh601398” 的記錄時,就可以得到第 2 條唯一記錄。如果我們查找次關鍵碼 “漲跌額” 爲 “-0.11” 的記錄時,就可以得到兩條記錄。

若表中不存在關鍵字等於給定值的記錄,則稱查找不成功,此時查找的結果可給出一個 “空” 記錄或 “空” 指針。

查找表按照操作方式來分有兩大種:靜態查找表動態查找表

靜態査找表( Static Search Table ):只作査找操作的查找表。它的主要操作有:

  1. 查詢某個 “特定的” 數據元素是否在查找表中。
  2. 檢索某個 “特定的” 數據元素和各種屬性。

按照我們大多數人的理解,查找,當然是在已經有的數據中找到我們需要的。靜態查找就是在幹這樣的事情,不過,現實中還有存在這樣的應用:查找的目的不僅僅只是查找。

比如網絡時代的新名詞,如反應年輕人生活的 “蝸居” 、 “蟻族” 、 “孩奴” 、 “啃老” 等,以及 “X 客” 系列如博客、播客、閃客、黑客、威客等,如果需要將它們收錄到漢語詞典中,顯然收錄時就需要查找它們是否存在,以及找到如果不存在時應該收錄的位置。再比如,如果你需要對某網站上億的註冊用戶進行清理工作,註銷一些非法用戶,你就需查找到它們後進行刪除,刪除後其實整個查找表也會發生變化。對於這樣的應用,我們就引入了動態査找表。

動態査找表(Dynamic Search Table ):在査找過程中同時插入査找表中不存在的數據元素,或者從査找表中刪除已經存在的某個數據元素。顯然動態查找表的操作就是兩個:

  1. 查找時插入數據元素。

  2. 查找時刪除數據元素。

爲了提高查找的效率,我們需要專門爲查找操作設置數據結構,這種面向查找操作的數據結構稱爲查找結構

從邏輯上來說,查找所基於的數據結構是集合,集合中的記錄之間沒有本質關係。可是要想獲得較高的查找性能,我們就不能不改變數據元素之間的關係,在存儲時可以將査找集合組織成表、樹等結構。

例如,對於靜態查找表來說,我們不妨應用線性表結構來組織數據,這樣可以使用順序査找算法,如果再對主關鍵字排序,則可以應用折半查找等技術進行高效的查找。

如果是需要動態查找,則會複雜一些,可以考慮二叉排序樹的查找技術

另外,還可以用散列表結構來解決一些查找問題,這些技術都將在後面的講解中說明 。

8.3 順序查找

試想一下,要在散落的一大堆書中找到你需要的那本有多麼麻煩。碰到這種情況的人大都會考慮做一件事,那就是把這些書排列整齊,比如豎起來放置在書架上,這樣根據書名,就很容易查找到需要的圖書,如圖 8-3-1 所示。

在這裏插入圖片描述

散落的圖書可以理解爲一個集合,而將它們排列整齊,就如同是將此集合構造成一個線性表。我們要針對這一線性表進行査找操作,因此它就是靜態查找表

此時圖書儘管已經排列整齊,但還沒有分類,因此我們要找書只能從頭到尾或從尾到頭一本一本查看,直到找到或全部查找完爲止。這就是我們現在要講的順序查找。

順序査找( Sequential Search )又叫線性査找,是最基本的査找技術,它的査找過程是:從表中第一個(或最後一個)記錄開始,逐個進行記錄的關鍵字和給定值比較,若某個記錄的關鍵字和給定值相等,則查找成功,找到所査的記錄;如果直到最後一個(或第一個)記錄,其關鍵字和給定值比較都不等時,則表中沒有所查的記錄,查找不成功

8.3.1 順序表查找算法

順序查找的算法實現如下:

/* 順序查找,a 爲數組,n 爲要查找的數組個數,key 爲要查找的關鍵字*/ 
int Sequential_Search(int *a, int n, int key)
{
	int i;
	for (i = 1; i <= n; i++)
	{
		if (a[i] == key)
		{
			return i;
		}
	}
	return 0;
}

這段代碼非常簡單,就是在數組 a ( 注意元素值從下標 1 開始 )中查看有沒有關鍵字( key ),當你需要查找複雜表結構的記錄時,只需要把數組 a 與關鍵字 key 定義成你需要的表結構和數據類型即可。

8.3.2 順序表查找優化

到這裏並非足夠完美,因爲每次循環時都需要對 i 是否越界,即是否小於等於 n 作判斷。事實上,還可以有更好一點的辦法,設置一個哨兵,可以解決不需要每次讓 i 與 n 作比較。看下面的改進後的順序查找算法代碼。

/* 有哨兵順序查找 */
int Sequential_Search2(int *a, int n, int key)
{
	int i;
	a[0] = key; /* 設置 a[0] 爲關鍵字值,我們稱之爲“哨兵” */
	i = n;	/* 循環從數組尾部開始 */
	while (a[i] != key)
	{
		i--;
	}
	return i; /* 返回 0 則說明查找失敗 */
}

此時代碼是從尾部開始查找,由於 a[0]=key ,也就是說,如果在 a[i] 中有 key 則返回i值,查找成功。否則一定在最終的 a[0] 處等於 key ,此時返回的是 0 ,即說明 a[1]〜a[n] 中沒有關鍵字 key ,查找失敗。

這種在查找方向的盡頭放置 “哨兵” 免去了在查找過程中每一次比較後都要判斷查找位置是否越界的小技巧,看似與原先差別不大,但在總數據較多時,效率提高很大,是非常好的編碼技巧。當然, “哨兵” 也不—定就一定要在數組開始,也可以在末端。

對於這種順序查找算法來說,查找成功最好的情況就是在第一個位置就找到了,算法時間複雜度爲 O(1) ,最壞的情況是在最後一位置才找到,需要 n 次比較,時間複雜度爲 O(n) ,當查找不成功時,需要 n+1 次比較,時間複雜度爲 O(n) 。我們之前推導過,關鍵字在任何一位置的概率是相同的,所以平均查找次數爲 (n+1)/2 ,所以最終時間複雜度還是 O(n)

很顯然,順序查找技術是有很大缺點的,n 很大時,查找效率極爲低下,不過優點也是有的,這個算法非常簡單,對靜態查找表的記錄沒有任何要求,在一些小型數據的查找時,是可以適用的。

另外,也正由於査找概率的不同,我們完全可以將容易查找到的記錄放在前面,而不常用的記錄放置在後面,效率就可以有大幅提高。

8.4 有序表查找

我們如果僅僅是把書整理在書架上,要找到一本書還是比較困難的,也就是剛纔講的需要逐個順序査找。但如果我們在整理書架時,將圖書按照書名的拼音排序放置,那麼要找到某一本書就相對容易了。說白了,就是對圖書做了有序排列,一個線性表有序時,對於查找總是很有幫助的

8.4.1 折半查找

我們在樹結構的二叉樹定義時,曾經提到過一個小遊戲,我在紙上已經寫好了一個 100 以內的正整數數字請你猜,問幾次可以猜出來,當時已經介紹瞭如何最快猜出這個數字。我們把這種每次取中間記錄查找的方法叫做折半查找,如圖 8-4-1 所示

在這裏插入圖片描述

折半査找( Binary Search )技術,又稱爲二分査找。它的前提是線性表中的記錄必須是關鍵碼有序(通常從小到大有序),線性表必須採用順序存儲。折半査找的基本思想是:在有序表中,取中間記錄作爲比較對象,若給定值與中間記錄的關鍵字相等,則査找成功;若給定值小於中間記錄的關鍵字,則在中間記錄的左半區繼續査找;若給定值大於中間記錄的關鍵字,則在中間記錄的右半區繼續査找。不斷重複上述過程,直到査找成功,或所有査找區域無記錄,査找失敗爲止

假設我們現在有這樣一個有序表數組 { 0, 1, 16, 24, 35, 47, 59, 62, 73, 88, 99 } ,除 0 下標外共 10 個數字。對它進行查找是否存在 62 這個數。我們來看折半查找的算法是如何工作的。

/* 折半查找 */
int Binary_Search(int *a, int n, int key)
{
	int low, high, mid;
	low = 1;	/* 定義最低下標爲記錄首位 */
	high = n;	/* 定義最高下標爲記錄末位 */
	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;	/* 若相等則說明 mid 即爲查找到的位置 */
		}
	}
	return 0;
}
  1. 程序開始運行,參數 a={ 0, 1, 16, 24, 35, 47, 59, 62, 73, 88, 99 } , n=10 , key=62 ,第 3〜6 行,此時 low=1 , high=10 ,如圖 8-4-2 所示。

    在這裏插入圖片描述

  2. 第 7〜22 行循環,進行查找。

  3. 第 9 行,mid 計算得 5 ,由於 a[5]=47<key ,所以執行了第 17 行, low=5+1=6 ,如圖 8-4-3 所示。

    在這裏插入圖片描述

  4. 再次循環,mid=(6+10)/2=8 ,此時 a[8]=73>key ,所以執行第 12 行, high=8-1=7 ,如圖 8-4-4 所示。

    在這裏插入圖片描述

  5. 再次循環,mid=(6+7)/2=6 ,此時 a[6]=59<key ,所以執行 17 行, low=6+1=7 ,如圖 8-4-5 所示。

    在這裏插入圖片描述

  6. 再次循環,mid=(7+7)/2=7 ,此時 a[7]=62=key ,査找成功,返回 7 。

該算法還是比較容易理解的,同時我們也能感覺到它的效率非常高。但到底高多少?關鍵在於此算法的時間複雜度分析。

首先,我們將這個數組的查找過程繪製成一棵二叉樹,如圖 8-4-6 所示,從圖上就可以理解,如果查找的關鍵字不是中間記錄 47 的話,折半查找等於是把靜態有序查找表分成了兩棵子樹,即查找結果只需要找其中的一半數據記錄即可,等於工作量少了一半,然後繼續折半查找,效率當然是非常高了。

在這裏插入圖片描述

我們之前講的二叉樹的性質 4:" 具有 n 個結點的完全二叉樹的深度爲 [log2n]+1 "。在這裏儘管折半查找判定二叉樹並不是完全二叉樹,但同樣相同的推導可以得出,最壞情況是查找到關鍵字或查找失敗的次數爲 [log2n]+1。

有人還在問最好的情況?那還用說嗎,當然是 1 次了。

因此最終我們折半算法的時間複雜度爲 O(logn) ,它顯然遠遠好於順序查找的 O(n) 時間複雜度了。

不過由於折半查找的前提條件是需要有序表順序存儲,對於靜態查找表,一次排序後不再變化,這樣的算法已經比較好了。但對於需要頻繁執行插入或刪除操作的數據集來說,維護有序的排序會帶來不小的工作量,那就不建議使用。

8.4.2 插值查找

現在我們的新問題是,爲什麼一定要折半,而不是折四分之一或者折更多呢?

打個比方,在英文詞典裏查 “apple”,你下意識裏翻開詞典是翻前面的書頁還是後面的書頁呢?如果再讓你查 “zoo”,你又怎麼查?很顯然,這裏你絕對不會是從中間開始查起,而是有一定目的的往前或往後翻。

在這裏插入圖片描述

同樣的,比如要在取值範圍 0~10000 之間 100 個元素從小到大均勻分佈的數組中查找 5,我們自然會考慮從數組下標較小的開始查找。

看來,我們的折半查找,還是有改進空間的。

折半查找代碼的第 9 句,我們略微等式變換後得到:

在這裏插入圖片描述

也就是 mid 等於最低下標 low 加上最高下標 high 與 low 的差的一半。算法科學家們考慮的就是將這個 1/2 進行改進,改進爲下面的計算方案:

在這裏插入圖片描述

將 1/2 改成了 在這裏插入圖片描述 有什麼道理呢?假設 a[11]={ 0, 1, 16, 24, 35, 47, 59, 62, 73, 88, 99 },low=1 , high = 10 ,則 a[low]=1 , a[high]=99 ,如果我們要找的是 key=16 時,按原來折半的做法,我們需要四次(如圖 8-4-6 所示)纔可以得到結果,但如果用新辦法,在這裏插入圖片描述 ,即 在這裏插入圖片描述 取整得到 mid=2 ,我們只需要二次就查找到結果了,顯然大大提高了查找的效率。

換句話說,我們只需要在折半查找算法的代碼中更改一下第 9 行代碼如下:

mid=low+ ( high-low )*( key-a[low] )/( a[high]-a[low] ); /* 插值 */

就得到了另一種有序表查找算法,插值查找法。插值査找( Interpolation Search )是根據要查找的關鍵字 key 與查找表中最大最小記錄的關鍵字比較後的查找方法其核心就在於插值的計算公式 在這裏插入圖片描述

應該說,從時間複雜度來看,它也是 O(logn) 但對於表長較大,而關鍵字分佈又比較均勻的查找表來說,插值查找算法的平均性能比折半查找要好得多。反之,數組中如果分佈類似 {0,1,2,2000,2001,……, 999998, 999999 } 這種極端不均勻的數據,用插值查找未必是很合適的選擇。

8.4.3 斐波那契查找

還有沒有其他辦法?我們折半查找是從中間分,也就是說,每一次查找總是一分爲二,無論數據偏大還是偏小,很多時候這都未必就是最合理的做法。除了插值查找,我們再介紹一種有序查找,斐波那契查找(Fibonacci Search),它是利用了黃金分割原理來實現的。

爲了能夠介紹清楚這個查找算法,我們先需要有一個斐波那契數列的數組,如圖 8-4-8 所示。

在這裏插入圖片描述
下面我們根據代碼來看程序是如何運行的。

/*斐波那契查找*/
int Fibonacci_Search(int *a, int n, int key)
{
	int low, high, mid, i, k;
	low = 1;	/*	定義最低下標爲記錄首位 */
	high = n;	/*	定義最高下標爲記錄末位 */
	k = 0;
	while (n > F[k] - 1)	/*	計算 n 位於斐波那契數列的位置 */
	{
		k++;
	}
	for (i = n; i < F[k] - 1; i++)	/* 將不滿的數值補全 */
	{
		a[i] = a[n];
	}
	while (low <= high)
	{
		mid = low + F[k - 1] - 1;	/*	計算當前分隔的下標 */
		if (key<a[mid])		/* 若查找記錄小於當前分隔記錄 */
		{
			high = mid - 1;	/*	最高下標調整到分隔下標 mid-1 處 */
			k = k - 1;		/* 斐波那契數列下標減一位 */
		}
		else if (key>a[mid]) /*	若査找記錄大於當前分隔記錄 */
		{
			low = mid + 1;	/* 最低下標調整到分隔下標 mid+1 處 */
			k = k - 2;		/* 斐波那契數列下標減兩位 */
		}
		else
		{
			if (mid <= n)
			{
				return mid;	/*	若相等則說明 mid 即爲查找到的位置 */
			}
			else
			{
				return n;	/*	若 mid>n 說明是補全數值,返回n */
			}
		}
	}
	return	0;
}
  1. 程序開始運行,參數 a={ 0, 1, 16, 24, 35, 47, 59, 62, 73, 88, 99 } , n=10 ,要查找的關鍵字 key=59 。注意此時我們已經有了事先計算好的全局變量數組 F 的具體數據,它是斐波那契數列,F={ 0, 1, 1, 2, 3, 5, 8, 13, 21, …… }。

    在這裏插入圖片描述

  2. 第 7〜11 行是計算當前的 n 處於斐波那契數列的位置。現在 n=10 ,F[6]<n<F[7] ,所以計算得出 k=7 。

  3. 第 12〜15 行,由於 k=7 ,計算時是以 F[7]=13 爲基礎,而 a 中最大的僅是 a[10] ,後面的 a[11] , a[12] 均未賦值,這不能構成有序數列,因此將它們都賦值爲最大的數組值,所以此時 a[11]=a[12]=a[10]=99 (此段代碼作用後面還有解釋)。

  4. 第 16〜40 行査找正式開始。

  5. 第 18 行,mid=1+ F[7-1]-1=8 ,也就是說,我們第一個要對比的數值是從下標爲 8 開始的。

  6. 由於此時 key=59 而 a[8]=73 ,因此執行第 21〜22 行,得到 high=7 , k=6 。

    在這裏插入圖片描述

  7. 再次循環, mid=1 + F[6-1]-1=5 。此時 a[5]=47<key ,因此執行第 26〜27 行,得到 low=6 , k=6-2=4 。注意此時 k 下調 2 個單位。

    在這裏插入圖片描述

  8. 再次循環, mid=6 + F[4-1]-1=7 。此時 a[7]=62>key ,因此執行第 21〜22 行,得到 high=6 , k=4-1=3 。

    在這裏插入圖片描述

  9. 再次循環, mid=6 + F[3-1]-1=6 。此時 a[6]=59=key ,因此執行第 31〜34 行,得到返回值爲 6 。程序運行結束。

如果 key=99 ,此時查找循環第一次時, mid=8 與上例是相同的,第二次循環時, mid=11 ,如果 a[11] 沒有值就會使得與 key 的比較失敗,爲了避免這樣的情況出現, 第 12〜15 行的代碼就起到這樣的作用。

斐波那契查找算法的核心在於:

  1. 當 key=a[mid] 時,査找就成功;
  2. 當 key<a[mid] 時,新範圍是第 low 個到第 mid-1 個,此時範圍個數爲 F[k-1]-1 個;
  3. 當 key>a[mid] 時,新範圍是第 m+1 個到第 high 個,此時範圍個數爲 F[k-2]-1 個。

在這裏插入圖片描述

也就是說,如果要查找的記錄在右側,則左側的數據都不用再判斷了,不斷反覆進行下去,對處於當中的大部分數據,其工作效率要高一些。所以儘管斐波那契查找的時間複雜也爲 O(logn),但就平均性能來說,斐波那契查找要優於折半查找。可惜如果是最壞情況,比如這裏 key=1 ,那麼始終都處於左側長半區在查找,則查找效率要低於折半查找。

應該說,三種有序表的查找本質上是分隔點的選擇不同,各有優劣,實際開發時可根據數據的特點綜合考慮再做出選擇

8.5 線性索引查找

我們前面講的幾種比較髙效的查找方法都是基於有序的基礎之上的,但事實上,很多數據集可能增長非常快,例如,某些微博網站或大型論壇的帖子和回覆總數每天都是成百萬上千萬條,如圖 8-5-1 所示,或者一些服務器的日誌信息記錄也可能是海量數據,要保證記錄全部是按照當中的某個關鍵字有序,其時間代價是非常高昂的,所以這種數據通常都是按先後順序存儲。

在這裏插入圖片描述

那麼對於這樣的查找表,我們如何能夠快速查找到需要的數據呢?辦法就是——索引。

數據結構的最終目的是提高數據的處理速度,索引是爲了加快查找速度而設計的一種數據結構。索引就是把一個關鍵字與它對應的記錄相關聯的過程,一個索引由若干個索引項構成,每個索引項至少應包含關鍵字和其對應的記錄在存儲器中的位置等信息。索引技術是組織大型數據庫以及磁盤文件的一種重要技術。

索引按照結構可以分爲線性索引、樹形索引和多級索引。我們這裏就只介紹線性索引技術。所謂線性索引就是將索引項集合組織爲線性結構,也稱爲索引表。我們重點介紹三種線性索引:稠密索引、分塊索引和倒排索引。

8.5.1 稠密索引

我母親年紀大了,記憶力不好,經常在家裏找不到東西,於是她想了一個辦法。她用一個小本子記錄了家裏所有小東西放置的位置,比如戶口本放在右手牀頭櫃下面抽屜中,針線放在電視櫃中間的抽屜中,鈔票放在衣櫃……總之,她老人家把這些小物品的放置位置都記錄在了小本子上,並且每隔一段時間還按照本子整理一遍家中的物品,用完都放回原處,這樣她就幾乎再沒有找不到東西。

記得有一次我申請職稱時,單位一定要我的大學畢業證,我在家裏找了很長時間未果,急得要死。和老媽一說,她的神奇小本子馬上發揮作用,一下子就找到了,原來被她整理後放到了衣櫥裏的抽屜裏。

從這件事情就可以看出,家中的物品儘管是無序的,但是如果有一個小本子記錄,尋找起來也是非常容易的,而這小本子就是索引。

稠密索引是指在線性索引中,將數據集中的每個記錄對應一個索引項,如圖 8-5-2 所示。

在這裏插入圖片描述

剛纔的小例子和稠密索引還是略有不同,家裏的東西畢竟少,小本子再多也就幾十頁,全部翻看完就幾分鐘時間,而稠密索引要應對的可能是成千上萬的數據,因此對於稠密索引這個索引表來說,索引項一定是按照關鍵碼有序的排列

索引項有序也就意味着,我們要查找關鍵字時,可以用到折半、插值、斐波那契等有序查找算法,大大提高了效率。比如上圖中,我要查找關鍵字是 18 的記錄, 如果直接從右側的數據表中查找,那隻能順序查找,需要查找 6 次纔可以查到結果。 而如果是從左側的索引表中査找,只需兩次折半查找就可以得到 18 對應的指針,最終查找到結果。

這顯然是稠密索引優點。但是如果數據集非常大,比如上億,那也就意味着索引也得同樣的數據集長度規模,對於內存有限的計算機來說,可能就需要反覆去訪問磁盤,查找性能反而大大下降了。

8.5.2 分塊索引

回想一下圖書館是如何藏書的。顯然它不會是順序擺放後,給我們一個稠密索引表去查,然後再找到書給你。圖書館的圖書分類擺放是一門非常完整的科學體系,而它最重要的一個特點就是分塊。

在這裏插入圖片描述

稠密索引因爲索引項與數據集的記錄個數相同,所以空間代價很大。爲了減少索引項的個數,我們可以對數據集進行分塊,使其分塊有序,然後再對每一塊建立一個索引項,從而減少索引項的個數

分塊有序,是把數據集的記錄分成了若干塊,並且這些塊需要滿足兩個條件:

  • 塊內無序,即每一塊內的記錄不要求有序。當然,你如果能夠讓塊內有序對查找來說更理想,不過這就要付出大量時間和空間的代價,因此通常我們不要求塊內有序。
  • 塊間有序,例如,要求第二塊所有記錄的關鍵字均要大於第一塊中所有記錄的關鍵字,第三塊的所有記錄的關鍵字均要大於第二塊的所有記錄關鍵字…… 因爲只有塊間有序,纔有可能在查找時帶來效率。

對於分塊有序的數據集,將每塊對應一個索引項,這種索引方法叫做分塊索引

如圖 8-5-4 所示,我們定義的分塊索引的索引項結構分三個數據項:

  • 最大關鍵碼,它存儲每一塊中的最大關鍵字,這樣的好處就是可以使得在它之後的下一塊中的最小關鍵字也能比這一塊最大的關鍵字要大;
  • 存儲了塊中的記錄個數,以便於循環時使用;
  • 用於指向塊首數據元素的指針,便於開始對這一塊中記錄進行遍歷。

在這裏插入圖片描述

在分塊索引表中查找,就是分兩步進行:

  1. 在分塊索引表中查找要查關鍵字所在的塊。由於分塊索引表是塊間有序的, 因此很容易利用折半、插值等算法得到結果。例如,在上圖的數據集中查找 62 ,我們可以很快可以從左上角的索引表中由 57<62<96 得到 62 在第三個塊中。

  2. 根據塊首指針找到相應的塊,並在塊中順序查找關鍵碼。因爲塊中可以是無序的,因此只能順序查找。

我們再來分析一下分塊索引的平均查找長度。設 n 個記錄的數據集被平均分成 m 塊,每個塊中有 t 條記錄,顯然 n=m x t ,或者說 m = n/t 。再假設 Lb 爲查找索引表的平均查找長度,因最好與最差的等概率原則,所以 Lb 的平均長度爲 (m+1)/2 。Lw 爲塊中查找記錄的平均查找長度,同理可知它的平均查找長度爲 (t+1)/2 。

這樣分塊索引查找的平均查找長度爲:

在這裏插入圖片描述

注意上面這個式子的推導是爲了讓整個分塊索引查找長度依賴 n 和 t 兩個變量。 從這裏了我們也就得到,平均長度不僅僅取決於數據集的總記錄數 n ,還和每一個塊的記錄個數 t 相關。最佳的情況就是分的塊數 m 與塊中的記錄數 t 相同,此時意味着 n= m x t = t^2,即 在這裏插入圖片描述

可見,分塊索引的效率比之順序查找的 O(n) 是高了不少,不過顯然它與折半查找的 O(logn) 相比還有不小的差距。因此在確定所在塊的過程中,由於塊間有序,所以可以應用折半、插值等手段來提高效率

總的來說,分塊索引在兼顧了對細分塊不需要有序的情況下,大大增加了整體查找的速度,所以普遍被用於數據庫表查找等技術的應用當中。

8.5.3 倒排索引

不知道大家有沒有對搜索引擎好奇過,無論你查找什麼樣的信息,它都可以在極短的時間內給你一些結果,如圖 8-5-5 所示。是什麼算法技術達到這樣的高效查找呢?

在這裏插入圖片描述

在這裏介紹最簡單的,也算是最基礎的搜索技術——倒排索引。

我們來看樣例,現在有兩篇極短的英文“文章” ——其實只能算是句子,我們暫認爲它是文章,編號分別是1 和 2 。

  1. Books and friends should be few but good ( 讀書如交友,應求少而精 )

  2. A good book is a good friend ( 好書如摯友 )

假設我們忽略掉如 “books” 、 “friends” 中的複數 “s” 以及如 “A” 這樣的大小寫差異。我們可以整理出這樣一張單詞表,如表 8-5-1 所示,並將單詞做了排序,也就是表格顯示了每個不同的單詞分別出現在哪篇文章中,比如 “good” 它在兩篇文章中都有出現,而 “is” 只是在文章 2 中才有。

在這裏插入圖片描述

有了這樣一張單詞表,我們要搜索文章,就非常方便了。如果你在搜索框中填寫 “book” 關鍵字。系統就先在這張單詞表中有序査找 “book” ,找到後將它對應的文章編號 1 和 2 的文章地址(通常在搜索引擎中就是網頁的標題和鏈接)返回,並告訴你,查找到兩條記錄,用時 0.0001 秒。由於單詞表是有序的,查找效率很高,返回的又只是文章的編號,所以整體速度都非常快。

如果沒有這張單詞表,爲了能證實所有的文章中有還是沒有關鍵字 “book” ,則需要對每一篇文章每一個單詞順序查找。在文章數是海量的情況下,這樣的做法只存在理論上可行性,現實中是沒有人願意使用的。

在這裏這張單詞表就是索引表,索引項的通用結構是:

  • 次關鍵碼,例如上面的 “英文單詞” ;
  • 記錄號表,例如上面的 “文章編號” 。

其中記錄號表存儲具有相同次關鍵字的所有記錄的記錄號(可以是指向記錄的指針或者是該記錄的主關鍵字)。這樣的索引方法就是倒排索引( inverted index )。倒排索引源於實際應用中需要根據屬性(或字段、次關鍵碼)的值來查找記錄。這種索引表中的每一項都包括一個屬性值和具有該屬性值的各記錄的地址。由於不是由記錄來確定屬性值,而是由屬性值來確定記錄的位置,因而稱爲倒排索引。

倒排索引的優點顯然就是查找記錄非常快,基本等於生成索引表後,查找時都不用去讀取記錄,就可以得到結果。但它的缺點是這個記錄號不定長,比如上例有 7 個單詞的文章編號只有一個,而 “book” 、 “friend” 、 “good” 有兩個文章編號,若是對多篇文章所有單詞建立倒排索引,那每個單詞都將對應相當多的文章編號,維護比較困難,插入和刪除操作都需要作相應的處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章