快速排序解释

快速排序 Quick Sort 与归并排序一样,也是典型的分治法的应用。 (如果有对 归并排序还不了解的童鞋,可以看看这里哟~ 归并排序)

快速排序的分治模式

1、选取基准值,获取划分位置。将原数组 a[l, r] 划分为两个子数组 a[l, mid - 1]a[mid + 1, r]。在前一个数组中所有元素都小于等于 a[mid],后一个数组中所有元素都大于等于 a[mid]。而此时的 a[mid] 的值就是我们所取的基准值,mid 就每次划分的位置;

2、递归调用快速排序函数,分别对两个子数组 a[l, mid - 1]a[mid + 1, r] 排序;

3、快速排序我们是在原数组上进行操作的,所以我们并不需要合并,最后 a[l, r] 已经有序。

快速排序的三种写法

快速排序比较普及的有三种写法,分别是 左右指针法 挖坑法前后指针法。主要是取得划分位置实现的方法有所不同

接下来会逐个介绍这三种快速排序的写法。


左右指针法

左右指针法步骤

1、首先 我们一般选取最左边的元素作为基准值 key

2、然后我们需要定义两个变量 ij

其中 i 为左指针(其实不是指针啦,只是为了方便这么叫它),初始 i = 0。 j 为右指针,初始 j = r - 1,向左遍历不断找到小于基准值 key 的元素。

3、我们先动右指针 j 向左遍历直到找到小于当前基准值 key 的元素;然后我们再动左指针 i 向右遍历直到找到大于当前基准值 key 的元素。

ij 分别找到它们要找的元素时,我们需要将两个元素进行位置交换。( 在这个过程中我们要保持 i < j )

4、重复步骤3,直到最后我们可爱的左右指针相遇,这时我们再将基准值 key,放到这两个指针指向的位置。此时我们就得到了当前划分的位置,基准值 key 也完成了归位。

左右指针法移动指针先后顺序问题

在上述步骤中,有些人会感到疑惑,那就是我们再移动指针时,每次都要 先移动右指针再移动左指针。为什么呢?

在取基准值时,我们一般都是将序列的最左边位置的元素作为基准值。我们每次交换完元素后,左右指针都会继续寻找他们要找的值,观感上就是相互靠近。而问题就出在我们退出循环的那一刻

我们一直保持 i < j,也就是说,我们会在 i == j 时退出循环。假设 在某次交换之后,此时 j 指向的是交换之后的一个大于基准值的元素,如果我们先动左指针 i 去寻找一个大于基准值的元素,然鹅还未找到就已经和右指针 j 相遇了,这个时候我们需要退出循环,交换基准值 key 到当前两个指针指向的位置。

但是!!!,此时 ij 指向的是他喵的大于基准值的元素,那么我们进行交换基准值位置操作后,这个大于基准值的元素就被换到了序列的最左端,很明显,这时候出现了非常非常非常严重的错误。

那如果我们先动右指针 j,去寻找一个小于基准值的元素,然鹅没有找到就已经和左指针 i 相遇了,这个时候退出循环,ij 指向的一定是一个小于等于基准值的值。

究其原因,这其实是我们取最左边的元素作为基准值导致的。我们需要保证每次交换过来的元素是小于等于基准值的,所以我们先动右指针再动左指针


左右指针法动态演示

我们以序列 [3,14,6,1,2,17,7] 为例进行动态演示。(后面还会有先动左指针的错误演示)

左右指针法正确演示

左右指针法错误演示


左右指针法核心代码

//左右指针法
int Partition_Hoare(vector<int> &a, int left, int right){
int i = left;
int j = right;
int key = a[left]; while(i != j){
while(i < j && a[j] >= key) //向左找到小于基准值的值的下标
j--;
while(i < j && a[i] <= key) //向右找到大于基准值的值的下标
i++;
swap(a[i], a[j]);
}
/* i等于j时跳出循环 当前基准值此时在下标为i的位置(合适的位置) */
swap(a[left], a[i]); //最左边的元素变为处于当前合适位置的元素,把基准值放在合适位置
return i; //返回合适位置(i,j都可以)
}

