hash

        十一、从头到尾彻底解析Hash表算法       

        分类:            01.Algorithms(研究) 89099人阅读 评论(194) 收藏 举报

              十一、从头到尾彻底解析Hash 表算法


作者:July、wuliming、pkuoliver 
出处:http://blog.csdn.net/v_JULY_v。 
说明:本文分为三部分内容,
    第一部分为一道百度面试题Top K算法的详解;第二部分为关于Hash表算法的详细阐述;第三部分为打造一个最快的Hash表算法。
------------------------------------

第一部分:Top K 算法详解
问题描述
百度面试题:
    搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。
    假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。),请你统计最热门的10个查询串,要求使用的内存不能超过1G。


必备知识:
    什么是哈希表?
    哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

    哈希表的做法其实很简单,就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。
    而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位文章第二、三部分,会针对Hash表详细阐述


问题解析:
    要统计最热门查询,首先就是要统计每个Query出现的次数,然后根据统计结果,找出Top 10。所以我们可以基于这个思路分两步来设计该算法。
    即,此问题的解决分为以下俩个步骤

第一步:Query统计
    Query统计有以下俩个方法,可供选择:
    1、直接排序法
    首先我们最先想到的的算法就是排序了,首先对这个日志里面的所有Query都进行排序,然后再遍历排好序的Query,统计每个Query出现的次数了。

    但是题目中有明确要求,那就是内存不能超过1G,一千万条记录,每条记录是255Byte,很显然要占据2.375G内存,这个条件就不满足要求了。

    让我们回忆一下数据结构课程上的内容,当数据量比较大而且内存无法装下的时候,我们可以采用外排序的方法来进行排序,这里我们可以采用归并排序,因为归并排序有一个比较好的时间复杂度O(NlgN)。

    排完序之后我们再对已经有序的Query文件进行遍历,统计每个Query出现的次数,再次写入文件中。

    综合分析一下,排序的时间复杂度是O(NlgN),而遍历的时间复杂度是O(N),因此该算法的总体时间复杂度就是O(N+NlgN)=O(NlgN)。

    2、Hash Table法
    在第1个方法中,我们采用了排序的办法来统计每个Query出现的次数,时间复杂度是NlgN,那么能不能有更好的方法来存储,而时间复杂度更低呢?

    题目中说明了,虽然有一千万个Query,但是由于重复度比较高,因此事实上只有300万的Query,每个Query255Byte,因此我们可以考虑把他们都放进内存中去,而现在只是需要一个合适的数据结构,在这里,Hash Table绝对是我们优先的选择,因为Hash Table的查询速度非常的快,几乎是O(1)的时间复杂度。

    那么,我们的算法就有了:维护一个Key为Query字串,Value为该Query出现次数的HashTable,每次读取一个Query,如果该字串不在Table中,那么加入该字串,并且将Value值设为1;如果该字串在Table中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度内完成了对该海量数据的处理。

    本方法相比算法1:在时间复杂度上提高了一个数量级,为O(N),但不仅仅是时间复杂度上的优化,该方法只需要IO数据文件一次,而算法1的IO次数较多的,因此该算法2比算法1在工程上有更好的可操作性。


第二步:找出Top 10
    算法一:普通排序
    我想对于排序算法大家都已经不陌生了,这里不在赘述,我们要注意的是排序算法的时间复杂度是NlgN,在本题目中,三百万条记录,用1G内存是可以存下的。

    算法二:部分排序
    题目要求是求出Top 10,因此我们没有必要对所有的Query都进行排序,我们只需要维护一个10个大小的数组,初始化放入10个Query,按照每个Query的统计次数由大到小排序,然后遍历这300万条记录,每读一条记录就和数组最后一个Query对比,如果小于这个Query,那么继续遍历,否则,将数组中最后一条数据淘汰,加入当前的Query。最后当所有的数据都遍历完毕之后,那么这个数组中的10个Query便是我们要找的Top10了。

    不难分析出,这样,算法的最坏时间复杂度是N*K, 其中K是指top多少。

    算法三:堆
    在算法二中,我们已经将时间复杂度由NlogN优化到NK,不得不说这是一个比较大的改进了,可是有没有更好的办法呢?

    分析一下,在算法二中,每次比较完成之后,需要的操作复杂度都是K,因为要把元素插入到一个线性表之中,而且采用的是顺序比较。这里我们注意一下,该数组是有序的,一次我们每次查找的时候可以采用二分的方法查找,这样操作的复杂度就降到了logK,可是,随之而来的问题就是数据移动,因为移动数据次数增多了。不过,这个算法还是比算法二有了改进。

    基于以上的分析,我们想想,有没有一种既能快速查找,又能快速移动元素的数据结构呢?回答是肯定的,那就是堆。
    借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此到这里,我们的算法可以改进为这样,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比

    思想与上述算法二一致,只是算法在算法三,我们采用了最小堆这种数据结构代替数组,把查找目标元素的时间复杂度有O(K)降到了O(logK)。
    那么这样,采用堆数据结构,算法三,最终的时间复杂度就降到了N‘logK,和算法二相比,又有了比较大的改进。

总结:
    至此,算法就完全结束了,经过上述第一步、先用Hash表统计每个Query出现的次数,O(N);然后第二步、采用堆数据结构找出Top 10,N*O(logK)。所以,我们最终的时间复杂度是:O(N) + N'*O(logK)。(N为1000万,N’为300万)。如果各位有什么更好的算法,欢迎留言评论。第一部分,完。

第二部分、Hash表 算法的详细解析

什么是Hash
     Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

    HASH主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做HASH值. 也可以说,hash就是找到一种数据内容和数据存放地址之间的映射关系。

    数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”,如图:


    左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。

    元素特征转变为数组下标的方法就是散列法。散列法当然不止一种,下面列出三种比较常用的:

1,除法散列法
最直观的一种,上图使用的就是这种散列法,公式:
      index = value % 16
学过汇编的都知道,求模数其实是通过一个除法运算得到的,所以叫“除法散列法”。

2,平方散列法
求index是非常频繁的操作,而乘法的运算要比除法来得省时(对现在的CPU来说,估计我们感觉不出来),所以我们考虑把除法换成乘法和一个位移操作。公式:
      index = (value * value) >> 28   右移,除以2^28。记法:左移变大,是乘。右移变小,是除。
如果数值分配比较均匀的话这种方法能得到不错的结果,但我上面画的那个图的各个元素的值算出来的index都是0——非常失败。也许你还有个问题,value如果很大,value * value不会溢出吗?答案是会的,但我们这个乘法不关心溢出,因为我们根本不是为了获取相乘结果,而是为了获取index。

3,斐波那契(Fibonacci)散列法

平方散列法的缺点是显而易见的,所以我们能不能找出一个理想的乘数,而不是拿value本身当作乘数呢?答案是肯定的。

1,对于16位整数而言,这个乘数是40503
2,对于32位整数而言,这个乘数是2654435769
3,对于64位整数而言,这个乘数是11400714819323198485

    这几个“理想乘数”是如何得出来的呢?这跟一个法则有关,叫黄金分割法则,而描述黄金分割法则的最经典表达式无疑就是著名的斐波那契数列,即如此形式的序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946,…。另外,斐波那契数列的值和太阳系八大行星的轨道半径的比例出奇吻合。

    对我们常见的32位整数而言,公式:
            index = (value * 2654435769) >> 28

    如果用这种斐波那契散列法的话,那上面的图就变成这样了:


很明显,用斐波那契散列法调整之后要比原来的取摸散列法好很多。

适用范围
    快速查找,删除的基本数据结构,通常需要总数据量可以放入内存。

基本原理及要点
    hash函数选择,针对字符串,整数,排列,具体相应的hash方法。
碰撞处理,一种是open hashing,也称为拉链法;另一种就是closed hashing,也称开地址法,opened addressing。

扩展
    d-left hashing中的d是多个的意思,我们先简化这个问题,看一看2-left hashing。2-left hashing指的是将一个哈希表分成长度相等的两半,分别叫做T1和T2,给T1和T2分别配备一个哈希函数,h1和h2。在存储一个新的key时,同 时用两个哈希函数进行计算,得出两个地址h1[key]和h2[key]。这时需要检查T1中的h1[key]位置和T2中的h2[key]位置,哪一个 位置已经存储的(有碰撞的)key比较多,然后将新key存储在负载少的位置。如果两边一样多,比如两个位置都为空或者都存储了一个key,就把新key 存储在左边的T1子表中,2-left也由此而来。在查找一个key时,必须进行两次hash,同时查找两个位置。

问题实例(海量数据处理)
    我们知道hash 表在海量数据处理中有着广泛的应用,下面,请看另一道百度面试题:
题目:海量日志数据,提取出某日访问百度次数最多的那个IP。
方案:IP的数目还是有限的,最多2^32个,所以可以考虑使用hash将ip直接存入内存,然后进行统计。

第三部分、最快的Hash表算法

    接下来,咱们来具体分析一下一个最快的Hasb表算法。
    我们由一个简单的问题逐步入手:有一个庞大的字符串数组,然后给你一个单独的字符串,让你从这个数组中查找是否有这个字符串并找到它,你会怎么做?有一个方法最简单,老老实实从头查到尾,一个一个比较,直到找到为止,我想只要学过程序设计的人都能把这样一个程序作出来,但要是有程序员把这样的程序交给用户,我只能用无语来评价,或许它真的能工作,但...也只能如此了。

    最合适的算法自然是使用HashTable(哈希表),先介绍介绍其中的基本知识,所谓Hash,一般是一个整数,通过某种算法,可以把一个字符串"压缩" 成一个整数。当然,无论如何,一个32位整数是无法对应回一个字符串的,但在程序中,两个字符串计算出的Hash值相等的可能非常小,下面看看在MPQ中的Hash算法:

函数一、以下的函数生成一个长度为0x500(合10进制数:1280)的cryptTable[0x500]

void prepareCryptTable()
{
    unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i;

    for( index1 = 0; index1 < 0x100; index1++ )
    {
        for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100 )
        {
            unsigned long temp1, temp2;

            seed = (seed * 125 + 3) % 0x2AAAAB;
            temp1 = (seed & 0xFFFF) << 0x10;

            seed = (seed * 125 + 3) % 0x2AAAAB;
            temp2 = (seed & 0xFFFF);

            cryptTable[index2] = ( temp1 | temp2 );
       }
   }
}

函数二、以下函数计算lpszFileName 字符串的hash值,其中dwHashType 为hash的类型,在下面的函数三、GetHashTablePos函数中调用此函数二,其可以取的值为0、1、2;该函数返回lpszFileName 字符串的hash值:

unsigned long HashString( char *lpszFileName, unsigned long dwHashType )
{
    unsigned char *key  = (unsigned char *)lpszFileName;
unsigned long seed1 = 0x7FED7FED;
unsigned long seed2 = 0xEEEEEEEE;
    int ch;

    while( *key != 0 )
    {
        ch = toupper(*key++);

        seed1 = cryptTable[(dwHashType << 8) + ch] ^ (seed1 + seed2);
        seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3;
    }
    return seed1;
}


    Blizzard的这个算法是非常高效的,被称为"One-Way Hash"( A one-way hash is a an algorithm that is constructed in such a way that deriving the original string (set of strings, actually) is virtually impossible)。举个例子,字符串"unitneutralacritter.grp"通过这个算法得到的结果是0xA26067F3。

  是不是把第一个算法改进一下,改成逐个比较字符串的Hash值就可以了呢,答案是,远远不够,要想得到最快的算法,就不能进行逐个的比较,通常是构造一个哈希表(Hash Table)来解决问题,哈希表是一个大数组,这个数组的容量根据程序的要求来定义,例如1024,每一个Hash值通过取模运算 (mod) 对应到数组中的一个位置,这样,只要比较这个字符串的哈希值对应的位置有没有被占用,就可以得到最后的结果了,想想这是什么速度?是的,是最快的O(1),现在仔细看看这个算法吧:

