首页 > 其他分享 >快速排序

快速排序

时间:2023-05-22 23:22:05浏览次数:23  
标签:end limits text sum 排序 快速 left

定义

快速排序(英语:Quicksort),又称分区交换排序(英语:partition-exchange sort),简称「快排」,是一种被广泛运用的排序算法。

基本原理与实现

快速排序的工作原理是通过 分治 的方式来将一个数组排序。

快速排序分为三个过程:

  1. 将数列划分为两部分(要求保证相对大小关系);
  2. 递归到两个子序列中分别进行快速排序;
  3. 不用合并,因为此时数列已经完全有序。

和归并排序不同,第一步并不是直接分成前后两个序列,而是在分的过程中要保证相对大小关系。具体来说,第一步要是要把数列分成两个部分,然后保证前一个子数列中的数都小于后一个子数列中的数。为了保证平均时间复杂度,一般是随机选择一个数 \(m\) 来当做两个子数列的分界。

之后,维护一前一后两个指针 \(p\) 和 \(q\) ,依次考虑当前的数是否放在了应该放的位置(前还是后)。如果当前的数没放对,比如说如果后面的指针 \(q\) 遇到了一个比 \(m\) 小的数,那么可以交换 \(p\) 和 \(q\) 位置上的数,再把 \(p\) 向后移一位。当前的数的位置全放对后,再移动指针继续处理,直到两个指针相遇。

其实,快速排序没有指定应如何具体实现第一步,不论是选择 \(m\) 的过程还是划分的过程,都有不止一种实现方法。

第三步中的序列已经分别有序且第一个序列中的数都小于第二个数,所以直接拼接起来就好了。

c++STL:

#include <bits/stdc++.h>
using namespace std;
int a[100010];
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    sort(a + 1, a + n + 1);
    for (int i = 1; i <= n; i++)
        cout << a[i] << ' ';
    return 0;
}

真正的实现

struct Range {
  int start, end;

  Range(int s = 0, int e = 0) { start = s, end = e; }
};

template <typename T>
void quick_sort(T arr[], const int len) {
  if (len <= 0) return;
  Range r[len];
  int p = 0;
  r[p++] = Range(0, len - 1);
  while (p) {
    Range range = r[--p];
    if (range.start >= range.end) continue;
    T mid = arr[range.end];
    int left = range.start, right = range.end - 1;
    while (left < right) {
      while (arr[left] < mid && left < right) left++;
      while (arr[right] >= mid && left < right) right--;
      std::swap(arr[left], arr[right]);
    }
    if (arr[left] >= arr[range.end])
      std::swap(arr[left], arr[range.end]);
    else
      left++;
    r[p++] = Range(range.start, left - 1);
    r[p++] = Range(left + 1, range.end);
  }
}

快速排序的最优时间复杂度和平均时间复杂度为 \(O(n\log n)\) ,最坏时间复杂度为 \(O(n^2)\) 。

对于最优情况,每一次选择的分界值都是序列的中位数,此时算法时间复杂度满足的递推式为
\(T(n) = 2T(\dfrac{n}{2}) + \Theta(n)\) ,由主定理, \(T(n) = \Theta(n\log n)\) 。

对于最坏情况,每一次选择的分界值都是序列的最值,此时算法时间复杂度满足的递推式为 \(T(n) = T(n - 1) + \Theta(n)\) ,累加可得 \(T(n) = \Theta(n^2)\) 。

对于平均情况,每一次选择的分界值可以看作是等概率随机的。
下面我们来证明这种情况下算法的时间复杂度是 \(O(n\log n)\) 。

引理 1: 当对 \(n\) 个元素的数组进行快速排序时,假设在划分元素时总共的比较次数为 \(X\) ,则快速排序的时间复杂度是 \(O(n + X)\) 。

由于在每次划分元素的过程中,都会选择一个元素作为分界,所以划分元素的过程至多发生 \(n\) 次。又由于划分元素的过程中比较的次数和其他基础操作的次数在一个数量级,所以总时间复杂度是 \(O(n + X)\) 的。

设 \(a_i\) 为原数组中第 \(i\) 小的数,定义 \(A_{i,j}\) 为 \(\{ a_i, a_{i+1}, \dots, a_j \}\) , \(X_{i,j}\) 是一个取值为 \(0\) 或者 \(1\) 的离散随机变量表示在排序过程中 \(a_i\) 是否和 \(a_j\) 发生比较。

显然每次选取的分界值是不同的,而元素只会和分界值比较,所以总比较次数

\[\begin{aligned} X = \sum \limits _ {i = 1} ^ {n - 1} \sum \limits _ {j = i + 1} ^ n X_{i,j} \end{aligned} \]

由期望的线性性,
和比较 $$ \begin{aligned} E[X] & = E \left[ \sum \limits _ {i = 1} ^ {n - 1} \sum \limits _ {j = i + 1} ^ n X_{i,j} \right] \ & = \sum \limits _ {i = 1} ^ {n - 1} \sum \limits _ {j = i + 1} ^ n E[X_{i,j}] \ & = \sum \limits _ {i = 1} ^ {n - 1} \sum \limits _ {j = i + 1} ^ n P(a_i\ \text{和}\ a_j\ \text{比较}) \end{aligned} $$

引理 2: \(a_i\) 和 \(a_j\) 比较的充要条件是 \(a_i\) 或 \(a_j\) 是集合 \(A_{i,j}\) 中第一个被选中的分界值。

先证必要性,即若 \(a_i\) 和 \(a_j\) 都不是集合\(A_{i,j}\) 中第一个被选中的分界值,则 \(a_i\) 不和 \(a_j\) 比较。