挖坑法

挖坑法步骤

挖坑法顾名思义,就是每次挖一个坑填入元素使其归位。和左右指针法一样,我们同样需要两个变量 ij

1、我们一般取最左边的元素为基准值,然后在基准值处挖空(挖坑)

2、先动右指针 j 向左找到一个小于基准值的元素,然后将其填入先前挖的坑中,同时 j 指向的位置也多出来一个坑位。

3、再动左指针 i 向右找到一个大于基准值的元素,然后将其填入先前挖的坑中,同时 i 指向的位置又多出来一个坑位。

4、重复上述操作,直到最后两个指针相遇,此时再在这个坑位上填入基准值,即完成了基准值的归位,也就是我们划分的位置。

(具体实现这个挖坑填坑的方式可以是元素值的覆盖,也可以是元素交换的形式,在后面的核心代码中会给出这两种方式)

挖坑法动态演示

我们以序列 [3,14,6,1,2,17,7] 为例进行动态演示。

图(1)

图(2)


挖坑法核心代码

//挖坑法1
int Partition_DigI(vector<int> &a, int left, int right){
int i = left;
int j = right;
int key = a[left]; while(i != j){
while(i < j && a[j] >= key) //必须先动右边的 因为取的基准值在左边
j--; //向左寻找直到找到比基准值小的
a[i] = a[j]; //挖坑 填入比基准值小的值 while(i < j && a[i] <= key) //再动左边的
i++; //向右寻找直到找到比基准值大的
a[j] = a[i]; //挖坑 填入比基准值大的值
}
/* i等于j时跳出循环 下标为i的位置即为合适的插入位置 */
a[i] = key; //在i,j相遇的地方填入基准值
return i;
} //挖坑法2
int Partition_DigII(vector<int> &a, int left, int right){
int i = left; //从左边开始
int j = right; //从右边开始
int key = a[left]; //将最左边的数设为基准值 while(i != j){
while(i < j && a[j] >= key) //必须先动右边的 因为取的基准值在左边
j--; //向左寻找直到找到比基准值小的
swap(a[i], a[j]); //交换
i++; //看下一个(可以省略) while(i < j && a[i] <= key) //再动左边的
i++; //向右寻找直到找到比基准值大的
swap(a[i], a[j]); //交换
j--; //看下一个(可以省略)
}
/* 一轮循环后基准值所在位置左边的数都比基准值小,右边的都比基准值大 */
/* i等于j时跳出循环 当前基准值此时在下标为i的位置(合适的位置) */
return i; //返回基准值合适位置
}

前后指针法

前后指针法步骤

前后指针法同样需要两个指针,一个是 pre,一个是 currpre 最后找到的是划分后左半部分最后一个小于基准值的元素。

1、一开始我们一般将前指针 pre 指向待排序序列的第一个元素,后指针 curr 指向 pre 后面的一个元素。

2、然后我们让 后指针 curr 去寻找小于当前基准值的元素,若找到这个元素,此时需要将这个小于基准值的元素和当前 前指针 pre 后一个的位置的元素进行交换,即与 pre + 1 位置的元素进行交换,同时前指针 pre 也往后移动一步。

3、在上述过程中我们需要保持 curr <= r,当 curr > r 退出循环时,需要将当前 pre 所指向的元素和基准值进行交换。最后返回这个pre,即为我们划分的位置。


前后指针法动态演示

我们以序列 [3,14,6,1,2,17,7] 为例进行动态演示。

图(1)

图(2)


前后指针法核心代码

//前后指针法
int Partition_PreAndCurr(vector<int> &a, int left, int right){
int pre = left; //pre最后找到左半部分最后一个小于基准值的元素
int curr = pre + 1; //curr找到pre指向元素之后的下一个小于基准值的元素
int key = a[left]; while(curr <= right){
if(a[curr] < key && ++pre != curr) //当a[curr]>=key时pre保持不动,直到最后curr找到下一个小于基准值的元素
swap(a[curr], a[pre]); //pre往后移动到其下一个位置,并交换前后指针指向的元素
curr++;
}
swap(a[left], a[pre]); //最后将基准值和此时处于pre位置的小于基准值的元素交换
return pre;
}

