线性时间选择算法
线性时间选择算法
顾名思义,“线性时间选择”就是“选择问题”的“线性时间”算法。
1. 选择问题
元素选择问题:给定一个能够线性排序的集合(该集合中有 n 个元素)和 一个整数 k(\(1 \le k \le n\)) ,找出这 n 个元素中第 k 小的元素。
时间下界:
-
当 \(k = 1 或 k = n\)时,时间复杂度为 \(O(n)\)
-
当 \(k \le n/log(n) 或 k \ge n - n/log(n)时\),时间复杂度为\(O(n + klog(n)) = O(n)\)(堆排序)
2. 解决方法
方法一:先排序,再找第 k 小的数。至少:\(O(n)\) 平均:\(O(nlog(n))\)时间
方法二:随机选择算法:使用快速排序方法, 最多对一段继续分解 最坏时间\(O(n^2)\), 平均时间\(O(n)\)
-
RamdomizedPartition(a, p, r) 快排中的分解算法
-
i=Ramdom(p,r) //在p:r中随机选择一个数i
-
交换 a[i] 与 a[p] //将a[i]换到左端点
-
执行Partition(a,p,r)
RamdomizedPartition(a, p, r) //排序a[p:r] { i = Ramdom(p, r); swap(a[i], a[p]); return Partition(a, p, r); }
-
-
RandomSelect(a, p, r)
RSelect(a,p,r,k) //选择a[p:r]中第k小数 { if (p == r) return a[p]; mid=RamdomizePartition(a, p, r); if( mid >= k) return (RSelect(a, p, mid, k)); else return (RSelect(a, mid + 1, r, k - mid); } //初略时间分析: T(n) = T(9n/10) + O(n) = O(n)
方法三:线性时间选择算法 Select() :对快速排序的改进,最坏时间\(O(n)\)。
- 将 n 个元素,分成\(\lceil n/5 \rceil\)组,取出每组的中位数(第三小的元素)
- 取出\(\lceil n/5 \rceil\)个中位数的中位数(Select函数可以取中位数)
- 利用快排中的分解函数 Partition(),以所求中位数为基准,划分 a[p : r] 为两段。
- 取其中一段进行递归。
template <class Type>
Type Select(Type a[], int p, int r, int k)
{
if( r - p < 75 )
{
直接对数组a[p:r]排序;
return a[p+k-1];
}
for( int i = 0; i <= (r - p - 4) / 5 ; i++ ) //分 n/5 组, 取各组中位数
{
swap(a[p + 5*i]至a[p+5*i+4]的第3小元素, a[p+i]);
}
Type x = Select(a, p, p+(r-p-4)/5, (r-p-4)/10); //取中位数的中位数, T(n/5)
int i = Partition(a, p, r, x), j = i - p + 1;
if ( k == j )
return a[i];
else if ( k < j )
return Select(a,p,i-1,k); //选择左片递归, 最多T(3n/4)
else
return Select(a,i+1,r,k-j); //选择右片递归, 最多T(3n/4)
}
时间复杂度分析:
总结:算法优化的历程
算法 | 快排 | 随机选择 | 线性时间选择 |
---|---|---|---|
时间复杂度 | \(O(nlog(n))\) | \(O(n) -- O(n^2)\) | \(O(n)\) |
基准值 | a[p] | random | 中位数 |
附:line-time-select.c
#include <stdio.h>
#define MAX 2000010
int num[MAX];
// 选择排序
void slsort(int p, int q)
{
for (int i = p + 1; i <= q; i++)
{
int temp = num[i], j = i - 1;
while (j >= p)
{
if (num[j] > temp)
{
num[j + 1] = num[j];
j--;
}
else
break;
}
num[j + 1] = temp;
}
}
// 分解函数
int Partition(int p, int q, int mid)
{
int i = p, j = q;
while (i <= q && j >= p)
{
while (num[i] < mid){i++;}
while (num[j] > mid){j--;}
if (i >= j)
break;
else
{
int temp = num[i];
num[i] = num[j];
num[j] = temp;
i++, j--;
}
}
return j;
}
// 选择函数
int Select(int p, int q, int k)
{
if (q - p < 75)
{
slsort(p, q);
return num[p + k - 1];
}
// 选出 n/5 组中每个组的中位数
for (int i = 0; i <= (q - p - 4) / 5; i++)
{
slsort(p + 5 * i, p + 5 * i + 4);
int temp = num[p + 5 * i + 2];
num[p + 5 * i + 2] = num[p + i];
num[p + i] = temp;
}
// 选出各种中位数的中位数 mid
int mid = Select(p, p + (q - p - 4) / 5, ((q - p - 4) / 5 + 1) / 2);
// 以 mid 为基准进行分解
int mid_id = Partition(p, q, mid);
int mid_rank = mid_id - p + 1;
// 递归条件判断
if (k == mid_rank)
{
return num[mid_id];
}
else if (k < mid_rank)
{
return Select(p, mid_id, k);
}
else
{
return Select(mid_id + 1, q, k - mid_rank);
}
}
int main(int argc, char const *argv[])
{
int i = 0, k;
while (scanf("%d", &num[i]) != EOF) {i++;}
scanf("%d", &k); // 选择第几小的元素
if( k > i)
printf("error!\n");
else
print("%d\n", Select(0, i, k));
return 0;
}