二叉搜索树及其模拟实现


在对map/multimap/set/multiset进行了简单的了解后,会发现这几个容器有个共同点是: 其底层都是按照二叉搜索树来实现的。那什么是二叉搜索树?其底层是二叉搜索树吗?

1.二叉搜索树(Binary Search Tree)

1.1二叉搜索树的概念

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

  1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  3. 它的左右子树也分别为二叉搜索树

示例:
int a[]={5,3,4,1,7,8,2,6,0,9}
在这里插入图片描述

1.2二叉搜索树操作

2.1查找

①若根节点不为空
如果根节点key==查找的key,返回ture
如果根节点key>查找key,在其左子树查找(重复①)
如果根节点key<查找key,在其右子树查找(重复①)
②重复①,如果找不到,说明该元素不存在,返回false

PNode Find(const T& data)
 {
	 PNode pCur = _pRoot;
	 //根节点不为空
	 while (pCur){
		 //如果根节点key==查找的key,找到
		 if (data == pCur->_data)
			 return pCur;
		//如果根节点key<查找key,在其右子树查找
		 else if (data < pCur->_pLeft)
			 pCur = pCur->_pLeft;
		 //如果根节点key>查找key,在其左子树查找
		 else
		 	 pCur = pCur->_pRight;
	}
	 return nullptr;
 }

通过 BST 查找节点,理想情况下我们需要检查的节点数可以减半。如下图中的 BST 树,包含了 15 个节点。从根节点开始执行查找算法,第一次比较决定我们是移向左子树还是右子树。对于任意一种情况,一旦执行这一步,我们需要访问的节点数就减少了一半,从 15 降到了 7。同样,下一步访问的节点也减少了一半,从 7 降到了 3,以此类推。根据这一特点,查找算法的时间复杂度应该是 O(log­2n)
在这里插入图片描述
实际上,对于 BST 查找算法来说,其十分依赖于树中节点的拓扑结构,也就是节点间的布局关系。下图描绘了一个节点插入顺序为 20, 50, 90, 150, 175, 200 的 BST 树。这些节点是按照递升顺序被插入的,结果就是这棵树没有广度(Breadth)可言。也就是说,它的拓扑结构其实就是将节点排布在一条线上,而不是以扇形结构散开,所以查找时间也为 O(n)。
在这里插入图片描述

当 BST 树中的节点以扇形结构散开时,对它的插入、删除和查找操作最优的情况下可以达到亚线性的运行时间 O(log2n)。因为当在 BST 中查找一个节点时,每一步比较操作后都会将节点的数量减少一半。尽管如此,如果拓扑结构像上图中的样子时,运行时间就会退减到线性时间 O(n)。因为每一步比较操作后还是需要逐个比较其余的节点。也就是说,在这种情况下,在 BST 中查找节点与在数组(Array)中查找就基本类似了。
因此,BST 算法查找时间依赖于树的拓扑结构。最佳情况是 O(log­2n),而最坏情况是 O(n)。

2.2插入

新插入的节点一定是一个新添加的叶子节点
在这里插入图片描述
BST 的插入算法的复杂度与查找算法的复杂度是一样的:最佳情况是 O(log­2n),而最坏情况是 O(n)。 因为它们对节点的查找定位策略是相同的。

a.如果树为空,直接插入,然后返回true
在这里插入图片描述
b.树不为空,按照二叉搜索树的性质查找插入位置,插入新节点
在这里插入图片描述

 bool Insert(const T& data)
 {
	 // 如果树为空,直接插入
	 if (nullptr == _pRoot){
 		_pRoot = new Node(data);
 		return true;
 	}
 	// 按照二叉搜索树的性质查找data在树中的插入位置
 	PNode pCur = _pRoot;
 	// 记录pCur的双亲,因为新元素最终插入在pCur双亲左右孩子的位置
 	PNode pParent = nullptr;
 	while (pCur){
 		pParent = pCur;
 		if (data < pCur->_data)
 			pCur = pCur->_pLeft;
 		else if (data > pCur->_data)
 			pCur = pCur->_pRight; 
 		else
 		// 元素已经在树中存在
 			return false;
 	}
 	// 插入元素
 	pCur = new Node(data);
 	if (data < pParent->_data)
 		pParent->_pLeft = pCur;
 	else
 		pParent->_pRight = pCur;
 	return true;
 }

2.3删除

先查找要删除的元素是否存在于二叉搜索树中,如果不存在,则返回,否则要删除的元素分四种情况:
a.要删除的节点无孩子节点
b.要删除的节点只有左孩子节点
c.要删除的节点只有右孩子节点
d.要删除的节点有左右孩子节点
实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题

  bool Erase(const T& data) {
        // 如果树为空,删除失败
        if (nullptr == _pRoot)
            return false;
        // 查找在data在树中的位置
        PNode pCur = _pRoot;
        PNode pParent = nullptr;
        while (pCur) {
            if (data == pCur->_data)
                break;
            else if (data < pCur->_data) {
                pParent = pCur;
                pCur = pCur->_pLeft;
            }
            else {
                pParent = pCur;
                pCur = pCur->_pRight;
            }
        }
        // data不在二叉搜索树中,无法删除
        if (nullptr == pCur)
            return false;
		Node* Del = pCur;
        // 当前节点是叶子节点或者只有左节点---可直接删除	
        if (nullptr == pCur->_pRight) {
            //要删除的节点是根节点
            if (pParent == nullptr)
                _pRoot = _pRoot->_pLeft;
            if (pParent->_pLeft == pCur)
                pParent->_pLeft = pCur->_pLeft;
            else
                pParent->_pRight = pCur->_pLeft;
        }

        // 当前节点只有右孩子---可直接删除
        else if (nullptr == pCur->_pLeft) {
      		 //要删除的节点是根节点
            if (pParent == nullptr)
                _pRoot = _pRoot->_pRight;
            if (pParent->_pLeft == pCur)
                pParent->_pLeft = pCur->_pRight;
            else
                pParent->_pRight = pCur->_pRight;
        }
        // 当前节点左右孩子都存在,直接删除不好删除,可以在其子树中找一个替代结点,比如:
        // 找其左子树中的最大节点,即左子树中最右侧的节点,或者其右子树中最小的节点,即右子树中最左侧的节点
        // 替代节点找到后,将替代节点中的值交给待删除节点,转换成删除替代节点
        else {
            PNode Replace = pCur->_pRight;
            PNode Pre = pCur;//替代节点的双亲节点
            if (Replace->_pLeft) {
                //用节点右子树的最小节点作为替代节点
                Pre = Replace;
                Replace = Replace->_pLeft;
            }
            //将替代节点中的值交给待删除节点,转换成删除替代节点
            pCur->_data = Replace->_data;
            if (Pre->_pLeft == Replace)
                Pre->_pLeft = Replace->_pRight;
            else
                Pre->_pRight = Replace->_pRight;
            Del = Replace;
        }
        delete Del;
        return true;
    }

1.3性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
在这里插入图片描述
最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:log2(N)
最差情况下,二叉搜索树退化为单支树,其平均比较次数为:N/2

2.二叉搜索树的模拟实现

#include <iostream>
using namespace std;
template<class T>
struct BSTNode
{
    BSTNode(const T& data = T())
        : _pLeft(nullptr)
        , _pRight(nullptr)
        , _data(data)
    {}
    BSTNode<T>* _pLeft;
    BSTNode<T>* _pRight;
    T _data;
};

template<class T>
class BSTree
{
    typedef BSTNode<T> Node;
    typedef Node* PNode;
public:
    BSTree()
        : _pRoot(nullptr) {
    }

    ~BSTree() {
        _Destroy(_pRoot);
    }

    void _Destroy(PNode _pRoot) {
        if (_pRoot == nullptr)
            return;
        _Destroy(_pRoot->_pLeft);
        _Destroy(_pRoot->_pRight);
        delete _pRoot;
    }
    //拷贝构造
    BSTree(const BSTree<T>& tree) {
        _pRoot = Copy(tree._pRoot);
    }
    //赋值运算符重载
    BSTree<T>& operator =(const BSTree<T>& tree) {
        if (this != &tree) {
            _Destroy(_pRoot);
            _pRoot = Copy(tree._pRoot);
        }
        return *this;
    }

    PNode Copy(PNode root) {
        if (root == nullptr)
            return root;
        PNode NewRoot = new Node(root->_data);
        NewRoot->_pLeft = Copy(root->_pLeft);
        NewRoot->_pRight = Copy(root->_pRight);
        return NewRoot;
    }

    void Print_Tree() {
        Inorder(_pRoot);
        cout << endl;
    }
    void Inorder(PNode root) {//中序遍历二叉搜索树 是其是从小到大的顺序
        if (root) {
            Inorder(root->_pLeft);
            cout << root->_data << " ";
            Inorder(root->_pRight);
        }
    }

    PNode Find(const T& data) {
        PNode pCur = _pRoot;
        while (pCur)
        {
            if (data == pCur->_data)
                return pCur;
            else if (data < pCur->_data)
                pCur = pCur->_pLeft;
            else
                pCur = pCur->_pRight;
        }
        return nullptr;
    }

    bool Insert(const T& data) {
        // 如果树为空,直接插入
        if (nullptr == _pRoot) {
            _pRoot = new Node(data);
            return true;
        }
        // 按照二叉搜索树的性质查找data在树中的插入位置
        PNode pCur = _pRoot;
        // 记录pCur的双亲,因为新元素最终插入在pCur双亲左右孩子的位置
        PNode pParent = nullptr;
        while (pCur) {
            pParent = pCur;
            if (data < pCur->_data)
                pCur = pCur->_pLeft;
            else if (data > pCur->_data)
                pCur = pCur->_pRight;
            else
                // 元素已经在树中存在
                return false;
        }
        // 插入元素
        pCur = new Node(data);
        if (data < pParent->_data)
            pParent->_pLeft = pCur;
        else
            pParent->_pRight = pCur;
        return true;
    }

