文章目录
4.选择排序
4.1 基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的 数据元素排完
4.2 排序实现
我们先来写一下,单次的排序:
int begin = 0, end = n - 1;
int maxi = begin, mini = begin;
for (int i = begin + 1; i <= end; i++)
{if (a[i] > a[maxi]){maxi = i;}if (a[i] < a[mini]){mini = i;}
}
Swap(&a[begin], &a[mini]);
if (begin == maxi)maxi = mini;
Swap(&a[end], &a[maxi]);
这里要注意的问题:
这里执行连续的交换是如果下标重叠就会出问题
也就是当maxi和mini同时选中一个下标时,就会出现一些问题
void SelectSort(int* a, int n)
{int begin = 0, end = n - 1;while (begin < end){int maxi = begin, mini = begin;for (int i = begin + 1; i <= end; i++){if (a[i] > a[maxi]){maxi = i;}if (a[i] < a[mini]){mini = i;}}//这里执行连续的交换是如果下标重叠就会出问题Swap(&a[begin], &a[mini]);if (begin == maxi)maxi = mini;Swap(&a[end], &a[maxi]);begin++, end--;}
}
5.快排
5.1 基本思想
left找比key大的数
right找比key小的数
将left和right的数交换直到相遇
最后将相遇的位置和key交换
我们会发现这个和二叉树有一些相似的,运用递归就可以解决了
5.2 递归实现快排
首先还是要实现单趟的排序:
int key = a[left];int begin = left, end = right;while (begin < end){//begin < end加判断防止直接走到下一个大的数while (begin < end && a[end] >= key){end--;}while (begin < end && a[begin] <= key){begin++;}Swap(&a[end], &a[begin]);}Swap(a[left], &a[begin]);
接着直接使用递归就可以了,和二叉树的实现方法差不多
void QuickSort(int* a, int left, int right)
{//递归结束条件if (left >= right){return;}int key = a[left];int begin = left, end = right;while (begin < end){//begin < end加判断防止直接走到下一个大的数while (begin < end && a[end] >= key){end--;}while (begin < end && a[begin] <= key){begin++;}Swap(&a[end], &a[begin]);}Swap(&a[left], &a[begin]);key = begin;//找到中间位置//分割区间[left,key-1][key+1,right]//让区间有序QuickSort(a, left, key - 1);QuickSort(a, key + 1, right);
}
5.3 快排优化
这样的快排有问题:
在同时排乱序的情况下,效率差距不大,但是当对已经有序的数组排序的时候,就显得效率太低了
这里主要的问题是每次选取的key都在数组的最前面,导致的简单问题复杂化
我们可以采取yi优化:
-
随机选key,但是有些不靠谱
-
三数取中:三个数中选不是最大也不是最小的数为key
int GetMidi(int* a, int left, int right)
{int midi = (left + right) / 2;if (a[left] < a[midi]){if (a[midi] < a[right]){return midi;}else if (a[left] < a[right]){return right;}else{return left;}}else //a[left] > a[midi]{if (a[midi] > a[right]){return midi;}else if (a[right] < a[left]){return left;}else{return right;}}
}
这样对于有序列表就排序的快了一些了:
我们还可以在优化一下:
在数据比较少的等候,仍然进行递归让整体的程序运行速度变得慢了,可与采取其余方式进行排序
svoid QuickSort(int* a, int left, int right)
{//递归结束条件if (left >= right){return;}if ((right - left) + 1 < 10){InsertSort(a + left, (right - left) + 1);}else{int midi = GetMidi(a, left, right);Swap(&a[left], &a[midi]);int key = a[left];int begin = left, end = right;while (begin < end){//begin < end加判断防止直接走到下一个大的数while (begin < end && a[end] >= key){end--;}while (begin < end && a[begin] <= key){begin++;}Swap(&a[end], &a[begin]);}Swap(&a[left], &a[begin]);key = begin;//找到中间位置//分割区间[left,key-1][key+1,right]//让区间有序QuickSort(a, left, key - 1);QuickSort(a, key + 1, right);}
}
5.4 双指针法完成快排
先来看一下思路:
- 这里让指针
prev
先走 - 指针
cur
去找到比key
小的数 - 找到比
key
小的数,此时如果prev++
和cur
不在一个位置时交换 - 当
cur
出界时将key
和prev
交换
写成代码如下:
void QuickSort02(int* a, int left, int right)
{if (left >= right){return;}int cur = left + 1;int prev = left;int key = left;while (cur <= right){if (a[cur] < a[key] && ++prev != cur){Swap(&a[cur], &a[prev]);}cur++;}Swap(&a[prev], &a[key]);key = prev;QuickSort02(a, left, key - 1);QuickSort02(a, key + 1, right);
}
5.5 快排的非递归
为什么要改成非递归呢?
如果递归的深度太深会导致栈溢出
那如何实现呢?
如图我们借助栈,来存放我们的right和left来让中间完成遍历
我们将数组分开按照深度优先遍历的方式来实现
写成代码:
int PartSort02(int* a, int left, int right)
{int prev = left;int cur = left + 1;int key = left;while (cur <= right){if (a[cur] < a[key] && ++prev != cur){Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[prev], &a[key]);return prev;
}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 key = PartSort02(a, begin, end);if (key + 1 < end)//因为需要先选begin,所以要先入end后入begin来取begin{STPush(&st, end);STPush(&st, key + 1);}if (begin < key - 1){STPush(&st, key - 1);STPush(&st, begin);}}STDestroy(&st);
}
循环每走一次就相当于之前的递归
6.归并排序
6.1 基本思想
下来看一下思路:
过程如下:
6.2 排序的实现
用递归的方法还是比较好写的:
void _MergeSort(int* a, int* tmp, int begin, int end)
{if (begin >= end){return;}int mid = (begin + end) / 2;_MergeSort(a, tmp, begin, mid);_MergeSort(a, tmp, mid + 1, end);//递归后已经有序了 归并int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int i = begin;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2]){tmp[i] = a[begin1];i++;begin1++;}else{tmp[i] = a[begin2];i++;begin2++;}}while (begin1 <= end1){tmp[i] = a[begin1];i++;begin1++;}while(begin2 <= end2){tmp[i] = a[begin2];i++;begin2++;}memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}void MergeSort(int* a, int n)
{int* tmp = malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc");return;}_MergeSort(a, tmp, 0, n - 1);free(tmp);tmp = NULL;
}
时间复杂度:N*logN
一层的时间复杂度为logN,一共有N层,所以时间复杂度为N*logN
6.3 归并排序的非递归
这里就没法用栈了,以为将其出栈后还要进行归并就还要用一个栈进行存储,就有些麻烦了
那我们直接进行循环处理
思路如下:
实现代码:
void _MergeSort(int* a, int* tmp, int left, int right)
{//递归将数组分开if (left >= right){return;}int mid = (right + left) / 2;_MergeSort(a, tmp ,left, mid);_MergeSort(a,tmp , mid + 1, right);//分开后,进行归并排序int i = left;int begin1 = left, end1 = mid;int begin2 = mid + 1, end2 = right;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2]){tmp[i] = a[begin1];i++;begin1++;}else{tmp[i] = a[begin2];i++;begin2++;}}//上面进行完,还有一组没有进行完,这里直接使用while,就不用if了while (begin1 <= end1){tmp[i] = a[begin1];begin1++;i++;}while (begin2 <= end2){tmp[i] = a[begin1];begin2++;i++;}memcpy(a + left, tmp + left, (right - left + 1) * sizeof(int));
}void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}_MergeSort(a, tmp, 0, n - 1);free(tmp);tmp = NULL;
}
但是这里会存在一些越界的问题:
- 这里针对后两种的方式我们可以采取将其不在使用归并排序了
- 针对第一种则可以将其写改一下在直接排即可
写成代码如下:
void MergeSortNonR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc");return;}int gap = 1;while (gap < n){for (int i = 0; i < n; i += 2 * gap){int begin1 = i, end1 = i + gap - 1;int begin2 = gap + i, end2 = i + gap * 2 - 1;//判断越界(第二组不存在不用归并)if (begin2 >= n){break;}if (end2 >= n)//只有第二组的end越界了,修正以下继续归并就行了{end2 = n - 1;}int j = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[j] = a[begin1];j++;begin1++;}else{tmp[j] = a[begin2];j++;begin2++;}}while (begin1 <= end1){tmp[j] = a[begin1];j++;begin1++;}while (begin2 <= end2){tmp[j] = a[begin2];j++;begin2++;}memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));}gap *= 2;}free(tmp);tmp = NULL;
}
7. 计数排序
7.1 基本思想
本质:利用count数组的自然序号排序
那么count数组的空间怎么开呢?
- 直接从最小到最大的数据来开,但是会造成空间浪费(也叫绝对映射)
- 找到最小的数,将最大的数减去最小的数,然后开空间,后再加上最小的数(相对映射)
7.2 排序实现
void CountSort(int* a, int n)
{int min = a[0], max = a[0];//选出最大最小值for (int i = 0; i < n; i++){if (a[i] < min){min = a[i];}if (a[i] > max){max = a[i];}}int range = max - min + 1;int* count = (int*)calloc( range, sizeof(int));if (count == NULL){perror("calloc fail");return;}//统计次数for (int i = 0; i < n; i++){count[a[i] - min]++;}int j = 0;for (int i = 0; i < range; i++){while (count[i]--){a[j] = i + min;j++;}}free(count);count = NULL;
}
7.3 排序的优缺点
这样的相对映射可以应对负数
时间复杂度为 O(N+range)
- 但是这个排序仅适用于整数且这些数范围要比较集中
- 而且空间复杂度也是比较高的
在满足这个条件下这个排序的性能是非常快的
8.排序总结
稳定性指的是相同值相对顺序是否一致,例如:
-
插入排序–稳定
-
希尔排序–不稳定
相同的数据预排序的时候分到不同的组,无法控制
-
选择排序–不稳定
在最后一次交换的时候,很有可能让确保不了最后的交换顺序
- 堆排序–不稳定
-
冒泡排序–稳定
-
快速排序–不稳定
key换到中间的时候也是不可控的
-
归并排序–稳定