题目:
在给定线性集中有n个元素,要求找出这n个元素中第k(1<=k<=n)小的元素。给定的线性集是无序的。要求在线性时间内完成,线性时间的要求是指在最坏情况下也要保证在O(n)时间内完成选择。输入3行,第一行为数据元素个数,第二行为需要确定的第k小的元素,第三行输入数据元素,用空格隔开。输出一行,输出第k小的元素。
思路:
若是没有要求在O(N)时间内完成选择,那么最简单的做法是先将数组进行排序(冒泡排序法,插入排序法,快速排序法),然后将第k个位置的数输出即可。用冒泡排序法和插入排序法的时间复杂度是O(n^2),快速排序法的平均复杂度是O(n),但在最坏的情况下,其复杂度也是O(n^2),因此,这几种排序法均不满足题目要求。
借鉴快速排序法的思想(不了解快速排序法的可以看我上一篇博客,快速排序算法),如果能让分割点在数组中至少为2n/3大,且不大于n/3个元素(即排序后的位置应该在中间的1/3),这样每次至少可以排除n/3个元素。T(n) <= T(2n/3) + O(n),因此T(n) = O(n)。
线性时间选择的具体算法步骤如下:
步骤1:将n个输入元素以每组m(本题选择m=5)个划分,存放到tmp二维数组中,每组分别排序,找出中位数,将每组的中位数存放到Sub数组中;
步骤2:找出这些中位数的中位数((...的中位数)直到最后的中位数不到10个数,此时直接运用冒泡排序法对这些中位数进行排序,取出这些中位数的中位数),以这个数为基准数,调用partition方法,将数组中小于基准数的数调换到基准数左边,大于基准数的数调到基准数右边;
步骤3:调用了partition方法后的基准数正是出于数组的正确位置(其余元素只是与基准元素比较后被调换到基准元素的左边或右边,并没有排序),返回基准数的下标,与k比较,若基准数下标等于k,说明第k小的数即为基准数;若基准数下标大于k,说明第k小的数在基准数左边,此时对基准数左边的子区间进行步骤1、2,;若基准数下标小于k,说明第k小的数在基准数右边,此时对基准数右边的子区间进行步骤1、2...直至第k小的元素为此时的基准数。
用不太规范的流程图示意一下:
代码如下:
// Chapter13_3.cpp : Defines the entry point for the application.
// 线性时间选择
// 在给定线性集中有n个元素,要求找出这n个元素中第k(1<=k<=n)小的元素。
// 给定的线性集是无序的。要求在线性时间内完成,线性时间的要求是指在最坏情况下
// 也要保证在O(N)时间内完成选择
// 输入3行,第一行为数据元素个数,第二行为需要确定的第k小的元素,第三行输入
// 数据元素,用空格隔开。输出一行,输出第k小的元素。
#include "stdafx.h"
#include<iostream>
using namespace std;
//申明快速排序函数,arr:数组,l:数组左边界,r:数组右边界,返回第target小的元素值
int Quick_select(int arr[],int l,int r,int target);
//交换函数
void swap(int *a,int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
//冒泡排序函数(从大到小),arr:数组,len:数组长度
void Bubble_Sort(int arr[],int len)
{
for(int i=0;i<len;i++)
{
for(int j=i+1;j<len;j++)
{
if(arr[i] > arr[j])
swap(&arr[i],&arr[j]);
}
}
}
//求基准数,arr:数组,l:数组左边界,r:数组右边界,
//返回该数组的基准数
int ChoosePivot(int arr[],int l,int r)
{
int size = r-l+1; //数组个数
int sub_num = size/5; //五个数为1组,一共sub_num组
int i,j,k=0;
//动态分配一个二维数组,每行存5个数
int **tmp = new int *[sub_num];
for(i=0;i<sub_num;i++)
tmp[i] = new int[5];
//存放二维数组中每行数据的中位数
int *Sub = new int[sub_num];
//二维数组存数据(将arr的值重新存放进tmp数组中)
for(i=0;i<sub_num;i++)
{
for(j=0;j<5;j++)
tmp[i][j] = arr[k++]; //将l到r之间的数据重新存储
//求每组数据的中位数,将计算结果存入Sub数组中
Sub[i] = Quick_select(tmp[i],0,4,2);
}
//求中位数的中位数
return Quick_select(Sub,0,sub_num-1,sub_num/2);
}
//将小于M的元素放在M左边,大于M的元素放在M右边,
//arr:数组,l:数组左边界,r:数组右边界,M:基准数,
//返回基准数在整个数组中的位置
int partition(int *arr,int l,int r,int M)
{
int i = l;
int j = r;
//找出基准数M在当前数组中的位置
for(int k=l;k<=r;k++)
{
if(arr[k] == M)
break;
}
//将基准数调换到当前数组的第一个元素
swap(arr[l],arr[k]);
//将小于M的元素放在M左边,大于M的元素放在M右边
while(i < j)
{
while(i<j && arr[j] > M)
j--;
if(i<j)
arr[i++] = arr[j];
while(i<j && arr[i] <= M)
i++;
if(i<j)
arr[j--] = arr[i];
}
arr[i] = M;
//返回基准元素在整个数组中的位置
return i;
}
//快速排序(查找)函数
int Quick_select(int arr[],int l,int r,int target)
{
//若要求的位置在数组以外,返回-1
if(target>r)
return -1;
//如果区间不只一个数
if(l<r)
{
//当区间的元素小于10个时,运用冒泡排序法对数组进行排序
if(r-l+1<10)
{
int *tmp = new int[r-l+1];
for(int i=l;i<=r;i++)
tmp[i-l] = arr[i];
Bubble_Sort(tmp,r-l+1);
return tmp[target-l];
}
//当区间的元素大于10个时,先选取基准数(中位数的中位数)
int M = ChoosePivot(arr,l,r);
int j;
//返回基准数在整个数组中的位置
int q = partition(arr,l,r,M);
//如果该位置是所求的位置,输出其对应的数值
if(q == target)
return arr[q];
//该位置大于所求的位置,说明所求的数在基准数的左边,对左边子区间进行递归
else if(q>target)
return Quick_select(arr,l,q-1,target);
//该位置小于所求的位置,说明所求的数在基准数的右边,对右边子区间进行递归
else if(q<target)
return Quick_select(arr,q+1,r,target);
}
else return arr[l];
}
int main()
{
int n,k;
cout << "输入数据元素个数:";
cin >> n;
cout << "输入k:";
cin >> k;
int *arr = new int[n]; //动态建立数组
for(int i=0;i<n;i++)
cin >> arr[i];
cout << "第" << k << "大的数为:" << Quick_select(arr,0,n-1,k-1) << endl;
cout << endl;
delete [n]arr;
system("pause");
return 0;
}
运行结果如下: