《算法导论》学习(十一)----线性时间下,从输入种找到第i小的数(顺序统计量)(C语言)

系列文章

《算法导论》学习(一)---- 插入排序和归并排序
《算法导论》学习(七)----堆排序和优先队列(C语言)
《算法导论》学习(八)----快速排序(C语言)
《算法导论》学习(九)----为什么比较排序算法时间复杂度的下界是确定的?
《算法学习》学习(十)----计数排序,基数排序,桶排序(C语言)



前言

本文主要讲解顺序统计量,直接解决了从一组输入中找到第i小的元素的问题,给出了相应的C语言代码,同时该算法时间性能很突出,最坏情况是线性时间代价


一、顺序统计量

1.什么是顺序统计量?

(1)定义

在一个由 n 个元素组成的集合中,第 i 个顺序统计量是该集合中第 i 小的元素。 在一个由n个元素组成的集合中,第i个顺序统计量是该集合中第i小的元素。 在一个由n个元素组成的集合中,第i个顺序统计量是该集合中第i小的元素。

(2)例子

对于一个元素集合,我们可以用顺序统计量来定义最大值和最小值
1.最小值是第一个顺序统计量
2.最大值是第n个顺序统计量

2.如何得到顺序统计量?

得到第i个顺序统计量的算法本质上就是求第i小的元素
我们可以有两个方法:

1.将元素直接排好序,直接提取
2.直接求解第i小的元素

我们都知道,排序算法可以达到平均时间代价O(n);同时还有能达到 Θ ( n l g n ) \Theta(nlgn) Θ(nlgn)的排序算法。

而单纯的得到最大值和最小值的时间代价的上界也一定是 O ( n ) O(n) O(n)

这样看来,排序算法似乎已经在性能上很优了,那我们能否得到更优的解呢?

3.能不能有更快的程序可以得到顺序统计量?

我们可以尝试方案2:直接求解第i小的元素。
这个算法也被称作选择算法。它的核心与快速排序的核心相同,都会用到将数组分割成小于标准值和大于标准值的两部分的操作。这个操作本质上是维护一种局部有序,这个局部有序恰好也是顺序统计量所要求的:

顺序统计量可以看作一种局部有序,即只要求在i位置上的值大于i以前的所有值,以前的值是否有序不要求

二、最坏情况为线性时间的选择算法

1.C语言代码

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



#define SIZE 10000
#define LIM 1000



void insertion_sort(int *x,int num)
{
    
    
	int i=0;//循环变量初始化 
	int j=0;//循环变量初始化 
	int tempval;//中间暂存变量初始化,因涉及两数交换所需
	/*
	第一个循环是要遍历一遍数组
	依次为每一个变量找到它合适的位置
	这个合适的位置是一个局部的范围
	范围是在这个变量之前的空间,包括这个变量
	随着循环的进行,到最后
	这个局部的范围就是所有变量
	那么就完成排序 
	*/ 
	for(i=1;i<num;i++)
	{
    
    
		j=i-1;//为位置指正赋值,目的是设置局部范围的界限
		/*
		第二个循环是找准第一个循环所确定变量的合适位置
		循环的方向是从确定变量位置往前
		循环条件包含了判断规则
		满足循环就需要进行交换数据
		不满足循环时,就是局部排序完成时 
		*/ 
		while((x[j+1]<x[j])&&(j>=0))
		{
    
    
			tempval=x[j+1];//数据交换 
			x[j+1]=x[j];
			x[j]=tempval;
			j=j-1;//循环变量赋值,推动循环进行 
		}
	}
}



//快速排序的分割函数
//该函数的目的是:
//1.随机选定一个中间标准值,该值是随机产生的
//2.然后将数组给定区间的所有元素进行“分类”
//3.将小于标准值的元素放到标准值的左边
//4.将大于标准值的元素放到标准值的右边
//5.最后是返回标准值所在的位置信息
int PARTITION_RAND(int *x,int left,int right,int std_val)
{
    
    
	//定义两个循环变量
	//1.i是永远指向小于标准值那组数据的最右端
	//2.j永远指向未分类的元素
	int i;
	int j;
	
	/*
	//模拟随机抽样
	int middle;
	int delt,n=0;
	delt=(right-left+1);
	//若只有一个元素,那就直接返回它本身
	if(delt==1)
	{
		return left;
	}
	middle=rand()%delt;
	n=middle+left;
	
	n=delt/2+right;

	int std_val=x[n];//得到标准值
	*/
	
	int temp;//中间暂存变量
	int num;
	//初始化索引变量
	i=left-1;
	j=left;

	//让索引变量进入循环
	for(j=left;j<=right;j++)
	{
    
    
		//如果元素小于等于标准值,那么就将该元素与i指向的下一个位置的元素交换,
		//如果元素大于标准值,那么就保留
		//这两个操作就可以将元素以i为界分割出来“两类”	
		if(x[j]<=std_val)
		{
    
    
			i++;
			temp=x[i];
			x[i]=x[j];
			x[j]=temp;
			//为了标记中间值的位置
			if(x[i]==std_val)
			{
    
    
				num=i;
			}
		}
	}
	//中间值可能并不在中间
	//我们现在标记了中间值的位置
	//同时i指向最右边的“小值”
	//那么交换两个位置的元素,就可以完成分类
	temp=x[i];
	x[i]=std_val;
	x[num]=temp;	
	//最后i就是中间值的位置
	return i;
}



//返回第i_min小的值 
//该算法先对整个数据进行分组,5个数一组,剩余的一组
//之后得到每组的中位数
//再得到每组的中位数中的中位数
//将最终的中位数传入快排的分割程序,得到该中位数在原数组中的确切位置
//若该中位数的位置是要求的,那么直接返回该值 
//若是要求的位置,然后以该中位数为界,递归地分割原数组,进入下一轮寻找 
int SELECT(int *a,int left,int right,int i_min,int size)
{
    
    
	//如果数组的数量只有1个,那么返回它本身即可 
	if(size==1)
	{
    
    
		return a[left];
	}
	
	int group;//组数 
	int surplus;//分组后剩余数
	
	//中位数是第k小值,即中位数的位置到数组的边界之间数的个数 
	int k=0;
	
	//循环索引变量 
	int i=0;
	int j=0;
	
	//得到分组,以及分组后剩余数据的个数 
	surplus=size%5;
	group=size/5;
	
	//分组缓存 
	int temp[5];
	//中位数的值 
	int m_v=0;
	//中位数在数组中的位置 
	int m_n=0;
	
	
	//组数和剩余数都大于等于1的情况 
	if(surplus>=1&&group>=1)
	{
    
    
		//申请缓存 
		int temp1[surplus];
		int middle[group+1];
	
		/*
		找到中位数 
		*/
		for(i=0;i<group;i++)
		{
    
    
			for(j=0;j<5;j++)
			{
    
    
				temp[j]=a[left+(5*i)+j];
			}
			insertion_sort(temp,5);
			middle[i]=temp[2];		
		}
		for(i=0;i<surplus;i++)
		{
    
    
			temp1[i]=a[left+group*5+i];
		}
		insertion_sort(temp1,surplus);
		middle[group]=temp[surplus/2];
		//给中位数数组再排序 
		insertion_sort(middle,group+1);
		//给中位数赋值 
		m_v=middle[(group+1)/2];		
	}
	//没有剩余数,组数大于等于1 
	else if(surplus==0&&group>=1)
	{
    
    
		int middle[group];
		
		/*
		找到中位数 
		*/
		for(i=0;i<group;i++)
		{
    
    
			for(j=0;j<5;j++)
			{
    
    
				temp[j]=a[left+(i*5)+j];
			}
			insertion_sort(temp,5);
			middle[i]=temp[2];		
		}
		//给中位数数组再排序 
		insertion_sort(middle,group);
		//给中位数赋值 
		m_v=middle[group/2];		
	}
	//数组总数小于5,组数等于0 
	else if(surplus>=0&&group==0)
	{
    
    
		int temp1[surplus];	
		for(i=0;i<surplus;i++)
		{
    
    
			temp1[i]=a[left+i];
		}
		insertion_sort(temp1,surplus);
		//给中位数赋值
		m_v=temp1[surplus/2];			
	}
	//发生错误 
	else
	{
    
    
		return -1;
	}
	
	
	//让中位数作为标准值传入,得到它再原数组中的位置 
	m_n=PARTITION_RAND(a,left,right,m_v);

	//计算包含中位数之前的元素个数 
	k=m_n-left+1;
	
	//调用依次SELECT就会打印一次,可以看出算法规模 
	printf("success\n");
	
	//如果中位数的位置满足要求,那么就直接返回该值	
	if(i_min==k)
	{
    
    
		return a[m_n];
	}
	//如果小于,分割后调用 
	else if(i_min<k)
	{
    
    
		return SELECT(a,left,m_n-1,i_min,k-1);		
	}
	//如果大于,分割后调用 
	//这里的i_min变成i_min-k,这样保持了递归的统一性 
	else if(i_min>k)
	{
    
    
		return SELECT(a,m_n+1,right,i_min-k,size-k);			
	}
	//发生错误 
	else
	{
    
    
		return -1;
	}	
}



