高级数据结构与算法 — 二叉搜索树

二叉搜索树概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  3. 它的左右子树也分别为二叉搜索树
  4. 键值是唯一的,所以二叉搜索树不能有相同的键值

例如:以下数据构建二叉树

int a [] = {5,3,4,1,7,8,2,6,0,9};

在这里插入图片描述

二叉搜索树的实现

1.树的结点与树

树的结点,有3个属性,数据,左孩子,右孩子,最初左孩子与右孩子都是空;树,只有一个根,最初构造为空树。

代码如下:

template<class T>
struct BTreeNode
{
    typedef BTreeNode<T> Node;

    //三个属性值
    Node* _left;//左子树
    Node* _right;//右子树
    T _val;//数据

    BTreeNode(const T& val = T())
       :_left(nullptr)
       ,_right(nullptr)
       ,_val(val)
    {}
};

template<class T>
class BTree
{
public:
    typedef  BTreeNode<T> Node;

    BTree()
       :_root(nullptr)
    {}
private:
    Node* _root;//根
};

2.查找

若根节点不为空:
 如果根节点val == 查找key,返回所找到的结点
 如果根节点val >查找key,在其左子树继续查找
 如果根节点val < 查找key,在其右子树继续查找
 若最后没有找到key,返回nullptr

代码如下:

    Node* find(const T& val)
    {
        Node* cur = _root;
        while(cur)
        {
            //相等返回结点
            if(cur->_val == val)
                return cur;
            //比根节点小则查找左子树
            else if(cur->_val > val)
                cur = cur->_left;
            //比根节点大则查找右子树
            else
                cur = cur->_right;
        }
        //没有找到返回nullptr
        return cur;
    }

2.插入

插入的具体过程:

  1. 若树为空,则直接插入
    在这里插入图片描述

  2. 树不空:

    1. 遍历树,当存在与插入数据相同的结点时,插入失败,直接返回false
    2. 若当前结点比插入结点的值大,则继续遍历当前结点的左子树,若当前结点比插入结点的值小,则继续遍历当前结点的右子树,直到找到一个空的位置,就是插入结点的位置
    3. 找到插入结点的位置后,若比插入位置的父结点大(为了能够找到父结点,还需要一个指针指向父结点),就把结点连在父结点的右边,否则就连在父结点的左边。
      在这里插入图片描述

代码如下:

bool insert(const T& val)
    {
        //如果是空树,则直接插入结点,返回true
        if(_root == nullptr)
        {
            _root = new Node(val);
            return true;
        }

        Node* cur = _root;
        Node* parent = nullptr;//指向父结点的指针

        //若不是空树,需要遍历找到插入结点的位置
        while(cur)
        {
            parent = cur;
            //相同的结点,直接返回false,因为搜索树中不能出现相同的值
            if(cur->_val == val)
                return false;
            //比根节点小则继续查找左子树
            else if(cur->_val > val)
                cur = cur->_left;
            //比根节点大则继续查找右子树
            else
                cur = cur->_right;
        }

        //创建结点
        cur = new Node(val);

        //插入:判断插入到父结点的左边还是右边
        if(parent->_val > val)
            parent->_left = cur;
        else
            parent->_right = cur;

        return true;
    }

测试:

二叉搜索树的中序遍历,会将树中的数据升序排列,我们可以据此来简单验证插入代码是否有错。

中序遍历代码:

void _inorder(Node* root)
    {
        if(root)
        {
            _inorder(root->_left);//先遍历左子树
            cout << root->_val << " ";//打印根结点
            _inorder(root->_right);//再遍历右子树
        }
    }

    //由于类外不能访问根结点,所以要进行包装
    void inorder()
    {
        _inorder(_root);
        cout << endl;
    }

可以通过随机数的方法,进行大量数据的验证:
在这里插入图片描述

3.拷贝

树存在资源,要进行深拷贝,浅拷贝会出现二次释放的问题。

拷贝思想:通过递归,依次拷贝原二叉树的结点,并且保存原二叉树的结构,拷贝完成后,再依次自下而上进行结点的连接。

代码如下:

    BTree(const BTree<T>& bt)
        :_root(copy(bt._root))
    {}

    Node* copy(Node* root)
    {
        if(root == nullptr)
            return nullptr;
        Node* newNode = new Node(root->_val);
        newNode->_left = copy(root->_left);
        newNode->_right = copy(root->_right);

        return newNode;
    }

可以根据代码,画一下拷贝的过程,有助于更好的理解。

4.销毁与析构

这也是一个递归操作,具体过程:

  1. 先销毁左子树
  2. 再销毁右子树
  3. 最后销毁根结点

代码如下:

    void destory(Node* root)
    {
        if(root)
        {
            destory(root->_left);
            destory(root->_right);
            delete root;
        }
    }

    ~BTree()
    {
        if(_root)
        {
            destory(_root);
            _root = nullptr;
        }
    }

5.删除结点

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:

  1. 要删除的结点是叶子结点
    1. 如果叶子结点是根节点,则直接删除叶子结点,并且将根节点置空
      在这里插入图片描述

    2. 如果不是根节点,判断该结点是父亲结点的左孩子还是右孩子,如果是左孩子则将左孩子置空并删除此结点;若是右孩子,则将右孩子的置空并删除此结点
      在这里插入图片描述

代码如下:

//遍历搜索二叉树,找到要删除的结点
        Node* cur = _root;
        Node* parent = nullptr;

        while(cur)
        {
            if(cur->_val == val)//找到结点,结束循环
                break;
            parent = cur;
            if(cur->_val > val)
                cur = cur->_left;
            else
                cur = cur->_right;
        }

        //如果cur为空,说明没有找到要删除的结点,则直接返回false
        if(cur == nullptr)
            return false;

        //判断cur是否是叶子结点
        if(cur->_left == nullptr && cur->_right == nullptr)
        {
            //判断这个结点是否是根结点
            if(cur == _root)
            {
                //根结点置空
                _root = nullptr;
            }
            else
            {
                //判断要删除的结点是父亲结点的哪一边
                if(parent->_left == cur)
                    parent->_left = nullptr;
                else
                    parent->_right = nullptr;
            }
            //删除结点
            delete cur;
        }
  1. 要删除的结点只有左孩子结点
    1. 若这个结点是根节点,则让根节点指向它的左孩子,并删除需要删除的结点
      在这里插入图片描述
    2. 不是跟根结点,若需要删除的结点在它父结点的左边,那父结点的左边就去连要删除结点的左边;若需要删除的结点在它父结点的右边,那父结点的右边就去连要删除结点的左边。
      在这里插入图片描述

代码如下:

        else if(cur->_right == nullptr)//需要删除的结点只有左孩子
        {
            //判断是否是根结点
            if(cur == _root)
            {
                _root = cur->_left;
            }
            else
            {
                //判断删除结点在父亲的哪一边
                if(parent->_left == cur)
                    parent->_left = cur->_left;
                else
                    parent->_right = cur->_left;
            }
            //删除cur
            delete cur;
        }
  1. 要删除的结点只有右孩子结点
    这与要删除的结点只有左孩子的思想一致,下面简单说说。
    1. 若这个结点是根节点,则让根节点指向它的右孩子,并删除需要删除的结点
      在这里插入图片描述
    2. 不是跟根结点,若需要删除的结点在它父结点的左边,那父结点的左边就去连要删除结点的右边;若需要删除的结点在它父结点的右边,那父结点的右边就去连要删除结点的右边。
      在这里插入图片描述

代码如下:

        else if(cur->_left == nullptr)//需要删除的结点只有右孩子
        {
            //判断是否是根节点
            if(cur == _root)
            {
                _root = cur->_right;
            }else
            {
                //判断删除结点在父结点的哪一边
                if(parent->_left == cur)
                    parent->_left = cur->_right;
                else
                    parent->_right = cur->_right;
            }
            delete cur;
        }
  1. 要删除的结点有左、右孩子结点
    在这里插入图片描述
    删除既有左孩子又有右孩子的结点是四种情况最麻烦的情况,比如说我们删除结点5,若我们直接删除,就会破坏树的结构;我们需要找到一个结点(叶子结点或者只有一边的结点),将其和结点5交换,再删除它,这样就归于上面三种情况的一种,删除起来比较简单,并且也不会破坏结构。那么需要交换的结点怎么找?结点5是根结点,选一个结点来代替它的位置,那么一定要大于左子树并且小于右子树,这样看来就只有两种结点符合:左子树的最大结点,右子树的最小结点。具体位置就是左子树的最右结点,或者右子树的最左结点。下面都以左子树的最右结点来说。

    具体流程:

    1. 先找到要删除结点的左子树的最右结点(leftRightMost)
    2. 将要删除的结点与leftRightMost交换
    3. 此时删除leftRightMost,这个结点没有右子树,但是不一定没有左子树,所以leftRightMost的父亲结点(要判断leftRightMost是父亲结点的左孩子还是右孩子),连接的就是leftRightMost的左子树,如果没有左子树,父亲结点连接nullptr。
      在这里插入图片描述
      在这里插入图片描述

代码如下:

        else//需要删除的结点既有左孩子也有右孩子
        {
            Node* leftRightMost = cur->_left;
            parent = cur;

            //找到左子树的最有结点
            while(leftRightMost->_right)
            {
                parent = leftRightMost;
                leftRightMost = leftRightMost->_right;
            }

            //交换左子树的最右结点和需要删除的结点
            swap(cur->_val, leftRightMost->_val);

            if(parent->_left == leftRightMost)
                parent->_left = leftRightMost->_left;
            else
                parent->_right = leftRightMost->_left;

            delete leftRightMost;
        }

erase接口的代码总结:

bool erase(const T& val)
    {
        //遍历搜索二叉树,找到要删除的结点
        Node* cur = _root;
        Node* parent = nullptr;

        while(cur)
        {
            if(cur->_val == val)//找到结点,结束循环
                break;
            parent = cur;
            if(cur->_val > val)
                cur = cur->_left;
            else
                cur = cur->_right;
        }

        //如果cur为空,说明没有找到要删除的结点,则直接返回false
        if(cur == nullptr)
            return false;

        //判断cur是否是叶子结点
        if(cur->_left == nullptr && cur->_right == nullptr)
        {
            //判断这个结点是否是根结点
            if(cur == _root)
            {
                //根结点置空
                _root = nullptr;
            }
            else
            {
                //判断要删除的结点是父亲结点的哪一边
                if(parent->_left == cur)
                    parent->_left = nullptr;
                else
                    parent->_right = nullptr;
            }
            //删除结点
            delete cur;
        }
        else if(cur->_right == nullptr)//需要删除的结点只有左孩子
        {
            //判断是否是根结点
            if(cur == _root)
            {
                _root = cur->_left;
            }
            else
            {
                //判断删除结点在父亲的哪一边
                if(parent->_left == cur)
                    parent->_left = cur->_left;
                else
                    parent->_right = cur->_left;
            }
            //删除cur
            delete cur;
        }
        else if(cur->_left == nullptr)//需要删除的结点只有右孩子
        {
            //判断是否是根节点
            if(cur == _root)
            {
                _root = cur->_right;
            }else
            {
                //判断删除结点在父结点的哪一边
                if(parent->_left == cur)
                    parent->_left = cur->_right;
                else
                    parent->_right = cur->_right;
            }
            delete cur;
        }
        else//需要删除的结点既有左孩子也有右孩子
        {
            Node* leftRightMost = cur->_left;
            parent = cur;

            //找到左子树的最有结点
            while(leftRightMost->_right)
            {
                parent = leftRightMost;
                leftRightMost = leftRightMost->_right;
            }

            //交换左子树的最右结点和需要删除的结点
            swap(cur->_val, leftRightMost->_val);

            if(parent->_left == leftRightMost)
                parent->_left = leftRightMost->_left;
            else
                parent->_right = leftRightMost->_left;

            delete leftRightMost;
        }
        return true;
    }

测试:
在这里插入图片描述

二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
在这里插入图片描述最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:㏒₂N
最差情况下,二叉搜索树退化为单支树,其平均比较次数为:N/2

二叉搜索树的应用

K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。

KV模型二叉搜索树都是通过key值来查找遍历;插入时,是插入key,value值;删除是通过key值来删除。

K模型二叉搜索树全部代码总结:

#include <iostream>
using namespace std;
#include<time.h>

template<class T>
struct BTreeNode
{
    typedef BTreeNode<T> Node;

    //三个属性值
    Node* _left;//左子树
    Node* _right;//右子树
    T _val;//数据

    BTreeNode(const T& val = T())
       :_left(nullptr)
       ,_right(nullptr)
       ,_val(val)
    {}
};

template<class T>
class BTree
{
public:
    typedef  BTreeNode<T> Node;

    BTree()
       :_root(nullptr)
    {}

    Node* find(const T& val)
    {
        Node* cur = _root;
        while(cur)
        {
            if(cur->_val == val)
                return cur;
            else if(cur->_val > val)
                cur = cur->_left;
            else
                cur = cur->_right;
        }
        return cur;
    }

    BTree(const BTree<T>& bt)
        :_root(copy(bt._root))
    {}

