二叉搜索树BST
二叉搜索树(英语:Binary Search Tree),也称二叉查找树、有序二叉树(英语:ordered binary tree),排序二叉树(英语:sorted binary tree),是指一棵空树或者具有下列性质的二叉树:
- 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
- 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
- 任意节点的左、右子树也分别为二叉查找树;
- 没有键值相等的节点。
中序遍历二叉查找树可得到一个关键字的有序序列,一个无序序列可以通过构造一棵二叉查找树变成一个有序序列,构造树的过程即为对无序序列进行查找的过程。每次插入的新的结点都是二叉查找树上新的叶子结点,在进行插入操作时,不必移动其它结点,只需改动某个结点的指针,由空变为非空即可。
一、查找问题
查找问题是一个非常基础的问题,查找本身也是一个非常常用的操作。
查找分为动态查找和静态查找。
所谓静态查找就是数据不再发生变化,换句话说,就是没有insert,delete操作,针对这种查找类型,我们可以使用二分查找法。
二分查找算法步骤:
// 递归版本 int binary_search(const int arr[], int start, int end, int khey) { if (start > end) return -1; int mid = start + (end - start) / 2; //直接平均可能會溢位,所以用此算法 if (arr[mid] > khey) return binary_search(arr, start, mid - 1, khey); else if (arr[mid] < khey) return binary_search(arr, mid + 1, end, khey); else return mid; //最後檢測相等是因為多數搜尋狀況不是大於要不就小於 }
// while循环 int binary_search(const int arr[], int start, int end, int khey) { int ret = -1; // 未搜索到数据返回-1下标 int mid; while (start <= end) { mid = start + (end - start) / 2; //直接平均可能會溢位,所以用此算法 if (arr[mid] < khey) start = mid + 1; else if (arr[mid] > khey) end = mid - 1; else { // 最後檢測相等是因為多數搜尋狀況不是大於要不就小於 ret = mid; break; } } return ret; // 单一出口 }
二分查找算法有一个前提就是数组是一个有序数组,在保证数组有序的前提下才能使用这种算法。那如果此时要向数组中增加和删除元素最坏的时间复杂度是O(n),另外,如果原数组不是有序的,那么还需要首先对数组进行排序,这样又会增加O(nlogn)的开销。
那么为什么不直接把数据存储成为类似二分搜索的形式呢?于是二叉搜索树就“闪亮登场”了。
二、二叉搜索树
针对动态查找,二叉查找树可以在期望O(logn)的时间复杂度(最坏情况下也是O(n):数列有序,树退化成线性表)内完成find,insert,delete操作。
虽然二叉查找树的最坏效率是O(n),但它支持动态查询,且有很多改进版的二叉查找树可以使树高为logn,,如SBT,AVL树,红黑树等。故不失为一种好的动态查找方法。
- 二叉搜索树的查找操作:Find
Position Find( ElementType X, BinTree BST ) { if( !BST ) return NULL; /* 查找失败*/ if( X > BST->Data ) return Find( X, BST->Right ); /* 在右子树中继续查找*/ Else if( X < BST->Data ) return Find( X, BST->Left ); /* 在左子树中继续查找*/ else /* X == BST->Data */ return BST; /* 查找成功 , 返回结点的找到结点的地址*/ }
由于非递归函数执行效率高,并且不会出现递归层数过多导致的栈溢出的情况,所以可以使用循环操作来取代递归。
Position IterFind( ElementType X, BinTree BST ) { while( BST ) { if( X > BST->Data ) BST = BST->Right; /* 向右子树中移动 , 继续查找*/ else if( X < BST->Data ) BST = BST->Left; /* 向左子树中移动 , 继续查找*/ else /* X == BST->Data */ return BST; /* 查找成功 , 返回结点的找到结点的地址*/ } return NULL; /* 查找失败*/ }
查找的效率决定于树的高度,所以之后出现了平衡二叉树来解决这个问题。
查找最大和最小元素:
1)最大元素 一定是在树的 最右分枝的端结点上
2)最小元素 一定是在树的 最左分枝的端结点上
查找最小元素的 递归 函数:
Position FindMin( BinTree BST ) { if( !BST ) return NULL; /* 空的二叉搜索树,返回NULL*/ else if( !BST->Left ) return BST; /* 找到最左叶结点并返回*/ else return FindMin( BST->Left ); /* 沿左分支继续查找*/ }
查找最大元素的 迭代 函数:
Position FindMax( BinTree BST ) { if(BST ) while( BST->Right ) BST = BST->Right; /* 沿右分支继续查找,直到最右叶结点*/ return BST; }
- 二叉搜索树的插入:Insert
关键是要找到元素应该插入的位置, 可以采用与Find类似的方法。
BinTree Insert( ElementType X, BinTree BST ) { if( !BST ){ /* 若原树为空 , 生成并返回一个结点的二叉搜索树*/ BST = malloc(sizeof(struct TreeNode)); BST->Data = X; BST->Left = BST->Right = NULL; }else{ /* 开始找要插入元素的位置*/ if( X < BST->Data ) /* 递归插入左子树*/ BST->Left = Insert( X, BST->Left); else if( X > BST->Data ) /* 递归插入右子树*/ BST->Right = Insert( X, BST->Right); /* else X 已经存在 , 什么都不做 */ } return BST; }
- 二叉搜索树的删除:Delete
考虑三种情况:
1)要删除的是叶结点 : 直接删除,并再修改其父结点指针--- 置为NULL
2)要删除的结点 只有一个孩子点 结点:将其 父结点 的指针指向要删除结点的孩子结点
3)要删除的结点 有左右两个子树:用另一结点替代被删除结点:右子树的最小元素 或者 左子树的最大元素。
这种方法的好处是,右子树的最小元素必不可能有两个孩子,左子树中的最大元素也必不可能有两个孩子,也就是将复杂情况转化成了简单情况。
BinTree Delete(ElementType X, BinTree BST) { Position Tmp; if (!BST) printf(" 要删除的元素未找到"); else if (X < BST -> Data) BST -> Left = Delete(X, BST -> Left); /* 左子树递归删除 */ else if (X > BST -> Data) BST -> Right = Delete(X, BST -> Right); /* 右子树递归删除 */ else /* 找到要删除的结点 */ if (BST -> Left && BST -> Right) { /* 被删除结点有左右两个子结点 */ Tmp = FindMin(BST -> Right); /* 在右子树中找最小的元素填充删除结点*/ BST -> Data = Tmp -> Data; /* 在删除结点的右子树中删除最小元素*/ BST -> Right = Delete(BST -> Data, BST -> Right); } else { /* 被删除结点有一个或无子结点*/ Tmp = BST; if (!BST -> Left) /* 有右孩子或无子结点*/ BST = BST -> Right; else if (!BST -> Right) /* 有左孩子或无子结点*/ BST = BST -> Left; free(Tmp); } return BST; }