二分搜索树

一、概述

二分搜索树(Binary Search Tree)是一种能够将链表插入的灵活性和有序数组查找的高效性结合起来的符号表实现。具体来说,就是使用每个结点含有两个链接(链表中每个结点只含有一个链接)的二叉查找树来高效地实现符号表,这也是计算机科学中最重要的算法之一。
顾名思义,其主要目的用于搜索,它是二叉树结构中最基本的一种数据结构,是后续理解B树、B+树、红黑树的基础,后三者在具体的工程实践中更常用,比如C++中STL就是利用红黑树做Map,B树用于磁盘上的数据库维护等,后三者均是在二叉搜索树的基础上演变而来的,理解二分搜索树是学习后者的基础。


与基础的数据结构如链表、堆、栈等基本结构一样,学习二叉搜索树的关键是深入理解访问与操作二叉树的算法及性能分析,本文如下部分首先介绍二叉搜索树的特征;然后重点介绍二叉搜索树的遍历、查找(包括最值查找、前驱后继查找)、以及插入和删除等操作,最后简单进行分析。

二、基本原理及性质

如果你不想看下面这些话,只想一句话明白搜索二叉树怎么实现:(key比自身小放左面,key比自身大放右边)。

每个结点的键值大于左孩子,小于右孩子。以左右孩子为根的子树认为二分搜索树(天然包括递归结构)
由于不一定是完全二叉树,用数组表示不方便,Node结点(指针)表示,

template <typename Key, typename Value>
class BST{
private:
    struct Node {
        Key key;    
        Value value;
        Node *left;     // 左孩子(指向Node的指针)
        Node *right;    // 右孩子(指向Node的指针)

        Node(Key key, Value value) {    // Node结构的构造函数
            this->key = key;
            this->value = value;
            this->left = this->right = NULL;
        }

        Node(Node *node){
            this->key = node->key;
            this->value = node->value;
            this->left = node->left;
            this->right = node->right;
        }
    };
    Node *root; // 根结点
    int count;  // 二分搜索树存的总结点数

public:
    BST(){  // 二分搜索树的构造函数
        root = NULL;
        count = 0;
    }
    ~BST(){ // 二分搜索树的析构函数
        destroy( root );
    }
    int size(){
        return count;
    }
    bool isEmpty(){
        return count == 0;
    }
}

1. 添加元素

三种不同情况:

  1. 60与根结点41比较,60大所以放到41右边
  2. 然后要插入到以58为根的二分搜索树中,60与58比较,60大所以放到58右边
  1. 28与根结点41比较,28小所以放到41左边
  2. 然后要插入到以22为根的二分搜索树中,28与22比较,28大所以放到22右边
  3. 然后要插入到以33为根的二分搜索树中,28与33比较,28小所以放到33左边
  1. 42与根结点41比较,42大所以放到41右边
  2. 然后要插入到以58为根的二分搜索树中,42与58比较,42小所以放到58左边
  3. 然后要插入到以50为根的二分搜索树中,42与50比较,42小所以放到50左边
  4. 由于原来50的左孩子也是42,两个值一样大,所以用新的42代替原来的42
public:
void insert(Key key, Value value) {
     root = insert(root, key, value);
}

private:
// 向以node为根的二叉搜索树中,插入结点(key, value)
// 返回插入新结点后的二叉搜索树的根
Node* insert(Node *node, Key key, Value value) {
    if( node == NULL ){      // 如果结点为空,就在此结点处加入x信息(边界条件)
        count ++;
        return new Node(key, value);
    }

    if( key == node->key )  
        node->value = value;  //如果相等,就把频率加1
    else if( key < node->key )
        node->left = insert( node->left , key, value);  // 如果x小于结点的值,就继续在结点的左子树中插入x
    else
        node->right = insert( node->right, key, value);  // 如果x大于结点的值,就继续在结点的右子树中插入x

    return node;    // 返回结点本身
}

2. contain包含

bool contain(Key key) {
    return contain(root, key);
}

// 查看以node为根的二叉搜索树中是否包含键值为key的结点
bool contain(Node* node, Key key) {
    if( node == NULL )
        return false;

    if( key == node->key )
        return true;
    else if( key < node->key )
        return contain( node->left , key );
    else // key > node->key
        return contain( node->right , key );
}

