6种排序的实现与总结,请各位指正

gnimgnot 2013-07-26 05:15:51
一共实现了6种比较常见的排序算法,分别是:
选择排序,插入排序,冒泡排序,归并排序,快速排序,堆排序

效率:
衡量一个算法的效率包括空间和时间,有时候还要考虑稳定性。
前3种排序的方法效率较低,实现也比较简单,适合规模比较小的排序,个人认为适合排序总量在10000以下的随机数组。
后3种排序的方法效率较高,实现稍微复杂一点,但也还好,适合规模较大的排序。

时间方面,前3种排序的复杂度都是O(N^2),后3种排序的复杂度都是O(N*LogN),即呈指数级减少(因为基本思路都是递归的方式分治)。当然了,这是平均情况。
空间方面,即是否需要额外的空间,只有归并排序需要一个数组长度相同的空间来存储排序的结果,即O(N)。快速排序的需要O(log2N)。其余排序都不需要额外的空间。
稳定性方面,只有插入排序和归并排序是稳定的。稳定性保证的是数组中值相等的数据在排序时顺序不变,这在纯int型数组时没什么意义,但如果是复杂数据结构的排序,如果改变了顺序则可能影响数据结构中其他字段的排序。
疑问:谁能告诉我为什么快速排序的空间复杂度是O(log2N)?

特点:
每种排序都有自己的特点,要不然也不会留传了这么久。以下是个人看法:
冒泡排序:比较SB,只适合教学,效率低,但易实现。但能保证稳定。
选择排序:比冒泡排序好一点,好的地方是交换次数少了,但仍然很SB。而且不稳定。
插入排序:有点像打扑克牌时的排序,但插入时会让数组的移动变多,如果是链表则效率很高。且能保证稳定。
归并排序:典型地递归分治,缺点是需要额外的空间来存储结果。但能保证稳定。
快速排序:跟归并排序很像,但区别是归并排序切分的中点是数组索引,快速排序切分的中点是第一个数据的值。相同的是,都要碰运气。但不稳定。
堆排序:思路很特别,花了好几个班车上的时间片,另外用了扑克牌演示每一步的过程才弄明白流程。但不稳定。

运行时间比较:
这里就专门写个测试程序来测试一下这6种排序算法的运行时间倒底区别有多大。
随机生成100,000个数:

const int N = 200000;  
int O = 0;

int* GenRandom()
{
srand( (unsigned)time( NULL ) );

int* a = new int[N];
for (int i = 0; i < N; i++)
{

a[i] = rand()*rand() ;
}
return a;
}


用下面的代码来计算时间:

SYSTEMTIME StartTime = {0};  
FILETIME StartFileTime = {0};
SYSTEMTIME EndTime= {0};
FILETIME SEndFileTime= {0};

int _tmain(int argc, _TCHAR* argv[])
{
int* a = GenRandom();
GetLocalTime(&StartTime);
printf("timeBefore %d:%d:%d \r\n", StartTime.wMinute, StartTime.wSecond, StartTime.wMilliseconds);

BubbleSort(a);
/*SelectionSort(a);
InsertSort(a);
MergeSort(a,0,N-1);
QuickSort(a,0,N-1);
HeapSort(a,0,N);*/

GetLocalTime(&EndTime);
printf("timeAfter %d:%d:%d \r\n", EndTime.wMinute, EndTime.wSecond, EndTime.wMilliseconds);

return 0;
}


依次得到的结果如下:
BubbleSort:1分37秒818,是的,你没看错。。
SelectionSort :12秒338。
InsertSort:1分11秒23。
MergeSort:1秒598
QuickSort:0秒036
HeapSort:0秒081

说多了都是泪....这才是100,000个数,假设要是千万级的,差别就更大了,可能冒泡需要几个小时。。而快速和堆排序都表现的相当优秀。
这也难怪快速排序叫做快速排序。

实现代码:
以防我的代码实现的有问题,影响了测试效果,特将代码列与此,如果有不足之处请各位指教和指正:


#include "stdafx.h"  
#include "windows.h"
#include "time.h"

const int N = 100000;
int O = 0;

int* GenRandom()
{
srand( (unsigned)time( NULL ) );

int* a = new int[N];
for (int i = 0; i < N; i++)
{

a[i] = rand()*rand();
}
return a;
}

