一、概述

  在Map的实现中,除了我们最常见的KEY值无序的HashMap之外,还有KEY有序的Map,比较常用的有两类,一类是按KEY值的大小有序的Map,这方面的代表是TreeMap,另外一种就保持了插入顺序的Map,这类的代表是LinkedHashMap. 本文介绍TreeMap.

  Java提供了两种可以用来排序的接口,分别是Comparable和Comparactor, 两者分别说明如下:

  1. Comparable

  目前这是一个泛型接口,只有一个方法compareTo,  参与比较的类需要实现这个接口,这样该类便具备了可比较性,该方法的参数表示用来比较的对象。根据这个接口的返回值是 > 0, 小于0还是等于0决定了两个比较对象之间的大小关系。

  如果一个类实现了这个接口,则该类的所有对象都是可比较的,也就是说,自身具备了比较的能力。JDK中的自带的一些类,比如Integer, Long等都具备比较的能力,原因就是它们实现了这个接口。

  这种方式适合那些比较功能是类的特点之一的类,比如前面说到的Integer.

  2. Comparator

  除了Comparable,JDK还提供了一个接口用来比较,这就是Comparator,其接口定义如下:

  定义了两个方法,其中equals方法可以理解为是Object.equals的增加版本,功能和equal类似,用于比较的方法是compare, 其接收两个参数,这两个参数不需要实现任何接口,本方法负责根据一定的规则来比较两个对象的大小。

  打个比较,这个接口的实现者好比是裁判,两个待比较的对象找它来比较大小,它根据自己定义的规则,结合两个对象的自身的数据,通过计算得出一个值,从而来决定哪个对象大哪个对象小。其本身并不参与比较。

  有的时候待比较的对象并不需要具备可比较性,只是在某些场合下需要对它们的一个序列进行比较,或者说,可能会按多种规则对同一组序列进行比较,这个时候,用comparator构造构建器就非常合适,首先它不会侵入原类,另外,多种规则就是多个比较器,非常方便。

  在JAVA里有关比较的下操作中,如集合排序等都提供了这两种方式的比较,我们可以根据情况进行选择。

二、实现原理分析

  顾名思议,TreeMap的实现是基于Tree的数据结构,JAVA中可以用来排序的Tree,最有名的莫过于红黑树,而TreeMap的实现也正是基于红黑树,为了更好的理解红黑树,前面我们已经分了两篇来详细说明红黑树,在此不再细说。本文主要从各个方面分析TreeMap的构建和使用。

  1. 创建

  创建一个TreeMap,总共有四种方式,如下:

  我们可以看到,TreeMap的这四种方式中其实主要分了两种,一是comparator为null的,一种是不为null的,作为一个有序的Map,如果comparator不为NULL,则自然是使用这个来比较,否则,KEY应该继承Comparable接口,这个也是前面解释过的。

  作为一个红黑树,那么整个数据的存储则必是一颗树,TreeMap的实现也遵守了这一点,我们可以看一下对于树的构造。

  首先,有一个表示根节点的变量。

  

  其次,我们看一下节点的定义,也具备红黑树节点的性质

  

  可以看到节点的定义中除了有key和value之外,还有左孩子,右孩子,父节点,以及节点的颜色,根据这三个属性,我们可以很方便的从任何一个节点向上一级或者向下一级导航。通过对节点设置颜色,很容易实现红黑树的相关算法。

  2. 添加

  添加指添加一个新节点到树的合适位置。那么这个分类两个部分,首先是要找到合适的节点,这个可以通过二叉树的查找算法来完成,查找之后有两个结果,如果找到相同的KEY了,则做更新操作,如果没有找到,则需要把节点和原来的树关联,关联之后,由于可能会破坏红黑树的性质,所以还要根据情况再做一次调整。

  具体的算法如下:

  

 public V put(K key, V value) {
Entry<K,V> t = root;
if (t == 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) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
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.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}

  上面的代码就是整个添加的过程,关键的地方进行了加粗显示,可以看到,在查找的时候,实现时根据比较器是否存在来决定采用哪一种方式进行比较,如果比较之后没有找到对应的节点,则parent变量记录了最后一个不为叶子节点的节点,该节点即为新节点的父节点。新增后,需要对树重新做调整。

  调整算法已经在红黑树部分介绍过了,所以这里就不再细说。

  3. 删除

  删除的话同样也是先通过二叉树的查找算法找到对应的节点,如果节点有两个孩子节点,则需要进一步查找其直接后继,然后针对其后继节点做删除操作。这样可以保证被删除的元素只有最多不超过一个节点。

  删除完成后,根据被删除节点的后继节点的情况做相应的删除修复操作,以保证删除后,原树还是一个红黑树,删除的完整代码如下:

private void deleteEntry(Entry<K,V> p) {
modCount++;
size--; // If strictly internal, copy successor's element to p and then make p
// point to successor.
if (p.left != null && p.right != null) {
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
} // p has 2 children // Start fixup at replacement node, if it exists.
Entry<K,V> replacement = (p.left != null ? p.left : p.right); if (replacement != null) {
// Link replacement to parent
replacement.parent = p.parent;
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement; // Null out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent = null; // Fix replacement
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // return if we are the only node.
root = null;
} else { // No children. Use self as phantom replacement and unlink.
if (p.color == BLACK)
fixAfterDeletion(p); if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}

  修复的操作在前面的红黑树中已经介绍了,就不再详细分析了。

  4. 遍历操作

  和普通的Map相比,作为一个排序Map,TreeMap也提供了各种遍历的操作,如headMap, tailMap,descendingMap等,我们可以方便的进行正序或倒序的遍历,这得益于节点的双向关联。在此不再一一描述

三、总结

  至此,基于红黑数的TreeMap就算是完全分析完了,这是一种很经典的实现,通过对其源码的分析我们可以更深刻的理解红黑树,也可以学习到一些好的设计思想。同时,也有助于我们正确地使用红黑树

容器--TreeMap的更多相关文章

  1. Map容器——TreeMap及常用API,Comparator和Comparable接口

    TreeMap及常用API ①   TreeMap类通过使用红黑树实现Map接口; ②   TreeMap提供按排序顺序存储键/值对的有效手段,同时允许快速检索; ③   不像散列(HashMap), ...

  2. Comparable接口——容器中自定义类排序

    1.容器TreeMap,默认根据Key对象中某个属性的从小到大排列元素. (1)如下代码示例,Key是整型数字,所以按照其从小到大的顺序排列 public class TestTreeMap { pu ...

  3. 数据结构 - Codeforces Round #353 (Div. 2) D. Tree Construction

    Tree Construction Problem's Link ------------------------------------------------------------------- ...

  4. [个人原创]关于java中对象排序的一些探讨(一)

    有的时候我们需要将自己定义的对象,有序输出.因为一般我们程序的中间结果需要存储在容器里,那么怎样对容器中的对象按照一定次序输出就是程序员经常需要考虑的问题.本片文章探讨了怎样有序化输出容器中的对象的问 ...

  5. Java中统计字符串中各个字符出现的次数

    import java.util.Iterator; import java.util.Set; import java.util.TreeMap; public class TreeMapDemo ...

  6. 给jdk写注释系列之jdk1.6容器(7)-TreeMap源码解析

    TreeMap是基于红黑树结构实现的一种Map,要分析TreeMap的实现首先就要对红黑树有所了解.      要了解什么是红黑树,就要了解它的存在主要是为了解决什么问题,对比其他数据结构比如数组,链 ...

  7. Java 容器 & 泛型:五、HashMap 和 TreeMap的自白

    Writer:BYSocket(泥沙砖瓦浆木匠) 微博:BYSocket 豆瓣:BYSocket Java 容器的文章这次应该是最后一篇了:Java 容器 系列. 今天泥瓦匠聊下 Maps. 一.Ma ...

  8. java:容器/集合(Map(HashMap,TreeMap)Collection和Collections,(Collection-value();)

    *Map接口:采用是键值对的方式存放数据.无序 HashMap: *常见的实现类: *--HashMap:基于哈希表的 Map 接口的实现. *常用的构造方法: * HashMap()构造一个具有默认 ...

  9. Java 容器源码分析之 TreeMap

    TreeMap 是一种基于红黑树实现的 Key-Value 结构.在使用集合视图在 HashMap 中迭代时,是不能保证迭代顺序的: LinkedHashMap 使用了双向链表,保证按照插入顺序或者访 ...

随机推荐

  1. CSharpGL(5)解析3DS文件并用CSharpGL渲染

    CSharpGL(5)解析3DS文件并用CSharpGL渲染 我曾经写过一个简单的*.3ds文件的解析器,但是只能解析最基本的顶点.索引信息,且此解析器是仿照别人的C++代码改写的,设计的也不好,不方 ...

  2. IOS SWIFT 启动流程学习

    其实和我们java.c一样通过一个main函数作为入口. main封装在了UIApplicationMain里面.所以后者变成启动入口. 他会扫描Info.plist,找到需要加载的入口storybo ...

  3. Gitcafe绑定自定义域名

    之前将自己练习写作工具替换为Markdown,部署工具改为Hexo,并且将托管地搬家到GitCafe之后,便是被各种的舒爽所围绕.具体折腾的详情参见使用Hexo搭建专属Blog.她们的组合方便快捷,让 ...

  4. 使用angular中ng-repeat , track by的用处

    我们见到最简单的例子是: <div ng-repeat="link in links" ></div> 如果item的值有重复的,比如links=[&quo ...

  5. mybatis的一些小总结

    好长时间没用mybatis了,现在项目忽然用mybatis,用的过程中出现了些问题,虽然解决了,不过这花的时间有些长了.总结用的过程中出现的一些问题 1.mapper.xml 之前一直用的自动生成,现 ...

  6. Content-Type List

    Content-Type List Description of Data Content Typical Filename Extensions MIME type/subtype       Te ...

  7. TSQL 聚合函数忽略NULL值

    max,min,sum,avg聚合函数会忽略null值,但不代表聚合函数不返回null值,如果表为空表,或聚合列都是null,则返回null.count 聚合函数忽略null值,如果聚合列都是null ...

  8. nodejs+edatagrid读取本地excel表格

     

  9. Dijkstra算法优先队列实现与Bellman_Ford队列实现的理解

    /* Dijkstra算法用优先队列来实现,实现了每一条边最多遍历一次. 要知道,我们从队列头部找到的都是到 已经"建好树"的最短距离以及该节点编号, 并由该节点去更新 树根 到其 ...

  10. Ruby之基本数据类型(三)

    前言 上一节我们介绍了常量以及变量的用法,这一节我们开始来介绍Ruby中的一些基本类型. 数值类型(Fixnum.Bignum.Float) Ruby的数值类型包括整数和浮点数.整数包括Fixnum和 ...