3. search查找

与contain一样,只是返回值不同

Value* search(Key key){
    return search( root , key );
}

// 在以node为根的二叉搜索树中查找key所对应的value
Value* search(Node* node, Key key){
    if( node == NULL )
        return NULL;

    if( key == node->key )
        return &(node->value);  // 返回value值对应的地址
    else if( key < node->key )
        return search( node->left , key );
    else
        return search( node->right, key );
}

4. 遍历(深度优先遍历)

前序遍历 中序遍历 后序遍历(相对于根结点)

前序遍历:先遍历根结点,然后遍历左子树,最后遍历右子树。(先访问当前结点,再依次递归访问左右子树)(遍历到左就打印)(遍历整个树,对元素做某些事情,最常用)



中序遍历:先遍历左子树,然后遍历根结点,最后遍历右子树。(先递归访问左子树,再访问自身,再递归访问右子树)(遍历到中间就打印)(元素从小到大排序,应用:排序)



后序遍历:先遍历左子树,然后遍历右子树,最后遍历根结点。(先递归访问左右子树,再访问自身结点)(遍历到右才打印)(先删除左右子结点,再删除自身,应用:释放整个二叉树)


void preOrder(){
    preOrder(root);
}

// 对以node为根的二叉搜索树进行前序遍历
void preOrder(Node* node){
    if( node != NULL ){
        cout<<node->key<<endl;
        preOrder(node->left);
        preOrder(node->right);
    }
}
void inOrder(){
    inOrder(root);
}

// 对以node为根的二叉搜索树进行中序遍历
void inOrder(Node* node){
    if( node != NULL ){
        inOrder(node->left);
        cout<<node->key<<endl;
        inOrder(node->right);
    }
}
void postOrder(){
    postOrder(root);
}

// 对以node为根的二叉搜索树进行后序遍历
void postOrder(Node* node){
    if( node != NULL ){
        postOrder(node->left);
        postOrder(node->right);
        cout<<node->key<<endl;
    }
}

销毁整个二叉树destroy(后序遍历)

void destroy(Node* node){
    if( node != NULL ){
        destroy(node->left);
        destroy(node->right);
        delete node;
        count--;
    }
}

5. 层序遍历(广度优先遍历)

引入队列(先进先出,后进后出)
只要队列不为空,就将元素拿出来遍历或操作
然后拿到它的左右孩子入队16 30
再将16拿出来遍历或操作,并将16的左右孩子13 22入队
然后30拿出来遍历或操作,并将30的左右孩子29 42入队

void levelOrder(){
    queue<Node*> q;
    q.push(root);
    while( !q.empty() ){    // 只要队列不为空,就拿出队首元素出队进行操作
        Node *node = q.front();
        q.pop();    // 队首元素出队

        cout<<node->key<<endl;  // 对队首元素某些操作

        if( node->left )            // 将它的左右孩子入队
            q.push( node->left );
        if( node->right )
            q.push( node->right );
    }
}

6. 最大值/最小值

通过二分搜索树的性质知道,结点的左孩子比它小,右孩子比它大。
寻找最小值就是一直沿着它的左孩子方向找,直到一个结点没有左孩子,说明没有元素比它还小了,那么该结点就是整个二分搜索树的最小值
寻找最大值就是一直沿着它的右孩子方向找,直到一个结点没有右孩子,说明没有元素比它还大了,那么该结点就是整个二分搜索树的最大值

Key minimum() {
    assert( count != 0 );
    Node* minNode = minimum( root );
    return minNode->key;
}

// 在以node为根的二叉搜索树中,返回最小键值的结点
Node* minimum(Node* node) {
    if( node->left == NULL )    // 边界条件。如果左孩子为空,说明它自己就是最小值了
        return node;

    return minimum(node->left);
}
Key maximum() {
    assert( count != 0 );
    Node* maxNode = maximum(root);
    return maxNode->key;
}

// 在以node为根的二叉搜索树中,返回最大键值的结点
Node* maximum(Node* node) {
    if( node->right == NULL )   // 边界条件。如果右孩子为空,说明它自己就是最大值了
        return node;

    return maximum(node->right);
}

7. 删除最小/大值所在结点

