目录
排序的定义
排序:所谓排序,就是使⼀串记录,按照其中的某个或某些关键字的⼤⼩,递增或递减的排列起来的 操作。而排序出现在我们的生活中的各种场景,手机购物的热度,学校排名等等。
排序种类
排序还是会分种类的,他们分为:
- 插入排序(直接插入排序和希尔排序)
- 选择排序(直接选择排序和堆排序)
- 交换排序(冒泡排序和快速排序)
- 归并排序(归并排序)
当然肯定还有其他的排序方式,但这7种排序方式也是最常用的排序的方法。
插入排序
基本思想:
直接插⼊排序是⼀种简单的插⼊排序法,其基本思想是:把待排序的记录按其关键码值的⼤⼩逐个插⼊到⼀个已经排好序的有序序列中,直到所有的记录插⼊完为⽌,得到⼀个新的有序序列 ,其实就类似我们玩扑克牌一样。
直接插入排序
原理:
以一个指针从头开始比较,确保时有序的情况,指针就向前移动,如果遇到不符合的位置,将其位置向后交换位置,交换后依旧不符合有序结构,就继续向后交换直至内容符合有序结构或者到0的位置,交换完成后指针保持退后的格数继续往前移动。

代码实现:
cpp
void InsertSort(int* arr, size_t sz)//直接插入插入排序
{
int tmp = 0;
for (int end = 1; end < sz; end++)
{
tmp = arr[end];
while (end > 0 && arr[end - 1] < tmp)
{
arr[end] = arr[end - 1];
end--;
}
arr[end] = tmp;
}
return;
}
总结:
元素集合越接近有序,直接插⼊排序算法的时间效率越⾼
时间复杂度:O(N^2)
空间复杂度:O(1)
希尔排序
原理:
希尔排序,那有的人会想到别的上面去了。


希尔排序肯定跟他们无关,希尔排序就比直接排序多的一个步骤 gap,把数组分割成多份,当成独一个的数组来直接插入排序,gap要记录数组的长度,每次循环都需要除以3加1,直到最后依次1执行完,退出循环。

代码实现:
cpp
void Swap(int* x, int* y)//交换
{
int tmp = *x;
*x = *y;
*y = tmp;
return;
}
void ShellSort(int* arr, size_t sz)//希尔排序
{
int gas = sz;
while (gas > 1)
{
gas = gas / 3 + 1;//最后的循环一定是1
for (int end = gas; end < sz; end++)//多组一起排序
{
int i = end;
while (arr[i - gas] < arr[i]/*判断升序还是降序*/ && i - gas >= 0)
{
Swap(&arr[i], &arr[i - gas]);
i -= gas;
}
}
}
}
总结:
希尔排序是直接插入排序的优化版本,当 gap > 1 时都是预排序,⽬的是让数组更接近于有序。当 gap == 1 时,数组已经接近有序 的了,这样就会很快。这样整体⽽⾔,可以达到优化的效果。
时间复杂度:O(N^1.3),每次排序时间是逐渐增加,在达到峰值是逐渐变小,当gap=1时,此段循环复杂度趋近N。希尔排序的时间复杂度在其他教材中都不统一。
选择排序
选择排序的基本思想:
每⼀次从待排序的数据元素中选出最⼩(或最⼤)的⼀个元素,存放在序列的起始位置,直到全部待 排序的数据元素排完 。
直接选择排序
原理:
取一个乱序数组,每次循环寻找出里面最小的数值放到数组的最前面,之后的循环不用访问前面已经排好位置的数组,每次循环的起始位置向前移动一位。

