目錄
1.1 AVL樹(自平衡二叉樹)又和搜索(排序)樹相比,有什麼區別呢?
5.10 輸入一顆二叉樹和一個整數,打印出二叉樹中結點值的和爲輸入整數的所有路徑
一、簡介
在計算機科學中,AVL樹是最先發明的自平衡二叉查找樹。在AVL樹中任何節點的兩個子樹的高度最大差別爲1,所以它也被稱爲高度平衡樹。增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。AVL樹得名於它的發明者G. M. Adelson-Velsky和E. M. Landis,他們在1962年的論文《An algorithm for the organization of information》中發表了它。
AVL樹本質上還是一棵二叉搜索樹,它的特點是:
1.本身首先是一棵二叉搜索樹。
2.帶有平衡條件:每個結點的左右子樹的高度之差的絕對值(平衡因子)最多爲1。
也就是說,AVL樹,本質上是帶了平衡功能的二叉查找樹(二叉排序樹,二叉搜索樹)。
下圖爲AVL(自平衡)二叉樹:
二叉查找樹(Binary Search Tree),(又:二叉搜索樹,二叉排序樹)它或者是一棵空樹,或者是具有下列性質的二叉樹:
1.若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
2.若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;
3.它的左、右子樹也分別爲二叉排序樹。
下圖即爲一個搜索(排序)樹
我們一說到樹,就會想到生活中的樹,像這樣:
大家有沒有發現,除了沒有樹葉不能開花或者結果之外,是不是和我們計算機中的數據結構——樹 一模一樣
(覺得不像的朋友,可以把上圖旋轉180°觀察)
而樹的根,就相當於我們數據結構中樹的根節點。
只不過計算機中的樹成長方式和我們生活中的樹的成長方式是相反的。
有常識的朋友都知道樹是從下往上生長,但是計算機中的樹則是從上往下生長。【類似紮根】
在我們開始探討樹之前,必須要先理解樹的一些基本術語:
節點:上面搜索樹圖片中的一個個圈圈叫節點。
根節點:最上層節點(也就是隻有一個圈圈)就是這棵樹的根節點。
子節點:節點下的左右兩節點。
孫子節點:子節點下的子節點。
父節點:子字節之上的節點。
祖父節點:父節點的父節點。
兄弟節點:相鄰的倆節點。
叔父節點:與父節點相鄰的節點。
左子樹:節點的左子節點下(包括)的所有節點。
右子樹:節點的右子節點下(包括)的所有節點。
高度:節點的最大層數 (比如上面搜索樹圖片中節點的根節點高度爲:4;其左子節點高度爲:3 ;右子節點高度爲:1)。
深度:高度的另一種說法
平衡因子:節點的左子樹高度減去右子樹高度的差。(AVL樹的條件:差值必須在【1,-1】區間中,即左右子樹高度比不能大於1)
左旋:逆時針旋轉兩個節點,讓一個節點被其右子節點取代,而該節點成爲右子節點的左子節點。
右旋:順時針旋轉兩個節點,讓一個節點被其左子節點取代,而該節點成爲左子節點的右子節點。
1.1 AVL樹(自平衡二叉樹)又和搜索(排序)樹相比,有什麼區別呢?
首先我們來對比這兩張圖有什麼區別(排除顏色)
搜索二叉樹 AVL樹
爲了讓大家更直觀,我用表格來展示
相同 | 不相同 | |
搜索樹 | 節點數量,節點值 | 根節點的平衡因子爲 2 |
AVL樹 | 節點數量,節點值 | 根節點的平衡因子爲 0 |
判斷搜索二叉樹是否爲平衡二叉樹(AVL)的基準就是平衡因子
1.2 爲什麼我們需要樹這種數據結構
爲了能更方便快捷地處理互聯網中的數據
1.3 爲什麼有了搜索樹還要二叉平衡樹
在本文上半部分中描述了搜索樹的性質 —— 任意一節點的左子節點小於當前節點,右子節點大於當前節點
添加新節點判斷節點大小從而找到該節點的位置
查找節點的時候,順從該樹的規則,查找的節點大於當前節點則找該節點的右子節點,反之查找當前節點的左子節點,直到找到該節點或者無節點時終止。二分查找算法就是用的這種思路。
但是如果加入的數據是按照大小規律來建立一棵搜索樹時(比如從小到大),按照搜索樹的性質,則會將搜索樹退化爲鏈表
比如:我們需要依次添加 1,2,3,4,5,6,7,8這幾個數據
搜索樹
如上圖所示,退化爲鏈表的搜索樹,在查找數據的時候,時間複雜度爲 O(n)
二叉樹
如上圖所示,二叉樹在添加數據的同時會對數據進行自平衡操作,防止退化爲鏈表,在查找數據的時候,時間複雜度爲 O(logn)
至此,又引出了一個新問題:每添加一個節點就需要進行自平衡處理,在大數據處理過程中會佔用大量資源,這其實並不可取。所以紅黑樹應時而生。不過本篇文章對紅黑樹不做過多贅述,有興趣可以自行了解。
二、AVL樹節點的自平衡處理
自平衡講解完畢
三、刪除節點
接下來我們進入比較難的一個環節——刪除節點
刪除的情況大致可以分爲三種:
1.待刪除的節點沒有子節點
2.待刪除的節點只有一個子節點
3.待刪除的節點有兩個子節點
刪除節點講解完畢
四、代碼實現
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Node {
public int key;
public Node leftNode;
public Node rightNode;
public int height;
@Override
public String toString(){
JSONObject jsonObject = new JSONObject();
jsonObject.put("key",this.key);
jsonObject.put("leftNode",this.leftNode);
jsonObject.put("rightNode",this.rightNode);
jsonObject.put("height",this.height);
return jsonObject.toJSONString();
}
}
/**
* 添加插入二叉樹節點
* @param node 根節點
* @param key 當前添加或插入的節點
* @return
*/
public static Node insert(Node node,int key){
if (node == null){
return newNode(node,key);
}
// 判斷當前添加的節點應該放在哪個位置,並返回其父節點
if (key < node.key){
node.leftNode = insert(node.leftNode,key);
}else if (key > node.key){
node.rightNode = insert(node.rightNode,key);
}else {
return node;
}
// 更新當前節點高度
node.height = max(height(node.leftNode),height(node.rightNode)) + 1;
// 獲取當前節點的平衡因子
int balance = getBalance(node);
/* 當前節點的左子樹高度與右子樹高度比大於1 並且 當前添加的節點小於其祖父節點的左子節點
* 如:
* 3
* /
* 2 -> 2
* / / \
* 1 1 3
* LL型 需進行右旋操作
*/
if (balance > 1 && key < node.leftNode.key){
return ll_state(node);
}
/* 當前節點的左子樹高度與右子樹高度比小於-1 並且 當前添加的節點大於其祖父節點的右子節點
* 如:
* 1
* \
* 2 -> 2
* \ / \
* 3 1 3
* RR型 需進行左旋操作
*/
if (balance < -1 && key > node.rightNode.key){
return rr_state(node);
}
/* 當前節點的左子樹高度與右子樹高度比大於1 並且 當前添加的節點大於其祖父節點的左子節點
* 如:
* 3 3
* / /
* 1 -> 2 -> 2
* \ / / \
* 2 1 1 3
* LR型 需先進行左旋操作 再進行右旋操作
*/
if (balance > 1 && key > node.leftNode.key){
node.leftNode = rr_state(node.leftNode);
return ll_state(node);
}
/* 當前節點的左子樹高度與右子樹高度比小於-1 並且 當前添加的節點小於其祖父節點的右子節點
* 如:
* 1 1
* \ \
* 3 -> 2 -> 2
* / \ / \
* 2 3 1 3
* RL型 需先進行右旋操作 再進行左旋操作
*/
if (balance < -1 && key < node.rightNode.key){
node.rightNode = ll_state(node.rightNode);
return rr_state(node);
}
return node;
}
private static Node rr_state(Node node){
Node right = node.rightNode;
node.rightNode = right.leftNode;
right.leftNode = node;
node.height = max(height(node.leftNode),height(node.rightNode)) + 1;
right.height = max(height(node.leftNode),height(node.rightNode)) + 1;
return right;
}
private static Node ll_state(Node node){
Node left = node.leftNode;
node.leftNode = left.rightNode;
left.rightNode = node;
node.height = max(height(node.leftNode),height(node.rightNode)) + 1;
left.height = max(height(node.leftNode),height(node.rightNode)) + 1;
return left;
}
private static int getBalance(Node node){
return height(node.leftNode) - height(node.rightNode);
}
private static int max(int a,int b){
return a > b?a:b;
}
private static int height(Node node){
if (node == null)
return 0;
return node.height;
}
private static Node newNode(Node node, int key) {
return new Node(key,null,null,1);
}
// 刪除節點
public static Node delete(Node root,int key){
if (root == null){
return root;
}
if (key < root.key){
root.leftNode = delete(root.leftNode,key);
}else if (key > root.key){
root.rightNode = delete(root.rightNode,key);
}else {
// 判斷當前節點是否有子節點
if (root.leftNode == null || root.rightNode == null){
root = root.leftNode != null?root.leftNode:root.rightNode;
}else {
// 如果當前節點有左右子節點 則找到當前節點的後繼子節點
Node temp = minNode(root.rightNode);
// 後繼子節點替換掉當前節點
root.key = temp.key;
// 刪除後繼子節點
root.rightNode = delete(root.rightNode,temp.key);
}
}
if (root == null){
return root;
}
root.height = max(height(root.leftNode),height(root.rightNode)) + 1;
int balance = getBalance(root);
// LL
if (balance > 1 && getBalance(root.leftNode) >= 0){
return ll_state(root);
}
// RR
if (balance < -1 && getBalance(root.rightNode) <= 0){
return rr_state(root);
}
// LR
if (balance > 1 && getBalance(root.leftNode) < 0){
root.leftNode = rr_state(root.leftNode);
return ll_state(root);
}
// RL
if (balance < -1 && getBalance(root.rightNode) > 0){
root.rightNode = ll_state(root.rightNode);
return rr_state(root);
}
return root;
}
private static Node minNode(Node node) {
Node parent = node;
Node current = node;
while (current != null){
parent = current;
current = current.leftNode;
}
return parent;
}
五、二叉樹的各種操作
5.1 節點查找
public static Node select(Node node,int key){
Node resultNode = node;
while (resultNode != null){
if (key < resultNode.key){
resultNode = resultNode.leftNode;
}else if (key > resultNode.key){
resultNode = resultNode.rightNode;
}else {
return resultNode;
}
}
return null;
}
5.2 層序遍歷
/**
* 將樹放入容器
*
* @param pRoot
* @return
*/
public static List<List<Integer>> Print(Node pRoot) {
List<List<Integer>> result = new ArrayList<>();
toData(pRoot,1,result);
return result;
}
/**
* 根據參數i構建容器 並將節點從上到下 從左至右依下標(i)放入相應下標容器的容器內
* @param pRoot 當前放入的節點
* @param i 創建容器的下標
* @param result 容器
*/
private static void toData(Node pRoot, int i, List<List<Integer>> result) {
if (pRoot != null){
if (i > result.size()){
result.add(new ArrayList<>());
}
result.get(i-1).add(pRoot.key);
toData(pRoot.leftNode,i+1,result);
toData(pRoot.rightNode,i+1,result);
}
}
5.3 獲取節點最大(最小)深度
/**
* 方式一:
* 獲取節點最小深度
* 葉子節點返回 1
*
* @param root 當前節點
* @return 當前樹的最小深度
*/
public static int minNodeDepth(Node root) {
if (root == null) {
return 0;
}
int l = minNodeDepth(root.leftNode);
int r = minNodeDepth(root.rightNode);
if (l == 0 || r == 0) {
return 1 + l + r;
} else {
return 1 + Math.min(l, r);
}
}
/**
* 方式二:
* 獲取樹最大深度
*
* @param root
* @return 當前樹最大深度
*/
public static int maxNodeDepth(Node root){
// 如果當前節點沒有子節點則返回 1 (葉子節點返回1)
if (root == null){
return 0;
}
return 1 + Math.max(maxNodeDepth(root.leftNode),maxNodeDepth(root.rightNode));
}
5.4 樹的前中後序遍歷
/**
* 前序遍歷
* 從樹的根節點開始,由左至右依次遍歷
*/
public static void preErgodic(Node root,List<Integer> lastData){
if (root != null) {
lastData.add(root.key);
preErgodic(root.leftNode,lastData);
preErgodic(root.rightNode,lastData);
}
}
/**
* 中序遍歷
* 從左子樹開始,由左至右依次遍歷
*/
public static void inErgodic(Node root,List<Integer> lastData){
if (root != null) {
inErgodic(root.leftNode,lastData);
lastData.add(root.key);
inErgodic(root.rightNode,lastData);
}
}
/**
* 後序遍歷
* 從左子樹最後一個左節點開始,由左至右依次遍歷
*/
public static void lastErgodic(Node root,List<Integer> lastData){
if (root != null) {
lastErgodic(root.leftNode,lastData);
lastErgodic(root.rightNode,lastData);
lastData.add(root.key);
}
}
5.5 重建二叉樹(基於前中序)
注:重建二叉樹必需要中序
/**
* 根據前序和中序遍歷重建二叉樹
* 操作方式與快排類似
*
* 1:根據前序集合第一個數爲根的條件,將中序集合分成兩塊
* 其中左板塊爲當前節點的左子節點,右板塊爲當前節點的右子節點
*
* 2:再通過遞歸用同樣的方式將左板塊與右板塊的左子節點和右子節點的
* 左右子節點找到並返回,直到任意一集合爲空 ->(當前節點沒有左或右子節點)
*
* @param pre 前序遍歷數組
* @param in 中序遍歷數組
* @return
*/
public static Node nodeBuild(int[] pre,int[] in){
if (pre.length == 0 || in.length == 0){
return null;
}
Node node = new Node(pre[0], null, null, 1);
for (int i = 0;i < in.length;i++){
if (in[i] == node.key){
// 左子樹構建
node.leftNode = nodeBuild(Arrays.copyOfRange(pre,1,i+1),Arrays.copyOfRange(in,0,i));
// 右子樹構建
node.rightNode = nodeBuild(Arrays.copyOfRange(pre,i+1,pre.length),Arrays.copyOfRange(in,i+1,in.length));
}
}
return node;
}
5.6 輸入兩顆樹,判斷兩樹關係(是否爲父子關係)
/**
* 判斷輸入的兩顆樹的關係 約定:空樹不是任何一顆樹的子結構
* @param node1
* @param node2
* @return
*/
public static Boolean hasSubtree(Node node1,Node node2){
if (node1 == null || node2 == null){
return false;
}
Boolean result = false;
if (node1.key == node2.key){
result = DoseTree1HaveTree2(node1, node2);
}
if (!result){
result = hasSubtree(node1.leftNode,node2);
}
if (!result){
result = hasSubtree(node1.rightNode,node2);
}
return result;
}
/**
* 對比兩棵樹各個節點,只有各節點的值相同才能滿足條件
*
* @param node1 大於或等於node2節點(對比的主體)
* @param node2
* @return
*/
private static Boolean DoseTree1HaveTree2(Node node1, Node node2) {
// 說明之前對比的節點值都是相同的
if (node2 == null){
return true;
}
if (node1 == null){
return false;
}
if (node1.key != node2.key){
return false;
}
return DoseTree1HaveTree2(node1.leftNode,node2.leftNode) &&
DoseTree1HaveTree2(node1.rightNode,node2.rightNode);
}
5.7 廣度優先遍歷
/**
* 廣度優先 一
* 模仿隊列實現
* 從上往下打印二叉樹的每一個結點, 同層結點從左至右打印
*
* @param root
* @return
*/
public static List<Integer> printFromTopToBottom(Node root){
List<Integer> rootList = new ArrayList<>();
if (root == null){
return rootList;
}
List<Node> queue = new ArrayList<>();
// 模仿入棧
queue.add(root);
while (!queue.isEmpty()){
// 模仿出棧
Node temp = queue.remove(0);
if (temp.leftNode != null){
queue.add(temp.leftNode);
}
if (temp.rightNode != null){
queue.add(temp.rightNode);
}
rootList.add(temp.key);
}
return rootList;
}
/**
* 廣度優先 二
* 使用隊列實現
* @param root
*/
public static void breadthFirstTraverse(Node root) {
Queue<Node> queue = new LinkedList<>();
// 入棧
queue.offer(root);
while (!queue.isEmpty()){
// 出棧
Node poll = queue.poll();
if (poll.leftNode != null){
queue.offer(poll.leftNode);
}
if (poll.rightNode != null){
queue.offer(poll.rightNode);
}
System.out.println(poll.key);
}
}
5.8 深度優先遍歷——前序遍歷
/**
* 前序遍歷
*
*/
public static void preErgodic(Node root,List<Integer> lastData){
if (root != null) {
lastData.add(root.key);
preErgodic(root.leftNode,lastData);
preErgodic(root.rightNode,lastData);
}
}
5.9 輸入一個數組判斷該數組是否爲某二叉樹的後序遍歷
/**
* 輸入一個整數數組,判斷該數組是不是某二叉樹的後序遍歷的結果
* 後序遍歷根爲最後一個節點
* 1.按照後序遍歷的特性將數組的最後一個數作爲根取出來
* 2.獲取最後一個大於根的座標,並根據座標將數組分爲左右兩組
* 左半組數據應小於根,右半組數據應大於根
* 3.判斷左右兩組數據是否滿足上訴條件
* 否:返回false
* 是:將左右半組再次按照1-3條件操作 直到數組內的值爲空 返回true
* @param roots
* @return
*/
public static Boolean verifySquenceOfBST(List<Integer> roots){
if (roots.size() == 0)
return false;
if (roots.size() == 1)
return true;
return lastNodeOperation(roots,0,roots.size()-1);
}
private static Boolean lastNodeOperation(List<Integer> roots, int start, int i) {
if (start >= i){
return true;
}
int lastNode = i;
// 獲取最後一個大於根節點的座標
while (start < i && roots.get(lastNode-1) > roots.get(i)){
lastNode--;
}
// 前面的值應該都比當前值小
for (int j = start; j < lastNode-1; j++) {
if (roots.get(j) > roots.get(lastNode))
return false;
}
return lastNodeOperation(roots, start, lastNode-1)
&& lastNodeOperation(roots,lastNode,i-1);
}
5.10 輸入一顆二叉樹和一個整數,打印出二叉樹中結點值的和爲輸入整數的所有路徑
/**
* 輸入一顆二叉樹和一個整數,打印出二叉樹中結點值的和爲輸入整數的所有路徑。
* 路徑定義爲從樹的根結點開始往下一直到葉結點所經過的結點形成一條路徑。
* @param root
* @param target
* @return
*/
public static List<List<Integer>> FindPath(Node root,int target) {
List<List<Integer>> paths = new ArrayList<>();
if (root != null){
find(paths,new ArrayList<>(),root,target);
}
return paths;
}
public static void find(List<List<Integer>> paths,List<Integer> path,Node root,int target){
path.add(root.key);
if (root.leftNode == null && root.rightNode == null){
if (root.key == target){
paths.add(path);
}
return;
}
// 過濾節點容器
List<Integer> path2 = new ArrayList<>();
path2.addAll(path);
if (root.leftNode != null){
find(paths,path,root.leftNode,target-root.key);
}
if (root.rightNode != null){
find(paths,path2,root.rightNode,target-root.key);
}
}
5.11 輸入一棵二叉樹,判斷該二叉樹是否是平衡二叉樹
/**
* 輸入一棵二叉樹,判斷該二叉樹是否是平衡二叉樹。
* 判斷規則:所有節點的左右字樹差必須在[1,-1]區間
* @param root
* @return
*/
public static boolean IsBalanced_Solution(Node root) {
return getDepth(root) != -1;
}
private static int getDepth(Node root) {
// 葉子節點高度返回 0
if (root == null){
return 0;
}
int l = getDepth(root.leftNode);
if (l == -1){
return -1;
}
int r = getDepth(root.rightNode);
if (r == -1){
return -1;
}
// 當前節點左右子樹高度差必須在[1,-1]區間
return Math.abs(l - r) > 1 ? -1 : Math.max(l,r) + 1;
}
5.12 給定一棵二叉搜索樹,請找出其中的第k小的結點
/**
* 給定一棵二叉搜索樹,請找出其中的第k小的結點。
*
* @param root 樹
* @param k 條件
* @return
*/
public static Node KthNode(Node root, int k) {
if (root == null || k <= 0) {
return null;
}
// 容器裏邊的數據爲已排好序的節點
List<Node> list = new ArrayList<>();
buildList(root, list);
if (k > list.size()) {
return null;
}
return list.get(k - 1);
}
public static void buildList(Node root, List<Node> list) {
if (root == null) {
return;
}
// 中序遍歷 確保二叉樹的順序爲已排好序的
buildList(root.leftNode, list);
list.add(root);
buildList(root.rightNode, list);
}
{\__/} {\__/}
( ·-·) (·-· )
/ >------------------------------------------------< \
| ☆ |
| ☆ |
| ★ |
| ☆ |
| ☆ |
| |
-------------------------------------