【数据结构】二叉搜索树——高阶数据结构的敲门砖

简介: 【数据结构】二叉搜索树——高阶数据结构的敲门砖

树概述

——在计算机中是一种很常见的数据结构

树是一种很强大的数据结构,数据库,linux操作系统管理和windows操作系统管理所有文件的结构就是一颗树形结构

每个树有且只有一个根节点——根节点个数只有一个,节点可存储数据。

根节点往下可以有多个叶子节点,每个叶子节点也可以有多个叶子节点

如下图

每一个节点都可以是父亲节点,每个父亲节点的叶子节点都是孩子节点

根节点的深度为0,最后一层节点的深度为lgN

如果每一层深度的孩子节点的个数小于等于2,便是二叉树

左边的节点称为左孩子节点,右边的节点称为右孩子节点

如下图

如何正确的看一颗二叉树呢?

3是根节点,6作为3的左孩子也可以是一颗子树,这颗树是以6为根节点的树,也是3的左子树。以22为根节点来作为3的右子树也是一样的。

最小的子树是左孩子和右孩子都为空的叶子节点

也就是说每一个节点都可以作为根节点,从而成为父亲节点的子树,子树又可以拆成子树,直到左右都为空的叶子节点为止(叶子节点也可以看作一颗子树)。

遍历一颗树应该用递归的思维,如下是遍历顺序

二叉树前序遍历:根 ——> 左子树——>右子树

二叉树中序遍历:左子树——>根——>右子树

二叉树后序遍历:左子树——>右子树——>根


二叉搜索树概述

概念

二叉搜索树是一种二叉树结构,二叉搜索树存储数据时需要符合如下规则(搜索树是支持泛型的,为了方便理解,小编以int类型为例):

左孩子节点的值 < 父亲节点的值 < 右孩子节点的值

如下示例:

特性

对于所有数据而言,

从整棵树的根节点开始,不断地找左子树,没有左孩子的左子树存的值最小

从整棵树的根节点开始,不断地找右子树,没有右孩子的右子树存的值最大

如果整棵树走中序遍历则是升序

那么用搜索二叉树找一个节点的效率如何呢?

查找的规则很简单,比根节点的值大往右子树走,比根节点的值小往左子树走。每查找一次就能“砍”掉一半数据,只需要查找 次。类似于二分查找。

上述查找条件是:整棵树基本符合二叉树结构。如果是比较极端的结构,效率会接近 。比如下述结构

二叉搜索树搜索的效率的上限很高,下限很低

元素操作

插入

二叉搜索树中不允许有相等的值存在,因为要维持 “左树的值小于根小于右数的值” 这一特性。

如果要插入的值在树中不存在,这个值会插入到中间某个位置吗?

答案是不会,因为这个值一定会从整棵树的根节点比到叶子节点,然后插入到叶子节点的后面。如下示意图:

删除

删除的情况比较复杂,大致可以分为两种情况

孩子节点的个数是1或0

假设被删节点为cur,被删节点的父亲节点为fatherNode

先找是否有cur,找不到,删除失败。

找到了,判断curfatherNode的左孩子还是右孩子

是左孩子,让fatherNode的左指针指向cur的孩子节点

是右孩子,让fatherNode的右指针指向cur的孩子节点

如果没有孩子节点,则fatherNode的指针指向空

改变指向关系后整棵树就没有cur节点了,最后释放cur的内存

fatherNode的左孩子被删应该有新的左孩子接替,右孩子应该有新的右孩子接替,这样才不会破坏二叉搜索树的结构,所以要判断curfatherNode的左孩子还是右孩子

孩子节点的个数是2

孩子节点是1或0的时候只需改变指向关系,然后再释放被删节点的内存即可。

那么被删节点的孩子个数是两个的时候还能这么做吗?

显然是不能的,因为一个父亲节点最多只能有两个孩子节点

删除孩子节点的个数是2的节点,最核心的思想是:

被删节点为cur但不直接删cur这个节点,因为这样会破坏这颗树的结构。

一个孩子节点为1或0的节点为leftMax_Node,让leftMax_Node节点的值和cur节点的交换,然后删leftMax_Node节点

通过替换的方法就完成了cur节点的删除

那么怎么找孩子节点为1或0的节点呢?

左子树的最大节点右子树的最小节点

注意:是被删节点cur的左树(右树),而不是整棵树的左树(右树)

在上文二叉树的特性中已经提过,找最大值或最小值,找的一定是叶子节点

整个二叉搜索树中,左子树存小值,右子树存大值。

如果选左子树的最大值放在根节点的位置,依旧符合左子树的值小于根小于右子树这一规则,删掉该节点不会破坏二叉树的结构。

左子树的最大节点或右子树的最小节点是完美的  “替罪羊

框架

节点的设计

有两个指针指向左右孩子

在用一个变量存值

template <class K>
class BSNode
{
public:
 
  BSNode(const K& Key = K())
    :_left(nullptr)
    ,_right(nullptr) 
    ,_key(Key)  
  {
  }
 
  BSNode<K>* _left;
  BSNode<K>* _right; 
  K _key;
 
};

二叉搜索树的设计

只需要一个节点指针指向整棵树的根节点即可

template <class K>
class BSTree
{
  typedef BSNode<K> node;
 
//......
 
private:
 
  node* _root;  
};

查找

查找的代码很简单,小编就不细讲了。

有一点要提醒大家,大家一定要用判断语句把查找条件分开,比根大就往右树查找,比根小就往左树查找。千万不要写成暴力查找——不管值的大小,遍历整棵树

代码如下

bool _FindNode(node* root, const K& key) 
{
  while (root)
  {
    if (root->_key < key) 
    {
      root = root->_right;
    }
    else if (root->_key > key)
    {
      root = root->_left;
    }
    else
    {
      return false;
    }
 
    return true;
  }
}

可以把上述代码设计成私有,再封装一层共有的函数,这样做的意义是不用传节点的指针了

bool FindNode(const K& key)
{
  return _FindNode(_root, key);           
}

插入

接口

bool Insert(const K& key)

首先应该判断整棵树是否为空,如果为空,直接让_root直接指向新节点

if (_root == nullptr) //如果为空
{
  _root = new node(key); //头节点指针指向新节点
  return true;
}

二叉搜索树是不允许有重复的值出现的

应该再写一遍查找的逻辑,因为_root是指向整棵树的根的,所以需要定义一个临时指针,当我们找到相等的值时,插入失败。

node* cur = _root;  
node* fatherNode = nullptr; 
 
while (cur) 
{
  if (cur->_key < key) //插入的元素比当前节点的值大,
  {
    fatherNode = cur; //记录父亲节点的指针
    cur = cur->_right;   //往右树找
  }
  else if (cur->_key > key)
  {
    fatherNode = cur;
    cur = cur->_left;
  }
  else
  {
    return false;
  }
}

没找到的话,说明临时指针已经找到空了,临时指针的父亲节点就是合适的叶子节点了,插在叶子节点的孩子位置即可,是左孩子还是右孩子需要和父亲节点比较大小

  node* newNode =  new node(key);
  if (fatherNode->_key < key)//判断要插入父亲节点的左边还是右边
  {
    fatherNode->_right = newNode; 
  }
  else
  {
    fatherNode->_left = newNode; 
  }

删除

接口

bool Erase(const K& key)

先判断是否是空树

if (nullptr == _root)//如果是空树,删除失败
{
  return false;
}

和插入类似,要判断被删的节点是否存在

node* cur = _root; //临时变量,cur是指要删除的节点
node* fatherNode = nullptr; //cur的父亲节点,
 
while (cur)//找要删除的节点
{
  if (cur->_key < key) 
  {
    fatherNode = cur;
  cur = cur->_right;
  }
  else if (cur->_key > key)
  {
    fatherNode = cur;
    cur = cur->_left;
  }
  else
  {
    break;
  }
}
if (nullptr == cur) //没找到要删除的节点,删除失败
{
  return false;
}

下面就是要删除节点的逻辑了,上文已经讲了删除的原理,这里细讲代码细节

孩子节点数量为1或0

