二叉搜索树 [part 1]
●二分查找法
●二叉搜索树
●插入新节点(递归实现)
●查找
●前中后序遍历
●层序遍历(广度优先遍历)
●删除节点
1 二分查找法
二分查找法只能作用在一个有序的序列中,时间复杂度为O(logn)
/*
非递归形式实现二分查找
在有序数组arr中,查找target
如果找到target,返回相应的索引index,如果没有找到target,返回-1
*/
template <typename Key, typename Value>
int binarySearch(T arr[], int n, T target){
int l = 0, r = n - 1;//在[l, r]左闭右闭区间之中查找target,注意:r = n - 1
while(l <= r){//注意:循环终止条件为l<=r,而不是<
int mid = l + (r - l)/2;//避免l+r整型溢出问题
if(arr[mid] == target)
return mid;
if(arr[mid] < target)
l = mid + 1;
else
r = mid - 1;
}
return -1;//target不在数组中
}
//递归形式实现二分查找
template <typename Key, typename Value>
int __binarySearch(T arr[], int l, int r, T target){//和非递归形式不一样,这里直接传入定义好的l和r
if(l > r)//和非递归中的循环终止条件含义相同(一个是l <= r时继续, 另一个是l > r时终止 )
return -1;
int mid = l + (r-l)/2;
if(arr[mid] == target)
return mid;
if(arr[mid] > target)
__binarySearch(arr, 0, mid-1, target);
else
__binarySearch(arr, mid+1, r, target);
}
template<typename T>
int binarySearch2(T arr[], int n, T target){
//写成两个函数是为了解决非递归中存在的索引从0到n-1的问题
return __binarySearch(arr, 0, n-1, target);
}
利用100W量级数据进行测试,得到两种方法的时间差距为:
2 二叉搜索树
2.1 优势
“查找表”(字典)是一种将数据以“键值对”的形式存在的数据结构,通常使用二叉搜索树实现
如果使用数组实现查找表,会存在以下缺点:
1、必须使用整数来表示key值
2、当key值较为稀疏时,使用该数组在空间上不经济
不同数据结构实现查找表的时间复杂度对比:
查找元素 | 插入元素 | 删除元素 | |
普通数组 | O(n) | O(n) | O(n) |
顺序数组 | O(logn) | O(n) | O(n) |
二分搜索树 | O(logn) | O(logn) | O(logn) |
(对于顺序数组的查找,可以使用二分查找法,在O(logn)的时间复杂度内完成)
二叉搜索树实现查找表的优点:
1、在查找、插入、删除数据上的高效
2、可以方便地回答很多数据之间的关系问题,例如:min、max、floor(前驱)、ceil(后继)、rank(排名)、select(选择排名第X位的数据)
2.2 定义
1、二叉搜索树首先是一棵二叉树
2、每个节点的键值都大于左孩子,每个节点的键值都小于右孩子
3、以左右孩子为根的子树仍然是一棵二叉搜索树
注意:二叉搜索树不一定是一棵完全二叉树
2.3 初始化
template <typename Key, typename Value>
class BST{
private:
struct Node{
Key key;
Value value;
Node *left;
Node *right;
Node(Key key, Value value){//结构体的构造函数
this->key = key;
this->value = value;
this->left = this->right = NULL;//左右孩子初始化为空
}
};
Node *root;//根节点
int count;//二叉搜索树的节点数
public:
BST(){//构造函数
root = NULL;
count = 0;
}
~BST(){//析构函数
//TODO: ~BST()
}
int size(){//返回二叉搜索树的节点数
return count;
}
bool isEmpty(){//判断二叉搜索树是否为空
return count == 0;
}
};
3 插入新节点(递归实现)
不断与根节点作比较,小于则递归左子树,大于则递归右子树,直到遇见空的可插入左/右子树(遇见相同值则覆盖)
template <typename Key, typename Value>
class BST{
......
public:
...
void insert(Key key, Value value){
//首先在类中定义一个public的insert()方法,用于调用private的insert()方法
root = insert(root, key, value);
}
private:
//向以node为根的二叉搜索树中插入节点(key, value),返回插入新节点后的二叉搜索树的根
//把向整棵二叉树中插入新节点利用递归转化为向子树中插入新节点
Node* insert(Node *node, Key key, Value value){
if(node == NULL){//当节点为空,传入参数key和value,创建一个新节点,总节点数加 1
count++;
return new Node(key, value);
}
if(key == node->key)//如果key相同,则覆盖value的值
node->value = value;
else if(key < node->key)//小于则递归插入左子树
node->left = insert(node->left, key, value);
else //大于则递归插入右子树
node->right = insert(node->right, key, value);
return node;
}
};
4 查找
查找操作与插入操作类似,不同点在于如果最终查找到了一个空节点,那么代表查找失败,元素不存在与该二叉搜索树中。
二叉搜索树的包含contain和查找search同质(contain:在二叉搜索树中是否包含键值为key的元素)
template <typename Key, typename Value>
class BST{
......
public:
...
bool contain(Key key){//新建一个新的public函数contain(),调用同名private递归函数contain()
return contain(root, key);
}
private:
...
//查看以node为根的二叉搜素树中是否包含键值为key的节点
bool contain(Node* node, Key key){
if(node == NULL)//找不到键值为key的节点,返回false
return false;
if(key == node->key)//找到了键值为key的节点,返回true
return true;
else if(key < node->key)
return contain(node->left, key);
else //key > node->key
return contain(node->right, key);
}
};
在写search()函数的时候,需要考虑search()函数的返回值是什么。
1、如果返回的是所查找到的节点,那么就需要将类中定义的private类型的Node结构体改为public类型,这就无法将数据结构隐藏,与封装的设计理念不符
2、如果返回的是键值key所指向的value的值,在search()函数查找不到的情况下,会出现value为空的情况。为了避免这种情况,这里可以通过先执行contain()函数,来确定键值key所指向的value的值是存在的,再进行查找操作
这里采用返回一个value*的形式,value*作为一个指针,可以存储一个空元素,如果查找失败,返回NULL,如果查找成功,返回的就是键值key指向的value的指针,用户在方法外部也可以通过该指针修改该元素
template <typename Key, typename Value>
class BST{
......
public:
...
Value* search(Key key){
return search(root, key)
}
private:
...
//在以node为根的二叉搜索树中查找key所对应的value
Value* search(Node* node, Key key){
if(node == NULL)
return NULL;
if(key == node->key)
//由于函数返回值为指向地址的指针,所以需要返回节点的value值所对应的地址
return &(node->value);
else if (key < node->key)
return search(node->left, key);
else //key > node->key
return search(node->right, key);
}
};
5 前中后序遍历
前序遍历:先访问当前节点,再依次递归访问左右子树
中序遍历:先递归访问左子树,再访问自身,再递归访问右子树
后序遍历:先递归访问左右子树,再访问自身节点
template <typename Key, typename Value>
class BST{
......
public:
...
// 前序遍历
void preOrder(){
preOrder(root);
}
// 中序遍历
void inOrder(){
inOrder(root);
}
// 后序遍历
void postOrder(){
postOrder(root);
}
private:
...
// 对以node为根的二叉搜索树进行前序遍历
void preOrder(Node* node){
if( node != NULL ){
cout<<node->key<<endl;
preOrder(node->left);
preOrder(node->right);
}
}
// 对以node为根的二叉搜索树进行中序遍历
void inOrder(Node* node){
if( node != NULL ){
inOrder(node->left);
cout<<node->key<<endl;
inOrder(node->right);
}
}
// 对以node为根的二叉搜索树进行后序遍历
void postOrder(Node* node){
if( node != NULL ){
postOrder(node->left);
postOrder(node->right);
cout<<node->key<<endl;
}
}
};
destory()函数的逻辑就是利用一次后序遍历,即先释放左右孩子节点,再释放根节点
template <typename Key, typename Value>
class BST{
......
private:
...
void destroy(Node* node){
if(node != NULL){
destroy(node->left);
destroy(node->right);
delete node;
count--;
}
}
};
6 层序遍历(广度优先遍历)
通过引入队列(FIFO)来实现广度优先遍历
template <typename Key, typename Value>
class BST{
......
public:
...
//层序遍历
void levelOrder(){
queue<Node*> q;
q.push(root);
while(!q.empty()){
Node *node = q.front();
q.pop();
cout<<node->key<<endl;
if(node->left)
q.push(node->left);
if(node->right)
q.push(node->right);
}
}
......
};
以上所有的遍历方式时间复杂度都为O(n),归并排序与快速排序本质上就是一棵二叉树的深度优先遍历过程
7 删除节点
删除操作本身很容易,难点在于删除一个节点之后如何操纵其左右孩子节点,使整棵二叉树保持二叉搜索树的性质
7.1 找到最小/最大值
首先考虑最简单的情况,删除最小/最大值。要删除最小/最大值,首先需要找到最小/最大值。通过二叉搜素树的性质可知,
沿着左孩子/右孩子不断向下寻找,直到左孩子/右孩子不存在,该节点就是整棵二叉搜素树的最小/最大值(注意:该节点不一定是叶子节点)
template <typename Key, typename Value>
class BST{
......
public:
...
//寻找最小的键值
Key minimun(){
assert(count != 0);
//使用递归的方式寻找二叉搜素树的最小值,返回值为最小值相应的Node
Node* minNode = minimun(root);
return minNode->key;
}
private:
...
//在以node为根的二叉搜索树中,返回最小键值的节点
Node* minimun(Node* node){
if(node->left == NULL)
return node;
return minimun(node->left);
}
};
7.2 删除最小/最大值
●首先要找到最小/最大节点(沿着根节点的左孩子/右孩子一路往下递归)
●如果为叶子节点,直接删除
如果为根节点,直接将其右(min)/左(max)孩子节点提为根节点
/*
需要判断当前节点的左孩子是否为空,如果为空,代表着已经找到了最小的节点,需要做的就是删除该节点并保持二叉搜索树性质
先判断右孩子是否存在,如果存在,右孩子节点就代替当前节点,成为其父亲节点新的左孩子
但是,即使右节点不存在,也可以直接将当前的右节点(也就是空节点)直接返回
表示父亲节点新的左孩子为空,所以直接返回右孩子节点就可以涵盖两种情况(右节点为空/不为空)
*/template <typename Key, typename Value>
class BST{
......
public:
...
//从二叉树中删除最小值所在的节点
void removeMin(){
if(root)//只有在根不为空时执行递归删除操作
root = removeMin(root);
}
private:
...
//删除掉以node为根的二叉搜索树的最小节点,返回删除节点后新的二叉搜素树的根
Node* removeMin(Node* node){
if(node->left == NULL){
Node* rightNode = node->right;
delete node;
count--;
return rightNode;
}
//如果上述if语句不成立,则继续递归查找左孩子
node->left = removeMin(node->left);
return node;
}
};
7.3 删除二叉搜索树中的任意节点(Hibbard Deletion)
上述删除最小值/最大值的节点对于删除只有一个左/右孩子或者同时没有左右孩子的情况,都是适用的(只有左孩子,提左孩子;只有右孩子,提右孩子)
如下图所示,删除值为58的节点,该节点没有右孩子
那么如何删除左右都有孩子的节点?
使用右子树中的最小值(根节点的后继)代替
(或左子树中的最大值(根节点的前驱))
delMin伪码:
删除左右都有孩子的节点d
找到 s = min( d->right )
s是d的后继
s->right = delMin(d->right)//删除右子树中的最小节点,并返回该节点的根,作为后继的右孩子
s->left = d->left
template <typename Key, typename Value>
class BST{
private:
struct Node{
...
Node(Node *node){//复制节点
this->key = node->key;
this->value = node->value;
this->left = node->left;
this->right = node->right;
}
};
...
public:
...
// 从二叉树中删除键值为key的节点
void remove(Key key){
root = remove(root, key);
}
private:
...
// 删除掉以node为根的二分搜索树中键值为key的节点,返回删除节点后新的二分搜索树的根
Node* remove(Node* node, Key key){
if( node == NULL )
return NULL;
if( key < node->key ){
node->left = remove( node->left , key );
return node;
}
else if( key > node->key ){
node->right = remove( node->right, key );
return node;
}
else{ // key == node->key
if( node->left == NULL ){
Node *rightNode = node->right;
delete node;
count --;
return rightNode;
}
if( node->right == NULL ){
Node *leftNode = node->left;
delete node;
count--;
return leftNode;
}
// node->left != NULL && node->right != NULL
//找到当前节点右子树的最小节点(后继),复制该节点
Node *successor = new Node(minimum(node->right));
count ++;
//删除该后继节点,返回该节点的根节点,作为之前提前复制的后继节点的右孩子
successor->right = removeMin(node->right);
successor->left = node->left;
delete node;
count --;
return successor;//返回该后继节点,作为新的根节点
}
}
};
删除二叉搜索树的任意一个节点的时间复杂度为O(logn)