清华OJ:PA3-3 重名剔除(Deduplicate)难题精解

题目:https://dsa.cs.tsinghua.edu.cn/oj/problem.shtml?id=1150

分析:

题目涉及字符串,提示用散列。不难想到散列码转换,根据邓公教材上的方法,字符串的散列码取作x_{0}a^{n-1}+x_{1}a^{n-2}+...x_{n-2}a^{1}+x_{n-1},其中常数a>=2,但若用这一方法,得到的值会超出int范围上限,不值得。故将上式改为x_{0}\times 1+x_{1}\times 2+...+x_{n-1}\times n+x_{n}\times (n+1),这样远远不会达到int范围上限。至于散列函数的设计,一般用除余法即可,故可在得到散列码后,直接除余,余数即散列表长,一般选用不小于输入规模上限的素数即可,此题是600001。转换后的散列码可能重复,等效于不同关键码(字符串)映射到同一地址,故需要解决冲突。

解决冲突的方法很多,常用的是开放定址法的线性探测法,但是这一方法会导致超时,原因在于当出现冲突时,最坏情况下需要遍历之前所有的非空桶单元,因此此法不妥。此题一般适用的方法是独立链法,即将冲突的关键码组织成列表放在映射到的桶单元中,发生冲突时,最坏情况下只需遍历当前桶单元的所有冲突关键码,相较于前一方法,时间成本得到了极大降低。

除了上述方法正确之外,还需要结合题目,此题要求重复的字符串只输出1次,故需要增设额外标志,一般是在桶单元的每个槽位(冲突单元)中设置,可以设为布尔型,初始化为假,若当前输入的字符串与映射到的字符串重复,且当前对应的槽位标志为假,则将其改为真,输出这个字符串,这样之后重复的字符串不会被输出。

有了正确的基本方法和结合题目的特殊方法,一般可以通过。至于每个槽位中的数据域的选取,一般选取字符串,而为了赋值方便,一般使用C++的string类型,但是这样做通过后,会发现最坏用例耗时1200+ms。不难发现,其原因在于每次字符串的赋值导致时间成本的迅速升高。于是,将数据域取字符指针,于是数据域的每次赋值只需O(1)的时间。经测试,此方法最坏用例仅耗时200+ms,相较于前一方法,效率提升了近5倍!

若槽位数据域选用字符指针,则需要注意,你需要在主函数之外定义一个二维字符数组,原因有两点,一是字符(串)指针需要取一个字符二维数组的第1维,这样就指向了一个字符串,而这个字符串的首字符地址是唯一的,这样可以保证每个槽位字符指针不至于重复。二是数组若在主函数之内定义,运行会出错。最后注意数组第2维度长度即字符串最大长度,需至少为41,原因是字符串需要有结束符。

掌握基本方法,利用好题目条件,注意和处理好和题目相关的所有细节,定能AC!

代码:

#include
#include
#define HASHSIZE 600001
using namespace std;
struct Slot {//每个桶对应的槽位,存储冲突,即映射到同一地址且不重复的字符串(实则字符指针) 
	char* data;//数据项,存储字符指针 
	bool repeat;//标志,判别字符串是否重复
	Slot* succ;//后继 
}buckets[HASHSIZE];//桶数组(散列表) 
char name[HASHSIZE][41];//字符二维数组(必须开头定义),存储输入字符串,注意二维长度为40+1个结束符=41 
void Insert(int addr, char* s) {//在相应地址中插入冲突的字符串(实则字符指针) 
	Slot* t = new Slot;
	t->data = s; t->repeat = false;//初始化当前字符串从未重复 
	t->succ = buckets[addr].succ;//链表头插法 
	buckets[addr].succ = t;
}
int HashCode(char* s) {//散列码转换(字符串转数字) 
	int sum = 0, len = strlen(s);
	for (int i = 0; idata, name[i])) {//若当前槽位的字符串重复 
			if (!p->repeat) {//检查当前槽位的字符串是否重复
				p->repeat = true;//若未重复,则标志为已重复过 
				puts(name[i]);//输出重复字符串 
			}break;//若重复过,则忽略,无论是否重复过,皆终止遍历 
		}
		else p = p->succ;
		if (!p)Insert(addr, name[i]);//若当前槽位空,则进行插入 
	}
	return 0;
}

 

你可能感兴趣的:(学堂在线PA)