直接把该最小值结点的右孩子代替自己的位置


void removeMin() {
    if( root )
        root = removeMin( root );
}

// 删除掉以node为根的二分搜索树中的最小结点
// 返回删除结点后新的二分搜索树的根
Node* removeMin(Node* node) {
    if( node->left == NULL ) {  // 看有没有左孩子,为空则它自己就是最小值
        Node* rightNode = node->right;  // 右孩子代替父结点的位置
        delete node;
        count --;
        return rightNode;
    }

    node->left = removeMin(node->left); // 继续查找最小值
    return node;
}
void removeMax() {
    if( root )
        root = removeMax( root );
}

// 删除掉以node为根的二分搜索树中的最大结点
// 返回删除结点后新的二分搜索树的根
Node* removeMax(Node* node) {   
    if( node->right == NULL ) { // 看有没有右孩子,为空则它自己就是最大值
        Node* leftNode = node->left;    // 右孩子代替父结点的位置
        delete node;
        count --;
        return leftNode;
    }

    node->right = removeMax(node->right);   // 继续查找最大值
    return node;
}

8. 删除任意结点

如果要删除的结点只有一个孩子,很简单,只要用它的孩子代替它的位置即可

如果要删除的结点左右孩子都有,有一个算法:Hibbard Deletion算法解决
从孩子中找一个结点代替要删除的结点d,然而既不应该是左孩子,也不应该是右孩子,应该是右子树中的最小值结点s:
s->right = delMin(d->right)将右子树中的最小值结点s删除掉,然后该右子树成为s结点的右子树(将s指向原来结点58的右子树),s结点的左孩子就是原来d结点的左孩子50
最后彻底将结点d删除掉:s->left = d->left

void remove(Key key) {
    root = remove(root, key);
}

// 删除掉以node为根的二分搜索树中键值为key的结点
// 返回删除结点后新的二分搜索树的根
Node* remove(Node* node, Key key) {
    if( node == NULL )  // 没有改结点
        return NULL;

    if( key < node->key ) { // 
        node->left = remove( node->left , key );
        return node;
    }
    else if( key > node->key ) {    // 在node右结点中寻找并删除
        node->right = remove( node->right, key );
        return node;
    }
    else{   // key == node->key就删除该结点

        if( node->left == NULL ) {  // 左孩子为空,只有右孩子
            Node *rightNode = node->right;  // 将右孩子node->right保存下来变成rightNode
            delete node;
            count --;
            return rightNode;   // 返回node的右结点rightNode
        }

        if( node->right == NULL ) { // 右孩子为空,只有左孩子
            Node *leftNode = node->left;    // 将左孩子node->left保存下来变成leftNode
            delete node;
            count--;
            return leftNode;    // 返回node的左结点leftNode
        }

        // node->left != NULL && node->right != NULL 左右孩子都不为空
        Node *successor = new Node(minimum(node->right));   // 找到右子树中的最小值作为要删除结点的后继.new Node()调用了构造函数,使之重新构建了一个node结点
        count ++;

        successor->right = removeMin(node->right);  // 后继的右孩子就是删除掉最小值之后返回的指针
        successor->left = node->left;   // 后继的左孩子就是原来要删除结点的左孩子

        delete node;
        count --;

        return successor;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容

  • 目录 0.树0.1 一般树的定义0.2 二叉树的定义 1.查找树ADT 2.查找树的实现2.1 二叉查找树2.2 ...
    王侦阅读 7,088评论 0 3
  • B树的定义 一棵m阶的B树满足下列条件: 树中每个结点至多有m个孩子。 除根结点和叶子结点外,其它每个结点至少有m...
    文档随手记阅读 13,129评论 0 25
  • 第一章 绪论 什么是数据结构? 数据结构的定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。 第二章...
    SeanCheney阅读 5,720评论 0 19
  • 1.树(Tree): 树是 n(n>=0) 个结点的有限集。当 n=0 时称为空树。在任意一颗非空树中:有且仅有一...
    ql2012jz阅读 970评论 0 3
  • 数据结构和算法--二叉树的实现 几种二叉树 1、二叉树 和普通的树相比,二叉树有如下特点: 每个结点最多只有两棵子...
    sunhaiyu阅读 6,413评论 0 14