浅谈六种基本排序方法 ——插入排序,选择排序,交换排序,希尔排序,堆排序及快排

作为一个学生党,写博客主要为了归纳自己所学的知识,加深对知识的理解以及方便以后复习,回顾。如果文中有错误,还希望各位大佬能够在评论中指正。

以下所给出的排序算法均为升序,所有的排序思想都摘抄于百度百科。

- 直接插入排序

插入排序的基本思想:每步将一个待排序的记录,按值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。
例如:
图片来自于百度百科

void insertSort(int *arr, int count) {		//int *arr:传入一个整形数组; int count : 数字总个数。
	int first;		//待排序记录
	int i;
	int j;
	int t;
	for(i = 1; i < count; i++) { 		//外层循环中,i位当前待排序记录的下标,i之前的记录为已排序记录。
		first = arr[i];
		for(j = 0; j < i && arr[j] <= first; j++) {	 //j从零开始,遍历已排序记录
		}
		for(t = i; t > j; t--) { 	//将j及其之后所有已排序记录后移一位,为插入做好准备。
		arr[t] = arr[t - 1];
		}
		arr[t] = first;		//插入待排序记录。
	}	
}

直接插入排序是一种稳定排序,时间复杂度为O(n^2)。直接插入排序的算法思想较为简单,代码实现也比较容易,在此不做多余的赘述。

- 选择排序

选择排序的基本思想:选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法。

第一次看到选择排序的基本思想时,对于“存放在序列的起始位置”这步操作感到十分不理解。在看到下面给出的例子之后,才发现这步应该是一次交换。具体操作应该是:遍历整个未排序序列,取出其中的最小值,然后将其最小值与未排序序列的第一个元素交换。

例如:
原始数据 ()(4 9 5 2 8 7 0 1)
第一次 (0)(9 5 2 8 7 4 1)
第二次 (0 1)(5 2 8 7 4 9)
第三次 (0 1 2)(5 8 7 4 9)
… … … …
最后一次 (0 1 2 4 5 7 8)(9)

上述例子每次都取待排序序列中的最小值放入已排序序列的末尾,所以最后一个数据便是所有数据中的最大值不需要进行选择,直接放入已排序序列的末尾即可。

代码如下:

void choiceSort(int *arr, int count) {	
	int i;
	int j;
	int tmp;
	int minIndex;		//记录最小值的下标
	for(i = 0; i < count -1; i++) {		//示例中解释过,对于最后一个数据可以不进行操作,因此,循环的结束条件为:i < count -1。
		for(minIndex = j = i; j < count; j++) {		//此层循环从未排序序列的第一个数据开始,取出其最小值下标,为交换做好准备。
			if(arr[minIndex] > arr[j]) {
			minIndex = j;	
		}
	}
		if(minIndex != i) {		//如果最小值下标与当前未排序序列的最小下标相同,则不需要进行交换。
			tmp = arr[minIndex];
			arr[minIndex] = arr[i];
			arr[i] = tmp;	
		}
	}
}

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

- 交换排序

交换排序的基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
交换排序也可以称为冒泡排序。每一趟排序结果是将为排序序列的最大值(或最小值)放到未排序序列的末尾。不断地缩小数据区间,完成排序。
例如:
原始序列 (4 9 5 2 8 7 0 1)
第一躺 (4 5 2 8 7 0 1)(9)
第二趟 (4 2 5 7 0 1)(8 9)
第三趟 (2 4 5 0 1)(7 8 9)
… … … …
最后一趟 (0)(1 2 4 5 7 8 9)

从上述例子以及选择排序所给出的解释中可以得出,对于交换排序,最后一个数据也可以不进行操作。

对于交换排序,还可以对其算法进行进一步的改良。如果某一趟循环中没有进行数据的交换操作,那么就可以认为序列已经完全顺序,不必再进行接下来的比较,直接结束循环即可。

代码如下:

