數據結構(5):樹

一、樹

爲什麼要使用樹,因爲數組刪除、插入的效率太低。而鏈表查找數據的速度太慢,在樹中能快速的查找數據項、插入數據項和刪除數據項。

樹

從上圖中我們可以發現“樹”這種結構和現實中的樹一樣的,節點之間用線相連,形成父子關係。

知道了樹,我們還需要知道以下概念

路徑:順着連接節點的邊從一個節點到另一個節點,所經過的節點順序排列稱爲路徑。

:樹最上面的節點稱爲根節點。一個樹只有一個根。而且從根到任何節點有且只有一條路徑。

父節點:每個節點都有一條邊向上連接到另一個節點,這個節點就稱爲是下面這個節點的父節點。

子節點:每個節點都一條邊向下連接到另一個節點,下面的節點就是該節點的子節點。

葉子節點:沒有子節點的節點

子樹:每個節點都可以作爲一個子樹的根,它和它所有的子節點,子節點的子節點組合在一起就是一個子樹。

訪問:訪問一個節點是爲了在這個節點上執行一些操作,如查看節點的數據項。但是如果僅僅是經過一個節點,不認爲是訪問了這個節點。

高度:節點到葉子節點的最長路徑(邊數)

深度:根節點到這個節點所經歷的邊的個數

:一個節點的層數是指從根開始到這個節點有多少代。

大家可以對比參考下圖理解概念

樹的概念

二、二叉查找樹

二叉樹

二叉樹:顧名思義,樹的每個節點最多只能有兩個節點的樹,稱爲二叉樹。

二叉查找樹:在二叉樹的基礎上實現的快速查找的樹,在二叉查找樹中的任意一個節點,左子樹每個節點的值,都要小於這個節點的值,而右子樹每個節點的值都應大於這個節點的值。

二叉查找樹

要想實現二叉查找樹,首先創建一個節點類存儲數據和左右子節點的引用

/**
 * 二叉樹節點
 */
public class Node {
    //數據項
    public long data;
    //左子節點
    public Node leftChild;
    //右子節點
    public Node rightChild;

    public Node(long data){
        this.data=data;
    }
}

創建Tree類保存對根節點的引用及操作節點的相關方法

/**
 * 二叉樹
 */
public class Tree {
    //根節點
    public Node root;
}

1、插入節點

從根節點開始查找一個相應的節點,這個節點將稱爲新插入節點的父節點。當父節點找到後,通過判斷新節點的值比父節點的值的大小來決定是連接到左子節點還是右子節點。

/**
 * 插入節點
 *
 * @param value
 */
public void insert(long value) {
    //將插入的值封裝爲一個節點
    Node newNode = new Node(value);
    //引用當前節點
    Node current = root;
    //引用父節點
    Node parentNode;
    //如果root爲null,也就是第一次插入
    if (root == null) {
        root = newNode;
        return;
    } else {

        while (true) {
            //父節點指向當前節點
            parentNode = current;
            //如果當前指向的節點數據比插入的要大,則向左走
            if (current.data > value) {
                current = current.leftChild;
                if (current == null) {
                    parentNode.leftChild = newNode;
                    return;
                }
            } else {
                current = current.rightChild;
                if (current == null) {
                    parentNode.rightChild = newNode;
                    return;
                }
            }
        }

    }
}

2、查找節點

從根節點開始查找,如果查找的節點值比當前節點的值小,則繼續查找其左子樹,否則查找其右子樹。

/**
 * 查找節點
 *
 * @param value
 * @return
 */
public Node find(long value) {
    //引用當前節點,從根節點開始
    Node current = root;

    //只要查找的節點不等於null 且不與查找的值相等就循環
    while (current != null && current.data != value) {

        //進行比較,比較查找值和當前節點的大小
        if (current.data > value) {
            current = current.leftChild;

        } else {
            current = current.rightChild;
        }

    }

    return current;

}

3、遍歷二叉查找樹

遍歷樹:遍歷樹是根據一個特定的順序訪問樹的每一個節點,根據順序的不同分爲前序、中序、後序遍歷三種,這三種遍歷方式的區別就在於打印根節點值的順序。

前序遍歷:1.訪問(打印)根節點2.前序遍歷左子樹3.前序遍歷右子樹

前序遍歷

/**
 * 前序遍歷
 */
public void frontOrder(Node localNode) {
    if (localNode != null) {
        //訪問根節點
        System.out.println(localNode.data);
        //前序遍歷左子樹
        frontOrder(localNode.leftChild);
        //前序遍歷右子樹
        frontOrder(localNode.rightChild);
    }
}

中序遍歷:1.中序遍歷左子樹2.訪問(打印)根節點3.中序遍歷右子樹

中序遍歷

