检索

  • 检索(Search ) 在一组记录集合中找到关键码值等于给定值的某个记录,或者找到关键码符合某种条件的一些记录。
  • 检索效率很重要,需要对数据进行特殊的存储处理。
  • 特殊处理方法:预排序、建立索引、散列技术、B树方法
  • 检索核心操作:关键码的比较
  • 平均检索长度(Average Search Length ):检索过程中对关键码的平均比较次数ASL
    ASL=npiCi

    其中 n 是可能检索的所有关键码,Ci是检索第 i 个关键码的次数,pi是检索第 i 个的概率。
  • 评估检索的算法:ASL,算法所需存储量,算法的复杂性

基于线性表的检索

顺序检索

对线性表中的所有记录,逐个把它们的关键码和给定值进行比较。

//Item 类(非完全)
template<class Type>
Class Item{
private:
    Type key;
public:
    Item(Type value):key(value){}
    Type getKey() {return key;}
    void setKey(Type value){key = value};
};
vector<Item<Type>*> dataList;
//顺序检索算法
template<class T>
int SeqSearch(vector<Item<Type>*>& dataList,int length,Type k){
    int i = length;
    dataList[0] = k;  //将第一个元素设置为给定值作为监视哨
    while(dataList[i] != k)
        i --;
    return i;  //找到第一个关键码符合的记录,返回
}

算法分析:

  • 最好情况:1次
  • 最坏情况:n+1次
  • 检索成功时,假设检索每个关键码的值相等,平均次数为
    ASL=i=1n1ni=n+12
  • 设检索成功的概率为 p
  • ASL=pn+12+(1p)(n+1)=(n+1)(1p2)
  • ASL在 n+12 n+1 之间

二分检索

  • 要求线性表有序
  • 每一次比较缩小一半的范围
template<class Type>
int BinSearch(vector<Item<Type>*>& dataList,int length,Type k){
    int low = 1; high = length;mid;
    while(low < high) //循环
    {
        mid = (low + high)/2;  //二分
        if(dataList[mid] == k)
            return mid;
        else if(dataList[mid] < k)
                low = mid;
        else if(dataList[mid] > k)
                high = mid;
    }
    return 0;  //检索失败返回0
}

算法分析

  • 最大检索长度: [log2[n+1]]

    ASL=i=1[log2(n+1)]1ni2i1=1n[(log2(n+1)1)(n+1)+1]log2(n+1)1

  • 优点:检索速度快。

  • 缺点:需要排序、不易增删

分块检索

  • 顺序与二分法的折衷:兼顾速度与灵活性
  • 分块的条件:不需要均匀,块内不一定要有序,只要保证前一块中的最大关键码小于后一块中的最小关键码。
  • 建立索引表,每个块用一个结点,记录块的最大关键码、起始位置、块的长度(可能不满)。
  • 索引表是递增的有序表。

性能分析

  • n 个元素分成b
  • 两级检索:检索块 ASLb +块内 ASLw ASL=ASLb+ASLw
  • 如果两级都用顺序检索
    • ASLb=b+12,ASLw=s+12ASL=b+12+s+12=b+s2+1=n+s22s+1
    • s=n 时,ASL取最小值, ASLn
    • 速度比顺序检索快,比二分检索慢。
    • 如果数据块放在外存,还会受到页块大小的制约。
  • 如果采用二分检索确定记录所在子表
    • ASL=log2(b+1)1+s+12log2(1+ns)+s2
  • 优点:插入删除容易、没有大量移动数据
  • 缺点:增加辅助存储空间、初始线性表分块排序、结点分布不均时速度下降

集合的检索

用位向量表示集合

  • 适用于密集型集合(数据范围小,集合中有效元素较多),比如查找“奇素数”

散列

检索是直接面向用户的操作。当问题规模很大时,前述基于关键码检索的时间效率可能使用户无法忍受。最理想的情况就是根据关键码直接找到记录的存储地址。

数组按下标读取就是 O(1) 的操作,和数组的规模无关。由此产生了散列方法。

基本思想

  • 一个确定的函数 h
  • 以结点的关键码K为自变量
  • 函数值 h(K) 作为结点的存储地址
  • 检索的时候根据这个函数计算存储位置
  • 散列表的存储空间通常是一个一维数组
  • 散列地址是数组下标
  • 负载因子 α=nm=
  • 冲突 不相同的关键码得到相同的散列地址
  • 同义词 发生冲突的两个关键码

