数据结构——查找算法

本文详细探讨了顺序查找、二分查找、分块查找和哈希查找在查找表中的应用,重点介绍了ASL的概念,以及它们在不同查找策略下的计算。讨论了各种查找算法的时间复杂度和适用场景,并给出了哈希查找中处理冲突的方法,如线性探测、二次探测和伪随机探测。
  • ASL(Average Search Length,平均查找长度)是在查找运算中平均需要和待查找值比较的关键字次数。
    在这里插入图片描述
  • 其中n为查找表中元素个数,Pi为查找第i个元素的概率,通常假设每个元素查找概率相同,Pi=1/n,Ci是找到第i个元素的比较次数。

一、顺序查找

  • 顺序查找(Sequential Search)的查找过程为:从表的一端开始,依次将记录的关键字和给定值进行比较,若某个记录的关键字和给定值相等,则查找成功;若在扫描整个表后仍未找到关键字和给定值相等的记录则表示查找失败。
  • 顺序查找方法既适用于线性表的顺序存储结构,也适用于线性表的链式存储结构。对于顺序存储结构查找成功时可以返回其在顺序表中的位置,对于链式存储结构在查找成功时则可以返回元素的指针,即结点所在地址。
template <class T>
int SeqSearch(T a[], int n, T key)
{
    int index = -1;
    for(int i = 0; i < n; i++)
    {
        if(key == a[i])
        {
            index = i;
            break;
        }
    }
    return index;
}
  • 在顺序查找(Sequence Search)表中,查找方式为从头扫到尾,找到待查找元素即查找成功,若到尾部没有找到,说明查找失败。所以说,Ci(第i个元素的比较次数)在于这个元素在查找表中的位置,如第0号元素就需要比较一次,第一号元素比较2次…第n号元素要比较n+1次。所以Ci=i。
    在这里插入图片描述
  • 顺序查找方法查找成功的平均比较次数为(n + 1)/2,当待查找元素不在查找表中时,扫描整个表都没有找到,即比较了n次,查找失败。
  • 顺序查找的时间复杂度为O(n)。

二、二分查找

  • 二分查找(Binary Search,折半查找)的查找过程是:从表的中间位置的记录开始,如果给定值和中间记录的关键字相等,则查找成功;如果给定值大于或者小于中间位置的记录的关键字,则在表中大于或小于中间记录的那一半中继续查找,重复查找操作,直到查找成功,或者在某一步时查找区间为空,则代表查找失败。
  • 折半查找要求线性表中的元素要按关键字有序排列,且由于每次查找都按位置进行操作,所以要求是顺序存储结构。
  • 为了标记查找过程中每一次的查找区间,使用low和high标记当前查找区间的下界和上界,使用mid标记区间的中间位置。
template <class T>
int BinarySearch(T a[], int n, T key)
{
    int low = 0;
    int high = n - 1; 
    while(low <= high)
    {
        int mid = (low + high) / 2;
        int midValue = a[mid];
        if(midValue < key)
        {
            low = mid + 1;
        }
        else if(midValue > key)
        {
            high = mid - 1;
        }
        else
        {
            return mid;
        }
    }
    return -1;
}
  • 折半查找的时间复杂度为O(log2(N)),要比顺序查找的O(N)效率更高,但折半查找只适用于有序表,且限于顺序存储结构。
    在这里插入图片描述
  • 对长度为n的有序表进行折半查找的判定树的高度为log2(n) + 1,log2(n)向下取整。
  • 在折半查找判定树中,某结点所在的层数即是查找该结点的比较次数,整个判定树代表的有序表的平均查找长度即为查找每个结点的比较次数之和除以有序表的长度。
  • 给11个数据元素有序表(2,3,10,15,20,25,28,29,30,35,40)采用折半查找。
    • 二叉查找判定树如下:
      在这里插入图片描述
    • 查找成功时总会找到途中某个内部结点,所以成功时的平均查找长度为:
      在这里插入图片描述
      • 即25查找一次,成功,10、30要查找2次,成功,2、15、28、35要查找3次,成功,3、20、29、40要查找4次,成功。
    • 不成功的平均查找长度为:
      在这里插入图片描述
      • 内部结点都能查找成功,而查找不成功的是空的外部结点,所以到查询到2的左孩子,15的左孩子,28的左孩子,35的左孩子,3的左右孩子,20的左右孩子,29的左右孩子,40的左右孩子时,都是查找不成功的。如我要找1,比25小,转向左子树,比较一次,比10小,转左子树,2次,比2 小,转左子树,3次,此时2无左子树,所以失败。

