1. 程式人生 > >重溫資料結構:二叉排序樹的查詢、插入、刪除

重溫資料結構:二叉排序樹的查詢、插入、刪除

讀完本文你將瞭解到:

我們知道,二分查詢可以縮短查詢的時間,但是有個要求就是 查詢的資料必須是有序的。每次查詢、操作時都要維護一個有序的資料集,於是有了二叉排序樹這個概念。

上篇文章 我們介紹了 二叉樹 的概念,二叉樹有左右子樹之分,想必在區分左右子樹時有一定的規則。

現在我們來介紹二叉樹的一種特殊形式 — 二叉排序樹,瞭解它的區分策略及常用操作。

什麼是二叉排序樹 Binary Sort Tree, BST

二叉排序樹,又稱二叉查詢樹、二叉搜尋樹。

二叉排序樹是具有下列性質的二叉樹:

  • 若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
  • 若右子樹不空,則右子樹上所有結點的值均大於或等於它的根結點的值;
  • 左、右子樹也分別為二叉排序樹。

這裡寫圖片描述

也就是說,二叉排序樹中,左子樹都比節點小,右子樹都比節點大,遞迴定義。

根據二叉排序樹這個特點我們可以知道,二叉排序樹的中序遍歷一定是從小到大的,比如上圖,中序遍歷結果是:

1 3 4 6 7 8 10 13 14

二叉排序樹的關鍵操作

1.查詢

根據二叉排序樹的定義,我們可以知道在查詢某個元素時:

  • 先比較它與根節點,相等就返回;或者根節點為空,說明樹為空,也返回;
  • 如果它比根節點小,就從根的左子樹裡進行遞迴查詢;
  • 如果它比根節點大,就從根的右子樹裡進行遞迴查詢。

可以看到,這就是一個 二分查詢

程式碼實現:

public class BinarySearchTree {
    private BinaryTreeNode mRoot;   //根節點

    public BinarySearchTree(BinaryTreeNode root) {
        mRoot = root;
    }

    /**
     * 在整個樹中查詢某個資料
     *
     * @param data
     * @return
     */
    public BinaryTreeNode search(int data) {
        return search(mRoot, data);
    }

    /**
     * 在指定二叉排序樹中查詢資料
     *
     * @param node
     * @param data
     * @return
     */
    public BinaryTreeNode search(BinaryTreeNode node, int data) {
        if (node == null || node.getData() == data) {    //節點為空或者相等,直接返回該節點
            return node;
        }
        if (data < node.getData()) {    //比節點小,就從左子樹裡遞迴查詢
            return search(node.getLeftChild(), data);
        } else {        //否則從右子樹
            return search(node.getRightChild(), data);
        }
    }
}

可以看到,在二叉排序樹中查詢是十分簡單的,但是這依賴於每次插入、刪除元素時對整個 排序樹 結構的維護。

2.插入

二叉樹中的插入,主要分兩步:查詢、插入:

  • 先查詢有沒有整個元素,有的話就不用插入了,直接返回;
  • 沒有就插入到之前查到(對比)好的合適的位置。

插入時除了設定資料,還需要跟父節點繫結,讓父節點意識到有你這個孩子:比父節點小的就是左孩子,大的就是右孩子。

程式碼實現:

/**
 * 插入到整個樹中
 *
 * @param data
 */
public void insert(int data) {
    if (mRoot == null) {     //如果當前是空樹,新建一個
        mRoot = new BinaryTreeNode();
        mRoot.setData(data);
        return;
    }

    searchAndInsert(null, mRoot, data);     //根節點的父親為 null

}

/**
 * 兩步走:查詢、插入
 *
 * @param parent 要繫結的父節點
 * @param node   當前比較節點
 * @param data   資料
 */
private BinaryTreeNode searchAndInsert(BinaryTreeNode parent, BinaryTreeNode node, int data) {
    if (node == null) {  //當前比較節點為 空,說明之前沒有這個資料,直接新建、插入
        node = new BinaryTreeNode();
        node.setData(data);
        if (parent != null) {    //父節點不為空,繫結關係
            if (data < parent.getData()) {
                parent.setLeftChild(node);
            } else {
                parent.setRightChild(node);
            }
        }
        return node;
    }
    //對比的節點不為空
    if (node.getData() == data) {    //已經有了,不用插入了
        return node;
    } else if (data < node.getData()) {   //比節點小,從左子樹裡查詢、插入
        return searchAndInsert(node, node.getLeftChild(), data);
    } else {
        return searchAndInsert(node, node.getRightChild(), data);
    }
}

3.刪除 *

插入操作和查詢比較類似,而刪除則相對複雜一點,需要根據刪除節點的情況分類來對待:

  • 如果要刪除的節點正好是葉子節點,直接刪除就 Ok 了;
  • 如果要刪除的節點還有子節點,就需要建立父節點和子節點的關係:
    • 如果只有左孩子或者右孩子,直接把這個孩子上移放到要刪除的位置就好了;
    • 如果有兩個孩子,就需要選一個合適的孩子節點作為新的根節點,該節點稱為 繼承節點

新節點要求要比所有左子樹大,比所有右子樹小,怎麼選擇呢?

**要比所有左子樹的值大、右子樹小,就從右子樹裡找最小的好了;
同樣也可以從左子樹裡找最大的。**

兩種選擇方法都可以,本文選用右子樹裡最小的節點,也就是右子樹中最左邊的節點。

程式碼實現:

/**
 * 在整個樹中 查詢指定資料節點的父親節點
 *
 * @param data
 * @return
 */
public BinaryTreeNode searchParent(int data) {
    return searchParent(null, mRoot, data);
}

/**
 * 在指定節點下 查詢指定資料節點的父親節點
 *
 * @param parent 當前比較節點的父節點
 * @param node   當前比較的節點
 * @param data   查詢的資料
 * @return
 */
public BinaryTreeNode searchParent(BinaryTreeNode parent, BinaryTreeNode node, int data) {
    if (node == null) { //比較的節點為空返回空
        return null;
    }
    if (node.getData() == data) {    //找到了目標節點,返回父節點
        return parent;
    } else if (data < node.getData()) {   //資料比當前節點小,左子樹中遞迴查詢
        return searchParent(node, node.getLeftChild(), data);
    } else {
        return searchParent(node, node.getRightChild(), data);
    }
}

/**
 * 刪除指定資料的節點
 *
 * @param data
 */
public void delete(int data) {
    if (mRoot == null || mRoot.getData() == data) {  //根節點為空或者要刪除的就是根節點,直接刪掉
        mRoot = null;
        return;
    }
    //在刪除之前需要找到它的父親
    BinaryTreeNode parent = searchParent(data);
    if (parent == null) {        //如果父節點為空,說明這個樹是空樹,沒法刪
        return;
    }

    //接下來該找要刪除的節點了
    BinaryTreeNode deleteNode = search(parent, data);
    if (deleteNode == null) {    //樹中找不到要刪除的節點
        return;
    }
    //刪除節點有 4 種情況
    //1.左右子樹都為空,說明是葉子節點,直接刪除
    if (deleteNode.getLeftChild() == null && deleteNode.getRightChild() == null) {
        //刪除節點
        deleteNode = null;
        //重置父節點的孩子狀態,告訴他你以後沒有這個兒子了
        if (parent.getLeftChild() != null && parent.getLeftChild().getData() == data) {
            parent.setLeftChild(null);
        } else {
            parent.setRightChild(null);
        }
        return;
    } else if (deleteNode.getLeftChild() != null && deleteNode.getRightChild() == null) {
        //2.要刪除的節點只有左子樹,左子樹要繼承位置
        if (parent.getLeftChild() != null && parent.getLeftChild().getData() == data) {
            parent.setLeftChild(deleteNode.getLeftChild());
        } else {
            parent.setRightChild(deleteNode.getLeftChild());
        }
        deleteNode = null;
        return;
    } else if (deleteNode.getRightChild() != null && deleteNode.getRightChild() == null) {
        //3.要刪除的節點只有右子樹,右子樹要繼承位置
        if (parent.getLeftChild() != null && parent.getLeftChild().getData() == data) {
            parent.setLeftChild(deleteNode.getRightChild());
        } else {
            parent.setRightChild(deleteNode.getRightChild());
        }

        deleteNode = null;
    } else {
        //4.要刪除的節點兒女雙全,既有左子樹又有右子樹,需要選一個合適的節點繼承,這裡使用右子樹中最左節點
        BinaryTreeNode copyOfDeleteNode = deleteNode;   //要刪除節點的副本,指向繼承節點的父節點
        BinaryTreeNode heresNode = deleteNode.getRightChild(); //要繼承位置的節點,初始為要刪除節點的右子樹的樹根
        //右子樹沒有左孩子了,他就是最小的,直接上位
        if (heresNode.getLeftChild() == null) {
            //上位後,兄弟變成了孩子
            heresNode.setLeftChild(deleteNode.getLeftChild());
        } else {
            //右子樹有左孩子,迴圈找到最左的,即最小的
            while (heresNode.getLeftChild() != null) {
                copyOfDeleteNode = heresNode;       //copyOfDeleteNode 指向繼承節點的父節點
                heresNode = heresNode.getLeftChild();
            }
            //找到了繼承節點,繼承節點的右子樹(如果有的話)要上移一位
            copyOfDeleteNode.setLeftChild(heresNode.getRightChild());
            //繼承節點先繼承家業,把自己的左右孩子變成要刪除節點的孩子
            heresNode.setLeftChild(deleteNode.getLeftChild());
            heresNode.setRightChild(deleteNode.getRightChild());
        }
        //最後就是確認位置,讓要刪除節點的父節點認識新兒子
        if (parent.getLeftChild() != null && parent.getLeftChild().getData() == data) {
            parent.setLeftChild(heresNode);
        } else {
            parent.setRightChild(heresNode);
        }
    }
}

