数据结构——跳表和散列

  • 虽然在n个元素的有序数组上折半查找所需要的时间为O(logn),但是在有序链表上查找所需要的时间为O(n)。
  • 为了提高有序链表的查找性能,可以在全部或部分节点上增加额外的指针。在查找时,通过这些指针,可以跳过链表的若千个节点,不必从左到右连续查看所有节点。
  • 增加了额外的向前指针的链表叫做跳表(skiplist)。它采用随机技术来决定链表的哪些节点应增加向前指针,以及增加多少个指针。基于这种随机技术,跳表的查找、插入、删除的平均时间复杂度为O(logn)。然而,最坏情况下的时间复杂度却变成O(n)。
  • 散列是用来查找、插入、删除的另一种随机方法。与跳表相比,它把操作时间提高Θ(1),但最坏情况下的时间仍为Θ(n)。
  • C++的STL中使用了散列的容器类有: hash_ map、hash_multimap、hash_ multiset、 hash_ set。

有序数组、有序链表、跳表和哈希表的渐近性能汇总

数据结构——跳表和散列_第1张图片

字典

抽象类dictionary
template<class Kclass E>
class dictionary
{
public:
	virtual ~dictionary() {}
	virtual bool empty() const = 0;
	//返回true, 当且仅当字典为空
	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;
	//往字典中插入一个数对
};

线性表描述

  • sortedChain的节点是pairNode的实例。pairNode 的实例有两个域:element 和next, 它们的类型分别是 pair 和 pairNode*。
方法sortedChain::find
template<class Kclass E>
pair<const K,E>* sortedChain<K,E>:: find(const K& theKey) const
{//返回匹配的数对的指针
	//如果不存在匹配的数对,则返回NULL
	pairNode<K,E>* currentNode = firstNode;
	//搜索关键字为theKey的数对
	while (currentNode != NULL && currentNode->element. first != theKey)
		currentNode = currentNode->next;
	//判断是否匹配
	if (currentNode != NULL && currentNode->element. first == theKey)
		//找到匹配数对
		return &currentNode->element;
	//无匹配的数对
	return NULL;
}
方法sortedChain::insert
template<class Kclass E>
void sortedChain<K,E>: :insert (const pair<const K,E>& thePair)
{//往字典中插入thePair,覆盖已经存在的匹配的数对
	pairNode<K,E> *p = firstNode , *tp = NULL;
	//移动指针tp,使thePair可以插在tp的后面
	while (P != NULL && p->element.first < thePair. first)
	{
		tp=p;
		P = p->next;
	}
	//检查是否有匹配的数对
	if (P != NULL G& 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;
}
方法sortedChain::erase
template<class Kclass E>
void sortedChain<K,E>::erase (const K& theKey)
{//删除关键字为theKey的数对
	pairNode<K,E> *p = firstNode, *tp = NULL;
	//搜索关键字为theKey的数对
	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--;
	}
}

散列表描述

  • 字典的另一种表示方法是散列( hashing)。它用一个散列函数(也称哈希函数)把字典的数对映射到一个散列表( 也称哈希表)的具体位置。
  • 如果数对p的关键字是k,散列函数为f, 那么在理想情况下,p在散列表中的位置为f(k)。

散列函数

  • 散列表的每一个位置叫一个( bucket ); 对关键字为k的数对:f(k) 是起始桶( home bucket);桶的数量等于散列表的长度或大小。因为散列函数可以把若千个关键字映射到同一个桶,所以桶要能够容纳多个数对。
  • 本章我们考虑两种极端情况。第一种情况是每一个桶只能存储一个数对。第二种情况是每一个桶都是一个可以容纳全部数对的线性表。
除法散列函数
  • 在多种散列函数中,最常用的是除法散列函数,它的形式如下: f(k)=k%D
  • 其中k是关键字,D是散列表的长度(即桶的数量),%为求模操作符。
  • 散列表的位置索引从0到D-1。
  • 当D=11 时,与关键字3,22,27,40,80,96分别对应的起始桶是f(3)=3, f(22)=0,f(27)=5, f(40)=7、f(80)=3 和f(96)=8。
  • 起始桶的值都在范围内
