检索

检索

  • 线性表的检索
    • 顺序检索
    • 二分检索法
    • 分块检索
  • 集合的检索
  • 散列表的检索
    • Hash
    • 散列函数
      • 除余法
      • 乘余取整法
      • 平方取中法
      • 数字分析法
      • 基数转换法
      • 折叠法
      • ELFhash字符串散列函数
    • 散列表冲突
      • 开散列法
      • 闭散列法
        • 线性探查
        • 改进线性探查
        • 二次探查
        • 伪随机数序列探查
      • 二级聚集
        • 双散列探查方法
    • 散列表的实现
      • 插入算法
      • 检索算法
      • 删除

线性表的检索

顺序检索

针对线性表里的所有记录,逐个进行关键码和给定值的比较
存储:可以顺序,链接
排序要求:无

template<class Type>
class Item
{
     
private:
    Type key;//关键码域
    //其它域
public:
    Item(Type value) : key(value)
    {
     }

    Type getKey()
    {
      return key; }

    void setKey(Type k)
    {
      key = k; }
};

template<class Type> vector<Item<Type> *> dataList;

template<class Type>
int SeqSearch(vector<Item<Type> *> &dataList, int length, Type k)
{
     
    int i = length;
    dataList[0]->SetKey(k);//将第0个元素设为待检索值,设监视哨
    while (dataList[i]->getKey() != k)
    {
     
        i--;
    }
    return i;//返回元素位置
}

性能:
检索成功:假设每个关键码等概率1/n,总共需要(n+1)/2
检索成功:假设检索失败时都需要比较n+1
成功的平均检索长度:
(n+1)/2 < ASL < n+1
优点:插入元素可以直接加在表尾
缺点:检索时间太长0(n)

二分检索法

将任一元素dataList[I].key与给定值K比较,三种情况:
1.Key=K,检索成功,返回dataList[I]
2.Key>K,若有则一定排在dataList[I]
3.Key,若有则一定排在dataList[I]

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 (k < dataList[mid]->getKey())
        {
     //右缩检索区间
            high = mid - 1;
        }
        else if (k > dataList[mid]->getKey())
        {
     //左缩检索区间
            low = mid + 1;
        }
        else
        {
     //成功返回位置
            return mid;
        }
    }
    return 0;//检索失败,返回0
}

性能:
最大检索长度:log2(n+1)
失败的检索长度:log2(n+1)
成功的平均检索长度为:ASL = log2(n+1) - 1
相当于一个决策树
优点:平均与最大检索长度相近,检索速度快
缺点:要排序、顺序存储,不易更新

分块检索

按块有序,相当于顺序检索与二分法的折中
性能:
分为两级检索
第一级:先在索引表中确定待查元素所在的块,ASLb
第二级:然后在块内检索待查的元素,ASLw
ASL = ASLb + ASLw = log2(1 + n/s) + s/2
优点:插入删除相对较易,没有大量记录移动
缺点:增加一个辅助数组的存储空间,初始线性表分块排序,当大量插入删除时,或节点分布不均时速度下降

集合的检索

template<size_t N>
class mySet
{
     
public:
    mySet();//构造函数
    typedef unsigned long ulong;
    enum
    {
     
        NB = 8 * sizeof(ulong),//unsigned long数据类型的位的数目
        LI = N == 0 ? 0 : (N - 1) / NB//数组最后一个元素的下标
    };
    ulong A[LI + 1];//存放位向量的数组
    mySet(ulong X);

    mySet<N> &set();//设置元素属性
    mySet<N> &set(size_t P, bool X = true);
    mySet<N> &reset();//把集合设置为空
    mySet<N> &reset(size_t P);//删除元素P
    bool at(size_t P) const;//属于运算
    size_t count() const;//集合中元素个数
    bool none() const;//判断是否为空集

    bool operator==(const mySet<N> &R) const;//等于
    bool operator!=(const mySet<N> &R) const;//不等
    bool operator<=(const mySet<N> &R) const;//包含于
    bool operator<(const mySet<N> &R) const;//真包含于
    bool operator>=(const mySet<N> &R) const;//包含
    bool operator>(const mySet<N> &R) const;//真包含
    bool operator&=(const mySet<N> &R) const;

