红黑树,哈希表...几种常见的符号表实现
符号表
符号表是一种通过把一个键(key)和一个值(value)联系起来,在调用时通过查找键来对键对应的值进行操作的数据结构(如c++中的map)。
符号表的主要操作有增,删,改,查四种,也可以对其进行扩展操作。下面,就对几种符号表的实现及部分扩展操作进行简要的介绍。
符号表的双数组实现
顾名思义,通过两个数组,一个存放key,一个存放value,来实现符号表。为了便于数据查找,要保证数组内数据的有序性。
这里给出完整代码:
//符号表(双数组实现版)
template<typename K,typename V>
class signTable{
vector<K> keyV;
vector<V> valueV;
size_t n = 0;
public:
signTable() = default;
size_t size() { return n; }
bool is_empty() { return n == 0; }
K max() { return is_empty() ? NULL : keyV[n - 1]; }
K min() { return is_empty() ? NULL : keyV[0]; }
K select(size_t r) { return (r < n) ? keyV[r] : NULL; }
size_t rank(K key);
size_t rank(K lo, K hi);
void insert(K key, V value);
V get(K key);
void dele(K key);
K ceiling(K key);
K floor(K key);
vector<K> cut_key(K lo, K hi);
};
template<typename K, typename V>
size_t signTable<K, V>::rank(K key) {
if (is_empty()) { return 0; }
int lo = 0, hi = n - 1, mid;
while (lo <= hi) {
mid = lo + (hi - lo) / 2;
if (key < keyV[mid]) { hi = mid - 1; }
else if (key > keyV[mid]) { lo = mid + 1; }
else { return mid; }
}
return lo;
}
template<typename K, typename V>
size_t signTable<K, V>::rank(K lo,K hi) {
if (hi <= lo) { return 0; }
else { return rank(hi) - rank(lo); }
}
template<typename K, typename V>
void signTable<K, V>::insert(K key, V value) {
size_t temp = rank(key);
if (temp == n) {
keyV.push_back(key);
valueV.push_back(value);
++n;
}
else if (keyV[temp] == key) { valueV[temp] = value; }
else {
keyV.push_back(keyV[n - 1]);
valueV.push_back(valueV[n - 1]);
for (int i = n - 1; i > temp; --i) {
keyV[i] = keyV[i - 1];
valueV[i] = valueV[i - 1];
}
keyV[temp] = key;
valueV[temp] = value;
++n;
}
}
template<typename K, typename V>
V signTable<K, V>::get(K key) {
size_t temp = rank(key);
if (temp == n || keyV[temp] != key) { return NULL; }
return valueV[temp];
}
template<typename K, typename V>
void signTable<K, V>::dele(K key) {
size_t temp = rank(key);
if (temp == n || keyV[temp] != key) { return; }
while (temp < n - 1) {
valueV[temp] = valueV[temp + 1];
keyV[temp] = keyV[++temp];
}
valueV.pop_back();
keyV.pop_back();
--n;
}
template<typename K, typename V>
K signTable<K, V>::ceiling(K key) {
size_t temp = rank(key);
if (temp == n) { return NULL; }
return keyV[temp];
}
template<typename K, typename V>
K signTable<K, V>::floor(K key) {
size_t temp = rank(key);
if (temp == 0 && keyV[temp] != key) { return NULL; }
if (temp == n || keyV[temp] != key) { return keyV[temp - 1]; }
return key;
}
template<typename K, typename V>
vector<K> signTable<K, V>::cut_key(K lo, K hi) {
vector<K> temp;
K temp1 = ceiling(lo);
if (!temp1) { return temp; }
size_t temp2 = rank(floor(hi));
for (size_t i = rank(temp1); i <= temp2; ++i) {
temp.push_back(keyV[i]);
}
return temp;
}
函数详细解释:
- rank(K key) :使用二分查找,找出所提供的key在表中的秩,如果表中没有key,则返回大于key的最小值(如果不存在则是表尾)的秩。
- rank(K lo, K hi) :两个key的秩的差值。
- insert(K key, V value) :插入和修改操作,如果key已存在,则修改value(表中key具有唯一性);key不存在则插入,在插入时仍保证表的有序性。
- get(K key) :查找操作,返回key对应的value,如果key不存在则返回NULL。
- dele(K key) :删除操作,删除后仍保证表的有序性。
- ceiling(K key) :天花板,返回大于等于key的最小值的秩(不存在返回Null)。
- floor(K key) :地板,返回小于等于key的最大值的秩(不存在返回Null)。
- cut_key(K lo, K hi) :切片,返回一个由大于等于lo小于等于hi的秩组成的vector。
rank通过二分查找来获取秩,而几乎所有操作都是通过rank提供的秩来实现的,这也保证了程序的高效性。双数组符号表的查找性能可以达到O(lgN)级别,但它的插入操作仍是O(N)级别。
符号表的二叉搜索树实现
用二叉树实现的符号表,每个节点存放key,value,n(以它为根节点的树的节点数)和指向左右树的智能指针(防止内存泄漏)。通过递归,自顶向下地进行查找,保证较低的时间复杂度。
这里给出完整代码:
//符号表(二叉搜索树实现版)
class BST {
struct Node {
int key;
int value;
shared_ptr<Node> left, right;
int n;
Node(int key, int value, int n) :key(key), value(value), n(n) {}
};
shared_ptr<Node>root;
int size(shared_ptr<Node> node) { return node ? node->n : 0; }
int get(shared_ptr<Node> node, int key) {
if (node == nullptr) { return 0; }
if (key < node->key) { return get(node->left, key); }
else if (key > node->key) { return get(node->right, key); }
else { return node->value; }
}
shared_ptr<Node> put(int key, int value, shared_ptr<Node> node) {
if (node == nullptr) { node = shared_ptr<Node>(new Node(key, value, 1)); }
else if (key < node->key) { node->left = put(key, value, node->left); }
else if (key > node->key) { node->right = put(key, value, node->right); }
else { node->value = value; }
node->n = size(node->left) + size(node->right) + 1;
return node;
}
shared_ptr<Node> max(shared_ptr<Node> node) {
if (node->right == nullptr) { return node; }
else { return max(node->right); }
}
shared_ptr<Node> min(shared_ptr<Node> node) {
if (node->left == nullptr) { return node; }
else { return min(node->left); }
}
shared_ptr<Node> floor(int key, shared_ptr<Node> node) {
if (node == nullptr) { return node; };
if (key < node->key) { return floor(key, node->left); }
else if (key == node->key) { return node; }
shared_ptr<Node> tem = floor(key,node->right);
if (tem) { return tem; }
return node;
}
shared_ptr<Node> ceiling(int key, shared_ptr<Node> node) {
if (node == nullptr) { return node; };
if (key > node->key) { return floor(key, node->right); }
else if (key == node->key) { return node; }
shared_ptr<Node> tem = floor(key, node->left);
if (tem) { return tem; }
return node;
}
shared_ptr<Node> select(int ran, shared_ptr<Node> node) {
if (node == nullptr) { return NULL; }
int t = size(node->left);
if (ran < t) { return select(ran, node->left); }
else if (ran > t) { return select(ran - t - 1, node->left); }
else { return node; }
}
int rank(int key, shared_ptr<Node> node) {
if (node == nullptr) { return NULL; }
if (key < node->key) { return rank(key, node->left); }
else if (key > node->key) { return size(node->left) + 1 + rank(key, node->right); }
else { return size(node->left); }
}
shared_ptr<Node> deleteMax(shared_ptr<Node> node) {
if (node->right == nullptr) { return node->left; }
node->right = deleteMax(node->right);
node->n = size(node->left) + 1 + size(node->right);
return node;
}
shared_ptr<Node> deleteMin(shared_ptr<Node> node) {
if (node->left == nullptr) { return node->right; }
node->left = deleteMin(node->left);
node->n = size(node->left) + 1 + size(node->right);
return node;
}
shared_ptr<Node> deleteKey(shared_ptr<Node> node,int key) {
if (node == nullptr) { return nullptr; }
if (key < node->key) { node->left = deleteKey(node->left, key); }
else if (key > node->key) { node->right = deleteKey(node->right, key); }
else {
if (node->left == nullptr) { return node->right; }
if (node->right == nullptr) { return node->left; }
shared_ptr<Node>tem = min(node->right);
node->key = tem->key;
node->value = tem->value;
node->right = deleteMin(node->right);
}
node->n = size(node->left) + 1 + size(node->right);
return node;
}
vector<int>& keys(shared_ptr<Node>node, vector<int>& vk, int l, int r) {
if (node == nullptr) { return vk; }
if (l < node->key) { vk = keys(node->left, vk, l, r); }
if (l <= node->key && r >= node->key) { vk.push_back(node->key); }
if (r > node->key) { vk = keys(node->right, vk, l, r); }
return vk;
}
public:
int size() { return size(root); }
int get(int key) { return get(root, key); }
void put(int key, int value) { root = put(key, value, root); }
int max() { return max(root)->key; }
int min() { return min(root)->key; }
int floor(int key) { return floor(key, root)->key; }
int ceiling(int key) { return ceiling(key, root)->key; }
int select(int ran) { return select(ran, root)->key; }
int rank(int key) { return rank(key, root); }
void deleteMax() { root = deleteMax(root); }
void deleteMin() { root = deleteMin(root); }
void deleteKey(int key) { root = deleteKey(root, key); }
vector<int>& keys(vector<int>& vk, int l, int r) { return keys(root, vk, l, r); }
};
这里给出增删改查和切片的函数实现:
- get(shared_ptr *<Node>* node, int key) :查,通过递归,一层层往下查找,如果key不存在则返回Null。
- put(int key, int value, shared_ptr<Node> node):增和改,先一层层向下递归查找,如找到key则对value进行修改,如果查到树底也没有key,就在树底增添一个新节点,并逐层递归更新节点的n。
- deleteKey(shared_ptr<Node> node,int key) :删,一层层向下递归查找,如找到key,在key只有单子树时返回子树,若key有双子树,则用key的右子树的最小值来替换它,然后删除右子树的最小值,并逐层递归更新节点的n。
- keys(shared_ptr<Node>node, vector<int>& vk, int l, int r) :切片,中序遍历二叉树,将key大于等于l,小于等于r的节点的key保存到vector中并返回。
二叉搜索树符号表并不困难,但却可以实现高效的查找和插入操作。其操作的平均时间复杂度均可达到O(1.39lgN)。它基本实现的良好性依赖于其中的key分布足够随机以消除长路径,但在实践应用中,最坏情况仍然会出现,最坏情况时的时间复杂度仍是我们不能接受的。
符号表的红黑二叉搜索树(左偏)实现
在介绍红黑树之前,我先简单介绍一下2-3树:
2-3树由两种结点组成:2-结点就是普通的二叉树节点;3-结点含有两个键和三条链接,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该节点的两个键之间,右链接指向的2-3树的键都大于该结点。
指向空树的链接称为空链接。
如图:
一棵完美平衡的2-3树中所有空链接到根节点的距离都是相同的,而一棵左偏红黑树就可以看成是一颗完美平衡的2-3树。由一个红色左链接相连的两个2-结点表示一个3-结点。
红黑树的结点与二叉树不同,在结点类中多了一个color成员,用来表示该结点的父结点与它链接的颜色。
在给出左偏红黑树的具体实现之前,还需要知道左偏红黑树满足如下标准:
- 红链接均为左链接。
- 没有同时链接两条红链接的结点(包括父链接)。
- 红黑树是黑色平衡的,任意空链接到根节点的路径上的黑链接数量相等。
这里给出完整代码:
//红黑二叉搜索树(左偏)
class RedBlackBST {
struct RBNode{
int key, value;
shared_ptr<RBNode>left, right;
bool color;
int n;
RBNode(int key,int value,int n,bool color):key(key),value(value),n(n),color(color){}
};
shared_ptr<RBNode>root;
bool isRed(shared_ptr<RBNode>node) {
if (node == nullptr) { return 0; }
return node->color;
}
shared_ptr<RBNode> min(shared_ptr<RBNode> node) {
if (node->left == nullptr) { return node; }
else { return min(node->left); }
}
int size(shared_ptr<RBNode> node) { return node ? node->n : 0; }
shared_ptr<RBNode>rotateLeft(shared_ptr<RBNode>node) {
shared_ptr<RBNode>temp = node->right;
node->right = temp->left;
temp->left = node;
temp->color = node->color;
node->color = 1;
temp->n = node->n;
node->n = size(node->left) + 1 + size(node->right);
return temp;
}
shared_ptr<RBNode>rotateRight(shared_ptr<RBNode>node) {
shared_ptr<RBNode>temp = node->left;
node->left = temp->right;
temp->right = node;
temp->color = node->color;
node->color = 1;
temp->n = node->n;
node->n = size(node->left) + 1 + size(node->right);
return temp;
}
void flipColors(shared_ptr<RBNode>node) {
node->color = 1;
node->left->color = 0;
node->right->color = 0;
}
int get(shared_ptr<RBNode>node, int key) {
if (node->value > key) { return get(node->left, key); }
else if (node->value < key) { return get(node->right, key); }
return node->value;
}
shared_ptr<RBNode>put(shared_ptr<RBNode>node, int key, int value) {
if (node == nullptr) { return node = shared_ptr<RBNode>(new RBNode(key, value, 1, 1)); }
if (key < node->value) { node->left = put(node->left, key, value); }
else if (key > node->value) { node->right = put(node->right, key, value); }
else {
node->key = key;
node->value = value;
}
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)) { flipColors(node); }
node->n = size(node->left) + 1 + size(node->right);
return node;
}
shared_ptr<RBNode>deleteMin(shared_ptr<RBNode>node) {
if (node->left == nullptr) { return node->right; }
if (isRed(node->left)) { node->left = deleteMin(node->left); }
else {
if (isRed(node->right)) { node = rotateLeft(node); }
else {
node->color = 0;
node->left->color = 1;
if (node->right) { node->right->color = 1; }
}
node->left = deleteMin(node->left);
}
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)) { flipColors(node); }
node->n = size(node->left) + 1 + size(node->right);
return node;
}
shared_ptr<RBNode>deleteMax(shared_ptr<RBNode>node) {
if (node->right == nullptr) { return node->left; }
if (isRed(node->right)) { node->right = deleteMax(node->right); }
else {
if (isRed(node->left)) { node = rotateRight(node); }
else {
node->color = 0;
node->right->color = 1;
if (node->left) { node->left->color = 1; }
}
node->right = deleteMax(node->right);
}
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)) { flipColors(node); }
node->n = size(node->left) + 1 + size(node->right);
return node;
}
shared_ptr<RBNode>deleteKey(shared_ptr<RBNode>node, int key) {
if (node == nullptr) { return nullptr; }
if (key < node->key) {
if (!isRed(node->left)) {
if (isRed(node->right)) { node = rotateLeft(node); }
else {
node->color = 0;
node->left->color = 1;
if (node->right) { node->right->color = 1; }
}
}
node->left = deleteKey(node->left, key);
}
else if (key > node->key) {
if (!isRed(node->right)) {
if (isRed(node->left)) { node = rotateRight(node); }
else {
node->color = 0;
node->right->color = 1;
if (node->left) { node->left->color = 1; }
}
}
node->right = deleteKey(node->right, key);
}
else {
if (node->left == nullptr) { return node->right; }
if (node->right == nullptr) { return node->left; }
shared_ptr<RBNode>tmp = min(node->right);
node->key = tmp->key;
node->value = tmp->value;
node->right = deleteMin(node->right);
}
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)) { flipColors(node); }
node->n = size(node->left) + 1 + size(node->right);
return node;
}
public:
int size() { return size(root); }
int get(int key) { return get(root, key); }
void put(int key, int value) {
root=put(root, key, value);
root->color = 0;
}
int min() { return min(root)->key; }
void deleteMin() {
root = deleteMin(root);
if (root) { root->color = 0; }
}
void deleteMax() {
root = deleteMax(root);
if (root) { root->color = 0; }
}
void deleteKey(int key) {
root = deleteKey(root, key);
if (root) { root->color = 0; }
}
};
给出增删改查函数实现:
-
红黑树的改和查与普通的二叉树一样:递归查找,key值比较。而红黑树的增删则围绕三种基础操作展开:
左旋(rotateLeft)、右旋(rotateRight)、颜色转换(flipColors)。- 左旋:当操作结点右链接为红,左链接为黑时使用,将右子节点的左树接在操作节点的右子树上,再将操作节点接在保存的右子节点的左子树上,使原来的右子节点作为根节点,从而使红链接移到左侧。
- 右旋:当操作结点左链接为红,右链接为黑时使用,和左链接类似,使红链接移到右侧。
- 颜色转换:当操作结点左右链接均为红时使用,将左右子节点的颜色置为黑,并将操作结点颜色置为红。如果用2-3树解释,则是为了保证树的稳定而做的提升高度操作。
-
put(shared_ptr<RBNode>node, int key, int value):增,和二叉树的增几乎一模一样,但有两点不同:1.在最后插入时链接为红色,以保证该节点的空链接到根节点间的黑色路径与其他空链接相等。2.要在返回上层前进行结点检查,看当前节点是否满足红黑树标准,如不满足,则将红链接向上传递。
-
deleteKey(shared_ptr<RBNode>node, int key) :删,仍是一层层向下递归查找,但在递归的过程中要保证下一次递归到的结点是红结点。如果下一个递归结点不是红结点,则要根据它的兄弟结点的颜色来判断是用右旋还是逆向颜色变换(当前结点变黑,两个字节点变红)来使递归结点变红。在找到key后,在key只有单子树时返回子树,若key有双子树,则用key的右子树的最小值来替换它,然后删除右子树的最小值,并逐层递归依据红黑树标准来恢复结点颜色并更新节点的n。
红黑树符号表虽然实现很困难,但却可以实现比二叉树更高效的查找和插入操作。因为树是平衡的,所以查找比二叉树更快。查找的内循环只会进行一次比较并更新一条链接,非常简短,其查找和插入的平均时间复杂度均可看作O(lgN)。
符号表的哈希表(散列表)实现
散列表又叫哈希表,通过散列函数来确定key和value的存放位置。C++的functional头文件中已经给出保证分步均匀的散列函数算法。下面主要讨论两种基于不同的碰撞处理的散列表实现。
-
基于拉链法的散列表实现
拉链法通过一条储存链表的数组实现。在散列函数将key键转化为数组的秩后,通过对数组对应位置储存的链表进行操作来进行数据的增,删,改,查。
这里给出完整代码:
//拉链法散列表(哈希表)
hash<int> h;
class SparateChainingHashST {
struct Node {
int key, value;
Node* next;
Node(int key,int value):key(key),value(value),next(nullptr){}
};
int sz = 0;
int cpc;
vector<Node*>sTable;
int hash(int key) { return h(key) % cpc; }
public:
SparateChainingHashST(int cpc = 997);
void put(int key, int value);
int get(int key);
void deleteKey(int key);
~SparateChainingHashST();
};
SparateChainingHashST::SparateChainingHashST(int cpc) : cpc(cpc) {
for (int i = 0; i < cpc; ++i) { sTable.push_back(nullptr); }
}
void SparateChainingHashST::put(int key, int value) {
int rank = hash(key);
if (!sTable[rank]) {
sTable[rank] = new Node(key, value);
++sz;
}
else{
Node* tmp = sTable[rank];
while(1){
if (tmp->key == key) {
tmp->value = value;
return;
}
if (tmp->next) { tmp = tmp->next; }
else { break; }
}
tmp->next = new Node(key, value);
++sz;
}
}
int SparateChainingHashST::get(int key) {
Node* tmp = sTable[hash(key)];
for (; tmp; tmp = tmp->next) {
if (tmp->key == key) { return tmp->value; }
}
return NULL;
}
void SparateChainingHashST::deleteKey(int key) {
int rank = hash(key);
Node* tmp;
if (tmp = sTable[rank]) { ;
if (tmp->key == key) {
sTable[rank] = tmp->next;
delete tmp;
--sz;
return;
}
while (tmp->next) {
if (tmp->next->key == key) {
Node* tmp2 = tmp->next;
tmp->next = tmp2->next;
delete tmp2;
--sz;
return;
}
}
}
}
SparateChainingHashST::~SparateChainingHashST() {
for (int i = 0; i < cpc; ++i) {
while (sTable[i]) {
Node* tmp = sTable[i]->next;
delete sTable[i];
--sz;
sTable[i] = tmp;
}
if (sz <= 0) { break; }
}
}
增删改查就是在通过散列函数找到对应位置链表后对链表的增删改查,这里不再赘述。
-
基于线性探测法的散列表实现
线性探测法需要两个数组,一个存放key,一个存放value,在key经散列函数转化为数组的秩后,会根据对应秩处数组所存的值进行处理。在所存值存在且与key不等的情况下,会对下一个位置的值进行重复判断。在储存值为空或储存值与key相等的情况下,才会进行对key和value的操作。
这里给出完整代码:
//线性探测法散列表(哈希表)(key不为0)
hash<int> h;
class LinearProbingHashST {
int sz = 0, cpc;
int* Key, * Value;
int hash(int key) { return h(key) % cpc; }
void resize(int ncpc);
public:
LinearProbingHashST(int cpc = 16) :cpc(cpc),Key(new int[cpc]),Value(new int[cpc]){
for (int i = 0; i < cpc; ++i) {
Key[i] = 0;
Value[i] = 0;
}
}
void put(int key, int value);
int get(int key);
void deleteKey(int key);
~LinearProbingHashST() {
delete[] Key;
delete[] Value;
}
};
void LinearProbingHashST::put(int key, int value) {
if (sz > cpc / 2) { resize(2 * cpc); }
int temp;
for (temp = hash(key); Key[temp]; temp = (temp + 1) % cpc) {
if (Key[temp] == key) {
Value[temp] = value;
return;
}
}
Key[temp] = key;
Value[temp] = value;
++sz;
}
int LinearProbingHashST::get(int key) {
for (int temp = hash(key); Key[temp]; temp = (temp + 1) % cpc) {
if (Key[temp] == key) { return Value[temp]; }
}
return NULL;
}
void LinearProbingHashST::deleteKey(int key) {
int temp = hash(key);
while (Key[temp]) {
if (Key[temp] == key) {
Key[temp] = 0;
Value[temp] = 0;
--sz;
temp = (temp + 1) % cpc;
while (Key[temp]) {
int tkey = Key[temp];
int tvalue = Value[temp];
Key[temp] = 0;
Value[temp] = 0;
--sz;
put(tkey, tvalue);
temp = (temp + 1) % cpc;
}
break;
}
temp = (temp + 1) % cpc;
}
if (sz > 0 && sz < cpc / 8) { resize(cpc / 2); }
}
void LinearProbingHashST::resize(int ncpc) {
int* tKey = new int[ncpc], * tValue = new int[ncpc];
for (int i = 0; i < ncpc; ++i) {
tKey[i] = 0;
tValue[i] = 0;
}
for (int i = 0; i < cpc; ++i) {
if (Key[i]) {
int trank = hash(Key[i]);
int temp;
for (temp = trank; tKey[temp]; temp = (temp + 1) % cpc) {}
tKey[temp] = Key[i];
tValue[temp] = Value[i];
}
}
delete[]Key;
delete[]Value;
Key = tKey;
Value = tValue;
cpc = ncpc;
}
增删改查就像上面所说的那样对数组进行操作,这里也不再赘述。
但是要注意一点:通过均摊分析和内存使用分析,要保证散列表内的元素占散列表容量的1/8到1/2。在这时,线性探测的平均次数在1.5到2.5之间,可以保证散列表时间与空间的综合性能达到最优。
这里给出调整数组大小(resize)的函数实现:
新建两个所需容量数组用于存放key和value,通过散列函数将原有的key和value散列到新数组中。释放原指针保证数组的内存,并将新数组头赋给原指针。设立新的容量值。
散列表通过散列函数实现了极高的搜索和查找性能,达到了惊人的O(1)级别。但数据在散列后不再有序,因此虽然散列表有着极高的性能,但却不是所有领域都适用。