均匀散列函数
  • 函数f(k)=k%D对范围[0,r]内的关键字是均匀散列函数,其中r是正整数。
  • 当D可以被诸如3、5和7这样的小奇数整除时,不会产生良好散列函数。因此,要使除法散列函数成为良好散列函数,**你要选择的除数D应该既不是偶数又不能被小的奇数整除。理想的D是一个素数。**当你不能用心算找到一个接近散列表长度的素数时,你应该选择.不能被2和19之间的数整除的D。
  • 因为在实际应用的字典中,关键字是相互关联的,所以你所选择的均匀散列函数,其值应该依赖关键字的所有位(而不是仅仅取决于前几位,或后几位,或中间几位)。当使用除法散列函数时,选择除数D为奇数,可以使散列值依赖关键字的所有位。当D既是素数又不能被小于20的数整除, 就能得到良好散列函数。
把一个字符串转换为一个不唯一的整数
int stringToInt(string s)
{//把s转换为一个非负整数,这种转换依赖s的所有字符
	int length = (int) s. length() ;
	//s中的字符个数
	int answer = 0;
	if (length%2 == 1)
	{//长度为奇数
		answer = s.at(length - 1) ;
		length--;
	}
	//长度是偶数
	for(int i=0;i<length;i+=2)
	{//同时转换两个字符
		answer += s.at(i) ;
		answer += ((int) s.at(i + 1)) << 8;
	}
	return (answer < 0) ? -answer : answer ;
}

线性探查

  • 解决溢出的方法叫作线性探查( Linear Probing )。在寻找下一个可用桶时,散列表被视为环形表。
  • 假设要查找关键字为k的数对,首先搜索起始桶f(k),然后把散列表当做环表继续搜索下一个桶,直到以下情况之一发生为止:
  • 1。存有关键字k的桶已找到,即找到了要查找的数对;
  • 2。到达一个空桶;
  • 3。又回到起始桶f(k)。
  • 后两种情况说明关键字为k的数对不存在。
  • 删除需要移动若干个数对。从删除位置的下一个桶开始,逐个检查每个桶,以确定要移动的元素,直至到达一个空桶或回到删除位置为止。在做删除移动时,一定要注意,不要把一个数对移到它的起始桶之前,否则,对这个数对的查找就可能失败。
线性探查的C++实现,hashTable 的数据成员和构造函数
  • 这个类使用了线性探查法。注意,散列表用一维数组table[]表示,类型为pair*。
//散列表的数据成员
pair<const K,E>** table; //散列表
hash<K> hash; //把类型K映射到一个非整数
int dSize; //字典中数对个数
int divisor; //散列函数除数

//构造函数
template<class Kclass 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;
}
hashTable::search
template<class Kclass 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 1 table[jl->first == theKey)
			return j ;
		j =(j+ 1)%divisor; //下一个桶
	}while(j!=i); //是否返回到起始桶?
	return j ; //表满
}
hashTable::find
template<class Kclass 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]; //找到匹配数对
hashTable::insert
template<class Kclass E>
void hashTable<K, E>::insert(const pair<const K,E>& thePair)
{//把数对thePair插入字典。若存在关键字相同的数对。则覆盖
	//若表满,则抛出异常
	//搜索散列表。查找匹配的数对
	int b = search(thePair.first) ;
	//检查匹配的数对是否存在
	if (table[b] == NULL) .
	//没有匹配的数对,而且表不满
	{
		table [b] = new pair<const K,E> (thePair) ;
		dSize++;
	}
	else
	{//检查是否有重复的关键字数对或是否表满
		if (table[b]->first == thePair.first)
		{//有重复的关键字数对,修改table[b]->second
			table[b]->second = thePair.second;
		}
		else  //表满
			throw hashTableFull() ;
	}
}

性能分析

  • 就平均性能而言,散列远远优于线性表。令U和S分别表示在一次不成功搜索和成功搜索中平均搜索的桶数,其中n是很大的值。这个平均值是由插人的n个关键字计算得来的。对于线性探查,有如下近似公式:
    数据结构——跳表和散列_第2张图片

  • 其中 a=n/b 为负载因子( loading factor)。

