Henry手记—.NET数据结构对象补遗之单链表

韩睿  ( 06/15/2003)

 

.NET Framework提供了众多常用的数据结构对象,放在System.Collections命名空间中。现有ArraylistQueueStackSortListHashTable等。但是奇怪的是,微软没有在Framework中加入链表、二叉搜索树等相当重要的数据结构对象。从本篇开始,我们将陆续将它们补充进我们自己的.NET类库,让它们更有效地成为工作的利器。同时,大家也会通过这些基本数据结构的实现,达到对Framework类与接口编程的深入理解,所以,不要错过。同时,我们也一起复习一下数据结构的内容吧。

Arraylist是近来被大家不断提及与关注的列表类,它是由数组构成的。虽然microsoft对数组作了大量的改进,提高了增减元素操作的效率、实现了动态扩充的性能。但是数组始终是数组:在对元素的排序、删除、添加操作时,效率仍很低;更重要的是,数组的大小是固定的,即使是动态数组,只不过是加入了重新分配空间的能力;同时,为了避免频繁地重新分配,不得不将数组初始化大小设得较大,这样就会大量地浪费内存。而事实上,FrameworkQueueStackHashtable等数据集合的实现,也部分或全部地使用了链表的方法,所以,自己动手实践一次,是分外有益的,Let’s go!

1.        单链表(singly linked list)

单链表是最简单的链表表示,因此我们先来对它进行分析与实现。用它来表示线性表时,每一个数据元素占用一个结点(node)。一个结点一般由两个域组成,一个域存放数据元素data;另一个域存放一个指向链表中下一个结点的指针link,它指出下一个结点的开始存储地址。

单链表的类实现,在每一本《数据结构》课本上都有详细的介绍:需要定义结点类和链表类,在链表类中实现对结点的操作。

在本文所要分析的链表类,我们采用嵌套定义的方式,将结点类定义在链表类的内部。所能进行的操作包括:添加(向链表尾插入结点)、插入(向链表中指定的索引位置插入结点)、按索引位置删除结点、按数据元素值删除结点、搜索数据元素值的索引、用For Each遍历链表中的值等。在我们的程序中,还会向大家展示如何在类中设计抛出异常、嵌套枚举器类等不常被提及的处理。

为了避免发生在链表头部或尾部进行插入或删除操作这样特殊的情况,给链表加一个数据为空的头节点。并加入尾结点指针,以方便添加功能的实现。

2.        ArrayList类分析

我们的单链表实现方法是模仿Framework现有的数据集合类的做法,而不是仅仅按照《数据结构》课本的C++定义方法来实现。因此需要先来学习一下Arraylist类的定义:

Public Class ArrayList                

   Implements IList, ICollection, IEnumerable, ICloneable 

也就是说ArrayList是实现了四个接口,我们当然要分别看一下这些接口的作用才好进行仿制工作。

1)IEnumerableIEnumerable是公开枚举数,该枚举数支持在集合上进行简单迭代。必须对它进行实现才能支持 Microsoft Visual Basic ForEach 语义。

2) ICollection:派生自IEnumerable接口,定义所有集合的大小、枚举数和同步方法。ICollection 接口是 System.Collections 命名空间中类的基接口。

3) IListIlist派生自ICollectionIDictionary IList 是基于 ICollection 接口的更专用的接口。IDictionary 实现是键/值对的集合,如 Hashtable 类。IList 实现是可被排序且可按照索引访问其成员的值的集合,如 ArrayList 类。

另:某些集合(如 Queue 类和 Stack 类)限制对其成员的访问,它们直接实现 ICollection 接口。或者当 IDictionary 接口和 IList 接口都不能满足所需集合的要求时,则从 ICollection 接口派生新集合类以提高灵活性。

以上三个接口的继承关系为:IEnumerable-> ICollection-> IList。也就是说ArrayList其实是通过对IList的实现,同时实现了ICollectionIEnumerable。因此我们只需要实现IList接口即可。

4)ICloneable:这个接口的作用就是实现克隆,即用与现有实例相同的值创建类的新实例。为纯粹我们的目标,这个接口不在本文中进行实现。留待后续文章中进行分析。

通过以上的分析,我们可以这样来定义我们即将要实现的单链表类了:

Public Class SLList    

   Implements IList   

要实现接口,即要实现接口中定义的方法或属性。IList接口的实现需要声明的方法与属性如图1所示,一共有15个。分属于IEnumerableICollectionIList

  

   1 单链表类SLList需要实现的来自于接口的方法与属性

