1.快速排序
1.1基本思想:
快速排序是通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
1.2基本思路:
(1)选择基准:从待排序的序列中选取一个元素作为基准,这个元素可以是序列的第一个、最后一个、中间的一个或者通过某种方式计算得到的值等。
(2)分区:重新排列序列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于序列的中间位置。这个称为分区操作。
(3)递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。
1.3图解过程:
1.3.1hoare版本:
(左边找大,右边找小(先从右边开始找),找到后交换两个位置的值,直到 left=right ,再把 tmp 位置的值与 left 位置的值交换)
动图展示:
这里 tmp 值从右边选取,所以从左先开始找,效果其实一样的。
1.3.2挖坑法:
(1)选 left 做 tmp(tmp 是单趟排序后能排到最后该待的位置的数据,left 同时也作为第一个坑)
(2)right 开始找,right 遇到比 tmp 大或相等的就往左边走,遇到比 tmp 小的就停下,然后将right 赋值给 left(left = right)(填 left 坑),right 则成为了新的坑,进行第三步。(若 right 超过了 left,直接进行第四步)
(3)left 开始找,left 遇到比 tmp 小或相等的就往右边走,遇到比 tmp 大的就停下,然后将 left 赋值给 right(right = left)(填 right 坑),left 则成为了新的坑,进行第二步。(若 left 超过了right,直接进行第四步)
(4)将 tmp 赋值给 left。
动图展示:
这个从右开始取 tmp 值,所以从左先开始找,效果一样。
1.3.3前后指针:
具体思路:先选定左边第一个为 tmp ,同时设定 left 位置为 prev,prev 的后一个位置为 cur,从a[cur] 开始和 tmp 比较,如果 a[cur] 比 tmp 小,就先将 prev++,再让 a[cur] 和 a[prev] 交换,然后 cur++,如果 a[cur] 比 tmp大,那么就不改变 prev,也不用交换,只对 cur++,以此类推,直到cur>right 就结束,最后将 a[prev] 和 tmp 交换就完成了一趟快排。
动图展示:
1.4代码实现:
1.4.1hoare版本:
// 快速排序hoare版本
int PartSort1(int* arr, int left, int right)
{
//优化1:三数取中
int mid = chose_middle(arr, left, right);
Swap(&arr[left], &arr[mid]);//交换两个位置值的函数
int key = left;
int begin = left;
int end = right;
while (begin < end)
{
//右边找小
while (begin < end && arr[end] >= arr[key])
{
end--;
}
//左边找大
while (begin < end && arr[begin] <= arr[key])
{
begin++;
}
Swap(&arr[begin], &arr[end]);//交换两个位置值的函数
}
Swap(&arr[key], &arr[begin]);//交换两个位置值的函数
return begin;
}
void Quick_sort(int* arr, int left, int right)
{
// 快速排序hoare版本
//int key = PartSort1(arr, left, right);
Quick_sort(arr, left, key - 1);
Quick_sort(arr, key + 1, right);
}
1.4.2挖坑法:
// 快速排序挖坑法
int PartSort2(int* arr, int left, int right)
{
//优化1:三数取中
//int mid = chose_middle(arr, left, right);
//Swap(&arr[left], &arr[mid]);
int pit = left;
int key = arr[left];
int begin = left;
int end = right;
while (begin < end)
{
while (arr[end] > key && begin < end)
{
end--;
}
Swap(&arr[end], &arr[pit]);
pit = end;
while (arr[begin] < key && begin < end)
{
begin++;
}
Swap(&arr[begin], &arr[pit]);
pit = begin;
Print_arr(arr, 9);
printf("\n");
}
Swap(&arr[pit], &key);
return begin;
}
void Quick_sort(int* arr, int left, int right)
{
// 快速排序挖坑法
int key = PartSort2(arr, left, right);
Quick_sort(arr, left, key - 1);
Quick_sort(arr, key + 1, right);
}
1.4.3前后指针:
// 快速排序前后指针法
int PartSort3(int* arr, int left, int right)
{
//优化1:三数取中
int mid = chose_middle(arr, left, right);
Swap(&arr[left], &arr[mid]);
int key = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (arr[cur] < arr[key] && ++prev != cur)
Swap(&arr[cur], &arr[prev]);
cur++;
}
Swap(&arr[prev], &arr[key]);
return prev;
}
void Quick_sort(int* arr, int left, int right)
{
// 快速排序前后指针法
int key = PartSort3(arr, left, right);
Quick_sort(arr, left, key - 1);
Quick_sort(arr, key + 1, right);
}
2.计数 排序
2.1基本思想:
计数排序在于将输入的数据值转化为键存储在额外开辟的数组空间中。用新数组对应下标位置的值来表示该数在原数组出现的次数,计数排序要求输入的数据必须是有确定范围的整数。
2.2基本思路:
(1)找出待排序数组中的最大值和最小值:这是为了确定计数数组(或称为桶)的大小和范围。计数数组的大小通常是最大值与最小值之差加1(如果包含负数,则需要调整以包含所有可能的值)。
(2)创建计数数组并初始化:根据步骤1确定的范围,创建一个新的数组,用于记录每个元素在原数组中出现的次数。这个数组的所有元素初始化为0。
(3)统计每个元素的出现次数:遍历原数组,对于每个元素,将其值作为计数数组的索引(如果元素是负数或范围超出常规,则需要进行适当的调整,如加上一个偏移量),并将该索引位置上的值加1。这样,计数数组的每个位置就记录了原数组中对应元素的出现次数。
(4)对计数数组进行累加(可选,但有助于后续排序):从计数数组的第一个元素开始,向后遍历,将每个元素的值更新为当前元素与前一个元素之和。这一步是为了确定每个元素在排序后数组中的位置。例如,如果某个元素在原数组中出现了n次,并且它是第k个不同的元素,那么它在排序后的数组中应该占据从第k个位置开始的n个连续位置
(5)根据计数数组重构原数组:这是排序的最后一步。从后向前遍历原数组(或者从后向前遍历计数数组,同时遍历一个指针指向排序后数组的末尾),对于原数组中的每个元素,找到它在计数数组中的索引,并根据计数数组的值(或者累加后的值)确定它在排序后数组中的位置,然后将该元素放到排序后数组的相应位置上,并减少计数数组中对应索引的值(如果进行了累加)。这个过程可能需要从后向前遍历原数组,以确保稳定性(即相等元素的相对顺序不变)。
(6)处理负数或特殊范围:如果原数组中包含负数或元素范围超出了常规整数范围,可能需要在步骤1和步骤2中进行适当的调整,如加上一个偏移量,以确保所有元素都能被正确地映射到计数数组的索引上。
2.3图解过程:
动图展示:
2.4代码实现:
// 计数排序
void CountSort(int* arr, int n)
{
int min = arr[0];
int max = arr[0];
for (int i = 1; i < n; i++)
{
if (arr[i] < min)
{
min = arr[i];
}
if (arr[i] > max)
{
max = arr[i];
}
}
//创建一个数组来计算(相对)下标出现的次数
int count = max - min + 1;
//calloc会自动初始化数组为0
//int* tmp = (int*)calloc(count,sizeof(int));
//if (tmp == NULL)
//{
// perror("calloc error");
// return;
//}
//使用malloc时需要初始化数组
int* tmp = (int*)malloc(count * sizeof(int));
if (tmp == NULL)
{
perror("malloc error");
return;
}
for (int i = 0; i < count; i++)
{
tmp[i] = 0;
}
//把原数组映射到开辟的数组中
for (int i = 0; i < n; i++)
{
tmp[arr[i] - min]++;
}
//排序
int j = 0;
for (int i = 0; i < count; i++)
{
while (tmp[i]--)
{
arr[j] = i + min;
j++;
}
}
free(tmp);
tmp = NULL;
}
3.归并排序
3.1基本思想:
归并排序基本思想是将一个数组分成两半,对每半部分递归地应用归并排序,然后将排序好的两半合并在一起。这个过程一直递归进行,直到数组被分割成只包含一个元素的子数组,这些子数组自然就是排序好的。然后,算法开始合并这些子数组,直到合并成一个完整的排序好的数组。
3.2基本思路:
(1)分解:将数组分解成两个较小的子数组,直到子数组的大小为1。
(2)递归进行排序并合并:递归地对子数组进行归并排序,并将已排序的子数组合并成一个大的有序数组,直到合并为1个完整的数组
3.3图解过程:
3.4代码实现:
void _Merge_sort(int* arr, int* arr1, int left, int right)
{
if (left >= right)
{
return;
}
//划分区间
int key = (left + right) / 2;
//[left,key],[key+1,right]
_Merge_sort(arr, arr1, left, key);
_Merge_sort(arr, arr1, key + 1, right);
//归并
int left1 = left;
int right1 = key;
int left2 = key + 1;
int right2 = right;
int i = left;
while (left1 <= right1 && left2 <= right2)
{
if (arr[left1] < arr[left2])
{
arr1[i] = arr[left1];
i++;
left1++;
}
else
{
arr1[i] = arr[left2];
i++;
left2++;
}
}
while (left1 <= right1)
{
arr1[i] = arr[left1];
i++;
left1++;
}
while (left2 <= right2)
{
arr1[i] = arr[left2];
i++;
left2++;
}
//交换完一次拷贝一次
memcpy(arr + left, arr1 + left, (right - left + 1) * sizeof(int));
}
//归并排序(递归)
void Merge_sort(int* arr, int n)
{
int* arr1 = (int*)malloc(sizeof(int) * n);
if (arr1==NULL)
{
perror("malloc error");
return;
}
_Merge_sort(arr, arr1, 0, n - 1);
free(arr1);
arr1 = NULL;
}