快速排序时间复杂度

假设时间复杂度为 T(n)。在进行取得划分位置上我们需要消耗 O(n) 的时间,在递归调用快速排序函数上,最好情况下如果每次我们取得的划分位置为n/2的位置,即消耗了 2T(n/2) 的时间。最坏情况下如果每次我们取得的划分位置为首或尾的位置,即消耗 T(n-1)+T(0)的时间。

故时间复杂度如下

最优时间复杂度

最坏时间复杂度


取基准值优化

对于取基准值,我们发现 如果当前所取的基准值恰好是当前序列中最小的数或者最大的数,那么我们的划分位置必然是首或尾的位置,如果每次都这个亚籽,我们消耗的时间会非常的多,这显然就是最坏情况下的时间复杂度的状况。

那么我们如何尽量地避免这件事呢?

我们自然会想到随机取数,这固然是一个很好的办法,但是这个世界充满了无限可能性。假如我们原先最左边的元素并不是最小或最大的一个元素,但是随机取到的元素却恰好是那个最小或最大的元素呢?

(当然随机取数这个方法也是值得尝试的,后续程序中我不会用这个方式实现取基准值的优化,如果有需要,只需要把取基准值的几行代码改成如下就可以了)

int random = rand() % (right - left + 1) + left;
swap(a[random], a[left]);
int key = a[left]

为了避免这种充满戏剧性的情况,我们采取 三数取中 的方法取基准值。

三数取中,就是取序列中的最左边,中间,以及最右边三个数进行比较,以这三个数大小为中的元素作为基准值。这样可以更大程度上避免划分位置不好的情况,即使其并不能彻底避免,但是是一个很好地办法。


取基准值优化核心代码

int GetMid(int a[], int left, int right){
int mid = (left + right)>>1;
if(a[left] < a[mid])
if(a[mid] < a[right])
return mid; //mid为中间值
else
if(a[left] < a[right])
return right; //right为中间值
else
return left; //left为中间值
else
if(a[mid] > a[right])
return mid; //mid为中间值
else
if(a[left] > a[right])
return right; //right为中间值
else
return left; //left为中间值
}

完整程序源代码

