算法之个人总结:Hash表之简单应用

来源:互联网 发布:centos nginx安装目录 编辑:程序博客网 时间:2024/04/30 10:18

出处:http://chelu01.blog.163.com/blog/static/9417780520102512312223/

转帖理由:作者说的很通俗易懂,相信你会有所收获的!

 

前段时间看了个微软编写的C库函数,在这个库函数里学到一个自我感觉相当牛比的小算法,说白了是Hash表的应用。大家都知道,Hash表最主要是用来实现查找功能的,再具体点是用常量级时间复杂度找到你想查找的东西。首先,我以一个小问题引入将要介绍的Hash算法,问题如下:

现有字符串str1str2,编写一个函数返回str1中有多少个字符在str2字符串中。

其实这个函数也很简单(假如我们不考虑时间复杂度的话),遍历str1字符串,其中对于str1中的每一个字符都要去判断是否在str2字符串中,如果在统计变量加1,即需要两个for循环,即时间复杂度为0(LenA*LenB),程序也很简单,如下:

//返回字符串str1中有多少个字符在str2字符串中

int CharCount(const char* str1, const char* str2)

{

   int ct = 0;

   const char* ptr;

 

   for (;  *str1 != '/0';  ++str1)

  {

    //每次从str2的开头开始遍历

     for (ptr = str2; *ptr != '/0'; ++ptr)

     {

      if (*str1 == *ptr)   //如果发现str1当前字符在str2字符串中

       {

        ++ct;

         break;

       }

    }

  }

 return ct;

}

这个程序每次判断str1中的字符是否在字符串str2中时,str2字符串均需要从开头遍历,即每次需要回溯,从而使得整个算法的时间复杂度很高,那么我们是否可以用常量级时间复杂度来执行上面程序的内循环?即我们是否可以用常量级时间复杂度来判断某字符是否在字符串str2中?答案是肯定的,此时就需要Hash表。

首先我先叙述下关于这个思路微软的算法是什么。即如何用常量级时间复杂度来判断一个字符是否在某个字符串中。首先我们先解决这个问题,你能否按照某种映射方式将256个字符一一映射到一个数组里,换句话说已知char arr[32],你能否将256个字符映射到这个数组里?

我们都知道ASCII值总共是256个(即2^8),如果我们将这256个字符分组,每组字符个数是8,那么可以分256/8组,即32组,因此对于ASCII值为c的字符,它属于编号为c / 8 组(组的编号是0-31),因此,我们可以建立一个含有32个元素的数组,编号相同的8个字符存放在一个单元,在一个单元里如何来区分这8个字符呢?如果接着分析下去,易知,每一组中的8个字符除以8的余数均是0-7,因此,我们可以根据这8个字符除以8的余数来分别区分它们,具体办法如下:用一个char类型数据,char类型数据的最低位来表示除以8余数是0的那个字符,倒数第二位表示除以8余数是1的字符,从而可以将每一组中的8个字符用一个char类型数据的每一位表示。例如:ASCII值是49的字符应该这样存放在Hash表中(含有32个元素的数组),首先49/8等于6 即编号是6,其次49%8等于1,即这个单元的char数据的bit1位是1,即arr[6]中的char数据的二进制应该为 00000010;

有了上述算法的思想,那么我们就可以用char arr[32]来存放一个字符串str2了,依次遍历这个字符串,按照上述原则依次标记数组中相应位置,字符串str2中每一个字符c的位置是arr[ c/8],其次使得该char数据的第c%8 bit 1,假如arr数组中每一个元素初始值均为0,那么可以这样实现:

              //使得编号为c/8的单元中的char数据的第 c%8 bit 1

                            arr[ c/8 ]   =  arr[ c/8 ]   |   ( 1 << (c % 8) );

如果将str2中的每一个字符按照上述原则标记好了,假如我们要在str2中查找字符a,那么可以判断编号为a/8的单元中的char数据的第 a%8 bit是否为1,如果是说明字符astr2中,即:

       if  (   (arr[a / 8]  &  ( 1  <<  (a % 8)  ) )  != 0 )

          //说明字符a存在的

看看利用这种方法是不是实现了常量时间复杂度查找某字符是否在字符串中?嘿嘿!

重新回到开头的那个问题,判断str1中的字符是否在str2中,查找算法就可以用上述思路来实现,即只需要遍历str1字符串,之后用上述常量时间复杂度的查找算法判断str1当前字符是否在str2中,易知时间复杂度是O(LenA),算法如下:

int CharCount_(const char* str1, const char* str2)

{

     char hash[32];

 

     //hash表的初始化

     for (int i = 0; i < 32; ++i)

          hash = 0;

 

     //hash保存str2中所有字符

     const char* pstr2 = str2;

     for (; *pstr2 != '/0'; ++pstr2)

     {

          hash[*pstr2 / 8] |= (1 << (*pstr2 % 8));

     }

     int ct = 0;

 

     //遍历str1字符串,判断每个字符是否在hash表中(是否在str2字符串中)

     for (; *str1 != '/0'; ++str1)

     {

          if (hash[*str1 / 8] & (1 << (*str1 % 8)))

             ++ct;

     }

         return ct;

}

其实上述算法还可以加快,我们都知道计算机在处理除法以及取模运算时相对是较慢的,因此我们可以用位运算来实现上面的 除法和取模运算,*str1 / 8 等价于 *str1 >> 3,另外,一个数N除以 2^m 的余数 实际上是N的低 m bit位所表示的十进制数,即*str1%(2^3)等价于 *str1 & 7(具体可以自己想),因此上述算法的极限代码是:

int CharCount_(const char* str1, const char* str2)

{

     char hash[32];

 

     //hash表的初始化

     for (int i = 0; i < 32; ++i)

          hash = 0;

 

     //hash保存str2中所有字符

     const char* pstr2 = str2;

     for (; *pstr2 != '/0'; ++pstr2)

     {

          hash[*pstr2 >> 3] |= (1 << (*pstr2 & 7));      //优化之一

     }

     int ct = 0;

 

     //遍历str1字符串,判断每个字符是否在hash表中(是否在str2字符串中)

     for (; *str1 != '/0'; ++str1)

     {

          if (hash[*str1 >> 3] & (1 << (*str1 & 7)))    //优化之二

             ++ct;

     }

         return ct;

}

至此,整个算法已经详细给出。

 

总结:

1)看到此不知道你是否彻底理解了上述Hash表的建立,其实细心的读者可能会提出这样的问题,为什么把256个字符分成32组(每组只存放8个字符)?呵呵,其实不一定非得分成32组,也可以分成16组,也可以分成8组,。。。其实算法的本质是在内存中用256bits来表示这256个字符是否存在,我们完全可以分成16组,每一组用16bit来表示,即 short arr[16];或者 分成8组,即int arr[8],自己可以实现下。

2)现在你能否实现一个这样的函数:

  size_t strspn (const char *s,const char * accept);

 

  函数说明:strspn()从参数s 字符串的开头计算连续的字符,而这些字符都完全是accept 所指字符串中的字符。简单的说,若strspn()返回的数值为n,则代表字符串s 开头连续有n 个字符都是属于字符串accept内的字符,即字符串s中第n+1个字符不属于accept字符串中,要求时间复杂度为0(N)(嘿嘿,其实这就是一个C库函数,相信你时间复杂度为0(N*N)的算法马上就能写出来吧)

3)回顾文章开始那个问题,其实是利用了空间换取时间的思想,即增加了算法的空间复杂度(因为我们额外建立了一个Hash数组),但是算法的时间复杂度变成了O(N),因此在内存空间不是问题的情况下,利用这种增加空间复杂度换取时间复杂度的思想值得我们去考虑。

4)如果你足够细心,那么利用本篇介绍的Hash算法,你此时一定能将C库函数strtok实现出来,个人认为这个库函数是字符串库函数中最最难的一个,不仅其功能难理解,实现起来也很困难,呵呵,其实我就是在看微软编写的strtok源码的时候才发现了本篇这个hash算法的,哈哈,strtok这个函数一定要去看哦,诺西两年笔试题目均从不同角度考察了该函数的实现。

 

总结2strspn函数代码补充:

//算法时间复杂度为O(Len1*Len2)

int MyStrspn(const char* str1, const char* str2)

{

   const char* temp;

   int ct = 0;

   //非法输入

   if (str1 == NULL || str2 == NULL)

      return ct;

   for (; *str1 != '/0'; ++str1)

   {

     //每次令temp指向str2字符串开头,来判断*str1 是否在字符串str1

      for (temp = str2; *temp != '/0'; ++temp)

      {

         if (*str1 == *temp) //*str1在字符串str2中,那么停止

            break;

      }

 

      if (*temp == '/0')  //如果遍历完str2了(说明*str1不在str2中)

         break;

      else

         ++ct;

   }

   return ct;

}

//时间复杂度为O(Len1)

int MyStrspn_(const char* str1, const char* str2)

{

   //非法输入

   if (str1 == NULL || str2 == NULL)

      return 0;

   //分成16组,每组是16 bits2字节)

   short Hash[16];

   //hash表的初始化

   for (int i = 0; i < 16; ++i)

      Hash = 0;

 

   //str2字符串每一个字符映射到数组Hash

   const char* Pstr2;

   for (Pstr2 = str2; *Pstr2 != '/0'; ++Pstr2)

      Hash[*Pstr2 / 16] |= 1 << (*Pstr2 % 16);

   int ct = 0;

      //while( *str1在字符串str2 && *str1 != '/0'

   while ((Hash[*str1 / 16] & (1 << (*str1 % 16))) && (*str1 != '/0'))

   {

      ++str1;

      ++ct;

   }

   return ct;

}

 

以上只是个人看法,个人理解,如有不足请指正!3Q

原创粉丝点击