The best way to end your fear is to face it yourself.
結束恐懼的最佳辦法就是自己面對。
本分分享了二叉搜尋樹的幾種實現,由簡入繁。說句題外話,馬上又是金三銀四的季節了,無論跳不跳槽,增加自己的知識儲備總是沒錯的。從程式碼裡讀原理,讀思想。
所有原始碼均已上傳至github:連結
日誌
2019年3月28日 AVL樹
準備
public class Node {
/**
* data
*/
public int data;
/**
* 左節點
*/
public Node left;
/**
* 右節點
*/
public Node right;
public Node(int data) {
this.data = data;
}
}複製程式碼
實現一個支援插入、刪除、查詢操作的二叉查詢樹,並且找二叉樹的最大值和最小值操作
插入操作
傳入一個data值,當tree為null,初始化tree,否則遍歷tree,比較data與node.data的值,大於的話插入右節點,否則插入左節點。
private void insert(int data) {
if (null == tree) {
tree = new Node(data);
return;
}
Node node = tree;
while (null != node) {
if (data > node.data) {
if (null == node.right) {
node.right = new Node(data);
return;
}
node = node.right;
} else {
if (null == node.left) {
node.left = new Node(data);
return;
}
node = node.left;
}
}
}複製程式碼
查詢
查詢的實現也比較簡單,遍歷tree的左右節點,直到找到data == node.data
private Node find(int data) {
Node node = tree;
while (null != node) {
if (data > node.data) {
node = node.right;
} else if (data < node.data) {
node = node.left;
} else {
return node;
}
}
return null;
}複製程式碼
找二叉樹的最大值
找二叉樹的最大值最小值比較簡單,按照樹的結構遍歷即可
private Node findMaxNode() {
if (null == tree) return null;
Node node = tree;
while (null != node.right) {
node = node.right;
}
return node;
}複製程式碼
找二叉樹的最小值
private Node findMinNode() {
if (null == tree) return null;
Node node = tree;
while (null != node.left) {
node = node.left;
}
return node;
}複製程式碼
刪除操作
刪除是這裡面最複雜的操作了,這裡詳細講一下。
針對刪除的節點的位置不同需要考慮以下三種情況:
- 如果要刪除的節點沒有子節點,只需要直接將父節點中,指向要刪除節點的指標置為 null。
- 如果要刪除的節點只有一個子節點(左子節點或者右子節點),只需要更新父節點中,指向要刪除節點的指標,讓它指向要刪除節點的子節點就可以了。
- 如果要刪除的節點有兩個子節點,這就比較複雜了。需要找到這個節點的右子樹中的最小節點,把它替換到要刪除的節點上。然後再刪除掉這個最小節點,因為最小節點肯定沒有左子節點(如果有左子結點,那就不是最小節點了)。
private void delete(int data) {
Node node = tree;
//node的父節點
Node pNode = null;
while (null != node && node.data != data) {
pNode = node;
if (data > node.data) {
node = node.right;
} else {
node = node.left;
}
}
if (null == node) return;
if (null != node.left && null != node.right) {
Node p = node.right;
//p的父節點
Node pp = node;
while (null != p.left) {
pp = p.left;
p = p.left;
}
//刪除操作
node.data = p.data;
node = p;
pNode = pp;
}
Node child;
if (null != node.left) {
child = node.left;
} else if (null != node.right) {
child = node.right;
} else {
child = null;
}
if (null == pNode) {
tree = child;
} else if (pNode.left == null) {
pNode.left = child;
} else {
pNode.right = child;
}
}複製程式碼
實現查詢二叉查詢樹中某個節點的後繼、前驅節點
ps:因為這裡測試的節點都是根節點,因為比較簡單,不需要遍歷該節點的父節點。
後續如果需要,歡迎留言,我補充一下。
查詢前驅節點
private Node findPreNode(Node node) {
if (null == node) return null;
if (null != node.left){
Node left = node.left;
while (null != left.right){
left = left.right;
}
return left;
}
return null;
}複製程式碼
查詢後繼節點
private Node findNextNode(Node node) {
if (null == node) return null;
if (null != node.right) {
Node right = node.right;
while (null != right.left) {
right = right.left;
}
return right;
}
return null;
}複製程式碼
實現二叉查詢樹前、中、後序以及層級遍歷
前序遍歷
public void preOrderTraversal(Node tree) {
if (null == tree) return;
System.out.print(tree.data + " ");
preOrderTraversal(tree.left);
preOrderTraversal(tree.right);
}複製程式碼
中序遍歷
public void inOrderTraversal(Node tree) {
if (null == tree) return;
inOrderTraversal(tree.left);
System.out.print(tree.data + " ");
inOrderTraversal(tree.right);
}複製程式碼
後序遍歷
public void postOrderTraversal(Node tree) {
if (null == tree) return;
postOrderTraversal(tree.left);
postOrderTraversal(tree.right);
System.out.print(tree.data + " ");
}複製程式碼
層級遍歷
層級遍歷的思想是維護一個隊裡,每一次遍歷tree,將tree的節點列印,然後將tree的left和right節點存入佇列裡(每一次列印時從佇列裡取即可)。
public void levelTraversal(Node tree) {
if (null == tree) return;
LinkedList<Node> linkedList = new LinkedList<>();
Node node;
linkedList.offer(tree);
while (!linkedList.isEmpty()) {
node = linkedList.poll();
System.out.print(node.data + " ");
if (null != node.left) {
linkedList.offer(node.left);
}
if (null != node.right) {
linkedList.offer(node.right);
}
}
}複製程式碼
AVL樹
定義
AVL樹是一種平衡二叉樹。貌似就windows對程式地址空間的管理用到了AVL樹。
- 左右子樹的高度差小於等於 1。
- 其每一個子樹均為平衡二叉樹。
核心
AVL樹核心在於左旋和右旋上,當出現不平衡的情況時,它會自動調整。
宣告一個AVL樹的Node類
在普通二叉樹的基礎上新增了幾個屬性
- balance 是平衡標識,它的區間是[-1,1]
- height 是該節點的高度,自上而下計算,從0開始
public class Node {
/**
* 資料
*/
int data;
/**
* 高度
*/
int height;
/**
* 平衡標識
*/
int balance;
/**
* 左子樹
*/
Node left;
/**
* 右子樹
*/
Node right;
/**
* 根
*/
Node parent;
/**
* 有參構造方法
*
* @param data 資料
* @param parent 父節點
*/
Node(int data, Node parent) {
this.data = data;
this.parent = parent;
}
}複製程式碼
插入
插入方法和常規插入的區別是需要將當前節點作為根節點插入,並且執行一下自平衡方法。
private void insert(int data) {
if (null == root) {
root = new Node(data, null);
} else {
Node node = root;
Node parent;
while (node.data != data) {
parent = node;
boolean goleft = node.data > data;
node = goleft ? node.left : node.right;
if (null == node) {
if (goleft) {
parent.left = new Node(data, parent);
} else {
parent.right = new Node(data, parent);
}
rebalance(parent);
break;
}
}
}
}複製程式碼
刪除
這裡是根據節點刪除。和常規的刪除相比,也是多了一步自平衡。
private void delete(Node node) {
if (null == node.left && null == node.right) {
if (null == node.parent) {
root = null;
} else {
Node parent = node.parent;
if (parent.left == node) {
parent.left = null;
} else {
parent.right = null;
}
rebalance(parent);
}
}
if (null != node.left) {
Node child = node.left;
while (null != child.right) {
child = child.right;
}
node.data = child.data;
delete(child);
} else {
Node child = node.right;
if (null != child) {
while (null != child.left) {
child = child.left;
}
node.data = child.data;
delete(child);
}
}
}複製程式碼
自平衡
有這麼四種情況。
這裡是否需要自平衡,是根據balance來判斷的,當balance為-2的時候,(如果該節點的左子樹的左子樹的高度小於右子樹高度,左旋,)右旋;當balance為2的時候,(如果該節點的右子樹的右子樹的高度小於左子樹高度,右旋,)左旋;最後判斷根節點是否為null,是則遞迴,否則賦值,停止遞迴;平衡完畢。
private void rebalance(Node node) {
setBalance(node);
if (node.balance == -2) {
if (getHeight(node.left.left) < getHeight(node.left.right)) {
node.left = rotateLeft(node.left);
}
node = rotateRight(node);
} else if (node.balance == 2) {
if (getHeight(node.right.right) < getHeight(node.right.left)) {
node.right = rotateRight(node.right);
}
node = rotateLeft(node);
}
if (null != node.parent) {
rebalance(node.parent);
} else {
root = node;
}
}複製程式碼
左右旋
左旋和右旋的原理是一樣的,只要瞭解一種即可。
private Node rotateLeft(Node node) {
Node right = node.right;
right.parent = node.parent;
node.right = right.left;
if (null != node.right)
node.right.parent = node;
right.left = node;
node.parent = right;
if (null != right.parent) {
if (right.parent.right == node) {
right.parent.right = right;
} else {
right.parent.left = right;
}
}
setBalance(node, right);
return right;
}複製程式碼
private Node rotateRight(Node node) {
Node left = node.left;
left.parent = node.parent;
node.left = left.right;
if (null != node.left)
node.left.parent = node;
left.right = node;
node.parent = left;
if (null != left.parent) {
if (left.parent.right == node) {
left.parent.right = left;
} else {
left.parent.left = left;
}
}
setBalance(node, left);
return left;
}複製程式碼
設定平衡點
首先是重置高度,取左右子樹高度的最大值加一作為該節點的高度。
其次是設定平衡點,用來判斷該節點是否平衡
private void setBalance(Node... nodes) {
for (Node node : nodes) {
if (null != node) {
//重置高度
node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;
node.balance = getHeight(node.right) - getHeight(node.left);
}
}
}複製程式碼
獲取樹的高度
private int getHeight(Node node) {
if (null != node)
return node.height;
return -1;
}複製程式碼
測試程式碼
這裡列印用的中序遍歷(中序遍歷可以從小到大列印出來)
private void print(Node node) {
if (null != node) {
print(node.left);
System.out.println(node.data + " " + node.balance);
print(node.right);
}
}
public static void main(String[] args) {
AVLTree avlTree = new AVLTree();
for (int i = 1; i < 10; i++) {
avlTree.insert(i);
}
avlTree.print();
}複製程式碼
測試結果
4
2 6
1 3 5 8
7 9
data - balance
end
您的點贊和關注是對我最大的支援,謝謝!