三、分块查找

  • 分块查找(索引顺序查找)是对顺序查找的一种改进,是一种介于顺序查找和二分查找之间的查找算法,分块查找的基本思想是:首先查找索引表,可用二分查找或顺序查找,然后在确定的块中进行顺序查找。

  • 分块查找要求表中每个块之间是有序的,即前块中最大关键字必须小于后块中的最小关键字,但块内元素的排列可无序。

  • 在分块查找中:

    • 需要建立一个索引表来划分块区域,通过定位某一块区域来查找相应信息
    • 索引表的表项包括两项内容:最大关键项、最大关键项块区域的起始地址
    • 同时索引表一定是有序的顺序表,可用顺序查找和折半查找两种方法查找索引表;
    • 而对索引表所标识的块区域中的数据是无序的,则只能使用顺序查找。
  • 分块索引信息如下:

    // 块结构信息
    template <class T>
    struct BlockInfo
    {
        T key;        // 索引区间的最大键值
        int start;      // 索引区间的起始地址(下标)
        int end;      // 索引区间的结束地址(下标)
    };
    
  • 分块查找算法实现如下:

    template <class T>
    int BlockSearch(T array[], int arrayLen, BlockInfo<T> blocks[], int indexLen, T key)
    {
        int ret = -1;
        // 折半查找索引表,找到关键值所在分块
        int low = 0, high = indexLen - 1;
        int mid = (low + high) / 2;
        while(low < high)
        {
            if(key == blocks[mid].key)
            {
                break;
            }
            else if (key < blocks[mid].key)
            {
                high = mid;
            }
            else
            {
                low = mid + 1;
            }
            mid = (low + high) / 2;
        }
    
        // 在分块内进行顺序查找
        for(int i = blocks[mid].start; i <= blocks[mid].end; i++)
        {
            if(key == array[i])
            {
                ret = i;
                break;
            }
        }
        return ret;
    }
    
  • 分块查找算法的运行效率受两部分影响:查找块的操作和块内查找的操作。查找块的操作可以采用顺序查找,也可以采用折半查找;块内查找的操作采用顺序查找的方式。

四、哈希查找

哈希表

  • 哈希表不同于线性表数表之处在于其查找关键字(key)时不需要遍历表,哈希表中的每一个元素都可以根据元素值计算出其存放的地址,从而达到查找时长为O(1)。

2、构造哈希函数

  • 哈希函数的要求:
    • 每一个关键字只能有一个地址与之对应。
    • 函数值域必须在表长范围之内。
    • 散列地址尽量均匀分布,尽可能的减少冲突。
直接定址法
  • 直接定址法直接利用某个线性函数对关键字映射,值为映射的哈希地址,哈希函数:
    f(key)=a∗key+bf(key)=a*key + bf(key)=akey+b
  • 直接定址法计算简单,不会产生哈希冲突,适合关键字分布均匀的情况。如果关键字分布不均匀,则会浪费大量空间。
除留余数法
  • 除留余数法即对key进行取模运算,哈希函数:
    f(key)=key%pf(key)=key \% pf(key)=key%p
  • 设查找表表长为m,p是一个不大于但最接近或者等于m的质数。
数字分析法
  • 数字分析法需要根据已有数据序列的特征设计哈希函数,适用于已知的关键字集合。如果更换了关键字,就需要重新构造新的散列函数。
平方取中法
  • 取关键字平方后的中间几位为哈希地址。通过平方扩大差别,另外中间几位与乘数的每一位相关,由此产生的散列地址较为均匀。这是一种较常用的构造哈希函数的方法。
折叠法
  • 折叠法是将key从左到右分割成位数相等的几个部分,然后将不同部分叠加求和,并按哈希表的表长,取后几位作为f(key)。
随机数法
  • 选择一个随机函数,取关键字的随机函数值为哈希地址,即
    f(key)=random(key)f(key)=random (key)f(key)=random(key)
  • 当关键字长度不等时采用此法构造哈希函数较恰当。

3、哈希冲突

开放地址法(闭散列法)
  • 开放地址法的核心思想是把发生冲突的元素放到哈希表中的另外一个位置。
线性探测法
  • 发生冲突时,逐位往后挪动,寻找合适位置,只要哈希表没满,就一定能找到一个不发生冲突的位置。addressi=( Hash(key) + di ),其中 di = 1,2,3···
  • 线性探测法的优点:只要散列表未填满,总能找到一个不发生冲突的地址。
  • 线性探测法的缺点:会产生二次聚集现象。
二次探测法
  • 发生冲突时,每次向后挪动k^2个单位(k为挪动次数)。addressi=( Hash(key) + di ),其中 di = 12,22,32···
  • 二次探测法的优点:可以避免 二次聚集现象。
  • 二次探测法的缺点:不能保证一定找到不发生冲突的地址。
伪随机探测法
  • 发生冲突时,每次向后挪动k个单位(k为伪随机生成数)。
  • 伪随机探测法的优点:可以避免二次聚集现象。
  • 伪随机探测法的缺点:不能保证一定找到不发生冲突的地址。
