iT邦幫忙

2021 iThome 鐵人賽

DAY 17
0

最大值與最小值

在一個有n個元素的,未經排序的陣列中,如果我們要找到最小值,我們可以將一個陣列進行排序,使用merge sort等等方式,接著回傳該陣列的第一個元素,那麼時間複雜度為https://chart.googleapis.com/chart?cht=tx&chl=O(nlgn)

或是我們可以遍歷整個陣列,進行n - 1次比較,並記錄下當前最小的元素,我們就可以找到該陣列中最小的元素。假設有一個陣列A, 長度為A.length = n

MINIMUM(A)
min = A[1]
for i = 2 to A.length
    if min > A[i]
        min = A[i]
return min

在這個情況下,我們只需要線性時間https://chart.googleapis.com/chart?cht=tx&chl=O(n)就可以找出最小值,最大值也是同理。

同時找到最大值和最小值

在某一些情況下,我們需要找出https://chart.googleapis.com/chart?cht=tx&chl=n個元素構成集合中的最大值和最小值,使用上面漸進式的方法,找出最大值和最小值皆需要https://chart.googleapis.com/chart?cht=tx&chl=n-1次比較,總共需要https://chart.googleapis.com/chart?cht=tx&chl=2n%20-%202次比較。

有一個方法可以只進行https://chart.googleapis.com/chart?cht=tx&chl=3%5Clceil%20n%2F2%20%5Crceil(https://chart.googleapis.com/chart?cht=tx&chl=%5Clceil%20%5Crceil表示向上取整數,如https://chart.googleapis.com/chart?cht=tx&chl=%5Clceil4.5%5Crceil%3D5)次比較就可以同時找出最大值和最小值。方法為將輸入的陣列元素成雙成對進行處理,先讓一對元素進行比較,把比較小的元素和目前記錄的最小值比較,最大的和目前記錄的最大值進行比較,如此,對每兩個元素只需要3次比較,以下範例

若n為偶數

A = [9,3,1,6,4,5], max=-1,min=999

1. 9和3進行比較,9比-1大,為目前max,3比999小,為目前min,總共進行3次比較
2. 1和6進行比較,6不比9大,max不動,1比3小,故1為目前的min,總共進行3次比較
3. 4和5進行比較,5不比9大,max不動,4不比1小,總共進行3次比較

總計進行9次比較,陣列長度n=6,比較次數3(6/2)=9

---------------------------------------------
若n為奇數

A = [9,3,1,6,4,5,7], max=-1,min=999

1. 先將max和min皆設為9,也就是陣列中第一個元素
2. 3和1進行比較,3比9小,max不動,3比9小,min設為3,總共進行3次比較
3. 6和4進行比較,6比9小,max不動,4比3大,min不動,總共進行3次比較
4. 5和7進行比較,5比9小,max不動,7比3大,min不動,總共進行3次比較

總計進行11次比較,陣列長度為n=7,比較次數3⌈7/3⌉=9

因此,無論陣列長度為何,比較次數皆為https://chart.googleapis.com/chart?cht=tx&chl=3%5Clceiln%2F2%5Crceil

執行時間的期望值為線性時間的選擇演算法

選擇演算法,能夠讓我們知道陣列中第https://chart.googleapis.com/chart?cht=tx&chl=i小的數字,時間複雜度為https://chart.googleapis.com/chart?cht=tx&chl=%5CTheta%20(n),這是一個使用分治法設計的演算法,概念上和quick sort很像,但不同之處在於quick sort畫將陣列劃分成兩個部分進行處理,而選擇演算法進行劃分,只會對其中一個部分進行處理。

選擇演算法為一種隨機演算法,因為他的部分行為是由隨機數產生器來決定的,以下為RANDOMIZED-SELECT的虛擬碼,他會回傳陣列https://chart.googleapis.com/chart?cht=tx&chl=A%5Bp...r%5D中第https://chart.googleapis.com/chart?cht=tx&chl=i小的元素。

