目录
1,搜索树
1.1 概念
1.2 查找
1.3 插入
1.4 删除(难点)
1.5 性能分析
1.6 和 java 类集的关系
2,搜索
2.1 概念及场景
2.2 模型
3,Map 的使用
3.1 关于Map的说明
3.2 关于Map.Entry的说明
3.3 Map的常用方法说明
4,Set 的说明
4.1 常见方法说明
5,哈希表
5.1 概念
5.2 冲突-概念
5.3 冲突-避免
5.4 冲突-避免-哈希函数设计
5.5 冲突-避免-负载因子调节(重点掌握)
5.6 冲突-解决
5.7 冲突-解决-闭散列
5.8 冲突-解决-开散列/哈希桶(重点掌握)
5.9 冲突严重时的解决办法
5.10 实现
5.11 使用泛型实现(传入指定类型)
5.12 性能分析
5.12 和 java 类集的关系
6,OJ练习
6.1 只出现一次的数字
6.2 随机链表的复制 (百度面试)
6.3 宝石与石头
6.4 坏键盘打字
6.5 前K个高频单词
1,搜索树
1.1 概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
为什么又称二叉排序树呢,因为对棵树进行中序遍历,其最终的结果是有序的
1.2 查找
虽然还没有实现一颗二叉搜索树,但是不影响我们写查找。
若没找到可进行异常处理,此代码没找到会出现空指针异常
1.3 插入
1.4 删除(难点)
1.5 性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
1.6 和 java 类集的关系
TreeMap 和 TreeSet 即 java 中利用搜索树实现的 Map 和 Set;实际上用的是红黑树,而红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础之上 + 颜色以及红黑树性质验证,关于红黑树的内容后序再进行讲解。
2,搜索
2.1 概念及场景
Map和set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。以前常见的搜索方式有:
1. 直接遍历,时间复杂度为O(N),元素如果比较多效率会非常慢
2. 二分查找,时间复杂度为O(log以2为底的N),但搜索前必须要求序列是有序的
上述查找比较适合静态类型的查找,即一般不会对区间进行插入和删除操作了,而现实中的查找比如:
1. 根据姓名查询考试成绩
2. 通讯录,即根据姓名查询联系方式
3. 不重复集合,即需要先搜索关键字是否已经在集合中
可能在查找时进行一些插入和删除的操作,即动态查找,那上述两种方式就不太适合了,本节介绍的Map和Set是一种适合动态查找的集合容器。
2.2 模型
一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以模型会有两种:
1. 纯 key 模型(TreeSet),比如:
有一个英文词典,快速查找一个单词是否在词典中
快速查找某个名字在不在通讯录中
2. Key-Value 模型(TreeMap),比如:
统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数>
梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号
而Map中存储的就是key-value的键值对,Set中只存储了Key。
3,Map 的使用
3.1 关于Map的说明
Map是一个接口类,该类没有继承自Collection,该类中存储的是结构的键值对,并且K一定是唯一的,不能重复。
3.2 关于Map.Entry的说明
Map.Entry是Map内部实现的用来存放键值对映射关系的内部类,该内部类中主要提供了 的获取,value的设置以及Key的比较方式。
3.3 Map的常用方法说明
1. put(K key,V value)
2. get(Object key) 通过key获取val
如果获取key没有对应的val,返回null
3. getOrDefault(object K,V DefaultValue) 如果获取key没有对应的val,给一个默认值
4. remove(Obejct key) 删除key对应的val
5,keySet() 将所有不重复的key放到Set集合
结论:当你在map当中存储元素的时候,key是不能有重复的,如果key重复了将会更新val值
6. entrySet() 返回所有key,value
注意:
1. Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap
2. Map中存放键值对的Key是唯一的,value是可以重复的(就是根据key确定当前元素的位置,不能重复)
3. 在TreeMap中插入键值对时,key不能为空,否则就会抛NullPointerException异常,value可以为空。但是HashMap的key和value都可以为空。
4. Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
5. Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
6. Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。
7. TreeMap和HashMap的区别【HashMap在课件最后会讲到】
8. 往TreeMap里面存储元素时,当前这个key值一定是可比较的,否则会报类型转换异常。
4,Set 的说明
Set与Map主要的不同有两点:Set是继承自Collection的接口类,Set中只存储了Key。
4.1 常见方法说明
1. add() Set当中不能存储重复的元素
2. iterator() 使用迭代器打印
......这些方法可以自己使用使用
我们来查看TreeSet的源码:
再来查看add的源码:
注意:
1. Set是继承自Collection的一个接口类
2. Set中只存储了key,并且要求key一定要唯一
3. TreeSet的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的
4. Set最大的功能就是对集合中的元素进行去重
5. 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序。
6. Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入
7. TreeSet中不能插入null的key,HashSet可以。
8. TreeSet和HashSet的区别【HashSet在课件最后会讲到】
5,哈希表
5.1 概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(log2为底的N ),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快 找到该元素。
当向该结构中:
插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
问题:按照上述哈希方式,向集合中插入元素14,会出现什么问题?
5.2 冲突-概念
对于两个数据元素的关键字,即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
5.3 冲突-避免
首先,我们需要明确一点,由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率。
比如我的数组容量是10,有可能我存储的关键字有12个(这是可以存下来的,下面讲到)。
比如容量为10,存两个元素,这两个元素都可能在同一位置,所以冲突是必然的,只能避免冲突
5.4 冲突-避免-哈希函数设计
设计合理的hash函数:
哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1 之间
哈希函数计算出来的地址能均匀分布在整个空间中
哈希函数应该比较简单
常见哈希函数:
1. 直接定制法--(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况使用场景:适合查找比较小且连续的情况
面试题:给定一个字符串 s
,找到它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1
。
2. 除留余数法--(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数: Hash(key) = key% p(p<=m),将关键码转换成哈希地址
5.5 冲突-避免-负载因子调节(重点掌握)
较低的负载因子:
所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。
已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。
5.6 冲突-解决
解决哈希冲突两种常见的方法是:闭散列和开散列
5.7 冲突-解决-闭散列
闭散列:也叫开放地址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
1. 线性探测
比如上面的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他 元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标 记的伪删除法来删除一个元素。
2. 二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:,或者:Hi。其中:i = 1,2,3…, H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置, m是表的大小。
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
5.8 冲突-解决-开散列/哈希桶(重点掌握)
java当中的HashMap也是以这样的方式来解决问题的
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。
5.9 冲突严重时的解决办法
刚才我们提到了,哈希桶其实可以看作将大集合的搜索问题转化为小集合的搜索问题了,那如果冲突严重,就意味着小集合的搜索性能其实也时不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如:
1. 每个桶的背后是另一个哈希表
2. 每个桶的背后是一棵搜索树
5.10 实现
实现的是一个简单哈希桶。
首先是一个数组,数组里面放的是头节点的地址
1. put()
2. get(int key) 通过key获取val
5.11 使用泛型实现(传入指定类型)
首先我们写一个学生类,我们认为学号一样就是同一个学生,但是呢实际上是不一样的
刚刚上面的代码,如果我们的key不是整型,而是一个引用类型,比如是一个Student,此时代码就执行不了,我们需要把一个引用对象转变为整数,我们可以通过hashcode方法来转变,上面我们认为同一个学号就是同一个学生,那么得到的hashcode整数也应该是一样的,即通过key去%数组长度得到的index位置也是一样的,也就会哈希到同一位置
但是当我运行的时候,得到的这两个整数并不一样,意味着说虽然学号一样,还是不认为你是同一个学生
那么在我们的HashMap来说,要获取key来%数组长度,你的对象肯定做不到,我们期望通过hashcode得到的这两个整数应该是同一个整数,但是不是,那我们起码还是要根据对象的学号生成这个整数,这时候我们需要重写equals和hashcode,重写equals返回true,false证明他是不是同一个人,重写hashcode返回是整数,如果返回的整数是一样的情况下,得到index的值就是一样的
当我们重写这两个方法以后,运行时这两个hashcode值就一样了,所以说我们以后自定义的类型,一定要重写hashcode和equals方法,甚至于说这个类还要实现Comparable接口,让他可比较
这个时候我们就知道了生成的hashcode值是一样的,将来我们key去%数组长度时就好实现了
所以,解决了这个问题,我们就可以使用泛型来实现了!!!
5.12 性能分析
虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的, 也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是 O(1) 。
5.12 和 java 类集的关系
1. HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set
2. java 中使用的是哈希桶方式解决冲突的
3. java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
4. java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。
所以HashSet的底层也是HashMap
6,OJ练习
6.1 只出现一次的数字
给你一个 非空 整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
但是对于此代码来说,还是使用异或时间快
6.2 随机链表的复制 (百度面试)
注意:这里不能使用TreeMap,因为他是一颗二叉搜索树,map.put的时候会根据key进行比较,这里没有说你的Node根据什么比较,且没有实现Comparable,所以在用TreeMap的时候,要看看你的key能不能进行比较,不能比较使用HashMap较好!!!
6.3 宝石与石头
6.4 坏键盘打字
旧键盘上坏了几个键,于是在敲一段文字的时候,对应的字符就不会出现。现在给出应该输入的一段文字、以及实际被输入的文字,请你列出肯定坏掉的那些键。
练习:为6.5做准备
6.5 前K个高频单词
代码:
此时的代码有一点问题:
我们确实是处理过,但是我们处理的是放满k个的时候,那没有放满k个呢?
所以,我们又要讨论一下!!!
代码: