1. 程式人生 > >“樹”據結構一:二叉搜尋樹(Binary Search Tree, BST)

“樹”據結構一:二叉搜尋樹(Binary Search Tree, BST)

前言

想寫兩篇關於AVL樹和B樹的較為詳細的介紹,發現需要先介紹二叉搜尋樹作為先導。

定義

二叉搜尋樹(Binary Search Thee, BST),也被稱為二叉排序樹(Binary Sort Tree, BST),無論哪種定義,都能表明其特點:有序,能夠用於快速搜尋。個人更傾向於稱其為二叉搜尋樹。

二叉搜尋樹,指的是這樣的一顆二叉樹:一個節點的左子節點小於(小於等於,如果允許存在相等元素的話)它,右子節點大於它。同樣地道理適用於其左子樹和右子樹。

來源

之所以有二叉搜尋樹,是為了搜尋方便。對於n個節點,一般情況下僅需要

O(log2n)的事件就能確定是否存在目標值。當然最壞情況下,二叉樹會退化為連結串列(比如只有左子樹),因此,對一個二叉搜尋樹進行自平衡是很重要的一部分內容,也就是所謂的AVL樹,有時候也被稱為平衡二叉搜尋樹。詳見“樹”據結構二:AVL樹

演算法

(由於主要想寫的是AVL樹和B樹,二叉搜尋樹的演算法這裡不詳細介紹了,哪天有陽光了再好好寫寫。)

資料結構

    class Node<T extends Comparable<T>> {

        protected T id = null;
        protected Node<T> parent = null
; protected Node<T> lesser = null; protected Node<T> greater = null; /** * Node constructor. * * @param parent * Parent link in tree. parent can be NULL. * @param id * T representing the node in the tree. */
protected Node(Node<T> parent, T id) { this.parent = parent; this.id = id; } /** * {@inheritDoc} */ @Override public String toString() { return "id=" + id + " parent=" + ((parent != null) ? parent.id : "NULL") + " lesser=" + ((lesser != null) ? lesser.id : "NULL") + " greater=" + ((greater != null) ? greater.id : "NULL"); } }

二叉搜尋樹的節點比較簡單,最基礎的是記錄其節點的值、左子節點、右子節點。當然,在實現的時候往往還保留父節點,這會給一些處理帶來很大的便利。

    /**
     * Locate T in the tree.
     * 
     * @param value
     *            T to locate in the tree.
     * @return Node<T> representing first reference of value in tree or NULL if
     *         not found.
     */
    protected Node<T> getNode(T value) {
        Node<T> node = root;
        while (node != null && node.id != null) {
            if (value.compareTo(node.id) < 0) {
                node = node.lesser;
            } else if (value.compareTo(node.id) > 0) {
                node = node.greater;
            } else if (value.compareTo(node.id) == 0) {
                return node;
            }
        }
        return null;
    }

二叉搜尋樹最重要的就是查。可以採用遞迴式查詢和非遞迴式查詢(一般用佇列實現)。這裡使用的是非遞迴方式。

遍歷

二叉搜尋樹的便利有三種方式:
- 前序遍歷:per-order,即根在前,然後左,最後右;
- 中序遍歷:in-order,即左在前,根在中,最後有。之所以稱其為in-order(按序),是因為對於一個二叉搜尋樹來說,中序遍歷就是按照其節點值的大小順序遍歷;
- 後序遍歷:post-order,即左在前,然後右,最後根。

所以遍歷的命名方式其實就是看中間節點到底是“前”、“中”還是“後”被訪問。

    /**
     * Add value to the tree and return the Node that was added. Tree can
     * contain multiple equal values.
     * 
     * @param value
     *            T to add to the tree.
     * @return Node<T> which was added to the tree.
     */
    protected Node<T> addValue(T value) {
        Node<T> newNode = this.creator.createNewNode(null, value);

        // If root is null, assign
        if (root == null) {
            root = newNode;
            size++;
            return newNode;
        }

        Node<T> node = root;
        while (node != null) {
            if (newNode.id.compareTo(node.id) <= 0) {
                // Less than or equal to goes left
                if (node.lesser == null) {
                    // New left node
                    node.lesser = newNode;
                    newNode.parent = node;
                    size++;
                    return newNode;
                }
                node = node.lesser;
            } else {
                // Greater than goes right
                if (node.greater == null) {
                    // New right node
                    node.greater = newNode;
                    newNode.parent = node;
                    size++;
                    return newNode;
                }
                node = node.greater;
            }
        }

        return newNode;
    }

增加一個節點也比較簡單,關鍵是跟節點進行比較,找到應該新增的位置(找到null為止),然後歸位(調整一下幾個指標的指向)即可。

刪節點值得好好說一說。當刪除一個節點的時候,往往需要對樹結構進行調整,根據維基百科的介紹,刪節點主要分為以下幾種情況:
1. 刪除一個沒有孩子的節點:直接刪了就行了(孤家寡人,揮一揮衣袖,不帶走一片雲彩);
2. 刪除一個只有一個孩子的節點:刪了之後用孩子取代其位置就行了(有點兒像繼承家產);
3. 刪除一個有兩個孩子的節點:比較麻煩。

對於第三種情況,如果稱被刪除的節點為D,可以選擇其中序遍歷的前驅E(左子樹的最右節點),或者中序遍歷的後繼E(右子樹的最左節點)來取代其位置。然後讓E的孩子來取代E的位置(E一定只有一個孩子,要不然E就不能被稱之為子樹的最右或最左節點)。

刪除二叉搜尋樹的擁有兩個孩子的節點

同時要注意的是,不能一直只選擇前驅/後繼,這樣的話相當於是讓一棵二叉搜尋樹往連結串列的方向發展。所以可以採用輪流的方式。

刪除一個節點:首先找到其替代節點,然後刪除該節點,並用替代節點替代該節點。

    /**
     * Remove the node using a replacement
     * 
     * @param nodeToRemoved
     *            Node<T> to remove from the tree.
     * @return nodeRemove
     *            Node<T> removed from the tree, it can be different
     *            then the parameter in some cases.
     */
    protected Node<T> removeNode(Node<T> nodeToRemoved) {
        if (nodeToRemoved != null) {
            Node<T> replacementNode = this.getReplacementNode(nodeToRemoved);
            replaceNodeWithNode(nodeToRemoved, replacementNode);
        }
        return nodeToRemoved;
    }

尋找替代節點:

    /**
     * Get the proper replacement node according to the binary search tree
     * algorithm from the tree.
     * 
     * @param nodeToRemoved
     *            Node<T> to find a replacement for.
     * @return Node<T> which can be used to replace nodeToRemoved. nodeToRemoved
     *         should NOT be NULL.
     */
    protected Node<T> getReplacementNode(Node<T> nodeToRemoved) {
        Node<T> replacement = null;

        // I. the node has two children
        if (nodeToRemoved.greater != null && nodeToRemoved.lesser != null) {
            // Two children.
            // Add some randomness to deletions, so we don't always use the
            // greatest/least on deletion

            // always choose the successor or predecessor will lead to an unbalanced tree
            if (modifications % 2 != 0) {
                replacement = this.getGreatest(nodeToRemoved.lesser);
                if (replacement == null)
                    replacement = nodeToRemoved.lesser;
            } else {
                replacement = this.getLeast(nodeToRemoved.greater);
                if (replacement == null)
                    replacement = nodeToRemoved.greater;
            }
            modifications++;

            // II. the node has only one child
        } else if (nodeToRemoved.lesser != null && nodeToRemoved.greater == null) {
            // Using the less subtree
            replacement = nodeToRemoved.lesser;
        } else if (nodeToRemoved.greater != null && nodeToRemoved.lesser == null) {
            // Using the greater subtree (there is no lesser subtree, no refactoring)
            replacement = nodeToRemoved.greater;
        }
        // III. the node has no children

        return replacement;
    }

尋找前驅:

    /**
     * Get greatest node in sub-tree rooted at startingNode. The search does not
     * include startingNode in it's results.
     * 
     * @param startingNode
     *            Root of tree to search.
     * @return Node<T> which represents the greatest node in the startingNode
     *         sub-tree or NULL if startingNode has no greater children.
     */
    protected Node<T> getGreatest(Node<T> startingNode) {
        if (startingNode == null)
            return null;

        Node<T> greater = startingNode.greater;
        while (greater != null && greater.id != null) {
            Node<T> node = greater.greater;
            if (node != null && node.id != null)
                greater = node;
            else
                break;
        }
        return greater;
    }

尋找後繼:

    /**
     * Get least node in sub-tree rooted at startingNode. The search does not
     * include startingNode in it's results.
     * 
     * @param startingNode
     *            Root of tree to search.
     * @return Node<T> which represents the least node in the startingNode
     *         sub-tree or NULL if startingNode has no lesser children.
     */
    protected Node<T> getLeast(Node<T> startingNode) {
        if (startingNode == null)
            return null;

        Node<T> lesser = startingNode.lesser;
        while (lesser != null && lesser.id != null) {
            Node<T> node = lesser.lesser;
            if (node != null && node.id != null)
                lesser = node;
            else
                break;
        }
        return lesser;
    }

刪除節點,並用替代節點取代其位置:

    /**
     * Replace a with b in the tree.
     * 
     * @param a
     *            Node<T> to remove replace in the tree. a should
     *            NOT be NULL.
     * @param b
     *            Node<T> to replace a in the tree. b
     *            can be NULL.
     */
    protected void replaceNodeWithNode(Node<T> a, Node<T> b) {
        if (b != null) {
            // Save for later
            Node<T> bLesser = b.lesser;
            Node<T> bGreater = b.greater;

            // I.
            // b regards a's children as his children
            // a's children regard b as their parent
            // (but a still regards his children as his children)

            // Replace b's branches with a's branches
            Node<T> aLesser = a.lesser;
            if (aLesser != null && aLesser != b) {
                b.lesser = aLesser;
                aLesser.parent = b;
            }
            Node<T> aGreater = a.greater;
            if (aGreater != null && aGreater != b) {
                b.greater = aGreater;
                aGreater.parent = b;
            }

            // II.
            // b's children and b's parent know about each other
            // (and b has no longer relation with them )

            // Remove link from b's parent to b
            Node<T> bParent = b.parent;
            if (bParent != null && bParent != a) {
                Node<T> bParentLesser = bParent.lesser;
                Node<T> bParentGreater = bParent.greater;
                // b is left child, then it at most has a right child(or its left child'll be the replacementNode)
                if (bParentLesser != null && bParentLesser == b) {
                    bParent.lesser = bGreater;
                    if (bGreater != null)
                        bGreater.parent = bParent;
                // b is right child, then it at most has a left child
                } else if (bParentGreater != null && bParentGreater == b) {
                    bParent.greater = bLesser;
                    if (bLesser != null)
                        bLesser.parent = bParent;
                }
            }
        }

        // III.
        // b regards a's parent as his parent
        // a's parent regards b as his child(but the parent should know about b is it's lesser or greater child)
        // (but a still regards his parent as his parent)

        // Update the link in the tree from a to b
        Node<T> parent = a.parent;
        if (parent == null) {
            // Replacing the root node
            root = b;
            if (root != null)
                root.parent = null;
        } else if (parent.lesser != null && (parent.lesser.id.compareTo(a.id) == 0)) {
            parent.lesser = b;
            if (b != null)
                b.parent = parent;
        } else if (parent.greater != null && (parent.greater.id.compareTo(a.id) == 0)) {
            parent.greater = b;
            if (b != null)
                b.parent = parent;
        }
        size--;

        // FINALLY.
        // node a should be released
        a = null;
    }

總結

以上就是二叉搜尋樹的大致用法,但是實際情況中,二叉搜尋樹用的並非很廣泛,因為很難保證資料的來源是絕對無序的。所以構造出來的樹自然就是非平衡的。因此AVL的使用才是大勢所趨。

參閱

相關推薦

結構搜尋Binary Search Tree, BST

前言 定義 來源 演算法 資料結構 查 遍歷 增 刪 總結 參閱 前言 想寫兩篇關於AVL樹和B樹的較為詳細的介紹,發現需要先介紹二叉搜尋樹作為先導。 定義 二叉搜尋樹(Binary Search Thee, BST),也被稱為二

結構35前序遍歷、中序遍歷和後序遍歷

tdi 代碼 nod 完成 循環 同時 reat pan 設置 遞歸算法底層的實現使用的是棧存儲結構,所以可以直接使用棧寫出相應的非遞歸算法。 先序遍歷的非遞歸算法 從樹的根結點出發,遍歷左孩子的同時,先將每個結點的右孩子壓棧。當遇到結點沒有左孩子的時候,取棧頂的右

資料結構實驗之查詢排序 SDUT 3373

二叉排序樹(Binary Sort Tree),又稱二叉查詢樹(Binary Search Tree),也稱二叉搜尋樹。 #include <stdio.h> #include <string.h> #include <stdlib.h> struct nod

SDUT3374資料結構實驗之查詢排序

判斷是否為同一棵二叉排序樹 解決這個問題需要兩步: 1.建立二叉排序樹 2.判斷兩棵樹是否相同 詳情看程式碼和註釋,懶人程式碼 #include <iostream> #include <cstring> using namespace std; type

演算法題三十搜尋BST的後序遍歷序列

題目描述 輸入一個整數陣列,判斷該陣列是不是某二叉搜尋樹的後序遍歷的結果。如果是則輸出Yes,否則輸出No。假設輸入的陣列的任意兩個數字都互不相同。 分析 後序遍歷序列中,最右邊數字也就是根結點,會把數集分為左右兩部分,左邊數集都小於root,右邊數集都大於root。左

資料結構搜尋

實現的操作 1.0 插入:找待插入的節點的位置(data<_root->_data左子樹,data>_root->_data 右子樹)data為待插入節點的值。 2.0 尋找:找節點的位置(data<_root->_data左子樹,

資料結構——4.1 搜尋

一、二叉搜尋樹 一棵二叉樹,可以為空;如果不為空,滿足以下性質: 1)非空左子樹的所有鍵值小於其根結點的鍵值 2)非空右子樹的所有鍵值大於其根結點的鍵值 3)左右子樹都是二叉搜尋樹 二、二叉搜尋樹操作的特別函式 1、查詢 1)Find ① 查詢從根結點開始,如果樹

資料結構作業排序及其相關操作

寫了一個簡單的。 因為自己對泛型瞭解的還是不夠到位,所以只能寫個demo版的。 這課樹沒辦法維持平衡,希望以後學一下紅黑樹,替罪羊樹etc.   /* * 簡單的二叉查詢樹 * 沒有自帶旋轉平衡 * 寫完這個我學一下 * avl樹還有紅黑樹 */ public c

Leetcode 938搜尋的範圍和最詳細的解法!!!

給定二叉搜尋樹的根結點 root,返回 L 和 R(含)之間的所有結點的值的和。 二叉搜尋樹保證具有唯一的值。 示例 1: 輸入:root = [10,5,15,3,7,null,18], L = 7, R = 15 輸出:32 示例 2: 輸入:root = [1

連結串列六搜尋與雙向連結串列

/** * 題目:二叉搜尋樹與雙向連結串列 * 描述:輸入一棵二叉搜尋樹,將該二叉搜尋樹轉換成一個排序的雙向連結串列。要求不能建立任何新的結點,只能調整樹中結點指標的指向 * 方案:在中序遍歷中新增前驅結點 * */ public class Six {

搜尋的後序遍歷序列

/** * 題目:二叉搜尋樹的後序遍歷序列 * 描述:輸入一個整數陣列,判斷該陣列是不是某二叉搜尋樹的後序遍歷的結果。如果是則輸出Yes,否則輸出No。假設輸入的陣列的任意兩個數字都互不相同 *   二叉搜尋樹,又叫二叉排序樹,它或者是一顆空樹,或者具有以下性

演算法題十九搜尋轉雙鏈表

題目描述 輸入一棵二叉搜尋樹,將該二叉搜尋樹轉換成一個排序的雙向連結串列。要求不能建立任何新的結點,只能調整樹中結點指標的指向。 輸入輸出示例: {10,6,4,8,14,12,16} from left to right:4,6,8,10,12,14,16 from right

PTA 資料結構——是否完全搜尋

7-2 是否完全二叉搜尋樹 (30 分) 將一系列給定數字順序插入一個初始為空的二叉搜尋樹(定義為左子樹鍵值大,右子樹鍵值小),你需要判斷最後的樹是否一棵完全二叉樹,並且給出其層序遍歷的結果。 輸入格式: 輸入第一行給出一個不超過20的正整數N;第

輸入搜尋,將該搜尋轉換成一個排序的雙向連結串列劍指offer

題目 輸入一棵二叉搜尋樹,將該二叉搜尋樹轉換成一個排序的雙向連結串列。要求不能建立任何新的結點,只能調整樹中結點指標的指向。 分析: 在二叉搜尋樹中,每個結點都有兩個分別指向其左、右子樹的指標,左子樹結點的值總是小於父結點的值,右子樹結點的值總是大於父結點的值。在雙向連結串列中,每個結點

面試題54搜尋的第k個結點

題意:給定一棵二叉搜尋樹,請找出其中的第k小的結點。例如, (5,3,7,2,4,6,8)    中,按結點數值大小順序第三小結點的值為4。 思路:中序遍歷就是從小到大排序,直接中序遍歷一下就好了。

劍指offer搜尋的後序遍歷序列

# -*- coding:utf-8 -*- class Solution: def VerifySquenceOfBST(self, sequence): # write code here ''' if sequen

劍指offer{面試題24搜尋的後序遍歷序列}

這個題似曾相識,之前劍指offer有一道題是判斷該子樹是否是樹的一部分,有異曲同工之妙,看到這種題,上倆就應該想遞迴。 public class Solution { public boolean VerifySquenceOfBST(int [] sequence) {

資料結構與演算法 -- 搜尋java實現

package com.huang.test.datastructure; import java.util.*; /** * 二叉搜尋樹 */ abstract class BstData<T> { BstData<T> left;

劍指 Offer - 26搜尋與雙向連結串列

題目描述 輸入一棵二叉搜尋樹,將該二叉搜尋樹轉換成一個排序的雙向連結串列。要求不能建立任何新的結點,只能調整樹中結點指標的指向 題目連結:https://www.nowcoder.com/practice/947f6eb80d944a84850b0538bf0ec3a5

劍指 Offer - 23搜尋的後序遍歷序列

題目描述 輸入一個整數陣列,判斷該陣列是不是某二叉搜尋樹的後序遍歷的結果。如果是則輸出Yes,否則輸出No。假設輸入的陣列的任意兩個數字都互不相同 題目連結:https://www.nowcoder.com/practice/a861533d45854474ac791d