3.        实际演练

由于单链表类要能够在用户扩展时当成基类使用,所以它所有的字段、方法和属性是Public或者是Protected的,并且在必要的地方用Overridable进行标记。

接下来,我们依序从类定义到功能实现一一来详细说明:

3.1 类定义

我们用嵌套类定义的方式来定义结点类与链表类:

Imports System.Collections

Public Class SLList

    Implements IList

    Protected Class ListNode

        Public NextNode As ListNode '指向下一个结点的引用

        Public Data As Object '用object类型来声明数据,可以达到template的效果

        Public Sub New(ByVal data As Object, _

                        ByVal nextNode As ListNode)

            '初始化结点

            Me.Data = data

            Me.NextNode = nextNode

        End Sub

        Public Sub New(ByVal data As Object)

            '作为尾结点的结点

            Me.New(data, Nothing)

        End Sub

    End Class

……

3.2 类内公用变量初始化

Protected head As ListNode = New ListNode(Nothing) '初始化头结点

Protected tail As ListNode = head  '指向链表尾的引用

Protected nodeCount As Integer = 0 '保存节点数目,通过Count方法返回

Protected version As Integer = 0 '记录链表变化的版本号

这里使用了version这个变量,它的作用在后面我们会详细讨论的。

3.3验证索引与数据元素

在类里定义了验证函数,验证失败,会抛出异常,这样用户在调用时可以用TryEnd Try来处理:

Protected Overridable Overloads Sub Validate(ByVal index As Integer)

        '验证index是否在可用范围内

        If index < 0 Or index >= nodeCount Then

            Throw New ArgumentOutOfRangeException("索引越界.")

        End If

End Sub

Protected Overridable Overloads Sub Validate(ByVal value As Object)

        '验证输入的数据是否存在

        If value Is Nothing Then

            Throw New ArgumentNullException()

        End If

End Sub

Protected Overridable Overloads Sub Validate(ByVal index As Integer, ByVal value As Object)

        '同时验证索引与数据元素

        Validate(index)

        Validate(value)

End Sub

 

3.4根据索引位置或数据元素值在链表中查找

在链表中定位是对其进行操作的基础,我们在类的内部定义两个Protected的查找函数:

Protected Overridable Function FindByIndex(ByVal index As Integer) As ListNode

        '通过index来查找链表中的结点

        Dim tempIndex As Integer = 0

        Dim current As ListNode = head.NextNode '从头结点后的第一个结点开始

        Dim returnValue As ListNode = Nothing '初始化返回结点

        Do '循环查找

            If index = tempIndex Then

                returnValue = current

            Else

                current = current.NextNode

                tempIndex += 1

            End If

        Loop Until current Is Nothing Or Not returnValue Is Nothing

        Return returnValue

End Function

 

Protected Overridable Function FindByValue(ByVal value As Object) As Integer

        '通过数据值来查找链表中的结点的index

        Dim tempIndex As Integer = 0

        Dim current As ListNode = head.NextNode '从头结点开始

        Dim returnValue As Integer = -1 '初始化返回值

        Do '循环查找

            If value.Equals(current.Data) Then

                returnValue = tempIndex

            Else

                current = current.NextNode

                tempIndex += 1

            End If

        Loop Until current Is Nothing Or returnValue > -1

        Return returnValue

End Function

有这样的基础,我们就可以实现IList接口的IndexOf索引方法了:

Public Overridable Function IndexOf(ByVal value As Object) _

                      As Integer Implements IList.IndexOf

        '通过结点数据值返回结点索引

        Validate(value) '先验证值

        Return FindByValue(value) '调用protected的查找方法

End Function

这样,我们在使用链表来查找某个值的索引时,如果value为空,会抛出一个异常;如果没有找到,会返回-1;找到了会返回该数据元素所在的索引位置。是不是与ArrayList的处理方法很相似?

另外,我们还需要实现Contains功能,用于判断链表中是否存在某个值:

 Public Overridable Function Contains(ByVal value As Object) _

                                         As Boolean Implements IList.Contains

        '在链表中查找value值

        Validate(value)

        If FindByValue(value) = -1 Then

            Return False '找不到

        Else

            Return True '找到了

        End If

End Function

3.5      添加结点

在上文第1节就提到过,添加结点有两种情况,向链表尾添加与按索引值插入:

Public Overridable Function Add(ByVal value As Object) _

                                    As Integer Implements IList.Add

        '向链表尾添加结点

        Validate(value) '先验证值

        tail.NextNode = New ListNode(value) '将现有尾结点的下一结点引用指向新结点

        tail = tail.NextNode '将新添加的结点设为尾结点

        version += 1 '更改版本号

        nodeCount += 1 '添加链表计数

        Return nodeCount - 1 '返回尾结点索引

End Function

 

Public Overridable Sub Insert(ByVal index As Integer, _

                         ByVal value As Object) Implements IList.Insert

        '向指定的索引处添加结点

        Validate(index, value) '验证索引与数据值

        Dim tempNode As ListNode = FindByIndex(index) '找到索引处的现有结点

        '定义新结点,新结点的下一结点引用指向索引号为index的结点

        Dim newNode As ListNode = New ListNode(value, tempNode)

    '将index-1处的结点的下一结点引用指向新结点

        FindByIndex(index - 1).NextNode = newNode

        version += 1 '更改版本号

        nodeCount += 1 '添加链表计数

End Sub

3.6      删除结点

Protected Overridable Sub RemoveNode(ByVal node As ListNode, ByVal index As Integer)

        '在类内部使用的删除结点

        '删除结点的方法是将它前一结点的下一结点引用指向它的后一结点

        Dim tempNode As ListNode = FindByIndex(index - 1)  '找到欲删除结点的前一结点

        tempNode.NextNode = node.NextNode

        If node Is tail Then

            tail = tempNode

        End If

        version += 1 '更改版本号

        nodeCount -= 1 '减少链表计数

End Sub

 

Public Overridable Sub Remove(ByVal value As Object) _

                                  Implements IList.Remove

        '类实现接口的删除方法

        Validate(value)

        RemoveAt(FindByValue(value))

End Sub

 

Public Overridable Sub RemoveAt(ByVal index As Integer) _

                                    Implements IList.RemoveAt

        '类实现接口的按索引进行删除的方法

        Validate(index)

        Dim node As ListNode = FindByIndex(index)

        RemoveNode(node, index)

End Sub

 

Public Overridable Sub Clear() Implements IList.Clear

        '清空链表

        head.NextNode = Nothing

        tail = head

        nodeCount = 0

        version = 0

End Sub

从上面的三个Remove方法来看,其实都是通过类内部的RemoveNode方法来进行删除,只不过向用户提供了两个接口:一个是根据索引值来删除,一个是通过比对数据元素值来删除。在这里要说明一下,单链表因为不能反向查找前一结点,因此删除的效率比双向链表低,这一点我们以后在实现双链表的时候还会提及。

3.7      复制

将链表中的数据元素按某一索引为起始,将元素复制到Array中去。这一方法在实际操作中分外有用:

Public Overridable Sub CopyTo(ByVal array As System.Array, _

                            ByVal index As Integer) Implements IList.CopyTo

        '从链表的索引处开始将元素复制到列表中去

        If array Is Nothing Then

            Throw New ArgumentNullException()

        ElseIf index < 0 Then

            Throw New ArgumentOutOfRangeException("索引越界")

        ElseIf index >= array.Length _

            Or (array.Length - index - 1) > nodeCount _

            Or array.Rank <> 1 Then

            Throw New ArgumentException()

        End If

        Dim current As ListNode = head.NextNode

        Dim position As Integer = index

        '循环复制

        While Not current Is Nothing

            array(position) = current.Data

            current = current.NextNode

            position += 1

        End While

End Sub

 ----

3.8      Item属性

Item属性提供给VB.NETlist(2)这样的方式操作列表,也就能赋予单链表和ArrayList数组一样的索引方式,尽管在实现上效率不如数组,但这样方便的操作手段我们不能放过:

Default Public Overridable Property Item( _

                 ByVal index As Integer) As Object Implements IList.Item

        'Item属性,读写链表的索引处结点的数据值

        Get

            Validate(index)

            Return FindByIndex(index).Data

        End Get

        Set(ByVal Value As Object)

            Validate(index, Value)

            FindByIndex(index).Data = Value

        End Set

End Property

  同样的,我们还提供计数功能,利用的就是上述各种操作中都在进行更改的nodeCount成员。

Public Overridable ReadOnly Property Count() As Integer _

                                Implements IList.Count

        '返回链表元素总数

        Get

            Return nodeCount

        End Get

End Property

