快速排序全面详解

news2024/11/26 4:22:44

目录

1 基本思想

2 排序步骤

3 代码实现

3.1 区间划分算法(hoare初始版本):

3.2 主框架

4 区间划分算法

4.1 hoare法

4.2 挖坑法

4.3 前后指针法

5 快排优化

5.1 取key方面的优化

5.2 递归方面的优化

5.3 区间划分方面的优化

6 快排非递归实现

6.1 栈实现(代码+图解)

6.2 队列实现

7 特性总结


1 基本思想

快速排序采用分治法,任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

2 排序步骤

  1. 选取基准值,通过区间划分算法把待排序序列分割成左右两部分。

  2. 左右序列中递归重复1。

3 代码实现

3.1 区间划分算法(hoare初始版本):

区间划分算法有三个版本:hoare法,挖坑法,前后指针法,这里介绍hoare法,也是快排的初始划分法。

三种划分方法的结果可能不同,但都是让小的值往前靠拢,大的值往后靠拢,让整个序列逐渐趋于有序。

步骤:

  1. 默认序列最左边的元素为基准值key,设置left,right指针;

  2. left找大,right找小,right要先找,都找到后交换a[left]和a[right];

  3. 重复步骤3

  4. 当left == right时,交换key和相遇位置的元素,完成分割。

走完这一趟后,key值左边都不比key大,key值右边都不比key小,key值到了他排序后应该在的位置,不需要挪动这个元素了。

图解:

算法分析:

为什么能保证相遇位置的值一定比key值小,然后交换?

关键点就是让right先找!

相遇有两种情况:

  • left往right靠拢,与right相遇:right先找到了小的元素,相遇后的值一定比key小。

  • right往left靠拢,与left相遇:left指针指向的元素是上一波交换过后的元素,该元素比key小。

假如我们让left先找的话,相遇位置比key值大,不能交换。

代码:

int Partion(int* a, int left, int right)
{
    int keyI = left;
​
    //left == right两个指针相遇,退出循环
    while (left < right)
    {
    
        //right先找,right找小
        while (left < right && a[right] >= a[keyI])
        {
            right--;
        }
​
        //left找大
        while (left < right && a[left] <= a[keyI])
        {
            left++;
        }
​
        //都找到了,交换
        Swap(&a[left], &a[right]);
    }
​
    //left和right相遇,交换key和相遇位置元素
    Swap(&a[keyI], &a[left]);
​
    return left;
}

划分方法一般不用hoare,是因为这种算法实现的代码很容易出现bug,比如:

  1. key值一般取最左边或者最右边的值,但是要注意key不能用变量保存,而是要保存key的下标keyI,否则最后key与相遇位置的交换并没有真正交换数组中的key。(注意:有些划分算法是用变量保存key,有些是保存下标keyI,视情况而定。)

  2. right找小和left找大的过程中,要保证left < right,否则可能出现数组越界,比如1,9,6,4,2,7,8,2 ;右边的值都比key大,会导致越界。

  3. a[right] >= a[keyI]或者a[left] <= a[keyI]时,才能--right或者++left;如果是a[right] > a[keyI]或者a[left] < a[keyI]可能出现死循环,比如a[left] == a[right] == key时,交换完后不进入内部while,外部while陷入死循环。

3.2 主框架

void _QuickSort(int* a, int begin, int end)
{
    if (begin >= end)
        return;
​
    //根据基准值把数组划分成左右序列
    int keyI = Partion(a, begin, end);
​
    //左右序列递归划分下去
    _QuickSort(a, begin, keyI - 1);
    _QuickSort(a, keyI + 1, end);
}
​
void QucikSort(int* a, int n)
{
    _QuickSort(a, 0, n - 1);
}

上述为快速排序递归实现的主框架,与二叉树前序遍历规则非常像。

二叉树的递归终止条件是空树,快排的终止条件是数组只有一个元素(left==right)或者数组区间不存在(left>right)。

浅画一下展开图:

4 区间划分算法

前面所说,hoare划分法有一定的缺陷,我们再介绍其他两种常用的划分方法。

4.1 hoare法

int Partion(int* a, int left, int right)
{
    int keyI = left;
​
    //left == right两个指针相遇,退出循环
    while (left < right)
    {
    
        //right先找,right找小
        while (left < right && a[right] >= a[keyI])
        {
            right--;
        }
​
        //left找大
        while (left < right && a[left] <= a[keyI])
        {
            left++;
        }
​
        //都找到了,交换
        Swap(&a[left], &a[right]);
    }
​
    //left和right相遇,交换key和相遇位置元素
    Swap(&a[keyI], &a[left]);
​
    return left;
}