typedef struct
{
    int nHashA;
    int nHashB;
    char bExists;
   ......
} SOMESTRUCTRUE;
一种可能的结构体定义?

函数三、下述函数为在Hash表中查找是否存在目标字符串,有则返回要查找字符串的Hash值,无则,return -1.

int GetHashTablePos( har *lpszString, SOMESTRUCTURE *lpTable )
//lpszString要在Hash表中查找的字符串,lpTable为存储字符串Hash值的Hash表。
{
    int nHash = HashString(lpszString);  //调用上述函数二,返回要查找字符串lpszString的Hash值。
    int nHashPos = nHash % nTableSize;

    if ( lpTable[nHashPos].bExists  &&  !strcmp( lpTable[nHashPos].pString, lpszString ) )
    {  //如果找到的Hash值在表中存在,且要查找的字符串与表中对应位置的字符串相同,
        return nHashPos;    //则返回上述调用函数二后,找到的Hash值
    }
    else
    {
        return -1; 
    }
}


    看到此,我想大家都在想一个很严重的问题:“如果两个字符串在哈希表中对应的位置相同怎么办?”,毕竟一个数组容量是有限的,这种可能性很大。解决该问题的方法很多,我首先想到的就是用“链表”,感谢大学里学的数据结构教会了这个百试百灵的法宝,我遇到的很多算法都可以转化成链表来解决,只要在哈希表的每个入口挂一个链表,保存所有对应的字符串就OK了。事情到此似乎有了完美的结局,如果是把问题独自交给我解决,此时我可能就要开始定义数据结构然后写代码了。

    然而Blizzard的程序员使用的方法则是更精妙的方法。基本原理就是:他们在哈希表中不是用一个哈希值而是用三个哈希值来校验字符串。

    MPQ使用文件名哈希表来跟踪内部的所有文件。但是这个表的格式与正常的哈希表有一些不同。首先,它没有使用哈希作为下标,把实际的文件名存储在表中用于验证,实际上它根本就没有存储文件名。而是使用了3种不同的哈希:一个用于哈希表的下标,两个用于验证。这两个验证哈希替代了实际文件名。
    当然了,这样仍然会出现2个不同的文件名哈希到3个同样的哈希。但是这种情况发生的概率平均是:1:18889465931478580854784,这个概率对于任何人来说应该都是足够小的。现在再回到数据结构上,Blizzard使用的哈希表没有使用链表,而采用"顺延"的方式来解决问题,看看这个算法:

函数四、lpszString 为要在hash表中查找的字符串;lpTable 为存储字符串hash值的hash表;nTableSize 为hash表的长度:

int GetHashTablePos( char *lpszString, MPQHASHTABLE *lpTable, int nTableSize )
{
    const int  HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2;

    int  nHash = HashString( lpszString, HASH_OFFSET );
    int  nHashA = HashString( lpszString, HASH_A );
    int  nHashB = HashString( lpszString, HASH_B );
    int  nHashStart = nHash % nTableSize;
    int  nHashPos = nHashStart;

    while ( lpTable[nHashPos].bExists )
   {
     /*如果仅仅是判断在该表中时候存在这个字符串,就比较这两个hash值就可以了,不用对
     *结构体中的字符串进行比较。这样会加快运行的速度?减少hash表占用的空间?这种
      *方法一般应用在什么场合?*/
        if (   lpTable[nHashPos].nHashA == nHashA
        &&  lpTable[nHashPos].nHashB == nHashB )
       {
            return nHashPos;
       }
       else
       {
            nHashPos = (nHashPos + 1) % nTableSize;
       }

        if (nHashPos == nHashStart)
              break;
    }
     return -1;
}

上述程序解释:

1.计算出字符串的三个哈希值(一个用来确定位置,另外两个用来校验)
2. 察看哈希表中的这个位置
3. 哈希表中这个位置为空吗?如果为空,则肯定该字符串不存在,返回-1。
4. 如果存在,则检查其他两个哈希值是否也匹配,如果匹配,则表示找到了该字符串,返回其Hash值。
5. 移到下一个位置,如果已经移到了表的末尾,则反绕到表的开始位置起继续查询 
6. 看看是不是又回到了原来的位置,如果是,则返回没找到
7. 回到3

ok,这就是本文中所说的最快的Hash表算法。什么?不够快?:D。欢迎,各位批评指正。

--------------------------------------------
补充1、一个简单的hash函数:

/*key为一个字符串,nTableLength为哈希表的长度
*该函数得到的hash值分布比较均匀*/
unsigned long getHashIndex( const char *key, int nTableLength )
{
    unsigned long nHash = 0;
  
    while (*key)
    {
        nHash = (nHash<<5) + nHash + *key++;
    }
       
    return ( nHash % nTableLength );
}

补充2、一个完整测试程序: 
    哈希表的数组是定长的,如果太大,则浪费,如果太小,体现不出效率。合适的数组大小是哈希表的性能的关键。哈希表的尺寸最好是一个质数。当然,根据不同的数据量,会有不同的哈希表的大小。对于数据量时多时少的应用,最好的设计是使用动态可变尺寸的哈希表,那么如果你发现哈希表尺寸太小了,比如其中的元素是哈希表尺寸的2倍时,我们就需要扩大哈希表尺寸,一般是扩大一倍。


    下面是哈希表尺寸大小的可能取值:

     17,            37,          79,        163,          331, 
    673,           1361,        2729,       5471,         10949,       
   21911,          43853,      87719,      175447,      350899,
  701819,         1403641,    2807303,     5614657,     11229331,  
