C++快速排序的分析与优化详解

相信学过数据结构与算法的朋友对于快速排序应该并不陌生,本文就以实例讲述了C++快速排序的分析与优化,对于C++算法的设计有很好的借鉴价值。具体分析如下:

一、快速排序的介绍

快速排序是一种排序算法,对包含n个数的输入数组,最坏的情况运行时间为Θ(n2)[Θ 读作theta]。虽然这个最坏情况的运行时间比较差,但快速排序通常是用于排序的最佳的实用选择。这是因为其平均情况下的性能相当好:期望的运行时间为 Θ(nlgn),且Θ(nlgn)记号中隐含的常数因子很小。另外,它还能够进行就地排序,在虚拟内存环境中也能很好的工作。

和归并排序一样,快速排序也是基于分治法(Divide and conquer):

分解:数组A[p..r]被划分成两个(可能为空)的子数组A[p..q-1]和A[q+1..r],使得A[p..q-1]中的每个元素都小于等于A[q],A[q+1..r]中的每个元素都大于等于A[q]。这样元素A[q]就位于其最终位置上了。

解决:通过递归调用快速排序,对子数组A[p..q-1]和A[q+1..r]排序。

合并:因为两个子数组是就地排序,不需要合并,整个数组已有序。

伪代码如下:

PARTITION(A, p, r)
  x = A[p]
  i = p
  for j=p+1 to r
    do if A[j] <= x
      then i = i+1
         exchange(A[i],A[j])
  exchange(A[p], A[i])
  return i 

QUICKSORT(A, p, r)
  if p < r
    then q = PARTITION(A, p, r)
       QUICKSORT(A, p, q-1)
       QUICKSORT(A, q+1, r)

二、性能分析

1、最坏情况

快速排序的最坏情况发生在当数组已经有序或者逆序排好的情况下。这样的话划分过程产生的两个区域中有一个没有元素,另一个包含n-1个元素。此时算法的运行时间可以递归地表示为:T(n) = T(n-1)+T(0)+Θ(n),递归式的解为T(n)=Θ(n^2)。可以看出,快速排序算法最坏情况运行时间并不比插入排序的更好。

2、最好情况

如果我们足够幸运,在每次划分操作中做到最平衡的划分,即将数组划分为n/2:n/2。此时得到的递归式为T(n) = 2T(n/2)+Θ(n),根据主定理的情况二可得T(n)=Θ(nlgn)。

3、平均情况

假设一:快排中的划分点非常偏斜,比如每次都将数组划分为1/10 : 9/10的两个子区域,这种情况下运行时间是多少呢?运行时间递归式为T(n) = T(n/10)+T(9n/10)+Θ(n),使用递归树解得T(n)=Θ(nlgn)。可以看出,当划分点非常偏斜的时候,运行时间仍然是Θ(nlgn)。

假设二:Partition所产生的划分既有“好的”,也有“差的”,它们交替出现。这种平均情况下运行时间又是多少呢?这时的递归式为(G表示Good,B表示Bad):

G(n) = 2B(n/2) + Θ(n)

B(n) = G(n-1) + Θ(n)

解:G(n) = 2(G(n/2-1) + Θ(n/2)) + Θ(n) = 2G(n/2-1) + Θ(n) = Θ(nlgn)

可以看出,当好、差划分交替出现时,快排的运行时间就如全是好的划分一样,仍然是Θ(nlgn) 。

三、快排的优化

经过上面的分析可以知道,在输入有序或逆序时快速排序很慢,在其余情况则表现良好。如果输入本身已被排序,那么就糟了。那么我们如何确保对于所有输 入,它均能够获得较好的平均情况性能呢?前面的快速排序我们默认使用数组中第一个元素作为主元。假设随机选择数组中的元素作为主元,则快排的运行时间将不 依赖于输入序列的顺序。我们把随机选择主元的快速排序叫做Randomized Quicksort。

在随机化的快速排序中,我们不是始终选择第一个元素作为主元,而是从数组A[p…r]中随机选择一个元素,然后将其与第一个元素交换。由于主元元素是随机选择的,我们期望在平均情况下,对输入数组的划分能够比较对称。

