数据结构
文章目录
- 数据结构
- 顺序表和链表的区别
- HashMap 和 Hashtable 的区别
- Java中用过哪些集合,说说底层实现,使用过哪些安全的集合类
- Java中线程安全的基本数据结构有哪些
- ArrayList、Vector和LinkedList有什么共同点与区别?
- ArrayList和LinkedList的区别
- HashMap、HashTable、CurcurrentHashMap
- HashMap的底层结构
- 初始化
- HashMap的put方法
- HashMap为什么是线程不安全的
- 解决HashMap线程不安全的方法
- JDK1.7中的不安全
- HashMap扩容机制1.7和1.8
- HashMap触发扩容条件
- 扩容
- 为什么链表转化为红黑树的阈值为8
- 为什么8的时候要转换为红黑树
- 为什么小于6又变回了链表
- 从红黑树中查找数据可以用递归的方式
- 各个常见的数据结构的插入和查询时间复杂度
- 头插法和尾插法区别?
- 如何决定使用HashMap还是TreeMap?
- Collection和Collections有什么区别?
- HashTable和HashSet、HashMap的区别
- HashSet底层
- HashMap传入1000条数据,如何设定初始值
- ConcurrentHashMap在get和put的时候需要加锁吗
- concurrentHashMap的锁机制/ConcurrentHashMap怎么解决线程安全
- ConcurrentHashMap在JDK1.8中为什么要使用内置锁Synchronized来替换ReentractLock重入锁?
- ConcurrentHashMap的并发度是什么?
- ConcurrentHashMap中的key和value可以为null吗?为什么?
- ConcurrentHashMap怎么解决线程安全
- CopyOnWriteArrayList了解过吗?
- 对CAS的理解
- ABA问题
- 循环时间长开销大
- CAS只能对单个变量有效
- 为什么使用迭代器
- 迭代器如何使用
- Map list set常见方法
- 迭代器如何使用
- 二叉树
- **什么是红黑树?**
- TreeMap(红黑树)
- 平衡二叉树
顺序表和链表的区别
顺序表:顺序表的特点是逻辑上相邻的数据元素,物理存储位置也相邻,并且顺序表的存储空间需要预先分配。
优点:
- 实现简单
- 不用为表示节点间的逻辑关系而增加额外的存储开销。
- 可以按照元素序号进行随机访问
缺点:
- 插入、删除时,平均移动表中的一半元素,因此n较大的顺序表效率低。
- 静态分配,程序执行之前必须明确规定存储规模,预先分配足够大的存储空间,估计过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。
链表:在链表中逻辑上相邻的数据元素,物理存储位置不一定相邻,它使用指针实现元素之间的逻辑关系。并且,链表的存储空间是动态分配的。
优点:
- 插入和删除更容易实现
缺点:
- 要占用额外的存储空间存储元素之间的关系
- 不支持随机访问
HashMap 和 Hashtable 的区别
- 线程是否安全:
HashMap
是非线程安全的,Hashtable
是线程安全的,因为Hashtable
内部的方法基本都经过synchronized
修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap
吧!); - 效率: 因为线程安全的问题,
HashMap
要比Hashtable
效率高一点。另外,Hashtable
基本被淘汰,不要在代码中使用它; - 对 Null key 和 Null value 的支持:
HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出NullPointerException
。 - 初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,
Hashtable
默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而HashMap
会将其扩充为 2 的幂次方大小(HashMap
中的tableSizeFor()
方法保证,下面给出了源代码)。也就是说HashMap
总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 - 底层数据结构: JDK1.8 以后的
HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
Java中用过哪些集合,说说底层实现,使用过哪些安全的集合类
HashMap:底层数组+链表+红黑树构成,当数组的容量大于64,链表大于8,由链表转换为红黑树,红黑树小于6时转化为链表,扩容是以2次幂进行扩容。
LinkedList:底层为双向链表,相比ArrayList,插入和删除的效率更高
ArrayList:底层为数组,相比LinkedList,支持随机访问
queue:单端队列 ;Deque:双端队列
HashSet:基于 HashMap
实现的,底层采用 HashMap
来保存元素
PriorityQueue: Object[]
数组来实现二叉堆
ArrayQueue::Object[]
数组 + 双指针
线程安全的集合:
ConcurrentHashMap:底层采用数组+红黑树+链表实现,采用 CAS 和 synchronized
来保证并发安全,synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
Java中线程安全的基本数据结构有哪些
HashTable: 哈希表的线程安全版,效率低
ConcurrentHashMap:哈希表的线程安全版,效率高,用于替代HashTable
Vector:线程安全版Arraylist
Stack:线程安全版栈
BlockingQueue及其子类:线程安全版队列
ArrayList、Vector和LinkedList有什么共同点与区别?
- ArrayList、Vector和LinkedList都是可伸缩的数组,即可以动态改变长度的数组。
- ArrayList和Vector都是基于存储元素的Object[] array来实现的,它们会在内存中开辟一块连续的空间来存储,支持下标、索引访问。但在涉及插入元素时可能需要移动容器中的元素,插入效率较低。当存储元素超过容器的初始化容量大小,ArrayList与Vector均会进行扩容。
- Vector是线程安全的,其大部分方法是直接或间接同步的。ArrayList不是线程安全的,其方法不具有同步性质。LinkedList也不是线程安全的。
- LinkedList采用双向列表实现,对数据索引需要从头开始遍历,因此随机访问效率较低,但在插入元素的时候不需要对数据进行移动,插入效率较高。
ArrayList和LinkedList的区别
- ArrayList是底层数组,LinkedList采用的是双向链表
- ArrayList支持随机访问,LinkedList需要从头到尾进行遍历查找
- ArrayList在插入元素是可能需要移动容器中的元素,插入效率低,当插入元素超过容器容量时会进行扩容
- LinkedList在插入元素时不需要对数据进行移动,插入效率比较高
HashMap、HashTable、CurcurrentHashMap
Map是一个接口,我们常用的实现类有HashMap、LinkedHashMap、TreeMap,HashTable。HashMap根据key的hashCode值来保存value,需要注意的是,HashMap不保证遍历的顺序和插入的顺序是一致的。HashMap允许有一条记录的key为null,但是对值是否为null不做要求。HashTable类是线程安全的,它使用synchronize来做线程安全,全局只有一把锁,在线程竞争比较激烈的情况下hashtable的效率是比较低下的。因为当一个线程访问hashtable的同步方法时,其他线程再次尝试访问的时候,会进入阻塞或者轮询状态,比如当线程1使用put进行元素添加的时候,线程2不但不能使用put来添加元素,而且不能使用get获取元素。所以,竞争会越来越激烈。相比之下,ConcurrentHashMap使用了分段锁技术来提高了并发度,不在同一段的数据互相不影响,多个线程对多个不同的段的操作是不会相互影响的。每个段使用一把锁。所以在需要线程安全的业务场景下,推荐使用ConcurrentHashMap,而HashTable不建议在新的代码中使用,如果需要线程安全,则使用ConcurrentHashMap,否则使用HashMap就足够了。
HashMap的底层结构
JDK1.8 之前是数组+链表,JDK1.8 之后是数组+链表+红黑树,当数组的容量大于64,链表大于8,由链表转换为红黑树,红黑树小于6时转化为链表,扩容是以2次幂进行扩容。
初始化
Node[] table 的初始化长度 length(默认值是 16);
Load factor 为负载因子(默认值是 0.75),在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多
说明:默认的负载因子 0.75 是对空间和时间效率的一个平衡选择。除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子 Load factor 的值,来控制每个数组下所挂的链表或红黑树长度减小; 相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子 loadFactor 的值,这个值可以大于 1。
threshold 是 HashMap 所能容纳的最大数据量的 Node(键值对)个数。threshold= length * Loadfactor。
HashMap的put方法
首先判断数组是否为空,如果为空的进行第一次扩容,通过hash算法,计算键值对在数组中的索引,如果当前位置为空,则直接插入数据,如果当前位置不为空,首先判断key是否存在,如果存在则直接覆盖value,如果不存在采用拉链法,放在链表的末尾,添加完成后,会判断size是否大于threshold,是否需要扩容,若扩容的话,数组大小为之前的2倍大小,扩容完成后,将原数组上的节点移动到新数组上。
HashMap为什么是线程不安全的
put的时候导致的多线程数据不一致。(JDK1.8)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; // tab-哈希数组, p-桶的首节点, n-数组长度, i-数组的下标
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; // HashMap使用的懒加载,在第一次put的时候,初始化容量和临界值
if ((p = tab[i = (n - 1) & hash]) == null) // 如果第i个桶为空,创建新的节点。
tab[i] = newNode(hash, key, value, null);// 这里寻找桶的位置,使用(n - 1) & hash,可以解释为什么扩容长度都是2的n次幂,2的n次幂减1后在和hash位运算,能更好的散列,避免哈希冲突
else { // 如果第i个桶不为空,即发生了哈希碰撞
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 插入节点的key、hash都和p相同,e = p,表示为首节点
else if (p instanceof TreeNode) // 不是首节点,判断p是否为红黑树的节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 在红黑树添加
else { // 链表节点
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);// 在链表尾部插入节点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);//如果链表长度大于8,转换红黑树
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break; // 判断链表上是否有重复的key,如果有,跳出当前循环
p = e;
}
}
if (e != null) { // 在桶中找到了key、hash值与插入元素相同的节点
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value; // 覆盖value
afterNodeAccess(e);
return oldValue;
}
}
++modCount; // 结构修改计数器+1
if (++size > threshold)
resize(); // 实际容量大于当前阈值,扩容
afterNodeInsertion(evict);
return null;
}
假设两个线程A
和B
都在进行put
操作,并且hash
函数计算出的插入下标是相同的,当线程A
执行完第6
行代码后被挂起。线程B
在该下标插入了元素。然后线程A
被唤醒,由于之前已经执行了hash
碰撞判断,此时不会再判断,而是直接插入,这就导致了线程B
插入的数据被线程A
覆盖了,所以线程不安全
解决HashMap线程不安全的方法
可以采用加锁的机制
可以采用concurrentHashMap、Hashtable类这种线程安全的实现类
使用Collections将HashMap包装成线程安全的Map。
JDK1.7中的不安全
HashMap的
扩容操作中,重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。
上面JDK1.7出现的问题,在JDK1.8中已经得到了很好的解决,因为JDK1.8直接在resize函数中完成了数据迁移。另外说一句,JDK1.8在进行元素插入时使用的是尾插法。
(7条消息) HashMap为什么是线程不安全的_逍遥大俠的博客-CSDN博客
HashMap扩容机制1.7和1.8
HashMap触发扩容条件
- hashMap默认的负载因子是0.75,即如果hashmap中的元素个数超过了总容量75%,则会触发扩容
- 如果某个桶中的链表长度大于等于8了,则会判断当前的hashmap的容量是否大于64,如果小于64,则会进行扩容;如果大于64,则将链表转为红黑树。
扩容
无论是JDK7还是JDK8,HashMap的扩容都是每次扩容为原来的两倍,即会产生一个新的数组newtable,我们需要把原来数组中的元素全部放到新的只不过元素求桶的位置的方法不太一样。
JDK1.7采用头插法,JDK1.8采用尾插法,JDK1.7 中 rehash 的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是 JDK1.8 不会倒置。
因此在 JDK1.8 在扩充 HashMap 的时候,不需要像 1.7 的实现那样重新计算hash,只需要看看原来的hash 值新增的那个bit 是1 还是0 就好了(hash&oldCap),是 0 的话索引没变,是 1 的话索引变成“原索引+oldCap”。
(7条消息) 史上最详细的 JDK 1.8 HashMap 源码解析_程序员囧辉的博客-CSDN博客_hashmap jdk1.8
为什么链表转化为红黑树的阈值为8
HashMap源码里面写了在理想情况下,桶中的节点数概率(链表长度)符合泊松分布,当桶中节点数(链表长度)为 8 的时候,出现的概率不足千万分之一,为了在极端的情况下保证性能。
为什么8的时候要转换为红黑树
链表长度超过 8 就转为红黑树的设计,更多的是为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低, 而此时转为红黑树更多的是一种保底策略,用来保证极端情况下查询的效率。并且出现8概率小于千万分之一概率,也就是长度为 8 的概率,把长度 8 作为转化的默认阈值。
为什么小于6又变回了链表
因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
从红黑树中查找数据可以用递归的方式
各个常见的数据结构的插入和查询时间复杂度
当HashMap出现Hash冲突的时候,插入和查找与其链表有关,可能为O(N);
头插法和尾插法区别?
头插法会形成循环链表
如何决定使用HashMap还是TreeMap?
如果经常需要对数据进行插入、删除等操作的就是用HashMap,如果需要自定义排序的就是用TreeMap
Collection和Collections有什么区别?
- Collection是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如List、Set等。
- Collections是一个包装类,包含了很多静态方法、不能被实例化,而是作为工具类使用,比如提供的排序方法: Collections.sort(list);提供的反转方法:Collections.reverse(list)。
HashTable和HashSet、HashMap的区别
Hashtable是线程安全的,不允许存入null值
HashMap是非线程安全的,用来存储键值对,允许null值
Hashset是非线程安全的,他存储的就是map的key值,不允许出现重复元素,是根据hashcode和equals来进行判断是否重复的
HashSet底层
Hashset 底层就是用 hashmap 实现的,set 的值就是 map 的 key,value 是一个不变的值 PRESENT
当你把对象加入 HashSet 时, HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同, HashSet 就不会让加入操作成功。
HashMap传入1000条数据,如何设定初始值
在进行HashMap初始化时,传递进来的 initialCapacity
(初始容量) 虽然经过 tableSizeFor()
方法调整后,直接赋值给 threshold
,但是它实际是 table
的尺寸,并且最终会通过 loadFactor
(默认为0.75)重新调整 threshold
。
虽然 HashMap
初始容量指定为 1000
,会被 tableSizeFor()
调整为 1024
,但是它只是表示 table
数组为 1024
,扩容的重要依据扩容阈值会在 resize()
中调整为 768(1024 * 0.75)
。
那构造时传多少才能让HashMap存1000条数据不需要动态扩容呢?
x * 0.75 > 1000, x = 1333.3,但是x会被tableSizeFor
动态的调整为2048
tableSizeFor()
这个方法返回大于输入参数且最接近的2的整数次幂的数
,则我们构造时传入1024~2048
之间的数,就会保证HashMap存1000条数据不需要动态扩容
。
ConcurrentHashMap在get和put的时候需要加锁吗
get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。
数组用volatile修饰主要是保证在数组扩容的时候保证可见性
put操作,存储对象时,将key和vaule传给put()方法:
- 如果没有初始化,就调用initTable()方法对数组进行初始化;
- 如果没有hash冲突则直接通过CAS进行无锁插入;
- 如果需要扩容,就先进行扩容,扩容为原来的两倍;
- 如果存在hash冲突,就通过加锁的方式进行插入,从而保证线程安全。(如果是链表就按照尾插法插入,如果是红黑树就按照红黑树的数据结构进行插入);
- 如果达到链表转红黑树条件,就将链表转为红黑树;
- 如果插入成功就调用addCount()方法进行计数并且检查是否需要扩容;
注意:在并发情况下ConcurrentHashMap会调用多个工作线程一起帮助扩容,这样效率会更高。
put的时候如果没有Hash冲突则采用CAS的方式无锁加入, 如果出现了hash冲突则采用加锁的方式进行插入
concurrentHashMap的锁机制/ConcurrentHashMap怎么解决线程安全
ConcurrentHashMap在JDK1.8中采用Node+CAS+Synchronized实现线程安全,取消了segment分段锁,直接使用Table数组存储键值对(与1.8中的HashMap一样),主要是使用Synchronized+CAS的方法来进行并发控制。在put()的时候如果CAS失败就说明存在竞争,会进行自旋
ConcurrentHashMap在JDK1.8中为什么要使用内置锁Synchronized来替换ReentractLock重入锁?
- 锁粒度降低了;
- 官方对synchronized进行了优化和升级,使得synchronized不那么“重”了;
- 在大数据量的操作下,对基于API的ReentractLock进行操作会有更大的内存开销;
ConcurrentHashMap的并发度是什么?
程序在运行时能够同时更新ConcurrentHashMap且不产生锁竞争的最大线程数默认是16,这个值可以在构造函数中设置。如果自己设置了并发度,ConcurrentHashMap会使用大于等于该值的最小的2的幂指数作为实际并发度,也就是比如你设置的值是17,那么实际并发度是32。
ConcurrentHashMap中的key和value可以为null吗?为什么?
不可以,因为源码中是这样判断的,进行put()操作的时候如果key为null或者value为null,会抛出NullPointerException空指针异常。
ConcurrentHashMap怎么解决线程安全
底层基于CAS + synchronized实现,所有操作都是线程安全的,允许多个线程同时进行put、remove等操作
在JDK1.8中使用了数组+红黑树+链表的方式实现,每一次put操作的时候都会对头节点进行加锁,这样降低了锁粒度提升了效率。
CopyOnWriteArrayList了解过吗?
CopyOnWriteArrayList是Java并发包里提供的并发类,简单来说它就是一个**线程安全且读操作无锁的ArrayList。**正如其名字一样,在写操作时会复制一份新的List,在新的List上完成写操作,然后再将原引用指向新的List。这样就保证了写操作的线程安全。
对CAS的理解
对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
ABA问题
因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
解决方案CAS类似于乐观锁,即每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。因此解决方案也可以跟乐观锁一样:
-
使用版本号机制,如手动增加版本号字段
-
Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前的标志是否等于预期标志,如果全部相等,则以原子方式将该应用和该标志的值设置为给定的更新值。
循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
可以使用破坏for循环的方式,当达到一定次数或者超过一定的时间之后就退出for循环
CAS只能对单个变量有效
- 可以加锁来解决。
- 封装成对象类解决。
为什么使用迭代器
优点:
1.可以不了解集合内部的数据结构,就可以直接遍历
2.不暴露内部的数据,可以直接外部遍历;
3.适用性强,基本上的集合都能使用迭代器;
迭代器如何使用
Collection集合元素的通用获取方式: 在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来;继续再判断,如果还有就再取出来。一直到把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
boolean | hasNext() | 判断集合中还有没有可以被取出的元素,如果有返回true |
---|---|---|
E | next() | 取出集合中的下一个元素 |
Map list set常见方法
map: put、clear、containsKey、containsValue、Set<Map.Entry<K,V>> entrySet()【map集合转换为set集合】、equals、get、hashCode、isEmpty、Set keySet()【取出map中所有的key】、remove、size、Collection values()【把value转换为一个collection集合】、
list:get、add、remove、clear、contains、equals、hascode、indexof【返回列表中首次出现指定元素的索引】、isEmpty、lastIndexOf、set、ListIterator listIterator() 【迭代器】、toArray、size、subLsit
set:add、remove、contains、toArray
List转换为数组:
- 使用for循环的方式
- 使用toArray()
数组转换为List:
-
for循环
-
使用asList()
ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arrays));
-
使用Collections.addAll()
List<String> list2 = new ArrayList<String>(arrays.length); Collections.addAll(list2, arrays);
迭代器如何使用
Collection集合元素的通用获取方式: 在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来;继续再判断,如果还有就再取出来。一直到把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
boolean | hasNext() | 判断集合中还有没有可以被取出的元素,如果有返回true |
---|---|---|
E | next() | 取出集合中的下一个元素 |
二叉树
二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。
什么是红黑树?
红黑树是一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是 Red 或 Black。
红黑树的特性:
1)每个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每个叶结点,即空结点是黑的。
4)如果一个结点是红的,那么它的孩子都是黑的。
5)对每个结点,从该结点到叶子结点的所有路径上包含相同数目的黑结点
TreeMap(红黑树)
TreeMap 是一个能比较元素大小的 Map 集合,会对传入的 key 进行了大小排序。其中,可以使用元素
的自然顺序,也可以使用集合中自定义的比较器来进行排序。
特点
不允许出现重复的 key;
可以插入 null 键,null 值;
可以对元素进行排序;
无序集合(插入和遍历顺序不一致)。
自然排序:TreeMap 的所有 key 必须实现 Comparable 接口,所有的 key 都是同一个类的对象
定制排序:创建 TreeMap 对象传入了一个 Comparator 对象,该对象负责对TreeMap 中所有的 key 进行排序,采用定制排序不要求 Map 的 key 实现Comparable 接口.
平衡二叉树
平衡二叉树本质上是特殊的二叉搜索树(二叉排序树),它具有二叉搜索树所有的特点,此外它有自己的特别的性质,如下:
(1)它是一棵空树或它的左右两个子树的高度差的绝对值不超过1;
(2)平衡二叉树的左右两个子树都是一棵平衡二叉树。