day11集合 映射 哈希表

集合

集合的特点

不存放重复的元素

集合使用场合

存放新增IP,统计新增IP量;存放词汇,统计词汇量等

集合的接口设计

Set

集合实现思路

集合可以通过我们的之前介绍的数据结构去实现,例如有动态数组,链表,二叉搜索数(AVL数,红黑树)等,因为我们之前说过红黑树的性能相对来说会比较好,所以demo中我们会采取红黑树去实现,具体代码会在文章末尾给出来


映射(Map)

Map概念

Map在一些编程语言之后又称作字典(dictionary,比如OC,Swift,Python等)

是一个键值对来的,买一个value都对应这个一个key,同理通过key可以获取到相应的value,如下图所示

map

Map中的每一个key都是唯一的

Map的接口设计

map

通过对比Map与Set,发现Map中所有的key组合在一起,其实就是一个Set,其实也可以间接利用Map来做内部实现

哈希表

哈希表出现原由?假设现在设计一个写字楼通讯录,存放所有公司的通讯信息,座机号码座位key(假设座机号码最长是8位),公司详情(名称地址等)作为value,添加,删除,搜索的时间复杂度要求是O(1)

我们可以通过数组来满足这个任务:

company 设计模式

这样设计会存在什么问题呢?

demo图

从demo图可以看到,这个就是一个哈希表,但是这样的设计会导致空间使用率非常低,并且浪费内存空间,因此这个时候哈希表的高效处理就派上用途了。

它是如何实现高效处理数据的?请看下图:


高效图

但是会存在一种问题,就是有可能会导致哈希冲突,意思就是两个不同的key,经过哈希函数计算出相同的结果,这种现象称为哈希冲突,如下:

哈希冲突

解决哈希冲突的方法

1.开放地址法(Open Addressing)

按照一定规则向其他地址探测,知道遇到空桶

2.再哈希法(Re-Hashing)

设计多个哈希函数

3.链地址法(Separate Chaining)

比如通过链表或者红黑树将同一index的元素串起来

链表
红黑树

哈希函数

生成哈希表中哈希函数的实现步骤如下:

1.下生成key的哈希值(必须是整数

2.再让key的哈希值跟数组的大小进行相关运算,生成一个索引值

public int hash(Object key){

    return hash_code(key) % table.lenght;

}

为了提高效率,可以使用&位运算取代%运算,墙体是数组的长度设计为2的幂

public int hash(Object key) {

    return hash_code(key) & (table.lenght - 1)

}

如何生成key的哈希值

良好的哈希函数能够让哈希值更加均匀分布,减少哈希冲突次数,提升哈希表性能

key的常见种类可能有如下:

1.整数,浮点数,字符串,自定义对象

2.不同种类的key,哈希值的生成方式不一样,当目标是一样的(尽量让每个key的哈希值是唯一,尽量让key的所有信息参与运算)

下面以JAVA语言为例说明相关内容:

在Java中,HashMap的key必须实现hashCode,equal方法,并且允许key为nil

当key为整数(整数值当做哈希值)

public static int hashCode(int Value){

    return value;

}


当key为浮点数

将存储的二进制格式转为整数值

float类型

public static int  hashCode(float value){

    return floatToIntBits(value);

}

Long类型

public static int hashCode(long value){

    return (int)(value ^ (value >>> 32));

}

Double类型

public static int hashCode(double value){

    long bits = doubleToLongBits(value);

    return (int)(bits ^ (bits >>> 32));

}

上面的>>>和^的作用是用来将value的高32bit和低32bit混合计算出32bit的哈希值,如下图所示:

浮点数计算哈希值

当key是字符串

字符串是由若干个字符组成的,比如字符串jack,由j,a,c,k四个字符组成,因此jack的哈希值可以表示为

j * n^3  + a * n^2  + c * n^1  + k * n^0 等价于[*j * n + a) * n + c] * n + k,在JDK中,乘数n为31,为什么使用31呢?

是因为31是一个奇素数(既是奇数,又是素数,也就是质数),素数和其他数相乘的结果比其他方式更容易产生唯一性,减少哈希冲突,并且JVM会将31 * i优化成(i << 5)-i;

自定义对象的哈希值

自定义对象哈希和key处理

自定义对象作为key

自定义对象作为key,最好同时重写hashCode,equals方法

equals:用于判断2个key是否为同一个key

自反性:对于任何非null的x,x.equals(x) 必须返回true

对称性:对于任何非null的x,y,如果y.equals(x)返回true,x.equals(y)必须返回true

传递性:对于任何非null的x,y,z,如果x.equals(y),y.equals(z)返回true,那么x.equals(z)必须返回true

一致性:对于任何非null的x,y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y),就会一致地返回true,或者一致地返回false,另外对于任何非null的x,x.equals(null)必须返回false

hasCode

必须保证equals为true的2个key的哈希值一样,反过来hashCode相等的key,不一定equals为true

