文章目录
0.说明
以下排序算法,传入n值均为数组个数,不是数组尾元素下标。此文略长,可自己选择喜欢排序方法进行学习,也可逐一学习。不过要有点耐心哦!
1.排序概念
排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
2.常见排序算法
下面排序主代码如下:
void main()
{
int a[] = { 7 ,3 ,1 ,8, 9, 12, 4, 25, 1, 11 };
int n = sizeof(a) / sizeof(a[0]);
//InsertSort(a, n);
//ShellSort(a, n);
//SelectSort(a, 0, n - 1);
//printfArr(a, n);
//MergeSort(a, n);
//CountSort(a, n);
//QuickSort(a, 0, n - 1);
//DigHoleSort(a, 0, n - 1);
QuickSortNonR(a, 0, n - 1);
printfArr(a, n);
return 0;
}
1. 插入排序
<1> 直接插入排序
(1)思想
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
(2) 画图理解
(3)代码思路
写代码时先写单层循环,把数组前n-1个数据想成有序拿数组尾部数据进行插入,单层循环控制没问题后,在写多层循环。
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
//单趟
int end = i;
//temp为插入值
int temp = a[end + 1];
while (end >= 0)
{
if (a[end] > temp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] =temp;
}
}
(4)特性
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
<2> 希尔排序
(1) 思想
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有记录分成个组,间隔一个整数gap的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。逐渐所小距离gap,当gap=1时(为插入排序),所有记录在统一组内排好序。
(2)画图理解
(3) 代码思路
这里整体思路和插入排序一样,只不过多了一个gap用来调整间距进行预排序,而gap的值也要不断进行缩小直至为1也就相当与一次插入排序。
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
//这里的3可以是任意的奇数,根据数据量来调整
gap = (gap / 3) + 1;
//通过图解我们可以看到,i取值应为[0,n - gap);
for (int i = 0; i < n - gap; i++)
{
int end = i;
int temp = a[end + gap];
while (end >= 0)
{
if (a[end] > temp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = temp;
}
}
}
(4)特性
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果
- 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N1.3—N2)
- 稳定性:不稳定
2. 选择排序
<1> 选择排序
(1)思想
每一次从待排序的数据元素中选出最小(或最大)位置的下标,将最小值存放在序列的起始位置,最大值放在序列的末尾位置。直到全部待排序的数据元素排完 。
(2)画图理解
(3) 代码思路
这里思路比较简单,每回选出最大值和最小值位置下标和队尾队头数据进行进行交换就可以了,要注意的是交换后,要对left和right进行处理。
void SelectSort(int* a, int left, int right)
{
while (left < right)
{
int MaxSub = left, MinSub = left;
//找出最大值和最小值下标
for (int i = left; i <= right; i++)
{
if (a[MaxSub] < a[i])
MaxSub = i;
if (a[MinSub] > a[i])
MinSub = i;
}
//将最大值和最小值分别放到队尾和队头上去
swap(&a[MinSub], &a[left]);
//这里存在一种情况,当left位置是最大值时,最大值会被放到最小值下标位置
if (MaxSub == left)
MaxSub = MinSub;
swap(&a[MaxSub], &a[right]);
left++;
right--;
}
}
(4)特性
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
<2> 堆排序
(1)参考文章: 堆排序
(2)特性
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
3. 交换排序
<1> 冒泡排序
(1)参考文章: 冒泡排序
(2)特性
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
<2> 快速排序
(1)参考文章: 快速排序
4.归并排序
(1)思想
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序(借助新数组,来进行存储有序数据,完成后在拷贝回原组)。若将两个有序表合并成一个有序表,称为二路归并。
(2)画图理解
方法一:递归实现
归并过程参考下图
方法二:非递归实现
(3)代码思路
方法一:递归实现
把一个大区间划分成两个小区间,让两个小区间有序,两个大区间一 一 比较,利用新空间暂时保存排序,排序完成之后拷贝回去原地址空间。递归实现主要是要考虑好返回条件,接着就是确保单趟程序没问题。
void _MergeSort1(int* a, int* temp, int left, int right)
{
if (left >= right)
{
return;
}
int mid = (left + right) >> 1; //(left + right)/2
int begain1 = left, end1 = mid;
int begain2 = mid + 1, end2 = right;
_MergeSort1(a, temp, begain1, end1);
_MergeSort1(a, temp, begain2, end2);
//排序
int i = left;
while (begain1 <= end1 && begain2 <= end2)
{
if (a[begain1] < a[begain2])
temp[i++] = a[begain1++];
else
temp[i++] = a[begain2++];
}
while(begain1 <= end1)
temp[i++] = a[begain1++];
while(begain2 <= end2)
temp[i++] = a[begain2++];
//拷贝回原数组
for (int j = left; j <= right; j++)
{
a[j] = temp[j];
}
}
void MergeSort(int* a, int n)
{
//开辟新数组
int* temp = (int*)malloc(sizeof(int) * n);
if (temp == NULL)
{
perror("MergeSort::malloc");
exit(-1);
}
_MergeSort1(a, temp, 0, n- 1);
free(temp);
}
方法二:非递归实现
主要思想,控制两段区间数据,进行比较排序(利用中间数组暂存)先11排序,在22排序,44排序…直至有序。区间逐渐放大。这里我们可以定义一个gap用来控制距离,因为传入的是两段数据故gap每轮循环翻一倍,
void _MergeSort2(int* a, int* temp, int begain1, int end1, int begain2, int end2)
{
//排序
int i = begain1;
int j = begain1;
while (begain1 <= end1 && begain2 <= end2)
{
if (a[begain1] < a[begain2])
temp[i++] = a[begain1++];
else
temp[i++] = a[begain2++];
}
while (begain1 <= end1)
temp[i++] = a[begain1++];
while (begain2 <= end2)
temp[i++] = a[begain2++];
//拷贝回原数组
for (; j <= end2; j++)
{
a[j] = temp[j];
}
}
void MergeSort(int* a, int n)
{
int* temp = (int*)malloc(sizeof(int) * n);
if (temp == NULL)
{
perror("MergeSort::malloc");
exit(-1);
}
//非递归
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n ; i += 2*gap)
{
//计算每回传入两段区间的位置
int begain1 = i, end1 = i + gap - 1;
int begain2 = i + gap, end2 = i + gap * 2 - 1;
//第二段区间有可能会越界,这里需要处理一下
if (begain2 >= n)
break;
if (end2 >= n)
end2 = n - 1;
_MergeSort2(a, temp, begain1, end1, begain2, end2);
}
gap *= 2;
}
free(temp);
}
(4)特性
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
5.计数排序
(1)思想
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。操作步骤:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
(2)画图理解
(3)代码思路
这里思路很简单,就是把数组中的元素放到另一数组下标位置上去来计数。不过要考虑的数临时数组开辟多少个合适呢最大值个吗?看一个如下数组77, 88, 99, 100,数组只有四个如果开辟100个空间的话难免会造成大量空间浪费,故我们开辟最大值减最小值加一个比较合适。
void CountSort(int* a, int n)
{
//选出最大值最小值
int Max = a[0];
int Min = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] > Max)
Max = a[i];
if (a[i] < Min)
Min = a[i];
}
//开辟最大值减最小值+1个空间
int temp_n = Max - Min + 1;
int* temp = (int*)malloc(sizeof(int) * temp_n);
if (temp == NULL)
{
perror("CountSort::malloc");
exit(-1);
}
memset(temp, 0, temp_n * sizeof(int));
//统计每个数的个数放到temp数组
for (int i = 0; i < n; i++)
{
temp[a[i] - Min]++;
}
int i = 0;
//还原回去原数组
for (int j = 0; j < Max - Min + 1; j++)
{
while (temp[j] > 0)
{
a[i++] = j + Min;
temp[j]--;
}
}
free(temp);
}
(4)特性
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
- 稳定性:稳定