前言
数据结构中有常见的八种排序:直接插入排序、希尔排序、选择排序、堆排序(上篇博客已经介绍了)、冒泡排序、归并排序、计数排序。
排序简单来说就是将有限集合中的数值排成递增或递减的操作。
下面就一起来看看这八大排序吧!
一、直接插入排序
1.1 思路
- 将数组的第一个元素看作有序序列,将它后面的一个元素插入有序序列中,那插入完后,它们就构成新的有序序列,再对新的有序序列插入它后面的一个元素…。这么依次插入到数组的最后一个元素。此时这是数组就已经有序了。
1.2 图解
1.3 实现
代码:
//交换函数
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//
void InsertSort(int* a, int sz)
{
for (int i = 0;i <sz-1;i++)
{
//单趟排序
// [0,end],tmp
//将tmp插入[0,end]区间。
int end=i;
int tmp = a[end+1];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else
break;
}
a[end + 1] = tmp;
}
}
1. 4 时间复杂度和稳定性
1.4.1 时间复杂度
- 当数组是递减的数组但要排递增数组时,或数组是递增时要排递减。那么时间复杂度是最坏的情况:O(n^2)
a. for循环第一次,while循环要走一次。for循环往后走,whille循环次数依次增加1。for循环最后一次,while循环要走n-2次,所以循环的次数就是一个等差数列的求和。最高次项就是n^2, - 当数组已经有序时,就是最好情况O(n)。
a. for循环走n次,while也是走n次。所以循环的次数就是2n次
1.4.2 稳定性
先来简单介绍一下排序的稳定性。当数组中有相同的数值时。排完序它们之间的前后顺序不会被改变。
直接插入排序是稳定的排序。
二、希尔排序
2.1 思路
- 定义一个变量gap,将数组元素之间间隔为gap的分为一组。此时可以将数组看作许多个小组,这些小组对应的元素之间间隔都是gap。
- 对每个小组之间进行直接插入排序。
- 排完序之后,希尔排序的第一步预排序就完成了。
- 再对预排完的数组进行一个直接插入排序,数组就有序。
- 最后一个问题就是gap,一般gap都是变化的。常见是这样变化的:gap/=2或gap=gap/3+1。
2.2 图解
2.3 实现
void ShellSort(int* a, int sz)
{
//1.预排序 当gap>1时,就是预排序
//2.直接插入排序 当gap==1时,就是直接插入排序
int gap = sz;
while (gap > 0)
{
gap /= 2;
for (int i = 0;i < sz- gap;i++)
{
int end = i;
int tmp = a[i+gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
}
2.4 时间复杂度和稳定性
2.4.1 时间复杂度
- 希尔排序的时间复杂度很难分析。我们暂时只需要知道它是O(log(N)*N)这个量级的就行了。
2.4.2 稳定性
如果数组是这个顺序:{9,6,8,6,7}。当gap=3时。
9和第二个6分为一组。当排升序时,第二个6就会调整到第一个6的前。前后顺序就被破坏了。
因此希尔排序是不稳定的。
三、选择排序
3.1 思路
- 遍历一遍数组,取出最大和最小数值。
- 将最小的放到左边,最大放在右边。
- 再遍历最小和最大值之间的元素。再选取较小和较大值…
3.2 图解
3. 3 实现
//时间复杂度:最好和最坏都是:O(n^2) 稳定性:不稳定
void SelectSort(int* a, int sz)
{
int left = 0, right = sz - 1;
while (left < right)
{
//单趟
int mini = left, maxi =left;
//遍历找最小和最大值的下标
for (int i = left;i <= right;i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[left], &a[mini]);
//如果maxi和left重合,maxi就会被交换到mini位置,一定要修正maxi的位置
if (left == maxi)
{
maxi = mini;
}
Swap(&a[right], &a[maxi]);
left++;
right--;
}
}
3.4 时间复杂度和稳定性
3.4.1 时间复杂度
- 最坏:O(n^2)
- 最好:O(n^2)
3.4.2 稳定性
当数组是这个顺序:{9,9,5,3,2,1}。
第一次选择,第一个9就会被交换到第二个9的后面。
因此,选择排序是不稳定的。
四、冒泡排序
4.1 思路
- 两个数依次比较交换,单趟结束后,最大数值就会到数组的尾部。
- 排完最大的,在排次大的。一直拍到最小值。
- 数组就有序了。
4.2 图解
4.3 实现
void BubbleSort(int* a, int sz)
{
bool exchange = false;
for (int j=0;j<sz-1;j++)
{
//单趟
for (int i = 1;i <= sz - 1;i++)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
exchange = true;
}
}
//如果单趟遍历后没有发生交换,说明数组已经有序。
if (exchange == false)
break;
}
}
4.4 时间复杂度和稳定性
4.4.1 时间复杂度
- 最坏:O(n^2),当数组是要排序的逆序是最坏的情况。
- 最好:O(n),数组已经有序。
4.4.2 稳定性
冒泡排序是稳定的。
五、快速排序
4.1 递归实现思路
- 选出一个关键字keyi,将它调整到应的数组顺序。
- 将数组按照排好的关键字为界限,看作新的两个数组。
- 递归新的两个数组,再重新找keyi调整。
- 当所有的数字在应到的位置,数组也有序了。
4.1.1 hoare法调整keyi
4.1.2 挖坑法调整
4.1.3 前后下标法调整
4.2 实现
4.2.1 hoare法
//hoare法
int PartSort1(int* a, int left, int right)
{
//三数取中
int midi = GetMidNum(a, left, right);
if (left != midi)
Swap(&a[left], &a[midi]);
int keyi = left;
while (left < right)
{
//右边找小
while (left < right && a[keyi] < a[right])
right--;
//左边找大
while (left < right && a[keyi] > a[left])
left++;
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[right]);
keyi = left;
return keyi;
}
void QuickSort(int* a, int left,int right)
{
if (left > right)
return;
int keyi = PartSort1(a, left, right);
//[left,keyi-1]keyi[keyi+1,right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
4.2.2 挖坑法
//挖坑法
int PartSort2(int* a, int left, int right)
{
//三数取中
int midi = GetMidNum(a, left, right);
if (left != midi)
Swap(&a[left], &a[midi]);
int keyi = left;
int key = a[keyi];
int hole = keyi;
while (left < right)
{
//右边找较小
while (left < right && a[right] > key)
right--;
a[hole] = a[right];
hole = right;
while (left < right && a[left] < key)
left++;
a[hole] = a[left];
hole = left;
}
a[hole] = key;
keyi = hole;
return keyi;
}
void QuickSort(int* a, int left,int right)
{
if (left > right)
return;
int keyi = PartSort2(a, left, right);
//[left,keyi-1]keyi[keyi+1,right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
4.2.3 前后下标法
//前后下标法
int PartSort3(int* a, int left, int right)
{
//三数取中
int midi = GetMidNum(a, left, right);
if (left != midi)
Swap(&a[left], &a[midi]);
int keyi = left;
int prev = left;
int cur = left;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
void QuickSort(int* a, int left,int right)
{
if (left > right)
return;
int keyi = PartSort3(a, left, right);
//[left,keyi-1]keyi[keyi+1,right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
4.3 非递归实现思路
4.3 实现
void QuickSortNorn(int* a, int left, int right)
{
ST st;
STInit(&st);
STPush(&st, right);
STPush(&st, left);
while (!STEmpty(&st))
{
//出区间
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
//排序
int keyi = PartSort1(a, begin, end);
//入两个新区间
if (keyi + 1 < end)
{
STPush(&st, end);
STPush(&st, keyi+1);
}
if (begin < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, begin);
}
}
STDestroy(&st);
}
4.4 时间复杂度和稳定性
4.4.1 时间复杂度
O(N*logN)
4.4.1 稳定性
快速排序是不稳定的。
六、归并排序
归并其实就是合并,将每有序的子区间合并成一个有序的区间就是归并排序的主要思路了。
6.1 递归实现的思路
6.2 代码实现(递归)
//将要排序的数值拷贝到tmp
void _MergeSort(int* a, int begin, int end,int* tmp)
{
int midi = (begin + end) / 2;
if (begin >= end)
return;
_MergeSort(a, begin, midi, tmp);
_MergeSort(a, midi+1, end, tmp);
int begin1 = begin, end1 = midi;
int begin2 = midi + 1, end2 = end;
int cur = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[cur++] = a[begin1++];
}
else
{
tmp[cur++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[cur++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[cur++] = a[begin2++];
}
//拷贝的起始位置不一定是数组的开头,要加begin,归并开始的位置。
memcpy(a+ begin, tmp+ begin, sizeof(int)*(end - begin +1));
}
//归并排序:非递归
void MergeSort(int* a, int sz)
{
int* tmp = (int*)malloc(sizeof(int) * sz);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
_MergeSort(a, 0, sz - 1,tmp);
free(tmp);
}
6.3 非递归思路
6.4 代码实现(非递归)
void MergeSortNorn(int* a, int sz)
{
int* tmp = (int*)malloc(sizeof(int) * sz);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
int gap = 1;
while (gap <=sz)
{
for (int i = 0;i <sz;i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + (2 * gap) - 1;
int cur = begin1;
归并一次拷贝到tmp一次的条件
//if (end1 >= sz || begin2 >=sz)
//{
// break;
//}
//if (end2 >= sz)
//{
// end2 = sz - 1;
//}
//一把拷贝的条件
if (end1 >= sz)
{
end1 = sz - 1;
begin2 = sz;
end2 = sz - 1;
}
else if (begin2 >= sz)
{
end2 = sz - 1;
}
else if (end2 >= sz)
{
end2 = sz - 1;
}
//printf("[%d,%d][%d,%d] ", begin1, end1, begin2, end2);
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[cur++] = a[begin1++];
}
else
{
tmp[cur++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[cur++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[cur++] = a[begin2++];
}
//归并一次拷贝到tmp一次
//memcpy(a, tmp, sizeof(int)*(end2 - i+1));
}
//一把拷贝
memcpy(a, tmp, sizeof(int) * sz);
//printf("\n");
gap *= 2;
}
free(tmp);
}
6.5 时间复杂度和空间复杂度
时间复杂度:O(N*log N)
空间复杂度:O(N)
6.6 稳定性
归并排序是稳定的。
七、计数排序
7.1 思路
7.2 代码
void CountSort(int* a, int sz)
{
//选出最大和最小值
int max = a[0], min = a[0];
for (int i = 1;i < sz;i++)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] < min)
{
min = a[i];
}
}
int range = max - min + 1;
int* countA = (int*)calloc(range,sizeof(int));
if (countA == NULL)
{
perror("calloc fail\n");
return;
}
//统计
for (int i = 0;i < sz;i++)
{
countA[a[i] - min]++;
}
//覆盖排序
int cur = 0;
for (int i = 0;i < range;i++)
{
while (countA[i]--)
{
a[cur++] = i+min;
}
}
}
7.3 计数排序的特点和时间复杂度
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(N+range)。range表示原数组范围
- 空间复杂度:O(N)
总结
以上就是数据结构中主要的排序了,还有一个堆排序已经在上篇博客中介绍了。每种排序都有特定的用途,要根据实际情况的挑选使用。
直接插入排序:数组越接近有序,时间复杂度就越高。它是稳定的排序。
希尔排序:它是对直接插入排序的优化。但它不是稳定的排序
它两都属于插入排序。
选择排序:它不管数组是否已经有序,它的时间复杂度是固定的:O(N^2)。它是不稳定的排序。
堆排序:它首先要在数组中建堆,然后交换堆顶和堆尾的数据再作调整。推荐用向下调整法建堆,时间复杂度更低。它是不稳定的排序。
它两属于选择排序。
冒泡排序:冒泡排序有个优化,当一趟比较下来,发现过程中没有交换。此时数组就已经有序。它也稳定的排序。
快速排序:它的关键是选出一个关键字,让这个关键字排到应在的位置上。它是不稳定的排序。
它两属于交换排序
最后一个是归并排序。
归并的条件是让要归并的区间已经有序。它是稳定的排序。