1. 二分查找法
学习二叉搜索树首先来看看二分查找。什么是二分查找呢?在一个有序数列中,找到某一元素的索引。没错只有一个有序数列, 才能使用二分查找法(排序的作用),二分查找法的思想在1946年提出,但是第一个没有Bug的二分查找法在1962年才出现。
其思想就是每次将查找元素与数组中间位置比较,如果查找元素比中间元素小,说明查找元素在左边,然后递归下去查找左边,否则查找右边。
// 二分查找法, 在有序数组arr中, 查找target
// 如果找到target, 返回相应的索引index
// 如果找不到target, 返回-1
template <typename T>
int binarySearch(T arr[], int n, T target) {
// 在arr[;...r]之中查找target
int l = 0, r = n - 1;
while(l <= r) {
// int mid = (l + r) / 2;
int mid = l + (r - l) / 2;//防止l + r 会int溢出
if(arr[mid] == target)
return mid;
else if( arr[mid] > target)
r = mid - 1;
else
l = mid + 1;
}
}
2. 二分搜索树
2.1 二分搜索树的优势
高效:不仅可以查找数据;还可以高效地插入,删除数据-动态维护数据;
还可以方便地回答很多数据之间关系问题,比如最大值、最小值、排名第几的元素、num排名第几等等问题
2.2 二分搜索树的定义
1. 二分搜索树是一个二叉树2. 每一个节点的键值大于左孩子,每一个节点的键值小于右孩子,以左右孩子为根的子树仍为二分搜索树
这里先定义一个二分搜索树类:其中的节点表示的是一个字典,通过key value的形式,数据顺序与key相关,与value无关。
template <typename Key, typename Value>
class BST {//Binary Search Tree
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;//size of bst
public:
BST() {
root = NULL;
count = 0;
}
int size() {
return count;
}
bool isEnpty() {
return count == 0;
}
};
3. 插入新的节点
//插入一个节点
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)
return new Node(key, value);
//当前节点的key等于插入的key,则覆盖元素即可
if(key == node->key)
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. 查找一个节点
//搜索键为key的值
//search的返回值可以为节点即Node *,但是Node不能为private,不符合封装的特性
//返回值Value,可能会出现查找不到这个问题
//所以最后使用Value *这种方法,这样当查找不到,可以返会NULL
Value* search(Key key) {
return search(root, key);
}
//搜索以node为根节点的二叉搜索树中键值为key的值
Value* search(Node* node, Key key) {
if(node == NULL)
return NULL;
if(node->key == key)
return &(node->value);
else if(node->key > key)
return search(node->left, key);
else //node->key < key
return search(node->right, key);
}
5. 二分搜索树的遍历(深度优先)
- 前序遍历:先访问当前节点,再依次递归访问左右子树
- 中序遍历:先递归访问左子树,再访问自身,再递归访问右子树
- 后续遍历:先递归访问左右子树,再访问自身
5.1 前序遍历
//二叉搜索树的前序遍历
//二叉搜索树的中序遍历
void inOrder() {
preOrder(root);
}
// 以node为根的二叉搜索树的中序遍历
void inOrder(Node *node) {
if(node != NULL) {
inOrder(node->left);
cout << node->key << endl;
inOrder(node->right);
}
}
5.2 中序遍历
//二叉搜索树的中序遍历
void inOrder() {
preOrder(root);
}
// 以node为根的二叉搜索树的中序遍历
void inOrder(Node *node) {
if(node != NULL) {
inOrder(node->left);
cout << node->key << endl;
inOrder(node->right);
}
}
5.3 后序遍历
//二叉搜索树的后续遍历
void postOrder() {
postOrder();
}
// 以node为根的二叉搜索树的后序遍历
void postOrder(Node *node) {
if(node != NULL) {
postOrder(node->left);
postOrder(node->right);
cout << node->key << endl;
}
}
5.4 析构函数
析构函数是为了释放二分搜索树每个节点的内存,必须要遍历所有的节点, 三种遍历方式,只有后序遍历可以按照顺序释放空间,由于其余遍历均会释放父亲节点内存在子节点之前,导致无法找到子节点的内存位置。
~BST() {
destroy(root);
}
//析构函数调用, 释放二叉搜索树的内存, 通过后序遍历来实现
void destroy(Node *node) {
if(node != NULL) {
destroy(node->left);
destroy(node->right);
delete node;
count--;
}
}
6. 二分搜索树的遍历(广度优先)
//二叉搜索树的层序遍历
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);
}
}
7. 二分搜索树 删除节点
7.1 删除最大值和最小值
这里以最小值为例,从根节点出发,向其左子树递归遍历,一旦某一节点的左孩子为NULL, 说明该节点为最小值,此时只需要将该节点的父节点的左孩子指针指向该节点的右孩子(可能为NULL)即可。
//删除最小值
void removeMin() {
if(root)
root = removeMin(root);
}
//删除最大值
void removeMax() {
if(root)
root = removeMax(root);
}
//删除最小值
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* removeMax(Node* node) {
if(node->right == NULL) {
Node* leftNode = node->left;
delete node;
count--;
return leftNode;
}
node->right = removeMax(node->right);
return node;
}
7.2 删除任意节点
当遇到删除节点两个孩子都存在时,我们可以找到右子树的最小值(左子树的最大值)来代替被删除的节点,该过程描述如下:
- 通过minimum函数找到右子树最小节点
- 将最小节点放在被删除的位置
- 将最小节点(已经在被删除的位置)的左孩子指向被删除节点的左孩子
- 将最小节点右孩子指向removeMin(node->right),node为被删除指针
//删除一个值
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 {
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的原因在于removeMin(node->right)会删除该节点
Node *successor = new Node(minimum(node->right));
successor->right = removeMin(node->right);
successor->left = node->left;
delete node;//这里没有count--的原因在与,removeMin(node->right)会减
return successor;
}
}
- 同样的数据,可以对应不同的二分搜索树中,二分搜索树可能退化为链表
- 当二分搜索树退化为链表会比链表实现效率低,原因在于
- 二分搜索树处理节点时会处理两个指针
- 二分搜索树采用了递归的形式
当二分搜索树退化为链表会比链表实现效率低,原因在于
8. 二分搜索树的局限性
同样的数据,可以对应不同的二分搜索树中,二分搜索树可能退化为链表。当二分搜索树退化为链表会比链表实现效率低,原因在于二分搜索树处理节点时会处理两个指针而且二分搜索树采用了递归的形式。