数据结构:红黑树的插入

数据结构:红黑树的插入

上一次我们介绍了红黑树的基本特性,和根据key查询节点的方法。这一次我们介绍如何插入一个新的节点。在我们对一颗红黑树进行插入或删除节点后可能会破坏这颗红黑树,使它变成一颗普通的二叉树。这个时候我们就需要通过一些操作使它重新变成一颗红黑树,这个操作就是旋转。

旋转分为左旋转和右旋转,下面我们分别介绍什么是左旋转,右旋转:

左旋转是一组操作的集合,对某个节点左旋转就是将该节点的右节点设置为它的父节点,该节点将变成左节点,同时将该节点的右节点的左节点设为该节点的右节点。说着比较拗口,我们结合下面的图形和代码实现过一遍就明白了:

private void leftRotation(Node h){
		Node m = h.rightNode;
		// 1. 将 k 节点设置为 h 的右节点
		h.rightNode = m.leftNode;
		if(null != m.leftNode){
			m.leftNode.parentNode = h;
		}
		// 2. 将 h 的父节点 赋给 m 的父节点,之后分 3 种情况讨论
		m.parentNode = h.parentNode;
		if(null == m.parentNode){ // I: 说明 h 原来是根节点,现在将 m 设置为新的根节点
			root = m;
		}else{
			if(h.key.compareTo(h.parentNode.key) < 0){ // II: 说明 h 原来是它父节点的左孩子,现在将 m 设置为新的左孩子
				h.parentNode.leftNode = m;
			}else {
				h.parentNode.rightNode = m; // III: 说明 h 原来是它父节点的右孩子,现在将 m 设置为新的右孩子
			}
		}
		// 3. 将 h 挂靠在 m 的左孩子上
		m.leftNode = h;
		h.parentNode = m;
	}

右旋转就是左旋转的逆向操作,对某个节点进行右旋转就是将该节点的左节点设置为它的父节点,该节点将变成右节点,同时将左节点的右节点设置为该节点的左节点。同样看图和代码实现:

	private void rightRotation(Node m){
		Node h = m.leftNode;
		// 1. 将 k 设置为 m 的左节点
		m.leftNode = h.rightNode;
		if(null != h.rightNode){
			h.rightNode.parentNode = m;
		}
		// 2. 将 m 的父节点 赋给 h 的父节点,之后分 3 种情况讨论
		h.parentNode = m.parentNode;
		if(null == m.parentNode){ // I: 说明 m 原来是根节点,现在将 h 设置为新的根节点
			root = h;
		}else{
			if(m.key.compareTo(m.parentNode.key) < 0){ // II: 说明 m 原来是它父节点的左孩子,现在将 h 设置为新的左孩子
				m.parentNode.leftNode = h;
			}else {
				m.parentNode.rightNode = h; // III: 说明 m 原来是它父节点的右孩子,现在将 h 设置为新的右孩子
			}
		}
		// 3. 将 m 挂靠在 h 的右孩子上
		h.rightNode = m;
		m.parentNode = h;
	}

插入一个节点分为以下几个步骤:

  1. 新建一个红色节点(为什么是红色节点呢?看下面的操作我们就明白,用红色节点操作更简单)
  2. 和普通的二叉排序树一样,找到插入的位置插入
  3. 通过旋转,重新着色操作使它重新成为一颗红黑树

我们发现关键在于第三步,第三步比较复杂可以分为两种大的情况讨论,

一 : 父节点是左子树,对于这种情况又可以分为以下3中小的情况考虑


二 : 父节点是右子树,对于这种情况也可以分为以下3中小的情况考虑


由于知乎上文字不能有颜色的区分,所以为了清除的表示整个过程,上面都是截图。

接下来我们通过代码来实现前面讲解的流程:

/**
	 * 插入新的节点,如果指定的key已经存在,则更新原来的值
	 * @param key
	 * @param value
	 */
	public void put(K key, V value) {
		Node newNode = new Node(key, value, null, null, RED, null);
		if(null == root){
			root = newNode;
			root.color = BLACK;
		}else{
			upsert(null, root, newNode);
		}
	}
	
	private void upsert(Node parent, Node current, Node newNode){
		if(null == current){
			if(newNode.key.compareTo(parent.key) > 0){
				parent.rightNode = newNode;
			}else{
				parent.leftNode = newNode;
			}
			newNode.parentNode = parent;
			upsertFix(newNode); // 插入新节点后 对红黑树进行修复
		}else{
			int result = newNode.key.compareTo(current.key);
			if(result == 0){
				current.value = newNode.value;
			}
			parent = current;
			if(result > 0){
				upsert(parent, parent.rightNode, newNode);
			}
			if(result < 0){
				upsert(parent, parent.leftNode, newNode);
			}
		}
	}
	
	private void upsertFix(Node newNode){
		Node parent = newNode.parentNode;
		if(RED == parent.color){ // 父节点如果是黑节点 则不需要处理
			Node grandfather = parent.parentNode;
			if(parent == grandfather.leftNode){ // 1. 父节点原来是 左节点
				Node uncle = grandfather.rightNode;
				if((null != uncle) && (RED == uncle.color)){ // case 3: 叔叔节点是红色
					uncleRedFix(newNode);
				}else{ // 叔叔节点为 NULL 或者 是黑色节点
					if(newNode.key.compareTo(parent.key) < 0){ // case 1: 叔叔节点是黑色,插入到左子树中
						leftNodeFix(grandfather, parent);
					}else{ // case 2: 叔叔节点是黑色,插入到右子树中
						leftRotation(parent);
						leftNodeFix(grandfather, newNode); //我们将 parent 节点作为“新插入的节点”,这样 真正新插入的节点 newNode 就是父节点
					}
				}
			}else{ // 1. 父节点原来是 右节点
				Node uncle = grandfather.leftNode;
				if((null != uncle) && (RED == uncle.color)){ // case 3: 叔叔节点是红色
					uncleRedFix(newNode);
				}else{ // 叔叔节点为 NULL 或者 是黑色节点
					if(newNode.key.compareTo(parent.key) > 0){ // case 1: 叔叔节点是黑色,插入到右子树中
						rightNodeFix(grandfather, parent);
					}else{ // case 2: 叔叔节点是黑色,插入到左子树中
						rightRotation(parent);
						rightNodeFix(grandfather, newNode); //我们将 parent 节点作为“新插入的节点”,这样 真正新插入的节点 newNode 就是父节点
					}
				}
			}
		}
	}
	
	/**
	 * 处理 父节点原来是 左节点 的 case 1 的情况: 叔叔节点是黑色,插入到左子树中
	 * @param grandfather
	 * @param parent
	 */
	private void leftNodeFix(Node grandfather, Node parent){
		parent.color = BLACK;
		grandfather.color = RED;
		rightRotation(grandfather);
	}
	
	/**
	 * 处理 父节点原来是 右节点 的 case 1 的情况: 叔叔节点是黑色,插入到右子树中
	 * @param grandfather
	 * @param parent
	 */
	private void rightNodeFix(Node grandfather, Node parent){
		parent.color = BLACK;
		grandfather.color = RED;
		leftRotation(grandfather);
	}
	
	/**
	 * 处理 case 3: 叔叔节点是红色
	 * @param newNode
	 */
	private void uncleRedFix(Node newNode){
		Node parent = newNode.parentNode;
		if((null != parent) && (RED == parent.color)){
			Node grandfather = parent.parentNode;
			Node uncle = grandfather.leftNode;
			if(parent == grandfather.leftNode){ 
				uncle = grandfather.rightNode;
			}
			parent.color = BLACK;
			uncle.color = BLACK;
			if(root != grandfather){
				grandfather.color = RED;
				upsertFix(grandfather);
			}
		}
	}

我们可以结合前面的图形,参照代码的注释来阅读代码。下面咱插入一些测试数据

public static void main(String[] args) {
		
		RedBlackTree<Integer, String> bst = new RedBlackTree<Integer, String>();
		
		bst.put(100, "v100");
		bst.put(50, "v50");
		bst.put(150, "v150");
		bst.put(20, "v20");
		bst.put(85, "v85");
		bst.put(10, "v10");
		bst.put(15, "a15");
		bst.put(75, "v75");
		bst.put(95, "v95");
		bst.put(65, "v65");
		bst.put(76, "v76");
		bst.put(60, "v60");
		bst.put(66, "v66");
		bst.put(61, "v61");
		
		System.out.println(bst.getRoot());
	}

我们可以先自己在纸上画一遍,看看画出来的结果是否和打印的结果一样。

好红黑树的插入我们就先介绍到这,下一次我们将介绍红黑树的删除,删除的过程和普通的二叉排序树一样,只不过删除后也需要对红黑树进行修复,好吧赶火车回家喽!

编辑于 2016-11-02 22:24