hash_map的简洁实现

 

hash_map是经常被使用的一种数据结构,而其实现方式也是多种多样。如果要求我们使用尽可能简单的方式实现hash_map,具体该如何做呢?

我们知道hash_map最重要两个概念是hash函数和冲突解决算法。hash_map键-值之间的映射关系,hash函数将键映射为内存地址,冲突解决算法用于解决不同的键映射为相同地址时候的情况。

数据结构和算法导论中介绍了大量的hash函数和冲突解决算法,如果选择实现精简的hash_map,那么可以选择“除留取余法”作为hash函数,选择“开散列地址链”作为冲入解决算法。这样的选择不仅使得hash_map实现简单,而且有很高的查询效率。其设计结构如下:

基于地址链的开散列哈希表实现

如图所示,键347通过除留取余法得到地址链的索引,然后将347对应的元素插入到该地址链的尾端。

为了实现地址链,我们需要定义链表节点类型Node。为了简化问题,我们将键值都定义为int类型。

struct Node//地址链节点
{
    int key;//键
    int value;//值
    Node*next;//链接指针
    Node(int k,int v):key(k),value(v),next(NULL){}
};

value用于记录节点数组,key在检索节点时会被使用,next保存链表关系。

基于此,定义hash_map类型SimHash。

#define SIZE 100 //地址链个数,足够大
class SimHash
{
    Node**map;//地址链数组
    size_t hash(int key)//hash函数,除留取余法
    {
        return key%SIZE;
    }
public:
    SimHash()
    {
        map=new Node*[SIZE];
        for(size_t i=0;i<SIZE;i++)map[i]=NULL;//初始化数组为空
    }
    ~SimHash()
    {
        for(size_t i=0;i<SIZE;i++)//清除所有节点
        {
            Node*p;
            while(p=map[i])
            {
                map[i]=p->next;
                delete p;
            }
        }
        delete[] map;//清除数组
    }
    void insert(int key,int value)
    {
        Node*f=find(key);//插入前查询
        if(f)
        {
            f->value=value;//存在键则覆盖
            return;
        }
        Node*p=map[hash(key)];//确定地址链索引
        Node*q=new Node(key,value);//创建节点
        while(p&&p->next)p=p->next;//索引到地址链末端
        if(p)p->next=q;//添加节点
        else map[hash(key)]=q;//地址链的第一个节点
    }
    void remove(int key)
    {
        Node*p=map[hash(key)];//确定地址链索引
        Node*q=NULL;//前驱指针
        while(p)
        {
            if(p->key==key)//找到键
            {
                if(q)q->next=p->next;//删除节点
                else map[hash(key)]=p->next;//删除地址链的最后一个节点
                delete p;
                break;
            }
            q=p;
            p=p->next;
        }
    }
    Node* find(int key)
    {
        Node*p=map[hash(key)];//确定地址链索引
        while(p)
        {
            if(p->key==key)break;//查询成功
            p=p->next;
        }
        return p;
    }
};

首先,我们需要一个Node**类型的指针map记录地址链数组的地址,至于数组大小,我们给一个较大的值(这里设置为100),为了简化问题,不需要考虑插入时表满等情况。在构造函数和析构函数内对map内存分配和撤销。另外,节点的内存是动态增加的,因此析构时需要单独处理每个地址链表。

对于数据结构,最关键的便是插入、删除、检索操作,为此定义操作insert、remove、find。

检索操作实现时,首先通过hash函数定位地址链表的索引,然后在地址链表上检索键是否存在即可。

插入操作实现时,首先会检索键是否存在,如果存在则仅仅更新对应节点的数据即可,否则创建新的节点,插入到链表的结尾。对于空链表需要做特殊处理。

删除操作实现时,需要使用两个指针记录节点的位置信息,当遇到满足键的节点时,就将该节点从链表内删除即可。如果删除链表的第一个节点,需要做特殊处理。

