常見的查找算法

1)順序查找

/*包含頭文件*/
#include <stdio.h>
#include <stdlib.h>   
#include <io.h>
#include <math.h>
#include <time.h>

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 20
typedef int Status; 
typedef struct
{
    int data[MAXSIZE];
    int length;    
}SeqList;
/*順序查找算法*/
int SequenceSearch(SeqList *seqList,int key)
{
    int i;
    //遍歷順序表
    for (i=0;i<seqList->length;i++)
    {
        //找到該元素
        if (seqList->data[i]==key) return i;
    }
    //沒有找到
    return -1;
}
/*打印結果*/
void Display(SeqList *seqList)
{
    int i;
    printf("\n**********展示結果**********\n");

    for (i=0;i<seqList->length;i++)
    {
        printf("%d ",seqList->data[i]);
    }
    printf("\n**********展示完畢**********\n");
}
#define N 9
void main()
{
    int i,j;
    SeqList seqList;

    //定義數組和初始化SeqList
    int d[N]={50,10,90,30,70,40,80,60,20};

    for (i=0;i<N;i++)
    {
        seqList.data[i]=d[i];
    }
    seqList.length=N;

    printf("***************順序查找***************\n");
    Display(&seqList);
    j=SequenceSearch(&seqList,70);
    if (j!=-1) printf("70在列表中的位置是:%d\n",j);
    else printf("對不起,沒有找到該元素!");

    getchar();
}

2)二分查找
二分查找也屬於順序表查找範圍,二分查找也稱爲折半查找。二分查找(有序)的時間複雜度爲O(LogN)。
1.待查找的列表必須有序。
2.必須使用線性表的順序存儲結構來存儲數據。

/*包含頭文件*/
#include <stdio.h>
#include <stdlib.h>   
#include <io.h>
#include <math.h>
#include <time.h>
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 20
typedef int Status; 
typedef struct
{
    int data[MAXSIZE];
    int length;    
}SeqList;
/*二分查找算法(折半查找)*/
int BinarySearch(SeqList *seqList,int key)
{
    /*下限*/
    int low=0;
    /*上限*/
    int high=seqList->length-1;
    while(low<=high) /*注意下限可以與上限重合的*/
    {
        int middle=(low+high)/2;
        /*判斷中間記錄是否與給定值相等*/
        if (seqList->data[middle]==key)
        {
            return middle;
        }
        else
        {
            /*縮小上限*/
            if (seqList->data[middle]>key) high=middle-1;
            /*擴大下限*/
            else low=middle+1;
        }
    }
    /*沒有找到*/
    return -1;
}
/*打印結果*/
void Display(SeqList *seqList)
{
    int i;
    printf("\n**********展示結果**********\n");

    for (i=0;i<seqList->length;i++)
    {
        printf("%d ",seqList->data[i]);
    }
    printf("\n**********展示完畢**********\n");
}
#define N 9
void main()
{
    int i,j;
    SeqList seqList;

    //定義數組和初始化SeqList
    int d[N]={10,20,30,40,50,60,70,80,90};

    for (i=0;i<N;i++)
    {
        seqList.data[i]=d[i];
    }
    seqList.length=N;
    printf("***************二分查找(C版)***************\n");
    Display(&seqList);
    j=BinarySearch(&seqList,40);
    if (j!=-1) printf("40在列表中的位置是:%d\n",j);
    else printf("對不起,沒有找到該元素!");
    getchar();
}

3)索引查找
索引查找又稱爲分塊查找,是一種介於順序查找和二分查找之間的一種查找方法,分塊查找的基本思想是:首先查找索引表,可用二分查找或順序查找,然後在確定的塊中進行順序查找。
分塊查找的時間複雜度爲O(√n)。
索引查找是在索引表和主表(即線性表的索引存儲結構)上進行的查找。
索引查找的過程是:
1) 首先根據給定的索引值K1,在索引表上查找出索引值等於KI的索引項,以確定對應予表在主表中的開始位置和長度,
2) 然後再根據給定的關鍵字K2,茬對應的子表中查找出關鍵字等於K2的元素(結點)。對索引表或子表進行查找時,若表是順序存儲的有序表,則既可進行順序查找,也可進行二分查找,否則只能進行順序查找。
需要弄清楚以下三個術語:
1.主表。即要查找的對象。
2.索引項。一般我們會將主表分成幾個子表,每個子表建立一個索引,這個索引就叫索引項。
3.索引表。即索引項的集合。
同時,索引項包括以下三點:
1.index,即索引指向主表的關鍵字。
2.start,即index在主表中的位置。
3.length,即子表的區間長度。

