创作不易,友友们给个三连吧!!
一、堆排序
堆排序已经在博主关于堆的实现过程中详细的讲过了,大家可以直接去看,很详细,这边不介绍了
DS:二叉树的顺序结构及堆的实现-CSDN博客
直接上代码:
void AdjustDown(int* a, int n, int parent)//升序要建大堆
{
int child = parent * 2 + 1;//假设左孩子比右孩子大
while (child < n)
{
//如果假设错误,就认错
if (child + 1 < n && a[child] < a[child + 1])
child++;
//如果孩子大于父亲,交换
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
//交换完后,让原来的孩子变成父亲,然后再去找新的孩子
parent = child;
child = parent * 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--;
}
}
建堆的时间复杂度是o(N),排序的时间复杂度是(N*logN)所以堆排序的总时间复杂度是N*logN
二、冒泡排序
2.1 思想
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
2.2 冒泡排序的实现
方法:从前往后,逐一比较相邻元素,前面的大于或小于后面的则进行交换,每一轮将当前轮最大或最小的元素冒泡到当前论的最后。重复上述过程最后就是有序的。
该排序比较简单,不画图了,不懂得可以看看博主之前写的文章有介绍:
C语言:深入理解指针(2)-CSDN博客
代码实现:
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
//每一趟拍好一个,最后排完n-1个,最后一个数就没必要比了
{
for (int j = 0; j < n - i - 1; j++)//第一趟需要比较的n-1次,第二次需要比较n-2次……
//所以结束条件跟着i变化
{
if (a[j] > a[j + 1])
Swap(&a[j], &a[j + 1]);
}
}
}
2.3 冒泡排序的改进
如果比较的过程中,比到中间的时候就有序了,但是我们并不知道,这个时候一直到后面进行的比较都是无效的,所以为了优化这个情况,我们设置一个标志,如果一趟排序中没有发生交换,说明后面都有序了,这个时候就停止冒泡排序!!
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
//每一趟拍好一个,最后排完n-1个,最后一个数就没必要比了
{
int flag = 1;//假设有序
for (int j = 0; j < n - i - 1; j++)//第一趟需要比较的n-1次,第二次需要比较n-2次……
//所以结束条件跟着i变化
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
flag = 0;//如果没有发生交换,说明不符合有序
}
}
if (flag == 1)
break;
}
}
2.4 复杂度分析
时间复杂度:O(N^2)
空间复杂度:O(1)
三、快速排序
3.1 思想
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法
其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
根据区间按照基准值划分的过程,有最早的hoare版本,包括后来深入研究之后的挖坑法、前后指针法、快排法、非递归法……以下博主会用图文的方式和大家一起学习不同方法的实现!!并在这个过程中理解为什么说这中排序方法是二叉树结构。
3.2 hoare快排实现
hoare大佬最初的思想是:
1、在数组中选取一个key值,一般是选择第一个元素或者最后一个元素(我们主要这边主要研究第一个元素做key值)
2、设置两个指针,一个指针right先从往右往左找比key小的值,找到之后,另一个指针left再从左往右走找比key大的值,然后都找到后就交换两边的值,重复上述过程,最后当两个指针相遇的时候,再将相遇点的值与key点的值交换,此时key的左边都是比key小的值,key的右边都是比key大的值,这样就完成了一次选基准值的排序。
3、完成一次找基准值的排序后,使数组有序的问题通过基准值key转化成使key左边和右边都有序的问题,也就是这个key为基准,再分割成两块区间,再分别用上述方法找各自区间的key值,继续重复之前的步骤(递归),直到区间不能被分割,此时的数组就实现了有序。
以上的说法可能大家还不太能理解,我们通过画图来理解hoare大佬的思想。
我们通过上图可以理解为什么说快速排序是一种二叉树结构的排序!!这个过程和二叉树的前序遍历非常相似。既然我们知道了hoare大佬具体想怎么干,我们就明白了这个快排是用递归去实现的,当然现在问题的关键就是我们如果进行基准排序去找到key值对应的位置,下面我们来分析一趟基准排序的过程
以上就是完成一次基准排序的过程,大家会发现此时恰好key的左边是比key小的,key的右边都是比key大的,其实这并不是巧合,我们后面会分析,这边我们先实现一下一次基准排序的代码!
基准排序代码实现:
int PartSort1(int* a, int left, int right)//hoare基准排序
{
int keyi = left;
while (left < right)
{
//右先找比key大的
while (left < right&&a[right] >= a[keyi])
right--;
//左找比key小的
while (left < right && a[left] <= a[keyi])
left++;
//找到后,就交换
Swap(&a[left], &a[right]);
}
//此时相遇了,把相遇的位置和keyi的位置交换
Swap(&a[left], &a[keyi]);
return left;
}
根据上述的一次基准排序的实现,我们来通过递归实现整体快排的代码:
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort1(a, left, right);
//根据基准值去分割区间,继续进行基准排序
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi+1,right);
}
思考以下问题:
1、怎么判断递归的结束条件呢??
2、hoare大佬是怎么确定每次相遇的时候,相遇点的位置的值一定比key的值小呢??
3、left可以从第二个位置开始走吗?
通过上述的分析,相信大家对hoare大佬的思想已经理解了!!
3.3 挖坑法快排实现
hoare大佬的思想真的是超凡脱俗,非常厉害,但是也有很多不容易理解的地方,后面就有人进行了改进,发明了挖坑版本的快排,虽然名字叫挖坑,但是比hoare会好理解很多,下面我们来进行挖坑版本的分析!
发明挖坑版本大佬的思想是:
1、先把keyi对应的值记住,然后在这个位置挖坑,right找大,找到了就停下来,将停下来的位置的值去填坑,然后right的位置变成新坑,left找小,找到后就把对应的值填坑,然后left成为新坑,以此类推下去,直到left和right相遇,将之前记住keyi的值填入坑内,这个时候就完成了一次基准排序。
2、除了基准排序的方法和hoare大佬不同,其余地方都是一样的,也是用递归去解决。
通过这个方法,我们来实现对应的基准排序算法
int PartSort2(int* a, int left, int right)//挖坑基准排序
{
int key = a[left];//记住key的值
int hole = left;//开始挖坑
while (left < right)
{
//右先找比key大的
while (left < right && a[right] >= key)
right--;
//找到后,填坑,然后挖新坑
a[hole] = a[right];
hole = right;
//左找比key小的
while (left < right && a[left] <= key)
left++;
//找到后,填坑,然后挖新坑
a[hole] = a[left];
hole = left;
}
//此时相遇了,把key值放在坑里
a[hole] = key;
return hole;
}
整体实现:
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort2(a, left, right);
//根据基准值去分割区间,继续进行基准排序
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi+1,right);
}
我们发现挖坑和填坑的思想特别好理解!!然后接下去就是前后指针版本快排 !!
3.4 前后指针法快排实现
发明前后指针版本大佬的思想是:
1、设置前指针prev,指向首元素,后指针cur指向第二个元素,如果cur开始找小,如果没找到小,就一直往前走,如果找到小的了,就先停下来,然后prev往前走一步,在和cur交换值,然后cur接着走……重复上述步骤,最后cur走出数组返回后,循环终止!此时prev指向的位置和keyi的位置交换。
下面我们来实现前后指针快排的基准排序
int PartSort3(int* a, int left, int right) //前后指针基准排序
{
int prev = left;
int cur = left + 1;
int keyi = left;
while (cur <= right)//cur走出数组循环停止
{
//cur一直在走,如果遇到比keyi小的,就停下来等perv走一步后交换,再接着走
if (a[cur] < a[keyi])
{
++prev;
Swap(&a[prev], &a[cur]);
}
cur++;
}
//cur出去后,prev的值和keyi交换
Swap(&a[keyi], &a[prev]);
return prev;
}
因为prev和cur指向一个位置的时候,其实没有交换的必要,所以可以将上面代码的优化一下
int PartSort3(int* a, int left, int right) //前后指针基准排序
{
int prev = left;
int cur = left + 1;
int keyi = left;
while (cur <= right)//cur走出数组循环停止
{
//cur一直在走,如果遇到比keyi小的,就停下来等perv走一步后交换,再接着走
if (a[cur] < a[keyi]&&++prev!=cur)
Swap(&a[prev], &a[cur]);
cur++;
}
//cur出去后,prev的值和keyi交换
Swap(&a[keyi], &a[prev]);
return prev;
}
整体快排的实现:
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort3(a, left, right);
//根据基准值去分割区间,继续进行基准排序
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi+1,right);
}
3.5 快排的复杂度分析
时间复杂度:O(logN*N)
如果使用了三数取中的优化方法(后面会介绍),那么快排就不可能出现O(N^2)的情况,所以最后时间复杂度是O(logN*N)
空间复杂度:O(logN)
因为递归是要开销栈帧的,空间可以重复利用而时间不可重复利用。所以这里至多递归到他的深度即h = logN,所以他的空间复杂度是O(logN)
3.6 快排的优化
快排优化的过程源于这道OJ题
力扣:排序数组
看到这题,你可能会很开心:这不就是一个排序吗?我把最厉害的快排用上肯定能过的!!
结果是这样的——
为什么呢??因为力扣的测试用例设置了一个有序的数组来针对快排(复杂度分析过原因了),本质上就是因为有序的时候前面的keyi太小了!!所以我们设置一个改进方法——三数取中,解决有序的情况
3.6.1 三数取中——针对有序
int GetMidIndex(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[right] < a[left])
return left;
else
return right;
}
else//a[left] >a[mid]
{
if (a[mid] > a[right])
return mid;
else if (a[right] > a[left])
return left;
else
return right;
}
}
封装后,我们前面三种方法的基准值排序都要加上以下代码,这样就可以确保keyi的值是中间值了
int mid = GetMidIndex(a, left, right);
Swap(&a[mid], &a[left]);
你可能觉得这样子应该能过了 ,那我们再测试一下——
结果你发现,又被针对了,有序的情况解决了,又面临着有大量重复数据的排序问题,我们来分析一下:
你会发现,重复其实跟有序的情况是一样的,每次right指针都会走到keyi的位置,这样子时间复杂度也变成O(N^2)了。所以为了解决重复的问题,我们有了三路划分的方法——
3.6.2 三路划分——针对重复
因为每次递归需要返回两个边界才行,所以没办法单独封装一个partsort函数
代码实现:
void QuickSort2(int* a, int left, int right)
{
if (left >= right)
return;
int mid = GetMidIndex(a, left, right);
Swap(&a[mid], &a[left]);
int phead = left;
int pcur = left + 1;
int ptail = right;
int key = a[left];
while (pcur <= ptail)
{
if (a[pcur] < key)
{
Swap(&a[pcur], &a[phead]);
pcur++;
phead++;
}
else if (a[pcur] > key)
{
Swap(&a[pcur], &a[ptail]);
ptail--;
}
else
pcur++;
}
QuickSort2(a, left, phead - 1);
QuickSort2(a, ptail+1,right);
}
结果我们发现,还是过不了,那究竟是哪里出问题了呢??
因为里面有一组测试用例针对了三数取中,也就是他把偏大和偏小的数都安排在恰好的位置,所以我们为了针对这种情况,将三数取中的其中一个数改成随机数,接下来我们对三数取中再优化
3.6.3 三数取中再优化
将三数取中的其中一个数的下标变成随机下标:
同时要记得在主函数加上时间种子
int GetMidIndex2(int* a, int left, int right)
{
int mid = left + (rand() % (right - left));
if (a[left] < a[mid])
{
if (a[mid] < a[right])
return mid;
else if (a[right] < a[left])
return left;
else
return right;
}
else//a[left] >a[mid]
{
if (a[mid] > a[right])
return mid;
else if (a[right] > a[left])
return left;
else
return right;
}
}
void QuickSort2(int* a, int left, int right)
{
if (left >= right)
return;
int mid = GetMidIndex2(a, left, right);
Swap(&a[mid], &a[left]);
int phead = left;
int pcur = left + 1;
int ptail = right;
int key = a[left];
while (pcur <= ptail)
{
if (a[pcur] < key)
{
Swap(&a[pcur], &a[phead]);
pcur++;
phead++;
}
else if (a[pcur] > key)
{
Swap(&a[pcur], &a[ptail]);
ptail--;
}
else
pcur++;
}
QuickSort2(a, left, phead - 1);
QuickSort2(a, ptail+1,right);
}
终于过了!!要重点理解快排的优化思想!!
3.7 非递归法快排实现
3.7.1 栈实现
我们发现无论是hoare、挖坑还是前后指针,本质区别就是基准排序方法不同,但最后还是用递归去解决的,递归虽好但是有些时候如果数据太大的话还是有可能造成栈空间不够的情况,因此我们应该研究一下如果非递归实现!!——利用栈!
利用栈实现非递归快排的大佬的思想是:
利用栈来存储基准排序需要处理的区间,每次从栈拿出需要处理的区间,再将分割的区间放进栈中,利用栈后进先出的特点,每次都会先处理左边的区间再处理右边的区间,模拟了递归的过程。
我们画个图来理解吧!!(类似二叉树的前序遍历)
非递归快排代码实现:
需要包含栈的相关代码
void QuickSortNonR1(int* a, int left, int right)
{
Stack st;
StackInit(&st);
//把区间压进去,一定要先压右区间!!
StackPush(&st, right);
StackPush(&st, left);
while (!StackEmpty(&st))
{
//将数据弹出来进行进准计算
int left = StackTop(&st);
StackPop(&st);
int right= StackTop(&st);
StackPop(&st);
//进行基准计算
int keyi = PartSort3(a, left, right);
//分割区间(left keyi-1)keyi(keyi+1,right)
//如果对应的区间还能分割,就继续压,要注意要先压后面在压前面
if (keyi + 1 < right)
{
StackPush(&st, right);
StackPush(&st, keyi+1);
}
if (keyi - 1 > left)
{
StackPush(&st, keyi-1);
StackPush(&st,left);
}
}
//会一直到栈为空,此时就拍好序了!!
StackDestory(&st);
}
3.7.2 队列实现
使用队列也是可以的!!就有点类似二叉树的层序遍历!
代码实现:
需要包含队列的一些文件
void QuickSortNonR2(int* a, int left, int right)
{
Queue q;
QueueInit(&q);
QueuePush(&q, left);
QueuePush(&q, right);
while (!QueueEmpty(&q))
{
int left = QueueFront(&q);
QueuePop(&q);
int right = QueueFront(&q);
QueuePop(&q);
int keyi = PartSort3(a, left, right);
if (keyi - 1 > left)
{
QueuePush(&q, left);
QueuePush(&q, keyi-1);
}
if (keyi + 1 <right)
{
QueuePush(&q, keyi +1);
QueuePush(&q, right);
}
}
QueueDestory(&q);
}