void swap(int& a, int& b)
{
int temp = 0;
temp = a;
a = b;
b = temp;
}

//small -> large
void SelectionSort(int* ua)
{
//round times,遍历N次
for (int i = 0; i < N-1; i++)
{
int nMinIndex = i; //最小值的索引
//每次确定一个值,从第一个值开始。。。第二次从第二个值开始
for (int j = i + 1; j < N; j++)
{
if( ua[nMinIndex] >= ua[j] )
{
nMinIndex = j;
}
O++;
}
swap(ua[i], ua[nMinIndex] );
}
}

//small -> large
void InsertSort(int* ua)
{
//round times
for (int i = 1; i <= N; i++)
{
for (int j = i; j > 0; j--)
{
if( ua[j] < ua[j-1] )
{
swap(ua[j], ua[j-1] );
}
}
}
}

//small -> large
void BubbleSort(int* ua)
{
O = 0;
//round times
for (int i = 0; i < N; i++)
{
/*printf("round %d \r\n", i);
for (int i = 0; i < N; i++)
{
printf("a[%d]=%d \r\n",i, *(ua+i));
} */

for (int j = 0; j < (N-i-1); j++)
{
if(ua[j] > ua[j+1])
{
swap(ua[j], ua[j+1] );
}
O++;
}

}
}

void Merge(int* ua, int nStart, int nMid, int nEnd)
{
int a[N];
int i = nStart;
int j = nMid + 1;

for (int k = nStart; k <= nEnd; k++)
{
a[k] = ua[k];
}

for (int k = nStart; k <= nEnd; k++)
{
if(i > nMid)
{
ua[k] = a[j++];
}
else if(j > nEnd)
{
ua[k] = a[i++];
}
else if( a[j] < a[i])
{
ua[k] = a[j++];
}
else
{
ua[k] = a[i++];
}

/*printf("round %d \r\n", k);
for (int k = nStart; k < nEnd; k++)
{
printf("a[%d]=%d \r\n", k, *(ua + k));
} */
}
}
//small -> large
void MergeSort(int* ua, int nStart, int nEnd)
{
//递归退出条件
if(nStart >= nEnd)
{
return;
}
int nMid = nStart + (nEnd - nStart) / 2;

MergeSort(ua, nStart, nMid);
MergeSort(ua, nMid+1, nEnd);
Merge(ua, nStart, nMid, nEnd);
}

int QuickPartition(int* ua, int nStart, int nEnd)
{
int i = nStart;
int j = nEnd + 1;

//中点值
int nFlagValue = ua[nStart];

while(1)
{
//找到左边大于中点的值,记录索引
while( ua[++i] < nFlagValue )
{
if( i == nEnd)
{
break;
}
}
//找到右边小于中点的值,记录索引
while( ua[--j] > nFlagValue )
{
if( j == nStart)
{
break;
}
}
//两边向中间靠拢的过程中相遇则退出
if( i >= j)
{
break;
}
//交换两边的值
swap( ua[i], ua[j] );
}
//将右边最后一个小于中点值的数与中点值交换位置,
//保证中点值的左边都小于中点值,右边都大于中点值
swap( ua[nStart], ua[j] );

//返回将右边最后一个小于中点值的数的索引,做为右边部分的中点值。
return j;
}
void QuickSort(int* ua, int nStart, int nEnd)
{
//递归退出条件
if(nStart >= nEnd)
{
return;
}
int nMid = QuickPartition(ua, nStart, nEnd);
QuickSort(ua, nStart, nMid-1);
QuickSort(ua, nMid+1, nEnd);
}

void HeapAdjust(int* ua, int nStart, int nEnd)
{
int nMaxIndex = 0;

while ( ( 2*nStart + 1) < nEnd )
{
nMaxIndex = 2*nStart + 1;
if ( ( 2*nStart + 2) < nEnd)
{
//比较左子树和右子树,记录较大值的Index
if (ua[2*nStart + 1] < ua[2*nStart + 2])
{
nMaxIndex++;
}
}

//如果父结点大于子节点,则退出,否则交换
if (ua[nStart] > ua[nMaxIndex])
{
break;
}
else
{
swap( ua[nStart], ua[nMaxIndex] );
//堆被破坏,继续递归调整
nStart = nMaxIndex;
}
}
/*for (int i = 0; i < N; i++)
{
printf("%d ",ua[i]);
}
printf("\r\n");*/
//printf("%d ", O++);
}