接口设计


方法图

接口的设计和实现会在例子中给出来,因为都是基于红黑树来实现的,所以这里就不一一再次解释,不懂可以回到红黑树文章想看一篇,然后再看例子哈,稍微有不同的就是元素节点类型不一样,以前是Node<E> 现在变成了Node<K,V>,并且还多出了哈希函数和装填因子,还有扰动计算等,在这里主要说几个方法:

private Node node(Node node, K k1) 查找Node节点图

Node节点

这个方法是先比较哈希值,1.哈希值大的从右边取,2.哈希值小的从左边取,3.哈希值相等判断key是否相同,如果相同直接返回node值4.如果哈希值相等,但是key不相等,并且key不等于空且是同一种类型并具有可比性的话,对比compareTo的结果,为什么需要对比呢,假设有个Person对象存放三个属相new Person("forest","23","m") new Person("macal","23","m"),如果compareTo比较的仅仅是年龄的话,判断cmp=0,就直接覆盖,这样的操作明显是错误的,所以这里需要获取compareTo的结果,cmp大于0的取后边,cmp小于0的取左边,cmp等于0的话,再扫描其他节点(左右节点

public V put(K key, V value) 添加元素

添加元素方法


大致分析思路和上面说寻找node节点的差不多,但是可以看到多啦searched这个bool变量值,这个bool变量值是用来标志是否已经搜索个整个树了,这样可以提高算法效率,另外或者这里有人会有疑问,就是,我将h1>h2和h1<h2,然后再将cmp=System.identityHashCode(k1)-System.identityHashCode(k2)统一成cmp=1,也可以实现效果呀,就算一直往右添加,但是红黑树有个fix的操作,最终也不会导致失衡,的确是达到效果,但是从实现上面去说的话,可以看到,性能差好多,因为每次进去一开头就是equal,如果equal不满足,马上就要遍历整棵树了,算法效率较低,你可以尝试一下;

先通过哈希值判断
没有通过哈希值判断


扰动计算private int hash(K key);

扰动计算

对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。简单点说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率;

private void resize() 扩容

resize

在讲扩容之前,我们先看看索引的情况先:

索引扩容前后更改

当扩容为原来容量的2倍时,节点的索引有两种情况,第一种:保持不变;第二种:index = index + 旧容量;

  扩容的思路是将我们原来哈希表上面的值,直接移到新的哈希表上,这样可以节省空间内存,可以看到上面方法中有一个移动的操作:

moveNode 部分代码图

可以看到会有一个重置的操作,为什么需要重置?是因为将节点移动过去之后,原来的parent,left,right等属性都还在,但是我们不能保持原来的这种关系,我们要维护的是新的关系表,所以需要重置,但是为了防止重置影响扩容,所在在扩容方法执行move之前,需要将left,right入队,这也是为什么moveNode要放在queue.offer 方法后面;

LinkedHashMap

linkedHashMap是指遵循一定的添加顺序的,使得遍历的结果是遵从添加顺序的,可以在HashMap的基础上维护元素的添加顺序,使得遍历的结果是遵从添加顺序的,设计形式如下图所示:

LinkedHashMap设计

因为是基于HashMap实现的,所以很多东西都是相同的,只有个小细节需要更改一下,第一个是需要自定义一个节点继承原来的节点Node,并在自定义的新节点里面添加两个变量子节点,命名为prev,next(因为是用双链表实现的),另外一个就是需要在put方法里面做修改,创建节点的时候,暴露一个创建节点的方法给子节点去实现(目的是为了创建的时候建立双链表的指向链接)代码图如下:

LinkedNode 创建方法

createNode 是由多肽方式实现的,实际就是在父类的put方法里面创建所调用,也就是添加元素,到此,添加元素已经可以说是设计完毕了,另外,还有一个方法就是删除方法,删除方法稍微会复杂点,因为涉及到度为2的叶子节点,我们用之前的双链表删除的方法是否还能达到效果呢?且看一下:

删除节点示例图
双链表删除代码

可以发现以前的删除双链表元素的方法在这里稍微会有点问题,正确的做法,是在执行删除代码之前,先互换一下删除节点和前驱\后继节点的位置,然后再进行删除,示例图如下:

交换后删除图

代码实现图:

代码图

第一个红框判断,如果是度为2的叶子节点,就会进入,因为节点会被重新赋值,所以会有不相等的情况

第二个红框交换prev的值

第三个红框交换next的值

第四个红框删除元素

到这里哈希表可以说是介绍完了,具体的实现代码会上传到demo中;

补充知识

1.关于使用%来计算索引

如果使用%来计算索引,建议把哈希表的长度设计为素数,这样可以大大减少哈希冲突

对比图

下面表格列出了不同数据规模对应的最佳素数,特点如下:每个素数略小于前一个素数的2倍,每个素数尽可能接近2的幂

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

推荐阅读更多精彩内容