struct IndexItem
{
    IndexKeyType index;//IndexKeyType爲事先定義的索引值類型
    int start;         //子表中第一個元素所在的下標位置
    int length;        //子表的長度域
};
typedef struct IndexItem indexlist[ILMSize];//ILMSize爲事先定義的整型常量,大於等於索引項數m
typedef struct ElemType mainlist[MaxSize];//MaxSize爲事先定義的整型常量
int Indsch(mainlist A, indexlist B, int m, IndexKeyType K1, KeyType K2)
{//利用主表A和大小爲 m 的索引表B索引查找索引值爲K1,關鍵字爲K2的記錄
 //返回該記錄在主表中的下標位置,若查找失敗則返回-1
    int i, j;
    for (i = 0; i < m; i++)
        if (K1 == B[i].index)
            break;
    if (i == m)
        return -1; //查找失敗
    j = B[i].start;
    while (j < B[i].start + B[i].length)
    {
        if (K2 == A[j].key)
            break;
        else
            j++;
    }
    if (j < B[i].start + B[i].length)
        return j; //查找成功
    else
        return -1; //查找失敗
}
若 IndexKeyType 被定義爲字符串類型,則算法中相應的條件改爲
 strcmp (K1, B[i].index) == 0;
同理,若KeyType 被定義爲字符串類型
則算法中相應的條件也應該改爲
strcmp (K2, A[j].key) == 0
 若每個子表在主表A中採用的是鏈接存儲,則只要把上面算法中的while循環
和其後的if語句進行如下修改即可:
while (j != -1)//用-1作爲空指針標記
{
    if (K2 == A[j].key)
        break;
    else
        j = A[j].next;
}
return j;
}

索引查找分析:
索引查找的比較次數等於算法中查找索引表的比較次數和查找相應子表的比較次數之和,假定索引表的長度爲m,子表長度爲s,
則索引查找的平均查找長度爲:
ASL= (1+m)/2 + (1+s)/2 = 1 + (m+s)/2
假定每個子表具有相同的長度,即s=n/m, 則 ASL = 1 + (m + n/m)/2 ,當m = n/m ,(即m = √▔n,此時s也等於√▔n), ASL = 1 + √▔n 最小 ,時間複雜度爲 O(√▔n)
可見,索引查找的速度快於順序查找,但低於二分查找。
4)二叉排序樹
二叉排序樹具有以下幾個特點:
1.若根節點有左子樹,則左子樹的所有節點都比根節點小。
2.若根節點有右子樹,則右子樹的所有節點都比根節點大。
3.根節點的左,右子樹也分別爲二叉排序樹。
構造一棵二叉排序樹的目的,其實並不是爲了排序,而是爲了提高查找和插入刪除的效率。
這裏寫圖片描述

/*包含頭文件*/
#include <stdio.h>
#include <stdlib.h>   
#include <io.h>
#include <math.h>
#include <time.h>
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 20
typedef int Status; 
/* 二叉樹的二叉鏈表結點結構定義 */
typedef  struct BiTNode    /* 結點結構 */
{
    int data;    /* 結點數據 */
    struct BiTNode *lchild, *rchild;    /* 左右孩子指針 */
} BiTNode, *BiTree; /**BiTree等價於typedef BiTNode *BiTree*/
/*查找二叉排序樹T中是否存在key(遞歸查找)*/
Status Search(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 Search(T->lchild, key, T, p);  /*  在左子樹中繼續查找 */
    else  
        return Search(T->rchild, key, T, p);  /*  在右子樹中繼續查找 */
}
/*  當二叉排序樹T中不存在關鍵字等於key的數據元素時, */
/*  插入key並返回TRUE,否則返回FALSE */
Status Insert(BiTree *T, int key)
{
    BiTree p,s;
    if (!Search(*T, key, NULL, &p)) /* 查找不成功 */
    {
        s = (BiTree)malloc(sizeof(BiTNode));
        s->data = key;  
        s->lchild = s->rchild = NULL;  
        if (!p) 
            *T = s;            /*  插入s爲新的根結點 */
        else if (key<p->data) 
            p->lchild = s;    /*  插入s爲左孩子 */
        else 
            p->rchild = s;  /*  插入s爲右孩子 */
        return TRUE;
    } 
    else 
        return FALSE;  /*  樹中已有關鍵字相同的結點,不再插入 */
}
/* 從二叉排序樹中刪除結點p,並重接它的左或右子樹。 */
Status DeleteBST(BiTree *p)
{
    BiTree q,s;
    if((*p)->rchild==NULL) /* 右子樹空則只需重接它的左子樹(待刪結點是葉子也走此分支) */
    {
        q=*p; *p=(*p)->lchild; free(q);
    }
    else if((*p)->lchild==NULL) /* 只需重接它的右子樹 */
    {
        q=*p; *p=(*p)->rchild; free(q);
    }
    else /* 左右子樹均不空 */
    {
        q=*p; s=(*p)->lchild;
        while(s->rchild) /* 轉左,然後向右到盡頭(找待刪結點的前驅) */
        {
            q=s;
            s=s->rchild;
        }
        (*p)->data=s->data; /*  s指向被刪結點的直接前驅(將被刪結點前驅的值取代被刪結點的值) */
        if(q!=*p)
            q->rchild=s->lchild; /*  重接q的右子樹 */ 
        else
            q->lchild=s->lchild; /*  重接q的左子樹 */
        free(s);
    }
    return TRUE;
}

