Map和Set
一.搜索树
1.概念:二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:若它的左子树不为空,则左子树上所有节点的值都小于根节点的值若它的右子树不为空,则右子树上所有节点的值都大于根节点的值它的左右子树也分别为二叉搜索树。所以堆二叉搜索树的中序遍历的结果是有序的。
2.查找:根据二叉搜索树的性质就可以理解
3.插入:只需要注意插入的数据在树中存在就不需要重复插入了。
4.1.删除:删除会比较复杂,所以分成三种情况,第一种情况就是删除的节点的左边为null,此时又可以分为三种情况,(1)如果删除的是根节点,直接就把根节点的右树的根节点作为新的根节点。(2)如果删除的不是根节点,删除的节点在根节点的左子树的根节点,那么此时直接将根节点的左子树变成此时根节点的左子树根节点的右子树节点。(3)如果删除的不是根节点,而是右子树的根节点,直接将根节点的右子树根节点变成右子树原本根节点的右子树。(图片顺序和情况顺序一致)
4.2.第二种情况,右子树是空的:又有三种情况:(1)如果删除的节点是根节点,那么直接将根节点变成左子树的根节点。(2)如果删除的节点不是根节点,此时删除的是左子树的根节点,此时直接让根节点的左子树的根节点变成原本左子树根节点的左子树。(3)如果删除的不是根节点,此时删除的是右子树的根节点,此时将根节点的右子树指向原来右子树根节点的左子树的根节点。(图片顺序和情况顺序一致)
4.3:删除的节点左右节点都不为空,这里没办法直接删除所有需要用到替换法,这里如果删除一个节点的话那么我们就需要通过找到他右子树的最小值,将最小值和需要删除的节点进行交换,此时再把交换后这个最小值的节点删除就好了。(这里也可以用从左树找最大值,也是一样的原理。)这里需要注意的是删除的情况也有两种:(1)删除的这个节点的右子树存在左子树,这个左子树无论有没有右子树都行。有右子树就让此时的右子树的根节点指向这个左子树的右子树的节点,若没有右子树就指向空。(2)当这个需要删除的节点的右子树没有左子树,只有右子树,或者右子树为空,那么此时就将需要删除的节点的父亲节点的指向指到此时删除节点的右子树。(图顺序和情况顺序一致)
4.4.代码:
二.Map和Set概念
Map和set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。Set:纯 key 模型,比如:有一个英文词典,快速查找一个单词是否在词典中快速查找某个名字在不在通讯录中 。Map:Key-Value 模型,比如:统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数>梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号
三.TreeMap和TreeSet
1.TreeMap部分方法如下,接下来进行实现:1.1.put方法设置key和value:
1.2.get方法通过key获取对应的value:
1.3.getOrDefault方法给不存在的key设置默认值:
1.4.remove方法删除key:
1.5.keySet方法返回所有的key值:(当为图二情况下,有相同的key值只会保存一个,TreeMap的底层是二叉搜索树的红黑树,那么存入的值就是key的值,那么就不会存入相同的key值,但是此时key值会被重置为最后一个)
1.6.entrySet方法获取key和value的对应关系:(两种打印方式)
1.7.Map.Entry<K,V>的两个方法,一个是getKey一个是getValue:
2.注意事项:(还有一个注意的是传入的key的值必须是能比较大小的,如果传入了自定义类型就必须写一个比较的方法,不然会报类型转换异常)3.TreeSet部分方法:
3.1.add方法的实现:(和TreeMap的put是一样的道理)
3.2.Iterator<E> iterator()的方法实现:
3.3.通过new TreeSet的对象我看到他的底层逻辑是new TreeMap对象:
3.4.注意事项:
四.哈希表
1.概念:通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系
2.哈希表一般的方法是这样的:
3.哈希冲突: 例子:2.图中4 % 10之后存在4下标,但是当key等于14的时候模10也是4,此时就是两个不同的key得到了一个下标值,这就是哈希冲突。
4.解决哈希冲突(任何哈希函数都会冲突,只能减低冲突率):第一种是设置合理的哈希函数,第二种是较低负载因子,第三种是闭散列,第四种是开散列。
4.1.1直接定制法--(常用):取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况
4.1.2.除留余数法--(常用):设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址 。
4.1.3.平方取中法--(了解):假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况 。
4.1.4.折叠法--(了解) :折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况 。
4.1.5.随机数法--(了解) :选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。通常应用于关键字长度不等时采用此法。
4.1.6.数学分析法--(了解) :设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。
4.2.1.负载因子:
4.2.2.负载因子和冲突率的关系:(负载因子一般在0.75进行扩容)
4.3.1.闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。 此时找下一个空位置的方法就有两种,第一种是线性探测,第二种是二次探测。
4.3.2.线性探测:插入14 和 44的元素的时候发生了冲突,44就从14的下标的后一个位置寻找空位置,找到空位置就插入44,这样插入数据容易造成冲突的数据集中在一起。
4.3.3.二次探测:二次探测是通过图二中的公式来进行计算的,当第一个冲突元素出现i就等于1,第二个元素就是i=2,以此类推,并切图二公式的加号可以变成减号。
4.4.1.开散列:开散列法又叫链地址法(开链法)也可以称为哈希桶,首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中(简单的说就是数组 + 链表(而链表可以变成树或者又是一个哈希表)):
5.哈希函数设置原则:
5.1. 哈希函数应该尽可能简单。
5.2. 哈希函数的值域必须在哈希表格的范围之内。
5.3. 哈希函数的值域应该尽可能均匀分布,即取每个位置应该是等概率的。
五.哈希桶的实现
- 1.前期工作:
- 2.put方法的实现:思路:通过给的key来用除留余数法进行计算下标,算出这个节点所在的下标位置之后,遍历这个数组下标的该节点判断是否存在这个节点,如果存在这个key值的节点,更新val值并且退出,如果没有就头插入这个节点。头插入之后要给usedSize++不能忘记了,然后判断是否需要扩容也就是通过重载因子的大小来判断是否超过0.75来计算的。
- 3.扩容方法的实现:思路:先创建一个原数组而被大小的数组,在不同长度的数组中需要重新哈希,因为当4和14在10和20长度下的数组存储的下标位置是不同的。重新哈希需要注意的是,我们通过遍历原数组的下标,拿到当前下标的节点,通过哈希之后先保存当前节点在原数组中的下一个节点的位置,再进行新数组中的头插,不然在新数组中进行了头插就找不到原来数组中下一个节点的位置了。直到把原数组中这个下标的所有节点全部遍历完再进行下一个坐标的节点的哈希。
- 4.获取key的val:
- 5.将类型改成泛型类的哈希桶:需要注意的是,引用类型比较是否相等的时候要用equals,而不是直接用==来进行判断是否相等!!!!
- 6.与java类集的关系:
六.OJ练习题
- 1.只出现一次的数字:(. - 力扣(LeetCode))
- 1.1.思路:最好的方法是直接全部异或起来是最快的,这里是为了练习Map和Set的方法才使用Set的方法来解决问题。思路:通过Set只有一个key值,将Set中未出现过的数据存入Set中,若Set中存在这个数据就直接删除,最后进行完Set中只会剩下出现过一次的数据,最后再遍历数组,找到出现过一次的数据并且返回,如果没有出现过一次的数据,就直接返回-1.
- 2.复制带随机指针的链表:(138. 随机链表的复制 - 力扣(LeetCode))
- 2.1.思路:通过哈希map的key和val的映射关系,保存新老节点的地址,这里需要遍历两次数据,第一次是为了保存新老节点的地址到map中,第二次循环数据是为了新节点能找到下一个节点的地址和随机节点的地址,这样才算完成了深拷贝。(不能用TreeMap,因为TreeMap的底层是搜索树,那么就会进行数据的比较,而我们这里并没进行数据的比较,那么就无法使用TreeMap来进行保存新老节点的地址。)
- 3.宝石与石头:(771. 宝石与石头 - 力扣(LeetCode))
- 3.1.思路:练习使用HashSet,将宝石的字符保存在Set中,然后遍历石头的字符串中是否存在宝石的字符,存在就直接拿计数器++。
- 4.坏键盘的打字:(旧键盘 (20)__牛客网 (nowcoder.com))
- 4.1.思路:先把每个字符变成大写,再把坏掉之后的输入字符串保存到set1中,再去和正确输入进行比对,然后再把打印过的坏键记录到set2中,这样就能打印出大写不重复的坏键盘的地方了。
- 5.前K个高频单词:(692. 前K个高频单词 - 力扣(LeetCode))
- 5.1:先解决第一个问题,给10个数据怎么找到第一个重复的数据:
- 5.2:第二个问题,给10个数据怎么去重?
- 5.3:第三个问题,给10个数据,重复数据出现的次数。
- 5.4.思路:这道题需要我记录每个单词出现重复次数,然后通过top-k问题来解决。其中需要注意的地方是,我们需要自己写一个比较器来比较哪个数据,我们是通过比较每个单词出现的频率来比较的。这里求的是前k个频率出现最多的,那么就是建立小根堆。这里还需要注意的是,当我们在自己建立比较器的时候需要注意,当堆中没有放满k个数据的时候且两个英文单词的频率一样,我们就按大根堆的形式插入,这样方便后面数据的统一调整,也就是按照英文词典的顺序来调整,这样就不会让一些数据丢失。最后的数据是需要逆置的,因为在把数据poll出来的时候是从最小的开始删除再传入List中,题中要求由高到低就需要用工具类接口来进行逆置。
七.Tree和Hash差别
- 1.Map差别:
- 2.Set差别: