Java實現(xiàn)紅黑樹(平衡二叉樹)的詳細過程
前言
在實現(xiàn)紅黑樹之前,我們先來了解一下符號表。
符號表的描述借鑒了Algorithms第四版,詳情在:https://algs4.cs.princeton.edu/home/
符號表有時候被稱為字典,就如同英語字典中,一個單詞對應一個解釋,符號表有時候又被稱之為索引,即書本最后將術語按照字母順序列出以方便查找的那部分??偟膩碚f,符號表就是將一個鍵和一個值聯(lián)系起來,就如Python中的字典,JAVA中的HashMap和HashTable,Redis中以鍵值對的存儲方式。
在如今的大數(shù)據(jù)時代,符號表的使用是非常頻繁的,但在一個擁有著海量數(shù)據(jù)的符號表中,如何去實現(xiàn)快速的查找以及插入數(shù)據(jù)就是高效的算法去完成的事情,可以說沒有這些算法的發(fā)明產(chǎn)生,信息時代無從談起。
既然是數(shù)據(jù)結構去實現(xiàn)符號表,這就要求我們對符號表的API,也就是符號表的功能去定義,前面我們說到既然符號表的使用是如何在海量數(shù)據(jù)中去查找,插入數(shù)據(jù),那么我們便定義符號表的API有增刪改查這四個基本功能。
/** * <p> * 符號表的基本API * </p> * @author qzlzzz * @version 1.0 * @since 2021/10/8 */ public interface RedBlackBST<Key extends Comparable<Key>,Value> { /** * 根據(jù)Key在符號表中找到Value * @param key the key * @return the value of key */ Value get(Key key); /** * 插入key-value,如果符號表中有Key,且Key不為空則將該Key的Value轉為傳入的Value * @param key the-key * @param value the-value */ void put(Key key,Value value); /** * 根據(jù)Key去符號表中刪除Key * @param key the key */ void delete(Key key); }
這里由于紅黑樹是平衡二叉樹,即意味著其有平衡性和有序性,因為其有序性的特點,因此我們可以范圍或根據(jù)位置去需找鍵,也可以查找到樹中的最小鍵和最大鍵。
至于什么是平衡性,文章后講,這里先停一停。
因此我們可以額外的定義:
/** * 根據(jù)位置返回鍵,如果沒有返回null * @param k the index of key * @return the key */ Key select(int k); /** * 返回紅黑樹中最小的鍵 * @return the min key in this tree */ Key min(); /** * 返回紅黑樹中最大的鍵 * @return the max key in this tree */ Key max(); /** * 返回小于該鍵的數(shù)量 * @param key the key * @return amount of key small than the key */ int rank(Key key);
接下來我們說說紅黑樹。
紅黑二叉查找樹
紅黑二叉查找樹實際上基于二叉查找樹上實現(xiàn)了2-3樹,也就是說紅黑二叉查找樹是一個2-3樹。所以在認識紅黑二叉查找樹之前,我們得了解2-3樹的原理,以及組成結構。
2-3樹
我們把含有一個鍵,兩個鏈接的結點稱為2-結點,標準的二叉查找樹其每個結點都是2-結點,在考慮好的情況下,我們構造標準二叉查找樹,一般能夠得到樹高為總鍵樹的對數(shù)的一個查找樹,其查找和插入操作都是對數(shù)級別的,但標準二叉查找樹的基本實現(xiàn)的良好性能取決于鍵值對分布的足夠亂以致于打消長路徑帶來的問題,但我們不能保證插入情況是隨機的,如果鍵值對的插入時順序插入的,就會帶來下面的問題:
從圖中我們可以看到,我們將A,B,C,D,E按順序插入的話,會得到一個鍵值與樹高成正比的二叉查找樹,其插入和查找的會從對數(shù)級別提到O(N)級別。
當然我們希望的肯定是無論鍵值對的情況是怎樣的,我們都能構造一個樹高與總鍵數(shù)成對數(shù),插入查找等操作均能夠在對數(shù)時間內完成的數(shù)據(jù)結構。也就是說,在順序插入的情況下,我們希望樹高依然為~lgN,這樣我們就能保證所有的查找都能在~lgN次比較結束。
為了保證查找樹的平衡性,我們需要一些靈活性,因此在這里我們允許樹中的一個結點保存多個鍵,我們引入3-結點,所謂的3-結點就是一個結點中有2個鍵,3個鏈接。
因此一顆2-3查找樹或為一顆空樹,或由2-結點和3-結點組成。在介紹2-3樹的操作前,我們將A,B,C,D,E,F,G,H順序插入得到的樹如下圖所示:
從圖中我們可以看出2-3樹的平衡性,靈活性,它保證了任意的插入得到的樹高依舊是總鍵的對數(shù)。
2-3樹的插入操作
理解2-3樹的插入操作,有利于去構造紅黑樹,在這里分三種情況:
- 插入新鍵,底層結點是2-結點
- 插入新鍵,底層結點是3-結點,父結點是2-結點
- 插入新鍵,底層結點是3-結點,父結點是3-結點
第一種情況
若插入新鍵,底層結點是2-結點的話,該底層結點變?yōu)?-結點,將插入的鍵保存其中即可。
第二種情況
若插入新鍵,底層結點是3-結點,底層結點先變成臨時的4-結點(3個鍵,4條鏈接),后4-結點中的中鍵吐出,使得父節(jié)點由2-結點變?yōu)?-結點,原4-結點中鍵兩邊的鍵變成兩個2-結點,原本由父結點指向子結點的一個鏈接,替換為原4-結點中鍵左右兩邊的鏈接,分別指向兩個新的2-結點。
第三種情況
若插入新鍵,底層結點是3-結點,其父結點也是3-結點的話,使得底層結點變?yōu)榕R時的4-結點,后4-結點中的中鍵吐出,使得父節(jié)點由3-結點變?yōu)榕R時的4-結點,原4-結點中鍵兩邊的鍵變成兩個2-結點,原本由父結點指向子結點的一個鏈接,替換為原4-結點中鍵左右兩邊的鏈接,分別指向兩個新的2-結點,隨后父節(jié)點也要吐出中鍵,重復上述的步驟,如果父節(jié)點的父節(jié)點也是3-結點,則繼續(xù)持續(xù)上述步驟,若根結點也是3-結點,根節(jié)點吐出中鍵,生成兩個2-結點后,整個樹高+1,但各個底層結點到根結點的路徑始終相等。
以上的三種變化是2-3樹的動態(tài)變化的核心,非常關鍵,我們可以在推演的過程種看到這種變化是自下向上的,而且是局部的變化,這種局部的變化并沒有影響2-3樹的有序性和平衡性。
同時我們也可以看出,如果要以代碼來實現(xiàn)2-3樹的話相當?shù)穆闊?,因為需要處理的情況實在太多。我們需要維護兩種不同類型的結點,將被查找的鍵和結中的每個鍵進行比較,將鏈接和其他信息從一個結點復制到另一個結點。實現(xiàn)這些需要大量的代碼,實現(xiàn)的這些代碼所帶來開銷或許還會比標準二叉查找樹要多。因此后面人們想出了結合標準二叉樹來實現(xiàn)2-3樹的數(shù)據(jù)結構,這便是紅黑樹。
實現(xiàn)紅黑二叉樹
紅黑樹是基于標準二叉樹來實現(xiàn)的,它實現(xiàn)2-3樹的關鍵點在于它把二叉樹的鏈接分為了紅和黑。它將兩個用紅鏈相鏈接的結點看為3-結點,而黑鏈鏈接的結點則視為2-結點。這也意味著我們完全不用去重新寫一個紅黑樹的get()方法,只需要使用標準二叉樹的get()方法就可以實現(xiàn)查找,不同點在于,要在put()方法中改動一下便能夠去實現(xiàn)一個紅黑二叉查找樹。實現(xiàn)紅黑樹代碼改動量少,但其后面的思想其實很復雜,由于篇幅的原因,對紅黑樹如何去實現(xiàn)2-3樹的三種變化的原理就不做過多描述。
首先定義結點
/** * <h3> * 紅黑樹的實現(xiàn),博客:https://www.cnblogs.com/qzlzzz/p/15395010.html * </h3> * @author qzlzzz * @since 2021/10/12 * @version 1.0 */ public class RedBlackBST<Key extends Comparable<Key>,Value> { private Node root;//根節(jié)點 //<父結點>指向自己<子結點>的鏈接是黑色的 private static final boolean RED = true; //<父結點>指向自己<子結點>的鏈接是黑色的 private static final boolean BLACK = false; /** * <p>紅黑樹的結點定義</p> * @author qzlzzz */ private class Node{ private boolean color;//指向該結點的鏈接的顏色 private Key key;//鍵 private Value value;//值 private Node left,right;//該結點指向左結點的鏈接和右結點的鏈接 private int n;//該子樹的結點樹 public Node(Key key,Value value,boolean color,int n){ this.key = key; this.value = value; this.color = color; this.n = n; } } }
若紅鏈接為右鏈接,使鏈接轉左。
在這里我們需要保持紅鏈接為左鏈接。但使紅鏈接保持為右鏈接也行,只不過左鏈接更好實現(xiàn)。
/** * 計算紅黑樹的結點總數(shù),內部調用了{@link RedBlackBST#size(Node)} * @return */ public int size(){ return size(root); } //計算某個子樹的結點總數(shù) private int size(Node x){ if (x == null) return 0; else return x.n; } /** * 將紅色右鏈接變?yōu)樽箧溄?,總體有序性不變,子樹結點數(shù)量不變 * @param h * @return */ private Node rotateLeft(Node h){ Node t = h.right; h.right = t.left; t.left = h; t.color = h.color; h.color = RED; t.n = h.n;//轉換后子樹的結點是不變的, h.n = size(h.left) + size(h.right) + 1; return t; }
轉換的代碼圖是這樣的:
這里的1 2 3指的是鍵的大小,并不是值,紅黑樹各個底層到根節(jié)點的黑鏈接總數(shù)的相同的,這符合了2-3樹中各個底層結點到根節(jié)點的距離相等。
這里將紅左鏈接轉換為右鏈接的思想是一樣的,讀者可以自己嘗試去實現(xiàn)。
判斷鏈接是否為紅鏈接
//判斷鏈接是否為紅色,不是返回false private boolean isRed(Node x){ if (x == null) return false; return x.color; }
若左右兩邊的鏈接皆為紅色,將兩邊鏈接顏色設置為黑色,并使指向自己鏈接的顏色設為紅
/** * <p>若左右兩邊的鏈接皆為紅色,將兩邊鏈接顏色設置為黑色,并使指向自己鏈接的顏色設為紅</p> * @param x */ private void changeColor(Node x){ x.color = true; x.left.color = false; x.right.color = true; }
為什么要這樣呢?
其實跟上述2-3樹的第二個操作脫不開關系。當結點為臨時4-結點時,吐出中鍵,兩邊的鍵變?yōu)閮蓚€2-結點,原指向臨時4-結點的鏈接變?yōu)樵?-結點中間兩邊的鏈接并指向新的2-結點,如果父結點為2-結點,則于原4-中鍵一起變成3-結點,若父節(jié)點是3-結點,則循環(huán)上述操作,由于我們要保持紅鏈接為做鏈接,中途若有右紅鏈接產(chǎn)生還需要使用rotateLeft()方法去轉換。
接下來讓我們以紅黑二叉樹實現(xiàn)符號表的get、put
/** * 通過鍵來查找值,內部調用{@link RedBlackBST#get(Node, Comparable)} * @param key * @return */ public Value get(Key key){ if (key == null) throw new IllegalArgumentException("argument to get() is null"); return get(root,key); } private Value get(Node x,Key key){ for (;;){ if (x == null) return null; int cmp = key.compareTo(x.key); if (cmp == 0) return x.value; else if (cmp < 0) x = x.left; else x = x.right; } }
/** * 插入鍵值對,內部使用{@link RedBlackBST#put(Node, Comparable, Object)} * @param key * @param value */ public void put(Key key,Value value){ if (key == null) throw new IllegalArgumentException("argument to put() is null"); root = put(root,key,value); root.color = false; } private Node put(Node x,Key key,Value value){ if (x == null) return new Node(key,value,RED,1); int cmp = key.compareTo(x.key); if (cmp == 0) {x.value = value;} else if (cmp < 0) x.left = put(x.left,key,value); else x.right = put(x.right,key,value); if (isRed(x.right) && !isRed(x.left)) x = rotateLeft(x); if (isRed(x.left) && isRed(x.left.left)) x = rotateRight(x); if (isRed(x.left) && isRed(x.right)) changeColor(x); x.n = size(x.left) + size(x.right) + 1; return x; }
至于put方法,后面的三個if語句則是:
- 當前結點的右鏈接為紅色的話,將其轉為左紅鏈接。當左右鏈接皆為紅色,調用changeColor()方法,使得其完成2-3樹的局部動態(tài)變化,也就是上述說的2-3樹的插入新鍵,底層結點是3-結點,父結點是2-結點的操作。
- 當前結點的左鏈接,以及左鏈接的左連接都為紅色的話,說明這是一個臨時的4-結點,我們需要將第一個左紅鏈接轉為右紅鏈接,然后得到一個左右鏈接都為紅的子樹,調用changeRed()方法使得其完成2-3樹的局部動態(tài)變化,也就是上述說的2-3樹的插入新鍵,底層結點是3-結點,父結點是2-結點的操作。
- 當左右鏈接都為紅色,調用changeColor()方法。
最后實現(xiàn)符號表的rank,select
/** * 根據(jù)位置返回鍵,內部調用{@link RedBlackBST#select(Node, int)} * @param k * @return */ public Key select(int k){ return select(root,k); } private Key select(Node x,int k){ while(x != null){ int t = x.left.N; if (t > k) x = x.left; else if (t < k){ x = x.right; k = k - t - 1; } else return x.key; } return null; } /** * 根據(jù)鍵,返回該鍵的數(shù)量,內部調用{@link RedBlackBST#rank(Node, Comparable)} * @param key * @return */ public int rank(Key key){ return rank(root,key); } private int rank(Node x,Key key){ while (x != null){ int cmp = key.compareTo(x.key); int count = x.left.N; if (cmp == 0) return (count < root.N ? count : 1 + root.left.N + count); else if (cmp < 0) x = x.left; else x = x.right; } return 0; }
最后以紅黑二叉樹的符號表實現(xiàn)完成了,讀者也可以嘗試將put()方法中的后三個語句放在判斷結點x為空的語句后面,有意思的是,此樹會變成一個2-3-4樹,也就說存在4-結點的一顆樹。
結尾
感謝zsh帥哥,若本文有什么需要改進或不足的地方請聯(lián)系我。
本文參考了:https://algs4.cs.princeton.edu/30searching/
到此這篇關于Java實現(xiàn)紅黑樹(平衡二叉樹)的文章就介紹到這了,更多相關Java實現(xiàn)紅黑樹(平衡二叉樹)內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Hibernate中Session.get()方法和load()方法的詳細比較
今天小編就為大家分享一篇關于Hibernate中Session.get()方法和load()方法的詳細比較,小編覺得內容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03詳解Java中super的幾種用法并與this的區(qū)別
這篇文章主要介紹了Java中super的幾種用法并與this的區(qū)別,有需要的朋友可以參考一下2013-12-12Docker?DockerFile部署java?jar項目包及Mysql和Redis的詳細過程
Dockerfile是一種用于構建Docker鏡像的文件格式,可以通過Dockerfile部署Java項目,這篇文章主要給大家介紹了關于Docker?DockerFile部署java?jar項目包及Mysql和Redis的詳細過程,需要的朋友可以參考下2023-12-12springBoot啟動時讓方法自動執(zhí)行的幾種實現(xiàn)方式
這篇文章主要介紹了springBoot啟動時讓方法自動執(zhí)行的幾種實現(xiàn)方式,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-03-03