從零開始養成算法·篇十九:查找算法

查找是在大量的信息中尋找一個特定的信息元素,在計算機應用中,查找是常用的基本運算,例如編譯程序中符號表的查找。本文簡單概括性的介紹了常見的七種查找算法,說是七種,其實二分查找、插值查找以及斐波那契查找都可以歸為一類——插值查找。插值查找和斐波那契查找是在二分查找的基礎上的優化查找算法。樹表查找和哈希查找會在后續的博文中進行詳細介紹。

查找定義:根據給定的某個值,在查找表中確定一個其關鍵字等于給定值的數據元素(或記錄)。

查找算法分類:
  1)靜態查找和動態查找;
    注:靜態或者動態都是針對查找表而言的。動態表指查找表中有刪除和插入操作的表。
  2)無序查找和有序查找。
    無序查找:被查找數列有序無序均可;
    有序查找:被查找數列必須為有序數列。

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

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

1. 順序查找

說明:順序查找適合于存儲結構為順序存儲或鏈接存儲的線性表。

基本思想:順序查找也稱為線形查找,屬于無序查找算法。從數據結構線形表的一端開始,順序掃描,依次將掃描到的結點關鍵字與給定值k相比較,若相等則表示查找成功;若掃描結束仍沒有找到關鍵字等于k的結點,表示查找失敗。

復雜度分析: 
  查找成功時的平均查找長度為:(假設每個數據元素的概率相等) ASL = 1/n(1+2+3+…+n) = (n+1)/2 ;
  當查找不成功時,需要n+1次比較,時間復雜度為O(n);
  所以,順序查找的時間復雜度為O(n)。

\color{red}{代碼示例:}

int Sequential_Search(int *a,int n,int key){
   for (int i = 1; i <= n ; i++)
       if (a[i] == key)
           return i;
  
   return 0;
}
2. 二分查找

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

基本思想:也稱為是折半查找,屬于有序查找算法。用給定值k先與中間結點的關鍵字比較,中間結點把線形表分成兩個子表,若相等則查找成功;若不相等,再根據k與該中間結點關鍵字的比較結果確定下一步查找哪個子表,這樣遞歸進行,直到查找到或查找結束發現表中沒有這樣的結點。

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

注:折半查找的前提條件是需要有序表順序存儲,對于靜態查找表,一次排序后不再變化,折半查找能得到不錯的效率。但對于需要頻繁執行插入或刪除操作的數據集來說,維護有序的排序會帶來不小的工作量,那就不建議使用?!洞笤挃祿Y構》

\color{red}{代碼示例:}

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;
   }
   
   return 0;
}
3. 插值查找

在介紹插值查找之前,首先考慮一個新問題,為什么上述算法一定要是折半,而不是折四分之一或者折更多呢?

打個比方,在英文字典里面查“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(log2(log2n))。

\color{red}{代碼示例:}

int Interpolation_Search(int *a,int n,int key){
   int low,high,mid;
   low = 1;
   high = n;
   
   while (low <= high) {
       
       mid = low+ (high-low)*(key-a[low])/(a[high]-a[low]);
   
       if (key < a[mid]) {
           high = mid-1;
       }else if(key > a[mid]){
           low = mid+1;
       }else
           return mid;
   }
   
   return 0;
}
4. 斐波那契查找

在介紹斐波那契查找算法之前,我們先介紹一下很它緊密相連并且大家都熟知的一個概念——黃金分割。

黃金比例又稱黃金分割,是指事物各部分間一定的數學比例關系,即將整體一分為二,較大部分與較小部分之比等于整體與較大部分之比,其比值約為1:0.618或1.618:1。

0.618被公認為最具有審美意義的比例數字,這個數值的作用不僅僅體現在諸如繪畫、雕塑、音樂、建筑等藝術領域,而且在管理、工程設計等方面也有著不可忽視的作用。因此被稱為黃金分割。

大家記不記得斐波那契數列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(從第三個數開始,后邊每一個數都是前兩個數的和)。然后我們會發現,隨著斐波那契數列的遞增,前后兩個數的比值會越來越接近0.618,利用這個特性,我們就可以將黃金比例運用到查找技術中。

基本思想:也是二分查找的一種提升算法,通過運用黃金比例的概念在數列中選擇查找點進行查找,提高查找效率。同樣地,斐波那契查找也屬于一種有序查找算法。
  相對于折半查找,一般將待比較的key值與第mid=(low+high)/2位置的元素比較,比較結果分三種情況:
1)相等,mid位置的元素即為所求
2)大于,low=mid+1;
3)小于,high=mid-1。

斐波那契查找與折半查找很相似,他是根據斐波那契序列的特點對有序表進行分割的。他要求開始表中記錄的個數為某個斐波那契數小1,及n=F(k)-1;

開始將k值與第F(k-1)位置的記錄進行比較(及mid=low+F(k-1)-1),比較結果也分為三種
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)。

\color{red}{代碼示例:}

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) {
       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 = k-1;
       }else if(key > a[mid]){
           low = mid+1;
           k = k-2;
           
       }else{
           if (mid <= n) {
               return mid;
           }else
           {
               return n;
           }
       }
   }
   return 0;
}
5. 最簡單的樹表查找算法——二叉樹查找算法

基本思想:二叉查找樹是先對待查找的數據進行生成樹,確保樹的左分支的值小于右分支的值,然后在就行和每個節點的父節點比較大小,查找最適合的范圍。 這個算法的查找效率很高,但是如果使用這種查找方法要首先創建樹。

