这里写目录标题
- <font color="#FF00FF">排序
- <font color="#FF00FF">插⼊排序
- [<font color="#FF00FF">1. 直接插入排序](#FF00FF">1. 直接插入排序)
- [<font color="#FF00FF">2. 希尔排序](#FF00FF">2. 希尔排序)
- <font color="#FF00FF">希尔排序的时间复杂度计算
- <font color="#FF00FF">直接选择排序
- <font color="#FF00FF">堆排序
- <font color="#FF00FF">冒泡排序
- <font color="#FF00FF">快速排序
- <font color="#FF00FF">⾮递归版本
- <font color="#FF00FF">归并排序
- <font color="#FF00FF">测试代码:排序性能对⽐
- <font color="#FF00FF">⾮⽐较排序
- <font color="#FF00FF">排序算法复杂度及稳定性分析
排序
概念:排序字面意思就是把数据按递增或递减的排列起来的操作。
排序的应用:
常见排序算法:
插⼊排序

直接插⼊排序是⼀种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到⼀个已经排好序的有序序列中,直到所有的记录插入完为止,得到⼀个新的有序序列.
1. 直接插入排序
新插入的数据与之前的有序区间进行比较,也就是与之前的数据(有序区间)依次比较,然后放到合适的位置。
代码实现
void InsertSort(int* a, int n)
{
for (int i = 0; i < n-1; i++)
{
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;
}
}
代码分析:首先我们需要一个循环来改变end的下标值,end是用来向前进行依次比较的变量,tmp保存end的下一位置的值,只要end大于0就进入第二个循环,以升序为例下标end>=0,并且end下标位置的值如果大于tmp,我们就把 a[end + 1] = a[end];此时end+1的位置的数据就是end了,而end+1位置的数据被改了,但我们用了tmp保存,然后循环继续比较,循环结束再把tmp放到合适的位置。
最后一步一样。
直接插⼊排序的特性总结
- 元素集合越接近有序,直接插⼊排序算法的时间效率越⾼
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
2. 希尔排序
希尔排序法⼜称缩⼩增量法。希尔排序法的基本思想是:先选定⼀个整数(通常是gap=n/3+1),把
待排序⽂件所有记录分成各组,所有的距离相等的记录分在同⼀组内,并对每⼀组内的记录进⾏排序,然后gap=gap/3+1得到下⼀个整数,再将数组分成各组,进⾏插⼊排序,当gap=1时,就相当于直接插⼊排序。
它是在直接插⼊排序算法的基础上进⾏改进⽽来的,综合来说它的效率肯定是要⾼于直接插⼊排序算法的。
希尔排序的特性总结
-
希尔排序是对直接插⼊排序的优化。
-
当 gap > 1 时都是预排序,⽬的是让数组更接近于有序。当 gap == 1 时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
代码实现void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
//推荐写法:除3
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
代码分析:首先把步长设置为元素个数,只要gap大于1进入循环 ,这里写gap=gap/3+1的原因是如果不写+1会导致gap=2时gap会为0,此时会出问题,因为我们必须保证最后一次gap一定为1,还有就是gap不能为1会死循环,之前直接插入排序是tmp=a[end+1],我们现在分组比较所以tmp应该是a[end+gap],这里的for循环是n-gap,因为当取到最后一个数据时,n-gap-1再加上gap为n-1也就取到了最后一个数据,如果循环条件是像直接插入排序那样为n-1的话,那最后一次数据时n-2加上gap,如果gap是3就越界了( int tmp = a[end + gap];),剩下的代码就跟直接插入排序差不多了,只不过这里的end不在+1或-1了,都要加减相应的步长否则分组也就没意义了。
在图中除了第一行和最后一行,每一行旁边注释的是变成这个图后的end的值,就是执行完这个代码了,end变成了什么,然后每个图的最后一行表示向end=?的位置插入数据,不是表示end变成了这个数,end还是倒数第二行的值。比如这个图中最后一行表示向end=0的位置插入tmp,并不是end变成了0,end还是-2,也就是倒数第二行的end值。
end=4时退出for循环,此时就是直接插入排序,按照代码执行就好。这里的例子比较特殊,还没执行到gap==1就已经有序了。
希尔排序的时间复杂度计算
外层循环:
外层循环的时间复杂度可以直接给出为:O(log2 n) 或者O(log3 n) ,即O(log n)
内层循环:
因此,希尔排序在最初和最后的排序的次数都为n,即前⼀阶段排序次数是逐渐上升的状态,当到达
某⼀顶点时,排序次数逐渐下降⾄n,⽽该顶点的计算暂时⽆法给出具体的计算过程只能算出大约是1.3.
希尔排序时间复杂度不好计算,因为 gap 的取值很多,导致很难去计算,因此很多书中给出的希尔排
序的时间复杂度都不固定。《数据结构(C语⾔版)》---严蔚敏书中给出的时间复杂度为:
直接选择排序
每⼀次从待排序的数据元素中选出最⼩(或最⼤)的⼀个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
代码实现:
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = begin, maxi = begin;
for (int i = begin+1; i <= end; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
if (begin == maxi)
{
maxi = mini;
}
swap(&a[mini], &a[begin]);
swap(&a[maxi], &a[end]);
++begin;
--end;
}
}
详细分析:
-
我们每次for循环都先找一个最大值maxi和一个最小值mini之后等循环结束后把最小值和起始位置begin交换,最大值和交换end交换,因为每次跳出for循环begin++和end--,所以每次for循环都能整出当前数据中最大和最小的数据,直到排序完成。
-
那么我们写的这句代码意义是什么呢,当我们的数据是731时我们测试一下,正好最近新出这个电影,我们就拿这个举个例子。
if (begin == maxi)
{
maxi = mini;
}

这是没有这道代码得分效果,我们看一下有的情况,
直接选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使⽤
- 时间复杂度:O(N *N)
- 空间复杂度:O(1)
堆排序
堆排序(Heapsort)是指利⽤堆积树(堆)这种数据结构所设计的⼀种排序算法,它是选择排序的⼀种。它是通过堆来进⾏选择数据。需要注意的是排升序要建⼤堆,排降序建小堆。这个我在二叉树博客里写了。
// 升序,建⼤堆
// 降序,建⼩堆
// O(N*logN)
void HeapSort(int* a, int n)
{
//向下调整建堆
for (int i = (n-1-1)/2; i >= 0; --i)//之前说过,向下调整算法有⼀个前提:左右⼦树必须是⼀个堆,才能调整,所以从最后一个父节点开始向下调整,n是数组个数,-1代表下标,再-1是左孩子,除以2是最后一个父节点。
{
AdjustDown(a, n, i);//保证每个数据都是堆结构
}
// O(N*logN)
int end = n - 1;//下标
while (end > 0)
{
Swap(&a[0], &a[end]);//第一个数据和最后一个数据交换
AdjustDown(a, end, 0);//从第一个数据开始向下调整,不包括最后一个数据,每次都减少一个数据。
--end;
}
}
冒泡排序

void BubbleSort(int* a, int n)
{
int exchange = 0;
for (int i = 0; i < n; i++)
{
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;
}
}
}
冒泡排序就是很基础的东西,每个数据两两比较依次冒泡最后有序,最后就是说可以在第二个for循环里设置个变量然后看是否进行了排序,如果没有那就直接终止循环,最好情况就是O(n)。
冒泡排序的特性总结
• 时间复杂度:O(N )
• 空间复杂度:O(1)
快速排序
快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序⽅法,其基本思想为:任取待排序元素
序列中的某元素作为基准值,按照该排序码将待排序集合分割成两⼦序列,左⼦序列中所有元素均⼩
于基准值,右⼦序列中所有元素均⼤于基准值,然后最左右⼦序列重复该过程,直到所有元素都排列
在相应位置上为⽌。
快速排序实现主框架:
void QuickSort(int* a, int left, int right)
{
if (left >= right) {
return;
}
//_QuickSort⽤于按照基准值将区间[left,right)中的元素进⾏划分
int meet = _QuickSort(a, left, right);
QuickSort(a, left, meet - 1);
QuickSort(a, meet + 1, right);
}
hoare版本
算法思路:
1)创建左右指针,确定基准值
2)从右向左找出⽐基准值⼩的数据,从左向右找出⽐基准值⼤的数据,左右指针数据交换,进⼊下次循环
代码实现:
int _QuickSort(int* a, int left, int right)
{
int keyi = left;
++left;
while (left <= right)
{
// 右边找⼩
while (left <= right && a[right] > a[keyi])
{
--right;
}
// 左边找大
while (left <= right && a[left] < a[keyi])
{
++left;
}
if (left <= right)
{
swap(&a[left++], &a[right--]);
}
}
swap(&a[keyi], &a[right]);
return right;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
//_QuickSort⽤于按照基准值将区间[left,right)中的元素进⾏划分
int meet = _QuickSort(a, left, right);
QuickSort(a, left, meet - 1);
QuickSort(a, meet + 1, right);
}
a[right] > a[keyi]; a[left] < a[keyi]);
这里没有=的原因是如果数据全是一样的他会找到最左边的第一个数据为基准值时间复杂度太高,快排之所以快,是因为每次快排都能分出左右子序列,所以这里没有等号的原因是让基准值尽可能在中间位置使快排更快。
while (left <= right)
最外面大的while循环为什么有等于号?
- 相遇值大于key值
因为最外层循环已经是left<=right了,所以内层必须一致才符合我们的要求,否则只有外层有=,内层没有会死循环而且会使我们找的数据在左边大于基准值导致出错。
为什么跳出循环后right位置的值⼀定不⼤于key?
当 left > right 时,即right⾛到left的左侧,⽽left扫描过的数据均不⼤于key,因此right此时指向的数据⼀定不⼤于key。
这三种情况right位置的值都不大于key。
挖坑法
思路:
创建左右指针。⾸先从右向左找出⽐基准⼩的数据,找到后⽴即放⼊左边坑中,当前位置变为新
的"坑",然后从左向右找出⽐基准⼤的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",结
束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)
快排就是把每个基准值放到合适的位置直到全部有序。
int _QuickSort(int* a, int left, int right)
{
int hole = left;
int key = a[hole];
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;
return hole;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
//_QuickSort⽤于按照基准值将区间[left,right)中的元素进⾏划分
int meet = _QuickSort(a, left, right);
QuickSort(a, left, meet - 1);
QuickSort(a, meet + 1, right);
}
- a[right] >key
这个没有等于号的原因上边解释过了,这里的while循环不需要等于号,因为他们相等时直接拿基准值添进这个坑就好了。
lomuto前后指针
创建前后指针,从左往右找⽐基准值⼩的进⾏交换,使得⼩的都排在基准值的左边。
int _QuickSort(int* a, int left, int right)
{
int prev = left, cur = left + 1;
int key = left;
while (cur <= right)
{
if (a[cur] < a[key] && ++prev != cur)
{
swap(&a[cur], &a[prev]);
}
++cur;
}
swap(&a[key], &a[prev]);
return prev;
}
定义两个变量prev和cur,cur指向位置的数据与key值比较
- arr[cur]<arr[keyi],prev向后走一步和cur交换
- arr[cur]>=arr[keyi],cur继续往后遍历
- ++prev != cur 这里防止同一数据进行重复交换
- 快速排序特性总结:
- 时间复杂度:O(nlogn)
- 空间复杂度:O(logn)
⾮递归版本
⾮递归版本的快速排序需要借助数据结构:栈
void QuickSortNonR(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 = begin;
int prev = begin;
int cur = begin + 1;
while (cur <= end)//找基准值
{
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[keyi], &a[prev]);
keyi = prev;//找基准值完成
// [begin, keyi-1] keyi [keyi+1, end]
if (keyi + 1 < end)//判断是否符合要求,例如[1.0]不是有效区间
{
STPush(&st, end);//入栈
STPush(&st, keyi + 1);//入栈
}
if (begin < keyi-1)//判断是否符合要求,例如[1.0]不是有效区间
{
STPush(&st, keyi-1);
STPush(&st, begin);
}
}
STDestroy(&st);//销毁

