C#中基础排序算法
Posted 苏州程序大白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#中基础排序算法相关的知识,希望对你有一定的参考价值。
C#中基础排序算法
大家好,我是苏州程序大白。今天是五一假最后一天了。大家做好上班的准备了吗???五一大家去哪里玩了。在评论区分享下。不多说了。下面讲讲C#中基本的排序算法。
在计算机中实现存储数据最普遍的两种操作就是排序和查找. 这是从计算机产业初始就已被确认的事实. 这意味着排序和查找也是计算机科学领域最值得研究的两种操作. 本书提到的许多数据结构的主要设计目的就是为了使排序和/或查找更加简单, 同时也是为了数据在结构内的存储更加有效。
本章会介绍有关数据排序和查找的基础算法. 这些算法仅依赖数组作为数据结构, 用到的最“高级”的编程技术只是递归. 本章还介绍了用来非正式分析不同算法之间速度与效率的方法, 此方法贯穿全书。
排序算法
人们在日常生活中所接触到的绝大多数数据都是经过排序的. 比如, 按照字母顺序查询字典. 或者按照名字的字母顺序在电话本中查询电话号码. 再或者邮局会按照下列几个步骤对邮件进行排序分发: 即首先按照邮政编码, 然后再按照街道名称, 最后还要按照姓名. 排序在数据处理中是十分基础的过程, 因而值得认真学习研究.。
正如先前提到的那样, 在编程领域, 对不同的排序算法技术已经有了海量的分析研究. 尽管有一些算法历史非常古老, 也已经做了改进, 但是仍然应该先学习几种简单的排序算法. 这些简单算法就是插入排序、冒泡排序以及选择排序. 这些算法的每一种都很容易理解和实现. 对于任意情况而言这些算法不是最好的全面算法, 但是对于少量数据集合或者其他特殊情况而言, 它们是时间成本与功能表现上性价比最好的算法。
数组类测试环境
为了检验这些算法, 首先需要构造一个可以实现并测试算法的测试环境. 这里将构造一个类来封装数组处理的一些常规操作, 即元素插入操作, 元素访问操作, 以及显示数组内容的操作. 下面是程序的代码:
using System;
class CArray
{
private int[] arr;
private int upper;
private int numElements;
//构造函数
public CArray(int size)
{
arr = new int[size]; //使用size初始化存储数据的数组arr
upper = size - 1; //使用upper标记当前最大数组索引
numElements = 0; //使用numElementsbiao标记实际有意义的元素数量
}
//指定位置插入元素
public void Insert(int item)
{
arr[numElements] = item;
numElements++;
}
//显示全部元素
public void DisplayElements()
{
for (int i = 0; i <= upper; i++)
Console.Write(arr[i] + " ");
Console.WriteLine();//无逻辑功能 换行用
}
//清空元素
public void Clear()
{
for (int i = 0; i <= upper; i++)
arr[i] = 0;
numElements = 0;
}
static void Main()
{
CArray nums = new CArray(50);
for (int i = 0; i <= 49; i++)
nums.Insert(i);
nums.DisplayElements();
Console.ReadKey();
}
}
程序的输出如下所示:
在利用CArray试验排序和查找算法之前, 先来讨论一下如何为CArray对象填充数据. 为了更有效地说明不同排序算法是如何运行的, 数据需要随机放置. 最好的实现方法就是使用随机数生成器来给数组的每个元素进行赋值.
在C#中用Random 类可以产生随机数. 这种类型的对象可以产生随机数. 为了实例化Random对象, 需要给这个类的构造函数传递一个种子值(seed)(随机函数返回的每个随机数, 本质上是使用伪随机算法产生的结果序列, 而这种子, 就是让你人为干扰随机计算结果的一个参数, 在同一个运行环境如电脑, 操作系统等不变的情况下, 随机种子不变, 每次随机出来的随机结果的序列就都一样, 原文在这里说把"种子看做随机数的上界(upper bound)"大错特错, 虽然可以理解写一本书存在存在错误是难免的, 但是这个错有点离谱).
下面是一段采用了随机数为CArray对象设置数据的代码 :
static void Main()
{
CArray nums = new CArray(10);
Random rnd = new Random(100);
for (int i = 0; i < 10; i++)
{
nums.Insert(rnd.Next(0, 100));
}
nums.DisplayElements();
Console.ReadLine();
}
代码运行结果如下(由于是随机, 每次都不一样) :
冒泡排序
首先要讨论的排序算法就是冒泡排序. 冒泡排序是可用的最慢排序算法之一, 但是它也是最容易理解和实现的排序算法之一, 所以这里把它作为最先介绍的排序算法.
这种排序算法的得名是由于数值“像气泡一样”从序列的一端浮动到另一端. 假设现在要把一列数按升序方式进行排序, 即较大数值浮动到列的右侧, 而较小数值则浮动到列的左侧. 这种效果可以通过下列操作来实现: 多次遍历整个列, 并且比较相邻的数值, 如果左侧的数值大于右侧数值就进行交换.
图3-1 举例说明了冒泡排序算法的工作原理. 图中的两个数字(2 和72)用圆圈进行了突出表示. 从图上可以看出数字72 是如何从数组的开头移动到数组中部的, 而数字2 又是如何从数组的后半部分移动到了数组的开头. (注意, 这图少了72逐渐移动到中间的过程, 这书的小错误真不少)
冒泡排序算法的逻辑如下 :
//添加到CArray类的冒泡排序函数
public void BubbleSort()
{
int temp;
//最外层循环, 从最后一个元素开始, 倒序前进, 到第二元素结束
for (int outer = upper; outer >= 1; outer--)
{
//内层循环,最初循环从头循环到最后一个元素次, 然后每轮的结束位置向前一位
//少一次的原因就是每次内层循环都会把一个最大的数放在末尾,下一轮循环不需要再比较末尾的数了
for (int inner = 0; inner <= outer - 1; inner++)
{
//每轮内层循环都从头开始逐个比较两个相邻元素, 每次循环比较索引+1
if ((int)arr[inner] > arr[inner + 1])
{
//如果前面的数大于后面的数, 二者交换位置
temp = arr[inner];
arr[inner] = arr[inner + 1];
arr[inner + 1] = temp;
}
}
}
}
这段代码有几个地方需要注意. 首先, 交换数组元素的代码是写在主程序中的一行, 而没有用子程序. 如果多次调用交换子程序, 就可能会降低排序的速度. 既然交换代码只有短短三行的长度, 所以不把代码放在子程序内也不会影响代码的清晰度。
(此处所说的子程序, 指的就是没有把交换元素的代码单独再写一个函数)
更加需要注意的是程序中最外层的循环是从数组的末尾处开始, 并且向数组的开始处移动. 如果回顾图3-1 就会知道, 每次内循环结束, 最大元素就会移动到数组末尾. 所以每次外循环时候, 都有一个末尾处的元素不需要内循环处理了, 因而算法不需要再访问这些数值了。
内层循环从数组的第一个元素开始, 会对索引为inner和inner+1 的两个相邻元素进行比较, 并且根据规则交换它们的数值。
检验排序过程
在开发算法的过程中可能要做的事情之一就是在程序运行期间观察代码的中间结果. 在使用Visual Studio. NET 的时候, 可以用IDE 自带的调试工具来实现. 然而, 有些时候全部真正要观测的却是数组的内容(或者是自行构建、排序或查找的数据结构的内容). 一种简便的实现方法是在代码的适当位置上插入控制台打印方法.
对于前面提到的BubbleSort方法而言, 检测数组在排序过程中如何变化的最佳位置就是在内、外层循环之间. 如果为两个循环的每次重复执行插入输出显示, 就可以看到数值在排序过程中如何在数组中移动的记录.
例如, 下面是添加了显示中间结果的BubbleSort方法:
public void BubbleSort()
{
int temp;
for (int outer = upper; outer >= 1; outer--)
{
for (int inner = 0; inner <= outer - 1; inner++)
{
if ((int)arr[inner] > arr[inner + 1])
{
temp = arr[inner];
arr[inner] = arr[inner + 1];
arr[inner + 1] = temp;
}
}
this.DisplayElements(); //显示每轮内层循环后的结果变化, 之前的代码都跟前面一样
}
}
上面的代码把DisplayElements()方法放置在了两个For循环之间. 然后修改Main函数如下 :
static void Main()
{
CArray nums = new CArray(10);
Random rnd = new Random(100);
for (int i = 0; i < 10; i++)
{
nums.Insert(rnd.Next(0, 100));
}
Console.WriteLine("排序前: ");
nums.DisplayElements();
Console.WriteLine("排序中...: ");
nums.BubbleSort();
Console.WriteLine("排序后: ");
nums.DisplayElements();
Console.ReadLine();
}
运行结果如下 :
选择排序
下一个要讨论的排序算法是选择排序. 对于有N个元素的数组来说, 这种排序是从数组的起始处开始, 把第一个元素与数组中其他元素进行比较. 然后, 将最小的元素放置在第 0 个位置上, 接着再从第1 个位置开始重复以上操作, 一直到第N-1个元素完成这种选择排序后才终止. 。
在选择排序算法中使用了两层循环. 外层循环从数组的第一个元素移动到数组第N-1个元素, 而内层循环则从数组的第二个元素移动到数组的最后一个元素, 并且内循环遍历一遍之后, 就会把找到的最小值赋值到本轮内循环最开始的索引位置上. 图3-2 举例说明了此算法是如何处理前面用到的CArray类数据的。
实现选择排序算法的代码如下所示 :
//添加到CArray类的选择排序函数
public void SelectionSort()
{
int min, temp;
//外层循环从第一个元素开始, 到倒数第二个元素结束,
//此处原文代码写错了, 写的是outer <= upper, 外循环多了
for (int outer = 0; outer <= upper-1; outer++) {
min = outer; //先将最小值索引指向当前外层循环变量对应的索引处
//内层循环从外层循环的索引后面一位开始, 到最后一个元素结束
for (int inner = outer + 1; inner <= upper; inner++) {
//在内层循环中找到从outer位开始到upper位之间的最小元素的索引
if (arr[inner] < arr[min])
min = inner;
}
//内层循环结束后, 找到的最小元素与outer索引位置的元素进行位置交换
temp = arr[outer];
arr[outer] = arr[min];
arr[min] = temp;
this.DisplayElements();
}
}
运行结果如下 :
插入排序
本章最后将要看到的基础排序算法是最容易理解的算法之一, 即插入排序算法.
插入排序算法类似于人们通常按照数字顺序或者字母顺序进行排序的方法. 假如我要求全班同学上交填有本人姓名、学号以及简短自我介绍的索引卡片. 而学生们交回来的卡片是随机排列的. 如果要把卡片按照字母排序排列, 就可以构建出一张座次表了.
所以, 我把这些卡片带回了办公室, 并且清理出了办公桌. 紧接着我拿出了第一张卡片. 卡片上的名字是Smith. 我把它放在办公桌最左侧的位置上, 然后又拿出了第二张卡片. 这张是Brown. 于是, 我把Smith的卡片移动到右侧, 并且把Brown 的卡片放到Smith原来的位置上. 下一张卡片是Williams. 不需要移动任何其他的卡片就可以把它放在最右侧的位置上. 接下来的卡片是Acklin. 它需要放置在队列的开始处, 所以其他所有的卡片都必须向右移动一个位置以便腾出空间放Acklin. 这就是插入排序算法的工作原理.
插入排序的代码如下所示, 跟着的是对此算法工作原理的解释说明:
//添加到CArray类的插入排序函数
public void InsertionSort()
{
int inner, temp;
//最外层循环从第二个元素开始, 到最后一个元素结束
for (int outer = 1; outer <= upper; outer++)
{
temp = arr[outer]; //用中间变量hold住本次要插入的值
inner = outer; //outer要标记循环的进度, 所以不能直接操作, 也用中间变量hold住
while (inner > 0 && arr[inner - 1] >= temp)
{
//只要是从本次要插入的值向前看, 但凡有比它大的数, 就往后移, 准备给它插入让出位置
arr[inner] = arr[inner - 1];
inner -= 1; //让位后索引向前移动一位
}
//最后一次让位后, inner也减了1, 所以正好指向空出来的位置, 狠狠插入
arr[inner] = temp;
this.DisplayElements();
}
}
插入排序算法有两层循环. 外层循环会逐个遍历数组元素, 而内层循环则会把外层循环所选择的元素与该元素在数组内的上一个元素进行比较. 如果外层循环选择的元素小于内层循环选择的元素, 那么数组元素都向右移以便为内层循环元素留出位置, 这就像前面例子描述的那样.
现在就来看看选择排序是如何处理前面实例中用来排序的数据集合的. 下面是程序的输出结果:
这个输出清楚地表明插入排序不是通过交换来处理的, 而是通过把较大的数组元素向右移动来为数组左侧较小元素留出空间的方式进行操作的。
基础排序算法的时间比较
上述三种排序算法在复杂度和理论上都是十分相似的, 所以在互相进行比较的时候性能表现应该是近似. 这里用Timing类来比较三种算法, 根据它们对庞大数据集合进行排序时所花费的时间判定出是否有谁会与众不同.
为了进行测试, 这里用到基本代码和之前为了说明每种算法的工作原理而使用的代码完全一样. 但是, 在下面这些测试中, 为了说明三种算法是如何处理较小数据集合和较大数据集合的, 数组的大小是有变化的. 时间测试程序要分别运行处理元素量为100、1000、甚至10000的几种情况. 下面是代码:
(注释掉三个排序函数中的this. DisplayElements()语句, 不然刷屏非常慢)
static void Main()
{
//原文用的100, 1000和10000, 我的电脑已经是成书10年后的电脑了太快, 1000个元素的话时间全是0
//我改成了5000, 50000, 500000
int numItems = 5000;
TimingComparison(numItems);
numItems = 10000;
TimingComparison(numItems);
numItems = 20000;
TimingComparison(numItems);
Console.ReadLine();
}
static void TimingComparison(int numItems)
{
Console.WriteLine(numItems + "个随机元素排序时:");
Timing sortTime = new Timing();
Random rnd = new Random(100);
CArray theArray = new CArray(numItems);
for (int i = 0; i < numItems; i++)
//此处原文用的rnd.NextDouble() * 100, 存在类型转换异常, 我改成了rnd.Next(), 下同
theArray.Insert(rnd.Next());
sortTime.StartTime();
theArray.SelectionSort();//注释掉方法里的this.DisplayElements(), 不然运行到天荒地老
sortTime.StopTime();
Console.WriteLine("选择排序耗时:" + sortTime.Result().TotalMilliseconds + "毫秒");
theArray.Clear();
for (int i = 0; i < numItems; i++)
theArray.Insert(rnd.Next());
sortTime.StartTime();
theArray.BubbleSort();//注释掉方法里的this.DisplayElements(), 不然运行到天荒地老
sortTime.StopTime();
Console.WriteLine("冒泡排序耗时:" + sortTime.Result().TotalMilliseconds + "毫秒");
theArray.Clear();
for (int i = 0; i < numItems; i++)
theArray.Insert(rnd.Next());//注释掉方法里的this.DisplayElements(), 不然运行到天荒地老
sortTime.StartTime();
theArray.InsertionSort();
sortTime.StopTime();
Console.WriteLine("插入排序耗时:" + sortTime.Result().TotalMilliseconds + "毫秒");
Console.WriteLine();
}
以上代码运行结果:
关注苏州程序大白,持续更新技术分享。谢谢大家支持
以上是关于C#中基础排序算法的主要内容,如果未能解决你的问题,请参考以下文章