Day-17 中位数与顺序统计

最大值与最小值

在一个有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&chl=A%5Bp%5D即可
  • 第3行呼叫RANDOMIZED-PARTITION把阵列https://chart.googleapis.com/chart?cht=tx&chl=A%5Bp...r%5D划分成https://chart.googleapis.com/chart?cht=tx&chl=A%5Bp...q-1%5Dhttps://chart.googleapis.com/chart?cht=tx&chl=A%5Bq%2B1...r%5D两个区块,将https://chart.googleapis.com/chart?cht=tx&chl=A%5Bq%5D作为基准点,也就是主元(pivot)
  • 第4行计算子阵列,分割出的阵列https://chart.googleapis.com/chart?cht=tx&chl=A%5Bp...q%5D中元素的个数https://chart.googleapis.com/chart?cht=tx&chl=khttps://chart.googleapis.com/chart?cht=tx&chl=q%20-%20p%20%2B%201https://chart.googleapis.com/chart?cht=tx&chl=%2B1指的是加上主元
  • 从第5行开始,根据https://chart.googleapis.com/chart?cht=tx&chl=ihttps://chart.googleapis.com/chart?cht=tx&chl=k的关系,存在不同的递回情况
    • 如果https://chart.googleapis.com/chart?cht=tx&chl=i%20%3D%3D%20k,表示主元(pivot)即为我们要寻找的元素
    • 如果https://chart.googleapis.com/chart?cht=tx&chl=i%20%3C%20k,表示元素在分割的左手边,因此改变分割的上界,也就是https://chart.googleapis.com/chart?cht=tx&chl=q-1
    • 如果https://chart.googleapis.com/chart?cht=tx&chl=i%20%3E%20k,也就是剩下的情况,表示元素在分割的右手边,改变分割的下界,也就是https://chart.googleapis.com/chart?cht=tx&chl=q%2B1,因为元素皆在右手边,此时会产生出新的分割,因此我们寻找的目标https://chart.googleapis.com/chart?cht=tx&chl=i也要相应跟着做调整,也就是https://chart.googleapis.com/chart?cht=tx&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&chl=1%20%3A%209,则递回关系式为
    https://chart.googleapis.com/chart?cht=tx&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&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&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&chl=i大的元素

  • 第二种(最差情况): 产生出的分割为https://chart.googleapis.com/chart?cht=tx&chl=0%20%3A%20n%20-%201,则递回关系式为
    https://chart.googleapis.com/chart?cht=tx&chl=T(n)%20%3D%20T(n-1)%20%2B%20%5CTheta(n),解出来结果为https://chart.googleapis.com/chart?cht=tx&chl=%5CTheta%20(n%5E2)

当产生出最差的分割,也就是其中一个分割没有任何元素,那时间复杂度为https://chart.googleapis.com/chart?cht=tx&chl=%5CTheta%20(n%5E2),比先进行排序後再进行元素挑选的https://chart.googleapis.com/chart?cht=tx&chl=%5CTheta%20(nlgn)还要差

但大多数的时候,我们都会得到幸运的情况,以下证明他的期望执行时间,也就是他的平均情况

我们可能得到https://chart.googleapis.com/chart?cht=tx&chl=0%3An-1的分割,也有可能得到https://chart.googleapis.com/chart?cht=tx&chl=5%3A5的分割,总共有https://chart.googleapis.com/chart?cht=tx&chl=n种划分的可能,要分析所有可能的划分,我们可以使用指示器随机变数(Indicator random variables)进行处理。

假设https://chart.googleapis.com/chart?cht=tx&chl=T(n)为演算法在阵列长度为https://chart.googleapis.com/chart?cht=tx&chl=n的执行时间,而整个执行时间取决於一个随机变数,随机变数决定了我们如何进行分割,随机变数对於某一个分割的选择需要是相互独立的。

令指示器随机变数https://chart.googleapis.com/chart?cht=tx&chl=X_k,表示任意分割的发生,https://chart.googleapis.com/chart?cht=tx&chl=k界於https://chart.googleapis.com/chart?cht=tx&chl=0https://chart.googleapis.com/chart?cht=tx&chl=n-1,则
https://chart.googleapis.com/chart?cht=tx&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&chl=0https://chart.googleapis.com/chart?cht=tx&chl=n-1,不论如何随机选择,产生出怎样的分割,每一种分割对应到的https://chart.googleapis.com/chart?cht=tx&chl=X_k不是https://chart.googleapis.com/chart?cht=tx&chl=1,就是https://chart.googleapis.com/chart?cht=tx&chl=0

上面https://chart.googleapis.com/chart?cht=tx&chl=T(n)就对应到了所有的分割情况,我们可以使用https://chart.googleapis.com/chart?cht=tx&chl=X_k来标记每一个分割的情况的发生,如果发生就表示https://chart.googleapis.com/chart?cht=tx&chl=1,反之为https://chart.googleapis.com/chart?cht=tx&chl=0,因此要求出https://chart.googleapis.com/chart?cht=tx&chl=T(n)的结果,可以用以下式子进行求解
https://chart.googleapis.com/chart?cht=tx&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&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&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&chl=X_khttps://chart.googleapis.com/chart?cht=tx&chl=T(max),两个随机变数是相互独立的,也就是不受彼此影响(这是我们的前提),https://chart.googleapis.com/chart?cht=tx&chl=X_k是进行分割时产生的随机数,https://chart.googleapis.com/chart?cht=tx&chl=T(max)是在递回中产生的随机数,因此,期望值可以分开处理。

