c++进阶--二叉搜索树模拟实现

目录

前言

一、二叉搜索树

1.二叉搜索树概念

2.二叉搜索树操作

二、二叉搜索树实现

0.定义一个节点

1.定义一棵树

2.增删改查

2.1.查找

2.2.插入

2.3.删除

2.3.1非递归删除法

a.只有左孩子 -- 删除14

b.只有右孩子-- 删除10

c.有左右孩子--删除8

2.3.2递归删除法

三、二叉搜索树应用

1.K模型(解决在不在的问题)

2.KV模型

3.二叉搜索树性能分析

总结


前言

本文中出现的源码已在本地vs2019下测试无误,上传至gitee:

https://gitee.com/a_young/binary-search-tree


一、二叉搜索树

1.二叉搜索树概念

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

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 左、右子树都是二叉搜索树

2.二叉搜索树操作

int a[] = {8,3,1,10,6,4,7,14,13};

 1.二叉搜索树的查找

  1. 从根节点开始比较,查找,如果比根节点值大,往右走查找,比根节点值小,往左走查找
  2. 最多查找高度次,走到空节点还没找到,则说明这个值在树中不存在。

2.二叉搜索树的插入

  1. 若为空树,则直接新增节点,赋值给root指针
  2. 树不为空,按性质查找插入位置,插入新节点

3.二叉搜索树的删除

  • 先查找元素是否在树中,如果不存在,返回。否则要删除的节点分为下面四种情况
  1. 要删除的结点是叶子节点
  2. 要删除的节点只有左孩子节点
  3. 要删除的节点只有右孩子节点
  4. 要删除的节点有左、右孩子节点

1可以看成2,3的一种情况。

  • 只有左孩子节点(如上图 14) :删除该节点,并使被删除节点的父节点指向被删节点的左孩子节点
  • 只有右孩子节点:删除该节点,并使被删除节点的父节点指向被删节点的右孩子节点
  • 有左右孩子节点:先寻找右树的最小节点(或者左树的最大节点),用它的值填补到被删除节点中,再处理该节点的删除问题。 详细处理代码以及坑往下。

二、二叉搜索树实现

0.定义一个节点

template<class k>
struct BSTreeNode
{
    //三个成员
    BSTreeNode<T>* _left;
    BSTreeNode<T>* _right;
    K _key;

    //构造函数
    BSTreeNode(const k& key)
    :_left(nullptr)
    ,_right(nullptr)
    ,key(key)
    {
    }

};

1.定义一棵树

实现构造,拷贝构造,析构。

  • 构造的时候用一个节点,初始化为空。
  • 拷贝构造,这里注意,必须是深拷贝,浅拷贝容易出现野指针。
  • 析构的时候使用后序递归删除即可。
template<class k>
class BSTree
{
   typedef BSTreeNode<T> Node;
   public:
   /* BSTree()
    :_root(nullptr)
    {}
    */
    BSTree() = default; //指定强制生成默认构造
    //拷贝构造
    BSTree(const BSTree<K> & t)
    {
       _root = copy(t._root);
    }

    //跟前序创建类似 后序回来才链接
    Node* copy(Node * root)
    {
       if(root == nullptr)
            return nullptr;

       Node * new_root = new Node(root ->key);
       new_root ->_left = Copy(root->_left);
       nre_root->_right = Copy(root->_right);
       return new_root;
       
    }
    //析构
    ~BSTree()
    {
        //使用后序递归
       Destory(_root);
    
    }

     void Destroy(Node * root)
     {
         if(root == nullprt)
            return ;
         Destroy(root->left);
         Destroy(root->right);
         delete root;
      }

    //成员函数
    //实现增删改查 protected封装
    //赋值
    BSTree<k>& operator=(BSTree<k> t)
    {
         swap(_root,t._root);
         return *this;   
    }
   private:
    
    Node * _root = nullptr; 

2.增删改查

2.1.查找

bool Find(const k& key)
{
    Node * cur = _root;
    while(cur)
    {
        if(cur->_ley <key)
            cur = cur->right;
        else if(cur->_key >key)
            cur = cur ->right;
        else
            {
                return true;
            }

    return false;
}
  

2.2.插入

//非递归解法
bool Insert(const k& key)
{
    //如果是一个空树
    if(_root == nullptr)
      {
           _root = new Node(key);        
            return true;
      }
      
    Node * parent = nullptr;
    Node * cur = _root;
    while(cur)
    {
        if (cur->_key < key)
		{
			parent = cur;
            //注意这里,cur更新前不是空,更新后才是空,更新前还可以访问right
		    cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
        else
            return false;
    }
    
    //找到对应位置后,开始插入
    cur = new Node(key)
    //链接
    if(parent->_key < key)
        parent ->_right = cur;
    else
        parent->_left = cur;
    
    return true;
}
    
            
        

这里使用递归的方式再来插入,一个很巧妙的使用引用的例子。假设我要在上图里面插入一个2,先去找到合适的位置,1的right是空,在这里new了一个Node,值为2,同时返回,此时这里的Node就是天然的上一层栈帧中的root->right,完美链接

bool _InsertR(Node& * root, const k& key)
{
    if(root == nullptr)
    {
        root = new Node(key);
         return true;
    }
    
   	if (root->_key < key)
	{
		return _InsertR(root->_right, key);
	}
	else if (root->_key > key)
	{
		return _InsertR(root->_left, key);
	}
	else
	{
		return false;
	}
}


bool InsertR(const k& key)
{
    return _InsertR(_root, key);
}

2.3.删除

2.3.1非递归删除法

bool _Erase(Node * root,const K& key)
{
    Node * parent = nullptr;
    Node * cur = _root;
    while(cur)
    {
        if(_cur->_key < key)
        {
            parent = cur;
            cur = cur->right;
        }

        else if(_cur->_key >key)
        {
            parent = cur;
            cur = cur->leftt;
        }

        //找到了,开始删除
        else
        {
            //a.只有左孩子
            //b.只有右孩子
            //c.左右孩子都有
            
            return true;

        }

}
bool Erase(const K& key)
{
   return  bool _Erase(Node * root,const K& key);
}

a.只有左孩子 -- 删除14

通过上面代码进行查找,找到了14,此时parent指向10,cur指向14,14只有左孩子,进行链接,14的左孩子成为10的右孩子,删除14。

if(cur ->_right  == nullptr)
 {
       if(parent ->_left == cur)
            parent ->_left = cur->left;
        
        else if(parent ->_right == cur)
            parent ->_right = cur->_right;

        delete cur;
}
        return true;
    

但是有特殊情况:如果有一棵树为只有两个节点,要删除的节点为根节点,则它的父节点parent就是空指针,无法进行判断删除。所以这里要进行判断,如果是,则更新root,再删除cur。

if(cur == _root)
{
    _root = cur->_left;
}

b.只有右孩子-- 删除10

通过查找找到了10,cur指向10,parent指向8,开始删除

if(cur == _root)
{
    _root = cur->_right;
}


else if( parent ->_left == cur)
{
        parent->left = cur->_right;
}

else if( parent ->_right == cur)
{
        parent ->right = cur->_right;
 }


delete cur;
return true;

c.有左右孩子--删除8

此时需要找到子树中的左树中的最右节点(最大)或者右数中的最左节点(最小)节点来替换掉根节点,如下我们使用右树中的最左节点去替换。 此时这个最左节点,它也可能有右孩子,肯定没有左孩子。所以我们要进行托孤,即这里要找到这个最左节点的父亲

假设删除树中的根节点8,树中10有左节点 9,9有自己的右孩子9.5,所以先找到右树的最左节点9,它的父亲10,托孤自己的右孩子9.5,为10的左孩子。9替代掉8,最后删除掉minRight这个位置的结构。

这里也需要小心避坑,如果这里删除8,minRight指向10,没有minRight->left,pminRight为空(所以pminRight不能刚开始就给空,需要给cur)。并且10只有自己的右孩子14,它要成为8的右孩子,所以托孤的时候需要判断。

/*Node* pminRight = nullptr;
Node * minRight = cur->_right;
while(minRight->_left)
{
    pminRight = minRight;
    minRight = minRight->left;
}

    cur ->_key = minRight ->_key;
    pminRight ->left = minRight->_right;

    delete minRight;
 */ 


Node * pminRight = cur;
Node * minRight = cur->_right;
while(minRight->_left)
{
    pminRight = minRight;
    minRight = minRight ->left;
}

    cur ->_key = minRight->_key;
    //进行托孤
    if(pminRight->left == minRight->right)
        pminRight->left = minRight->right;
    else if(pminRight->right == minRight->left)
        pminRight->right = minRight->right;

    delete minRight;

                                          

2.3.2递归删除法

bool _EraseR(Node*& root, const K& key)
{
	if (root == nullptr)
	 return false;

	if (root->_key < key)
	{
		return _EraseR(root->_right, key);
	}
	else if (root->_key > key)
	{
		return _EraseR(root->_left, key);
	}
	else
	{
		Node* del = root;

		// 开始准备删除
		if (root->_right == nullptr)
		{
		    root = root->_left;
		}
		else if (root->_left == nullptr)
		{
			root = root->_right;
		}
		else
        {
            //这里找左树的最右节点
            Node * maxleft = root->_left;
            while(maxleft->_right)
            {
                maxleft = maxleft ->right;
            }

 
            swap(root->_key, maxleft->_key);
            return _EraseR(root->_left,key);
          }
        delete del;
        return true;
}

三、二叉搜索树应用

1.K模型(解决在不在的问题)

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

  • 比如门禁系统:芯片中有个人信息,根据个人信息在数据库中查找,如果在,就通过。

2.KV模型

每一个关键码key,都有与之对应的值value,即<key,value>的键值对,该方式在生活中非常常见

  • 比如英汉词典就是中文与英文的对应关系,通过英文可以快速找到与之对应的中文,英文单词与其对应的中文<word,chinese>构成一种键值对。
  • 统计单词次数,统计结束,给定单词就可以快速找到出现的次数。<key,count>构成一种键值对。                
  • //改造二叉搜索树为kv结构
    template<class k, class v>
    struct BSTNode
    {
        BSTNode(const k& key = k(), const v&value = v())
        :_pLeft(nullptr),_pRight(nullptr),_key(key),_value(vaule)
        {}
    
        BSTNode<T> * _pleft;
        BSTNode<T> * _pright;
        k _key;
        v _value;
    };
    
    template<class k, class v>
    class BSTree
    {
        typedef BSTNode<k,v> Node;
        typedef Node* pNode;
        public:
            BSTree(): _pRoot(nullptr){}
            pNode Find(const K& key);
            bool Insert(const k& key ,const v& value);
            bool Erase(const k& key);
        private:
            pNode _pRoot;
    }
    
    void TestBSTree()
    {
     // 输入单词,查找单词对应的中文翻译
     BSTree<string, string> dict;
     dict.Insert("string", "字符串");
     dict.Insert("tree", "树");
     dict.Insert("left", "左边、剩余");
     dict.Insert("right", "右边");
     dict.Insert("sort", "排序");
     // 插入词库中所有单词
     string str;
     while (cin>>str)
     {
     BSTreeNode<string, string>* ret = dict.Find(str);
     if (ret == nullptr)
     {
     cout << "单词拼写错误,词库中没有这个单词:" <<str <<endl;
     }
     else
     {
     cout << str << "中文翻译:" << ret->_value << endl;
     }
     }
    }
    void TestBSTree()
    {
     // 统计水果出现的次数
     string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", 
    "苹果", "香蕉", "苹果", "香蕉" };
     BSTree<string, int> countTree;
     for (const auto& str : arr)
     {
     // 先查找水果在不在搜索树中
     // 1、不在,说明水果第一次出现,则插入<水果, 1>
     // 2、在,则查找到的节点中水果对应的次数++
     //BSTreeNode<string, int>* ret = countTree.Find(str);
     auto ret = countTree.Find(str);
     if (ret == NULL)
     {
     countTree.Insert(str, 1);
     }
     else
     {
     ret->_value++;
     }
     }
     countTree.InOrder();
    }
                                                              

3.二叉搜索树性能分析

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

对于有n个节点的二叉搜索树,若每个元素查找概率相等,查找查毒是二叉树的深度函数,节点越多,比较次数越多。

最优:完全二叉树,O(logN)

最差:单支O(N)

如果退化为单支,二叉搜索树性能很差,所以使用AVL树和红黑树,后续文章继续介绍。


总结

本文主要介绍了二叉搜索树实现以及应用,技术有限,如有错误请指正。

猜你喜欢

转载自blog.csdn.net/jolly0514/article/details/132153682