演算法導論-中位數和順序統計量

QingLiXueShi發表於2015-03-19

在一個由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,做的修改是將劃分的主元也作為輸入引數。

相關文章