void HeapSort(int* ua, int nStart, int nEnd)
{
for (int i = nEnd/2 -1; i >= 0 ; i--)
{
HeapAdjust( ua, i, nEnd);
}

for (int i = nEnd-1; i > 0; i--)
{
swap(ua[0], ua[i]);
HeapAdjust(ua, 0, i);
}
}

SYSTEMTIME StartTime = {0};
FILETIME StartFileTime = {0};
SYSTEMTIME EndTime= {0};
FILETIME SEndFileTime= {0};

int _tmain(int argc, _TCHAR* argv[])
{
int* a = GenRandom();
GetLocalTime(&StartTime);
printf("timeBefore %d:%d:%d \r\n", StartTime.wMinute, StartTime.wSecond, StartTime.wMilliseconds);

//BubbleSort(a);
//SelectionSort(a);
//InsertSort(a);
//MergeSort(a,0,N-1);
//QuickSort(a,0,N-1);
HeapSort(a,0,N);

GetLocalTime(&EndTime);
printf("timeAfter %d:%d:%d \r\n", EndTime.wMinute, EndTime.wSecond, EndTime.wMilliseconds);
printf("times %d \r\n", O);

return 0;
}


也可见Blog:
http://blog.csdn.net/cuit/article/details/9497065
...全文
1052 44 打赏 收藏 转发到动态 举报
写回复
用AI写文章
44 条回复
切换为时间正序
请发表友善的回复…
发表回复
纯洁的老黄瓜 2013-11-20
  • 打赏
  • 举报
回复
算法和数据结构是我的弱中之弱,占楼学习一下
FlyToTMoon 2013-11-20
  • 打赏
  • 举报
回复
MARK,学习下。。
方紫涵 2013-11-20
  • 打赏
  • 举报
回复
整好用到啊,多谢版主
PowerBI木小桼 2013-08-11
  • 打赏
  • 举报
回复
每种方法都有其优势,具体情况具体分析吧
max_min_ 2013-08-11
  • 打赏
  • 举报
回复
置顶吧!
fzamygsd 2013-08-10
  • 打赏
  • 举报
回复
好帖、受教了
c090869 2013-08-10
  • 打赏
  • 举报
回复
学习了,谢谢。 花了好几天才看懂了堆排序。 好像快速排序加个条件,也可以保证它的稳定性。
  • 打赏
  • 举报
回复
make 学习。
赵4老师 2013-07-31
  • 打赏
  • 举报
回复
<1000个元素,冒泡排序 <100000个元素,qsort函数 <10000000个元素,放数据库中,建索引,(B+树排序) ≥10000000个元素,没用到过。
大尾巴猫 2013-07-30
  • 打赏
  • 举报
回复
贴一下我实现的快速排序,经过优化,效率和std::sort一样。 经过1000万和5000万多组数据测试,和std::sort用时相差 +-1%以内。
template <typename T>
inline void QuickSort(T arr[], int n)   //快速排序
{
	QSpartition(arr, 0, n - 1);
	InsertSort(arr, n);          //最后用插入排序对整个数组排序
}

template <typename T>
void QSpartition(T arr[], int low, int high)     //快速排序递归函数
{
    
//	if (low >= high)     //有插入排序,不需要这句
//		continue;
	
	const int min_insert = 128;
	if (high - low < min_insert)  //数据小于一定数量的时候用直接插入排序
	{
		return;  //最后用插入排序对整个数组排序
	}

	int i, j;
	T temp;
	
    //下面一段是前中后3个数据进行插入排序
	i = (low + high) / 2;
	if (arr[i] < arr[low])
	{
		temp = arr[i];
		arr[i] = arr[low];
		arr[low] = temp;
	}
	if (arr[high] < arr[i])
	{
		temp = arr[high];
		arr[high] = arr[i];
		arr[i] = temp;
		if (arr[i] < arr[low])
		{
			temp = arr[i];
			arr[i] = arr[low];
			arr[low] = temp;
		}
	}
//	if (high - low < 3)   //小区间用插入排序,这里就不需要判断了
//		return;           //不用插入排序,只有2个数据的时候排序错误
	
	temp = arr[i];           
	arr[i] = arr[low + 1];  //中值放在最左边
	i = low + 1;            //左右边界缩小1
	j = high - 1;           //边界2个数字插入排序排过,满足左<=中<=右

	while (i < j)
	{
		while (temp < arr[j] && i < j)   //从右往左扫描小于目标的值,应该放在左半部分
			j--;
		if (i < j)                        //找到后放在左边
			arr[i++] = arr[j];
		else
			break;
		while (temp > arr[i] && i < j)   //从左往右扫描大于目标的值,要放在右边
			i++;
		if (i< j)
			arr[j--] = arr[i];
		else
			break;
	}
	arr[i] = temp;                       // i = j,正好是分界线,回填目标值
	if ((i - low) > 1) 
		QSpartition(arr, low, i - 1);     //递归左边
	if ((high - i) > 1)
		QSpartition(arr, i + 1, high);     //递归右边
}

