• 算法之线性时间选择(最坏情况下)


    线性时间选择(Linear Select):这个名字不太好理解,什么叫线性时间选择?一句话,在线性时间内完成选择。一般情况下是这样的,我们想要找出一个数组中的最大值或最小值,那就只需要一次排列,然后输出第一个或最后一个元素就行了,但如果是要找出一个数组中的第k小的元素呢?

      在一般情况下,可以用RandomizedSelect方法来找出第k小的元素,平均时间是O(n),但在最坏情况下,所用的时间则是n^2,因此,本文讨论的就是在最坏情况下,如何在O(n)时间内完成选择。算法的思路总体有些复杂,但每一步其实不难,下面即给大家介绍最坏情况下的线性时间选择算法。

    (1):将n个输入元素以每组5个地划分,共划分出(n/5)个组,每个组分别进行排列,找出中位数,然后按照每个组的顺序,把每个组的中位数与整个数组的前(n/5)个数交换;

    (2):那么,前(n/5)个数就是各组的中位数了,然后,我们通过select方法找出这些中位数的中位数,以这个中位数的中位数为基准,调用partition方法;

    (3):调用了partition方法后的基准元素正是处于数组的正确位置(前边的元素都比基准元素小,后边的元素都比基准元素大),记下基准元素前边的元素个数leftNum,如果k小于或等于leftNum,则在基准位置前的这部分调用select方法即可,如果在k大于leftNum,则在基准位置后的这部分调用select方法。

    下面,我直接把代码贴出,读者可以通过我的注释来理解每一步的意义。

     1 private static int select(int[] a,int l,int r,int k){
     2     if(r - l < 75){
     3         insertSort(a, l, r);    //用快速排序进行排序
     4         return a[l + k - 1];
     5     }
     6     int group = (r-l+5)/5;
     7     for(int i = 0;i<group;i++){
     8         int left = l+5*i;
     9         int right = (l + i * 5 + 4) > r ? r : l + i * 5 + 4;  //如果超出右边界就用右边界赋值
    10         int mid = (left+right)/2;
    11         insertSort(a, left, right);
    12         swap(a, l + i, mid);     // 将各组中位数与前i个 
    13     }
    14     int pivot = select(a,l,l+group-1,(group+1)/2);  //找出中位数的中位数
    15     int p = partition(a,l,r,pivot);    //用中位数的中位数作为基准的位置
    16     int leftNum = p - l;       //leftNum用来记录基准位置的前边的元素个数
    17     if (k == leftNum + 1)
    18         return a[p];
    19     else if (k <= leftNum)
    20         return select(a, l, p - 1, k);
    21     else                    //若k在基准位子的后边,则要从基准位置的后边数起,即第(k - leftNum - 1)个
    22         return select(a, p + 1, r, k - leftNum - 1);
    23 }

    到此大家也可以看出,这里的partition方法与前边讲到过的快速排序所用到的partition方法稍有不同,参数个数都变了,但其实变化只是很小,只是取消了一开始定义基准位置的步骤而已,代码如下:

     1 private static int partition(int[] a,int l,int r,int pivot){   //适用于线性时间选择的partition方法
     2     int i = l;
     3     int j = r;
     4     while(true){
     5         while(a[i] <= pivot && i < r)
     6             ++i;   //i一直向后移动,直到出现a[i]>pivot
     7         while(a[j] > pivot)
     8             --j;   //j一直向前移动,直到出现a[j]<pivot
     9         if(i >= j) break;
    10         swap(a,i,j);
    11     }
    12     a[l] = a[j];
    13     a[j] = pivot;
    14     return j;
    15 }

    下面是select方法中,如果输入规模小于75时用到的插入排序算法代码:

     1 private static void insertSort(int[] a, int law, int high) {    //插入排序
     2        for (int i = law + 1; i <= high; i++) {  
     3            int key = a[i];  
     4            int j = i - 1;  
     5            while (j >= law && a[j] > key) {  
     6                a[j + 1] = a[j];  
     7                j--;  
     8            }  
     9            a[j + 1] = key;  
    10        }  
    11 }

    适用于数组元素之间的swap方法如下:

    1 private static void swap(int[] a,int i,int j){
    2     int temp = a[i];
    3     a[i] = a[j];
    4     a[j] = temp;
    5 }

      各位可能有个疑问,为什么输入规模不足75时调用插入排序而不用线性时间选择呢?那是因为当输入规模不足75时,因为输入规模太小,时间复杂度几乎是一个常量,因此没有必要用到比较复杂的线性时间选择算法。

      我还看到一个比较好懂的学习线性时间选择的动画,能形象地看到线性时间选择的执行过程,链接如下:

    http://resource.jingpinke.com/details?uuid=ff808081-22e8911b-0122-e8912643-048d&objectId=oid:ff808081-22e8911b-0122-e8912643-048e

      如果有不足之处或者对该算法有更好的建议,请提出!

  • 相关阅读:
    从进入这里,没有写过什么文章,现在开始吧
    24)
    23)
    22)
    21)
    20)
    19)
    18)
    17)
    16)
  • 原文地址:https://www.cnblogs.com/Not-Famous/p/3653945.html
Copyright © 2020-2023  润新知