给定一个关键字 key 和 一个集合 Collection, 确定这个Collection是否包含给定的key,如果包含,找出该key对应的位置,这个过程为 查找 (Search)。
如果在 查找 过程中, Collection的结构始终不发生变化,那么称之为静态查找; 反之,需要频繁的向这个集合中添加、删除元素,从这样的集合中进行查找则称之为动态查找。
动态查找技术一般用树来存储查找集合,如二叉查找树。而二叉查找树的查找效率又与自身形态密切相关,需要对二叉查找树平衡化,保持其查找高效。本文就二叉查找树和平衡二叉树作简单介绍和分析以及代码实现,仅供学习参考。
1、二叉查找树
1.1 二叉查找树的定义
二叉查找树(Binary Search Tree, BST),也有称作二叉排序树,二叉检索树,二叉搜索树,检索树 等等。其数据结构表示和二叉树相同,在此基础之上还有其特殊的性质,递归定义为:
一颗非空的二叉查找树(Binary Search Tree, BST),满足如下特性:
- 若根节点的左子树非空,则左子树所有节点数值小于根节点;
- 若根节点的右子树非空,则右子树所有节点数值大于根节点;
- 左右子树分别为一颗二叉查找树.
- // define data type for the key
- typedef int keyType;
- // define binary search tree data structure
- struct BinaryTreeNode {
- keyType key;
- BinaryTreeNode* left; // left child
- BinaryTreeNode* right; // right child
- };
- // alias for the tree
- typedef BinaryTreeNode bstree;
- // alias for the tree node
- typedef BinaryTreeNode bstnode;
1.2 基本操作(查找、插入和删除节点)
(1) 查找key :从定义一颗查找二叉树来看,可以利用其特性根据关键字进行快速查找,这和顺序表的二分查找一样。
- // search BST by the given key
- bstnode* search_by_key(bstree* tree, keyType key) {
- bstnode* node = tree;
- int found = 0;
- while (NULL != node) {
- if (key == node->key) {
- found = 1;
- break;
- }
- node = (key > node->key) ? node->right : node->left;
- }
- return found ? node : NULL;
- }
(2) 插入key :向BST中插入一个key,不能破坏BST的规则
- // insert a key to BST
- int insert_key(bstree* &tree, keyType key) {
- if (NULL == tree) {
- tree = (bstnode*) malloc(sizeof(bstnode));
- tree->key = key;
- return 1;
- }
- int found = 0;
- bstnode* curr = tree;
- bstnode* prev = NULL;
- while (NULL != curr) {
- // if already exists
- if (key == curr->key) {
- found = 1;
- break;
- }
- prev = curr;
- curr = (key > curr->key) ? curr->right : curr->left;
- }
- if (!found && NULL == curr) {
- curr = (bstnode*) malloc(sizeof(bstnode));
- curr->key = key;
- ((key > prev->key) ? prev->right : prev->left) = curr;
- return 1;
- }
- return 0;
- }
(3) 删除key :在BST中删除一个key,不能破坏BST的规则
删除操作需要注意:1. 若删除的元素是树根,则需要确保删除之后还能构建成新的树; 2. 按照查找二叉树的特性,按照中序遍历则必定有序,所以在删除某个节点之后可以寻找其中序前去或者后继来替代该节点。
- // delete a key from the BST
- int delete_key(bstree* &tree, keyType key) {
- // 设定临时树根,其key假设为无穷大,防止出现删除root的情况
- // 主要作用是让原来树的root可以像其他节点一样参与运算
- bstnode* head = (bstnode*) malloc(sizeof(bstnode));
- head->left = tree;
- bstnode *curr = tree, *prev = head;
- bstnode *t1 = NULL, *t2 = NULL;
- int found = 0;
- while (NULL != curr) {
- if (key == curr->key) {
- found = 1;
- break;
- }
- prev = curr;
- curr = ((key > curr->key) ? curr->right : curr->left);
- }
- if (found) {
- // delete the node with the given key
- if (NULL == curr->left) { // when the left child of the node is NULL
- ((curr == prev->left) ? prev->left : prev->right) = curr->right;
- free(curr);
- } else if (NULL == curr->right) { // when the right child of the node is NULL
- ((curr == prev->left) ? prev->left : prev->right) = curr->left;
- free(curr);
- } else { // when the node has two children
- // 按照二叉查找树的特性,保持中序遍历顺序即可
- // 即用该节点的中序前序后者后继替代该节点即可
- t1 = curr->left;
- while (NULL != t1->right) {
- t2 = t1;
- t1 = t1->right;
- }
- curr->key = t1->key;
- ((NULL == t2) ? curr->left : t2->right) = t1->left;
- free(t1);
- }
- }
- // 还原树根
- tree = head->left;
- free(head);
- return found;
- }
测试 操作
- int main() {
- cout << "插入构建二叉查找树:4 2 5 3 1 6" << endl;
- bstree * bst = NULL;
- insert_key(bst, 4);
- insert_key(bst, 2);
- insert_key(bst, 5);
- insert_key(bst, 3);
- insert_key(bst, 1);
- insert_key(bst, 6);
- cout << endl;
- cout << "遍历二叉查找树 :" << endl << "前序\t : ";
- rpre_order(bst);
- cout << endl << "中序\t : ";
- rin_order(bst);
- cout << endl;
- cout << endl;
- cout << "删除元素 4 : ";
- int r = delete_key(bst, 4);
- if (r) {
- cout << "Success";
- } else {
- cout << "Failed";
- }
- cout << endl;
- cout << "删除元素 32 : ";
- r = delete_key(bst, 32);
- if (r) {
- cout << "Success";
- } else {
- cout << "Failed";
- }
- cout << endl;
- cout << endl;
- cout << "刪除之后 :" << endl << "前序\t : ";
- rpre_order(bst);
- cout << endl << "中序\t : ";
- rin_order(bst);
- cout << endl;
- //
- destory_btree(bst);
- return 0;
- }
测试结果:
- 插入构建二叉查找树:4 2 5 3 1 6
- 遍历二叉查找树 :
- 前序 : 4 2 1 3 5 6
- 中序 : 1 2 3 4 5 6
- 删除元素 4 : Success
- 删除元素 32 : Failed
- 刪除之后 :
- 前序 : 3 2 1 5 6
- 中序 : 1 2 3 5 6
1.3 小结
对于一颗二叉查找树来说,如果分布较为对称,每次比较都会将查找分为缩减一半,其时间复杂度为 Log2(N), 最坏的情况下,即这个查找树二叉树退化成只有左子树或者右子树的情况下,其复杂度为N .
这样看来,二叉查找树的查找效率和它自身的形态有密切的关系,一颗形态分布匀称的查找二叉树才能发挥出它的真正的效率。
在经过多次插入、删除操作之后,如果不加控制,一颗二叉查找树很有可能退化,这样就大大降低了其查找效率。