到这里,我们是不是已经实现了基本的链表操作功能了?但是,还有一些重要机制并没有揭示给读者朋友,让我们继续吧。

3.9      For Each循环枚举的实现

For Each是个很强大的遍历方法,要实现这个功能,链表类必须实现IEnumerable接口,在IList我们通过实现继承自IEnumerableGetEnumerator函数来达到这一目标。但该函数返回的是IEnumerator枚举器类型,因此我们要实现自己的枚举器来达到对链表的处理:

IEnumerator 是所有枚举数的基接口。枚举数只允许读取集合中的数据。枚举数无法用于修改基础集合。IEnumerator接口支持两种方法和一个属性。MoveNext方法能在集合中一次移动一条记录。Reset方法能使枚举器复位到集合的起始。Current只读属性能从集合返回当前记录。

最初,枚举数被定位于集合中第一个元素的前面。Reset 也将枚举数返回到此位置。在此位置,调用 Current 会引发异常。因此,在读取 Current 的值之前,必须调用 MoveNext 将枚举数提前到集合的第一个元素。

在调用 MoveNext Reset 之前,Current 返回同一对象。MoveNext Current 设置为下一个元素。

在传递到集合的末尾之后,枚举数放在集合中最后一个元素后面,且调用 MoveNext 会返回 false。如果最后一次调用 MoveNext 返回 false,则调用 Current 会引发异常。若要再次将 Current 设置为集合的第一个元素,可以调用 Reset,然后再调用 MoveNext

只要集合保持不变,枚举数就保持有效。如果对集合进行了更改(例如添加、修改或删除元素),则该枚举数将失效且不可恢复,并且下一次对 MoveNext Reset 的调用将引发 InvalidOperationException。如果在 MoveNext Current 之间修改集合,那么即使枚举数已经无效,Current 也将返回它所设置成的元素。现在您就会理解我们在前面不断看到的那个version的用法了。

由于我们在之前把ListNode定义为嵌套类,同时需要调用SLList类的成员,所以把自定义的实现IEnumerator接口的类置入SLList类中,成为第二个嵌套类:

Public Overridable Function GetEnumerator() As IEnumerator _

                                   Implements IList.GetEnumerator

        Return New SLListEnumerator(Me)

    End Function

下面就是实现IEnumerator接口的类了。

    Protected Class SLListEnumerator

        Implements IEnumerator

        '嵌套在SLList内部的计数器类

        Protected list As SLList

        Protected currentElement As Object

        Protected currentNode As ListNode '嵌套在内部才可以使用这个类

        Protected version As Integer '内部版本记录,用于比对链表是否已有改变

 

        Public Sub New(ByVal list As SLList)

            '初始化

            Me.list = list

            Me.version = list.version

            Me.currentElement = list

            Me.currentNode = list.head

        End Sub

 

        Protected Overridable Sub VerifyListIsUnchanged()

            '判断版本是否在枚举器创建后还发生了改变

            If Not version = list.version Then

                Throw New InvalidOperationException( _

                  "该链表在枚举器创建后发生了改变")

            End If

        End Sub

 

        Public Overridable ReadOnly Property Current() As Object _

                                    Implements IEnumerator.Current

            '返回当前记录

            Get

                '判断是否已经到了链表尾或是否调用了MoveNext查找记录

                If currentElement Is list Then

                    If currentNode Is list.head Then

                        Throw New InvalidOperationException( _

                          "Current方法在MoveNext被调用前是无效的.")

                    Else

                        Throw New InvalidOperationException( _

                          "已到达集合尾,因此Current方法无效")

                    End If

                End If

                Return currentElement

            End Get

        End Property

 

        Public Overridable Function MoveNext() As Boolean _

                           Implements IEnumerator.MoveNext

            '将枚举器继续移到下一个对象

            VerifyListIsUnchanged()

            If Not currentNode.NextNode Is Nothing Then

                currentNode = currentNode.NextNode

                currentElement = currentNode.Data

                Return True

            Else

                currentElement = list

                currentNode = list.head

                Return False

            End If

        End Function

 

        Public Overridable Sub Reset() Implements IEnumerator.Reset

            '将枚举器重置为其初始位置

            VerifyListIsUnchanged()

            currentNode = list.head

            currentElement = list

        End Sub

End Class

----

3.10      其他属性

