目录
1.插入排序
1.1基本插入排序
大致思路:
插入排序其实和我们平常打扑克相似,假如我们左手拿牌,右手摸牌,在摸到第一张牌将其放到左手上,后序每次摸到的牌和左手上的牌进行大小比较然后插入所对应的位置,最后左手手上的牌就有序了,只不过写程序时需要从后往前依次比较,没法像我们一眼就知道牌插哪里。
步骤
1.因为插入排序需要有已经被排好的数据,所以我们可以认为第一个数据已经有序,从它的后一个开始插入。
2.[0,end]是有序序列,将[end+1]处的元素tmp插入,从后往前依次比较
3.如果tmp<a[end],a[end]后移,end--,重复此步骤直到找到小于tmp的元素或者tmp就是最小元素
4.tmp置于小于它的元素前或a[0](tmp最小)
5.重复2-4
更为形象的动图:
代码如下:
void InsertSort(int* a, int n) {
for (int i = 0; i < n - 1; i++) {
//[0,end] end+1
//end以及end之前的元素有序,将end+1处的元素插入
int end = i;
int tmp = a[end + 1];
//单趟排序
while (end >= 0) {
//插入的数小于end处的数
if (tmp < a[end]) {
//end处的数后移
a[end + 1] = a[end];
end--;
}
//插入的数大于end处的数
else {
break;
}
}
//代码到此处有两种情况
//1.循环走完没有找到比tmp小的数,让a[0]=tmp;
//2.tmp > end处的数,让a[end+1]=tmp;
a[end + 1] = tmp;
}
}
时间复杂度: 最坏O(N^2)【待排数组逆序or接近逆序】
最好O(N)【待排数组有序or接近有序】
空间复杂度: O(1)
1.1.2希尔排序
大致思路:
因为我们知道插入排序当待排数组有序或接近有序时,排序的速度会快很多,所以我们可以在对数组进行插入排序之前先预排序。那如何进行预排序呢?
步骤
选定一个小于N的整数gap,将原数组分为gap组每组gap个数据,对这gap组数据依次进行插入排序,然后gap再减小重复上述步骤,当gap减小至1时就是插入排序了。(gap可以取N/2或者N/3+1)
动图:
代码如下:
void ShellSort(int* a,int n) {
int gap = n;
while (gap > 1) {
//gap/2或者gap/3+1都行
gap = gap / 3 + 1;
//单趟排序
for (int i = 0; i < n - gap; i++) {
int end = i;
int tmp = a[end + gap];
while (end >= 0) {
if (tmp < a[end]) {
a[end + gap] = a[end];
end -= gap;
}
else {
break;
}
}
a[end + gap] = tmp;
}
}
}
时间复杂度: O(N^1.3)
空间复杂度: O(1)
2.选择排序
2.1基本选择排序
大致思路:
每次从待排序列中选出一个最大值,将其放到序列的末尾位置,直到全部放完。但是我们在这个基础上完全可以再选出一个最小值放到序列的起始位置,这样排序速度比之前快了1倍。
动图:
代码如下:
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[mini]) {
mini = i;
}
if (a[i] > a[maxi]) {
maxi = i;
}
}
Swap(&a[mini], &a[begin]);
//当mini处的值为最大值时特殊处理
if (maxi == begin) {
maxi = mini;
}
Swap(&a[maxi], &a[end]);
begin++;
end--;
}
}
时间复杂度: O(N^2)
空间复杂度: O(1)
2.2堆排序
大致思路:
对原数组进行向下调整算法,建大根堆,然后每次交换堆头数据和堆尾,再使用向下调整算法把剩下的序列建大根堆,重复上述步骤直至排序完成。
关于堆的详细内容以及算法可以参考堆,向下调整算法,向上调整算法,数组建堆算法,堆排序,建堆时间复杂度的推理_数组 调整为 堆 算法-CSDN博客
代码如下:
void AdjustDown(int* a, int n, int parent) {
int child = parent * 2 + 1;
while (child < n) {
if (child + 1 < n && a[child + 1] > a[child]) {
child++;
}
if (a[child] > a[parent]) {
Swap(&a[parent], &a[child]);
parent = child;
child = child * 2 + 1;
}
else {
break;
}
}
}
void HeapSort(int* a, int n) {
for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0) {
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
3.交换排序
3.1冒泡排序
大致思路:
数组中的数据两两进行比较,把大的换到右边,一趟下来后最大的就到了右边,重复上述步骤即可完成排序。
动图:
void BubbleSort(int* a, int n) {
for (int j = 0; j < n - 1; j++) {
int flag = 0;
for (int i = 0; i < n - 1 - j; i++) {
if (a[i] > a[i + 1]) {
Swap(&a[i + 1], &a[i]);
flag = 1;
}
}
//如果一趟下来没有发生交换说明数组已有序
if (!flag) {
break;
}
}
}
3.2快速排序
Hoare版(递归)
大致思路:
选取keyi(一般取最左或最右),假如此处取最左,left为左指针,right为右指针,让right指针从后往前找小于a[keyi]的数,left从前往后找大于a[keyi]的数,因为此处选取的keyi是最左,则right先走,当right找到小的数时就停下,让left走,然后left找到大的数的时候就交换right和left处的值,之后重复上述步骤直到left>=right,再交换a[keyi]和a[left],再让keyi等于left,这样一趟下来,keyi左边的值都小于等于它,右边的值都大于等于它。此时再使用分治的思想对keyi左右两边的序列进行上述步骤。往复操作后直到只有一个数据或者无数据时则不需要再分,此时此部分可看作有序。
单趟动图:
代码如下:
//三数取中
int GetMidi(int* a, int left, int right) {
int mid = (left + right) / 2;
if (a[left] > a[mid]) {
if (a[mid] > a[right]) {
return mid;
}
else if (a[left]>a[right]) {
return right;
}
else {
return left;
}
}
else//a[left]<=a[mid]
{
if (a[left] > a[right]) {
return left;
}
else if (a[right]>a[mid]) {
return mid;
}
else {
return right;
}
}
}
void QuickSort1(int* a,int left,int right) {
if (left >= right) {
return;
}
//三数取中,防止keyi取到最小
int mid = GetMidi(a, left, right);
Swap(&a[mid],&a[left]);
int begin = left, end = right;
int keyi = left;
while (left < right) {
//右边找小
while (left<right&&a[right] >= a[keyi]) {
right--;
}
//左边找大
while (left<right&&a[left] <= a[keyi]) {
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
//[begin,keyi-1] keyi [keyi+1,end]
QuickSort1(a,begin, keyi - 1);
QuickSort1(a,keyi+1, end);
}
时间复杂度: O(N*logN)
空间复杂度: O(1)
非递归
大致思路:
需要一个栈来存放left,right,利用栈后进先出的性质,先对左区间进行排序,再对右区间进行排序,类似二叉树的先根遍历。
代码如下:
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) {
STPush(&st, end);
STPush(&st, keyi + 1);
}
if (begin < keyi - 1) {
STPush(&st, keyi - 1);
STPush(&st, begin);
}
}
STDestroy(&st);
}
挖坑法
和Hoare版的没什么差别这里就不赘述。
附上动图:
前后指针法
大致思路:
先选取keyi此处取最左,两个指针prev=left(前指针),cur=left+1(后指针),a[cur]如果大于a[keyi]则只需cur往后走一步,如果a[cur]小于a[keyi],则进行判断++prev是否等于cur,如果等于则只需cur往后走一步,如果不等于则交换a[cur]和a[prev]然后cur往后走一步,当cur>right时结束,交换a[keyi]和a[prev],这样keyi左边的数小于它,右边的数大于它,再对keyi左右进行分治即可,往复操作后直到只有一个数据或者无数据时则不需要再分,此时此部分可看作有序。
代码如下:
//前后指针快排
void QuickSort2(int* a, int left, int right) {
if (left >= right) {
return;
}
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right) {
if (a[cur] < a[keyi] && ++prev != cur) {
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
keyi = prev;
//[left,keyi-1] keyi [keyi+1,right]
QuickSort2(a,left,keyi-1);
QuickSort2(a,keyi+1,right);
}