RANDOMIZED-SELECT(A, p, r, i)
    if p == r
         return A[p]
    q = RANDOMIZED-PARTITION(A, p, r)
    k = q - p + 1
    if i == k
        return A[q]
    else if i < k
        return RANDOMIZED-SELECT(A, p, q - 1, i)
    else 
        return RANDOMIZED-SELECT(A, q + 1, r, i - k)
    
RANDOMIZED-PARTITION(A, low ,high)
    i = RANDOM(p, r)
    exchange A[high] with A[i]
    return PARTITION(A, low, high)

PARTITION(A, low, high)
    pivot = A[high]
    i = low - 1
    for j = low to high - 1
        if A[j] <= pivot
            i = i + 1
            exchange A[i] with A[j]
    exchangeA[i + 1] with A[high]
    return i + 1

RANDOMIZED-SELECT的情況如下:

  • 第1行先檢查陣列中只有一個元素的情況,在這個情況下直接回傳https://chart.googleapis.com/chart?cht=tx&amp;chl=A%5Bp%5D即可
  • 第3行呼叫RANDOMIZED-PARTITION把陣列https://chart.googleapis.com/chart?cht=tx&amp;chl=A%5Bp...r%5D劃分成https://chart.googleapis.com/chart?cht=tx&amp;chl=A%5Bp...q-1%5Dhttps://chart.googleapis.com/chart?cht=tx&amp;chl=A%5Bq%2B1...r%5D兩個區塊,將https://chart.googleapis.com/chart?cht=tx&amp;chl=A%5Bq%5D作為基準點,也就是主元(pivot)
  • 第4行計算子陣列,分割出的陣列https://chart.googleapis.com/chart?cht=tx&amp;chl=A%5Bp...q%5D中元素的個數https://chart.googleapis.com/chart?cht=tx&amp;chl=khttps://chart.googleapis.com/chart?cht=tx&amp;chl=q%20-%20p%20%2B%201https://chart.googleapis.com/chart?cht=tx&amp;chl=%2B1指的是加上主元
  • 從第5行開始,根據https://chart.googleapis.com/chart?cht=tx&amp;chl=ihttps://chart.googleapis.com/chart?cht=tx&amp;chl=k的關係,存在不同的遞迴情況
    • 如果https://chart.googleapis.com/chart?cht=tx&amp;chl=i%20%3D%3D%20k,表示主元(pivot)即為我們要尋找的元素
    • 如果https://chart.googleapis.com/chart?cht=tx&amp;chl=i%20%3C%20k,表示元素在分割的左手邊,因此改變分割的上界,也就是https://chart.googleapis.com/chart?cht=tx&amp;chl=q-1
    • 如果https://chart.googleapis.com/chart?cht=tx&amp;chl=i%20%3E%20k,也就是剩下的情況,表示元素在分割的右手邊,改變分割的下界,也就是https://chart.googleapis.com/chart?cht=tx&amp;chl=q%2B1,因為元素皆在右手邊,此時會產生出新的分割,因此我們尋找的目標https://chart.googleapis.com/chart?cht=tx&amp;chl=i也要相應跟著做調整,也就是https://chart.googleapis.com/chart?cht=tx&amp;chl=i-k

Example:

給定陣列A[8] = {6,10,13,5,8,3,2,11},目標為大小排名第7的數字,i = 7

1. 假設我們以A[0],也就是6作為主元(pivot),則會產生出以下分割
A'[8] = {2,5,3,6,8,13,10,11}

2.經過分割後,我們可以知道6是排名第4名的元素,=ˊ我們要尋找的元素為第7名的元素,
因此判定元素在右邊的分割,遞迴呼叫後,k = 4,此時我們看到的為右邊的分割
A'[4] = {8,13,10,11}
我們要尋找的數變成在這個子陣列中,排名第3(7 - 4 = 3)的元素,透過這種方式不斷遞迴呼叫,找到我們所求的數字。

