一. 二分查找法
- 对于有序数列,才能使用二分查找法(排序的作用)
- 递归实现通常思维起来更容易, 但性能不如 while循环法
代码
#include <iostream>
#include <cassert>
#include <ctime>
using namespace std;
// 二分查找法,在有序数组arr中,查找target
// 如果找到target,返回相应的索引index
// 如果没有找到target,返回-1
template<typename T>
int binarySearch(T arr[], int n, T target){
// 在arr[l...r]之中查找target
int l = 0, r = n-1;
while( l <= r ){
//int mid = (l + r)/2;
// 防止极端情况下的整形溢出,使用下面的逻辑求出mid
int mid = l + (r-l)/2;
if( arr[mid] == target )
return mid;
if( arr[mid] > target )
r = mid - 1;
else
l = mid + 1;
}
return -1;
}
// 用递归的方式写二分查找法
template<typename T>
int __binarySearch2(T arr[], int l, int r, T target){
if( l > r )
return -1;
//int mid = (l+r)/2;
// 防止极端情况下的整形溢出,使用下面的逻辑求出mid
int mid = l + (r-l)/2;
if( arr[mid] == target )
return mid;
else if( arr[mid] > target )
return __binarySearch2(arr, l, mid-1, target);
else
return __binarySearch2(arr, mid+1, r, target);
}
template<typename T>
int binarySearch2(T arr[], int n, T target){
return __binarySearch2( arr , 0 , n-1, target);
}
// 比较非递归和递归写法的二分查找的效率
// 非递归算法在性能上有微弱优势
int main() {
int n = 1000000;
int* a = new int[n];
for( int i = 0 ; i < n ; i ++ )
a[i] = i;
// 测试非递归二分查找法
clock_t startTime = clock();
// 对于我们的待查找数组[0...N)
// 对[0...N)区间的数值使用二分查找,最终结果应该就是数字本身
// 对[N...2*N)区间的数值使用二分查找,因为这些数字不在arr中,结果为-1
for( int i = 0 ; i < 2*n ; i ++ ){
int v = binarySearch(a, n, i);
if( i < n )
assert( v == i );
else
assert( v == -1 );
}
clock_t endTime = clock();
cout << "Binary Search (Without Recursion): " << double(endTime - startTime) / CLOCKS_PER_SEC << " s"<<endl;
// 测试递归的二分查找法
startTime = clock();
// 对于我们的待查找数组[0...N)
// 对[0...N)区间的数值使用二分查找,最终结果应该就是数字本身
// 对[N...2*N)区间的数值使用二分查找,因为这些数字不在arr中,结果为-1
for( int i = 0 ; i < 2*n ; i ++ ){
int v = binarySearch2(a, n, i);
if( i < n )
assert( v == i );
else
assert( v == -1 );
}
endTime = clock();
cout << "Binary Search (Recursion): " << double(endTime - startTime) / CLOCKS_PER_SEC << " s"<<endl;
delete[] a;
return 0;
}
二. 二分搜索树基础 (Binary Search Tree)
二分搜索数的优势
- 体现在 实现类似 python中的 dict 数据结构 key-value
- 高效
不仅可以查找数据; 还可以高效地插入, 删除数据-动态维护数据
可以方面的回答很多数据之间的关系问题
min, max, floor, ceil, rank, select
特点
二分搜索数的基础结构编写
main.cpp
#include <iostream>
using namespace std;
// 二分搜索树
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;
}
};
int main() {
return 0;
}
三. 二分搜索数的节点插入
- 从根节点开始寻找合适的位置
- 找到满足 左子节点 < 父节点 < 右子节点
代码 main.cpp
class BST{
...
private:
...
// 向二分搜索树中插入一个新的(key, value)数据对
void insert(Key key, Value value){
root = insert(root, key, value);
}
private:
// 向以node为根的二分搜索树中, 插入节点(key, value), 使用递归算法
// 返回插入新节点后的二分搜索树的根
Node* insert(Node *node, Key key, Value value){
if( node == NULL ){
count ++;
return new Node(key, value);
}
if( key == node->key )
node->value = value;
else if( key < node->key )
node->left = insert( node->left , key, value);
else // key > node->key
node->right = insert( node->right, key, value);
return node;
}
};
...
四. 二分搜索树的查找
- 查找的逻辑和insert 类似
- 实现 contain(是否包含) 和 search(查找key对应的value)
代码 main.cpp
// 二分搜索树
template <typename Key, typename Value>
class BST{
...
public:
...
// 查看二分搜索树中是否存在键key
bool contain(Key key){
return contain(root, key);
}
// 在二分搜索树中搜索键key所对应的值。如果这个值不存在, 则返回NULL
Value* search(Key key){
return search( root , key );
}
private:
...
// 查看以node为根的二分搜索树中是否包含键值为key的节点, 使用递归算法
bool contain(Node* node, Key key){
if( node == NULL )
return false;
if( key == node->key )
return true;
else if( key < node->key )
return contain( node->left , key );
else // key > node->key
return contain( node->right , key );
}
// 在以node为根的二分搜索树中查找key所对应的value, 递归算法
// 若value不存在, 则返回NULL
Value* search(Node* node, Key key){
if( node == NULL )
return NULL;
if( key == node->key )
return &(node->value);
else if( key < node->key )
return search( node->left , key );
else // key > node->key
return search( node->right, key );
}
};
五. 二分搜索树的遍历(深度优先遍历)
二分搜索树的 前中后序遍历
- 前序遍历: 先访问当前节点, 再依次递归访问左右子树
- 中序遍历(可以实现排序): 先递归访问左子树, 再访问当前节点和 递归访问右子树
- 后序遍历(可以实现释放): 先递归访问左右子树, 再访问当前节点
代码 main.cpp
// 二分搜索树
template <typename Key, typename Value>
class BST{
private:
...
// 二分搜索树的前序遍历
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;
}
}
// 释放以node为根的二分搜索树的所有节点
// 采用后续遍历的递归算法
void destroy(Node* node){
if( node != NULL ){
destroy( node->left );
destroy( node->right );
delete node;
count --;
}
}
};
// 测试二分搜索树的前中后序遍历
int main() {
srand(time(NULL));
BST<int,int> bst = BST<int,int>();
// 取n个取值范围在[0...m)的随机整数放进二分搜索树中
int N = 10;
int M = 100;
for( int i = 0 ; i < N ; i ++ ){
int key = rand()%M;
// 为了后续测试方便,这里value值取和key值一样
int value = key;
cout<<key<<" ";
bst.insert(key,value);
}
cout<<endl;
// 测试二分搜索树的size()
cout<<"size: "<<bst.size()<<endl<<endl;
// 测试二分搜索树的前序遍历 preOrder
cout<<"preOrder: "<<endl;
bst.preOrder();
cout<<endl;
// 测试二分搜索树的中序遍历 inOrder
cout<<"inOrder: "<<endl;
bst.inOrder();
cout<<endl;
// 测试二分搜索树的后序遍历 postOrder
cout<<"postOrder: "<<endl;
bst.postOrder();
cout<<endl;
return 0;
}
六. 层序遍历(广度优先遍历)
代码
main.cpp
...
#include <queue>
...
// 二分搜索树的层序遍历
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 );
}
}
...
七. 删除最大值, 最小值
- 最左的node 即最小值
- 最右边的node 即 最大值
代码 main.cpp
// 寻找二分搜索树的最小的键值
Key minimum(){
assert( count != 0 );
Node* minNode = minimum( root );
return minNode->key;
}
// 寻找二分搜索树的最大的键值
Key maximum(){
assert( count != 0 );
Node* maxNode = maximum(root);
return maxNode->key;
}
// 从二分搜索树中删除最小值所在节点
void removeMin(){
if( root )
root = removeMin( root );
}
// 从二分搜索树中删除最大值所在节点
void removeMax(){
if( root )
root = removeMax( root );
}
...
// 返回以node为根的二分搜索树的最小键值所在的节点
Node* minimum(Node* node){
if( node->left == NULL )
return node;
return minimum(node->left);
}
// 返回以node为根的二分搜索树的最大键值所在的节点
Node* maximum(Node* node){
if( node->right == NULL )
return node;
return maximum(node->right);
}
// 删除掉以node为根的二分搜索树中的最小节点
// 返回删除节点后新的二分搜索树的根
Node* removeMin(Node* node){
if( node->left == NULL ){
Node* rightNode = node->right;
delete node;
count --;
return rightNode;
}
node->left = removeMin(node->left);
return node;
}
// 删除掉以node为根的二分搜索树中的最大节点
// 返回删除节点后新的二分搜索树的根
Node* removeMax(Node* node){
if( node->right == NULL ){
Node* leftNode = node->left;
// 为什么node-left 一定比 node的父节点小?
// 因为insert的时候都是从root开始查找合适的位置的
delete node;
count --;
return leftNode;
}
node->right = removeMax(node->right);
return node;
}
八. 二分搜索树节点的删除
代码 main.cpp
// 二分搜索树
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(Node *node){
this->key = node->key;
this->value = node->value;
this->left = node->left;
this->right = node->right;
}
};
...
// 从二分搜索树中删除键值为key的节点
void remove(Key key){
root = remove(root, key);
}
...
// 删除掉以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 *successor = new Node(minimum(node->right));
count ++;
successor->right = removeMin(node->right);
successor->left = node->left;
delete node;
count --;
return successor;
}
}
补充说明
- 我们除了使用 s = min(d->right) 代替被删除节点d,
- 也可以用 p = max(d->left) 代替被删除节点d
九. 二分搜索树的局限性
如果二分搜索树 极不平衡, 会导致算法从O(logn)退化为O(n)级别
极不平衡的二分搜索树
1
\
2
\
3
\
4
\
5