排序之快排

本文详细介绍了快速排序算法,包括其基本概念、分析、两种分区实现(Lomuto和Hoare)以及时间复杂度和空间复杂度的讨论。通过代码展示了Lomuto和Hoare分区方案,并提供了C#实现。快速排序在平均情况下具有O(nlogn)的时间复杂度,但在最坏情况下可能达到O(n^2)。
摘要由CSDN通过智能技术生成

一、概述

  快速排序Quicksort),又称分区交换排序partition-exchange sort),简称快排,是一种比较排序算法,最早由 东尼·霍尔Tony Hoare)提出。在平均状况下,排序 n n n 个元素要 O ( n log ⁡ n ) O(n \log n) O(nlogn) 次比较。在最坏状况下则需要 O ( n 2 ) O(n^2) O(n2) 次比较,但这种状况并不常见。通常情况下快速排序比其他算法更快,因为它的内部循环inner loop)可以在大部分的架构上有效率地达成。
快排示意图

二、分析

  快速排序使用分治策略Divide and conquer)来把一个序列(list)分为 较小较大 的 2 个子序列,然后对两个子序列递归排序
排序步骤为:

  • 选择基准值:从数列中挑出一个元素,称为“基准”(pivot),
  • 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成。
  • 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。
  • 递归结束条件:递归数组的大小是 ,排序结束。

注:快排是不稳定排序,如需实现稳定排序需加入额外的辅助数据。
  比如将原本的数据 data 变成 { data, index }

三、实现

  不同的基准值方法,对排序的时间性能有决定性影响。下面介绍两种常用的分区法

1、Lomuto 分区方案

  最早由 Nico Lomuto 提出,此方案选择最后一个元素作为基数,由于此方案代码更 紧凑易于理解,常被用于教程。但整体效率低于 Hoare 的原始方案。特别是在整个数组已经有序时,效率下降到 O ( n 2 ) O(n^2) O(n2)

  • 排序过程示意
    亮黄色为基数
    绿色为排序完成
    橙色为交换数据
    在这里插入图片描述
Lomuto 分区排序示意图
- Lomuto 分区伪代码
// 对数组进行排序
// list 数据
// lo 最小索引
// hi 最大索引
algorithm quicksort(list, lo, hi)
{ 
    // 判断索引是否合法  
    if lo >= hi || lo < 0 then     
        return
              
    // 对数组进行分区并获得基数 pivot 的 index 
    p := partition(list, lo, hi)         

    // 对分区进行排序
    quicksort(list, lo, p - 1) // 左分区 
    quicksort(list, p + 1, hi) // 右分区
}
       
// 将数组分成两个区
algorithm partition(list, lo, hi) 
{  
    pivot := list[hi] // 选择最后一个元素作为基数 pivot  
    i := lo - 1  // 临时枢轴(分区轴)索引
    for j := lo to hi - 1 do     
    {
        // 当前元素小于等于 基数 时    
        if list[j] <= pivot       
        {
            // 更新分区位置      
            i := i + 1      
            // 交换当前元素与临时枢轴索引处的元素     
            swap list[i] with list[j]   
        }
    }
    
    // 将基数 pivot 移动到分区中间(在较小和较大元素之间) 
    i := i + 1  
    swap list[i] with list[hi]  
    return i // 最终基数的索引
}

2、Hoare 分区法

  使用数组的中间元素作为基准值,Hoare 的方案比 Lomuto 的分区方案更有效,因为它平均执行的交换次数减少了三倍。
排序过程示意图
Hoare 分区排序示意图

Hoare 分区排序示意图
  • Hoare 分区伪代码
// 对数组进行排序
// list 数据
// lo 最小索引
// hi 最大索引
algorithm quicksort(list, lo, hi) 
{
    if(lo >= 0 && hi >= 0 && lo < hi)
    {
        p := partition(list, lo, hi)
    } 

    quicksort(list, lo, p) // Note: the pivot is now included
    quicksort(list, p + 1, hi) 
}

algorithm partition(list, lo, hi) 
{
    // 基准值
    pivot := list[ floor((hi + lo) / 2) ] // 取数组中间元素作为基准值

    // Left index
    i := lo - 1 

    // Right index
    j := hi + 1

    loop forever 
        // 从左往右 找到一个比 基准值 大的数
        do i := i + 1 while list[i] < pivot
        
        // 从右往左 找到一个比 基准值 小的数
        do j := j - 1 while list[j] > pivot

        // 如果下标交叉 则返回
        if i ≥ j then return j
        
        // 交互查找到两个数位置
        swap list[i] with list[j]
}

3、代码实现

using System.Collections.Generic;