實作(找出最大值和最小值)

#include <stdio.h>
#include <time.h>
#include <stdlib.h>

long long randomized_select(long long *,int, int, int);
int randomized_partition(long long*, int, int);
int partition(long long *, int, int);
void swap(long long *, long long *);
int main(void)
{
    long long number = 0;
    long long data[100] = {-1};
    int index = 0;
    while(scanf("%lld", &number) != EOF)
    {
        data[index] = number;
        index = index + 1;
    }
    printf("%lld %lld", randomized_select(data,0,index - 1,index), randomized_select(data,0,index - 1,1));
}

long long randomized_select(long long *A, int p, int r, int i)
{
    if(p == r)
    {
        return A[p];
    }
    int q = randomized_partition(A, p, r);
    int k = q - p + 1;
    if(i == k)
    {
        return A[q];
    }
    else if(i < k)
    {
        return randomized_select(A, p, q - 1, i);
    }
    else
    {
        return randomized_select(A, q + 1, r, i - k);
    }
    
}

int randomized_partition(long long* A, int low, int high)
{
    int i = low + rand() % (high - low + 1);
    swap(&A[i], &A[high]);
    return partition(A, low, high);
}
int partition(long long *a, int low, int high)
{
    int pivot = a[high];
    int i = low - 1;
    for(int j = low; j <= high - 1; j++)
    {
        if(a[j] <= pivot)
        {
            i = i + 1;
            swap(&a[i], &a[j]);
        }
    }
    swap(&a[i + 1], &a[high]);
    return i + 1;
}

void swap(long long *a, long long *b)
{
    long long temp = *a;
    *a = *b;
    *b = temp;
}

效率分析

我們假設陣列中的元素皆不相等,假設出現以下幾種情況

  • 第一種(好的情況): 產生出的分割為https://chart.googleapis.com/chart?cht=tx&amp;chl=1%20%3A%209,則遞迴關係式為
    https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%20T(n)%20%3C%3D%20T(%5Cfrac%209%20%7B10%7D%20n)%20%2B%20%5CTheta(n)
    使用主定理解遞迴式,https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%20n%5E%7Blog_ba%7D%3Dn%5E%7Blog_%7B9%2F%7B10%7D%7D1%7D%3D0%EF%BC%8C%5CTheta(n)%20%3E%20n%5E%7Blog_ba%7D,屬於Case 3.
    因此得出
    https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%20T(n)%20%3C%3D%20T(%5Cfrac%209%20%7B10%7D%20n)%20%2B%20%5CTheta(n)%20%3D%20%5CTheta(n)

也就是在產生出1 : 9分割時,我們可以在線性時間內找到第https://chart.googleapis.com/chart?cht=tx&amp;chl=i大的元素

  • 第二種(最差情況): 產生出的分割為https://chart.googleapis.com/chart?cht=tx&amp;chl=0%20%3A%20n%20-%201,則遞迴關係式為
    https://chart.googleapis.com/chart?cht=tx&amp;chl=T(n)%20%3D%20T(n-1)%20%2B%20%5CTheta(n),解出來結果為https://chart.googleapis.com/chart?cht=tx&amp;chl=%5CTheta%20(n%5E2)

當產生出最差的分割,也就是其中一個分割沒有任何元素,那時間複雜度為https://chart.googleapis.com/chart?cht=tx&amp;chl=%5CTheta%20(n%5E2),比先進行排序後再進行元素挑選的https://chart.googleapis.com/chart?cht=tx&amp;chl=%5CTheta%20(nlgn)還要差

但大多數的時候,我們都會得到幸運的情況,以下證明他的期望執行時間,也就是他的平均情況