    friend mySet<N> operator&(const mySet<N> &L, const mySet<N> &R);//交
    friend mySet<N> operator|(const mySet<N> &L, const mySet<N> &R);//并
    friend mySet<N> operator-(const mySet<N> &L, const mySet<N> &R);//差
    friend mySet<N> operator^(const mySet<N> &L, const mySet<N> &R);//异或
    friend mySet<N> operator&(const mySet<N> &L, const mySet<N> &R);
};

template<size_t N>
mySet<N> &mySet<N>::set(size_t P, bool X)
{
     //设置集合元素
    if (X)
    {
     //X为真,位向量中相应值设为1
        A[P / NB] |= (ulong) 1 << (P % NB);//P对应的元素进行按位或运算
    }
    else
    {
     //X为假,位向量中相应值设为0
        A[P / NB] &= ~((ulong) 1 << (P % NB));
    }
    return (*this);
}

template<size_t N>
mySet<N> &mySet<N>::operator&=(const mySet<N> &R)
{
     //赋值交
    for (int i = LI; i >= 0; i--)
    {
     //从低位到高位
        A[i] &= R.A[i];//以ulong元素为单位按位交
    }
    return (*this);
}

template<size_t N>
mySet<N> operator&(const mySet<N> &L, const mySet<N> &R)
{
     //交
    return (mySet<N>(L) &= R);
}

散列表的检索

Hash

一个确定的函数关系h,以节点的关键码K为自变量,函数值h(K)作为节点的存储地址
检索时也是根据这个函数计算其存储位置,通常散列表的存储空间是一个一维数组,散列地址是数组的下标
例如,散列函数的值为key中首尾字母在字母表中序号的平均值

int H3(char key[])
{
     
    int i = 0;
    while ((i < 8) && (key[i] != '\0'))
    {
     
        i++;
    }
    return ((key[0] + key[i - 1] - 2 * 'a') / 2);
}

几个概念:
1.负载因子a=n/m:散列表的空间大小为m,填入表中的结点数为n。
2.冲突:某个散列函数对于不相等的关键码计算出了相同的散列地址,在实际应用中,不产生冲突的散列函数极少存在。
3.同义词:发生冲突的两个关键码

散列函数

把关键值码值映射到存储位置的函数,通常用h来表示
Address = Hash(key)
散列函数选取原则:运算尽可能简单,函数的值域必须在表长的范围内,尽可能使得关键码不同时,其散列函数值亦不相同。

常用散列函数选取方法:
除余法,乘余取整法,平方取中法,数字分析法,基数转换法,折叠法,ELFhash字符串散列函数

除余法

用关键码x除以M(往往取散列表长度),并取余数作为散列地址。散列函数为:
h(x) = x mod M
通常选择一个质数作为M值,函数值依赖于自变量x的所有位,而不仅仅是最右边k个低位

M不用偶数的原因:
若把M设置为偶数,x是偶数h(x)也是偶数,x是奇数h(x)也是奇数。缺点:分布不均匀,如果偶数 关键码比奇数关键码出现概率大,那么函数值就不能均匀分布

M不用幂的原因:
若把M设置为2的幂,那么h(x) = x mod 2^k仅仅是x(用二进制表示)最右边的k个位
若把M设置为10的幂,那么h(x) = x mod 10^k仅仅是x(用十进制表示)最右边的k个位

乘余取整法

先让关键码key乘上一个常数A (0 < A < 1),提取乘积的小数部分,然后再用整数n乘以这个值,对结果向下取整,把它作为散列地址,散列函数为:
hash(key) = [n * (A * key % 1)]
若地址空间为p位,就取n = 2^p,所求的散列地址正好是计算出来的a * key % 1 = A * key - [A * key]值的小数点后最左p位(bit)值,此方法的优点:对n的选择无关紧要

平方取中法

此时可采用平方取中法:先通过求关键码的平方来扩大差别,再取其中的几位或其组合作为散列地址。

例如:
一组二进制关键码:(00000100, 00000110, 000001010, 000001001, 000000111)
平方结果:(00010000, 00100100, 01100010, 01010001, 00110001)
若表长为4个二进制位,则可取中间四位作为散列地址:(0100, 1001, 1000, 0100, 1100)