以上,便是一个较简单的hash_map的实现了,代码行约80行左右,当然这肯定不是最简单的,如果你有兴趣可以再做进一步的简化。

如果考虑哈希表的键值类型、特殊键类型的hash映射(字符串类型键如何映射为数值)、特殊键类型的比较处理(怎么比较两个自定义类型键是否相等)、索引运算重载这些问题的话,hash_map的实现就变得复杂了。不过这些在STL内实现的比较完整,若你感兴趣可以多做了解。这里我给出自己的一个考虑以上问题的简单hash_map实现。

struct str_hash
{
    size_t operator()(const char* str)const
    {
        size_t ret=0;
        while(*str)ret=(ret<<5)+*str++;
        return ret;
    }
}; struct str_cmp
{
    bool operator()(const char* str1,const char* str2)const
    {
        while(*str1&&*str2)
        {
            if(*str1++!=*str2++)return false;
        }
        return !*str1&&!*str2;
    }
}; template<class K,class T>
class Hashnode
{
public:
    Hashnode(K k,T d):key(k),data(d),next(NULL)
    {}
    K key;
    T data;
    Hashnode*next;
}; template<class K,class T,class H,class C>
class Hashmap
{
    Hashnode<K,T>**map;//hash链表数组
    size_t div;//数组大小
    size_t hash(K key)//hash函数,获取桶号——除留余数法
    {
        return h(key)%div;
    }
    H h;
    C c;
    Hashnode<K,T>* _find(K key)
    {
        size_t pos=hash(key);//获取桶号
        for(Hashnode<K,T>*p=map[pos];p;p=p->next)
        {
            if(c(p->key,key))//找到了key
            {
                return p;
            }
        }
        return NULL;
    }
public:
    size_t size;//hash表容量
    size_t count;//元素个数
    Hashmap(size_t sz=6):size(6),div(2),count(0)
    {
        if(sz>6)
        {
            size=sz;
            div=size/3;
        }
        map=new Hashnode<K,T>*[div];
        for(size_t i=0;i<div;i++)map[i]=NULL;
    }     ~Hashmap()
    {
        for(size_t i=0;i<div;i++)
        {
            while(true)
            {
                Hashnode<K,T>*p=map[i];
                if(p)
                {
                    map[i]=p->next;
                    delete p;
                }
                else break;
            }
        }
        delete[] map;
    }     void insert(K key,T data)
    {
        if(count>=size)//表满,增加空间
        {
            Hashnode<K,T>**oldmap=map;
            size_t olddiv=div;
            size*=2;
            div=size/3;
            map=new Hashnode<K,T>*[div];
            for(size_t i=0;i<div;i++)map[i]=NULL;
            for(size_t i=0;i<olddiv;i++)
            {
                for(Hashnode<K,T>*p=oldmap[i],*t;p;p=t)
                {
                    t=p->next;
                    p->next=NULL;//消除后继信息
                    size_t pos=hash(p->key);//重新映射
                    Hashnode<K,T>*q;
                    for(q=map[pos];q&&q->next;q=q->next);
                    if(!q)map[pos]=p;
                    else q->next=p;
                }
            }
            delete oldmap;
        }
        Hashnode<K,T>*p=_find(key);//已经存在,替换数据
        if(p)p->data=data;
        else
        {
            size_t pos=hash(key);//获取桶号
            Hashnode<K,T>*p;
            for(p=map[pos];p&&p->next;p=p->next);//索引到最后位置
            Hashnode<K,T>*q=new Hashnode<K,T>(key,data);
            if(!p)map[pos]=q;//插入数据节点
            else p->next=q;
            count++;
        }        
    }     void remove(K key)
    {
        if(count<=size/2)//元素少于一半
        {
            Hashnode<K,T>**oldmap=map;
            size_t olddiv=div;
            size/=2;
            div=size/3;
            map=new Hashnode<K,T>*[div];
            for(size_t i=0;i<div;i++)map[i]=NULL;
            for(size_t i=0;i<olddiv;i++)
            {
                for(Hashnode<K,T>*p=oldmap[i],*t;p;p=t)
                {
                    t=p->next;
                    p->next=NULL;//消除后继信息
                    size_t pos=hash(p->key);//重新映射
                    Hashnode<K,T>*q;
                    for(q=map[pos];q&&q->next;q=q->next);
                    if(!q)map[pos]=p;
                    else q->next=p;
                }
            }
            delete oldmap;
        }
        size_t pos=hash(key);//获取桶号
        for(Hashnode<K,T>*p=map[pos],*q=NULL;p;p=p->next)
        {
            if(c(p->key,key))//找到了key
            {
                if(q)q->next=p->next;
                else map[pos]=p->next;
                delete p;
                count--;
                break;
            }
            q=p;
        }
    }     T& get(K key)
    {
        Hashnode<K,T>*p=_find(key);
        if(p)return p->data;
        //没有key,插入map[key]=0
        insert(key,0);
        return get(key);
    }     T& operator[](K key)
    {
        return get(key);
    }     bool find(K key)
    {
        return !!_find(key);
    }
};
int main()
{
    Hashmap<char*,int,str_hash,str_cmp> hashmap;
    hashmap["A"]=1;
    hashmap["B"]=2;
    hashmap["C"]=3;
    hashmap["D"]=4;
    hashmap["E"]=5;
    hashmap["F"]=6;
    hashmap["G"]=7;
    hashmap["H"]=8;
    cout<<hashmap["A"]<<" "<<hashmap["B"]<<endl;
    cout<<hashmap["C"]<<" "<<hashmap["D"]<<endl;
    cout<<hashmap["E"]<<" "<<hashmap["F"]<<endl;
    cout<<hashmap["G"]<<" "<<hashmap["H"]<<endl;
    cout<<hashmap.size<<endl;
    hashmap.remove("A");
    hashmap.remove("B");
    hashmap.remove("C");
    hashmap.remove("D");
    hashmap.remove("E");
    cout<<hashmap.size<<endl;
    cout<<hashmap["A"]<<" "<<hashmap["B"]<<endl;
    cout<<hashmap["C"]<<" "<<hashmap["D"]<<endl;
    cout<<hashmap["E"]<<" "<<hashmap["F"]<<endl;
    cout<<hashmap["G"]<<" "<<hashmap["H"]<<endl;
    cout<<hashmap.size<<endl;
    return 0;
}

