紅黑二叉樹優化哈希表


續哈希表那兩篇博客

我們已經知道了哈希表有多麼多麼的牛逼,但是呢,有沒有想過一個問題:對於用拉鍊法構造出來的哈希表,我們在查找一個元素時,還是要遍歷後面的拉鍊。解決這個問題的一個比較好的方法就是紅黑二叉樹

這篇博客分析紅黑二叉樹的

  • 左旋
  • 右旋
  • 插入

參考文章:

一、紅黑二叉樹簡介

紅黑樹(Red Black Tree) 是一種自平衡二叉查找樹
紅黑樹和AVL樹類似,都是在進行插入和刪除操作時通過特定操作保持二叉查找樹的平衡,從而獲得較高的查找性能。
它可以在O(log n)時間內做查找,插入和刪除,這裏的n 是樹中元素的數目。 ——百度百科

紅黑二叉樹的五條性質(五條全部滿足就是紅黑二叉樹,只要有一條不符則不是

1. 每個結點的顏色只能是紅色或黑色。
2. 根結點是黑色的。
3. 每個葉子結點都帶有兩個空的黑色結點(被稱爲黑哨兵),如果一個結點n的只有一個左孩子,那麼n的右孩子是一個黑哨兵;如果結點n只有一個右孩子,那麼n的左孩子是一個黑哨兵。
4. 如果一個結點是紅的,則它的兩個兒子都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。
5. 對於每個結點來說,從該結點到其子孫葉結點的所有路徑上包含相同數目的黑結點。

二、定義結點、顏色等

public class RBNode<T extends Comparable<T>> {
		RBNode<T> left; // 左孩子
		RBNode<T> right; // 右孩子
		RBNode<T> parent; // 父節點
		boolean color; // 顏色
		T key; // 關鍵字

		public RBNode(T key, boolean color, RBNode<T> left, RBNode<T> right, RBNode<T> parent) {
			this.key = key;
			this.color = color;
			this.left = left;
			this.right = right;
			this.parent = parent;
		}
	}

左旋和右旋比較繞,需要仔細慢慢消化理解,我花了很長時間才搞明白。
一定要結合圖去寫代碼,否則必出錯(大神除外)

三、左旋

在這裏插入圖片描述
圖片源:https://blog.csdn.net/sd09044901guic/article/details/84955116#commentBox

private void leftRotate(RBNode<T> x) {

		// 首先設置x的右孩子y
		RBNode<T> y = x.right;
		y.parent = x;

		// 判斷x是否有父親
		if (x.parent == null) { // 沒有就把y變成父親
			y = root;
		} else {
			if (x == x.parent.left) { // 如果x是父親的左孩子,就把y變成父親的左孩子
				x.parent.left = y;
			} else if (x == x.parent.right) { // 同上
				x.parent.right = y;
			}
		}
		x.parent = y;
		x.right = y.left; // y的左孩子變成x的右孩子
		y.left = x; // x變成y的左孩子
	}

四、右旋

在這裏插入圖片描述

private void rightRotate(RBNode<T> y) {
		// 右旋,y是x的父親,y變成x的兒子,x右兒子變成y的左兒子

		// 設置y的左兒子x
		RBNode<T> x = y.left;
		x.parent = y;

		if (y.parent == null) { // y沒爸,就把x變成根節點
			x = root;
		} else {
			if (y == y.parent.left) { // 如果y是它爸的左兒子,就把x變成y爸的左兒子
				x = y.parent.left;
			} else if (y == y.parent.right) { // 同上
				x = y.parent.right;
			}
		}
		y.parent = x; // 把x變成y的父親
		y.left = x.right; // x右兒子給y做左兒子
		x.right = y; // y變成x的右兒子
	}

五、插入

首先要注意一個問題,插入的元素初始顏色都是紅色,因爲紅色不會違背性質五。這樣使需要討論的情況更少,問題更簡單,

插入要分情況討論,每種情況都要去滿足紅黑二叉樹的五條性質

  1. 父親結點是黑色
    - 此時直接滿足五條性質

  2. 父親節點是紅色(此時考慮叔叔結點)
    叔叔結點爲紅

    			1、父親、叔叔變黑
    			2、新節點、祖父變紅
    			3、由於祖父變紅,兩紅不相鄰,往上查找,循環使其滿足性質四
    

    叔叔結點爲黑

    		①父親是祖父右孩子,新節點是父親右孩子
    			1、父變黑
    			2、祖父變紅
    			3、祖父右旋
    			
    		②父親是祖父左孩子,新節點是父親右孩子
    			1、父親左旋
    			2、交換新節點與父親的位置
    			3、經過上兩步就變成了①,再用①操作即可
    			
    		③父親是祖父右孩子,新節點是父親右孩子
    			1、父變黑
    			2、祖父變紅
    			3、祖父左旋
    			
    		④父親是祖父右孩子,新節點是父親左孩子 
    			1、父右旋
    			2、交換新節點與父親位置
    			3、經過上兩步就變成了③,再用③操作即可
    

在這裏插入圖片描述

//定義一些常用的方法
private RBNode<T> parentOf(RBNode<T> node) {
		return node != null ? node.parent : null;
	}

	private boolean isRed(RBNode<T> node) {
		return ((node != null) && (node.color == RED)) ? true : false;
	}

	private void setBlack(RBTree<T>.RBNode<T> node) {
		if (node != null) {
			node.color = BLACK;
		}
	}

	private void setRed(RBNode<T> node) {
		if (node != null) {
			node.color = RED;
		}
	}

新插入的結點都先插到葉子節點處

//先進行插入,再去滿足性質
private void insert(RBNode<T> node) {
		int cmp;
		RBNode<T> y = null;
		RBNode<T> x = this.root;

		// 1、將紅黑樹當作一顆二叉查找樹,將結點添加到二叉查找樹中
		while (x != null) {
			y = x;
			cmp = node.key.compareTo(x.key);
			if (cmp < 0)
				x = x.left;
			else
				x = x.right;
		}

		node.parent = y;
		if (y != null) {
			cmp = node.key.compareTo(y.key);
			if (cmp < 0) {
				y.left = node;
			} else {
				y.right = node;
			}
		} else {
			this.root = node;
		}
		// 2、設置節點的顏色爲紅色
		node.color = RED;

		// 3、將他重新修正爲一顆二叉查找樹
		insertFixUp(node);
	}

下面對插入結點後的樹進行修正,使其變成合法的紅黑二叉樹

private void insertFixUp(RBNode<T> node) {
		RBNode<T> parent, gparent;
		// 當父親節點存在並且父親節點是黑色,則不用考慮,不影響
		// 若父節點存在並且父節點是紅色
		while (((parent = parentOf(node)) != null) && isRed(parent)) {
			/*
			 * 分幾種情況考慮: 
			 * 1、叔叔紅色 把叔叔、父親變成黑,祖父變紅(再往上判斷祖父的父親,循環) 
			 * 2、叔叔黑色
			 * (1.1)父親是左孩子,新插入結點是左孩子 
			 * (1.2)父親是左孩子,新節點是右孩子 
			 * (2.1)父親是右孩子,新插入結點是右孩子
			 * (2.2)父親是右孩子,新插入結點是左孩子
			 */
			gparent = parentOf(parent);
			//當父親是左孩子
			if (parent == gparent.left) {
				RBNode<T> uncle = gparent.right;
				// case 1:叔叔結點是紅色
				if ((uncle != null) && isRed(uncle)) {
					setBlack(uncle);
					setBlack(parent);
					setRed(gparent);
					node = gparent;
					continue;
				}

				//case2:叔叔是黑色,新節點是右孩子
				if(parent.right == node){
					RBNode<T> tmp;
					leftRotate(parent);
					tmp = parent;
					parent = node;
					node = tmp;
				}	//把這種情況變成case3
				
				// case3:叔叔是黑色,新節點是左孩子
				setBlack(parent);
				setRed(gparent);
				rightRotate(gparent);
			}else{
				//當父親是右孩子
				RBNode<T> uncle = gparent.left;				
				//case1:叔叔結點是紅色
				if((uncle!=null) && isRed(uncle)){
					setBlack(uncle);
					setBlack(parent);
					setRed(gparent);
					node = gparent;
					continue;
				}
				
				//case2:叔叔是黑色,當前結點是左孩子
				if(parent.left == node){
					RBNode<T> tmp;
					rightRotate(parent);
					tmp = parent;
					parent = node;
					node = tmp;
				}
				
				//case3:叔叔是黑色,當前節點是右孩子
				setBlack(parent);
				setRed(gparent);
				leftRotate(gparent);				
			}
		}

		//將根節點設置爲黑色
		setBlack(this.root);
	}

插入操作相對於刪除操作還要簡單一點,這篇就到這,下篇學習刪除操作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章