数据结构 Map和Set
文章目录
- 📕1. 二叉搜索树
- ✏️1.1 查找操作
- ✏️1.2 插入操作
- ✏️1.3 删除操作
- 📕2. Map的使用
- ✏️2.1 Map的常用方法
- ✏️2.2 TreeMap和HashMap的区别
- ✏️2.3 HashMap的底层实现
- 📕3. Set的使用
- ✏️3.1 Set的常用方法
- ✏️3.2 TreeSet和HashSet的区别
- 📕4. 哈希表
- ✏️4.1 哈希表冲突
- ✏️4.2 避免哈希表冲突
- ✏️4.3 解决哈希表冲突
- 📏4.3.1 开放地址法
- 📏4.3.2 拉链法
📕1. 二叉搜索树
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
✏️1.1 查找操作
//搜索public TreeNode search(int value){TreeNode cur = root;while(cur!=null){if(value<cur.value){cur = cur.left;}else if(value>cur.value){cur = cur.right;} else if (value == cur.value) {return cur;}}return null;}
✏️1.2 插入操作
//插入public boolean insert(int value){TreeNode treeNode = new TreeNode(value);if (root==null){root = treeNode;return true;}TreeNode cur = root;TreeNode parent = null;while (cur!=null){if (value>cur.value){parent = cur;cur = cur.right;} else if (value<cur.value) {parent = cur;cur = cur.left;}else if (value==cur.value){return false;}}//整体思路是先找到要插入节点的父节点,比父节点的值大就插入到父节点的右边,反之插入到父节点的左边if (value>parent.value){parent.right = treeNode;}else {parent.left = treeNode;}return true;}
✏️1.3 删除操作
待补充!!!
📕2. Map的使用
Map是⼀个接⼝,可以有HashMap实现或者TreeMap实现,该类没有继承⾃Collection,该类中存储的是<K,V>结构的键值对,并且K⼀定是唯⼀的,不能重复。
Map.Entry<K, V> 是Map内部实现的⽤来存放<key, value>键值对映射关系的内部类,该内部类中主要提供了<key, value>的获取,value的设置以及Key的⽐较⽅式。Map.Entry<K,V>并没有提供设置Key的⽅法
✏️2.1 Map的常用方法
💡💡💡注意:
- Map是⼀个接⼝,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap
- Map中存放键值对的Key是唯⼀的,value是可以重复的
- TreeMap中插⼊键值对时,key不能为空,否则就会抛NullPointerException异常,value可以为空。但是HashMap的key和value都可以为空。
- Map中的Key可以全部分离出来,存储到Set中来进⾏访问(因为Key不能重复)
- Map中的value可以全部分离出来,存储在Collection的任何⼀个⼦集合中(value可能有重复)
- Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然
后再来进⾏重新插⼊
✏️2.2 TreeMap和HashMap的区别
✏️2.3 HashMap的底层实现
- JDK1.8 之前
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。
- JDK1.8 之后
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树。这样做的目的是减少搜索时间:链表的查询效率为 O(n)(n 是链表的长度),红黑树是一种自平衡二叉搜索树,其查询效率为 O(log n)。当链表较短时,O(n) 和 O(log n) 的性能差异不明显。但当链表变长时,查询性能会显著下降。
为什么优先扩容而非直接转为红黑树?
数组扩容能减少哈希冲突的发生概率(即将元素重新分散到新的、更大的数组中),这在多数情况下比直接转换为红黑树更高效。红黑树需要保持自平衡,维护成本较高。并且,过早引入红黑树反而会增加复杂度。
为什么选择阈值 8 和 64?
- 泊松分布表明,链表长度达到 8 的概率极低(小于千万分之一)。在绝大多数情况下,链表长度都不会超过 8。阈值设置为 8,可以保证性能和空间效率的平衡。
- 数组长度阈值 64 同样是经过实践验证的经验值。在小数组中扩容成本低,优先扩容可以避免过早引入红黑树。数组大小达到 64 时,冲突概率较高,此时红黑树的性能优势开始显现。
📕3. Set的使用
Set与Map主要的不同有两点:Set是继承⾃Collection的接⼝类,Set中只存储了Key
✏️3.1 Set的常用方法
💡💡💡注意:
- Set是继承⾃Collection的⼀个接⼝
- Set中只存储了key,并且要求key⼀定要唯一
- TreeSet的底层是使⽤Map来实现的,其使⽤key与Object的⼀个默认对象作为键值对插⼊到Map中的
- Set最⼤的功能就是对集合中的元素进⾏去重
- 实现Set接⼝的常⽤类有TreeSet和HashSet
- Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插⼊
- reeSet中不能插⼊null的key,HashSet可以
✏️3.2 TreeSet和HashSet的区别
📕4. 哈希表
哈希表(Hash table),又称为散列表,是一种根据关键码值(Key value)直接进行访问的数据结构。它通过将关键码值映射到表中的一个位置来访问记录,从而加快查找的速度。这个映射函数称为散列函数,存放记录的数组称为散列表。
例如:
数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的⼤⼩
⽤该⽅法进⾏搜索不必进⾏多次关键码的⽐较,因此搜索的速度⽐较快
✏️4.1 哈希表冲突
对于两个数据元素的关键字 ki 和 kj(i != j),有 i != j ,但有:Hash( ki ) == Hash( kj ),
即:不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
把具有不同关键码⽽具有相同哈希地址的数据元素称为“同义词”。
✏️4.2 避免哈希表冲突
⾸先,我们需要明确⼀点,由于我们哈希表底层数组的容量往往是⼩于实际要存储的关键字的数量的,这就导致⼀个问题,冲突的发⽣是必然的,但我们能做的应该是尽量的降低冲突率。
- 🌈设计哈希函数
引起哈希冲突的⼀个原因可能是:哈希函数设计不够合理。常见的哈希函数有:
- 直接定制法(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B优点:简单、均匀缺点:需
要事先知道关键字的分布情况使⽤场景:适合查找⽐较⼩且连续的情况 - 除留余数法(常用)
设散列表中允许的地址数为m,取⼀个不⼤于m,但最接近或者等于m的质数p作为除数,按
照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址 - 平方取中法(了解)
- 折叠法(了解)
- 随机数法(了解)
- 数学分析法(了解)
- 🌈调节负载因子
所以当冲突率达到⼀个⽆法忍受的程度时,我们需要通过降低负载因⼦来变相的降低冲突率。
已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的⼤⼩。
✏️4.3 解决哈希表冲突
解决哈希冲突两种常⻅的⽅法是:开放地址法和拉链法
📏4.3.1 开放地址法
开放地址法 :当发⽣哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下⼀个” 空位置中去。那如何寻找下⼀个空位置呢?
- 🌈线性探测
⽐如上⾯的场景,现在需要插⼊元素44,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发⽣哈希冲突。
线性探测:从发⽣冲突的位置开始,依次向后探测,直到寻找到下⼀个空位置为⽌。
采⽤闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。⽐如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采⽤标记的伪删除法来删除⼀个元素。
- 🌈二次探测
线性探测的缺陷是产⽣冲突的数据堆积在⼀块,这与其找下⼀个空位置有关系,因为找空位置的⽅式就是挨着往后逐个去找,因此⼆次探测为了避免该问题,找下⼀个空位置的⽅法为: Hi = ( H0 +(-) i^2 )% m ),其中:i = 1,2,3…, 是通过散列函数Hash(x)对元素的关键码key 进⾏计算得到的位置,m是表的⼤⼩。对于2.1中如果要插⼊44,产⽣冲突,使⽤解决后的情况为:
📏4.3.2 拉链法
开散列法⼜叫链地址法(开链法),⾸先对关键码集合⽤散列函数计算散列地址,具有相同地址的关键码归于同⼀⼦集合,每⼀个⼦集合称为⼀个桶,各个桶中的元素通过⼀个单链表链接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发⽣哈希冲突的元素。
拉链法,可以认为是把⼀个在⼤集合中的搜索问题转化为在⼩集合中做搜索了。