hash_map的简洁实现的更多相关文章

  1. 用神奇的currentColor制作简洁的颜色动画效果

    先上一个兼容性总结图:老版本ie可以直接用复杂方法了,套用某表情包的话:  2016年了,做前端你还考虑兼容IE6?你这简直是自暴自弃! 好了,知道了兼容性,我们可以放心的使用了. 在CSS3中扩展了 ...

  2. Bootstrap 简洁、直观、强悍的前端开发框架,让web开发更迅速、简单。

    Bootstrap 简洁.直观.强悍的前端开发框架,让web开发更迅速.简单.

  3. jQuery弹出提示信息简洁版(自动消失)

    之前看了有一些现成的blockUI.Boxy.tipswindow等的jQuery弹出层插件,可是我的要求并不高,只需要在保存后弹出提示信息即可,至于复杂点的弹出层-可以编辑的,我是直接用bootst ...

  4. gnuplot: 一种更为简洁的曲线,柱状图绘图软件

    gnuplot: 一种更为简洁的曲线,柱状图绘图软件 gnuplot: 一种更为简洁的曲线,柱状图绘图软件 Zhong Xiewei Wed Jun 25 gnuplot简单介绍 关于gnuplot的 ...

  5. 利用简洁的图片预加载组件提升h5移动页面的用户体验

    在做h5移动页面,相信大家一定碰到过页面已经打开,但是里面的图片还未加载出来的情况,这种问题虽然不影响页面的功能,但是不利于用户体验.抛开网速的原因,解决这个问题有多方面的思路:最基本的,要从http ...

  6. 一款简洁大气的jquery日期日历插件

    本jquery插件名为manhuaDate,暂时只支持jquery 1.9.0以下版本,比如jquery-1.8.3.min.js 查看效果网址:http://keleyi.com/a/bjad/em ...

  7. Dev Winform 简洁界面模板制作

    今天看到Dev的安装程序,发现界面很漂亮如下图: 于是想到做个类似的简洁明了的界面出来,平常开发小程序什么的都方便很多. 1.首先是自己添加了一个XtraForm,我们发现它有点丑(我为了性能,习惯把 ...

  8. 转一篇简洁的UIView动画编程方法

    iOS  中的 UIView 动画编程其实还是很简单的,像 CSS3 一样,在给定的时间内完成状态连续性的变化呈现.比如背景色,Frame 大小,位移.翻转,特明度等. 以前我使用的编程方式都是用下面 ...

  9. IOS 日期的简洁格式展示

    首先我要解释一下标题的意义,日期的简洁格式展示,之所以简介,是因为让人一目了然,不需要思考是什么时候. 在详细一点就是我们在微信朋友圈中 所看到的时间格式. 例如:刚刚 -几分钟前-几小时前等等. 今 ...

随机推荐

  1. vs2015 command prompt here

    网上搜的很多方法都不能用,比如:http://app.paraesthesia.com/CommandPromptHere/ 主要是都搞错了注册表路径,写成了: HKCR,Directory\Shel ...

  2. 【转】HTTP POST GET 本质区别详解

    一 原理区别 一般在浏览器中输入网址访问资源都是通过GET方式:在FORM提交中,可以通过Method指定提交方式为GET或者POST,默认为GET提交 Http定义了与服务器交互的不同方法,最基本的 ...

  3. 运费模版源码(.net)

    之前写了一篇关于nop商城系统中运费模版模块相关的随笔,说要把源码贴出来,一直没有贴,现在我把源码贴出来,有任何问题欢迎留言讨论. 源码是在nop上写的,所以文件夹结构和nop的文件夹对应,源码包含的 ...

  4. c# 筛选进程命令行,得其ProcessId(唯一标示符,简称pid),再通过pid结束进程

    不说别的,上代码 部分using: using System.Diagnostics; using System.Management; 其中要引用System.Management 1.通过筛选Co ...

  5. Spring In action chapter1_wiringBeans

    Automatically wiring beans Spring attacks automatic wiring from two angles: Component scanning-Sprin ...

  6. 对偶理论、拉格朗日对偶问题、LP线性规划对偶性质

    Lagrange 对偶问题 定义其的对偶问题: Lagrange函数 考虑线性规划问题 若取集合约束D={x|x≥0},则该线性规划问题的Lagrange函数为 线性规划的对偶问题为: 对偶定理原问题 ...

  7. Java程序,求学员的平均成绩

    第一步,系统提示输入学员的人数. 第二步,逐一获取学员的分数,并累计. 第三步,求平均成绩,并输出. import java.util.Scanner; public class chengji { ...

  8. Web大文件下载控件(down2)-示例更新-Xproer.HttpDownloader

    版权所有 2009-2016 荆门泽优软件有限公司 保留所有权利 官方网站:http://www.ncmem.com/ 产品首页:http://www.ncmem.com/webapp/down2/i ...

  9. Cassandra对读写请求的处理机制

    1 写请求: 单数据中心: 向所有副本发写请求, 所有副本都写数据, 只要有一致性水平指定数目的节点返回正确响应, 就认为写成功. 多数据中心: 客户端发起写数据请求后, 本地代理节点会把请求发给每个 ...

  10. 如何在两个activity之间传递bitmap

    1.需求 在项目开发过程中,打印小票前需要添加打印预览功能,交易数据在打印前转成bitmap然后直接打印,为了显示这个bitmap需要将其传给显示activity. 2.解决方法 把bitmap存储为 ...