2020.8.5第三十天
一、使用樹這種結構的原因:
●陣列儲存方式的分析
➢優點:通過下標方式訪問元素,速度快。對於有序陣列,還可使用二分查詢提高檢索速度。
➢缺點:如果要檢索具體某個值,或者插入值(按一定順序)會整體移動,效率較低
●鏈式儲存方式的分析
➢優點:在一定程度上對陣列儲存方式有優化(比如:插入一個數值節點,只需要將插入節點,連結到連結串列中即可,刪除效率也很好)。
➢缺點:在進行檢索時,效率仍然較低,比如(檢索某個值,需要從頭節點開始遍歷)
●樹儲存方式的分析
➢能提高資料儲存,讀取的效率,比如利用二叉排序樹(Binary SortTree),既可以保證資料的檢索速度,同時也可以保證資料的插入,刪除,修改的速度。
二、二叉樹
➢樹有很多種,每個節點最多隻能有兩個子節點的一種形式稱為二叉樹。
➢二叉樹的子節點分為左節點和右節點。
➢如果該二叉樹的所有葉子節點都在最後一層,並且結點總數=2^n-1,n為層.數,則我們稱為滿二叉樹。
➢如果該二叉樹的所有葉子節點都在最後一層或者倒數第二層,而且最後一層的葉子節點在左邊連續,倒數第二層的葉子節點在右邊連續,我們稱為完全二叉樹。
●前序遍歷:先輸出父節點,再遍歷左子樹和右子樹
●中序遍歷:先遍歷左子樹,再輸出父節點,再遍歷右子樹
●後序遍歷:先遍歷左子樹,再遍歷右子樹,最後輸出父節點
●小結:看輸出父節點的順序,就確定是前序,中序還是後序
package com.xudong.DataStructures; public class BinaryTreeDemo { public static void main(String[] args) { //建立一個二叉樹 BinaryTree binaryTree = new BinaryTree(); //建立需要的節點 HeroNode1 root = new HeroNode1(1, "宋江"); HeroNode1 node2 = new HeroNode1(2, "吳用"); HeroNode1 node3 = new HeroNode1(3, "盧俊義"); HeroNode1 node4 = new HeroNode1(4, "林沖"); HeroNode1 node5 = new HeroNode1(5, "關勝"); //方式一:手動建立二叉樹 root.setLeft(node2); root.setRight(node3); node3.setRight(node4); node3.setLeft(node5); binaryTree.setRoot(root); //測試 System.out.println("前序遍歷:"); binaryTree.preOrder(); System.out.println("中序遍歷:"); binaryTree.infixOrder(); System.out.println("後序遍歷:"); binaryTree.postOrder(); System.out.println("======================"); //前序查詢 System.out.println("前序查詢:"); HeroNode1 resNode = binaryTree.preOrderSearch(5); if (resNode != null){ System.out.printf("找到了資訊為no= %d name= %s 的英雄。\n",resNode.getNo(),resNode.getName()); }else { System.out.println("沒有找到該英雄!"); } //中序查詢 System.out.println("中序查詢:"); resNode = binaryTree.infixOrderSearch(5); if (resNode != null){ System.out.printf("找到了資訊為no= %d name= %s 的英雄。\n",resNode.getNo(),resNode.getName()); }else { System.out.println("沒有找到該英雄!"); } //後序查詢 System.out.println("後序查詢:"); resNode = binaryTree.postOrderSearch(5); if (resNode != null){ System.out.printf("找到了資訊為no= %d name= %s 的英雄。\n",resNode.getNo(),resNode.getName()); }else { System.out.println("沒有找到該英雄!"); } System.out.println("===================="); //刪除節點 System.out.println("刪除前,前序遍歷:"); binaryTree.preOrder(); binaryTree.delNode(5); System.out.println("刪除後,前序遍歷:"); binaryTree.preOrder(); System.out.println("--------------------"); System.out.println("刪除前,中序遍歷:"); binaryTree.infixOrder(); binaryTree.delNode(4); System.out.println("刪除後,中序遍歷:"); binaryTree.infixOrder(); System.out.println("--------------------"); System.out.println("刪除前,後序遍歷:"); binaryTree.postOrder(); binaryTree.delNode(3); System.out.println("刪除後,後序遍歷:"); binaryTree.postOrder(); } } //定義BinaryTree二叉樹 class BinaryTree{ private HeroNode1 root; public void setRoot(HeroNode1 root) { this.root = root; } //前序遍歷 public void preOrder(){ if (this.root != null){ this.root.preOrder(); }else { System.out.println("二叉樹為空,無法遍歷!"); } } //中序遍歷 public void infixOrder(){ if (this.root != null){ this.root.infixOrder(); }else { System.out.println("二叉樹為空,無法遍歷!"); } } //後序遍歷 public void postOrder(){ if (this.root != null){ this.root.postOrder(); }else { System.out.println("二叉樹為空,無法遍歷!"); } } //前序查詢 public HeroNode1 preOrderSearch(int no){ if (root != null){ return root.preOrderSearch(no); }else { return null; } } //中序查詢 public HeroNode1 infixOrderSearch(int no){ if (root != null){ return root.infixOrderSearch(no); }else { return null; } } //後序查詢 public HeroNode1 postOrderSearch(int no){ if (root != null){ return root.postOrderSearch(no); }else { return null; } } //刪除節點 public void delNode(int no){ if (root != null){ //立即判斷root是不是要刪除的節點 if (root.getNo() == no){ root = null; }else { //遞迴刪除 root.delNode(no); } }else { System.out.println("空樹!不能刪除"); } } } //建立heroNode節點 class HeroNode1{ private int no; private String name; private HeroNode1 left; private HeroNode1 right; public HeroNode1(int no, String name) { this.no = no; this.name = name; } public int getNo() { return no; } public void setNo(int no) { this.no = no; } public String getName() { return name; } public void setName(String name) { this.name = name; } public HeroNode1 getLeft() { return left; } public void setLeft(HeroNode1 left) { this.left = left; } public HeroNode1 getRight() { return right; } public void setRight(HeroNode1 right) { this.right = right; } @Override public String toString() { return "HeroNode1{" + "no=" + no + ", name='" + name + '\'' + '}'; } //前序遍歷的方法 public void preOrder(){ System.out.println(this);//先輸出父節點 //遞歸向左子樹前序遍歷 if (this.left != null){ this.left.preOrder(); } //遞歸向右子樹前序遍歷 if (this.right != null){ this.right.preOrder(); } } //中序遍歷的方法 public void infixOrder(){ //遞歸向左子樹中序遍歷 if (this.left != null){ this.left.infixOrder(); } //輸出父節點 System.out.println(this); //遞歸向右子樹中序遍歷 if (this.right != null){ this.right.infixOrder(); } } //前序遍歷的方法 public void postOrder(){ //遞歸向左子樹中序遍歷 if (this.left != null){ this.left.postOrder(); } //遞歸向右子樹中序遍歷 if (this.right != null){ this.right.postOrder(); } //輸出父節點 System.out.println(this); } //前序遍歷查詢 public HeroNode1 preOrderSearch(int no){ //比較當前節點是不是 if (this.no == no){ return this; } //向左遞迴查詢 HeroNode1 resNode = null; if (this.left != null){ resNode = this.left.preOrderSearch(no); } if (resNode != null){//說明在左子樹找到 return resNode; } //向右遞迴查詢 if (this.right != null){ resNode = this.right.preOrderSearch(no); } if (resNode != null){//說明在右子樹找到 return resNode; } return resNode; } //中序遍歷查詢 public HeroNode1 infixOrderSearch(int no){ //向左遞迴查詢 HeroNode1 resNode = null; if (this.left != null){ resNode = this.left.infixOrderSearch(no); } if (resNode != null){//說明在左子樹找到 return resNode; } //如果沒找到,比較當前節點是不是 if (this.no == no){ return this; } //向右遞迴查詢 if (this.right != null){ resNode = this.right.infixOrderSearch(no); } if (resNode != null){//說明在右子樹找到 return resNode; } return resNode; } //後序遍歷查詢 public HeroNode1 postOrderSearch(int no){ //向左遞迴查詢 HeroNode1 resNode = null; if (this.left != null){ resNode = this.left.postOrderSearch(no); } if (resNode != null){//說明在左子樹找到 return resNode; } //向右遞迴查詢 if (this.right != null){ resNode = this.right.postOrderSearch(no); } if (resNode != null){//說明在右子樹找到 return resNode; } //如果沒找到,比較當前節點是不是 if (this.no == no){ return this; } return resNode; } //遞迴刪除節點 //1.如果刪除的是葉子結點,則刪除該節點 //2.如果刪除的節點是非葉子節點,則刪除該子數 public void delNode(int no){ //向左節點遞迴刪除 if (this.left != null && this.left.no == no){ this.left = null; return; } //向右節點遞迴刪除 if (this.right != null && this.right.no == no){ this.right = null; return; } //向左子樹遞迴刪除 if (this.left != null){ this.left.delNode(no); } //向右子樹遞迴刪除 if (this.right != null){ this.right.delNode(no); } } }
三、順序儲存二叉樹
●從資料儲存來看,陣列儲存方式和樹的儲存方式可以相互轉換,即陣列可以轉換成樹,樹也可以轉換成陣列。
順序儲存二叉樹的特點:
➢順序二叉樹通常只考慮完全二叉樹
➢第n個元素的左子節點カ為【2n+ 1】
➢第n個元素的右子節點カ【2n+2】
➢第n個元素的父子節點為【(n-1)/2】
package com.xudong; public class arrBinaryTreeDemo { public static void main(String[] args) { int[] arr = {1,2,3,4,5,6,7}; ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr); arrBinaryTree.preOrder(); } } //實現順序儲存二叉樹遍歷 class ArrBinaryTree{ private int[] arr;//儲存資料節點的陣列 public ArrBinaryTree(int[] arr) { this.arr = arr; } //過載perOrder public void preOrder(){ this.preOrder(0); } //順序儲存二叉樹的前序遍歷 public void preOrder(int index){ if (arr == null || arr.length == 0){ System.out.println("陣列為空!"); } //輸出當前元素 System.out.println(arr[index]); //向左遞迴遍歷 if ((index * 2 + 1) < arr.length){ preOrder(2 * index + 1); } //向右遞迴遍歷 if ((index * 2 + 2) < arr.length){ preOrder(2 * index + 2); } } }
四、線索化二叉樹
➢n個結點的二叉連結串列中含有n+1【公式2n-(n-1)=n+1】個空指標域。利用二叉連結串列中的空指標域,存放指向該結點在某種遍歷次序下的前驅和後繼節點的指標(這種附加的指標稱為"線索")
➢這種加上了線索的二叉連結串列稱為線索連結串列,相應的二叉樹稱為線索二叉樹(Threaded BinaryTree)。根據線索性質的不同,線索二叉樹可分為前序線索二叉樹、 中序線索二叉樹和後序線索二叉樹三種
➢一個結點的前一個節點,稱為前驅節點
➢一個結點的後一個節點,稱為後繼節點
線索化二叉樹應用案例
遍歷線索化二叉樹
➢說明:對前面的中序線索化的二叉樹,進行遍歷
➢分析:因為線索化後,各個結點指向有變化,因此原來的遍歷方式不能使用,這時需要使用新的方式遍歷線索化二叉樹,各個節點可以通過線型方式遍歷,因此無需使用遞迴方式,這樣也提高了遍歷的效率。遍歷的次序應當和中序遍歷保持一致。
package com.xudong.DataStructures;
public class ThreadedBinaryTreeDemo {
public static void main(String[] args) {
HeroNode3 root = new HeroNode3(1, "Tom");
HeroNode3 node2 = new HeroNode3(3, "jack");
HeroNode3 node3 = new HeroNode3(6, "smith");
HeroNode3 node4 = new HeroNode3(8, "mary");
HeroNode3 node5 = new HeroNode3(10, "king");
HeroNode3 node6 = new HeroNode3(14, "dim");
//手動建立線索二叉樹
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
threadedBinaryTree.setRoot(root);
threadedBinaryTree.threadedNodes();
//測試10號節點
HeroNode3 leftNode = node5.getLeft();
HeroNode3 rightNode = node5.getRight();
System.out.println("10號節點的前驅節點是 =" + leftNode);
System.out.println("10號節點的後繼節點是 =" + rightNode);
//遍歷線索化二叉樹
System.out.println("使用線索化的方式遍歷線索化二叉樹:");
threadedBinaryTree.threadedList();
}
}
//
//定義ThreadedBinaryTree二叉樹
class ThreadedBinaryTree{
private HeroNode3 root;
//在遞迴線索化時,pre總保留前一個節點
private HeroNode3 pre = null;
public void setRoot(HeroNode3 root) {
this.root = root;
}
//過載threadedNodes方法
public void threadedNodes(){
this.threadedNodes(root);
}
//對二叉樹進行中序線索化的方法.node就是當前需要線索化的節點
public void threadedNodes(HeroNode3 node){
//如果node == null,不能線索化
if (node == null){
return;
}
//1.線索話左子樹
threadedNodes(node.getLeft());
//2.線索話當前節點
//處理當前節點的前驅結點
if (node.getLeft() == null){
//讓當前節點的左指標指向前驅節點
node.setLeft(pre);
//修改當前節點的左指標的型別,指向前驅節點
node.setLeftType(1);
}
//處理後繼節點
if (pre != null && pre.getRight() == null){
//讓前驅節點的右指標指向當前節點
pre.setRight(node);
//修改前驅節點的右指標型別
pre.setRightType(1);
}
//每處理一個節點後,讓當前節點是下一個節點的前驅節點
pre = node;
//3.線索話右子樹
threadedNodes(node.getRight());
}
//線索化的遍歷線索二叉樹
public void threadedList(){
//定義一個變數,儲存當前遍歷的節點
HeroNode3 node = root;
while (node != null){
//當leftType == 1 時,說明該節點時按照線索化處理後的有效節點
while (node.getLeftType() == 0){
node = node.getLeft();
}
//列印當前這個節點
System.out.println(node);
//如果當前節點的右指標指向的是後繼節點,就一直輸出
while (node.getRightType() == 1){
//獲得當前節點的後繼節點
node = node.getRight();
System.out.println(node);
}
//替換這個遍歷的節點
node = node.getRight();
}
}
}
//建立heroNode節點
class HeroNode3{
private int no;
private String name;
private HeroNode3 left;
private HeroNode3 right;
//如果leftType == 0 表示指向的是左子樹,如果是 1 則表示指向前驅節點
//如果rightType == 0 表示指向的是右子樹,如果是 1 則表示指向後繼節點
private int leftType;
private int rightType;
public int getLeftType() {
return leftType;
}
public void setLeftType(int leftType) {
this.leftType = leftType;
}
public int getRightType() {
return rightType;
}
public void setRightType(int rightType) {
this.rightType = rightType;
}
public HeroNode3(int no, String name) {
this.no = no;
this.name = name;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public HeroNode3 getLeft() {
return left;
}
public void setLeft(HeroNode3 left) {
this.left = left;
}
public HeroNode3 getRight() {
return right;
}
public void setRight(HeroNode3 right) {
this.right = right;
}
@Override
public String toString() {
return "HeroNode3{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
五、赫夫曼樹
●給定n個權值作為n個葉子結點,構造一棵二叉樹,若該樹的帶權路徑長度(wpl)達到最小,稱這樣的二叉樹為最優二叉樹,也稱為哈夫曼樹(HuffmanTree),還有的書翻譯為霍夫曼樹。
●赫夫曼樹是帶權路徑長度最短的樹,權值較大的結點離根較近。
➢路徑和路徑長度:在一棵樹中,從一個結點往下可以達到的孩子或孫子結點之間的通路,稱為路徑。通路中分支的數目稱為路徑長度。若規定根結點的層數為1,則從根結點到第L層結點的路徑長度為L-1
➢結點的權及帶權路徑長度:若將樹中結點賦給一個有著某種含義的數值,則這個數值稱為該結點的權。結點的帶權路徑長度為:從根結點到該結點之間的路徑長度與該結點的權的乘積
➢樹的帶權路徑長度:樹的帶權路徑長度規定為所有葉子結點的帶權路徑長度之和,記為WPL(weighted path length) ,權值越大的結點離根結點越近的二叉樹才是最優二叉樹。
➢WPL最小的就是赫夫曼樹
構成赫夫曼樹的步驟
1)從小到大進行排序, 將每一個數據, 每個資料都是一個節點,每個節點可以看成是一顆最簡單的二叉樹
2)取出根節 點權值最小的兩顆二叉樹
3)組成一顆新的二叉樹,該新的二叉樹的根節點的權值是前面兩顆二叉樹根節點權值的和
4)再將這顆新的二叉樹,以根節點的權值大小再次排序,不斷重複1-2-3-4的步驟,直到數列中,所有的資料都被處理,就得到一顆赫夫曼樹
package com.xudong.DataStructures;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class HuffmanTreeDemo {
public static void main(String[] args) {
int arr[] = {13,7,8,3,29,6,1};
Node root = createHuffmanTree(arr);
preOrder(root);
}
//前序遍歷的方法
public static void preOrder(Node root){
if (root != null){
root.preOrder();
}else {
System.out.println("這是個空樹!");
}
}
//建立赫夫曼樹
/**
* @param arr 需要建立成赫夫曼樹的陣列
* @return 建立好後的赫夫曼樹的root節點
*/
public static Node createHuffmanTree(int[] arr){
//將arr的每個元素構成一個Node放入ArrayList中
List<Node> nodes = new ArrayList<>();
for (int value : arr){
nodes.add(new Node(value));
}
while (nodes.size() > 1){
//排序
Collections.sort(nodes);
//1.取出權值最小的節點(二叉樹)
Node leftNode = nodes.get(0);
//2.取出權值第二小的節點(二叉樹)
Node rightNode = nodes.get(1);
//3.建立一個新的二叉樹
Node parent = new Node(leftNode.value + rightNode.value);
parent.left = leftNode;
parent.right = rightNode;
//4.從ArrayList刪除處理過的二叉樹
nodes.remove(leftNode);
nodes.remove(rightNode);
//5.將parent加入到nodes
nodes.add(parent);
}
//返回赫夫曼樹root節點
return nodes.get(0);
}
}
//建立節點類,讓Node物件持續進行Collections集合排序
class Node implements Comparable<Node>{
int value;//節點權值
Node left;//指向左子節點
Node right;//指向右子節點
//前序遍歷
public void preOrder(){
System.out.println(this);
if (this.left != null){
this.left.preOrder();
}
if (this.right != null){
this.right.preOrder();
}
}
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
@Override
public int compareTo(Node o) {
//從小到大排序(從大到小加負號)
return (this.value - o.value);
}
}
六、二叉排序樹
●二叉排序樹BST:(Binary Sort(Search) Tree),對於二叉排序樹的任何一個非葉子節點,要求左子節點的值比當前節點的值小,右子節點的值比當前節點的值大。
●特別說明:如果有相同的值,可以將該節點放在左子節點或右子節點
package com.xudong.DataStructures;
public class BinarySortTreeDemo {
public static void main(String[] args) {
int[] arr = {7,3,10,12,5,1,9,2};
BinarySortTree binarySortTree = new BinarySortTree();
//新增節點
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Node2(arr[i]));
}
//中序遍歷二叉排序樹
System.out.println("中序遍歷二叉排序樹:");
binarySortTree.infixOrder();
//刪除葉子結點
binarySortTree.delNode(2);
binarySortTree.delNode(5);
binarySortTree.delNode(9);
binarySortTree.delNode(12);
binarySortTree.delNode(7);
binarySortTree.delNode(3);
binarySortTree.delNode(10);
System.out.println("刪除節點後:");
binarySortTree.infixOrder();
}
}
//建立二叉排序樹
class BinarySortTree{
private Node2 root;
//查詢要刪除的節點
public Node2 search(int value){
if (root == null){
return null;
}else {
return root.search(value);
}
}
//查詢父節點
public Node2 searchParent(int value){
if (root == null){
return null;
}else {
return root.searchParent(value);
}
}
//刪除右樹的最小節點
public int delRightTreeMin(Node2 node){
Node2 target = node;
//迴圈查詢左子節點,就找到最小值
while (target.left != null){
target = target.left;
}
//此時target就指向了最小節點
delNode(target.value);
return target.value;
}
//刪除節點
public void delNode(int value){
if (root == null){
return;
}else {
//找到要刪除的節點
Node2 targetNode = search(value);
//如果沒有找到要刪除的節點
if (targetNode == null){
return;
}
//如果發現二叉樹只有一個節點
if (root.left == null && root.right == null){
root = null;
return;
}
//找到targetNode的父節點
Node2 parent = searchParent(value);
//(一)如果要刪除的節點是葉子結點
if (targetNode.left == null && targetNode.right == null){
//若targetNode是父節點的左子節點
if (parent.left != null && parent.left.value == value){
parent.left = null;
}else if (parent.right != null && parent.right.value == value){//若targetNode是父節點的右子節點
parent.right = null;
}
}else if (targetNode.left != null && targetNode.right != null){//(三)刪除右兩顆子樹的節點
int minVal = delRightTreeMin(targetNode.right);
targetNode.value = minVal;
}else {//(二)刪除只有一顆子樹的節點
//如果要刪除的節點有左子節點
if (targetNode.left != null){
if (parent != null){
//如果targetNode 是 Parent的左子節點
if (parent.left.value == value){
parent.left = targetNode.left;
}else {//如果targetNode 是 Parent的右子節點
parent.right = targetNode.left;
}
}else {
root = targetNode.left;
}
}else {//如果要刪除的節點有右子節點
if (parent != null){
//如果targetNode 是 Parent的左子節點
if (parent.left.value == value){
parent.left = targetNode.right;
}else {//如果targetNode 是 Parent的右子節點
parent.right = targetNode.right;
}
}else {
root = targetNode.right;
}
}
}
}
}
//新增節點
public void add(Node2 node){
if (root == null){
root = node;
}else {
root.add(node);
}
}
//中序查詢
public void infixOrder(){
if (root != null){
root.infixOrder();
}else {
System.out.println("二叉排序樹為空!");
}
}
}
//建立Node節點
class Node2{
int value;
Node2 left;
Node2 right;
public Node2(int value) {
this.value = value;
}
//-------------刪除節點-----------------
//查詢要刪除的節點
public Node2 search(int value){
if (value == this.value){//找到就是該節點
return this;
}else if (value < this.value){//如果查詢的值小於當前節點,則向左遞迴查詢
//若左子節點為空
if (this.left == null){
return null;
}
return this.left.search(value);
}else {//如果查詢的值不小於當前節點,則向右子樹遞迴查詢
if (this.right == null){
return null;
}
return this.right.search(value);
}
}
//查詢要刪除節點的父節點
public Node2 searchParent(int value){//返回的是要刪除節點的父節點
//如果當前節點就是要刪除的節點的父節點,那麼返回
if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)){
return this;
}else {
//如果查詢的值小於當前節點的值,並且當前節點的左子節點不為空
if (value < this.value && this.left != null){
return this.left.searchParent(value);//向左子樹遞迴查詢
}else if (value >= this.value && this.right != null){
return this.right.searchParent(value);//向右子樹遞迴查詢
}else {
return null;//沒有找到就返回
}
}
}
//
//以二叉排序樹的方式新增節點
public void add(Node2 node){
if (node == null){
return;
}
//判斷當前節點的值與當前子樹根節點的關係
if (node.value < this.value){
//如果當前左子樹節點為空
if (this.left == null){
this.left = node;
}else {
//遞迴的向左子樹新增
this.left.add(node);
}
}else {
//如果當前右子樹節點為空
if (this.right == null){
this.right = node;
}else {
//遞迴的向右子樹新增
this.right.add(node);
}
}
}
@Override
public String toString() {
return "Node2{" +
"value=" + value +
'}';
}
//中序遍歷
public void infixOrder(){
//遞歸向左子樹中序遍歷
if (this.left != null){
this.left.infixOrder();
}
//輸出父節點
System.out.println(this);
//遞歸向右子樹中序遍歷
if (this.right != null){
this.right.infixOrder();
}
}
}
七、平衡二叉樹(AVL樹)
平衡二叉樹介紹
在二叉排序樹的基礎上實現
●平衡二叉樹也叫平衡二叉搜尋樹(Self-balancing binary searchtree)又被稱為AVL樹,可以保證查詢效率較高。
●具有以下特點:它是一棵空樹或它的根節點左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。平衡二叉樹的常用實現方法有紅黑樹、AVL、替罪羊樹、Treap、 伸展樹等。
左旋轉
右旋轉
雙旋轉
➢當符合右旋轉條件時,它的左子樹的右子樹高度大於它左子樹的高度時,就先對當前節點的左節點進行左旋轉,再對當前節點進行右旋轉
➢當符合左旋轉條件時,它的右子樹的左子樹高度大於它右子樹的高度時,就先對當前節點的右節點進行右旋轉,再對當前節點進行左旋轉
package com.xudong.DataStructures;
public class AVLTreeDemo {
public static void main(String[] args) {
//int[] arr = {4,3,6,5,7,8};
//int[] arr = {10,12,8,9,7,6};
int[] arr = {10,11,7,6,8,9};
AVLTree avlTree = new AVLTree();
//新增節點
for (int i = 0; i < arr.length; i++) {
avlTree.add(new Node3(arr[i]));
}
System.out.println("中序遍歷:");
avlTree.infixOrder();
System.out.println("在平衡處理之後:");
System.out.println("樹的高度:" + avlTree.getRoot().height());
System.out.println("樹的左子樹高度:" + avlTree.getRoot().leftHeight());
System.out.println("樹的右子樹高度:" + avlTree.getRoot().rightHeight());
System.out.println("當前根節點:" + avlTree.getRoot());
}
}
//建立AVL樹
class AVLTree{
private Node3 root;
public Node3 getRoot() {
return root;
}
//查詢要刪除的節點
public Node3 search(int value){
if (root == null){
return null;
}else {
return root.search(value);
}
}
//查詢父節點
public Node3 searchParent(int value){
if (root == null){
return null;
}else {
return root.searchParent(value);
}
}
//刪除右樹的最小節點
public int delRightTreeMin(Node3 node){
Node3 target = node;
//迴圈查詢左子節點,就找到最小值
while (target.left != null){
target = target.left;
}
//此時target就指向了最小節點
delNode(target.value);
return target.value;
}
//刪除節點
public void delNode(int value){
if (root == null){
return;
}else {
//找到要刪除的節點
Node3 targetNode = search(value);
//如果沒有找到要刪除的節點
if (targetNode == null){
return;
}
//如果發現二叉樹只有一個節點
if (root.left == null && root.right == null){
root = null;
return;
}
//找到targetNode的父節點
Node3 parent = searchParent(value);
//(一)如果要刪除的節點是葉子結點
if (targetNode.left == null && targetNode.right == null){
//若targetNode是父節點的左子節點
if (parent.left != null && parent.left.value == value){
parent.left = null;
}else if (parent.right != null && parent.right.value == value){//若targetNode是父節點的右子節點
parent.right = null;
}
}else if (targetNode.left != null && targetNode.right != null){//(三)刪除右兩顆子樹的節點
int minVal = delRightTreeMin(targetNode.right);
targetNode.value = minVal;
}else {//(二)刪除只有一顆子樹的節點
//如果要刪除的節點有左子節點
if (targetNode.left != null){
if (parent != null){
//如果targetNode 是 Parent的左子節點
if (parent.left.value == value){
parent.left = targetNode.left;
}else {//如果targetNode 是 Parent的右子節點
parent.right = targetNode.left;
}
}else {
root = targetNode.left;
}
}else {//如果要刪除的節點有右子節點
if (parent != null){
//如果targetNode 是 Parent的左子節點
if (parent.left.value == value){
parent.left = targetNode.right;
}else {//如果targetNode 是 Parent的右子節點
parent.right = targetNode.right;
}
}else {
root = targetNode.right;
}
}
}
}
}
//新增節點
public void add(Node3 node){
if (root == null){
root = node;
}else {
root.add(node);
}
}
//中序查詢
public void infixOrder(){
if (root != null){
root.infixOrder();
}else {
System.out.println("二叉排序樹為空!");
}
}
}
//建立Node節點
class Node3{
int value;
Node3 left;
Node3 right;
public Node3(int value) {
this.value = value;
}
//找到以根節點為節點的樹高度
public int height(){
return Math.max(left == null ? 0 : left.height(),right == null ? 0 : right.height()) + 1;
}
//返回左子樹的高度
public int leftHeight(){
if (left == null){
return 0;
}
return left.height();
}
//返回左子樹的高度
public int rightHeight(){
if (right == null){
return 0;
}
return right.height();
}
//左旋轉的方法
private void leftRotate(){
//建立新的節點,以當前根節點的值
Node3 newNode = new Node3(value);
//把新節點的左子樹設定成當前節點的左子樹
newNode.left = left;
//把新的節點的右子樹設定成當前節點右子樹的左子樹
newNode.right = right.left;
//把當前結點的值替換成右子節點的值
value = right.value;
//把當前節點的右子樹設定成當前節點右子樹的右子樹
right = right.right;
//把當前節點的左子節點設定成新的節點
left = newNode;
}
//右旋轉
private void rightRotate(){
Node3 newNode = new Node3(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
//-------------刪除節點-----------------
//查詢要刪除的節點
public Node3 search(int value){
if (value == this.value){//找到就是該節點
return this;
}else if (value < this.value){//如果查詢的值小於當前節點,則向左遞迴查詢
//若左子節點為空
if (this.left == null){
return null;
}
return this.left.search(value);
}else {//如果查詢的值不小於當前節點,則向右子樹遞迴查詢
if (this.right == null){
return null;
}
return this.right.search(value);
}
}
//查詢要刪除節點的父節點
public Node3 searchParent(int value){//返回的是要刪除節點的父節點
//如果當前節點就是要刪除的節點的父節點,那麼返回
if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)){
return this;
}else {
//如果查詢的值小於當前節點的值,並且當前節點的左子節點不為空
if (value < this.value && this.left != null){
return this.left.searchParent(value);//向左子樹遞迴查詢
}else if (value >= this.value && this.right != null){
return this.right.searchParent(value);//向右子樹遞迴查詢
}else {
return null;//沒有找到就返回
}
}
}
//
//以二叉排序樹的方式新增節點
public void add(Node3 node){
if (node == null){
return;
}
//判斷當前節點的值與當前子樹根節點的關係
if (node.value < this.value){
//如果當前左子樹節點為空
if (this.left == null){
this.left = node;
}else {
//遞迴的向左子樹新增
this.left.add(node);
}
}else {
//如果當前右子樹節點為空
if (this.right == null){
this.right = node;
}else {
//遞迴的向右子樹新增
this.right.add(node);
}
}
//當新增完一個節點後,(右-左)子樹高度差大於1時,左旋轉
if (rightHeight() - leftHeight() > 1){
//如果它的右子樹的左子樹高度大於它的右子樹高度
if (right != null && right.leftHeight() > right.rightHeight()){
//先對當前節點的右節點(右子樹)進行右旋轉
right.rightRotate();
//再對當前節點進行左旋轉
leftRotate();
}else {
leftRotate();
}
return;
}
//當新增完一個節點後,(左-右)子樹高度差大於1時,左旋轉
if (leftHeight() - rightHeight() > 1){
//如果它的左子樹的右子樹高度大於它的左子樹高度
if (left != null && left.rightHeight() > left.leftHeight()){
//先對當前節點的左節點(左子樹)進行左旋轉
left.leftRotate();
//再對當前節點進行右旋轉
rightRotate();
}else {
rightRotate();
}
}
}
@Override
public String toString() {
return "Node2{" +
"value=" + value +
'}';
}
//中序遍歷
public void infixOrder(){
//遞歸向左子樹中序遍歷
if (this.left != null){
this.left.infixOrder();
}
//輸出父節點
System.out.println(this);
//遞歸向右子樹中序遍歷
if (this.right != null){
this.right.infixOrder();
}
}
}
八、多路查詢樹
●二叉樹需要載入到記憶體的,如果二叉樹的節點少,沒有什麼問題,但是如果二叉樹的節點很多(此如1億),就存在如下問題
➢問題1:在構建叉樹時,需要多次進行1/o操作海量資料存在資料庫或檔案中),節點海量,構建=叉樹時,速度有影響
➢問題2:節點海量,也會造成二叉樹的高度很大,會降低操作速度
1. 多叉樹
●在二叉樹中,每個節點有資料項,最多有兩個子節點。如果允許每個節點可以有更多的資料項和更多的子節點,就是多叉樹(multiwaytree)
●後面的2-3樹,2-3-4樹就是多叉樹,多叉樹通過重新組織節點,減少樹的高度,能對二叉樹進行優化。
2. B樹
●如圖B樹通過重新組織節點,降低了樹的高度
●檔案系統及資料庫系統的設計者利用了磁碟預讀原理,將一個節點的大小設為等於一個頁(頁得大小通常為4k),這樣每個節點只需要一次I/O就可以完全載入
●將樹的度M設定為1024,在600億個元素中最多隻需要4次I/O操作就可以讀取到想要的元素, B樹(廣泛應用於檔案儲存系統以及資料庫系統中)
3. B+樹
B+樹是B樹的變體,也是一種多路搜尋樹
4.B*樹
B*樹是B+樹的變體,在B+樹的非根和非葉子節點再增加指向兄弟的指標
5. 2-3樹
2-3樹是最簡單的B樹結構
●2-3樹的所有葉子節點都在同一層(只要是B樹都滿足這個條件)
●有兩個子節點的節點叫二節點,二節點要麼沒有子節點,要麼有兩個子節點.
●有三個子節點的節點叫三節點,三節點要麼沒有子節點,要麼有三個子節點.
●2-3樹是由二節點和三節點構成的樹。