优化版:每次不仅可以找最小数值,也可以寻找最大数值。
代码实现:
cpp
void Swap(int* x, int* y)//交换
{
int tmp = *x;
*x = *y;
*y = tmp;
return;
}
void SelectSort(int* arr, size_t sz)//直接选择排序
{
int end, bigen, mini, maxi;
//end bigen 是每次循环数组的边界
//mini maxi 是每次循环找到的最小数值和最大数值位置
end = maxi = sz - 1;
bigen = mini = 0;
while (bigen < end)
{
maxi = end;
mini = bigen;
for (int i = bigen; i <= end; i++)
{
if (arr[i] < arr[mini])
{
mini = i;
}
if (arr[i] > arr[maxi])
{
maxi = i;
}
}
//防止 二次交换位置的情况。
if (end == mini && bigen == maxi)
{
Swap(&arr[end], &arr[bigen]);
}
else
{
Swap(&arr[end], &arr[maxi]);
Swap(&arr[bigen], &arr[mini]);
}
end--;
bigen++;
}
}
总结:
时间复杂度:O(N^2);
空间复杂度:O(1);
直接选择排序思考⾮常好理解,但是效率不是很好。实际中很少使⽤
堆排序
堆排序其实已经在前面的树章节已经讲过了,原理就不过多赘述了。
堆是一种特殊的完全二叉树,满足以下性质:
- 最大堆:每个节点的值大于或等于其子节点的值(根节点为最大值)。
- 最小堆:每个节点的值小于或等于其子节点的值(根节点为最小值)。
堆通常通过数组实现,利用完全二叉树的性质进行索引映射(父节点索引为
i,左子节点为2i+1,右子节点为2i+2)。堆排序的基本概念
堆排序是一种基于二叉堆数据结构的比较排序算法。二叉堆是一种完全二叉树,分为最大堆和最小堆。最大堆中父节点的值大于或等于子节点的值,最小堆中父节点的值小于或等于子节点的值。堆排序通常使用最大堆实现升序排序。
堆排序的核心步骤
构建最大堆
将待排序的数组视为完全二叉树,从最后一个非叶子节点开始,逐步调整子树使其满足最大堆性质。非叶子节点的索引为
n/2 - 1(n为数组长度)。调整堆(Heapify)
从当前非叶子节点开始,比较其与左右子节点的值。若子节点的值更大,则将父节点与较大的子节点交换,并递归调整受影响的子树。
排序过程
将堆顶元素(最大值)与数组末尾元素交换,缩小堆的范围(排除已排序部分),重新调整剩余部分为最大堆。重复此过程直至堆的大小为 1。
代码实现:
cpp
void Swap(int* x, int* y)//交换
{
int tmp = *x;
*x = *y;
*y = tmp;
return;
}
void AdjustDown(int* arr, int i, int sz)//向下调整 而sz 的作用只有一个防止 越界访问 递归的结束条件是已经符合堆的条件就不用退出递归
{
int fa = i ,bro=i*2+1;
if (arr[fa] > arr[bro] && bro < sz)
{
fa = bro;
}
if (arr[fa] > arr[bro + 1] && bro + 1 < sz)
{
fa = bro + 1;
}
if (fa != i)//如果没有实现交换fa 保持i就可以退出循环 而不是不等于 sz 如果与sz 等于就会导致死递归下去 死循环
{
Swap(&arr[i], &arr[fa]);
AdjustDown(arr, fa, sz);
}
}
void HeapSort(int* arr, size_t sz)//堆排序 大堆 升序 时间复杂度 nlogn
{
for (int i = (sz - 1 - 1)/2; i >= 0; i--)//建堆 大堆
{
AdjustDown(arr, i, sz);
}
int end = sz - 1;
while (end != 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, 0, end);//注意点
end--;
}
}
总结:
时间复杂度:O(N logN);
空间复杂度:O(1);
交换排序
交换排序基本思想:
所谓交换,就是根据序列中两个记录键值的⽐较结果来对换这两个记录在序列中的位置 交换排序的特点是:将键值较⼤的记录向序列的尾部移动,键值较⼩的记录向序列的前部移动
冒泡排序
原理:
冒泡排序其实我们接触最多的排序,因为比较简单的排序思想。类似直接选择排序,但意思反过来,把其中最小或者最大的内容排到最后的位置,其次类推。
代码实现:
cpp
void Swap(int* x, int* y)//交换
{
int tmp = *x;
*x = *y;
*y = tmp;
return;
}
void BubbleSort(int* arr, size_t sz)//冒泡排序 时间复杂度 N^2
{
assert(arr);
int falg = 1;
for (int i = 0; i < sz; i++)
{
falg = 1;
for (int j = 0; j < sz - 1 - i; j++)
{
if (arr[j] < arr[j + 1])
{
Swap(&arr[j], &arr[j + 1]);
falg = 0;
}
}
if (falg)
{
break;
}
}
return;
总结:
冒泡排序是这7种排序方法种最简单的排序思想,但时间复杂比较多的那一档。
时间复杂度:O(N^2);
空间复杂度:O(N)。
快速排序
原理:
找一个基准比较通过比较,把整个数组分为两个数组,一个是小于基准的数组,一个是大于基准的数组,让后对这一对数组继续使用快排,找基准比较分开成为两个数组,直到数组每一个数组的元素只剩下一个为止。
递归思想:
cpp
void QuickSort(int* arr, int left,int right)
{
assert(arr);
if (left >= right)
{
return;
}
int keti = _QuickSort(arr, left, right);
QuickSort(arr, left, keti - 1);//左节点
QuickSort(arr, keti + 1, right);//右节点
}
通过找到keti来递归分开两个数组来实现。找通常我们会以 left 的开始为基准,要来实现keti 的寻找还是有挺多的方法。
hoare版本
1)创建左右指针,确定基准值
2)从右向左找出⽐基准值⼩的数据,从左向右找出⽐基准值⼤的数据,左右指针数据交换,进⼊下次循环
问题1:为什么跳出循环后right位置的值⼀定不⼤于key?
当 left > right 时,即right⾛到left的左侧,⽽left扫描过的数据均不⼤于key,因此right此时指向的数据⼀定不⼤于key
cpp
//hoare版本
int _QuickSort1(int* arr,int left,int right)
{
int keti=left;
left++;
while (left <= right)
{
while (left <= right && arr[right] > arr[keti])
right--;
while (left <= right && arr[left] < arr[keti])
left++;
if (left <= right)
{
Swap(&arr[left++], &arr[right--]);
}
}
Swap(&arr[right], &arr[keti]);
return right;
}
**注意点:**arrright > arrketi和arrleft < arrketi一定不可以写等于号,会导致在全部元素相等的时候时间复杂度变大。后面的实现方法都是。
挖坑法:
cpp
int _QuickSort2(int* arr, int left, int right)
{
//挖坑法
int tmp = arr[left];
int mid = left++;
while (left <= right)
{
while (left <= right && arr[right] > tmp)
right--;
if (left <= right)
{
Swap(&arr[mid], &arr[right]);
mid = right--;
}
while (left <= right && arr[left] < tmp)
left++;
if (left <= right)
{
Swap(&arr[mid], &arr[left]);
mid = left++;
}
}
return mid;
}

lomuto前后指针
cpp
int _QuickSort3(int* arr, int left, int right)
{
int prev = left, net = left + 1;
while (net <= right)
{
if (arr[net] < arr[left] && ++prev != net)
{
Swap(&arr[net], &arr[prev]);
}
net++;
}
Swap(&arr[prev], &arr[left]);
return prev;
}

非递归方法:
利用数据结构栈的来实现
cpp
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int low;
int high;
} Range;
void swap(int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
int partition(int arr[], int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
swap(&arr[i], &arr[j]);
}
}
swap(&arr[i + 1], &arr[high]);
return i + 1;
}
void quick_sort(int arr[], int n) {
Range *stack = malloc(n * sizeof(Range));
int top = -1;
stack[++top] = (Range){0, n - 1};
while (top >= 0) {
Range r = stack[top--];
int low = r.low;
int high = r.high;
if (low < high) {
int pi = partition(arr, low, high);
if (pi - 1 > low) {
stack[++top] = (Range){low, pi - 1};
}
if (pi + 1 < high) {
stack[++top] = (Range){pi + 1, high};
}
}
}
free(stack);
}
int main() {
int arr[] = {10, 7, 8, 9, 1, 5};
int n = sizeof(arr) / sizeof(arr[0]);
quick_sort(arr, n);
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
原理就是通过栈的思想,keti 的地址还是需要使用前面三种的方法,通过入栈记录人每一次keti 的分割出来数组的位置直到栈为空的时候。
总结:
时间复杂度:O(nlogn);
空间复杂度:O(1);
归并排序
原理:
先把整一个数值分解成一个元素的数组进行排序,因为一个元素的数组就默认有序数组,把两个的有序数组进行插入排序,两个两个的拼接重组就构成新的有序数组。
代码实现:
cpp
void _MergeSort(int* arr, int left, int right,int* tmp)
{
if (left >= right)
{
return;
}
int mid = (left + right) / 2;
_MergeSort(arr, left, mid, tmp);
_MergeSort(arr, mid + 1, right, tmp);
int bigen1 = left, bigen2 = mid + 1, end1 = mid, end2 = right, i = left;
while (bigen1 <= end1 && bigen2 <= end2)
{
if (arr[bigen1] < arr[bigen2])
{
tmp[i++] = arr[bigen1++];
}
else
{
tmp[i++] = arr[bigen2++];
}
}
while (bigen1 <= end1)
{
tmp[i++] = arr[bigen1++];
}
while (bigen2 <= end2)
{
tmp[i++] = arr[bigen2++];
}
memcpy(arr, tmp, sizeof(int) * (right + 1));
}
void MergeSort(int* arr, int sz)
{
int* tmp = (int*)malloc(sz * sizeof(int));
_MergeSort(arr, 0, sz - 1,tmp);
free(tmp);
tmp = NULL;
}
与快速排序的步骤相反
总结:
时间复杂度:O(NlogN)
空间复杂度:O(N)
感谢观看!
悠仁さん