耐心整理的精品笔记,花点时间耐心看完,相信会有收获 ^_^ )
本文目录
选择问题:找数组a的 [s, e] 范围内的第k小元素。同时,也就能求出中位数。
- 解法一:RandomizedSelect算法,期望为 时间的选择算法。不稳定,会有比较差的时间出现。
- 解法二:BFPTR算法,最坏情况为 时间的选择算法。理论上是最快的方法,最坏都是线性时间。
两种算法都是通过分治的思想实现的, BFPTR算法可以说是RandomizedSelect算法的一种改进,写起来稍微麻烦一点。
解法一:RandomizedSelect算法
一、算法描述
对数组a的 [s, e] 范围处理:
1、分(divide)
随机找到一个元素,记为pivot。然后将数组按照与pivot的大小关系分为两部分,分别分到pivot的两边。
其实这个分边的操作有点像快排里面的partiton操作,只是我们的pivot是随机选取的。为了方便,我们选取最后一个元素a[e]为pivot,具体操作如下图所示:
具体如何移动和实现的,之后会具体说。
2、治(conquer)
现在数组已经分好了,pivot所在位置的下标记为pivot_index,那么在pivot左侧(包括pivot)一共有 pivot_index - s + 1 个元素,记为num。然后判断,第k小元素在哪个部分:
- 刚好是pivot:直接返回pivot就行
- 在pivot左侧:对pivot左侧的元素 —— 数组a的 [s, pivot_index - 1] 递归地找第k小元素
- 在pivot右侧:对pivot右侧的元素 —— 数组a的 [pivot_index + 1, e] 递归地找第k - num 小元素
二、算法实现
1、partition的实现
pivot选取为a[e],取两个游标 i、j 分别起始指向下标 s 和 e - 1。分别向中间推进,当 i 指向一个大于 pivot 值时停下,当 j 指向一个小于 pivot 值时停下,然后将两者互换。直到 i > j 结束,然后将 pivot 换到合适位置。
下面是具体操作的图解:
主要注意:结束时的条件、pivot 最终移动到合适的位置。
/* 将数组a的[s, e]范围,按照与pivot的大小关系,划分到pivot两侧 *
* 返回pivot最终的下标
* 注:pivot是随机选取的 */
int RandomizedPartition(int a[], int s, int e) {
int pivot = a[e]; //取最后一个元素为pivot来划分
int i = s, j = e - 1;
while (i < j) {
while (a[i] <= pivot && i < e - 1)
i++;
while (a[j] >= pivot && j > s)
j--;
if (i < j)
swap(a[i], a[j]);
}
swap(a[i], a[e]); //将pivot移动到合适位置
return i;
}
2、RandomizedSelect的实现
/* 找数组a[s, e]范围内的第k小元素 */
int RandomizedSelect(int a[], int s, int e, int k) {
int pivot_index = RandomizedPartition(a, s, e); //按照与pivot的大小关系,划分到pivot两侧。返回pivot最终的下标
int num = pivot_index - s + 1; //pivot(包括在内)左侧的元素个数
if (num == k)//第k小元素恰好是pivot
return a[pivot_index];
else if (k < num) //在pivot左边找
return RandomizedSelect(a, s, pivot_index - 1, k);
else //在pivot右边找
return RandomizedSelect(a, pivot_index + 1, e, k - num);
}
完整代码:
/* 找数组a[s, e]范围内的第k小元素
* 特殊情况:中位数... */
#include <algorithm>
using namespace std;
/* 将数组a的[s, e]范围,按照与pivot的大小关系,划分到pivot两侧 *
* 返回pivot最终的下标
* 注:pivot是随机选取的 */
int RandomizedPartition(int a[], int s, int e) {
int pivot = a[e]; //取最后一个元素为pivot来划分
int i = s, j = e - 1;
while (i < j) {
while (a[i] < pivot)
i++;
while (a[j] > pivot)
j--;
if (i < j)
swap(a[i], a[j]);
}
swap(a[i], a[e]); //将pivot移动到合适位置
return i;
}
/* 找数组a[s, e]范围内的第k小元素 */
int RandomizedSelect(int a[], int s, int e, int k) {
int pivot_index = RandomizedPartition(a, s, e); //按照与pivot的大小关系,划分到pivot两侧。返回pivot最终的下标
int num = pivot_index - s + 1; //pivot(包括在内)左侧的元素个数
if (num == k)//第k小元素恰好是pivot
return a[pivot_index];
else if (k < num) //在pivot左边找
return RandomizedSelect(a, s, pivot_index - 1, k);
else //在pivot右边找
return RandomizedSelect(a, pivot_index + 1, e, k - num);
}
int main() {
int a[10] = {0, 9, 32, -2, 23, 33, 5, 1, 12, 76};
printf("中位数:%d\n", RandomizedSelect(a, 0, 9, 10 / 2)); //测试输出中位数(小)
printf("最大数:%d\n", RandomizedSelect(a, 0, 9, 1)); //测试输出最大数
printf("最小数:%d\n", RandomizedSelect(a, 0, 9, 10)); //测试输出最小数
printf("%d\n", RandomizedSelect(a, 0, 9, 7)); //其他测试
}
解法二:BFPTR算法
其实RandomizedSelect算法会出现比较差的时间的原因就是:由于pivot是随机选取的,paritition分的不够均匀,导致算法会低效。那么BFPTR算法其实就是在这个问题上改进而来的:我们将pivot选取为一个相对来说靠中间的量,使得partition分配后pivot两侧元素个数差别不多。
1、选取pivot
既然两个算法的差别就是pivot的选取,那么pivot怎么找呢?通过验证(略,具体看算法书),这样子是最合适的:将数组a的 [s, e] 分组,5个一组,最终不足5个的也分到一组。分别每组进行选择排序,同时能找出每组的中位数,将这些中位数抽出,继续在这些中位数中找"中位数"。
🔺 注意!
很多算法书上面描述的是找中位数的中位数,其实有些费解,我看了很多资料才明白,这应该是中文书翻译的锅(吐了)。其实应该这样子表述:找中位数的"中位数",第一个中位数不打引号,是因为确实是每组的中位数的集合;第二个中位数打上了引号,是因为它最后找出的不是真正的中位数,只是一个相对靠中间的数。
🔺 如何理解呢?
因为每次将每组的中位数抽出来继续寻找"中位数",这是一个递归的调用。递归的终点是什么?是最终传入的数组只能分为一组(也就是不超过五个元素),那么这时直接返回第一个元素(不要问为啥,就是近似嘛,可以验证这是合理的)。
所以说,pivot是一个近似的中位数,而非真正的中位数有这两个原因:
- 每组中位数的中位数不一定就是整体的中位数
- 况且递归的终点也是近似的,随机找的第一个元素嘛
理解了pivot如何选取,就可以直接上代码了:
/* 在数组a的[s, e]范围内找到一个大致的"中位数" */
int FindMid(int a[], int s, int e) {
int cnt = 0; //实时记录分组的编号(从零依次)
int i;
/* 五数一组,每组排序后找到中位数,调换到a数组前列 */
for (i = s; i + 4 <= s; i += 5) {
InsertSort(a, i, i + 4); //排序
swap(a[s + cnt], a[i + 2]); //将中位数调换到a数组前第cnt个
cnt++; //小组增加一个
}
/* 处理剩余元素 */
if (i < e) {
InsertSort(a, i, e); //排序
swap(a[s + cnt], a[(i + e) / 2]); //将中位数调换到a数组前第cnt个
cnt++;
}
if (cnt <= 1) //递归的终点:只有一个/没有分组的时候
return a[s];
return FindMid(a, s, s + cnt - 1); //继续递归求解 调到前面的数(每组的中位数)的 "中位数"
}
2、partition的实现
具体操作和上面描述的RandomizedSelect算法的partition的实现一样,就不重复赘述了,有需要看看上面具体说的。只是pivot是我们的FindMid函数计算出来的。有一点小区别要注意:为了方便移动,pivot需要我们自己最先移动到a[e]位置,最后再移动回合适位置。
另外有一种特殊情况要注意,就是当序列不需要调整时(pivot为最大元素),应单独考虑。
具体代码如下:
/* 将数组a的[s, e]范围,按照与pivot的大小关系,划分到pivot两侧 *
* 返回pivot最终的下标 */
int partition(int a[], int s, int e, int pivot) {
int p = FindIndex(a, s, e, pivot); //找到pivot的下标
swap(a[p], a[e]); //将pivot换到最末端
int i = s, j = e - 1;
while (i < j) {
while (a[i] <= pivot)
i++;
while (a[j] >= pivot)
j--;
if (i < j)
swap(a[i], a[j]);
}
if(a[i] < pivot) //所有元素都比pivot小,原序列不需要调整
return e;
else {
swap(a, i, e); //将pivot转到合适位置
return i;
}
}
3、BFPTR核心算法
具体操作和上面描述的RandomizedSelect算法的实现一样是一样的,就不重复赘述了,有需要看看上面具体说的。
/* 找数组的[s, e]范围内第k小元素 */
int BFPTR(int a[], int s, int e, int k) {
int pivot = FindMid(a, s, e); //找到一个大致的"中位数"
int pivot_index = partition(a, s, e, pivot); //按照与pivot的大小关系,划分到pivot两侧。返回pivot最终的下标
int num = pivot_index - s + 1; //pivot(包括在内)左侧的元素个数
if (num == k)//第k小元素恰好是pivot
return pivot;
else if (k < num) //在pivot左边找
return BFPTR(a, s, pivot_index - 1, k);
else //在pivot右边找
return BFPTR(a, pivot_index + 1, e, k - num);
}
完整代码
// Created by A on 2020/2/26.
/* 找数组a[s, e]范围内的第k小元素
* 特殊情况:中位数...
* 最差时间复杂度为线性 O(N)*/
#include <cstdio>
#include <algorithm>
using namespace std;
/* 增序的插入排序
* 对数组a的[s, e]范围排序 */
void InsertSort(int a[], int s, int e) {
for (int i = s + 1; i <= e; i++) {
int temp = a[i]; //取出待插入元素
int t = i; //待插入的位置
/* 当待插入元素 小于 待插入位置的前一个元素 */
while (t > 0 && temp < a[t - 1]) {
a[t] = a[t - 1]; //将前一个元素后移
t--; //待插入位置前移
}
a[t] = temp; //插入合适位置
}
}
/* 在数组a的[s, e]范围内找到一个大致的"中位数" */
int FindMid(int a[], int s, int e) {
int cnt = 0; //实时记录分组的编号(从零依次)
int i;
/* 五数一组,每组排序后找到中位数,调换到a数组前列 */
for (i = s; i + 4 <= s; i += 5) {
InsertSort(a, i, i + 4); //排序
swap(a[s + cnt], a[i + 2]); //将中位数调换到a数组前第cnt个
cnt++; //小组增加一个
}
/* 处理剩余元素 */
if (i < e) {
InsertSort(a, i, e); //排序
swap(a[s + cnt], a[(i + e) / 2]); //将中位数调换到a数组前第cnt个
cnt++;
}
if (cnt <= 1) //递归的终点:只有一个/没有分组的时候
return a[s];
return FindMid(a, s, s + cnt - 1); //继续递归求解 调到前面的数(每组的中位数)的 "中位数"
}
/* 找到数组a的[s, e]范围内 key对应的下标 */
int FindIndex(int a[], int s, int e, int key) {
for (int i = s; i <= e; i++) {
if (a[i] == key)
return i;
}
}
/* 将数组a的[s, e]范围,按照与pivot的大小关系,划分到pivot两侧 *
* 返回pivot最终的下标 */
int partition(int a[], int s, int e, int pivot) {
int p = FindIndex(a, s, e, pivot); //找到pivot的下标
swap(a[p], a[e]); //将pivot换到最末端
int i = s, j = e - 1;
while (i < j) {
while (a[i] <= pivot)
i++;
while (a[j] >= pivot)
j--;
if (i < j)
swap(a[i], a[j]);
}
swap(a[e], a[i]); //将pivot移动到合适位置
return i;
}
/* 找数组的[s, e]范围内第k小元素 */
int BFPTR(int a[], int s, int e, int k) {
int pivot = FindMid(a, s, e); //找到一个大致的"中位数"
int pivot_index = partition(a, s, e, pivot); //按照与pivot的大小关系,划分到pivot两侧。返回pivot最终的下标
int num = pivot_index - s + 1; //pivot(包括在内)左侧的元素个数
if (num == k)//第k小元素恰好是pivot
return pivot;
else if (k < num) //在pivot左边找
return BFPTR(a, s, pivot_index - 1, k);
else //在pivot右边找
return BFPTR(a, pivot_index + 1, e, k - num);
}
int main() {
int a[10] = {0, 9, 32, -2, 23, 33, 5, 1, 12, 76};
printf("中位数:%d\n", BFPTR(a, 0 , 9, 10 / 2)); //测试输出中位数(小)
printf("最大数:%d\n", BFPTR(a, 0 , 9, 1)); //测试输出最大数
printf("最小数:%d\n", BFPTR(a, 0 , 9, 10)); //测试输出最小数
printf("%d\n", BFPTR(a, 0 , 9, 7)); //其他测试
}
具体题目
1、油井问题
成绩 10 开启时间 2020年02月25日 星期二 08:55 折扣 0.8 折扣时间 2020年04月30日 星期四 23:55 允许迟交 否 关闭时间 2020年04月30日 星期四 23:55
主油管道为东西向,确定主油管道的南北位置,使南北向油井喷油管道和最小。要求线性时间完成。
1<= 油井数量 <=2 000 000
输入要求:
输入有油井数量行,第 K 行为第 K 油井的坐标 X ,Y 。其中, 0<=X<2^31,0<=Y<2^31 。
输出要求:
输出有一行, N 为主管道最优位置的最小值
注意:用快排做的不给分!!
友情提示:可以采用while(scanf("%d,%d",&x,&y) != EOF)的数据读入方式。
测试输入 期待的输出 时间限制 内存限制 额外进程 测试用例 1 以文本方式显示
- 41,969978↵
- 26500,413356↵
- 11478,550396↵
- 24464,567225↵
- 23281,613747↵
- 491,766290↵
- 4827,77476↵
- 14604,597006↵
- 292,706822↵
- 18716,289610↵
- 5447,914746↵
以文本方式显示
- 597006↵
1秒 64M 0
本题的x坐标是没有意义的,仔细想想应该知道,最终是需要找出y坐标的中位数才是油管的最佳位置。既然说不能用快排直接找出中位数,那么就可以通过上面的算法来寻找中位数。
我是用BFPTR来实现的,会比较快,完整AC代码如下:
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 2000005;
int a[N];
/* 增序的插入排序
* 对数组a的[s, e]范围排序 */
void InsertSort(int a[], int s, int e) {
for (int i = s + 1; i <= e; i++) {
int temp = a[i]; //取出待插入元素
int t = i; //待插入的位置
/* 当待插入元素 小于 待插入位置的前一个元素 */
while (t > 0 && temp < a[t - 1]) {
a[t] = a[t - 1]; //将前一个元素后移
t--; //待插入位置前移
}
a[t] = temp; //插入合适位置
}
}
/* 在数组a的[s, e]范围内找到一个大致的"中位数" */
int FindMid(int a[], int s, int e) {
int cnt = 0; //实时记录分组的编号(从零依次)
int i;
/* 五数一组,每组排序后找到中位数,调换到a数组前列 */
for (i = s; i + 4 <= s; i += 5) {
InsertSort(a, i, i + 4); //排序
swap(a[s + cnt], a[i + 2]); //将中位数调换到a数组前第cnt个
cnt++; //小组增加一个
}
/* 处理剩余元素 */
if (i < e) {
InsertSort(a, i, e); //排序
swap(a[s + cnt], a[(i + e) / 2]); //将中位数调换到a数组前第cnt个
cnt++;
}
if (cnt <= 1) //递归的终点:只有一个/没有分组的时候
return a[s];
return FindMid(a, s, s + cnt - 1); //继续递归求解 调到前面的数(每组的中位数)的 "中位数"
}
/* 找到数组a的[s, e]范围内 key对应的下标 */
int FindIndex(int a[], int s, int e, int key) {
for (int i = s; i <= e; i++) {
if (a[i] == key)
return i;
}
}
/* 将数组a的[s, e]范围,按照与pivot的大小关系,划分到pivot两侧 *
* 返回pivot最终的下标 */
int partition(int a[], int s, int e, int pivot) {
int p = FindIndex(a, s, e, pivot); //找到pivot的下标
swap(a[p], a[e]); //将pivot换到最末端
int i = s, j = e - 1;
while (i < j) {
while (a[i] <= pivot)
i++;
while (a[j] >= pivot)
j--;
if (i < j)
swap(a[i], a[j]);
}
swap(a[e], a[i]); //将pivot移动到合适位置
return i;
}
/* 找数组的[s, e]范围内第k小元素 */
int BFPTR(int a[], int s, int e, int k) {
int pivot = FindMid(a, s, e); //找到一个大致的"中位数"
int pivot_index = partition(a, s, e, pivot); //按照与pivot的大小关系,划分到pivot两侧。返回pivot最终的下标
int num = pivot_index - s + 1; //pivot(包括在内)左侧的元素个数
if (num == k)//第k小元素恰好是pivot
return pivot;
else if (k < num) //在pivot左边找
return BFPTR(a, s, pivot_index - 1, k);
else //在pivot右边找
return BFPTR(a, pivot_index + 1, e, k - num);
}
int main() {
int x, y[N], n = 0;
while (EOF != scanf("%d,%d", &x, &y[n]))
n++;
printf("%d\n", BFPTR(y, 0, n - 1, (n + 1) / 2));
}
结果还是比较理想的,算是比较快:
end
欢迎关注个人公众号“ 鸡翅编程 ”,这里是认真且乖巧的码农一枚。
---- 做最乖巧的博客er,做最扎实的程序员 ----
旨在用心写好每一篇文章,平常会把笔记汇总成推送更新~