节点的孩子树不管是1还是0,可以只写左孩子为空的情况和右孩子为空的情况,左右孩子都为空的情况可以不写。

因为左右孩子都为空的情况,可以走左孩子为空的逻辑,只是指向新节点还是指向空的问题。

  if (cur->_left == nullptr) //要删除的节点左为空
  {
    if (cur == _root)  //极端情况,
    {
      _root = cur->_right;
      delete cur;
      return true;
    }
 
    if (fatherNode->_left == cur)  //判断要删除的节点在父亲节点的左边还是右边
      fatherNode->_left = cur->_right;
    else                                   //改变指向
      fatherNode->_right = cur->_right;
 
    delete cur; //删除
    return true;
 
  }
  else if (cur->_right == nullptr)//要删除的节点右为空
  {
    if (cur == _root) //极端情况
    {
      _root = cur->_left;  
      delete cur; 
      return true;
    }
 
    if (fatherNode->_left == cur)  //判断要删除的节点在父亲节点的左边还是右边
      fatherNode->_left = cur->_left; 
    else                               //改变指向
      fatherNode->_right = cur->_left; 
 
    delete cur; //删除节点
    return true;
  }

极端情况做了特殊处理

极端情况如下图所示

孩子节点数量为2

else //要删除的节点左右都不为空
{
  node* leftMax_Node = cur->_left;  //左树最大节点 (被删节点的左树)
  node* leftMax_FatherNode = cur; //左树最大节点的父亲节点             
 
  while (leftMax_Node->_right)      //找代替节点,左树的最右节点(找左树的最大值)
  {
    leftMax_FatherNode = leftMax_Node; 
 
    leftMax_Node = leftMax_Node->_right;     
  }
 
  std::swap(cur->_key, leftMax_Node->_key);   //交换完之后,左树的最大节点就是要删的值了  
 
  if (leftMax_FatherNode->_left == leftMax_Node)  //要判断被删节点是父亲节点的左孩子还是右孩子
    leftMax_FatherNode->_left = leftMax_Node->_left;
  else
    leftMax_FatherNode->_right = leftMax_Node->_left;
 
  delete leftMax_Node;    
  return true;
}
相关文章
|
3月前
|
存储 Java Serverless
【数据结构】哈希表&二叉搜索树详解
本文详细介绍了二叉搜索树和哈希表这两种数据结构。二叉搜索树是一种特殊二叉树,具有左子树节点值小于根节点、右子树节点值大于根节点的特点,并且不允许键值重复。文章给出了插入、删除和搜索等方法的具体实现。哈希表则通过哈希函数将键名映射为数组下标,实现快速查找,其插入、删除和查找操作时间复杂度理想情况下为O(1)。文中还讨论了哈希函数的设计原则、哈希冲突的解决方法及哈希表的实现细节。
68 8
【数据结构】哈希表&二叉搜索树详解
|
2月前
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现(三)
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现
|
2月前
|
存储
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现(二)
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现
|
2月前
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现(一)
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现
|
2月前
【数据结构】二叉搜索树的功能实现详解
【数据结构】二叉搜索树的功能实现详解
35 0
|
6月前
数据结构学习记录——判断是否为同一颗二叉搜索树(题意理解、求解思路、程序搭建框架、具体函数的实现)
数据结构学习记录——判断是否为同一颗二叉搜索树(题意理解、求解思路、程序搭建框架、具体函数的实现)
64 2
|
6月前
|
机器学习/深度学习
数据结构学习记录——堆的小习题(对由同样的n个整数构成的二叉搜索树(查找树)和最小堆,下面哪个说法是不正确的)
数据结构学习记录——堆的小习题(对由同样的n个整数构成的二叉搜索树(查找树)和最小堆,下面哪个说法是不正确的)
48 1
|
5月前
|
存储 算法 C++
【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧
【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧
73 0
|
5月前
|
存储
【数据结构】AVL树——平衡二叉搜索树
【数据结构】AVL树——平衡二叉搜索树
|
6月前
|
算法
数据结构和算法学习记录——小习题-二叉树的遍历&二叉搜索树
数据结构和算法学习记录——小习题-二叉树的遍历&二叉搜索树
41 0