    bool Erase(const T& data) {
        // 如果树为空,删除失败
        if (nullptr == _pRoot)
            return false;
        // 查找在data在树中的位置
        PNode pCur = _pRoot;
        PNode pParent = nullptr;
        while (pCur) {
            if (data == pCur->_data)
                break;
            else if (data < pCur->_data) {
                pParent = pCur;
                pCur = pCur->_pLeft;
            }
            else {
                pParent = pCur;
                pCur = pCur->_pRight;
            }
        }
        // data不在二叉搜索树中,无法删除
        if (nullptr == pCur)
            return false;

        Node* Del = pCur;
        // 当前节点是叶子节点或者只有左节点---可直接删除	
        if (nullptr == pCur->_pRight) {
            //要删除的节点是根节点
            if (pParent == nullptr)
                _pRoot = _pRoot->_pLeft;
            if (pParent->_pLeft == pCur)
                pParent->_pLeft = pCur->_pLeft;
            else
                pParent->_pRight = pCur->_pLeft;
        }

        // 当前节点只有右孩子---可直接删除
        else if (nullptr == pCur->_pLeft) {
            if (pParent == nullptr)
                _pRoot = _pRoot->_pRight;
            if (pParent->_pLeft == pCur)
                pParent->_pLeft = pCur->_pRight;
            else
                pParent->_pRight = pCur->_pRight;
        }
        // 当前节点左右孩子都存在,直接删除不好删除,可以在其子树中找一个替代结点,比如:
        // 找其左子树中的最大节点,即左子树中最右侧的节点,或者其右子树中最小的节点,即右子树中最左侧的节点
        // 替代节点找到后,将替代节点中的值交给待删除节点,转换成删除替代节点
        else {
            PNode Replace = pCur->_pRight;
            PNode Pre = pCur;//替代节点的双亲节点
            if (Replace->_pLeft) {
                //用节点右子树的最小节点作为替代节点
                Pre = Replace;
                Replace = Replace->_pLeft;
            }
            //将替代节点中的值交给待删除节点,转换成删除替代节点
            pCur->_data = Replace->_data;
            if (Pre->_pLeft == Replace)
                Pre->_pLeft = Replace->_pRight;
            else
                Pre->_pRight = Replace->_pRight;
            Del = Replace;
        }
        delete Del;
        return true;
    }
 private:
    PNode _pRoot;
};
int main() {
    BSTree<int> tree1;
    tree1.Insert(3);
    tree1.Insert(5);
    tree1.Insert(6);
    tree1.Insert(4);
    tree1.Insert(7);
    tree1.Insert(8);
    tree1.Print_Tree();

    BSTNode<int>* add = tree1.Find(5);
    if (add == nullptr)
        cout << "二叉树中没有这个值" << endl;
    else
        cout << "在二叉搜索树找到了该值" <<" "<<add->_data << endl;
    tree1.Erase(6);
    cout << "删除后的搜索二叉树为:" << endl;
    tree1.Print_Tree();

    BSTree<int> tree2(tree1);
    cout << "Tree2:";
    tree2.Print_Tree();
    return 0;
}

这里选择中序遍历二叉搜索树,是因为中序遍历二叉搜索树,就是其值从小到大的排序。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,让我们来理解一下什么是最优二叉搜索树。 最优二叉搜索树是一种特殊的二叉搜索树,其中每个节点都有一个关键字和一个概率值。该树的目标是最小化搜索的期望成本,即期望搜索次数乘以每次搜索的成本。这通常用来表示在一组键被搜索时所需的最小平均比较次数。 现在,让我们来看看如何使用动态规划解决这个问题。 首先,我们需要定义两个数组:一个是 $e[i,j]$,表示在 $i$ 到 $j$ 的节点范围内构建最优二叉搜索树的期望代价,另一个是 $w[i,j]$,表示在 $i$ 到 $j$ 的节点范围内所有节点的概率之和。 接下来,我们需要使用以下公式来计算 $e[i,j]$ 和 $w[i,j]$: $e[i,j] = \begin{cases} q_{i-1} & j = i - 1\\ \min_{i\le k \le j}\{e[i,k-1] + e[k+1,j] + w[i,j]\} & i \le j \end{cases}$ $w[i,j] = \begin{cases} p_i + q_i & j = i\\ w[i,j-1] + p_j + q_j & j > i\\ \end{cases}$ 其中,$p_i$ 和 $q_i$ 分别表示键 $i$ 的查找概率和未查找概率。 最后,我们可以使用动态规划算法来计算最优二叉搜索树。具体步骤如下: 1. 初始化 $e[i,i-1] = q_{i-1}$ 和 $w[i,i-1] = q_{i-1}$,其中 $1\le i \le n+1$,$n$ 为节点数。 2. 对于所有 $i,j$,按照节点范围由小到大的顺序计算 $e[i,j]$ 和 $w[i,j]$。 3. 最终的最优解为 $e[1,n]$。 通过使用随机化查找数字及其查找成功概率,我们可以计算出最优二叉搜索树,并且可以保证它是最优的。同时,我们还可以通过动态规划算法来分析求解的过程,并且解释说明每个步骤的含义和作用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值