https://chart.googleapis.com/chart?cht=tx&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&chl=E%5BX_k%5Dhttps://chart.googleapis.com/chart?cht=tx&chl=X_k表示所有分割情况是否发生,而分割的情况有https://chart.googleapis.com/chart?cht=tx&chl=n种,每一种情况皆机率均等的出现,因此,https://chart.googleapis.com/chart?cht=tx&chl=E%5BX_k%5D%3D1%2Fn

https://chart.googleapis.com/chart?cht=tx&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&chl=max函数,可以发现到当https://chart.googleapis.com/chart?cht=tx&chl=k%3D0https://chart.googleapis.com/chart?cht=tx&chl=k%3Dn-1时,https://chart.googleapis.com/chart?cht=tx&chl=max函数输出的结果皆是相同的,因此,我们可以只计算整个式子一半的项数,接着乘上两倍就可以了。

https://chart.googleapis.com/chart?cht=tx&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&chl=E%5BT(n)%5D的上限是线性时间,那麽证明就完成了。使用代换法让https://chart.googleapis.com/chart?cht=tx&chl=E%5BT(n)%5D%3DO(n)


一开始递回有两个假设,我们将运用这三个假设来实现代换法:

  1. 假设有一个常数https://chart.googleapis.com/chart?cht=tx&chl=c满足递回的初始条件,使得https://chart.googleapis.com/chart?cht=tx&chl=E%5BT(n)%5D%3C%3Dcn
  2. 假设当https://chart.googleapis.com/chart?cht=tx&chl=n小於某一个常数时,存在https://chart.googleapis.com/chart?cht=tx&chl=T(n)%3DO(1)
  3. 假设存在一个常数https://chart.googleapis.com/chart?cht=tx&chl=a,使得对所有https://chart.googleapis.com/chart?cht=tx&chl=n%3E0,满足https://chart.googleapis.com/chart?cht=tx&chl=%5CTheta(n)%3C%3Da*n

在上面我们得到这样得结果
https://chart.googleapis.com/chart?cht=tx&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&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&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&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&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&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&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&chl=n足够大时,https://chart.googleapis.com/chart?cht=tx&chl=%5Cdisplaystyle%20cn%20-%20(%5Cfrac%20%7Bcn%7D%204%20-%20%5Cfrac%20c%202%20-an)的值最多为https://chart.googleapis.com/chart?cht=tx&chl=cn,也就是
https://chart.googleapis.com/chart?cht=tx&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&chl=n提出来,并两边乘上https://chart.googleapis.com/chart?cht=tx&chl=c%2F2
https://chart.googleapis.com/chart?cht=tx&chl=%5Cdisplaystyle%20n(%5Cfrac%20c%204-a)%20%3E%3D%20%5Cfrac%20c%202,将两式同除https://chart.googleapis.com/chart?cht=tx&chl=%5Cdisplaystyle%20(%5Cfrac%20c%204-a),得到
https://chart.googleapis.com/chart?cht=tx&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&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&chl=n%20%3C%20%5Cfrac%20%7B2c%7D%20%7Bc-4a%7D时,也就是我们归纳假设的第二点,存在https://chart.googleapis.com/chart?cht=tx&chl=T(n)%20%3D%20O(1)


结论: 当阵列中所有元素皆不相同,平均情况下,我们可以在https://chart.googleapis.com/chart?cht=tx&chl=%5CTheta%20(n)的情况下找出第https://chart.googleapis.com/chart?cht=tx&chl=i大的元素,而最差情况为https://chart.googleapis.com/chart?cht=tx&chl=%5CTheta%20(n%5E2)

参考资料:Introduction to algorithms 3rd


<<:  Parser 的单元测试

>>:  DVWA练习-Weak Session IDs

前言与自我回顾

欢迎大家来看我的文章,这次我挑战的主题是 Android 架构,就如同我简介中说的,关於架构方面的文...

Day 24 用户帐号及资料删除定义规划实作

昨天分享了规划用户资料数据下载的规划,今天就根据GDPR第17条和 CCPA 法第1798.105条...

【Day 30】实作 - 如何在 AWS Quicksight 设定告警以及结语

昨天我们提到 AWS CloudWatch Alarms,今天我们就来介绍 AWS QuickSig...

[JAVA 环境]JDK与环境变数安装

JAVA 优点: 跨平台 物件导向特性 广泛应用於企业及 Web 应用开发和行动应用开发。 编译语言...

工作排程器--Windows的忠实程序秘书

今天要来介绍一个Windwos内建工具叫做工作排程器(Task Scheduler),他可以预先计划...