數據結構與算法--關於查找的常見算法

1. 靜態查找

靜態查找是“真正的查找”。因爲在靜態查找過程中僅僅是執行“查找”的操作,即:
(1)查看某特定的關鍵字是否在表中(判斷性查找)
(2)檢索某特定關鍵字數據元素的各種屬性(檢索性查找)。
這兩種操作都只是獲取已經存在的一個表中的數據信息,不對錶的數據元素和結構進行任何改變,這就是所謂的靜態查找。

常見的靜態查找:順序查找插值查找二分查找裴波拉契查找

1.1 順序查找

順序查找,又稱爲線性查找。是最基本的查找技術。其查找過程:從表中的第一個或者最後一個記錄開始,逐個比較關鍵字和給定值。若相等,則查找成功,找到所查記錄;如果查找完,關鍵字和給定值都不相等,則沒有所查記錄。

代碼實現:

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

在順序查找時,我們可以對其添加哨兵,數組a的第0個位置作爲哨兵,來存儲查找的值

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

1.2 折半查找

折半查找又叫二分查找,也是常有的查找算法。

折半查找的前提是線性表中的記錄必須是關鍵碼有序(通常是從小到大),表必須採用順序存儲

折半查找思路:

  • 在有序表中,去中間記錄作爲比較對象,若給定值與中間記錄的關鍵字相等則查找成功
  • 若小於中間記錄的關鍵字,則在中間記錄的左半區繼續查找
  • 若給定值大於中間記錄的關鍵字,則在中間記錄的右半區繼續查找
  • 不斷重複以上過程,直到查找成功,或者所有查找區域無記錄,則查找失敗未知。
// 假設 數組a,從小到大有序
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]) {
            // ✅若key比a[mid] 小,則將最高下標調整到中位下標小一位;
            high = mid-1;
        }else if(key > a[mid]){
             // ✅若key比a[mid] 大,則將最低下標調整到中位下標大一位;
            low = mid+1;
        }else
            // ✅若相等則說明mid即爲查找到的位置;
            return mid;
    }
   return 0;
}

1.3 插值查找

假設數據a[11] = {0, 1, 16, 24, 35, 47, 59, 62, 73, 88, 99},有折半查找key = 16時,low = 1height = 10,則a[low] = 1a[height] = 99,需要查找四次

折半查找的公式如下:
在這裏插入圖片描述
我們對其優化,mid等於最低下標low加上最高下標height與最低下標low差值的一半。

然後將1/2改成下面的形式
在這裏插入圖片描述

那麼,
在這裏插入圖片描述
2.377取整,則mid = 2,我們只需要查找兩次就能得到結果。這就是插值查找

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]) {
            // ✅若key比a[mid]插值小,則將最高下標調整到插值下標小一位;
            high = mid-1;
        }else if(key > a[mid]){
            // ✅若key比a[mid]插值 大,則將最低下標調整到插值下標大一位;
            low = mid+1;
        }else
            //若相等則說明mid即爲查找到的位置;
            return mid;
    }
    
    return 0;
}

1.4 裴波拉契查找

裴波拉契查找需要依靠裴波拉契數列裴波拉契

裴波拉契查找公式:

  • mid = low + F[k-1] - 1
  • key < a[mid]k = k - 1
  • key > a[mid]k = k - 2

假設有下面的一個數列:

F爲裴波拉契數列,假設查找 n = 10,k = 99

首先n = 10F[6] < n < F[7],,所以計算得出k = 7。 找出n位於斐波那契數列的位置。

F[7] = 13, 而a最大僅有a[10]。後面的a[11],a[12] 是未賦值。不能構成有序數列。所以將後續的2個元素賦值 a[11] = a[12] = a[10] = 99

在這裏插入圖片描述

  • 第一次查找
    low = 1,k = 7
    mid = low + F[k-1] - 1 = 1 + F[7-1] - 1 = 1 + 8 - 1 = 8
    key > a[8] (99 > 73)
    k = k - 2 = 7 - 2 = 5
    low = mid + 1 = 9

  • 第二次查找
    low = 9,k = 5
    mid = low + F[k-1] - 1 = 9 + F[5-1] - 1 = 9 + 3 - 1 = 11
    key > a[11] (99 = 99)
    mid > n ,11>10,則返回n。所以返回10
    表示找到key =99 在數組a中的位置,在10這個位置

思路:

1. 先計算n位於斐波拉契數列的位置
2. 將數組a不滿的位置補全值
3. 循環 low <= high
4. mid = low+F[k-1]-1;
	4.1 key < a[mid],high = mid-1;k = k - 1
	4.2 key > a[mid],low = mid+1;k = k - 2
	4.3 判斷 mid <= n,返回 mid 或者 n

代碼實現:

int F[100]; /* 斐波那契數列 */
int Fibonacci_Search(int *a,int n,int key){
  
    int low,high,mid,i,k;
    //最低下標爲記錄的首位;
    low = 1;
    //最高下標爲記錄的末位;
    high = n;
    k = 0;
    
    //1.計算n爲斐波拉契數列的位置;
    while (n > F[k]-1) {
        k++;
    }
    
    //2.將數組a不滿的位置補全值;
    for(i = n;i < F[k]-1;i++)
        a[i] = a[n];
    
    //3.
    while (low <= high) {
        
        //計算當前分隔的下標;
        mid = low+F[k-1]-1;
        if (key < a[mid]) {
            //若查找的記錄小於當前分隔記錄;
            //將最高下標調整到分隔下標mid-1處;
            high = mid-1;
            //斐波拉契數列下標減1位;
            k = k-1;
            
        }else if(key > a[mid]){
            //若查找的記錄大於當前的分隔記錄;
            //最低下標調整到分隔下標mid+1處
            low = mid+1;
            //斐波拉契數列下標減2位;
            k = k-2;
            
        }else{
            if (mid <= n) {
                //若相等則說明,mid即爲查找的位置;
                return mid;
            }else
            {
                //若mid>n,說明是補全數值,返回n;
                return n;
            }
        }
    }
    return 0;
}

2. 動態查找(二叉搜索樹)

動態查找:它更像是一個對錶進行創建、擴充、修改、刪除的過程。

動態查找的過程中對錶的操作會多兩個動作:
(1)首先也有一個“判斷性查找”的過程,如果某特定的關鍵字在表中不存在,則按照一定的規則將其插入表中;
(2)如果已經存在,則可以對其執行刪除操作。

常見的動態查找(表):各種樹(二叉搜索樹、AVL、B/B+樹、紅黑樹等等)、哈希表

接下來來了解一下二叉排序樹,假設有以下一組數a[62,88,58,47,35,73,51,99,37,93]。當我們用順序存儲的線性表進行存儲時,假設要查找key = 93時,要遍歷很多次才能找到。

那麼嗎,我們藉助二叉樹來存儲,在存儲的過程中,比雙親節點小的,存儲到左子樹,比雙親節點大的,存儲到右子樹,可以通過比較縮小查找範圍,其實這樣存儲的二叉樹就是二叉排序樹

二叉排序樹又稱爲二叉查找樹,它或者是一個空樹,或者具有以下性質的二叉樹:

  • 若左子樹不爲空,則左子樹上所以的節點均小於其根結構的值
  • 若右子樹不爲空,則右子樹上所以的節點均小於其根結構的值
  • 其左右子樹分別是二叉排序樹

對上面的數組,以二叉排序樹的形式存儲,如下:
在這裏插入圖片描述

2.1 查找數據

假設查找key = 93,根據上圖,則:

  • 第一次,62 < 93,縮小查找範圍到其右子樹
  • 第二次,88 < 93,縮小查找範圍到其右子樹
  • 第三次,99 > 93,縮小查找範圍到其左子樹
  • 第四次,93 = 93,成功,返回true

那麼代碼怎麼實現呢?先來定義一下二叉樹的二叉鏈表結點結構

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 100

typedef int Status;


