一直以來,我都很少使用也避免使用到樹和圖,總覺得它們神秘而又復(fù)雜,但是樹在一些運(yùn)算和查找中也不可避免的要使用到,那么今天我們就鼓起勇氣來學(xué)習(xí)下樹,爭取在被問到和使用時(shí)不再那么慫。
1. 什么是二叉樹?
二叉樹也是一棵樹(這不是廢話么,雖說是廢話,但這是事實(shí)),但它的每個(gè)節(jié)點(diǎn)最多只有2個(gè)兒子(這個(gè)才是重點(diǎn))。
1.1 如何實(shí)現(xiàn)
在了解了二叉樹的概念后,我們?nèi)绾味x一顆樹的節(jié)點(diǎn)?so easy.
// 節(jié)點(diǎn)
public class BinaryNode {
// 存放的信息
Object data;
// 左兒子
BinaryNode left;
// 右兒子
BinaryNode right;
}
1.2 三種遍歷算法
構(gòu)造完樹之后,不可避免的就是讀取(或者叫做遍歷,不然存儲(chǔ)成這樣干哈啊)。
遍歷是對(duì)樹的一種最基本的運(yùn)算,所謂遍歷二叉樹,就是按一定的規(guī)則和順序走遍二叉樹的所有結(jié)點(diǎn),使每一個(gè)結(jié)點(diǎn)都被訪問一次,而且只被訪問一次。
由于二叉樹是非線性結(jié)構(gòu),因此,樹的遍歷實(shí)質(zhì)上是將二叉樹的各個(gè)結(jié)點(diǎn)轉(zhuǎn)換成為一個(gè)線性序列來表示。
對(duì)于樹的遍歷,按照訪問根節(jié)點(diǎn)的次序不同,主要有以下三種遍歷算法:
- 先序遍歷
- 后序遍歷
- 中序遍歷
除了上面三種遍歷,我們還會(huì)帶大家學(xué)習(xí)下深度優(yōu)先遍歷和廣度優(yōu)先遍歷。
我們首先給出一個(gè)假設(shè):
L:左子樹
D:根
R:右子樹
1.2.1 先序遍歷(DLR)
先序遍歷:根節(jié)點(diǎn)->左子樹->右子樹
算法的簡單實(shí)現(xiàn)如下:
public static void DLR(BinaryNode node) {
// 訪問根節(jié)點(diǎn)
System.out.print(node.data + " ");
// 遍歷左子樹
if (node.left != null) {
DLR(node.left);
}
// 遍歷右子樹
if (node.right != null) {
DLR(node.right);
}
}
1.2.2 后序遍歷(LRD)
后序遍歷:左子樹->右子樹->根節(jié)點(diǎn)
public static void LRD(BinaryNode node) {
// 遍歷左子樹
if (node.left != null) {
LRD(node.left);
}
// 遍歷右子樹
if (node.right != null) {
LRD(node.right);
}
// 訪問根節(jié)點(diǎn)
System.out.print(node.data + " ");
}
1.2.3 中序遍歷(LDR)
中序遍歷:左子樹->根節(jié)點(diǎn)->右子樹
public static void LDR(BinaryNode node) {
// 遍歷左子樹
if (node.left != null) {
LDR(node.left);
}
// 訪問根節(jié)點(diǎn)
System.out.print(node.data + "");
// 遍歷右子樹
if (node.right != null) {
LDR(node.right);
}
}
1.2.4 深度優(yōu)先遍歷
英文縮寫為DFS即Depth First Search.其過程簡要來說是對(duì)每一個(gè)可能的分支路徑深入到不能再深入為止,而且每個(gè)節(jié)點(diǎn)只能訪問一次。
深度優(yōu)先遍歷需要使用到棧這種數(shù)據(jù)結(jié)構(gòu),棧具有先進(jìn)后出的特點(diǎn)。
如上圖,我們來分析下深度優(yōu)先遍歷的過程。
- 首先根節(jié)點(diǎn)A入棧,stack(A)。
- 將A節(jié)點(diǎn)彈出,因?yàn)锳存在 B C兩個(gè)子節(jié)點(diǎn),根據(jù)定義和棧特點(diǎn),首先將C(右兒子)壓入棧中,然后將B(左兒子)壓入棧中,stack(C B)
- 彈出棧頂元素B節(jié)點(diǎn)彈出,將節(jié)點(diǎn) E 和 D壓入棧中,stack(C E D)。
- 彈出棧頂元素D,由于節(jié)點(diǎn)D只存在一個(gè)子節(jié)點(diǎn)H,因此H直接入棧,stack(C E H).
- 彈出棧頂元素H,元素H不存在子元素,stack(C E).
- 彈出棧頂元素E,元素E不存在子元素,stack(C).
- 彈出棧頂元素C,子節(jié)點(diǎn)G F分別入棧,stack(G F).
- F出棧,stack(G)。
- G出棧,stack()。
- 遍歷結(jié)束。
深度優(yōu)先遍歷的結(jié)果為: A B D H E C F G.
通過上面的分析,是不是覺得深度優(yōu)先遍歷其實(shí)也沒那么難啊,下面我們來動(dòng)手實(shí)現(xiàn)這段代碼(過程理解清楚了,代碼實(shí)現(xiàn)起來很簡單)。
private void depthFirst(AVLTreeNode<T> node) {
if (node == null) {
return;
}
Stack<AVLTreeNode> stack = new Stack<>();
// 根節(jié)點(diǎn)入棧,然后進(jìn)行后續(xù)操作
stack.push(node);
while (!stack.isEmpty()) {
AVLTreeNode root = stack.pop();
// 彈出棧頂元素,進(jìn)行訪問。
System.out.println(root.key + " ");
// 首先將右節(jié)點(diǎn)入棧
if (root.right != null) {
stack.push(node.right);
}
// 然后左節(jié)點(diǎn)入棧
if (root.left != null) {
stack.push(node.left);
}
}
}
1.2.5 廣度優(yōu)先遍歷
英文縮寫為BFS即Breadth FirstSearch。其過程檢驗(yàn)來說是對(duì)每一層節(jié)點(diǎn)依次訪問,訪問完一層進(jìn)入下一層,而且每個(gè)節(jié)點(diǎn)只能訪問一次。對(duì)于上面的例子來說,廣度優(yōu)先遍歷的 結(jié)果是:A,B,C,D,E,F,G,H(假設(shè)每層節(jié)點(diǎn)從左到右訪問)。
廣度優(yōu)先遍歷需要使用到隊(duì)列這種數(shù)據(jù)結(jié)構(gòu),隊(duì)列具有先進(jìn)先出的特點(diǎn)。
如上圖所示,我們來分析廣度優(yōu)先遍歷的過程。
- 首先將A節(jié)點(diǎn)插入隊(duì)列中,queue(A);
- 將A節(jié)點(diǎn)彈出,同時(shí)將A的子節(jié)點(diǎn)B,C插入隊(duì)列中,此時(shí)B在隊(duì)列首,C在隊(duì)列尾部,queue(B,C);
- 將B節(jié)點(diǎn)彈出,同時(shí)將B的子節(jié)點(diǎn)D,E插入隊(duì)列中,此時(shí)C在隊(duì)列首,E在隊(duì)列尾部,queue(C,D,E);
- 將C節(jié)點(diǎn)彈出,同時(shí)將C的子節(jié)點(diǎn)F,G插入隊(duì)列中,此時(shí)D在隊(duì)列首,G在隊(duì)列尾部,queue(D,E,F(xiàn),G);
- 將D節(jié)點(diǎn)彈出,同時(shí)將D節(jié)點(diǎn)的子節(jié)點(diǎn)H插入隊(duì)列中,此時(shí)E在隊(duì)列首,H在隊(duì)列尾部,queue(E,F(xiàn),G,H);
- E F G H分別彈出(這四個(gè)節(jié)點(diǎn)均不存在子節(jié)點(diǎn))。
廣度優(yōu)先遍歷結(jié)果為:A B C D E F G H
動(dòng)手來實(shí)現(xiàn)廣度優(yōu)先遍歷,代碼如下:
public void breadthFirst() {
breadthFirst(root);
}
private void breadthFirst(AVLTreeNode<T> node) {
if (node == null) {
return;
}
Queue<AVLTreeNode> queue = new ArrayDeque<>();
// 根節(jié)點(diǎn)入棧
queue.add(node);
while (!queue.isEmpty()) {
AVLTreeNode root = queue.poll();
System.out.print(node.key + " ");
if (root.left != null) {
queue.add(node.left);
}
if (root.right != null) {
queue.add(node.right);
}
}
}
1.3 示例
光說不練假把式,本章節(jié)我們來舉個(gè)簡單的例子來實(shí)戰(zhàn)下。
1.3.1 構(gòu)造一棵樹
以上圖表達(dá)式二叉樹為例,我們來看下1.2章節(jié)三個(gè)遍歷算法的輸出結(jié)果:
先序遍歷:+ + a * b c * + * d e f g
中序遍歷:a + b * c + d * e + f * g
后序遍歷:a b c * + d e * f + g * +
1.3.2 根據(jù)表達(dá)式構(gòu)造樹
現(xiàn)在假如我們拿到了一個(gè)后綴表達(dá)式的輸入:a b c * + d e * f + g * + ,那么如何根據(jù)它來構(gòu)造樹呢?
我們前面已經(jīng)分析了后序遍歷算法(LRD),其實(shí)這就是一個(gè)逆序的過程。
我們一次一個(gè)符號(hào)讀入表達(dá)式,如果符號(hào)為操作數(shù),那么我們就新建一個(gè)節(jié)點(diǎn)并入棧;如果符號(hào)為操作符,我們就從棧中彈出兩個(gè)操作數(shù)T1和T2(T1先彈出)并形成一棵新的樹,該樹的根就是操作符,它的左右子樹分別為T2和T1,然后將該樹壓入棧中。
現(xiàn)在輸入為:a b + c d e + * *
很明顯前兩個(gè)為操作數(shù),因此創(chuàng)建兩個(gè)節(jié)點(diǎn)并將它們壓入棧中。
接著'+'被讀取,因此兩棵樹被彈出,一顆新的樹形成,并被壓入棧中。
然后,c、d、e分別被讀入,在每個(gè)節(jié)點(diǎn)被創(chuàng)建后,被壓入棧中。
然后'+'被讀入,因此我們彈出兩個(gè)節(jié)點(diǎn)形成一棵新的樹并壓入棧中。
繼續(xù)讀取,‘*’被讀入,我們彈出兩顆樹形成一棵新的樹并壓入棧中。
最后,讀入最后一個(gè)符號(hào),兩顆樹合并,最后的樹留在棧中。
下面,我們將上面的分析過程轉(zhuǎn)換為代碼:
public static BinaryNode buildTreeFromLRD(String content) {
Stack<BinaryNode> stack = new Stack();
char[] chars = content.toCharArray();
for (int index = 0; index < chars.length; index++) {
char data = chars[index];
if (data >= 'a' && data <= 'z') {
BinaryNode numNode = new BinaryNode();
numNode.data = String.valueOf(data);
stack.push(numNode);
} else {
BinaryNode root = new BinaryNode();
root.data = String.valueOf(data);
root.right = stack.pop();
root.left = stack.pop();
stack.push(root);
}
}
return stack.pop();
}
1.3.3 求表達(dá)式的值
求表達(dá)式的值是我們學(xué)習(xí)中或者面試中一個(gè)不可避免的知識(shí)點(diǎn),本章節(jié)就結(jié)合二叉樹的遍歷,教你如何輕松實(shí)現(xiàn)表達(dá)式求值。
我們的輸入一般為中綴表達(dá)式(我們二叉樹中序遍歷的結(jié)果),由于表達(dá)式可能存在(),因此可能會(huì)出現(xiàn)優(yōu)先級(jí)的問題。而如果將其轉(zhuǎn)換為后綴表達(dá)式,則無需考慮運(yùn)算符優(yōu)先級(jí)的問題。
因此本章節(jié)講解兩個(gè)部分的內(nèi)容:
- 將包含()的中綴表達(dá)式轉(zhuǎn)換為后綴表達(dá)式
- 求解后綴表達(dá)的值
1.3.3.1 中綴->后綴
中綴表達(dá)式轉(zhuǎn)換為后綴表達(dá)式時(shí),主要使用棧這種結(jié)構(gòu),并根據(jù)一定的規(guī)則進(jìn)行處理。
中綴表達(dá)式示例:a + b * c + ( d * e + f ) * g
期望的后綴表達(dá)式:a b c * + d e * f + g * +
我們使用op棧來存儲(chǔ)操作符,使用re棧來存儲(chǔ)操作數(shù)。
中綴表達(dá)式轉(zhuǎn)換為后綴表達(dá)式最為重要的就遇到一個(gè)操作數(shù)或者運(yùn)算符的時(shí)候應(yīng)該怎么處理, 也就是優(yōu)先級(jí)的確認(rèn)。
遇到操作數(shù)的時(shí)候直接推進(jìn)re棧。
遇到運(yùn)算符的時(shí)候,需要比較該運(yùn)算符與op棧頭元素的運(yùn)算符的優(yōu)先級(jí),如果該運(yùn)算符優(yōu)先級(jí)高于棧頂運(yùn)算符(不包括等于)則將該運(yùn)算符推進(jìn)op, 否則彈出op棧的頭元素直到頭元素運(yùn)算符的優(yōu)先級(jí)低于該運(yùn)算符,最后將該運(yùn)算符推進(jìn)op中。
當(dāng)然有特殊情況,也就是遇到左括號(hào)“(”的時(shí)候直接推進(jìn)op, 當(dāng)其他運(yùn)算符與左括號(hào)比較優(yōu)先級(jí)的時(shí)候可認(rèn)為左括號(hào)的優(yōu)先級(jí)最低。遇到右括號(hào)的時(shí)候,不斷彈出棧的頭元素直到遇到左括號(hào),左括號(hào)彈出后不入re棧(后綴表達(dá)式不需要括號(hào))。
接下來我們詳細(xì)討論每種情況:
“+”:優(yōu)先級(jí)是最低的,幾乎所有運(yùn)算符都要彈出(包括加號(hào)自身),除了左括號(hào)。
“-”:與加號(hào)優(yōu)先級(jí)相同,同上。
“*”:優(yōu)先級(jí)稍高,只有遇到乘號(hào),除號(hào),以及冪號(hào)的時(shí)候才需要彈出。
“/”:與乘號(hào)優(yōu)先級(jí)相同,同上。
“^”:優(yōu)先級(jí)最高,只有遇到它自身的時(shí)候才需要彈出。
首先符號(hào)a被讀入,它被存入re棧;然后‘+’被讀入并被放入op棧中;接下來b讀入并流向re棧。
op棧:+
re棧:a b‘ * ’被讀入,因?yàn)閛p棧棧頂元素[+]優(yōu)先級(jí)比[ * ]低,因此re棧無輸出,且*入op棧。
op棧:+ *
re棧:a b c‘+’被讀入。因?yàn)閇+]優(yōu)先級(jí)低于op棧棧頂元素[ * ],因此將[ * ]號(hào)從op棧彈出并輸出到re棧;現(xiàn)在op棧剩下了[+],由于讀入的[+]不比op棧中的[+]優(yōu)先級(jí)低而是具備相同的優(yōu)先級(jí),因此彈出op棧中的[+]并輸出到re棧,并將讀取到的[+]輸入到op棧。
op棧:+
re棧:a b c * +‘(’被讀入。直接輸入到op棧中。
op棧:+ (
re棧:a b c * +d被讀入。直接將d輸出到re棧中。
op棧:+ (
re棧:a b c * + d‘*’被讀入。由于除非在處理閉括號(hào)否則開括號(hào)不會(huì)從棧中彈出,因此[ * ]直接輸出到op棧。
op棧:+ ( *
re棧:a b c * + d‘+’被讀入。比較優(yōu)先級(jí)'+'<' * ',‘ * ’從op棧彈出輸出至re棧,‘+’棧輸出至op棧。
op棧:+ ( +
re棧:a b c * + d *f被讀入。直接將f輸出到re棧中。
op棧:+ ( +
re棧:a b c * + d * f‘)’被讀入。遇到了),因此將op棧中元素彈出,直到最近的(彈出。
op棧:+
re棧:a b c * + d * f +' * '被讀入。' * '>'+',*號(hào)直接入op棧。
op棧:+ *
re棧:a b c * + d * f +g被讀入。直接將g輸出到re棧。
op棧:+ *
re棧:a b c * + d * f + g表達(dá)式已經(jīng)讀取完畢,將op棧中的元素出棧,并輸出到re棧中。
op棧:
re棧:a b c * + d * f + g * +
過程已經(jīng)清楚了,那么下面讓我們動(dòng)手來實(shí)現(xiàn)這個(gè)轉(zhuǎn)換過程。(只處理了簡單的加減乘除和()的情況)
/**
* 將中綴表達(dá)式轉(zhuǎn)換為后綴表達(dá)式
*
* @param midStat 中綴表達(dá)式
* @return 轉(zhuǎn)換后的后綴表達(dá)式
*/
public static String convertMid2Suffix(String midStat) {
Stack<Character> op = new Stack<>();
Stack<Character> re = new Stack<>();
char[] chars = midStat.toCharArray();
for (int index = 0; index < chars.length; index++) {
char data = chars[index];
if (data >= 'a' && data <= 'z' || data >= '0' && data <= '9') {
// 操作數(shù)直接進(jìn)入re棧
re.push(data);
} else {
// 棧為空時(shí),直接入棧
if (op.size() == 0) {
op.push(data);
continue;
}
// 處理運(yùn)算符優(yōu)先級(jí)
while (op.size() > 0) {
// peek方法只讀取,不出棧
Character exist = op.peek();
// 處理特殊情況,讀入的為左括號(hào)時(shí),直接入op棧
if (data == '(') {
op.push(data);
break;
}
// 讀入為右括號(hào)時(shí),op出棧輸出到re棧,直到最近的左括號(hào)彈出為止
if (data == ')') {
if (exist == '(') {
op.pop();
break;
}
re.push(exist);
op.pop();
continue;
}
if (exist == '(') {
op.push(data);
break;
}
int compareResult = compareOp(data, exist);
// 當(dāng)前讀入的運(yùn)算符大于op棧棧頂?shù)倪\(yùn)算符,直接輸入到op棧
if (compareResult > 0) {
op.push(data);
break;
}
// 讀入運(yùn)算符與op棧棧頂運(yùn)算符優(yōu)先級(jí)相同
if (compareResult == 0) {
// op棧棧頂運(yùn)算符彈出,并輸出至re棧中
re.push(exist);
op.pop();
if (op.size() == 0) {
op.push(data);
break;
}
}
if (compareResult < 0) {
// op棧棧頂運(yùn)算符彈出,并輸出至re棧中
re.push(exist);
op.pop();
if (op.size() == 0) {
op.push(data);
break;
}
}
}
}
}
while(op.size()>0){
re.push(op.pop());
}
StringBuilder result = new StringBuilder();
while (re.size() > 0) {
result.append(re.pop());
}
return result.reverse().toString();
}
public static int compareOp(Character left, Character right) {
String pmOp = "-+";
String mdOp = "*/";
if (left == right) {
return 0;
}
if (pmOp.indexOf(left) >= 0 && pmOp.indexOf(right) >= 0) {
return 0;
}
if (mdOp.indexOf(left) >= 0 && mdOp.indexOf(right) >= 0) {
return 0;
}
if (pmOp.indexOf(left) >= 0 && mdOp.indexOf(right) >= 0) {
return -1;
}
if (mdOp.indexOf(left) >= 0 && pmOp.indexOf(right) >= 0) {
return 1;
}
return 0;
}
1.3.3.2 后綴表達(dá)式求值
在上個(gè)章節(jié),我們詳細(xì)分析了如何將我們?nèi)粘R姷乃阈g(shù)表達(dá)式轉(zhuǎn)換為后綴表達(dá)式,接下來我們來分析下如何求解后綴表達(dá)式的值。
后綴表達(dá)式:6 5 2 3 + 8 * + 3 + *
中綴表達(dá)式為:6 * ((5+(2+3) * 8)+3)
照例我們來分析下求解的過程:
6523 入棧
re棧:6 5 2 3+讀入。此時(shí)2 3出棧,進(jìn)行+運(yùn)算,并將結(jié)果入棧。
re棧:6 5 58讀入。
re棧:6 5 5 8*讀入。此時(shí)5 8出棧,進(jìn)行 * 運(yùn)算后將結(jié)果入棧。
re棧:6 5 40+讀入。此時(shí)5 40出棧,進(jìn)行+運(yùn)算后入棧。
re棧:6 453讀入。
re棧:6 45 3+讀入。此時(shí)45 3出棧,進(jìn)行+運(yùn)算后入棧。
re棧:6 48'*' 入棧。此時(shí)6 48 出棧,進(jìn)行 * 運(yùn)算后結(jié)果入棧。
re棧:288288出棧,運(yùn)算結(jié)束。
讓我們動(dòng)手來實(shí)現(xiàn)這段代碼吧。
public static Integer computeSuffixStat(String statement) {
Stack<Integer> re = new Stack<>();
char[] chars = statement.toCharArray();
for (int index = 0; index < chars.length; index++) {
char charData = chars[index];
if (charData >= '0' && charData <= '9') {
Integer data = Integer.parseInt(String.valueOf(charData));
re.push(data);
}
if (charData == '+') {
Integer result = re.pop() + re.pop();
re.push(result);
}
if (charData == '-') {
Integer aft = re.pop();
Integer pre = re.pop();
Integer result = pre - aft;
re.push(result);
}
if (charData == '*') {
Integer result = re.pop() * re.pop();
re.push(result);
}
if (charData == '/') {
Integer aft = re.pop();
Integer pre = re.pop();
Integer result = pre / aft;
re.push(result);
}
}
System.out.println(re.pop());
return re.pop();
}
2.2 完全二叉樹
定義:若設(shè)二叉樹的深度為h,除第 h 層外,其它各層 (1~h-1) 的結(jié)點(diǎn)數(shù)都達(dá)到最大個(gè)數(shù),第 h 層所有的結(jié)點(diǎn)都連續(xù)集中在最左邊,這就是完全二叉樹。
完全二叉樹有如下特點(diǎn):
- 只允許最后一層有空缺結(jié)點(diǎn)且空缺在右邊,即葉子結(jié)點(diǎn)只能在層次最大的兩層上出現(xiàn)。
- 對(duì)任一結(jié)點(diǎn),如果其右子樹的深度為j,則其左子樹的深度必為j或j+1。 即度為1的點(diǎn)只有1個(gè)或0個(gè)。
2.2.1 分析
出于簡便起見,完全二叉樹通常采用數(shù)組而不是鏈表存儲(chǔ),其存儲(chǔ)結(jié)構(gòu)如下:
var tree:array[1..n]of longint;{n:integer;n>=1}
對(duì)于tree[i],有如下特點(diǎn):
(1)若i為奇數(shù)且i>1,那么tree的左兄弟為tree[i-1];
(2)若i為偶數(shù)且i<n,那么tree的右兄弟為tree[i+1];
(3)若i>1,tree的父親節(jié)點(diǎn)為tree[i div 2];
(4)若2i<=n,那么tree的左孩子為tree[2i];若2i+1<=n,那么tree的右孩子為tree[2i+1];
(5)若i>n div 2,那么tree[i]為葉子結(jié)點(diǎn)(對(duì)應(yīng)于(3));
(6)若i<(n-1) div 2.那么tree[i]必有兩個(gè)孩子(對(duì)應(yīng)于(4))。
(7)滿二叉樹一定是完全二叉樹,完全二叉樹不一定是滿二叉樹。
完全二叉樹第i層至多有2(i-1)個(gè)節(jié)點(diǎn),共i層的完全二叉樹最多有2i-1個(gè)節(jié)點(diǎn)。
2.2.2 滿二叉樹
除最后一層無任何子節(jié)點(diǎn)外,每一層上的所有結(jié)點(diǎn)都有兩個(gè)子結(jié)點(diǎn)二叉樹。
國內(nèi)教程定義:一個(gè)二叉樹,如果每一個(gè)層的結(jié)點(diǎn)數(shù)都達(dá)到最大值,則這個(gè)二叉樹就是滿二叉樹。也就是說,如果一個(gè)二叉樹的層數(shù)為K,且結(jié)點(diǎn)總數(shù)是(2^k) -1 ,則它就是滿二叉樹。
滿二叉樹是一種特俗的完全二叉樹。
2.3 ADT-二叉查找樹
二叉樹的一個(gè)重要的應(yīng)用就是它們在查找中的使用。假設(shè)每個(gè)節(jié)點(diǎn)存儲(chǔ)一項(xiàng)數(shù)據(jù)。
使二叉樹成為二叉查找樹的性質(zhì)是:對(duì)于二叉樹中的每個(gè)節(jié)點(diǎn)X,它的左子樹中所有項(xiàng)的值都小于X中的項(xiàng),它的右子樹中所有項(xiàng)的值大于X中的項(xiàng)。注意,這意味著該樹所有的元素可以使用某種一致的方式排序。
二叉查找樹的平均深度為O(logN),所以一般不必?fù)?dān)心棧空間被用盡,我們一般使用遞歸來處理查找、刪除等操作。
二叉查找樹要求所有項(xiàng)均能排序,這就要求我們實(shí)現(xiàn)Comparable接口。該接口告訴我們,樹中任意兩項(xiàng)都可以使用compareTo方法進(jìn)行比較。
2.3.1 二叉查找樹定義
根據(jù)二叉查找樹的要求,我們首先讓BinaryNode實(shí)現(xiàn)Comparable接口。
// 樹節(jié)點(diǎn)
class BinaryNode implements Comparable {
Integer element;
BinaryNode left;
BinaryNode right;
public BinaryNode{
}
public BinaryNode(Integer element, BinaryNode left, BinaryNode right) {
this.element = element;
this.left = left;
this.right = right;
}
@Override
public int compareTo(@NonNull Object o) {
return this.element - (Integer) o;
}
}
下面我們完成對(duì)BinarySearchTree的定義(只定義關(guān)鍵輪廓,具體接口后續(xù)進(jìn)行分析)。
public class BinarySearchTree {
//定義樹的根節(jié)點(diǎn)
BinaryNode root;
public BinarySearchTree() {
this.root = null;
}
public void makeEmpty() {
this.root = null;
}
public boolean isEmpty() {
return this.root == null;
}
// 判斷是否包含某個(gè)元素
public boolean contains(Integer x) {
//TODO:后續(xù)講解
return false;
}
// 查找最小值
public BinaryNode findMin(){
//TODO:后續(xù)講解
return null;
}
// 查找最大值
public BinaryNode findMax(){
//TODO:后續(xù)講解
return null;
}
// 按照順序插入值
public void insert(Integer x){
//TODO:后續(xù)講解
}
// 刪除某個(gè)值
public void remove(Integer x){
//TODO:后續(xù)講解
}
// 打印樹
public void printTree(){
//TODO:后續(xù)講解
}
}
2.3.2 contains-查找
如果在樹T中包含項(xiàng)X的節(jié)點(diǎn),那么該操作返回true,否則返回false。
樹的結(jié)構(gòu)使得這種操作變得非常簡單,如果T為空集,直接返回false;否則我們就對(duì)T的左子樹或右子樹進(jìn)行遞歸查找,直到找到該項(xiàng)X。
代碼實(shí)現(xiàn)如下:
/**
* 是否包含某個(gè)元素
* @param x 待查找對(duì)象
* @return 查找結(jié)果
*/
public boolean contains(Integer x) {
// 首次查找從根節(jié)點(diǎn)開始
return contains(x,root);
}
private boolean contains(Integer x, BinaryNode node) {
// 根節(jié)點(diǎn)為空的情況,不需要再查找
if (node == null) {
return false;
}
// 與當(dāng)前節(jié)點(diǎn)進(jìn)行比較
int compareResult = x.compareTo(node.element);
// 小于當(dāng)前節(jié)點(diǎn)的值,就遞歸遍歷左子樹
if (compareResult < 0) {
return contains(x, node.left);
}
// 大于當(dāng)前節(jié)點(diǎn)的值,就遞歸遍歷右子樹
else if (compareResult > 0) {
return contains(x, node.right);
}
// 等于當(dāng)前節(jié)點(diǎn)值,直接返回
else {
return true;
}
}
2.3.3 查找最小值節(jié)點(diǎn)
從根開始并且只要有左子樹就向左進(jìn)行,終止點(diǎn)就是最小元素節(jié)點(diǎn)。
// 查找最小值
public BinaryNode findMin() {
return findMin(root);
}
private BinaryNode findMin(BinaryNode node) {
// 當(dāng)前節(jié)點(diǎn)為null,直接返回null
if (node == null) {
return null;
}
// 不存在左子樹,返回當(dāng)前節(jié)點(diǎn)
if (node.left == null) {
return node;
}
// 遞歸遍歷左子樹
return findMin(node.left);
}
2.3.4 查找最大值節(jié)點(diǎn)
從根開始并且只要有右子樹就向右進(jìn)行,終止點(diǎn)就是最大元素節(jié)點(diǎn)。作為與findMin的對(duì)比,findMax方法我們拋棄了常用的遞歸,使用了常見的while循環(huán)來查找。
// 查找最大值
public BinaryNode findMax() {
return findMax(root);
}
private BinaryNode findMax(BinaryNode node) {
if (node != null) {
while (node.right != null) {
node = node.right;
}
}
return node;
}
2.3.5 insert-插入
插入操作在概念上是簡單的,為了將X插入樹T中,我們像contains一樣沿著樹查找。
如果找到X,那么我們可以什么都不做,也可以做一些更新操作。
否則,就將X插入到遍歷路徑上的最后一個(gè)節(jié)點(diǎn)。
代碼實(shí)現(xiàn)如下:
public void insert(Integer x) {
root = insert(x, root);
}
// 返回的插入節(jié)點(diǎn)的根節(jié)點(diǎn)
private BinaryNode insert(Integer x, BinaryNode node) {
// 如果當(dāng)前節(jié)點(diǎn)為null,新建節(jié)點(diǎn)返回
if (node == null) {
return new BinaryNode(x, null, null);
}
// 與當(dāng)前節(jié)點(diǎn)比較
int compareResult = x.compareTo(node.element);
// 小于當(dāng)前節(jié)點(diǎn)值,遞歸插入左子樹,并將返回值設(shè)置為當(dāng)前節(jié)點(diǎn)的left
if (compareResult < 0) {
node.left = insert(x, node.left);
}
// 大于當(dāng)前節(jié)點(diǎn)值,遞歸插入右子樹,并將返回值設(shè)置為當(dāng)前節(jié)點(diǎn)的right
if (compareResult > 0) {
node.right = insert(x, node.right);
}
// 等于當(dāng)前的值,不做任何處理
if (compareResult == 0) {
// do some update or do noting
}
return node;
}
2.3.6 remove-刪除
正如許多數(shù)據(jù)結(jié)構(gòu)一樣,最難的操作是remove,一旦我們發(fā)現(xiàn)了要?jiǎng)h除的元素,就要考慮幾種可能的情況:
- 當(dāng)待刪除的節(jié)點(diǎn)為葉子節(jié)點(diǎn)時(shí),直接刪除。
- 當(dāng)待刪除節(jié)點(diǎn)只有一個(gè)兒子節(jié)點(diǎn)時(shí),把兒子節(jié)點(diǎn)代替該節(jié)點(diǎn)的位置,然后刪除該節(jié)點(diǎn)。
- 當(dāng)待刪除的節(jié)點(diǎn)有兩個(gè)兒子節(jié)點(diǎn)時(shí),一般的刪除策略是用其右子樹的最小的數(shù)據(jù)代替該節(jié)點(diǎn)的數(shù)據(jù)并遞歸刪除那個(gè)節(jié)點(diǎn)(現(xiàn)在它是空的);因?yàn)橛易訕涞淖钚」?jié)點(diǎn)不可能有左兒子,所以第二次Delete要容易。
// 刪除某個(gè)值
public void remove(Integer x) {
remove(x, root);
}
private BinaryNode remove(Integer x, BinaryNode node) {
if (node == null) {
return null;
}
int compareResult = x.compareTo(node.element);
if (compareResult < 0) {
node.left = remove(x, node.left);
}
if (compareResult > 0) {
node.right = remove(x, node.right);
}
if (compareResult == 0) {
if (node.left != null && node.right != null) {
node.element = findMin(node.right).element;
node.right = remove(node.element, node.right);
} else {
node = (node.left != null) ? node.left : node.right;
}
}
return node;
}
2.4 AVL樹
AVL樹是高度平衡的而二叉樹。它的特點(diǎn)是:AVL樹中任何節(jié)點(diǎn)的兩個(gè)子樹的高度最大差別為1。
先來明確幾個(gè)概念:
平衡因子:將二叉樹上節(jié)點(diǎn)的左子樹高度減去右子樹高度的值稱為該節(jié)點(diǎn)的平衡因子BF(Balance Factor)。
最小不平衡子樹:距離插入節(jié)點(diǎn)最近的,且平衡因子的絕對(duì)值大于1的節(jié)點(diǎn)為根的子樹。
左邊二叉樹的節(jié)點(diǎn)45的BF = 1,插入節(jié)點(diǎn)43后,節(jié)點(diǎn)45的BF=2。
節(jié)點(diǎn)45是距離插入點(diǎn)43最近的BF不在[-1,1]范圍內(nèi)的節(jié)點(diǎn),因此以節(jié)點(diǎn)45為根的子樹為最小不平衡子樹。
2.4.1 AVL樹節(jié)點(diǎn)實(shí)現(xiàn)
public class AVLTreeNode<T extends Comparable> {
// 存儲(chǔ)的數(shù)據(jù)-用于排序
T key;
// 節(jié)點(diǎn)高度-用于計(jì)算父節(jié)點(diǎn)的BF
int height;
// 左兒子 & 右兒子
AVLTreeNode<T> left;
AVLTreeNode<T> right;
public AVLTreeNode() {
}
public AVLTreeNode(T key, AVLTreeNode<T> left, AVLTreeNode<T> right) {
this.key = key;
this.left = left;
this.right = right;
this.height = 0;
}
}
節(jié)點(diǎn)的定義還是比較簡單的,相對(duì)于之前的定義多了一個(gè)height屬性用于計(jì)算父節(jié)點(diǎn)的BF。
2.4.2 AVL樹定義
public class AVLTree<T extends Comparable> {
// 定義樹的根節(jié)點(diǎn)
AVLTreeNode<T> root;
public AVLTree() {
root = null;
}
}
我們之定義了一個(gè)根節(jié)點(diǎn),后續(xù)分析逐漸豐富其該有的操作。
2.4.3 獲取樹的高度
public int height() {
return height(root);
}
private int height(AVLTreeNode<T> tree) {
if (tree != null){
return tree.height;
}
return 0;
}
本文章將空樹的高度定義為0,高度以樹的層次為準(zhǔn),根節(jié)點(diǎn)的高度為1,依次類推。
2.4.4 AVL失衡調(diào)整
如果在AVL樹中進(jìn)行插入或刪除節(jié)點(diǎn)后,可能導(dǎo)致AVL樹失去平衡。這種不平衡可能出現(xiàn)在下面四種情況中:
- 對(duì)a的左兒子的左子樹進(jìn)行一次插入。(LL)
- 對(duì)a的左兒子的右子樹進(jìn)行一次插入。(LR)
- 對(duì)a的右兒子的左子樹進(jìn)行一次插入。(RL)
- 對(duì)a的右兒子的右子樹進(jìn)行一次插入。(RR)
其中1、4是關(guān)于a點(diǎn)的鏡像對(duì)稱,2、3是關(guān)于a點(diǎn)的鏡像對(duì)稱。
第一種情況(1、4)需要通過對(duì)樹的一次單旋轉(zhuǎn)完成調(diào)整。
第二種情況(2、3)需要通過對(duì)樹的一次雙旋轉(zhuǎn)完成調(diào)整。
LL旋轉(zhuǎn)
在左子樹上插入左孩子導(dǎo)致AVL樹失衡,"根的左子樹的高度"比"根的右子樹的高度"大2。
針對(duì)該情況,我們需要進(jìn)行單右旋轉(zhuǎn)來完成對(duì)樹的調(diào)整。
圖中左邊是旋轉(zhuǎn)之前的樹,右邊是旋轉(zhuǎn)之后的樹。從中可以發(fā)現(xiàn),旋轉(zhuǎn)之后的樹又變成了AVL樹,而且該旋轉(zhuǎn)只需要一次即可完成。
對(duì)于LL旋轉(zhuǎn),你可以這樣理解為:LL旋轉(zhuǎn)是圍繞"失去平衡的AVL根節(jié)點(diǎn)"進(jìn)行的,也就是節(jié)點(diǎn)4;而且由于是LL情況,就將節(jié)點(diǎn)4進(jìn)行一次順時(shí)針旋轉(zhuǎn)。
代碼實(shí)現(xiàn):
/**
* 進(jìn)行一次單右旋轉(zhuǎn)
*
* @param node 最小失衡樹根節(jié)點(diǎn)
*/
private AVLTreeNode<T> rightRotation(AVLTreeNode<T> node) {
AVLTreeNode<T> left = node.left;
node.left = left.right;
left.right = node;
// 更新高度
node.height = Math.max(height(node.left), height(node.right)) + 1;
left.height = Math.max(height(left.left), height(left.right)) + 1;
return left;
}
RR旋轉(zhuǎn)
在右子樹插入右孩子導(dǎo)致AVL失衡時(shí),我們需要進(jìn)行單左旋調(diào)整。旋轉(zhuǎn)圍繞最小失衡子樹的根節(jié)點(diǎn)進(jìn)行。
/**
* 進(jìn)行一次單左旋轉(zhuǎn)
*
* @param node 最小失衡樹根節(jié)點(diǎn)
*/
private AVLTreeNode<T> leftRotation(AVLTreeNode<T> node) {
AVLTreeNode<T> right = node.right;
node.right = right.left;
right.left = node;
// 更新高度
node.height = Math.max(height(node.left), height(node.right)) + 1;
right.height = Math.max(height(right.left), height(right.right)) + 1;
return right;
}
RL旋轉(zhuǎn)
“在右子樹上插入左孩子導(dǎo)致AVL樹失衡",此時(shí)我們需要進(jìn)行先右旋后左旋的調(diào)整。
/**
* 先右旋后左旋
*
* @param node 失衡樹根節(jié)點(diǎn)
* @return 旋轉(zhuǎn)后的根節(jié)點(diǎn)
*/
private AVLTreeNode<T> rightLeftRotation(AVLTreeNode<T> node) {
node.right = rightRoation(node.right);
return leftRoation(node);
}
LR旋轉(zhuǎn)
“在左子樹上插入右孩子導(dǎo)致AVL樹失衡",此時(shí)我們需要進(jìn)行先左旋后右旋的調(diào)整。
/**
* 先左旋后右旋
*
* @param node 失衡樹根節(jié)點(diǎn)
* @return 旋轉(zhuǎn)后的根節(jié)點(diǎn)
*/
private AVLTreeNode<T> leftRightRotation(AVLTreeNode<T> node) {
node.left = leftRoation(node.left);
return rightLeftRoation(node);
}
2.4.5 插入
代碼實(shí)現(xiàn)如下:
public void insert(T key) {
root = insert(root, key);
}
private AVLTreeNode<T> insert(AVLTreeNode<T> root, T key) {
if (root == null) {
root = new AVLTreeNode(key, null, null);
} else {
int cmp = key.compareTo(root.key);
// 插入左子樹的情況
if (cmp < 0) {
root.left = insert(root.left, key);
if (height(root.left) - height(root.right) == 2) {
// 情況二:插入到左子樹的左孩子節(jié)點(diǎn)上,進(jìn)行右旋
if (key.compareTo(root.left.key) < 0) {
root = rightRoation(root);
}
//情況四:插入到左子樹的右孩子節(jié)點(diǎn)上,進(jìn)行先左后右旋轉(zhuǎn)
else {
root = leftRightRoation(root);
}
}
}
// 插入右子樹的情況
if (cmp > 0) {
root.right = insert(root.right, key);
if (height(root.right) - height(root.left) == 2) {
// //情況一:插入右子樹的右節(jié)點(diǎn),進(jìn)行左旋
if (key.compareTo(root.right.key) > 0) {
root = leftRoation(root);
}
// 情況三:插入右子樹的左節(jié)點(diǎn),進(jìn)行先右再左旋轉(zhuǎn)
else {
root = rightLeftRoation(root);
}
}
}
if (cmp == 0) {
System.out.println("無法添加相同的節(jié)點(diǎn)");
}
}
root.height = Math.max(height(root.left), height(root.right)) + 1;
return root;
}
2.4.6 刪除
刪除節(jié)點(diǎn)也可能導(dǎo)致AVL樹的失衡,實(shí)際上刪除節(jié)點(diǎn)和插入節(jié)點(diǎn)是一種互逆的操作:
刪除右子樹的節(jié)點(diǎn)導(dǎo)致AVL樹失衡時(shí),相當(dāng)于在左子樹插入節(jié)點(diǎn)導(dǎo)致AVL樹失衡,即情況情況二或情況四。
刪除左子樹的節(jié)點(diǎn)導(dǎo)致AVL樹失衡時(shí),相當(dāng)于在右子樹插入節(jié)點(diǎn)導(dǎo)致AVL樹失衡,即情況情況一或情況三。
另外,AVL樹也是一棵二叉排序樹,因此在刪除節(jié)點(diǎn)時(shí)也要維護(hù)二叉排序樹的性質(zhì)。
代碼實(shí)現(xiàn)如下:
public AVLTreeNode<T> remove(T key) {
return remove(root, key);
}
private AVLTreeNode<T> remove(AVLTreeNode<T> node, T key) {
if (node != null) {
int cmp = key.compareTo(node.key);
if (cmp == 0) {
if (node.left != null && node.right != null) {
// 左子樹比右子樹高,在左子樹上選擇節(jié)點(diǎn)進(jìn)行替換
if (height(node.left) > height(node.right)) {
//使用左子樹最大節(jié)點(diǎn)來代替被刪節(jié)點(diǎn),而刪除該最大節(jié)點(diǎn)
AVLTreeNode<T> leftMax = findMax(node.left);
node.key = leftMax.key;
remove(node.left, leftMax.key);
}
//在右子樹上選擇節(jié)點(diǎn)進(jìn)行替換
else {
//使用最小節(jié)點(diǎn)來代替被刪節(jié)點(diǎn),而刪除該最小節(jié)點(diǎn)
AVLTreeNode<T> rightMin = findMin(node.right);
node.key = rightMin.key;
remove(node.right, rightMin.key);
}
} else {
AVLTreeNode<T> tmp = node;
node = (node.left != null) ? node.left : node.right;
tmp = null;
}
}
if (cmp > 0) {
node.right = remove(node.right, key);
if (height(node.left) - height(node.right) == 2) {
//相當(dāng)于在左子樹上插入右節(jié)點(diǎn)造成的失衡(情況四)
if (height(node.left.right) > height(node.left.left))
node = leftRightRotation(node);
else//相當(dāng)于在左子樹上插入左節(jié)點(diǎn)造成的失衡(情況二)
node = rightRotation(node);
}
}
if (cmp < 0) {
node.left = remove(node.left, key);
if (height(node.right) - height(node.left) == 2) {
//相當(dāng)于在右子樹上插入左節(jié)點(diǎn)造成的失衡(情況三)
if (height(node.right.left) > height(node.right.right))
node = rightLeftRotation(node);
else//相當(dāng)于在右子樹上插入右節(jié)點(diǎn)造成的失衡(情況一)
node = leftRotation(node);
}
}
return node;
}
return null;
}
// 查找最大值
public AVLTreeNode findMax() {
return findMax(root);
}
private AVLTreeNode<T> findMax(AVLTreeNode<T> node) {
if (node != null) {
while (node.right != null) {
node = node.right;
}
}
return node;
}
private AVLTreeNode<T> findMin(AVLTreeNode<T> node) {
// 當(dāng)前節(jié)點(diǎn)為null,直接返回null
if (node == null) {
return null;
}
// 不存在左子樹,返回當(dāng)前節(jié)點(diǎn)
if (node.left == null) {
return node;
}
// 遞歸遍歷左子樹
return findMin(node.left);
}