/**
 * 中序遍歷
 *
 * @param localNode
 */
public void inOrder(Node localNode) {
    if (localNode != null) {
        //中序遍歷左子樹
        inOrder(localNode.leftChild);
        //訪問根節點
        System.out.println(localNode.data);
        //中序右子樹
        inOrder(localNode.rightChild);
    }
}

後序遍歷:1.後序遍歷左子樹2.後序遍歷右子樹3.訪問(打印)根節點

後序遍歷

/**
 * 後序遍歷
 */
public void afterOrder(Node localNode) {
    if (localNode != null) {
        //後序遍歷左子樹
        afterOrder(localNode.leftChild);
        //後序遍歷右子樹
        afterOrder(localNode.rightChild);
        //訪問根節點
        System.out.println(localNode.data);
    }
}

4、刪除二叉樹節點

刪除節點是二叉樹操作中最複雜的。在刪除之前首先要查找要刪的節點。找到節點後,這個要刪除的節點可能會有三種情況需要考慮。

1.該節點是葉子節點,沒有子節點

要刪除的節點是葉子節點時

要刪除葉節點,只需要改變該節點的父節點的引用值,將指向該節點的引用設置爲null就可以了。

2.該節點有一個子節點

要刪除的節點只有一個子節點時

改變父節點的引用,將其直接指向要刪除節點的子節點。

3.該節點有兩個子節點

要刪除有兩個子節點的節點,就需要使用它的中序後繼來替代該節點,如下圖中50有兩個子節點。

刪除有兩個子節點的節點

那麼什麼是要刪除的節點的中序後繼呢,對比上圖,我們來找要刪除節點的右子節點的左子節點,直到左葉子節點。這個節點爲54那麼剛好可以作爲替換50而不影響這個二叉查找樹的規則。

/**
 * 查找中序後繼
 *
 * @param delNode
 * @return
 */
public Node getSuccessor(Node delNode) {
    Node successor = delNode;
    Node successorParent = delNode;
    //要刪除節點的右子樹
    Node current = delNode.rightChild;

    //向左子樹查找直到最終一個左子節點
    while (current != null) {
        successorParent = successor;
        successor = current;
        current = current.leftChild;

    }
    //如果中序節點不爲要刪除的右子樹
    if (successor != delNode.rightChild) {
        //將查到的節點的父節點的左子節點(也就是查到的節點)指向查到節點的右子樹
        successorParent.leftChild = successor.rightChild;
        //將查到的節點的右子樹指向要刪除節點的右子樹
        successor.rightChild = delNode.rightChild;
    }
    return successor;
}

實現刪除

/**
 * 刪除節點
 *
 * @param value
 * @return
 */
public boolean delete(long value) {
    //引用當前節點,從根節點開始
    Node current = root;
    //引用當前節點的父節點
    Node parent = root;
    //是否爲左節點
    boolean isLeftChild = true;

    if (current == null) {
        return false;
    }
    while (current.data != value) {
        parent = current;
        //進行比較,比較查找值和當前節點的大小
        if (current.data > value) {
            current = current.leftChild;
            isLeftChild = true;
        } else {
            current = current.rightChild;
            isLeftChild = false;
        }
        //如果查找不到
        if (current == null) {
            return false;
        }
    }
    //刪除葉子節點
    if (current.leftChild == null && current.rightChild == null) {
        if (current == root) {
            root = null;
        } else
            //如果它是父節點的左子節點
            if (isLeftChild) {
                parent.leftChild = null;
            } else {
                parent.rightChild = null;
            }
    } else if (current.rightChild == null) {//如果只有一個節點,當前節點是左
        if (current == root) {
            root = current.leftChild;
        } else
            //如果當前是左節點,則爲父節點的左節點賦值
            if (isLeftChild) {
                parent.leftChild = current.leftChild;
            } else {
                parent.rightChild = current.leftChild;
            }
    } else if (current.leftChild == null) {//只有一個子節點
        if (current == root) {
            root = current.rightChild;
        } else if (isLeftChild) {
            parent.leftChild = current.rightChild;
        } else {
            parent.rightChild = current.rightChild;
        }
    } else {
        //查找要刪除節點的中序後繼
        Node successor = getSuccessor(current);
        if (current == root){
            root = successor;
        }else if(isLeftChild){
            parent.leftChild = successor;

        }else{
            parent.rightChild = successor;
        }
        successor.leftChild = current.leftChild;

    }

    return true;
}

三、其他

  1. 使用中序遍歷二叉查找樹,輸出數列是有序的,時間複雜度是O(n),因此是十分高效的。
  2. 有序的數列插入到二叉查找樹中,將產生不平衡的二叉查找樹,影響二叉查找樹的性能。
  3. 學習二叉樹可多畫圖推演
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章