数据结构学习笔记---010
- 数据结构之排序算法
- 1、排序的基本概念及其运用
- 1.1、常见排序算法的实现
- 2、插入排序的实现
- 2.1、直接插入排序
- 2.1.1、直接插入排序的实现
- 2.1.1.1、直接插入排序InsertSort.h
- 2.1.1.2、直接插入排序InsertSort.c
- 2.1.1.3、直接插入排序main.c
- 2.1.2、直接插入排序的小结
- 2.2、希尔排序
- 2.2.1、希尔排序的实现
- 2.2.1.1、希尔排序ShellSort.h
- 2.2.1.2、希尔排序ShellSort.c
- 2.2.1.3、希尔排序main.c
- 2.2.2、希尔排序的小结
- 3、选择排序的实现
- 3.1、直接选择排序
- 3.1.1、直接选择排序的实现
- 3.1.1.1、直接选择排序SelectSort.h
- 3.1.1.2、直接选择排序SelectSort.c
- 3.1.1.3、直接选择排序main.c
- 3.1.2、直接选择排序的小结
- 3.2、堆排序
- 3.2.1、堆排序的实现
- 3.2.1.1、堆排序HeapSort.h
- 3.2.1.2、堆排序HeapSort.c
- 3.2.1.3、堆排序main.c
- 3.2.2、堆排序的小结
- 4、交换排序的实现
- 4.1、冒泡排序
- 4.1.1、冒泡排序的实现
- 4.1.1.1、冒泡排序BubbleSort.h
- 4.1.1.2、冒泡排序BubbleSort.c
- 4.1.1.3、冒泡排序main.c
- 4.1.2、冒泡排序的小结
- 4.2、快速排序
- 4.2.1、快速排序的实现
- 4.2.1.1、快速排序QuickSort.h
- 4.2.1.2、快速排序QuickSort.c
- 4.2.1.3、快速排序main.c
- 4.2.2、快速排序的小结
- 5、计数和归并排序的实现
- 5.1、计数排序
- 5.1.1、计数排序的实现
- 5.1.1.1、计数排序CountSort.h
- 5.1.1.2、计数排序CountSort.c
- 5.1.1.3、计数排序main.c
- 5.1.2、计数排序的小结
- 5.2、归并排序
- 5.2.1、归并排序的实现
- 5.2.1.1、归并排序MergeSort.h
- 5.2.1.2、归并排序MergeSort.c
- 5.2.1.3、归并排序main.h
- 5.2.2、归并排序的小结
- 6、排序算法复杂度及稳定性分析
- 6.1、归并排序AllSort.h
- 6.2、归并排序AllSort.c
- 6.3、归并排序main.c
- 7、排序算法总结
数据结构之排序算法
前言:
前篇学习了 数据结构的二叉树的知识,那么这篇继续学习关于数据结构的排序算法知识。
/知识点汇总/
1、排序的基本概念及其运用
记录:在排序问题中,通常将数据元素称为记录。
排序:所谓的排序,就是使一串记录,按照其中某个关键字的大小,递增或递减的排序起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i] = t[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍然在r[j]之前,则称这种排序算法是稳定的。否则称为不稳定的。
正序、逆序:若待排序的记录序列已排好序,称此纪录序列为正序;若待排序列记录的排序顺序与排好序的顺序正好相反,称此纪录序列为逆序或反序。
趟:在排序过程中,将待排序的记录序列扫描一遍称为一趟。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
运用:
生活中各种排序的需求场景。 比如,学校成绩排名,物品的销量排名等。
1.1、常见排序算法的实现
常见排序算法的实现分类:
1.插入排序:直接插入排序,希尔排序
2.选择排序:选择排序,堆排序
3.交换排序:冒泡排序,快速排序
4.计数和归并排序:计数排序、归并排序
2、插入排序的实现
插入排序:已知一组数据,插入数据使其逐次有序。
插入排序是一种简单的插入排序法其基本思想:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
2.1、直接插入排序
直接插入排序是指插入排序中最简单的排序方法,类似于玩纸牌时整理手中纸牌的过程,其基本思想是依次将待排序序列中的每一个记录插入已知排好的序列中,直到全部都排好序。
存在以下两个关键性问题:
1.如何构造初识的有序序列?
2.如何查找待插入的有序序列?
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移
2.1.1、直接插入排序的实现
直接插入排序的实现:普遍采用类似于分治的思想,先控制好单趟排序。先局部再整体。
声明和定义分离:
直接插入排序InsertSort.h
直接插入排序InsertSort.c
直接插入排序main.c
2.1.1.1、直接插入排序InsertSort.h
放置头文件,函数声明,注释信息。后面的头文件同理,不多赘述。
#include <stdio.h>
//直接插入排序
void InsertSort(int* a, int n);
//打印数据元素
void PrintArray(int* a, int n);
2.1.1.2、直接插入排序InsertSort.c
根据分治思想,所以先思考编写单趟的思路,直接插入的算法思想就是将一个新元素,在已经有序的序列中,去进行比较再插入的操作。但是我们一开始并不知道要插入的序列是否有序,那么不妨假设 [0,end] 闭区间为有序的序列,继续将end+1作为要插入的新元素。所以,不管我们一开始的初识序列是否有序,能确定的是只有第一个元素那么肯定有序,然后再根据假设的区间进行比较插入操作,再后移插入之后的元素即可,所以单趟的思路就是如此。
简述核心就是,将end+1的元素与end的元素比较,大就直接插入在end后面,有序序列范围就是[0,end+1];否则,小就将end右移,范围[0,end].单趟的比较插入就是这样。多趟就是在前面的基础上,逐步n次(n为问题的规模)循环即可。
为了方便理解,画出单趟的图形演示,如下图所示:
边界考虑:当end+1下标的新元素,比有序序列的首元素都小,那么就得考虑边界问题,避免非法访问等问题。
单趟插入代码实现:
//单趟
void InsertSortByOnce(int* a, int len)
{//假设有序序列:[0,end]//新元素下标:end+1int end;int tmp = a[end + 1];//保存新元素值至变量tmp//边界while (end >= 0){//比较end下标的元素的大小if (tmp < a[end]){a[end + 1] = a[end];//后移--end;//更新继续比较的下标}else //tmp >= a[end]{break;}}//尾插,同时解决end移到了 end = -1的情况三。a[end + 1] = tmp;
}
多趟插入代码实现:
多趟只需要把单趟执行循环衔接起来即可。end的初始值与序列的数据个数相关,所以end = i;其次,多趟循环的边界,因为下标从0开始那么应该是在n-1结束,防止越界,又因为参数是传入的数组长度。
#include "InsertSort.h"//直接插入排序 -- O(N^2)
void InsertSort(int* a, int n)
{for (int i = 0; i < n-1; i++)//结束位置n-1,满足防止越界{int end = i;int tmp = a[end + 1];while (end >= 0){if (tmp < a[end]){a[end + 1] = a[end];--end;}else{break;}}//同时解决end移到了 end = -1的情况。a[end + 1] = tmp;}
}//打印数据元素
void PrintArray(int* a, int n)
{for (int i = 0; i < n; i++){printf("%d ", a[i]);}printf("\n");
}
2.1.1.3、直接插入排序main.c
#include "InsertSort.h"int main()
{int arr[] = { 3,1,4,6,2,8,5,0,7,10 ,6};InsertSort(arr, sizeof(arr) / sizeof(arr[0]));PrintArray(arr, sizeof(arr) / sizeof(arr[0]));return 0;
}
测试运行结果:
2.1.2、直接插入排序的小结
1.元素集合越接近有序,直接插入排序算法的实践效率越高;
2.时间复杂度为:O(n^2) 最好情况是:O(N) --> 默认顺序恰好有序
3.稳定性:稳定
2.2、希尔排序
希尔排序就是对直接插入排序的改进。希尔排序通过引入“增量”概念,将整个待排序的记录序列分割成若干子序列分别进行直接插入排序,从而减少数据移动的次数,提高排序效率。
基本思想:先确定一个增量gap,是将整个待排序记录序列分割为若干个gap长度的子序列,在子序列内分别进行直接进行插入排序,重复上述分组和排序的工作,待整个序列基本有序,gap=1时,再对全部记录进行一次直接插入排序。
存在两个关键性问题;
1.应如何分割待排序记录,才能保证整个序列逐步向基本有序发展?
2.子序列内如何进行直接插入排序?
2.2.1、希尔排序的实现
希尔排序的实现:普遍采用于分治的思想,先控制好单趟排序。即先局部再整体。
声明和定义分离:
直接插入排序ShellSort.h
直接插入排序ShellSort.c
直接插入排序main.c
两个步骤:
步骤1.预排序(使接近有序) – 引入gap增量划分为若干组,再分别每组进行插入排序;
gap取得越大,待排序序列中大的值更快调到后面,小的值更快调到前面,但越不接近于有序;
gap取得越小,调得越慢,但是越接近有序;
当gap == 1 时就是直接插入排序了。
步骤2.预排序后,最后统一进行,直接插入排序。
那么如何决定gap的大小呢?
首先肯定不是定值,也可以发现gap常常随着问题规模而变化;
所以总结出来一般有两种方法:关键在于始终要保证gap最后都能化为1。
根据数的奇偶性:gap /= 2(gap>1) 或者gap /= 3 + 1
2.2.1.1、希尔排序ShellSort.h
#include <stdio.h>
//希尔排序
void ShellSort(int* a, int n);
//打印数据元素
void PrintArray(int* a, int n);
2.2.1.2、希尔排序ShellSort.c
根据分治思想,首先得思考单趟的排序处理,而且希尔的主要核心思想就是上述的两个步骤,即根据gap执行预排序,当gap=1时,再执行直接插入排序。
那么单趟的思路:首先确定一个gap值(这里以gap=3为演示),然后将待排序的序列按照gap为间距进行分组,再然后将每组分别进行直接插入排序(注意:并没有改变每组元素各个下标,只在各自组内下标位置间进行插入排序),最后在根据组数进行边界考虑,分析是否越界的情况。边界恰好是每组越界前的元素下标是每组的最后一个元素,所以得出结论,边界控制为n-gap
为了方便理解,画出单趟的图形演示,如下图所示:
单趟希尔代码实现:
//单趟思路实现
void ShellSortByOnce(int* a, int n)
{//单趟for (int j = 0; j < gap; j++)//gap组{//第一组,i=0//i = j,j巧妙决定了为第几组for (int i = j; i < n - gap; i += gap) //相当于全局替1为--> gap{int end = i;int tmp = a[end + gap];//每组各个元素间距gapwhile (end >= 0){if (tmp < a[end]){a[end + gap] = a[end];end -= gap;}else{break;}}a[end + gap] = tmp;}}
}
明白了单趟的思路,那么多趟就是循环嵌套,其中值得注意的是,这里单趟只是为了方便演示数据gap取的定值3,但是实际应用中,gap是非定值,所以关于gap具体怎么取?以及gap取多少合适?
书上说,关键在于始终要保证gap最后都能化为1。
所以,提供两种常用的方法:根据数的奇偶性:gap /= 2(gap>1) 或者gap /= 3 + 1
多趟希尔代码实现:
//希尔排序
void ShellSort(int* a, int n)
{//int gap = 3;int gap = n;while (gap > 1)//直至gap等于1.执行最后一次直接插入排序,完成排序{//gap = gap / 2;gap = gap / 3 + 1;//单趟for (int j = 0; j < gap; j++)//gap组{//第一组//i = j,j巧妙决定了为第几组for (int i = j; i < n - gap; i += gap) //相当于全局替1为--> gap{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;}}}
}
当然还有更加新奇的实现思路,巧妙地运用各组各元素和gap的关系,实现多组并排反复横跳进行比较交换数据的操作,具体可以看我下面画的演示图:
多组并排希尔代码实现:
//希尔排序
//写法2:优化算法 -- 多组并排
void ShellSort(int* a, int n)
{//int gap = 3;int gap = n;while (gap > 1){//gap = gap / 2;gap = gap / 3 + 1;for (int i = 0; i < n - gap; i++) //i++决定了依次反复切换不同组进行插入排序{//单趟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;}}
}
//打印数据元素
void PrintArray(int* a, int n)
{for (int i = 0; i < n; i++){printf("%d ", a[i]);}printf("\n");
}
可知,多组并排与各个组排的关键区别就在于 i++ 和 i += gap。
2.2.1.3、希尔排序main.c
#include "ShellSort.h"int main()
{int arr[] = { 3,1,4,6,2,8,5,0,7,10 ,6};ShellSort(arr, sizeof(arr) / sizeof(arr[0]));PrintArray(arr, sizeof(arr) / sizeof(arr[0]));return 0;
}
测试运行结果:
2.2.2、希尔排序的小结
希尔排序的特性总结:
1.希尔排序就是对直接插入排序的改进。
2.当增量gap>1时,都是预排序,目的是让数组更接近于有序;当gap == 1时,数组已经接近于有序的了,这样就会很快。这样整体而言,可以达到优化效果。
3.希尔排序的时间复杂度不好确定(通过大量数据测试大约处于O(N^1.3),因为根据实际情况的增量并不相同,所以根据书上说的,希尔提出的最早方法是:d1= [n/2] d(i+1) = [di/2],且增量序列互质,显然最后一个增量必须是1。
4.稳定性:所以也就得出该排序算法并不稳定。
3、选择排序的实现
选择排序基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在起始位置,直到全部待排序的数据元素排完。
3.1、直接选择排序
直接选择排序(简单选择排序)是选择排序中最简单的排序方法;
其基本思想:是从第i趟排序在待排序r[i]~r[n]中选取最小或最大的数据元素;若它不是这组元素中的最后一个(或第一个)元素,则将它与这组元素中的最后一个(第一个)元素进行交换;并在剩余的数组集合中,重复直到剩余最后一个元素。
存在两个关键性问题:
1.如何在待排序中选出最小的记录?
2.如何确定待排序序列中的最小记录在有序序列中的位置?
3.1.1、直接选择排序的实现
直接插入排序的实现:根据直接选择的思想,得要明确对最大值或最小值的处理,以及比较和交换。
声明和定义分离:
直接选择排序SelectSort.h
直接选择排序SelectSort.c
直接选择排序main.c
3.1.1.1、直接选择排序SelectSort.h
#include <stdio.h>//直接选择排序
void SelectSort(int* a, int n);
//打印数据元素
void PrintArray(int* a, int n);
3.1.1.2、直接选择排序SelectSort.c
首先,知道选择排序的思想,直接选择,最大值或最小值,然后分别放在最左边的位置或最右边的位置,那么既然如此就设计一个区间,以分治的思想一个区间一个区间的,完成每一个区间的最小值和最大值,再分别放入当前区间的最小位置和最大位置。依次类推完成排序即可。
为了方便理解,画出图形大致演示一下,如下图所示:
直接选择排序代码实现
#include "SelectSort.h"//直接选择排序 最好的情况:O(N^2)
static void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
void SelectSort(int* a, int n)
{int begin = 0;int end = n - 1;while (begin < end){int mini = begin;int maxi = begin;for (int i = begin + 1; i <= end; i++){if (a[i] < a[mini]){mini = i;}if (a[i] > a[maxi]){maxi = i;}}Swap(&a[begin], &a[mini]);if (maxi == begin){maxi = mini;}Swap(&a[end], &a[maxi]);++begin;--