查找算法之順序查找,折半查找,二叉查找樹

查找表的概念

  查找表是由同一類型的數據元素構成的集合。例如電話號碼簿和字典都可以看作是一張查找表。
  在查找表中只做查找操作,而不改動表中數據元素,稱此類查找表爲靜態查找表;反之,在查找表中做查找操作的同時進行插入數據或者刪除數據的操作,稱此類表爲動態查找表。

順序查找

  順序查找的查找過程爲:從表中的最後一個數據元素開始,逐個同記錄的關鍵字做比較,如果匹配成功,則查找成功;反之,如果直到表中第一個關鍵字查找完也沒有成功匹配,則查找失敗
同時,在程序中初始化創建查找表時,由於是順序存儲,所以將所有的數據元素存儲在數組中,但是把第一個位置留給了用戶用於查找的關鍵字。例如,在順序表{1,2,3,4,5,6}中查找數據元素值爲 7 的元素,則添加後的順序表爲:
在這裏插入圖片描述
            圖1
  順序表的一端添加用戶用於搜索的關鍵字,稱作“監視哨”。
  圖 1 中監視哨的位置也可放在數據元素 6 的後面(這種情況下,整個查找的順序應有逆向查找改爲順序查找)。
  放置好監視哨之後,順序表遍歷從沒有監視哨的一端依次進行,如果查找表中有用戶需要的數據,則程序輸出該位置;反之,程序會運行至監視哨,此時匹配成功,程序停止運行,但是結果是查找失敗。
代碼實現:

/*
 * @Description: 順序查找算法
 * @Version: V1.0
 * @Autor: Carlos
 * @Date: 2020-05-22 15:52:11
 * @LastEditors: Carlos
 * @LastEditTime: 2020-06-03 16:56:06
 */ 
#include <stdio.h>
#include <stdlib.h>
#define keyType int
typedef struct {
    //查找表中每個數據元素的值
    keyType key;
    //如果需要,還可以添加其他屬性
}ElemType;

typedef struct{
    //存放查找表中數據元素的數組
    ElemType *elem;
    //記錄查找表中數據的總數量
    int length;
}SSTable;
/**
 * @Description: 創建查找表
 * @Param: SSTable **st 指向結構體指針的指針,即指針變量的指針,int length 創建的二叉樹的長度
 * @Return: 無
 * @Author: Carlos
 */
void Create(SSTable **st,int length){
    (*st)=(SSTable*)malloc(sizeof(SSTable));
    (*st)->length=length;
    //結構體指針分配空間
    (*st)->elem =(ElemType*)malloc((length+1)*sizeof(ElemType));
    printf("輸入表中的數據元素:\n");
    //根據查找表中數據元素的總長度,在存儲時,從數組下標爲 1 的空間開始存儲數據
    for (int i=1; i<=length; i++) {
        scanf("%d",&((*st)->elem[i].key));
    }
}
/**
 * @Description: 查找表查找的功能函數,其中key爲關鍵字
 * @Param: SSTable *st指向結構體變量的指針,keyType key 要查找的元素
 * @Return: key在查找表中的位置
 * @Author: Carlos
 */
int Search_seq(SSTable *st,keyType key){
    //將關鍵字作爲一個數據元素存放到查找表的第一個位置,起監視哨的作用
    st->elem[0].key=key;
    int i=st->length;
    //從查找表的最後一個數據元素依次遍歷,一直遍歷到數組下標爲0
    while (st->elem[i].key!=key) {
        i--;
    }
    //如果 i=0,說明查找失敗;反之,返回的是含有關鍵字key的數據元素在查找表中的位置
    return i;
}
int main(int argc, const char * argv[]) {
    SSTable *st;
    Create(&st, 6);
    getchar();
    printf("請輸入查找數據的關鍵字:\n");
    int key;
    scanf("%d",&key);
    int location=Search_seq(st, key);
    if (location==0) {
        printf("查找失敗");
    }else{
        printf("數據在查找表中的位置爲:%d",location);
    }
    return 0;
}

折半查找

  折半查找,也稱二分查找,在某些情況下相比於順序查找,使用折半查找算法的效率更高。但是該算法的使用的前提是靜態查找表中的數據必須是有序的。
  例如,在{5,21,13,19,37,75,56,64,88 ,80,92}這個查找表使用折半查找算法查找數據之前,需要首先對該表中的數據按照所查的關鍵字進行排序:{5,13,19,21,37,56,64,75,80,88,92}。
  在折半查找之前對查找表按照所查的關鍵字進行排序的意思是:若查找表中存儲的數據元素含有多個關鍵字時,使用哪種關鍵字做折半查找,就需要提前以該關鍵字對所有數據進行排序。

