排序算法 总结&思考(二)


写排序算法总是计算机招聘笔试面试必考,也最检验考基础能力的题~续前一篇介绍其他四种排序算法

5. 轮轮选拔 の 冒泡排序 (bubble sort)

冒泡排序也是原理比较好懂,但是实现效率不高的排序方法之一,比较适合初学者熟悉理解算法的过程

“冒泡”本身就是一个很形象的比喻。在此排序算法中,每一轮会前后两两比较,将较小的数(或较大)前移。遍历整个数组后,很容易想到第一个数就是当前数组中最小(或最大)的数,很像一个比较轻的气泡从水底慢慢上浮~。这样每轮可以比较巧妙地选出数组剩下中最小的,最终实现排序

冒泡排序在复杂度上和直接插入排序一样,前者是每轮“选出”第i小的,而后者则是每轮“处理”第i个。最坏情况和平均情况时间复杂度均是O(n²),最好情况下,即基本有序时为O(n),同时他们也都是稳定的

void bubblesort(int a[],int low,int high)
{
	int i=0,j=0;
	int changed ;
	for(i=0;i<high-low;i++)
	{
		changed = 0;
		for(j=high;j>low+i;j--)
			if(a[j]<a[j-1])
			{swap(a[j],a[j-1]);changed = 1;}
		if(changed == 0)
			return;
	}
}

一趟排序中如果没有交换元素,那么可以认为所有元素都满足次序了,排序也就完成了~

6. 基准分类 の 快速排序 (quick sort)

终于到了大家喜闻乐见的快速排序了!作为应用最广泛的排序算法之一,不了解快速排序的几乎都算不上一个入门的程序员。当然,并不是要对代码死记硬背,理解原理才是关键。

快速排序基于分治&递归的思想,将数组分成两个部分,前一部分比某个元素小,后一部分比某个元素大,那么把这个元素放在中间就可以了。剩下的工作就是对前后两个小数组分别进行快速排序,直到仅有一个元素为止

写快速排序的代码并不难,需要注意交换和递归调用过程中对“边界”元素的正确处理

int partition(int a[],int low,int high)
{
	//select first element as pivot
	int pivot = a[low];
	while(low<high)
	{
		while(low<high&&a[high]>=pivot)
			high --;
		if(low<high)
			//copy this smaller element to head place
			a[low++] = a[high];
		while(low<high&&a[low]<=pivot)
			low ++;
		if(low<high)
			//copy this larger place to tail place
			a[high--] = a[low];
	}
	//when break while ,means low = high, last move pivot to its right place
	a[low] = pivot;
	return low;
}

void quicksort(int a[],int low,int high)
{
	int pivot;
	if(low<high)
	{
		pivot = partition(a,low,high);
		quicksort(a,low,pivot-1);
		quicksort(a,pivot+1,high);
	}
}

快速排序为什么会快?也许以一般人的思维,这种选中间元素,分区,递归调用的方式更复杂也更慢,因为人脑总是习惯认为“做简单的事”最快,不希望记忆太多东西。而计算机则不一样,它天生就是用来算得,能够很好地处理“记忆”某个位置的需求。

当选取的枢纽恰好为“中间”元素时,快速排序每次近似对数组进行“二分”操作,能够达到最好的时间复杂度O(nlogn),它的平均时间复杂度也是这样。而最坏情况会退化为冒泡排序一样的O(n²)。所以,快速排序也不一定是最快的。好的枢纽元素选取会对算法效率取决定性作用。因为枢纽元素的不确定性,快速排序也是不稳定的。

7. 合二为一 の 归并排序

归并排序也是比较经典的排序算法之一,多用于外部排序。区别于前面6种可分为插入,选择,交换三类,它是独立的一种类型

与快速排序的“从上到下,从大到小”不同,归并排序恰好是相反的。它(纯归并排序)是从单个元素开始,逐步选择,两两合并,最终成为有序的一个数组

有序数组“归并”不难理解,可能难想到的是用这种方式来排序一个无序数组,即把单个元素也看成是最小的有序数组,然后依次合并

void merge(int a[],int b[],int low,int mid,int high)
{
	int i=low,j=mid+1,k=low;
	while(i<=mid&&j<=high)
		if(a[i]<=a[j]) //select the smaller element to be
			b[k++]=a[i++];
		else
			b[k++]=a[j++];
	while(i<=mid)
		b[k++]=a[i++];
	while(j<=high)
		b[k++]=a[j++];
	for(i=low;i<=high;i++)//copy the array back
		a[i]=b[i];
}
void mergeSort(int a[],int b[],int low,int high)
{
	if(low<high)
	{
		int mid=(low+high)/2;
		mergeSort(a,b,low,mid);
		mergeSort(a,b,mid+1,high);
		merge(a,b,low,mid,high); //merge the two sorted array
	}
}