/* 若二叉排序樹T中存在關鍵字等於key的數據元素時,則刪除該數據元素結點, */
/* 並返回TRUE;否則返回FALSE。 */
Status Delete(BiTree *T,int key)
{ 
    if(!*T) /* 不存在關鍵字等於key的數據元素 */ 
        return FALSE;
    else
    {
        if (key==(*T)->data) /* 找到關鍵字等於key的數據元素 */ 
            return DeleteBST(T);
        else if (key<(*T)->data)
            return Delete(&(*T)->lchild,key);
        else
            return Delete(&(*T)->rchild,key);
    }
}

/*二叉樹中序遍歷*/
void LDR(BiTree T)
{
    if (T!=NULL)
    {
        LDR(T->lchild);
        printf("%d ",T->data);
        LDR(T->rchild);
    }
}
#define N 10
void main()
{
    int i,j;
    BiTree T=NULL;
    //定義數組和初始化SeqList
    int d[N]={62,88,58,47,35,73,51,99,37,93};

    for (i=0;i<N;i++)
    {
        Insert(&T,d[i]);
    }
    printf("***************二叉排序樹查找(C版)***************\n");
    printf("初始化二叉排序樹\n中序遍歷數據:");
    LDR(T);

    printf("\n***************刪除節點1***************\n");
    Delete(&T,93);
    printf("刪除葉節點93\n中序遍歷後:");
    LDR(T);
    printf("\n***************刪除節點2***************\n");
    Delete(&T,47);
    printf("刪除雙孩子節點47\n中序遍歷後:");
    LDR(T);
    getchar();
}

在隨機情況下,二叉排序樹的平均查找長度和logn是等數量級的。然而在某些情況下(P(46.5%)),尚需在構成二叉排序樹的過程中進行平行化處理,成爲平衡二叉樹。

平衡二叉樹(Balanced Binary Tree)又被稱爲AVL樹(有別於AVL算法),且具有以下性質:它是一 棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。
構造與調整方法 平衡二叉樹的常用算法有紅黑樹、AVL、Treap等。 最小二叉平衡樹的節點的公式如下 F(n)=F(n-1)+F(n-2)+1 這個類似於一個遞歸的數列,可以參考Fibonacci數列,1是根節點,F(n-1)是左子樹的節點數量,F(n-2)是右子樹的節點數量。
5)哈希查找
哈希是在記錄的存儲位置和記錄的關鍵字之間建立一個確定的對應關係f,使得每個關鍵字key對應一個存儲位置f(key)。查找時,根據這個確定的對應關係找到給定值的映射f(key),若查找集合中存在這個記錄,則必定在f(key)的位置上。哈希技術既是一種存儲方法,也是一種查找方法。
六種哈希函數的構造方法:
1 直接定址法:
函數公式:f(key)=a*key+b (a,b爲常數)
這種方法的優點是:簡單,均勻,不會產生衝突。但是需要事先知道關鍵字的分佈情況,適合查找表較小並且連續的情況。
2 數字分析法:
比如我們的11位手機號碼“136XXXX7887”,其中前三位是接入號,一般對應不同運營公司的子品牌,如130是聯通如意通,136是移動神州行,153是電信等。中間四們是HLR識別號,表示用戶歸屬地。最後四們纔是真正的用戶號。
若我們現在要存儲某家公司員工登記表,如果用手機號碼作爲關鍵字,那麼極有可能前7位都是相同的,所以我們選擇後面的四們作爲哈希地址就是不錯的選擇。
3 平方取中法:
故名思義,比如關鍵字是1234,那麼它的平方就是1522756,再抽取中間的3位就是227作爲哈希地址。
4 摺疊法:
摺疊法是將關鍵字從左到右分割成位數相等的幾個部分(最後一部分位數不夠可以短些),然後將這幾部分疊加求和,並按哈希表表長,取後幾位作爲哈希地址。
比如我們的關鍵字是9876543210,哈希表表長三位,我們將它分爲四組,987|654|321|0 ,然後將它們疊加求和987+654+321+0=1962,再求後3位即得到哈希地址爲962,哈哈,是不是很有意思。
5 除留餘數法:
函數公式:f(key)=key mod p (p<=m)m爲哈希表表長。
這種方法是最常用的哈希函數構造方法。
6 隨機數法:
函數公式:f(key)= random(key)。
這裏random是隨機函數,當關鍵字的長度不等是,採用這種方法比較合適。
兩種哈希函數衝突解決方法:
我們設計得最好的哈希函數也不可能完全避免衝突,當我們在使用哈希函數後發現兩個關鍵字key1!=key2,但是卻有f(key1)=f(key2),即發生衝突。
方法一:開放定址法:
開放定址法就是一旦發生了衝突,就去尋找下一個空的哈希地址,只要哈希表足夠大,空的哈希地址總是能找到,然後將記錄插入。這種方法是最常用的解決衝突的方法。
方法二:再哈希法:
即在同義詞產生地址衝突時計算另一個哈希函數地址,直到衝突不再發生。
方法三:鏈地址法:
將所有關鍵字爲同義詞的記錄存儲在同一線性鏈表中。
方法四:建立一個公共溢出區:
這也是處理衝突的一種方法,所有關鍵字和基本表中關鍵字爲同義詞的記錄,不管它們是由哈希函數得到的哈希地址是什麼,一旦發生衝突,都填入溢出表。

