第十一章 散列表

 

摘要:

  本章介绍了散列表(hash table)的概念、散列函数的设计及散列冲突的处理。散列表类似与字典的目录,查找的元素都有一个key与之对应,在实践当中,散列技术的效率是很高的,合理的设计散函数和冲突处理方法,可以使得在散列表中查找一个元素的期望时间为O(1)。散列表是普通数组概念的推广,在散列表中,不是直接把关键字用作数组下标,而是根据关键字通过散列函数计算出来的。书中介绍散列表非常注重推理和证明,看的时候迷迷糊糊的,再次证明了数学真的很重要。在STL中map容器的功能就是散列表的功能,但是map采用的是红黑树实现的,后面接着学习,关于map的操作可以参考:http://www.cplusplus.com/reference/map/

1、直接寻址表

  当关键字的的全域(范围)U比较小的时,直接寻址是简单有效的技术,一般可以采用数组实现直接寻址表,数组下标对应的就是关键字的值,即具有关键字k的元素被放在直接寻址表的槽k中。直接寻址表的字典操作实现比较简单,直接操作数组即可以,只需O(1)的时间。

2、散列表

  直接寻址表的不足之处在于当关键字的范围U很大时,在计算机内存容量的限制下,构造一个存储|U|大小的表不太实际。当存储在字典中的关键字集合K比所有可能的关键字域U要小的多时,散列表需要的存储空间要比直接寻址表少的很多。散列表通过散列函数h计算出关键字k在槽的位置。散列函数h将关键字域U映射到散列表T[0....m-1]的槽位上。即h:U->{0,1...,m-1}。采用散列函数的目的在于缩小需要处理的小标范围,从而降低了空间的开销。

  散列表存在的问题:两个关键字可能映射到同一个槽上,即碰撞(collision)。需要找到有效的办法来解决碰撞。

3、散列函数

  好的散列函数的特点是每个关键字都等可能的散列到m个槽位上的任何一个中去,并与其他的关键字已被散列到哪一个槽位无关。多数散列函数都是假定关键字域为自然数N={0,1,2,....},如果给的关键字不是自然数,则必须有一种方法将它们解释为自然数。例如对关键字为字符串时,可以通过将字符串中每个字符的ASCII码相加,转换为自然数。书中介绍了三种设计方案:除法散列法、乘法散法和全域散列法。

(1)除法散列法

  通过取k除以m的余数,将关键字k映射到m个槽的某一个中去。散列函数为:h(k)=k mod m 。m不应是2的幂,通常m的值是与2的整数幂不太接近的质数。

(2)乘法散列法

  乘法散列法构造散列函数需要两个步骤。第一步,用关键字k乘上常数A(0<A<1),并抽取kA的小数部分。然后,用m乘以这个值的小数部分,再取结果的底。散列函数如下:h(k) = m(kA mod 1)。其中“kA mod 1”是取kA的小数部分。

(3)全域散列

  给定一组散列函数H,每次进行散列时候从H中随机的选择一个散列函数h,使得h独立于要存储的关键字。全域散列函数类的平均性能是比较好的。

4、碰撞处理

   通常有两类方法处理碰撞:开放寻址(Open Addressing)法和链接(Chaining)法。前者是将所有结点均存放在散列表T[0..m-1]中;后者通常是把散列到同一槽中的所有元素放在一个链表中,而将此链表的头指针放在散列表T[0..m-1]中。

(1)开放寻址法

  所有的元素都在散列表中,每一个表项或包含动态集合的一个元素,或包含NIL。这种方法中散列表可能被填满,以致于不能插入任何新的元素。在开放寻址法中,当要插入一个元素时,可以连续地检查或探测散列表的各项,直到有一个空槽来放置待插入的关键字为止。有三种技术用于开放寻址法:线性探测、二次探测以及双重探测。

