目录
承接上文:7大排序算法-- 直接插入,希尔,冒泡,选择 --精解_luck++的博客-CSDN博客
堆排序:
关于堆排序的讲解 大家可以看一下我的这篇文章:【数据结构】堆的建立 (时间复杂度计算-堆排序)---超细致_luck++的博客-CSDN博客
快速排序:
实际上快速排序和 冒泡排序被分在同一类:都是比较后进行交换的操作
快速排序这里我知道的有三个版本:
hoare版本:
这个版本是原始发明快速排序的人 想出来的
我们之前写排序也是先把 单趟排序 写出来 在来解决多趟排序
咋们这里的单趟排序的要求:
首先要选出一个 keyi 的 标准值
我们排序后的数组是:
左边都是小于keyi的值的数
右边都是大于keyi的值的数
大家发现这个动图的特点了嘛?
首先我们的keyi的选择 一般都是选择 最左边 或者是 最右边 的数
🥇 我们的右边的 猩猩惺惺先走 找比keyi小的数 找到停下
🥈 左边的熊猫后走 找比keyi大的数 找到停下 与猩猩的位置的数据交换
🥉 直到熊猫与猩猩相遇 再将相遇点与keyi的数据交换
这样就达到我们的目的了 大家思考一下有没有什么问题
一定会相遇?
首先他们之间相遇肯定是必然的 因为我们每次移动都是移动一边的动物 永远都只有一个动物在动所以是一定会有个动物动并又可能相遇的
为什么与keyi交换就达到目标?
① 因为 我们每次停下的都是找小的猩猩 所以相遇的时候的位置也是一个小于keyi的位置 当然这是右边的 熊猫 来与 猩猩 相遇 注意:我们的猩猩是停下来的 猩猩没停下来而相遇是下面的这些情况
② 还有一种可能就是我们的猩猩一直没找到一直往前面走 直到相遇 此时我们的熊猫是没有动的 所以相遇交换也只是原地交换
③ 还有一种可能就是我们得左右已经交换过了现在轮到右边得猩猩走 但是一直没有找到最终与左边还没有走得熊猫相遇 此时与keyi交换依旧是满足条件的 因为交换过后 我们还没来得及走的熊猫的位置已经因为交换变成一个小于keyi的值了 所以不需要担心相遇的时候交换会是一个比keyi大的数
还有一点就是我们的猩猩从右边往左走 找的是小 这样隐含的意思就是不找大呗 那么我们的猩猩就会略过大的数 让那些大的数依旧留在那边 左边找大也是同样的道理 让小的数留在左边(注意:这个也许可以帮助你理解 排升序 还是 降序)
上述就是为什么我们这样操作会达到我们的目标了
那现在大家想想为什么要我们的右边的猩猩先走呢 不能左边先走呢?
好 为了解决我们上面的问题 现在我们假设我们依然要排序 一个左小有大的数组(keyi依旧选择左边得数) 假设我们右边的熊猫先走 要排一个左小右大的数组 那我们有右边先走就是找大 把小的数留在左边 那就只右边后走找小咯 根据上面的道理类推 可以得出最终相遇得点是一个比keyi大得位置 最后与keyi交换 那么咋们这个数组不就成了 大-小-大 了嘛
总结: 如果keyi在左 那么一定是右先走 keyi在右 一定是左先走 至于找大找小 就涉及到是排升序还是降序 后面会说到哦
int keyi = left;
while (left < right)//升序
{
while (a[right] > a[keyi])
{
right--;
}
while (a[left] < a[keyi])
{
left++;
}
swap(a + right, a + left);
}
swap(a + keyi, a + right);
这个是我们初步写成的代码 还是让大家找找问题吧(大家可以对照着下面这两组数据查找)
首先我们不加 = 号的话是会死循环的 大家看:
我们的右边先走 我们找小 6小于6嘛 不小唉 那我右边不能走了 左边呢是不是也是一样 我们左边的其实位置是在keyi的位置 这样咋们不就死循环了嘛
不加限制条件就会越界(注意:这里我们已经加上):
如果不加限制条件 我们右边先走 6>1 5>1 4>1 都是大于keyi的值的 我们已经加了 = 符号了最后 1=1 right 又 减减 一次那我们的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 + right, a + left);
}
swap(a + keyi, a + right);
现在问题来了 我们单趟已经排完序了 接下来的多趟怎么排呢?
写之前大家想一想 我们单趟排序给我们带来的是什么 给我们队数组的排序提供了什么帮助?
其实结果很简单 我们不是在最后 猩猩 与 熊猫 相遇后 keyi的值与相遇点的值进行了交换嘛那我们此时相遇点的位置放的keyi的值
我们keyi的值是不是放到那里就可以不变了呢?因为它左边是比他小的 右边是比他大的数 假设我们本身排完序之后 keyi 的值要大于 5 个数 那我这里把小于它的数排到它的左边不正好就满足keyi的位置在这 5 个位置的前面了嘛 只不过左边和右边还不一定就有序了 这样我们此时keyi的值的位置就是 正确✔ 的不用再移动它了
我们提到了单趟排完之后 左边和右边是不一定有序 但是如果我们把左边和右边变得有序那我们整体是不是就有序了 大家想想是不是这个道理? 可是怎么解决左右的问题呢?
我们此时就要用到 二叉树中提到的一个 分治 的思想 可以这么理解:
就是这种一边的工作完成完 最后一起提交给上一级 上一级也是同样的操作
所以对于这种分治的思想 我们一般是采用递归来实现
我们是在我们目前第一次单趟排序后keyi的值 左边 和 右边再进行一次同样的排序 紧接着继续递归到下一层
大家看这样能理解嘛 每选出一个 keyi 那么这个keyi位置的数就会排到应该在的位置上 而他两边的数也会接近有序 也可能已经有序
我们要递归到下一层 就必须选出该层的keyi 最终结果:
当然大家要注意 我们选择区间的时候要注意 有的区间只有一个数 有的区间不存在
int PartSort1(int* a, int left, int right)//key在左那先走得一方必在右 选大选小 取决于排升序 or
{ //排降序
int keyi = left;
while (left < right)//升序
{
while (left < right && a[right] >= a[keyi])
{
right--;
}
while (left < right && a[left] <= a[keyi])
{
left++;
}
swap(a + right, a + left);
}
swap(a + keyi, a + right);
return right;
}
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);
}
这个是我们部分的栈帧展开图 希望可以帮助大家理解
🕳挖坑法:
挖坑法其实和hoare版本差不了太多 只是形式上有点差别
① 我们首先记录keyi的位置为 坑🕳
② 右边的先走(这里和hoare版本那里一样 先走的一方与keyi 相对) 找小 把数据填到 坑🕳 的位置上 让后当前小的数据(汽车)的位置设为新的 坑位🕳
③ 左边的再走 也是一样的道理 每次挪动数据 都要更新坑位🕳
④ 相遇的位置和坑的位置相同 坑的位置一直都在停留等待的那一方
我们的 挖坑法🕳 与 hoare版本在 数据移动上 是不一样的 hoare版本是两个数据交换(使用swap函数) 我们的 挖坑法🕳 不一样的是 这个是覆盖 我们首先记录keyi的值 直到最后再把它放在它该有的位子
大家思考一下 这个和hoare版本哪一个会更好一点呢?
其实都差不多 哈哈哈 区别并不大 硬要说有什么区别的话 :可能就是我们的挖坑法更好理解了把 我们不用太去关心为什么相遇的位置该是keyi等等的问题
我们只用知道 左边做keyi为坑🕳 那我们就应该找一个小的数放过去 所以我们右边的汽车先走找小填过去 此时的坑🕳更新为靠近右边的位置那我左边的摩托不就是要找个大的数填入过去嘛 最后相遇了 相遇了的位置左右两边 一边大 一边小 此时把keyi放进去不就正好满足条件了嘛
int keyi = a[left];
int pit = left;
while (left < right)
{
while (left < right && a[right] >= keyi)
{
right--;
}
a[pit] = a[right];
pit = right;
while (left < right && a[left] <= keyi)
{
left++;
}
a[pit] = a[left];
pit = left;
}
a[pit] = keyi;
注意:两个版本的时间复杂度都是O(N)
前后指针法:
这个方法较之前的两种方法 可能有点难理解一些
大家看明白这个动图了嘛 思路很简单:
① 我们的cur往前面走 找小
② 找到 ++prev 交换prev和cur的值
其实总的来讲就是我们把那些小的数往左边移动 大的数又往右边推 而夹在他们中间的数都是大于keyi的数
int PartSort3(int* a, int left, int right)
{
int prev, cur;
int keyi = left;
prev = left;
cur = left + 1;
while (cur <= right)
{
if (a[cur] <= a[keyi] && a[++prev] != a[cur])//附加的判断条件 意思是我的prev和cur中
{ //间得有值才让交换
swap(a + prev, a + cur);
}
cur++;
}
swap(a + keyi, a + prev);
return prev;
}
咋们这里的小于等于 可以没有 等于 因为相等的数 在左 在右 都是不影响的
咋们这里的限制条件: 只有小于了 并且++prev后不等于cur(就是prev不紧跟在cur后面) 才交换 就是不用自己交换自己了
如果你设置的keyi是在右边 那么我们cur就应该从第一个位置开始遍历 而prev是始终都在cur的前面的 所以 cur=left,prev=left-1;
遍历的时候最右边的数据作为 keyi 就不用去遍历它了 所以我们cur遍历到最后一个数据时 就可以结束了
还有一点要注意 我们cur与prev之间的是比keyi大的数 而keyi的位置又在右边是要放一个大的数 所以我们最后的 prev 还要再加加一次 这样才是一个大于keyi的数放过去
快排的时间复杂度:
我们这样快速排序的几个版本就说完了 现在我们来想想快排的时间复杂度 又会是多少呢?
最坏的情况其实还是存在的 就好像我们的现在的数组是已经有序的了 或者是接近有序的了 现在我们选择 最左 或 最右 的话就又可能选到 最大最小 的数了 而且这种最坏的情况是非常危险的 应为一旦数据过大 就会栈溢出了
那针对这种情况我们 该怎么解决呢 不然我们的快排在这方面就太拉了
其实很简单 就是改变我们选keyi的方式就可以了 我们之前是直接选择最左最右 现在我们不那么做了 我们再 左-中-右 选一个
int tmp = CheckMidKey(a, (right - left + 1) >> 1, left, right);
swap(a + left, a + tmp);
我们依旧还是选择左边做 keyi 只不过找到tmp 后把tmp的值和当前left的值换一下不就可以了嘛 此时left就是我们想要的数了
我们此时三数取中:
int CheckMidKey(int* a, int mid, int left, int right)
{
if (a[mid] < a[right])
{
if (a[mid] > a[left])
{
return mid;
}
else if (a[right] < a[left])
{
return right;
}
else
{
return left;
}
}
else//a[mid]>a[right]
{
if (a[mid] < a[left])
{
return mid;
}
else if (a[right] > a[left])
{
return right;
}
else
{
return left;
}
}
}
小区间优化:
对于快排的优化 还不止这一个
小区间优化: 就是当我们递归到数据已经不那么多的时候就不用递归了 直接用其他的排序来解决 因为我们数据很多时 我们递归可以很好的提高效率 毕竟时O(N*logN) 但是当我们数据变少了的时候 这个效率的提高就没那么明显了
好比我们现在一个区间只有5个数据了
所以我们采用小区间优化的话 会消去很多次的递归调用
void QuickSort(int* a, int left, int right)
{
if (left >= right) return;
if (right - left + 1<= 10)
{
InsertSort(a + left, right - left + 1);
}
int keyi = PartSort2(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
right-left 加1 是因为我们的left和 right 都是下标 下标是从0开始的所以加 1
我么的插入排序 的其实位置 是 a+left 因为区间又不一定是从头开始的 也可能是keyi右边的数 就是从当前 left 的位置开始的
这样我们的快排就说明完了 感谢大家可以看到这里!!!预祝大家都能收到心仪大厂的offer!!