最近写项目写得让人有点烦躁,于是找了点新鲜的东西搞——二叉查找树(BST),来提提兴趣,废话不多说,现在就让我们进入BST的世界吧!
1. 定义
二叉查找树(Binary Search Tree),又称二叉排序树(Binary Sort Tree),亦称二叉搜索树。二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
若左子树不空,则左子树上所有结点的值均小于或等于它的根结点的值;
若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
- 左、右子树也分别为二叉排序树;
比如:
其实说了这么多,不就是左边的节点比根节点小,右边的节点比根节点大嘛。哼~(当然还有相等的情况)
2. BST的来源
数组和链表以及BST在使用数据时的优缺点:
数据结构 | 优点 | 缺点 |
---|---|---|
数组 | 直接使用下标查找 | 不便于删除或者插入 |
链表 | 删除或者插入方便 | 不便于查找 |
BST | 兼顾具有两者的优点 | 。。。 |
可见,BST相比于其他数据结构的优势在于查找、插入的时间复杂度较低。后面会分析到。
3. 用途:用于查找和排序一段序列
基于BST的数据组织形式,我们能看出来的是:它的中序遍历序列是升序序列
在现实生活中,用的最多的是二叉平衡树,有种特殊的二叉平衡树就是红黑树,Java集合中的TreeSet和TreeMap,C++STL中的set,map以及LInux虚拟内存的管理,都是通过红黑树去实现的,还有哈弗曼树编码方面的应用,以及B-Tree,B+-Tree在文件系统中的应用
4. 时间复杂度的分析
- 最好情况,二叉查找树同时也是一棵完全二叉树,时间复杂度为
- 最差情况,输入的数据正好是升序或降序序列,此时二叉查找树退化成单链表,时间复杂度变为
- 最好情况,二叉查找树同时也是一棵完全二叉树,时间复杂度为
- 平均情况,时间复杂度为
5.代码实现与分析:
基于BST实现的数据结构,我们得到它的感性思路就是 ”小的向左,大的向右“
(1)数据组织形式:即 myhead.h 文件
int test = 0; //用来测试destory()函数
template <class T>
class Node
{
public:
int key = 0;
Node *left = nullptr;
Node *right = nullptr;
Node(int key_t ):key(key_t),left(nullptr),right(nullptr){ }
};
template <typename T>
class BST
{
private:
Node<T> *header; //header结点并非根结点,header->left指向的才是根结点。
Node<T> *insert_real(int key, Node<T> *&node);
Node<T> *&find_real(int key, Node<T> *&node);
void in_order(Node<T> *node);
int destory(Node<T> *p);
public:
BST();
~BST();
Node<T> *insert(int key);
// (递归实现)查找"二叉树"中键值为key的节点
Node<T> *&find(int key);
//(非递归实现)查找"二叉树"中键值为key的节点
Node<T> *loop_find(T key) ;
// 查找最小结点:返回最小结点的键值。
T minimum();
// 查找最大结点:返回最大结点的键值。
// T maximum(); 思路同上
void erase(int key);
void print();
};
需要注意的是”header结点并非根结点,header->left指向的才是根结点。“。基本思路参考自网上,后面会发出链接地址。其实这种思路在我去实现线索二叉树的时候就遇到过,原作者说是实现上的一种技巧,不知道是不是在实现任何树时的一种“方法”,就像是带头节点的链表一样,用以程序员方便书写代码。
(2) 插入与查找很简单,就不讲了,看代码就行。我们主要說一下删除操作
(3) 删除:
节点的删除有4种情况,如下:
对于图二,图三,图四这三种情况,我们采取的实现方法是:假设最高层的节点是p,要删除的节点是x,要删除的节点的子节点是y,那么
图2 p->right = x->left ; delete x ;
图3 p->right = x->right ; delete x
图4 p->right = nullptr ; delete x;
写为一句话就是:
p->right = x->left ? x->left :x->right ;
(如果要删除的节点 x 在左边,思路相同)
而对于图1 ,我们找到要删除结点(在中序遍历中)的后继,用后继替换要删除的结点(当然用前驱去替换也是可以的)。详情见下图:
步骤就是:
1. 找到 x ,y
2. 改变 y 的指向
3. 将 x 给 t
4. 改变 x 的 left 和 right
template <typename T>
void BST<T>::erase(int key)
{
Node<T> *&p = find_real(key, header->left);
if (p)
{
Node<T> *t = p;
if (t->left && t->right)
{
/* 左右都有的情况*/
/* 找到 x ,y */
Node<T> *y = t;
Node<T> *x = t->right;
while (x->left)
{
y = x;
x = x->left;
}
/*改变 y 的指向*/
if (y == t) // y 与 t 重合的情况
y->right = x->right;
else
y->left = x->right;
/* 将 x 给 t */
p = x ;
/* 改变 x 的 left 和 right */
x->left = t->left;
x->right = t->right;
}
else
{
p = t->left ? t->left : t->right;
}
delete t;
}
else
{
cout << key << "不存在二叉查找树中 !!!" << endl;
}
return;
}
(4) 完整代码:BST.cpp 文件
#include <iostream>
#include "myhead.h"
using namespace std;
/* 线索二叉树也是在树的头节点上面补上一个指引节点之后就变得简单了, 可能就跟链表带头接点一样,会简化大部分的树的问题,需要总结哦 */
template <typename T>
BST<T>::BST()
{
header = new Node<T>(-100);
}
template <typename T>
BST<T>::~BST()
{
destory(header);
}
template <typename T>
int BST<T>::destory(Node<T> *p)
{
if (p == nullptr)
return 0;
test++;
cout << "test == " << test << endl; //顺带吧多申请的那个节点也释放了
destory(p->left); //注意先后次序,如果先把p销毁,那么就会找不到p->left,p->right
destory(p->right);
delete p;
p = nullptr;
}
template <typename T>
Node<T> *BST<T>::insert(int key)
{
return insert_real(key, header->left);
}
template <typename T>
Node<T> *BST<T>::insert_real(int key, Node<T> *&node)
{
if (node == nullptr)
return node = new Node<T>(key);
if (key <= node->key)
insert_real(key, node->left);
else if (key > node->key)
insert_real(key, node->right);
else
return nullptr;
}
template <typename T>
void BST<T>::print()
{
in_order(header->left);
}
template <typename T>
void BST<T>::in_order(Node<T> *root)
{
if (root == nullptr)
return;
in_order(root->left);
cout << root->key << " ";
in_order(root->right);
}
template <typename T>
Node<T> *&BST<T>::find(int key)
{
return find_real(key, header->left);
}
template <typename T>
Node<T> *BST<T>::loop_find(T key)
{
Node<T> *p = header->left; // p 指向根节点
while (p->key != key)
{
if (key <= p->key)
p = p->left;
else
p = p->right;
}
return p;
}
template <typename T>
Node<T> *&BST<T>::find_real(int key, Node<T> *&node)
{
if (node == nullptr)
return node;
if (key < node->key)
find_real(key, node->left);
else if (key > node->key)
find_real(key, node->right);
else // 只剩下相等了
return node;
}
template <typename T>
void BST<T>::erase(int key)
{
Node<T> *&p = find_real(key, header->left);
if (p)
{
Node<T> *t = p;
if (t->left && t->right)
{
/* 左右都有的情况*/
/* 找到 x ,y */
Node<T> *y = t;
Node<T> *x = t->right;
while (x->left)
{
y = x;
x = x->left;
}
/*改变 y 的指向*/
if (y == t) // y 与 t 重合的情况
y->right = x->right;
else
y->left = x->right;
/* 将 x 给 t */
p = x;
/* 改变 x 的 left 和 right */
x->left = t->left;
x->right = t->right;
}
else
{
p = t->left ? t->left : t->right;
}
delete t;
}
else
{
cout << key << "不存在二叉查找树中 !!!" << endl;
}
return;
}
template <class T>
T BST<T>::minimum()
{
Node<T> *p = header->left;
if (p)
{
while (p->left)
{
p = p->left;
}
return p->key;
}
else
{
cout << "这是一个空的二叉查找树 !!" << endl;
return T(-100);
}
}
int main(void)
{
BST<int> bst;
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(7);
bst.insert(16);
bst.insert(15);
bst.insert(19);
bst.insert(14);
bst.insert(10);
bst.insert(11);
bst.insert(8); //测试重复数字
bst.print(); //中序遍历
cout << endl;
// test "find"
Node<int> *p = nullptr;
cout << ((p = bst.find(9)) ? p->key : -1) << endl; // 9
cout << ((p = bst.find(100)) ? p->key : -666) << endl; // -666
cout << ((p = bst.loop_find(14)) ? p->key : -1) << endl; // 14
cout << "min number == " << bst.minimum() << endl; // 3
// test "erase"
bst.erase(16); // 测试 y 与 t 重合的情况
bst.print();
cout << endl;
bst.erase(9);
bst.print();
cout << endl;
bst.erase(1000);
bst.erase(7);
bst.print();
cout << endl;
return 0;
}
基于上图的测试结果:
因为我们只剩8个节点,而又多给了一个节点,所以 test 最终等于 9
6. 如何提升查找效率:
维持BST的平衡,不让其处于”单链表“的形态,后面讲到红黑树时会再次提到。
7.附录:
1. 完全二叉树:
若设二叉树的深度为 h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
下面是完全二叉树的基本形态:
完全二叉树的性质:
1) 深度为k的完全二叉树,至少有2^(k-1)个节点,至多有2^k-1个节点。
2) 树高 h=log2(n) + 1。(n 是节点 )
参考链接:https://subetter.com/articles/2018/05/binary-search-tree.html