C# Tips之快速排序

写在前面

C# Tips是博主开启的第一个项目,以C#为示例语言,旨在普及编程技巧、经典算法以及计算机视觉、图形学知识。

为什么选择C#

博主学习语言的历程是C直接到C#,为什么跳过了C++?因为C#比C++更能够满足教学需要,比如ArcGIS Engine、数据库等课程都要和C#打交道。所以选择C#的第一个原因是:不能白白浪费了学C#交的学费。

C#经常以速度不如C++而被人诟病(实际上在某些方面C#的速度要快于C++,深有体会的就是编译运行速度),但是Visual Studio这款强大的IDE又对C#偏心(VS对C++智能提示几乎为零),博主比较喜欢C#在VS里的编程风格,综上选择了C#语言作为示例。

1 排序算法

下面这篇博客对线性数据结构的排序算法的介绍已经非常详细,在此不再赘述。

十大经典排序算法

天底下没有两全其美的事,排序算法也如此,一般情况下,时间复杂度低的空间复杂度高(非比较排序),占用空间少的排序时间长(比较排序)。快速排序算法在这些算法中算是比较实在的,时间和空间复杂度都相对居中,所以咱今天就欺负欺负老实人,拿它来说事。

2 快速排序算法

这一部分写给快排没掌握好或者曾经掌握过但是忘了的人~~

2.1 原理

快速排序算法实际上用到了二分的思想,以下以升序为例:

  1. 从数据集A中选取某基准元素A0,剩下的元素集合记为A-;
  2. 将A-中比A0小的元素放在A0左边,记为A<;比A0大的元素放在A0右边,记为A>;
  3. 令A<、A>分别=A,如果A中元素数量<2,结束;否则回到1。

想要实现以上操作,需要明确以下两个问题:

  1. 基准元素A0怎么选?
  2. 怎么将A-中的元素放到A0的两侧?

下面就选A中第一个元素为基准元素(其他元素可自行尝试),利用快速排序给数据集"6 4 5 1 3 2"排序。

6 4 5 1 3 2->2 4 5 1 3 6(1次排序结束)
2 4 5 1 3 6->1 2 5 4 3 6(2次排序结束)
1 2 5 4 3 6->1 2 3 4 5 6(3次排序结束)
1 2 3 4 5 6->1 2 3 4 5 6(4次排序结束)

看到这里,有同学肯定要问了:为什么1次排序的结果是2 4 5 1 3 6而不是4 5 1 3 2 6?这就涉及到了第二个问题。

快速排序算法是基于元素交换的算法,所以并不会真正地将A-中的元素全部拿出来,分别与A0比较,其采取的策略如下:
哨兵机制
如图圆圈代表A<集合的哨兵,方块代表A>集合的哨兵,两个哨兵分别向中间移动,当两个哨兵刚好错过时(圆圈跑到了方块的右边),哨兵移动结束,基准元素与方块所在元素进行交换

哨兵移动的情况分为以下几种:

  1. 圆圈元素<=基准元素 || 圆圈索引==基准索引,则向右移动圆圈;
  2. 若1不满足,满足 方块元素>=基准元素 || 方块索引==基准索引,则向左移动方块;
  3. 若1、2都不满足,说明圆圈元素>基准元素 && 方块元素<基准元素,则交换圆圈和方块元素,两哨兵分别向中间移动一步。

哨兵移动

2.2 代码实现

快速排序算法可以基于递归和非递归两种方式实现,无论是哪一种实现方法,都需要实现交换,所以先“交换”为敬!

/// <summary>
/// 交换两个数据
/// </summary>
/// <param name="data">源数据</param>
/// <param name="index1">索引1</param>
/// <param name="index2">索引2</param>
public void Swap(dynamic data, int index1, int index2)
{
    if (data == null)
        return;
    var temp = data[index1];
    data[index1] = data[index2];
    data[index2] = temp;
}

这种交换方式适用于任意类型的一维数组、列表。但注意,dynamic可能会在数据量很大时增加运算时间,所以在数据类型确定时最好使用确定的数据类型。

除此之外,哨兵移动后都需要找到方块哨兵最终落脚的位置。