链地址法(开散列法)
  • 链地址法的基本思想是:把具有相同散列地址的记录放在同一个单链表中,称为同义词链表。有 m个散列地址就有 m个单链表,同时用数组 HT[0…m-1]存放各个链表的头指针,凡是散列地址为 i 的记录都以结点方式插入到以HT[i]为头结点的单链表中。

4、哈希查找

  • 哈希查找是一种快速查找算法,不需要对关键字进行比较,而是根据关键字使用哈希函数直接计算得到其地址。

  • 当查找某一元素的时候,首先通过哈希函数计算其哈希地址,然后比较该地址的值是否等于目标值,如果相等则查找结束,否则利用处理冲突的方法确定新的地址,再进行比较。如果哈希地址为空,则查找失败。

  • 哈希查找算法实现:

    • 首先定义一个散列表结构
    • 对散列表进行初始化
    • 对散列表进行插入操作
    • 根据不同的情况选择散列函数和处理冲突的方法
  • 哈希查找算法实现如下:

    #include "stdio.h"
    
    #define SIZE 10
    
    template <class T>
    struct HashTable
    {
        T key;        //关键字 
        int used;       //占用(冲突)标志,0表示没被占用,1表示被占用 
    };
    
    template <class T>
    void CreateHashTable(HashTable<T> tbl[], T data[], int len)
    {
        for(int  i = 0; i < len + 1; i++ ) //把哈希表被占用标志置为0 
        {
            tbl[i].used = 0;
        }
        for(int i=0; i < len; i++ )
        {
            T addr = data[i] % (SIZE + 1);// 除留余数法计算哈希地址 
            int k = 0;//记录冲突次数 
            while(k++ < SIZE + 1)
            {
                if(tbl[addr].used == 0 )
                {
                    tbl[addr].used = 1;//表示该位置已经被占用 
                    tbl[addr].key = data[i];
                    break;
                }
                else
                {
                    addr = (addr + 1) % (SIZE + 1); //处理冲突 
                }
            }	
        }
    }
    
    template <class T>
    int HashSearch(HashTable<T> tbl[], T key, int len)
    {
        T addr = key % (len + 1);//计算Hash地址 
        int loc = -1; 
        int k = 0;//记录冲突次数 
        while(k++ < len + 1)
        {
            if(tbl[addr].key == key )
            {
                loc = addr;
                break;
            }
            else
            {
                addr = (addr + 1) % (len + 1); //处理冲突 
            }	
        }
        return loc;
    }
    
    
    int main(int argc, char* argv[])
    {
        HashTable<int> HashTbl[SIZE + 1];
    
        int data[SIZE] = { 10, 8, 14, 15, 20, 31 };
        printf( "Data:  \n" );
        for(int i = 0; i < SIZE; i++ )
        {
            printf("data[%d] = %5d\n", i, data[i] );
        }
        printf("\n");
    
        CreateHashTable(HashTbl, data, SIZE + 1);
        printf("HashTable:  \n" );
        for(int i = 0; i < SIZE + 1; i++)
        {
            printf("tbl[%d] = %5d\n", i, HashTbl[i].key);
        }
        printf("\n");
    
        for(int i = 0; i < SIZE; i++)
        {
            int loc = HashSearch<int>(HashTbl, data[i], SIZE + 1);
            printf("%5d 's loc = %5d\n", data[i], loc);
        }
        
        return 0;
    }
    
  • 查找成功的平均查找长度是指查找到哈希表中已有关键字的平均探测次数。

  • 查找不成功的平均查找长度是指在哈希表中找不到待查的元素,最后找到空位置元素的探测次数平均值。

  • 散列表长度为13,地址空间为0~12,散列函数H(k) =K mod 13,关键字序列{19,14,23,01,68,20,84,27,55,11,10,79} 所以线性探测结果为:
    在这里插入图片描述

  • 根据探测次数,哈希查找的ASL为:
    在这里插入图片描述
    在这里插入图片描述

    • 哈希查找成功:12个元素,每个元素的探测次数之和除以12就行。
    • 哈希查找失败:散列表长度为13,根据定义,假设待查关键字不在散列表中,要一直找到空元素才算查找失败。H[0]为空,与待查找元素不等,不成功,比较一次,H[1],此时H[1]的元素与原本放在H[1]的元素不等(假设不在散列表在之中,但也不是空的),继续向后比,与H[2]比也不等,继续向后,一直到H[12],也不等,继续向后时,回到H[0],为空,也不等,查找失败,总计比较13次,然后计算第二号元素,一样的比较,一直把每个位置都统计一遍,从而得出ASL不成功的。
  • 源码:https://github.com/scorpiostudio/HelloCode

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值