//結點結構
typedef  struct BiTNode
{
    //結點數據
    int data;
    //左右孩子指針
    struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

查找思想:

利用遞歸,比較key 和 節點data的值,相等,則返回true,
大於,則遞歸右子樹,
小於,則遞歸左子樹
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.2 插入數據

當向二叉搜索樹中插入某個key時,要先進行查找

插入思路:

1. 先查找插入的值,是否存在二叉樹中,存在,則插入失敗
2. 不存在,則插入到合適的位置
	2.1 初始化節點,並用 key 對其 data 賦值,左右子樹爲 NULL
	2.2 通過最後查找返回的節點 P進行比較,
		key < p,新節點插入爲左孩子
		key > p,新節點插入爲右孩子
Status InsertBST(BiTree *T, int key) {
    
    BiTree p,s;
    //✅ 查找插入的值是否存在二叉樹中;查找失敗則->
    if (!SearchBST(*T, key, NULL, &p)) {
        
        //✅ 初始化結點s,並將key賦值給s,將s的左右孩子結點暫時設置爲NULL
        s = (BiTree)malloc(sizeof(BiTNode));
        s->data = key;
        s->lchild = s->rchild = NULL;
        
        //✅ 比較,插入
        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;
}

2.3 刪除數據

二叉搜索樹的刪除,可以分爲三種情況:

  • 刪除的數據是葉子節點,比如上圖中的 37,51,73,93,這樣的節點直接刪除即可,不會影響二叉搜索樹的結構
  • 刪除的數據只有左子樹或者右子樹,比如58,35,99,先刪除,然後在將其左子樹或者右子樹,連接到被刪除節點的雙親節點的左子樹或者右子樹上。
  • 刪除的數據,既有左子樹,又有右子樹,比如47

那麼第三種這樣的節點怎麼刪除呢?我們來分析一下:

1. 定義兩個變量temp 和 p 都指向 被刪除節點 47.
2. 定義 s 指向待刪除節點的左子樹
3. 在待刪除節點的左子樹中,從右邊找到直接前驅(中序遍歷 29 , 35,37,47---)
4. 用 temp  保存好直接前驅的雙親節點

此時如下圖:
在這裏插入圖片描述

5. 將要刪除節點的 p 的數據賦值成 s->data,即:將37 賦值給到 p,替換 47
6. 判斷,如果 temp 不等於 p, 將 s 的左子樹賦值給 temp 的右子樹
7. 判斷,如果 temp 等於 p, 將 s 的左子樹賦值給 temp 的左子樹
8. 釋放  s 指向的節點 

最終如下:
在這裏插入圖片描述

代碼實現:

Status Delete(BiTree *p){

    BiTree temp,s;
    if((*p)->rchild == NULL){
       
        //✅ 情況1: 如果當前刪除的結點,右子樹爲空.那麼則只需要重新連接它的左子樹(刪除葉子節點時,其左右孩子爲NULL)
        //✅ 將結點p臨時存儲到temp中;
        temp = *p;
        //✅ 將p指向到p的左子樹上;
        *p = (*p)->lchild;
        //✅ 釋放需要刪除的temp結點;
        free(temp);
        
    }else if((*p)->lchild == NULL){
        
        //✅ 情況2:如果當前刪除的結點,左子樹爲空.那麼則只需要重新連接它的右子樹(刪除葉子節點時,其左右孩子爲NULL)
        //✅ 將結點p存儲到temp中;
        temp = *p;
        //✅ 將p指向到p的右子樹上;
        *p = (*p)->rchild;
        //✅ 釋放需要刪除的temp結點
        free(temp);
    }else{
        
        //✅ 情況③:刪除的當前結點的左右子樹均不爲空;
       
        //✅ 1. 將結點p存儲到臨時變量temp, 並且讓結點s指向p的左子樹
        temp = *p;
        s = (*p)->lchild;
      
        //✅ 2. 將s指針,向右到盡頭(目的是找到待刪結點的前驅)
        //-在待刪除的結點的左子樹中,從右邊找到直接前驅
        //-使用`temp`保存好直接前驅的雙親結點
        while (s->rchild) {
            temp = s;
            s = s->rchild;
        }
        
        //✅ 3. 將要刪除的結點p數據賦值成s->data;
        (*p)->data = s->data;
        
        //✅ 4. 重連子樹
        //✅ 如果temp 不等於p,則將S->lchild 賦值給temp->rchild
        //✅ 如果temp 等於p,則將S->lchild 賦值給temp->lchild
        if(temp != *p)
            temp->rchild = s->lchild;
        else
            temp->lchild = s->lchild;
        
        //✅ 5. 刪除s指向的結點; free(s)
        free(s);
    }
    
    return  TRUE;
}

//4.查找結點,並將其在二叉排序中刪除;
/* 若二叉排序樹T中存在關鍵字等於key的數據元素時,則刪除該數據元素結點, */
/* 並返回TRUE;否則返回FALSE。 */
Status DeleteBST(BiTree *T,int key)
{
    //✅ 1. 不存在關鍵字等於key的數據元素
    if(!*T)
        return FALSE;
    else
    {
        //✅ 2. 找到關鍵字等於key的數據元素
        if (key==(*T)->data)
            return Delete(T);
        else if (key<(*T)->data)
            //✅ 3. 關鍵字key小於當前結點,則縮小查找範圍到它的左子樹;
            return DeleteBST(&(*T)->lchild,key);
        else
            //✅ 3. 關鍵字key大於當前結點,則縮小查找範圍到它的右子樹;
            return DeleteBST(&(*T)->rchild,key);
        
    }
}

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