/// <summary>
/// 获得方块哨兵落脚索引
/// </summary>
/// <param name="data">源数据</param>
/// <param name="L">头索引</param>
/// <param name="R">尾索引</param>
/// <returns>索引</returns>
public int Partition(dynamic data, int L, int R)
{
    int P = L;
    while (L <= R)
    {
        if (data[L].CompareTo(data[P]) < 0 || L == P)
            L++;
        else if (data[R].CompareTo(data[P]) > 0 || R == P)
            R--;
        else
        {
            Swap(data, L, R);
            L++;
            R--;
        }
    }
    return R;
}

同样注意,在数据类型确定时,尽量不要使用CompareTo

2.2.1 基于递归

由下面代码实现递归:

/// <summary>
/// 递归快速排序
/// </summary>
/// <param name="data">源数据</param>
/// <param name="L">头索引</param>
/// <param name="R">尾索引</param>
public void QuickSort(dynamic data, int L, int R)
{
    int P = Partition(data, L, R);
    if (L < P)
        Swap(data, L, P);
    if (P - 1 > L)
        QuickSort(data, L, P - 1);
    if (P + 1 < R)
        QuickSort(data, P + 1, R);
}
2.2.2 基于非递归

递归算法都可以利用非递归算法实现,快速排序算法可以借助队列实现非递归。

/// <summary>
/// 非递归快速排序
/// </summary>
/// <param name="data">源数据</param>
/// <param name="L0">头索引</param>
/// <param name="R0">尾索引</param>
public void QuickSort2(dynamic data,int L0, int R0)
{
    Queue<int> queue1=new Queue<int>(), queue2 = new Queue<int>();
    queue1.Enqueue(L0);
    queue2.Enqueue(R0);
    while(queue1.Count()!=0&&queue2.Count() != 0)
    {
        int L,R,P;
        L = queue1.Dequeue();
        R = queue2.Dequeue();
        P = Partition(data, L, R,null);
        if (P > L)
            Swap(data, L, P);
        if (P - 1 > L)
        {
            queue1.Enqueue(L);
            queue2.Enqueue(P - 1);
        }
        if (P + 1 < R)
        {
            queue1.Enqueue(P + 1);
            queue2.Enqueue(R);
        }
    }
}

3 有序性检验

这一部分写给以为看完了上一部分就学会快排的人~~

3.1 快排弊端

快排好快啊!这是第一次使用快排时发出的感叹。但是快排真的有这么完美吗?要知道,在最坏的情况下,快排的时间复杂度也会达到O(n2),拿最简单的两种情况举例来说:
1)1 2 3 4 5 6
2)6 5 4 3 2 1
很容易发现,这两种情况下的数据都是有序的,因此在快排前做个有序性检验还是有必要的。

/// <summary>
/// 判断数据是否有序
/// </summary>
/// <param name="data">源数据</param>
/// <param name="count">数据量</param>
/// <returns>1为升序,-1为降序,0为无序</returns>
public int CheckInOrder(dynamic data,int count)
{
    int order=-1;//降序
    int i = 1;
    for (; i < count; i++)
    {
        if (data[i].CompareTo(data[i - 1]) > 0)
        {
            order = 1;
            break;
        }
        else if (data[i].CompareTo(data[i - 1]) < 0)
            break;
    }
    i++;
    if (order==1)
    {
        for (; i < count; i++)
        {
            if (data[i].CompareTo(data[i - 1]) < 0)
                return 0;
        }
    }
    else
    {
        for (; i < count; i++)
        {
            if (data[i].CompareTo(data[i - 1]) >0)
                return 0;
        }
    }
    return order;
}

但是这就完了吗?并没有,因为有时候检验出来数据是升序的,但是我们需要的是降序,那么还需要添加一个数据反转的方法。

/// <summary>
/// 反转数据
/// </summary>
/// <param name="data">源数据</param>
/// <param name="count">数据量</param>
public void Reverse(dynamic data,int count)
{
    for (int i = 0, j = count - 1; i < j; i++, j--)
        Swap(data, i, j);
}
3.2 时间比较

测试数据数据量为10000,范围为0-9999的整数,需求是按升序排序,快速排序采用非递归方式(有序数据递归会导致栈溢出),测试时间计算方式为5次测试结果去最大最小平均值。不知道怎么插入三线表,就用图片代替啦。

快速排序测试结果
由测试结果可以明显看出:对于有序数据,粗暴的快排真的是无能为力,而有序性检验发挥了强大的作用;对于无序数据,快排是真快,但有序性检验并未占用太多时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值