namespace MarsUtil.Algorithm.Sort
{
    public class Quicksort
    {
        #region Lomuto 分区法
        // Lomuto 分区法,以最后一个元素做基准值 
        public void QuicksortByLomuto(List<int> list, int low, int hight)
        {
            // 判断索引是否合法  
            if (low >= hight || low < 0)
            {
                return;
            }

            // 对数组进行分区并获得基准值 pivot 的 index 
            int pivotIndex = _PartitionByLomuto(list, low, hight);

            // 对分区进行排序
            QuicksortByLomuto(list, low, pivotIndex - 1); // 左分区 
            QuicksortByLomuto(list, pivotIndex + 1, hight); // 右分区
        }

        private int _PartitionByLomuto(List<int> list, int low, int hight)
        {
            int pivot = list[hight]; // 选择最后一个元素作为基准值 pivot  
            int i = low - 1;  // 临时枢轴(分区轴)索引
            for (int j = low; j < hight; j++)
            {
                // 当前元素小于等于 基准值 时    
                if (list[j] <= pivot)
                {
                    // 更新分区位置      
                    i += 1;
                    // 交换当前元素与临时枢轴索引处的元素     
                    _Swap(list, i, j);
                }
            }

            // 将基准值 pivot 移动到分区中间(在较小和较大元素之间) 
            i += 1;
            _Swap(list, i, hight);

            // 最终基准值的索引
            return i;
        }
        #endregion

        #region Hoare 分区法
        // Hoare 分区法 使用数组的中间元素作为基准值
        // 对数组进行排序
        // list 数据
        // lo 最小索引
        // hi 最大索引
       public void  QuicksortByHoare(List<int>  list, int low, int hight)
        {
            // 判断索引是否合法  
            if (low >= hight || low < 0)
            {
                return;
            }
            
            int p = _PartitionByHoare(list, low, hight);

            QuicksortByHoare(list, low, p); // Note: the pivot is now included
            QuicksortByHoare(list, p + 1, hight);
        }

        // 将数组分成两个区
        private int _PartitionByHoare(List<int> list, int lo, int hi)
        {
            // 基准值
            int pivot = list[(int)((hi + lo) / 2.0)]; // 取数组中间元素作为基准值

            // Left index
            int i = lo - 1;

            // Right index
            int j = hi + 1;

            while (true)
            {
                // Move the left index to the right at least once and while the element at
                // the left index is less than the pivot
                i += 1;
                while (list[i] < pivot)
                {
                    i += 1;
                }

                // Move the right index to the left at least once and while the element at
                // the right index is greater than the pivot
                j -= 1;
                while (list[j] > pivot)
                {
                    j -= 1;
                }

                // If the indices crossed, return
                if (i >= j)
                {
                    return j;
                }

                // Swap the elements at the left and right indices
                _Swap(list, i, j);
            }
        }
        #endregion

        private void _Swap(List<int> list, int index1, int index2)
        {
            int temp = list[index1];
            list[index1] = list[index2];
            list[index2] = temp;
        }
    }

}

四、复杂度分析

1、时间复杂度

快排的平均时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn) ,具体求证果然如下所示,其中
n − 1 n - 1 n1 为查找基准值的花费, i i i 代表分区后第一段数据的个数, ( n − i − 1 ) (n - i - 1) (ni1) 为第二段数据的个数,这里的求和是罗列所有的分区情况并最终求平均 1 n \frac{1}{n} n1
在这里插入图片描述

计算过程
在这里插入图片描述

求解递归给出 C ( n ) = 2 n ln ⁡ n ≈ 1.39 n log ⁡ 2 n C ( n ) = 2 n \ln n ≈ 1.39 n \log_2 n C(n)=2nlnn1.39nlog2n
舍去常数最终结果为: O ( n log ⁡ n ) O(n\log n) O(nlogn)

2、空间复杂度

  快速排序所使用的空间,依照使用的版本而定。使用原地(in-place)分割的快速排序版本,即上面两个版本,在任何递归调用前,仅会使用固定的额外空间。然而,如果需要产生 O ( log ⁡ n ) O(\log n) O(logn) 嵌套递归调用,它需要在他们每一个存储一个固定数量的信息。因为最好的情况下需要 O(log n) }次的嵌套递归调用,所以它需要 O ( log ⁡ n ) O(\log n) O(logn) 的空间。最坏情况下需要 O ( n ) O(n) O(n) 次嵌套递归调用,因此需要 O ( n ) O(n) O(n) 的空间。

参考资料
[1] 快速排序:https://zh.wikipedia.org/wiki/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F
[2] Quicksort:https://en.wikipedia.org/wiki/Quicksort

欢迎关注个人公众号,实时推送最新博文!
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值