数据结构基本排序算法


排序算法分为两大类,一类是 内部排序,是指待排序记录能够直接在内存中就能完成的排序;另一类是 外部排序,待排序记录数量很多,以致内存不能够一次读入全部的记录,在排序过程中需要对外存进行访问。

内部排序,包含插入排序,交换排序,选择排序,归并排序和基数排序,以下主要对这几种排序做一个介绍。

插入排序

直接插入排序

直接插入排序是将待排记录插入到一个有序序列中。比如一个有序序列为 a[0 … m],表示有m+1个元素,下标从 0 到 m。将 num 插入到该有序序列中,首先从后往前查找待排记录能够插入到有序序列中的位置,然后将序列往后移,再将待排记录插入到该位置。

void InsertSort (int arr[], int n)
{
	int i,j;
	for (i=1; i<n; i++) {
		if (arr[i] < arr[i-1]) {
			int tmp = arr[i];
			for (j=i-1; j>=0 && tmp<arr[j]) {
				arr[j+1] = arr[j];	//将arr元素后移
			}
			arr[j+1] = tmp;	//将待排记录插入j+1处,此处为待排记录插入的位置
		}
	}
}

直接插入排序,在最坏情况下,如果有序序列是非递减的,那么当每次待排记录都是最小值时,都需要比较全部关键字,时间复杂度是O(n^2);如果待排序列是有序的,那么每次只需要比较一次就可以,时间复杂度为O(n)。在待排记录不大,且基本有序的情况下,使用直接插入排序更为有效。

直接插入排序的平均时间复杂度为 O(n^2)

二分插入排序(折半插入)

二分插入排序,可以说是直接插入排序的优化版。直接插入排序,在每次查找待排记录在有序表中的插入位置时,是通过从后往前逐个比较的方式进行的(类似于顺序表的查询),如果将查找过程改为二分查找,能够明显较少比较的次数,但是,找到了待插入的位置,同样需要将有序表待插入的位置的所有元素往右移动,不能减少移动的次数。

void BInsertSort (int arr[], int n)
{
	int i, j;
	int low = 0, high = 0, mid = 0;
	for (i=1; i<n; i++) {
		if (arr[i] < arr[i-1]) {
			int tmp = arr[i];
			low = 0;
			high = i-1;
			while (low < high) {
				mid = (low + high) >> 1;	//去中间值
				if (tmp > arr[mid])	low = mid + 1;
				else
					high = mid - 1;
			}
			for (j=i-1; j>=high; j--) {
				arr[j+1] = arr[j];
			}
			arr[j + 1] = tmp;
		}
	}
}

折半插入排序的时间复杂度仍为 O(n^2)

希尔排序

希尔排序,又称为“缩小增量排序”,也是一种插入排序。它的算法思想是,先将整个待排记录序列分割成若干个子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对整个待排记录进行一次直接插入排序。

子序列不是简单的“逐段分割”,而是按照某个增量,将相邻的相差增量的记录组成一个子序列。加入增量为dk:

void ShellInsert (int arr[], int n, int dk)
{
	int i;
	for (i=dk; i<n; i++) {
		if (arr[i] < arr[i-dk]) {
			int tmp = arr[i];	//借助一个辅助空间
			
			for (j=i-dk; j>=0 && tmp<arr[j]; j-=dk) {
				arr[j+dk] = arr[j];
			}
			arr[j+dk] = tmp;
		}
	}
}

希尔排序的增量取法是一个比较复杂的问题,一般可取为

...9, 5, 3, 2, 1
...40, 13, 4, 1

增量序列中的值,除了1之外,不能有其他的公因子,也就是1就是他们的最大公约数,并且最后一个增量的值必须取值为1。

最后一个增量的值取值为1,此时经过前面的排序,待排序列已经基本有序,使用直接插入排序效果明显。

交换排序

我们通常所见到冒泡排序和快速排序都属于交换排序。

冒泡排序

冒泡排序的思想,是在n个元素的待排序列中,从前往后,每次与相邻元素比较,如果相邻元素大,就交换位置,如此,每一趟排序,都会有一个元素在正确的位置上。经过n次后,待排记录将变成了从小到大的有序表。

void BubbleSort (int arr[], int n)
{
	int length = n;
	int i,j ;
	int bcheck = 0;

	for (i=0; i<length; i++) {
		for (j=0; j<length-1-i; j++) {
			if (arr[j] > arr[j+1]) {
					Swap (arr[j], arr[j+1]);	//一趟排序后arr[i]就是最小的元素
					bcheck = 1;
			}
		}
		if(!bcheck)
			return;
	}
}

冒泡排序每一趟都需要跟未排好序的所有记录进行比较,时间复杂度为O(n^2)

快速排序

快速排序是一种非常常见的排序算法。快速排序的基本思想是分治和填坑。首先在待排序列中确定一个key,将待排序列中的所有元素与key进行比较,大于key的交换到右边,小于key的交换到左边。这样就将待排记录分成两部分,左半部分的所有元素均不大于key,右半部分的所有元素的值均不小于key。同时,再按照上述的方法,进行递归,就能将所有元素都能放到正确的位置。

void QuickSort (int arr[], int low, int high)
{
	int pivot = arr[low];
	int left = low;
	int right = high;

	while (low < high) {
		//将比pivot小的放在左边
		while (low<high && arr[high]>=pivot) high--;
		if (low < high) {
			arr[low++] = arr[high];
		}

		//将比pivot大的放在右边
		while (low<high && arr[low]<=pivot) low++;
		if (low < high) {
			arr[high--] = arr[low];
		}
	}
	arr[low] = pivot;

	QuickSort (arr, 0, low-1);
	QuickSort (arr, low+1, right);
}

快速排序的时间复杂度为O(nlogn)。比如将待排序列看成是一棵树,每次分割就像创建了左右子树,一直往下创建左右子树的过程,树的最大深度就是 logn。

在所有相同的时间复杂度的排序算法中,性能是最好的。但是,在待排序列基本有序的情况下,快速排序就相当于冒泡,时间复杂度为 O(n^2)

当待排记录数目较小,且基本有序时,使用快速排序效果不好,使用插入排序会更好,所以一般,会将插入排序和快速排序一起使用,以达到优化的目的,如下所示,在快速排序前面加上这么一段代码:

if ((high - low + 1) <10) {
	InsertSort (arr, (high-low+1));
	return;
}

当数组中有相同的元素较多时,上述的这种快速排序,会产生很多冗余,如果每次分割之后,对与key相同的元素,都聚集到key的左右,那么,就可以跳过这些元素,直接对左右部分剩下的元素继续快速排序,这用能够明显的减少迭代次数。

  1. 在划分过程中,左边的部分,将与key相同的元素,分布到最左边;右边的部分,将与key相同的部分,分布到最右边
  2. 划分完之后,将key相等的元素分布到key的两边
  3. 对左右两边不等于key的部分在进行快速排序
void QSort (int arr[], int low, int high)
{
	int first = low, last = high;
	int left = low, right = high;
	int i,j;

	if ((high - low + 1) < 10) {
		InsertSort (arr, (high - low + 1));
	}

	int pivot = arr[low];
	while (low < high) {
		while (low<high && arr[high]>=pivot) {
			if (arr[high] == pivot) {	//将与pivot相等的元素交换到最右边
				Swap (arr[high], arr[right]);
				right--;
			}
			high--;
		}
		if (low < high) {
			arr[low++] = arr[high];
		}

		while (low<high && arr[low]<=pivot) {
			if (arr[low] == pivot) {	//将左边相等的pivot聚集到最左边
				Swap (arr[low], arr[left]);
				left++;
			}
			low++;
		}
		if (low < high) {
			arr[high--] = arr[low];
		}
	}
	arr[low] = pivot;

	//将左边最左边的与pivot相等的元素聚集到pivot的左边
	i = low - 1;
	j = first;
	while (j<left && arr[j]==pivot) {
		Swap (arr[i], arr[j]);
		j++;
		i--;
	}
	letf = j;	//左边去除相同的pivot后的位置
	i = low + 1;
	j = last;
	while (j>right && arr[j]==pivot) {
		Swap (arr[i], arr[j]);
		i++;
		j--;
	}
	right = i;	//右边去除相同的pivot后的位置
	
	QSort (arr, first, left);
	QSort (arr, right, last);
}

