由于TreeMap其内部使用红黑树算法的实现,小菜能力不足,看完红黑树的我,脑子里一片浆糊,只蹦出了三个字:什么鬼!对此不做深度解释。待日后能力有所提高之时,再来更新此贴。但对于排序二叉树部分还是比较好理解的,所以就讲讲排序二叉树那部分。
TreeMap简单介绍
1.TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
2.TreeMap的基本操作 containsKey、get、put和 remove 的时间复杂度是 log(n) 。
3.TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。
在学习了之前的TreeSet之后,再来学习TreeSet的使用应该会容易很多,因为TreeSet其底层还是依靠着TreeMap。作为TreeMap的key,应遵守与TreeSet一样的规则,自定义的元素需实现Comparable接口,或者在其构造函数传入Comparator(比较器)。对于两者的使用,详细参见:Java基础-集合框架之Set
TreeMap的Entry
public static void main(String[] args) {
TreeMap<String, Integer> treeMap = new TreeMap<>();
treeMap.put("abc", 3);//自动装箱
treeMap.put("bcd", 4);
treeMap.put("cde", 6);
treeMap.put("aab", 1);
treeMap.put("cac", 5);
treeMap.put("cde", 10);
treeMap.put("abb", 2);
Set<Entry<String, Integer>> entrySet = treeMap.entrySet();
for (Entry<String, Integer> entry : entrySet) {
System.out.print(entry.getKey() + "=" + entry.getValue() + " ");
}
}
//outPut:aab=1 abb=2 abc=3 bcd=4 cac=5 cde=10
看其结果,由于未对TreeMap传入比较器,所以会根据key进行自然排序(String 类实现了Comparable接口)。简单说可以说TreeMap只是比HashMap多了个排序功能。
看其源码(JDK1.8):
//键值对实体,用于存储键值对基本信息
static final class Entry<K,V> implements Map.Entry<K,V> {
K key; //key
V value; //value
Entry<K,V> left; //左孩子
Entry<K,V> right; //右孩子
Entry<K,V> parent; //父结点
boolean color = BLACK; //红黑树的节点表示颜色的属性
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
//此处省略部分源码
}
TreeMap的部分构造及其成员属性
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
// 用于保持顺序的比较器,如果为空的话使用自然顺保持Key的顺序
private final Comparator<? super K> comparator;
// 根节点
private transient Entry<K,V> root;
// 元素个数,即树中的结点数量
private transient int size = 0;
public TreeMap() {//空参构造,表明使用自然顺序维持节点的顺序
comparator = null;//此时key必须实现Comparable接口,否则会报错
}
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;//提供指定比较器
}
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;//表明使用自然顺序维持节点的顺序
putAll(m);//将传入的map集合中的元素添加到treeMap中
}
}
简单谈谈TreeMap的实现
前面说了treeMap其底层使用的是红黑树,在此之前,我们先来了解下什么是 排序二叉树。
首先二叉树,顾名思义,每个结点其最多只有两个子结点,看上面的Entry也可知,其只有左孩子和右孩子两个结点
排序二叉树
排序二叉树是一种特殊结构的二叉树,可以非常方便地对树中所有节点进行排序和检索。
排序二叉树要么是一棵空二叉树,要么是具有下列性质:
1.若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值。
2.若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值。
排序二叉树的添加结点:
以根节点当前节点开始搜索,拿被添加的节点的值和当前节点的值比较。
如果被添加的节点的值更小,则以当前节点的左子节点作为新的当前节点。
如果被添加的节点的值更大,则以当前节点的右子节点作为新的当前节点。
重复上述两个步骤,直到新的当前节点为空,则此地方就是添加节点的地方。
排序二叉树的检索结点:
以根节点当前节点开始检索,拿被检索的节点的值和当前节点的值比较。
如果被检索的节点的值更小,则以当前节点的左子节点作为新的当前节点。
如果被检索的节点的值更大,则以当前节点的右子节点作为新的当前节点。
重复上述两个步骤,直到被检索的节点的值和当前节点的值相等,如果找不到返回null。
打住!!讲了一大串的排序二叉树,和红黑树有什么关系?
排序二叉树虽然可以快速检索,但在最坏的情况下:如果插入的节点集本身就是有序的,要么是由小 到大排列,要么是由大到小排列,那么最后得到的排序二叉树将变成链表:所有节点只有左节点(如果插 入节点集本身是大到小排列);或所有节点只有右节点(如果插入节点集本身是小到大排列)。在这种情 况下,排序二叉树就变成了普通链表,其检索效率就会很差。
为了改变排序二叉树存在的不足,Rudolf Bayer 与 1972 年发明了另一种改进后的排序二叉树:红黑树
红黑树在原有的排序二叉树增加了如下几个要求:
性质 1:每个节点要么是红色,要么是黑色。
性质 2:根节点永远是黑色的。
性质 3:所有的叶节点都是空节点(即 null),并且是黑色的。
性质 4:每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点。
性质 5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
后面对于红黑树的维护和调整不在做其深入解释了,以免误导大家
源码的简单分析(JDK1.8)
public V put(K key, V value) {
Entry<K,V> t = root;//根节点
if (t == null) {//如果根节点为空
//这里看其注释可得检查其类型和是否为空,说明当你添加null时会抛异常
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);//创建一个结点使其成为根节点
size = 1;
modCount++;
return null;
}
int cmp;//比较结果变量
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {//如果cpr不为空,那么说明在构造函数传入了比较器
do {
parent = t;//首先parent为根节点,而后循环指向其子结点
cmp = cpr.compare(key, t.key);//获得比较结果
if (cmp < 0)//cmp小于0,说明key应当放置在其节点的左边
t = t.left;//t指向其左孩子继续循环
else if (cmp > 0)//cmp打于0,说明key应当放置在其节点的右边
t = t.right;//t指向其右孩子继续循环
else//相等说明重复了,将其值替换,并直接返回
return t.setValue(value);
} while (t != null);
}
else {//否则使用自然排序
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;//向上转型
do {//以下比较程序和上述相同,不再概述
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);//运行到此说明未发现重复结点则创建一个新结点
if (cmp < 0)//根据最后的一次比较结果判断其为parent的左孩子还是右孩子
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);//红黑树部分,进行调整平衡
size++;//结点个数+1
modCount++;//修改+1
return null;
}
红黑树部分由于代码越整理发现脑子越乱,就不做过多的说明了。
剩余部分可以参考下:chenssy的博客-TreeMap 很详细,大神就是不一样。
综合小习
斗地主模拟扑克洗牌发牌,发完牌后对其进行整理并打印每个人手中的牌
public static void main(String[] args) {
//1,买一副扑克,其实就是自己创建一个集合对象,将扑克牌存储进去
String[] num = {"3","4","5","6","7","8","9","10","J","Q","K","A","2"};
String[] color = {"方片","梅花","红桃","黑桃"};
HashMap<Integer, String> hashMap = new HashMap<>();
ArrayList<Integer> list = new ArrayList<>();
//拼接扑克牌并索引和扑克牌存储在hashMap中,list为扑克集合(其存放索引,根据hashMap可得其扑克牌信息)
int count = 1;
for (String nu : num) {
for (String co : color) {//这里添加的顺序不可乱,因为后面抓牌需整理,所以这里要从小到大添加
hashMap.put(count, co.concat(nu));
list.add(count++);
}
}
//添加大小王
hashMap.put(count, "小王");
list.add(count++);
hashMap.put(count, "大王");
list.add(count);
Collections.shuffle(list);//洗牌,打乱索引
//三个玩家和底牌.这里使用TreeSet是为了使索引有序(即整理抓到的扑克牌)
TreeSet<Integer> xiaoMing = new TreeSet<>();
TreeSet<Integer> xiaoHong = new TreeSet<>();
TreeSet<Integer> xiaoBai = new TreeSet<>();
TreeSet<Integer> dipai = new TreeSet<>();
//模拟发牌
for(int i = 0 ;i < list.size() ;i++) {
if(i >= list.size() - 3) {//三张底牌
dipai.add(list.get(i));
} else if(i % 3 == 0) {//小明的牌
xiaoMing.add(list.get(i));
} else if (i % 3 == 1) {//小红的牌
xiaoHong.add(list.get(i));
} else {//小白的牌
xiaoBai.add(list.get(i));
}
}
show(hashMap, xiaoMing, "小明");
show(hashMap, xiaoHong, "小红");
show(hashMap, xiaoBai, "小白");
show(hashMap, dipai, "底牌");
}
private static void show(HashMap<Integer, String> hashMap,TreeSet<Integer> treeSet ,String name) {
System.out.print(name + "的牌是:");
for(Integer i : treeSet) { //从hashMap中获取扑克牌信息
System.out.print(hashMap.get(i) + " ");
}
System.out.println();
}
参考结果:
小明的牌是:方片3 梅花3 红桃3 黑桃5 红桃6 方片7 梅花9 红桃9 黑桃9 方片J 梅花J 红桃J 黑桃Q 方片A 梅花A 黑桃A 梅花2
小红的牌是:梅花4 方片5 红桃5 方片6 梅花6 黑桃6 梅花7 黑桃7 梅花8 方片9 方片10 梅花10 红桃10 方片K 红桃K 黑桃K 小王
小白的牌是:黑桃3 方片4 红桃4 黑桃4 梅花5 红桃7 方片8 红桃8 黑桃8 黑桃J 方片Q 梅花Q 红桃Q 梅花K 方片2 黑桃2 大王
底牌的牌是:黑桃10 红桃A 红桃2
在此,可能集合框架已经就学习完毕了,需告一段落了
最后在对其之前的集合框架学习做一个最后的总结
/**
* Collection
* List(存取有序,有索引,可以重复)
* ArrayList
* 底层是数组实现的,线程不安全,查找和修改快,增和删比较慢
* LinkedList
* 底层是链表实现的,线程不安全,增和删比较快,查找和修改比较慢
* Vector
* 底层是数组实现的,线程安全的,无论增删改查都慢
* 如果查找和修改多,用ArrayList
* 如果增和删多,用LinkedList
* 如果都多,用ArrayList
* Set(存取无序,无索引,不可以重复)
* HashSet
* 底层是哈希算法实现
* LinkedHashSet
* 底层是链表实现,但是也是可以保证元素唯一,和HashSet原理一样
* TreeSet
* 底层是二叉树算法实现
* 一般在开发的时候不需要对存储的元素排序,所以在开发的时候大多用HashSet,HashSet的效率比较高
* TreeSet在面试的时候比较多,问你有几种排序方式,和几种排序方式的区别
* Map
* HashMap
* 底层是哈希算法,针对键有效
* LinkedHashMap
* 底层是链表,针对键有效
* TreeMap
* 底层是二叉树算法,针对键有效
* 开发中用HashMap比较多
*/