【搞定Java基础-集合】第七篇 TreeMap 和红黑树

目录

1、红黑树简介

2、TreeMap 的数据结构

3、TreeMap 的 put() 方法

3.1  红黑树增加节点

一. 为根节点

二. 父节点为黑色

三. 若父节点 P 和 P 的兄弟节点 U 都为红色

四. 若父节点 P 为红色,叔父节点 U 为黑色或者缺少,且新增节点 N 为 P 节点的右孩子

五. 父节点 P 为红色,叔父节点 U 为黑色或者缺少,新增节点 N 为父节点 P 左孩子

3.2  TreeMap 的 put() 方法实现分析

4、TreeMap 的 delete() 方法

4.1  红黑树删除节点


本文转发自:http://cmsblogs.com/?p=1013

TreeMap 的实现是红黑树算法的实现,所以要了解 TreeMap 就必须对红黑树有一定的了解,其实这篇博文的名字叫做:根据红黑树的算法来分析 TreeMap 的实现,但是为了与 Java 提高篇系列博文保持一致还是叫做 TreeMap 比较好。通过这篇博文你可以获得如下知识点:

1、红黑树的基本概念;

2、红黑树增加节点、删除节点的实现过程;

3、红黑树左旋转、右旋转的复杂过程;

4、Java 中 TreeMap 是如何通过 put、deleteEntry 两个来实现红黑树增加、删除节点的。

我想通过这篇博文你对 TreeMap 一定有了更深的认识。好了,下面先简单普及红黑树知识。

1、红黑树简介

可以看博主在《数据结构与算法》专栏中的:红黑树详解

红黑树又称“红-黑二叉树”,它首先是一颗二叉树,它具体二叉树所有的特性。同时红黑树更是一颗自平衡的排序二叉树

我们知道一颗基本的二叉树他们都需要满足一个基本性质:即树中的任何节点的值大于它的左子节点,且小于它的右子节点。按照这个基本性质使得树的检索效率大大提高。我们知道在生成二叉树的过程是非常容易失衡的,最坏的情况就是一边倒(只有右 / 左子树),这样势必会导致二叉树的检索效率大大降低(O(n)),所以为了维持二叉树的平衡,大牛们提出了各种实现的算法,如:AVLSBT伸展树TREAP ,红黑树等等。

平衡二叉树必须具备如下特性:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一个等高子节点,其左右子树的高度都相近。

红黑树顾名思义就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。对于一棵有效的红黑树二叉树而言,我们必须增加如下规则:

1. 每个节点都只能是红色或者黑色;

2. 根节点是黑色;

3. 每个叶节点(NIL节点,空节点)是黑色的;

4. 如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点;

5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的因为操作比如插入、 删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。所以红黑树它是复杂而高效的,其检索效率O(log n)。下图为一颗典型的红黑二叉树。

对于红黑二叉树而言它主要包括三大基本操作:左旋、右旋、着色。

  • 左旋

2014051700004

                    

  • 右旋 

2014051700005

由于本文主要是讲解 Java 中 TreeMap,所以并没有对红黑树进行非常深入的了解和研究,如果诸位想对其进行更加深入的研究下面几篇较好的博文:

1. 红黑树系列集锦

2. 红黑树数据结构剖析

3. 红黑树

2、TreeMap 的数据结构

TreeMap 的定义如下:

public class TreeMap<K,V>
             extends AbstractMap<K,V>
             implements NavigableMap<K,V>, Cloneable, java.io.Serializable

TreeMap 继承了 AbstractMap,实现了NavigableMap、Cloneable、Serializable三个接口。其中 AbstractMap 表明TreeMap 为一个 Map ,即支持 key-value 的集合, NavigableMap(更多)则意味着它支持一系列的导航方法,具备针对给定搜索目标返回最接近匹配项的导航方法 。

TreeMap 中同时也包含了如下几个重要的属性:

// 比较器,因为TreeMap是有序的,通过comparator接口我们可以对TreeMap的内部排序进行精密的控制
private final Comparator<? super K> comparator;
// TreeMap红-黑节点,为TreeMap的内部类
private transient Entry<K,V> root = null;
// 容器大小
private transient int size = 0;
// TreeMap修改次数
private transient int modCount = 0;
// 红黑树的节点颜色--红色
private static final boolean RED = false;
// 红黑树的节点颜色--黑色
private static final boolean BLACK = true;

对于叶子节点 Entry 是 TreeMap 的内部类,它有几个重要的属性:

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;     // 颜色:默认是黑色
	
	// ... 省略
}

注: 前面只是开胃菜,下面是本篇博文的重中之重,在下面两节我将重点讲解 treeMap 的 put()、delete() 方法。通过这两个方法我们会了解红黑树增加、删除节点的核心算法。

3、TreeMap 的 put() 方法

在了解 TreeMap 的 put() 方法之前,我们先了解红黑树增加节点的算法。

3.1  红黑树增加节点

红黑树在新增节点过程中比较复杂,复杂归复杂它同样必须要依据上面提到的五点规范,同时由于规则1、 2、 3基本都会满足,下面我们主要讨论规则4、5。假设我们这里有一棵最简单的树,我们规定新增的节点为 N,它的父节点为P,P的兄弟节点为U,P的父节点为G。

2014051700007

对于新节点的插入有如下三个关键地方:

1、插入新节点总是红色节点;

2、如果插入节点的父节点是黑色,能维持性质;

3、如果插入节点的父节点是红色,破坏了性质。故插入算法就是通过重新着色或旋转,来维持性质 。

为了保证下面的阐述更加清晰和根据便于参考,我这里将红黑树的五点规定再贴一遍:

1、每个节点都只能是红色或者黑色;

2、根节点是黑色;

3、每个叶节点(NIL节点,空节点)是黑色的;

4、如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点;

5、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

一. 为根节点

若新插入的节点 N 没有父节点,则直接当做根据节点插入即可,同时将颜色设置为黑色。(如图1)

二. 父节点为黑色

这种情况新节点 N 同样是直接插入,同时颜色为红色,由于根据规则四它会存在两个黑色的叶子节点,值为 null。同时由于新增节点 N 为红色,所以通过它的子节点的路径依然会保存着相同的黑色节点数,同样满足规则5。(如图2)

三. 若父节点 P 和 P 的兄弟节点 U 都为红色

对于这种情况若直接插入肯定会出现不平衡现象。怎么处理?

P、U 节点变黑、G 节点变红。这时由于经过节点 P、U 的路径都必须经过 G ,所以在这些路径上面的黑节点数目还是相同的。但是经过上面的处理,可能 G 节点的父节点也是红色,这个时候我们需要将 G 节点当做新增节点递归处理。

四. 若父节点 P 为红色,叔父节点 U 为黑色或者缺少,且新增节点 N 为 P 节点的右孩子

对于这种情况我们对新增节点N、P进行一次左旋转。这里所产生的结果其实并没有完成,还不是平衡的(违反了规则四),这是我们需要进行情况 5 的操作。

20140517000010

五. 父节点 P 为红色,叔父节点 U 为黑色或者缺少,新增节点 N 为父节点 P 左孩子

这种情况有可能是由于情况四而产生的,也有可能不是。对于这种情况先以 P 节点为中心进行右旋转,在旋转后产生的树中,节点 P 是节点 N、G 的父节点。但是这棵树并不规范,它违反了规则4,所以我们将P、G节点的颜色进行交换,使之其满足规范。开始时所有的路径都需要经过 G 其他们的黑色节点数一样,但是现在所有的路径改为经过 P,且 P 为整棵树的唯一黑色节点,所以调整后的树同样满足规范 5。

20140517000011

【疑问:感觉上图中第三张图有错误,出现了相邻的两个红色节点。所以 N、G 应该是黑色,U 是红色?】

上面展示了红黑树新增节点的五种情况,这五种情况涵盖了所有的新增可能,不管这棵红黑树多么复杂,都可以根据这五种情况来进行生成。下面就来分析 Java 中的 TreeMap 是如何来实现红黑树的。

3.2  TreeMap 的 put() 方法实现分析

在 TreeMap 的 put() 的实现方法中主要分为两个步骤:

第一:构建排序二叉树;//就是先插入到它顺序的位置

第二:平衡二叉树。//再通过左旋右旋着色来调整

对于排序二叉树的创建,其添加节点的过程如下:

1、以根节点为初始节点进行检索;

2、与当前节点进行比对,若新增节点值较大,则以当前节点的右子节点作为新的当前节点。否则以当前节点的左子节点作为新的当前节点;

3、循环递归2步骤知道检索出合适的叶子节点为止;

4、将新增节点与3步骤中找到的节点进行比对,如果新增节点较大,则添加为右子节点;否则添加为左子节点。

按照这个步骤我们就可以将一个新增节点添加到排序二叉树中合适的位置。如下:

public V put(K key, V value) {
   // 用t表示二叉树的当前节点
	Entry<K,V> t = root;
	// t为null表示一个空树,即TreeMap中没有任何元素,直接插入
	if (t == null) {
		// 比较key值,个人觉得这句代码没有任何意义,空树还需要比较. 排序?
		compare(key, key); // type (and possibly null) check
		// 将新的key-value键值对创建为一个Entry节点,并将该节点赋予给root
		root = new Entry<>(key, value, null);
		// 容器的size = 1,表示TreeMap集合中存在一个元素
		size = 1;
		// 修改次数 + 1
		modCount++;
		return null;
	}
	int cmp;     // cmp表示key排序的返回结果
	Entry<K,V> parent;   // 父节点
	// split comparator and comparable paths
	Comparator<? super K> cpr = comparator;    // 指定的排序算法
	// 如果cpr不为空,则采用既定的排序算法进行创建TreeMap集合
	if (cpr != null) {
		do {
			 parent = t;      // parent指向上次循环后的t
			// 比较新增节点的key和当前节点key的大小
			cmp = cpr.compare(key, t.key);
			// cmp返回值小于0,表示新增节点的key小于当前节点的key,则以当前节点的左子节点作为新的当前节点
			if (cmp < 0)
				t = t.left;
			// cmp返回值大于0,表示新增节点的key大于当前节点的key,则以当前节点的右子节点作为新的当前节点
			else if (cmp > 0)
				t = t.right;
			// cmp返回值等于0,表示两个key值相等,则新值覆盖旧值,并返回新值
			else
				return t.setValue(value);
		} while (t != null);
	}
	// 如果cpr为空,则采用默认的排序算法进行创建TreeMap集合
	else {
		if (key == null)     // key值为空抛出异常
			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);
	}
	// 将新增节点当做parent的子节点
	Entry<K,V> e = new Entry<>(key, value, parent);
	// 如果新增节点的key小于parent的key,则当做左子节点
	if (cmp < 0)
		parent.left = e;
	// 如果新增节点的key大于parent的key,则当做右子节点
	else
		parent.right = e;
	/*
	 *  上面已经完成了排序二叉树的的构建,将新增节点插入该树中的合适位置
	 *  下面fixAfterInsertion()方法就是对这棵树进行调整. 平衡,具体过程参考上面的五种情况
	 */
	fixAfterInsertion(e);
	// TreeMap元素数量 + 1
	size++;
	// TreeMap容器修改次数 + 1
	modCount++;
	return null;
}

上面代码中 do{} 代码块是实现排序二叉树的核心算法,通过该算法我们可以确认新增节点在该树的正确位置。找到正确位置后将插入即可,这样做了其实还没有完成,因为我知道 TreeMap 的底层实现是红黑树,红黑树是一棵平衡排序二叉树,普通的排序二叉树可能会出现失衡的情况,所以下一步就是要进行调整。fixAfterInsertion(e) 的调整过程务必会涉及到红黑树的左旋、右旋、着色三个基本操作。代码如下:


// 新增节点后的修复操作,x 表示新增节点
private void fixAfterInsertion(Entry<K,V> x) {
	x.color = RED;    //新增节点的颜色为红色
 
	// 循环 直到 x不是根节点,且x的父节点不为红色
	while (x != null && x != root && x.parent.color == RED) {
		// 如果X的父节点(P)是其父节点的父节点(G)的左节点
		if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
			// 获取X的叔节点(U)
			Entry<K,V> y = rightOf(parentOf(parentOf(x)));
			// 如果X的叔节点(U)为红色(情况三)
			if (colorOf(y) == RED) {     
				// 将X的父节点(P)设置为黑色
				setColor(parentOf(x), BLACK);
				// 将X的叔节点(U)设置为黑色
				setColor(y, BLACK);
				// 将X的父节点的父节点(G)设置红色
				setColor(parentOf(parentOf(x)), RED);
				x = parentOf(parentOf(x));
			}
			// 如果X的叔节点(U为黑色);这里会存在两种情况(情况四. 情况五)**
			else {   
				// 如果X节点为其父节点(P)的右子树,则进行左旋转(情况四)**
				if (x == rightOf(parentOf(x))) {
					// 将X的父节点作为X
					x = parentOf(x);
					// 右旋转
					rotateLeft(x);
				}
				// ****(情况五)**
				// 将X的父节点(P)设置为黑色**
				setColor(parentOf(x), BLACK);
				// 将X的父节点的父节点(G)设置红色
				setColor(parentOf(parentOf(x)), RED);
				// 以X的父节点的父节点(G)为中心右旋转
				rotateRight(parentOf(parentOf(x)));
			}
		}
		// 如果X的父节点(P)是其父节点的父节点(G)的右节点
		else {
			// 获取X的叔节点(U)
			Entry<K,V> y = leftOf(parentOf(parentOf(x)));
			// 如果X的叔节点(U) 为红色(情况三)
			if (colorOf(y) == RED) {
				// 将X的父节点(P)设置为黑色
				setColor(parentOf(x), BLACK);
				// 将X的叔节点(U)设置为黑色
				setColor(y, BLACK);
				// 将X的父节点的父节点(G)设置红色
				setColor(parentOf(parentOf(x)), RED);
				x = parentOf(parentOf(x));
			}
			// 如果X的叔节点(U为黑色);这里会存在两种情况(情况四. 情况五)
			else {
				// 如果X节点为其父节点(P)的右子树,则进行左旋转(情况四)
				if (x == leftOf(parentOf(x))) {
					// 将X的父节点作为X
					x = parentOf(x);
					// 右旋转
					rotateRight(x);
				}
				//(情况五)
				// 将X的父节点(P)设置为黑色
				setColor(parentOf(x), BLACK);
				// 将X的父节点的父节点(G)设置红色
				setColor(parentOf(parentOf(x)), RED);
				// 以X的父节点的父节点(G)为中心右旋转
				rotateLeft(parentOf(parentOf(x)));
			}
		}
	}
	// 将根节点G强制设置为黑色
	root.color = BLACK;
}

对这段代码的研究我们发现,其处理过程完全符合红黑树新增节点的处理过程。所以在看这段代码的过程一定要对红黑树的新增节点过程有了解。在这个代码中还包含几个重要的操作。左旋(rotateLeft())、右旋(rotateRight())、着色(setColor())。

  • 左旋:rotateLeft()

所谓左旋转,就是将新增节点(N)当做其父节点(P),将其父节点P当做新增节点(N)的左子节点。即:G.left —> N ,N.left —> P。

private void rotateLeft(Entry<K,V> p) {
	if (p != null) {
		// 获取P的右子节点,其实这里就相当于新增节点N(情况四而言)
		Entry<K,V> r = p.right;
		// 将R的左子树设置为P的右子树
		p.right = r.left;
		// 若R的左子树不为空,则将P设置为R左子树的父亲
		if (r.left != null)
			r.left.parent = p;
		// 将P的父亲设置R的父亲
		r.parent = p.parent;
		// 如果P的父亲为空,则将R设置为跟节点
		if (p.parent == null)
			root = r;
		// 如果P为其父节点(G)的左子树,则将R设置为P父节点(G)左子树
		else if (p.parent.left == p)
			p.parent.left = r;
		// 否则R设置为P的父节点(G)的右子树
		else
			p.parent.right = r;
		// 将P设置为R的左子树
		r.left = p;
		// 将R设置为P的父节点
		p.parent = r;
	}
}
  • 右旋:rotateRight()

所谓右旋转即将新增节点(P)当做其父节点(G),将其父节点G当做新增节点(P)的右子节点:P.right —> G,G.parent —> P。

private void rotateRight(Entry<K,V> p) {
	if (p != null) {
		// 将L设置为P的左子树
		Entry<K,V> l = p.left;
		// 将L的右子树设置为P的左子树
		p.left = l.right;
		// 若L的右子树不为空,则将P设置L的右子树的父节点
		if (l.right != null)
			l.right.parent = p;
		// 将P的父节点设置为L的父节点
		l.parent = p.parent;
		// 如果P的父节点为空,则将L设置根节点
		if (p.parent == null)
			root = l;
		// 若P为其父节点的右子树,则将L设置为P的父节点的右子树
		else if (p.parent.right == p)
			p.parent.right = l;
		// 否则将L设置为P的父节点的左子树
		else
			p.parent.left = l;
		// 将P设置为L的右子树
		l.right = p;
		// 将L设置为P的父节点
		p.parent = l;
	}
}
  • 着色:setColor()

着色就是改变该节点的颜色,在红黑树中,它是依靠节点的颜色来维持平衡的。


private static <K,V> void setColor(Entry<K,V> p, boolean c) {
	if (p != null)
		p.color = c;
}

4、TreeMap 的 delete() 方法

4.1  红黑树删除节点

针对于红黑树的增加节点而言,删除显得更加复杂,使原本就复杂的红黑树变得更加复杂。同时删除节点和增加节点一样,同样是找到删除的节点,删除之后调整红黑树。但是这里的删除节点并不是直接删除,而是通过走了“弯路”,通过一种捷径来删除的:找到被删除的节点D的子节点F,用F来替代D,不是直接删除D,因为D被F替代了,直接删除F即可。所以这里就将删除父节点D的事情转变为了删除子节点F的事情,这样处理就将复杂的删除事件简单化了。子节点F的规则是:右分支最左边,或者 左分支最右边的。20140517000012

未完

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值