數據結構之二叉樹(一)

一、二叉樹的定義

二叉樹是一種遞歸的非線性數據結構。它要麼是一棵空樹,要麼是一棵由根結點和根結點上的兩棵互不相交的子樹(左子樹、右子樹)組成的非空樹。它有五種形態,圖示如下:
在這裏插入圖片描述
這裏對圖示做一些說明:
首先,二叉樹上的結點最多隻有兩個子樹,分別是左子樹和右子樹,而子樹也是一棵二叉樹,它可能是上面五種形態的任何一種。也就是說當一棵二叉樹只有一個根結點時,代表其兩棵子樹爲空。所以,一定要形成這樣的一個認識,一棵二叉樹絕對是由上述五種形態的二叉樹組合而成的。

二、關於二叉樹的術語

定義術語的目的是爲了後期描述其性質或一些操作方便,所以有一個印象即可,不記得再來這裏看一下就好了。

在這裏插入圖片描述

  1. 樹的結點(node):包含一個數據元素及兩個指向子樹的分支;
  2. 孩子結點(child node):結點的子樹的根稱爲該結點的孩子;
  3. 雙親結點:B 結點是A 結點的孩子,則A結點是B 結點的雙親;
  4. 兄弟結點:同一雙親的孩子結點; 堂兄結點:同一層上結點;
  5. 祖先結點: 從根到該結點的所經分支上的所有結點;
  6. 子孫結點:以某結點爲根的子樹中任一結點都稱爲該結點的子孫;
  7. 結點層:根結點的層定義爲1;根的孩子爲第二層結點,依此類推;
  8. 樹的深度:樹中最大的結點層;
  9. 結點的度:結點子樹的個數;
  10. 樹的高度:任一棵樹到任一葉子結點的最大長度,葉子結點高度爲1;
  11. 樹的度: 樹中最大的結點度;
  12. 葉子結點:也叫終端結點,是度爲 0 的結點,即最後的沒有子樹的結點;
  13. 分枝結點:度不爲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.");
        }
    }
添加結點

添加結點的邏輯比較簡單,首先得知道一點,二叉樹添加的結點一定會成爲葉子結點(這句話再讀三遍)。添加結點的過程以圖示解釋:
在這裏插入圖片描述
添加結點的過程是一個遞歸的過程。從根結點開始,要添加的結點710小,需要添加到根結點的左子樹上,所以與左子樹根結點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;
    }

總結:二叉樹添加結點的過程其實就是找到添加結點的合適位置。而找這個位置的過程就是不斷與樹上的結點比較,發現比樹上結點小,就去與它的左孩子比較,如果比結點大,就與它的右孩子比較,如果相等,則默認不加入。直到到達二叉樹的葉子結點的位置,將新添加的結點作爲葉子結點放在二叉樹上。

刪除結點

而刪除結點的過程呢就有點複雜了。它分爲三種情況:

  1. 被刪除結點沒有孩子結點
    在這裏插入圖片描述
  2. 被刪除結點只有一個左孩子或只有一個右孩子
    在這裏插入圖片描述
    當結點7是結點6的左孩子時同理。
  3. 被刪除結點既有左孩子又有右孩子
    在這裏插入圖片描述
    上面圖上的文字如果需要可以多讀幾遍,我下面再舉個複雜例子說明:
    在這裏插入圖片描述
    好了,相信你看完就理解刪除結點的邏輯了。
    代碼如下:
   //刪除指定的結點
    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;
            }
        }
    }

有點長,關於二叉樹的遍歷就放在另外一篇文章了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章