数据结构复习:用自己的方式实现List
Hello,我是Snake,欢迎阅读我的文章--数据结构复习:用自己的方式实现List
最近复习起数据结构,真是后悔原来上课不好好听课.可以说当学校开设数据结构这门课程的时候虽然我知道他重要,但是我一直都在睡觉,而现在重新拿起这本书,我要好好把它看完,而这不叫复习了,叫做学习.
在书的第二章开始介绍数据结构的时候就提到了线性表,线性表理所当然的成为了数据结构中最简单的结构,而基础的线性表有2种,其一是顺序表,其二是链表.因为顺序表实在是太简单,简单到我们平常天天碰到的数组就是一个最典型的顺序表,而C#又提供了一组完美的数组操作方法,这样看来实现顺序表实在是没有什么挑战性(定义一个数组,搞2个方法就完事了).而链表不同,
链表是有N个包裹着实际数据的特殊类型的集合,而这些类型在内存中实际上又没什么联系,他们的关系在于这个特殊类的一个属性(Next)引用了下一个类型,以此类推,1的Next引用2,2的Next引用3..最终形成一个关系链,"链表"这个名字也由此而来.
废话不多说,我们就来实现一个泛型List, we call it MyList<T>
首先我们建立一个新的类,叫做MyList.cs,并且再建立一个上面所说的"特殊的类"代码如下:
namespace proj_0329
{
/// <summary>
/// Write a new Generic List called MyList.
/// </summary>
public class MyList<T>:IEnumerable<T>,IEnumerator<T>
{
//这就是上面所说的特殊类型,是个嵌套类,它没必要被其他任何类型访问到
class MyListNode<t>
{
public MyListNode(t val, MyListNode<t> next)
{
this.val = val;
this.next = next;
}
private t val;
private MyListNode<t> next;
public t Value { get { return val; } set { val = value; } }
//本属性用于对下一个节点的引用.
public MyListNode<t> Next { get { return next; } set { next = value; } }
}
//构造函数
public MyList()
{
head = new MyListNode<T>(default(T), null);
rear = null;
length = 0;
index = -1;
}
//私有变量
//这是一个特殊的节点,它不包含值,它的index应该为-1,它的next才是MyList的第"零"个元素.
private MyListNode<T> head;
//此变量方便于添加新的节点.
private MyListNode<T> rear;
//MyList的总长度
private int length;
//用于IEnumerator接口的Current属性,返回当前被访问到第几个元素.因为一下的实现,所以初始值为-1.
private int index;
public int Length { get { return length; } }
//索引器,访问元素方便
public T this[int i]
{
get
{
return Seek(i);
}
}
//添加元�
public void Add(T item)
{
MyListNode<T> node = new MyListNode<T>(item, null);
if (length == 0)//当length==0,我们要操作特殊的head节点.
{
head.Next = node;
rear = node;
//他们2个应该都指向现在被添加的第一项.
}
else
{
rear.Next = node;//把尾部的节点的Next属性设置为当前要插入的属性
rear = node;//再把尾部节点设置为当前对象.
}
length++;
}
public void Remove(int i)
{
if (i > length || i < 0)
return;
if (i > 0)
{
MyListNode<T> prev = SeekNode(i - 1);//找到当前要操作节点的前趋.
MyListNode<T> cur = prev.Next;//获取当前节点.
prev.Next = cur.Next;//把前节点的Next属性设置为当前节点的Next属性,也就是说当前节点被架空,失去引用的它将被GC处理.
cur = null;//让当前对象彻底从内存里消失吧!
}
else
{
//又要考虑head节点
MyListNode<T> tmpVal = head.Next;//获取head节点的下一个节点,也就是MyList的第"零"个元素的下一节点.
head.Next = tmpVal.Next;//再把head的下一节点设置为第"零"个元素的下一节点.此时第"零"个元素被架空,失去引用.
tmpVal = null;
}
length--;
}
public T Seek(int i)
{
return SeekNode(i).Value;
}
//私有方法,为方便获取节点,索引器和Seek只要返回当前节点的值就完成工作.
private MyListNode<T> SeekNode(int i)
{
int j = 0;
MyListNode<T> tmpNode = head.Next;
if (i < 0 || i > Length)
throw new ArgumentOutOfRangeException();
if (tmpNode == null)
return null;
while (j < i && j < Length)
{
if (tmpNode.Next != null)
{
tmpNode = tmpNode.Next;
j++;
}
else
break;
}
if (j == i)
return tmpNode;
else
return null;
}
//余下的都是完成接口的实现,这就没什么好说的了,毕竟foreach还是比较顺手的循环方式.
#region IEnumerable<T> 成员
public IEnumerator<T> GetEnumerator()
{
return this as IEnumerator<T>;
}
#endregion
#region IEnumerable 成员
IEnumerator IEnumerable.GetEnumerator()
{
return this as IEnumerator;
}
#endregion
#region IEnumerator<T> 成员
public T Current
{
get { return this[index]; }
}
#endregion
#region IDisposable 成员
public void Dispose()
{
head.Next = null;
GC.SuppressFinalize(this);
}
#endregion
#region IEnumerator 成员
object System.Collections.IEnumerator.Current
{
get { return this[index]; }
}
public bool MoveNext()
{
index++;
return index < Length;
}
public void Reset()
{
index = -1;
}
#endregion
}
}
认清机制:
从以上的代码可以看出我们并没有一个绝对的容器来装放这些MyListNodes,也就是说我们根本不能像一个数组来那样直观得可以知道到底是那个变量里装着我们这些链表数据,我们的代码里只能获取到链表的头和尾.此时的MyList并不认识我们链表的第n(n不是链表头或者尾)个元素,可以说当我们建立完链表之后就像放羊一样地让这些"孩子"自由流浪,但前提是排名第n的"孩子"一定要给排名第n-1的"孩子"留下联系电话,至于这"孩子"将来要到哪里去,要定身于天涯还是海角,我们并不管他.这样就可以动态得管理这些元素所占用的空间,世界无限大(指内存),我们也就可以随意的添加和删除元素,只要它还没满到装不下一个新的元素为止.相对于顺序表的预先分配空间,就好象母亲买了一个N室1厅的房子,一切尽在掌握中,当孩子住满这些房间,也就不能再添加元素了.
另外既然这些孩子散落在世界各地,而母亲一时间也不知道孩子们究竟在哪里,那这些孩子们会不会最终被垃圾回收机制给回收?答案是否定的.GC定义只要一个在当前程序中的任何数据,只有当它失去了所有的引用才会被回收,而链表的第n个孩子总是还有一个最亲的亲人:n-1,n-1知道n的电话号码,妈妈就不会失去与n的联系,GC也不会将黑手伸向n.
链表真是一个有意思的数据结构阿.
性能考虑:
根据以上的说法我们可以非常清楚得认清链表与顺序表的性能差别.对于顺序表,试想一下如果妈妈叫孩子们吃饭的情景,因为孩子们都在妈妈的掌控之中,每个人的手机号码她都有,即便孩子出门溜达也能立刻喊她们回来吃饭.
链表就比较悲剧,如果我们要叫第n个孩子回家半点事,母亲就得从"head"孩子那儿要到排名老1的孩子的电话号码,再像老1要老2的号码,以此类推,直到母亲拿到了第n个孩子的电话号码为止,而这小子此时可能还在国外,这时把它从国外叫回身边可又要花不少时间.
还好在电子世界中"孩子"们的寿命可能不到1秒钟,他们的办事速度(指电脑的运行速度)比人的办事速度快了多了去了,以上的事情在我们不自觉的情况下飞速的运作着.
最后根据以上的算法,如果我们要获取最后一个元素是不是要把链表整个遍历一遍?答案是肯定的.但是我们可以给MyListNode添加前趋元素的引用,并且通过更好的优化算法来获取更快的访问速度.
祝各个ASP.NET程序员都能很好的认清.net框架实现,学好数据结构和一些基本的算法,彻底.NET平台"慢"的劣势!

2010年04月08日 17:14
在文章的最后我要提醒大家的是.Net Framework 中的List可是基于数组实现的变长数据集合,并非我上面所用到的链表,毕竟通常来说数据的查询频率要比增加和删除来的高一些.
2010年04月24日 00:50
博主真是个好学的童鞋,我在校时,虽然是计算机专业但是学校给安排的教学太杂,基本啥都没学会,大三才爱上了.NET。我也是大专生,毕业近1年,从事.NET企业应用开发,RIA(尤其Silverlight)爱好者,不断提升自己的能力中,欢迎回访。
2010年04月24日 00:50
第一篇文章居然不能留言,所以就留这儿了。