参考博文 三种快速排序及优化 (http://blog.csdn.net/insistgogo/article/details/7785038)

选择排序

简单选择排序和堆排序都属于选择排序算法。

简单选择排序

简单选择排序的思想,就是在待排序列中,每次选择一个最小或者最大的元素,放在正确的位置上,这样,当n次排序后,所有的元素都按照大小顺序放在了正确的位置上。

void SelectSort (int arr[], int n)
{
	int i;
	int min = 0;
	for (i=0; i<n; i++) {
		min = SelectMin(arr, i, n);	//从arr中的i 到 n中选取最小的值
		if (i != min) {
			Swap (arr[i], arr[min]);
		}
	}
}

简单选择排序的时间复杂度为 O(n^2)

简单选择排序算法思想很简单,每次选出一个最小(最大)值,需要比较n-1次,然后将该值放在正确的位置,要优化该算法,主要是要能够减少比较次数。下面介绍另一种选择排序算法 —— 堆排序。

堆排序

堆的定义如下:

n个元素的序列 {k1, k2, k3 …, kn},当且仅当满足下列关系时,称之为堆:

  ki <= k2i

  ki <= k2i+1



  ki >= k2i

  ki >= k2i+1

其中,i = 1, 2, …, 不大于n/2的最大整数。

以一维数组作为待排序列的存储结构,将此序列看成是一棵完全二叉树,根据堆的定义可知,此完全二叉树的非终端节点不大于(不小于)它的左右孩子节点。这种堆称之为小顶堆(大顶堆)。

n个记录的待排序列中,将此序列看成是完全二叉树,那么最后一个非终端阶段为i<=n/2 的最大整数,对 i 做堆调整,将arr[i]与左右孩子比较,arr[2*i]arr[2*i+1],如果arr[i]比左右孩子大,那么将左右孩子中的最小的那个与arr[i]交换,使之成为小顶堆。然后继续调整i-1的那个非终端节点,直到整棵完全二叉树都成为小顶堆。

调整完之后,输出根节点元素的值,即为最小元素,然后将最后一个元素代替根节点,进行堆调整,调整完之后,继续输出根节点的值,即得到第二个最小元素,在将剩余的最后元素放到根节点的位置,一直重复上述动作,直到得到所有节点,这就是堆排序。 (在输出堆顶的最小值之后,使剩下的n-1个元素重新建成一个小顶堆,又可以得到n个元素中的次小值。)

void HeapAdjust (int arr[], int s, int m) //创建小顶堆
{
	//在arr[s .. m]中,除了arr[s]外,其余都符合小顶堆,需要对arr[s]进行调整
	int rs = arr[s];
	int i;

	for (i=2*s; i<=m; i*=2) {
		if (i<m && arr[i]>arr[i+1])	//判断哪一个孩子更小
			i++;
		
		if (arr[i] > rs)	break;
		arr[s] = arr[i];
		s = i;
	}
	arr[s] = rs;
}

//数组下标需要从1开始,否则,2i将不是i的左孩子
void HeapSort (int arr[], int n)
{
	int length = n;
	int i;

	// 建立小顶堆
	for (i=length>>1; i>=1; i--) {
		HeapAdjust (arr, i, length);
	}

	//堆排序,数字下标从1开始,当i为1时表示的是最后一个节点,不需要调整
	for (i=length; i>1; i--) {
		Swap (arr[1], arr[i]);

		HeapAdjust (arr, 1, i-1);
	}
}

堆排序的方法对记录数较少的文件不值得提倡,但是对待排序记录数大的情况是很有效果的。它的主要运行时间是建立初始堆和不断调整堆。

在一个深度为k的堆中,筛选算法进行关键字比较次数,假如每一层都需要比较左右孩子,那么至多比较2(k-1)次。

堆排序的时间复杂度为 O(nlogn),最坏情况下的时间复杂度仍为 O(nlogn),但是相同条件下,快速排序最坏情况下的时间复杂度为 O(n^2)

归并排序

归并排序是将两个或者两个以上的有序表组合成一个新的有序表。无论是顺序表,还是链表,都可以在 O(m+n) 的时间量级上实现。

2-路归并排序试讲一维数组中前后相邻的两个有序序列合并成一个有序序列。其算法为:

void Merge (int a[], int b[], int s, m, t)
{
	//将a[s .. m] 和 a[m+1 .. t]合并成 b[s .. t]
	int i = s;
	int j = m + 1;
	int k = s;

	for (i=s, j=m+1; i<=m && j<=t; k++) {
		if (a[i] > a[j]) {
			b[k] = a[j++];
		} else {
			b[k] = a[i++];
		}
	}

	while (i <= m) {
		b[k++] = a[i++];
	}

	while (j <= t) {
		b[k++] = a[j++];
	}
}

void MSort (int a[], int b[], int s, int t)
{
	//将 a[s .. t] 归并为 b[s .. t]
	int m = (s + t) >> 1;

	if (s == t) b[s] = a[s];
	else {
		MSort (a, c, s, m);
		MSort (a, c, m+1, t);
		Merge (c, b, s, m, t);
	}
}

void MergeSort (int a[], int n)
{
	MSort (a, a, 0, n);
}

归并算法的时间复杂度为 O(nlogn),空间复杂度为 O(n)

当文件中的待排记录数很大时,内存不足以一次性全部读取,排序,这个时候,可以将文件划分成若干个小文件,然后对每一个小文件利用前面所讲的那些排序方法,比如快速排序、堆排序等排好序之后在写入文件中,将每一个小文件都排好序之后,再将这个文件进行归并。

假如每一个小文件都是按照从小到达的顺序排列,取每一个小文件的第一个记录,同时也是每一个有序的小文件中的最小记录,进行比较,可通过堆排序,获取最小值,然后将该值写入到最后归并的文件中,同时,再从取得的最小值的小文件中读取下一个记录,重复 上述动作。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫步旅人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值