C#实现快速排序算法

快速排序是应用最广泛的排序算法,流行的原因是它实现简单,适用于各种不同情况的输入数据且在一般情况下比其他排序都快得多。

快速排序是原地排序(只需要一个很小的辅助栈),将长度为 N 的数组排序所需的时间和 N lg N 成正比。

1.算法

快速排序也是一种分治的排序算法。它将一个数组分成两个子数组,将两部分独立地排序。

快速排序和归并排序是互补:归并排序是将数组分成两个子数组分别排序,并将有序数组归并,这样数组就是有序的了;而快速排序将数组通过切分变成部分有序数组,然后拆成成两个子数组,当两个子数组都有序时整个数组也就有序了。

归并排序的递归调用发生在处理数组之前,快速排序的递归调用是发生在处理数组之后。

快速排序中切分的位置取决于数组的内容。

    public class Quick: BaseSort
    {
        public new static long usedTimes = 0;
        public static void Sort(IComparable[] a)
        {
            usedTimes = 0;
            Stopwatch timer = new Stopwatch();
            timer.Start();
            Sort(a,0,a.Length-1);
            timer.Stop();
            usedTimes = timer.ElapsedMilliseconds;
        }

        public static void Sort(IComparable[] a, int lo, int hi)
        {
            if (hi <= lo)
                return;

            //切分
            int j = Partition(a,lo,hi);
            //Console.WriteLine(j);
            Sort(a,lo,j-1);
            Sort(a,j+1,hi);
        }
        public static int CompareCount = 0;
        public static int Partition(IComparable[] a, int lo, int hi)
        {
            int i = lo;
            int j = hi + 1;
            var v = a[lo];

            while (true)
            {
                //从左往右依次和 v 比较,直到找到 >= v 的值 索引i (索引i 左边的值都小于切分元素)
                while (Less(a[++i], v))
                {
                    CompareCount++;
                    if (i == hi)
                        break;
                }

                //从右往左依次和 v 比较,直到找到 <= v 的值 索引j(索引j 右边的值都大于于切分元素)
                while (Less(v, a[--j]))
                {
                    CompareCount++;
                    if (j == lo)
                        break;
                }

                //当 i >= j 时就找到了切分元素位置,位置为 j ,退出循环
                if (i >= j)
                    break;
                //如果 i 和 j 没有相遇,将 i 和 j 的值交换,继续循环,直到相遇
                Exch(a,i,j);
            }

            //将切分元素放到切分位置 j
            Exch(a,lo,j);
            return j;
        }
    }

该算法的关键在于切分 ,在Sort(IComparable[] a, int lo, int hi) 方法中,切分后 a[lo] 到 a[j-1] 中的所有元素都不大于 a[ j ] ,a[j+1] 到 a[ hi ] 中的所有元素都不小于 a[ j ] ,然后对左子数组和右子数组进行递归。因为切分的过程总能排定一个元素,当切分到剩一个元素时,子数组就是有序的。当左数组和右数组都有序时,整个数组也就有序了。

切分方法:先随意取 a[ lo ] 作为切分元素,即那个将被排定的元素。然后从数组的左端向右扫描直到找到大于等于它的元素,再从数组右端向左开始扫描直到找到小于等于它的元素。如果找到的这两个元素不是同一个元素(索引不同),那么这个元素就还没被排定;如果相同意味着这个元素已被排定。如果没被排定,就交换这两个元素,继续扫描,直到左右索引相遇,即可返回将切分元素和找到的索引位置的元素交换,返回索引 j 。

排序实列:

注意点

1. 原地切分

该算法是原地切分,如果使用辅助数组需要将切分后的数组复制回去的额外开销。

2.越界

要防止扫描指针跑出数组边界。

3.保持随机性

该算法对所有子数组都是一视同仁的。

4.终止循环

该算法有三个循环都要注意什么时候终止。

5.处理切分元素值有重复的情况

该算法左右扫描都会在遇到相等值时停下来,尽管这样会不必要的等值交换,但在某些情况下能够避免算法的运行时间变为平方级别。

6.终止递归

任何递归调用都要先考虑什么时候终止。

2.性能

快速排序运行时间的增长量级为 NlogN 。

快速排序切分方法的内循环用一个递增的索引将数组元素和一个定值比较,这种循环很简洁。它比归并排序和希尔排序都快,因为归并排序和希尔排序在内循环中移动元素。

快速排序的另一个速度优势在于它的比较次数很少(如果每次都对半分的话需要 N/2 * logN 次比较)。但是排序效率还是依赖切分数组的效果,而这依赖于切分元素的值 。切分一个较大的数据组,切分可能发生在任何一个位置。