数字分析法

设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各个位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种符号出现的几率均等,在某些位上分布不均匀,只有某几种符号经常出现。
可根据散列表的大小,选取其中各种符号分布均匀的若干位作为散列地址。

各位数字中符号分布的均匀度λ
λ = ∑ (a^k - n / r)^2
a^k表示第i个符号在第k位上出现的次数
n/k表示各种符号在n个数中均匀出现的期望值
λ值越小,表明在该位(第k位)各种符号分布得越均匀

例如:

9 9 2 1 4 8
9 9 1 2 6 9
9 9 0 5 2 7
9 9 1 6 3 0
9 9 1 8 0 5
9 9 1 5 5 8
9 9 2 0 4 7
9 9 0 0 0 1

假设散列表地址范围有3位数字,取各关键码的456位做为记录的散列地址,也可以把第123和第5位相加,舍去进位,变成一位数,与第46位合起来作为散列地址。

数学分析法仅适用于事先明确知道表中所有关键码每一位数值的分布情况,它完全依赖于关键码集合
如果换一个关键码集合,选择哪几位数据要重新决定

基数转换法

把关键码看成是另一进制上的数后,再把它转换成原来进制上的数,取其中若干位作为散列地址,一般取大于原来基数的数作为转换的基数,并且两个基数要互素

例如,给定一个十进制数的关键码是(210485)10,把它看成以13为基数的十三进制数(210485)13,再把它转换为十进制
(210485)13 = 2*13^5 + 1*13^4 + 4*13^2 + 8*13 +5 = (771932)10
假设散列表地址是10000,则可取低4位1932作为散列地址

折叠法

关键码所含的位数很多,采用平方取中法计算太复杂
折叠法:将关键码分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列地址。

两种叠加方法:
移位叠加-把各部分的最后一位对其相加。
分界叠加-各部分不折断,沿各部分的分界来回折叠,然后对其相加,将相加的结果当作散列地址。

例如:
如果一本书的编号是04-42-20586-4
移位叠加:
5864 + 4220 + 04 = [1]0088
h(key) = 0088
分界叠加:
5864 + 0224 + 04 = 6092
h(key) = 6092

ELFhash字符串散列函数

用于UNIX系统V4.0“可执行链接格式”
(Executable and Linking Format,即ELF)

int ELFhash(char *key)
{
     
    unsigned long h = 0;
    while (*key)
    {
     
        h = (h << 4) + *key++;
        unsigned long g = h & 0xF0000000L;
        if (g)
        {
     
            h ^= g >> 24;
        }
        h &= ~g;
    }
    return h % M;
}

特征:
长字符串和短字符串都很有效,字符串中每个字符都有同样的作用,对于散列表中的位置不可能产生不平均的分布。
应用:
在实际应用中应根据关键码的特点,选取适当的散列函数。
若关键码不是整数而是字符串时,可以把每个字符串转换成整数,再应用平方取中法

散列表冲突

开散列法

将hash值相同的数字以链表形式存储在一起,形成一个邻接矩阵

闭散列法

d = h(K)称为K的基地址
当冲突发生时,使用某种方法为关键码K生成一个散列地址序列
d1, d2, d3, ..., dm-1所有d是后继散列地址
形成探查的方法不同,所得到的解决冲突的方法也不同
插入和检索函数都假定每个关键码的探查序列中至少有一个存储位置是空的,也可以限制探查序列长度

可能产生的问题-聚集
散列地址不同的结点,争夺同一后继散列地址
小的聚集可能汇合成大的聚集
导致很长的探查序列

几种常见闭散列方法:
线性探查
二次探查
伪随机数序列探查
双散列探查法

线性探查

如果记录的基位置存储位置被占用,那么就在表中下移,直到找到一个空存储位置,依次探查下述地址单元:d + 1, d + 2, ..., M - 1, 0, 1, ..., d - 1,用于简单线性探查的探查函数是:p(K, i) = i
线性探查优点:表中所有的存储位置都可以作为插入新记录的候选位置

改进线性探查

每次跳过常数c个而不是1个槽,探查序列中的第i个槽是(h(K) + i*c) mod M,基位置相邻的记录就不会进入同一个探查序列。
探查函数是p(K, i) = i*c,必须使常数与M互素

二次探查

探查增量序列依次为:1^2, -1^2, 2^2, -2^2, ...,,即地址公式是d(2i-1) = (d + i^2) % Md(2i) = (d - i^2) % M
用于简单线性探查的探查函数是p(K, 2i - 1) = i * ip(K, 2i) = -i * i

伪随机数序列探查

p(K, i) = perm[i - 1]这里perm是一个长度为M-1的数组,包含值从1到M-1的随机序列

void permute(int *array, int n)
{
     //产生n个数的伪随机排列
	for (int i = 1; i <= n; i++)
	{
     
		swap(array[i - 1], array[Random(i)]);
	}
}

二级聚集

消除基本聚集:基地址不同的关键码,其探查序列有所重叠,伪随机探查和二次探查可以消除。
二级聚集:两个关键码散列到同一个基地址,还是得到同样的探查序列,所产生的聚集,原因探查序列只是基地址的函数,而不是原来关键码的函数。

双散列探查方法

避免二级聚集:探查序列是原来关键码值的函数,而不仅仅是基位置的函数。
双散列探查法:利用第二个散列函数作为常数p(K, i) = i * h(key)。探查序列函数d = h(key)di = (d + i *h2(key)) % M

散列表的实现

ADT字典

template<class Key, class Elem, class KEComp, class EEComp>
class hashdict
{
     
private:
    Elem *HT;//散列表
    int M;//散列表大小
    int currcnt;//现有元素数目
    Elem EMPTY;//空槽
    int h(int x) const;//散列函数
    int h(char *x) const;//字符串散列函数
    int p(Key K, int i);//探查函数
public:
    hashdict(int sz, Elem e)
    {
     
        M = sz;
        EMPTY = e;
        currcnt = 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 Elem &);
    Elem hashDelete(const Key &K);
    int size()
    {
     //元素数目
        return currcnt;
    }
};

插入算法

散列函数h,假设给定的值是K
若表中该地址对应的空间未被利用,则把待插入记录填入地址,如果该地址中的值与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));//home存储基位置
    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;//插入元素e
    return true;
}

检索算法

与插入类似,采用的探查序列也相同。
假设散列函数h,给定的值为K,若表中该地址对应的空间未被占用,则检索失败。否则将该地址中的值与K比较,若相等则检索成功。否则,按建表时设定的处理冲突方法查找探查序列的下一个地址,如此反复下去,关键码比较想等,检索成功,走到探测序列尾部还没找到,检索失败。

template<class Key, class Elem, class KEComp, class EEComp>
bool hashdict<Key, Elem, KEComp, EEComp>::hashSearch(const Key &K, Elem &e) const
{
     
    int i = 0;
    int home = h(K);
    int pos = home;//初始位置
    while (!EEComp::eq(EMPTY, HT[pos]))
    {
     
        if (KEComp::eq(K, HT[pos]))
        {
     //找到
            e = HT[pos];
            return true;
        }
        i++;
        pos = (home + p(K, i)) % M;
    }
    return false;
}

删除

只有开散列方法(分离的同义词子表)可以真正删除
闭散列方法都只能作标记(墓碑),不能真正删除,若真正删除了探查序列将断掉。

template<class Key, class Elem, class KEComp, class EEComp>
Elem
hashdict<Key, Elem, KEComp, EEComp>::hashDelete(const Key &K)
{
     //带墓碑的删除算法
    int i = 0;
    int home = h(K);
    int pos = home;//初始位置
    while (!EEComp::eq(EMPTY, HT[pos]))
    {
     
        if (KEComp::eq(K, HT[pos]))
        {
     
            int 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 i = 0;
    int insplace;
    int home = h(getkey(e));
    int pos = home;//初始位置
    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;
        }
        pos = (home + p(getkey(e), ++i)) % M;
    }
    if (!tomb_pos)
    {
     
        insplace = pos;
    }
    HT[insplace] = e;
    return true;
    return EMPTY;
}

你可能感兴趣的:(数据结构,算法,hash)