小白学算法4.1——二叉查找树
标签: 小白学算法
1.什么是二叉查找树(Binary Search Tree)
二叉查找树是一种特殊的二叉树,相对于普通的二叉树,其有如下特点:
- 若任意结点的左子树不空,则左子树上所有结点的值均小于它的根结点的值
- 任意结点的右子树不空,则右子树上所有结点的值均大于它的根结点的值
- 任意结点的左、右子树也分别为二叉查找树
- 没有键值相等的结点
二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低,为 O(logN) 。二叉查找树常见的操作有插入、查找、取最值、删除、排名、范围查找等操作,本文主要讲解前四种操作。
本文中字符为key
,数值为value
,结点定义如下:
class Node
{
public:
Node(char k, int v)//constructor
{
key = k;
value = v;
left = NULL;
right = NULL;
}
char key;
int value;
Node* left;
Node* right;
//往往还会定义一个结点计数器
//int count;count = left->count + right->count + 1
};
二叉查找树定义如下:
class BST
{
public:
int get(char key);
void put(char key, int value);
BST() {root = NULL;}//constructor
int min();
int max();
void delete_min();
void delete_node(char key);
Node* root;
private:
Node* put(Node* x, char key, int value);
int get(Node* x, char key);
Node* min(Node* x);
Node* max(Node* x);
Node* delete_min(Node* x);
Node* delete_node(Node* x, char key);
};
此处应当有一个析构函数的,防止内存泄露。
1.添加
添加元素的具体算法如下:
- 如果当前结点为空,新建结点添加元素
- 当添加元素大于当前结点键时,向右走
- 当添加元素小于当前结点键时,向左走
- 当添加元素等于当前结点键时,更新当前结点的值
根结点是第一个当前结点,重复上面的步骤,直到元素添加完成。
void BST::put(char key, int value)
{
root = put(root, key, value);
}
Node* BST::put(Node* x, char key, int value)
{
//如果不存在键值为key的结点,新建一个结点
if (x == NULL)
{
Node* pNode = new Node(key, value);
return pNode;
}
//key大于当前结点键值,向右走;小于当前结点键值,向左走;等于则更新
if (key > x->key) x->right = put(x->right, key, value);
else if (key < x->key) x->left = put(x->left, key, value);
else x->value = value;
return x;
}
2.查找
查找有两种结果,分别是命中(返回value
)与未命中(返回NULL
)。查找的算法具体如下:
- 如果当前结点为空,则未命中
- 如果查找键大于当前结点键,向右走
- 如果查找键小于当前结点键,向左走
- 如果查找键等于当前结点键,返回当前结点value
根结点是第一个当前结点,递归上面的步骤,直到查找完成。
int BST::get(char key)
{
return get(root, key);
}
int BST::get(Node* x, char key)
{
//未命中返回NULL
if (x == NULL) return NULL;
//大于当前key,向右走;小于当前key,向左走;等于则返回
if (key > x->key) return get(x->right, key);
else if (key < x->key) return get(x->left, key);
else return x->value;
}
3.取最值
取最值有两个操作,一个是取最大值,一个是取最小值。取最小值从根结点开始,不断地向左走,直到走到最左边的结点;取最大值从根结点开始,不断地向右走,直到走到最右边的结点。
int BST::min()
{
return min(root)->value;
}
Node* BST::min(Node* x)
{
if (x->left != NULL)
return min(x->left);
return x;
}
int BST::max()
{
return max(root)->value;
}
Node* BST::max(Node* x)
{
if (x->right != NULL)
return max(x->right);
return x;
}
4.删除
删除操作较为麻烦,因为在删除结点后,还要保证二叉查找树的有序性,即左小右大。先考虑简单的情况:删除最小值结点。
最小值结点一定是最左边的结点,如果最小值结点没有右子结点,则直接删除最小值结点;如果有右子结点,则在删除最小值结点后,将其右子结点作为其父结点的左子结点。
void BST::delete_min()
{
root = delete_min(root);
}
Node* BST::delete_min(Node* x)
{
if (x->left == NULL)
{
//释放空间
Node* temp = x->right;
delete x;
x = NULL;
return temp;
}
x->left = delete_min(x->left);
return x;
}
在delete_min
之上,考虑删除任意结点的问题,算法如下:
- 如果没有要删除的结点,则不作任何操作
- 根据当前结点的键值和待删除结点的大小关系,决定向左走还是向右走,直到找到待删除结点,记为
node1
- 如果
node1
没有子结点则直接删除 - 如果
node1
只有一个子结点,直接用其子结点代替node1
- 如果有两个子结点,需要先删除
node1
,用后选择一个子结点将其代替,记为node2
,node2
需要大于左子树中的任意一个结点,小于右子树中的任意一个结点,比较简单的一个方法就是从node1
的右子树中选出最小结点作为node2
void BST::delete_node(char key)
{
root = delete_node(root, key);
}
Node* BST::delete_node(Node* x, char key)
{
if (x == NULL) return NULL;//无该元素则不进行任何操作
if (key < x->key) x->left = delete_node(x->left, key);
else if (key > x->key) x->right = delete_node(x->right, key);
else//删除结点
{
if (x->right == NULL) return x->left;
if (x->left == NULL) return x->right;
Node* t = x;
t = min(x->right);
x->key = t->key;
x->value = t->value;
x->right = delete_min(x->right);
}
return x;
}
6.实际操作
对二叉查找树进行中序遍历,会得到一个从小到大的序列,可以用这个方法检查一下代码的正确性。在main()
函数中,执行以下操作:
- 建立二叉查找树
- 中序遍历输出二叉查找树
- 输出二叉查找树中最小键的值
- 删除最小键结点
- 中序遍历输出二叉查找树
- 删除结点
'E'
- 中序遍历输出二叉查找树
//递归实现中序遍历
void mid_order(Node* x)
{
if (x)
{
mid_order(x->left);
cout<<x->key<<" ";
mid_order(x->right);
}
}
int main(int argc, char* argv[])
{
BST bst;
char data[] = {'S', 'E', 'A', 'R', 'C', 'H', 'E', 'X', 'A', 'M', 'P', 'L', 'E', '\0'};
int index = 0;
while(data[index] != '\0')
{
bst.put(data[index], index);
index++;
}
mid_order(bst.root);
cout<<endl;
cout<<bst.min()<<endl;
bst.delete_min();
mid_order(bst.root);
cout<<endl;
bst.delete_node('E');
mid_order(bst.root);
cout<<endl;
return 0;
}
结果如下:
- 最小键为
A
,其value
是8
- 最小结点是
A
,删除A
后C
为E
的左子结点 - 删除结点
E
后,H
为S
的左子结点,M
为R
的左子结点,详见后图
建立的二叉查找树如图所示,结点从左向右依次增大:
删除结点E过程示意图:
删除结点E后的二叉查找树:
7.总结
- 二叉查找树查找、插入的时间复杂度较低,为 O(logN)
- 二叉查找树实际的运行时间取决于树的形状,而树的形状又取决于插入结点的顺序。最好情况下,二叉树是一个完全平衡树;最坏情况下,二叉树是一条链的形状
- 二叉查找树中的查找比二分查找慢一些,但是插入数据却比二分查找快很多,综合考虑二分查找树较优
- 本文中二叉查找树各种功能通过递归实现,实际上多用非递归实现,效率高
- 基于二叉树衍生了很多高效的数据结构,如红黑树