void swapSort(int *arr, int count) {
	int i;
 	int j;
	int tmp;
	boolean swap = TRUE;		//通过swap来实现提前结束循环的目的。
	for(i = 0; swap && i < count - 1; i++) {		//最后一个数据已经是最小值或最大值,没有再进行操作的必要。
		swap = FALSE;
		for(j = i + 1; j < count; j++) {
			if(arr[i] > arr[j]) {
			tmp = arr[i];
			arr[i] = arr[j];
			arr[j] = tmp;
			swap = TRUE;
			}
		}
	}
}

交换排序是一种稳定排序,时间复杂度未O(n^2)。

- 希尔排序

希尔排序的基本思想:希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

希尔增量:希尔排序Shellsort是指希尔提出了一种冲破二次时间屏障的算法,希尔增量是希尔排序中希尔给出的增量序列ht = N / 2, h[k] = h[k+1] / 2,即{N/2, (N / 2)/2, …, 1}。

例如:
图片来自于百度百科
上图增量序列的取值依次为5、2、1。

希尔排序的时间复杂度会受到增量的影响。
下面所给出的例子中会用的Hibbard增量。对数据总数进行计算,得到小于该数字的最大质数。将该质数除以2即位为增量序列的最大增量。

希尔排序是对插入排序的优化,因此,对于上述插入排序的代码进行改进用来实现希尔排序的内部操作。
希尔排序每趟排序的分组由希尔增量决定。各组的第一个数据的下标从零开始,依次加1,到本次增量减1结束。通过第一个数据的下标以及希尔增量和数据总数即可确定该组的成员,然后对该组进行插入排序。
对于插入排序的改进,需要传入分组第一个数据的下标,希尔增量,数据总数来确定每组的数据。对于每个分组的排序操作,除了循环的增量为希尔增量外,其他操作均与插入排序相同。

代码实现如下:

static void shellInsertSort(int *arr, int count, int step, int start) {		//传入数组,总个数,希尔增量以及分组第一个数据的下标,对该分组进行插入排序。
	int first;
	int i;
	int j;
	int t;
	for(i = start + step; i < count; i += step) {		//循环步长为希尔增量。
		first = arr[i];
		for(j = start; j < i && arr[j] <= first; j += step) {
		}
		for(t = i; t > j; t -= step) {
			arr[t] = arr[t - step];
		}
		arr[t] = first;
	}	
}
void ShellSort(int *arr, int count) {
	int step = count;
	int start;
	step |= step >> 1;
	step |= step >> 2;
	step |= step >> 4;
	step |= step >> 8;
	step |= step >> 16;		//上述或等运算的结果为取一个小于数据总量的最大质数,这里不做过多解释。
	for(step = step >> 1; step > 0; step >>= 1) {		//此循环控制希尔排序的增量序列
		for(start = 0; start < step ; start++) {		//此循环控制每趟排序各组第一个数据的下标
		shellInsertSort(arr, count, step , start);
		}
	}
}

希尔排序是非稳定排序,时间复杂度随增量不同会产生变化。

- 堆排序

堆排序的基本思想:堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。

想要理解堆排序,首先需要对二叉树有一定的了解。并且能够认识到一维数组可以看作一棵完全二叉树。

二叉树的几个基本性质
最后一个非叶子节点下标:n/2 - 1;
父节点的下标为:i,其左孩子节点下标为2i + 1;右孩子节点下标为2i + 2。

例如:
在这里插入图片描述
此图所给出的调整过程有所省略。调整的完整操作参考第一幅图以及adjustHeap函数。

上图给出了堆排序的不完整过程,大根堆的调整操作是所有操作的核心。
在第一幅图中,第三次调整时破坏了下面已经调整好的内容,因此在编写此部分代码时需要考虑到这种情况,并进行处理。
第二幅图中可以看出,总数据个数为1时,进行最后一次交换,即可结束;此时并不需要再进行调整。为了使代码更加简洁,在形成大根堆时可以不对根节点进行调整,将其并入第二幅图的操作中。