二叉查找樹(BinarySearch Tree,也叫二叉搜索樹,或稱二叉排序樹Binary Sort Tree)或者是一棵空樹,或者是具有下列性質的二叉樹:
1)若任意節點的左子樹不空,則左子樹上所有結點的值均小于它的根結點的值;
2)若任意節點的右子樹不空,則右子樹上所有結點的值均大于它的根結點的值;
3)任意節點的左、右子樹也分別為二叉查找樹。

二叉查找樹性質:對二叉查找樹進行中序遍歷,即可得到有序的數列。

復雜度分析:它和二分查找一樣,插入和查找的時間復雜度均為O(logn),但是在最壞的情況下仍然會有O(n)的時間復雜度。原因在于插入和刪除元素的時候,樹沒有保持平衡(比如,我們查找上圖(b)中的“93”,我們需要進行n次查找操作)。我們追求的是在最壞的情況下仍然有較好的時間復雜度,這就是平衡查找樹設計的初衷。

基于二叉查找樹進行優化,進而可以得到其他的樹表查找算法,如平衡樹、紅黑樹等高效算法。

二叉排序樹的查找、插入、刪除等操作

\color{red}{代碼示例:}

//二叉樹的二叉鏈表結點結構定義
//結點結構
typedef  struct BiTNode
{
   //結點數據
   int data;
   //左右孩子指針
   struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

//1.二叉排序樹--查找
/*
遞歸查找二叉排序樹T中,是否存在key;
指針f指向T的雙親,器初始值為NULL;
若查找成功,則指針p指向該數據元素的結點,并且返回TRUE;
若指針p指向查找路徑上訪問的最后一個結點則返回FALSE;
*/
Status SearchBST(BiTree T,int key,BiTree f, BiTree *p){
  
   if (!T)    /*  查找不成功 */
   {
       *p = f;
       return FALSE;
   }
   else if (key==T->data) /*  查找成功 */
   {
       *p = T;
       return TRUE;
   }
   else if (key<T->data)
       return SearchBST(T->lchild, key, T, p);  /*  在左子樹中繼續查找 */
   else
       return SearchBST(T->rchild, key, T, p);  /*  在右子樹中繼續查找 */
}

//2.二叉排序樹-插入
/*  當二叉排序樹T中不存在關鍵字等于key的數據元素時, */
/*  插入key并返回TRUE,否則返回FALSE */
Status InsertBST(BiTree *T, int key) {
   
   BiTree p,s;
   //1.查找插入的值是否存在二叉樹中;查找失敗則->
   if (!SearchBST(*T, key, NULL, &p)) {
       
       //2.初始化結點s,并將key賦值給s,將s的左右孩子結點暫時設置為NULL
       s = (BiTree)malloc(sizeof(BiTNode));
       s->data = key;
       s->lchild = s->rchild = NULL;
       
       //3.
       if (!p) {
           //如果p為空,則將s作為二叉樹新的根結點;
           *T = s;
       }else if(key < p->data){
           //如果key<p->data,則將s插入為左孩子;
           p->lchild = s;
       }else
           //如果key>p->data,則將s插入為右孩子;
           p->rchild = s;
       
       return  TRUE;
   }
   
   return FALSE;
}

//3.從二叉排序樹中刪除結點p,并重接它的左或者右子樹;
Status Delete(BiTree *p){
   
   BiTree temp,s;
   
   
   if((*p)->rchild == NULL){
      
       //情況1: 如果當前刪除的結點,右子樹為空.那么則只需要重新連接它的左子樹;
       //①將結點p臨時存儲到temp中;
       temp = *p;
       //②將p指向到p的左子樹上;
       *p = (*p)->lchild;
       //③釋放需要刪除的temp結點;
       free(temp);
       
   }else if((*p)->lchild == NULL){
       
       //情況2:如果當前刪除的結點,左子樹為空.那么則只需要重新連接它的右子樹;
       //①將結點p存儲到temp中;
       temp = *p;
       //②將p指向到p的右子樹上;
       *p = (*p)->rchild;
       //③釋放需要刪除的temp結點
       free(temp);
   }else{
       
       //情況③:刪除的當前結點的左右子樹均不為空;
      
       //①將結點p存儲到臨時變量temp, 并且讓結點s指向p的左子樹
       temp = *p;
       s = (*p)->lchild;
     
       //②將s指針,向右到盡頭(目的是找到待刪結點的前驅)
       //-在待刪除的結點的左子樹中,從右邊找到直接前驅
       //-使用`temp`保存好直接前驅的雙親結點
       while (s->rchild) {
           temp = s;
           s = s->rchild;
       }
       
       //③將要刪除的結點p數據賦值成s->data;
       (*p)->data = s->data;
       
       //④重連子樹
       //-如果temp 不等于p,則將S->lchild 賦值給temp->rchild
       //-如果temp 等于p,則將S->lchild 賦值給temp->lchild
       if(temp != *p)
           temp->rchild = s->lchild;
       else
           temp->lchild = s->lchild;
       
       //⑤刪除s指向的結點; free(s)
       free(s);
   }
   
   return  TRUE;
}

//4.查找結點,并將其在二叉排序中刪除;
/* 若二叉排序樹T中存在關鍵字等于key的數據元素時,則刪除該數據元素結點, */
/* 并返回TRUE;否則返回FALSE。 */
Status DeleteBST(BiTree *T,int key)
{
   //不存在關鍵字等于key的數據元素
   if(!*T)
       return FALSE;
   else
   {
       //找到關鍵字等于key的數據元素
       if (key==(*T)->data)
           return Delete(T);
       else if (key<(*T)->data)
           //關鍵字key小于當前結點,則縮小查找范圍到它的左子樹;
           return DeleteBST(&(*T)->lchild,key);
       else
           //關鍵字key大于當前結點,則縮小查找范圍到它的右子樹;
           return DeleteBST(&(*T)->rchild,key);
       
   }
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374