一、排序的概念及其运用
1.1排序的概念
排序的严格定义:假设含有 个记录的序列为{r1,r2. ... ... .rn}其相应的别为 ,其相应的关键字分别为{k1, k2, ……kn},需确定1,2,……n 的一种排列p1, p2, ……pn,使其相应的关键字满足 kpl<=kp2<=……<=kpn(非递减或者非递增)关系,即使得序列成为一个按关键字的序列(rp1, rp2,……rpn),这样的操作就称为排序。
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
啥是稳定性,稳定性的概念:
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序和外部排序的概念:
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
对于内排序来说,排序算法的性能主要是受三个方面影响:
1.时间性能:排序是数据处理中经常执行的一种操作,往往属于系统的核心部分,因此排序算法的时间开销是衡量其好坏的最重要的标志。在内排序中,主要进行两种操作:比较和移动 比较指关键字之间的比较,这是要做排序最起码的操作。 移动指记录从一个 位置移动到另一个位置,事实上,移动可以通过改变记录的存储方式来予以避免。
2.辅助空间:评价排序算法的另一个主要标准是执行算法所需要的辅助存储空间。辅助存储空 间是除了存放待排序所占用的存储空间之外,执行算法所需要的其他存储空间。
3.算法的复杂性:注意这里指的是算法本身的复杂度,而不是指算法的时间复杂度。显然算法过于复杂也会影响排序的性能。
1.2常见的排序算法
二、常见的排序算法及其实现
2.1冒泡排序
我想大家都对冒泡排序不陌生,毕竟这是许多初学者最开始学的一种排序算法,今天让我们剖析一些这个算法吧!
冒泡排序 Bubble Sort 一种交换排序,其的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
冒泡排序的代码:
void BubbleSort(int* a, int n) { for (int i = 0; i < n - 1; i++) { int exchange = 0;//定义一个变量判断是否发生了交换,若没有交换则直接break出去 for (int j = 0; j < n - i - 1; j++) { if (a[j] > a[j + 1]) { exchange = 1; Swap(&a[j], &a[j + 1]); } } if (exchange == 0)//说明没有交换过 { break; } } }
如果是用C语言写的代码建议大家写一个Swap函数和PrintfArray输出数组的函数
Swap和PrintfArray函数代码:
void Swap(int* pa, int* pb) { int tmp = *pa; *pa = *pb; *pb = tmp; } void PrintfArray(int* a, int n) { int i = 0; for (i = 0; i < n; i++) { printf("%d ", a[i]); } printf("\n"); }
2.2插入排序
2.2.1插入排序的基本思想
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
2.2.2插入排序的操作:
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与 array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移即可。
2.2.3直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定
插入排序代码:
void InsertSort(int* a, int n) { for (int i = 0; i < n - 1; i++) { //[0, end] end + 1 int end = i; //单趟排序; [0, end] 有序的。 end + 1位置的值,插入进入,保持他依旧有序 int tmp = a[end + 1]; while (end >= 0) { if (tmp < a[end]) { a[end + 1] = a[end];//先交换 --end;//往前 } else { break; } a[end + 1] = tmp;//tmp的值改到了之前的end位置 } } }
冒泡排序和插入排序对比:
2.3堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆 升序:建大堆
.降序:建小堆
2. 利用堆删除思想来进行排序 建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
书中的描述:
堆排序 (Heap Sort) 就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是, 将待排序的序列构造成一个大根堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值) .然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值。如此反复执行,便能得到一个有序序列了。
代码示例:(小根堆)
void AdjustDown(HPDataType* a, size_t size, size_t root) { size_t parent = root; size_t child = parent * 2 + 1;//默认是左孩子 while (child < size) { //1、选出左右孩子中小的那个,而且保证不会越界访问 if (child + 1 < size && a[child + 1] <a[child])//建大堆时< 改 > { ++child;//左孩子变为右孩子 } if (a[child] < a[parent])//建大堆时< 改 > { Swap(&a[child], &a[parent]); parent = child;//继续计算 child = parent * 2 + 1;//默认还是计算左孩子 } else { break; } } } void HeapSort2(int* a, int n) { //向上调整--建堆 //for (int i = 1; i < n; ++i) //{ // AdjustUp(a, i); //} //向下调整--建堆O(N) for (int i = (n - 1 - 1) / 2; i >= 0; --i) { AdjustDown(a, n, i);//为什么向下调整要多传一个参数,因为当child>=size 时说明已经到了边界 } size_t end = n - 1;//n - 1是最后一个数据的下标 while (end > 0) { Swap(&a[0], &a[end]); AdjustDown(a, end, 0); //次大的数到了倒数第二个位置 --end; } } int main() { // TestHeap(); int a[] = { 4 , 2, 7, 8, 5, 1, 0, 6 }; HeapSort(a, sizeof(a) / sizeof(int)); for (int i = 0; i < sizeof(a) / sizeof(int); i++) { printf("%d ", a[i]); } printf("\n"); return 0; }
2.4选择排序
2.4.1基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的 数据元素排完 。
2.4.2直接选择排序:
1、在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素
2、若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
3、在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素。
2.4.3直接选择排序的特性总结:
1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
选择排序与堆排序对比:
选择排序在每选出一个数需要遍历N次
堆排序建好堆后只需logN
所以堆排序的时间复杂度是o(n*logN),而选择排序是o(n^2);
选择排序的代码:
思路:每一次选出最大最小值,最大放最后,最小放后面,再left++,right--缩小区间
void SelectSort(int* a, int n) { int left = 0, right = n - 1; while (left < right) { int mini = left, maxi = left; for (int i = left + 1; i <= right; i++) { if (a[i] < a[mini]) { mini = i; } if (a[i] > a[maxi]) { maxi = i; } } Swap(&a[left], &a[mini]); //maxi可能与left重叠 if (left == maxi) { maxi = mini; } Swap(&a[right], &a[maxi]); left++; right--; } }
2.5希尔排序
希尔排序我们可以简单理解成是插入排序加强版,而且是史诗级加强。
2.5.1希尔排序的原理:
现在,我要讲解的算法叫希尔排序 (Shell Sort ,希尔排序是 DL.Shell 1959年提出来的一种排序算法,在这之前排序算法的时间复杂度基本都是 O(n^2)的,希尔排序算法是突破这个时间复杂度的第一批算法。
原理:
先选定一个整数,把待排序文件中所有记录分成个 组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工 作。当到达=1时,所有记录在统一组内排好序。
2.5.2希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就 会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的 希尔排序的时间复杂度都不固定。
希尔排序代码:
void ShellSort(int* a, int n) { //预排序 int gap = n; while (gap > 1) { gap = gap / 3 + 1; for (int i = 0; i < n - gap; i += gap)//为什么是到n - gap因为 //结束前最后一次执行的下标是n - gap - 1 { int end = i; int tmp = a[end + gap]; while (end >= 0) { if (tmp < a[end]) { a[end + gap] = a[end]; end -= gap; } else { break; } a[end + gap] = tmp; } } } }
三、排序算法复杂度及稳定性分析
3.1算法的空间特征
·需要使用堆栈或者递归实现:快排和归并排序
·常数空间复杂度:堆排序、希尔、插入、冒泡、选择
3.2算法与数据结构的关系
数据不敏感:归并排序、选择排序、堆排序
数据敏感:冒泡、快排、插入、希尔。
3.3算法的分类:
分治:归并和快排
基于插入:希尔、直接插入、选择
基于选择:堆排序、直接选择
3.4算法的稳定性:
稳定算法:归并、插入、冒泡
不稳定:堆排序、快排、希尔、选择
数据量为1万时插入、希尔、和冒泡算法的时间:
数据量为10万时插入、希尔、和冒泡算法的时间:
数据量为1000万时算法的时间(只比较堆排序和希尔排序)