本文将描述用于一个简单数据库的接口——字典,并定义其ADT,实现在数据库中存储、查询和删除记录的功能。可以将本文实现的ADT作为一个基准,与用来实现字典的各种数据结构相比较。
关键码(key)和可比对象(comparable)
- 一条记录的唯一标识作为被检索对象,这个标识称为关键码。
- 为了实现检索功能,关键码还应具有可比性,即对于至少对于两个关键码,能够判断两者是否相等。一般来说,要求能对关键码定义一个全序,即任意两个同类型关键码都能比较大小。
- 使用具有全序关系的关键码类型,可以更好组织数据库从而提高检索效率。比如,在一个有序序列中使用二分法检索。
- 多数数据类型都有天然的序,如integer、float、double和character string等。
对于多维域,我们则可以利用其多维特征为其定义一个序
1。
字典ADT
template <typename Key, typename E>
class Dictionary
{
private:
void operator =(const Dictionary&) {}
Dictionary(const Dictionary&) {}
public:
Dictionary() {}
virtual ~Dictionary() {}
virtual void clear() = 0;
virtual void insert(const Key& k, const E& e) = 0;
virtual E remove(const Key& k) = 0;
virtual E removeAny() = 0;
virtual E find(const Key& k) const = 0;
virtual int size() = 0;
};
注:
- insert和find函数为这个类的核心,clear用于把字典重新初始化。
- removeAny函数目的是允许用户随意
遍历
整个字典,使用户得到其不知道关键码的记录。 - 这里的
void operator =(const Dictionary&) {}
和Dictionary(const Dictionary&) {}
用private保护起来,说明该字典ADT不支持=赋值复制
以及构造函数复制
。
键-值对
对于一个保存了很多相同类型记录的数据库,用户可能有从不同角度对它进行检索的需求。这一需求对应到字典中的基本操作就是找到与关键码值相对应的记录。因此,我们需要有一种机制,来保证关键码的类型足够泛化。
在这里,我们采用将关键码和值存储成相关联的关系的方法:字典中任何一个基本元素包含了一条记录以及与该记录相关的关键码,这就是所谓的键-值对(KVpair)。下面我们给出KVpair类的实现:
template <typename Key, typename E>
class KVpair
{
private:
Key k;
E e;
public:
KVpair() {}
KVpair(Key kval, E eval) { k = kval; e = eval; }
/* Copy constructor */
KVpair(const KVpair& o) { k = o.k; e = o.e; }
/* Assignment operator */
void operator =(const KVpair& o) { k = o.k; e = o.e; }
/* Data member access functions */
Key key() { return k; }
void setKey(Key ink) { k = ink; }
E value() { return e; }
};
字典(顺序表实现)
定义了字典ADT,并考虑好如何存储键-值对后,现在就可以考虑实现它的方法了。可以通过两种方法实现:顺序表或链表,本文采用前者。针对字典执行insert和find的比例不同,我们分别给出顺序表字典的两种实现:无序顺序表字典、有序顺序表字典。
无序顺序表字典
template <typename Key, typename E>
class UALdict : public Dictionary<Key, E>
{
private:
AList<KVpair<Key, E> >* list; //注意这里两个“>”间有一个空格,否则变成位运算符
public:
UALdict(int size=defaultSize) // Constructor
{ list = new Alist<KVpair<Key, E> >(size); }
~UALdict() { delete list; } // Destructor
void clear() { list->clear(); } //Reinitialize
void insert(const Key&k, const E& e)
{
KVpair<Key, E> temp(k, e);
list->append(temp);
}
E remove(const Key& k)
{
E temp = find(k);
if(temp != NULL) list->remove();
return temp;
}
E find(const Key& k) const
{
for(list->moveToStart(); list->currPst() < list->length(); list->next())
{
KVpair<Key, E> temp = list->getValue();
if(k == temp.key()) return temp.value();
//注:value是指向记录的指针
}
return NULL;
}
int size() { return list->length(); }
};
注:这里UALdict和AList是has-a依赖关系,UALdict继承的是ADT类Dictionary。
有序顺序表字典
实现字典的另一种可选方法是使用有序线性表。使用此法的优点是可以通过二分法来提高find的检索速度。
有序表和无序表不同,有序表不允许用户控制在什么位置上插入元素,所以一个有序表的插入操作与无序表有很大差别。另外,在有序表中不允许用户随意在表尾插入记录(append操作)。由此可以看出,需要在List ADT的基础上定义一个新类,但是有序表不能通过直接继承List ADT来实现
。
SAList (sorted array list)
template <typename Key, typename E>
class SAList: protected AList<KVpair<Key, E> >
{
public:
SAList(int size=defaultSize) : AList<KVpair<Key, E> >(size) {}
~SAList() {}
void insert(KVpair<Key, E>& it)
{
KVpair<Key, E> curr;
for(moveToStart(); currPos() < length(); next())
{
curr = getValue();
if(curr.key() > it.key()) break;
}
AList<KVpair<Key, E> >:: insert(it);
}
AList<KVpair<Key, E> >:: clear;
AList<KVpair<Key, E> >:: remove;
AList<KVpair<Key, E> >:: moveToStart;
AList<KVpair<Key, E> >:: moveToEnd;
AList<KVpair<Key, E> >:: prev;
AList<KVpair<Key, E> >:: next;
AList<KVpair<Key, E> >:: length;
AList<KVpair<Key, E> >:: currPos;
AList<KVpair<Key, E> >:: moveToPos;
AList<KVpair<Key, E> >:: getValue;
};
保护性继承
从上面代码可以看到,类SAList不直接继承类AList,但是它把类AList作为一个受保护基类
来使用。
这样,AList函数对SAList的用户并不直接有效
:AList的许多函数对SAList的用户是有用的。因此,大多数AList函数可以不加修改地直接传递给SAList的用户,例如:AList<KVpair<Key, E> >:: remove;
;但是,insert函数被一个新的函数替换,并且AList的append函数被隐藏 起来。
SALdict
template <typename Key, typename E>
class SALdict : public Dictionary<Key, E>
{
private:
SAList<Key, E>* list;
public:
SALdict(int size=defaultSize)
{ list = new SAList<Key, E>(size); }
~SALdict() { delete list; }
void clear() { list->clear(); }
void insert(const Key&k, const E& e)
{
KVpair<Key, E> temp(k, e);
list->insert(temp);
}
E remove(const Key& k)
{
E temp = find(k);
if(temp != NULL) list->remove();
return temp;
}
E removeAny()
{
Assert(size() != 0, "Dictionrary is empty");
list->moveToEnd();
list->prev();
KVpair<Key, E> e = list->remove();
return e.value();
}
E find(const Key& k) const
{
int l = -1;
int r = list->length();
while(l+1 != r)
{
int i = (l+r)/2;
list->moveToPos(i);
KVpair<Key, E> temp = list->getValue();
if(k < temp.key()) r = i;
if(k == temp.key()) return temp.value();
if(k > temp.key()) l = i;
}
return NULL;
}
int size() { return list->length(); }
};
注: 有序表中find函数使用了二分法检索,代价为O(logn),相较于无需表的find函数有很大改善。遗憾的是,insert函数的时间代价由无序表的常数时间变为有序表的O(n)。字典ADT的有序表实现是否比无序表更有效取决于执行insert和find的相对次数。如果find操作远多于insert操作,则值得使用有序表来实现字典。
比较类
对于给定的两个关键码,没有很好的办法进行比较。一种可能的办法是简单地利用==,<=, >=。如果关键码的数据类型是int,这个方法可以很好的解决问题。但是如果关键码的数据类型是字符串指针,又或者是其他一些类型,则可能无法给出人们想要的结果。
C++支持运算符重载,因此可以要求使用字典的用户对其给出的关键码数据类型的 ==、<=、>=三个运算符进行重载。这个要求可能变成使用者的一种义务。而这种义务隐藏在字典类的代码中,并没有体现在字典的接口中,因此很多使用者会忽略这个义务,从而导致很多莫名其妙的错误,这种错误编译器无法发现。
一种灵活的解决办法是:让用户自己提供定义,用这个定义来比较关键码与记录,或者比较记录与记录。把这样的定义作为模板的一个参数,那么提供比较运算符就成为该接口的部分。这样的设计称为“策略”模式,因为用户要明确提供做某些操作的策略。在某些情况下,利用比较类(comparator class)从记录中抽取关键码
,比存储键-值对
更合适。
下面给出两个比较类的例子:
// Comparator class for integer keys
class intintCompare
{
public:
static bool lt(int x, int y) { return x < y; }
static bool eq(int x, int y) { return x == y; }
static bool gt(int x, int y) { return x > y; }
};
// Compare two character strings
class CCCompare
{
public:
static bool lt(char* x, char* y)
{ return strcmp(x, y) < 0; }
static bool eq(char* x, char* y)
{ return strcmp(x, y) == 0; }
static bool gt(char* x, char* y)
{ return strcmp(x, y) > 0; }
};
注:比较类的应用十分广泛,在堆中的比较、排序算法等很多程序设计中都会用到,请特别重视。