前言
假定使用场景是传过来一个数组,我们对里面的数据进行从小到大的升序排列,博客里的例子均为升序排序
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include "Stack.h"
// 插入排序
void InsertSort(int* a, int n);
// 希尔排序
void ShellSort(int* a, int n);
// 选择排序
void SelectSort(int* a, int n);
// 堆排序
void AdjustDown(int* a, int n, int root);
void HeapSort(int* a, int n);
// 冒泡排序
void BubbleSort(int* a, int n);
// 快速排序递归实现
// 快速排序hoare版本
int PartSort1(int* a, int left, int right);
// 快速排序挖坑法
int PartSort2(int* a, int left, int right);
// 快速排序前后指针法
int PartSort3(int* a, int left, int right);
void QuickSort(int* a, int left, int right);
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right);
// 归并排序递归实现
void MergeSort(int* a, int n);
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
以下是其他测试/为了方便写的函数:
//交换两个参数的值
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//打印数组内容
void Print(int* a,int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
一、直接插入排序
思路:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
比如说当我们对一个数组进行插入排序的时候,就可以当作是在“逐个把数据插入到前面已经排好的数组中”
在这个插入的过程中,遍历的长度逐渐增大,一共是(1+2+3+…+n-1)次,所以这种排序的时间复杂度是O(n2),但由于没有开辟额外的空间,所以空间复杂度为O(1)
插入数据的规则就是:先把即将插入的数据储存起来,然后用待插入数据与数组的数据从后往前进行比较,如果比这个数据小,就让数组中的数据向后移动一位,反之则把存放的数据插入到这个位置中
就拿第二趟排序以后的一趟排序过程做演示:
要想实现多趟排序,需要注意我们待插入数据的下标依次从0/1增加到(n-1);
下面是直接插入排序的完整代码:
// 直接插入排序
void InsertSort(int* a, int n)
{
int i;
for (i = 0; i < n - 1; i++)
{
int end = i, tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
直接插入排序接收到的数组越有序,那么排序的效率就越高,当接收到的数组内容本来就是有序的话,那么它的时间复杂度仅为O(n)
二、希尔排序
希尔排序建立在直接插入排序的基础之上,性能要比直接插入高很多;
希尔排序的主要排序过程主要分为"预排序"和"直接插入排序"两个部分
之前我们提到过,直接插入排序所接收到的数据越有序,那么直接插入排序的效率就越高,希尔排序的预排序过程的意义,就在于使数组中的数据尽量有序
预排序本身也是个直接插入排序,只不过它是在数组中,每隔几个数据取一个数据,对这些被取出来的数据进行直接插入排序
下面是一个完整预排序过程的示意图:
可以发现,当gap等于1的时候,预排序过程也就随之结束,接下来希尔排序的过程就是一个简单的直接插入排序,换句话说我们还需要需要保证gap在经过数次循环的调整以后能变成1,代码如下:
// 希尔排序
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap /= 2;
//gap=gap/3+1;//这种写法效率还会高一点
int i;
for (i = 0; i < n - gap; i++)
{
int end = i, 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(n1.3)
三、选择排序
这个排序的思路是:每一次从待排序的数据元素中选出最小和最大各一个元素,存放在序列的起始位置和末尾位置,直到全部待排序的数据元素排完 。
我们可以在待排数组的开头设置一个叫begin的指针,末尾设置一个叫end的指针,这一趟跑完,把其他待排数据中最大和最小的数据选出来,再把它们放到正确的位置以后以后,begin++,end–,begin>>=end以后,循环停止,代码如下:
// 选择排序
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int i = begin;
int maxi = end;
int mini = begin;
while (i <= end)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
i++;
}
Swap(&a[maxi], &a[end]);
if (mini == end)
mini = maxi;
Swap(&a[mini], &a[begin]);
begin++;
end--;
}
}
时间复杂度为O(n2)
四、堆排序
在了解堆排序以前,如果你此前不了解堆的概念和上/下调整的话,希望你能看一下我的这篇博客:建堆算法与向上/下调整
这里面最重要的一点其实是:排升序建大堆,排降序建小堆
在建完大堆以后,堆顶数据当然是最大的,现在我们把它与堆底数据交换,然后执行向下调整,这样最大的数据就排好了,次大的数据也移动到了堆顶方便我们进一步操作,假设堆里面一共有n个元素,我们再把这个动作执行(n-1)遍,堆排序就完成了:
示意图:
代码:
void AdjustDown(int* a, int n, int root)
{
int child = root * 2 + 1;
while (child < n)
{
if (a[child] < a[child + 1] && child + 1 < n)
child++;
if(a[root]<a[child])
Swap(&a[root], &a[child]);
root = child;
child = root * 2 + 1;
}
}
//堆排序
void HeapSort(int* a, int n)
{
int i;
int end = n - 1;
for (i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
while (end >= 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
时间复杂度:O(n*logn)
此处的logn指的是log以2为底n的对数
五、快速排序
快速排序的特点在于每次至少能确定一个元素在数组中的最终位置,下面介绍的前3种思路其实大差不差,都是在一次单趟排序以后确定一个元素的最终位置,然后递归处理这个元素的左右区间
1.霍尔版本
这个时候需要把元素个数为n的数组arr开头的元素记为key,并设置指针L指向arr[0],指针R指向arr[n-1]:
步骤1:R先往前走,找到一个比key小的数也暂时停下来,L往后走,找到一个比key大的数就暂时停下来;
步骤2:交换L和R指向的值;
步骤3:重复步骤1,如果L和R相遇的时候,交换L和R的相遇位置对应元素的值的和key,反之则进行步骤二
循环往复,当L和R相遇的时候,单趟排序就结束了
key一般选最左边或者最右边的值;(key在左边则R先走)
示意图:
上面这个单趟排序完成以后,我们最初设定的key的值的最终位置就被确定了,之后的过程就需要依次对"被key分割的左右区间"各跑一次单趟排序,单趟代码如下:
int PartSort1(int* a, int left, int right)
{
int L = left;
int R = right;
int mid = GetMid(a, left, right);
Swap(&a[left], &a[mid]);
int key = left;
while (L < R)
{
while (L < R && a[R] >= a[key])
{
R--;
}
while (L < R && a[L] <= a[key])
{
L++;
}
Swap(&a[L], &a[R]);
}
Swap(&a[left], &a[L]);
return a[L];
}
由于前三种排序方法的返回值都为排好的key的地址,所以最后整合起来的写法都一样,就没有先整合到成品QuickSort里
细心的读者看到了PartSort1里面还有一个叫做GetMid的函数,这个函数是用来解决性能问题的,目的在于让key的最终位置尽可能地处在数组的中间,具体代码如下:
int GetMid(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[right])
{
if (a[mid] < a[left])
{
return left;
}
else if (a[mid] > a[right])
{
return right;
}
else
{
return mid;
}
}
else
{
if (a[mid] > a[left])
{
return left;
}
else if (a[mid] < a[right])
{
return right;
}
else
{
return mid;
}
}
}
2.挖坑法
这个方法没有像上面那样取key,而是把key位置的元素3挖出来,形成一个洞:
接下来还是R先向左移动找比3小的元素,找到以后把这个元素扔进洞里:
接下来L向右移动寻找比3大的元素,找到以后把它扔进右边的洞里:
重复上面的操作,最后L和R相遇时,把最开始挖出来的元素填到坑中:
最后返回这个hole位置所对应的下标,方便后续的递归写法,具体代码如下:
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int mid = GetMid(a, left, right);
Swap(&a[left], &a[mid]);
int key = left;
int L = left, R = right,hole=left;
while (L < R)
{
while (L < R && a[R] >= a[key])
{
R--;
}
a[hole] = a[R];
hole = R;
while (L < R && a[L] <= a[key])
{
L++;
}
a[hole] = a[L];
hole = L;
}
a[hole] = a[key];
return hole;
}
3.双指针版本
这种写法也需要设置key值和,不过两个指针的位置和上面有些不同:
规则是cur先向右走找比key小的数,如果找到了,就让prev向后移动一位,然后交换prev和cur指向位置的值
只有cur在移动过程中碰到了比key大的值,prev和cur的相对距离才能被拉开
下面是数组经过第一次有意义的元素交换以后,数组内容的变化:
反复执行上述的步骤,最终cur会越界,此时交换prev指向的元素和key对应的元素:
代码写法:
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int cur = left + 1, prev = left;
int mid = GetMid(a, left, right);
Swap(&a[left], &a[mid]);
int key = left;
while (cur <= right)
{
if (a[key] >= a[cur] && ++prev != cur)
Swap(&a[prev], &a[cur]);
cur++;
}
Swap(&a[prev], &a[key]);
key = prev;
return key;
}
以上就是所有的递归写法了,最后为了优化快速排序的性能,在为小区间排序的时候使用插入排序来减少栈的开销,至于里面是PartSort1还是2和3,就随你喜欢了
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
if ((right - left + 1) < 15)
{
// 小区间情况
InsertSort(a + left, right - left + 1);
}
else
{
int keyi = PartSort3(a, left, right);//随你喜欢
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
}
4.非递归写法
快速排序的非递归写法需要用一下栈这个数据结构;
思路就是把你想要排序的这段数组的左右下标依次入栈,把这两个下标对应的闭区间排序完以后,在把排序以后形成的子区间的4个下标依次压进栈去,这个过程要注意区间的左右顺序,还有就是不要越界:
单趟排序沿用上面说的三种就可以了
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
Stack p = {0};
StackInit(&p);
StackPush(&p, left);
StackPush(&p, right);
while (!StackEmpty(&p))
{
int R = StackTop(&p);
StackPop(&p);
int L = StackTop(&p);
StackPop(&p);
int key=PartSort3(a, L, R);
if (key - 1 > L)
{
StackPush(&p, L);
StackPush(&p, key - 1);
}
if (key + 1 < R)
{
StackPush(&p, key + 1);
StackPush(&p, R);
}
}
StackDestory(&p);
}
六、归并排序
1.递归写法
如果说快速排序的递归写法很像二叉树中的前序遍历的话,那么归并排序就对应着二叉树中的后序遍历
思路是把数组从中间分割成两组,并继续分割,直到一组只有一个元素:
然后从下往上,每两组进行有序合并:
合并到最后你会发现你一直在重复一道“合并有序数组”的oj题
代码如下:
void _MergeSort(int* arr, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
int begin1 = begin,end1=mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
_MergeSort(arr, begin1, end1, tmp);
_MergeSort(arr, begin2, end2, tmp);
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[i++] = arr[begin1++];
}
else
{
tmp[i++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = arr[begin2++];
}
memcpy(arr+begin, tmp + begin, sizeof(int)* (end - begin + 1));
}
// 归并排序递归实现
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(n * sizeof(int));
if (tmp == NULL)
{
perror("MergeSort-malloc\n");
return;
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
return;
}
2.非递归写法
rangeN这个变量用来控制下标和数组区间的下标
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("MergeSort-malloc\n");
exit(-1);
}
int i = 0;
int rangeN = 1;
while (rangeN < n)
{
for (i = 0; i < n; i += 2*rangeN)//range一定时跳到下一组
{
int begin1 = i;
int end1 = i + rangeN - 1;
int begin2 = i + rangeN;
int end2 = i + 2 * rangeN - 1;
int j = i;//
if (end1 >= n)
{
end1 = n - 1;
begin2 = n;
end2 = n - 1;
}
else if (begin2 >= n)//
{
begin2 = n + 1;
end2 = n;
}
else if (end2 >= n)//
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
}
rangeN *= 2;
memcpy(a, tmp, sizeof(int) * (n));
}
free(tmp);
tmp = NULL;
return;
}
总结
表格中的快速排序和归并排序指的是递归写法
稳定性:
在待排序的数组如果出现相同的数据,排序后如果能保持这些数据的相对不变,那我们称这种排序是稳定的