核心问题

  1. 如何构造散列函数?
    Address=Hash(Key)

    • 运算尽可能简单
    • 值域必须在表长范围内
    • 尽可能使避免冲突
    • 综合考虑:关键码长度、散列表大小、关键码分布情况、记录的检索频率
  2. 如何解决冲突?
    • 开散列方法(open hash ),也叫拉链法(separate chaining ),把发生冲突的关键码存在散列表主表之外。
    • 闭散列方法(closed hashing ),也叫开地址法(open addressing),把发生冲突的关键码存在表中另一个槽内。

常用散列函数

  1. 除余法
    • 用散列长度模关键码 h(x)x(modM)
    • M 一般取质数,增大分布均匀的可能性:函数值依赖于关键码x所有位
    • 不用偶数、幂
    • 缺点:连续的关键码映射成连续的函数值,可能导致散列性能降低。
  2. 乘余取整法
    • 先让关键码乘一个数 A(0<A<1) ,取小数部分,乘以散列长度 n ,向下取整。h(x)=[(x×A[x×A])×n]
    • 优点:对 n 的选择无关紧要:地址空间有k位,就取 n=2k
    • Knuth认为A可以取任何值,但是取黄金分割最理想
  3. 平方取中法
    通过平方扩大关键码之间的差别,再取其中几位或组合作为散列地址
  4. 数字分析法

    • n d位数为例
    • 每一位可能有 r 个可能的符号
    • r种不同的符号在各位上出现的频率不一定相同
    • 根据散列表的大小,选取各符号分布均匀的若干位作为散列地址
    • 计算各个位上符号分布的均匀度 λk=ri=1(αkin/r)2
    • 其中 αi 表示第 i 个符号在第k位上出现的次数。
    • λ 越小,说明在 k 位上分布越均匀
    • 仅适用于事先明确知道表中所有关键码在每一位上的分布情况,完全依赖于关键码的集合
    • 如果换一个关键码的集合,就要重新进行分析。
  5. 基数转换法

    • 把关键码看成另一进制上的数
    • 把它转换成原来进制上的数
    • 取其中若干位作为散列地址
    • 一般取大于原来基数的数作为转换的基数,并且两个基数要互素
  6. 折叠法
    • 将关键码分割成位数相同的几部分(最后一部分位数不一定相同),取叠加和作为散列地址。
    • 分为移位叠加和分界叠加。
  7. ELFhash字符串散列函数

开散列方法

  1. 拉链法
    • 插入同义词时,可以对同义词链排序插入。
    • 性能分析:给定一个大小为M存储 n 个记录的表,理想情况下,散列把记录在表中M个位置平均放置,使得每个链表中有nM个记录。如果 M>n ,散列方法的平均代价就 Θ(1)
    • 不适用与外存检索

2.桶式散列

  • 适合于存储在磁盘的散列表
  • 一个文件分成多个存储桶,每一个存储桶包含一个和多个页块。每个页块包含若干记录,页块之间用指针连接。
  • 散列函数 h(K) 表示关键码是 K 的记录所在的存储桶序号。