<1>线性探测

  给定一个普通的散列函数h':U —>{0,1,.....,m-1},线性探测方法采用的散列函数为:h(k,i) = (h'(k)+i)mod m,i=0,1,....,m-1  

     探测时从i=0开始,首先探查T[h'(k)],然后依次探测T[h'(k)+1],…,直到T[h'(k)+m-1],此后又循环到T[0],T[1],…,直到探测到T[h'(k)-1]为止。探测过程终止于三种情况: 
  (1)若当前探测的单元为空,则表示查找失败(若是插入则将key写入其中); 
  (2)若当前探测的单元中含有key,则查找成功,但对于插入意味着失败; 
  (3)若探测到T[h'(k)-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。

  线性探测方法较容易实现,但是存在一次群集问题,即连续被占用的槽的序列变的越来越长。采用例子进行说明线性探测过程,已知一组关键字为(26,36,41,38,44,15,68,12,6,51),用除余法构造散列函数,初始情况如下图所示:

散列过程如下图所示:

<2>二次探测

   二次探测法的探查序列是:h(k,i) =(h'(k)+i*i)%m ,0≤i≤m-1 。初次的探测位置为T[h'(k)],后序的探测位置在次基础上加一个偏移量,该偏移量以二次的方式依赖于i。该方法的缺陷是不易探查到整个散列空间。

<3>双重散列

  该方法是开放寻址的最好方法之一,因为其产生的排列具有随机选择的排列的许多特性。采用的散列函数为:h(k,i)=(h1(k)+ih2(k)) mod m。其中h1和h2为辅助散列函数。初始探测位置为T[h1(k)],后续的探测位置在此基础上加上偏移量h2(k)模m。

 (2)链接法

  将所有关键字为同义词的结点链接在同一个链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。

  举例说明链接法的执行过程,设有一组关键字为(26,36,41,38,44,15,68,12,6,51),用除余法构造散列函数,初始情况如下图所示:

最终结果如下图所示:

 5、字符串散列

  通常都是将元素的key转换为数字进行散列,如果key本身就是整数,那么散列函数可以采用keymod tablesize(要保证tablesize是质数)。而在实际工作中经常用字符串作为关键字,例如身姓名、职位等等。这个时候需要设计一个好的散列函数进程处理关键字为字符串的元素。参考《数据结构与算法分析》第5章,有以下几种处理方法:

方法1:将字符串的所有的字符的ASCII码值进行相加,将所得和作为元素的关键字。设计的散列函数如下所示:

int hash(const string& key,int tablesize)
{
    int hashVal = 0;
    for(int i=0;i<key.length();i++)
           hashVal += key[i];
    return hashVal % tableSize;
}

 此方法的缺点是不能有效的分布元素,例如假设关键字是有8个字母构成的字符串,散列表的长度为10007。字母最大的ASCII码为127,按照方法1可得到关键字对应的最大数值为127×8=1016,也就是说通过散列函数映射时只能映射到散列表的槽0-1016之间,这样导致大部分槽没有用到,分布不均匀,从而效率低下。

方法2:假设关键字至少有三个字母构成,散列函数只是取前三个字母进行散列。设计的散列函数如下所示:

int hash(const string& key,int tablesize)
 {
         //27 represents the number of letters plus the blank
         return (key[0]+27*key[1]+729*key[2])%tablesize;
 }

该方法只是取字符串的前三个字符的ASCII码进行散列,最大的得到的数值是2851,如果散列的长度为10007,那么只有28%的空间被用到,大部分空间没有用到。因此如果散列表太大,就不太适用。

方法3:借助Horner's 规则,构造一个质数(通常是37)的多项式,(非常的巧妙,不知道为何是37)。计算公式为:key[keysize-i-1]37^i,0<=i<keysize求和。设计的散列函数如下所示:

int hash(const string & key,int tablesize)
{
        int hashVal = 0;
        for(int i =0;i<key.length();i++)
            hashVal = 37*hashVal + key[i];
        hashVal %= tableSize;
        if(hashVal<0)  //计算的hashVal溢出
           hashVal += tableSize;
       return hashVal;
}

 该方法存在的问题是如果字符串关键字比较长,散列函数的计算过程就变长,有可能导致计算的hashVal溢出。针对这种情况可以采取字符串的部分字符进行计算,例如计算偶数或者奇数位的字符。

6、再散列(rehashing)

  如果散列表满了,再往散列表中插入新的元素时候就会失败。这个时候可以通过创建另外一个散列表,使得新的散列表的长度是当前散列表的2倍多一些,重新计算各个元素的hash值,插入到新的散列表中。再散列的问题是在什么时候进行最好,有三种情况可以判断是否该进行再散列:

(1)当散列表将快要满了,给定一个范围,例如散列被中已经被用到了80%,这个时候进行再散列。

(2)当插入一个新元素失败时候,进行再散列。

(3)根据装载因子(存放n个元素的、具有m个槽位的散列表T,装载因子α=n/m,即每个链子中的平均存储的元素数目)进行判断,当装载因子达到一定的阈值时候,进行在散列。

  在采用链接法处理碰撞问题时,采用第三种方法进行在散列效率最好。

7、实例练习

  看完书后,有一股想把hash表实现的冲动。在此设计的散列表针对的是关键字为字符串的元素,采用字符串散列函数方法3进行设计散列函数,采用链接方法处理碰撞,然后采用根据装载因子(指定为1,同时将n个元素映射到一个链表上,即n==m时候)进行再散列。采用C++,借助vector和list,设计的hash表框架如下:

#include<iostream>
#include<string>
#include<vector>
#include<list>
#include<algorithm>
#include<cstdlib>
#include<cmath>
using namespace std;
int nextPrime(const int n);
template <typename T>
class HashTable
{
public:
    HashTable(int size=101);
    int insert(const T& x);
    int remove(const T& x);
    int contains(const T&x);
    void make_empty();
    void display() const;
private:
    vector<list<T>> lists;
    size_t currentSize;
    int hash(const string &key);
    int myhash(const T& x);
    void rehash();
};

template<typename T>
HashTable<T>::HashTable(int size)
{
    lists=vector<list<T>>(size);
    currentSize=0;
}

template<typename T>
int HashTable<T>::hash(const string &key)
{
    int hashVal=0;
    int tableSize=lists.size();
    size_t i=0;
    for(i=0;i<key.length();++i)
        hashVal=hashVal*37+key[i];
    hashVal=hashVal%tableSize;
    if(hashVal<0)
        hashVal+=tableSize;
    return hashVal;
}

template<typename T>
int HashTable<T>::myhash(const T &x)
{
    string key=x.getName();
    return hash(key);
}

template<typename T>
int HashTable<T>::insert(const T &x)
{
    list<T> &whichlist=lists[myhash(x)];
    if(find(whichlist.begin(),whichlist.end(),x)!=whichlist.end())
        return 0;
    whichlist.push_back(x);
    currentSize+=1;
    if(currentSize>lists.size())
        rehash();
    return 1;
}

template <typename T>
int HashTable<T>::remove(const T &x)
{
    typename list<T>::iterator iter;
    list<T> &whichlist=lists[myhash(x)];
    iter=find(whichlist.begin(),whichlist.end(),x);
    if(iter!=whichlist.end())
    {
        whichlist.erase(iter);
        currentSize--;
        return 1;//delete success
    }
    return 0; //delete fail
}

template <typename T>
int HashTable<T>::contains(const T &x)
{
    list<T> whichlist;
    whichlist=lists[myhash(x)];
    if(find(whichlist.begin(),whichlist.end(),x)!=whichlist.end())
        return 1;
    else
        return 0;
}

template <typename T>
void HashTable<T>::make_empty()
{
    int i;
    for(i=0;i<lists.size;i++)
        lists[i].clear();
    currentSize=0;
}

template<typename T>
void HashTable<T>::rehash()
{
    vector<list<T>> oldlists=lists;
    lists.resize(nextPrime(2*lists.size()));
    size_t i;
    for(i=0;i<lists.size();i++)
        lists.clear();
    //遍历整个vector
    for(i=0;i<oldlists.size();i++)
    {
        typename list<T>::iterator iter=oldlists[i].begin();
        //对list中的每个元素重新插入
        while(iter!=oldlists[i].end())
            insert(*iter++);
    }
}

template<typename T>
void HashTable<T>::display() const
{
    size_t i;
    for(i=0;i<lists.size();i++)
    {
        cout<<i<<" : ";
        typename std::list<T>::const_iterator iter=lists[i].begin();
        while(iter!=lists[i].end())
        {
            cout<<*iter++<<" ";
        }
        cout<<endl;
    }
}

int nextPrime(const int n)
{
    int ret,i;
    ret=n;
    while(1)
    {
        int flag=1;
        for(i=2;i<sqrt(ret);i++)
        {
            if(ret%i==0)
            {
                flag=0;
                break;
            }
        }
        if(flag==1)
            break;
        else
        {
            ret++;
            continue;
        }
    }
    return ret;
}

class Employee
{
public:
    Employee(){}
    Employee(const string n,int s=0):name(n),salary(s) {}
    const string &getName()const {return name;}
    bool operator==(const Employee &rhs) const
    {
        return getName()==rhs.getName();
    }
    bool operator!=(const Employee &rhs) const
    {
        return !(*this==rhs);
    }
    friend ostream& operator<<(ostream &os,const Employee &e)
    {
        os<<"("<<e.name<<","<<e.salary<<")";
        return os;
    }
private:
    string name;
    int salary;
};

int main()
{
    Employee e1("Tom",6000);
    Employee e2("Anker",7000);
    Employee e3("Jermey",8000);
    Employee e4("Lucy",7500);
    HashTable<Employee> emp_table(13);

    emp_table.insert(e1);
    emp_table.insert(e2);
    emp_table.insert(e3);
    emp_table.insert(e4);

    cout<<"Hash table is: "<<endl;
    emp_table.display();
    if(emp_table.contains(e4) == 1)
        cout<<"Tom is exist in hash table"<<endl;
    if(emp_table.remove(e1) == 1)
          cout<<"Removing Tom form the hash table successfully"<<endl;
    if(emp_table.contains(e1) == 1)
        cout<<"Tom is exist in hash table"<<endl;
    else
        cout<<"Tom is not exist in hash table"<<endl;
    //emp_table.display();
    exit(0);
}

  运行结果:

时间: 2024-11-13 07:59:15

第十一章 散列表的相关文章

第十一章 事件[《.net框架程序设计》读书笔记]

.net框架|笔记|程序|设计 第十一章 事件 摘要: ?????? 本章讲述事件的应用,包括: n???????? 发布事件设计模式 n???????? 侦听事件的方法 n???????? 显式控制事件注册 n???????? 一个类型定义多个事件并减少内存资源 ? 一.???????????? 发布事件 1.发布事件的类型提供的功能: l???????? 允许其他对象登记事件 l???????? 允许其他对象注销事件 l???????? 维护一个登记对象列表,在事件发生时通知相应的登记对象

方法-基于散列表的电话号码查询系统设计

问题描述 基于散列表的电话号码查询系统设计 基于散列表的电话号码查询系统设计 基本要求: 1) 设每个记录有下列数据项:电话号码.用户名.地址: 2) 从键盘输入各记录,分别以电话号码和用户名为关键字建立散列表: 3) 采用一定的方法解决冲突: 4) 查找并显示给定电话号码的记录: 5) 查找并显示给定用户名的记录. 扩展要求: 1) 系统功能的完善: 2) 设计不同的散列函数,比较冲突率: 3) 在散列函数确定的前提下,尝试各种不同类型处理冲突的方法,考察 平均查找长度的变化. 解决方案 参考

