排序算法总结

趁空闲时间,我决定把所学过的排序算法实现并总结下,以便温故知新。

将提到的排序算法有:冒泡排序,快速排序,选择排序,堆排序,插入排序,合并排序,希尔排序,计数排序,基数排序,桶排序。

所有排序结果都默认为排成从前往后升序。程序都在VS2008中运行成功。

 

交换排序:

1,冒泡排序:

冒泡排序是最简单的排序,是刚学c语言时最早接触到的一个算法。

他的思想就是,对待排序元素的关键字从后往前进行多遍扫描,遇到相邻两个关键字次序与排序规则不符时,就将这两个元素进行交换。这样关键字较小的那个元素就像一个泡泡一样,从最后面冒到最前面来。

不多说了,直接写出代码:

#include <iostream>
using namespace std;

void BubbleSort(int a[], int n) {
	for (int i = 0; i < n; i++) //遍历n次
		for (int j = n-1; j > i; j--) {
			if (a[j] < a[j-1]) { //当前比较前面键值,使当前总为最小的
				swap(a[j-1], a[j]);//交换
			}
		}
}
int main()
{
	int num[6] = {23,45,13,2,99,78};
	cout << "冒泡排序前:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << num[i] << " ";
	}
	cout << endl;
	BubbleSort(num, 6);
	cout << "冒泡排序后:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << num[i] << " ";
	}
	cout << endl;
	return 0;
}


这样的代码是效率比较低的,下面是改进后的冒泡排序:

void BubbleSort(int a[], int n) {
	int i, j, flag = 0;//使用flag标记在某次遍历时出现了交换元素
	for (i = 0; i < n; i++) {
		for (j = n-1; j > i; j--) {
			if (a[j-1] > a[j]) {
				swap(a[j], a[j-1]);
				flag = 1;
			}
		}
		if (flag == 0) break;//没有出现交换的情况说明已经是完成了排序
	}
}
冒泡的时间复杂度为O(n 2),空间复杂度O(1)。稳定。


2,快速排序:

快速排序采用了分治算法策略,它是冒泡排序的一种改进。

基本思路是:把待排列的数据分为两个子列,从数列中挑出一个数作为基准,遍历其他数据,把小于它的放前面,大的放在基准的后面。之后,通过递归,将各个子序列划分为更小的序列,直到把小于基准值元素的子数列和大于基准值元素的字数列排序。

快速排序示意图:


它的示例代码如下:

#include <iostream>
using namespace std;

//拆分为两个子列
int Partition(int a[], int left, int right) {
	int base = a[left];
	while (left < right) {
		while (left < right && a[right]>base) //从右往左找出第一个比基准小的数据
			--right;
		a[left] = a[right]; //将这个数放到基准的左边
		while (left < right && a[left]<base) //从左往右找出第一个比基准大的数据
			++left;
		a[right] = a[left]; //放到右边
	}
	a[left] = base;
	return left; //返回基准的位置
}
void  QuickSort(int a[], int left, int right) {
	int i;
	if (left < right) {
		i = Partition(a, left, right);
		QuickSort(a, left, i-1);
		QuickSort(a, i+1, right);
	}
}
int main()
{
	int num[6] = {23,45,13,2,99,78};
	cout << "排序前:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << num[i] << " ";
	}
	cout << endl;
	QuickSort(num, 0, 5);
	cout << "排序后:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << num[i] << " ";
	}
	cout << endl;
	return 0;
}


快速排序时间复杂度为O(nlog2n),空间复杂度O(nlog2n),但是不稳定。快速排序平时用得比较多,比如algorithm里面的sort就是快速排序。

 #include <algorithm>
void sort( iterator start, iterator end );
void sort( iterator start, iterator end, StrictWeakOrdering cmp );

但是它不适合对链式结构进行排序。


选择排序:

基本思想就是每次遍历,选出键值最小的数据,依次放在已排序好的数列后面,直到全部记录排完为止。

1,直接选择排序:

对待排序的序列,选出关键字最小的数据,将它和第一个位置的数据交换,接着,选出关键字次小的数据,将它与第二个位置上的数据交换。以此类推,直到完成整个过程。

所以如果有n个数据,那个需要遍历n-1遍。其实现代码如下:

#include <iostream>
using namespace std;

void SelectSort(int a[], int n) {
	int i, j, small;
	for (i = 0; i < n-1; ++i) {
		small = i;
		for (j = i+1; j < n; ++j) {
			if (a[small] > a[j]) small = j;
		}
		if (small != i)
			swap(a[small], a[i]);
	}
}
int main()
{
	int num[6] = {23,45,13,2,99,78};
	cout << "排序前:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << num[i] << " ";
	}
	cout << endl;
	SelectSort(num, 6);
	cout << "排序后:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << num[i] << " ";
	}
	cout << endl;
	return 0;
}
直接选择排序的时间复杂度为O(n 2)。同样适合对链式结构进行排序,只不过需要修改实现代码。


2,树形选择排序:

树形选择排序也称为竞标赛排序,基本思想是:两两比较,选出第一个最小值,将原来的叶子节点设置为∞,进行同样的两两比较,既可选出第二个最小值,如此往复。缺点:占用的辅助存储空间较多,和“∞”进行多余的比较等。为了弥补这些缺点J.willioms提出了另一种形式的选择排序——堆排序。它的时间复杂度为O(nlog2n)。

  首先将待排序记录两两分组,将每组的最小值设置为他们的父结点,把所有这些筛选出来的父结点再两两分组,选出每组的最小值并设置为这组的父结点。一层一层筛选,直到选出根结点,就是最小值了,然后将其对应的叶结点设置为∞。

中心思想类似于锦标赛,先分组然后层层选拨,将每次选拨出来的根结点的值单独记录下来。然后把之前选拨出来的值,设置为∞(无穷大)。那么再次比较时,之前选拨出来的值已经变成无穷大了,自然会被忽略。但有一个问题就是每一次选拨都会重新进行比较。和“∞”进行的多余的比较太多,而且占用过多的辅助存储空间。借助树形选择排序的思想,理解堆排序就会更容易一些。

……

时间复杂度为O (n log2n),空间复杂度O(n)。是稳定排序 。


3,堆排序:

堆排序就是利用堆的特性排序,堆是一个二叉树,如果从上至下,从左至右按照节点的关键字大小顺序排列节点,那就是堆排序。堆排序的排序过程如下图所示:

 

其排序示例实现如下:

#include <iostream>

using namespace std;

//用数组二叉树
void HeapAdjust(int a[], int s, int n)//构成堆
{
	int j;
	while(2*s + 1 < n) //第s个结点有右子树 
	{
		j=2*s + 1 ;
		if((j+1) < n)
		{            
			if(a[j] < a[j+1])//右左子树小于右子树,则需要比较右子树
				j++; //序号增加1,指向右子树 
		}
		if(a[s] < a[j])//比较s与j为序号的数据
		{            
			swap(a[s], a[j]);           
			s = j ;//堆被破坏,需要重新调整
		}
		else //比较左右孩子均大则堆未破坏,不再需要调整
			break;
	}
}
void HeapSort(int a[],int n)//堆排序
{
	int t, i;
	int j;
	for(i = n/2 - 1; i >= 0; i--)    //将a[0,n-1]建成大根堆
		HeapAdjust(a, i, n);
	for(i = n-1; i > 0; i--)
	{
		swap(a[0], a[i]);
		HeapAdjust(a, 0, i);        //将a[0]至a[i]重新调整为堆
	}  
}
int main()
{
	int num[6] = {23,45,13,2,99,78};

	cout << "排序前:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << num[i] << " ";
	}
	cout << endl;

	HeapSort(num, 6);

	cout << "排序后:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << num[i] << " ";
	}
	cout << endl;
	return 0;
}
堆排序的时间复杂度为O(nlog 2n),空间复杂度为O(1)。不稳定。

插入排序:

插入排序基本思想,每次将一个待排序的数据按照其关键字的大小插入到前面已经排序好的数据中的适当位置,直到全部数据排序完成。

1,直接插入排序

直接插入排序是一种简单直观的排序算法,它的工作原理是通过建有序序列,对于没有排序的数据,在已排序序列中从后往前扫描,找到相应位置,并插入。故,如果是数组这样的连续空间的数据序列,那就每次插入都要将其位置的后面数据都向后移动。

其示例实现为:

#include <iostream>
using namespace std;

void InsertSort(int a[], int n) {
	int i, j, temp;
	for (i = 1; i < n; ++i) {
		temp = a[i]; //先保存当前值
		for (j = i-1; j >= 0 && temp < a[j]; --j) //从后往前移,直到找到适合位置
			a[j+1] = a[j]; //往后移一位,腾出位置
		a[j+1] = temp; //将值放入已找出的适当位置
	}
}
int main()
{
	int num[6] = {23,45,13,2,99,78};
	cout << "排序前:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << num[i] << " ";
	}
	cout << endl;
	InsertSort(num, 6);
	cout << "排序后:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << num[i] << " ";
	}
	cout << endl;
	return 0;
}

这样的直接插入排序空间复杂度O(1),但是时间复杂度为O(n 2)

还有几种对直接插入排序改进的几种排序:折半插入排序,2-路插入排序,表插入排序。


2,希尔排序

希尔排序(Shell Sort)是插入排序的一种。是针对直接插入排序算法的改进。该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。

它的具体做法是:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成(n除以d1)个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。

示意图:


具体示例代码:

#include <iostream>
using namespace std;

void ShellSort(int a[], int n) {
	int d, i, j, temp;
	d = n/2; //分成n/2组
	while (d >= 1) { 
		for (i = d; i < n; ++i) { //对每组进行直接插入排序
			temp = a[i];
			j = i - d;
			while (j >= 0 && a[j] > x) {
				a[j+d] = a[j];
				j -= d;
			}
			a[j+d] = temp;
		}
		d /= 2;
	}
}
int main()
{
	int num[6] = {23,45,13,2,99,78};
	cout << "排序前:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << num[i] << " ";
	}
	cout << endl;
	ShellSort()
	cout << "排序后:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << num[i] << " ";
	}
	cout << endl;
	return 0;
}

希尔排序时间复杂度为O(n 4/3),空间复杂度为O(1),但是不适合在链表结构上使用。


合并排序:

 合并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。合并排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。合并排序也叫归并排序。

其具体是的实现实例:

#include <iostream>
using namespace std;

//2-路合并
void Merge(int a[], int r[], int left, int mid, int n) {//a[]归并到r[]
	int s1 = left, s2 = mid+1, s3 = left;
	while (s1 <= mid && s2 <= n)//比较较小的数据填充到a[]
		if (a[s1] <= a[s2]) 
			r[s3++] = a[s1++];
		else 
			r[s3++] = a[s2++];
	while (s1 <= mid) r[s3++] = a[s1++];//将未填充的数补全
	while (s2 <= n) r[s3++] = a[s2++];//将未填充的数补全
}
//2-路归并排序
void MergePass(int a[], int r[], int n, int len) {
	int beg = 0, end;
	while (beg + len < n) {
		end = beg + 2*len - 1;
		if (end >= n)//最后一个可能少于len个
			end = n - 1;
		Merge(a, r, beg, beg+len-1, end);//合并
		beg = end + 1;//提供给下次开始
	}
	if (beg < n)
		while (beg < n) {
			r[beg] = a[beg];
			beg++;
		}
}
void MergeSort(int a[], int n) {
	int len = 1;//当前进行归并有序数列的长度
	int f = 0;
	int *p = (int *)malloc(sizeof(int)*n);
	while(len < n) {
		if (f) MergePass(p, a, n, len);//交替归并到p和a
		else MergePass(a, p, n, len);
		len *= 2;
		f = 1 - f;
	}
	if (f) //排序后是归并到p的情况,复制回到a
		for (f = 0; f < n; f++)
			a[f] = p[f];
	free(p);
}
int main()
{
	int num[6] = {23,45,13,2,99,78};
	cout << "排序前:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << num[i] << " ";
	}
	cout << endl;
	MergeSort(num, 6);
	cout << "排序后:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << num[i] << " ";
	}
	cout << endl;
	return 0;
}
2-路归并排序的时间复杂度为O(nlog 2n),空间复杂度为O(n)

分配排序:

1,桶排序:

假定:输入是由一个随机过程产生的[0, 1)区间上均匀分布的实数。将区间[0, 1)划分为n个大小相等的子区间(桶),每桶大小1/n:[0, 1/n), [1/n, 2/n), [2/n, 3/n),…,[k/n, (k+1)/n ),…将n个输入元素分配到这些桶中,对桶中元素进行排序,然后依次连接桶输入0 ≤A[1..n] <1辅助数组B[0..n-1]是一指针数组,指向桶(链表)。

他的排序过程如图:


桶排序只是用于关键字取值范围较小的情况,否则会因为所需箱子的数目太多而导致资源的浪费。这种排序的使用价值不大,他一般用于基数排序的一个中间过程。


2,基数排序:

基数排序是桶排序的一种改进和推广。它的基本思想是,先设立r个队列,队列编号分别为0~r-1,(r为关键字的基数),然后按照下面的规则对关键字进行“分配”和“收集”。

1, 按照最低有效位的值,把n个关键字分配到上述的r个队列里,然后从小到达将各队列中关键字收集起来。

2, 再按低次有效位的值把刚刚收集起来的关键字分配到r个队列中,重复收集工作。

3, 重复上述分配和收集工作,直到最高的有效位。(也就是说,如果数位为d,则需要重复进行d次。d由所有元素中最长的一个元素的位数计量。)

图示如下:


上图过程,就是先按个位分配然后收集,再按十位,百位分配和收集,最后就得出排序结果。

在C++中可以使用库函数lexicongraphical_compare()进行字典次序比较。

每一趟分配的时间是O(n),所以总时间的开销为O(d(n+r)) = O(n),通常d,r为常数。空间负复杂度为O(n+r)。基数排序使用于采用链式结构存储结构的排序。


3,计数排序:

 计数排序是一个类似于桶排序的排序算法,其优势是对已知数量范围的数组进行排序。它创建一个长度为这个数据范围的数组C,C中每个元素记录要排序数组中对应记录的出现个数。这个算法于1954年由 Harold H. Seward 提出。

计数排序算法的基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。例如,如果输入序列中只有17个元素的值小于x的值,则x可以直接存放在输出序列的第18个位置上。

#include <iostream>

using namespace std;
//n为个数,k为最大值,b为输出
void CountingSort(int a[], int b[], int n, int k)
{
	int* c = new int[k+1];
	memset(c, 0, (k+1) * sizeof(int));
 	for (int j = 0; j < n; j++) c[a[j]]++;//保存每个下标的值的个数
	for (int j = 1; j <= k; j++) c[j] += c[j - 1];//从前到后累计的位置
	for (int j = n - 1; j >= 0; j--)
	{
		b[c[a[j]] - 1] = a[j];
		c[a[j]]--;

	}
	delete []c;
}

int main()
{
	int num[6] = {23,45,13,2,99,78};
	int out[6];
	cout << "排序前:" << endl;
	int max = 0;
	for (int i = 0; i < 6; i++) {
		if (max < num[i]) max = num[i];
		cout << num[i] << " ";
	}
	cout << endl;

	CountingSort(num, out, 6, max);

	cout << "排序后:" << endl;
	for (int i = 0; i < 6; i++) {
		cout << out[i] << " ";
	}
	cout << endl;
	return 0;
}

可以看出计数排序很快,O(n)的时间复杂度,但是我们平时还是用的很少,是因为它的缺陷:需要一个至少等于待排序数组取值范围的缓冲区。



  • 7
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值