最近复习起数据结构,真是后悔原来上课不好好听课.可以说当学校开设数据结构这门课程的时候虽然我知道他重要,但是我一直都在睡觉,而现在重新拿起这本书,我要好好把它看完,而这不叫复习了,叫做学习. 在书的第二章开始介绍数据结构的时候就提到了线性表,线性表理所当然的成为了数据结构中最简单的结构,而基础的线性表有2种,其一是顺序表,其二是链表.
因为顺序表实在是太简单,简单到我们平常天天碰到的数组就是一个最典型的顺序表,而C#又提供了一组完美的数组操作方法,这样看来实现顺序表实在是没有什么挑战性(定义一个数组,搞2个方法就完事了).而链表不同,链表和顺序表最大的差别在于顺序表要预先分配内存空间,它的所有子元素都尽在掌握,而链表是动态存储空间,并且对于它的子元素则采取了更为open的管理方式(本篇末尾会提到).
由结构来看,链表是有N个包裹着实际数据的特殊类型的集合,而这些类型在内存中实际上又没什么关系,他们的关系在于这个特殊类的一个属性(Next)引用了下一个类型,以此类推,1的Next引用2,2的Next引用3..最终形成一个关系链,"链表"这个名字也由此而来. 废话不多说,我们就来实现一个泛型List, we call it MyList<T>
首先我们建立一个新的类,叫做MyList.cs,并且再建立一个上面所说的"特殊的类"代码如下(说明也包含在内):
代码
1
namespace
proj_0329
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