我們可能得到https://chart.googleapis.com/chart?cht=tx&amp;chl=0%3An-1的分割,也有可能得到https://chart.googleapis.com/chart?cht=tx&amp;chl=5%3A5的分割,總共有https://chart.googleapis.com/chart?cht=tx&amp;chl=n種劃分的可能,要分析所有可能的劃分,我們可以使用指示器隨機變數(Indicator random variables)進行處理。

假設https://chart.googleapis.com/chart?cht=tx&amp;chl=T(n)為演算法在陣列長度為https://chart.googleapis.com/chart?cht=tx&amp;chl=n的執行時間,而整個執行時間取決於一個隨機變數,隨機變數決定了我們如何進行分割,隨機變數對於某一個分割的選擇需要是相互獨立的。

令指示器隨機變數https://chart.googleapis.com/chart?cht=tx&amp;chl=X_k,表示任意分割的發生,https://chart.googleapis.com/chart?cht=tx&amp;chl=k界於https://chart.googleapis.com/chart?cht=tx&amp;chl=0https://chart.googleapis.com/chart?cht=tx&amp;chl=n-1,則
https://chart.googleapis.com/chart?cht=tx&amp;chl=X_k%3D%5Cleft%5C%7B%201%20%20%5C%20if%5C%20partition%5C%20genterates%5C%20A%5Bk%5D%20and%20A%5Bn-k-1%5D%20split.%5C%5C%200%20%20%5C%20otherwise.%20%5Cright.

因此給定一個下標https://chart.googleapis.com/chart?cht=tx&amp;chl=0https://chart.googleapis.com/chart?cht=tx&amp;chl=n-1,不論如何隨機選擇,產生出怎樣的分割,每一種分割對應到的https://chart.googleapis.com/chart?cht=tx&amp;chl=X_k不是https://chart.googleapis.com/chart?cht=tx&amp;chl=1,就是https://chart.googleapis.com/chart?cht=tx&amp;chl=0

上面https://chart.googleapis.com/chart?cht=tx&amp;chl=T(n)就對應到了所有的分割情況,我們可以使用https://chart.googleapis.com/chart?cht=tx&amp;chl=X_k來標記每一個分割的情況的發生,如果發生就表示https://chart.googleapis.com/chart?cht=tx&amp;chl=1,反之為https://chart.googleapis.com/chart?cht=tx&amp;chl=0,因此要求出https://chart.googleapis.com/chart?cht=tx&amp;chl=T(n)的結果,可以用以下式子進行求解
https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%20T(n)%20%3C%3D%20%5Csum_%7Bk%3D0%7D%5E%7Bn-1%7D%20X_k(T(max%2C(k-1%2Cn-k))%20%2B%20%5CTheta(n))

而我們想知道平均情況,因此使用期望值進行計算

https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%20E%5BT(n)%5D%20%3D%20E%5B%5Csum_%7Bk%3D0%7D%5E%7Bn-1%7D%20X_k(T(max%2C(k-1%2Cn-k))%20%2B%20%5CTheta(n))%5D
https://chart.googleapis.com/chart?cht=tx&amp;chl=%5C%5C%5Cdisplaystyle%20%3D%5Csum_%7Bk%3D0%7D%5E%7Bn-1%7DE%5BX_k(T(max%2C(k-1%2Cn-k))%20%2B%20%5CTheta(n))%5D

這裡需要計算含有兩個隨機變數函數的期望值,https://chart.googleapis.com/chart?cht=tx&amp;chl=X_khttps://chart.googleapis.com/chart?cht=tx&amp;chl=T(max),兩個隨機變數是相互獨立的,也就是不受彼此影響(這是我們的前提),https://chart.googleapis.com/chart?cht=tx&amp;chl=X_k是進行分割時產生的隨機數,https://chart.googleapis.com/chart?cht=tx&amp;chl=T(max)是在遞迴中產生的隨機數,因此,期望值可以分開處理。