折半查找算法

  對靜態查找表{5,13,19,21,37,56,64,75,80,88,92}採用折半查找算法查找關鍵字爲 21 的過程爲:
在這裏插入圖片描述
              圖2
後一個關鍵字,指針 mid 指向處於 low 和 high 指針中間位置的關鍵字。在查找的過程中每次都同 mid 指向的關鍵字進行比較,由於整個表中的數據是有序的,因此在比較之後就可以知道要查找的關鍵字的大致位置。
  例如在查找關鍵字 21 時,首先同 56 作比較,由於21 < 56,而且這個查找表是按照升序進行排序的,所以可以判定如果靜態查找表中有 21 這個關鍵字,就一定存在於 low 和 mid 指向的區域中間。
  因此,再次遍歷時需要更新 high 指針和 mid 指針的位置,令 high 指針移動到 mid 指針的左側一個位置上,同時令 mid 重新指向 low 指針和 high 指針的中間位置。如圖3所示:
在這裏插入圖片描述
              圖3
  同樣,用 21 同 mid 指針指向的 19 作比較,19 < 21,所以可以判定 21 如果存在,肯定處於 mid 和 high 指向的區域中。所以令 low 指向 mid 右側一個位置上,同時更新 mid 的位置。
在這裏插入圖片描述
              圖4
  當第三次做判斷時,發現 mid 就是關鍵字 21 ,查找結束。
  注意:在做查找的過程中,如果 low 指針和 high 指針的中間位置在計算時位於兩個關鍵字中間,即求得 mid 的位置不是整數,需要統一做取整操作。
  折半查找的實現代碼:

/*
 * @Description: 折半查找.前提是靜態查找表中的數據必須是有序的。
 * @Version: V1.0
 * @Autor: Carlos
 * @Date: 2020-05-22 16:09:01
 * @LastEditors: Carlos
 * @LastEditTime: 2020-06-03 16:58:14
 */ 
#include <stdio.h>
#include <stdlib.h>
#define keyType int
typedef struct {
    //查找表中每個數據元素的值
    keyType key;
    //如果需要,還可以添加其他屬性
}ElemType;

typedef struct{
    //存放查找表中數據元素的數組
    ElemType *elem;
    //記錄查找表中數據的總數量
    int length;
}SSTable;
/**
 * @Description: 創建查找表
 * @Param: SSTable **st 指向結構體指針的指針,即指針變量的指針,int length 創建的二叉樹的長度
 * @Return: 無
 * @Author: Carlos
 */
void Create(SSTable **st,int length){
    (*st)=(SSTable*)malloc(sizeof(SSTable));
    (*st)->length=length;
    (*st)->elem = (ElemType*)malloc((length+1)*sizeof(ElemType));
    printf("輸入表中的數據元素:\n");
    //根據查找表中數據元素的總長度,在存儲時,從數組下標爲 1 的空間開始存儲數據
    for (int i=1; i<=length; i++) {
        scanf("%d",&((*st)->elem[i].key));
    }
}
//折半查找算法
/**
 * @Description: 折半查找算法
 * @Param: SSTable *ST 指向結構體的指針,keyType key 要插入的元素
 * @Return: 成功的返回key在查找表中的位置,否則返回0
 * @Author: Carlos
 */
int Search_Bin(SSTable *ST,keyType key){
    //初始狀態 low 指針指向第一個關鍵字
    int low=1;
    //high 指向最後一個關鍵字
    int high=ST->length;
    int mid;
    while (low<=high) {
        //int 本身爲整形,所以,mid 每次爲取整的整數
        mid=(low+high)/2;
        //如果 mid 指向的同要查找的相等,返回 mid 所指向的位置
        if (ST->elem[mid].key==key)
        {
            return mid;
        }
        //如果mid指向的關鍵字較大,則更新 high 指針的位置
        else if(ST->elem[mid].key>key)
        {
            high=mid-1;
        }
        //反之,則更新 low 指針的位置
        else{
            low=mid+1;
        }
    }
    return 0;
}

int main(int argc, const char * argv[]) {
    SSTable *st;
    Create(&st, 11);
    getchar();
    printf("請輸入查找數據的關鍵字:\n");
    int key;
    scanf("%d",&key);
    int location=Search_Bin(st, key);
    //如果返回值爲 0,則證明查找表中未查到 key 值,
    if (location==0) {
        printf("查找表中無該元素");
    }else{
        printf("數據在查找表中的位置爲:%d",location);
    }
    return 0;
}

