算法导论-中位数和顺序统计量

在一个由n个元素组成的集合中,第i个顺序统计量是该集合中第i小的元素。一个中位数是它所属集合的“中点元素”。当n为奇数时,中位数是唯一的,位于i=(n+1)/2处;当n为偶数时,存在两个中位数,分别位于i=n/2和i=n/2+1处。如果不考虑n的奇偶性,中位数总是出现在i=⌊(n+1)/2⌋处(下中位数)和i=⌈(n+2)/2⌉(上中位数)。

1、最小值和最大值

在一个有n个元素的集合中,需要做n-1次(上界)比较才能找到最小值或最大值。

int MiniValue(const int *A, int len)
{
    int minValue = A[0];

    for (int i = 1; i < len; ++i)
    {
        if (A[i] < minValue)
        {
            minValue = A[i];
        }
    }

    return minValue;
}

那么如何同时找到最小值和最小值呢?

比较简单的思路是:只要分别独立找出最大值和最小值,各需要n-1次比较,共需2n-2次比较。下面给出一个算法,只需要最多3⌊n/2⌋次比较就可以同时找到最大值和最小值。

思路是:记录已知的最大值和最小值,对输出元素成对地进行处理。

(1)首先,将一对输入元素相互比较,然后将较小的与当前最小值比较,较大的与当前最大值比较,这样每对元素共需3次比较。

(2)如果n是奇数,将最小值和最大值的初值都设为第一个元素的值,然后成对地处理余下的元素;如果n是偶数,就对前两个元素做一次比较,决定最大值和最小值的初值,然后成对地处理余下的元素。

 1 void MinMaxValue(const int *A, int len, int &minValue, int &maxValue)
 2 {
 3     int i, tmpMin, tmpMax;
 4 
 5     if (len % 2 == 0 && len != 0)
 6     {
 7         minValue = A[0] < A[1] ? A[0] : A[1];
 8         maxValue = A[0] + A[1] - minValue;
 9         i = 2;
10     }
11     else
12     {
13         minValue = A[0];
14         maxValue = A[0];
15         i = 1;
16     }
17 
18     for (; i < len; i += 2)
19     {
20         if (A[i] < A[i + 1])
21         {
22             tmpMin = A[i];
23             tmpMax = A[i + 1];
24         }
25         else
26         {
27             tmpMin = A[i + 1];
28             tmpMax = A[i];
29         }
30 
31         if (tmpMin < minValue)
32         {
33             minValue = tmpMin;
34         }
35 
36         if (maxValue < tmpMax)
37         {
38             maxValue = tmpMax;
39         }
40     }
41 }

如果n是奇数,共进行3⌊n/2⌋次比较。如果n是偶数,共进行3(n-2)/2+1=3n/2-2次比较。

2、期望为线性时间的选择算法

下面介绍一种解决选择问题的分治算法。

#include <iostream>
using namespace std;

int RandomizedPartition(int *A, int low, int high)
{
    int key = A[high], tmp;
    int i = low - 1, j;

    for (j = low; j < high; ++j)
    {
        if (A[j] <= key)
        {
            i = i + 1;

            tmp = A[i];
            A[i] = A[j];
            A[j] = tmp;
        }
    }

    A[high] = A[i + 1];
    A[i + 1] = key;

    return i + 1;
}

int RandomizedSelect(int *A, int low, int high, int i)
{
    if (low == high)
    {
        return A[low];
    }

    int q = RandomizedPartition(A, low, high);
    int k = q - low + 1;

    if (i == k)
    {
        return A[q];
    }
    else if (i < k)
    {
        return RandomizedSelect(A, low, q - 1, i);
    }
    else
    {
        return RandomizedSelect(A, q + 1, high, i - k);
    }
}

//test
int main()
{
    int A[] = {10, 9, 5, 4, 3, 2, 1, 8, 7, 6};

    for (int i = 1; i <= 10; ++i)
    {
        cout << RandomizedSelect(A, 0, 9, i) << ' ';
    }
    cout << endl;

    return 0;
}

说明:

(1)RandomizedSelect的最坏运行时间为,因为每次划分时可能总是按余下的元素中最大的来进行划分,而划分操作需要时间。

(2)与快速排序不同的是,快速排序会递归处理划分的两边,而RandomizedSelect只处理划分的一边,这一差异体现在性能上就是快速排序的期望运行时间为,而RandomizedSelect的期望运行时间为

3、最坏情况为线性时间的选择算法

下面介绍一个最坏情况运行时间为O(n)的选择算法Select。

步骤1:将输入数组的n个元素划分为⌊n/5⌋组,每组5个元素,且至多只有一组由剩下的n mod 5个元素组成。

步骤2:寻找这⌈n/5⌉组中每一组的中位数:首先对每组元素进行插入排序,然后确定每组有序元素的中位数。

步骤3:对步骤2中找出的⌈n/5⌉个中位数,递归调用Select以找出其中位数x。

步骤4:利用修改过的Partition版本,按中位数的中位数x对输入数组进行划分。

步骤5:k为划分的低区元素个数+1。如果i=k,返回x;如果i<k,则在低区递归调用Select来找出第i小的元素;如果i>k,则在高区递归查找第i-k小的元素。

说明:Select算法通过对输入数组的递归划分来找出所需元素,在该算法中能够保证得到对数组的一个好的划分。Select使用的也是快速排序的确定性划分算法Partition,做的修改是将划分的主元也作为输入参数。

原文地址:https://www.cnblogs.com/mengwang024/p/4349432.html