二叉查找树(BST)
思路
二叉查找树保证任意一个结点的左结点都小于它,而右结点都大于它。
- 插入
插入一个结点时首先查找是否有相同的键,若有则更新即可。
否则直到查找到应该插入的点,创建结点并使得其父结点指向它。 - 查找
递归地查找根结点的子结点,直到找到。 - 删除
删除是BST最复杂的操作。
若待删除结点是叶子结点,那么只需简单地删掉即可。
若待删除结点只有一个子结点,那么只需用其子结点代替自己在树中的位置即可。
最复杂的是待删除结点有两个子结点的情况,这种情况需要使用其后继结点来代替它的位置,这样的话就可以保持BST的有序性。
所谓后继结点,就是指待删除结点右子树中的最小结点。删除的步骤可以分为以下:
- 找到待删除结点node。
- 找到其后继结点sub。
- 用sub代替node的位置。
- 在node的右子树中删除sub结点
对比自己写的C++代码和书中的Java代码,感觉C++好麻烦…
评估
因为插入和查找的行为几乎一致,因此时间复杂度也相同,取决于树的形状。
最坏的情况是有N层,就像一个数组,其时间复杂度为
O
(
N
)
O(N)
O(N)。而最好的情况则是一颗平衡二叉树,其时间复杂度为
O
(
l
o
g
N
)
O(logN)
O(logN)。
查找所需的平均比较次数为
2
l
n
N
2lnN
2lnN
代码
/*
* Key min() 返回最小键
* Key max() 返回最大键
* Key floor(Key key) 返回key向下取整的键
* Key celling(Key key) 返回key向上取整了键
* Key select(int k) 返回排名为k的键(有k个键比它小)
* int rank(Key key) 返回key的排名
* void deleteMin() 删除最小键
* void Delete(Key key) 删除任意键
* void print() 中序遍历,按升序打印键
* vector<Key> &range(Key lo, Key hi) 返回一个存储范围内键的vector
*/
template<typename Key, typename Value>
struct Node {
Key key;
Value val;
Node *left;
Node *right;
int N; // 以该结点为根的树中结点的总数
Node(Key key, Value val, int N) {
this->key = key;
this->val = val;
this->N = N;
this->left = nullptr;
this->right = nullptr;
}
};
template<typename Key, typename Value>
class BST {
private:
Node<Key, Value> *root;
int size(Node<Key, Value> *root) {
if (root == nullptr)
return 0;
else
return root->N;
}
Value get(Node<Key, Value> *node, Key key) {
if (node == nullptr)
return nullptr;
if (node->key == key)
return node->val;
else if (key < node->key)
return get(node->left, key);
else
return get(node->right, key);
}
Node<Key, Value> *put(Node<Key, Value> *node, Key key, Value val) {
// 不存在时,建立新结点
if (node == nullptr) {
return new Node<Key, Value>(key, val, 1);
}
// 如果存在,则更新
if (node->key == key)
node->val = val;
// 否则查找
else if (key < node->key)
node->left = put(node->left, key, val);
else
node->right = put(node->right, key, val);
updateN(root);
return node;
}
Node<Key, Value> *select(Node<Key, Value> *node, int k) {
if (k < size(node->left))
return select(node->left, k);
else if (k > size(node->left))
return select(node->right, k - size(node->left) - 1);
else
return node;
}
int *rank(Node<Key, Value> *node, Key key) {
if (key == node->key)
return size(node->left);
else if (key < size(node->left))
return rank(node->left, key);
else
return rank(node->right, key) + size(node->left) + 1;
}
int updateN(Node<Key, Value> *node) {
if (node == nullptr)
return 0;
else
node->N = updateN(node->left) + updateN(node->right) + 1;
}
void print(Node<Key, Value> *node) {
if (node == nullptr)
return;
print(node->left);
std::cout << node->key << " ";
print(node->right);
}
void range(std::vector<Key> &arr, Node<Key, Value> *node, Key lo, Key hi) {
if (node == nullptr)
return;
if (lo < node->key)
range(arr, node->left, lo, hi);
if (lo <= node->key && node->key <= hi)
arr.push_back(node->key);
if (node->key < hi)
range(arr, node->right, lo, hi);
}
Node<Key, Value> *Delete(Node<Key, Value> *node, Key key) {
if (node == nullptr)
return nullptr;
if (key < node->key)
node->left = Delete(node->left, key);
else if (key > node->key)
node->right = Delete(node->right, key);
else if (key == node->key) {
if (node->left != nullptr && node->right != nullptr) {
// 左右子结点都不为空
Node<Key, Value> *DeletedRight = node->right;
// 找到后继结点sub(subsequent)
Node<Key, Value> *sub = DeletedRight;
Node<Key, Value> *pre = node;
while (sub->left != nullptr) {
pre = sub;
sub = sub->left;
}
// 让后继结点代替deleted的位置
// sub和右结点是同一结点时需要特殊处理
if (sub == DeletedRight) {
sub->left = node->left;
delete node;
return sub;
}
else {
sub->right = DeletedRight;
sub->left = node->left;
pre->left = nullptr;
delete node;
return sub;
}
}
else if (node->left == nullptr && node->right == nullptr) {
delete node;
return nullptr;
}
else if (node->left == nullptr) {
Node<Key, Value> *newRight = node->right;
delete node;
return newRight;
}
else if (node->right == nullptr) {
Node<Key, Value> *newLeft = node->left;
delete node;
return newLeft;
}
}
return node;
}
// 用于析构函数
void destructor(Node<Key, Value> *node) {
if (node == nullptr)
return;
destructor(node->left);
destructor(node->right);
delete node;
}
public:
BST() {
root = nullptr;
}
~BST() {
destructor(root);
}
int size() {
return size(root);
}
Value get(Key key) {
return get(root, key);
}
void put(Key key, Value val) {
root = put(root, key, val);
}
Key min() {
Node<Key, Value> *cur = root;
while (cur->left != nullptr)
cur = cur->left;
return cur->key;
}
Key max() {
Node<Key, Value> *cur = root;
while (cur->right != nullptr)
cur = cur->right;
return cur->key;
}
Key floor(Key key) {
Node<Key, Value> *cur = root;
while (cur->left != nullptr && key < cur->key)
cur = cur->left;
if (cur->right != nullptr && cur->right->key <= key)
return cur->right->key;
else
return cur->key;
}
Key celling(Key key) {
Node<Key, Value> *cur = root;
while (cur->right != nullptr && key > cur->key)
cur = cur->right;
if (cur->left != nullptr && cur->left->key >= key)
return cur->left->key;
else
return cur->key;
}
Key select(int k) {
return select(root, k)->key;
}
int rank(Key key) {
return rank(root, key);
}
void deleteMin() {
Node<Key, Value> *pre = root;
Node<Key, Value> *cur = root->left;
if (cur == nullptr) {
if (root->right != nullptr) {
root = root->right;
delete pre;
}
else {
delete root;
root = nullptr;
}
updateN(root);
return;
}
while (cur->left != nullptr) {
cur = cur->left;
pre = pre->left;
}
pre->left = cur->right;
delete cur;
updateN(root);
}
void Delete(Key key) {
root = Delete(root, key);
updateN(root);
}
void print() {
print(root);
}
std::vector<Key> range(Key lo, Key hi) {
std::vector<Key> arr;
range(arr, root, lo, hi);
return arr;
}
};
2-3查找树
BST的形状和输入相关,如果想要最坏情况下也只需要对数级别的查找,就需要一个平衡二叉树。下面介绍的2-3查找树就是一颗完美平衡二叉树(即所有空链接到根结点的距离都相等)。
如果说BST的结点是2-结点,即每个结点有两条链接、一个键,那么2-3查找树就是再引入了3-结点,即有三条链接和2个键的结点。它的左链接的键小于该结点,右链接的键大于该结点,中间链接的键则在该结点两个键之间。
对于2-3查找树的构造,可以分为以下几种情况:
- 向2-结点中插入新键
只需将其替换为3-结点即可。 - 向3-结点中插入新键
直接插入使其暂时成为一个4-结点,然后将中间大小的键插入到其父结点中,然后把该3-结点分成2个2-结点。如果此时其父结点变成了4-结点,就重复该操作。如果直到根结点变成了4-结点,就将其中间键提出来作为根结点。
构造结果和输入顺序有关,但都满足定义。
注意如果不把4-结点划分成2个2-结点,则构造后可能不满足“任意空链接到根结点的距离相等”,也可能构造出不符合大小顺序的2-3查找树。如下例:
给该2-3查找树插入13得:
将13插入其父结点得到:
而非:
这样的话最终得到的结果就是:
而非:
很显然结点19有一个空链接和其他空链接距离不等。且此时18在13的左子树。
红黑树
利用2-3查找树来定义红黑树,就是将所有3-结点分成两个2-结点,并将其用一条红链接连接。其他普通的2-结点则用黑链接连接。将所有红链接拉平,红黑树就变成了2-3查找树。
红黑树自己的定义则是:
- 红链接均为左链接
- 没有任何一个结点同时和2条红链接相连
- 红黑树是完美黑色平衡的,即任意空链接到根结点所经过的黑链接数相等。
- 旋转
红黑树需要一个将红链接旋转的操作,用以支持插入操作。 - 插入
类似于2-3树在插入时总是先把待插入的元素放到一个结点中,再将其取出放到正确位置。红黑树在插入时也总是插入一条红链接,然后进行后续操作。
若插入的是2-结点,则只需简单地旋转即可。
若插入的是3-结点(即插入后有了两条相连的红链接),则需要进行颜色转换。
最终,插入操作可以归纳为以下3种情况:- 若右结点是红色而左结点是黑色,则左旋转。(或其对称情况)
- 若左结点和左结点的左结点都是红色,则右旋转。(或其对称情况)
- 若左右结点都是红色,则颜色转换。
注意顺序不能颠倒,后面的每一种情况都可能是前一种情况的结果。
另外,每当由于插入操作使得根结点变红时,都需要将其恢复为黑色,此时数的高度+1。
评估
红黑树的所有操作都是对数级别的,只有范围查找除外(它需要的额外时间和返回的键的数量成正比)。
代码
const bool RED = true;
const bool BLACK = false;
template<typename Key, typename Value>
struct Node {
Key key;
Value val;
Node *left;
Node *right;
int N;
// 一个结点的颜色是其与其父结点的链接的颜色
bool color; // true表示红,false表示黑
Node(Key _key, Value _val, int N, bool color) : key(_key), val(_val) {
this->N = N;
this->color = color;
}
};
template<typename Key, typename Value>
class RedBlackBST {
private:
Node<Key, Value> *root;
int size(Node<Key, Value> *node) { return node ? node->N : 0; }
bool isRed(Node<Key, Value> *node) { return node ? node->color : false; }
// 将node结点的右红链接左旋转
Node<Key, Value> *rotateLeft(Node<Key, Value> *node) {
Node<Key, Value> *nodeRight = node->right;
node->right = nodeRight->left;
nodeRight->left = node;
nodeRight->color = node->color;
node->color = RED;
nodeRight->N = node->N;
node->N = 1 + size(node->left) + size(node->right);
return nodeRight;
}
Node<Key, Value> *rotateRight(Node<Key, Value> *node) {
Node<Key, Value> *nodeLeft = node->left;
node->left = nodeLeft->right;
nodeLeft->right = node;
nodeLeft->color = node->color;
node->color = RED;
nodeLeft->N = node->N;
node->N = 1 + size(node->left) + size(node->right);
return nodeLeft;
}
// 将node的两个红链接转为黑色,将node的链接转为红色
void flipColor(Node<Key, Value> *node) {
node->color = RED;
node->left->color = BLACK;
node->right->color = BLACK;
}
Node<Key, Value> *put(Node<Key, Value> *node, Key key, Value val) {
if (node == nullptr)
return new Node(key, val, 1, RED);
if (key < node->key)
node->left = put(node->left, key, val);
else if (key > node->key)
node->right = put(node->right, key, val);
else
node->val = val;
// 注意下面的顺序不能颠倒,后面的每一种情况都可能是前一种情况的结果
// 当红链接是右链接时,左旋转
if (isRed(node->right) && !isRed(node->left))
node = rotateLeft(node);
// 当左子结点和它的左子结点都是红链接时,右旋转
if (isRed(node->left) && isRed(node->left->left))
node = rotateRight(node);
// 当左右子结点都是红链接时
if (isRed(node->left) && isRed(node->right))
flipColor(node);
node->N = 1 + size(node->left) + size(node->right);
return node;
}
public:
void put(Key key, Value val) {
root = put(root, key, val);
root->color = BLACK;
}
};