[算法] c#数据结构(线形表、数组、栈、位标志)
线性表线性表是最简单也是最常用的一种数据结构。例如,英文字母表(A,B,…,Z)是一个线性表,表中的每一个英文字母是一个数据元素;又如成绩单是一个线性表,表中的每一行是一个数据元素,每个数据元素又是由学号、姓名、成绩等数据项组成。线性表是最简单的数据结构,而顺序表和链表作为线性表的两种重要存在形式,是堆栈、队列、串以及树和图等数据结构的实现基础,内容非常重要,一定要认真对待。本章将介绍线性表的定义、线性表的顺序存储结构和链式存储结构以及相关算法实现。这些存储结构在C#类库中都有相对应的集合类,本章也将对这些集合类的原理、实现及使用做进一步地探讨。线性表的定义线性表(Linear List)是具有相同特性的数据元素的一个有限序列(如图2.1所示)。该序列中所含元素的个数称作线性表的长度。线性表中的元素在位置上是有序的,好比储户去银行排队取钱,人们依次排列,排在前面的先取钱,排在后面的后取钱。这种位置上的有序性就是一种线性关系。由此可以看出线性表的前后两个元素存在一一对应的关系。但是需要注意,这种前后关系是逻辑而非物理意义上的。就好比银行进行了改革,使用排队机进行排队,所有储户分散在银行的各个角落,他们取钱的顺序是依据从排队机获取的纸条上的号数来决定的。
http://images.cnblogs.com/cnblogs_com/abatei/122178/C2-1.jpg
线性表的顺序存储结构—顺序表线性表的顺序存储结构是指用一块地址连续的存储空间依次存储线性表的数据元素。这种存储方式好比改革前的银行,需要在业务窗口前排队取钱。由此可以看出顺序表中逻辑上相邻的元素在物理上也是相邻的。顺序表的特点1.
容量固定存储顺序表的元素需要一整块内存空间,因而顺序表的容量一旦确定,便不能更改。为了理解这一点,可以把内存空间想象成一栋学生宿舍楼,一个班集的学生就是一张线性表,宿舍楼的每个房间都可以住10个学生。如图2.2所示,在新生入学时A班的40个学生被安排进了前4个宿舍(101~104),B班的50个学生被安排进了随后的5个宿舍(105~109)。当两个班的学生住进宿舍之后,A班又补录了10个学生,这时如果希望A班的所有学生住在连续的5间宿舍已经变得不可能,因为105宿舍已经住进了B班的学生。其实计算机的内存也存在同样的情况,当为某个表分配了一片固定的内存空间后,这个空间周围的其它内存空间极有可能马上被占用,你无法任意地改变已经分配的内存空间的大小。
http://images.cnblogs.com/cnblogs_com/abatei/122178/C2-2.jpg
1.
访问速度快在顺序表使用索引访问数据元素是非常简单、快速的。如图2.3所示,假设顺序表中的第一个元素的位置是Loc,每个数据元素所占用的存储空间为n,那么可以很快地计算出第i个元素的存储地址为:Loc + (i – 1) * n。http://images.cnblogs.com/cnblogs_com/abatei/122178/C2-3.jpg
2.2.2 数组线性表的顺序存储结构在C#中的最直接表现形式就是数组。在C#语言中,数组是最基础也是存取速度最快的一种集合类型。数组是引用类型,保存它们所需的内存空间会在托管堆上分配,一旦数组被创建,其中的所有元素将被初始化为它们的默认值。
int[] arrInt =
new
int[5];
arrInt[2] =
5;
arrInt[4] =
3;
以上代码声明了一个值类型int的数组,并把它的长度初始化为5,最后分别给第3和第5个元素赋值。它在内存中的分布如图2.4所示:http://images.cnblogs.com/cnblogs_com/abatei/122178/C2-4.jpg
从图2.4可以得知,new int会在托管堆中会划分一块能够存放5个整型数据的内存空间,并且每个元素都会被初始化为0,这意味着数组在被创建的同时就拥有了值。另外数组的长度一旦确定就不能再被更改,这使得数组没有添加和删除元素的操作。任何对于数组的添加和删除元素的操作都只能是逻辑意义上的。注意:在托管堆中创建数组时,除了数组元素,数组对象所占有的内存块还包含一个类型对象指针、同步真索引等额外成员。也就是说new int在内存中划分的空间大于20个字节,图2.4省略了这些额外成员。当数组元素为值类型时,数组对象存放的是值类型对象本身。当元素为引用类型时,数组对象存放的则是对象的引用(指针)。
Control[] arrCtrl =
new Control[5];
arrCtrl[0] =
new Button();
arrCtrl[3] =
new Label();
以上代码声明了一个引用类型Control的数组,并把它的长度初始化为5,最后分别给第1和第4个元素赋值。两个值是分别Button和Label对象,虽然它们都继承自Control类,但两者却是不同类,它们的大小不一样。但数组中各个元素的大小是相同的,这些大小不同的对象是如何存储的呢?下面是arrCtrl数组的内存分布图:http://images.cnblogs.com/cnblogs_com/abatei/122178/C2-5.jpg
由图2.5可知,new Control在托管堆中划分了一块能够存放5个指针的内存空间,并且每个元素都被初始化为null。当使用arrCtrl=new Button() 给数组元素赋值时,首先在托管堆中创建一个Button对象,然后把Button的内存地址存放在数组的第一个元素中,这样就可以通过数组中的指针访问Button对象了。数组有很多优点,但它的缺点也非常明显。在实际编程中,经常需要对集合中的元素进行添加和删除,也需要动态地改变集合的大小,数组显然无法满足这些需求。怎么样才能使数组具有改变空间大小的功能呢?
2.2.3 ArrayList如果要动态地改变数组所占用内存空间的大小,则需以数组为基础进一步抽象,以实现这个功能。以图2.2的学生宿舍为例,为了使A班的所学生住在连续的宿舍内,可以把A班的学生全部搬迁到连续的5间空宿舍内。其效果如图2.6所示:
http://images.cnblogs.com/cnblogs_com/abatei/122178/C2-6.jpg
现实中,为了让一个班新加入的10个学生能跟原来的学生住在一起而把班级整体搬迁,这样的做法显示不合适,因为搬迁的成本太高。但在计算机中,内存成片区域间的拷贝成本是非常低的,这样的解决方案是合理可行的。但是这个解决方案还存在问题,如果一个班级频繁地有新学生加入,为了保证学生能住在连续的宿舍内,整个班级就不得不频繁地搬迁。可以采用以空间换时间的做法来解决这个问题,在学生每次搬迁时,都让班级宿舍的数量是原来的两倍。也就是说,如果原来一个班级有4间宿舍,搬迁后就变为8间,再次搬迁则变为16间。如图2.2所示,A班的宿舍为201~208。206~208这三间宿舍做为本班备用宿舍,不再允许其他班级的学生搬入。C#中的ArrayList正是采用上述方法来动态改变数组大小的。ArrayList又被称为动态数组,它的存储空间可以被动态改变,同时还拥有添加、删除元素的功能。下面列出了ArrayList的部分核心代码:【ArrayList.cs】
using System;
namespace LinearList
{
public
class ArrayList
{
// 成员变量
private
const
int _defaultCapacity =
4; //默认初始容量
private
object[] _items; //用于存放元素的数组
private
int _size; //指示当前元素个数
//当元素个数为零时的数组状态
private
static
readonly
object[] emptyArray =
new
object[0];
// 方法
public ArrayList() //默认构造方法
{ //这样做可以避免元素个数为零时的访问出错
this._items = emptyArray;
}
//指定ArrayList初始容量的构造方法
public ArrayList(int capacity)
{
if (capacity <
0)
{ //当容量参数为负数时引发异常
throw
new ArgumentOutOfRangeException("capacity",
"为ArrayList指定的初始容量不能为负数");
}
//按照capacity参数指定的长度的值初始化数组
this._items =
new
object;
}
//添加一个方法
public
virtual
int Add(object value)
{ //当空间满时
if (this._size ==
this._items.Length)
{ //调整空间
this.EnsureCapacity(this._size +
1);
}
this._items[this._size] = value; //添加元素
return
this._size++; //使长度加1
}
//动态调整数组空间
private
void EnsureCapacity(int min)
{
if (this._items.Length < min)
{ //空间加倍
int num = (this._items.Length ==
0) ?
_defaultCapacity : (this._items.Length *
2);
if (num < min)
{
num = min;
}
//调用Capacity的set访问器以按照num的值调整数组空间
this.Capacity = num;
}
}
//在指定索引入插入指定元素
public
virtual
void Insert(int index, object value)
{
if ((index <
0) || (index >
this._size))
{
throw
new ArgumentOutOfRangeException("index", "索引超出范围");
}
if (this._size ==
this._items.Length)
{ //当空间满时调整空间
this.EnsureCapacity(this._size +
1);
}
if (index <
this._size)
{ //插入点后面的所有元素向后移动一位
Array.Copy(this._items, index,
this._items, index +
1, this._size - index);
}
this._items = value; //插入元素
this._size++; //使长度加1
}
//移除指定索引的元素
public
virtual
void RemoveAt(int index)
{
if ((index <
0) || (index >=
this._size))
{
throw
new ArgumentOutOfRangeException("index", "索引超出范围");
}
this._size--; //使长度减1
if (index <
this._size)
{ //使被删除元素后的所有元素向前移动一位
Array.Copy(this._items, index +
1,
this._items, index, this._size - index);
}
this._items[this._size] =
null; //最后一位空出的元素置空
}
//裁减空间,使存储空间正好适合元素个数
public
virtual
void TrimToSize()
{
this.Capacity =
this._size;
}
// 属性
public
virtual
int Capacity //指示ArrayList的存储空间
{
get
{
return
this._items.Length;
}
set
{
if (value !=
this._items.Length)
{
if (value <
this._size)
{
throw
new ArgumentOutOfRangeException("value", "容量太小");
}
if (value >
0)
{ //开辟一块新的内存空间存储元素
object[] destinationArray =
new
object;
if (this._size >
0)
{ //把元素搬迁到新空间内
Array.Copy(this._items, 0,
destinationArray, 0, this._size);
}
this._items = destinationArray;
}
else
//最小空间为_defaultCapacity所指定的数目,这里是4
{
this._items =
new
object;
}
}
}
}
public
virtual
int Count //只读属性,指示当前元素个数
{
get
{
return
this._size;
}
}
public
virtual
object
this[int index] //索引器
{
get
//获取指定索引的元素值
{
if ((index <
0) || (index >=
this._size))
{
throw
new ArgumentOutOfRangeException("index", "索引超出范围");
}
return
this._items;
}
set
//设置指定索引的元素值
{
if ((index <
0) || (index >=
this._size))
{
throw
new ArgumentOutOfRangeException("index", "索引超出范围");
}
this._items = value;
}
}
}
}
上述代码通过在一个数组(第8行代码的成员变量_items)的基础上做进一步抽象,构建了一个可动态改变空间的顺序表ArrayList,并实现了一些基础操作,下面对之进行一一介绍。1.
初始化这里实现了2种初始方法,第一种为13~16行代码,它把顺序表空间初始化为一个0长度数组。这样做的目的是为了调用方便。做为成员变量的object类型数组_items默认会被初始化为null,如果不把它初始化为0长度数组,在使用代码 ArrayList arr = new ArrayList() 来创建ArrayList后试图访问它的Count属性将会导致错误发生。第二种初始化方法为18~27行代码,它根据capacity参数所指定的值来初始化_items数组的长度,如果初始化一个长度为100的ArrayList数组可以使用如下代码:ArrayList arr = new ArrayList(100)当可以预见ArrayList所操作的大概元素个数时,使用这种方法可以在一定程序上避免数组重复创建和数据迁移,以提高性能和减少内存垃圾回收的压力。2.
动态改变存储空间操作39~52行的EnsureCapacity(int min)方法用于空间不足时使空间加倍,从代码:int num = (this._items.Length == 0) ? _defaultCapacity : (this._items.Length * 2);可以得知,当元素个数为0是,空间增长为4,否则将翻倍。改变空间大小的代码是在Capacity属性中的set访问器中实现的(代码99~122行)。代码object[] destinationArray = new object;创建了一个新的object数组,它在内存中开辟了一个新的空间用于存放元素。代码Array.Copy(this._items, 0, destinationArray, 0, this._size);把_items数组中的元素全部拷贝到新数组destinationArray中,可以把它理解为数据搬新家。最后通过this._items = destinationArray;使用于存放数据的成员变量_items指向新的数组对象destinationArray。88~91行的TrimToSize()方法用于裁减多余空间,实际的裁减操作也是在Capacity属性中的set访问器中实现。这个操作也会导致数组的重新创建和数据迁移,建议一般情况下不使用此操作,除非集合中的剩余空间很多。3.
元素的读写操作131~149行代码实现了一个索引器,这样就可以使用中括号加索引号来读取和给元素赋值,使ArrayList的使用看上去和数组很相似。4.
元素的添加和插入操作29~37行的Add(object value)方法实现了添加元素的功能。元素添加在集合的末尾,成员变量_size用于指示当前元素个数,它总是指向集合中的最后一个元素。54~71行的Insert(int index, object value)方法用于在指定索引处插入一个元素。为了保证顺序表中的每个元素物理上相邻,插入点后面的所有元素都将后移一位,其效果如图2.7(a)所示。
http://images.cnblogs.com/cnblogs_com/abatei/122178/C2-7.jpg
1.
元素的删除操作73~86行的RemoveAt(int index)方法用于删除指定索引的元素,删除指定元素后,删除点后的所有元素将向前移动一位其效果如图2.7(b)所示。下面对ArrayList类进行测试。【例2-1】ArrayList的使用新建一个控制台应用程序,并在项目中把上面的ArrayList.cs文件做为一个【现有项】添加进去。在代码窗体前面使用如下语句加入LinearList命名空间:using LinearList;并在Main方法中输入如下代码:using System;
using LinearList;
namespace Demo2_1
{
class Program
{
static
void Main(string[] args)
{
ArrayList arr =
new ArrayList();
Console.WriteLine("arr现在的容量为:"
+ arr.Capacity +
"长度为:"
+ arr.Count);
arr.Add(1); //添加一个元素
Console.WriteLine("arr现在的容量为:"
+ arr.Capacity +
"长度为:"
+ arr.Count);
for (int i =
2; i <=
5; i++)
{ //添加4个元素,完成后元素总数达到5个
arr.Add(i);
}
Console.WriteLine("arr现在的容量为:"
+ arr.Capacity +
"长度为:"
+ arr.Count);
for (int i =
6; i <=
9; i++)
{ //添加4个元素,完成后元素总数达到9个
arr.Add(i);
}
Console.WriteLine("arr现在的容量为:"
+ arr.Capacity +
"长度为:"
+ arr.Count);
for (int i =
0; i < arr.Count; i++) //打印所有元素
{
Console.Write(i +
"
");
}
//删除两个元素
arr.RemoveAt(arr.Count -
1);
arr.RemoveAt(arr.Count -
1);
Console.WriteLine(); //换行
for (int i =
0; i < arr.Count; i++) //打印所有元素
{
Console.Write(i +
"
");
}
Console.WriteLine(); //换行
Console.WriteLine("arr现在的容量为:"
+ arr.Capacity +
"长度为:"
+ arr.Count);
arr.TrimToSize(); //载减多余空间
Console.WriteLine("arr现在的容量为:"
+ arr.Capacity +
"长度为:"
+ arr.Count);
Console.ReadLine();
}
}
}
运行结果:如图2.8所示。
http://images.cnblogs.com/cnblogs_com/abatei/122178/C2-8.jpg
由运行结果可以得知,数组对象arr的容量随着元素的不断增加,从0→4→8→16不断改变,在删除两个元素之后,容量还保持在16不变,在通过调用TrimToSize()裁减空间后,容量最终变为7。2.2.4 类型安全数组和ArrayList的本质区别在于前者是类型安全的,而后者不是类型安全的。ArrayList为了兼容所有类型的对象,使用了object数组,这给使用带来了一些的麻烦。如下例所示:【例2-2】数组和ArrayList的对比本例使用了C#类库中的ArrayList而不是前面自定义的ArrayList,它存在于System.Collections命名空间中。新建一个控制台应用程序,引入System.Collections命名空间,并在Main()方法中输入如下代码:using System;
using System.Collections;
namespace Demo2_2
{
class Program
{
static
void Main(string[] args)
{
int[] arr =
new
int[2];
arr[0] =
5;
arr[1] =
6;
int result = arr[0] * arr[1];
Console.WriteLine(result);
ArrayList arrL =
new ArrayList();
arrL.Add(5);
arrL.Add(6);
result = (int)arrL[0] * (int)arrL[1];
Console.WriteLine(result);
Console.ReadLine();
}
}
}
运行结果:
3030
本例使用数组和ArrayList分别做了相同的事情,但使用方法却大相径庭。首先数组在创建时就已经确定只接收int类型数据,并且它的长度是固定的。而ArrayList则可以接收任意object类型,而事实上,C#中的所有类均是object类型的子类。其次数组没有添加元素的功能,因为在数组创建时,各个元素就已经存在,只是被初始化为0而已,只能通过下标改变各个元素的值。而ArrayList只有把元素添加进去后才可以通过下标访问相应的元素。最后,在使用集合中的元素时,数组不需要进行强制类型转换,而ArrayList必须要经过强制类型转换才能使用。这是因为ArrayList实际存放的是object对象,要按照这些对象原本的类型来使用就必须要使用强制类型转换。ArrayList的这个特点带来了类型安全问题,如:ArrayList arrL =
new ArrayList();
arrL.Add(5);
arrL.Add("Hello World");
arrL.Add(new Button());
以上代码在集合中存放了各种各样的数据类型,但这样做是被允许的。这种类型的不安全一方面给程序带来了隐患,另一方面如果集合中存放的是值类型还会产生装箱和拆箱操作,降低了程序的性能。.NET 2.0版本的泛型的出现完美地解决了上述问题,新版本使用System.Collections.Generic命名空间下的List<T>类取代了原来的ArrayList类。下面演示了泛型List<T>类的使用:List<int> arrL=new List<int>();
arrL.Add(1);
arrL.Add(2);
可以看到,第一行代码在集合创建时就已经把元素类型限定为int,它是类型安全的,同时避免了装箱和拆箱操作。强烈建议在实际编程中使用List<T>代替ArrayList。位标志由于有读者对《C#程序设计基础教程与实训》214页的那个例子无法理解,所以专门写一篇文章对位标志进行讲解,希望可以对初学者有一些帮助。要理解这篇文章,首先要知道什么是位运算,什么是二进制,这些是计算机一级的内容,我不打算在这里讲解。如果有不懂的请上网用Google了解或查找相关书籍了解,写这篇文章的目的在于让你知道为什么要使用位运算,使用它会带来什么好处。我们首先从一个问题着手:如果你正在编写一个课程管理系统,你会以什么样的形式来表示你今天哪些节有课哪些节没课呢?比如一天有12节课,如下图所示
http://images.cnblogs.com/cnblogs_com/abatei/F1.jpg
可能大家会问,为什么只是记录哪些节有课,哪些节没课呢?这样的信息有什么作用呢?当然有用,比如你要查找某同学今天上午1、2节是否有课,又比如在排课系统中,把某门课排到星期二下午6、7节,首先就要确定这两节课是否已经分配给了其他课程。。。。。。我想大部分人最初能想到的就是使用一个bool数组来表示它(这大部分人当中当然也包括我自己),使用代码来表示就是下面这个样子:bool[] arrCourse = new bool;arrCourse=true;arrCourse=true;好!恭喜你,有了一个很不错的开始,很好地完成了任务,以上代码执行完的结果如下图所示:
http://images.cnblogs.com/cnblogs_com/abatei/F2.jpg
在数组中,当元素值为T时,表明这一节有课,为F时,表明这一节没课。现在感觉相当良好,现在可以开始排课了。比如我们现在需要在某天排入某门课,这一天现在的课程状态如下图所示:
http://images.cnblogs.com/cnblogs_com/abatei/F3.jpg
如果我们要把这门课排到第1、2节,就需要做出如下判断if(arrCourse==false && arrCourse==false)如果我们需要把排到1、2、3节,就需要做出如下判断if(arrCourse==false && arrCourse==false && arrCourse==false)如果需要把课排到1~5节或1~13节。。。!现在有点晕了,怎么这么麻烦啊!当然,你要是真的这么写程序那就是真的有点麻烦了,这样的判断应该借助循环。但无论怎么样,判断的次数是不能减少的,这样的数据结构也不能让人满意。是否有更好的解决方案呢?好!我们尝试把F变为0,T变为1,看看效果怎么样:
http://images.cnblogs.com/cnblogs_com/abatei/F4.jpg
这幅图表示的不是一个二进制数吗?用计算器来转换一下,它就是十进制的902。哈哈!真是天才的想法,用一个整数就可以表示这一天的上课状态了。现在我要把新课排在1、2节只需要把902跟二进制的(110000000000)也就是十进制的3072进行“与”运算(902 & 3072)就OK了,结果为0,表明可以排进去,结果不为0表明1、2节至少有一节已排入其他课了。如果要排入的是下午6、7节,只需跟二进制(000001100000)也就是十进制的96进行“与”运算。现在不再需要进行多次判断,只需一次运算,判断结果是否为0就解决了所有问题。现在大家应该明白位运算有多好了吧!Windows中存在着大量的位标志,如果曾经使用Windows API进行编程应该对此有很深的体会。举个例子:假设Windows字体存在以下几种样式:加粗,斜体,下划线。一个字体即可以同时拥有以上三种样式,也可以只拥有一种样式,或拥有其中任意两种样式,或者干脆一种都没有。如何表示字体的样式呢?当然最佳的方案就是使用位标志。如用第一个位表示字体是否加精,第二、三个位分别表示是否是斜体和下划线,如下图所示:
http://images.cnblogs.com/cnblogs_com/abatei/F5.jpg
当位标志为:010------(十进制的2)时,表示这个字体只有斜体这种样式当位标志为:011------(十进制的3)时,表示这个字体为粗体,同时又是斜体。。。。。。。所以当你要把某行字变为粗体时,只需让它跟二进制的(001)进行“或”运算就行了把某行字变为斜体时,只需让它跟二进制的(010)进行“或”运算把某行字变为带下划线时,只需让它跟二进制的(100)进行“或”运算
如果要把粗体去掉呢?只需把001求补变为110,然后跟位标志进行“与”运算就行了把某行字的斜体去掉,只需把010求补变为101,然后跟位标志进行“与”运算就行了把某行字的下划线去掉,只需把100求补变为011,然后跟位标志进行“与”运算就行了
好,现在你应该理解了课本216页的那些古怪代码了吧!当然,字体样式不止这些,加粗是否放在第一个位我也不知道,但道理就是这样的。如果还不明白,请先把位运算学好了再来看这篇文章。课本的第四章也对几个位运算符&,|,~做了详细的讲解。
顶!!!!!!!!!!
页:
[1]