二叉查找樹

  動態查找表中做查找操作時,若查找成功可以對其進行刪除;如果查找失敗,即表中無該關鍵字,可以將該關鍵字插入到表中。
  動態查找表的表示方式有多種,本節介紹一種使用樹結構表示動態查找表的實現方法——二叉排序樹(又稱爲“二叉查找樹”)。

二叉查找樹概念

  二叉排序樹要麼是空二叉樹,要麼具有如下特點:

  • 二叉排序樹中,如果其根結點有左子樹,那麼左子樹上所有結點的值都小於根結點的值;
  • 二叉排序樹中,如果其根結點有右子樹,那麼右子樹上所有結點的值都大小根結點的值;
  • 二叉排序樹的左右子樹也要求都是二叉排序樹;

  例如,圖 5 就是一個二叉排序樹:
在這裏插入圖片描述
              圖5

使用二叉排序樹查找關鍵字

  二叉排序樹中查找某關鍵字時,查找過程類似於次優二叉樹,在二叉排序樹不爲空樹的前提下,首先將被查找值同樹的根結點進行比較,會有 3 種不同的結果:

  1. 如果相等,查找成功;
  2. 如果比較結果爲根結點的關鍵字值較大,則說明該關鍵字可能存在其左子樹中;
  3. 如果比較結果爲根結點的關鍵字值較小,則說明該關鍵字可能存在其右子樹中;
    實現函數爲:(運用遞歸的方法)
/**
 * @Description: 二叉排序樹查找算法
 * @Param: BiTree T  KeyType key  BiTree f BiTree *p
 * @Return: 刪除成功 TRUE 刪除失敗 FALSE
 * @Author: Carlos
 */
int SearchBST(BiTree T, KeyType key, BiTree f, BiTree *p)
{
    //如果 T 指針爲空,說明查找失敗,令 p 指針指向查找過程中最後一個葉子結點,並返回查找失敗的信息
    if (!T)
    {
        *p = f;
        return FALSE;
    }
    //如果相等,令 p 指針指向該關鍵字,並返回查找成功信息
    else if (key == T->data)
    {
        *p = T;
        return TRUE;
    }
    //如果 key 值比 T 根結點的值小,則查找其左子樹;反之,查找其右子樹
    else if (key < T->data)
    {
        return SearchBST(T->lchild, key, T, p);
    }
    else
    {
        return SearchBST(T->rchild, key, T, p);
    }
}

二叉排序樹中插入關鍵字

  二叉排序樹本身是動態查找表的一種表示形式,有時會在查找過程中插入或者刪除表中元素,當因爲查找失敗而需要插入數據元素時,該數據元素的插入位置一定位於二叉排序樹的葉子結點,並且一定是查找失敗時訪問的最後一個結點的左孩子或者右孩子。
  例如,在圖 1 的二叉排序樹中做查找關鍵字 1 的操作,當查找到關鍵字 3 所在的葉子結點時,判斷出表中沒有該關鍵字,此時關鍵字 1 的插入位置爲關鍵字 3 的左孩子。
  所以,二叉排序樹表示動態查找表做插入操作,只需要稍微更改一下上面的代碼就可以實現,具體實現代碼爲:

/**
 * @Description: 二叉排序樹查找算法
 * @Param: BiTree T  KeyType key  BiTree f BiTree *p
 * @Return: 刪除成功 TRUE 刪除失敗 FALSE
 * @Author: Carlos
 */
int SearchBST(BiTree T, KeyType key, BiTree f, BiTree *p)
{
    //如果 T 指針爲空,說明查找失敗,令 p 指針指向查找過程中最後一個葉子結點,並返回查找失敗的信息
    if (!T)
    {
        *p = f;
        return FALSE;
    }
    //如果相等,令 p 指針指向該關鍵字,並返回查找成功信息
    else if (key == T->data)
    {
        *p = T;
        return TRUE;
    }
    //如果 key 值比 T 根結點的值小,則查找其左子樹;反之,查找其右子樹
    else if (key < T->data)
    {
        return SearchBST(T->lchild, key, T, p);
    }
    else
    {
        return SearchBST(T->rchild, key, T, p);
    }
}
/**
 * @Description: 二叉樹插入函數
 * @Param: BiTree *T 二叉樹結構體指針的指針  ElemType e 要插入的元素
 * @Return: 刪除成功 TRUE 刪除失敗 FALSE
 * @Author: Carlos
 */