4.2 挖坑法

步骤:

  1. 默认序列最左边的元素为基准值key,把值挖走用key变量保存,该位置为一个坑。

  2. 右边找小,找到后把值填给坑位,该位置成为新的坑位。

  3. 左边找大,找到后把值填给坑位,该位置成为新的坑位。

  4. 重复步骤2~3。

  5. 左右相遇,相遇位置也是个坑位,key值填入坑位。

图解:

代码:

int Partion2(int* a, int left, int right)
{
    int key = a[left];
    int hole = left;
​
    while (left < right)
    {
        while (left < right && a[right] >= key)
        {
            right--;
        }
        a[hole] = a[right];
        hole = right;
​
        while (left < right && a[left] <= key)
        {
            left++;
        }
        a[hole] = a[left];
        hole = left;
    }
​
    a[hole] = key; 
​
    return hole;
}

与前面代码不同的是,这里的key值我们不存下标,用一个变量保存。

4.3 前后指针法

步骤:

  1. 默认序列最左边的元素为基准值key,设置prev指针 == left,cur指针 == left+1。

  2. cur找小,找到后,prev++,a[prev]和a[cur]交换。

  3. 重复步骤2。

  4. cur走完以后,a[prev]和key交换。

图解:

代码:

int Partion3(int* a, int left, int right)
{
    int keyI = left;
    int prev = left, cur = prev + 1;
​
    while (cur <= right)
    {
        if (a[cur] < a[keyI] && ++prev != cur)
            Swap(&a[prev], &a[cur]);
​
        ++cur;
    }
    Swap(&a[prev], &a[keyI]);
​
    return prev; 
}

为了避免自己和自己交换,prev先++判断和cur是否相等,相等就不交换。

很明显这种分割方法的代码相比前面两种简单了许多,这种划分法也是最常用的。

5 快排优化

5.1 取key方面的优化

最理想的情况就是key值每次都是中间的值,快排的递归就是一个完美的二分。

快排在面对一些极端数据时效率会明显下降;就比如完全有序的序列,这种序列的基准值key如果再取最左边或者最右边的数,key值就是这个序列的最值,复杂度会变成O(N^2):

这时候就可以用三数取中法来解决这个弊端,三个数为:a[left],a[mid],a[right],这样就可以尽量避免key值选到最值的情况。

//三数取中法选key值
int GetMidIndex(int* a, int left, int right) 
{
    int mid = (left + right) / 2; 
​
    if (a[left] < a[mid])
    {
        if (a[mid] < a[right])
            return mid;
        else if (a[left] > a[right])
            return left; 
        else
            return right; 
    }
    else  //mid < left
    {
        if (a[left] < a[right])
            return left;
        else if (a[mid] > a[right])
            return mid; 
        else
            return right; 
    }
}
​
//前后指针划分
int Partion3(int* a, int left, int right)
{
    //中间值的下标为midI,a[left]替换为此中间值
    int midI = GetMidIndex(a, left, right);  
    Swap(&a[left], &a[midI]); 
​
    int keyI = left;
    int prev = left, cur = prev + 1;
​
    while (cur <= right)
    {
        if (a[cur] < a[keyI] && ++prev != cur)
            Swap(&a[prev], &a[cur]);
​
        ++cur;
    }
    Swap(&a[prev], &a[keyI]);
​
    return prev; 
}

除了三数取中法,我们还可以考虑随机数法,都能在一定程度上避免这种极端情况。

srand((unsigned int)time(NULL));
​
//前后指针划分
int Partion3(int* a, int left, int right)
{
    //随机数取key
    int keyI = left + rand() % (right - left + 1);
    Swap(&a[left], &a[keyI]); 
​
    int keyI = left;
    int prev = left, cur = prev + 1;
​
    while (cur <= right)
    {
        if (a[cur] < a[keyI] && ++prev != cur)
            Swap(&a[prev], &a[cur]);
​
        ++cur;
    }
    Swap(&a[prev], &a[keyI]);
​
    return prev; 
}

5.2 递归方面的优化

我们知道,递归深度太深并不是一件好事,所以我们可以针对递归方面来进行优化,减少绝大多数的递归调用。

如何优化呢?当递归到区间内元素个数<=10时,调用直接插入排序。