代码如下:

static void adjustHeap(int *arr, int count, int root) {		//完成大根堆的调整
	int leftChild;
	int rightChild;
	int maxIndex;
	int tmp;
	while(root <= count / 2 - 1) {		//while循环中处理了调整时破坏了下层内容的情况。
		leftChild = 2 * root + 1;
		rightChild = leftChild + 1;
		maxIndex = (rightChild >= count ? leftChild :
		(arr[leftChild] > arr[rightChild] ? leftChild : rightChild));
		maxIndex = (arr[root] > arr[maxIndex] ? root : maxIndex);
	if(maxIndex == root) {		//如果maxIndex == root,则不需要交换,也不会对下层内容产生影响。
		return;
	}
	tmp = arr[root];
	arr[root] = arr[maxIndex];
	arr[maxIndex] = tmp;
	root = maxIndex;
	}
}
void heapSort(int *arr, int count) {
	int root;
	int tmp;
	for(root = count / 2 - 1; root > 0; root--) {		//此层循环不对根节点进行调整,将其放入形成升序数组的循环中。
		adjustHeap(arr, count, root);
	}
	for(; count > 0; count--) {		//此层循环用来形成升序数组
		adjustHeap(arr, count, 0);
		tmp = arr[0];
		arr[0] = arr[count - 1];
		arr[count - 1] = tmp;
	}
}

堆排序是非稳定排序,时间复杂度为O(nlogn)。

- 快排

快排的基本思想:快速排序(Quicksort)是对冒泡排序的一种改进。
快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

例如:
首先用tmp保存数组第一个元素的值,分别用start,end来记录开始位置,结束位置。将tmp作为哨兵使用,根据快排的基本思想,将整个数组分为两部分。

继续进行手工过程可以发现,对于快排,需要用到递归来实现。

运用递归必须先考虑递归的结束条件,当初始条件为start >= end,意味着对于下标从start到end的分组完成操作,此次结束。

每一趟排序进行完毕后,需要返回start以此作为下一次分组的依据。

代码如下:

static int onceQuickSort(int start, int end, int *arr) {		//对从start到end分组的数据完成一次快排,并返回其结束位置的下标,作为下一次分组的依据。
	int tmp = arr[start];
	while(start < end) {		//在本循环中,因为对start、end的值会做出更改,因此,每次都需要比较它们的大小关系。
		while(start < end && arr[end] >= tmp) {
			end--;
		}
		if(start < end) {
			arr[start] = arr[end];
			start++;
		}
		while(start < end && arr[start] <= tmp) {
			start++;
		}
		if(start < end) {
			arr[end] = arr[start];
			end--;
		}
	}
	arr[start] = tmp;

	return start;
}
static void innerQuickSort(int start, int end, int *arr) {
	int middle;
	if(start >= end) {		//此处也可以写为start == end,这里写为"start >= end"可以防止初始值输入错误造成程序崩溃的现象。
		return;
	}
	middle = onceQuickSort(start, end, arr);
	innerQuickSort(start, middle - 1, arr);
	innerQuickSort(middle + 1, end, arr);
}
void quickSort(int *arr, int count) {		//这么做的意义在于与上面五种排序进行统一,以后可以用指向函数的指针有选择的调用想要使用的排序方法。
innerQuickSort(0, count - 1, arr);
}

小结

就排序的速度而言,那么快排几乎是最佳选择。但是快排也有一定的局限性,对于完全顺序或者完全逆序且数据量十分庞大的数据,在递归的过程中会存在系统堆栈溢出的风险。
如果数据已经基本有序,插入排序或者希尔排序只需要进行比较以及少量的插入操作即可完成,速度会有大幅度提升,且不会出现快排在面对几乎完全顺序的数据是可能出现的系统堆栈溢出的风险。
选择排序和堆排序相对稳定,没有最优情况和最差情况的区分。

熬了三个通宵,总算是完成了。如果文中有错误或者不足,还请各位大佬在评论中指正。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值