查找(順序查找、二分查找、插值查找)


查找定義 :根據給定的某個值,在查找表中肯定一個其關鍵字等於給定值的數據元素(或記錄)。
查找表分類:靜態查找表和動態查找表。
動態查找表:在查找過程當中同時插入查找表中不存在的數據元素,或者從查找表中刪除已經存在的某個數據元素。

1. 順序查找

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

/*有哨兵順序查找-優化最簡單的順序查找*/
/*優化部分:a[0]存放要查找的關鍵字key,減小了數組越界的比較,若是查找表長度很大,仍是比最簡單的順序查找快不少的。ps:a[0] = key的目的就是讓上述的簡單順序查找的兩次判斷修正爲一次a[i]與a[0]是否相等的一次判斷。*/
int Sequential_Search2(int *a, int n, int key)
{
     int i;
     a[0] = key;
     for(i = n; a[i] != a[0]; i--);
     return i;
}
  平均查找長度 (Average Search Length,ASL)
     需和指定key進行比較的關鍵字的個數的指望值,成爲查找算法在查找成功時的平均查找長度。
     對於含有n個數據元素的查找表,查找成功的平均查找長度爲:ASL = Pi*Ci的和。
     Pi:查找表中第i個數據元素的機率。
     Ci:找到第i個數據元素時已經比較過的次數。
     順序查找 查找 成功 時的平均查找長度爲:
     (假設每一個數據元素的機率相等) ASL = 1/n(1+2+3+…+n) = (n+1)/2 ;
     當查找 不成功 時,須要n+1次比較,時間複雜度爲O(n);


2. 有序表查找


2.1 折半查找

前提:線性表中的記錄必須是關鍵字有序(一般從小到大),線性表必須採用 順序存儲。
基本思想:取中間記錄做爲比較對象,若給定值與中間記錄的關鍵字相等,則查找成功;若給定值小於中間記錄的關鍵字,則在中間記錄左半區繼續查找;不然,在右半區查找。不斷重複,知道查找成功或者查找失敗爲止。
/*折半查找,非遞歸*/
int Binary_Search(int *a, int n, int key)
{
     int low, high;
     int mid;
     low = 1;
     high = n;
    
     while(low < high) //能夠有等號 low < = high ,不影響判斷。
     {
          mid = (low + high) / 2;  //能夠修正爲 mid = (low + high) >> 1;
          if(a[mid] == key)
               return mid;
          if(a[mid] > key)
               high = mid - 1;
          if(a[mid] < key)
               low = mid + 1; //對於if else語句,能夠考慮條件表達式,減小代碼的行數。
     }
     return 0;
}
/*折半查找,遞歸實現*/
int Binary_Search2(int *a, int low, int high, int key)
{
     int mid = (low + high) / 2;
     if(a[mid] == key)
          return mid;
     if(a[mid] > key)
          return Binary_Search2(a, low, mid-1, key);     //有沒有return均可以。
     else
          return Binary_Search2(a, mid+1, high, key);     //有沒有return均可以。
}
/*      ASL: 「具備n個節點的徹底二叉樹的深刻爲[log2n]+1,儘管折半查找斷定二叉樹不是徹底二叉樹,但一樣相同的推導能夠得出,
               最壞狀況是查找到關鍵字或查找失敗的次數爲[log2n]+1」,最好狀況是1,因此折半查找的時間複雜度爲o(logN)>o(n).

     注:折半查找的前提條件是須要有序表順序存儲,對於靜態查找表,一次排序後再也不變化,這樣的算法已經比較好了。但對於須要
     頻繁執行插入或刪除操做的數據集來講,維護有序的排序會帶來不小的工做量,那就不建議使用。——《大話數據結構》
*/

2.2 插值查找

首先考慮一個新問題,爲何必定要是折半,而不是折四分之一或者折更多呢?
打個比方,在英文字典裏面查「apple」,你下意識翻開字典是翻前面的書頁仍是後面的書頁呢?若是再讓你查「zoo」,你又怎麼查?很顯然,這裏你絕對不會是從中間開始查起,而是有必定目的的往前或日後翻。
一樣的,好比要在取值範圍1 ~ 10000 之間 100 個元素從小到大均勻分佈的數組中查找5, 咱們天然會考慮從數組下標較小的開始查找。
通過以上分析,折半查找這種查找方式,仍是有改進空間的,並不必定是折半的!
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(n),可是對於表長較大,而關鍵字分佈又比較均勻的查找表來講,插值查找算法的平均性能比折半查找要好的多。反之,數組中若是分佈很是不均勻,那麼差值查找未必是很合適的選擇。

2.3 斐波那契查找

原理:利用斐波那契數列的性質,黃金分割的原理來肯定mid的位置。
代碼以下:
/*斐波那契 查找*/
int Fbonacci_Search(int *a, int n, int key)
{
	int low,high,mid,i,k;
	int F[] = {0,1,1,2,3,5,8,13,21,34};		//經典的斐波那契數列已經早就定義好,也能夠遞歸本身求解。
	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;
			k--;
		}
		else 
		{
			if(key > a[mid])
			{
				low = mid + 1;
				k = k -2;
			}
			else
			{
				if(mid <= n)
					return mid;
				else
					return n;
			}
		}
	}
}

總結:
折半查找進行加法與除法運算(mid = (low + high) / 2),插值查找進行復雜的四則運算( mid = low + (key - a[low] / (a[high] - a[low]) * (high - low)) ),二斐波那契查找只是運用簡單家減法運算 (mid  = low + f[k-1] -1) ,在海量的數據查找過程當中,這種席位的差異會影響最終的查找效率。三種有序表的查找本質上是分割點的選擇不一樣,各有優劣,實際開發可根據數據的特色綜合考慮再作決定。