1. 红黑树(R-B Tree)
- 非严格均衡的二叉搜索树。
- TreeMap和TreeSet都是基于红黑树实现的,而Jdk8中HashMap当链表长度大于8时也会转化为红黑树。
- 红黑树的特点:
- 节点分为红色或者黑色;
- 根节点必为黑色;
- 叶子节点都为黑色,且为null;
- 连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点;
- 从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点;
- 新加入到红黑树的节点为红色节点。
2. HashMap、ConcurrentHashmap、HashTable
2.1 HashMap
-
存储键值对的数据结构。
-
使用散列(hashsing)实现,在O(1)时间内查找、插入以及删除一个元素。
-
底层用数组+链表/ 红黑树实现。
-
不是线程安全的,在实现的时候没有做任何同步操作,同一时刻多个线程都可以对其进行读写,数据很可能会被破坏,所以并发会出问题。
-
元素个数大于等于 容量*负载因子(默认0.75f) 时,需要进行扩容。新建一个大小为原来2倍的数组,并重新计算元素在新数组中的位置,将原来的条目重新装载到新数组中。【称为再散列】
- 再散列非常影响性能,如果已经预知hashmap中元素的个数,那么设置hashmap的初始容量能够有效的提高hashmap的性能。
-
jdk1.7
- 数组加链表,数组的每个元素是一个链表。
当冲突严重时,链表越来越长,查询的效率越来越低,由于查询需要遍历链表,时间复杂度为O(N)。
- 数组加链表,数组的每个元素是一个链表。
-
jdk1.8
- 数组加链表/红黑树。
- 由于红黑树是一种平衡二叉搜索树,可以在O(logN)的时间内实现查找(优于链表的O(N))。
- 设定用于判断是否需要将链表转换为红黑树的阈值(8)。如果当前链表的长度大于预设的阈值,就要转换为红黑树。
- 数组加链表/红黑树。
2.2. ConcurrentHashMap
线程安全的,采用锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的table,提高并发能力,解决HashTable效率低下的问题。
jdk1.8与jdk1.7实现的比较:
- JDK1.8的实现进一步降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)。
- JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了。
- JDK1.8使用红黑树来优化链表。基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替长度在一定阈值以上的链表。
-
jdk1.7
-
由一个Segment数组和多个HashEntry组成。Segment数组的意义就是将一个大的table分割成多个小的table来分别进行加锁,也就是上面的提到的锁分离技术。而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样。
-
理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程获取锁访问一个 Segment 时,不会影响到其他的 Segment。
-
Segment 是 ConcurrentHashMap 的一个内部类,继承了ReentrantLock。
-
HashEntry和Hashmap中的类似,只是value 和指向链表下一个元素的引用变量next 用volatile 修饰,保证了获取时的可见性。
-
需要两次定位。首先通过key的hashcode定位到具体的segment;然后再一次通过key的hashcode定位到当前segment的数组中具体的HashEntry。
-
get操作不需要加锁。因为HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
-
put操作仍然需要加锁处理。因为value 是用 volatile 关键词修饰的,并不能保证并发的原子性。
- 首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put。
- 尝试自旋获取锁。如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。
- 通过key的 hashcode 将当前 Segment 中的数组定位到具体的HashEntry。
- 遍历该位置的链表,判断HashEntry的Key是否与传入的 key相等,相等则用新值覆盖旧的 value。
- 若链表为空或未找到与传入的key相同的key,则新建一个HashEntry,先判断是否需要扩容,再加到链表中。
- 最后释放当前 Segment 的锁。
-
-
jdk1.8
-
摒弃了Segment,直接用Node数组+链表/ 红黑树的数据结构来实现,使用Synchronized和CAS来实现并发控制。
-
将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。其中的 val和next 都用了 volatile 修饰,保证了可见性。
-
ConcurrentHashMap的初始化其实是一个空实现,并没有做任何事。这也是和其他的集合类有区别的地方,初始化操作并不是在构造函数实现的,而是在put操作中实现。
-
put方法
-
判断是否需要初始化
-
根据key的hashcode计算出数组中对应的下标,如果定位到的Node为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
-
判断是否需要扩容。
-
如果以上判断都不满足,也就是存在hash冲突,就要进行加锁操作。利用 synchronized 锁写入数据。【锁定的对象为定位到的Node,即链表或红黑树的头节点】
- 如果该Node是链表结构,则采用遍历链表的方式写入数据。【判断链表元素的key是否与要插入的key相同,若相同则用新值覆盖旧值;若没有找到key相同的元素,就新建一个Node插入链表尾部】
- 如果是红黑树结构,就用红黑树的方式写入数据。
-
判断链表长度是否大于等于设定的阈值,若需要即把链表转换为红黑树。
-
如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容。【helpTransfer()方法调用多个工作线程一起帮助进行扩容,这样的效率就会更高,而不是只有检查到要扩容的那个线程进行扩容操作,其他线程就要等待扩容操作完成才能工作】
-
-
get方法
- 根据key的hashcode确定对应的数组的下标,如果是首节点是就直接返回。
- 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回。
- 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null。
-
2.3 HashTable
- 实现原理与jdk1.7HashMap的实现原理是相同的。散列、数组+链表。
- 是线程安全的。它的方法使用synchronized关键字修饰,锁住整个HashTable。这就意味着所有的线程都在竞争同一把锁,在多线程的环境下,无疑是效率低下的。
2.4 HashTable与HashMap比较
HashTable | HashMap | |
---|---|---|
继承关系 | 继承Dictionary,实现Map接口 | 继承AbstractMap,实现Map接口 |
线程安全性 | 线程安全 | 线程不安全 |
key和value | 都不允许null值 | null可以作为键,这样的键只有一个,key为null的键值对永远都放在以table[0]为头结点的链表中;可以有一个或多个键所对应的值为null |
效率 | 低 | 高 |
3. 二叉堆
- 是一棵完全二叉树;
- 完全二叉树:每一层都是满的,或者最后一层不满且最后一层的叶子都是靠左放置的。
- 每个节点大于等于他的任意一个孩子。
4. 二叉树
4.1 二叉树
是一种层次结构,要么是空集,要么是由一个称为根的元素和两棵不同的二叉树组成的,分别是左子树和右子树。允许两棵子树中的一棵或两棵为空。
4.2 二叉搜索树
对于树中的每一个节点,它的左子树中节点的值都小于该节点的值,右子树中节点的值都大于该节点的值。
4.3 二分查找的判定树(Decision Tree)或比较树(Comparison Tree)
二分查找过程可用二叉树来描述:把当前查找区间的中间位置上的结点作为根,左子表和右子表中的结点分别作为根的左子树和右子树。由此得到的二叉树,称为描述二分查找的判定树(Decision Tree)或比较树(Comparison Tree)。
- 例题
4.4 哈夫曼树
4.5 线索二叉树
- 在二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化。
- 左线索指前驱,右线索指后继(前驱和后继按照具体的遍历顺序而定)
- 例题
5. 图
- 邻接矩阵:若图中有n个顶点,使用n*n的二维矩阵来表示边。若从顶点i到顶点j存在一条边,那么matrix[i][j]为1,否则为0。
- 邻接表:顶点i的邻接顶点线性表包含了所有与i有边相连的顶点。顶点i的邻接边线性表包含了所有与i有边相连的边。
- 邻接矩阵较适合存储边数多的图(稠密图),邻接表较适合存储边数少的图(稀疏图)。