https://chart.googleapis.com/chart?cht=tx&amp;chl=%5C%5C%5Cdisplaystyle%20%3D%5Csum_%7Bk%3D0%7D%5E%7Bn-1%7D%5C%20E%5BX_k%5D%5C%20E%5B(T(max%2C(k-1%2Cn-k))%5D%20%2B%20%5CTheta(n))

https://chart.googleapis.com/chart?cht=tx&amp;chl=E%5BX_k%5Dhttps://chart.googleapis.com/chart?cht=tx&amp;chl=X_k表示所有分割情況是否發生,而分割的情況有https://chart.googleapis.com/chart?cht=tx&amp;chl=n種,每一種情況皆機率均等的出現,因此,https://chart.googleapis.com/chart?cht=tx&amp;chl=E%5BX_k%5D%3D1%2Fn

https://chart.googleapis.com/chart?cht=tx&amp;chl=%5C%5C%5Cdisplaystyle%20%3D%5Csum_%7Bk%3D0%7D%5E%7Bn-1%7D%5C%201%2Fn%5C%20E%5B(T(max%2C(k-1%2Cn-k))%5D%20%2B%20%5CTheta(n))

接著處理https://chart.googleapis.com/chart?cht=tx&amp;chl=max函數,可以發現到當https://chart.googleapis.com/chart?cht=tx&amp;chl=k%3D0https://chart.googleapis.com/chart?cht=tx&amp;chl=k%3Dn-1時,https://chart.googleapis.com/chart?cht=tx&amp;chl=max函數輸出的結果皆是相同的,因此,我們可以只計算整個式子一半的項數,接著乘上兩倍就可以了。

https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%20%3D%202*%201%2Fn%20%5Csum_%7Bk%3Dn%2F2%7D%5E%7Bn-1%7D%20E%5BT(k)%5D%20%2B%20%5CTheta(n)

我們的目標是要得出在平均情況下,這個演算法執行時間仍然為線性時間,觀察我們的式子,如果我們能夠證明https://chart.googleapis.com/chart?cht=tx&amp;chl=E%5BT(n)%5D的上限是線性時間,那麼證明就完成了。使用代換法讓https://chart.googleapis.com/chart?cht=tx&amp;chl=E%5BT(n)%5D%3DO(n)


一開始遞迴有兩個假設,我們將運用這三個假設來實現代換法:

  1. 假設有一個常數https://chart.googleapis.com/chart?cht=tx&amp;chl=c滿足遞迴的初始條件,使得https://chart.googleapis.com/chart?cht=tx&amp;chl=E%5BT(n)%5D%3C%3Dcn
  2. 假設當https://chart.googleapis.com/chart?cht=tx&amp;chl=n小於某一個常數時,存在https://chart.googleapis.com/chart?cht=tx&amp;chl=T(n)%3DO(1)
  3. 假設存在一個常數https://chart.googleapis.com/chart?cht=tx&amp;chl=a,使得對所有https://chart.googleapis.com/chart?cht=tx&amp;chl=n%3E0,滿足https://chart.googleapis.com/chart?cht=tx&amp;chl=%5CTheta(n)%3C%3Da*n

