0. 哈希表概述
哈希表也叫做散列表( hash 有“剁碎”的意思),hash table。
哈希表可以高效的处理数据。是根据关键码值(Key value)而直接进行访问的数据结构。
也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。
这个映射函数叫做散列函数,存放记录的数组叫做散列表。
举个例子:
存储三个数据:
(“Zhangsan” , 178);
(“Lisi”, 173);
(“Wangwu”, 175);
其处理过程类似于下图图一所示:
将 key 值传入哈希函数,经过一定的运算处理,可以得到对应 table 数组的索引值,然后将 value 存入数组。
即:Zhangsan 经过哈希函数的处理,得到索引为 11,将 Zhangsan 对应的值 178 存入数组索引为 11 的地方。...
注:哈希表除了将 value 178 存入该位置,其key也是连同一起存入的。
哈希函数的复杂度为 O(1), table 为一个数组。
添加、搜索、删除的流程都是类似的:
- 利用哈希函数生成 key 对应的 index【O(1)】
- 根据 index 操作定位数组元素【O(1)】
哈希表是【空间换时间】的典型应用。
哈希表内部的数组元素,很多地方也叫 Bucket(桶),整个数组叫 Buckets 或者 Bucket Array。
1. 哈希冲突
哈希冲突也叫做哈希碰撞。当不同的 key,经过哈希函数计算出相同的结果,即 key1 ≠ key2,hash(key1) = hash(key2)。 如下图二中,Zhangsan 和 Wangwu,两个不同的 key,但经过哈希函数计算后,得到的索引值相同。
当不同的 key,经过哈希函数计算出相同的结果时,如果直接将 key 对应的值存储在数组相同的索引位置,将会出现严重的问题。因此必须解决哈希冲突。
解决哈希冲突的常见方法:
1)开放定址法(Open Addressing)
按照一定规则向其他地址探测,直到遇到空桶。
开放寻址法:位置被占了,通过一定的规则,另外再找个位置。
可以使用线性探测法:当前位置被占用了,继续看该位置的后一个位置是否可用,针对上图的话也就是 index = 3 的位置被占用了,就看 index = 4 的位置,如果没有被占用,那就放到这里。如果 index = 4 也的位置也被占用,就继续往下找,以此类推,直到找到空位置。
当然也可以使用平方探测法。
2)再哈希法(Re-Hashing)
设计多个哈希函数
3)链地址法(Separate Chaining)
通过链表(或者红黑树)将同一 index 的元素串起来。
开放定址法是在数组上另外找个新位置。对于拉链法,还是在该索引位置,采用链表(或者红黑树)。
如下图图3,11、21、31 要放在 1 这个索引位置上,11 先来,先存储。然后 21、31 也定位到这个索引,此时采用链表将 21 和 31 存储起来。这样对于table(本质是数组)同一个索引位置对应的值是一个链表。
当然,如果链表长度达到一定阈值后,链表就会转换成树结构(如图中索引为 63 的元素值),当然如果长度小于一定阈值后,就会还原成链表。这样可以解决链表过长导致的性能问题。
JDK1.8 的哈希冲突解决方案
JDK1.8 中的哈希表是使用链表+红黑树解决哈希冲突。
默认使用单向链表将元素串起来。
每次都是从头节点开始遍历;单向链表比双向链表少一个指针,可以节省内存空间。
在添加元素时,可能会由单向链表转为红黑树来存储元素。
比如当哈希表容量 ≥ 64 且 单向链表的节点数量大于 8 时。
当红黑树节点数量少到一定程度时,又会转为单向链表。
2. 哈希函数
良好的哈希函数:让哈希值更加均匀分布 → 减少哈希冲突次数 → 提升哈希表的性能。
哈希表中哈希函数的实现步骤大概如下
- 先生成 key 的哈希值(必须是整数);
- 再让 key 的哈希值跟数组的大小进行相关运算,生成一个索引值。
/**
* 哈希函数计算出 key 对应的索引值
* 1)生成 key 的哈希值(整数)
* 2)将生成的哈希值同 数组大小进行运算,得到索引值
*/
public int hash(Object key) {
return hash_code(key) % table.length;
}
为了提高效率,可以使用 & 位运算取代 % 运算【前提:将数组的长度设计为 2 的幂(2n)】
// 使用 & 位运算取代 % 运算【前提:将数组的长度设计为 2 的幂(2n)】
public int hash(Object key) {
return hash_code(key) & table.length;
}
2.1 如何生成key的哈希值
key 的常见种类可能有:整数、浮点数、字符串、自定义对象。
不同种类的 key,哈希值的生成方式不一样,但目标是一致的:
- 尽量让每个 key 的哈希值是唯一的;
- 尽量让 key 的所有信息参与运算。
在 Java 中,HashMap 的 key 必须实现 hashCode、equals 方法,也允许 key 为 null。
接下来针对不同类型的 key,看看其 hashCode 的生成方式。
2.1.1 整数
整数值当做哈希值:
比如 10 的哈希值就是 10
public static int hashCode(int value) {
return value;
}
Integer.hashCode(10); //10
2.1.2 浮点数
将存储的二进制格式转为整数值
public static int hashCode(float value) {
return floatToIntBits(value);
}
Float.hashCode((10.01); //1092626678
2.1.3 Long 和 Double 的哈希值
java 中,哈希值是 int 类型,32位
">>>" 无符号右移运算; "^" 异或运算。
public static int hashCode(long value) {
return (int)(value ^ (value >>> 32));
}
Long.hashCode(10101); //10101
public static int hashCode(double value) {
long bits = doubleToLongBits(value);
return (int)(bits ^ (bits >>> 32));
}
Double.hashCode(100100100.01); //1369301201
2.1.4 字符串的哈希值
我们先看看整数 1024 是怎么样计算出来的?
1024 = 1 * 103 + 0 * 102 + 2 * 101 + 4 * 100
字符串是由若干个字符组成的,比如字符串 lisi,由 l、i、s、i 四个字符组成(字符的本质就是一个整数)。因此,lisi 的哈希值可以表示为 l ∗ n3 + i ∗ n2 + s ∗ n1 + i ∗ n0,等价于 [(j ∗ n + a) ∗ n + c] ∗ n + k。
在 JDK 中,乘数 n 为 31,为什么使用 31?
31 是一个奇素数,JVM会将 31 * i 优化成 (i << 5) – i
同样的思路,字符串的哈希值
String str = "lisi";
int hashCode = 0;
int len = str.length();
for (int i = 0; i < len; i++) {
char c = str.charAt(i);
// hashCode = 31 * hashCode + c;
// 31 * i 优化成 (i << 5) - i
hashCode = ((hashCode << 5) - hashCode) + c;
}
System.out.println("自定义 hashCode 计算结果:" + hashCode); //3322003
System.out.println("jdk 内部 hashCode 结果:" + str.hashCode()); //3322003
2.1.5 自定义对象的哈希值
自定义对象的哈希值,默认和其内存地址相关。如自定义 Person 类:
/**
* Person 类
*/
public class Person {
private String name; //姓名
private int age; //年龄
private float height; //身高
public Person(String name, int age, float height) {
this.name = name;
this.age = age;
this.height = height;
}
}
测试类中:
public static void main(String[] args) {
Person p1 = new Person("Zhangsan", 18, 175);
Person p2 = new Person("Zhangsan", 18, 175);
System.out.println(p1.hashCode());
System.out.println(p2.hashCode());
}
打印信息:
p1 哈希值:325040804
p2 哈希值:1173230247
如果不重写 hashCode() 方法,直接获取到的哈希值,和对象的内存相关,也就是说即便是信息完全相同,哈希值也不相同。
为了达到 person 完全相同时,哈希值也相同,则需要重写 hashCode() 方法
/**
* Person 类
*/
public class Person {
private String name; //姓名
private int age; //年龄
private float height; //身高
public Person(String name, int age, float height) {
this.name = name;
this.age = age;
this.height = height;
}
@Override
public int hashCode() {
int hashCode = (name != null ? name.hashCode() : 0);
hashCode = hashCode * 31 + Integer.hashCode(age);
hashCode = hashCode * 31 + Float.hashCode(height);
return hashCode;
}
}
public static void main(String[] args) {
Person p1 = new Person("Zhangsan", 18, 175);
Person p2 = new Person("Zhangsan", 18, 175);
System.out.println("p1 哈希值:" + p1.hashCode());
System.out.println("p2 哈希值:" + p2.hashCode());
}
打印信息:
p1 哈希值:777175490
p2 哈希值:777175490
之前提到:在 Java 中,HashMap 的 key 必须实现 hashCode、equals 方法。
那么为什么重写了 hashCode,还要实现 equals 方法呢?
不同的对象,哈希值可能相同,最终得到的索引值相同;不同的对象,哈希值可能不相同,但最终得到的索引值也可能相同。
所以,当自定义对象作为 key 时,索引值相同,需要遍历判断链表中节点存储的元素是否相同,即此时需要判断 key 是否相同(即 equals)。
如下图中,p1 和 p2 的各属性值均相同,按照我们上面重写的 hashCode 方法的规则,二者的哈希值相同,其计算的索引相同,这个时候即出现哈希冲突,当 p1 存储在索引为 1 之后,存储 p2 时,索引相同,需要比较 key,即 p1 和 p2 是否相同(equals),
重写 equals 方法
@Override
/**
* 用来比较两个 person 对象是否相等
*/
public boolean equals(Object obj) {
// 内存地址
if (this == obj) return true;
if (obj == null || obj.getClass() != getClass()) return false;
// 比较成员变量
Person person = (Person) obj;
return (person.name == null ? name == null : person.name.equals(name))
&& person.age == age
&& person.height == height;
}
如果自定义对象要作为哈希表的 key,那么根据实际需求,当相关成员值相同即判定二者相同是,则 hashCode 和 equals 只需要对相应的成员进行运算判断即可,即 hashCode 方法中使用的成员和 equals 中使用的成员,必须相同。
我们前面提到过对于 jdk1.8 在添加元素时会出现单向链表和红黑树的转换,为了简便我们直接将哈希表数组的元素存放为红黑树的实现,数据元素存放红黑树的根节点,即存储的是节点,而非整个红黑树对象。
存储节点可以简化红黑树,比如里面的size属性可以删除
针对自定义对象作为 key 的小结:
-
自定义对象作为
key,最好同时重写hashCode、equals方法。 -
equals:用以判断 2 个 key 是否为同一个 key。
- 自反性:对于任何非 null 的 x,x.equals(x)必须返回true;
- 对称性:对于任何非 null 的 x、y,如果 y.equals(x) 返回 true,x.equals(y) 必须返回 true;
- 传递性:对于任何非 null 的 x、y、z,如果 x.equals(y)、y.equals(z) 返回 true,那么x.equals(z) 必须返回 true;
- 一致性:对于任何非 null 的 x、y,只要 equals 的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y) 就会一致地返回 true,或者一致地返回 false;
- 对于任何非 null 的 x,x.equals(null) 必须返回 false。
-
hashCode :必须保证 equals 为 true 的 2 个 key 的哈希值一样。
-
反过来 hashCode 相等的 key,不一定 equals 为 true。
不重写 hashCode 方法只重写 equals 会有什么后果?
可能会导致 2 个 equals 为 true 的 key 同时存在哈希表中。
HashMap 存储的 key 不要求可比较性,所以针对 key 的 compare 不能使用之前红黑树的 compare 逻辑,所以需要重新实现 compare。
在 HashMap 的接口中,添加(put) 或者 查询 中针对 key 的查找比较,其判断逻辑大致如下:
- 比较两个 key 的 hash值大小,大于 、小于;
- 如果 hash 值相等时,判断两个 key 是否相同(equals);
- 如果 equlas 不相等,且两个 key 不为空,且可比较(实现了 Comparable)且两者是同一种类型且两者比较值不相等;
- 如果不满足3,则执行节点的左右子树进行扫描(遍历每个节点判断节点的key);
- 最后只能比较两个key的内存大小,
插入的时候,需要执行需要有第5点的处理逻辑,在查询的时候则不需要该逻辑(因为添加的时候已经给了规则,查询最后有遍历兜底)。
3. 代码
HashMap 节点类
protected static class Node<K, V> {
int hash;
K key;
V value;
boolean color = RED;
Node<K, V> left;
Node<K, V> right;
Node<K, V> parent;
public Node(K key, V value, Node<K, V> parent) {
this.key = key;
int hash = key == null ? 0 : key.hashCode();
this.hash = hash ^ (hash >>> 16);
this.value = value;
this.parent = parent;
}
public boolean hasTwoChildren() {
return left != null && right != null;
}
public boolean isLeftChild() {
return parent != null && this == parent.left;
}
public boolean isRightChild() {
return parent != null && this == parent.right;
}
public Node<K, V> sibling() {
if (isLeftChild()) {
return parent.right;
}
if (isRightChild()) {
return parent.left;
}
return null;
}
@Override
public String toString() {
return "Node_" + key + "_" + value;
}
}
是否包含 key
这个时候对 key 的比较和红黑树中的比较的区别
/**
* 是否包含 key
*/
public boolean containsKey(K key) {
return node(key) != null;
}
private Node<K, V> node(K key) {
Node<K, V> root = table[index(key)];
return root == null ? null : node(root, key);
}
/**
* 查询 key 对应的节点
* @param node table 中对应的index的红黑树的根节点
* @param k1 查询的 key
* @return
*/
private Node<K, V> node(Node<K, V> node, K k1) {
// 查询 key 的哈希值
int h1 = hash(k1);
// 存储查找结果
Node<K, V> result = null;
int cmp = 0;
while (node != null) {
K k2 = node.key;
int h2 = node.hash;
// 1.先比较哈希值
if (h1 > h2) {
node = node.right;
} else if (h1 < h2) {
node = node.left;
// 2.hash 值相等,比较equals
} else if (Objects.equals(k1, k2)) {
return node;
// 3. equlas 不相等,且两个 key 不为空,且可比较(实现了 Comparable)且两者是同一种类型且两者比较值不相等
} else if (k1 != null && k2 != null
&& k1 instanceof Comparable
&& k1.getClass() == k2.getClass()
&& (cmp = ((Comparable)k1).compareTo(k2)) != 0) {
node = cmp > 0 ? node.right : node.left;
// 4.节点的左右子树进行扫描
// 4.1 节点右子树进行扫描
} else if (node.right != null && (result = node(node.right, k1)) != null) {
return result;
// 4.2 节点左子树进行扫描
} else { // 只能往左边找
node = node.left;
}
}
return null;
}
完整的代码
/**
* HashMap 实现类
*/
public class HashMap<K, V> {
private static final boolean RED = false;
private static final boolean BLACK = true;
private int size; //哈希表所有节点值(每个索引对应的红黑树的所有节点之和)
private Node<K, V>[] table;
private static final int DEFAULT_CAPACITY = 1 << 4; //数组(table)默认容量
private static final float DEFAULT_LOAD_FACTOR = 0.75f; //装填因子
public HashMap() {
table = new Node[DEFAULT_CAPACITY];
}
public int size() {
return size;
}
/**
* 是否为空
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 清空
*/
public void clear() {
if (size == 0) return;
size = 0;
for (int i = 0; i < table.length; i++) {
table[i] = null;
}
}
/**
* 添加
*/
public V put(K key, V value) {
// 扩容判断
resize();
int index = index(key);
// 取出index位置的红黑树根节点
Node<K, V> root = table[index];
if (root == null) {
root = createNode(key, value, null);
table[index] = root;
size++;
fixAfterPut(root);
return null;
}
// 添加新的节点到红黑树上面
Node<K, V> parent = root;
Node<K, V> node = root;
int cmp = 0;
K k1 = key;
int h1 = hash(k1);
Node<K, V> result = null;
boolean searched = false; // 是否已经搜索过这个key
do {
parent = node;
K k2 = node.key;
int h2 = node.hash;
if (h1 > h2) {
cmp = 1;
} else if (h1 < h2) {
cmp = -1;
} else if (Objects.equals(k1, k2)) {
cmp = 0;
} else if (k1 != null && k2 != null
&& k1 instanceof Comparable
&& k1.getClass() == k2.getClass()
&& (cmp = ((Comparable)k1).compareTo(k2)) != 0) {
} else if (searched) { // 已经扫描了
cmp = System.identityHashCode(k1) - System.identityHashCode(k2);
} else { // searched == false; 还没有扫描,然后再根据内存地址大小决定左右
if ((node.left != null && (result = node(node.left, k1)) != null)
|| (node.right != null && (result = node(node.right, k1)) != null)) {
// 已经存在这个key
node = result;
cmp = 0;
} else { // 不存在这个key
searched = true;
cmp = System.identityHashCode(k1) - System.identityHashCode(k2);
}
}
if (cmp > 0) {
node = node.right;
} else if (cmp < 0) {
node = node.left;
} else { // 相等
V oldValue = node.value;
node.key = key;
node.value = value;
node.hash = h1;
return oldValue;
}
} while (node != null);
// 看看插入到父节点的哪个位置
Node<K, V> newNode = createNode(key, value, parent);
if (cmp > 0) {
parent.right = newNode;
} else {
parent.left = newNode;
}
size++;
// 新添加节点之后的处理
fixAfterPut(newNode);
return null;
}
public V get(K key) {
Node<K, V> node = node(key);
return node != null ? node.value : null;
}
public V remove(K key) {
return remove(node(key));
}
/**
* 是否包含 key
*/
public boolean containsKey(K key) {
return node(key) != null;
}
/**
* 是否包含 value
*/
public boolean containsValue(V value) {
if (size == 0) return false;
Queue<Node<K, V>> queue = new LinkedList<>();
for (int i = 0; i < table.length; i++) {
if (table[i] == null) continue;
queue.offer(table[i]);
while (!queue.isEmpty()) {
Node<K, V> node = queue.poll();
if (Objects.equals(value, node.value)) return true;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
return false;
}
/**
* 遍历访问所有节点
*/
public void traversal(Visitor<K, V> visitor) {
if (size == 0 || visitor == null) return;
Queue<Node<K, V>> queue = new LinkedList<>();
for (int i = 0; i < table.length; i++) {
if (table[i] == null) continue;
queue.offer(table[i]);
while (!queue.isEmpty()) {
Node<K, V> node = queue.poll();
if (visitor.visit(node.key, node.value)) return;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
}
protected Node<K, V> createNode(K key, V value, Node<K, V> parent) {
return new Node<>(key, value, parent);
}
private void resize() {
// 装填因子 <= 0.75
if (size / table.length <= DEFAULT_LOAD_FACTOR) return;
Node<K, V>[] oldTable = table;
table = new Node[oldTable.length << 1];
Queue<Node<K, V>> queue = new LinkedList<>();
for (int i = 0; i < oldTable.length; i++) {
if (oldTable[i] == null) continue;
queue.offer(oldTable[i]);
while (!queue.isEmpty()) {
Node<K, V> node = queue.poll();
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
// 挪动代码得放到最后面
moveNode(node);
}
}
}
private void moveNode(Node<K, V> newNode) {
// 重置
newNode.parent = null;
newNode.left = null;
newNode.right = null;
newNode.color = RED;
int index = index(newNode);
// 取出index位置的红黑树根节点
Node<K, V> root = table[index];
if (root == null) {
root = newNode;
table[index] = root;
fixAfterPut(root);
return;
}
// 添加新的节点到红黑树上面
Node<K, V> parent = root;
Node<K, V> node = root;
int cmp = 0;
K k1 = newNode.key;
int h1 = newNode.hash;
do {
parent = node;
K k2 = node.key;
int h2 = node.hash;
// 挪动的时候只需要判断是往左还是往右,根本不需要判断equals以及遍历(因为添加《put》的时候已经判断过了,这里只是扩容挪动而已),而且cmp也不可能等于0
if (h1 > h2) {
cmp = 1;
} else if (h1 < h2) {
cmp = -1;
} else if (k1 != null && k2 != null
&& k1 instanceof Comparable
&& k1.getClass() == k2.getClass()
&& (cmp = ((Comparable)k1).compareTo(k2)) != 0) {
} else {
cmp = System.identityHashCode(k1) - System.identityHashCode(k2);
}
if (cmp > 0) {
node = node.right;
} else if (cmp < 0) {
node = node.left;
}
} while (node != null);
// 看看插入到父节点的哪个位置
newNode.parent = parent;
if (cmp > 0) {
parent.right = newNode;
} else {
parent.left = newNode;
}
// 新添加节点之后的处理
fixAfterPut(newNode);
}
protected V remove(Node<K, V> node) {
if (node == null) return null;
Node<K, V> willNode = node;
size--;
V oldValue = node.value;
if (node.hasTwoChildren()) { // 度为2的节点
// 找到后继节点
Node<K, V> s = successor(node);
// 用后继节点的值覆盖度为2的节点的值
node.key = s.key;
node.value = s.value;
node.hash = s.hash;
// 删除后继节点
node = s;
}
// 删除node节点(node的度必然是1或者0)
Node<K, V> replacement = node.left != null ? node.left : node.right;
int index = index(node);
if (replacement != null) { // node是度为1的节点
// 更改parent
replacement.parent = node.parent;
// 更改parent的left、right的指向
if (node.parent == null) { // node是度为1的节点并且是根节点
table[index] = replacement;
} else if (node == node.parent.left) {
node.parent.left = replacement;
} else { // node == node.parent.right
node.parent.right = replacement;
}
// 删除节点之后的处理
fixAfterRemove(replacement);
} else if (node.parent == null) { // node是叶子节点并且是根节点
table[index] = null;
} else { // node是叶子节点,但不是根节点
if (node == node.parent.left) {
node.parent.left = null;
} else { // node == node.parent.right
node.parent.right = null;
}
// 删除节点之后的处理
fixAfterRemove(node);
}
// 交给子类去处理
afterRemove(willNode, node);
return oldValue;
}
private Node<K, V> successor(Node<K, V> node) {
if (node == null) return null;
// 前驱节点在左子树当中(right.left.left.left....)
Node<K, V> p = node.right;
if (p != null) {
while (p.left != null) {
p = p.left;
}
return p;
}
// 从父节点、祖父节点中寻找前驱节点
while (node.parent != null && node == node.parent.right) {
node = node.parent;
}
return node.parent;
}
private Node<K, V> node(K key) {
Node<K, V> root = table[index(key)];
return root == null ? null : node(root, key);
}
private Node<K, V> node(Node<K, V> node, K k1) {
int h1 = hash(k1);
// 存储查找结果
Node<K, V> result = null;
int cmp = 0;
while (node != null) {
K k2 = node.key;
int h2 = node.hash;
// 先比较哈希值
if (h1 > h2) {
node = node.right;
} else if (h1 < h2) {
node = node.left;
} else if (Objects.equals(k1, k2)) {
return node;
} else if (k1 != null && k2 != null
&& k1 instanceof Comparable
&& k1.getClass() == k2.getClass()
&& (cmp = ((Comparable)k1).compareTo(k2)) != 0) {
node = cmp > 0 ? node.right : node.left;
} else if (node.right != null && (result = node(node.right, k1)) != null) {
return result;
} else { // 只能往左边找
node = node.left;
}
}
return null;
}
/**
* 根据key生成对应的索引(在桶数组中的位置)
*/
private int index(K key) {
return hash(key) & (table.length - 1);
}
private int hash(K key) {
if (key == null) return 0;
int hash = key.hashCode();
return hash ^ (hash >>> 16);
}
private int index(Node<K, V> node) {
return node.hash & (table.length - 1);
}
private void fixAfterRemove(Node<K, V> node) {
// 如果删除的节点是红色
// 或者 用以取代删除节点的子节点是红色
if (isRed(node)) {
black(node);
return;
}
Node<K, V> parent = node.parent;
if (parent == null) return;
// 删除的是黑色叶子节点【下溢】
// 判断被删除的node是左还是右
boolean left = parent.left == null || node.isLeftChild();
Node<K, V> sibling = left ? parent.right : parent.left;
if (left) { // 被删除的节点在左边,兄弟节点在右边
if (isRed(sibling)) { // 兄弟节点是红色
black(sibling);
red(parent);
rotateLeft(parent);
// 更换兄弟
sibling = parent.right;
}
// 兄弟节点必然是黑色
if (isBlack(sibling.left) && isBlack(sibling.right)) {
// 兄弟节点没有1个红色子节点,父节点要向下跟兄弟节点合并
boolean parentBlack = isBlack(parent);
black(parent);
red(sibling);
if (parentBlack) {
fixAfterRemove(parent);
}
} else { // 兄弟节点至少有1个红色子节点,向兄弟节点借元素
// 兄弟节点的左边是黑色,兄弟要先旋转
if (isBlack(sibling.right)) {
rotateRight(sibling);
sibling = parent.right;
}
color(sibling, colorOf(parent));
black(sibling.right);
black(parent);
rotateLeft(parent);
}
} else { // 被删除的节点在右边,兄弟节点在左边
if (isRed(sibling)) { // 兄弟节点是红色
black(sibling);
red(parent);
rotateRight(parent);
// 更换兄弟
sibling = parent.left;
}
// 兄弟节点必然是黑色
if (isBlack(sibling.left) && isBlack(sibling.right)) {
// 兄弟节点没有1个红色子节点,父节点要向下跟兄弟节点合并
boolean parentBlack = isBlack(parent);
black(parent);
red(sibling);
if (parentBlack) {
fixAfterRemove(parent);
}
} else { // 兄弟节点至少有1个红色子节点,向兄弟节点借元素
// 兄弟节点的左边是黑色,兄弟要先旋转
if (isBlack(sibling.left)) {
rotateLeft(sibling);
sibling = parent.left;
}
color(sibling, colorOf(parent));
black(sibling.left);
black(parent);
rotateRight(parent);
}
}
}
private void fixAfterPut(Node<K, V> node) {
Node<K, V> parent = node.parent;
// 添加的是根节点 或者 上溢到达了根节点
if (parent == null) {
black(node);
return;
}
// 如果父节点是黑色,直接返回
if (isBlack(parent)) return;
// 叔父节点
Node<K, V> uncle = parent.sibling();
// 祖父节点
Node<K, V> grand = red(parent.parent);
if (isRed(uncle)) { // 叔父节点是红色【B树节点上溢】
black(parent);
black(uncle);
// 把祖父节点当做是新添加的节点
fixAfterPut(grand);
return;
}
// 叔父节点不是红色
if (parent.isLeftChild()) { // L
if (node.isLeftChild()) { // LL
black(parent);
} else { // LR
black(node);
rotateLeft(parent);
}
rotateRight(grand);
} else { // R
if (node.isLeftChild()) { // RL
black(node);
rotateRight(parent);
} else { // RR
black(parent);
}
rotateLeft(grand);
}
}
private void rotateLeft(Node<K, V> grand) {
Node<K, V> parent = grand.right;
Node<K, V> child = parent.left;
grand.right = child;
parent.left = grand;
afterRotate(grand, parent, child);
}
private void rotateRight(Node<K, V> grand) {
Node<K, V> parent = grand.left;
Node<K, V> child = parent.right;
grand.left = child;
parent.right = grand;
afterRotate(grand, parent, child);
}
private void afterRotate(Node<K, V> grand, Node<K, V> parent, Node<K, V> child) {
// 让parent称为子树的根节点
parent.parent = grand.parent;
if (grand.isLeftChild()) {
grand.parent.left = parent;
} else if (grand.isRightChild()) {
grand.parent.right = parent;
} else { // grand是root节点
table[index(grand)] = parent;
}
// 更新child的parent
if (child != null) {
child.parent = grand;
}
// 更新grand的parent
grand.parent = parent;
}
private Node<K, V> color(Node<K, V> node, boolean color) {
if (node == null) return node;
node.color = color;
return node;
}
private Node<K, V> red(Node<K, V> node) {
return color(node, RED);
}
private Node<K, V> black(Node<K, V> node) {
return color(node, BLACK);
}
private boolean colorOf(Node<K, V> node) {
return node == null ? BLACK : node.color;
}
private boolean isBlack(Node<K, V> node) {
return colorOf(node) == BLACK;
}
private boolean isRed(Node<K, V> node) {
return colorOf(node) == RED;
}
protected static class Node<K, V> {
int hash;
K key;
V value;
boolean color = RED;
Node<K, V> left;
Node<K, V> right;
Node<K, V> parent;
public Node(K key, V value, Node<K, V> parent) {
this.key = key;
int hash = key == null ? 0 : key.hashCode();
this.hash = hash ^ (hash >>> 16);
this.value = value;
this.parent = parent;
}
public boolean hasTwoChildren() {
return left != null && right != null;
}
public boolean isLeftChild() {
return parent != null && this == parent.left;
}
public boolean isRightChild() {
return parent != null && this == parent.right;
}
public Node<K, V> sibling() {
if (isLeftChild()) {
return parent.right;
}
if (isRightChild()) {
return parent.left;
}
return null;
}
}
}
4. LinkedHashMap
LinkedHashMap 在 HashMap 的基础上维护元素的添加顺序,使得遍历的结果是遵从添加顺序的。
其顺序是通过双向链表记录每个添加节点
在通过 hashcode 得到 index 添加节点的时候,链表的每个节点需要记录节点的上一个节点(pre)和下一个节点(next);
注意:双向链表是针对所有索引位置的所有红黑树
如下图6,绿色虚线即为遍历的顺序,和添加的顺序一样。
因此 LinkedHashMap 的添加元素是,需要额外补充双向链表的代码逻辑,同时在输出元素时,也要补充双向链表删除元素的逻辑
注意:删除元素时,注意其节点类需要在 HashMap 节点类(上面代码中的 Node 类)的基础上,额外新增两个成员 prev 和 next,即:
public static class LinkedNode<K, V> extends Node<K, V> {
LinkedNode<K, V> prev;
LinkedNode<K, V> next;
public LinkedNode(K key, V value, Node<K, V> parent) {
super(key, value, parent);
}
}
在 LinkedHashMap 的添加元素时,需要额外补充双向链表的代码逻辑,同时在输出元素时,也要补充双向链表删除元素的逻辑
注意:删除元素时,当删除度为2的节点node时,需要注意更换 node 与 前驱\后继节点 的连接位置。
LinkedHashMap 的代码,略...