[叩响C#之门] 22.7 Hashtable类 散列表的基本原理和用法

22.7 Hashtable类 散列表(Hashtable)又叫做字典(Dictionary),能够非常快速的添加.删除和查找元素,是现在检索速度最快的数据结构.

oracle的散列表索引

有许多涉及散列表的数据结构可用做索引.我们假定读者知道用作主存数据结构的散列表.在这种结构中有一个散列函数,它以查找键(我们可称之为散列键)为参数并计算出一个介于0到B-1的整数,其中B是桶的数目.桶数组,即一个序号从0~B-1的数组中包含B个链表的头,每一个对应于数组中的一个桶.如果记录的查找键为K,那么通过将该记录链接到桶号为h(K)的桶列表中来存储它,其中h是散列函数. 1.辅存散列表 有的散列表包含大量记录,记录如此之多,以至于它们主要存放在辅助存储器上,这样的散列表在一些细小而重要的方

java编程思想-java编程四线第二十一章 线程SynchronizationComparisons.java有错误

问题描述 java编程四线第二十一章 线程SynchronizationComparisons.java有错误 //BaseLine 和AtomicTest 是线程不安全的 ,求解答 //: concurrency/SynchronizationComparisons.java// Comparing the performance of explicit Locks// and Atomics versus the synchronized keyword.import java.util.c

java 数据结构- 分离链接散列表,线性探测,平方探测 java 程序代码

问题描述 分离链接散列表,线性探测,平方探测 java 程序代码 给定输入{4371,1323,6173,4199,4344,9679,1989}和散列函数h(x)=x mod 10 分离链接散列表,线性探测,平方探测 java 程序的代码

gwt-关于《GWT揭秘》书中的第十一章项目问题

问题描述 关于<GWT揭秘>书中的第十一章项目问题 我是一名新手,最近在看<GWT揭秘>,照着书把GwtFlow这个项目搭建了一下,但是点击"申请报销"按钮触发ApplyAction类中的handleEvent(BaseEvent be)方法并不能使HtmlPanel类中已经重写的setHtml(String html)方法得到执行,也就是说永远不可能fireEvent(HtmlReady),也就没法改变htmlReady的值,进而也就没法show申请表(App

[转]李战大师-悟透delphi 第十一章 面向对象数据库基础

第十一章  面向对象数据库基础 第二节 数据对象的标识我们在关系数据库的设计和开发中,可能经常需要一些唯一的编号或标识,用来作为关键字,以区别每一个不同的人,每一张不同的单据,每一次不同的信息登记,等等.并且,我们也一直采用这些编号和标识,作为关系的连接字段.但是,要保证编号或标识是完全唯一的,却是一个不大不小的难题.下面我们将详细讨论这一问题,并希望能从另一个高度来理解这一问题.不过,我们首先来看看问题是怎样由来的.现在,给大家讲一个故事. 从前,在北京降生了一个漂亮的小女孩.接生的李阿姨说,

python 教程 第十一章、 异常

第十一章. 异常 1)    try/except/else格式 try: s = raw_input('--> ') except EOFError: print 'Why did you do an EOF on me?' except: print 'Error occurred.' else: print 'Done' except参数说明: except:             Catch all (or all other) exception types. except name