1 排序基本概念与分类
1.1 排序的稳定性
假设ki = kj,且在排序前的序列中r i领先于rj 。
如果排序后r i仍然领先于rj,则称所用的排序方法是稳定的。反之,如果使得排序后的序列中rj领先于r i,则称所用的排序方法是不稳定的。
1.2 内排序和外排序
内排序: 内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。
外排序: 外排序由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次蒋欢数据才能进行
这里主要介绍内排序的多种方法.
对于内排序来说,排序算法的性能主要是受3个方面影响
- 时间性能
- 辅助空间
- 算法复杂性
1.3 时间复杂图和空间复杂度
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | In-place | 稳定 |
选择排序 | O(n2) | O(n2) | O(n2) | O(1) | In-place | 不稳定 |
插入排序 | O(n2) | O(n) | O(n2) | O(1) | In-place | 稳定 |
希尔排序 | O(n log n) | O(n log2 n) | O(n log2 n) | O(1) | In-place | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | Out-place | 稳定 |
快速排序 | O(n log n) | O(n log n) | O(n2) | O(log n) | In-place | 不稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | In-place | 不稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | Out-place | 稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | Out-place | 稳定 |
桶排序 | O(n+k) | O(n+k) | O(n2) | O(n+k) | Out-place | 稳定 |
- 时间复杂度
- 冒泡、选择、直接排序需要两个for循环,每次只关注一个元素,平均时间复杂度为O(n2)(一遍找元素O(n),一遍找位置O(n))
- 希尔、归并、快速、堆基于二分思想,log以2为底,平均时间复杂度为O(n log n)(一遍找元素O(n),一遍找位置O(logn))
- 稳定性记忆:“快希选堆”(快牺牲稳定性)
2 排序详解
运行环境代码:
**
//默认从小到大排序
#include <stdio.h>
#include <windows.h>
#define MAX 20 //基数排序
#define BASE 10//基数排序
void bubble_sort(int *arr, int len); //冒泡排序
void select_sort(int *arr, int len); //选择排序
void insertion_sort(int *arr, int len); //插入排序
void shell_sort(int *arr, int len); //希尔排序
void merge_sort_iteration(int *arr, int len); //归并排序(迭代)
void merge_sort_recursive(int *arr, int reg[], int start, int end);//归并排序(递归)
void quick_sort_recursive(int *arr, int start, int end); //快速排序(递归)
void quick_sort(int *arr, int len); //快速排序封装
void heap_sort(int *arr, int len); //堆排序
void max_heapify(int *arr, int start, int end); //堆调整
void radix_sort(int *arr, int len); //基数数排
void count_sort(int *arr, int reg[], int len); //计数排序
//交换
void swap(int *x, int *y)
{
int t = *x;
*x = *y;
*y = t;
}
int main()
{
int arr[] = {22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70};
int len = (int)sizeof(arr)/sizeof(*arr);
int *reg = (int *)malloc(len * sizeof(int));
//merge_sort_recursive(arr, reg, 0, len - 1);
radix_sort(arr, len);
//count_sort(arr, reg, len);
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
system("pause");
return 0;
}
2.1 冒泡排序
冒泡排序:一种交换排序,两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止
从小到大: 一开始交换的区间为0~ N-1,将第1个数和第2个数进行比较,前面大于后面,交换两个数,否则不交换。再比较第2个数和第3个数,前面大于后面,交换两个数否则不交换。依次进行,最大的数会放在数组最后的位置。然后将范围变为0~N-2,数组第二大的数会放在数组倒数第二的位置。依次进行整个交换过程,最后范围只剩一个数时数组即为有序。
void bubble_sort(int *arr, int len) //冒泡排序
{
int i, j;
for (i = 0; i < len - 1; i++) {
for (j = 0; j < len - 1 - i; j--) {
if (arr[j] < arr[j+1]) {
swap(arr[j], arr[j+1]);
}
}
}
return 0;
}
2.2 选择排序
选择排序:通过n-i次关键字之间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i个记录交换之
从小到大: 一开始从0~ n-1区间上选择一个最小值,将其放在位置0上,然后在1~n-1范围上选取最小值放在位置1上。重复过程直到剩下最后一个元素,数组即为有序。
void select_sort(int *arr, int len) //选择排序
{
int i, j;
for(i = 0; i < len - 1; i++) {
for(j = i + 1; j < len; j++) {
if (arr[j] < arr[i]) {
swap(arr[j], arr[i]);
}
}
}
return 0;
}
2.3 插入排序
插入排序:直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
从小到大: 首先位置1上的数和位置0上的数进行比较,如果位置1上的数小于位置0上的数,将位置0上的数向后移一位,将1插入到0位置,否则不处理。位置k上的数和之前的数依次进行比较,如果位置K上的数更小,将之前的数向后移位,最后将位置k上的数插入不满足条件点,反之不处理。
void insertion_sort(int *arr, int len) //插入排序
{
int i, j, temp;
for (i = 1; i < len; i++) {
temp = arr[i]
for (j = i; j > 0; j--) {
if (arr[j - 1] > temp) {
arr[j] = arr[j - 1];
}
else {
break;
}
}
arr[j] = temp;
}
}
2.4 希尔排序
希尔排序:希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
思路分析:希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
基本步骤:在此选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。
具体流程:
void shell_sort(int *arr, int len) //希尔排序
{
int i, j, temp;
int gap = len/2; //初始分组数量为len/2;同一组两数据之间的间隔
while (gap > 0) {
for (i = gap; i < len; i++) {
temp = arr[i];
for (j = i - gap; j >= 0; j -= gap) {
if (arr[j] > temp) {
arr[j + gap] = arr[j];
}
else {
break;
}
}
arr[j + gap] = temp;
}
gap /= 2;
}
}
2.5 归并排序
归并排序:利用归并的思想实现的排序方法。假设初始序列含有n个有序的子序列,每个子序列的长度为1,然后两两合并,得到 ⌈n/2⌉(⌈x⌉表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,……,如此重复,直到得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序
具体流程:
归并排序(迭代):
void merge_sort_iteration(int *arr, int len) //归并排序(迭代)
{
int *a = arr;
int *b = (int *)malloc(len *sizeof(int)); //申请额外空间
int seg;//分组大小(长度)
int start;//开始位
//分组最后每组长度为1,即 seg = 1; 每循环一次,每组长度*2;
for (seg = 1; seg < len; seg += seg) {
//开始两两合并排序
for (start = 0; start < len; start += seg + seg) {
int low = start, mid = min(start + seg, len),
high = min(start + seg + seg, len);
int k = low;
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
while (start1 < end1 && start2 < end2) {
b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
}
while (start1 < end1) {
b[k++] = a[start1++];
}
while (start2 < end2) {
b[k++] = a[start2++];
}
}
int *temp = a;
a = b;
b = temp;
}
if (a != arr) {
int i;
for (i = 0; i < len; i++) {
b[i] = a[i];
}
b = a;
}
free(b);
}
归并排序(递归):
//初始(数组下标)start = 0; end = len - 1;
//reg:额外空间
void merge_sort_recursive(int *arr, int reg[], int start, int end)//归并排序(递归)
{
if (start >= end) {
return;
}
int len = end - start, mid = (len >> 1) + start;
int start1 = start, end1 = mid;
int start2 = mid + 1, end2 = end;
merge_sort_recursive(arr, reg, start1, end1); //先处理左半边
merge_sort_recursive(arr, reg, start2, end2); //后处理右半边
int k = start;
while (start1 <= end1 && start2 <= end2) {
reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
}
while (start1 <= end1) {
reg[k++] = arr[start1++];
}
while (start2 <= end2) {
reg[k++] = arr[start2++];
}
for (k = start; k <= end; k++) {
arr[k] = reg[k];
}
}
2.6 快速排序
快速排序:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的
快速排序(递归):
void quick_sort_recursive(int *arr, int start, int end)//快速排序
{
//递归过程先写结束条件
if (start >= end) {
return;
}
int mid = arr[end];
int left = start, right = end - 1;
//最后一个数据为关键字,将小于关键字的放在前半部分,大于关键字的放在后半部分
while (left < right) {
while (arr[left] < mid && left < right) {
left++;
}
while (arr[right] >= mid && left < right) {
right--;
}
swap(&arr[left], &arr[right]); //交换两个大小相反的数据
}
//存放关键字的位置,放在左右两边交界
if (arr[left] >= arr[end]) {
swap(&arr[left], &arr[end]);
} else {
left++;
}
//如果左边还没排完,左边继续排序
if (left) {
quick_sort_recursive(arr, start, left -1);//左排序
}
quick_sort_recursive(arr, left + 1, end); //右排序
}
void quick_sort(int *arr, int len) //快速排序封装
{
quick_sort_recursive(arr, 0, len - 1);
}
2.7 堆排序(待补充)
堆(大顶堆)排序:
- 将待排序的序列构造成一个大顶堆,此时整个序列的最大值就是堆顶的根结点。
- 将根结点(最大值)与堆数组的末尾元素交换,此时末尾元素就是最大值,然后将剩余的n-1个序列重新构成> 一个堆,这样就得到n个元素的次大值。反复执行得到有序序列。
堆:堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
void heap_sort(int *arr, int len) //堆排序
{
int i;
// 初始化,i从最后一个父节点开始调整
for (i = len / 2 - 1; i >= 0; i--)
max_heapify(arr, i, len - 1);
// 先將第一个元素和已排好元素前一位做交换,再重新调整,直到排序完毕
for (i = len - 1; i > 0; i--) {
swap(&arr[0], &arr[i]);
max_heapify(arr, 0, i - 1);
}
}
void max_heapify(int *arr, int start, int end) //堆调整
{
int dad = start; //父节点
//子节点,链表的顺序存储模式,
//若arr[0]为根结点,son = dad * 2 + 1,则他的孩子结点为arr[1]和arr[2]
//若arr[1]为根结点,son = dad * 2,则他的孩子结点为arr[2]和arr[3]
int son = dad * 2 + 1;
//子节点在范围内执行循环
while (son <= end) {
//两个子节点选择最大的
if (son + 1 <= end && arr[son] < arr[son + 1])
son++;
//如果父节点大于子节点代表调整完毕,直接跳出函数
if(arr[dad] > arr[son]) {
return;
}
//否则交换父子内容再继续子节点和孙结点比较
else {
swap(&arr[dad], &arr[son]);
dad = son;
son = dad * 2 + 1;
}
}
}
2.8 基数排序
基数排序: 基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
void radix_sort(int *arr, int len)
{
int i, b[MAX], m = arr[0], exp = 1;
for (i = 1; i < len; i++) {
if (arr[i] > m) {
m = arr[i];
}
}
while (m / exp > 0) {
int bucket[BASE] = { 0 };
for (i = 0; i < len; i++) {
bucket[(arr[i] / exp) % BASE]++;
}
for (i = 1; i < BASE; i++) {
bucket[i] += bucket[i - 1];
}
for (i = len - 1; i >= 0; i--) {
b[--bucket[(arr[i] / exp) % BASE]] = arr[i];
}
for (i = 0; i < len; i++) {
arr[i] = b[i];
}
exp *= BASE;
}
}
2.9计数排序
计数排序:计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
基本步骤:
-
找出待排序的数组中最大和最小的元素
-
统计数组中每个值为i的元素出现的次数,存入数组C的第i项
-
对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
-
反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
void count_sort(int *arr, int reg[], int len)
{
int *count_arr = (int *) malloc(sizeof(int) * 100);
int i, j, k;
for (k = 0; k < 100; k++)
count_arr[k] = 0;
for (i = 0; i < len; i++)
count_arr[arr[i]]++;
for (k = 1; k < 100; k++)
count_arr[k] += count_arr[k - 1];
for (j = len; j > 0; j--)
reg[--count_arr[arr[j - 1]]] = arr[j - 1];
for (j = len; j > 0; j--)
arr[j - 1] = reg[j - 1];
free(count_arr);
}
2.10桶排序
桶排序:工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法)或是以递归方式继续使用桶排序进行排序)。
元素分布在桶中:
然后,元素在每个桶中排序: