分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow

也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!

               

                     程序员编程艺术:第三章续、Top K算法问题的实现


    作者:July,zhouzhenren,yansha。
    致谢:微软100题实现组,狂想曲创作组。
    时间:2011年05月08日
    微博:http://weibo.com/julyweibo 
    出处:http://blog.csdn.net/v_JULY_v 
    wiki:http://tctop.wikispaces.com/
-----------------------------------------------

 

前奏
    在上一篇文章,程序员面试题狂想曲:第三章、寻找最小的k个数中,后来为了论证类似快速排序中partition的方法在最坏情况下,能在O(N)的时间复杂度内找到最小的k个数,而前前后后updated了10余次。所谓功夫不负苦心人,终于得到了一个想要的结果。

    简单总结如下(详情,请参考原文第三章):
    1、RANDOMIZED-SELECT,以序列中随机选取一个元素作为主元,可达到线性期望时间O(N)的复杂度。
    2、SELECT,快速选择算法,以序列中“五分化中项的中项”,或“中位数的中位数”作为主元(枢纽元),则不容置疑的可保证在最坏情况下亦为O(N)的复杂度。

    本章,咱们来阐述寻找最小的k个数的反面,即寻找最大的k个数,但此刻可能就有读者质疑了,寻找最大的k个数和寻找最小的k个数,原理不是一样的么?

    是的,的确是一样,但这个寻找最大的k个数的问题的实用范围更广,因为它牵扯到了一个Top K算法问题,以及有关搜索引擎,海量数据处理等广泛的问题,所以本文特意对这个Top K算法问题,进行阐述以及实现(侧重实现,因为那样看起来,会更令人激动人心),算是第三章的续。ok,有任何问题,欢迎随时不吝指正。谢谢。

 

说明

      关于寻找最小K个数能做到最坏情况下为O(N)的算法及证明,请参考原第三章,寻找最小的k个数,本文的代码不保证O(N)的平均时间复杂度,只是根据第三章有办法可以做到而已(如上面总结的,2、SELECT,快速选择算法,以序列中“五分化中项的中项”,或“中位数的中位数”作为主元或枢纽元的方法,原第三章已经严格论证并得到结果)。

 

第一节、寻找最小的第k个数

     在进入寻找最大的k个数的主题之前,先补充下关于寻找最k小的数的三种简单实现。由于堆的完整实现,第三章:第五节,堆结构实现,处理海量数据中已经给出,下面主要给出类似快速排序中partition过程的代码实现:

寻找最小的k个数,实现一(下段代码经本文评论下多位读者指出有问题:a [ i ]=a [ j ]=pivot时,则会产生一个无限循环,在Mark Allen Weiss的数据结构与算法分析C++描述中文版的P209-P210有描述,读者可参看之。特此说明,因本文代码存在问题的地方还有几处,故请待后续统一修正.2012.08.21):

//[email protected] mark allen weiss && July && yansha  //July,yansha、updated,2011.05.08.    //本程序,后经飞羽找出错误,已经修正。  //随机选取枢纽元,寻找最小的第k个数  #include <iostream>  #include <stdlib.h>  using namespace std;    int my_rand(int low, int high)  {      int size = high - low + 1;      return  low + rand() % size;   }    //q_select places the kth smallest element in a[k]  int q_select(int a[], int k, int left, int right){    if(k > right || k < left)    {//         cout<<"---------"<<endl;   //为了处理当k大于数组中元素个数的异常情况        return false;    }    //真正的三数中值作为枢纽元方法,关键代码就是下述六行 int midIndex = (left + right) / 2if(a[left] < a[midIndex])  swap(a[left], a[midIndex]); if(a[right] < a[midIndex])  swap(a[right], a[midIndex]); if(a[right] < a[left])  swap(a[right], a[left]); swap(a[left], a[right]);    int pivot = a[right];   //之前是int pivot = right,特此,修正。    // 申请两个移动指针并初始化    int i = left;    int j = right-1;    // 根据枢纽元素的值对数组进行一次划分    for (;;)    {  while(a[i] < pivot)   i++;        while(a[j] > pivot)            j--;  //a[i] >= pivot, a[j] <= pivot        if (i < j)            swap(a[i], a[j]); //a[i] <= a[j]        else            break;    }    swap(a[i], a[right]);    /* 对三种情况进行处理    1、如果i=k,即返回的主元即为我们要找的第k小的元素,那么直接返回主元a[i]即可;    2、如果i>k,那么接下来要到低区间A[0....m-1]中寻找,丢掉高区间;    3、如果i<k,那么接下来要到高区间A[m+1...n-1]中寻找,丢掉低区间。    */ if (i == k)  return trueelse if (i > k)  return q_select(a, k, left, i-1); else return q_select(a, k, i+1, right);}  int main()  {      int i;      int a[] = {7, 8, 9, 54, 6, 4, 11, 1, 2, 33};      q_select(a, 4, 0, sizeof(a) / sizeof(int) - 1);      return 0;  }  

寻找最小的第k个数,实现二:

  1. //[email protected] July  
  2. //yansha、updated,2011.05.08。  
  3. // 数组中寻找第k小元素,实现二  
  4. #include <iostream>  
  5. using namespace std;  
  6.   
  7. const int numOfArray = 10;  
  8.   
  9. // 这里并非真正随机  
  10. int my_rand(int low, int high)  
  11. {  
  12.     int size = high - low + 1;  
  13.     return low + rand() % size;   
  14. }  
  15.   
  16. // 以最末元素作为主元对数组进行一次划分  
  17. int partition(int array[], int left, int right)      
  18. {                              
  19.     int pos = right;  
  20.     for(int index = right - 1; index >= left; index--)  
  21.     {  
  22.         if(array[index] > array[right])  
  23.             swap(array[--pos], array[index]);  
  24.     }  
  25.     swap(array[pos], array[right]);  
  26.     return pos;  
  27. }  
  28.   
  29. // 随机快排的partition过程   
  30. int random_partition(int array[], int left, int right)          
  31. {  
  32.     // 随机从范围left到right中取一个值作为主元  
  33.     int index = my_rand(left, right);                          
  34.     swap(array[right], array[index]);      
  35.       
  36.     // 对数组进行划分,并返回主元在数组中的位置  
  37.     return partition(array, left, right);                   
  38. }  
  39.   
  40. // 以线性时间返回数组array[left...right]中第k小的元素  
  41. int random_select(int array[], int left, int right, int k)      
  42. {  
  43.     // 处理异常情况  
  44.     if (k < 1 || k > (right - left + 1))  
  45.         return -1;  
  46.       
  47.     // 主元在数组中的位置  
  48.     int pos = random_partition(array, left, right);       
  49.       
  50.     /* 对三种情况进行处理:(m = i - left + 1) 
  51.     1、如果m=k,即返回的主元即为我们要找的第k小的元素,那么直接返回主元array[i]即可; 
  52.     2、如果m>k,那么接下来要到低区间array[left....pos-1]中寻找,丢掉高区间; 
  53.     3、如果m<k,那么接下来要到高区间array[pos+1...right]中寻找,丢掉低区间。 
  54.     */  
  55.     int m = pos - left + 1;  
  56.     if(m == k)  
  57.         return array[pos];                               
  58.     else if (m > k)         
  59.         return random_select(array, left, pos - 1, k);  
  60.     else   
  61.         return random_select(array, pos + 1, right, k - m);  
  62. }  
  63.   
  64. int main()  
  65. {      
  66.     int array[numOfArray] = {7, 8, 9, 54, 6, 4, 2, 1, 12, 33};  
  67.     cout << random_select(array, 0, numOfArray - 1, 4) << endl;  
  68.     return 0;  
  69. }  
//[email protected] July//yansha、updated,2011.05.08。// 数组中寻找第k小元素,实现二#include <iostream>using namespace std;const int numOfArray = 10;// 这里并非真正随机int my_rand(int low, int high){    int size = high - low + 1;    return low + rand() % size; }// 以最末元素作为主元对数组进行一次划分int partition(int array[], int left, int right)    {                                int pos = right;    for(int index = right - 1; index >= left; index--)    {        if(array[index] > array[right])            swap(array[--pos], array[index]);    }    swap(array[pos], array[right]);    return pos;}// 随机快排的partition过程 int random_partition(int array[], int left, int right)        {    // 随机从范围left到right中取一个值作为主元    int index = my_rand(left, right);                            swap(array[right], array[index]);            // 对数组进行划分,并返回主元在数组中的位置    return partition(array, left, right);                 }// 以线性时间返回数组array[left...right]中第k小的元素int random_select(int array[], int left, int right, int k)    {    // 处理异常情况    if (k < 1 || k > (right - left + 1))        return -1;     // 主元在数组中的位置    int pos = random_partition(array, left, right);       /* 对三种情况进行处理:(m = i - left + 1) 1、如果m=k,即返回的主元即为我们要找的第k小的元素,那么直接返回主元array[i]即可; 2、如果m>k,那么接下来要到低区间array[left....pos-1]中寻找,丢掉高区间; 3、如果m<k,那么接下来要到高区间array[pos+1...right]中寻找,丢掉低区间。 */    int m = pos - left + 1;    if(m == k)        return array[pos];                                 else if (m > k)               return random_select(array, left, pos - 1, k);    else         return random_select(array, pos + 1, right, k - m);}int main(){        int array[numOfArray] = {7, 8, 9, 54, 6, 4, 2, 1, 12, 33};    cout << random_select(array, 0, numOfArray - 1, 4) << endl;    return 0;}

寻找最小的第k个数,实现三:

  1. //求取无序数组中第K个数,本程序枢纽元的选取有问题,不作推荐。    
  2. //[email protected] 飞羽   
  3. //July、yansha,updated,2011.05.18。     
  4. #include <iostream>    
  5. #include <time.h>   
  6. using namespace std;     
  7.   
  8. int kth_elem(int a[], int low, int high, int k)     
  9. {     
  10.     int pivot = a[low];    
  11.     //这个程序之所以做不到O(N)的最最重要的原因,就在于这个枢纽元的选取。           
  12.     //而这个程序直接选取数组中第一个元素作为枢纽元,是做不到平均时间复杂度为 O(N)的。  
  13.       
  14.     //要 做到,就必须 把上面选取枢纽元的 代码改掉,要么是随机选择数组中某一元素作为枢纽元,能达到线性期望的时间  
  15.     //要么是选取数组中中位数的中位数作为枢纽元,保证最坏情况下,依然为线性O(N)的平均时间复杂度。  
  16.     int low_temp = low;     
  17.     int high_temp = high;     
  18.     while(low < high)     
  19.     {     
  20.         while(low < high && a[high] >= pivot)       
  21.             --high;     
  22.         a[low] = a[high];     
  23.         while(low < high && a[low] < pivot)     
  24.             ++low;     
  25.         a[high] = a[low];     
  26.     }     
  27.     a[low] = pivot;     
  28.       
  29.     //以下就是主要思想中所述的内容     
  30.     if(low == k - 1)      
  31.         return a[low];     
  32.     else if(low > k - 1)      
  33.         return kth_elem(a, low_temp, low - 1, k);     
  34.     else      
  35.         return kth_elem(a, low + 1, high_temp, k);     
  36. }     
  37.   
  38. int main()   //以后尽量不再用随机产生的数组进行测试,没多大必要。  
  39. {  
  40.     for (int num = 5000; num < 50000001; num *= 10)  
  41.     {  
  42.         int *array = new int[num];  
  43.           
  44.         int j = num / 10;  
  45.         int acc = 0;  
  46.         for (int k = 1; k <= num; k += j)  
  47.         {  
  48.             // 随机生成数据  
  49.             srand(unsigned(time(0)));  
  50.             for(int i = 0; i < num; i++)     
  51.                 array[i] = rand() * RAND_MAX + rand();      
  52.             //”如果数组本身就是利用随机化产生的话,那么选择其中任何一个元素作为枢轴都可以看作等价于随机选择枢轴,  
  53.             //(虽然这不叫随机选择枢纽)”,这句话,是完全不成立的,是错误的。  
  54.               
  55.             //“因为你总是选择 随机数组中第一个元素 作为枢纽元,不是 随机选择枢纽元”  
  56.             //相当于把上面这句话中前面的 “随机” 两字去掉,就是:  
  57.             //因为 你总是选择数组中第一个元素作为枢纽元,不是 随机选择枢纽元。  
  58.             //所以,这个程序,始终做不到平均时间复杂度为O(N)。  
  59.               
  60.             //随机数组和给定一个非有序而随机手动输入的数组,是一个道理。稍后,还将就程序的运行结果继续解释这个问题。  
  61.             //July、updated,2011.05.18。  
  62.               
  63.             // 计算一次查找所需的时钟周期数  
  64.             clock_t start = clock();  
  65.             int data = kth_elem(array, 0, num - 1, k);  
  66.             clock_t end = clock();  
  67.             acc += (end - start);  
  68.         }  
  69.         cout << "The average time of searching a date in the array size of " << num << " is " << acc / 10 << endl;  
  70.     }  
  71.     return 0;     
  72. }    
//求取无序数组中第K个数,本程序枢纽元的选取有问题,不作推荐。  //[email protected] 飞羽 //July、yansha,updated,2011.05.18。   #include <iostream>  #include <time.h> using namespace std;   int kth_elem(int a[], int low, int high, int k)   {       int pivot = a[low];      //这个程序之所以做不到O(N)的最最重要的原因,就在于这个枢纽元的选取。          //而这个程序直接选取数组中第一个元素作为枢纽元,是做不到平均时间复杂度为 O(N)的。  //要 做到,就必须 把上面选取枢纽元的 代码改掉,要么是随机选择数组中某一元素作为枢纽元,能达到线性期望的时间 //要么是选取数组中中位数的中位数作为枢纽元,保证最坏情况下,依然为线性O(N)的平均时间复杂度。    int low_temp = low;       int high_temp = high;       while(low < high)       {           while(low < high && a[high] >= pivot)                 --high;           a[low] = a[high];           while(low < high && a[low] < pivot)               ++low;           a[high] = a[low];       }       a[low] = pivot;        //以下就是主要思想中所述的内容       if(low == k - 1)            return a[low];       else if(low > k - 1)            return kth_elem(a, low_temp, low - 1, k);       else            return kth_elem(a, low + 1, high_temp, k);   }   int main()   //以后尽量不再用随机产生的数组进行测试,没多大必要。{ for (int num = 5000; num < 50000001; num *= 10) {  int *array = new int[num];    int j = num / 10;  int acc = 0;  for (int k = 1; k <= num; k += j)  {   // 随机生成数据   srand(unsigned(time(0)));   for(int i = 0; i < num; i++)       array[i] = rand() * RAND_MAX + rand();       //”如果数组本身就是利用随机化产生的话,那么选择其中任何一个元素作为枢轴都可以看作等价于随机选择枢轴,   //(虽然这不叫随机选择枢纽)”,这句话,是完全不成立的,是错误的。      //“因为你总是选择 随机数组中第一个元素 作为枢纽元,不是 随机选择枢纽元”   //相当于把上面这句话中前面的 “随机” 两字去掉,就是:   //因为 你总是选择数组中第一个元素作为枢纽元,不是 随机选择枢纽元。   //所以,这个程序,始终做不到平均时间复杂度为O(N)。      //随机数组和给定一个非有序而随机手动输入的数组,是一个道理。稍后,还将就程序的运行结果继续解释这个问题。   //July、updated,2011.05.18。      // 计算一次查找所需的时钟周期数   clock_t start = clock();   int data = kth_elem(array, 0, num - 1, k);   clock_t end = clock();   acc += (end - start);  }  cout << "The average time of searching a date in the array size of " << num << " is " << acc / 10 << endl; }    return 0;   }     测试:
The average time of searching a date in the array size of 5000 is 0
The average time of searching a date in the array size of 50000 is 1
The average time of searching a date in the array size of 500000 is 12
The average time of searching a date in the array size of 5000000 is 114
The average time of searching a date in the array size of 50000000 is 1159
Press any key to continue

通过测试这个程序,我们竟发现这个程序的运行时间是线性的?
或许,你还没有意识到这个问题,ok,听我慢慢道来。
我们之前说,要保证这个算法是线性的,就一定要在枢纽元的选取上下足功夫:
1、要么是随机选取枢纽元作为划分元素
2、要么是取中位数的中位数作为枢纽元划分元素
 
现在,这程序直接选取了数组中第一个元素作为枢纽元
竟然,也能做到线性O(N)的复杂度,这不是自相矛盾么?
你觉得这个程序的运行时间是线性O(N),是巧合还是确定会是如此?

哈哈,且看1、@well:根据上面的运行结果不能判断线性,如果人家是O(n^1.1) 也有可能啊,而且部分数据始终是拟合,还是要数学证明才可靠。2、@July:同时,随机数组中选取一个元素作为枢纽元!=> 随机数组中随机选取一个元素作为枢纽元(如果是随机选取随机数组中的一个元素作为主元,那就不同了,跟随机选取数组中一个元素作为枢纽元一样了)。3、@飞羽:正是因为数组本身是随机的,所以选择第一个元素和随机选择其它的数是等价的(由等概率产生保证),这第3点,我与飞羽有分歧,至于谁对谁错,待时间让我考证。