本文并没有全部实现所有的IList接口方法与属性的使用,但还是要声明出来如下四种属性。由于本文的单链表类没有提供内置的线程同步(都说完了,下次说什么呀。呵呵),因此IsSynchronized返回为falseSyncRoot返回的是类实例的引用。枚举数没有对集合的独占访问权;因此,枚举一个集合在本质上不是一个线程安全的过程。甚至在对集合进行同步处理时,其他线程仍可以修改该集合,这会导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中用SyncLock锁定集合,或者捕捉由于其他线程进行的更改而引发的异常。象SSList类这样的处理,每一次锁定会阻塞全部对象,要提供粒度更小的线程安全,我们以后会继续讨论的。

Public Overridable ReadOnly Property IsSynchronized() As Boolean _

                                Implements IList.IsSynchronized

        Get

            Return False

        End Get

End Property

 

Public Overridable ReadOnly Property SyncRoot() As Object _

                                Implements IList.SyncRoot

        Get

            Return Me

        End Get

End Property

IsFixedSize属性是指通过类实现时,获取一个值,该值指示 IList 是否具有固定大小。链表类没有固定大小,所以返回的当然是false

Public Overridable ReadOnly Property IsFixedSize() As Boolean _

                                Implements IList.IsFixedSize

        Get

            Return False

        End Get

End Property

IsReadOnly属性是指通过类实现时,获取一个指示 IList 是否为只读的值。当然也是返回false喽。

Public Overridable ReadOnly Property IsReadOnly() As Boolean _

                                Implements IList.IsReadOnly

        Get

            Return False

        End Get

End Property

4.        调用示例

在上面我们已经详细分析与列举了SSList类的实现,现在在来看一下它的结构,如图2所示:

                      

             图2 类结构(含两个嵌套类)

那么,怎么样来使用我们的单链表类呢?其实,很容易,与Arraylist类的使用方法相当相近,只是我们不用设定列表的长度而已,在另一个vb文件里,我们来对SLList类进行操作:

'使用示例

 Dim lst As New SLList()

 Try

     lst.Add("Henry") '添加

     lst.Add("jjj")

     lst.Insert(1, "kkk") '插入

     lst.Remove("jjj") '按值删除

     lst.RemoveAt(1) '按索引号删除

     lst(0) = "Jerry" '更改

     Dim i As Integer = lst.IndexOf("jjj") '取索引号

 

     '多种循环遍历

     Dim collectionItem As Object

     Dim loopCounter As Integer

     Dim enumCollection As IEnumerator

     '第一种

     For Each collectionItem In lst

          Console.WriteLine(collectionItem)

     Next

     '第二种

     For loopCounter = 0 To lst.Count - 1

          Console.WriteLine(lst.Item(loopCounter))

     Next

     '第三种

     enumCollection = lst.GetEnumerator()

     Do While enumCollection.MoveNext

         Console.WriteLine(enumCollection.Current)

     Loop

Catch ex As Exception

     MsgBox(ex.ToString)

End Try

现在来看一下version的作用:

     '第三种

     enumCollection = lst.GetEnumerator()

     lst.Add("Kelly")

     Do While enumCollection.MoveNext

         Console.WriteLine(enumCollection.Current)

     Loop

这时您运行一下,就会得到一个异常,提示为:

System.InvalidOperationException: 该链表在枚举器创建后发生了改变

这不正是我们在VerifyListIsUnchanged方法中定义的出错信息吗?现在大家也就明白我们在操作Arraylist或其它.net类出错时,为什么会带有相应的提示文字了吧?象“索引越界”等我们定义过的出错提示在本例中也会被捕捉到。请自行测试。

5.     小结与展望

本文和大家一起深入collections集合类的实现,我们在实际工作中处理列表类时用的是Arraylist(我也是,呵呵)。但是链表的插入、删除的处理效率远高于ArrayList(单链表加上前驱指针会更加提高效率),当然,索引取值的能力不如ArrayList。我们在处理某些频繁删除与插入的列表时,还是值得考虑是否采用链表来实现我们的需求。

限于篇幅与时间,对于排序、同步等ArrayList具有的相应功能没有过多涉及,感兴趣的朋友请继续关注我的专栏。下次再见!

----

声明:本文版权与解释权归韩睿所有,如需转载,请保留完整的内容及此声明。

QQ: 18349592

E-Mail: henry7685@hotmail.com

 请访问本人专栏:http://www.csdn.net/develop/author/netauthor/Latitude/


作者Blog: http://blog.csdn.net/Latitude/







  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值