#include<iostream>
#include<vector>
#include<time.h>
using namespace std; /* 1 */
//左右指针法
int Partition_Hoare(vector<int> &a, int left, int right){
int i = left;
int j = right;
int key = a[left]; while(i != j){
while(i < j && a[j] >= key) //向左找到小于基准值的值的下标
j--;
while(i < j && a[i] <= key) //向右找到大于基准值的值的下标
i++;
swap(a[i], a[j]);
}
/* i等于j时跳出循环 当前基准值此时在下标为i的位置(合适的位置) */
swap(a[left], a[i]); //最左边的元素变为处于当前合适位置的元素,把基准值放在合适位置
return i; //返回合适位置(i,j都可以)
} /* 2 */
//挖坑法1
int Partition_DigI(vector<int> &a, int left, int right){
int i = left;
int j = right;
int key = a[left]; while(i != j){
while(i < j && a[j] >= key) //必须先动右边的 因为取的基准值在左边
j--; //向左寻找直到找到比基准值小的
a[i] = a[j]; //挖坑 填入比基准值小的值 while(i < j && a[i] <= key) //再动左边的
i++; //向右寻找直到找到比基准值大的
a[j] = a[i]; //挖坑 填入比基准值大的值
}
/* i等于j时跳出循环 下标为i的位置即为合适的插入位置 */
a[i] = key; //在i,j相遇的地方填入基准值
return i;
} //挖坑法2
int Partition_DigII(vector<int> &a, int left, int right){
int i = left; //从左边开始
int j = right; //从右边开始
int key = a[left]; //将最左边的数设为基准值 while(i != j){
while(i < j && a[j] >= key) //必须先动右边的 因为取的基准值在左边
j--; //向左寻找直到找到比基准值小的
swap(a[i], a[j]); //交换
i++; //看下一个(可以省略) while(i < j && a[i] <= key) //再动左边的
i++; //向右寻找直到找到比基准值大的
swap(a[i], a[j]); //交换
j--; //看下一个(可以省略)
}
/* 一轮循环后基准值所在位置左边的数都比基准值小,右边的都比基准值大 */
/* i等于j时跳出循环 当前基准值此时在下标为i的位置(合适的位置) */
return i; //返回基准值合适位置
} /* 3 */
//前后指针法
int Partition_PreAndCurr(vector<int> &a, int left, int right){
int pre = left; //pre最后找到左半部分最后一个小于基准值的元素
int curr = pre + 1; //curr找到pre指向元素之后的下一个小于基准值的元素
int key = a[left]; while(curr <= right){
if(a[curr] < key && ++pre != curr) //当a[curr]>=key时pre保持不动,直到最后curr找到下一个小于基准值的元素
swap(a[curr], a[pre]); //pre往后移动到其下一个位置,并交换前后指针指向的元素
curr++;
}
swap(a[left], a[pre]); //最后将基准值和此时处于pre位置的小于基准值的元素交换
return pre;
} /* 4 */
//取基准值的优化 三数取中
int GetMid(int a[], int left, int right){
int mid = (left + right)>>1;
if(a[left] < a[mid])
if(a[mid] < a[right])
return mid; //mid为中间值
else
if(a[left] < a[right])
return right; //right为中间值
else
return left; //left为中间值
else
if(a[mid] > a[right])
return mid; //mid为中间值
else
if(a[left] > a[right])
return right; //right为中间值
else
return left; //left为中间值
} //左右指针法 三数取中优化
int Partition_Better(vector<int> &a, int left, int right){
int pos = GetMid(a, left, right);
swap(a[pos], a[left]); //将取得的基准值和第一个位置交换 int i = left;
int j = right;
int key = a[left]; while(i != j){
while(i < j && a[j] >= key)
j--;
while(i < j && a[i] <= key)
i++;
swap(a[i], a[j]);
} swap(a[i], a[left]);
return i;
} /*******************************************************************/
void Quick_Sort(vector<int> &a, int left, int right){
if( left >= right )
return; //int i = Partition_Hoare(a, left ,right); //分割 C(n) = n - 1
//int i = Partition_Dig(a, left, right);
//int i = Partition_DigII(a, left, right);
//int i = Partition_PreAndCurr(a, left, right); int i = Partition_Better(a, left, right);
Quick_Sort(a, left, i - 1); //递归左边的部分
Quick_Sort(a, i + 1, right); //递归右边的部分
} void show(vector<int> &v){
for(auto &x : v)
cout<<x<<" ";
cout<<endl;
} main(){
vector<int> v;
srand((int)time(0));
int n = 50;
while(n--)
v.push_back(rand() % 100 + 1);
show(v); Quick_Sort(v, 0, v.size() - 1); cout<<endl<<endl;
show(v);
}

程序运行结果图