关于上面第3点我和飞羽的分歧,在我们进一步讨论之后,一致认定(不过,相信,你看到了上面程序更新的注释之后,你应该有几分领会了):

  1. 我们说输入一个数组的元素,不按其顺序输入:如,1,2,3,4,5,6,7,而是这样输入:5,7,6,4,3,1,2,这就叫随机输入,而这种情况就相当于上述程序主函数中所产生的随机数组。然而选取随机输入的数组或随机数组中第一个元素作为主元,我们不能称之为说是随机选取枢纽元。
  2. 因为,随机数产生器产生的数据是随机的,没错,但你要知道,你总是选取随机数组的第一个元素作为枢纽元,这不叫随机选取枢纽元。
  3. 所以,上述程序的主函数中随机产生的数组对这个程序的算法而言,没有任何意义,就是帮忙产生了一个随机数组,帮助我们完成了测试,且方便我们测试大数据量而已,就这么简单。
  4. 且一般来说,我们看一个程序的 时间复杂度,是不考虑 其输入情况的,即不考虑主函数,正如这个 kth number 的程序所见,你每次都是随机选取数组中第一个元素作为枢纽元,而并不是随机选择枢纽元,所以,做不到平均时间复杂度为O(N)。 

所以:想要保证此快速选择算法为O(N)的复杂度,只有两种途径,那就是保证划分的枢纽元元素的选取是:
1、随机的(注,此枢纽元随机不等同于数组随机)
2、五分化中项的中项,或中位数的中位数。

所以,虽然咱们对于一切心知肚明,但上面程序的运行结果说明不了任何问题,这也从侧面再次佐证了咱们第三章中观点的正确无误性。 

updated:

    非常感谢飞羽等人的工作,将上述三个版本综合到了一起(待进一步测试):

///下面的代码对July博客中的三个版本代码进行重新改写。欢迎指出错误。  ///先把它们贴在这里,还要进行随机化数据测试。待发...    //modified by 飞羽 at 2011.5.11  /////Top_K_test    //修改了下命名规范,July、updated,2011.05.12。  #include <iostream>  #include <stdlib.h>  using namespace std;    inline int my_rand(int low, int high)  {      int size = high - low + 1;      return  low + rand() % size;  }    int partition(int array[], int left, int right)  {      int pivot = array[right];      int pos = left-1;      for(int index = left; index < right; index++)      {          if(array[index] <= pivot)              swap(array[++pos], array[index]);      }      swap(array[++pos], array[right]);      return pos;//返回pivot所在位置  }    bool median_select(int array[], int left, int right, int k)  {      //第k小元素,实际上应该在数组中下标为k-1      if (k-1 > right || k-1 < left)             return false;        //真正的三数中值作为枢纽元方法,关键代码就是下述六行      int midIndex=(left+right)/2;      if(array[left]<array[midIndex])          swap(array[left],array[midIndex]);      if(array[right]<array[midIndex])          swap(array[right],array[midIndex]);      if(array[right]<array[left])          swap(array[right],array[left]);      swap(array[left], array[right]);            int pos = partition(array, left, right);            if (pos == k-1)          return true;      else if (pos > k-1)          return median_select(array, left, pos-1, k);      else return median_select(array, pos+1, right, k);  }    bool rand_select(int array[], int left, int right, int k)  {      //第k小元素,实际上应该在数组中下标为k-1      if (k-1 > right || k-1 < left)             return false;        //随机从数组中选取枢纽元元素      int Index = my_rand(left, right);      swap(array[Index], array[right]);            int pos = partition(array, left, right);            if (pos == k-1)          return true;      else if (pos > k-1)          return rand_select(array, left, pos-1, k);      else return rand_select(array, pos+1, right, k);  }    bool kth_select(int array[], int left, int right, int k)  {      //直接取最原始的划分操作      if (k-1 > right || k-1 < left)             return false;        int pos = partition(array, left, right);      if(pos == k-1)          return true;      else if(pos > k-1)          return kth_select(array, left, pos-1, k);      else return kth_select(array, pos+1, right, k);  }    int main()  {      int array1[] = {7, 8, 9, 54, 6, 4, 11, 1, 2, 33};       int array2[] = {7, 8, 9, 54, 6, 4, 11, 1, 2, 33};       int array3[] = {7, 8, 9, 54, 6, 4, 11, 1, 2, 33};             int numOfArray = sizeof(array1) / sizeof(int);      for(int i=0; i<numOfArray; i++)          printf("%d/t",array1[i]);            int K = 9;      bool flag1 = median_select(array1, 0, numOfArray-1, K);      bool flag2 = rand_select(array2, 0, numOfArray-1, K);      bool flag3 = kth_select(array3, 0, numOfArray-1, K);      if(!flag1)           return 1;      for(i=0; i<K; i++)          printf("%d/t",array1[i]);      printf("/n");            if(!flag2)           return 1;      for(i=0; i<K; i++)          printf("%d/t",array2[i]);      printf("/n");            if(!flag3)           return 1;      for(i=0; i<K; i++)          printf("%d/t",array3[i]);      printf("/n");            return 0;  }  

    说明:@飞羽:因为预先设定了K,经过分割算法后,数组肯定被划分为array[0...k-1]和array[k...length-1],注意到经过Select_K_Version操作后,数组是被不断地分割的,使得比array[k-1]的元素小的全在左边,题目要求的是最小的K个元素,当然也就是array[0...k-1],所以输出的结果就是前k个最小的数:

7       8       9       54      6       4       11      1       2       33
4       1       2       6       7       8       9       11      33
7       6       4       1       2       8       9       11      33
7       8       9       6       4       11      1       2       33
Press any key to continue

(更多,请参见:此狂想曲系列tctop修订wiki页面:http://tctop.wikispaces.com/


第二节、寻找最大的k个数
把之前第三章的问题,改几个字,即成为寻找最大的k个数的问题了,如下所述:
查找最大的k个元素
题目描述:输入n个整数,输出其中最大的k个。
例如输入1,2,3,4,5,6,7和8这8个数字,则最大的4个数字为8,7,6和5。

    分析:由于寻找最大的k个数的问题与之前的寻找最小的k个数的问题,本质是一样的,所以,这里就简单阐述下思路,ok,考验你举一反三能力的时间到了:

    1、排序,快速排序。我们知道,快速排序平均所费时间为n*logn,从小到大排序这n个数,然后再遍历序列中后k个元素输出,即可,总的时间复杂度为O(n*logn+k)=O(n*logn)。

    2、排序,选择排序。用选择或交换排序,即遍历n个数,先把最先遍历到得k个数存入大小为k的数组之中,对这k个数,利用选择或交换排序,找到k个数中的最小数kmin(kmin设为k个元素的数组中最小元素),用时O(k)(你应该知道,插入或选择排序查找操作需要O(k)的时间),后再继续遍历后n-k个数,x与kmin比较:如果x>kmin,则x代替kmin,并再次重新找出k个元素的数组中最小元素kmin‘(多谢jiyeyuran 提醒修正);如果x<kmin,则不更新数组。这样,每次更新或不更新数组的所用的时间为O(k)或O(0),整趟下来,总的时间复杂度平均下来为:n*O(k)=O(n*k)。

    3、维护k个元素的最小堆,原理与上述第2个方案一致,即用容量为k的最小堆存储最先遍历到的k个数,并假设它们即是最大的k个数,建堆费时O(k),并调整堆(费时O(logk))后,有k1>k2>...kmin(kmin设为小顶堆中最大元素)。继续遍历数列,每次遍历一个元素x,与堆顶元素比较,若x>kmin,则更新堆(用时logk),否则不更新堆。这样下来,总费时O(k*logk+(n-k)*logk)=O(n*logk)。此方法得益于在堆中,查找等各项操作时间复杂度均为logk(不然,就如上述思路2所述:直接用数组也可以找出最大的k个元素,用时O(n*k))。

    4、按编程之美第141页上解法二的所述,类似快速排序的划分方法,N个数存储在数组S中,再从数组中随机选取一个数X,把数组划分为Sa和Sb俩部分,Sa>=X>=Sb,如果要查找的k个元素小于Sa的元素个数,则返回Sa中较大的k个元素,否则返回Sa中所有的元素+Sb中最大的k-|Sa|个元素。不断递归下去,把问题分解成更小的问题,平均时间复杂度为O(N)(编程之美所述的n*logk的复杂度有误,应为O(N),特此订正。其严格证明,请参考第三章:程序员面试题狂想曲:第三章、寻找最小的k个数、updated 10次)。
   .........

   其它的方法,在此不再重复了,同时,寻找最小的k个数借助堆的实现,代码在上一篇文章第三章已有给出,更多,可参考第三章,只要把最大堆改成最小堆,即可。


第三节、Top K 算法问题
3.1、搜索引擎热门查询统计

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

    分析:这个问题在之前的这篇文章十一、从头到尾彻底解析Hash表算法里,已经有所解答。方法是:

    第一步、先对这批海量数据预处理,在O(N)的时间内用Hash表完成统计(之前写成了排序,特此订正。July、2011.04.27);
    第二步、借助堆这个数据结构,找出Top K,时间复杂度为N‘logK。
        即,借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此,维护一个K(该题目中是10)大小的小根堆(K1>K2>....Kmin,Kmin设为堆顶元素),然后遍历300万的Query,分别和根元素Kmin进行对比比较(如上第2节思路3所述,若X>Kmin,则更新并调整堆,否则,不更新),我们最终的时间复杂度是:O(N) + N'*O(logK),(N为1000万,N’为300万)。ok,更多,详情,请参考原文。

    或者:采用trie树,关键字域存该查询串出现的次数,没有出现为0。最后用10个元素的最小推来对出现频率进行排序。

    ok,本章里,咱们来实现这个问题,为了降低实现上的难度,假设这些记录全部是一些英文单词,即用户在搜索框里敲入一个英文单词,然后查询搜索结果,最后,要你统计输入单词中频率最大的前K个单词。ok,复杂问题简单化了之后,编写代码实现也相对轻松多了,画的简单示意图(绘制者,yansha),如下:

程序员编程艺术 第三章续 Top K算法问题的实现

完整源码:

  1. //[email protected] &&July  
  2. //July、updated,2011.05.08  
  3.   
  4. //题目描述:  
  5. //搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的  
  6. //长度为1-255字节。假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果  
  7. //除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门),  
  8. //请你统计最热门的10个查询串,要求使用的内存不能超过1G。  
  9.   
  10. #include <iostream>  
  11. #include <string>  
  12. #include <assert.h>  
  13. using namespace std;  
  14.   
  15. #define HASHLEN 2807303  
  16. #define WORDLEN 30  
  17.   
  18. // 结点指针  
  19. typedef struct node_no_space *ptr_no_space;  
  20. typedef struct node_has_space *ptr_has_space;  
  21. ptr_no_space head[HASHLEN];  
  22.   
  23. struct node_no_space   
  24. {  
  25.     char *word;  
  26.     int count;  
  27.     ptr_no_space next;  
  28. };  
  29.   
  30. struct node_has_space  
  31. {  
  32.     char word[WORDLEN];  
  33.     int count;  
  34.     ptr_has_space next;  
  35. };  
  36.   
  37. // 最简单hash函数  
  38. int hash_function(char const *p)  
  39. {  
  40.     int value = 0;  
  41.     while (*p != '/0')  
  42.     {  
  43.         value = value * 31 + *p++;  
  44.         if (value > HASHLEN)  
  45.             value = value % HASHLEN;  
  46.     }  
  47.     return value;  
  48. }  
  49.   
  50. // 添加单词到hash表  
  51. void append_word(char const *str)  
  52. {  
  53.     int index = hash_function(str);  
  54.     ptr_no_space p = head[index];  
  55.     while (p != NULL)  
  56.     {  
  57.         if (strcmp(str, p->word) == 0)  
  58.         {  
  59.             (p->count)++;  
  60.             return;  
  61.         }  
  62.         p = p->next;  
  63.     }  
  64.       
  65.     // 新建一个结点  
  66.     ptr_no_space q = new node_no_space;  
  67.     q->count = 1;  
  68.     q->word = new char [strlen(str)+1];  
  69.     strcpy(q->word, str);  
  70.     q->next = head[index];  
  71.     head[index] = q;  
  72. }  
  73.   
  74.   
  75. // 将单词处理结果写入文件  
  76. void write_to_file()  
  77. {  
  78.     FILE *fp = fopen("result.txt""w");  
  79.     assert(fp);  
  80.       
  81.     int i = 0;  
  82.     while (i < HASHLEN)  
  83.     {  
  84.         for (ptr_no_space p = head[i]; p != NULL; p = p->next)  
  85.             fprintf(fp, "%s  %d/n", p->word, p->count);  
  86.         i++;  
  87.     }  
  88.     fclose(fp);  
  89. }  
  90.   
  91. // 从上往下筛选,保持小根堆  
  92. void sift_down(node_has_space heap[], int i, int len)  
  93. {  
  94.     int min_index = -1;  
  95.     int left = 2 * i;  
  96.     int right = 2 * i + 1;  
  97.       
  98.     if (left <= len && heap[left].count < heap[i].count)  
  99.         min_index = left;  
  100.     else  
  101.         min_index = i;  
  102.       
  103.     if (right <= len && heap[right].count < heap[min_index].count)  
  104.         min_index = right;  
  105.       
  106.     if (min_index != i)  
  107.     {  
  108.         // 交换结点元素  
  109.         swap(heap[i].count, heap[min_index].count);  
  110.           
  111.         char buffer[WORDLEN];  
  112.         strcpy(buffer, heap[i].word);  
  113.         strcpy(heap[i].word, heap[min_index].word);  
  114.         strcpy(heap[min_index].word, buffer);  
  115.           
  116.         sift_down(heap, min_index, len);  
  117.     }  
  118. }  
  119.   
  120. // 建立小根堆  
  121. void build_min_heap(node_has_space heap[], int len)  
  122. {  
  123.     if (heap == NULL)  
  124.         return;  
  125.       
  126.     int index = len / 2;  
  127.     for (int i = index; i >= 1; i--)  
  128.         sift_down(heap, i, len);  
  129. }  
  130.   
  131. // 去除字符串前后符号  
  132. void handle_symbol(char *str, int n)  
  133. {  
  134.     while (str[n] < '0' || (str[n] > '9' && str[n] < 'A') || (str[n] > 'Z' && str[n] < 'a') || str[n] > 'z')  
  135.     {  
  136.         str[n] = '/0';  
  137.         n--;  
  138.     }  
  139.       
  140.     while (str[0] < '0' || (str[0] > '9' && str[0] < 'A') || (str[0] > 'Z' && str[0] < 'a') || str[0] > 'z')  
  141.     {  
  142.         int i = 0;  
  143.         while (i < n)  
  144.         {  
  145.             str[i] = str[i+1];  
  146.             i++;  
  147.         }  
  148.         str[i] = '/0';  
  149.         n--;  
  150.     }  
  151. }  
  152.   
  153. int main()  
  154. {  
  155.     char str[WORDLEN];  
  156.     for (int i = 0; i < HASHLEN; i++)  
  157.         head[i] = NULL;  
  158.       
  159.     // 将字符串用hash函数转换成一个整数并统计出现频率  
  160.     FILE *fp_passage = fopen("string.txt""r");  
  161.     assert(fp_passage);  
  162.     while (fscanf(fp_passage, "%s", str) != EOF)  
  163.     {  
  164.         int n = strlen(str) - 1;  
  165.         if (n > 0)  
  166.             handle_symbol(str, n);  
  167.         append_word(str);  
  168.     }  
  169.     fclose(fp_passage);  
  170.       
  171.     // 将统计结果输入文件  
  172.     write_to_file();  
  173.       
  174.     int n = 10;  
  175.     ptr_has_space heap = new node_has_space [n+1];  
  176.       
  177.     int c;  
  178.       
  179.     FILE *fp_word = fopen("result.txt""r");  
  180.     assert(fp_word);  
  181.     for (int j = 1; j <= n; j++)  
  182.     {  
  183.         fscanf(fp_word, "%s %d", &str, &c);  
  184.         heap[j].count = c;  
  185.         strcpy(heap[j].word, str);  
  186.     }  
  187.       
  188.     // 建立小根堆  
  189.     build_min_heap(heap, n);  
  190.       
  191.     // 查找出现频率最大的10个单词  
  192.     while (fscanf(fp_word, "%s %d", &str, &c) != EOF)  
  193.     {  
  194.         if (c > heap[1].count)  
  195.         {  
  196.             heap[1].count = c;  
  197.             strcpy(heap[1].word, str);  
  198.             sift_down(heap, 1, n);  
  199.         }  
  200.     }  
  201.     fclose(fp_word);  
  202.       
  203.     // 输出出现频率最大的单词  
  204.     for (int k = 1; k <= n; k++)  
  205.         cout << heap[k].count << " " << heap[k].word << endl;  
  206.       
  207.     return 0;  
  208. }  
//[email protected] &&July//July、updated,2011.05.08//题目描述://搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的//长度为1-255字节。假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果//除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门),//请你统计最热门的10个查询串,要求使用的内存不能超过1G。#include <iostream>#include <string>#include <assert.h>using namespace std;#define HASHLEN 2807303#define WORDLEN 30// 结点指针typedef struct node_no_space *ptr_no_space;typedef struct node_has_space *ptr_has_space;ptr_no_space head[HASHLEN];struct node_no_space { char *word; int count; ptr_no_space next;};struct node_has_space{ char word[WORDLEN]; int count; ptr_has_space next;};// 最简单hash函数int hash_function(char const *p){ int value = 0; while (*p != '/0') {  value = value * 31 + *p++;  if (value > HASHLEN)   value = value % HASHLEN; } return value;}// 添加单词到hash表void append_word(char const *str){ int index = hash_function(str); ptr_no_space p = head[index]; while (p != NULL) {  if (strcmp(str, p->word) == 0)  {   (p->count)++;   return;  }  p = p->next; }  // 新建一个结点 ptr_no_space q = new node_no_space; q->count = 1; q->word = new char [strlen(str)+1]; strcpy(q->word, str); q->next = head[index]; head[index] = q;}// 将单词处理结果写入文件void write_to_file(){ FILE *fp = fopen("result.txt", "w"); assert(fp);  int i = 0; while (i < HASHLEN) {  for (ptr_no_space p = head[i]; p != NULL; p = p->next)   fprintf(fp, "%s  %d/n", p->word, p->count);  i++; } fclose(fp);}// 从上往下筛选,保持小根堆void sift_down(node_has_space heap[], int i, int len){ int min_index = -1; int left = 2 * i; int right = 2 * i + 1;  if (left <= len && heap[left].count < heap[i].count)  min_index = left; else  min_index = i;  if (right <= len && heap[right].count < heap[min_index].count)  min_index = right;  if (min_index != i) {  // 交换结点元素  swap(heap[i].count, heap[min_index].count);    char buffer[WORDLEN];  strcpy(buffer, heap[i].word);  strcpy(heap[i].word, heap[min_index].word);  strcpy(heap[min_index].word, buffer);    sift_down(heap, min_index, len); }}// 建立小根堆void build_min_heap(node_has_space heap[], int len){ if (heap == NULL)  return;  int index = len / 2; for (int i = index; i >= 1; i--)  sift_down(heap, i, len);}// 去除字符串前后符号void handle_symbol(char *str, int n){ while (str[n] < '0' || (str[n] > '9' && str[n] < 'A') || (str[n] > 'Z' && str[n] < 'a') || str[n] > 'z') {  str[n] = '/0';  n--; }  while (str[0] < '0' || (str[0] > '9' && str[0] < 'A') || (str[0] > 'Z' && str[0] < 'a') || str[0] > 'z') {  int i = 0;  while (i < n)  {   str[i] = str[i+1];   i++;  }  str[i] = '/0';  n--; }}int main(){ char str[WORDLEN]; for (int i = 0; i < HASHLEN; i++)  head[i] = NULL;  // 将字符串用hash函数转换成一个整数并统计出现频率 FILE *fp_passage = fopen("string.txt", "r"); assert(fp_passage); while (fscanf(fp_passage, "%s", str) != EOF) {  int n = strlen(str) - 1;  if (n > 0)   handle_symbol(str, n);  append_word(str); } fclose(fp_passage);  // 将统计结果输入文件 write_to_file();  int n = 10; ptr_has_space heap = new node_has_space [n+1];  int c;  FILE *fp_word = fopen("result.txt", "r"); assert(fp_word); for (int j = 1; j <= n; j++) {  fscanf(fp_word, "%s %d", &str, &c);  heap[j].count = c;  strcpy(heap[j].word, str); }  // 建立小根堆 build_min_heap(heap, n);  // 查找出现频率最大的10个单词 while (fscanf(fp_word, "%s %d", &str, &c) != EOF) {  if (c > heap[1].count)  {   heap[1].count = c;   strcpy(heap[1].word, str);   sift_down(heap, 1, n);  } } fclose(fp_word);  // 输出出现频率最大的单词 for (int k = 1; k <= n; k++)  cout << heap[k].count << " " << heap[k].word << endl;  return 0;}

程序测试:咱们接下来,来对下面的通过用户输入单词后,搜索引擎记录下来,“大量”单词记录进行统计(同时,令K=10,即要你找出10个最热门查询的单词):

程序员编程艺术 第三章续 Top K算法问题的实现

运行结果:根据程序的运行结果,可以看到,搜索引擎记录下来的查询次数最多的10个单词为(注,并未要求这10个数要有序输出):in(312次),it(384次),a(432),that(456),MPQ(408),of(504),and(624),is(456),the(1008),to(936)。

程序员编程艺术 第三章续 Top K算法问题的实现

    读者反馈from 杨忠胜:3.1节的代码第38行 hash_function(char const *p)有误吧,这样的话,不能修改p的值(但是函数需要修改指针的值),要想不修改*p指向的内容,应该是const char *p; 此外,您程序中的/t,  /n有误,C语言是\t,\n。
    感谢这位读者的来信,日后统一订正。谢谢。

 

3.2、统计出现次数最多的数据

题目描述:
给你上千万或上亿数据(有重复),统计其中出现次数最多的前N个数据。

    分析:上千万或上亿的数据,现在的机器的内存应该能存下(也许可以,也许不可以)。所以考虑采用hash_map/搜索二叉树/红黑树等来进行统计次数。然后就是取出前N个出现次数最多的数据了。当然,也可以堆实现。

    ok,此题与上题类似,最好的方法是用hash_map统计出现的次数,然后再借用堆找出出现次数最多的N个数据。不过,上一题统计搜索引擎最热门的查询已经采用过hash表统计单词出现的次数,特此,本题咱们改用红黑树取代之前的用hash表,来完成最初的统计,然后用堆更新,找出出现次数最多的前N个数据。

    同时,正好个人此前用c && c++ 语言实现过红黑树,那么,代码能借用就借用吧。
完整代码

  1. //[email protected] zhouzhenren &&July  
  2. //July、updated,2011.05.08.  
  3.   
  4. //题目描述:  
  5. //上千万或上亿数据(有重复),统计其中出现次数最多的前N个数据  
  6.   
  7. //解决方案:  
  8. //1、采用红黑树(本程序中有关红黑树的实现代码来源于@July)来进行统计次数。  
  9. //2、然后遍历整棵树,同时采用最小堆更新前N个出现次数最多的数据。  
  10.   
  11. //声明:版权所有,引用必须注明出处。  
  12. #define PARENT(i) (i)/2  
  13. #define LEFT(i)   2*(i)  
  14. #define RIGHT(i)  2*(i)+1  
  15.   
  16. #include <stdio.h>  
  17. #include <stdlib.h>  
  18. #include <string.h>  
  19.   
  20. typedef enum rb_color{ RED, BLACK }RB_COLOR;  
  21. typedef struct rb_node  
  22. {  
  23.     int key;  
  24.     int data;  
  25.     RB_COLOR color;  
  26.     struct rb_node* left;  
  27.     struct rb_node* right;  
  28.     struct rb_node* parent;  
  29. }RB_NODE;  
  30.   
  31. RB_NODE* RB_CreatNode(int key, int data)  
  32. {  
  33.     RB_NODE* node = (RB_NODE*)malloc(sizeof(RB_NODE));  
  34.     if (NULL == node)  
  35.     {  
  36.         printf("malloc error!");  
  37.         exit(-1);  
  38.     }  
  39.       
  40.     node->key = key;  
  41.     node->data = data;  
  42.     node->color = RED;  
  43.     node->left = NULL;  
  44.     node->right = NULL;  
  45.     node->parent = NULL;  
  46.       
  47.     return node;  
  48. }  
  49.   
  50. /**  
  51. * 左旋   
  52. * 
  53. *  node           right  
  54. *  / /    ==>     / /  
  55. * a  right     node  y  
  56. *     / /       / /      
  57. *     b  y     a   b     
  58. */    
  59. RB_NODE* RB_RotateLeft(RB_NODE* node, RB_NODE* root)  
  60. {    
  61.     RB_NODE* right = node->right;    // 指定指针指向 right<--node->right    
  62.       
  63.     if ((node->right = right->left))      
  64.         right->left->parent = node;  // 好比上面的注释图,node成为b的父母  
  65.       
  66.     right->left = node;   // node成为right的左孩子   
  67.       
  68.     if ((right->parent = node->parent))    
  69.     {    
  70.         if (node == node->parent->right)    
  71.             node->parent->right = right;  
  72.         else    
  73.             node->parent->left = right;  
  74.     }    
  75.     else    
  76.         root = right;    
  77.       
  78.     node->parent = right;  //right成为node的父母    
  79.       
  80.     return root;    
  81. }    
  82.   
  83. /** 
  84. * 右旋   
  85. * 
  86. *      node            left  
  87. *       / /             / /  
  88. *     left y   ==>     a  node  
  89. *     / /                  / /  
  90. *    a   b                b   y   
  91. */    
  92. RB_NODE* RB_RotateRight(RB_NODE* node, RB_NODE* root)    
  93. {    
  94.     RB_NODE* left = node->left;    
  95.       
  96.     if ((node->left = left->right))    
  97.         left->right->parent = node;    
  98.       
  99.     left->right = node;    
  100.       
  101.     if ((left->parent = node->parent))    
  102.     {    
  103.         if (node == node->parent->right)      
  104.             node->parent->right = left;    
  105.         else    
  106.             node->parent->left = left;    
  107.     }    
  108.     else    
  109.         root = left;    
  110.       
  111.     node->parent = left;    
  112.       
  113.     return root;    
  114. }    
  115.   
  116. /**  
  117. * 红黑树的3种插入情况   
  118. * 用z表示当前结点, p[z]表示父母、p[p[z]]表示祖父, y表示叔叔. 
  119. */  
  120. RB_NODE* RB_Insert_Rebalance(RB_NODE* node, RB_NODE* root)    
  121. {    
  122.     RB_NODE *parent, *gparent, *uncle, *tmp;  //父母p[z]、祖父p[p[z]]、叔叔y、临时结点*tmp    
  123.       
  124.     while ((parent = node->parent) && parent->color == RED)    
  125.     { // parent 为node的父母,且当父母的颜色为红时    
  126.         gparent = parent->parent;   // gparent为祖父    
  127.           
  128.         if (parent == gparent->left)  // 当祖父的左孩子即为父母时,其实上述几行语句,无非就是理顺孩子、父母、祖父的关系。  
  129.         {  
  130.             uncle = gparent->right; // 定义叔叔的概念,叔叔y就是父母的右孩子。    
  131.             if (uncle && uncle->color == RED) // 情况1:z的叔叔y是红色的    
  132.             {   &

    给我老师的人工智能教程打call!http://blog.csdn.net/jiangjunshow

    程序员编程艺术 第三章续 Top K算法问题的实现

相关文章:

  • 2021-11-28
  • 2021-08-03
  • 2021-07-24
  • 2022-01-09
  • 2022-12-23
  • 2021-11-18
猜你喜欢
  • 2021-05-07
  • 2021-06-04
相关资源
相似解决方案