第一章 Collections 类、泛型类和Timing类概述

摘抄《数据结构与算法(C#语言描述)》 删除很多废话

1.1群集(collection)的定义

  群集是一种结构化的数据类型。存储数据,并且提供数据的添、删、改操作,以及对群集不同属性值的设置与返回操作。

  群集分为两类:线性与非线性群集。

  线性群集是一张元素列表,表中元素顺次相连。(1、2、3、4)计算机世界中数组为线性群集。

  非线性群集所包含的元素在群集内没有位置次序之分。(2,3,6,1)计算机世界中 树、堆、图和集都是非线性群集。

1.2群集(collection)的描述

  在两种主要的群集类中有几个子类别。线性的群集可能是直接存取群集,也可能是顺序存取的群集。而线性的群集既可以是层次群集,也可以是组群集。

1.2.1直接存取群集(collection)

  直接存取集群最常见的实例就是数组。这里把数组定义为具有相同数据类型的元素群集,而且所有数组元素可通过索引直接进行存取访问(Item[0]、Item[1]、Item[2])

  数组可以是静态的,这样当声明数组的时候便于针对程序长度来固定指定元素的数量。数组也可以是动态的,通过ReDim或者ReDim Preserve 语句就可以增加数组元素的数量。(注意此方法适用于VB,C#请使用Array.Resize(obj,n))

  在c#语言中,数组不只是内置的数据类型,它还是一种类。

  我们可以用数组类存储一个线性群集。向数组添加新元素很容易,只需要简单的吧新元素放置在数组尾部。但是,在数组中插入一个元素就不那么高效了。因为要给插入的元素空出位置,所以必须按顺序移动数组元素。从数组的尾部删除一个元素也很效率,但是删除指定任意位置的元素就没有那么效率了。

  字符串是直接存取群集的另外一种类型。字符串是字符的群集。和存取数组元素的方式一样也可以基于字符串的索引对其进行存取。在C#语言中,字符串也是作为类对象来实现的。这个类包含一个在字符串上执行标准操作的庞大的方法集合,其中操作有串连接、返回子串、插入字符、移除字符等等。

  C#字符串是不可变的。意味着一旦初始化就不能再改变。当要修改字符串的时候,不是改变原始字符串而是创建一个字符串的副本。在某些情况下这种行为可能会导致性能急剧下降,所以.NET框架提供了StringBuilder类来让用户处理可变字符串。

  结构(在其他编程语言中也被称为记录)是最后一种直接存取的群集类型。结构是一种复合数据类型。它所包含的数据可能拥有许多不同的数据类型。由于把这些数据的值分别存储在分散的变量内是很容易变混淆的,所以编程语言采用结构来存储此类数据。

  C#语言的结构所增加的强大能力就是为执行存储在数据上的操作定义了方法。尽管不能从结构继承或推导出一种新的类型,但是这种做法使得结构在某些地方很像一个类。

using System;
public struct Name
{
private string fname, mname, lname;
public Name(string first, string middle, string last)
{
fname = first;
mname = middle;
lname = last;
}
public string firstName
{
get { return fname; }
set { fname = firstName; }
}
public string middleName
{
get { return mname; }
set { mname = middleName; }
}
public string lastName
{
get { return lname; }
set { lname = lastName; }
}
public override string ToString()
{
return (String.Format("{0} {1} {2}", fname, mname,lname));
}
public string Initials()
{
return (String.Format("{0}{1}{2}", fname.Substring(, ),mname.Substring(, ), lname.Substring(, )));
}
}
public class NameTest
{
static void Main() {
Name myName = new Name("Michael", "Mason", "McMillan");
string fullName, inits;
fullName = myName.ToString();
inits = myName.Initials();
Console.WriteLine("My name is {0}.", fullName);
Console.WriteLine("My initials are {0}.", inits);
}
}

  虽然.NET 环境中许多元素都是作为类来实现的(比如数组和字符串),但是语言的几个主要元素还是作为结构来实现的,比如数字数据类型。例如,整数类型就是作为 Int32 结构来实现的。采用 Int32的方法之一就是把用字符串表示的数转换成为整数的 Parse 方法。

1.2.2顺序存取群集Sequential Access Collections

  顺序存取群集是把群集元素按顺序存储的表。这里此类群集称为线性表。线性表在创建时没有大小限制,他们可以动态扩展和收缩。不能对线性表中数据项进行直接存取访问,需要通过数据项的位置对其进行存取。线性表的第一个元素在表头的位置,最后元素在表的末尾。由于不能直接存取线性表元素,为了访问某元素就需要遍历线性表直到找到元素的位置为止。线性表的实现通常允许两种遍历表的方法:一种是单向从前往后遍历,而另外一种则是双向遍历,即从前向后和从后向前遍历。

  线性表的一个简单实例就是购物清单。顺次写下要购买的全部商品就会形成一张购物清单。在购物时一旦找到某商品就将它从清单中划掉。

  线性表既可以是有序的,也可以是无序的。有序线性表具有顺次对应的有序值。而无序线性表则是由无序元素组成的。二叉查找算法与简单线性查找法在查找表中数据时线性表的顺序会产生很大的差异。

  线性表的某些类型限制访问数据元素。这类线性表有堆栈和队列。堆栈是一种只允许在表头存取数据的表。在表的顶端放置数据项,而且也只能从表的顶端移出数据项。正因为这种原因,堆栈被称为后进先出结构,这里把向堆栈添加数据项称为入栈(压栈),而把从堆栈中移出数据项称为出栈。

  堆栈是非常常见的一种数据结构,特别是在计算机编程中尤为普及。在堆栈的众多应用中,它常用于算数表达式计算和平衡符号。

  队列是一种只允许在表尾进行数据项添加,表头进行移出操作的表。他被称为先进先出,这里把向队列添加数据项称为EnQueue,而把从队列中移出数据项称为DeQueue。

  队列既可以用于调度操作系统任务的系统编程,也可以用于模拟研究的编程。在每一种能想象到的少量情况下,队列都可以模拟等待队列产生极好的结构。优先队列是队列的一种特殊类型。它允许最先移出队列的数据项具有最高优先级。

  索引群集这类群集一种散列表,它存储了一组与关键字相关关联的数据值,在散列表中有一个被称为散列函数的特殊函数。此函数会取走一个数据值,并且把此数据值称为关键字转换成用来取回数据的整数索引。然后此索引会用来存取方位关键字相关联数据记录。C#语言中有一个HashTable的类用来存储散列的数据。

  另外一种索引群集就是字典,字典也被称为联合,他是由一系列键值对构成。此结构类似字典.NET框架中有几种Dictionary类。

1.2.3层次群集

  非线性群集分为两大主要类型:层次群集和组群集。层次群集是一组划分了层次的数据项集合。位于某层的数据项可能会有位于较底层上的数据项。

  树一种常见的层次集群,树群看上去像是一颗倒立的树,其中一个数据项作为根,而其他数据项则作为叶子挂在根的下面。树的元素被称为节点,而且在特定节点下面的元素被称为是此节点的孩子。

  树在几种不同领域都有应用,大多数现代操作系统的文件系统都是采用树群集设计而成的,其中一个目录作为根,其他子目录作为根的孩子。

  二叉树是树群集的一种特殊类型,树中每个节点最多只有二个孩子。二叉树可以变成二叉查找树,这样做可以极大的提高查找大数据的效率。实现的方法依据从根到要存储的数据的节点的路径为最短路径的方式来放置节点。

  还有一种树类型就是堆。堆这样的组织就是为了便于把最小数据值始终放置在根节点上。在删除时会移除根节点,堆的插入和删除操作总会导致堆重组,因为只有这样才能把最小数据值始终放置在根节点上,我们经常会用堆来排序,这被称为堆排序。通过反复删除根节点以及重组堆的方式就可以对存储在堆内的数据元素进行排序。

1.2.4组群集

  数据项为无序的非线性群集被称为组。集合、图、和网络是组群集的三种主要类型。

  集合是一种无序的数据值群集,并且每一个数据值都是唯一的。就像整数一样,班级中学生的列表就是一个集合的实例。在集合上执行的操作包括联合和交叉。

  图是由节点集合以及与节点相连的边集合组成的。图用来对必须访问图中每个节点的情况进行建模,而且有些时候还要按照特定顺序进行访问。这样做的目的是为了找到“遍历”图的最有效的方法。图可用于计算机科学和数学研究领域。大家可能听说过“旅行商”问题。这就是图问题的一种特殊类型,此问题需要在预算允许的情况下拜访所有城市线路中最有效完整的旅行路线。

  此问题是被称为NP-完全问题的其中一部分内容。这就意味着针对类型大量问题是无法知道确切解决方案的。10个城市就需要的阶乘这么多条路线,这就等于是3628800条路线。如果把问题扩展为100座城市就需要检查100的阶乘条路线。就目前是无法用现在的方法实现的。因此需要找到一种近似的解决方案。

  网络是一种图的特殊类型。网络的每一条边都被赋予了权。权同使用某边从一个节点移动到另外一个节点所花费的代价。带权的城市网络,其中这里的权是两座城市的距离。

1.3 CollectionBase类

  .NET框架库不包括用于存储数据的通用Collection类,但是大家可以使用抽象类CollectionBase类来够着自己的Collection类。CollectionBase类为程序员提供了实现定制Collection类的能力。CollectionBase类隐含的实现了两个为构造Collection类所必须的接口,即ICollection和IEnumerable,而留给程序员的工作就只是对这些作为Collection类特殊内容方法的实现。

1.3.1定义 Collection类

  在C#语言中定义一个Collection类最简单的方法就是把在System.Collections库中已找到的抽象类CollectionBase
类作为基础类。此类提供了一套可以实现构造自身群集的抽象方法集合。CollectionBase 类还提供了一种基础的数据
结构——InnerList(一个 ArrayList)。此结构可以用作自身类的基础。本章节会看到如何使用 CollectionBase 来构造
Collection类。

1.3.3实现 Collection类

  弥补Collection类的全部方法包括一些与类是基础数据结构InnerList相交互的类型。要实现的方法:Add、Remove、Count、Clear方法。尽管定义的其他方法可以使类更有用,但是上述这些方法是类的绝对基本要素。

  首先从 Add方法开始。这种方法有一个参数,即 Object变量。此变量用来保存群集要添加的数据项。

public void Add(Object item)
{
InnerList.Add(item);
}

ArrayList把数据项作为对象(即Object数据类型)类存储。这就是把数据项声明为Object的原因。

Remove方法执行与上述类似

public void Remove(Object item)
{
InnerList.Remove(item);
}

接下来是 Count 方法。Count 最常作为属性来实现,但是这里更喜欢把它用作方法。而且,由于是在基础类
CollectionBase 中实现Count,所以必须用新的关键词来隐藏在 CollectionBase 中找到的Count的定义:

public new int Count()
{
return InnerList.Count;
}

Clear方法把全部数据项从InnerList中移除掉。这里也需要在方法定义中使用新的关键词:

public new void Clear()
{
InnerList.Clear();
}

了解这些内容足够开始了。下面来看一个用 Collection类且带有完整类定义的程序:

using System;
using System.Collections;
public class Collection : CollectionBase
{
public void Add(Object item)
{
InnerList.Add(item);
}
public void Remove(Object item)
{
InnerList.Remove(item);
}
public new void Clear()
{
InnerList.Clear();
}
public new int Count()
{
return InnerList.Count;
}
} class chapter1
{
static void Main()
{
Collection names = new Collection();
names.Add("David");
names.Add("Bernica");
names.Add("Raymond");
names.Add("Clayton");
foreach (Object name in names)
{
Console.WriteLine(name);
}
Console.WriteLine("Number of names: " + names.Count());
names.Remove("Raymond");
Console.WriteLine("Number of names: " + names.Count());
names.Clear();
Console.WriteLine("Number of names: " + names.Count());
}
}

为了创建一个更加有用的Collection类,还可以实现几种其他的方法。大家可以在练习中实现一些这样的方法。

1.4泛型编程

  面向对象编程的问题之一就是“代码膨胀”的特征。为了说明方法参数所有可能的数据类型而需要重载某种方法或重载一套方法集合的时候,就会发生某种类型的代码膨胀。代码膨胀的解决方案之一就是使某个值呈现多种数据类型的能力,同时仅提供此值的一种定义。这种方法被称为是泛型编程。

  泛型编程提供数据类型的“占位符”,它在编译时由特定的数据类型填充。C#语言中这个占位符用一对尖括号(< >)和放
在括号间的标识符来表示。

  泛型编程第一个规范实例就是Swap函数。下面是C#语言中范型 Swap函数的定义:

static void Swap<T>(ref T val1, ref T val2)
{
T temp;
temp = val1;
val1 = val2;
val2 = temp;
}

  虽然泛型编程的这种用法可能是十分有用的,但是 C#语言提供了备用的泛型数据结构库。在System.Collection.Generics 命名空间中都可以找到这些数据结构,而且在讨论作为此命名空间内容的数据结构的时候,还将对它的使用做分析。虽然通常情况下这些类具有和非泛型数据结构类相同的功能性,但是由于其他方法及其用途没有什么不同,所以通常会为了如何实例化类的对象而限制泛型类的讨论。

1.5时间测试

  由于本书采用了一种实用的方法来分析数据结构与算法检测,所以这里避开使用大 O分析法,而采用运行简单基准测试的方式来代替。这种测试将会说明运行一段代码需要多少秒数(或者无论什么时间单位)。
基准法测试是用时间测试的方式来衡量运行完整算法所花费的时间长度。如同科学一样,基准测试也像是一门艺术,而且为了获得精确分析需要很小心测试代码的方法。下面就来进行详细讨论。

1.5.1一个简单化的时间测试

  首先时间测试需要一些代码。出于简单的考虑,这里将测试一个有关控制台数组内容的子程序。代码如下所示:

static void DisplayNums(int[] arr)
{
for (int i = ; i <= arr.GetUpperBound(); i++)
Console.Write(arr[i] + " ");
}

  为了测试这个子程序,需要创建一个变量,并且把子程序调用时的系统时间赋值给此变量。此外,还需要一个变量用来存储子程序返回时的时间。根据这些内容写出了下述这段代码:

DateTime startTime;
TimeSpan endTime;
startTime = DateTime.Now;
endTime = DateTime.Now.Subtract(startTime);

  在作者笔记本(运行环境:机器主频1.4mHz,操作系统Windows XP专业版)上运行此代码时,子程序的运行时间大约为 5 秒左右(4.9917 秒)。虽然这段代码对执行时间测试好像很有道理,但是在.NET 环境下运行时间代码是完全不合适的。为什么呢?

  首先,代码测量的是从子程序调用开始到子程序返回主程序之间流失的时间。但是测试所测量的时间也包含了与 C#程序同时运行的其他进程所用的时间。

  其次,时间代码不考虑.NET 环境下执行的无用单元收集。在类似.NET这样的运行时间环境中,系统可能在执行无用单元收集的任何一个时间暂停。时间代码实例没有考虑无用单元收集时间,以及很容易受无用单元收集影响的结果时间。那么到底应该怎么做呢?

1.5.2 用于.NET环境的时间测试

  在.NET 环境中需要考虑程序运行中的线程以及无用单元收集可能在任何时候发生的事实。所以在编写时间测试代码时需要考虑这些情况。

  先来看一下如何处理无用单元收集。首先讨论一下无用单元收集的用途。C#语言用有时被称为堆的内存来给参考类型(例如字符串、数组以及类事例对象)分配存储空间。堆是用来保存数据项(前面提到的类型)的内存区域。诸如普通变量这样的值类型则存储在堆栈中。引用的参考数据也存储在堆栈中,但是实际的数据则是以参考类型的形式存储在堆中。

  当声明变量的子程序完全执行结束时就可以释放掉存储在堆栈中的变量。而另一方面,存储在堆中的变量则会一直保留到调用无用单元收集进程的时候。当没有引用堆数据的行为时,只有通过无用单元收集才可以移除这些数据。

  在程序执行过程中无用单元收集可能会发生在任何时候。然而需要确保在实现时间测试代码时没有运行无用单元收集器。但是也许大家听说过通过强制调用无用单元收集器来进行专门的无用单元收集。.NET环境为执行无用单元收集调用提供了专门的对象——GC。为了使系统执行无用单元收集,可以有如下简单书写: GC.Collect();

  但是不是所有都要这样做的。存储在堆中的每一个对象都有一个称为 finalizer 的专门方法。finalizer方法是在删除对象之前执行的最后一步。有关 finalizer 方法的问题是,这些方法不是按照系统方式运行的。事实上,甚至无法确信对象的 finalizer 方法是否真的执行了,但是知道在确定删除对象之前需要执行此对象的 finalizer 方法。为了确信这一点,我们添加了一行代码来告诉程序等待堆上对象的所有 finalizer 方法都运行后再继续。此代码行如下: GC.WaitForPendingFinalizers( );

  已经清除了一个障碍,现在就剩下一个问题了采用正确的线程。在.NET环境中,程序运行在被称为应用程序域的进程中。这就允许操作系统在同一时间内分开运行每个不同的程序。在进程内,程序或程序的一部分是在线程内运行的。操作系统通过线程来分配程序的执行时间。在用时间测试程序代码时,需要确信正在进行时间测试的代码就在为自身程序分配的进程中,而不在操作系统执行的其他任务里。

  在.NET 框架下通过使用Process类可以做到这一点。Process类拥有的方法允许选取当前的进程、选取程序运行其内的线程,以及选取存储线程开始执行时间的计时器。这些方法中的每一个都可以合并成一个调用。此调用会把它的返回值赋值给一个变量用来存储开始时间(TimeSpan对象)。如下列代码所示(没错,就是两行代码):

  TimeSpan startingTime;
  startingTime = Process.GetCurrentProcess().Threads[0].UserProcessorTime;

  剩下要做的就是在进行时间测试的代码段停止时捕获时间。做法如下:

  duration = Process.GetCurrentProcess().Threads[0].UserProcessorTime.Subtract(startingTime);
  现在把所有这些合并成一个程序。此程序的代码和先前测试代码是一样的:

 using System;
using System.Diagnostics;
class chapter1
{
static void Main()
{
int[] nums = new int[];
BuildArray(nums);
TimeSpan duration;
DisplayNums(nums);
DisplayNums(nums);
DisplayNums(nums);
duration = Process.GetCurrentProcess().TotalProcessorTime
Console.WriteLine("Time: " + duration.TotalSecond
}
static void BuildArray(int[] arr)
{
for (int i = ; i <= ; i++)
arr[i] = i;
}
static void DisplayNums(int[] arr)
{
for (int i = ; i <= arr.GetUpperBound(); i++)
Console.Write(arr[i] + " ");
}
}

  采用新改进的时间测试代码后,程序的返回值为0.2526。把此数值与先前第一版时间测试代码返回的将近 5秒的数值进行比较。很明显,这两种时间测试方法之间存在显著差异。因而.NET 环境中的时间测试代码应该使用.NET方法来做。

1.5.3Timing Test 类

  虽然不需要一个类来运行时间测试代码,但是把代码作为类来重写是有意义的,主要原因是如果能够减少测试的代码行数量,就能保证代码的清晰。

  Timing类需要下列数据成员:startingTime——用来存储正在测试的代码的开始时间。     duration——用来存储正在测试的代码的终止时间。 straingTime 和 duration 这两个成员用来存储时间,而且为这些数据成员选择使用 TimeSpan 数据类型。这里就
采用一种构造器方法,此默认构造器会把数据成员全部置为0。 正如看到的那样,Timing类是很小的,它只需要少量方法。下面是定义:

public class Timing
{
TimeSpan startingTime;
TimeSpan duration;
public Timing()
{
startingTime = new TimeSpan();
duration = new TimeSpan();
}
public void StopTime()
{
duration =Process.GetCurrentProcess().Threads[].UserProcessorTime.Subtract(startingTime);
}
public void startTime()
{
GC.Collect();
GC.WaitForPendingFinalizers();
startingTime =Process.GetCurrentProcess().Threads[].UserProcessorTime;
}
public TimeSpan Result()
{
return duration;
}
}
class chapter1
{
static void Main()
{
int[] nums = new int[];
BuildArray(nums);
Timing tObj = new Timing();
tObj.startTime();
DisplayNums(nums);
tObj.stopTime();
Console.WriteLine("time (.NET): " + tObj.Result().TotalSeconds);
}
static void BuildArray(int[] arr)
{
for (int i = ; i < ; i++)
arr[i] = i;
}
static void DisplayNums(int[] arr)
{
for (int i = ; i <= arr.GetUpperBound(); i++)
Console.Write(arr[i] + " ");
}
}

小结

  本章对此书中经常会用到的三种重要技术进行了回顾。尽管不需要编写整个程序,但是一些程序的代码以及要讨论的库都采用面向对象的方式来编写。自行开发的 Collection 类说明了许多基本面向对象的概念,而且这些概念看似贯穿全书。范型编程允许程序员通过限制需要编写或重载的方法的数量来简化几种数据结构的定义。Timing类提供了简单有效的方法来衡量所要学习的数据结构与算法的性能。

练习

  1.  请创建一个名为 Test 的类。此类包含的数据成员有学生姓名和描述试卷编号的一个整数。这个类会在下述情况下使用:当学生提交测试时,他们会把试卷面朝下放到桌子上。如果某位学生需要检查自己试卷的答案,那么老师就需要把试卷堆反过来以便第一份试卷在上面。然后从第一份试卷开始顺次查找,直到找到需要的试卷。随后,就把找到的试卷从试卷堆中取出来。当学生检查完自己的试卷后,再把此试卷重新放到试卷堆的末尾。下面请编写一个窗口应用程序来模拟这种情况。程序包含用户录入姓名和试卷编号的文本框。还要有一个格式列表框用来显示试卷的最终列表。
  应用窗口需要提供四个操作按钮:1.提交试卷;2.学生查看试卷;3.返回一份试卷;以及4.退出。请执行下列操作来测试你的应用程序: 1.录入某姓名和试卷编号。并且把试卷插入到名为 submittedTests的群集里。 2.录入某姓名,从 submittedTests 中删除相关试卷,并且把此试卷插入到名为 outForChecking 的群集里。3.录入学生姓名,从outForChecking中删除相应试卷,并且把此试卷插入到submittedTests中。4,点击退出按钮。退出按钮不会终止应用程序,而是从 outForChecking 中删除所有试卷,并且把它们全部插入到 submittedTests 中,同时显示所有已提交的试卷列表。

2.  请对 Collection类添加下列方法:
a.                Insert
b.                Contains
c.                IndexOf
d.                RemoveAt

3.  请使用Timing类来比较向Collection类和 ArrayList类分别添加了100000个整数时的性能。

4.  请构建属于自己的 Collection类,并且此类不是由CollectionBase 派生而来的。请在实现中使用范型。

习题下载

上一篇:【spring boot】application.properties官方完整文档【参考使用】


下一篇:java 支付宝 第三方即时到账支付 接口