22458671,       44917381,    89834777,    179669557,   359339171, 
718678369,      1437356741,  2147483647

以下为该程序的完整源码,已在linux下测试通过:

  1. #include <stdio.h> 
  2. #include <ctype.h>     //多谢citylove指正。 
  3. //crytTable[]里面保存的是HashString函数里面将会用到的一些数据,在prepareCryptTable 
  4. //函数里面初始化 
  5. unsigned long cryptTable[0x500]; 
  6.  
  7. //以下的函数生成一个长度为0x500(合10进制数:1280)的cryptTable[0x500] 
  8. void prepareCryptTable() 
  9. {  
  10.     unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i; 
  11.  
  12.     for( index1 = 0; index1 < 0x100; index1++ ) 
  13.     {  
  14.         for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100 ) 
  15.         {  
  16.             unsigned long temp1, temp2; 
  17.  
  18.             seed = (seed * 125 + 3) % 0x2AAAAB; 
  19.             temp1 = (seed & 0xFFFF) << 0x10; 
  20.  
  21.             seed = (seed * 125 + 3) % 0x2AAAAB; 
  22.             temp2 = (seed & 0xFFFF); 
  23.  
  24.             cryptTable[index2] = ( temp1 | temp2 );  
  25.        }  
  26.    }  
  27.  
  28. //以下函数计算lpszFileName 字符串的hash值,其中dwHashType 为hash的类型, 
  29. //在下面GetHashTablePos函数里面调用本函数,其可以取的值为0、1、2;该函数 
  30. //返回lpszFileName 字符串的hash值; 
  31. unsigned long HashString( char *lpszFileName, unsigned long dwHashType ) 
  32. {  
  33.     unsigned char *key  = (unsigned char *)lpszFileName; 
  34. unsigned long seed1 = 0x7FED7FED; 
  35. unsigned long seed2 = 0xEEEEEEEE; 
  36.     int ch; 
  37.  
  38.     while( *key != 0 ) 
  39.     {  
  40.         ch = toupper(*key++); 
  41.  
  42.         seed1 = cryptTable[(dwHashType << 8) + ch] ^ (seed1 + seed2); 
  43.         seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3;  
  44.     } 
  45.     return seed1;  
  46.  
  47. //在main中测试argv[1]的三个hash值: 
  48. //./hash  "arr/units.dat" 
  49. //./hash  "unit/neutral/acritter.grp" 
  50. int main( int argc, char **argv ) 
  51.     unsigned long ulHashValue; 
  52.     int i = 0; 
  53.  
  54.     if ( argc != 2 ) 
  55.     { 
  56.         printf("please input two arguments/n"); 
  57.         return -1; 
  58.     } 
  59.  
  60.      /*初始化数组:crytTable[0x500]*/ 
  61.      prepareCryptTable(); 
  62.  
  63.      /*打印数组crytTable[0x500]里面的值*/ 
  64.      for ( ; i < 0x500; i++ ) 
  65.      { 
  66.          if ( i % 10 == 0 ) 
  67.          { 
  68.              printf("/n"); 
  69.          } 
  70.  
  71.          printf("%-12X", cryptTable[i] ); 
  72.      } 
  73.  
  74.      ulHashValue = HashString( argv[1], 0 ); 
  75.      printf("/n----%X ----/n", ulHashValue ); 
  76.  
  77.      ulHashValue = HashString( argv[1], 1 ); 
  78.      printf("----%X ----/n", ulHashValue ); 
  79.  
  80.      ulHashValue = HashString( argv[1], 2 ); 
  81.      printf("----%X ----/n", ulHashValue ); 
  82.  
  83.      return 0; 

相关引用:
1、http://blog.redfox66.com/
2、http://blog.csdn.net/wuliming_sc/

更多请参见本hash算法之后续:十一(续)、倒排索引关键词Hash不重复编码实践。完。

查看评论
130楼 heirenhua 2013-08-14 12:49发表[回复] [引用] [举报]
首先非常感谢博主的文章,结合实例讲解知识点,让我对基础知识的理解更为深刻,特别是您用基础的知识解决这些“高深”的题目,极大地增强了我的自信心,谢谢!

然后,针对“第二步 找出TOP10“的部分排序中,我觉得”每读一条记录就和数组最后一个Query对比,如果小于这个Query,那么继续遍历,否则,将数组中最后一条数据淘汰,加入当前的Query。“这一句话表达有歧义,当前遍历的Query如果大于数组最后一个元素,那么应该是淘汰数组最后一个元素,并将当前Query加入到数组”适当“的位置。因为当前Query可能比数组倒数第二、三、四...个元素大,不能简单的用当前Query代替数组最后一个元素。
Re: fengzhe0411 2013-08-14 23:27发表[回复] [引用] [举报]
回复heirenhua:这里应该说的是和最小(或最大)的那个比较,计算最小或者最大的时间复杂度是O(k),而用堆的话就是O(1),但是调整堆要用O(logk)
129楼 zefei123 2013-08-09 19:51发表[回复] [引用] [举报]
写的非常好,受益了
128楼 Allen_Fancyzg 2013-06-21 11:08发表[回复] [引用] [举报]
作者您好,针对Top K我有一些优化。首先一般K都很小,例子中是找出TOP10,那么如果K<1百万的话,可知数据量1千万去重后不到三百万,运用均摊的思想可知,如果记录的引用计数小于等于3,那么肯定不是top K中的数据,从而减少了在K中的比较。这个优化不是量级上的,嘿嘿~
Re: wuyanzan606 2013-08-20 10:12发表[回复] [引用] [举报]
回复fancyzg:这个靠谱,相当实用。。
127楼 xiaowife20 2013-06-15 23:29发表[回复] [引用] [举报]
我是觉得你那个地方有点问题,就是数组那个NK,你说每次和最小的那个数字比,如果比他大,就和他交换,但是你要知道如果他不但比最后个数字大,而且还比倒数第二个也大,那么以后你的数组的末尾不一定是数组里面最小的。所以有点错
126楼 sctianhu 2013-06-11 13:49发表[回复] [引用] [举报]
HASH强大,楼主强大!
学习了。 顺带转一个了。谢谢!
125楼 iliveido 2013-05-28 16:14发表[回复] [引用] [举报]
这篇文章属上佳之作,通俗易懂且行文流畅。BTW:如果严格匹配两个字符串是否相等,用哈希是最优的。如果是不严格相等,仅为相近或相似,则可用相似哈希算法。
124楼 lithstup 2013-05-28 11:29发表[回复] [引用] [举报]
太牛了!!!!!!
123楼 gyang 2013-05-27 16:22发表[回复] [引用] [举报]
楼主牛逼,虽然我是写java的,但算法思想都是相通的,感谢楼主~~
122楼 kinlins 2013-05-24 15:43发表[回复] [引用] [举报]
哥,你讲的太牛逼了。我看数据结构散列半天没想到有啥用,看完这篇文章回去再细读,觉得都有用了
121楼 hala_baidu 2013-05-05 20:42发表[回复] [引用] [举报]
太感谢了! 我最近正好要学hash和海量数据,看完对楼主感激的内牛满面啊,对我太有帮助了! 真想请你吃饭去啊!
Re: v_JULY_v 2013-05-24 15:53发表[回复] [引用] [举报]
回复gl361148026:好滴呢!我在微博上http://weibo.com/julyweibo 等着哈:-)
120楼 superyf0924 2013-05-02 15:35发表[回复] [引用] [举报]
关于hash查找那个,逐个比较的复杂度是o(n),用hash查找复杂度是o(1),但是建hash表却要花o(n)啊。到底哪里节省了时间呢?还是说我理解错了,求指教!
Re: Shajide 2013-06-12 20:23发表[回复] [引用] [举报]
回复superyf0924:个人认为,是浪费了建表的时间,省了查询的时间。因为建表的时间慢一点无所谓。就建一次。
119楼 lixiaoyuan12 2013-04-20 13:33发表[回复] [引用] [举报]
正好解决了我最近在看的海量数据存储的问题,感谢楼主~~~~
118楼 Vincent乐 2013-01-05 11:08发表[回复] [引用] [举报]
用三个hash函数的逻辑好像不太对?
根据第一个hash值,在数组中定位,然后比较另外两个hash值。如果相等,返回true。如果不相等,直接退出不就好了?后面的比较感觉没有必要。??
Re: Chobitssp 2013-01-25 20:56发表[回复] [引用] [举报]
回复chlele0105:为了应对两个不同字符串拥有相同hash值的情况
117楼 乡下程序员 2013-01-02 14:54发表[回复] [引用] [举报]
堆排序那部分。是不是遍历一遍Query得到Top10的Value,但此时的key是不知道的是吧?然后就再遍历一遍,得到Top10Value对应的key是吗?
这个是我根据大哥你的思路,碰到的问题之一。
116楼 shenfuding_cn 2012-12-03 10:52发表[回复] [引用] [举报]
自己对数据结构和算法方面的很弱,你能不能给我介绍一本这方面的书呢,我想学习一下
115楼 baaanana 2012-09-26 21:04发表[回复] [引用] [举报]
有个地方不是很了解,为什么hash表的长度为质数会好?
Re: Aviation 2012-10-08 12:38发表[回复] [引用] [举报]
回复baaanana:现在java中默认的都是要求2的次方 好像是计算机执行%运算是最快的 以前也是要求质数 不知道到底哪个是对的 求解
114楼 xianmuou 2012-09-07 10:34发表[回复] [引用] [举报]
有一点不能明白:“然而Blizzard的程序员使用的方法则是更精妙的方法。基本原理就是:他们在哈希表中不是用一个哈希值而是用三个哈希值来校验字符串。”    这个方法能解决查找冲突吗?貌似它只是减少了对比字符串KEY的时间跟存储的空间。我的理解对吗?如果建表的时候采取的也是“顺延”方式,那么在查找的时候采用上面的那个查找算法会出现问题吧。
113楼 wzwjustdoit 2012-08-21 20:36发表[回复] [引用] [举报]
高深的算法啊,不知道有谁能在面试时写出那么牛逼的哈希函数。。。
112楼 cugb_zlf 2012-08-20 16:31发表[回复] [引用] [举报]
膜拜
111楼 一丝晨光 2012-08-10 10:10发表[回复] [引用] [举报]
我就觉得哪里看到似的:楼主中间关于MPQ的说明是从
http://hi.baidu.com/cpuramdisk/item/ca805bc619c69ac7984aa014
这里摘录的吗?
110楼 gfenghappy 2012-08-02 09:51发表[回复] [引用] [举报]
请教:有一个庞大的字符串数组,然后给你一个单独的字符
串,让你从这个数组中查找是否有这个字符串并找到它,你会怎么做?这个高效的做法是采用hash表,我想请教的是,如果庞大的字符串为12adfasdfasdfasdfasdfasdf......,而要查找的字符串为:123ad。那么怎么做hash呢?是分别对:12adf、2adfa、adfas、dfasd等等这样依次做hash么?这里没太看明白,请帮忙解答下。谢谢。
Re: 量子人生 2012-11-02 23:54发表[回复] [引用] [举报]
回复gfenghappy:注意是是说的字符串数组,不是很长很长的字符串
Re: danielxc29 2012-12-19 20:58发表[回复] [引用] [举报]
回复unifirst:你说的这个有KMP算法的,O(n)的复杂度
109楼 rejoice914 2012-07-22 23:20发表[回复] [引用] [举报]
引用“cowcga”的评论:如果用hash的方法统计Query,貌似有个问题,hash函数产生的冲突没有考虑,也就是2个Quer...

TO 楼主,我也有这个疑问。
若果2个Query 取到的哈希值一样,那就是有冲突,这是时候value值+1的话是不是有问题呢,也就是错误统计了
108楼 boy_zh 2012-05-31 20:31发表[回复] [引用] [举报]
用HashTable统计Query的次数时,并没有考虑每次读取Query时判断Query是不是已经存在于HashTable中所需要的时间。
107楼 lengyuewuxin 2012-05-18 10:11发表[回复] [引用] [举报]
把字符串存储在哈希表中的话,光哈希表占的内存就要超过1G了吧!1G也就一千万个字节呀!300百万个字符为255Bit好像超过了1G!
Re: jinlongzhang2012 2012-05-23 22:05发表[回复] [引用] [举报]
回复lengyuewuxin:1G是千兆,约为十亿字节吧,你好像弄错了
106楼 shaopeng5115 2012-05-12 21:01发表[回复] [引用] [举报]
博主在讲解哈希表的时候最好是先举例,然后在讲解,比如,可以举例怎样把"a" "ab" "abc" "abcd" "abcde" "abcdef" "abcdefg"这几个实际的字符串构造成哈希表,这样讲解起来比较直观
然后在讲解hash表的理论知识,貌似没有提到一个很关键的hash表的填装因子,这个参数跟hash表的平均查找长度直接相关
Re: v_JULY_v 2012-05-14 23:26发表[回复] [引用] [举报]
回复shaopeng5115:真不错的建议
105楼 cowcga 2012-05-09 15:18发表[回复] [引用] [举报]
如果用hash的方法统计Query,貌似有个问题,hash函数产生的冲突没有考虑,也就是2个Query的hash值一样,但你把他们统计成一样的了。
Re: peterzhe 2012-09-20 01:44发表[回复] [引用] [举报]
回复cowcga:文章不是说  在hash数组后面跟链表么   就可以解决冲突了啊
104楼 zhanshaoxin 2012-05-09 09:56发表[回复] [引用] [举报]
300 万的数据1G内存放得下吗?每个255Byte,不是bit.
Re: liuhuiyi 2012-12-05 17:10发表[回复] [引用] [举报]
回复zhanshaoxin:内存都是以字节为单位的,你看看你的电脑内存2G就是指2GB
Re: lengyuewuxin 2012-05-18 10:53发表[回复] [引用] [举报]
回复zhanshaoxin:楼上正解呀,我也这么认为的!
Re: 终点 2012-08-11 20:51发表[回复] [引用] [举报]
300万*0.255kb=765000kb=747mb<1G啊!回复lengyuewuxin:
103楼 coFinder 2012-04-28 21:41发表[回复] [引用] [举报]
1、直接排序法的时候你说用归并排序:怎么就能实现需要2G内存的操作,1g之内就解决了呢?

归并(Merge)排序法不是将两个(或两个以上)有序表合并成一个新的有序表么?(算法学都不是很好,见笑了)
102楼 fqhlysliuyansen 2012-04-01 11:28发表[回复] [引用] [举报]
顶楼主大牛 
还望以后多出精品
Re: v_JULY_v 2012-04-02 10:15发表[回复] [引用] [举报]
回复fqhlysliuyansen:NO problem
101楼 a06062125 2012-03-31 20:35发表[回复] [引用] [举报]
楼主是不是这个意思,从剩下的Query中取出一个,分别与大小为K的数组中的每个Query比较频率,这样时间复杂度就为N*K,是这样的吗?
100楼 a06062125 2012-03-31 20:31发表[回复] [引用] [举报]
楼主,同72楼的疑问
在第二步的算法二:部分排序中,
首先维护一个有序的Query数组,然后取其它的Query与最小的Query频率对比,如果大则加入当前的Query。加入后是不是应该对新的Query数组进行排序,以确保每次都是和出现次数最小的那个Query进行对比?
还有对时间复杂度为N*K还是不太明白,楼主能不能详细解释一下,呵呵
Re: pengkelian 2013-02-11 20:58发表[回复] [引用] [举报]
回复a06062125:寻找最大的数用最小堆干嘛啊?
Re: v_JULY_v 2012-03-31 21:08发表[回复] [引用] [举报]
回复a06062125:1、部分排序中,如寻找最大的k个数,采取的是堆机制,比如可以建立最小堆,堆顶存放的是整个堆中最小的数,现在遍历N个数,把最先遍历到的k个数存放到最小堆中,并假设它们就是我们要找的最大的k个数,X1>X2...Xmin(堆顶),而后遍历后续的N-K个数,一一与堆顶元素进行比较,如果遍历到的Xn大于堆顶元素Xmin,则把Xn放入堆中,而后更新整个堆,更新的时间复杂度为logK,如果Xn<Xmin,则不更新堆,整个过程的复杂度为O(K)+O((N-K)*logK)=O(N*logK)。
2、至于你所问的关于N*K的复杂度则就更简单了,同样,寻找最小的k个数,这时如果你采取的数据结构不是堆,而是数组的话,和上面同样的原理,遍历N个数,把最先遍历到的k个数存入数组中,并假设它们就是你要找的最小的k个数,并对数组进行排序,找到整个数组中最小的数Xmin,而后遍历后面的N-K个数,如果比Xmin大,则放入数组中,而重新排序一次数组每次所花费的时间为O(K),这里可做选择或交换排序,每排序一次,花费开销O(K),所以才有最后的O(N*K)之说。
具体还可以看看blog内程序员编程艺术第3章的内容。

我这么解释,你明白了么?
99楼 画蛇添足 2012-03-30 21:36发表[回复] [引用] [举报]
写的真是精辟!
98楼 牛逼飞飞 2012-03-20 10:37发表[回复] [引用] [举报]
太牛叉了,膜拜算法工程师
97楼 likun_tech 2012-03-02 19:18发表[回复] [引用] [举报]
最后的描述:那个是用三个hash函数的逻辑是不是有问题?
根据第一个hash值,在数组中定位,然后比较另外两个hash值。如果相等,返回true。
如果不相等,怎么还要在其他的数组单元进行比较呢?个人觉得这个事没有必要的。应该是在该数组单元相对应的链表中进行其他的比较(第一个hash值相同的情况下,后两个hash值不同的形成链表)
Re: iii_9 2012-05-24 03:52发表[回复] [引用] [举报]
回复likun_tech:“Blizzard使用的哈希表没有使用链表,而采用"顺延"的方式来解决问题。”
访问链表会慢,访问数组快,但是访问数组得直到空位才能停下,所以有可能比较次数会多。具体选哪种得看实际问题,并不是只有链表这一种
96楼 XIE_KUN 2012-02-16 12:53发表[回复] [引用] [举报]
楼主,我有个疑问,就是那个顺延方式,到底是是采用怎样的方式呢,就如57楼所言,感觉还是不明白,希望能详细解释下,谢谢!
95楼 lsnwbd 2011-11-04 16:04发表[回复] [引用] [举报]
好东西,学习了
94楼 etlds 2011-11-03 20:58发表[回复] [引用] [举报]
多谢!学习了,很强大的文章,后面的HASH函数没细看,好伤脑筋。。。
93楼 Never_for_Never 2011-10-29 17:12发表[回复] [引用] [举报]
因你,果断开了CSDN博客;特此说明!
向大侠多多学习!
92楼 Java_beginer1 2011-10-22 11:31发表[回复] [引用] [举报]
那一堆hash函数看的头疼
91楼 jpc 2011-09-18 03:05发表[回复] [引用] [举报]
有个问题不解 第一步的目的是为了统计频率 直接排序法的目的是这个吗?为何需要排序操作?
90楼 zhangxixrh 2011-09-17 23:55发表[回复] [引用] [举报]
写的太好了,非常感谢
89楼 giianhui 2011-09-14 11:54发表[回复] [引用] [举报]
受益匪浅
88楼 tfeizsh 2011-09-13 13:59发表[回复] [引用] [举报]
看不懂啊,好深奥
87楼 Dreamer 2011-09-13 11:03发表[回复] [引用] [举报]
楼主好强大,好好学习ing...
86楼 Bronts 2011-08-25 23:39发表[回复] [引用] [举报]
楼主,引用别人的图,在参考资料里注明一下吧,http://www.cppblog.com/guogangj/archive/2009/10/15/98699.html
85楼 hanweify 2011-08-21 11:24发表[回复] [引用] [举报]
看不懂呢
84楼 xiaoma_mj 2011-08-18 09:54发表[回复] [引用] [举报]
好帖!楼主强大!学习ing~
83楼 star143133 2011-08-13 22:58发表[回复] [引用] [举报]
学习中
82楼 云计算爱好者 2011-08-05 15:08发表[回复] [引用] [举报]
这是我看过最强大的一个帖子!
81楼 Softwarelb0809 2011-07-30 17:12发表[回复] [引用] [举报]
楼主很强大,多看看你的文章有长进啊
80楼 liuzhengxi2010 2011-06-28 22:53发表[回复] [引用] [举报]
[e01]
79楼 liuzhengxi2010 2011-06-28 22:53发表[回复] [引用] [举报]
以后要每天上你的博客学学学习习习
Re: v_JULY_v 2011-06-28 23:15发表[回复] [引用] [举报]
回复 liuzhengxi2010:[e03]
78楼 xietingzi123456789 2011-06-19 11:03发表[回复] [引用] [举报]
学习中, 很多不太懂啊,深奥
77楼 ithaijun 2011-06-19 10:51发表[回复] [引用] [举报]
其实我感觉上面同学发言对于那个一边遍历一边计数就行,完全没有必要同时进行排序的……
76楼 lost_ding 2011-06-17 21:26发表[回复] [引用] [举报]
“Blizzard使用的哈希表没有使用链表,而采用&quot;顺延&quot;的方式来解决问题”,我对“顺延”法有一点疑问:如果在数组的每一项后不挂接一个链表,而是采用顺延,是不是相当于:对于哈希表的每一个位置,只能存放一个数据,如此,一个哈希表岂不是只能存放TableSize个数据,那怎么处理数据冲突呢?
Re: lost_ding 2011-06-17 21:28发表[回复] [引用] [举报]
回复 lost_ding:不知是否我理解错误,求教!
75楼 南京浪人甲 2011-06-16 18:09发表[回复] [引用] [举报]
哦,我错了。。。
74楼 南京浪人甲 2011-06-16 18:07发表[回复] [引用] [举报]
“算法二:部分排序
    。。。。。

    不难分析出,这样,算法的最坏时间复杂度是N*K, 其中K是指top多少。”
对于这部分我有点疑问,最坏的情况,是不是每次遍历的时候,都要剔除最后一个数,而且还要把K个数重新拍一遍啊?那么时间复杂度是不是就成了N*K*logK了?
Re: mabinma 2013-01-17 21:34发表[回复] [引用] [举报]
回复shanki_pm:是 这一部分我也没怎么看懂 我理解为:
1)top10里面不就行排序,每次新来一个query直接找其中最小的元素(复杂度O(K)),大于最小的元素就替换掉,否则保持不变。真个过程数组都保持无序。只需要最后对数据排一下顺序就可。这样的话复杂度为O(N*K)
2)维持数组的顺序,对新来的元素,如果频率大于最后一个(即最小的一个)则替换掉,然后进行重新排序。当然这个排序可以是顺序的,因为除了最后一个元素外,其它都是有序的,只需要找打插叙的位置即可,平均情况下需要找K/2次。所以复杂度为O(N*K)。

