文章目录
1、树的介绍
1.1、排序二叉树
HashMap
和HashSet
的共同实现机制是哈希表,一个共同的限制是没有顺序。
TreeSet
和TreeMap
这两个类的共同实现基础是排序二叉树
排序二叉树也是二叉树,但它没有重复元素,而且是有序的二叉树
如果左子树不为空,则左子树上的所有节点都小于该节点
如果右子树不为空,则右子树上的所有节点都大于该节点
1.2、常用算法
1.2.1、查找
1.2.2、遍历
1.2.3、插入
1.2.4、删除
1.3、平衡二叉树
1.5哈希与树的比较
与哈希表一样,树也是计算程序中一种最重要的数据结构和思维方式,为了能够快速操作数据,
哈希
和树
是两种基本的思维方式,不需要顺序,,优先考虑哈希,需要顺序,考虑树除了容器类
TreeMap
和TreeSet
,数据库中的索引结构也是基于树的(不过基于B树,而不是二叉树),而索引是能够在大量数据中快速访问数据的关键
2、TreeMap的介绍
键有顺序
2.1、基本用法
TreeMap有两个基本构造方法
public TreeMap()
public TreeMap(Comparator<>)
- 第一个默认构造方法,如果使用默认构造方法,要求
Map中的键实现Comparable接口
,TreeMap内部进行各种比较时会调用键的Comparable接口中的CompareTo
方法 - 第二个接受一个比较器对象comparator,如果comparator不为null,在TreeMap内部进行比较时会调用这个comparator的compare方法,而不再调用键的
compareTo
方法
应该使用哪一个?第一个更简单,但要求键实现Comparable
接口,且期望的排序和键的比较结果是一致的;第二个更为灵活,不要求键实现Compstsble
接口,比较器可以灵活复杂的方式进行
2.2、实现原理
TreeMap内部是使用红黑树实现的,红黑树是一种大致平衡的排序二叉树
TreeMap内部成员
//comparator就是比较器,在构造方法中传递,如果没穿就是null
private final Comparator<? super K> comparator;
//root指向树的根节点,从根节点可以访问到每个节点
private transient Entry<K,V> root = null;
//当前节点数
private transient int size = 0;
private transient int modCount = 0;
//红黑树中的节点
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;//键
V value;//值
Entry<K,V> left = null;//左孩子
Entry<K,V> right = null;//右孩子
Entry<K,V> parent;//父节点
boolean color = BLACK;//颜色
2.2、put方法
添加第一个节点
public V put(K key, V value) {
Entry<K,V> t = root;
/*添加第一个节点 */
if (t == null) {
//为了检查key的类型和null,如果类型不匹配或为null,那么compare方法会抛出异常
compare(key, key); // type (and possibly null) check
//新建一个节点,指向根节点
root = new Entry<>(key, value, null);
size = 1;
//用于迭代过程中检测结构性变化
modCount++;
return null;
}
/*添加第一个节点 */
向红黑树中添加节点
//如果红黑树已经存在,会执行下面的代码
//添加的关键是寻找父节点,寻找父节点根据是否设置了comparator分为两种情况
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
//设置了cpmomparator的情况;寻找父节点
if (cpr != null) {
//寻找是一个从根节点开始循环的过程中
//在循环过程中
//cmp:保存了比较结果; t指向当前比较节点; parent为t的父节点;
//循环结束后 parent就是要找的父节点
/**
* t一开始指向父亲节点,从根节点开始比较键,如果小于根节点,就将t设为左孩子。
* 与左孩子比较,大于就与右孩子比较,就这样一直比较下去,直到t为null或者比较结果为0
*
* 如果比较结果为0,就表示已经有这个键,设置值,然后返还。
*
* 如果t为null,则退出循环时,parebnrt就指向待插入节点的父节点
*/
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);
}
//没有设置comparator的寻找父节点
//基本逻辑是一样的,当退出循环时parent指向父节点,只是如果没哟设置comparator
//则假设key一定实现了Comparable接口,使用Comparanble接口的compareTo方法进行比较
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;
稍微总结一下,其基本思想就是:循环比较找到父节点,并插入作为其左孩子或右孩子,然后调整保持树的大致平衡
2.3、根据键获取值get
//根据键获取值
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
//如果比较器不为空,调用单独的方法getEntryUsingComparator
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
//否则 假定key实现了Comparable的接口
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
//找的逻辑很简答,从根开始,小于往左面找,大于往右面找
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
2.4、查看是否包含某个键
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
2.5、查看是否包含某个值
TreeMap可以高效的按键进行查找,但如果要根据值进行查找,则需要遍历
//查看是否包含某个值
//TreeMap可以高效的按键进行查找,但如果要根据值进行查找,则需要遍历
public boolean containsValue(Object value) {
//getFirstEntry() :返回左下角的元素
//successor返回给定节点的后继节点
//从第一个节点开始,逐个比较,直到找到为止
for (Entry<K,V> e = getFirstEntry(); e != null; e = successor(e))
if (valEquals(value, e.value))//valEquals()就是比较值;
return true;
return false;
}
//找后继节点
//就是中序遍历
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
return null;
//如果有右孩子,则后继节点为右子树最小节点
else if (t.right != null) {
Entry<K,V> p = t.right;
while (p.left != null)
p = p.left;
return p;
} else {
//如果没有右子树;后继节点为祖先节点
//从当前节点网上找,如果他是父节点的右孩子则继续找父节点,直到它不是右孩子或父节点为空
//第一个非柚子节点父亲节点就是后继节点
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
//getFirst返回第一个节点,左下角,不断往左下角找
final Entry<K,V> getFirstEntry() {
Entry<K,V> p = root;
if (p != null)
while (p.left != null)
p = p.left;
return p;
}
static final boolean valEquals(Object o1, Object o2) {
return (o1==null ? o2==null : o1.equals(o2));
}
2.6、根据键删除键值对 remove
//根据键删除键值对
public V remove(Object key) {
//根据key找到节点
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
//待返回的值
V oldValue = p.value;
/**
* 节点有三种情况:
*
* 叶子节点:直接修改父节点对应引用职位null即可
*
* 只有一个孩子: 在父亲节点和孩子节点直接建立连接
*
* 有两个孩子:先找到后继节点,找到后,替换当前节点的内容为后继节点,然后再删除后继节点(因为这个后继节点一定没有左孩子,所以就将两个孩子的情况转换为前面两种情况)
*
*/
deleteEntry(p);
return oldValue;
}
/**
* Delete node p, and then rebalance the tree.
* 节点有三种情况:
*
* 叶子节点:直接修改父节点对应引用职位null即可
*
* 只有一个孩子: 在父亲节点和孩子节点直接建立连接
*
* 有两个孩子:先找到后继节点,找到后,替换当前节点的内容为后继节点,然后再删除后继节点(因为这个后继节点一定没有左孩子,所以就将两个孩子的情况转换为前面两种情况)
*
* */
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) {
//s为后继
Entry<K,V> s = successor(p);
//当前节点的key设置为后继的key和value
p.key = s.key;
p.value = s.value;
//然后将待删除的节点p指向了s,这样就转换为了一个孩子或叶子节点的情况
p = s;
} // p has 2 children
// Start fixup at replacement node, if it exists.
//p为待删除节点,replaceMent为要替换p的孩子节点,主体代码就是在p的父节点
//p.parent和replacement之间建立连接
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);
}
//叶子节点
//具体分为两种情况:一种是删除最后一个节点,修改root为null
else if (p.parent == null) { // return if we are the only node.
root = null;
}
//另一种是根据带删除节点是父节点的左孩子还是右孩子,响应的设置孩子节点为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;
}
}
}