哈希详解

一、导引

考虑下面这样一个例子:如果数据中存在重复的元素,请给出输出第一个重复字符的算法。最简单最直接的办法当然是:在给定的字符串中,检查每个字符是否重复。这种方法的时间复杂度为O(n^2)。

现在给出一个更好的解决方案:我们知道可能的字符集合大小是256(为了简单期起见,假设只有ASCII字符)。由此创建一个长度为256的数组,并将其所有元素初始化为0。将每个输入字符放到该数组对应的位置,并增加其计数。由于使用的是数组,所以它仅需要常数时间就可以到达任意指定的位置。当扫描输入字符时,若得到了一个计数器值已经是1的字符,则可以认为该字符就是第一个重复的字符。

char FirstRepeatedChar(char[] str){
        int count[256];
        for(int i=0;i<256;++i)
             count[i] = 0;
        for(int i=0;i<str.length;++i){
            if(count[str[i]]==1){
                System.out.println(str[i]);
                break;
            }
        }
        if(i==len)
          System.out.println("No Repeated Characters");
        return 0;
}

但是如果我们给定的数组是数字而不是字符,那么关键字的取值范围将为无穷大(字符的个数是256是已知的,但是数字的范围是未知的)。使用简单的数组来解决那些关键字取值范围巨大的问题并不是一个正确的选择。所以要想解决这个问题,则需要以某种方式将所有这些可能的关键字映射到可能的内存位置,将关键字映射到存储位置的过程成为散列

二、散列表的简介

Java中最底层的数据存储方式有两种,一种是数组,另一种就是链表。数组查询速度快,但是增加和删除元素速度慢;链表则正好相反。有没有一种数据结构能够综合一下数组和链表,以便发挥它们各自的优势呢?答案就是散列表散列表也叫作哈希表,哈希表有较快的查询速度,以及较快的增删速度,所以很适合在海量数据的环境中使用

一般实现哈希表的方法采用“拉链法”,我们可以理解为“链表的数组”,如下图:

散列表

从上图我们可以发现哈希表是由数组+链表组成的。当我们面临较少的存储位置和较多可能的关键字时,仅利用简单数组是没有足够的内存空间的。一种解决方案就是使用散列表。散列表是一种数据结构,利用散列函数将关键字映射到其关联的值。

三、散列函数

一个好的散列函数应该具有以下特点:

  • 最大限度地减少冲突
  • 简单并快速计算
  • 将键值在散列表中均匀分布
  • 能使用关键字提供的所有信息
  • 对一组给定关键字具有一个高负载因子

其中负载因子=散列表中元素的个数 / 散列表的长度,此参数指出了散列函数是否将关键字均匀分布

四、冲突

冲突是指两个记录存储在相同位置的情况。目前解决冲突最常用的是直接链接法和开放定址法。

  • 直接链接法:链表数组的应用
      分离链接法
  • 开放定址法:基于数组实现
      线性探测法(线性搜索)
      二次探测法(非线性搜索)
      双重散列法(使用两个散列函数)

(1)分离链接法

基于链接法的冲突解决方案是将散列表与链表形式结合起来实现的。当两个或多个记录散列到相同的位置时,这些记录将构成一个单项链表。

分离链接法

(2)开放定址法

这种方法是通过探测来解决冲突的。

1.线性探测法

探测间隔为固定值1。在线性探测中,从发生冲突的原始位置按顺序搜索散列表,如果表中的某个位置被占据,则查找下一个位置。必要时,还可以从表的最后一个位置循环到表的第一个位置进行搜索。用于再次散列的函数如下:

rehash(key) = (n+1)%tablesize

线性探测的一个问题是,表项往往在散列表中聚集,集散列表包含一组连续的被占据的位置,这一现象称为聚集。因此,散列表中的某部分可能相当密集,即使另一部分元素相对较少。因此聚集会导致较长的探测搜索,从而降低整体效率。

2.二次探测法

探测间隔的增加与散列值成正比(因此间隔线性地增加,索引值由一个二次函数描述)。在二次探测中,从发生冲突的初始位置i开始,如果某个位置被占据,则探测i+1^2、i+2 ^2 、i+3^2、i+4 ^2等位置。如果有必要,将从表的最后一个位置循环到表的第一个位置进行探测。再次散列的函数如下:

rehash(key) = (n+k^2)%tablesize

【例子】

表长是11(0..10);散列函数:h(key) = key mod 11

31 mod 11 = 9

19 mod 11 = 8

2 mod 11 = 2

13 mod 11 = 2 → (2+1^2)mod 11 = 3

25 mod 11 = 3 → (3+1^2)mod 11 = 4

24 mod 11 = 2 → (2+1^2)mod 11 , (2+2^2)mod 11 = 6

聚集问题可以使用二次探测方法消除。但是还是存在出现聚集的可能。聚集是由多个关键字映射到同一个散列值引起的,所以与这些关键字相关的探测序列将随着重复冲突的出现而被延长。

3.双重散列法

探测间隔由另一个散列函数计算生成,双重散列法用一种更好的方式减少了聚集。由于探测序列的增量使用第二个散列函数计算,所以第二个散列函数h2应遵循:

h2(key)≠0且h2≠h1

算法首先探测位置h1(key)。若该位置已被占据,那么继续探测位置h1(key)+h2(key),h1(key)+2×h2(key)....。

【例子】

表长是11(0..10)

散列函数:假设h1(key) = key mod 11、h2(key) =7- (key mod 7)

插入关键字:

58 mod 11 = 3

14 mod 11 = 3 → (3+7) mod 11 =10

91 mod 11 = 3 → (3+7) mod 11 , (3+2×7) mod 11=6

25 mod 11 = 3 → (3+3) mod 11 , (3+2×3) mod 11 = 9

五、哈希的应用

Java的每个类都有hashCode方法,hashCode方法返回该对象的哈希码值。

那么为什么对象都要有hashCode方法呢?考虑一种情况,当向集合中插入对象时,如何判别在集合中是否已经存在该对象了?也许大多数人都会想到调用equals方法来逐个进行比较,这个方法确实可行。但是如果集合中已经存在一万条数据或者更多的数据,如果采用equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值来进行重复的判断。所以,之所以有hashCode方法,是因为在批量的对象比较中,hashCode要比equals来得快

两个对象equals相等那么hashcode是一定相等的;equals不相等hashcode可能相等可以不相等。因为hashCode说白了是地址值经过一系列的复杂运算得到的结果,而Object中的equals方法底层比较的就是地址值,所以equals()相等,hashCode必定相等;equals()不等,在java底层进行哈希运算的时候有一定的几率出现相等的hashCode,所以hashCode可等可不等。

如果你重写了equals()方法,那么一定要记得重写hashCode()方法。重写的原则就是按照equals( )中比较两个对象是否一致的条件用到的属性来重写hashCode()

  // HASH
    @Override
    public int hashCode()
    {
        int hash = 17;
        //根据id生成hashCode
        if (this.id != null)
            hash = hash * 31 + this.id.hashCode();

        return(hash);
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o)
            return(true);
        //利用getClass()获得当前对象的类型
        if (o==null || !this.getClass().equals(o.getClass()))
            return(false);

        C c = (C)o;
        //通过id判断两个对象是否相等
        return(
            (this.id == c.id) ||
            (this.id != null && this.id.equals(c.id))
        );
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容