int InsertBST(BiTree *T, ElemType e)
{
    BiTree p = NULL;
    //如果查找不成功,需做插入操作
    if (!SearchBST((*T), e, NULL, &p))
    {
        //初始化插入結點
        BiTree s = (BiTree)malloc(sizeof(BiTree));
        s->data = e;
        s->lchild = s->rchild = NULL;
        //如果 p 爲NULL,說明該二叉排序樹爲空樹,此時插入的結點爲整棵樹的根結點
        if (!p)
        {
            *T = s;
        }
        //如果 p 不爲 NULL,則 p 指向的爲查找失敗的最後一個葉子結點,只需要通過比較 p 和 e 的值確定 s 到底是 p 的左孩子還是右孩子
        else if (e < p->data)
        {
            p->lchild = s;
        }
        else
        {
            p->rchild = s;
        }
        return TRUE;
    }
    //如果查找成功,不需要做插入操作,插入失敗
    return FALSE;
}

  通過使用二叉排序樹對動態查找表做查找和插入的操作,同時在中序遍歷二叉排序樹時,可以得到有關所有關鍵字的一個有序的序列。
  例如,假設原二叉排序樹爲空樹,在對動態查找表 {3,5,7,2,1} 做查找以及插入操作時,可以構建出一個含有表中所有關鍵字的二叉排序樹,過程如圖6 所示:
在這裏插入圖片描述
              圖6
  通過不斷的查找和插入操作,最終構建的二叉排序樹如圖 6(5) 所示。當使用中序遍歷算法遍歷二叉排序樹時,得到的序列爲:1 2 3 5 7 ,爲有序序列。
一個無序序列可以通過構建一棵二叉排序樹,從而變成一個有序序列。

二叉排序樹中刪除關鍵字

  在查找過程中,如果在使用二叉排序樹表示的動態查找表中刪除某個數據元素時,需要在成功刪除該結點的同時,依舊使這棵樹爲二叉排序樹。
  假設要刪除的爲結點 p,則對於二叉排序樹來說,需要根據結點 p 所在不同的位置作不同的操作,有以下 3 種可能:

  1. 結點 p 爲葉子結點,此時只需要刪除該結點,並修改其雙親結點的指針即可;
  2. 結點 p 只有左子樹或者只有右子樹,此時只需要將其左子樹或者右子樹直接變爲結點 p 雙親結點的左子樹即可;
  3. 結點 p 左右子樹都有,此時有兩種處理方式:
      (1).令結點 p 的左子樹爲其雙親結點的左子樹;結點 p 的右子樹爲其自身直接前驅結點的右子樹,如圖7所示;
    在這裏插入圖片描述
                  圖7
      (2)用結點 p 的直接前驅(或直接後繼)來代替結點 p,同時在二叉排序樹中對其直接前驅(或直接後繼)做刪除操作。如圖 8 爲使用直接前驅代替結點 p:
    在這裏插入圖片描述
                  圖8
      圖 8中,在對左圖進行中序遍歷時,得到的結點 p 的直接前驅結點爲結點 s,所以直接用結點 s 覆蓋結點 p,由於結點 s 還有左孩子,根據第 2 條規則,直接將其變爲雙親結點的右孩子。
      具體實現代碼:(可運行)
/*
 * @Description: 二叉查找樹
 * @Version: V1.0
 * @Autor: Carlos
 * @Date: 2020-06-02 15:50:31
 * @LastEditors: Carlos
 * @LastEditTime: 2020-06-03 16:49:46
 */
