目录
- 1. 二叉排序树(二叉搜索树)
- 1.1 二叉搜索树的查找
- 1.2 二叉搜索树的插入
- 1.3 二叉搜索树的删除(7种情况)
- 1.4 二叉搜索树和TreeMap、TreeSet的关系
- 2. Map和Set的区别与联系
- 2.1 从接口框架的角度分析
- 2.2 从存储的模型角度分析【2种模型】
- 3. 关于Map
- 3.1 Map的注意事项
- 3.2 TreeMap 和 HashMap的对比
- 3.3 Map.Entry<k,v>的用法 (遍历)
- 4. 关于Set
- 4.1 Set的注意事项
- 4.2 TreeSet 和 HashSet的对比
- 4.3 迭代器遍历Set
- 5. 哈希表
- 5.1 哈希函数
- 5.2 冲突
- 5.3 冲突的解决:开放地址法和链地址法
- 5.4 哈希表与Java集合类的关系
- 6. HashMap源码分析
- 6.1 成员变量
- 6.2 构造方法
- 6.3 put方法源码
1. 二叉排序树(二叉搜索树)
二叉搜索树又称二叉排序树
- 若左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若右子树不为空,则右子树上所有节点的值都大于根节点的值
- 左右子树也分别是二叉搜索树
【左小右大】
1.1 二叉搜索树的查找
【二叉搜索树的查找,一般情况下一次可以干掉很多数据】
为了保证每次可以干掉很多数据
思路:取遍历节点为cur,每次要查找的值val和cur的val进行比较,如果要查找的值比cur的val小,则去cur的左边继续查找,如果大则去cur的右边继续查找。
public boolean search(int val) {
TreeNode cur = root;
while (cur != null) {
if (cur.val == val) {
return true;
}else if (cur.val > val) {
cur = cur.left;
}else {
cur = cur.right;
}
}
return false;
}
1.2 二叉搜索树的插入
思路:整体思路与查找的思路类似,但是二叉搜索树的每次插入的节点一定是叶子节点,因此需要记录待插入节点的双亲。
当cur为空时,parent记录了待插入结点的父亲位置,再用val和parent的val比较进行插入。
public void insert (int val) {
Node node = new Node(val);
if (root == null) {
root = node;
return;
}
Node cur = root;
Node parent = root;
while (cur != null) {
if (cur.val == val) {
return;
}else if (cur.val > val) {
parent = cur;
cur = cur.left;
}else {
parent = cur;
cur = cur.right;
}
}
if (parent.val > val) {
parent.left = node;
}else {
parent.right = node;
}
}
1.3 二叉搜索树的删除(7种情况)
设待删除结点为cur,待删除结点的双亲结点为parent
整体看分以下3种大情况:
①cur的左为空
②cur的右为空
③cur的左和右都不为空
继续细分:cur是不是root;cur不是root的话,是parent的左还是parent的右
故有3 + 3 + 1 = 7种情况。
- cur.left == null
①cur是root
则 root = cur.right;
②cur不是root,cur是parent.left
则 parent.left = cur.right;
③cur不是root,cur是parent.right
则 parent.right = cur.right; - cur.right == null
①cur是root
则 root = cur.left;
②cur不是root,cur是parent.left
则 parent.left = cur.left;
③cur不是root,cur是parent.right
则 parent.right = cur.left; - cur.left != null && cur.right != null
此时需要使用替换法进行删除,即在它的右子树种寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题。1.4 二叉搜索树和TreeMap、TreeSet的关系
TreeMap和TreeSet是Java种利用搜索树实现的Map和Set,实际上用的是红黑树,红黑树是一颗近似平衡的二叉搜索树,即在二叉搜索树的基础之上+颜色以及红黑树性质。
2. Map和Set的区别与联系
2.1 从接口框架的角度分析
Map是一个独立的接口,而Set继承自Collection接口。
TreeMap 和 TreeSet 都继承了一个Sorted接口,说明 Tree某某 都是经过排序的,即 Tree某某 都是关于key有序的。
2.2 从存储的模型角度分析【2种模型】
- Map中存储的是键值对key-value
- Set中只存储了key
3. 关于Map
Map是一个接口类,该类没有继承collection,该类中存储的是<k,v>结构的键值对,并且k一定是唯一的,不能重复。
3.1 Map的注意事项
- Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap。
Map<Integer, Integer> map1 = new HashMap<>();
Map<Integer, Integer> map2 = new TreeMap<>();
- Map中存放键值对的键key是唯一的,key不可以重复,但是value可以重复。
- 在TreeMap中插入键值队的时候,key不能为空,否则会抛出NPE(空指针异常),但是value可以为空,因为TreeMap中的key是要进行比较的;反而HashMap的key和value都可以为空,因为HashMap不涉及比较。
- Map中的key可以全部分离出来,存储到Set中进行访问(因为key不能重复,set是天然的去重),Map中的value可以全部分离出来,存储到Collection的任何一个子集合中(value可能有重复)
3.2 TreeMap 和 HashMap的对比
Map底层结构 | TreeMap | HashMap |
---|---|---|
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间复杂度 | O(log2N) | O(1) |
是否有序 | 关于key有序 | 无序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找区别 | 需要进行元素比较 | 通过哈希函数计算哈希地址 |
比较与覆写 | key必须能够比较,否则会抛出类型转换异常 | 自定义类型需要覆写equals和hashcode方法 |
应用场景 | 需要key有序的场景下 | key是否有序不关心,需要更高的时间性能 |
3.3 Map.Entry<k,v>的用法 (遍历)
Set<Map.Entry <k,v>> entrySet(), 返回所有的key-value映射关系
Map<Integer, Integer> map = new TreeMap<>();
map.put(1,2);
map.put(2,3);
map.put(3,6);
//key一定是可以进行比较的
for(Map.Entry<Integer,Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
分析:
打印所有的键值对
entrySet():将Map中的键值对放在Set中返回了。
4. 关于Set
Set与Map主要的不同有2点:
①Set是继承自Collection的接口类
②Set中只存储了Key
Map不能使用迭代器遍历,但是Set可以,因为Set实现了Iterable接口
4.1 Set的注意事项
-
Set是继承自Collection的一个接口类
-
Set中只存储了key,并且要求key一定要唯一
-
Set的底层使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中
-
Set最大的功能就是对集合中的元素进行去重
-
TreeSet中不能插入null的key,但是HashSet可以
4.2 TreeSet 和 HashSet的对比
Set底层结构 | TreeSet | HashSet |
---|---|---|
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间复杂度 | O(log2N) | O(1) |
是否有序 | 关于key有序 | 不一定有序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找区别 | 按照红黑树的特性进行插入和删除 | 通过哈希函数计算哈希地址 |
比较与覆写 | key必须能够比较,否则会抛出类型转换异常 | 自定义类型需要覆写equals和hashcode方法 |
应用场景 | 需要key有序的场景下 | key是否有序不关心,需要更高的时间性能 |
4.3 迭代器遍历Set
Iterator< E> iterator() 返回迭代器,可以利用其进行遍历
Set<Integer> set = new TreeSet<>();
set.add(1);
set.add(3);
set.add(2);
set.add(8);
set.add(4);
Iterator<Integer> iterator = set.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}
运行结果:
5. 哈希表
5.1 哈希函数
- 哈希表的效率非常高,查找、删除、插入的时间复杂度都是O(1)。
- 理想的搜索方法:不经过任何比较,一次直接从表中得到想要搜索的元素,即一一映射关系。
- 哈希方法中使用的转换函数称为哈希函数,构造出来的结构称为哈希表(Hash Table)。
- 哈希函数计算出来的地址能均匀分布在整个空间中。
5.2 冲突
- 冲突: 不同关键字通过相同哈希函数计算出相同的哈希地址
- 冲突的发生是必然的,冲突是不可避免的,只能降低冲突率。
- 避免冲突:负载因子调节,其值= 填入表中的元素个数/哈希表的长度,因此可以增加哈希表的长度避免冲突。Java的系统库限制了荷载因子为0.75。
5.3 冲突的解决:开放地址法和链地址法
- 开放地址法(闭散列)
①线性探测
②二次探测
闭散列最大的缺陷:空间利用率比较低 - 链地址法(开散列) 数组+链表+红黑树
思路:首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于桶一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
注意:数组长度>=64,链表长度>=8,就会变成红黑树。
5.4 哈希表与Java集合类的关系
- HashMap和HashSet是用哈希表实现的Map和Set。
- Java会在冲突链表长度大于一定阈值后,将链表转变成红黑树(搜索树)。
- Java中计算哈希值实际上调用的是类的hashcode方法,进行key的相等性比较是调用key的equals方法。
- 所有自定义类的HashMap的key或者HashSet的值,必须重写hashcode和euqals方法,而且必须做到euqals相等的对象,hashcode一定也是一样的。
- 扩容之后要注意每个元素都要重新计算hashcode哈希值。
面试问题:
- 问:如果2个对象hashcode一样,equals一定一样吗?
答:不一定,hashcode一样只能证明我们要找的位置一样,位置一样的下面有很多值,无法确定2个对象的euqals。- 问:如果2个对象equals一样,hashcode一定一样吗?
答:一定,equals一样则hashcode一定一样。
6. HashMap源码分析
6.1 成员变量
- 默认容量:16,最大容量:2的30次方
- 默认的负载因子:0.75
- 树化的条件(数组+链表变成红黑树)
链表长度超过8,数组的容量大于等于64 - 解树的条件(红黑树退化为链表+数组)
链表的阈值为6的时候
- table数组的每一个元素是node类型的结点地址
6.2 构造方法
- 不带参数的构造方法
负载因子等于默认的负载因子0.75,没有给table数组进行初始化,意味着table数组没有分配内存,数组的大小是0。
(所以,为什么等会put元素可以put进去?----> 说明在第一次put的时候,会把数组进行初始化为默认容量16)
- 带2个参数的构造方法
一个是给定的容量参数,一个是给定的负载因子参数
如果给定的参数容量大于2的30次方,则容量为2的30次方;
如果给定的参数容量小于0或者给定的负载因子小于0那么就抛异常。
最后负载因子满足条件直接赋值,但是容量还需要经过tableSizeFor进一步筛选。
tableSizeFor里面:返回最接近目标的一个二次幂整数。
例如:
传入10:2的3次方=8,2的4次方16,因此返回16。
面试题:调用构造方法给了1000,请问最后哈希数组的长度是多少?
答:1024
HashMap的最大容量保证是2的n次方,但是如果没有传入一个2的次方怎么办?
答:HashMap会将传入的参数做校验,返回距离传参最近的一个2的n次方的值,例如传入15会初始化为16。
那么,为什么返回二次幂呢?
分析put源码。
6.3 put方法源码
①先把key给到hash函数中,hash(key),调用hash方法,将引用类型key转换成整数类型
②hash这个方法中调用了hashcode方法,如果key重写了hashcode方法则会调用自己的hashcode方法,如果没有重写,则会调用Object类的hashcode方法。
h是通过hashcode方法得到的32位的整数,h 异或上 h右移16位
为什么要右移16位?
答:为了更好的均匀分布,低16位和高16位异或,可以让结果更均匀。
③i = (n - 1) & hash 等价于 n % len,位运算一定是最快的,一定要保证n是2的次幂,这样2个公式才等价。【所以初始化的长度为2的整数次幂】