本文使用JAVA語言進行描述。其實本質上什麼語言描述數據結構都是可以的。
二叉樹基礎
二叉樹的根節點
二叉樹遞歸結構:
上面是一個滿二叉樹。但是實際中也有二叉樹不是滿的。
二分搜索樹
二分搜索樹也不一定是滿的。
所以使用二分搜索樹需要具備的條件:
存儲的元素必須有可比較性。
樹結構的定義代碼實現:
/**
* Created by hongyonghan on 2020/4/19.
*/
//爲了讓這個二分搜索樹具有可比較性。那麼繼承Comparable的接口。
public class BST<E extends Comparable<E>> {
private class Node{
private E e;
private Node left,right;
public Node(E e) {
this.e = e;
left=null;
right=null;
}
}
private Node root;
private int size;
public BST() {
root=null;
}
public int size()
{
return size;
}
public boolean isEmpty()
{
return size == 0;
}
}
添加新元素
如果樹之前是空的話,那麼添加一個新元素之後,這個元素就是樹的根。比如這裏添加元素41.如果新加一個元素22,那麼會根據二分搜索樹的性質,將22作爲二分搜索樹的左子樹的根節點。
那麼該將60添加到哪個位置呢?邏輯如下:
60大於41,在右子樹,60大於58,在右子樹。所以添加如下:
這裏的二分搜索樹不包含重複元素。
如果想包含重複元素的話,只需要改變定義:左子樹小於等於節點,或者右子樹大於等於節點。
增加代碼和查詢代碼:
public void add(E e)
{
root=add(root,e);
}
private Node add(Node node,E e)
{
if (node == null)
{
size++;
return new Node(e);
}
if (e.compareTo(node.e) < 0)
{
node.left=add(node.left,e);
}else if (e.compareTo(node.e) > 0)
{
node.right = add(node.right,e);
}
return node;
}
//是否包含元素e
public boolean contains(E e)
{
//整體看以整顆二分搜索樹爲根的二分搜索樹中是否包含元素e
return contains(root,e);
}
//看以node爲根的二分搜索樹中是否包含元素e,遞歸算法
private boolean contains(Node node,E e)
{
if (node == null)
return false;
if (e.compareTo(node.e) == 0)
return true;
else if (e.compareTo(node.e) < 0)
{
//如果不在node上面,那就去node的左子樹去找,否則去node的右子樹去找
return contains(node.left,e);
}
else
{
return contains(node.right,e);
}
}
遍歷操作
前序遍歷
前序遍歷代碼:
//二分搜索樹的前序遍歷
public void preOrder()
{
preOrder(root);
}
//前序遍歷以node爲根的二分搜索樹,遞歸算法
private void preOrder(Node node)
{
//遞歸終止條件
if (node == null)
{
return;
}
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
前序遍歷是最自然和最常用的遍歷方式。
中序遍歷
中序遍歷體現在訪問這個節點的順序在中間。
你會發現,中序遍歷其實就是按照數字大小來進行遍歷的。原因就是因爲左子樹的節點要比根節點的數字小,右子樹的節點要比根節點大。所以遍歷的時候,也是先遍歷左子樹,再根節點,再右子樹。
//中序遍歷遞歸實現
public void inOrder()
{
inOrder(root);
}
private void inOrder(Node node)
{
if (node == null)
{
return;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
後序遍歷
應用:釋放內存。
//後序遍歷遞歸實現
public void postOrder()
{
postOrder(root);
}
private void postOrder(Node node)
{
if (node == null)
return;
postOrder(node.left);
postOrder(node.right);
System.out.println(node.e);
}
對每一個節點使用遞歸遍歷的方式,會有三次遍歷的機會。分別是:在遍歷左子樹之前會訪問這個根節點。在遍歷左子樹完成之後會回到根節點,會訪問這個根節點,在遍歷右子樹之後會回到這個根節點,會再遍歷一次。
那麼對一棵樹來說,前序遍歷的過程:
這是前序遍歷一棵樹的部分過程,從28->16->13,下面的點表示的是這個訪問了幾次。
訪問左子樹完成之後:
前序遍歷的完整過程:
這裏面的藍色的點就是第一次訪問這個節點時候。然後對這個節點進行輸出操作。
中序遍歷:
中序遍歷是當第二次訪問到這個節點時候進行操作,第一次訪問到該節點不進行操作。
例如這裏的16,第一次訪問16時,不輸出16,只是第二次訪問16時才輸出16.
下面是左子樹完成遍歷的過程。
圖中藍色的點是對節點進行操作的時候:
後序遍歷:
只有第三次訪問節點的時候,纔對節點進行操作。
部分樹的操作如下:
前序遍歷的非遞歸的寫法
前序遍歷的遞歸寫法:
這裏藉助棧的特性記錄遍歷的位置:
首先,訪問28節點,將節點壓入棧,也就是接下來要訪問28這個根節點了。
然後訪問28這個節點,節點訪問完成之後,28這個節點出棧
然後準備訪問兩個子節點,壓棧30,16兩個元素。因爲先訪問16,所以要先壓棧30.
然後16出棧,壓棧22和13.
然後出棧13,壓入13的子節點,但是子節點爲空,什麼都不用壓入了。然後出棧22.訪問22節點。
然後出棧30,訪問節點30;
然後將42和29入棧,然後訪1問29節點。
然後取棧頂元素42.
遍歷完成!
//前序遍歷的非遞歸的遍歷。
public void preOrderNR()
{
Stack<Node> stack=new Stack<>();
//先入棧根節點
stack.push(root);
while (!stack.isEmpty())
{
//cur節點就是當前要訪問的節點
Node cur=stack.pop();
System.out.println(cur.e);
if (cur.right != null)
stack.push(cur.right);
if (cur.left != null)
stack.push(cur.left);
}
}
廣度優先遍歷
按照層數來進行遍歷。
這個是使用非遞歸的方式進行遍歷的。使用隊列的結構。
先將根節點入隊,然後再將左右孩子入隊。
具體展示如圖:
初始化的時候將隊首入隊:
然後遍歷的時候先看隊首是誰,訪問隊首之後,將隊首出隊。這裏是指28出隊。
然後將第二層16和30入隊。因爲隊列裏面是先進先出,所以將樹的左子樹先入隊,然後右子樹再入隊,這裏是16先入隊,然後30再入隊。
然後取出隊首元素16,對16進行訪問。然後將16的孩子13和22入隊。
然後30出隊,對30進行訪問。並對30的兩個孩子29和42進行入隊。
然後將13進行出隊,並進行訪問。13s是葉子節點,沒有元素入隊。
然後直到隊首爲空:遍歷結束:
編程實現:
//層序遍歷(廣度優先遍歷)
public void levelOrder()
{
Queue<Node> q=new LinkedList<>();
//將根節點入隊
q.add(root);
while (!q.isEmpty())
{
Node cur=q.remove();
System.out.println(cur.e);
if (cur.left != null)
q.add(cur.left);
if(cur.right != null)
q.add(cur.right);
}
}
中序遍歷遞歸實現
//中序遍歷遞歸實現
public void inOrder()
{
inOrder(root);
}
private void inOrder(Node node)
{
if (node == null)
{
return;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
廣度優先遍歷可以更快的找到你想要查找的元素。主要用於搜索上面。
刪除節點
找到樹中的最小值和最大值
不同樹的結構是不同的,所以最小值可能不都在最左邊的葉子節點,這裏16就是最小值。
所以最小值是向左走再也走不動的位置。最大值是向右走再也走不動了。這裏最大值是30。
代碼實現:
//找到樹中元素的最小值
public E minimum()
{
if (size==0)
throw new IllegalArgumentException("BST is empty");
return minimum(root).e;
}
private Node minimum(Node node)
{
if (node.left == null)
return node;
return minimum(node.left);
}
//找到樹中元素的最大值
public E maximum()
{
if (size==0)
throw new IllegalArgumentException("BST is empty");
return maximum(root).e;
}
private Node maximum(Node node)
{
if (node.right == null)
return node;
return maximum(node.right);
}
刪除二分搜索樹的最小值
如果最小值是這種情況:
這個是個葉子節點,直接將這個節點刪除就行了。
但是如果最小值不是葉子節點呢?
那麼就可以將22刪除後,然後將後面的右子樹做成41的左子樹。
刪除二分搜索樹的最大值
如果是葉子節點,直接將葉子節點刪除就好。
刪除後:
如果不是葉子節點,例如這裏的58這個節點,將這個節點刪除之後,直接將這個節點的左子樹變成41的右子樹就可以了。
//從二分搜索樹中刪除最小值所在節點,返回最小值
public E removeMin()
{
E ret=minimum();
root=removeMin(root);
return ret;
}
//刪除掉以node爲根的二分搜索樹中的最小節點
//返回刪除節點後的新的二分搜索樹的節點
private Node removeMin(Node node)
{
if (node.left == null)
{
//但是可能有右子樹,那麼這個右子樹要保存起來
Node rightNode=node.right;
node.right=null;
size--;
return rightNode;
}
node.left=removeMin(node.left);
return node;
}
//從二分搜索樹中刪除最大值所在的節點
public E removeMax()
{
E ret=removeMax();
root=removeMax(root);
return ret;
}
//刪除掉以node爲根的二分搜索樹中的最大節點
//返回刪除節點後新的二分搜索樹的根
private Node removeMax(Node node)
{
if (node.right == null)
{
Node leftNode=node.left;
node.left=null;
size--;
return leftNode;
}
node.right=removeMax(node.right);
return node;
}
刪除任意元素
1.刪除的節點只有左孩子(或者右孩子)的話。例如下面刪除的節點是58.
就是將這個節點刪除後,然後將這個50當作根節點的右孩子。
2.刪除左右都有孩子的節點d
刪除58這個節點之後,要找一個新的節點來替代他,那麼這裏找的是比58這個節點還大的那個節點。也就是找到58的右子樹中最小的那個節點。也就是59那個節點。
刪除之後:
然後刪除d,s成爲新的子樹的根。
代碼如下:
//從二分搜索樹中刪除元素e的節點
public void remove(E e)
{
root=remove(root,e);
}
//刪除掉以node爲根的二分搜索樹中值爲e的節點,遞歸算法
//返回刪除節點後新的二分搜索樹的根
private Node remove(Node node,E e)
{
if (node == null)
{
return null;
}
if (e.compareTo(node.e) < 0)
{
//去左子樹去找待刪除的元素
node.left=remove(node.left,e);
return node;
}
else if (e.compareTo(node.e) > 0)
{
//將刪除的那個
node.right=remove(node.right,e);
return node;
}
else
{
//e==node.e
//待刪除節點左子樹爲空的情況
if (node.left == null)
{
Node rightNode=node.right;
node.right=null;
size--;
return rightNode;
}
//待刪除節點右子樹爲空的情況
if (node.right == null)
{
Node leftNode=node.left;
node.left=null;
size--;
return leftNode;
}
//待刪除的節點左右子樹都不爲空的情況
//找到比待刪除節點大的最小節點,即待刪除節點右子樹的最小節點。
//用這個節點頂替待刪除節點的位置。
Node successor = minimum(node.right);
successor.right=removeMin(node.right);
successor.left=node.left;
node.left=node.right=null;
return successor;
}
}
二分搜索樹的順序性
1.使用中序遍歷的方法得到的就是有序的。
2.使用刪除最大值和最小值的方法得到的也是有序的。