排序
一、比较排序
插入排序
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到⼀个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
实际中我们玩扑克牌时,就用了插入排序的思想:
直接插入排序
end表示有序序列
的最后一个位置,tmp为排序数据。
//Sort.c
//直接插入排序
void InsertSort(int* arr, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];
end--;
}
else {
break;
}
}
arr[end + 1] = tmp;
}
}
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
希尔排序
//Sort.c
//希尔排序
void ShellSort(int* arr, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else {
break;
}
}
arr[end + gap] = tmp;
}
}
}
直接选择排序
定义好两个“指针”begin
、end
,分别指向待排序序列中的第一个元素和最后一个元素。在序列中找出最大值和最小值,且这个最大最小值不是这组元素中第一个数/最后一个数,则最小值与第一个元素交换,最大值与最后一个元素交换,这样就有两个元素排好了。剩下的元素重复上面的步骤。
排升序,最小值与待排序序列中第一个元素先交换,会有最大值就是第一个元素的情况,那么这时最大值“指针”需要先到最小值的位置,然后再交换:
//Sort.c
//直接选择排序
void SelectSort(int* arr, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
//找最大值arr[maxi]、最小值arr[mini]
int maxi = begin, mini = begin;
for (int i = begin + 1; i <= end; i++)
{
if (arr[i] > arr[maxi])
{
maxi = i;
}
if (arr[i] < arr[mini])
{
mini = i;
}
}
//maxi就是第一个元素下标--begin
if (maxi == begin)
{
maxi = mini;
}
Swap(&arr[mini], &arr[begin]);
Swap(&arr[maxi], &arr[end]);
begin++;
end--;
}
}
特性
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度: O(n^2)
- 空间复杂度: O(1)
快速排序
递归版本
递归的意义就是找基准值来二分
找基准值划分成左右两个序列
每一个左右序列都要重复刚刚的排序
等于的时候也要交换,不交换的话会影响递归的时间复杂度。
找基准值的3种方法
1.hoare版本
当left <= right,left从左往右走找比基准值大的数据,right从右往左走找比基准值小的数据,找到后,left和right数据交换,left++,right–;当left > right,right和keyi数据交换。
//Sort.c
int _QuickSort(int* arr, int left, int right)
{
int keyi = left;
++left;//起始位置在keyi+1,因为自己跟自己比较没必要
while (left <= right)
{
//left从左往右走找比基准值大的数据
while (left <= right && arr[left] < arr[keyi])
{
++left;
}
//right从右往左走找比基准值小的数据
while (left <= right && arr[right] > arr[keyi])
{
--right;
}
if (left <= right)
{
Swap(&arr[left++], &arr[right--]);
}
}
Swap(&arr[right], &arr[keyi]);
return right;
}
//快速排序——hoare版本
void QuickSort(int* arr, int left, int right)
{
//序列中只有一个数据时,left == right,不用找基准值了,即不用排序了
//那就是没有>的情况,为了覆盖全部情况,所以是 >=
if (left >= right)
{
return;
}
//找基准值
int keyi = _QuickSort(arr, left, right);
//左序列:[left, keyi - 1]
//右序列:[keyi + 1, right]
QuickSort(arr, left, keyi - 1);
QuickSort(arr, keyi + 1, right);
}
2.挖坑法
//快速排序---挖坑法
int _QuickSort(int* arr, int left, int right)
{
int hole = left;
int key = arr[hole];
while (left < right)
{
//先:right从右往左找比keyi小的数据
//后:left从左往右找比keyi大的数据
while (left < right && arr[right] >= key)
{
right--;
}
if (left < right)
{
arr[hole] = arr[right];
hole = right;
}
while (left < right && arr[left] <= key)
{
left++;
}
if (left < right)
{
arr[hole] = arr[left];
hole = left;
}
}
arr[hole] = key;
return hole;
}
3.lomuto前后指针法
-
初始时,key是序列中第一个元素,即基准值。prev指针指向序列中的第一个元素,cur指针指向prev指针的下一个位置。
-
循环判断cur指针指向的元素是否小于key,若小于,则prev指针走到下一个位置,prev与cur指向的数据交换,然后cur++,否则直接cur++。
-
直到cur越界,跳出循环,此时prev指向的数据与key交换,prev的位置就是基准值的位置,此时key左边的数据都比key小,右边的数据都比key大。
//快速排序找基准值---lomuto前后指针法
int _QuickSort(int* arr, int left, int right)
{
int keyi = left;
int prev = left, cur = prev + 1;
while (cur <= right)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[cur], &arr[prev]);
}
cur++;
}
Swap(&arr[keyi], &arr[prev]);
return prev;
}
非递归版本
非递归版本的快速排序需要借助数据结构——栈,之前在讲解二叉树的层序遍历以及判断是否是完全二叉树借助了数据结构队列时,说明了怎么添加队列的实现方法,这里也同理,就不再赘述。
非递归也是借助基准值来划分左右序列,需要将序列进行保存,以进行下一次排序。
- 创建栈,写好初始化、销毁。将最右、最左下标依次入栈,循环判断当栈不为空时,先后取两次栈顶并出栈。
- 找基准值——lomuto前后指针法,基准值划分序列为左序列[left, keyi - 1]、右序列[keyi + 1, right]。
- 将左序列最右、最左下标依次入栈,右序列最右、最左下标依次入栈,循环判断栈是否为空。
- 每一个序列的基准值排好了就是排序结果。
//快速排序---非递归版本
void QuickSortNonR(int* arr, int left, int right)
{
ST st;
STInit(&st);
StackPush(&st, right);
StackPush(&st, left);
while (!StackEmpty(&st))
{
int begin = StackTop(&st);
StackPop(&st);
int end = StackTop(&st);
StackPop(&st);
//找基准值
int keyi = begin;
int prev = begin, cur = prev + 1;
while (cur <= end)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[cur], &arr[prev]);
}
cur++;
}
Swap(&arr[prev], &arr[keyi]);
keyi = prev;
//基准值划分了左序列[begin, keyi - 1]、右序列[keyi + 1, end]
if (begin < keyi - 1)
{
StackPush(&st, keyi - 1);
StackPush(&st, begin);
}
if (keyi + 1 < end)
{
StackPush(&st, end);
StackPush(&st, keyi + 1);
}
}
STDestroy(&st);
}
归并排序
递归版本
思路及代码实现
思路分为两步:
- 分解,用到了递归,根据
mid = (left + right) / 2
划分左右两个序列,左序列[left, mid],右序列[mid + 1, right],当分解到只有一个数字的有序序列时,就不再二分,如上图中绿线一行。(在当前这个情况下不可能出现大于的情况,但是为了覆盖全部的可能性,就写 >=)
- 合并两个序列,为了避免排序过程中出现错误,创建一个临时数组tmp进行排序。tmp中有序的数据导入到原数组中。
//Sort.c
void _MergeSort(int* arr, int left, int right, int* tmp)
{
//分解
if (left >= right)
{
return;
}
int mid = (left + right) / 2;
//根据mid划分左右两个序列:左序列[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++];
}
}
//左序列中的数据没有全部放到tmp数组中
//右序列中的数据没有全部放到tmp数组中
while (begin1 <= end1)
{
tmp[index++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = arr[begin2++];
}
//tmp中有序的数据导入到原数组中
for (int i = left; i <= right; i++)
{
arr[i] = tmp[i];
}
}
//归并排序---递归版本
void MergeSort(int* arr, int n)
{
int* tmp = (int*)malloc(n * sizeof(int));
if (tmp == NULL)
{
perror("malloc fail!");
exit(1);
}
_MergeSort(arr, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
时间复杂度nlogn
递归版本的归并排序时间复杂度 = 单次递归的时间复杂度 * 递归次数
递归次数与二叉树的高度有关,h = log(n+1),1可以省去,h=logn,即递归次数也是logn
空间复杂度nlogn
与递归次数有关,每次递归都会开辟函数栈帧
非递归版本
通过gap来控制一组里有多少个数据
//归并排序---非递归版本
void MergeSortNonR(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail!\n");
exit(1);
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2*gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
if (begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
//两个有序序列进行合并
int index = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[index++] = arr[begin1++];
}
else {
tmp[index++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
tmp[index++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = arr[begin2++];
}
//导入到原数组中
memcpy(arr + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
}
二、非比较排序
计数排序
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
1)(在count数组中)统计相同元素出现次数
2)根据统计的结果将次数还原到原来的序列(arr)中
count数组的大小如何确定?
//Sort.c
//计数排序
void CountSort(int* arr, int n)
{
int max = arr[0], min = arr[0];
for (int i = 1; i < n; i++)
{
if (arr[i] < min)
{
min = arr[i];
}
if (arr[i] > max)
{
max = arr[i];
}
}
//count数组的大小
int range = max - min + 1;
//用calloc也行
//int* count = (int*)calloc(range, sizeof(int));
//if (count == NULL)
//{
// perror("calloc fail!");
// exit(1);
//}
int* count = (int*)malloc(sizeof(int) * range);
if (count == NULL)
{
perror("malloc fail!");
exit(1);
}
memset(count, 0, sizeof(int) * range);
//在count数组中统计相同元素出现的次数
for (int i = 0; i < n; i++)
{
count[arr[i] - min]++;
}
//将数据还原到数组arr中
int index = 0;
for (int i = 0; i < range; i++)
{
//数据出现的次数count[i]
//原数据---i + min
while (count[i]--)
{
arr[index++] = i + min;
}
}
}
计数排序的特性:在数据范围集中时,效率很高,但是适用范围及场景有限。例如数组:
int arr[] = { 1, 6, 9, 1000, 100000000 };
这样,count数组的空间:max - min + 1
太多,会造成更多的空间浪费掉,这种情况下使用计数排序就不太好。
时间复杂度O(N + range)
空间复杂度O(range)
三、排序算法复杂度及稳定性分析
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。