散列方法可以极大提高查找效率,将查找数据的时间复杂度降至O(1),基本思想是以空间换时间,依靠一个散列数组来实现。
散列问题的题型按查找元素的类型可大致分为以下三种:
1.整数(如int型)的查找
2.字符(如char型)的查找
3.字符串(如string型或者char数组等)的查找
—————————————————————————
使用散列方法解决上述三类问题的关键在于散列函数的选取。散列函数需要将具体要查找的元素映射唯一映射到散列数组的下标。
第一种整数查找最简单,常用的方法就是直接映射,将整数本身的值作为散列数组的下标。
第二种字符的查找,只需在第一种基础上略作调整,如,将小写字母映射为散列数组的下标0~25,大写字母映射为散列数组的下标26~51,数字映射为散列数组的下标52~61,其他更多符号依次类推即可。(当然,更简便的方法时直接将字符的ASCII码作为散列数组的下标,这时散列函数的大小为256)。
这里我容易犯的错是有时候想使用getchar来获取字符以求只需要设置一个用于暂存单个字符的字符变量,输入时使用while判断是否为空格或换行符来遍历一个字符串的所有字符,而试图节省字符数组空间,而实际上,每次我这么用都很伤脑筋,运行进入了死循环,结果导致运行超时,一分都没有。不知道你们有没有犯过这种错误或是遇到这种问题,不过还是建议大家不要学我,先解决逻辑,再试图去优化时间和空间性能。(特别是像我这样的入门学者,能解决问题就已经很棒了,有时候真不该太钻牛角尖。)
对于第三类问题,字符串的查找,可以通过将字符串看作字符表示的整数的方法将字符串唯一映射为整数,典型的例子是十六进制数,就是一个字符串,其中不仅有数字还有A~F字母,将该字符串按十六进制转换为十进制的方法就能转化为唯一对应的整数(如果是纯大写字母组成的字符串,可以看作二十六进制数进行转化,如A代表0,Z代表25等。此法虽好,不过值得注意的是,不同字符较多时,这种散列方法只能支持较少字符数的字符串,比如说二十六进制情况下,26的7次方已经达到了80亿,超出了int能表示的范围,int能表示的范围大概在20亿左右,因此用这种散列函数构造法,使用int大小的数组,只能查找不超过6位的纯大写字母串)。
——————————————————————————————
由于散列方法的良好的查找效率,非常适合用于求集合的交、并、查问题。简单讨论以下应用思路。(这里假设集合中的元素都互不相同)。
求集合交集,假设两个集合和一个布尔型散列数组(初始化全为false),先将第一个集合中的元素逐个在对应散列数组中设置值为true,在遍历第二个集合中的元素时,逐个检查当前访问的元素对应的散列数组中的值,若为true,则输出到交集中。多个集合的交集都可以分解为求两两交集,将上述求两个集合交集的语句封装为一个函数,再进行对应扩展即可。
求集合并集,假设两个集合和一个布尔型散列数组(初始化全为false),先将第一个集合中的元素逐个在对应散列数组中设置值为true,在将第二个集合中的元素逐个在对应散列数组中设置值为true。最后遍历散列数组,输出值为true的元素对应的散列数组下标到并集中即可。
求集合差集,如求S1-S2。该问题与求集合S1与S2的并集可以类比以下,只需将第二个集合中的元素逐个在对应散列数组中设置值为true改为设置值为false即可。思考一下,若是求可能含有重复元素的差集是否还能使用该方法呢?答案是否定的。那么怎么做呢?将布尔型散列数组改为int型数组即可,初始化全为0,原设true改为++,原设false改为--,依次思路做修改输出差集即可。
求集合差集(元素互不相同)样例及代码如下:
#include <cstdio>
const int MAXVALUE = 10005;
bool isAppear[MAXVALUE] = {false};
int main(){
int n,m;
int idx;
scanf("%d%d",&n,&m);
for(int i=0; i<n; i++){
scanf("%d",&idx);
isAppear[idx] = true;
}
for(int j=0; j<m; j++){
scanf("%d",&idx);
isAppear[idx] = false;
}
bool firLine = true;
for(int k=0; k<MAXVALUE; k++){
if(isAppear[k]){
if(!firLine) printf(" ");
firLine = false;
printf("%d",k);
}
}
return 0;
}
求集合差集(元素可能重复)样例及代码如下:
#include <cstdio>
const int MAXVALUE = 10005;
int isAppear[MAXVALUE] = {0};
int main(){
int n,m;
int idx;
scanf("%d%d",&n,&m);
for(int i=0; i<n; i++){
scanf("%d",&idx);
isAppear[idx] ++;
}
for(int j=0; j<m; j++){
scanf("%d",&idx);
isAppear[idx] --;
}
bool firLine = true;
for(int k=0; k<MAXVALUE; k++){
if(isAppear[k] > 0){
while(isAppear[k]){
if(!firLine) printf(" ");
firLine = false;
printf("%d",k);
isAppear[k] --;
}
}
}
return 0;
}
——————————————————————————————
这篇文章就差不多到这啦,发布上篇博客自我反省之后,我写代码的思路明显清洗很多,特别是在设置遍历和写循环时,思路没有因为变量混乱和不直观易懂的因素干扰。下面是我总结的设置变量名的小技巧,帮助正确清晰设置变量名或函数名(这里有两个主要好处:1.一是防止与C++的标识符或引入库中的函数名重复,这会决定程序能否正确运行;2.二是变量名通俗易懂,有助于思考逻辑关系。)
技巧1:对常量设置变量名全为大写,以直观区分变量,如MAXSIZE,MAXLEN,MAXVALUE
技巧2:对单个单词就能表示的变量名最好不要直接使用原单词,可以使用原单词的缩写或词尾加个-s。如不要使用array作为数组名,可以使用arr或arrays。
技巧3:对变量一般使用多个单词的组合或多个单词缩写的小写字母组合,其中除第一个单词外的单词首字母大写以和其他单词分隔区别,这种命名法更通俗易懂,且不会与标识符之类的重合。这是最通用的变量命名方法,保证不仅自己看得懂,别人也看得懂。如散列函数命名为getHashKey,用于存储元素是否出现过的布尔型变量命名为isAppear等。