目录
1.常见排序算法

前面一讲已经讲解了插入排序和选择排序,这一讲我将讲解交换排序,但是有些算法的实现将依靠之前的数据结构知识,所以如果想要看懂这些算法的实现需要打牢数据结构的基础。每一个算法都需要一定的测试用例,所以我们需要给定一些测试用例来测试该算法是否适用于每一个算法,我们现在给定一些测试用例:1 2 3 4 5 6 7 8 9;9 8 4 2 4 1 3 7 5 ;2 3 4 3 4 2 7 9 2 4;100 101 195 124 125 129;以上四种测试用例对于不同的排序算法每一个测试用例可能会表现出不同的时间复杂度,每一个测试用例也有每一个测试用例对应的时间复杂度最好的一种或几种算法,我们在实现排序时我们需要一些预定的函数,如:交换函数,测试运行时间的函数等。
2.排序算法的预定函数
2.1交换函数
我们需要两个整型类型的指针(之后我们排序排列的都是整型),我们暂时不用typedef了,否则这样比较难理解。
cs
//交换函数
void Swap(int* a, int* b)
{
int p = *a;
*a = *b;
*b = p;
}
2.2测试算法运行时间的函数
这个函数我们权当只是用来测试每一个排序算法最终在实际运用时的表现,我们先进行自己的测试用例的测试,代码无误了再进行实际运用的测试。我们每一次学完这个算法就把这个算法的注释删除即可。
cs
// 测试排序的性能对⽐
void TestOP()
{
srand(time(0));
const int N = 100000;
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);
int* a3 = (int*)malloc(sizeof(int) * N);
int* a4 = (int*)malloc(sizeof(int) * N);
int* a5 = (int*)malloc(sizeof(int) * N);
int* a6 = (int*)malloc(sizeof(int) * N);
int* a7 = (int*)malloc(sizeof(int) * N);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
a7[i] = a1[i];
}
int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
int begin3 = clock();
SelectSort(a3, N);
int end3 = clock();
int begin4 = clock();
HeapSort(a4, N);
int end4 = clock();
int begin5 = clock();
QuickSort(a5, 0, N - 1);
int end5 = clock();
int begin6 = clock();
MergeSort(a6, N);
int end6 = clock();
int begin7 = clock();
BubbleSort(a7, N);
int end7 = clock();
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
printf("QuickSort:%d\n", end5 - begin5);
printf("MergeSort:%d\n", end6 - begin6);
printf("BubbleSort:%d\n", end7 - begin7);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
free(a7);
}
上面的排序算法除了冒泡排序外我们之前基本上没学过,所以每一次我们都和之前学过的排序算法进行比较。
2.3已经实现过的排序算法
这个是为了测试之前的算法和现在实现的算法的比较,我们已经讲解过了插入排序,所以下面代码列出了插入排序的两个代码和选择排序的两个代码:
cs
//直接插入排序(升序)
void InsertSort1(int* arr, int n)
{
for (int i = 0; i < n - 1; i++)
{
//我们把需要比较的数放入tmp中,然后先与第i个数据比较,依次往前进行比较插入
int end = i;
int tmp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];
end--;
}
else
{
break;
}
}
//如果end=-1我们不能进行数组越界,所以要进行+1操作
arr[end + 1] = tmp;
}
}
//希尔排序
void ShellSort1(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;
}
}
}
//直接选择排序
void SelectSort1(int* arr, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int max, min;
max = min = begin;
for (int i = begin + 1; i <= end; i++)
{
//我们从开始位置后面的数据进行遍历,原因不用解释
if (arr[i] < arr[min])
{
min = i;
}
if (arr[i] > arr[max])
{
max = i;
}
}
if (max == begin)
{
max = min;
}
Swap(&arr[min], &arr[begin]);
Swap(&arr[max], &arr[end]);
begin++;
end--;
}
}
其中堆排序代码过长,我这里就不复制了,需要的可以去看我之前的博客:数据结构初阶-堆的代码实现-CSDN博客。
3.交换排序的实现
3.1冒泡排序
冒泡排序是通过比较相邻的两个数来进行交换实现升序(降序)的过程,之前我们都有了解过,所以这里就不赘述了,我们只分析它与其他算法的对比即可,代码实现如下:
cs
//冒泡排序
void BubbleSort(int* arr, int n)
{
for (int i = 0; i < n - 1; i++)
{
int exchange = 0;
for (int j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
exchange = 1;
Swap(&arr[j], &arr[j + 1]);
}
}
if (exchange == 0)
{
break;
}
}
}
排序之后的结果如下:




冒泡排序在实际应用中的表现如何呢?
我们发现冒泡排序直接用了79s,等得我都觉得我的代码有问题了,这个算法也是非常的烂,不像我们的堆排序也是只有7ms啊,这种算法太低端了,所以之后不要总用冒泡排序了!
3.2快速排序
基本思想:任取待排序元素序列中的一个元素作为基准值,按照该排序码讲待排序的集合分割成量子序列,左子序列的所有元素均小于基准值,右子序列中的所有元素均大于基准值,然后分割出的左子序列和右子序列重复该过程,这类似于我们的二叉树,需要用到递归思想,但是我们也有非递归的快速排序,只是递归排序代码比较简单而已。
3.2.1递归的快速排序
3.2.1.1hoare版本的排序
我们默认基准值为第一个元素,然后创建两个指针,一个从左到右找比基准值大的,一个从右往左找比基准值小的,然后把二者交换,left++,right--直至相遇,所以最终代码如下:
cs
//hoare版本找基准值排序
int _QuickSort(int* arr, int left, int right)
{
int key = left;
left++;
while (left <= right)
{
//right从右往左找小
//并且不能让left>right否则会越界
//第一个循环不能省略这个条件,可能在第一个循环的遍历过程中出现right<=left
while (arr[right] > arr[key] && left <= right)
{
right--;
}
//left从左往右找大
while (arr[left] < arr[key] && left <= right)
{
left++;
}
if (left <= right)
{
Swap(&arr[left++], &arr[right--]);
}
}
Swap(&arr[key], &arr[right]);
return right;
}
//快速排序
void QuickSort(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
//找基准值并进行排序
int key = _QuickSort(arr, left, right);
//把数组分为三部分: [left,key-1] key [key+1,right]
QuickSort(arr, left, key - 1);
QuickSort(arr, key + 1, right);
}
我们运行后结果如下:

发现排序没有问题,我们要理解这个思想:
为什么不在内存循环中加一个=呢?
如果我们加个等号,我们假设有这个数组:1 8 1 8 1 8 1 8 1 8我们第一次循环结束后,没交换的情况下,right=1,left=1,所以我们相遇后最终返回的是下标0(后面要--),所以这样的排序很烂,直接用相等的时候也结束循环是可以的。
我们来测试一下hoare版本的快速排序算法,我们可以把模式改为Release版本,因为这样我们就不需要调试了,我们已经验证过代码无误了(等待的时间很久,主要是冒泡排序花的时间太久了),结果如下:

这个快速排序用了21ms,竟然比堆排序的运行时间还久!主要是这个快排算法需要两层循环,如果在最坏的情况下时间复杂度达到了O(n^2),最坏的情况当然是数组已经排成升序,每一次循环都直接导致了第二个循环都没开始就结束了,递归了n次又加上循环了n次,所以导致时间较久,但相对于堆排序至少代码简单多了,这个已经算初级的快速排序了,还有很多。
3.2.1.2挖坑法
思路:创建左右指针,从右往左找出比基准值小的数据,找到后立即放入左边坑中,当前位置成为新的"坑",然后从左往右找出比基准值大的数据,找到后立即放入右边的坑中......依次类推,直至左>右为止,把基准值放入最后一个坑中,返回最后一个坑的下标,这个算法只是改变了找基准值的方法,并且我们需要再循环中找的是小的和大的,不需要找相等的,这相对于另外一个算法就循环次数少一些,因为我们可以每次循环使更多的数据被交换。代码实现如下:
cs
//挖坑法找基准值排序
int _QuickSort(int* arr, int left, int right)
{
int hole = left;
int key = arr[hole];
while (left < right)
{
while (left < right && arr[right] >= key)
{
right--;
}
arr[hole] = arr[right];
hole = right;
while (left < right && arr[left] <= key)
{
left++;
}
arr[hole] = arr[left];
hole = left;
}
arr[hole] = key;
return hole;
}
//快速排序
void QuickSort(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
//找基准值并进行排序
int key = _QuickSort(arr, left, right);
//把数组分为三部分: [left,key-1] key [key+1,right]
QuickSort(arr, left, key - 1);
QuickSort(arr, key + 1, right);
}
我们遇到几个问题,我在这里解释一下:
为什么开始的时候不用left++,这样不会多循环一次吗?
假设第一个元素(基准值)就是最小值,如果此时left++,那么会导致我们最终hole到达了下标为1 的位置,这样我们就会导致第一步或者前面出错,所以不能这样。
为什么不要在相等的时候交换?
这样根本没有意义,这个hole改来改去结果一点意义都没有,放到前面和后面的hole没有多少区别,而且这样循环也会变多,没必要。
为什么不在相等的时候继续循环?
如果hole==0那么就会导致最终结果越界了,所以我们不能这样做,我们最终一个元素不用排序。
我们来测试一下这个方法的实际应用中的表现吧!最终运行结果如下:

可能这个随机数有些乱,也是直接无语了好吧,这个冒泡排序都用了80s,但是这个快速排序还是比较快的,所以我们说快速排序还算快的一种算法,实现也比较简单!
3.2.1.3lomuto前后指针法
初始时prev指向序列开头,cur指向prev指针的后一个位置,cur指向的数据比基准值要小的情况下,我们让prev++,prev和cur数据交换,再cur++;如果cur只想的数据不小于基准值,则cur++。这个方法也是比较难理解,主要是我们要找到的是小于的位置,而prev位置一直是小于基准值的位置,最后我们让基准值和prev位置的数据交换,思想都差不多,代码如下:
cs
// lomuto前后指针法
int _QuickSort(int* arr, int left, int right)
{
int prev = left;
int cur = prev + 1;
//key也可以不创建,之后又不会发生改变,只是方便理解而已
int key = left;
while (cur <= right)
{
if (arr[cur] < arr[key] && ++prev != cur)
{
//我们如果把一个位置的数进行交换是没必要的
//并且如果把++prev放到前面就会先执行该语句会出错
Swap(&arr[cur], &arr[prev]);
}
++cur;
}
Swap(&arr[prev], &arr[key]);
return prev;
}
//快速排序
void QuickSort(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
//找基准值并进行排序
int key = _QuickSort(arr, left, right);
//把数组分为三部分: [left,key-1] key [key+1,right]
QuickSort(arr, left, key - 1);
QuickSort(arr, key + 1, right);
}
最终运行结果如下:




我们来测试它在实际应用中的表现:

可能是我的电脑没电了还是什么,运行也是越来越慢了,但是快速排序还是一如竟往的快,但是这个冒泡排序也太慢了。
3.2.2非递归版本的快速排序
这个版本的快速排序需要借助数据结构-栈,至于栈的东西我之前写过博客,需要的直接去看:数据结构初阶-栈及其应用-CSDN博客 。主要思想如下:我们开始先把数组的right入栈,再把数组的left入栈,然后出栈,先出的是begin,后出的是end,然后和之前的找基准值的方法一样,只不过最终我们是用循环来实现的,其中,如果key+1>right则越界了,不要入栈,无右序列,若left==right,则只有一个元素,不要入栈,同理,若key-1<begin也不用入栈,直至栈为 空循环结束,最终代码如下:
cs
// 之前没有在代码中实现这个函数,所以在这里添上
// 取栈顶元素
STDataType StackTop(ST* ps)
{
assert(!StackEmpty(ps));
return ps->arr[ps->size - 1];
}
// 非递归版本的快速排序
void QuickSort(int* arr, int left, int right)
{
ST st;
SLInit(&st);
//先让right和left入栈,防止栈为空
StackPush(&st, right);
StackPush(&st, left);
while (!StackEmpty(&st))
{
//取栈顶元素
int begin = StackTop(&st);
//要出栈
StackPop(&st);
int end = StackTop(&st);
StackPop(&st);
//对[begin,end]使用双指针法
int prev = begin;
int cur = prev + 1;
int key = begin;
while (cur <= end)
{
if (arr[cur] < arr[key] && ++prev != cur)
{
Swap(&arr[prev], &arr[cur]);
}
++cur;
}
Swap(&arr[key], &arr[prev]);
//记得加上这句话,之前递归的是直接返回了prev,而这里不行,我刚刚也没发现问题
key = prev;
//数组分为以下三部分:[begin,prev-1]prev [prev+1,end]
//先入右子数组
//从右至左开始入
if (prev + 1 < end)
{
StackPush(&st, end);
StackPush(&st, prev + 1);
}
//再入左子数组
if (key - 1 > begin)
{
StackPush(&st, key - 1);
StackPush(&st, begin);
}
}
STDestroy(&st);
}
结果如下:



我们来测试它在实际应用中的表现吧!结果如下:

可以看尺寸这个排序算法的时间比希尔排序时间还久,所以说之前的递归的快速排序更好一些,而且那些更容易理解,这个只能看代码而很难想出来!
4.总结
快速排序的实现方式有很多种,以后我们学习了C++等可能有更好的排序算法,总体来说快速排序的运行时间较短,是一个好的算法,非常建议大家在实战中使用该算法!下节我准备把归并排序和非比较排序直接讲了,就不和其他的算法进行比较了,只要知道这个算法就可以了,喜欢的可以一键三连哦,下节再见!