#include <stdio.h>
#include <stdlib.h>   
#include <io.h>
#include <math.h>
#include <time.h>
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 7 /* 定義散列表長爲數組的長度 */
#define NULLKEY -32768 
typedef int Status;    
typedef struct
{
    int *elem; /* 數據元素存儲地址,動態分配數組 */
    int count; /*  當前數據元素個數 */
}HashTable;
int m=0; /* 散列表表長,全局變量 */
/*初始化*/
Status Init(HashTable *hashTable)
{
    int i;
    m=HASHSIZE;
    hashTable->elem= (int *)malloc(m*sizeof(int)); //申請內存
    hashTable->count=m;
    for (i=0;i<m;i++)
    {
        hashTable->elem[i]=NULLKEY;
    }
    return OK;
}
/*哈希函數(除留餘數法)*/
int Hash(int data)
{
    return data%m;
}
/*插入*/
void Insert(HashTable *hashTable,int data)
{
    int hashAddress=Hash(data); //求哈希地址
    //發生衝突
    while(hashTable->elem[hashAddress]!=NULLKEY)
    {
        //利用開放定址的線性探測法解決衝突
        hashAddress=(++hashAddress)%m;
    }
    //插入值
    hashTable->elem[hashAddress]=data;
}
/*查找*/
int Search(HashTable *hashTable,int data)
{
    int hashAddress=Hash(data); //求哈希地址

    //發生衝突
    while(hashTable->elem[hashAddress]!=data)
    {
        //利用開放定址的線性探測法解決衝突
        hashAddress=(++hashAddress)%m;

        if (hashTable->elem[hashAddress]==NULLKEY||hashAddress==Hash(data)) return -1;
    }
    //查找成功
    return hashAddress;
}
/*打印結果*/
void Display(HashTable *hashTable)
{
    int i;
    printf("\n**********展示結果**********\n");

    for (i=0;i<hashTable->count;i++)
    {
        printf("%d ",hashTable->elem[i]);
    }
    printf("\n**********展示完畢**********\n");
}
void main()
{
    int i,j,result;
    HashTable hashTable;
    int arr[HASHSIZE]={13,29,27,28,26,30,38};
    printf("***************哈希查找(C語言版)***************\n");
    //初始化哈希表
    Init(&hashTable);
    //插入數據
    for (i=0;i<HASHSIZE;i++)
    {
        Insert(&hashTable,arr[i]);
    }
    Display(&hashTable);
    //查找數據
    result= Search(&hashTable,29);
    if (result==-1) printf("對不起,沒有找到!");
    else printf("29在哈希表中的位置是:%d",result);
    getchar();
}

性能分析:
雖然哈希表是在關鍵字和存儲位置之間建立了對應關係,但是由於衝突的發生,哈希表的查找仍然是一個和關鍵字比較的過程,不過哈希表平均查找長度比順序查找要小得多,比二分查找也小。
查找過程中需和給定值進行比較的關鍵字個數取決於下列三個因素:哈希函數、處理衝突的方法和哈希表的裝填因子。
哈希函數的”好壞”首先影響出現衝突的頻繁程度,但如果哈希函數是均勻的,則一般不考慮它對平均查找長度的影響。
對同一組關鍵字,設定相同的哈希函數,但使用不同的衝突處理方法,會得到不同的哈希表,它們的平均查找長度也不同。
一般情況下,處理衝突方法相同的哈希表,其平均查找長度依賴於哈希表的裝填因子α。顯然,α越小,產生衝突的機會就越,但α過小,空間的浪費就過多。通過選擇一個合適的裝填因子α,可以將平均查找長度限定在一個範圍內。

發佈了39 篇原創文章 · 獲贊 1 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章