伪代码如下:

RANDOMIZED-PARTITION(A, p, r)
  i = RANDOM(p, r)
  exchange(A[p], A[i])
  return PARTITION(A, p, r) 

RANDOMIZED-QUICKSORT(A, p, r)
  if p < r
    then q = RANDOMIZED-PARTITION(A, p, r)
      RANDOMIZED-QUICKSORT(A, p, q-1)
      RANDOMIZED-QUICKSORT(A, q+1, r)

我们对3万个元素的有序序列分别进行传统的快速排序 和 随机化的快速排序,并比较它们的运行时间:

/*************************************************************************
  > File Name: QuickSort.cpp
  > Author: SongLee
 ************************************************************************/
#include<iostream>
#include<cstdlib> // srand rand
#include<ctime> // clock_t clock
using namespace std; 

void swap(int &a, int &b)
{
  int tmp = a;
  a = b;
  b = tmp;
} 

// 传统划分操作
int Partition(int A[], int low, int high)
{
  int pivot = A[low];
  int i = low;
  for(int j=low+1; j<=high; ++j)
  {
    if(A[j] <= pivot)
    {
      ++i;
      swap(A[i], A[j]);
    }
  }
  swap(A[i], A[low]);
  return i;
} 

// 随机化划分操作,随机选择pivot
int Partition_Random(int A[], int low, int high)
{
  srand(time(NULL));
  int i = rand() % (high+1);
  swap(A[low], A[i]);
  return Partition(A, low, high);
} 

// 传统快排
void QuickSort(int A[], int low, int high)
{
  if(low < high)
  {
    int pos = Partition(A, low, high);
    QuickSort(A, low, pos-1);
    QuickSort(A, pos+1, high);
  }
} 

// 随机化快速排序
void QuickSort_Random(int A[], int low, int high)
{
  if(low < high)
  {
    int pos = Partition_Random(A, low, high);
    QuickSort_Random(A, low, pos-1);
    QuickSort_Random(A, pos+1, high);
  }
} 

int main()
{
  clock_t t1, t2;
  // 初始化数组
  int A[30000];
  for(int i=0; i<30000; ++i)
    A[i] = i+1; 

  t1 = clock();
  QuickSort(A, 0, 30000-1);
  t1 = clock() - t1;
  cout << "Traditional quicksort took "<< t1 << " clicks(about " << ((float)t1)/CLOCKS_PER_SEC << " seconds)." << endl; 

  t2 = clock();
  QuickSort_Random(A, 0, 30000-1);
  t2 = clock() - t2;
  cout << "Randomized quicksort took "<< t2 << " clicks(about " << ((float)t2)/CLOCKS_PER_SEC << " seconds)." << endl; 

  return 0;
}

运行结果如下:

[songlee@localhost ~]$ ./QuickSort
Traditional quicksort took 1210309 clicks(about 1.21031 seconds).
Randomized quicksort took 457573 clicks(about 0.457573 seconds).
[songlee@localhost ~]$ ./QuickSort
Traditional quicksort took 1208038 clicks(about 1.20804 seconds).
Randomized quicksort took 644950 clicks(about 0.64495 seconds). 

从运行结果可以看出,对于有序的输入,随机化版本的快速排序的效率会高很多。

问题记录:

我们知道交换两个变量的值有以下三种方法:

int tmp = a; // 方法一
a = b;
b = tmp 

a = a+b; // 方法二
b = a-b;
a = a-b; 

a = a^b; // 方法三
b = a^b;
a = a^b;

但是你会发现在本程序中,如果swap函数使用后面两种方法会出错。由于方法二和方法三没有使用中间变量,它们交换值的原理是直接对变量的内存单元进行操作。如果两个变量对应的同一内存单元,则经过两次加减或异或操作,内存单元的值已经变为了0,因而不能实现变量值交换。所以当需要交换值的变量可能是同一变量时,必须使用第三变量实现交换,否则会对变量清零。

