數據結構|二叉搜索樹

二叉搜索樹

1 定義

二叉搜索樹,也稱爲二叉查找樹或者二叉排序樹。假設節點node是二叉搜索樹中的某個結點,結點node包含左右指針和數據值x,如果node結點的左結點上存在left結點(數據值爲y),則y值小於x值,如果node結點的右結點上存在right結點(數據值爲z),則z值大於x值。因爲這個特性,它經常用於二分查找。

2 性質

  • 若樹中任意結點的左子樹不爲空,則左子樹上的所有結點的值均小於該結點的值;
  • 若樹中任意結點的右子樹不爲空,則右子樹上的所有結點的值均大於該結點的值;
  • 樹中任意結點的左、右子樹都爲二叉搜索樹;

3 結點結構

二叉搜索樹的結點結構跟普通二叉樹結點一樣,擁有左孩子的指針,右孩子的指針以及數據域。可以用下面的圖來表示:
結點結構
Java代碼定義爲:

class TreeNode<T> {
    TreeNode<T> leftNode;
    TreeNode<T> rightNode;
    T nodeValue;
    
    public TreeNode(T object) {
        this.nodeValue = object;
    }
}

4 創建二叉搜索樹

首先給出一個無序的數組int[] num = {11,10,9,22,13,30,17},我們以這個無序數組爲例建立一個二叉搜索樹:
第一步:i = 0,num[0]=11,二叉搜索樹爲空樹,則根節點爲11;
1
第二步:i = 1,num[1]=10,與根結點11比較,小於根結點且根結點的左子樹爲空,則作爲根結點的左結點;
2
第三步:i = 2, num[2]=9,小於根結點11且左結點不爲空,繼續與根結點的左結點10比較,小於10且該結點的左結點爲空,則作爲10的左結點;
3
第四步:i = 3,num[3]=22,大於根節點11且根結點的右結點爲空,則作爲根結點的右結點;
4
第五步:i = 4,num[4]=13,大於根結點11且根結點的右結點不爲空,小於根結點的右結點,且其左結點爲空,則作爲根結點的右結點的左結點;
5
第六步:i = 5,num[5]= 30,大於根節點11且根結點的右結點不爲空,大於根結點的右結點,且其右結點爲空,則作爲根結點的右結點的右結點;
6
第七步:i = 6,num[6]=17,大於根結點11且根結點的右結點不爲空,小於根結點的右結點,且其左結點不爲空,大於其左結點且其左結點的右結點爲空,則作爲根結點的右結點的左結點的右結點;
7
則其最後的二叉樹爲:
0
注意:二叉搜索樹的中序遍歷就是一個排序的數組。

用Java代碼實現整個插入的過程:

    /**
     * 二叉搜索樹插入
     * @param value
     */
    public void insert(Integer value) {
        TreeNode<Integer> node = new TreeNode<Integer>(value);
        if (rootNode == null) {
            rootNode = node; // 賦值根結點
            return;
        }
        TreeNode<Integer> currentNode = rootNode;
        TreeNode<Integer> parentNode = rootNode;
        boolean isLeftNode = false;
        while (currentNode != null) {
            parentNode = currentNode;
            if ((int)value > (int)parentNode.nodeValue) {
                currentNode = currentNode.rightNode; // 大於當前節點的值則往右子樹查找
                isLeftNode = false;
            } else if ((int)value < (int)parentNode.nodeValue) {
                currentNode = currentNode.leftNode;  // 小於當前節點的值則往左子樹查找
                isLeftNode = true;
            } else {
                return;  // 如果與結點值相同則結束
            }
        }
        
        if (isLeftNode) {
            parentNode.leftNode = node;
        } else {
            parentNode.rightNode = node;
        }
    }

插入完成後則可以用利用中序遍歷打印二叉樹的節點,看看是否是排好序的數組。我們這裏給出二叉樹的前序、中序和後序遍歷的Java代碼:

/**
 * 中序遍歷
 * @param root 根結點
 */
public void middleOrder(TreeNode<T> root) {
    if (root != null) {
        middleOrder(root.leftNode);
        System.out.println(root.nodeValue + " ");
        middleOrder(root.rightNode);
    }
}

/**
 * 前序遍歷
 * @param root 根結點
 */
public void preOrder(TreeNode<T> root) {
    if (root != null) {
        System.out.println(root.nodeValue + " ");
        middleOrder(root.leftNode);
        middleOrder(root.rightNode);
    } 
}

/**
 * 後序遍歷
 * @param root 根結點
 */
public void postOrder(TreeNode<T> root) {
    if (root != null) {
        middleOrder(root.leftNode);
        middleOrder(root.rightNode);
        System.out.println(root.nodeValue + " ");
    } 
}

5 查找過程

二叉搜索樹的查找過程:

  • 若樹爲空,則查找結束,無匹配;
  • 如果當前結點的值與需要查找的值相等,則查找成功;
  • 如果當前結點的值大於查找的值,則遞歸查找結點的左結點;
  • 如果當前結點的值小於查找的值,則遞歸查找結點的右結點;

Java代碼實現查找過程:

/**
 * 查找結點
 * @param value
 * @return
 */
public boolean search(Integer value) {
    if (rootNode == null) {
        return false;
    }
    TreeNode<Integer> currentNode = rootNode;
    TreeNode<Integer> parentNode = rootNode;
    while (currentNode != null) {
        parentNode = currentNode;
        if ((int)value < (int)parentNode.nodeValue) {
            currentNode = parentNode.leftNode;
        } else if ((int)value > (int)parentNode.nodeValue) {
            currentNode = parentNode.rightNode;
        } else {
            return true;
        }
    }
    
    return false;
}

6 插入過程

二叉搜索樹的插入需要做兩件事:

  • 一是與結點的值作比較,若有相等則插入結束;
  • 二是把待插入結點插入到合適的位置;

我們以上面的二叉搜索樹爲例,插入值爲12的結點。插入過程和結果如圖所示:
二叉搜索樹插入
插入過程的Java代碼同上面構建過程的insert方法.

7 刪除過程

二叉搜索樹的刪除稍微複雜一點,主要是因爲刪除結點後需要保證二叉搜索樹的性質。那麼主要分下面幾種情況討論:

  • 刪除結點爲葉子節點,則根據其在父結點的左右,把父結點對應的子結點置爲null;
  • 刪除結點只有左結點(左子樹),或者只有右結點(右子樹),則把左結點或者右結點直接替換當前結點;
  • 刪除結點既有左子樹又有右子樹,情況就比較複雜,需要找到它的前驅結點或者後繼結點。前驅結點是小於它的最大結點,後繼結點是大於他的最小結點;前面講過二叉搜索樹的中序遍歷,刪除結點的左邊則是它的前驅,右邊則是它的後繼,刪除它,可以用它的前驅或者後繼來替代它的位置,那麼怎麼在二叉樹中找到它的前驅結點或者後繼結點呢?

前驅結點的尋找:根據二叉搜索樹的性質知道,某結點的前驅結點爲其左子樹的右結點的右結點的右結點….右結點;
前驅結點

圖中 結點72的前驅是65

後繼節點的尋找:根據二叉搜索樹的性質知道,某結點的後繼節點爲其右子樹的左結點的左結點的左結點…左結點;
後驅結點

圖中 結點20的後繼結點爲25
這裏以後繼結點替換刪除結點爲例,進行說明,下面是動態展示圖:

後繼結點

根據上面的動態圖,可以知道後繼結點如何替換刪除結點:後繼結點的右結點爲後繼結點父結點的左孩子(後繼結點不存在左結點),刪除結點的左結點爲後繼結點的左孩子,後繼結點爲刪除結點父結點的左結點
Java代碼獲取前驅結點和後繼結點:

/**
 * 尋找前驅結點
 * @param delNode
 * @return
 */
public TreeNode<Integer> getPrecursorNode(TreeNode<Integer> delNode) {
    
    TreeNode<Integer> currentNode = delNode.leftNode;
    TreeNode<Integer> precursorNode = delNode;
    TreeNode<Integer> precursorParentNode = delNode;
    
    while(currentNode!=null) {
        precursorParentNode = precursorNode;
        precursorNode = currentNode;
        currentNode = currentNode.rightNode;
    }
    // 前驅結點爲刪除結點左子樹的右結點...右結點
    if (delNode.leftNode != precursorNode) {
        precursorParentNode.rightNode = precursorNode.leftNode;
        precursorNode.leftNode = delNode.leftNode;
    }
    return precursorNode;
}

/**
 * 尋找後繼結點
 * @param delNode
 * @return
 */
public TreeNode<Integer> getSuccessorNode(TreeNode<Integer> delNode) {
    
    TreeNode<Integer> currentNode = delNode.rightNode;
    TreeNode<Integer> successorNode = delNode;
    TreeNode<Integer> successorParentNode = delNode;
    
    while(currentNode!=null) {
        successorParentNode = successorNode;
        successorNode = currentNode;
        currentNode = currentNode.leftNode;
    }
    
	// 後繼結點爲刪除結點右子樹的左結點…左結點
    if (delNode.rightNode != successorNode) {
        successorParentNode.leftNode = successorNode.rightNode;
        successorNode.rightNode = delNode.rightNode;
    }
    return successorNode;
}

刪除結點的Java代碼:

/**
 * 刪除結點
 * @param value
 * @return
 */
public boolean deleteNode(Integer value) {
    if (rootNode == null) return false;
    TreeNode<Integer> currentNode = rootNode;
    TreeNode<Integer> parentNode = rootNode;
    boolean isLeftNode = false;
    
    // 找到要刪除的結點
    while (!value.equals(currentNode.nodeValue)) {
        parentNode = currentNode;
        if (value > parentNode.nodeValue) {
            currentNode = parentNode.rightNode;
            isLeftNode = false;
        } else if (value < parentNode.nodeValue) {
            currentNode = parentNode.leftNode;
            isLeftNode = true;
        }
        if (currentNode == null) {
            return false;
        }
    }
    
    // currentNode即爲要刪除的結點 處理四種情況
    // 當前結點爲葉子結點
    if (currentNode.leftNode == null && currentNode.rightNode == null) {
        if (currentNode == rootNode) {
            rootNode = null;
        } else {
            if (isLeftNode) {
                currentNode.leftNode = null;
            } else {
                currentNode.rightNode = null;
            }
        }
        return true;
    } else if (currentNode.leftNode == null) { // 刪除結點左結點爲空
        if (currentNode == rootNode) {
            rootNode = currentNode.rightNode;
        } else {
            if (isLeftNode) {
                parentNode.leftNode = currentNode.rightNode;
            } else {
                parentNode.rightNode = currentNode.rightNode;
            }
        }
        return true;
    } else if (currentNode.rightNode == null) { // 刪除結點右結點爲空
        if (currentNode == rootNode) {
            rootNode = currentNode.leftNode;
        } else {
            if (isLeftNode) {
                parentNode.leftNode = currentNode.leftNode;
            } else {
                parentNode.rightNode = currentNode.leftNode;
            }
        }
        return true;
    } else { // 刪除結點含有左右結點
        TreeNode<Integer> successorNode = getSuccessorNode(currentNode);
        if (currentNode == rootNode) {
            rootNode = successorNode;
        } else {
            if (isLeftNode) {
                parentNode.leftNode = successorNode;
            } else {
                parentNode.rightNode = successorNode;
            }
        }
        successorNode.leftNode = currentNode.leftNode;
        return true;
    }
}

8 源碼地址

github地址

END

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