執行程式碼測試

可以看到,二叉排序樹的查詢、新增較簡單,刪除邏輯比較多,我們以下圖為例:

這裡寫圖片描述

測試程式碼:

@Test
public void delete() throws Exception {
    //亂序插入到二叉排序樹中
    BinarySearchTree binarySearchTree = new BinarySearchTree(null);
    binarySearchTree.insert(8);
    binarySearchTree.insert(3);
    binarySearchTree.insert(1);
    binarySearchTree.insert(6);
    binarySearchTree.insert(4);
    binarySearchTree.insert(7);
    binarySearchTree.insert(10);
    binarySearchTree.insert(13);
    binarySearchTree.insert(14);

    //中序遍歷
    binarySearchTree.iterateMediumOrder(binarySearchTree.getRoot());
    System.out.println("");
    //查詢某個資料
    System.out.println(binarySearchTree.search(10).getData());
    //刪除某個資料對應的元素
    binarySearchTree.delete(6);
    //中序遍歷刪除後的二叉排序樹
    binarySearchTree.iterateMediumOrder(binarySearchTree.getRoot());
}

執行結果:

shixinzhang

一道面試題

輸入一棵二元查詢樹,將該二元查詢樹轉換成一個排序的雙向連結串列。要求不能建立任何新的結點,只調整指標的指向。 比如將二元查詢樹:

                                        10
                                      /    \
                                    6       14
                                  /  \     /  \
                               4     8  12    16

轉換成雙向連結串列後為:4=6=8=10=12=14=16

解析:
這題據說是微軟的面試題,乍看起來貌似很麻煩,又是二叉排序樹又是雙向連結串列的,其實考察的都是很基礎的東西,明眼人一看就發現只要將這棵樹中序遍歷後就是將二叉樹節點排序(不然它為啥叫二叉排序樹呢…),那麼我們只要將這棵樹中序遍歷,遍歷到一個節點就將該節點的左指標指向上一個遍歷的節點,並將上一個遍歷的節點的右指標指向現在正在遍歷的節點,那麼當我們遍歷完整棵樹後,我們的雙向連結串列也改好啦!這樣既不用新增多餘節點,也不用新增多餘的指標變數。

你可以寫下程式碼試試。

總結

  二叉排序樹的效能取決於二叉樹的層數:

  • 最好的情況是 O(logn),存在於完全二叉排序樹情況下,其訪問效能近似於折半查詢;
  • 最差時候會是 O(n),比如插入的元素是有序的,生成的二叉排序樹就是一個連結串列,這種情況下,需要遍歷全部元素才行(見下圖 b)。

shixinzhang

Thanks

《輕鬆學演算法》