数据结构复习:用自己的方式实现List<T>(4/1晨更新)
最近复习起数据结构,真是后悔原来上课不好好听课.可以说当学校开设数据结构这门课程的时候虽然我知道他重要,但是我一直都在睡觉,而现在重新拿起这本书,我要好好把它看完,而这不叫复习了,叫做学习. 在书的第二章开始介绍数据结构的时候就提到了线性表,线性表理所当然的成为了数据结构中最简单的结构,而基础的线性表有2种,其一是顺序表,其二是链表.
因为顺序表实在是太简单,简单到我们平常天天碰到的数组就是一个最典型的顺序表,而C#又提供了一组完美的数组操作方法,这样看来实现顺序表实在是没有什么挑战性(定义一个数组,搞2个方法就完事了).而链表不同,链表和顺序表最大的差别在于顺序表要预先分配内存空间,它的所有子元素都尽在掌握,而链表是动态存储空间,并且对于它的子元素则采取了更为open的管理方式(本篇末尾会提到).
由结构来看,链表是有N个包裹着实际数据的特殊类型的集合,而这些类型在内存中实际上又没什么关系,他们的关系在于这个特殊类的一个属性(Next)引用了下一个类型,以此类推,1的Next引用2,2的Next引用3..最终形成一个关系链,"链表"这个名字也由此而来. 废话不多说,我们就来实现一个泛型List, we call it MyList<T>
首先我们建立一个新的类,叫做MyList.cs,并且再建立一个上面所说的"特殊的类"代码如下(说明也包含在内):
2 {
3 /// <summary>
4 /// Write a new Generic List called MyList.
5 /// </summary>
6 public class MyList<T>:IEnumerable<T>,IEnumerator<T>
7 {
8 //这就是上面所说的特殊类型,是个嵌套类,它没必要被其他任何类型访问到
9 class MyListNode<t>
10 {
11 public MyListNode(t val, MyListNode<t> next)
12 {
13 this.val = val;
14 this.next = next;
15 }
16 private t val;
17 private MyListNode<t> next;
18 public t Value { get { return val; } set { val = value; } }
19 //本属性用于对下一个节点的引用.
20 public MyListNode<t> Next { get { return next; } set { next = value; } }
21 }
22 //构造函数
23 public MyList()
24 {
25 head = new MyListNode<T>(default(T), null);
26 rear = null;
27 length = 0;
28 index = -1;
29 }
30 //私有变量
31 //这是一个特殊的节点,它不包含值,它的index应该为-1,它的next才是MyList的第"零"个元素.
32 private MyListNode<T> head;
33 //此变量方便于添加新的节点.
34 private MyListNode<T> rear;
35 //MyList的总长度
36 private int length;
37 //用于IEnumerator接口的Current属性,返回当前被访问到第几个元素.因为一下的实现,所以初始值为-1.
38 private int index;
39
40 public int Length { get { return length; } }
41
42 //索引器,访问元素方便
43 public T this[int i]
44 {
45 get
46 {
47 return Seek(i);
48 }
49 }
50
51 //添加元素
52 public void Add(T item)
53 {
54 MyListNode<T> node = new MyListNode<T>(item, null);
55 if (length == 0)//当length==0,我们要操作特殊的head节点.
56 {
57 head.Next = node;
58 rear = node;
59 //他们2个应该都指向现在被添加的第一项.
60 }
61 else
62 {
63 rear.Next = node;//把尾部的节点的Next属性设置为当前要插入的属性
64 rear = node;//再把尾部节点设置为当前对象.
65 }
66 length++;
67 }
68
69 public void Remove(int i)
70 {
71 if (i > length || i < 0)
72 return;
73 if (i > 0)
74 {
75 MyListNode<T> prev = SeekNode(i - 1);//找到当前要操作节点的前趋.
76 MyListNode<T> cur = prev.Next;//获取当前节点.
77 prev.Next = cur.Next;//把前节点的Next属性设置为当前节点的Next属性,也就是说当前节点被架空,失去引用的它将被GC处理.
78 cur = null;//让当前对象彻底从内存里消失吧!
79 }
80 else
81 {
82 //又要考虑head节点
83 MyListNode<T> tmpVal = head.Next;//获取head节点的下一个节点,也就是MyList的第"零"个元素的下一节点.
84 head.Next = tmpVal.Next;//再把head的下一节点设置为第"零"个元素的下一节点.此时第"零"个元素被架空,失去引用.
85 tmpVal = null;
86 }
87 length--;
88 }
89
90 public T Seek(int i)
91 {
92 return SeekNode(i).Value;
93 }
94
95 //私有方法,为方便获取节点,索引器和Seek只要返回当前节点的值就完成工作.
96 private MyListNode<T> SeekNode(int i)
97 {
98 int j = 0;
99 MyListNode<T> tmpNode = head.Next;
100 if (i < 0 || i > Length)
101 throw new ArgumentOutOfRangeException();
102 if (tmpNode == null)
103 return null;
104 while (j < i && j < Length)
105 {
106 if (tmpNode.Next != null)
107 {
108 tmpNode = tmpNode.Next;
109 j++;
110 }
111 else
112 break;
113 }
114 if (j == i)
115 return tmpNode;
116 else
117 return null;
118 }
119 //余下的都是完成接口的实现,这就没什么好说的了,毕竟foreach还是比较顺手的循环方式.
120
121 #region IEnumerable<T> 成员
122
123 public IEnumerator<T> GetEnumerator()
124 {
125 return this as IEnumerator<T>;
126 }
127
128 #endregion
129
130 #region IEnumerable 成员
131
132 IEnumerator IEnumerable.GetEnumerator()
133 {
134 return this as IEnumerator;
135 }
136
137 #endregion
138
139 #region IEnumerator<T> 成员
140
141 public T Current
142 {
143 get { return this[index]; }
144 }
145
146 #endregion
147
148 #region IDisposable 成员
149
150 public void Dispose()
151 {
152 head.Next = null;
153 GC.SuppressFinalize(this);
154 }
155
156 #endregion
157
158 #region IEnumerator 成员
159
160 object System.Collections.IEnumerator.Current
161 {
162 get { return this[index]; }
163 }
164
165 public bool MoveNext()
166 {
167 index++;
168 return index < Length;
169 }
170
171 public void Reset()
172 {
173 index = -1;
174 }
175
176 #endregion
177 }
178 }
179
认清机制:
从以上的代码可以看出我们并没有一个绝对的容器来装放这些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平台"慢"的劣势!
最后希望大家能到我的独立博客里看看:http://bugunow.com/blog