跳表和散列
- 跳表:增加了额外的向前指针的链表。采用随机技术来决定链表的哪些节点应该增加向前指针,以及增加多少个指针。
- 散列:用来查找、插入、删除的另一种随机方法
10.1 字典
10.2抽象数据类型
ADT
C++抽象类
# include<utility>//提供std::pair结构,使用pair.first与pair.second来访问数据
template<class K, class E>
class dictionary
{
public:
virtual ~dictionary(){}
//返回true,当且仅当字典为空
virtual bool empty() const = 0;
//返回字典中数对的数目
virtual int size() const = 0;
//返回匹配数对的指针
virtual pair<const K,E>* find(const K&) const = 0;
//删除匹配的数对
virtual void erase(const K&) = 0;
//往字典中插入一个数对
virtual void insert(const pair<const K,E>&) = 0;
};
10.3线性表描述
- 字典的线性结构描述
- 线性表描述
跳表描述- 散列表描述
数组描述
链表描述
template<typename K,typename E>
struct pairNode{
pair<const K,E>element;
pairNode<const K,E>* next;
};
类sortedChain
template<typename K, typename E>
class sortedChain : public dictionary<K, E>
{
protected:
pairNode<const K, E>* firstNode;//指向链表第一个节点的指针
int dSize;//表中的数对个数
public:
sortedChain() { firstNode = NULL; dSize = 0; }
~sortedChain();
bool empty() const { return dSize == 0; }
int size() const { return dSize; }
//返回关键字theKey匹配的数对的指针,若不存在匹配的数对,则返回NULL
pair<const K, E>* find(const K& theKey) const;
//插入一个数对thePair,覆盖已经存在的数对
void insert(const pair<const K, E>& thePair);
//删除关键字theKey匹配的数对
void erase(const K& theKey);
};
方法find
按给定键值搜索数对,若找到则返回对应数对指针,没有则返回空指针
template<class K, class E>
pair<const K,E>* sortedChain<K,E>::find(const K& theKey) const
{
pairNode<K,E>* currentNode = firstNode;
//搜索关键字为theKey的数对
//循环结束时有三种可能:1.current指向节点包含一个大于目标键值的数据对;2.包含一个等于目标键值的数据对;3.指向空,链表结束
while(currentNode != NULL && currentNode->element.first != theKey)
currentNode = currentNode->next;
//判断是否匹配
if(currentNode != NULL && currentNode->element.first == theKey)
//找到
return ¤tNode->element;
//无匹配的数对
return NULL;
}
方法insert
- 插入给定数对p到链表中
- 若已有节点包含对应键值,更新其值
- 否则插入一个新节点
- 按find中方法找到插入位置时,需要同时定位前置节点
template<class K,class E>
void sortedChain<K,E>::insert(const pair<const K,E>& thePair)
{//往字典中插入thePair,覆盖已经存在的匹配的数对
pairNode<K,E> *p = firstNode, *tp = NULL;//跟踪p
//移动指针tp,使thePair可以插在tp的后面
//tp为p的前置节点
while(p != NULL && p->element.first < thePair.first)
{
tp = p;
p = p-> next;
}
//检查是否有匹配的数对
//有匹配的数对
while(p != NULL && p->element.first == thePair.first)
{//替换旧值
p->element.second = thePair.second;
return;
}
//无匹配的数对,为thePair建立新节点
pairNode<K,E> *newNode = new pairNode<K,E>(thePair, p);
//在tp之后插入新节点
if(tp == NULL) firstNode = newNode;
else tp->next = newNode;
dSize++;
return;
}
方法erase
- 若有该键值,删除
- 没有,不做操作
template<class K, class E>
void sortedChain<K,E>::erase(const K& theKey)
{//删除关键字为theKey的数对
pairNode<K,E> *p = firstNode, *tp = NULL;
//搜索关键字为theKey的数对
//tp为p的前置节点
while(p !=NULL && p->element.first < theKey)
{
tp = p;
p = p->next;
}
//确定是否匹配
if(p != NULL && p->element.first == theKey)
{//找到一个匹配的数对
//从链表中删除p
if(tp == NULL) firstNode = p->next;//p是第一个节点(头节点)
else tp->next = p->next;
delete p;
dSize--;
}
}
10.5散列表描述
10.5.1理想散列
- 散列方法:
- 理想散列:假定散列表中的一个位置最多存放一个数对,每一个可能的键值也映射到唯一的位置。
10.5.2散列函数和散列表
- 桶(bucket):散列表的每一个位置叫一个桶
- 起始桶(home bucket):对关键字为k的数对,f(k)是起始桶
- 散列表的长度或大小:桶的数量。
- 在除法散列中,桶的数量为D
- 冲突和溢出
- 冲突(collision):两个不同的关键字起始桶号相同时就出现冲突
- 溢出(overflow):存储桶中若没有空间时就发生溢出
- 解决溢出的方法:
- 线性探查
- 链式散列
- 解决溢出的方法:
- 当每个桶只能存储一个数对时,碰撞和溢出就会同时发生
10.5.3线性探查
类hashTable
template<class K,class E>
class hashTable <K,E>:public dictionary<K,E>
{
public:
hashTable(int theDivisor = 11);
~hashTable() { delete[] table; }
bool empty() const { return dSize == 0; }
int size() const { return dSize; }
//返回关键字theKey匹配的数对的指针,若不存在则返回NULL
pair<const K,E>* find(const K&)const;
//在字典中插入一个数对thePair,若存在关键字相同的数对,则覆盖
void insert(const pair<const K,E>&);
protected:
int search(const K&)const;
pair<const K,E>**table;//散列表
int divisor;//散列函数的除数
hash<K>hash;//把类型k映射到一个非负整数
int dSize;//字典中数对的个数
}
构造函数
template<class K, class E>
hashTable<K,E>::hashTable(int theDivisor)
{
divisor = theDivisor;
dSize = 0;
//分配和初始化散列表数组
table = new pair<const K,E>* [divisor];
for(int i = 0; i < divisor; i++)
table[i] = NULL;
}
方法search
template<class K, class E>
int hashTable<K,E>::search(const K& theKey) const
{//搜索一个公开地址散列表,查找关键字为theKey的数对;
//如果匹配的数对存在,返回它的位置
//否则,如果散列表不满,则返回关键字为theKey的数对可以插入的位置
int i = (int)hash(theKey) % divisor;//起始桶
int j = i;//从起始桶开始
do
{
if(table[j]==NULL || table[j]->first == theKey)//注意条件顺序
return j;
j = (j+1) % divisor;//下一个桶
}while (j != i);//是否返回到起始桶?
return j;//表满
}
方法find
template<class K, class E>
pair<const K,E>* hashTable<K,E>::find(const K& theKey) const
{//返回匹配数对的指针,如果匹配数对不存在,则返回NULL
//搜索散列表
int b = search(theKey);
//判断table[b]是否是匹配数对
if(table[b] == NULL || table[b]->first != theKey)
return NULL;//没有找到
return table[b];//找到匹配数对
}
方法insert
template<class K, class E>
void hashTable<K,E>::insert(const pair<const K,E>& thePair)
{//把数对thePair插入字典,若存在关键字相同的数对,则覆盖;若表满,则抛出异常
//搜索散列表,查找匹配的数对
int b = search(thePair.first);
//检查匹配的数对是否存在
//情况2:直接插入新数对
if(table[b] == NULL)
{
//没有匹配的数对,而且表不满
table[b] = new pair<const K,E>(thePair);
dSize++;
}
else
{//检查是否有重复的关键字数对或是否表满
//情况1,修改数对中的值
if(table[b]->first == thePair.first)
{
table[b]->second = thePair.second;
}
else
//情况三,无法插入
throw hashTableFull();//表满
}
}
方法erase
性能分析
- 除余散列中除数D的选择
10.5.4链式散列
类hashChains
template<class K, class E>
class hashChains : public dictionary<K, E> {
protected:
sortedChain<K,E> *table;//直接复用sortedChain为桶内链表
int divisor;
hash<K> hash;
int dSize;
public:
hashChains(int theDivisor = 11) : divisor(theDivisor), dSize(0)
{ table = new sortedChain<K,E>[divisor]; }
~hashChains() { delete [] table; }
bool empty() const { return dSize == 0; }
int size() const { return dSize; }
pairNode<const K,E>* find(const K& theKey) const
{ return table[hash(theKey)% divisor].find(theKey); }//使用哈希函数定位到桶后直接搜索
void insert(const pair<const K,E>& thePair);
void erase(const K& theKey)
{ table[hash(theKey) % divisor].erase(theKey); //使用哈希函数定位到桶后直接删除
方法insert
template<class K, class E>
void hashChains<K,E>::insert(const pair<const K, E> &thePair)
{
int homeBucket = (int) hash(thePair.first) % divisor;
int homeSize = table[homeBucket].size();
table[homeBucket].insert(thePair);
//插入操作有可能只是修改存在键值,需要判断链表长度是否变化
if ( table[homeBucket].size() > homeSize )
dSize++;
}
与线性探查比较
-
空间
-
时间复杂性
- 平均性能:
- 平均性能:
10.4 跳表表示
10.6应用——文本压缩
- LZW(Lempel、Ziv、Welch)压缩文本
- 基于原始数据,创建一个字典,字典中存放文本中字符串到编码的映射。压缩时用字典中的编码来代替原始数据中的相应字符串
- 规则:
- 开始,为该文本文件中所有可能出现的字母分配一个代码,构成初始字典
- LZW压缩器不断地在输入文件的未编码部分中寻找在字典中出现的最长的前缀p,输出前缀p相应的代码,若输入文件中的下一个字符为c,则为pc分配一个代码,并插入字典
示例:
10.6.2 LZW压缩的实现
//设当前前缀p为空,读取下一个字符c;
循环(当前字符串pc不为空)
{
if(当前字符串pc在字典中)
当前前缀p = 当前字符串pc;
else
{
将当前字符串pc插入到字典中;
输出当前前缀p的编码;
p=c;
}
读取下一个字符c;
}
输出最后一个当前前缀p的编码;
10.6.3 LZW解压缩方法
- 解压时,需要重新构建字典
- 把分配给单一字母的代码插入字典中
- 输入第一个代码,用相应的文本(第一个代码一定对应于一个单一的字母)代替
- 设当前输入代码为p,q为p前面的代码。两种情况:1)在字典中;2)不在字典中
- 1)当p在字典中时
- 找到与p相关的文本
text(p)
并输出 text(q)fc(p)
插入字典
- 找到与p相关的文本
- 2)当p不在字典中, 此时只会是一种情况,
text(p)=text(q)fc(q)
- 输出
text(q)fc(q)
,(p,text(p))
插入字典 - qp对应文本串:
text(q)text(q)fc(q)
实现:
- 1)当p在字典中时
(1)把分配给单一字母的代码插入字典中。
(2)输入第一个代码q,输出相应的文本(第一个代码一定对应于一个单一的字母)(3)循环:
输入下一个代码p;
if(p在字典中)
{
输出代码p对应的文本串text(p);
将text(q)fc(p)及代码插入到字典中
}
else
{
text(p)=text(q)fc(q);
输出代码p对应的文本串text(q)fc(q);
将text(q)fc(q)及代码插入到字典中
}
q=p;