[排序算法] 快速排序 (C++) (含三种写法)的更多相关文章

  1. 数据结构和算法(Golang实现)(25)排序算法-快速排序

    快速排序 快速排序是一种分治策略的排序算法,是由英国计算机科学家Tony Hoare发明的, 该算法被发布在1961年的Communications of the ACM 国际计算机学会月刊. 注:A ...

  2. javascript数组去重算法-----4(另一种写法__2)

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  3. javascript数组去重算法-----4(另一种写法)

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. 链接属性rel=’external’、rel=’nofollow’、rel=’external nofollow’三种写法的区别

    链接属性rel='external'.rel='nofollow'.rel='external nofollow'三种写法的区别   大家应该都知道rel='nofllow'的作用,它是告诉搜索引擎, ...

  5. jquery 在页面中三种写法

    jQuery 分 2 个系列版本 1.x 与 2.x,主要的区别在于 2.x 不再兼容 IE6.7.8浏览器,这样做的目的是为了兼容移动端开发.由于减少了一些代码,使得该版本比 jQuery 1.x ...

  6. 总结 React 组件的三种写法 及最佳实践 [涨经验]

    React 专注于 view 层,组件化则是 React 的基础,也是其核心理念之一,一个完整的应用将由一个个独立的组件拼装而成. 截至目前 React 已经更新到 v15.4.2,由于 ES6 的普 ...

  7. insert into 语句的三种写法

    insert into 语句的三种写法 方式1. INSERT INTO t1(field1,field2) VALUES (v001,v002);            // 明确只插入一条Valu ...

  8. HTML颜色的三种写法

    颜色的三种写法: 1.16进制代码     #000000 2.英文字母         red 3.rgba                rgba(0-255,0,0,0-1) 例如: <b ...

  9. (转)Ext.Button点击事件的三种写法

    转自:http://maidini.blog.163.com/blog/static/377627042008111061844345/ ExtJs的写法太灵活了,现在收集了关于Button点击事件的 ...

  10. setInterval()的三种写法

    前言: setInterval("fun()",time)有两个参数:fun()为要执行的函数:time为多久执行一次函数,单位是毫秒: 我们做一个简单的例子,就是每隔5s弹出一个 ...

随机推荐

  1. K8S_三种Port区别总结

    nodePort: 外部流量访问K8S集群中Service入口的一种方式 比如外部用户要访问k8s集群中的一个Web应用,那么我们可以配置对应service的type=NodePort,nodePor ...

  2. dp-LIS LCS 模型

    最长上升子序列问题: https://www.cnblogs.com/sxq-study/p/12303589.html 一:两遍LIS问题 1:题目: 怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为 ...

  3. Python 第四次实验

    1.(程序设计)定义函数def GetRandomChar(),返回一个随机的数字或大写或小写字母,每个符号出现的机会相等.调用该函数8次,生成并输出一个8位的验证码.用户输入该验证码,如果验证码正确 ...

  4. MasaFramework -- 锁与分布式锁

    前言 什么是锁?什么是分布式锁?它们之间有什么样的关系? 什么是锁 加锁(lock)是2018年公布的计算机科学技术名词,是指将控制变量置位,控制共享资源不能被其他线程访问.通过加锁,可以确保在同一时 ...

  5. 使用SSH连接Windows Server

    之前发过一篇在Windows Server上启用SSH服务器的文章.最近正好有这个需求,需要使用密钥免密登录服务器,试了一下,发现之前的方法不行了.需要再修正一些文件权限. 需要使用Repair-Au ...

  6. Kafka开启SASL认证 【windowe详细版】

    一.JAAS配置 Zookeeper配置JAAS zookeeper环境下新增一个配置文件,如zk_server_jass.conf,内容如下: Server { org.apache.kafka.c ...

  7. 第六章:Django 综合篇 - 17:CSRF与AJAX

    CSRF(Cross-site request forgery)跨站请求伪造,是一种常见的网络攻击手段,具体内容和含义请大家自行百度. Django为我们提供了防范CSRF攻击的机制. 一.基本使用 ...

  8. 8.X 的安装配置简化体现

    文章转载自:https://mp.weixin.qq.com/s/CEYYKUub81mk59B3lqAcBA 节点无需任何安全配置,即可实现 TLS 加密通信.Https 加密通信. TLS 应用于 ...

  9. Traefik SRE 之使用 Prometheus 进行监控报警

    当我们使用 Traefik 作为 Kubernetes 的 Ingress 控制器的时候,我们自然也非常有必要对其进行监控.本文我们将探讨如何使用 Prometheus 和 Grafana 从 Tra ...

  10. Elasticsearch之集群角色类型

    角色划分 在Elasticsearch中,有很多角色,常用的角色有如下: Master Node:主节点 Master eligible nodes:合格节点 Data Node:数据节点 Coord ...