快速排序的最好情况是每次都能将数组对半分。在这种情况下快速排序所用的比较次数正好满足分治递归的 C(N) = 2C(N/2) + N 公式。2C(N/2) 表示将两个子数组排序的成本, N 表示 切分元素和所有数组元素比较的成本。C(N) ~ N log N 。平均而言切分元素都能落在数组中间。

快速排序在最坏情况下需要 ~ N^2 / 2 次比较,即每次切分总有一个数组是空的(逆序),比较次数为: N + (N-1)+ (N-2) ...+2+1 = (N+1)N/2 。这种情况不仅算法所需的时间是平方级别的,所需的空间是线性的。

快速排序平均需要 ~ 2N lnN 次比较(以及1/6的交换)。归并排序也可以做到这个量级,但是快速排序移动数据次数少(即交换次数),所以快速排序更快,尽管比较次数比归并排序多了约 39%。

当数组切分不平衡时(第一次用最小的切分,第二次用第二小的切分...)会倒置一个大数组需要切分很多次(上面说的逆序),所以非重复数组需要随机打乱;当存在大量重复元素时,排序过程会进行很多次交换,重复数组可以使用稍后提到的三向切分的快速排序。

3.改进

1.切换到插入排序

当将一个大数组切分成一定小的数组时使用插入排序给小数组排序,这样就不需要继续递归调用 Sort() 方法了。

将if (hi <= lo)return; 改为

if(hi <= lo + M){ Insertion.Sort(a, lo, hi); return;}

转换参数 M 的最佳值和系统相关,5 ~ 15 最佳。

2.三取样切分

改进快速排序性能的另一个方法是使用子数组的一小部分元素的中位数来切分数组。这样得到的切分更好,但是需要计算中位数。一般取样大小为3并用大小居中的元素切分最好。

3.熵最优排序

实际应用中经常会出现大量重复元素的数组,这会影响快排的性能。例如,一个全部重复的子数组就不需要排序了,但上面的算法会继续切分。三向切分的快速排序可以将有大量重复元素的数组从线性对数级别提高到线性级别。

三向切分是将数组切分成小于,等于和大于三部分。它从左到右遍历数组一次,维护一个指针 lt 使得 a[lo ... lt-1] 中的元素都小于 v(切分元素),一个指针 gt 使得 a[gt+1 ... hi] 中的元素都大于 v ,一个指针 i 使得 a[ lt ... i ] 中元素都等于 v , a[ i ... gt ] 中的元素都还未确定。一开始 lo 和 i 相等:

  • a[i]小于v:与 a[i] 交换 a[lt] 并增加lt 和i
  • a[i]大于v:将 a[i] 与  a[gt] 交换并减少gt
  • a[i]等于v:递增i

    public class Quick: BaseSort
    {
        public new static long usedTimes = 0;

        //三向切分
        public static void Sort3Way(IComparable[] a)
        {
            usedTimes = 0;
            Stopwatch timer = new Stopwatch();
            timer.Start();
            Sort3Way(a, 0, a.Length - 1);
            timer.Stop();
            usedTimes = timer.ElapsedMilliseconds;
        }
        public static void Sort3Way(IComparable[] a, int lo, int hi)
        {
            if (hi < lo)
                return;
            int lt = lo, i = lo + 1, gt = hi;
            IComparable v = a[lo];
            while (i <= gt)
            {
                int cmp = a[i].CompareTo(v);
                if (cmp < 0)
                    Exch(a, lt++, i++);
                else if (cmp > 0)
                    Exch(a, i, gt--);
                else
                    i++;

            }

            Sort3Way(a,lo,lt-1);
            Sort3Way(a,gt+1,hi);
        }
    }

对于若干不同主键的随机数组,归并排序的时间复杂度是线性对数的,而三向切分快速排序是线性的。三向切分最坏的情况是所有键值都不同。当存在重复主键时,性能回避归并排序好很多。

