java中map 和 set的底层实现是通过搜索树和哈希函桶来实现
搜索树
二叉搜索树有叫二叉排序树 他具有以下的特点
若存在左节点,那么他左节点的值一定小于根节点
若存在右节点,那么他右节点的值一定大于根节点
它的左右子树也是搜索树
对他进行中序遍历会的到一个升序的数组,且能达到去重的效果
二叉搜索树的实现
字段的实现
二叉树的插入
先找到要插入的位置,在插入时和节点的值比较,若小于该节点的值就往做边走,大于就往右边走
,这样一直走到null,那么这个位置就是我们要插入的位置,但是我们没有这个位置的父节点,所以需要一个parent来记录父节点
非递归
递归
查找
其实在实现插入的时后也就随便实现了查找,在给二叉树找到合适的插入位置时,当该值和节点值相同时就返回true 若没有那就返回false
删除
找到要删除的值对于的节点
删除比较麻烦 要分3中情况考虑
1,若删除的节点的左节点为空 ,那么直接删除该节点就好
2,若右节点为空,那么也是直接删除节点就好
3,如果左右都不为空 那么就不呢直接删除节点了,因为左右还要节点,这要使用左边节点替代?还是右边?且左右节点都可能右节点的,显然不行
那么我们可以使用替换删除,使用15左树最大的值或者右树最小的值去取代15
搜索树是否创建成功,可以使用中序遍历来检查,正确的搜索树中序遍历会的到一个升序的数组
map set 是一种专门用来进行搜索的数据结构,搜索的效率和他实例化子类(Tree hash)有关
模型
一般吧查找数据的关键字称为key 而与key对应的值就是value
K-V模型 比如查找单词 和对应单词的出现次数 <单词,出现的次数> 对于的数据结构时Map
纯K模型 只有Key 对于的数据结构是Set
map的使用
put(key,value) | 插入数据在map中 key独一无二的 |
get(key) | 根据key返回对应的value |
containsKey(key) | 判断key是否存在于map中 返回值时boolean类型 |
containsValue(value) | 判断value是否存在于map中 返回值时boolean类型 |
getOrDefault(key,default) | 获得key对应的value,如果 不存在那么就返回default的值 |
isEmpty() | map是不是为空 |
clear() | 清空map |
remove(key) | 删除对应key-value键值,并 返回key对应的value |
remove(key,value) | 删除对应的key-value键值 返回值时boolean类型 |
size() | 返回键值对个数 |
replace(key,value) | 将key对应的value修改为传 入的value值,并返回的是key 未修改前的值 |
repalce(key,oldValue,newValue) | 将key对应的oldValue修改为 newValue对应的值,返回值是 boolean类型,若k-v不匹配,那么 替换会失败 |
toString() | 返回一个字符串,包括map所有 k-v数据 |
keySet() | 将map中的所有key以set返回 |
values() | 放回value的可重复集,返回类型为 Collection<V> |
set的使用
add(key) | 插入key到set中 |
contains(key) | 判断set中是否存在key 返回值是boolean类型 |
remove(key) | 删除key |
size() | 返回set大小 |
clear() | 清空set |
toArray() | 将set中的所有key以数组形式返回 |
toArray(T[] a) | 将set中的所有key以数组形式返回 传入的a是是key类型的数组 返回的也是key类型的数组 |
isEmpty() | set是否为空 |
iterator() | 返回一个迭代器 可用来遍历set |
containsAll(Collection<?> c) | c集合中的值是否在set都存在, 若是那么就返回true 否false |
removeAll(Collection<?> c) | 删除set中含有的集合c中的值 |
addAll(Collection<? extends E> c) | 将c中的所有值加到set中,会去重 |
map和set
map的底层结构 | TreeMap | HashMap |
底层结构 | 红黑树(也是搜索树) | 哈希桶 |
插入,删除,查找 时间复杂度 | O(logN) | O(1) |
是否有序 | 关于k有序 | 无序 |
删除 插入 查找区别 | 需要进行元素之间的比较 | 通过哈希函数直接进行查找 |
适用范围 | 需要key有序的情况下 | 不在乎有无序,需要更高的效率 |
set的底层结构 | TreeSet | HashSet |
底层结构 | 红黑树 | 哈希桶 |
插入,删除,查找 时间复杂度 | O(logN) | O(1) |
是否有序 | 关于k有序 | 无序 |
删除 插入 查找区别 | 需要进行元素之间的比较 | 通过哈希函数直接进行查找 |
适用范围 | 需要key有序的情况下 | 不在乎有无序,需要更高的效率 |
哈希表
在顺序结构和树结构中进行数据的查询时,因为没有与查找的数据对应的关键码,所以在查找数据时会进行多次的数据之间的比较,而就是这些比较会大大增加我们的查找时间,所以时间复杂度会到达logN 或n^2这么高
而理想的查找查找方式是不同过任何比较,一次就找到这个数据,那么如果构造一种数据结构,他可以将数据和数据的关键码进行一一映射的关系,通过这个关键码就可以联系到该数据的位置,那么他的时间复杂度就可以达到O(1)
向该数据结构中插入数据
通过插入该函数的关键码来计算出该数据的储存位置,并吧该数据储存在该位置
查找数据
也是通过关键码计算出该数据的储存位置,在该位置找到对饮的数据
这种方法便是哈希(散列)方法,据此对应的数据结构就是哈希表(散列表),将关键码转换为特殊的数值的函数便是哈希函数
粗略的哈希表的表示
这样如果要找8这个数据,我们可以通过哈希函数计算出他对饮的下标便是0 然后直接arr[0]就得到了8 这样没有通过比较,一次变就找到了需要的数据
哈希冲突
对于俩个数据的关键字在哈希函数中计算出哈希地址可能会出现相同的值的情况,比如1 % 7 = 1 8 % 7 = 1 这种情况就叫做哈希冲突 或者是哈希碰撞,若有不同的数据通过哈希函数计算出相同的哈希地址那么就称这写数据叫做 "同义词"
避免
在哈希底层的空间大小是小于实际储存数据所需要的大小,因为要达到O(1)的访问速度,那么就需要用到数组,通过下标直接访问该位置,算出的哈希地址会是充满随机性的,数组的大小不够是经常的事,那么发生冲突就是难以避免的,而我们能做的也就是降低冲突率
减低冲突率的方法
哈希函数的设计
设计的哈希函数的定义域应该包含需要储存的所有关键码,如果散列表有m个地址,那么设计的哈希函数的定义域就该在0 ~ m-1之间
哈希函数计算出的哈希地址应该平均分布在定义域之间
哈希函数要足够简单
当然设计哈希函数是很困难的是,直接用java中给的就好
哈希函数设计的越好,他的冲突率就越低,但是不能避免哈希冲突的发生
负载因子调节
散列表的负载因子定义 a = 有效元素的个数 / 总的储存空间的大小
因为表长的定值,所以a和储存和储存的元素个数的数量成正比,而储存的元素数量越多,那么他的发生哈希冲突的概率越高,在开发地址中冲突率应该在0.7~0.8之间,超过这个区间,那么在查找过程中发生哈希冲突的概率按曲线上升,会大大减低哈希表的查询效率
要降低负载因子的大小,要么减少储存的元素个数,当然这时不行的,那么就只有增大储存空间的大小
冲突解决
解决哈希冲突的俩总常用方式,开散列和闭散列
闭散列
闭散列 也叫开放定址法,当发生哈希冲突时,若哈希表为装满,那么说明还有空位置,那么就把这个数据放到这个空位中去,那么这个空位要怎么寻找呢
线性探测
将数据从发生哈希冲突的这个位置开始,向后寻找,找到空的为位置,那么就把这个值放到这里
.
对于闭散列,在删除时不能直接吧该数据直接改变,会影响之后数据的查询,比如如果删除了2这个数据,那么在之后查找22这个数据时,会发现计算到的哈希地址为2,为2这个位置为空,那么查询就出了错误
二次探测
线性探测的缺点是会产生冲突数据的冗余,因为该数据是直接向后找到空位然后储存
为了降低线性探测的冲突数据冗余的情况,适二次探测来将冗余数据分散开来储存
那么二次探测寻找空位置的方式就是:add = (x + i^2)% m,x是计算出的哈希地址,i是1,2,3……m是数组的大小
对与闭散列最大的缺点就是空间利用率低,这也是哈希的缺陷
开散列 --哈希桶(更合理的冲突解决方法)
开散列又叫开地址法(开链法),数组中储存的是一个链表,将发生冲突的元素储存在一个链表中
这样查找时秩序要遍历这个链表就能找到数据
当表中的a达到的负载因子,那么就要对数组进行resize,扩容数组时需要遍历整个哈希表,然后吧数据放到对应的位置上
若随着数据的插入越来越多,链表上的数据多与一定的数目之后(64),那么这时的查询效率会大大减低,所以这时又会将该链表转换为红黑树(高度比较平衡的搜索树),用来增加查询的效率
开散列的实现
字段设计
add
先计算储存的有效元素个数是否小于负载因子,若不是那么就要扩容了,是的话那么直接吧该值放到对应的下标中,放的时候要对下标进行判断,若这个下标已经有数据存在,那么就见该值插入这个数据后,
poll
通过哈希函数计算除该数据对应的下标,然后判断该下标是否存在数据,若有,那么遍历这个链表来找到对应数值并删除
是否存在该数据
和删除时查找删除数据一样的
判断是否需要扩容
但达到的负载因子大小后 扩容
遍历整个哈希桶,并将每个数据放到新扩容的桶中去
哈希桶实现其实是哈希函数+数组+链表实现的,不过既然已经存在了链表,那么为什么查找效率还是O(1)呢
那是因为即使有了链表,但我们仍然可以认为链表长度时个常数,查找所需的实际可以忽略
在java中进行哈希值的计算实际上调用的时HashCode这个类,而对key的比较是调用的是equals这个方法,所以如果要将自定义类储存在HashMap或者HashSet中的话,那么需要重写equals和HashCode这俩个方法
在java类的继承中Map是没有继承与collection接口,而Set是继承了collection接口
EDN~~