数据结构 C#描述 第三章 (更新)
第三章:基本的排序算法
对存储在计算机中的数据的两个基本操作是排序和查找。在计算机工业初期这是个事实,也说明了查找和排序是计算机科学中研究最多的。在本书中讨论的很多的数据结构都被设计成了排序或者查找很容易和高效的数据存储结构。
本章向你介绍排序和查找算法的基础。这些算法仅仅只是在用在当把数据序列当成一种地和具结构和仅仅用在“高级”程序设计中。本章还会介绍我们本书用来分析算法的速度和效率的技术。
排序算法
在每天的生活中我们打交道的数据都是有序的。我们查字典,通过名字查电话号码的,邮局通过不同的方式将邮件排序,如:邮编,街道号,名字等等。排序是与数据打交道时的基本的处理,也值得我们进一步的学习。
正如我们稍后提到的,现在对于不同的排序技术已经有了很多的研究。虽然已经有了很多很高明和复杂的排序算法,但是你还是应该从简单的排序算法开始学习。排序算法有:插入排序,冒泡排序,选择排序。每一种算法都很容易理解和实现。它们并不是所有的排序算法,但是对于少数的数据和一些特定的情况,它们是最好的算法。
Array类的测试平台
为了测试这些算法,我们首先需要一个测试并且实现它们的平台。我们将建立一个类,这个类封装了对一个数据集的基本操作:元素的插入,访问以及显示整个数据集。代码如下:
using System;
class CArray
{
private int[] arr;
private int upper;
private int numElements;
public CArray(int size)
{
arr = new int[size];
upper = size - 1;
numElements = 0;
}
public void Insert(int item)
{
arr[numElements] = item;
numElements++;
}
public void DisplayElements()
{
for (int i = 0; i <=upper; i++)
Console.Write(arr[i] + " ");
}
public void Clear()
{
for (int i = 0; i <= upper; i++)
arr[i] = 0;
numElements = 0;
}
}
public class Test
{
static void Main()
{
CArray nums = new CArray(10);
for (int i = 0; i < 10; i++)
{
nums.Insert(i);
}
nums.DisplayElements();
}
}
结果显示如下:
我们先不检查CArray类的排序和查找的算法,让我们讨论我们是怎样将数据存储到一个CArray类的对象中的。为了说明不同的排序算法工作的高效性,存储的数据必须是随机的。那么我们最好用一个随机数产生器来给数据集的每个元素赋值。
能够用C#语言中的Random类来产生随机数。为了初始化一个Random对象,你必须把一个种子传入类的构造器中。这个种子就是随机器能够产生的数的最大上限。
下面来看另外的一个程序,我们用随机器产生随机数,然后将数存储到CArray中:
static void Main()
{
CArray nums = new CArray(10);
Random rnd = new Random(100);
for (int i = 0; i < 10; i++)
{
nums.Insert((int)(rnd.NextDouble() * 100));
}
nums.DisplayElements();
}
结果显示如下:
冒泡排序
我们首先来测试冒泡排序。冒泡排序是可排序算法中最慢的排序之,但是它却是最简单而且最容易理解的算法,尤其作为我们理解排序算法时,最容易上手。
给这个排行算法这样命名,是因为算法中的值就像“冒上的汽泡”从数据列表的最下面“冒”到上面。假如你想把一列数据升序排序,大的值在右边,小的值在左边。这种排序就会引起数据的多次移动,而且多次很它们之间相邻的数据的比较,交换位置。
如下图中就描述了冒泡算法是怎样工作的。如图将用圈标识的两个数据插入到数据集中。你可以看到72是怎样从数据集的前端一道移动到中间,以及2是怎样从中间移动到最前端的。
3.1
下面的代码就描述了冒泡算法:
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;
}
}
}
}
在这段代码中有一些要注意的地方。首先,代码中交换数据时是分行写代码,而没有将交换写为一个子程序(方法)。当一个交换的功能要被多次调用时,就要将之写为一个方法。因为交换的代码仅仅只有3行,所以不应该牺牲代码的清晰而来将交换实现为一个子程序。
更重要的,注意到最外面的循环是从数据集的最后开始慢慢向前移动的。如果你回头看图3.1,最大值它的合适的位置是在数据集的末尾。这就说明大的值到达合适的位置后,外面的循环就不在访问这个大的值了。
在内部循环中,循环第一个开始向后,直到它到达最后元素的前面。内部的循环主要是在必要的时候交换两个相邻数据的值。
检测排序处理
当你开发一个算法的时候,你可能想知道当程序运行时中间结果。当你用Visual Stuio.NET的时候,你可以用IDE中的Debugging调式调试工具。有时候,你真正想看的是数据集的显示。一个很简单的方法就是在代码的合适的位置插入一段显示的代码。
正如前面提到的冒泡排序算法,最好检查数据交换的位置就是在外部循环和内部循环之间。如果我们在两个循环都做遍历,我们可以看到一个数据记录是怎样在排序时移动的。
例如,下面是修改后的冒泡排序算法来显示中间结果的:
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()方法在两个循环之间。主程序修改如下:
static void Main()
{
CArray nums = new CArray(10);
Random rnd = new Random(100);
for (int i = 0; i < 10; i++)
{
nums.Insert((int)(rnd.NextDouble() * 100));
}
nums.DisplayElements();
Console.WriteLine("排序之前:");
nums.DisplayElements();
Console.WriteLine("排序时:");
nums.BubbleSort();
Console.WriteLine("排序之后");
nums.DisplayElements();
}
结果显示如下:
选择排序
接下来的排序就是选择排序。这种排序从数据集的前段开始,和数据集中的元素一个个的比较。最小的元素放在位置0处,然后排序算法从位置1开发进行。直到出最后一个位置外,其他的位置都重新开始一个循环。
在选择排序中用双重循环。外部的循环将第一个元素移到下一个,或者最后,而内部的循环从第二个元素开始直到最后一个,寻找当前值比被移动的元素值的小的值。每一次内部循环后,最小的值就被放到了数据集的合适的位置。如下3.2所示:
选择排序的实现算法的代码如下:
public void SelectionSort()
{
int min, temp;
for (int outer = 0; outer <= upper; outer++)
{
min = outer;
for (int inner = outer + 1; inner <= upper; inner++)
if (arr[inner] < arr[min])
min = inner;
temp = arr[outer];
arr[outer] = arr[min];
arr[min] = temp;
}
}
3.2
本章中我们讨论的最后的一个排序算法是插入排序。
插入排序
在把事物按照数量或者是字母排序时插入排序是最常用的。例如,我要班上的学生把写有他们的名字,编号的卡片上交。学生交卡片时的顺序是随机的,但是我想按照字母排序,以便于我们建立座位表。
我把卡片带回到我的办公室,清理完我的桌子后,我拿起第一张卡片。卡上的名字是Smith。我把它放在桌子左边的最上面,然后我拿起第二张。第二张是 Brown,我把将Smith移到右边,然后把Brown放到Smith的位置。下一张是Williams。它必须放在最右边,而不需要移动其他的卡片。下一张是Acklin。它必须放在最左边,所以其他的卡片必须右移而挪出空间。这描述了插入排序的工作情况。
下面的代码就是实现插入排序的代码:
public void InsertionSort()
{
int inner, temp;
for (int outer = 1; outer <= upper; outer++)
{
temp = arr[outer];
inner = outer;
while (inner > 0 && arr[inner - 1] >= temp)
{
arr[inner] = arr[inner - 1];
inner--;
}
arr[inner] = temp;
}
}
插入排序有两个循环。外部的循环移动数据集内的元素,而内部循环则比较外部循环中的元素和它的下一个元素。如果在外部循环中被选中的元素比内部循环中选中的元素小,那么数据集中的元素要右移动腾出空间,就如上面的例子描述的一样。
比较几个排序算法的时间测试
这三个排序算法在复杂性和理论性上都很简单。我们用时间测试类来比较三个算法,看看在排序大数量的数据时,谁的效率更高些。
为了便于测试,我们就用之前的相同的代码来说明算法的执行。在下面的测试中,数据集的大小是个变量,这样做是为了说明当数据量大和数据量小时,三个算法的执行情况。时间测试算法分别测试当数据量是100,1000,10000进行的。代码如下:
static void Main()
{
Timing sortTime = new Timing();
Random rnd = new Random(100);
int numItems = 1000;
CArray theArray = new CArray(numItems);
for (int i = 0; i < numItems; i++)
{
theArray.Insert((int)(rnd.NextDouble() * 100));
}
sortTime.startTime();
theArray.SelectionSort();
sortTime.stopTime();
Console.WriteLine("Time for Selection sort:" + sortTime.getResult().TotalMilliseconds);
theArray.Clear();
for (int i = 0; i < numItems; i++)
theArray.Insert((int)(rnd.NextDouble() * 100));
sortTime.startTime();
theArray.BubbleSort();
sortTime.stopTime();
Console.WriteLine("The time for Bubble sort :" + sortTime.getResult().TotalMilliseconds);
theArray.Clear();
for (int i = 0; i < numItems; i++)
theArray.Insert((int)(rnd.NextDouble() * 100));
sortTime.startTime();
theArray.InsertionSort();
sortTime.stopTime();
Console.WriteLine("The time for Insert sort :" + sortTime.getResult().TotalMilliseconds);
}
输出显示如下:
说明了选择排序和冒泡排序执行的速度一样,而插入排序是他们两者的一半。
让我们来比较当数据集有1000个数据时的情况:
从这里我们可以看到数据集的大小使得执行的速度有很大的不同。选择排序是冒泡排序的100倍,是插入排序的200倍。
当我们把数据集的大小增大到10000的时候,可以看到:
三个算法的执行速度明显的下降了,虽然选择排序依然比其他两个算法快,但是,在处理大数据量的时候,他们都不是理想的算法。我们将在第16章中介绍一些效率更高的算法。