void _QuickSort(int* a, int begin, int end)
{
    if (begin >= end)
        return;
​
    //区间内元素个数 <= 10,调用直接插入排序
    if (end - begin + 1 <= 10)
    {
        InsertSort(a + begin, end - begin + 1);
        //注意:起始地址是a + begin,不是a
    }
    else
    {
        //根据基准值把数组划分成左右序列
        int keyI = Partion3(a, begin, end); 
​
        //左右序列递归划分下去
        _QuickSort(a, begin, keyI - 1); 
        _QuickSort(a, keyI + 1, end); 
    }
}

这种优化其实可以减少绝大多数的递归调用,我们把快排的递归划分想象成一颗二叉树,区间长度小于10的数组大概在这棵二叉树的最后三层,而最后三层占了整棵树结点个数的80%多(最后一层50%,倒数第二层25%...),类比快排的递归来看,我们省去了80%多的递归调用,并且对于数据规模较小的情况下,直插和快排的效率差不了多少,所以这是一个极大的优化,算法库中的sort函数也大多是这种优化。

5.3 区间划分方面的优化

快排针对某些极端数据,效率会下降至O(N^2),这种极端数据我们前面说过:

  • 完全有序的序列算是一个,

  • 还有一种极端数据就是数组中某个元素(我们称为x)大量出现,甚至数组中全部都是一个元素x。

针对情况一,我们可以优化取key来解决这个问题,针对情况二,这种方法不奏效。

那么我们可以从区间划分算法下手:

  • 以前的区间划分算法(前后指针法)是双路划分,也就是一遍走完之后,数组被划分成[left, keyI - 1], keyI, [keyI + 1, right]三部分,左区间 < key, 右区间 >= key,然后左右两个区间再递归划分下去;

  • 这种划分方法有一个弊端,就是x大量出现时(甚至整个数组都是一种元素x),会导致左右区间的元素数量严重失衡,导致快排效率下降。

  • 这里我们就可以使用三路划分了,所谓三路划分,就是数组被划分成三部分:< key、==key、 和> key三个部分,我们只需递归划分<key和>key这两部分的区间。

  • 由于key值很容易取到x(一旦取到x,左右区间的size一定会大大减小),这种算法一定程度上提高了效率。

如何实现?详见LeetCode912:

class Solution {
public:
    pair<int, int> Partion(vector<int>& nums, int begin, int end)
    {
        //随机数取key
        int keyI = begin + rand() % (end - begin + 1);
        swap(nums[begin], nums[keyI]);
​
        int key = nums[begin];
        int left = begin, right  = end, cur = left + 1;
        while(cur <= right)
        {
            if(nums[cur] < key)
            {
                swap(nums[left++], nums[cur++]);
            }
            else if(nums[cur] == key)
            {
                cur++;
            }
            else
            {
                swap(nums[right--], nums[cur]);
            }
        }
        return make_pair(left, right);
    }
​
    void QuickSort(vector<int>& nums, int begin, int end)
    {
        if(end - begin + 1 <= 1)
            return;
​
        pair<int, int> p = Partion(nums, begin, end);
​
        QuickSort(nums, begin, p.first - 1);
        QuickSort(nums, p.second + 1, end);
    }
​
    vector<int> sortArray(vector<int>& nums) 
    {
        srand((unsigned int)time(NULL));
        QuickSort(nums, 0, nums.size() - 1);
        return nums;
    }
};

6 快排非递归实现

快排的非递归我们可以使用一个栈(深度优先遍历)或者一个队列实现(广度优先遍历)。

6.1 栈实现(代码+图解)

void QuickSortNonRByStack(int* a, int n)
{
    Stack st; 
    StackInit(&st);
    int begin = 0, end = n - 1;
    //先Push右边界,在Push左边界
    //记住push顺序,取top的时候左右不要取反了
    StackPush(&st, end);
    StackPush(&st, begin);
​
    while (!StackEmpty(&st))
    {
        int begin = StackTop(&st);  
        StackPop(&st);  
        int end = StackTop(&st); 
        StackPop(&st); 
​
        int keyI = Partion3(a, begin, end);
        //[begin, keyI - 1]  keyI  [keyI + 1, end]
​
        //先递归到左区间,所以右区间先入栈
        if (keyI + 1 < end) 
        {
            //先Push右边界,在Push左边界
            StackPush(&st, end);   
            StackPush(&st, keyI + 1);   
        }
​
        if (begin < keyI - 1)
        {
            //先Push右边界,在Push左边界
            StackPush(&st, keyI - 1); 
            StackPush(&st, begin);
        }
    }
​
    StackDestory(&st);
}
​

 

6.2 队列实现