int main()
{
    
    
	int a[SIZE];
	int b=0;
	int c=0;
	srand((unsigned)time(NULL));
	int i=0;
	//得到随机数组 
	for(i=0;i<SIZE;i++)
	{
    
    
		a[i]=rand()%LIM;
	}
	b=rand()%SIZE;		
	//将随机数组打印出来 
	for(i=0;i<SIZE;i++)
	{
    
    
		printf("%5d",a[i]);
	}
	printf("\n");
	//对数组进行桶排序 
	c=SELECT(a,0,SIZE-1,b,SIZE);
	//对数组进行排序
	//实际上不需要,这里用作验证 
	insertion_sort(a,SIZE);
	//将排序后的新数组打印出来 
	printf("min_%dth data is %5d\n",b+1,c);	
	printf("correct data is %5d\n",a[b-1]);
	return 0;
}

2.算法逻辑

(1)基本思想

1.首先我们的核心算法是利用快速排序操作的一部分-----”分类“操作。
(1)将某一个数作为标准值,然后返回这个数的下标num。
(2)然后我们拿num与要求的第i个顺序统计量的i作比较:

1.如果i=num,那么直接返回标准值
2.如果i<num,那么将数组的边界设置为left~num-1,进入递归
3.如果i>num,那么将数组的边界设置为num+1~right,进入递归

(3)当然递归调用到数组只有一个数的时候,直接返回它本身

2.关于标准值的选取策略:
我们的标准值选取不是随意的,而是用如下操作:
==首先将数据分组,每组5个,剩余的不足5个的元素自成一组;之后将每组的中位数找到,然后再将所有中位数的中位数找到,将该数作为标准值 ==

过程的描述如图:
在这里插入图片描述
通过这个操作,我们可以发现:如果i>num时,那么我们只需要在阴影部分中寻找即可。

(2)算法特点

该算法的特点就是最坏的情况为线性时间代价;但是时间代价的常数项较大。

(3)算法时间性能分析

实际运行规模

算法的时间代价最坏情况也是线性的,这集中体现在递归调用次数少上面:
下面是算法的实际运行情况,每一个success代表递归调用一次
1.数量10000时:
在这里插入图片描述
2.数量1000时:
在这里插入图片描述
3.数量100时
在这里插入图片描述
我们可以发现,随着输入规模10倍的扩大,递归调用次数并没有随之扩大10倍,说明一般情况下时间代价是小于线性时间的


总结

文章的不妥之处请各位读者包涵并且指正
本文参考了插入排序和快速排序的代码,下面给出文章链接:
《算法导论》学习(一)---- 插入排序和归并排序
《算法导论》学习(八)----快速排序(C语言)

猜你喜欢

转载自blog.csdn.net/weixin_52042488/article/details/126842201