    Node* copy(Node* root)
    {
        if(root == nullptr)
            return nullptr;
        Node* newNode = new Node(root->_val);
        newNode->_left = copy(root->_left);
        newNode->_right = copy(root->_right);

        return newNode;
    }

    bool insert(const T& val)
    {
        //如果是空树,则直接插入结点,返回true
        if(_root == nullptr)
        {
            _root = new Node(val);
            return true;
        }

        Node* cur = _root;
        Node* parent = nullptr;//指向父结点的指针

        //若不是空树,需要遍历找到插入结点的位置
        while(cur)
        {
            parent = cur;
            //相同的结点,直接返回false,因为搜索树中不能出现相同的值
            if(cur->_val == val)
                return false;
            //比根节点小则继续查找左子树
            else if(cur->_val > val)
                cur = cur->_left;
            //比根节点大则继续查找右子树
            else
                cur = cur->_right;
        }

        //创建结点
        cur = new Node(val);

        //插入:判断插入到父结点的左边还是右边
        if(parent->_val > val)
            parent->_left = cur;
        else
            parent->_right = cur;

        return true;
    }

    void _inorder(Node* root)
    {
        if(root)
        {
            _inorder(root->_left);
            cout << root->_val << " ";
            _inorder(root->_right);
        }
    }

    bool erase(const T& val)
    {
        //遍历搜索二叉树,找到要删除的结点
        Node* cur = _root;
        Node* parent = nullptr;

        while(cur)
        {
            if(cur->_val == val)//找到结点,结束循环
                break;
            parent = cur;
            if(cur->_val > val)
                cur = cur->_left;
            else
                cur = cur->_right;
        }

        //如果cur为空,说明没有找到要删除的结点,则直接返回false
        if(cur == nullptr)
            return false;

        //判断cur是否是叶子结点
        if(cur->_left == nullptr && cur->_right == nullptr)
        {
            //判断这个结点是否是根结点
            if(cur == _root)
            {
                //根结点置空
                _root = nullptr;
            }
            else
            {
                //判断要删除的结点是父亲结点的哪一边
                if(parent->_left == cur)
                    parent->_left = nullptr;
                else
                    parent->_right = nullptr;
            }
            //删除结点
            delete cur;
        }
        else if(cur->_right == nullptr)//需要删除的结点只有左孩子
        {
            //判断是否是根结点
            if(cur == _root)
            {
                _root = cur->_left;
            }
            else
            {
                //判断删除结点在父亲的哪一边
                if(parent->_left == cur)
                    parent->_left = cur->_left;
                else
                    parent->_right = cur->_left;
            }
            //删除cur
            delete cur;
        }
        else if(cur->_left == nullptr)//需要删除的结点只有右孩子
        {
            //判断是否是根节点
            if(cur == _root)
            {
                _root = cur->_right;
            }else
            {
                //判断删除结点在父结点的哪一边
                if(parent->_left == cur)
                    parent->_left = cur->_right;
                else
                    parent->_right = cur->_right;
            }
            delete cur;
        }
        else//需要删除的结点既有左孩子也有右孩子
        {
            Node* leftRightMost = cur->_left;
            parent = cur;

            //找到左子树的最有结点
            while(leftRightMost->_right)
            {
                parent = leftRightMost;
                leftRightMost = leftRightMost->_right;
            }

            //交换左子树的最右结点和需要删除的结点
            swap(cur->_val, leftRightMost->_val);

            if(parent->_left == leftRightMost)
                parent->_left = leftRightMost->_left;
            else
                parent->_right = leftRightMost->_left;

            delete leftRightMost;
        }
        return true;
    }

    void inorder()
    {
        _inorder(_root);
        cout << endl;
    }

    void destory(Node* root)
    {
        if(root)
        {
            destory(root->_left);
            destory(root->_right);
            delete root;
        }
    }

    ~BTree()
    {
        if(_root)
        {
            destory(_root);
            _root = nullptr;
        }
    }

private:
    Node* _root;//根
};

KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。 该种方式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word,chinese>就构成一种键值对。

二叉搜索树KV模型代码总结:

#include <iostream>
using namespace std;
#include<time.h>

template<class K, class V>
struct BTreeNode
{
    typedef BTreeNode<K, V> Node;

    Node* _left;//左子树
    Node* _right;//右子树
    K _key;
    V _value;

    BTreeNode(const K& key = K(), const V& val = V())
            :_left(nullptr)
            ,_right(nullptr)
            ,_key(key)
            ,_value(val)
    {}
};

template<class K, class V>
class BTree
{
public:
    typedef  BTreeNode<K, V> Node;

    BTree()
            :_root(nullptr)
    {}

    Node* find(const K& key)
    {
        Node* cur = _root;
        while(cur)
        {
            if(cur->_key == key)
                return cur;
            else if(cur->_key > key)
                cur = cur->_left;
            else
                cur = cur->_right;
        }
        return cur;
    }

    BTree(const BTree<K, V>& bt)
            :_root(copy(bt._root))
    {}

    Node* copy(Node* root)
    {
        if(root == nullptr)
            return nullptr;
        Node* newNode = new Node(root->_key, root->_value);
        newNode->_left = copy(root->_left);
        newNode->_right = copy(root->_right);

        return newNode;
    }

    bool insert(const K& key, const V& val)
    {
        //如果是空树,则直接插入结点,返回true
        if(_root == nullptr)
        {
            _root = new Node(key, val);
            return true;
        }

        Node* cur = _root;
        Node* parent = nullptr;//指向父结点的指针

        //若不是空树,需要遍历找到插入结点的位置
        while(cur)
        {
            parent = cur;
            //相同的结点,直接返回false,因为搜索树中不能出现相同的值
            if(cur->_key == key)
                return false;
                //比根节点小则继续查找左子树
            else if(cur->_key > key)
                cur = cur->_left;
                //比根节点大则继续查找右子树
            else
                cur = cur->_right;
        }

        //创建结点
        cur = new Node(key, val);

        //插入:判断插入到父结点的左边还是右边
        if(parent->_key > key)
            parent->_left = cur;
        else
            parent->_right = cur;

        return true;
    }

    void _inorder(Node* root)
    {
        if(root)
        {
            _inorder(root->_left);
            cout << root->_key<< "-->" << root->_value << " ";
            _inorder(root->_right);
        }
    }

    bool erase(const K& key)
    {
        //遍历搜索二叉树,找到要删除的结点
        Node* cur = _root;
        Node* parent = nullptr;

        while(cur)
        {
            if(cur->_key == key)//找到结点,结束循环
                break;
            parent = cur;
            if(cur->_key > key)
                cur = cur->_left;
            else
                cur = cur->_right;
        }

        //如果cur为空,说明没有找到要删除的结点,则直接返回false
        if(cur == nullptr)
            return false;

        //判断cur是否是叶子结点
        if(cur->_left == nullptr && cur->_right == nullptr)
        {
            //判断这个结点是否是根结点
            if(cur == _root)
            {
                //根结点置空
                _root = nullptr;
            }
            else
            {
                //判断要删除的结点是父亲结点的哪一边
                if(parent->_left == cur)
                    parent->_left = nullptr;
                else
                    parent->_right = nullptr;
            }
            //删除结点
            delete cur;
        }
        else if(cur->_right == nullptr)//需要删除的结点只有左孩子
        {
            //判断是否是根结点
            if(cur == _root)
            {
                _root = cur->_left;
            }
            else
            {
                //判断删除结点在父亲的哪一边
                if(parent->_left == cur)
                    parent->_left = cur->_left;
                else
                    parent->_right = cur->_left;
            }
            //删除cur
            delete cur;
        }
        else if(cur->_left == nullptr)//需要删除的结点只有右孩子
        {
            //判断是否是根节点
            if(cur == _root)
            {
                _root = cur->_right;
            }else
            {
                //判断删除结点在父结点的哪一边
                if(parent->_left == cur)
                    parent->_left = cur->_right;
                else
                    parent->_right = cur->_right;
            }
            delete cur;
        }
        else//需要删除的结点既有左孩子也有右孩子
        {
            Node* leftRightMost = cur->_left;
            parent = cur;

            //找到左子树的最有结点
            while(leftRightMost->_right)
            {
                parent = leftRightMost;
                leftRightMost = leftRightMost->_right;
            }

            //交换左子树的最右结点和需要删除的结点
            swap(cur->_key, leftRightMost->_key);
            swap(cur->_value,leftRightMost->_value);

            if(parent->_left == leftRightMost)
                parent->_left = leftRightMost->_left;
            else
                parent->_right = leftRightMost->_left;

            delete leftRightMost;
        }
        return true;
    }

    void inorder()
    {
        _inorder(_root);
        cout << endl;
    }

    void destory(Node* root)
    {
        if(root)
        {
            destory(root->_left);
            destory(root->_right);
            delete root;
        }
    }

    ~BTree()
    {
        if(_root)
        {
            destory(_root);
            _root = nullptr;
        }
    }

private:
    Node* _root;//根
};
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值