哈希——无冲突应用

来源:互联网 发布:教学电子白板软件下载 编辑:程序博客网 时间:2024/05/16 18:48

        众所周知,哈希的速度是灰常快的,敢号称时间复杂度为O(1)的,呵呵,它和快排有一拼(目前只有快排敢叫“快排”这个名号)。而且,它偏偏还非常好用,这注定了它不平凡的存在。倒是目前网说哈希时,多是介绍各种哈希函数的构造及如何避免冲突,然后动不动就扯到MD5这些东西上去了。照这样说来,貌似哈希不常用啊。其实说白了,管他那么多干嘛,就不冲突着用嘛,自然就省去了那些“拉链门”啊神马的东西了。平时没事来个哈希就用用,速度那么快的说。
        在这篇文章中说到的东西呢,用到的基本上全是不会出现冲突的哈希,也就是人们常说的“直接定址法”,这里说一下它在平时程序中的应用(注:本文中所有代码均为C代码;再注:本人是水人,所以所举例也全是水题,大神就掠过吧,呵呵)。这里只为抛砖,有玉的尽管砸来吧!


先来看看这个常见的东西吧:
如果n大于5就输出”Yes”,否则输出”No”,不用说,马上写出了这样的语句:

if (n > 5) {printf("Yes");} else {printf("No");}

呃,如果写成一行也就是:

printf(n > 5? “Yes”: “No”);
那现在,用哈希这样写:
char hash[2][] = { “No”, “Yes” };printf(hash[n > 5]);
        呵呵,看着有点怪,而且还不如上面写成一行来的方便,莫急,在这用哈希确实是有点那啥了,嘿嘿。那看下面这些题吧。


1.按ascii码哈希
        字符串中去掉指定字母。
        例如:s:”hello world”去掉t:”aeiou”中字母后为”hll wrld”。
        相信这种简单问题,对高手来说不是问题,但咱就从这说起。
        首先说这个题一般人会这样写:
char s[] = "hello world";char t[] = "aeiou";int i, j, len;for (i = len = 0; s[i]; i++) {for (j = 0; t[j]; j++) {if (s[i] == t[j])break;}if (!t[j])s[len++] = s[i];}s[len] = 0;printf(s); 
        即用一个for循环(或一个if(s[i] != ‘a’ ...))了事,要不,用哈希试试?
char s[] = "hello world";char t[] = "aeiou";char hash[128];int i, len;memset(hash, 0, sizeof(hash));for (i = 0; t[i]; i++) {hash[t[i]] = 1;}for (i = len = 0; s[i]; i++) {if (!hash[s[i]])s[len++] = s[i];}s[len] = 0;printf(s);
        貌似没减少多少代码,反倒多用了好多空间,但是时间复杂度降低了啊。
        由上面这个例子看来,哈希表(不若说成是集合)的初始化是很简单方便的,而且在用的时候和初始化时操作都是一样的,都是通过hash[s[i]]实现的。所以基本上这也可以当作是一个模版来用的。
        说到这,可以拿道poj水题看一下。poj2339 Rock, Scissors, Paper就是个石头剪刀布来回换,模拟一下就完了。主要就是判断周围四边的字符,如果就那么写的话,咳咳,如果我没算错,要写上3*4=12个或写一起也得3个长长的if吧,估计要写晕菜,不若来张哈希表,只要1个if就搞定了:
// 这些是初始化,也就短短几行char t[128];t['R'] = 'P';  t['P'] = 'S';  t['S'] = 'R';  // 判断上下左右有没有相克的,下面这些单独写个函数就行了char c = t[s[i][j]];if (s[i-1][j] == c  || s[i+1][j] == c  || s[i][j-1] == c  || s[i][j+1] == c)  return c;  elsereturn s[i][j]; 
        哈,是不是一下就感觉代码量锐减呢,呵呵,而且还不会对速度上有影响,何乐而不用之呢?这里再说一下天梯1487,用个哈希多水的就过了,时间也不慢呀。1230就更不用说了,绝对的哈希啊。


