二叉搜索树实现

树的导览

树由节点(nodes)和边(edges)构成,如下图所示。整棵树有一个最上端节点,称为根节点(root)。每个节点可以拥有具有方向的边(directed edges),用来和其他节点相连。相连的节点之中,在上者称为父节点(parent),在下者称为子节点(child)。

一些基本概念:

  • 节点的度:一个节点含有的子树的个数,如果最多只允许两个子节点,即为二叉树
  • 叶节点:无子节点,即度为 0 的节点称为叶节点
  • 兄弟节点:不同的节点拥有相同的父节点
  • 路径长度:根节点到任何节点之间有唯一路径,路径所经过的边数
  • 节点的深度:根节点到任一节点的路径长度,根节点的深度永远为 0
  • 节点的高度:某节点到其叶节点的路径长度

树的导览

二叉搜索树的概念

二叉搜索树可以提供对数时间的元素插入和访问,是一种特殊的二叉树,具有以下规则:

  • 任何节点的键值一定大于其左子树中的每一个节点的键值
  • 任何节点的键值一定小于其右子树中的每一个节点的键值

因此二叉搜索树的最左节点是最小元素,最右节点是最大元素。

二叉搜索树

二叉搜索树的实现

节点的定义

该节点设计很简单:

  • 包含三个成员变量:节点值、左孩子指针、右孩子指针
  • 构造函数:将成员变量初始化
/// @brief 二叉树的节点
/// @tparam K 节点值的类型
template<class K>
struct BSTreeNode {
  BSTreeNode(const K& key = K()) 
      : _key(key), 
        _left(nullptr), 
        _right(nullptr) {}
        
  K _key;					       // 节点值
  BSTreeNode<K>* _left;	 // 左指针
  BSTreeNode<K>* _right; // 右指针
};

接口总览

文章完整代码:BinarySearchTree · 秦国维/data-structure

template<class K>
class BSTree {
  typedef BSTreeNode<K> Node;
 public:
  Node* Find(const K& key);
  Node* _FindR(Node* _root, const K& key);
  Node* FindR(const K& key);
  
  bool Insert(const K& key);
  bool _InsertR(Node*& _root, const K& key);
  bool InsertR(const K& key);
  
  bool Erase(const K& key);
  bool _EraseR(Node*& root, const K& key);
  bool EraseR(const K& key);
 private:
  Node* _root = nullptr;
};

查找

根据二叉搜索树的特性,可以在二叉搜索树快速的找到指定值:

  • 若 key 值大于当前节点的值,在当前节点的右子树中查找
  • 若 key 值小于当前节点的值,在当前节点的左子树中查找
  • 若 key 值等于当前节点的值,返回当前节点的地址
  • 若找到空,查找失败,返回空指针

非递归

/// @brief 查找指定 key 值
/// @param key 要查找的 key
/// @return 找到返回节点的指针,没找到返回空指针
Node* Find(const K& key) {
  Node* cur = _root;
  while (cur != nullptr) {
    // key 值与当前节点值比较
    if (key > cur->_key) {
      cur = cur->_right;
    } else if (key < cur->_key) {
      cur = cur->_left;
    } else {
      return cur;
    }
  }
  return nullptr;
}

递归

Node* _FindR(Node* root, const K& key) {
  if (root == nullptr) {
    return nullptr;
  }
  
  // key 值与当前节点值比较
  if (key > root->_key) {
    // key 值大于当前节点的值,递归到右子树查找
    return _FindR(root->_right, key);
  } else if (key < root->_key) {
    // key 值小于当前节点的值,递归到左子树查找
    return _FindR(root->_left, key);
  } else {
    return root;
  }
}

Node* FindR(const K& key) {
  return _FindR(_root, key);
}

插入

根据二叉搜索树的性质,插入操作也很简单:

  • 如果是空树,将插入的节点作为根节点
  • 如果不是空树,利用性质找到该插入的位置,将节点插入

插入新元素时,从根节点开始,遇到键值较大的就向左,遇到键值较小的就向右,一直到尾端,即为插入点。

二叉树插入

非递归

使用非递归插入函数时,需要定义一个 parent 指针,该指针用来指示插入节点的父节点,以便将新节点插入。

/// @brief 在二叉搜索树中插入指定节点
/// @param key 节点的 key 值
/// @return 成功返回 true,失败返回 false
bool Insert(const K& key) {
  if (_root == nullptr) {
    // 第一个插入的节点,构建为根
    _root = new Node(key);
    return true;
  }
  
  // 先找到要插入的位置
  Node* parent = nullptr;
  Node* cur = _root;
  while (cur != nullptr) {
    if (key > cur->_key) {
      parent = cur;
      cur = cur->_right;
    } else if (key < cur->_key) {
      parent = cur;
      cur = cur->_left;
    } else {
      // 已经有该值了,插入失败
      return false;
    }
  }
  
  // 创建要插入的节点
  cur = new Node(key);
  // 看插入节点在父节点哪边
  if (key > parent->_key) {
    parent->_right = cur;
  } else {
    parent->_left = cur;
  }
  return true;
}

递归

递归版本的插入相对于非递归版本更简单,要注意的是 Node* 参数一定要传引用,这样才能改变父亲指针的指向。

bool _InsertR(Node*& _root, const K& key) {
  if (_root == nullptr) {
    // 因为传递的是引用,所以可以直接改变指向
    _root = new Node(key);
    return true;
  }
  
  // 判断 key 与当前节点值大小关系
  if (key > _root->_key) {
    // key 更大就递归到右子树插入
    return _InsertR(_root->_right, key);
  } else if (key < _root->_key) {
    // key 更小就递归到左子树插入
    return _InsertR(_root->_left, key);
  } else {
    return false;
  }
  return false;	// 为了消除编译警告
}

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

删除

删除相对与插入就复杂的多了,需要考虑三种情况:

  • 待删除节点没有子树
  • 待删除节点有一个子树
  • 待删除节点有两个子树

下面就来讨论这三种情况:

待删除节点没有子树

这种情况比较简单,直接将指向该节点的指针置空即可。

待删除节点有一个子树

如果节点 Q 只有一个子节点,就直接将 Q 的子节点连至 Q 的父节点,并将 Q 删除。

删除1

待删除节点有两个子树

这时就比较复杂了,需要使用替换法。如果 Q 有两个节点,就以右子树的最小节点取代 Q(左子树的最大节点也可以)。

右子树最小节点获取方法:从右子节点开始,一直向左找到底。

为什么右子树最小节点可以替换当前节点?

因为右子树最小节点一定大于当前节点左子树中的所有节点,又一定小于右子树中的其他节点,故不会破坏二叉搜索树的规则。

删除2

非递归

该实现版本同样需要定义一个 parent 指针,以便将其孩子托付给父亲。

/// @brief 在二叉搜索树中删除指针节点
/// @param key 删除节点的 key
/// @return 删除成功返回 true,失败返回 false
bool Erase(const K& key) {
  Node* parent = nullptr;
  Node* cur = _root;
  // 先找要删除节点的位置
  while (cur != nullptr) {
    if (key > cur->_key) {
      // 大于就到右子树中找
      parent = cur;
      cur = cur->_right;
    } else if (key < cur->_key) {
      // 小于就到左子树中找
      parent = cur;
      cur = cur->_left;
    } else {
      // 找到要删除的节点了
      if (cur->_left == nullptr) {
        // cur 左孩子为空,即只有一个右孩子或没有孩子
        if (parent == nullptr) {
          // 如果要删除的是根节点,更换新根
          _root = _root->_right;
        } else {
          if (parent->_left == cur) {
            parent->_left = cur->_right;
          } else {
            parent->_right = cur->_right;
          }
        }
        
        // 释放掉 cur 指向的空间
        delete cur;
        cur = nullptr;
      } else if (cur->_right == nullptr) {
        // 此时说明只有左孩子
        if (parent == nullptr) {
          // 如果要删除的是根节点,更换新根
          _root = _root->_left;
        } else {
          if (parent->_left == cur) {
            parent->_left = cur->_left;
          } else {
            parent->_right = cur->_left;
          }
        }
      
        // 释放掉 cur 指向的空间
        delete cur;
        cur = nullptr;
      } else {
        // 此时有左右孩子,需要使用替换法删除
        Node* minParent = cur;
        Node* min = cur->_right;
        // 先找到右子树的最左节点
        while (min->_left != nullptr) {
          minParent = min;
          min = min->_left;
        }
      
        // 将最小节点的值替换到 cur 上
        cur->_key = min->_key;
        if (minParent->_left == min) {
          // 最小节点位于父节点的左边,将它的右子树托付给父节点
          minParent->_left = min->_right;
        } else {
          // 最小节点位于父节点的右边,此时是 cur 的右子树没有左子树
          minParent->_right = min->_right;
        }
        
        delete min;
        min = nullptr;
      } // if (cur->_left == nullptr)
      return true;
    } // if (key > cur->_key)
  } // while (cur != nullptr)
  // 没找到要删除的节点,返回 false
  return false;
}

递归

递归版本的删除相对于非递归版本更简单,要注意的是 Node* 参数一定要传引用,这样才能改变父亲指针的指向,将节点删除。

bool _EraseR(Node*& root, const K& key) {
  if (root == nullptr) {
    // 没找到要删除的节点返回 false
    return false;
  }
  
  if (key > root->_key) {
    // key 更大就到右子树中删除
    _EraseR(root->_right, key);
  } else if (key < root->_key) {
    // key 更小就到左子树中删除
    _EraseR(root->_left, key);
  } else {
    Node* del = root;
    if (root->_left == nullptr) {
      // 此时左为空或左右为空,只需要将右子树给父节点即可
      // 左右为空时,右子树为空,不会违反规则
      root = root->_right;
    } else if (root->_right == nullptr) {
      // 此时右为空,只需要将左子树给父节点
      root = root->_left;
    } else {
      // 有两个孩子,需要替换法删除
      Node* min = root->_right;
      while (min->_left != nullptr) {
        min = min->_left;
      }
    
      // 把最小的节点的值换上
      root->_key = min->_key;
      // 递归到右子树,将 minnode 删掉
      // 一定要 return,否则会重复释放
      return _EraseR(root->_right, min->_key);
    } // if (root->_left == nullptr)
  
    delete del;
    del = nullptr;
    return true;
  } // if (key > root->_key)
  return false;	// 为了消除编译警告
}

bool EraseR(const K& key) {
  return _EraseR(_root, key);
}
  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值