需要注意的是,与前面算法不同,归并排序需要一个额外的空间来保存排序的数组。代码中a为待排序数组,b为临时数组,长度大于等于a(为了和前面代码使用风格一致,把临时数组也做为参数,实际使用中不需要)

由于有了空间代价,归并排序的时间复杂度可以略微提升一些。有趣的是,这个算法无视原数组的有序性,即不管原数组是有序还是无序,所进行的操作都是一样多的。这样,归并排序的时间复杂度任何时候都是O(nlogn),因为它都是两两合并的。

实际应用中往往对小集合先进行简单选择排序,然后对多个有序集合进行归并排序

8 优先级策略 の  基数排序

说到最后一个基数排序,我想提的问题是“什么是排序”

回到本质,排序大致就是按照某种优先级和顺序规则,将一些数据重新排列。只不过通常所说的排序采用的优先级规则是大家公认的数字大小。在这一默认优先级中,首先以最高位开始,按大小顺序排列,如果最高位相同则比较次高位,如此得到。也就是说,这类默认排序规则是按照数位优先级来决定的

基数排序的挖掘了默认排序规则,按照位数从低到高一遍遍“收集”重排。恰好就是默认排序的规则。后一次的收集重排可能会“覆盖”前一次的顺序,体现在数位优先级上。如果某两个数从某一位开始,前面的数字都相同,那么决定他们相对顺序就在这一位的“收集”重排上。后续操作也不会改变了

基数排序步骤比较简单,举例子:

设最大的是3位数,就把每个数按最多位数补齐。从个位开始,十位,百位,从低到高依次执行“收集重排”操作

“收集重排”操作是指以指定位为基准,将数组里的元素依次取出,放到对应位0~9的“桶”中,最后再从0~9的桶里取出元素放回原数组,这样做的目的是,使操作后数组元素的顺序对该位来说的有序的(暂时忽略其他位)

这样经过3轮排序,就实现了高优先级对低优先级的覆盖,数位越高,优先级越高。最终整个数组就是“公认”有序了

基数排序也需要辅助存储空间,带来的好处是时间上可能的缩短,但是它有缺点就是不能用于正负数的同时排序,没有真正单步的“比较”机制

int  getLoopTimes(int num)
{
	int count = 0 ;
	do {
		count++;
		num = num / 10;
	}while(num!=0);
	return count;
}
int findMaxNum( int a[],int low,int high)
{
	int i ;
	int max = 0;
	for( i = low ; i <= high ; i++) {
		if(a[i] > max)
			max = a[i];
	}
	return max;
}
//gather numbers into buckets, then put them back
void gatherAndPut(int a[] , int low,int high , int loop)
{
	//build some buckets.for num 0~9
	int buckets[10][50] = {0} ;
	int i , j ,k ,digitNum = 1;
	for(i=1;i<loop;i++)
		digitNum*= 10;
	for( i = low ; i <= high ; i++ ) {
		int index = (a[i] / digitNum) % 10;
		for(j = 0 ; j < 50 ; j++) {
			if(buckets[index][j] ==0) {
				buckets[index ][j]  =  a[i] ;
				break;
			}
		}
	}
	//put elements back
	 k = low ;
	for(i = 0 ; i < 10 ; i++) {
		for(j = 0 ; j < 50 ; j++) {
			if(buckets[i][j] !=0) {
				a[k] = buckets[i][j] ;
				buckets[i][j]=0;
				k++;
			}
			else break;
		}
	}
}
void radixSort(int a[] , int low,int high)
{
	//get the maxnum of array
	int maxNum = findMaxNum(a, low , high );
	//get the digits of maxnum, for loop times
	int loopTimes = getLoopTimes(maxNum);
	int i ;
	//do gather-and-put
	for( i = 1 ; i <= loopTimes ; i++) {
		gatherAndPut(a , low ,high , i );
	}
}

代码写的比较粗糙,未经过优化,主要是体现基数排序的思想

基数排序的时间复杂度与数字最大位数d有关,为O(d(n+r)),其中r为每位的基数,在自然数中为10,而d与n相关,所以实际时间复杂度是O(nlogn),算法是稳定的

基本排序算法总结:

1) 插入排序的原理:向有序序列中依次插入无序序列中待排序的记录,直到无序序列为空,对应的有序序列即为排序的结果,其主旨是“插入”。直接插入 希尔排序

 2) 交换排序的原理:先比较大小,如果逆序就进行交换,直到有序。其主旨是“若逆序就交换”。  冒泡排序 快速排序
 3) 选择排序的原理:先找关键字最小的记录,再放到已排好序的序列后面,依次选择,直到全部有序,其主旨是“选择”。简单选择 堆排序

 4) 归并排序的原理:依次对两个有序子序列进行“合并”,直到合并为一个有序序列为止,其主旨是“合并”。 
 5) 基数排序的原理:按待排序记录的关键字的组成成分进行排序的一种方法,即依次比较各个记录关键字相应“位”的值,进行排序,直到比较完所有的“位”,即得到一个有序的序列。

实际排序算法的选择:

(1) 若n较小(如n值小于50),对排序稳定性不作要求时,宜采用选择排序方法,直接插入排序法。但如果规模相同,且记录本身所包含的信息域比较多的情况下应首选简单选择排序方法。因为直接插入排序方法中记录位置的移动操作次数比简单选择排序多,所以选用简单选择排序为宜。    

(2) 如果序列的初始状态已经是一个按关键字基本有序的序列,则选择直接插入排序方法和冒泡排序方法比较合适,因为“基本”有序的序列在排序时进行记录位置的移动次数比较少。 

 (3) 如果n较大,则应采用时间复杂度为O(nlog2n)的排序方法,即快速排序、堆排序或归并排序方法。快速排序是目前公认的内部排序的最好方法,当待排序的关键字是随机分布时,快速排序所需的平均时间最少;堆排序所需的时间与快速排序相同,但辅助空间少于快速排序,并且不会出现最坏情况下时间复杂性达到O(n2)的状况。这两种排序方法都是不稳定的,若要求排序稳定则可选用归并排序。通常可以将它和直接插入排序结合在一起用。先利用直接插入排序求得两个子文件,然后,再进行两两归并

 

--后记

排序算法也不仅仅用于数字int型,只要有了约定的顺序规则,字符串等类型也可以用这些思想进行排序

有最好的算法,只有最合适的。实际应用中往往会遇到各种特殊条件,需要经过改进和优化,掌握基本的算法思想是必须的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在信号处理领域,DOA(Direction of Arrival)估计是一项关键技术,主要用于确定多个信号源到达接收阵列的方向。本文将详细探讨三种ESPRIT(Estimation of Signal Parameters via Rotational Invariance Techniques)算法在DOA估计中的实现,以及它们在MATLAB环境中的具体应用。 ESPRIT算法是由Paul Kailath等人于1986年提出的,其核心思想是利用阵列数据的旋转不变性来估计信号源的角度。这种算法相比传统的 MUSIC(Multiple Signal Classification)算法具有较低的计算复杂度,且无需进行特征值分解,因此在实际应用中颇具优势。 1. 普通ESPRIT算法 普通ESPRIT算法分为两个主要步骤:构造等效旋转不变系统和估计角度。通过空间平移(如延时)构建两个子阵列,使得它们之间的关系具有旋转不变性。然后,通过对子阵列数据进行最小二乘拟合,可以得到信号源的角频率估计,进一步转换为DOA估计。 2. 常规ESPRIT算法实现 在描述中提到的`common_esprit_method1.m`和`common_esprit_method2.m`是两种不同的普通ESPRIT算法实现。它们可能在实现细节上略有差异,比如选择子阵列的方式、参数估计的策略等。MATLAB代码通常会包含预处理步骤(如数据归一化)、子阵列构造、旋转不变性矩阵的建立、最小二乘估计等部分。通过运行这两个文件,可以比较它们在估计精度和计算效率上的异同。 3. TLS_ESPRIT算法 TLS(Total Least Squares)ESPRIT是对普通ESPRIT的优化,它考虑了数据噪声的影响,提高了估计的稳健性。在TLS_ESPRIT算法中,不假设数据噪声是高斯白噪声,而是采用总最小二乘准则来拟合数据。这使得算法在噪声环境下表现更优。`TLS_esprit.m`文件应该包含了TLS_ESPRIT算法的完整实现,包括TLS估计的步骤和旋转不变性矩阵的改进处理。 在实际应用中,选择合适的ESPRIT变体取决于系统条件,例如噪声水平、信号质量以及计算资源。通过MATLAB实现,研究者和工程师可以方便地比较不同算法的效果,并根据需要进行调整和优化。同时,这些代码也为教学和学习DOA估计提供了一个直观的平台,有助于深入理解ESPRIT算法的工作原理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值