一、二叉樹的定義
二叉樹是一種遞歸的非線性數據結構。它要麼是一棵空樹,要麼是一棵由根結點和根結點上的兩棵互不相交的子樹(左子樹、右子樹)組成的非空樹。它有五種形態,圖示如下:
這裏對圖示做一些說明:
首先,二叉樹上的結點最多隻有兩個子樹,分別是左子樹和右子樹,而子樹也是一棵二叉樹,它可能是上面五種形態的任何一種。也就是說當一棵二叉樹只有一個根結點時,代表其兩棵子樹爲空。所以,一定要形成這樣的一個認識,一棵二叉樹絕對是由上述五種形態的二叉樹組合而成的。
二、關於二叉樹的術語
定義術語的目的是爲了後期描述其性質或一些操作方便,所以有一個印象即可,不記得再來這裏看一下就好了。
- 樹的結點(node):包含一個數據元素及兩個指向子樹的分支;
- 孩子結點(child node):結點的子樹的根稱爲該結點的孩子;
- 雙親結點:B 結點是A 結點的孩子,則A結點是B 結點的雙親;
- 兄弟結點:同一雙親的孩子結點; 堂兄結點:同一層上結點;
- 祖先結點: 從根到該結點的所經分支上的所有結點;
- 子孫結點:以某結點爲根的子樹中任一結點都稱爲該結點的子孫;
- 結點層:根結點的層定義爲1;根的孩子爲第二層結點,依此類推;
- 樹的深度:樹中最大的結點層;
- 結點的度:結點子樹的個數;
- 樹的高度:任一棵樹到任一葉子結點的最大長度,葉子結點高度爲1;
- 樹的度: 樹中最大的結點度;
- 葉子結點:也叫終端結點,是度爲 0 的結點,即最後的沒有子樹的結點;
- 分枝結點:度不爲0的結點;
二叉樹有幾種類型:
1、滿二叉樹:樹上的任一結點除葉子結點外都有兩棵子樹
2、完全二叉樹:葉子結點只會出現在最後一層或倒數第二層,結點不可能只有右子樹。也就是說,完全二叉樹按照從上至下從左至右,能無間隔的給結點排序。圖示如下:
3、二叉搜索樹:在二叉樹的基礎上保證左子樹上元素都比根結點小,右子樹上元素都比根結點大。
我們後面的討論都是基於二叉搜索樹。
三、代碼實現
首先定義出二叉樹上的結點類:
public class Node {
K key;
V value;
Node left;
Node right;
int height;
public Node(K key, V value) {
this.key = key;
this.value = value;
this.left = null;
this.right = null;
//結點默認添加爲葉子結點,故高度爲1
this.height = 1;
}
}
爲了安全,結點類可以設計爲二叉樹的內部類:如下所示:
public class BinaryTree<K extends Comparable<K>,V> {
private class Node {
K key;
V value;
Node left;
Node right;
int height;
public Node(K key, V value) {
this.key = key;
this.value = value;
this.left = null;
this.right = null;
//結點默認添加爲葉子結點,故高度爲1
this.height = 1;
}
}
private Node root;
private int size;
public BinaryTree() {
root = null;
size = 0;
}
//獲取AVL樹中結點個數
public int getSize() {
return size;
}
//判空
public boolean isEmpty() {
return size == 0;
}
}
下面說一下求結點高度的方法:
由定義知:結點的高度永遠比其孩子中最大的高度大1,如果孩子爲null時,它的高度爲0。因此可以通過遞歸求取:
求取樹的高度
//獲得結點的高度
public int getHeight(Node node) {
if(node == null){
return 0;
}
return node.height;
}
尋找樹上key最大和最小的結點
由於我們的二叉搜索樹保存的數據是一個鍵值對,而且排序是按照key來排的,因此我們搜索的時候也是基於key來比較的。
//找到以node爲根結點的樹上的最大結點
private Node findMax(Node node) {
if(node == null) {
return null;
}
while(node.right != null) {
node = node.right;
}
return node;
}
//找到以node爲根結點的樹上的最小結點
private Node findMin(Node node) {
if(node == null){
return null;
}
while(node.left != null){
node = node.left;
}
return node;
}
方法定義爲private是因爲我們的結點類是私有內部類,外部訪問不了。
尋找指定的結點
//在以node爲根的樹上尋找key的結點
private Node findNode(Node node, K key) {
if(node == null){
return null;
}
if(node.key.compareTo(key) == 0) {
return node;
}else if(node.key.compareTo(key) > 0) {
return findNode(node.left, key);
}else{
return findNode(node.right, key);
}
}
另外定義幾個常用方法如下:
//獲取key的結點的value
public V get(K key) {
Node node = findNode(root, key);
if(node != null){
return node.value;
}
return null;
}
//獲取以node爲根,key爲鍵的樹上的結點
private Node getNode(Node node, K key) {
return findNode(node, key);
}
//設置key的結點的值
public void set(K key, V value) {
Node node = findNode(root, key);
if(node != null) {
node.value = value;
}else{
throw new IllegalStateException("Node doesn't exist.");
}
}
添加結點
添加結點的邏輯比較簡單,首先得知道一點,二叉樹添加的結點一定會成爲葉子結點(這句話再讀三遍)。添加結點的過程以圖示解釋:
添加結點的過程是一個遞歸的過程。從根結點開始,要添加的結點7比10小,需要添加到根結點的左子樹上,所以與左子樹根結點8比較,比8小,需要添加到以8爲根結點的左子樹上,所以與根結點爲6的左子樹比較,比6大,添加到以6爲根結點的右子樹上,發現6的右子樹爲null,因此將新添加的結點作爲其右子樹。添加後的二叉樹如下:
代碼如下:
//添加結點
public void add(K key, V value) {
root = add(root, key, value);
}
private Node add(Node node, K key, V value) {
if(node == null) { //找到了合適的位置,直接添加
node = new Node(key,value);
size ++;
//維護結點的高度
node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;
}
if(node.key.compareTo(key) > 0) {
//比當前根結點小,遞歸添加到其左子樹上
node.left = add(node.left, key, value);
}else if(node.key.compareTo(key) < 0){
//比當前結點大,遞歸添加到其右子樹上
node.right = add(node.right, key, value);
}else {
node.value = value;
}
return node;
}
總結:二叉樹添加結點的過程其實就是找到添加結點的合適位置。而找這個位置的過程就是不斷與樹上的結點比較,發現比樹上結點小,就去與它的左孩子比較,如果比結點大,就與它的右孩子比較,如果相等,則默認不加入。直到到達二叉樹的葉子結點的位置,將新添加的結點作爲葉子結點放在二叉樹上。
刪除結點
而刪除結點的過程呢就有點複雜了。它分爲三種情況:
- 被刪除結點沒有孩子結點
- 被刪除結點只有一個左孩子或只有一個右孩子
當結點7是結點6的左孩子時同理。 - 被刪除結點既有左孩子又有右孩子
上面圖上的文字如果需要可以多讀幾遍,我下面再舉個複雜例子說明:
好了,相信你看完就理解刪除結點的邏輯了。
代碼如下:
//刪除指定的結點
public V remove(K key) {
Node node = getNode(root, key);
if(node != null) {
return node.value;
}
return null;
}
private Node remove(Node node, K key) {
if(node == null) {
return null;
}
if(node.key.compareTo(key) > 0) {
node.left = remove(node.left, key);
return node;
}else if(node.key.compareTo(key) < 0) {
node.right = remove(node.right, key);
return node;
}else { //找到了要刪除的結點
if(node.left == null) { //沒有左孩子或者沒有孩子結點
Node rightNode = node.right;
size --;
node.right = null;
return rightNode;
}else if(node.right == null) { //沒有右孩子
Node leftNode = node.left;
size --;
node.left = null;
return leftNode;
}else {
//找到右子樹最小結點
Node successor = findMin(node.right);
//這裏代碼的目的是與目標結點互換
successor.right = removeMin(node.right);
successor.left = node.left;
return successor;
}
}
}
有點長,關於二叉樹的遍歷就放在另外一篇文章了。