闭散列方法

  • 基地址位置d0=h(K)
  • 当冲突发生时,使用某种探查方法为关键码 K 生成一个散列地址序列d1,d2,,所有的 di 是后继散列地址
  • 当插入 K 时,如果基地址上的结点已经被其他数据元素占据,按这种探查方法后继地址,找到第一个空闲位置。如果满了,报告溢出。
  • 检索的策略(探查方法)必须和插入相同。
  • 探查方法

    1. 线性(顺序)探查
      • 如果记录的基位置被占用,顺序地查找表中第一个空闲地址。
        p(K,i)=i
      • 缺点:会发生聚集,导致很长的探查序列。
      • 改进:每次跳 c 个位置,但是还是可能导致聚集(clustering)。
      • p(K,i)=ic
      • 二次探查
        探查的增量是
        p(K,i)={12,12,22,22,}
      • 伪随机数序列探查
        p(K,i)=perm[i1]

        • 优点:二次探查和伪随机数序列探查都可以消除聚集。
        • 缺点:二次探查和伪随机数序列探查都不能消除二次聚集,即如果两个关键码散列到同一个基地址,得到的探查序列还是相同的,因而发生聚集。
      • 双散列探查
        • 探查序列不仅仅是基地址的函数,还是是原来关键码的函数
        • 使用两个散列函数 h1,h2
        • 利用第二个散列函数作为常数,每次跳过常数项,做线性检查
        • p(K,i)=ih2(key)
        • h2(key) 必须与 M 互素。
        • 不易产生聚集,但是计算量增大。
  • 算法实现

    • 字典(dictionary),一种特殊的集合,元素是(关键码,属性值)二元组。
    • 同一个字典内关键码必须是不同的。
    • 主要操作:依据关键码来存储和析取值 insert(key,value),lookup(key)

      template<class Key,class Elem,class KEComp,class EEComp>class hashdict{
      private:
          Elem * HT;  //散列表
          int M;  //散列表大小
          int current;  //现有元素数目
          Elem EMPTY;  //空槽
          int p(Key K,int i);  //探查函数
          int h(int x) const;  //散列函数
          int h(char * x) const;  //字符串散列函数
      public: 
          hashdict(int sz,Elem e){
          M = sz;
          EMPTY = e;
          current = 0;
          HT = new Elem[sz];
          for(int i = 0;i < M;i++)
              HT[i] = EMPTY;
          }
          ~hashdict(){delete [] HT;}
      
          bool hashSearch(const Key&,Elem &) const;
          bool hashInsert(const Key& K);
          Elem hasDelete(const Key& K);
          int size() {return current;}
      };
      • 插入算法:基地址未被占用则直接插入,如果非空闲且已经是K则报告已经插入,否则按照探查方法找到下一个地址并循环,直到插入成功。

        //插入算法
        template<class Key,class Elem,class KEComp,class EEComp>
        bool hashdict<Key,Elem,KEComp,EEComp>::hashInsert(const Elem& e){
            int home = h(getkey(e));  //找到新结点基地址
            int i = 0;
            int pos  = home;
            while(!EEComp::eq(EMPTY,HT[pos])){
                if(EEComp::eq(e,HT[pos])) 
                    return false;
                i++;
                pos = (home + p(getkey(e),i)) % M;  //探查
            }
            HT[pos] = e;
            return true;
        }
      • 检索算法:查找 K 对应的基地址,如果空闲则报错,如果非空闲且等于K则检索成功,否则则按照探查方法找到下一个地址并循环,知道检索成功。

        //检索算法
        template<class Key,class Elem,class KEComp,class EEComp>
        bool hashdict<Key,Elem,KEComp,EEComp>::hashSearch(const Key& K,Elem e){
            int  i = 0;
            pos =  home = h(K);  // 初始位置
            while(!EEComp::eq(HT[pos],EMPTY){
                if(KEComp(HT[pos],K)){  //KE、EE的定义是什么?
                    e = HT[pos]; //这是说e=K?检索的结果不应该是地址吗?
                    return true;
                }
                i ++;
                pos = (home + p(K,i)) % M;
            }
            return false;
        }
    • 删除算法:删除不能影响后面的检索,空出的位置应该可以放新的插入。只有开散列方法才可以真正实现删除,闭散列方法只能标记墓碑,不能真正删除。

      //带墓碑的删除算法
      template<class Key,class Elem,class KEComp,class EEComp>
      bool hashdict<Key,Elem,KEComp,EEComp>::hashDelete(const Key& K,Elem e){
          int i = 0;
          pos = home = h(K);  //初始位置
          while(!EEComp::eq(EMPTY,HT[pos])){
              if(EEComp::eq(K,HT[pos])){
                  Key temp = HT[pos];
                  HT[pos] = TOMB;
                  return temp;
              }
              i++;
              pos = (home + p(K,i)) % M;
          }
          return EMPTY;
      }
      
      //带墓碑的插入算法
      template<class Key,class Elem,class KEComp,class EEComp>
      bool hashdict<Key,Elem,KEComp,EEComp>::hashInsert(const Elem& e){
          int insplace;
          int i = 0;
          int pos  = home = h(getkey(e));  
          bool tomb_pos = false;
          while(!EEComp::eq(EMPTY,HT[pos])){
              if(EEComp::eq(e,HT[pos])) 
                  return false;
              if(EEComp::eq(TOMB,HT[pos]) && !tomb_pos)  
              {
                  insplace = pos;
                  tomb_pos = true;
              }
              i++;
              pos = (home + p(getkey(e),i)) % M;  //探查
          }
          if(!tomb_pos)
              insplace = pos;  //如果没有墓碑,则insplace位于空槽位置
          HT[implace] = e;
          return true;
      }

    • 本文主要参考Wang Tengjiao课件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值