归并排序
归并排序算法思想:
归并排序(MERGE-SORT)是建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法(Divide
andConquer)的⼀个⾮常典型的应⽤。将已有序的⼦序列合并,得到完全有序的序列;即先使每个
⼦序列有序,再使⼦序列段间有序。若将两个有序表合并成⼀个有序表,称为⼆路归并。归并排序核
⼼步骤:
void _MergeSort(int* arr,int left,int right,int* tmp)
{
if (left >= right)//相等表示只有一个序列,只有一个数字,例如上面横绿色线的10
{
return;
}
int mid = (left + right) / 2;//分解
//[left,mid] [mid+1,right]
_MergeSort(arr, left, mid, tmp);//左区间
_MergeSort(arr, mid + 1, right, tmp);//右·区间
//合并
//[left,mid] [mid+1,right]
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int index = begin1;
while (begin1 <= end1 && begin2 <= end2)//不能越界
{
if (arr[begin1] < arr[begin2])
{
tmp[index++] = arr[begin1++];
}
else {
tmp[index++] = arr[begin2++];
}
}
//要么begin1越界 要么begin2越界
while (begin1 <= end1)
{
tmp[index++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = arr[begin2++];
}
//[left,mid] [mid+1,right]
//把tmp中的数据拷贝回arr中
for (int i = left; i < right; i++)
{
arr[i] = tmp[i];
}
}
void MergeSort(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
_MergeSort(arr, 0, n - 1, tmp);
free(tmp);
}
归并排序特性总结:
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
测试代码:排序性能对⽐
/ 测试排序的性能对⽐
void TestOP()
{
srand(time(0));
const int N = 100000;
int* a1 = (int*)malloc(sizeof(int)*N);
int* a2 = (int*)malloc(sizeof(int)*N);
int* a3 = (int*)malloc(sizeof(int)*N);
int* a4 = (int*)malloc(sizeof(int)*N);
int* a5 = (int*)malloc(sizeof(int)*N);
int* a6 = (int*)malloc(sizeof(int)*N);
int* a7 = (int*)malloc(sizeof(int)*N);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
a7[i] = a1[i];
}
int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
int begin3 = clock();
SelectSort(a3, N);
int end3 = clock();
int begin4 = clock();
HeapSort(a4, N);
int end4 = clock();
int begin5 = clock();
QuickSort(a5, 0, N-1);
int end5 = clock();
int begin6 = clock();
MergeSort(a6, N);
int end6 = clock();
int begin7 = clock();
BubbleSort(a7, N);
int end7 = clock();
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
printf("QuickSort:%d\n", end5 - begin5);
printf("MergeSort:%d\n", end6 - begin6);
printf("BubbleSort:%d\n", end7 - begin7);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
free(a7);
}
大家可以输入代码对比一下效率。
⾮⽐较排序
计数排序
计数排序⼜称为鸽巢原理,是对哈希直接定址法的变形应⽤。操作步骤:
1)统计相同元素出现次数
2)根据统计的结果将序列回收到原来的序列中
void CountSort(int* a, int n)
{
int min = a[0], max = a[0];//给最大值和最小值一个初始值
for (int i = 1; i < n; i++)
{
if (a[i] > max)
max = a[i];//找到最大值
if (a[i] < min)
min = a[i];//找到最小值
}
int range = max - min + 1;//确定申请空间
int* count = (int*)malloc(sizeof(int) * range);//创建新数组
if (count == NULL)//申请失败
{
perror("malloc fail");
return;
}
memset(count, 0, sizeof(int) * range);//将cout数组数据全置为0
// 统计次数
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;`//统计次数需要减去最小值,100-100=0,0++=1
}
// 排序
int j = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--)//根据count数组中的次数来确认存放到原数组中的数据个数
{
a[j++] = i + min;这里不是统计次数,需要加最小值放回原数组
}
}
}
计数排序的特性:
计数排序在数据范围集中时,效率很⾼,但是适⽤范围及场景有限。
时间复杂度:O(N + range)
空间复杂度:O(range)
稳定性:稳定
排序算法复杂度及稳定性分析
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的
相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,⽽在排序后的序列中,r[i]仍在r[j]之
前,则称这种排序算法是稳定的;否则称为不稳定的。
1.
堆顶和堆底交换不稳定