HashMap
AVL树:
在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。
特点:
1.本身首先是一棵二叉搜索树。
2.带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。
也就是说,AVL树,本质上是带了平衡功能的二叉查找树(二叉排序树,二叉搜索树)。
树的左旋:绕着子结点逆时针转动,如下图:
树的右旋:绕着子结点顺时针转动,如下图
红黑树:
是一种自平衡二叉查找树,是一种特化的AVL树,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。
若一棵二叉查找树是红黑树,则它的任一子树必为红黑树。红黑树是一种平衡二叉查找树的变体,它的左右子树高差有可能大于 1,所以红黑树不是严格意义上的平衡二叉树(AVL),但 对之进行平衡的代价较低, 其平均统计性能要强于 AVL 。
红黑树的实际应用非常广泛,比如Linux内核中的完全公平调度器、高精度计时器、ext3文件系统等等,各种语言的函数库如Java的TreeMap和TreeSet。
红黑树的定义如下:
1.每个结点是红的或者黑的
2.根结点是黑的
3.每个叶结点是黑的(每个结点有默认的黑色的NULL结点)
4.如果一个结点是红的,则它的两个儿子都是黑的(父子结点之间不能出现两个连续的红结点)
5.对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点(黑色平衡)
在算法导论中,新插入的结点优先默认是红色的,优先满足第五条定义,再满足其他定义。
案例
1.假设现在插入一个根结点(10)
1 | 步骤: |
2.再插入一个结点(20)
1 | 步骤: |
这是一颗正常的红黑树。因为每个节点,它的末尾节点都有隐藏的黑色空节点,所以满足第三条定义。
再插入一个结点(30)
1 | 步骤: |
再插入一个结点(40)
1 | 步骤: |
再插入两个结点(5)(25)
1 | 步骤: |
再插入一个结点(50)
1 | 步骤: |
再插入两个结点(35)(60)
1 | 步骤: |
1 | 步骤: |
再插入个结点(6)
1 | 步骤: |
1 | 步骤: |
总结
当插入一个新节点(红色)时:
父节点是黑色的,不用进行调整
父节点是红色的,并且
1.叔叔是空的,旋转+变色
2.叔叔是红色,父节点+叔叔节点变黑色,祖父节点变红色
3.叔叔是黑色,旋转+变色
源码
树化条件
1 | //当链表长度达到阈值8时,转换成红黑树 |
红黑树存储结构-TreeNode
1 | //红黑树存储结构对象 |
所以说,上文TreeNode
实际上是Node
的子类,它有着两个属性,一个next
,一个prev
,也就是说,TreeNode
还是一个双向链表。
添加元素
1 | public V put(K key, V value) { |
1 | static final int hash(Object key) { |
由于JDK1.8的HashMap有了红黑树的保障,所以相对于计算Hash值没有JDK1.7那么复杂。
1 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent, |
链表树化
数组扩容或构建双向链表
在JDK1.7中,采用头插法插入到链表的头部,而在JDK1.8中,采用的是尾插法插入到链表中,并且当链表的数量大于8时,也就是添加第九个元素时,会调用树化方法treeifyBin
,根据条件将链表转换成红黑树。
1 | //链表树化红黑树 |
1 | //把Node类型转换成TreeNode类型 |
真正树化
接下来是真正的将链表树化的逻辑方法:
1 | final void treeify(Node<K,V>[] tab) { |
根节点移动到双向链表头部
1 | static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) { |
红黑树添加新节点
1 | final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, |
1 | /* |
1 | //左旋 |
完成左旋之后进行变色,变完色之后右旋
1 | //通过调用 root = rotateLeft(root, xpp); 所以参数 p = xpp |
扩容
再看一下扩容方法:
1 | final Node<K,V>[] resize() { |
链表扩容
链表转移,使用尾插法,链表元素的hash值&旧数组长度,等于0的元素转移到lo链表、其余的元素转移到hi链表,最终newTab[j] = loHead、newTab[j + oldCap] = hiHead完成链表扩容
红黑树扩容
遍历数组,获取数组上的红黑树root节点进行操作,树节点(treeNode)存在双向链表结构,使用next指针如链表转移一般进行操作,构建高低位链表,记录高位、低位节点个数,头部节点放入数组索引位置
a、节点个数如果小于6则结构改回单向链表(TreeNode->Node)
b、大于6个,如果高低位链表仅存在一个,不需要额外操作,因为整棵树已经迁移;如果两个都存在,重新进行树化
c、tab[index] = loHead,tab[index + bit] = hiHead
1 | final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { |
1 | //遍历双向链表,把TreeNode类型转换成Node类型,建立单向链表,返回头结点 |
get
方法
1 | public V get(Object key) { |
1 | final Node<K,V> getNode(int hash, Object key) { |
1 | final TreeNode<K,V> getTreeNode(int h, Object k) { |
1 | /* |
ConcurrentHashMap
1 | //表示当前的整个ConcurrentHashMap正在扩容 |
红黑树结构对象-TreeBin
1 | //把整个红黑树结构封在TreeBin对象中 |
ConcurrentHashMap
的数组中将存储Node
类型和TreeBin
类型,TreeBin
用于承载红黑树的整个结构,其中有一个root
属性存储红黑树的根节点。
这样做的目的是为了后面对红黑树根节点加锁时,直接对TreeBin
对象加锁,可以不用考虑在加锁的过程中,红黑树的根节点变化情况。
添加元素
1 | public V put(K key, V value) { |
1 | final V putVal(K key, V value, boolean onlyIfAbsent) { |
初始化
1 | private final Node<K,V>[] initTable() { |
树化
1 | private final void treeifyBin(Node<K,V>[] tab, int index) { |
计数
1 | //用于累加计数元素数量 |
1 | //x 表示需要增加的值 |
1 | //x:累加值 wasUncontended: false 表示已经进行过CAS操作并且失败了 |
1 | //用该对象占用在数组上,表示这个位置正在扩容 |
1 | //在putval中,有一个分值逻辑代码是这样的,表示如果当前的f.hash == MOVED时,则进行帮助扩容逻辑 |
1 | //多线程进行扩容操作 |
1 | //tab 原数组 , nextTab 新数组 |
分析完添加元素和累加计数的分治方法之后,再看看统计元素大小的size
方法:
1 | public int size() { |
1 | //baseCount + CounterCell[] 的数据,累加起来就是总的Size |
“本篇文章主要摘自参考资料”