几种常见排序及优化版本

       排序算法是很常见的当然也是相当重要的算法之一,排序又分了好几种算法,在不同的场景下我们应该明白如何去选择用哪一种,而这就要综合很多种因素,包括时间,空间复杂度以及所给数据的具体情况,这里就先来介绍一下算法的分类:



1.冒泡排序

这种排序应该是我们接触最早的,它的思想就是:比较相邻的两个关键字,如果反序则交换,直到没有反序的记录为止。如果我们有n个元素,那么需要进行n-1趟的冒泡,而每一趟比较的元素个数为n-i-1,两个for循环就解决了。这里就不再详细缀诉了,下面是实现的优化版本:

void BubbleSort(int* arr,size_t sz)
{
	assert(arr);
	bool flag=true;         //作为标志位
	for(int i=0;i<sz-1;++i)   //控制趟数
	{
		flag=true;    
		for(int j=0;j<sz-i-1;++j)  //控制每趟需要比较的次数
		{
			if(arr[j]>arr[j+1])
			{
				std::swap(arr[j],arr[j+1]);
				flag=false;
				/*int tmp=arr[j];
				arr[j]=arr[j+1];
				arr[j+1]=tmp;*/
			}
		}
		if(flag)
			break;
	}
}

这是以前写的一篇比较详细的冒泡排序:


我们重点看一下时间复杂度的分析:
最好的情况,也就是要排序的数据本身有序,我们需要进行n-1次的比较,没有数据交换,时间复杂度为O(n).
最坏的情况下,就是逆序的时候,此时需要比较n*(n-1)/2次(等差数列),并作等数量级的移动,所以时间复杂度为O(n^2).

2.选择排序
简单来说,选择排序就是通过n-i次关键字之间的比较,从n-i-1个记录中选出关键字最小或最大的记录,并和第i个(1<=i<=n)个记录交换。

void SelectSort1(int* arr,size_t sz)
{
	assert(arr);
	int i=0;
	int min=0;
	for(i=0;i<sz;++i)
	{
		min=i;     //将当前下标定义为最小下标
		for(int j=i+1;j<sz;++j)  //从后面的数据选择小的
		{
			if(arr[j]<arr[min])
				min=j;
		}
		if(min!=i)
		{
			std::swap(arr[i],arr[min]);
		}
	}
}

无论最好还是最坏的情况,比较次数都是一样多,都为n*(n-1)/2次,通俗的讲就是每一趟都需要遍历一遍找到最小数的正确位置上;交换次数最好的情况下为0次,最坏的情况下为n-1次,最终的排序时间是比较次数与交换次数的和,因此总的时间复杂度为O(n^2),性能略优于冒泡。

优化:每次遍历的同时选出最大的和最小的数据,将两者放在正确位置上,这样遍历的次数就减少一半,性能就提高了一些,但时间复杂度忽略常数后依然为O(n^2)。

void SelectSort2(int* arr,size_t sz)
{
	//优化:一次选择两个数,小的放在左边,大的放在右边
	assert(arr);
	int left=0;
	int right=sz-1;
	while(left<right)
	{
		int min=left;
		int max=left;
		for(int i=left;i<=right;++i)
		{
			if(arr[i]<arr[min])
				min=i;
		
			if(arr[i]>arr[max])
				max=i;
		}

		std::swap(arr[min],arr[left]);

		if(max==left)
			max=min;

		std::swap(arr[max],arr[right]);

		++left;
		--right;
	}
}


详细分析过程还可看:


3.直接插入排序
它的思想大概是:将一个记录插入到已经排好序的的有序表中,从而得到一个新的,记录数增1的有序表。

void InsertSort(int* arr,size_t sz)
{
	assert(arr);
	for(int index=1;index<sz;++index)  //先默认下标为0的元素有序
	{
		int pos=index-1;
		int tmp=arr[index];
		while(pos>=0 && tmp < arr[pos])
		{
			std::swap(arr[pos],arr[pos+1]);
			--pos;
		}
		arr[pos+1]=tmp;
	}
}

不太明白还可以看详解哦:

时间复杂度分析:
最好的情况,待排序序列本身为有序的,需要进行n-1次比较,没有移动,时间复杂度为O(n).
最坏的情况,待排序序列为逆序的,此时需比较(n+2)*(n+1)/2,而移动次数也达到(n+4)*(n-1)/2次,可以举个例子去理解一下,总的来说,忽略常数时间复杂度依然为O(n^2),但待排序序列是随机的,那么平均比较和移动的次数约为(n^2)/4,同样的O(n^2)直接插入排序还是比冒泡和选择排序性能要优一些。

4.希尔排序

如图分析:


希尔排序实际上是在优化直接插入排序,它的目的是让较大数据尽可能快的调到序列后边,让较小数据尽可能快的调到前面来,让序列快速趋于基本有序。但是这里比较麻烦的是它的gap该如何去取,目前还是个数学难题,这里先采取gap=gap/3 +1的方式。
这种算法的时间复杂度为O(n^(3/2)).要优于直接插入排序。

void ShellSort(int* arr,size_t sz)
{
	assert(arr);
	int gap=sz;
	while(gap > 1)  
	{
		gap=gap/3 +1;  //保证最后一个增量值为1 

		for(int index=gap;index<sz;++index)  //先默认下标为0的元素有序
	    {
			int pos=index-gap;
			int tmp=arr[index];
			while(pos>=0 && tmp < arr[pos])
			{
				std::swap(arr[pos],arr[pos+gap]);
				pos-=gap;
			}
			arr[pos+gap]=tmp;
	    }
	}
}



5.堆排序
这种排序以前也是写过的,这里就不再介绍它的原理了,不明白的可以去看一下:

void AdjustDown(int *a, size_t root, size_t size)  
{   
    size_t parent = root;  
    size_t child = parent * 2 + 1;  //先指向左孩子
    while (child < size)  
    {  
        if (child + 1 < size && a[child] < a[child + 1])  
        {  
            ++child;  
        }  
        if (a[parent] < a[child])  
        {  
            std::swap(a[parent], a[child]);  
            parent = child;  
            child = parent*2 + 1;  
        }  
        else  
        {  
            break;  
        }  
    }  
}  
void HeapSort(int *arr,int sz)
{
	assert(arr);
	//升序建大堆
	for(int i=(sz-2)/2;i>=0;--i)   //从倒数第一个非叶子节点开始向下调整
	{
		AdjustDown(arr,i,sz);
	}

	for (size_t i=0;i<sz;++i)      
    {  
        std::swap(arr[0],arr[sz-1-i]);   
        AdjustDown(arr,0,sz-1-i);  
    }  
}

时间复杂度分析:
在建堆的过程中,时间复杂度为O(n*lgn),调堆也是O(n*lgn),所以总的时间复杂度为O(n*lgn).效率确实比上面的几种都要高,但他也有缺陷的地方,一般堆排序适用于数组存储的数据,而对链表存储的数据时没办法的;其次,在序列接近有序的时候,插入排序会更优一些。

有关快排,归并算法会在下一篇继续介绍……




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值