void QuickSortNonRByQueue(int* a, int n)
{
    Queue q;
    QueueInit(&q);
    int begin = 0, end = n - 1; 
    QueuePush(&q, begin); 
    QueuePush(&q, end); 
​
    while (!QueueEmpty(&q))
    {
        int begin = QueueFront(&q);
        QueuePop(&q);
        int end = QueueFront(&q); 
        QueuePop(&q); 
​
        int keyI = Partion3(a, begin, end); 
​
        if (begin < keyI - 1) 
        {
            QueuePush(&q, begin);  
            QueuePush(&q, keyI - 1);
        }
​
        if (keyI + 1 < end) 
        {
            QueuePush(&q, keyI + 1); 
            QueuePush(&q, end);   
        }
    }
    QueueDestory(&q); 
}

7 特性总结

  • 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

  • 时间复杂度:O(NlogN)

最好情况,每次key都在中间位置,正好二分

最坏情况,每次key都是最值,复杂度O(N^2)

平均情况(带优化),复杂度O(NlogN)

  • 空间复杂度:O(logN)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1092958.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

终极Whois查询工具:优雅美观、功能强大、信息全面

1. 引言 这个程序的适用面不是很广&#xff0c;但对于域名爱好者&#xff0c;我想这会是一个不错的工具。 查询一个域名的Whois&#xff0c;这样的工具有很多。但是显示的数据却是有点差强人意&#xff0c;一次偶然的机会发现了 who.cx 这个whois工具&#xff0c;不得不说界面…

Android组件通信——Service(二十七)

1. Service 1.1 知识点 &#xff08;1&#xff09;掌握Service与Activity的区别&#xff1b; &#xff08;2&#xff09;掌握Service的定义及使用&#xff1b; &#xff08;3&#xff09;可以使用ServiceConnection 接口绑定一个Service&#xff1b; &#xff08;4&#x…

java js 经纬度转换 大地坐标(高斯投影坐标)与经纬度互相转换

项目中有大地坐标(高斯投影坐标)与经纬度互相转换的需求 写了个工具类; 有java和js代码 如图 java代码中将坐标系和带宽已作为参数传入方法,使用时只需调用不同方法: js端没有将坐标系和带宽作为参数 如果有需要可以替换注释地方 或者自行修改为传参方式: 接下来贴代码: jav…

C++笔记之如何给 `const char*` 类型变量赋值

C笔记之如何给 const char* 类型变量赋值 code review! 文章目录 C笔记之如何给 const char* 类型变量赋值1.在C中&#xff0c;如果你要给一个 const char* 变量赋值&#xff0c;你通常有几种方法来做这件事&#xff0c;具体取决于你的需求。下面是一些常见的方法&#xff1a;…

使用JAVA发送邮件

这里用java代码编写发送邮件我采用jar包&#xff0c;需要先点击这里下载三个jar包&#xff1a;这三个包分别为&#xff1a;additionnal.jar&#xff1b;activation.jar&#xff1b;mail.jar。这三个包缺一不可&#xff0c;如果少添加或未添加均会报下面这个错误&#xff1a; C…

芯片学习记录SN74HC14DR

SN74HC14DR 芯片介绍 该设备包含六个独立的逆变器使用施密特触发器输入。每个门执行正逻辑中的布尔函数Y/A("/"表示“非”)。 引脚信息 引脚名称I/O电平功能11AI0~vcc输入21YO0~vcc输出7GND-电源14VCC-3.3v电源&#xff08;2~6V&#xff09;Y/A 推荐使用条件 参数…

jwt的使用概念工具类与切入spa项目

jwt的概念 JWT&#xff08;JSON Web Token&#xff09;是一种用于身份验证和授权的开放标准&#xff0c;它是一种轻量级的、安全的、基于JSON的令牌机制。 JWT由三部分组成&#xff1a;头部&#xff08;Header&#xff09;、载荷&#xff08;Payload&#xff09;和签名&#…

C++笔记之std::async的用法

C笔记之std::async的用法 code review! 文章目录 C笔记之std::async的用法1.概念2.C 异步任务的使用示例 - 使用 std::async 和 std::future3. std::launch::async 和 std::launch::deferred 1.概念 std::async 是 C 标准库中的一个函数&#xff0c;用于创建异步任务&#xf…

leetCode 1035.不相交的线 动态规划 + 滚动数组 (最长公共子序列)

1035. 不相交的线 - 力扣&#xff08;LeetCode&#xff09; 在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。 现在&#xff0c;可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线&#xff0c;这些直线需要同时满足满足&#xff1a; nums1[i] nums2[j]…

