快速排序
原理:
排序的基本操作是比较2个数,比如a和b,比较结果只有2种排序情况ab或ba。从比较结果来看(逆向思维),比较结果将空间分为2份,如果每次比较都能完美地二分,即二分后两边的概率是相等(即二分后左右两边处于一个平衡状态),那么对于n个数,每个数找到自己的位置,最终需要的步骤是log(n!)=O(nlogn).
方法一:hoare法
思路:
- 每轮选择一个基准元素(比如第一个)
- 将待排序的记录分割成两部分,左侧的元素值均比基准元素值小,右侧比基准值大,
- 最后,将基准元素与尾指针指向的数交换
- 然后分别对这两部分用同样的方法排序。一般基于递归实现。
图解hoare法:
C代码如下:
#include <stdio.h>
#include <stdlib.h>
void quicksort(int * a, int left, int right) {
int i, j, t, temp;
if (left > right) return;
temp = a[left]; // temp中存的就是基准数
i = left;
j = right;
while (i != j) { // 顺序很重要,要先从右边开始找
// 符合条件,往左继续找
while (a[j] >= temp && i < j) j--;
// 符合条件,往右继续找
while (a[i] <= temp && i < j) i++;
if (i < j) // 交换两个数在数组中的位置
{
t = a[i];
a[i] = a[j];
a[j] = t;
}
}
// 最终将基准数归位
a[left] = a[i];
a[i] = temp;
quicksort(a, left, i - 1); // 继续处理左边的,这里是一个递归的过程
quicksort(a, i + 1, right); // 继续处理右边的 ,这里是一个递归的过程
}
int main() {
int i;
int arr[] = { 6, 1, 2, 7, 9, 3, 4, 5, 10, 8 };
int num = sizeof(arr) / sizeof(arr[0]);
quicksort(arr, 0, num-1); //快速排序调用
printf("快速排序后的结果为:");
for (i = 0; i < num; i++){
printf("%d ", arr[i]);
}
printf("\n");
system("pause");
return 0;
}
代码生成图
方法二:挖坑法
思路:
- 取第一个数据为基准数,拿出来,把第一个位置作为坑
- 一个指针从尾开始找比其小的数,找到以后,放在坑里,该指针所在的位置作为新的坑
- 另一个指针从头开始找比key大的数,找到以后,放在坑里,该指针所在的位置作为新的坑
- 直到头尾指针相遇,然后把第一个数据放在最后一个坑里
图解挖坑法:
C代码如下:
void quicksort2(int* a, int left, int right){
if (left > right)
return;
int key = a[left];
int begin = left;
int end = right;
while (begin<end){
while (a[end]>key && begin<end){
end--;
}
a[begin] = a[end];
while (a[begin]<key && begin<end){
begin++;
}
a[end] = a[begin];
}
a[begin] = key;
quicksort2(a, left, end - 1);//继续处理左边的,这里是一个递归的过程
quicksort2(a, end + 1, right);//继续处理右边的 ,这里是一个递归的过程
}
代码生成图
方法三:前后指针法
思路:
- 取第一个数据key,让prev指向第一个数据,让cur指向第二个数据。
- cur依次遍历每一个数据,如果cur所指向的数据比key小,就把它和prev++所指向的数据交换,每交换一次,prev++
- 最后,让prev所指向的数据和key交换。
C代码如下:
void Swap(int* num1, int* num2)
{
int temp = *num1;
*num1 = *num2;
*num2 = temp;
}
void quicksort3(int * a, int left, int right){
if (left > right)
return;
int key = a[left];
int cur = left + 1;
int prev = left;
while (cur <= right){
if (a[cur]<key && ++prev != cur){
Swap(&a[cur], &a[prev]);
}
++cur;
}
Swap(&a[prev], &a[left]);//交换数据时,传的是实际数据的地址,不能用key代替。
quicksort3(a, left, prev - 1);//继续处理左边的,这里是一个递归的过程
quicksort3(a, prev + 1, right);//继续处理右边的 ,这里是一个递归的过程
}
代码生成图
算法解析:
- 快排的代码中两个while的顺序不能做调换,必须从基准数的对面开始查找。(基准值选在数组的左侧,先从右往左遍历,再从左往右遍历。反之则依然)可通过数组{ 6, 1, 2, 7, 9}自行检验。
- 快速排序是不稳定排序,时间复杂度为O(nlogn)。快速排序是通常被认为在O(nlog2n)的排序方法中平均性能最好的,但最坏情况下快速排序退化为O(n2)。
- 冒泡排序每次只调整了一个数或几个数的相对关系,而快速排序每遍都让两边保持相对关系。
相关知识点习题
- 为什么快速排序的时间复杂度为 O ( n l o g n ) 、 O ( n 2 ) O(nlogn)、O(n^2) O(nlogn)、O(n2),空间复杂度为 O ( l o g n ) — — O ( n ) O(logn) —— O(n) O(logn)——O(n)
快速排序的性能高度依赖于你选择的基准值。
- 最糟情况: O ( n 2 ) O(n^2) O(n2)
假设你总是将第一个元素用作基准值,且要处理的数组是有序的。由于快速排序算法不检查输入数组是否有序,因此它依然尝试对其进行排序。注意,数组并没有被分成两半,相反,其中一个子数组始终为空,这导致调用栈非常长。(调用栈的高度为数组的长度——栈长为 O(n))
如何优化?
针对取极值的方式进行优化 ------ 让取到极值的概率将可能的低
采取三数取中方式:最左侧、最右侧、中间,然后以三个数据最中间的数据作为基准值
- 平均情况: O ( n l o g n ) O(nlogn) O(nlogn)
假设你总是将中间的元素用作基准值,在这种情况下,调用栈的高度矮了许多!因为你每次都将数组分成两半,所以不需要那么多递归调用。你很快就到达了基线条件,因此调用栈短得多——栈长为O(log n)。
在调用栈的每层都涉及O(n)个元素,所以每层栈需要的时间为O(n)。因此整个算法需要的时间为:
- 在最佳情况下: O ( n ) ∗ O ( l o g n ) = O ( n l o g n ) O(n) * O(log n) = O(n log n) O(n)∗O(logn)=O(nlogn)
- 在最糟情况下: O ( n ) ∗ O ( n ) = O ( n 2 ) O(n) * O(n) = O(n2) O(n)∗O(n)=O(n2)
函数的每次调用需要栈空间,而栈的高度在不同情况下是不同的,所以空间复杂度为 O ( l o g n ) — — O ( n ) O(logn) —— O(n) O(logn)——O(n)
- 归并排序和快速排序的平均时间复杂度都是 O(nlogn),为什么说快速排序一般优于归并排序?
因为快速排序内存写的操作比归并排序少。
快速排序中效率的主要来源之一是引用位置,在引用位置中,计算机硬件经过优化,因此访问彼此相邻的内存位置往往比访问分散在整个内存中的内存位置更快。quicksort中的分区步骤通常具有很好的局部性,因为它访问前面和后面附近的连续数组元素。因此,快速排序往往比其他排序算法(如heapsort)执行得更好,尽管它通常执行大致相同数量的比较和交换,因为在heapsort的情况下,访问更加分散。
此外,quicksort通常比其他排序算法快得多,因为它在原地运行,而不需要创建任何辅助数组来保存临时值。 与merge sort相比,这是一个巨大的优势,因为分配和释放辅助数组所需的时间是显而易见的。就地操作也提高了quicksort的位置。
使用链表时,这两个优点都不一定适用。由于链表单元通常分散在整个内存中,因此访问相邻的链表单元没有额外的局部性好处。因此,quicksort的一个巨大的性能优势被消耗殆尽。类似地,就地工作的好处不再适用,因为merge sort的链表算法不需要任何额外的辅助存储空间。
也就是说,快速排序在链接列表中仍然非常快。合并排序往往更快,因为它更均匀地将列表分成两半,并且每次执行合并所做的工作比执行分区步骤所做的工作更少。
- 5000个数字中如何快速的找到中位数
利用快排的思想,快排可以将将他作为基准点的左右,大小分开,这样的话也不一定非要对着5000个数字排好序。
- 寻找数组中第K大的数
- 对数组A进行排序,然后遍历一遍就可以找到第K大的数字。该方法的时间复杂度为O(N*logN)
- 利用简单选择排序法的思想,每次通过比较选出最大的数字来,比较上K次就能找出第K大的数字来。该方法的时间复杂度为O(N*K),最坏情况下为O(N^2)。
- 可以利用快排的思想,首先快排每次执行都能确定一个元素的最终的位置,如果这个位置是n-k(其中n是数组A的长度)的话,那么就相当于找到了第K大的元素。设确定的元素位置m的话,如果m > n - k大的话,那么第K大的数字一定在A[0] ~ A[m - 1]之间;如果m < n - k的话,那么第K大的数字一定在A[m+1]~A[n - 1]之间。
递归实现:
#include <iostream>
#include <vector>
using namespace std;
int QuickSort(vector<int>& v, int left, int right, int k){
if(left > right) return -1;
int i = left, j = right, tmp = v[left];
while(i < j){
while(v[j] >= tmp && i < j) j--;
while(v[i] <= tmp && i < j) i++;
if(i < j) swap(v[i], v[j]);
}
v[left] = v[i];
v[i] = tmp;
if(i == k-1) return v[i];
else if(i < k-1) return QuickSort(v, i+1, right, k);
else return QuickSort(v, left, i-1, k);
}
int main(){
int arr[] = { 6, -1, 2, 7, 29, 3, 4, 5, 11, 8 };
vector<int> v(arr, arr+10);
int k = 1;
cout << "查找第K个大小的数字:";
cout << QuickSort(v, 0, 9, 10-k+1) << endl;
return 0;
}
代码生成图:
如有不同见解,欢迎留言讨论!