2.按位哈希
           嗯,这里就直接借鉴一下poj2777 Count Color好了,相信这道水题大家还是有目共睹的,也相信大家都看得出来是按位判断的,所以在这就好办了,每个颜色对应一位,区区30位,一个int就足够了。正如大家所想的那样,按位哈希,咳,说白了,就是为了节约空间才用到的,但刚好这道题用按位哈希反倒更好。(再插一句嘴,这题大家都知道用线段树,但在求有多少种颜色时,可以借用到树状数组的lowbit来计数。空间有限,这里不贴代码了,给个网址自己看吧:http://blog.csdn.net/sg_siqing/article/details/12209027

        但是按位哈希有一个不好的地方就是它每一位能且只能表示两种状态,这就对它的使用造成了一定的限制,所以它的适用范围是:有且只有两种情况出现,才能用。而且还有个缺点,就是它必然要用到哈希函数去算位,在这点上不如上面可以直接用ascii码直接当下标的用。所以在这并不推荐这种用法,除非就是为了节约那7/8的空间。


3.减量哈希

       提前说明哈,这个名字,减量哈希是我自己想的,它主要用于随机数中,这里只说思路,每次将随机到的随机数移出待选队列,再对队列的长度进行随机,得到的是不重复的随机值。这种办法,在不同的语言中有不同的实现方法,主要是有不同的初始值,最好结合实际语言情况选择恰当的方式。


4.数据结构
        I)演变出的字典树
        这里说的不是别人,就是大家熟悉的“字典树”,我左看、右看、上看、下看,怎么看它怎么是哈希,或许这就是传说中所谓的“多重哈希”吧。话说一张哈希表就够浪费空间的了,由这么多张哈希表组成的字典树岂不是浪费中的浪费?!但对于字符串来说,那查找效率,啧啧,没话说。
        II)辅助的线性表
        神马意思?哈希要和线性表一起用吗?是啊,有时不用冥思苦想哈希函数,直接拿个线性表来将结果存里边就完了,完事拿下标指过来就行了啊。这样算下来,嘿嘿,还真不用担心冲突呢,因为它永远不会冲突,来一个存一个,直接在线性表末尾添加。删除也方便,删一个就拿最后一个补过来就好了,时间复杂度还是O(1),只是改改指针,改改表长就完了嘛。再说了,删除的时候也不多啊,真删除的时候,还不定用着什么法呢,随机应变吧。
        好了,多说无益,不如来道题看看啊。poj1451 T9,自认为是字典树题呀。两个字典树,一张哈希表,一张线性表水的(呵呵,大神莫笑)。首先让哈希表将两个字典树联系起来,再在第二个字典树结构中安插一个指针指向线性表,这样下来,基本上所有操作都是O(1)了。而这所有的一切,都是通过下标联系起来的。无独有偶的是,天梯1985GameZ游戏排名系统也是可以这样用的,以平衡二叉树为基,辅以哈希表,完爆无压力啊。
        同样的,这里只给出代码的网址:
        poj1451:http://blog.csdn.net/sg_siqing/article/details/12207153
        wikioi1985:http://blog.csdn.net/sg_siqing/article/details/14647649


5.密码学应用
           网上基本都会说的东西:MD5,都说这个东西是用的散列表,也就是哈希,我没写过,也没详细地看过,不清楚。但我写过一个叫DES加密的代码,这东西的代码里面绝对用的到哈希啊!有兴趣的可以看一下,这里不做过多阐述:http://blog.csdn.net/sg_siqing/article/details/21085471


6.冲突
           冲突大多来自字符串的处理,而大多问题字符串数量在百万级别或以上,即最小106吧。而一个整数,却是可以存下231约109数量级大小的数,但开这么大的数组有点吃不消,那就折中一下吧,去网上找一个109在以内尽量不冲突的字符串哈希算法,这样哈希表容量大约是字符串量的100倍以上,冲突量会大大减小。计算出字符串的哈希值来,却不保存原字符串的情况下,用平衡二叉树来记录这些哈希值,便可以减小空间的开辟,但同时也将时间复杂度提高了一个logn。注意:这种方法不一定可行,只是个设想,因为即使100倍以上空间仍有可能出现冲突,这就取决于字符串哈希函数的计算问题了。


7.优缺点
        先说缺点吧:
        I)浪费空间严重,太严重了啊!!!;
        II)乍看之下不容易理解,可读性低;
        III)一般需要其他数据结构辅助,提供键列表或值列表。
        ……
        再说说优点吧:
        I)速度快,大家都知道的;
        II)使用方便,操作复杂度基本上都是1;
        III)汇聚或简化了代码,相似代码可以写成一句话,免去多个if或switch判断;
        ……


8.关键
           大家都知道,二分查找时比较的是value,是值的大小,最后找到对应的下标。而相反的,哈希则是很直接地拿value值过来当下标用,一步到位找到对应的状态。所以,哈希的关键之处在于其值与下标之间的转换。
           在本文伊始,就说道,这里不会说到冲突的处理方法,为什么呢?因为哈希本身是非常好用的东西,如果硬是要把冲突处理加进去,就会使代码量瞬间飙升,窃以为这是得不偿失的做法,反是只要不冲突时便用的十分顺心,而且代码量非常之少,非常之精简(难道好的算法都这样?!不解)。
这里再说几例:线段树和树状数组是对兄弟,而只要不用到更新区间且查询区间,相信更多的人会选择树状数组吧。同时树状数组中的lowbit还可以一步取得一个数二进制中最后一位1,这是线段树中所没有的;双栈(不是双向栈)可以做到O(1)实现空间的手工分配和回收,或栈中最大/最小问题;快排中的双指针可以实现在O(n)内找到前k大/小,或查找和为某值的两个数……诸此种种,尽显各种数据结构或算法之风骚,所以它们不擅长之处,就让那些擅长的去做吧!


9.哈希?
           可能有人说了,这不是哈希,这个叫索引,我说,爱啥啥,关我什么事?我又不是去咬文嚼字地去给它个贴切的名字,我就叫它哈希,我用我的,叫自己的名,让别人说去吧,哈哈哈哈。。。。。。
1 0
原创粉丝点击