写在前面
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 原理
快速排序算法实际上用到了二分的思想,以下以升序为例:
- 从数据集A中选取某基准元素A0,剩下的元素集合记为A-;
- 将A-中比A0小的元素放在A0左边,记为A<;比A0大的元素放在A0右边,记为A>;
- 令A<、A>分别=A,如果A中元素数量<2,结束;否则回到1。
想要实现以上操作,需要明确以下两个问题:
- 基准元素A0怎么选?
- 怎么将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不满足,满足 方块元素>=基准元素 || 方块索引==基准索引,则向左移动方块;
- 若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次测试结果去最大最小平均值。不知道怎么插入三线表,就用图片代替啦。
由测试结果可以明显看出:对于有序数据,粗暴的快排真的是无能为力,而有序性检验发挥了强大的作用;对于无序数据,快排是真快,但有序性检验并未占用太多时间。