高效检索计算机和网络中的海量信息,是处理它们的前提。本节开始,分三节总结三种经典的数据类型,用于实现高效的查找方法,亦即符号表。该三种数据类型分别为:二叉查找树、红黑树、和散列表。算法实现语言采用C++。
关于符号表的定义:
符号表是一种存储键值对的数据结构,支持两种操作:插入(input),即将一组新的键值对存入表中; 查找(get),即根据给定的键得到相应的值。
表一 一般有序的泛型符号表的API
public class ST< Key, Value> | 功能 |
---|---|
ST() | 创建一张符号表 |
void put(Key key, Value val) | 将键值对存入表中(空值则删除对应键) |
Value get(Key key) | 获取键key对应的值(键不存在则返回null) |
void del(Key key) | 从表中删去键key |
bool isEmpty() | 表是否为空 |
bool contains(Key key) | 键key在表中是否有对应的值 |
int size() | 表中键的数量 |
Key min() | 最小的键 |
Key max() | 最大的键 |
Key floor( Key key) | 小于等于key的最大键 |
Key ceiling(Key key) | 大于等于key的最小键 |
int rank( Key key) | key的排名/小于key的键的数量 |
Key select(int k) | 排名为k的键 |
void delMin() | 删除最小的键 |
void delMax() | 删除最大的键 |
int size(Key lo, Key hi) | [lo…hi]之间键的数量 |
vector keys(Key lo, key hi) | [lo…hi]之间的所有键,已排序 |
vector< Key> keys() | 表中所有键的集合 |
在符号表中,没有指定具体的处理对象类型,而是使用了泛型。这样可以保证应对不同的使用场景。针对不同的对象类型,空键和空值在表达上存在不同。
1、基于无序链表的顺序查找
该方法使用链表结构存储键值对。get()方法顺序地搜索键表查找给定的键,put()方法同样顺序地搜索链表查找给定的键,如果找到则更新关联的值为新值,否则利用给定的键值对创建一个新的结点并将其插入到链表的开头。
复杂度:在含有N个键值对的基于无序链表的符号表中,未命中的查找和插入操作的需要N次比较;命中查找在平均情况下需要N/2次比较,最坏情况为N次比较;向一个空表插入N个不同的键值需要大约 N2/2 N 2 / 2 次比较。
本节内容在利用C++实现过程中,只实现了部分主要方法——该类符号表效率比较低,实用性不高。这里实现该方法的目的是,通过理解简单的方法,可以更好地学习复杂的方法,同时能够更深刻地体会好的数据结构与方法好在哪里。
//c++
// 模板基类
template <class Key, class Value> class ST
{
public:
virtual ~ST(void)
{}
virtual void put(Key key, Value val) = 0;
virtual Value get(Key key) const = 0 ;
virtual int size() const = 0;
virtual void del(Key key) = 0;
virtual bool contains(Key key) const { return false; }
virtual bool isEmpty() const = 0;
};
//////////////////////////////////////////////////////////////////////////
// 派生类
template<class Key,class Value> class SequentialSearchST :
public ST<Key,Value>
{
struct Node
{
Key key;
Value val;
Node* next;
Node(Key key, Value val, Node* next)
{
this->key = key;
this->val = val;
this->next = next;
}
};
Node* root;
int n;
public:
SequentialSearchST() : root(NULL), n(0) { }
SequentialSearchST(Key key, Value val);
~SequentialSearchST()
{
for (int i = 0; i < n; i++)
{
if(root == NULL)
break;
Node* p = first;
root = root->next;
delete p;
}
}
void put(Key key, Value val) ;
Value get(Key key) const ;
int size() const { return n ; }
void del(Key key) ;
bool contains(Key key) const { return get(key) != NULL; }
bool isEmpty() const { return n > 0; }
void print();
// 略。
// Key min();
// Key max();
// Key floor();
// Key ceiling();
// int rank();
// Key select( int k);
// void delMax();
// void delMin();
};
///////////////////////////////////////////////
template<class Key, class Value>
SequentialSearchST<Key, Value>::SequentialSearchST(Key key, Value val)
{
root = new Node(key,val,NULL);
n = 1;
}
//----------------------------------------------
template<class Key, class Value> Value
SequentialSearchST<Key, Value>::get(Key key) const
{
if( 0 == n ) return NULL;
for (Node* x = root; x != NULL; x = x->next )
{
if( key == x->key)
return x->val;
}
return NULL;
}
//----------------------------------------------
template<class Key, class Value> void
SequentialSearchST<Key, Value>::put(Key key, Value val)
{
for (Node* x = root; x != NULL ; x = x->next )
{
if( key == x->key)
{
x->val = val;
return;
}
}
// 新结点作为根节点
Node* p = new Node(key,val,root);
root = p;
n++;
}
//----------------------------------------------
// 删除某结点时,需要将其父结点指向其子节点。
// 当删除结点为根节点root时,由于没有父结点,做特殊处理。
template<class Key, class Value> void
SequentialSearchST<Key, Value>::del(Key key)
{
if( 0 == n ) return ; // 空链表 返回
if( root->key == key )
{ Node* p = root; root = root->next; delete p; n--; }
else
{
Node* x = root->next;
Node* parent = root;
while( x )
{
if( x-> key == key )
{
parent->next = x->next;
delete x; n--;
break;
}
else
{
x = x->next;
parent = x;
}
}
}
}
//----------------------------------------------
template<class Key, class Value> void
SequentialSearchST<Key, Value>::print()
{
if( 0 == n ) return ; // 空链表 返回
for (Node* x = root; x != NULL ; x = x->next)
{
std::cout<< x->key <<" "<< x->val<<"\n ";
}
std::cout<<std::endl;
}
////////////////////////////////////////
// 测试方法
// 输入文件名和一个表示字符最小长度的整数,读取文件中字符串,将不小于
// 设定长度的字符串作为键值存入符号表中,对应的值为字符串出现的次数。
void readTxt(string filename, int minlen);
int main()
{
int minlen = 0;
string file;
cout<< "input minLen & file: \n";
cin >> minlen >> file;
readTxt(file,minlen);
}
void readTxt(string file, int minlen)
{
ifstream infile;
infile.open(file.data()); //将文件流对象与文件连接起来
if(!infile.is_open())
{
std::cout<<"Open file err. \n";
return;
}
string s;
SequentialSearchST<string,int> *st =
new SequentialSearchST<string,int>();
int nline = 0;
while(getline(infile,s))
{
nline++;
vector<string> vstr;
splitStr(s,vstr," ");
// for(int i = 0; i < vstr.size(); i++ )
// cout << vstr[i] << ",";
// cout<<endl;
for(int i = 0; i < vstr.size(); i++ )
{
if(vstr[i].length() < minlen)
continue;
int val = st->get(vstr[i]);
st->put(vstr[i],val+1);
}
}
// st->print();
cout <<"Size: "<< st->size() << endl;
cout <<"get: "<< st->get("business") <<endl;
st->del("business");
cout <<"del: "<< st->contains("business") << endl;
cout <<"Size: "<< st->size() << endl;
fin.close();
}
>>>>>>>>>
>output:
input minLen & file:
8 tale.txt
Size: 5126
get: 122
del: 0
Size: 5125
请按任意键继续. . .
测试数据与说明
数据下载: https://pan.baidu.com/s/1uZEhnlWB7kh6xk3vA018Dg 密码: ced7
表二 大型测试输入流性质
tinyTale.txt | tale.txt | leipzig1M.txt | |
---|---|---|---|
不同的单词数 | 不同的单词数 | 不同的单词数 | |
所有单词 | 20 | 10,674 | 534,580 |
长度大于等于8的单词 | 3 | 5,126 | 299,593 |
长度大于等于10的单词 | 2 | 2,257 | 165,555 |
2、基于有序数组的二分查找
与无序链表不同,有序数组利用两个数组的连续内存来分别保存键和值。使用有序数据保存键的好处是,二分查找法利用数组的索引进行键的查找可以大大减少键的比较次数。具体实现见C++代码。
//////////////////////////////////////////////////////////////////////////
template<class Key,class Value> class BinarySearchST : public ST<Key,Value>
{
Key *keys;
Value *vals;
int N;
public:
BinarySearchST():N(0),capacity(2)
{
keys = new Key[capacity];
vals = new Value[capacity];
}
BinarySearchST(int capacity)
{
keys = new Key[capacity];
vals = new Value[capacity];
N = 0;
this->capacity = capacity;
}
~BinarySearchST()
{
delete[] keys;
delete[] vals;
}
int rank(Key key) const;
void put(Key key, Value val) ;
Value get(Key key) const ;
int size() const { return N; }
void del(Key key) ;
bool contains(Key key) const;
bool isEmpty() const { return N == 0; }
void print();
};
////////////////////////////////////////////////
//----------------------------------------------
template<class Key,class Value> void
BinarySearchST<Key,Value>::put(Key key, Value val)
{
if( capacity/2 < N) // resize the memory
{
capacity = 2*capacity;
Key* larger_keys = new Key[capacity];
Value* larger_vals = new Value[capacity];
for (int i= 0; i < N; i++)
{
larger_keys[i] = keys[i];
larger_vals[i] = vals[i];
}
delete [] keys;
delete [] vals;
keys = larger_keys;
vals = larger_vals;
}
int i = rank(key);
// already exist
if( contains(key) )
{
vals[i] = val; return;
}
//get a place for new key, move the front to the end
for(int j = N-1; j >= i; j--)
{
keys[j+1] = keys[j];
vals[j+1] = vals[j];
}
keys[i] = key; vals[i] = val; // add new key
N++;
}
//----------------------------------------------
template<class Key, class Value> Value
BinarySearchST<Key,Value>::get(Key key) const
{
if(isEmpty())
return 0; // init to 0 is important
if( !contains(key) )
return 0; // init to 0 is important
int i = rank(key);
return vals[i];
}
//----------------------------------------------
template<class Key, class Value> int
BinarySearchST<Key,Value>::rank(Key key) const
{
int lo = 0, hi = N-1;
while ( lo <= hi )
{
int mid = lo + (hi - lo)/2; // (lo + hi)/2 may by overflow
if( key > keys[mid] )
lo = mid + 1;
else if( key < keys[mid] )
hi = mid - 1;
else
return mid;
}
// hi may be less than 0
// and do not check it when use( get , put method )
return lo;
}
//----------------------------------------------
template<class Key, class Value> bool
BinarySearchST<Key,Value>::contains(Key key) const
{
if(isEmpty()) return false;
int i = rank(key);
if( i < N && keys[i] == key) return true;
else return false;
}
//----------------------------------------------
template<class Key, class Value> void
BinarySearchST<Key,Value>::del(Key key)
{
if( isEmpty() ) return ;
if( !contains(key) ) // not found
return;
int i = rank(key);
for(int j = i; j < N-2; j++)
{
keys[j] = keys[j+1];
vals[j] = vals[j+1];
}
keys[N-1] = keys[N];
vals[N-1] = vals[N];
N--;
}
//----------------------------------------------
template<class Key, class Value> void
BinarySearchST<Key,Value>::print()
{
for (int i = 0; i < N; i++)
std::cout<<"Key: "<<keys[i]<<", Value: "<<vals[i]<<std::endl;
std::cout<<endl;
}
// 测试方法
将上之前的测试方法中的符号表定义
SequentialSearchST<string,int> *st =
new SequentialSearchST<string,int>();
改成:
BinarySearchST<string,int> *st =
new BinarySearchST<string,int>();
即可。
>>>>>>>
>output
input minLen & file:
8 tale.txt
Size: 5126
get: 122
del: 0
Size: 5125
请按任意键继续. . .
基于有序数组的二分查找符号表,其实现的核心思想即是利用排名函数 int rank(Key key) i n t r a n k ( K e y k e y ) 获取待插入的键在现有键表中的位置。然后通过移位操作腾出对应的数组位置,用于保存新的键值对。
复杂度:在N个键的有序数组中进行二分查找最多需要比较的次数大约为 lgN l g N (数组两段元素); 向一个空符号表中插入N个元素在最坏情况下访问数组次数约为 N2 N 2 (键倒序插入); 向大小为N的有序数组中插入一个新的元素需要访问数组的次数在最坏情况下约为2N次(数组元素全部后移)。
符号表的实现这里将要介绍6种方法,先给出一个简单预览,有一个更好的整体认识。
表三 符号表的各种实现的优缺点
数据结构 | 实现 | 优点 | 缺点 |
---|---|---|---|
链表(顺序查找) | SequentialSearchST | 适用于小型问题 | 大型符号表很慢 |
有序数组(二分查找) | BinarySearchST | 最优的查找效率和空间要求,可以进行有序性相关操作 | 插入操作很慢 |
二叉查找树 | BST | 实现简单,可以进行有序性相关操作 | 没有性能上界的保证,链接需要额外空间 |
平衡二叉查找树 | RedBlackBST | 最优的查找和插入效率,可以进行有序性相关操作 | 链接需要额外空间 |
散列表 | SeparateChainHashST LinearProbingHashST | 能够快速地查找和插入常见的数据 | 需要计算类型数据的散列 无法进行有序性相关操作链接和空结点需要额外空间 |
水平有限,如有错误欢迎指正。下一节内容将介绍BST。
5-15-2018 By Unicorn Lewis