template <typename T>
void InsertSort(T arr[], int n)   //直接插入排序
{
	int i, j;
	T temp;

	for (i = 1; i < n; i++)
	{
		temp = arr[i];
		for (j = i - 1; j >= 0; j--)
		{
			if (temp < arr[j])            //逐个往前比较,碰到大于目标的,拉过来
				arr[j + 1] = arr[j];
			else
				break;
		}
		arr[j + 1] = temp;               //把目标值填入空位
	}
}
湛卢VV 2013-07-29
  • 打赏
  • 举报
回复
好久没看到这么牛牛的帖子了
gnimgnot 2013-07-29
  • 打赏
  • 举报
回复
引用 30 楼 stereoMatching 的回复:
[quote=引用 28 楼 cuit 的回复:] stl的sort就是快速排序
没这么单纯,大多是各种排序的混合体 就我看过的,资料多的时候用快速排序,少的时候用insertion sort 我举std::sort是当个例子,对演算法有兴趣,而且想提高代码复用性的人 没一个会不对std::sort如何同时达到高效排序又具有如此高的可复用性没兴趣的[/quote] 谢谢,我会看看它的实现。
AndyStevens 2013-07-29
  • 打赏
  • 举报
回复
1.快排的一个好的实现效率会远远高于归并,优化技术如Randomized ,尾递归,小数列插入排序 等等。 2.meger有真正的非递归版本和in-place版本 3.堆排序的结构的特殊化可以用于实现一些特殊的应用,如优先级队列 4.另外一些特殊的条件下可以使用一些线性时间的排序,如基数排序,计数排序,桶排序等等,可以达到时间复杂度θ(n)
大漠孤鸿 2013-07-29
  • 打赏
  • 举报
回复
引用 27 楼 cuit 的回复:
[quote=引用 16 楼 liyanfasd 的回复:] LZ写的插入排序通过swap交换元素位置失效率明显降低。 通过标记移位法时间效率会更高一下。
//small -> large  
void InsertSort(int* ua)  
{  
    int i, j, tmp;
	//round times    
    for (i = 1; i < N; i++)    
    {    
        j = i;
		tmp = ua[i];
		while(j > 0 && tmp < ua[j - 1])
		{
			ua[j] = ua[j - 1];
			j--;
		}
		ua[j] = tmp;
    }    
}  
时间耗费和选择差不多。
谢谢,但这种是否增加了空间复杂度?[/quote] 只是定义了一个临时变量,空间复杂度是O(1)。建议LZ试试这种方法,看看是不是比频繁调用函数交换变量的效率高。
stereoMatching 2013-07-29
  • 打赏
  • 举报
回复
引用 28 楼 cuit 的回复:
stl的sort就是快速排序
没这么单纯,大多是各种排序的混合体 就我看过的,资料多的时候用快速排序,少的时候用insertion sort 我举std::sort是当个例子,对演算法有兴趣,而且想提高代码复用性的人 没一个会不对std::sort如何同时达到高效排序又具有如此高的可复用性没兴趣的
xiangzhihappy 2013-07-29
  • 打赏
  • 举报
回复
路过~~学习啦!!
gnimgnot 2013-07-29
  • 打赏
  • 举报
回复
引用 20 楼 stereoMatching 的回复:
楼主真有心,稍微跟std::sort比了一下

const int N = 200000;
int O = 0;

//做点小小的修改,免得在loop中重复分配memory
int* GenRandom()
{
    srand( (unsigned)time( NULL ) );

    static std::vector<int> num(N);
    for (int i = 0; i < N; i++)
    {
        num[i] = rand()*rand() ;
    }
    return &num[0];
}

//测试的部分
double sum = 0;
    {        
        for(size_t i = 0; i != 1000; ++i){
            int* num = GenRandom();
            //用c++11的chrono写的测试时间的函数,精确到毫秒
            timeElapsed<std::chrono::system_clock, std::milli, double> elasped;            
            //结果写在旁边
            QuickSort(num, 0, N-1); //16462ms
            //std::sort(num, num + N); // 13732.3ms
            //HeapSort(num,0,N); //  24006.8ms
            sum += elasped.get_elapsed();
        }
    }
    std::cout<<"elasped = "<<sum<<std::endl;
结论 : std::sort还是比较快,而且可以吃下不同的数据结构,不同的比较方法(需要满足strict weak ordering)
stl的sort就是快速排序
gnimgnot 2013-07-29
  • 打赏
  • 举报
回复
引用 16 楼 liyanfasd 的回复:
LZ写的插入排序通过swap交换元素位置失效率明显降低。 通过标记移位法时间效率会更高一下。
//small -> large  
void InsertSort(int* ua)  
{  
    int i, j, tmp;
	//round times    
    for (i = 1; i < N; i++)    
    {    
        j = i;
		tmp = ua[i + 1];
		while(j > 0 && tmp < ua[j - 1])
		{
			ua[j] = ua[j - 1];
			j--;
		}
		ua[j] = tmp;
    }    
}  
时间耗费和选择差不多。
谢谢,但这种是否增加了空间复杂度?
大尾巴猫 2013-07-29
  • 打赏
  • 举报
回复
引用 8 楼 u010936098 的回复:
稳定选择排序: 1:准备一个附加线性表,然后循环从原线性表中选择最小/最大值添加到附加表尾,最后复制回来或替换原表。这是最原始的选择排序思路,是稳定排序。缺点是空间复杂度O(N) 2:循环从待排序数据中选择最小/最大元素,然后插入到正确位置。这是将原始选择排序改进为空间O(1)的版本,是稳定排序。但如果是顺序存储结构时,会增加元素移动次数。
对第1种有点疑问 每次选择一个最小的添加到辅助数组是可以,这样的算法是稳定的。 但是,怎么保证原数组中把已经选择过的数据去除呢? 赋值为某一个值,比如说0,不太合适,其他未处理数据也可能是0. 只有另外设置一个数组标记哪些已经取过了。 这样开销是否太大了。 第2种实现没问题,选择好以后,类似插入排序这样,把前面的都拖回来。移动次数太多了。 可见,稳定的选择排序可以实现,但相比其他简单排序(冒泡、插入)没明显优势而且增加了算法的空间和时间复杂度,所以最可行的选择排序就是通常的交换实现,不稳定。
rocktyt 2013-07-29
  • 打赏
  • 举报
回复
引用 11 楼 cuit 的回复:
[quote=引用 3 楼 rocktyt2 的回复:] 随机数生成的函数,生成的数随机性有问题吧 rand()*rand()这个算法生成的数根本不是随机的,首先分布就会集中于合数,然后中间的数出现概率会比两头的数高很多
首先我解释下用rand()*rand()的原因是因为rand()只能产生最大为32767的数,而我需要排100000个数,为了尽量避免重复。 另外想请教的是,你说的“rand()*rand()这个算法生成的数根本不是随机的”指的是rand()根本不是随机的还是说rand()*rand()不是随机的?如果你生成随机数,使用什么方式? 再者,集中于合数对于测试结果的影响是? 以及,中间的数出现概率会比两头的数高对于测试结果的影响是? 敬请解答,谢谢 [/quote]很简单,你生成的大于32767的数全部都是合数,分布显然是不均匀的 正确的应该是rand()<<32 | rand()
加载更多回复(24)

69,373

社区成员

发帖
与我相关
我的任务
社区描述
C语言相关问题讨论
社区管理员
  • C语言
  • 花神庙码农
  • 架构师李肯
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