例子
  • 设计一个可容纳近1000个数对的散列表。要求成功搜索时的平均搜索桶数不得超过4,不成功搜索时的平均搜索桶数不得超过50.5。由U的公式,得到a≤0.9,由S的公式,得到4≥0.5+1/(2(1-a))或a≤6/7。因此,a≤min{0.9,6/7}=6/7。因此b最小为[7n/61](向上取整)=1167。b=D=1171 是一个合适的值。
  • 另一种计算D的方法是,根据散列表可用空间的最大值来确定b的最大可能值, 然后取D为不大于b的最大整数,而且要么是素数,要么不能被小于20的数整除。例如,如果散列表最多有530个桶,则23*23=529是D和b的最佳选择。

链式散列

数据结构——跳表和散列_第3张图片
数据结构——跳表和散列_第4张图片

跳表表示

  • 在一个用有序链表描述的n个数对的字典中进行查找,至多需要n次关键字比较。
  • 如果在链表的中部节点加一个指针,则比较次数可以减少到n/2+1。这时,为了查找一个数对,首先与中间的数对比较。如果查找的数对关键字比较小,则仅在链表的左半部分继续查找,否则,在链表右半部分继续查找。
    数据结构——跳表和散列_第5张图片
  • 对n个数对而言,0级链表包括所有数对,1 级链表每2个数对取一个,2级链表每4个数对取一个,i级链表每2^i 个数对取一个。
  • 一个数对属于i级链表,当且仅当它属于0 ~ i 级链表,但不属于i+1级(若该链表存在)链表。
  • 我们把诸如图10-1c的结构称为跳表。在该结构中有一组等级链表。0级链表包含所有数对,1级链表的数对是0级链表数对的一个子集。
  • i级链表的数对是i-1级链表数对的子集。
  • 图10-1c是跳表的一个规则结构,i 级链表所有的数对均属于i-1级链表。
插入和删除
  • 在插入和删除时,要保持图10-1c跳表的规则结构,需要耗时O(n)。在规则的跳表结构中,i级链表有 n/2^i 个记录,在插入时要尽量逼近这种结构。
  • 插入的新数对属于i级链表的概率为1/2^i 。在实际确定新数对所属的链表级别时,应考虑各种可能的情况。把新数对插入i级链表的可能性为p ^i .
  • 在图10-1c中,p=0.5。 对一般的p,链表的级数为[ logw1/p(n) ](向下取整)+1。
  • 在一个规则的跳表中,i级链表包含1/p个i-1级链表的节点。
  • 在插入时,要为新数对分配一个级(即确定它属于哪一级链表), 分配过程由后面将要介绍的随机数生成器来完成。若新数对属于i级链表,则插入结果仅影响由虚线切割的0 ~ i级链表指针。图10-1e是插人77的结果。
级的分配
  • 在规则的跳表结构中,i-l 级链表的数对个数与i级链表的数对个数之比是一个分数p。因此,属于i-1级链表的数对同时属于i级链表的概率为p。
  • 假设用一个统一的随机数生成器产生0和1间的实数,产生的随机数≤p的概率为p。若下一个随机数≤p,则新数对应在1级链表上。要确定该数对是否在2级链表上,要由下一个随机数来决定。若新的随机数≤p,则该元素也属于2级链表。重复这个过程,直到一个随机数>p为止。
  • 这种方法有潜在的缺点,某些数对被分配的级数可能特别大,远远超过log1/p(N),其中N为字典数对的最大预期数目。为避免这种情况,可以设定一个级数的上限maxLevel, 最大值为[ log1/p(N) ](向上取整)- 1
  • 这种方法还有一个缺点,即使采用了级数的上限maxLevel,还可能出现这样的情况:在插入一个新数对之前有3个链表,而在插人之后就有了10个链表。也就是说,尽管3 ~ 8级链表没有数对,新数对却被分配到9级链表。换句话说,在插入前后,没有3 ~ 8级链表。因为这些空级链表并没有什么好处,我们可以把新记录的链表等级调整为3。

文件压缩(LZW)

数据结构——跳表和散列_第6张图片

  • LZW压缩方法把文本字符串映射为数字编码。首先,该文本串中所有可能出现的字母都被分配个代码。例如,要压缩的文本串是S=aabbbbbaabaaba,它由字符a和b组成,a的代码是0, b的代码是1。
  • 字符串和编码的映射关系存储在一个数对字典中,每个数对形如( key, value),其中key是字符串,value 是该字符串的代码。

你可能感兴趣的:(笔记,c++,算法,数据结构)