• 《算法导论》读书笔记之第9章 中位数和顺序统计学


    摘要:

      本章所讨论的问题是在一个由n个不同数值构成的集合中选择第i个顺序统计量问题。主要讲的内容是如何在线性时间内O(n)时间内在集合S中选择第i小的元素,最基本的是选择集合的最大值和最小值。一般情况下选择的元素是随机的,最大值和最小值是特殊情况,书中重点介绍了如何采用分治算法来实现选择第i小的元素,并借助中位数进行优化处理,保证最坏保证运行时间是线性的O(n)。

    1、基本概念

      顺序统计量:在一个由n个元素组成的集合中,第i个顺序统计量是值该集合中第i小的元素。例如最小值是第1个顺序统计量,最大值是第n个顺序统计量。

          中位数:一般来说,中位数是指它所在集合的“中间元素”,当n为奇数时,中位数是唯一的,出现位置为n/2;当n为偶数时候,存在两个中位数,位置分别为n/2(上中位数)和n/2+1(下中位数)。

    2、选择问题描述

      输入:一个包含n个(不同的)数的集合A和一个数i,1≤i≤n。

         输出:元素x∈A,它恰大于A中其他的i-1个元素。

    最直接的办法就是采用一种排序算法先对集合A进行排序,然后输出第i个元素即可。可以采用前面讲到的归并排序、堆排序和快速排序,运行时间为O(nlgn)。接下来书中由浅入深的讲如何在线性时间内解决这个问题。

    3、最大值和最小值

      要在集合中选择最大值和最小值,可以通过两两元素比较,并记录最大值和最小值,n元素的集合需要比较n-1次,这样运行时间为O(n)。举个例子来说明,现在要求和集合A={32,12,23,67,45,78}的最大值,开始假设第一个元素最大,即max=1,然后从第二个元素开始向后比较,记录最大值的位置。执行过程如下图所示:

    书中给出的求最小值的伪代码如下:

    1 MINMUN(A)
    2    min = A[1]
    3    for i=1 to length(A)
    4       do if min > A[i]
    5                then  min >= A[i]
    6   return min

    问题:
    (1)同时找出集合的最大值和最小值

    方法1:按照上面讲到的方法,分别独立的找出集合的最大值和最小值,各用n-1次比较,共有2n-2次比较。

    方法2:可否将最大值和最小值结合在一起寻找呢?答案是可以的,在两两比较过程中同时记录最大值和最小值,这样最大需要3n/2次比较。现在的做法不是将每一个      输入元素与当前的最大值和最小值进行比较,而是成对的处理元素,先将一对输入元素进行比较,然后把较大者与当前最大值比较,较小者与当前最小者比较,因此每两个元素需要3次比较。初始设置最大值和最小值方法:如何n为奇数,就将最大值和最小值都设置为第一个元素的值,然后成对的处理后续的元素。如果n为偶数,那么先比较前面两个元素的值,较大的设置为最大值,较小的设置为最小值,然后成对处理后续的元素。这样做的目的保证能够成对的处理后续的元素。举个例子说明这个过程,假设现在要找出集合A={32,23,12,67,45,78,10,39,9,58}最大值和最小值,执行过程如下:

    从图中可以看出方法2要比方法一要好,减少了元素之间的比较次数。现在用C语言实现方法2,程序如下:

    View Code
     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 
     4 //return max and min value by pointer
     5 void get_max_min(int *datas,int length,int* ptrmax,int* ptrmin)
     6 {
     7     int i,maxtmp,mintmp;
     8     //judge length is even or odd
     9     if(length %2 == 0)
    10     {
    11         if(datas[0] > datas[1])
    12         {
    13             *ptrmax = datas[0];
    14             *ptrmin = datas[1];
    15         }
    16         else
    17         {
    18             *ptrmax = datas[1];
    19             *ptrmin = datas[0];
    20         }
    21     }
    22     else
    23     {
    24         *ptrmax = datas[0];
    25         *ptrmin = datas[0];
    26     }
    27     for(i=2;i<length;i+=2)
    28     {
    29         if(datas[i] > datas[i+1])
    30         {
    31             maxtmp = datas[i];
    32             mintmp = datas[i+1];
    33         }
    34         else
    35         {
    36             maxtmp = datas[i+1];
    37             mintmp = datas[i];
    38         }
    39         if(*ptrmax < maxtmp)
    40             *ptrmax = maxtmp;
    41         if(*ptrmin > mintmp)
    42             *ptrmin = mintmp;
    43     }
    44 }
    45 
    46 int main()
    47 {
    48     int max,min;
    49     int i;
    50     int datas[10] = {23,12,34,26,78,45,87,15,60,19};
    51     get_max_min(datas,10,&max,&min);
    52     printf("All elements in set are:\n");
    53     for(i=0;i<10;++i)
    54         printf("%d ",datas[i]);
    55     putchar('\n');
    56     printf("max=%d\tmin=%d\n",max,min);
    57     exit(0);
    58 }

    程序测试结果如下:

    (2)如何找出找出n个元素中的第2小元素。

    解答:类似与上面的同时找出最大值和最小值的方法2,变成同时找最小值和第2小元素值。先初始化最小值和第2小的值,然后成对比较后续的元素,找出较小的元素与当前最小值和第二小值进行比较,在三者中找出最小值和第二小值。

    4、以期望线性时间做选择

      一般的选择问题似乎要比选择最大值和最小值要难,但是这两种问题的运行时间是相同的,都是θ(n)。书中介绍了采用分治算法解决一般的选择问题,其过程与快速排序过程中划分类似。每次划分集合可以确定一个元素的最终位置,根据这个位置可以判断是否是我们要求的第i小的元素。如果不是,那么我们只关心划分产出两个子部分中的其中一个,根据i的值来判断是前一个还是后一个,然后接着对子数组进行划分,重复此过程,直到找到第i个小的元素。划分可以采用随机划分,这样能够保证期望时间是θ(n)(假设所有元素是不同的)。

      给个例子说明此过程,假设现有集合A={32,23,12,67,45,78,10,39,9,58},要求其第5小的元素,假设在划分过程中以总是以最后一个元素为主元素进行划分。执行过程如下图所示:

    书中给出了返回A[p...r]中的第i小元素的伪代码:

     1 RANDOMIZED_SELECT(A,p,r,i)
     2       if p==r
     3          then return A[p]
     4       q = RANDOMIZED_PARTITION(A,p,r)
     5       k = q-p+1;
     6       if i==k
     7          then return A[q]
     8       else  if i<k
     9           then return RANDOMIZED_SELECT(A,p,q-1,i)
    10       else
    11           return RANDOMIZED_SELECT(A,p,q-1,i-k)

    RANDOMIZED_SELECT通过对输入数组的递归划分来找出所求元素,该算法要保证对数组的划分是个好划分才更加高效。RANDOMIZED_SELECT的最坏情况运行时间为θ(n^2),即使是选择最小元素也是如此。因为在每次划分过程中,导致划分后两边不对称,总好是按照剩下元素中最大的划分进行。为了更好的选择过程,我采用C语言实现求集合A={32,23,12,67,45,78,10,39,9,58}的第i小的元素,完成程序如下:

    View Code
     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 #include <time.h>
     4 
     5 size_t  randomized_partition(int* datas,size_t beg,size_t last);
     6 void swap(int* a,int *b);
     7 int randomized_select_one(int* datas,int beg,int last,int i);
     8 int randomized_select_two(int* datas,int length,int i);
     9 
    10 int main()
    11 {
    12     int datas[10]={32,23,12,67,45,78,10,39,9,58};
    13     int i,ret;
    14     printf("The array is: \n");
    15     for(i=0;i<10;++i)
    16         printf("%d ",datas[i]);
    17     printf("\n");
    18     for(i=1;i<=10;++i)
    19     {
    20        //ret=randomized_select_one(datas,0,9,i);
    21        ret=randomized_select_two(datas,10,i);
    22        printf("The %dth least number is: %d \n",i,datas[i-1]);
    23     }
    24     exit(0);
    25 }
    26 /*
    27 参数介绍:datas是待划分的数组,数组下标从0开始。
    28 beg代表开始位置,last代表结束位置、封闭区间[beg,last]
    29 */
    30 size_t  randomized_partition(int* datas,size_t beg,size_t last)
    31 {
    32     int len,i,j,index;
    33     len = last-beg+1;
    34     //随机获取一个主元
    35     srand(time(NULL));
    36     index = beg + rand()%len;
    37     //将主元交换到末尾
    38     swap(datas+index,datas+last);
    39     //从第一个元素开始向后查找主元的位置
    40     i=beg;
    41     for(j=beg;j<last;j++)
    42     {
    43         if(datas[j] < datas[last])
    44         {
    45             swap(datas+i,datas+j);
    46             i++;
    47         }
    48     }
    49     //最终确定主元的位置
    50     swap(datas+i,datas+last);
    51     return i;
    52 }
    53 /*
    54 参数介绍:datas是待查找的数组,数组下标从0开始。
    55 beg代表开始位置,last代表结束位置、封闭区间[beg,last]
    56 i表示要要查找第i小元素,i从1开始
    57 */
    58 int randomized_select_one(int* datas,int beg,int last,int i)
    59 {
    60     int pivot,k;
    61     if(beg == last)
    62         return datas[beg];
    63     pivot = randomized_partition(datas,beg,last);
    64     k = pivot-beg+1;
    65     if(k == i)
    66         return datas[pivot];
    67     else if(k < i)
    68         randomized_select_one(datas,pivot+1,last,i-k);
    69     else
    70         randomized_select_one(datas,beg,pivot-1,i);
    71 }
    72 /*
    73 参数介绍:datas是待查找的数组,数组下标从0开始。
    74 length表示数组的长度,数组下标范围[0,length-1]
    75 i表示要要查找第i小元素,i从1开始
    76 */
    77 int randomized_select_two(int* datas,int length,int i)
    78 {
    79     int pivot,k,j;
    80     if(length == 1)
    81       return datas[length-1];
    82     pivot = randomized_partition(datas,0,length-1);
    83     //确定是主元是第k小
    84     k = pivot+1;
    85     if(k == i)
    86         return datas[pivot];
    87     else if(k < i)
    88         randomized_select_two(datas+k,length-k,i-k);
    89     else
    90         randomized_select_two(datas,pivot,i);
    91 }
    92 
    93 void swap(int* a,int *b)
    94 {
    95     int temp = *a;
    96     *a = *b;
    97     *b = temp;
    98 }

    程序测试结果如下所示:


    程序中要注意的细节问题是:C语言中数组的小标是从0开始的,而要求第i小元素中的i是从1开始的,即第1小的元素对应与最终的主元位置0,依次类推。

    5、最坏情况线性时间的选择

      SELECT算法的思想是要保证对数组的划分是个好的划分,对PARTITION过程进行了修改。现在通过SELECT算法来确定n个元素的输入数组中的第i小的元素,具体操作步骤如下:

    (1)将输入数组的n个元素划分为n/5(上取整)组,每组5个元素,且至多只有一个组有剩下的n%5个元素组成。(为何是5,而不是其他数,有点不明白。)

    (2)寻找每个组织中中位数。首先对每组中的元素(至多为5个)进行插入排序,然后从排序后的序列中选择出中位数。

    (3)对第2步中找出的n/5(上取整)个中位数,递归调用SELECT以找出其中位数x。(如果是偶数去下中位数)

    (4)调用PARTITION过程,按照中位数x对输入数组进行划分。确定中位数x的位置k。

    (5)如果i=k,则返回x。否则,如果i<k,则在地区间递归调用SELECT以找出第i小的元素,若干i>k,则在高区找第(i-k)个最小元素。

    SELECT算法通过中位数进行划分,可以保证每次划分是对称的,这样就能保证最坏情况下运行时间为θ(n)。举个例子说明此过程,求集合A={32,23,12,67,45,78,10,39,9,58,125,84}的第5小的元素,操作过程如下图所示:

    现在采用C语言实现上面的例子,完整程序如下所示:

    View Code
     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 
     4 int partition(int* datas,int beg,int last,int mid);
     5 int select(int* datas,int length,int i);
     6 void swap(int* a,int *b);
     7 
     8 int main()
     9 {
    10     int datas[12]={32,23,12,67,45,78,10,39,9,58,125,84};
    11     int i,ret;
    12     printf("The array is: \n");
    13     for(i=0;i<12;++i)
    14         printf("%d ",datas[i]);
    15     printf("\n");
    16     for(i=1;i<=12;++i)
    17     {
    18        ret=select(datas,12,i);
    19        printf("The %dth least number is: %d \n",i,datas[i-1]);
    20     }
    21     exit(0);
    22 }
    23 
    24 int partition(int* datas,int beg,int last,int mid)
    25 {
    26     int i,j;
    27     swap(datas+mid,datas+last);
    28     i=beg;
    29     for(j=beg;j<last;j++)
    30     {
    31         if(datas[j] < datas[last])
    32         {
    33             swap(datas+i,datas+j);
    34             i++;
    35         }
    36     }
    37     swap(datas+i,datas+last);
    38     return i;
    39 }
    40 
    41 int select(int* datas,int length,int i)
    42 {
    43     int groups,pivot;
    44     int j,k,t,q,beg,glen;
    45     int mid;
    46     int temp,index;
    47     int *pmid;
    48     if(length == 1)
    49         return datas[length-1];
    50     if(length % 5 == 0)
    51         groups = length/5;
    52     else
    53         groups = length/5 +1;
    54     pmid = (int*)malloc(sizeof(int)*groups);
    55     index = 0;
    56     for(j=0;j<groups;j++)
    57     {
    58         beg = j*5;
    59         glen = beg+5;
    60         for(t=beg+1;t<glen && t<length;t++)
    61         {
    62             temp = datas[t];
    63             for(q=t-1;q>=beg && datas[q] > datas[q+1];q--)
    64                     swap(datas+q,datas+q+1);
    65             swap(datas+q+1,&temp);
    66         }
    67         glen = glen < length ? glen : length;
    68         pmid[index++] = beg+(glen-beg)/2;
    69     }
    70     for(t=1;t<groups;t++)
    71     {
    72         temp = pmid[t];
    73         for(q=t-1;q>=0 && datas[pmid[q]] > datas[pmid[q+1]];q--)
    74             swap(pmid+q,pmid+q+1);
    75         swap(pmid+q+1,&temp);
    76     }
    77    //printf("mid indx = %d,mid value=%d\n",pmid[groups/2],datas[pmid[groups/2]]);
    78     mid = pmid[groups/2];
    79     pivot = partition(datas,0,length-1,mid);
    80     //printf("pivot=%d,value=%d\n",pivot,datas[pivot]);
    81     k = pivot+1;
    82     if(k == i)
    83         return datas[pivot];
    84     else if(k < i)
    85         return select(datas+k,length-k,i-k);
    86     else
    87         return select(datas,pivot,i);
    88 
    89 }
    90 
    91 void swap(int* a,int *b)
    92 {
    93     int temp = *a;
    94     *a = *b;
    95     *b = temp;
    96 }

    程序测试结果如下所示:

    总结

      本章中的选择算法之所以具有线性运行时间,是因为这些算法没有进行排序,线性时间的行为并不是因为对输入做假设所得到的结果。

  • 相关阅读:
    元学习Meta Learning/Learning to learn
    TRAINING A CLASSIFIER训练分类器(pytorch官网60分钟闪电战第四节)
    NEURAL NETWORKS神经网络(pytorch官网60分钟闪电战第三节)
    AUTOGRAD: 自动分化(pytorch官网60分钟闪电战第二节)
    WHAT IS PYTORCH?(pytorch官网60分钟闪电战第一节)
    windows找不到gpedit.msc
    The “freeze_support()“ line can be omitted if the program is not going to be frozen to produ
    torch.mul() 和 torch.mm() 的区别
    vue面试题(2)
    JS输出题练习
  • 原文地址:https://www.cnblogs.com/Anker/p/2877311.html
Copyright © 2020-2023  润新知