考虑计算 和比较 \(P(a_i\ \text{和}\ a_j\ \text{比较})\) 。在 \(A_{i,j}\) 中某个元素被选为分界值之前, $A_{i,j}] 中的元素都在数组的同一子序列中。所以 \(A_{i,j}\) 中每个元素都会被等可能地第一个被选为分界值。由于 \(A_{i,j}\) 中有 \(j - i + 1\) 个元素,由引理 2,
和比较或是集合中第一个被选中的分界值

\[P(a_i \text{和} a_j \text{比较}) = P(a_i \text{或} a_j \text{是集合} A_{i,j} \text{中第一个被选中的分界值}) = \dfrac{2}{j-i+1} \]

所以
和比较

\[\begin{aligned} E[X] & = \sum \limits _ {i = 1} ^ {n - 1} \sum \limits _ {j = i + 1} ^ n P(a_i\ \text{和}\ a_j\ \text{比较}) \\ & = \sum \limits _ {i = 1} ^ {n - 1} \sum \limits _ {j = i + 1} ^ n \dfrac{2}{j - i + 1} \\ & = \sum \limits _ {i = 1} ^ {n - 1} \sum \limits _ {k = 2} ^ {n - i + 1} \dfrac{2}{k} \\ & = \sum \limits _ {i = 1} ^ {n - 1} O(\log n) \\ & = O(n \log n) \end{aligned} \]

由此,快速排序的期望时间复杂度为 \(O(n \log n)\) 。

标签:end,limits,text,sum,排序,快速,left
From: https://www.cnblogs.com/devdede/p/17422047.html

相关文章

  • 实现堆,堆排序,解决堆的TOPK问题
    这篇博客我会尽我自己的所能讲解堆,同时详细的解释堆中重要的向下和向上调整算法,以及推排序的两种实现方法,和堆的TOPK问题。堆是什么我们之前已经介绍过了树,而堆就是一种完全二叉树。这里我放一张二叉树的图下面我来解释一下满二叉树,和完全二叉树的区别:满二叉树是指除了叶子节点外,每......
  • 前端学习 node 快速入门 系列 —— 事件循环
    事件循环本篇将对以下问题进行讨论:浏览器有事件循环,node也有事件循环,两者有什么异同?node核心特性(事件驱动和非阻塞I/O)和事件循环有什么关系?node中的高并发和高性能和事件循环有关系吗?node不适合什么场景?有人说Node是单线程,有人又说node存在多线程,哪个正确?如果一......
  • 数组排序输出(函数模板)
    对于输入的每一批数,按从小到大排序后输出。一行输入为一批数,第一个输入为数据类型(1表示整数,2表示字符型数,3表示有一位小数的浮点数,4表示字符串,0表示输入结束),第二个输入为该批数的数量size(0<size<=10),接下来为size个指定类型的数据。输出将从小到大顺序输出数据。函数接口定义:sor......
  • 经典的快速排序算法
    经典的快速排序算法其中将一个数组按照枢纽元的大小将其分成左右两个部分的算法成为快速算法这个写个避免了判断相等的情况当遇到元素与枢纽元相等时停止目的是为了产生两个相对平衡的左右数组voidtestQuickSort(){intarr[]={2,4,3,9,6,5,7,0,2,1};//int......
  • Visual Studio快速调试当前页面
    一般项目多个HTML页面,但是调试时只想快速打开当前活动窗口页面默认使用快捷键 ctrl+shift+w但是这样三键并不方便,自定义更改为两键则非常的银杏化。工具--选项--搜索键盘,然后在“显示命令包含”中搜索‘在浏览器查看",选择文件.在浏览器中查看,然后自定义同时按下alt+x快捷键......
  • xiaofeng.NET系列之 netcore c#快速导出数据CSV格式 winfrom wpf
    一个导出buttonnuget搜索 usingXiaoFeng.IO;usingXiaoFeng; privatevoidbutton1_Click(objectsender,EventArgse){varsavedlg=newFolderBrowserDialog(){Description="选择保存的路径",......
  • 使用docker快速部署mysql
    查看mysql镜像https://container-registry.oracle.com/ 创建容器mysql5.7从oracle容器仓库中拉取mysql5.7社区版本[root]#dockerpullcontainer-registry.oracle.com/mysql/community-server:5.7 查看镜像信息[root]#dockerimagesREPOSITORY......
  • UG快速导工程图攻略-通过导出图层设置,可以减少显示切换图层时间
    1.将不同零件放在不用图层呢2.显示所有图层后,导所需要的工程图视图(导出数据基于此图层显示数据,如果图层显示为1-7,导出设置为2-8,则最终只会显示2-7)3.在导出的数据中设置所需要的图层4.仅显示所显示图层......
  • 冒泡排序
    【三】冒泡排序基本思想:两个数比较大小,较大的数下沉,较小的数冒起来。过程:比较相邻的两个数据,如果第二个数小,就交换位置。从后向前两两比较,一直到比较最前两个数据。最终最小数被交换到起始的位置,这样第一个最小数的位置就排好了。继续重复上述过程,依次将第2.3...n-1个......
  • 数字名片工具 BBlog:使用一个链接,快速创建和分享你的信息主页和数字花园
    数字名片BBlog:使用一个链接,快速创建和分享你的信息主页和数字花园随着移动互联网技术的快速发展,数字名片产品已成为现代社交和网络营销的重要工具。数字名片可以帮助个人和企业在各种场合中展示和分享联系信息,同时还具有便捷、环保、易于管理等诸多优点。在本文中,我们将介绍......