#include <stdio.h>
#include <stdlib.h>
#define TRUE 1
#define FALSE 0
#define ElemType int
#define KeyType int
/* 二叉排序樹的節點結構定義 */
typedef struct BiTNode
{
    int data;
    struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
/**
 * @Description: 二叉排序樹查找算法
 * @Param: BiTree T  KeyType key  BiTree f BiTree *p
 * @Return: 刪除成功 TRUE 刪除失敗 FALSE
 * @Author: Carlos
 */
int SearchBST(BiTree T, KeyType key, BiTree f, BiTree *p)
{
    //如果 T 指針爲空,說明查找失敗,令 p 指針指向查找過程中最後一個葉子結點,並返回查找失敗的信息
    if (!T)
    {
        *p = f;
        return FALSE;
    }
    //如果相等,令 p 指針指向該關鍵字,並返回查找成功信息
    else if (key == T->data)
    {
        *p = T;
        return TRUE;
    }
    //如果 key 值比 T 根結點的值小,則查找其左子樹;反之,查找其右子樹
    else if (key < T->data)
    {
        return SearchBST(T->lchild, key, T, p);
    }
    else
    {
        return SearchBST(T->rchild, key, T, p);
    }
}
/**
 * @Description: 二叉樹插入函數
 * @Param: BiTree *T 二叉樹結構體指針的指針  ElemType e 要插入的元素
 * @Return: 刪除成功 TRUE 刪除失敗 FALSE
 * @Author: Carlos
 */
int InsertBST(BiTree *T, ElemType e)
{
    BiTree p = NULL;
    //如果查找不成功,需做插入操作
    if (!SearchBST((*T), e, NULL, &p))
    {
        //初始化插入結點
        BiTree s = (BiTree)malloc(sizeof(BiTree));
        s->data = e;
        s->lchild = s->rchild = NULL;
        //如果 p 爲NULL,說明該二叉排序樹爲空樹,此時插入的結點爲整棵樹的根結點
        if (!p)
        {
            *T = s;
        }
        //如果 p 不爲 NULL,則 p 指向的爲查找失敗的最後一個葉子結點,只需要通過比較 p 和 e 的值確定 s 到底是 p 的左孩子還是右孩子
        else if (e < p->data)
        {
            p->lchild = s;
        }
        else
        {
            p->rchild = s;
        }
        return TRUE;
    }
    //如果查找成功,不需要做插入操作,插入失敗
    return FALSE;
}
/**
 * @Description: 刪除節點的函數
 * @Param: BiTree *p 指向結構體指針的指針
 * @Return: 刪除成功 TRUE
 * @Author: Carlos
 */
int Delete(BiTree *p)
{
    BiTree q, s;
    //情況 1,結點 p 本身爲葉子結點,直接刪除即可
    if (!(*p)->lchild && !(*p)->rchild)
    {
        *p = NULL;
    }
    //左子樹爲空,只需用結點 p 的右子樹根結點代替結點 p 即可;
    else if (!(*p)->lchild)
    {
        q = *p;
        *p = (*p)->rchild;
        free(q);
        q = NULL;
    }
    //右子樹爲空,只需用結點 p 的左子樹根結點代替結點 p 即可;
    else if (!(*p)->rchild)
    {
        q = *p;
        //這裏不是指針 *p 指向左子樹,而是將左子樹存儲的結點的地址賦值給指針變量 p
        *p = (*p)->lchild;
        free(q);
        q = NULL;
    }
    //左右子樹均不爲空,採用第 2 種方式
    else
    {
        q = *p;
        s = (*p)->lchild;
        //遍歷,找到結點 p 的直接前驅
        while (s->rchild)
        {
            //指向p節點左子樹最右邊節點的前一個。保留下來
            q = s;
            s = s->rchild;
        }
        //直接改變結點 p 的值
        (*p)->data = s->data;
        //判斷結點 p 的左子樹 s 是否有右子樹,分爲兩種情況討論  如果有右子樹,s一定會指向右子樹的葉子節點。q 此時指向的是葉子節點的父節點。 q != *p二者不等說明有右子樹
        if (q != *p)
        {
            //若有,則在刪除直接前驅結點的同時,令前驅的左孩子結點改爲 q 指向結點的孩子結點
            q->rchild = s->lchild;
        }
        else
        //q == *p ==NULL 說明沒有右子樹
        {
            //否則,直接將左子樹上移即可
            q->lchild = s->lchild;
        }
        free(s);
        s = NULL;
    }
    return TRUE;
}
/**
 * @Description: 刪除二叉樹中的元素
 * @Param: BiTree *T 指向二叉樹結構體的指針 int key 要刪除的元素
 * @Return: 刪除成功 TRUE 刪除失敗 FALSE
 * @Author: Carlos
 */
int DeleteBST(BiTree *T, int key)
{
    if (!(*T))
    { //不存在關鍵字等於key的數據元素
        return FALSE;
    }
    else
    {
        if (key == (*T)->data)
        {
            Delete(T);
            return TRUE;
        }
        else if (key < (*T)->data)
        {
            //使用遞歸的方式
            return DeleteBST(&(*T)->lchild, key);
        }
        else
        {
            return DeleteBST(&(*T)->rchild, key);
        }
    }
}
/**
 * @Description: 中序遍歷輸出二叉樹
 * @Param: BiTree t 結構體變量
 * @Return: 無
 * @Author: Carlos
 */
void order(BiTree t) 
{
    if (t == NULL)
    {
        return;
    }
    order(t->lchild);
    printf("%d ", t->data);
    order(t->rchild);
}
int main()
{
    int i;
    int a[5] = {3, 4, 2, 5, 9};
    BiTree T = NULL;
    for (i = 0; i < 5; i++)
    {
        InsertBST(&T, a[i]);
    }
    printf("中序遍歷二叉排序樹:\n");
    order(T);
    printf("\n");
    printf("刪除3後,中序遍歷二叉排序樹:\n");
    DeleteBST(&T, 3);
    order(T);
}

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