3.4 构造方法

思维导图&#xff1a; 3.4.1 定义构造方法 ### Java中的构造方法 #### **定义与目的** 构造方法&#xff0c;也称为构造器&#xff0c;是一个特殊的成员方法&#xff0c;用于在实例化对象时为对象赋值或执行初始化操作。其主要目的是确保对象在被创建时具有有效和合适的初始状…

学习记忆——数学篇——代数——记忆宫殿——卧室

明确需放置的大件物品 整式、分式 &#xff08;1&#xff09;整式&#xff1a;运算、因式定理 &#xff08;2&#xff09;分式&#xff1a;运算函数、方程与不等式 &#xff08;1&#xff09;函数&#xff1a;一元二次函数、 &#xff08;2&#xff09;方程&#xff1a;一元二…

Mysql高级——事务(2)

MySQL事务日志 事务有4种特性&#xff1a;原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢&#xff1f; 事务的隔离性由锁机制实现。 而事务的原子性、一致性和持久性由事务的 redo 日志和undo 日志来保证。 REDO LOG 称为重做日志&#xff0c;…

默认关闭idea2020的注释doc的rendered view模式(阅读模式)

idea2020的javadoc有了一个rendered模式,在开发的时候感觉很不习惯… 打开sessings,在编辑器的外观中取消这个选项,默认情况下doc就是编辑模式了,点击左侧的图标就会变为rendered view模式

输入法显示到语言栏_状态栏

设置–时间和语言–语言–最右侧"相关设置"中的"拼写、键入和键盘设置" 最下方的"高级键盘设置"–“使用桌面语言栏(如果可用)” 点击"语言栏选项" 接下来就是不同输入法的设置了 搜狗输入法:右键输入法选择"隐藏状态栏"–…

全新整合热搜榜单热门榜单内容系统聚合源码/带教程安装

源码简介&#xff1a; 在移动互联网时代&#xff0c;我们每天都会接收到大量的信息&#xff0c;但是想要知道哪些是最热门的话题和内容&#xff0c;往往需要花费很多精力去搜索和筛选。因为有这个需要&#xff0c;一个全新整合热搜榜单热门榜单内容系统聚合源码就应运而生了&a…

Zabbix监控系统详解2:基于Proxy分布式实现Web应用监控及Zabbix 高可用集群的搭建

文章目录 1. zabbix-proxy的分布式监控的概述1.1 分布式监控的主要作用1.2 监控数据流向1.3 构成组件1.3.1 zabbix-server1.3.2 Database1.3.3 zabbix-proxy1.3.4 zabbix-agent1.3.5 web 界面 2. 部署zabbix代理服务器2.1 前置准备2.2 配置 zabbix 的下载源&#xff0c;安装 za…

1.1 Windows驱动开发:配置驱动开发环境

在进行驱动开发之前&#xff0c;您需要先安装适当的开发环境和工具。首先&#xff0c;您需要安装Windows驱动开发工具包&#xff08;WDK&#xff09;&#xff0c;这是一组驱动开发所需的工具、库、示例和文档。然后&#xff0c;您需要安装Visual Studio开发环境&#xff0c;以便…

回归预测 | MATLAB实现BO-LSSVM贝叶斯优化算法优化最小二乘支持向量机数据回归预测(多指标,多图)

回归预测 | MATLAB实现BO-LSSVM贝叶斯优化算法优化最小二乘支持向量机数据回归预测&#xff08;多指标&#xff0c;多图&#xff09; 目录 回归预测 | MATLAB实现BO-LSSVM贝叶斯优化算法优化最小二乘支持向量机数据回归预测&#xff08;多指标&#xff0c;多图&#xff09;效果…

Stream流中的常用方法(forEach,filter,map,count,limit,skip,concat)和Stream流的特点

1、forEach方法 该方法接收一个Consumer接口是一个 消费型 的函数式接口&#xff0c;可以传递Lambda表达式&#xff0c;消费数据用来遍历流中的数据&#xff0c;是一个 终结 方法&#xff0c;遍历 之后就不能继续调用Stream流中的其他方法 package com.csdn.stream; import jav…

【数字IC设计】DC自动添加门控时钟

简介 数字电路的动态功耗主要是由于寄存器翻转带来的&#xff0c;为了降低芯片内部功耗&#xff0c;门控时钟的方案应运而生。作为低功耗设计的一种方法&#xff0c;门控时钟是指在数据无效时将寄存器的时钟关闭&#xff0c;以此来降低动态功耗。 在下图中&#xff0c;展示了…