到此这篇关于C#实现快速排序的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • C#算法之散列表

    目录 1.散列函数 正整数 浮点数 字符串 组合键 将 HashCode() 的返回值转化为一个数组索引 自定义的 HashCode 软缓存 2.基于拉链法的散列表 散列表的大小 删除操作 有序性相关的操作 3.基于线性探测法的散列表 删除操作 键簇 线性探测法的性能分析 调整数组大小 拉链法 均摊分析 4.内存的使用 如果所有的键都是小整数,我们可以使用一个数组来实现无序的符号表,将键作为数组的索引而数组中键 i 处存储的就是它对应的值.散列表就是用来处理这种情况,它是简易方法的扩展并能够处理

  • C#实现平衡查找树

    目录 1. 2-3查找树 1.查找 2.向 2- 结点中插入新键 3.向一棵只含有一个 3- 结点的树中插入新键 4.向一个父结点为 2- 结点的 3- 结点中插入新键 5.向一个父结点为 3- 结点的 3- 结点插入新键 6.分解根结点 7.局部变换 8.全局性质 2.红黑二叉查找树 1.定义 2.一一对应 3.颜色表示 4.旋转 5.在旋转后重置父结点的链接 6.向单个 2- 结点中插入新键 7.向树底部的 2- 结点插入新键 8.向一棵双键树(即一个 3- 结点)中插入新键 9.颜色变换

  • C#实现希尔排序

    对于大规模乱序的数组,插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点地从数组地一段移动到另一端.希尔排序改进了插入排序,交换不相邻地元素以对数组地局部进行排序,最终用插入排序将局部有序的数组排序. 希尔排序的思想是使数组中任意间隔为 h 的元素都是有序的.这样的数组成为 h 有序数组.换句话说,一个 h 有序数组就是 h 个相互独立的有序数组组合在一起的一个数组. 在进行排序时,刚开始 h 很大,就能将元素移动到很远的地方,为实现更小的 h 有序创造方便.h 递减到 1 时,相当于

  • C#实现二叉查找树

    目录 1.实现API 1.数据结构 2.查找 3.插入 4.分析 有序性相关的方法和删除操作 1.最大键和最小键 2.向上取整和向下取整 3.选择操作 4.排名 5.删除最大键和删除最小键 6.删除操作 7.范围查找 8.性能分析 对于符号表,要支持高效的插入操作,就需要一种链式结构.但单链表无法使用二分查找,因为二分查找的高效来自于能够快速通过索引取得任何子数组的中间元素,链表只能遍历(详细描述).为了将二分查找的效率和链表的灵活性结合,需要更复杂的数据结构:二叉查找树.具体来说,就是使用每个

  • C#使用符号表实现查找算法

    高效检索海量信息(经典查找算法)是现代信息世界的基础设施.我们使用符号表描述一张抽象的表格,将信息(值)存储在其中,然后按照指定的键来搜索并获取这些信息.键和值的具体意义取决于不同的应用.符号表中可能会保存很多键和很多信息,因此实现一张高效的符号表是很重要的任务. 符号表有时被称为字典,有时被称为索引. 1.符号表 符号表是一种存储键值对的数据结构,支持两种操作:插入(put),即将一组新的键值对存入表中:查找(get),即根据给定的键得到相应的值.符号表最主要的目的就是将一个健和一个值联系起来

  • C#实现优先队列和堆排序

    目录 优先队列 1.API 2.初级实现 3.堆的定义 二叉堆表示法 4.堆的算法 上浮(由下至上的堆的有序化) 下沉(由上至下的堆的有序化) 改进 堆排序 1.堆的构造 2.下沉排序 先下沉后上浮 优先队列 许多应用程序都需要处理有序的元素,但不一定要求它们全部有序,或是不一定要一次就将它们排序.很多情况下是收集一些元素,处理当前键值最大的元素,然后再收集更多的元素,再处理当前键值最大的元素.这种情况下,需要的数据结构支持两种操作:删除最大的元素和插入元素.这种数据结构类型叫优先队列. 这里,

  • PHP 快速排序算法详解

    概念 这里借用百度百科的一张图来,非常形象: 快速排序算法是对冒泡算法的一个优化.他的思想是先对数组进行分割, 把大的元素数值放到一个临时数组里,把小的元素数值放到另一个临时数组里(这个分割的点可以是数组中的任意一个元素值,一般用第一个元素,即$array[0]),然后继续把这两个临时数组重复上面拆分,最后把小的数组元素和大的数组元素合并起来.这里用到了递归的思想. PHP实现 复制代码 代码如下: /*     快速排序 */ function quickSort($array) {    

  • Go语言展现快速排序算法全过程的思路及代码示例

    快速排序算法 快速排序是一个递归的思想,首先选择一个数作为基数,把数组中小于它的数放在它的左边,把大于它的数放在它的右边,然后对左右两边的数递归进行排序. 算法的关键部分是实现数组的划分,即怎么把数组的元素划分成两部分,使得左边的数比基数小,右边的数比基数大.划分有许多不同的实现方法,这里主要使用单向扫描的方法,后面再稍微介绍双向扫描的方法. 选择最右边的数字作为基数.使用一个变量j记录当前左边数字(比基数小的数)的最右的下标值.然后使用变量i从左到右遍历数组,如果a[i]比基数小,说明a[i]

  • 深入解析快速排序算法的原理及其Go语言版实现

    快速排序是一种基于分治技术的重要排序算法.不像归并排序是按照元素在数组中的位置对它们进行划分,快速排序按照元素的值对它们进行划分.具体来说,它对给定数组中的元素进行重新排列,以得到一个快速排序的分区.在一个分区中,所有在s下标之前的元素都小于等于A[s],所有在s下标之后的元素都大于等于A[s]. 显然,建立了一个分区以后,A[s]已经位于它在有序数组中的最终位置,接下来我们可以继续对A[s]前和A[s]后的子数组分别进行排序(使用同样的方法). 为了排序一个数组A的全部元素,初始调用的是QUI

  • Java实现快速排序算法(Quicktsort)

    快速排序算法介绍快速排序和归并排序都使用分治法来设计算法,区别在于归并排序把数组分为两个基本等长的子数组,分别排好序之后还要进行归并(Merge)操作,而快速排序拆分子数组的时候显得更有艺术,取一个基准元素,拆分之后基准元素左边的元素都比基准元素小,右边的元素都不小于基准元素,这样只需要分别对两个子数组排序即可,不再像归并排序一样需要归并操作.基准元素的选取对算法的效率影响很大,最好的情况是两个子数组大小基本相当.为简单起见,我们选择最后一个元素,更高级的做法可以先找一个中位数并把中位数与最后一

  • 浅析java快速排序算法

    快速排序是找出一个元素(理论上可以随便找一个)作为基准(pivot),然后对数组进行分区操作,使基准左边元素的值都不大于基准值,基准右边的元素值 都不小于基准值,如此作为基准的元素调整到排序后的正确位置.递归快速排序,将其他n-1个元素也调整到排序后的正确位置.最后每个元素都是在排序后的正 确位置,排序完成.所以快速排序算法的核心算法是分区操作,即如何调整基准的位置以及调整返回基准的最终位置以便分治递归. 一趟快速排序的算法是: 1)设置两个变量i.j,排序开始的时候:i=0,j=N-1: 2)

  • C#快速排序算法实例分析

    本文实例讲述了C#快速排序算法.分享给大家供大家参考.具体实现方法如下: public static int[] QuickSort(int[] arr) { if (arr.Length <= 1) return arr; int pivot = arr.Length - 1; int[] less = GetLessThanEqualToPivot(arr, pivot); int[] greater = GetGreaterThanPivot(arr, pivot); return Con

  • 详解Java中使用泛型实现快速排序算法的方法

    快速排序算法概念 快速排序一般基于递归实现.其思路是这样的: 1.选定一个合适的值(理想情况中值最好,但实现中一般使用数组第一个值),称为"枢轴"(pivot). 2.基于这个值,将数组分为两部分,较小的分在左边,较大的分在右边. 3.可以肯定,如此一轮下来,这个枢轴的位置一定在最终位置上. 4.对两个子数组分别重复上述过程,直到每个数组只有一个元素. 5.排序完成. 基本实现方式: public static void quickSort(int[] arr){ qsort(arr,

  • Python实现的快速排序算法详解

    本文实例讲述了Python实现的快速排序算法.分享给大家供大家参考,具体如下: 快速排序基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列. 如序列[6,8,1,4,3,9],选择6作为基准数.从右向左扫描,寻找比基准数小的数字为3,交换6和3的位置,[3,8,1,4,6,9],接着从左向右扫描,寻找比基准数大的数字为8,交换6和8的位置

  • java实现快速排序算法

    1.算法概念. 快速排序(Quicksort)是对冒泡排序的一种改进.由C. A. R. Hoare在1962年提出. 2.算法思想. 通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列. 3.实现思路. ①以第一个关键字 K 1 为控制字,将 [K 1 ,K 2 ,-,K n ] 分成两个子区,使左区所有关键字小于等于 K 1 ,右区所有关键字大于

  • 常用排序算法整理分享(快速排序算法、希尔排序)

    整理了几个排序算法,通过测试来看,最快的还是快速排序算法,简直不是一个数量级的速度. 复制代码 代码如下: #include <stdio.h>#include <stdlib.h>#include <stdint.h>#include <stdbool.h>#include <time.h>#include <unistd.h> //一些排序算法整理//插入排序算法//直接插入排序voiddirect_insert_sort(int

随机推荐