概述–设计哈希表
分为set和map
就是根据哈希函数映射到存储的地址,把一个大的集合,映射到一个小的集合
哈希函数 与 碰撞策略 非常重要
题目一 设计哈希set
-
方法一. 大数组法(这种方法太占地方,太占空间)
-
方法二.再哈希法 (二维数组)
-
方法三 使用链表的拉链法
先使用数组,数组的大小要使用质数
哈希函数 M%N=r N要选取质数
M=km N=kn M=qN+r q是商 km=qkn+r ; r=km-qkn; r=k(m-qn) r就是 k的倍数 其取值范围就是
(0,k,2k,3k,N-1)如果k是2 就是 2 4 6 那中间的 1 3就白白浪费了,所以尽量让k是1 k是公约数,所以N尽量为质数 -
方法四 开地址法
如果有冲突,就存入相邻的下一个位置
hash下划线1(key)就是最原始的key的哈希值 下面的这个7是数组的长度
随着不停地往里面放,很快就会产生聚集的现象,都聚集到了一起,哈希冲突的可能性变大了,所以我们可以给hash表扩容,当超过一定的负载因子的时候(hash表里的有效元素/hash表长度),就给hash表扩容
哈希函数的技巧 -
当长度是2的次幂的时候(所以每次扩容要两倍两倍的扩),满足: hash(key) & (长度-1); 就 相当于 hash%currentLen ,使用这种算法非常的快,因为是位运算,不需要再取模了,当长度是2的次幂的时候,长度-1的二进制就全部都是1,与hash(key)相与的时候,就只剩剩余的几位了,其中的hash需要按照如下计算
-
使用右移16位与异或
int hashCode= (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
是因为当数组长度比较小的时候,hash(key) & (长度-1) hash的高几位都被与的时候与掉了,都是0,这样真正起作用的就是低16位了
有的hashCode的低16位可能是一样的,就增大了哈希冲突的概率,这样把高16位移动到低位,再与原来相异或(相同为0,相异为1),高16位还是没有变,但是低16位就有了高16位的特征,这样哈希冲突概率降低
开地址法要删除的时候,不能直接把找到的元素设置为空,因为它附近可能保存了许多同hash值的元素,你置空了相当于这里就没有冲突了,就会插入重复元素,所以插入的时候,看到这里是-1,也不能插入,必须要继续向后检查有没有重复元素
附加知识点: 从arraylist里边遍历边删除,会抛出concurrentModificationException
public static void removeWayThree(ArrayList<Integer> arrayList) {
for (Integer value : arrayList) {
if (value.equals(3)) {//3是要删除的元素
arrayList.remove(value);
}
System.out.println("当前arrayList是"+arrayList.toString());
}
}
删除的几种姿势
弄成字节码
List a = new ArrayList();
a.add("1");
a.add("2");
Iterator i$ = a.iterator();
do
{
if(!i$.hasNext())
break;
String temp = (String)i$.next();
if("1".equals(temp))
a.remove(temp);
} while(true);
可以看到是先调用的hashNext 这个函数里判断了cursor和size相不相等,如果相等就直接退出循环了,所以删除1不会抛异常,注意这里后面调用的remove不是iterator类里的remove 是arrayList里的remove,arrayList里的remove删除之后,只增加了modCount,expectedModCount没有增加,下次再到next的时候expectedModCount和modCount就不一样了,就会抛出异常
所以应该使用iterator的remove,Itr是ArrayList的内部类
ArrayList.this.remove(lastRet)
ArrayList.this是指引用的是外部类的remove
LinkedList里也有内部类ListIterator,里面也有modCount和excpectedModCount
只出现一次的数字
自己异或自己就是0
0异或自己就是自己
所以只要把所有的数字都异或起来,得到的就是重复数字
两数之和
我当时总是两段式的想法,先把 数值->出现这个数值的索引列表 给维护起来
然后再次遍历数组,如果这个数的对子在这个列表里,那么也未必就找到了符合答案要求的两数
1.有可能key对应的这个列表里有一个索引,而这个索引就是它自己,那不可取
[3,2,5] target=6的3时的情况
2.有可能key对应的列表里有一个索引就是自己 还有一个索引不是自己,那可以取不是自己的那个 [3,3,2] target=6的情况
3.有可能key对应的列表里只有一个索引,不是自己,这个比较理想,直接返回就行
[2,4] target=6的情况
所以要尽量不把自己放进hashmap里放了很麻烦,不能一上来就把自己放到map里
而是如果对子在直接就返回答案,不在的话,存储起来
字符串中的第一个唯一字符
这个题目除了使用hashmap以外,还可以使用队列实现,队列有一个先进先出的特性,很适合用来找出第一个满足某个条件的元素。
使用hashmap你需要遍历两遍,但是使用队列+hashmap你只需要遍历一次,就可以找到,队列的队首就是答案,使用hashmap是为了快速得到字符有没有出现过
遍历字符串,如果hashmap里没有正在遍历的字符,就把字符->索引入map和队列里,如果map里已经有正在遍历的字符了,则把字符对应的索引更新为-1,说明已经重复了,并且把这个已经重复的字符出队,并且把队列中剩余的重复字符都出队,保证队首是非重复的
用到了延时删除的技巧,先标记,下次再删除
存在重复元素2
哈希法:
没有必要维护两个哈希表,还遍历两遍,可以只维护一个长度较小的哈希表,里面放数组里的元素和它出现的次数,这样遍历长度更长的哈希表的时候,可以直接在次数上减,如果减到0了,那就说明长度短的数组已经没有这个元素了,这就是拓展问题的解法
归并排序法
可以先把数组排序,然后用归并排序解
日志速率限制器
可以用一个hashmap解决问题,但是这样日志越来越多,最后把内存撑死了,可以使用队列的思想,把时间已经失效的日志从队列删除,有点类似于滑窗,但是这是一次会使很多元素失效的滑窗
存在重复元素2
思路一: 可以使用哈希表实现
只保存元素的最大索引
思路二: 使用滑动窗口实现
使用滑动窗口的时候没有必要单独写一个for循环,创建窗口
可以使用窗口的最后一个值来进行窗口的移动,并且窗口维护一个hashset,窗口移动的时候,把老的元素从窗口中拿出去,加人新的元素,如果在hashset里,新的元素有重复的,那么就符合题目的要求
字母异位词分组
其实是把各个元素设计一种归一的方式
比如说eat tea都归一化成aet
归一的方式有很多种 比如 排序 但是 这种都是小写字母,可以再进行一次哈希,26个字母表有的话,就设置成出现的次数,就是把元素编码成了 a1b3 这种格式
移位字符串分组
不需要和别的字符串比,字符串A里的每个字符比B里的每个字符的差值都是一样的
,这样的思路没有办法写哈希表的key,要尽量和自己相比较,就是和自己字符串首字符的差值,比如 abc 和 efg 和首字母的差值都是 012 012,首字母的差值一样的话,两个字符串里的字符差值也就一样了
从b->a 只要走一步 b-a
从a->b要走 整整一圈除去从b->a 也就是 26-(b-a)
为了二者的统一 可以都加上26再%26
(26+ a-b)%26 a更小 相当于 走 整整一圈除去从b->a 也就是 26-(b-a)
(26+b-a)%26 当b大于a的时候
重复的子树
把树序列化以后放到hashmap里去重,重点在把子树压成字符串,不是最优解,因为要拼接上字符那长长的字符串,也需要O(N)的时间,可以把每个子树的子结构都标个id,每个子树就可以表示为<根, 左子树id, 右子树Id> 这样,拼接的时候也是常量时间,整体就只有O(N)的时间复杂度
两个数据结构:
树结构map
3.0.0 -> 1
2,1,0 -> 2
每个子结构出现的次数:
1->2
2->2
注意: 如果用字符串进行压缩,只能选取前序和后序,中序不能
因为没办法区分开,
字符串表示都是 # 0 # 0 # 无法区分究竟哪个0是根节点
常数时间插入、删除和获取随机元素
想要查找 删除 增加都是O(1)的时间复杂度,查找只有hash表是O(1)的时间复杂度,链表删除需要记录要删除哪个结点,还要保存该结点的前一个结点和后一个结点,不如用数组,删除数组的最后一个元素也是O(1),要删除指定的元素,只要把数组的最后一个元素和指定的元素互换,然后删除最后一个元素就可以了,用哈希表记录元素存储的下标,就知道元素存储的地址了