(0)

相关推荐

  • C/C++实现快速排序的方法

    快速排序不会直接得到最终结果,只会把比k大和比k小的数分到k的两边.(你可以想象一下i和j是两个机器人,数据就是大小不一的石头,先取走i前面的石头留出回旋的空间,然后他们轮流分别挑选比k大和比k小的石头扔给对面,最后在他们中间把取走的那块石头放回去,于是比这块石头大的全扔给了j那一边,小的全扔给了i那一边.只是这次运气好,扔完一次刚好排整齐.)为了得到最后结果,需要再次对下标2两边的数组分别执行此步骤,然后再分解数组,直到数组不能再分解为止(只有一个数据),才能得到正确结果. -- 取自百度百科

  • c++实现对输入数组进行快速排序的示例(推荐)

    废话不多说,直接上代码 #include "stdafx.h" #include <iostream> #include <string> #include <vector> using namespace std; void quickSort(vector<int> &a, int, int); void swap(int &a, int&b); vector<string> split(strin

  • c++快速排序详解

    说一说快速排序 快速排序,实际中最常用的一种排序算法,速度快,效率高,在N*logN的同等级算法中效率名列前茅.· 基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分所有数据要小,然后再按此方法对这两部分数据分别进行快速排序.整个排序过程可以递归进行,以此达到整个数据变成有序序列. 将数列变成上述形式,这一步很关键,做好这一步,才能对主元左右的部分进行递归调用.以下是实现这一部分的代码: int partition_sort(int arr[],int l

  • C++ 先对数组排序,在进行折半查找

    第一步:输入15个整数 第二步:对这15个数进行排序 第三部:输入一个数,在后在排好序的数中进行折半查找,判断该数的位置 实现代码如下: 方法一: 选择排序法+循环折半查找法 复制代码 代码如下: #include<iostream>using namespace std;int main(){ int a[15]; int n,i; void array_sort(int a[], int n); int zeban(int a[], int start ,int end,int n); c

  • C++快速排序的分析与优化详解

    相信学过数据结构与算法的朋友对于快速排序应该并不陌生,本文就以实例讲述了C++快速排序的分析与优化,对于C++算法的设计有很好的借鉴价值.具体分析如下: 一.快速排序的介绍 快速排序是一种排序算法,对包含n个数的输入数组,最坏的情况运行时间为Θ(n2)[Θ 读作theta].虽然这个最坏情况的运行时间比较差,但快速排序通常是用于排序的最佳的实用选择.这是因为其平均情况下的性能相当好:期望的运行时间为 Θ(nlgn),且Θ(nlgn)记号中隐含的常数因子很小.另外,它还能够进行就地排序,在虚拟内存

  • nginx源码分析线程池详解

    nginx源码分析线程池详解 一.前言 nginx是采用多进程模型,master和worker之间主要通过pipe管道的方式进行通信,多进程的优势就在于各个进程互不影响.但是经常会有人问道,nginx为什么不采用多线程模型(这个除了之前一篇文章讲到的情况,别的只有去问作者了,HAHA).其实,nginx代码中提供了一个thread_pool(线程池)的核心模块来处理多任务的.下面就本人对该thread_pool这个模块的理解来跟大家做些分享(文中错误.不足还请大家指出,谢谢) 二.thread_

  • 基于Python中单例模式的几种实现方式及优化详解

    单例模式 单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在.当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场. 比如,某个服务器程序的配置信息存放在一个文件中,客户端通过一个 AppConfig 的类来读取配置文件的信息.如果在程序运行期间,有很多地方都需要使用配置文件的内容,也就是说,很多地方都需要创建 AppConfig 对象的实例,这就导致系统中存在多个 AppConfig 的实例对象,而这样会严重浪

  • Vue编译器源码分析compileToFunctions作用详解

    目录 引言 Vue.prototype.$mount函数体 源码出处 options.delimiters & options.comments compileToFunctions函数逐行分析 createFunction 函数源码 引言 Vue编译器源码分析 接上篇文章我们来分析:compileToFunctions的作用. 经过前面的讲解,我们已经知道了 compileToFunctions 的真正来源你可能会问为什么要弄的这么复杂?为了搞清楚这个问题,我们还需要继续接触完整的代码. 下面

  • java自旋锁和JVM对锁的优化详解

    目录 背景 好处 AtomicLong的实现 getAndIncrement方法 实验 缺点 适用场景 JVM对锁做了哪些优化? 自适应的自旋锁 锁消除 锁粗化 偏向锁/ 轻量级锁/ 重量级锁 锁升级 背景 先上图 由此可见,非自旋锁如果拿不到锁会把线程阻塞,直到被唤醒: 自旋锁拿不到锁会一直尝试 为什么要这样? 好处 阻塞和唤醒线程都是需要高昂的开销的,如果同步代码块中的内容不复杂,那么可能转换线程带来的开销比实际业务代码执行的开销还要大. 在很多场景下,可能我们的同步代码块的内容并不多,所以

  • JS 加载性能Tree Shaking优化详解

    目录 正文 什么是 Tree Shaking 寻找 Tree Shaking 的机会 防止 Babel 将 ES6 模块转换为 CommonJS 模块 留意 side effects 只导入你需要的 更复杂的情况 总结 正文 随着 web 应用复杂性增加,JS 代码文件的大小也在不断的攀升,截住 2021年9月,在 httparchive 上有统计显示——在移动设备上 JS 传输大小大约为 447 KB,桌面端 JS 传输大小大约为 495 KB,注意这仅仅是在网络中传输的 JS 文件大小,JS

  • Android性能优化之弱网优化详解

    目录 弱网优化 1.Serializable原理 1.1 分析过程 1.2 Serializable接口 1.3 ObjectOutputStream 1.4 序列化后二进制文件的一点解读 1.5 常见的集合类的序列化问题 1.5.1 HashMap 1.5.2 ArrayList 2.Parcelable 2.1 Parcel的简介 2.2 Parcelable的三大过程介绍(序列化.反序列化.描述) 2.2.1 描述 2.2.2 序列化 2.2.3 反序列化 2.3 Parcelable的实

  • MySQL慢查询分析工具pt-query-digest详解

    目录 一.简介 二.安装pt-query-digest 三.pt-query-digest语法及重要选项 四.分析pt-query-digest输出结果 五.用法示例 一.简介 pt-query-digest是用于分析mysql慢查询的一个工具,它可以分析binlog.General log.slowlog,也可以通过SHOWPROCESSLIST或者通过tcpdump抓取的MySQL协议数据来进行分析.可以把分析结果输出到文件中,分析过程是先对查询语句的条件进行参数化,然后对参数化以后的查询进

  • Python实现随机森林RF模型超参数的优化详解

    目录 1 代码分段讲解 1.1 数据与模型准备 1.2 超参数范围给定 1.3 超参数随机匹配择优 1.4 超参数遍历匹配择优 1.5 模型运行与精度评定 2 完整代码 本文介绍基于Python的随机森林(Random Forest,RF)回归代码,以及模型超参数(包括决策树个数与最大深度.最小分离样本数.最小叶子节点样本数.最大分离特征数等)自动优化的代码. 本文是在上一篇文章Python实现随机森林RF并对比自变量的重要性的基础上完成的,因此本次仅对随机森林模型超参数自动择优部分的代码加以详

  • Java太阳系小游戏分析和源码详解

    最近看了面向对象的一些知识,然后跟着老师的讲解做了一个太阳系各行星绕太阳转的小游戏,来练习巩固一下最近学的知识: 用到知识点:类的继承.方法的重载与重写.多态.封装等 分析: 1.需要加载图片.画图 2.建一个面板,主页面 3.行星类 效果图: 先看一下源码结构图: 现在逐步分析各个类的功能: 1)工具类-----util包中 --Constant类   封装了游戏中用到的常量 --GameUtil类  封装了游戏的图片加载功能 --MyFrame类  封装了游戏面板的构造,用于各面板的父类 -

随机推荐