虽然理解上说得通,但是不知是否和作者意图一致。
73楼 zisefengye 2011-06-16 12:56发表[回复] [引用] [举报]
抱歉,我没细看你后续内容,原来你还是把数据按大小插入数组的,是我不够细心
Re: v_JULY_v 2011-06-16 14:17发表[回复] [引用] [举报]
回复 zisefengye:[e03]
72楼 zisefengye 2011-06-16 12:13发表[回复] [引用] [举报]
对于第二步的算法二:部分排序,我有点不明白。我假设数组是这样的。100 95 90 87 81 77 70 66 54 45,假如下次读取了98,那么数组最后一条记录是98,当读取80或者任意小于98而大于数组中其他记录的数时,能保证这个数组中的记录是我们要的top10的记录么?
71楼 zccwqw 2011-06-07 15:40发表[回复] [引用] [举报]
你好,有个问题想请教,MPQ采用的是“顺延”的方式存储,那么哈希表的长度必然是比要存储的字符串个数多。当一个字符串不存在的时候,函数4中就要遍历整个哈希表。岂不是遍历更多?我的理解是此时比较的是int值而不是字符串,所以快,不知道你的理解是?
70楼 prettykernel 2011-05-18 16:01发表[回复] [引用] [举报]
但是题目中有明确要求,那就是内存不能超过1G,一千万条记录,每条记录是225Byte,很显然要占据2.55G内存,这个条件就不满足要求了。
其中,225Byte应为255Byte,占据2.55G内存应为占据2.375G内存
Re: v_JULY_v 2011-05-18 17:11发表[回复] [引用] [举报]
回复 erazy0:恩,谢谢指正,这就修正,[e10]
69楼 njusthsy 2011-05-06 16:43发表[回复] [引用] [举报]
支持!!!
68楼 lovestanford 2011-05-05 16:31发表[回复] [引用] [举报]
羡慕,学习
67楼 hairen 2011-05-03 07:35发表[回复] [引用] [举报]
群主高!![e01][e01][e01]
66楼 cnlm2 2011-04-21 18:05发表[回复] [引用] [举报]
[e03]作者研究的不错!
65楼 雪野狼孤 2011-04-21 10:39发表[回复] [引用] [举报]
[e07]还是没太明白!!
64楼 d34501 2011-04-20 23:37发表[回复] [引用] [举报]
当使用最大堆时,在进行堆排序后,数组top[k]中的k个Query是从大到小排列,后面进行比较时,是与top[k]中最后一个元素top[k-1]进行比较;当使用最小堆时,堆排序后数组top[k]中的元素是从小到大排列,这时后面的Query是与top[0]进行比较。[e04]
Re: v_JULY_v 2011-04-21 07:32发表[回复] [引用] [举报]
回复 d34501:最大堆是用来查找最小的k个数,非top k。你与堆中最后一个元素top [k-1],那样只会耗费搜索时间。为何不用最小堆,直接与堆顶元素比较列?即快捷,又达标。
Re: v_JULY_v 2011-04-21 07:26发表[回复] [引用] [举报]
回复 d34501:另外,有一个问题,帮你指出,既然都建立了最小堆,便是用堆来存储top k个数,与数组无任何瓜葛。
Re: v_JULY_v 2011-04-21 07:22发表[回复] [引用] [举报]
回复 d34501:回复 d34501:no,寻找最大的K个数,或top k,只能建立最小堆的,与堆顶最小元素比较,堆顶最小元素设为Kmin,Km大于K3大于K2大于K1大于Kmin,遇到比top[0],即Kmin大的元素,就用它替换掉Kmin,否则,不更新堆。
63楼 d34501 2011-04-20 23:37发表[回复] [引用] [举报]
在第二部分:找出Top 10中的堆排序算法,既可以用最大堆,也可用最小堆。
查看更多评论
发表评论
  • 用 户 名:
  • shamozhizhou511
  •  
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
TOP
  • 个人资料
    • 访问:4762351次
    • 积分:27371分
    • 排名:第47名
    • 原创:138篇
    • 转载:0篇
    • 译文:5篇
    • 评论:10749条
    • 博客公告
    • ①.本blog开通于2010年10月11日,高级C++/算法交流群:128691433;北京程序员联盟:172727781。②.狂热算法,热爱数据挖掘,关注机器学习、统计分析,爱好文学数学。③.微博:研究者July,邮箱:zhoulei97@aliyun.com,或zhoulei0907@yahoo.cn,July,二零一三年八月七日。
    • 我的微博
      • 博客专栏
      • 最新评论
        量子统计
        • 1
          点赞
        • 3
          收藏
          觉得还不错? 一键收藏
        • 0
          评论

        “相关推荐”对你有帮助么?

        • 非常没帮助
        • 没帮助
        • 一般
        • 有帮助
        • 非常有帮助
        提交
        评论
        添加红包

        请填写红包祝福语或标题

        红包个数最小为10个

        红包金额最低5元

        当前余额3.43前往充值 >
        需支付:10.00
        成就一亿技术人!
        领取后你会自动成为博主和红包主的粉丝 规则
        hope_wisdom
        发出的红包
        实付
        使用余额支付
        点击重新获取
        扫码支付
        钱包余额 0

        抵扣说明:

        1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
        2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

        余额充值