常见集合类
1 List
常用的List包括ArrayList和LinkedList:
- ArrayList:底层采用数组实现,数据访问的复杂度为O(1),指定位置插入数据复杂度为O(n)。
- LinkedList:底层采用链表实现,数据访问的复杂度为O(n),指定位置插入数据复杂度为O(1)。
2 Map
2.1 哈希表
2.1.1 哈希表定义
哈希表本质是一个数组,数组中每一个元素称为一个箱子,箱子中存放的是键值对。
哈希表的存储过程如下:
- 根据key结合hashCode方法计算它的哈希值h
- 假设箱子的个数为n,那么这个键值对应该放在第(h%n)个箱子中
- 假设该箱子已经有了键值对,就应该用开放寻址法或拉链法(链式存储)解决冲突
哈希表的查询过程如下:
- 根据key结合hashCode方法计算它的哈希值h
- 假设箱子的个数为n,那么这个键值对位于第(h%n)个箱子中
- 对该箱子中的元素通过key值遍历查询直到获取到元素或者遍历结束返回
哈希表还有一个最重要的属性:负载因子(loadFactor),用于衡量哈希表的空满程度,计算公式为:
负载因子 = 总键值对数/箱子个数
其中,负载因子越大,意味着哈希表越满,越容易导致冲突,性能也就越低,因此,一般当负载因子大于某个常数(通常为0.75)时,哈希表会自动扩容。哈希表自动扩容时,一般会创建两倍于原来个数的箱子,因此即使key的哈希值不变,对箱子个数取余的结果也会发生改变,因此所有键值对的存放位置都有可能发生变化,这个过程称为重哈希(rehash)。
由上可知,尽管哈希表实现了数据的快速查找,但是也带来了两个严重的问题:
(1)如果哈希表中本来的箱子就比较多,扩容时需要重新哈希并移动数据,性能影响较大
(2)如果哈希表设计不合理,哈希表极端情况下会变成线性表,性能较低
因此,如果我们能够预知哈希表中的元素数目,就能通过合理的设置初始容量大小来达到尽量少的扩容的目的以提高哈希表性能。
2.2 HashMap
2.2.1 数据结构
HashMap的数据结构为数组+链表+红黑树,如下图所示:
哈希冲突的解决主要是通过使用链表和红黑树,具体的选取取决于箱子中存储的键值对数目,若数目少于8个,则只需要关注插删性能,因此采用链表,若数目不少于8个,则对查询效率影响较大,此时适合采用红黑树存储。
2.2.2 为什么8作为链表和红黑树转化的界限
首先桶中元素的数目是符合泊松分布的,其中,桶中元素数目概率如下所示:
结合上图,元素数目达到7的概率不到百万分之一。并且对于链表和红黑树的平均查找路径上,当元素数目为6时基本上一致,由于需要考虑链表到红黑树的转化时间,因此一般以7为分水岭,以8作为界限。
2.3 LinedHashMap
2.3.1 基本原理
LinkedHashMap是HashMap的子类,但是其内部还有一个双向链表维护键值对的顺序,每个键值对既位于哈希表中,也位于双向链表中,LinkedHashMap支持插入顺序、访问顺序两种顺序。其中:
- 插入顺序:先添加的在前面,后添加的在后面
- 访问顺序:最近访问的置于尾部,从未访问过的置于头部
【插入排序】:默认顺序为插入顺序,由于hashMap使用node存储,因此天生就是插入顺序,因此LinkedHashMap在使用上未重写put方法,而是重写了get方法,当用户选定根据访问顺序排序时,当前访问的元素会至于末尾。
【访问排序】:重写了get方法,当用户主动的将accessOrder置为true且get或put已存在的数据时会将该数据置于尾部。
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
/**
* 头部节点
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 尾部节点
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* 访问顺序指定: <tt>true</tt> 访问顺序排序
* <tt>false</tt> 插入顺序排序
*/
final boolean accessOrder;
Demo:
class DemoClass {
public static void main(String[] args) {
LinkedHashMap map = new LinkedHashMap(10, 0.75f, true);
map.put("hello", "hello");
map.put("hello1", "hello1");
map.put("hello2", "hello2");
map.put("hello2", "hello2");
map.put("hello", "hello");
map.put("hello1", "hello1");
System.out.println(map.values());
}
}
2.3.2 如何实现LRU
LRU(Least Recently Used,最近最少使用):当超过设定的容量时会淘汰不常使用的数据。
LinkedHashMap可实现LRU缓存的原因有两个:
- LinkedHashMap是一个Map,基于K-V结构,与缓存完全一致
- LinkedHashMap有一个boolean属性可以让用户指定是否实现LRU
实现逻辑:
class RLU<K,V> extends LinkedHashMap<K,V>{
/** 设置缓存的最大容量,超过此容量的顶部元素会被淘汰掉**/
private int maxElementNumber;
public RLU(int maxElementNumber){
super(16, 0.75f, true);
this.maxElementNumber = maxElementNumber;
}
/**
* 在LinkedHashMap添加元素后,会调用removeEldestEntry方法,若方法返回true,则头部元素被删除后再将
* 此元素添加到尾部,否则直接添加到尾部。在LinkedHashMap中的实现始终返回false,该子类重写后即可实现对
* 容量的控制。
*/
2.3 TreeMap
2.3.1 基本原理
TreeMap是基于红黑树实现的排序Map,对于增删改查的时间复杂度均为logn,相比于hashMap和LinkedHashMap这些hash表的复杂度O(1),TreeMap的增删改查的时间复杂度为logn就显得有些效率低,但是hashMap并不能保证任何顺序性,LinkedHashMap只能保证了Map的遍历顺序和put顺序一致的有序性。
2.3.2 如何有序
TreeMap默认排序规则:按照key的字典顺序来排序(升序)。
也可自定义排序规则:要实现Comparator接口。Demo如下:
public static void main(String[] args) {
TreeMap map = new TreeMap(new Comparator<String>() {
public int compare(String o1, String o2) {
return o2.length() - o1.length();
}
});
map.put("hello", "hello");
map.put("hello1", "hello1");
map.put("hello11", "hello2");
System.out.println(map.get("hello1")); // 查找复杂度:log(n)
}