Set源码解析(红黑树)
之前粗略看了一下List和Map,今咱来聊一下Set。
主要看以下几个:
(1)HashSet
(2)Collections.synchronizedSet
(3)LinkedHashSet
(4)CopyOnWriteArraySet
(5)TreeSet
一、HashSet
HashSet是日常搬砖中最常用的,如果有了解过HashMap的实现,那么HashSet你也就理解了。
(1)HashSet底层就是用HashMap实现的,也就是非线程安全。
private transient HashMap<E,Object> map; //构造一个初始容量16,加载因子是0.75的HashMap public HashSet() { map = new HashMap<>(); }
既然是用HashMap实现的,那么问题来了,Set存的只是一个对象,而HashMap存的是<K,V>键值对,怎么用HashMap作为Set的底层呢?
(2)新增元素
private static final Object PRESENT = new Object(); public boolean add(E e) { return map.put(e, PRESENT)==null; }
可以看到,HashSet只使用了HashMap的key,而值PRESENT是一个对象常量。这也是为什么Set里面的元素都是唯一的。
返回是map.put(e, PRESENT)==null表达式,HashMap在新增值的时候如果已有则返回旧值,反之,返回null,所以这也是Set判断重复添加值的时候是否添加成功。
二、Collections.synchronizedSet
这是用Collections工具类直接生成的,可以知道其是线程安全的
(1)底层使用synchronizedSet实现
public static <T> Set<T> synchronizedSet(Set<T> s) { return new SynchronizedSet<>(s); }
而synchronizedSet又是继承自SynchronizedCollection,其方法都是使用synchronized关键字实现线程安全的。
static class SynchronizedSet<E> extends SynchronizedCollection<E> implements Set<E> { private static final long serialVersionUID = 487447009682186044L; SynchronizedSet(Set<E> s) { super(s); } SynchronizedSet(Set<E> s, Object mutex) { super(s, mutex); } public boolean equals(Object o) { if (this == o) return true; synchronized (mutex) {return c.equals(o);} } public int hashCode() { synchronized (mutex) {return c.hashCode();} } }
三、LinkedHashSet
LinkedHashSet是HashSet的子类,但底层跟HashSet有些区别。
(1)默认构造
public LinkedHashSet() { super(16, .75f, true); }
点进super,可以看到底层是初始容量为16,加载因子为0.75的双向链表
HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
具体LinkedHashMap如何实现可看上一篇Map源码解析。
四、CopyOnWriteArraySet
这个类名字比较牛掰,写时复制Set,也是JUC包中多线程安全的类。
(1)底层结构
public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList<E>(); }
其底层是用CopyOnWriteArrayList实现的。
关于CopyOnWriteArrayList是如何实现的,可具体查看List源码解析。
五、TreeSet
TreeSet的底层是TreeMap,而TreeMap底层数据结构是红黑树。
public TreeSet() { this(new TreeMap<E,Object>()); }
在Map源码解析那一篇并没有说TreeMap,那么今天刚好看一下红黑树。
TreeMap的节点结构如下:
class Entry<K,V> implements Map.Entry<K,V> { K key; V value; Entry<K,V> left; Entry<K,V> right; Entry<K,V> parent; boolean color = BLACK; }
红黑树其实就是平衡二叉树,其还有五点定义:
(1)节点是黑色或者红色。
(2)根节点是黑色。
(3)每个红色节点的两个子节点都是黑色节点。
(4)每个叶子的节点都是黑色的空节点(NULL)。
(5)从任意节点到其每个叶子节点的所有路径都包含相同数量的黑色节点。
下图就是一个红黑树
当插入和删除节点的时候,就会破坏红黑树的平衡,此时需要对节点进行变色和旋转以达到平衡。
比如我插入一个节点21 。
可以看到,此时破坏了规则3(每个红色节点的两个子节点都是黑色节点),
此时我们可以进行变色(此时只关注根节点的右子树)
(1)将22变成黑色
此时变色之后破坏了规则5(从任意节点到其每个叶子节点的所有路径都包含相同数量的黑色节点。),比如17->15只有1个黑色节点,17->21有两个黑色节点。
(2)将25变成红色
此时违反了规则3(每个红色节点的两个子节点都是黑色节点),需要把17和27都变成黑色
(3)将17和27变成黑色
可以看到,此时根节点的右子树已经符合其他规则了,但不是平衡树,接下来看下整棵树
可以看到,整棵树是违反规则5的(从任意节点到其每个叶子节点的所有路径都包含相同数量的黑色节点。),比如13->6有2个黑节点,而13->21有3个,所以这是需要进行左旋转。
(4)左旋转,(以13为点,将右子树向上拨,将17作为根节点,将17原有的左子树15节点作为13节点的右子节点)
此时左右子树违反了规则5的(从任意节点到其每个叶子节点的所有路径都包含相同数量的黑色节点。),比如17->7有3个黑色节点,但17->21才2个黑色节点,此时将13进行变色
(5)13变色,13的子节点相应进行变色。
但此时对左子树来说还是不符合规则5(从任意节点到其每个叶子节点的所有路径都包含相同数量的黑色节点。),比如13->1有1个黑色节点,13->6有两个,此时需要右旋转
(6)右旋转(以8为轴点,将13往下拨,变为8的右节点,8原有的右节点11变成13的左节点)
右旋转之后左子树是不符合规则5的(从任意节点到其每个叶子节点的所有路径都包含相同数量的黑色节点。),所以进行将8进行变色:
变色之后的树是符合红黑树的规则了。
以上就是红黑树增加节点的时候,结构重新恢复平衡的步骤。(插入节点默认是红色的)。
红黑树在增删的时候,需要先进行节点变色,如果变色已经解决不了了,就相应进行旋转,旋转之后如果不成功,再重复上述步骤。
上述就是Set源码解析了,顺带也重新了解了一个红黑树。
=======================================================
我是Liusy,一个喜欢健身的程序员。
提前祝大家伙中秋国庆快乐!!!
欢迎关注微信公众号【Liusy01】,一起交流Java技术及健身,获取更多干货。