在上面我們得到這樣得結果
https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%20E%5BT(n)%5D%3C%3D2*%201%2Fn%5Csum_%7Bk%3D%5Clfloor%20n%2F2%20%5Crfloor%7D%5E%7Bn-1%7DE%5BT(k)%5D%2B%5CTheta(n),使用代換法進行代換
https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%20E%5BT(n)%5D%3C%3D2*%201%2Fn%5Csum_%7Bk%3D%5Clfloor%20n%2F2%20%5Crfloor%7D%5E%7Bn-1%7Dck%2Ban
https://chart.googleapis.com/chart?cht=tx&amp;chl=%5C%5C%5Cdisplaystyle%3D2c%2Fn(%5Csum_%7Bk%3D%5Clfloor%20n%2F2%20%5Crfloor%7D%5E%7Bn-1%7Dk)%2Ban
https://chart.googleapis.com/chart?cht=tx&amp;chl=%5C%5C%5Cdisplaystyle%20%3D%202c%2Fn(%5Csum_%7Bk%3D1%7D%5E%7Bn-1%7Dk-%5Csum_%7Bk%3D1%7D%5E%7B%5Clfloor%20n%2F2%20%5Crfloor-1%7Dk)%2Ban
https://chart.googleapis.com/chart?cht=tx&amp;chl=%5C%5C%5Cdisplaystyle%3D%5Cfrac%20%7B2c%7D%20n(%5Cfrac%20%7B(n-1)n%7D%202%20-%20%5Cfrac%20%7B(%5Clfloor%20n%2F2%20%5Crfloor-1)%5Clfloor%20n%2F2%20%5Crfloor%7D%202)%2Ban%3C%3D%5Cfrac%20%7B2c%7D%20n(%5Cfrac%20%7B(n-1)n%7D%202%20-%20%5Cfrac%20%7B((n%2F2)-2)(n%2F2)-1%7D%202)%2Ban
https://chart.googleapis.com/chart?cht=tx&amp;chl=%5C%5C%5Cdisplaystyle%3Dc(%5Cfrac%20%7B3n%7D%204%20%2B%20%5Cfrac%201%202%20-%202)%2Ban%3C%3D%5Cfrac%20%7B3cn%7D%204%20%2B%5Cfrac%20c%202%20%2Ban

https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%5Cfrac%20%7B3cn%7D%204%20%2B%5Cfrac%20c%202%20%2Ban%20%3C%3D%20cn%20-(%5Cfrac%20%7Bcn%7D%204%20-%20%5Cfrac%20c%202%20-an)

https://chart.googleapis.com/chart?cht=tx&amp;chl=n足夠大時,https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%20cn%20-%20(%5Cfrac%20%7Bcn%7D%204%20-%20%5Cfrac%20c%202%20-an)的值最多為https://chart.googleapis.com/chart?cht=tx&amp;chl=cn,也就是
https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%20(%5Cfrac%20%7Bcn%7D%204%20-%20%5Cfrac%20c%202%20-an)%20%3E%3D0,將https://chart.googleapis.com/chart?cht=tx&amp;chl=n提出來,並兩邊乘上https://chart.googleapis.com/chart?cht=tx&amp;chl=c%2F2
https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%20n(%5Cfrac%20c%204-a)%20%3E%3D%20%5Cfrac%20c%202,將兩式同除https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%20(%5Cfrac%20c%204-a),得到
https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%20n%20%3E%3D%20%5Cfrac%20%7Bc%2F2%7D%20%7Bc%2F4-a%7D%20%3D%20%5Cfrac%20%7B2c%7D%20%7Bc-4a%7D

因此,https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Cdisplaystyle%20E%5BT(n)%5D%3D%5Cfrac%20%7B3cn%7D%204%20%2B%5Cfrac%20c%202%20%2Ban%20%3C%3D%20n,而https://chart.googleapis.com/chart?cht=tx&amp;chl=n%20%3C%20%5Cfrac%20%7B2c%7D%20%7Bc-4a%7D時,也就是我們歸納假設的第二點,存在https://chart.googleapis.com/chart?cht=tx&amp;chl=T(n)%20%3D%20O(1)


結論: 當陣列中所有元素皆不相同,平均情況下,我們可以在https://chart.googleapis.com/chart?cht=tx&amp;chl=%5CTheta%20(n)的情況下找出第https://chart.googleapis.com/chart?cht=tx&amp;chl=i大的元素,而最差情況為https://chart.googleapis.com/chart?cht=tx&amp;chl=%5CTheta%20(n%5E2)

參考資料:Introduction to algorithms 3rd


上一篇
Day-16 雇用問題, 指示器隨機變數(indicator random variable), 隨機化演算法
下一篇
Day-18 BFPRT演算法
系列文
從0開始啃Introduction of algorithms心得與記錄30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言