数据结构与算法-Part3——线性表

线性表(Linear list)是一种基本的线性数据结构,其数据元素间有线性逻辑关系,并且可以在线性表的任意位置进行插入和删除数据元素的操作。

线性表数据结构可以用顺序存储结构和链式存储结构两种方式实现,前者称为顺序表(sequenced list),后者称为链表(linked list)

一、线性表的概念和类型定义

1:抽象数据类型层面的线性表

1)线性表的数据元素

在线性表中,我们使用具有某种抽象类型的数据元素a_{i}表示线性表中的位置i的数据元素。

线性表是由n(n>=0)个数据元素{\color{Red} }a_{o},a_{1},a_{2},...a_{n-1}组成的有限序列,记作:

LinearList={a_{o},a_{1},a_{2},...a_{n-1} }

其中,n表示线性表的元素个数,称为线性表的长度

当n=0,表示线性表中没有元素,称为空表

当n>0,对于线性表中第i个数据元素a_{i}有且仅有一个前驱和后继元素

2)线性表的基本操作

Initialize:初始化。创建一个线性表实例,并对该实例进行初始化

Get/Set:访问。对线性表中指定位置的数据元素进行取值或置值操作

Insert:插入。一种常见的插入操作是在表尾添加新元素(Add)

Count:计算长度。

Remove:删除。删除线性表指定位置的数据元素

Copy:复制。重新复制一个线性表

Join:合并。将两个或两个以上的线性表合并起来,形成一个新的线性表

Search:查找。

Sort:排序。以递增或递减的次序进行排序

Traversal:遍历。按次序访问线性表中的所有元素,每个元素只访问一次

2:C#中的线性表类

C#类库中定义了一个非泛型线性表ArrayList类和泛型线性表List<T>类

线性表的元素数目可按需求动态增加,可在表中任意位置进行插入和删除数据元素的操作,一般的数组不具备这种特性

例:以顺序表求解约瑟夫环(Joseph)问题

约瑟夫环问题:有n个人围坐在一个圆桌周围,把n个人按次编号为1,2,....n,从编号是s的人开始报数,数到第d个人离席,然后从离席的下一位重新开始报数,数到d的人离席.....如此反复,直到最后生一个人在座位上为止。比如n=5,s=1,d=2,离席顺序为2,4,1,5,最后留在座位上的是3号

解决代码如下:

using System;
using System.Collections;
public class JosephProgram
{
    public static void Main(string[] arg)   //声明静态方法,类型是字符串的数组
    {
        JosephRing(5,1,2);               //调用JosephRing
    }
    public static void Show(ArrayList arrayList)   //声明静态Show方法,类型是动态数组
    {
        foreach(object o in arrayList)            //Show方法内容是遍历动态数组arrayList
        {
            Console.Write(o+" ");
        }
        Console.WriteLine();
    }
    public static void JosephRing(int n,int s,int d)   //声明静态方法约瑟夫环,重载类型为三个整型数,n,s,d
    { 
        ArrayList aRing = new ArrayList();            //创建并实例化动态数组aRing
        int i,j,k;                                    //创建三个整型变量
        for(i=1;i<=n;i++)          
        {
            aRing.Add(i);                            //for循环,从1开始,每次给动态数组aRing中添加i
        }
        Show(aRing);                                //调用Show方法,把创建好的aRing动态数组展现出来

        i=s-2;                                      //第s个人的下标为s-1,i初始指向第s个人的前一位置
        k=n;                                        //每轮的当前人数
        while(k>1)
        {
            j=0;
            while(j<d)
            {
                j++;                                //计数
                i=(i+1)%k;                        //取模运算实现环形位置记录
            }
            Console.WriteLine("out:"+aRing[i]);
            aRing.RemoveAt(i);                   //第i个人出环,删除第i个位置的元素
            k--;i=(i-1)%k;
            Show(aRing);
        }
        Console.WriteLine("\n{0} is the last person)",aRing[0]);
    }
}

程序结果如下:

1 2 3 4 5 
out:2
1 3 4 5
out:4
1 3 5
out:1
3 5
out:5
3

3 is the last person)

二、线性表的顺序存储结构

顺序存储结构指的是用一组连续的内存空间来顺序存放线性表的数据元素,数据元素在内存空间中的物理存储次序与它们的逻辑次序是一致的,即数据元素a_{i}与前驱和后继元素在物理和逻辑上都是相邻的,因此每个元素在内存中占用的存储空间的大小是相同的。

假设每个元素占据c个存储单元,第一个数据元素的地址为Loc(a_{o}),它也是起始地址,则第i个元素a_{i}的地址为:Loc(a_{o})=Loc(a_{0})+i\times c,i=0,1,2...n-1

因此,每个数据元素的地址是该元素在线性表中逻辑位序的线性函数,每次寻址一个元素所花费时间是相同的,顺序表的存储结构如下:

下标元素内容元素地址
oa_{0}Loc(a_{0})
1a_{1}Loc(a_{1})
.........
ia_{i+1}Loc(a_{0})+i\times c
.........
n-1a_{n-1}Loc(a_{0})+(n-1)\times c

1:顺序表的类型定义

程序中的数组对象可以得到连续的存储空间,因此数组可以作为实现顺序表的基础。我们将顺序表用C#的自定义类刻画和表现出来,将类命名为SequencedList<T>,该类声明如下:

public class SequencedList<T>

{
   private T[] items;
   private int count = 0;
   其他成员...
}

字段items是一维数组变量,将记录顺序表的数据元素所占用的存储空间,数组的元素类型为T,和泛型顺序表的类型参数T相同的类型

字段count表示顺序表长度

数组items的长度items.Length告知顺序表的当前容量

2:顺序表的操作

1)顺序表的初始化

public SequencedList(int c)     //构造空的顺序表,分配c个存储单元
{
   items = new T[c];     //申请c个存储单元
   count = 0;             //此时顺序表中元素个数为0,长度为0
}

构造方法可以有多个重载形式,方便调用者以不同的方式初始化顺序表对象。

下面是一个重载的构造方法,不带参数,又称缺省构造方法,自动构造具有16个存储单元的空表,编码如下:

public SequencedList(): this(16)
{

}

下面是另一个重载的构造方法,它以一个数组的多个元素作为初值来构造顺序表实例,该操作的实现代码如下:

public SequencedList(T[]itemArray)     //
{
   count = itemArray.Length;   //计算元素个数
   int capacity = count +16;
      items=newT[capacity];
   for(int i =0; i<count; i++)
   {
      items[i]= itemArray[i];
   }
}

2)返回顺序表长度

该操作告知线性表实例中的数据元素的个数,将这个操作通过定义成类或公有整形属性Count来实现,编码如下:
 

public int Count
{
  get
  {
     return count;
  }
}

在SequencedList类中将返回顺序表长度的功能定义为类的属性(property)成员,功能上与将其定义为方法成员类似,但是相对后者,该操作更简洁。方法的调用必须加上括号,而属性的调用则无需括号

3)判断顺序表的空状态和满状态

通过定义bool类型的属性Empty来实现判断顺序表为空的功能,返回为true则为空。

Empty应设计为只读属性,功能实现如下:

public bool Empty
{
  get
     {
        ruturn count ==0;
     }
}

通过定义bool类型的属性Full来实现判断顺序表当前预分配的空间已满的功能,返回ture则为满。

Full应设计为只读属性,功能实现如下:

public bool Full
{
  get
     {
        ruturn count ==items.Length;
     }
}

4)获取或设置顺序表容量

定义共有属性Capacity供外部获取或设置顺序表的当前容量。

获取顺序表的容量,仅是简单地返回数据成员items数组的长度,设置顺序表的新容量,则要依次进行以下操作:
重新分配指定大小的存储空间作为顺序表的"数据仓库",将原数组中的数据元素逐个拷贝到新数组具体代码如下:

public int Capacity
{
    get
    {
        return items.Length;
    }
    set
    {
        int n=value;
        T[] copu = new T[n];     //重新分配指定大小的存储空间
        if(count>n )count =n;
        Array.Copy(items,copy,count);//将原数组中的元素拷贝
        items = Copy;      //items指向新数组
    }
}

5)获取或设置指定位置的数据元素值

通过定义索引器(indexer)来提供获得或设置顺序表的第i个数据元素值得功能,并实现对顺序表实例进行类似于数组的访问。

我们从0开始的索引参数i来指示顺序表的第i个元素。实现如下:

public T this[int i]
{
    get
    {
        if(i>=0&&i<count)
        {
            return items[i]
        }
        else
        {
            throw new IndexOutOfRangeException("iNDEX Out Of Range Exception in"+this.GetType());
        }
    }
    set
    {
        if
        {
            (i>=0&&i<count)
            {
                items[i]=value;
            }
        }
        else
        {
            throw new IndexOutOfRangeException("iNDEX Out Of Range Exception in"+this.GetType());
        }
    }
}

6)查找具有特定值的元素

在线性表中顺序查找具有特定值k的元素过程为:从线性表的第一个元素开始,以此检查线性表中的数据元素是否等于k.若当前元素与k相等,则成功,否则继续查找

分别用Cointains方法和IndexOf方法实现。Cointains方法查找成功返回true,不成功返回false。IndexOf方法查找成功返回k首次出现的位置,否则返回1。该操作实现如下:

public bool Contains(t k)
{
    int j = IndexOf(k);
    if(j!=-1)
       return true;
    else
       return false; 
}


public int IndexOf(T k)
{
    int j=0;
    while(j<Count&&! k.Equals(items[j]))
       j++;
    if(j>=0&&j<count)
       return j;
    else
       return -1;
}

7)在顺序表的指定位置插入数据元素

在线性表指定位置插入一个新的数据元素,插入后,其素有元素仍构成一个连续表,插入位置后,所有元素的下标向后移。该操作的代码实现如下:

public void Insert(int i,T k)
{
    int n = items.Length;
    if(Count>=n)                 //若顺序表的当前空间已满,需要调用Capacity属性重新分配
    {
        Capacity=n*2;             //容量加倍
    }
    if(i<0)i=0;
    if(i>count)i=count;
    if(i<count)
    {
        for(int j=Count-1;j>=i;j--)
           items[j+1]=items[j];
    }
    items[i]=k;
    count++;
    return;
}

一种常见的插入操作是在线性表的尾部添加(Add)一个新元素,实现如下:

public void Add(T k)
{
    int n = items.Length;
    if(Count>=n)                 //若顺序表的当前空间已满,需要调用Capacity属性重新分配
    {
        Capacity=n*2;             //容量加倍
    }
    items[count]=k;
    count++;
}

8)删除顺序表指定位置的数据元素

删除线性表指定位置的数据元素,同时保证更改后的数据集合仍然具有线性表的连续性,顺序显然需要前置一位。算法实现如下:

public void RemoveAt(int i)
{
    if(i>=0&&i<count)
    {
        for(int j=i+1;j<Count;j++)
        {
            items[j-1]=items[j];
        }
        Count--;
    }
    else
    {
        throw new IndexOutOfRangeException("Index Of Range Exception in"+this.GetType());
    }
}

一种常见的删除操作是从表中移除特定对象的第一个匹配项,算法实现如下:

public void Remove(T k)
{
    int i =IndexOf(k);             //查找K值位置i
    if(i!=-1)
    {
        for(int j=i+1;j<Count;j++)   //删除第i个值
        {
            items[j-1]=items[j];
        }
        Count--;
    }
    else
    {
        Console.WriteLine(k+"值未找到,无法删除");
    }
}

9)输出顺序表

这是一个使用工具方法,它能在控制台上显示顺序表对象的内容,代码实现如下:

public void Show(bool showTypeName=false)
{
    if(shouTypeName)
       Console.Write("SequencedList");
    for(int i=0;i<this.count;i++)
       Console.Write(items[i]+" ")
    Console.WriteLine();
}

3:顺序表操作的算法分析

顺序表应该具有以下特点:

1)随机访问:顺序表的存储次序直接反应了其逻辑次序,可以直接访问任意位置的数据元素,时间复杂度为O(1);

2)存储密度高:所有的存储空间都可以用来存放数据元素;

3)插入和删除操作的效率不高:每次插入和删除一个数据元素,都可能移动大量的数据元素,其平均移动次数是线性表长度的一般,时间复杂度为O(n);

4)预分配数组空间时,需要给出数组存储单元的个数,这个数值只能根据不同的情况估算

使用顺序表求解约瑟夫环问题的改进算法:

在上面的解答中,每当一个数据元素出环,都需要删除表中相应位置的元素,这时必须移动其他元素(ArrayList类完成该操作),操作的时间复杂度高。为了避免这个问题,我们采用一种设置特殊标志的变通方法,将应出环元素相应位置的置零,以后在执行中跳过值为0的单元,

改进方法:

using System.Collections.Generic;

namespace listtest
{
    public class JosephNewSolution
    {
        public static void Main(string [] args)
        {
            (new JosephNewSolution()).JosephRing(5,1,2);
        }
        public void JosephRing(int n,int s,int d)
        {
            const int KilledValue=0;
            int i,j,k;
            int[] a=new int[n];
        for(i = 0; i<n; i++)            //n个人依次插入线性表
        {
            a[i]=i+1;
        }
        SequencedList<int> ring1=new SequencedList<int>(a);
        
        ring1.Show(true);
        i=s-2;                            //每轮的当前人数
        k=n;                             //n-1个人依次出环
        while(k>1)
        {
            j=0;
            while(j<d)
            {
                i=(i+1)%n;
                if(ring1[i]!=KilledValue)
                j++;
            }
            Console.WriteLine("out:"+ring1[i]);
            ring1[i]=KilledValue;
            k--;
            ring1.Show(true);
        }
        i=0;
        while(i<n&&ring1[i]==KilledValue)
           i++;
        Console.WriteLine("The Last Person is "+ring1[i]);
        }
    }
}

粗略的预算前后两种算法的运行速度,如果去掉算法中显示中间结果的语句,对于n=50000的情况,前一个算法用时1.828秒,而该算法耗时0.016秒,相差100多倍

三、线性表的链式存储结构

线性表的链式存储结构是指将线性表的数据元素分别存在一个个链结点(link node)中,每个链接点由数据元素域和一个或若干个指针域组成,指针用来指向其他结点。

指向线性链表第一个结点的指针被称为线性链表的头指针。一个线性链表由头指针指向链表的头结点(head node),头结点的链指向第一个数据节点(first node),最后一个结点的链为空(null),链表的数据节点个数称为链表的长度,长度为0被称为空表。

线性链表根据结点所包含的链的个数分为单向链表和双向链表。

1:线性链表的结点结构

1)声明自引用的结点类

自引用的类包含一个属于同一类型对象的成员,因为对象类型的变量为引用类型,该成员存储的内容是某个对象(实例)的引用,实际起着记录对象地址的作用,例如:

public class SingleLinkedNode<T>
{
  T item;                      //数据域,存放结点值
  SingleLinkedNode<T>next;     //指针域,后继结点的作用
}

SingleLinkedNode<T>类的定义声明了两个成员变量:item和next

item构成结点的值域,用于记载(结点)数据;next构成结点的链域,用于引用同类的对象,例如其他节点,实例变量next称为链(link),它是一种引用类型

所以SingleLinkedNode<T>类构成子引用的类

2)创建并使用结点对象

创建和维护动态数据结构需要动态内存分配(dynamic memory allocation),即一个程序在运行时申请所需的内存空间,系统分配内存后程序才可以使用,使用完成后释放不再需要的空间

使用new操作符创建对象并为之分配内存

SingleLinkedNode<string> p,q;   //声明p,q是SingleLinkedNode<string>类的变量,其实是类似于int,string这种
p= new SingleLinkedNode<string>();
q= new SingleLinkedNode<string>();//创建SingleLinkedNode类的一个对象,由q引用

如果没有可用内存,new操作符产生一个OutOfMemoryException类型的异常

结点对象的两个成员变量item和next记录该实例的状态,称为实例变量。由p引起对象的这两个实例成员变量的语法格式为p.item和p.next,通过下述语句可以将p,q结点对象连接起来;

p.next=q;

这时称结点p指向结点q

2:单向链表

在线性链表中,如果每个结点只有一个链,则只能表达一种链接关系,这种结构称为单项链表(single linked list)

1)单向链表的结点类

C#语言描述单向链表的结点结构,声明泛型类SingleLinkedNode如下:
 

using System.Text;
public class SingleLinkedNode<T>
{
    private T item;                       //存放结点值
    private SingleLinkedNode<T>next;     //后继结点的引用
    //构造值为K的结点
    public SingleLinkedNode(T k)
    {
        item=k;next=null;
    }
    //无参数时构造缺省值的结点
    public SingleLinkedNode()
    {
        next=null;
    }
    //获取和设置结点值
    public T Item
    {
        get{return item;}
        set{item=value;}
    }
    public SingleLinkedNode<T> Next
    {
        get{return next;}
        set{next=value;}
    }
    //输出以本结点为第一结点的单向链表
    public void Show()
    {
        SingleLinkedNode<T>p=this;
        while(p!=null)
        {
          Console.Write(p.Item);
          p=p.next;
          if(p!=null)
             Console.Write("->");
          else
             Console.Write(".");
        }
        Console.WriteLine();
}
//重写ToString方法
public override string ToString()
{
    StringBuilder s=new StringBuilder();
    SingleLinkedNode<T>p=this;
    while(p!=null)
    {
       s.Append(p.Item);
       p=p.next;
       if(p!=null) s.Append("->");
       else s.Append(".");
    }
    return s.ToString();
}
}

SingleLinkedNode类声明了单向链表的结点类型,用这个类型定义和创建的实例即表示一个具体的链结点对象,通过它的成员变量next的引用方式,指向其他结点,以实现线性表中各数据元素的逻辑关系。

SingleLinkedNode类提供的共有属性Next和Item允许外部模块来间接访问next和item

2)单向链表类

C#语言描述单向链表,声明泛型类SingleLinkedList<T>如下:

public class SingleLinkedList<T>
{
    private SingleLinkedList<T>head;   //指向链表的头结点
    public SingleLinkedList<T>Head
    {
        get{return head;}
        set{head=value;}
    }
}

线性表的各种操作将作为SingleLinkedList类的不同属性和方法成员予以实现

3)单向链表的操作

①建立单向链表

建立单向链表,单向链表的初始化。用SingleLinkedList类的构造方法建立一条空链表,它仅包含一个头结点,操作如下:

public SingleLinkedList()
{
  head=new SingleLinkedNode<T>();  //构造头结点,它仅仅是一个标志结点

}

构造一条单向链表,使其第一个数据节点为指定的结点:通过重载SingleLinkList类的构造方法来实现,代码如下:

public SingleLinkedList(SingleLinkedNode<T>f):this()
{
  head.Next=f;
}

 设变量rear指向原链表的最后一个结点,q指向新创建的结点,则下列语句将q结点链在rear结点之后,并更新rear,使其指向新链尾结点:

rear.Next=q;  //q结点链入原链表尾
rear=q;  //更新rear,指向新链尾结点

这样就将q作为最后一个结点链入到表中,这样就将建立一条单向链表,该操作初始化代码如下:
 

piblic SingleLinkedList(T[] itemArray):this()
{
    SingleLinkedNode<T>rear.q;
    rear=head;             //rear指向链表尾结点
    for(int i = 0; i < itemArray.Length; i++) 
    {
        q=new SingleLinkedNode<T>(itemArray[i]);  //建立结点q
        rear.Next=q;
        rear=q;
    }
}

②返回链表长度

该操作告知线性链表的数据元素个数

用count实现

必须从第一个数据结点计数到最后一个几点,编码如下:

public virtual int Count
{
    get{
        SingleLinkedNode<T>p=Head.Next;
        while(p!=null){
            n++;
            p=p.next;
        }
        return n;
    }
}

③判断单向链表是否为空

用Bool类型的属性Empty来实现

当头结点的链域为空(head.Next)为null时,表示链表为空,编码如下:

public virtual bool Empty
{
   get{return false;}
}

在链表的实现中,采用动态分配的方式为每个结点分配内存空间,当有一个数据元素需要加入链表时,就向系统申请一个结点的存储空间,在编程时,可认为系统所提供的可用空间是足够大的,一般不必判断链表是否已满

如果仍想在链表类中定义一个Full属性,可以让它总是返回false,编码如下:

public virtual bool Full
{
   get{return false;}
}

 

④获取或设置指定位置的数据元素值

在链表实现中,不能像顺序结构那样根据数据节点的序号直接找到该结点。

在单向链表的每个结点都有一个指向后继结点的链域,如果以索引参数i来指定结点的位置,则必须从表头顺着链找到相应的结点,以达到获取或设置的目的。

用从0开始的索引参数i来指示线性链表的第i个元素,实现操作如下:

public virtual T this[int i]
{
    get
    {
        if(i<0){
            throw new IndexOutOfRangeException("Index is nagative in"+this.GetType());
            }
            int n =0;
            SingleLinkedNode<T>q=head.Next;
            while(q!=null&&n!=i{
                n++;
                q=q.Next;
            
        if(q==null){
            throw new IndexOutOfRangeException("Index is nagative in"+this.GetType());
            }
        }

        return q.Item;
    }
    set
    {
        if(i<0){
            throw new IndexOutOfRangeException("Index is nagative in"+this.GetType());
        }
        int n =0;
        SingleLinkedNode<T>q=head.Next;
        while(q!=null&&n!=i{
                n++;
                q=q.Next;
            
        if(q==null){
            throw new IndexOutOfRangeException("Index is nagative in"+this.GetType());
            }
        }
        q.Item=value;
    }
}

⑤输出单向链表

将已建立的单向链表按顺序在控制台输出其每个结点的值,从head.Next所指向的结点(链表的第一个数据结点)开始,首先访问结点,再沿着其链方向到达后继结点,访问该结点,知道最后一个结点。

设p指向链表中的某结点,由结点p到达其后继结点的语句是:

p=p.next;

输出整个链表的操作系统实现代码如下:

public virtual void Show(bool showTypeName=false)
{
    if(showTypeName){
        Console.Write("SingleLinkedList: ");
    SingleLinkedNode<T>q=head.Next;
    if(q!=null)q.Show();
}

因此,输出链表的功能,是由链表结点类的Show()方法配合链表类的SHow()方法一起完成的。

⑥插入结点

从表头顺着链找到相应的结点,再插入新的结点

生成值为K的新节点并做相应准备工作如下:

SingleLinkedNode<T> p,q;
SingleLinkedNode<T> t=new SingleLinkedNode<T>(k);

找到正确的位置后,设p指向链表的某结点,在p之后插入新的结点t,形成新的链表,语句如下:

t.next=p.next;

p.next=t;

完整实现如下:

public virtual async void Insert(int i,T k){
    int k=0;
    SingleLinkedNode<T>p=head;
    SingleLinkedNode<T>q=p.next;
    if(i<0) i=0;
    SingleLinkedNode<T>t=new SingleLinkedNode<T>(k);
    while(q!=null)
    {
        if(j==i)break;
        p=q;
        q=q.next;
        j++;
    }
    t.Next=p.Next;
    p.Next=t;
}

插入操作中的另一个常见情况是在线性表的表尾添加一个元素k,可以定义一个Add方法实现:
 

public virtual void Add(T k);{
    SingleLinkedNode<T>p=head;
    SinglelinkedNode<T>q=p.Next;
    SinglelinkedNode<T>t=new SinglelinkedNode<T>(k);
    while(q!=null){
        p=q;
        q=q.next;
    } 
    q.next=t;
}

⑦删除结点

要在单向链表中删除指定位置的结点,需要把结点从链表中退出,并改变相邻结点的链接关系。

删除p的后继结点q的语句是:

p.Next=q.Next;

执行该操作前要根据不同的要求定位将被删除的结点q和它的前驱结点p。

删除结点的操作的实现程序如下:

public void Remove(T K){
    SingleLinkedNode<T>p=head;
    SingleLinkedNode<T>q=p.Next;
    while(q!=null){
        if(K.Equals(q.Item)){
            p.Next=q.Next;
            return;
        }
            p=q;
            q=q.Next;
    }
    Console.WriteLine(K+"值未找到,无法删除");
}
public async void RemoveAt(int i)
{
    int j=0;
    SingleLinkedNode<T>p=head;
    SingleLinkedNode<T>q=p.Next;
    while(q!=null)
    {
        if(j==i)
        {
            p.Next=q.Next;
            return;
        }
        p=q;
        q=q.Next;
        j++;
    }
    throw new IndexOutOfRangeException("Index Out Of Range Exception"+this.GetType());
}

⑧单向链表逆转

设已建立一条单向链表,现欲将各结点的链域next改为指向其前驱结点,使得单向链表逆转过来。

设p指向链表的某一结点,front和q分别指向p的前驱和后继结点,则使p.Next指向前驱结点的语句是:p.next=front;

单向链表逆转算法描述如下:

①:第一次循环时,front=null,p指向链表的第一个数据节点,执行 语句:

p.Next=front ;

②:以p!=null为循环条件,front,p和q等变量沿链表方向前进而一次更新,对于p指向的每一个结点,执行语句:

p.Next=front;

③循环结束后,front指向原链表的最后一个结点,该结点应成为新链表的第一个数据结点,需由head.Next指向,语句为:

head.Next=front;

Reverse方法的实现:

public virtual void Reverse()
{
    SingleLinkedNoder<T>q=null,front=null;
    SingleLinkedNoder<T>p=head.Next;
    while(p!=null)
    {
        q=p.Next;
        p.Next=front;
        front=p;
        p=q;
    }
    head.Next=front;
}

测试代码如下

using System;
using System.Collections.Generic;
namespace listtest
{
    class SingleLinkedListTest
    {
        public static void Main(string[] args)
        {
            int[] ia= new int[8];
            RandomizeIntArray(ia);
            SingleLinkedList<int> a=new SingleLinkedList<int>(ia);//以8个随机数建立单向链表
            a.Show();
            Console.WriteLine("Reverse!");
            a.Reverse();
            a.Show();
        }
        static void RandomizeIntArray(int[] ia)
        {
            Random random =new Random();
            for(int i=0; i<ia.Length; i++)
            {
                ia[i]=random.Next(100);   //产生随机数
            }
            return;
        }
    }
}

输出结果则为反向调换。

4)两种存储结构性能比较

①:顺序表能够如同访问数组元素一样,直接访问数据元素,链表不能直接访问任意指定位置的数据元素,只能从第一个结点开始,沿着链方向,一次查找后继结点,直到指定位置,才能访问该结点的数据元素

②:插入和删除。顺序表的插入和删除元素很不方便,有时需要移动大量元素,而链表只需要简单的改动相应结点的链即可。

③:存储密度。顺序表每个单元存储密度高,数据结点的全部空间都用来存放数据元素,而链表的结点存储密度比较低,每个结点不仅要包含数据的值,还要包含后继结点的引用。

④:存储空间的动态利用特性。顺序表中不易动态利用存储空间,例如进行插入操作时要判断顺序表预分配的存储空间是否已满,当原空间满了,需要重新分配存储空间,并将原空间的数据拷贝到新空间,然后进行操作,如果预分配空间过多,会浪费空间。而链表中插入新结点,程序会动态向系统申请一个存储单元,只要系统资源够用,就会分配到需要的存储空间。

⑤:查找和排序。顺序表具有元素的随机访问特性,查找和排序可以比较方便地实施多种算法,如折半查找和快速排序算法等。而链表中实施一些查找和排序算法相对比较复杂

顺序表SequencedList和链表SingleLinkedList都可以用来建立具体地线性表实例,一般情况下,解决某个问题关注的是线性表的抽象功能,而不必关注线性表的存储结构和实现细节

3:单项循环链表

如果在单向链表中,将最后一个结点的链域设置为指向链表的头结点,这样的链表成为单向循环链表(circle linked list)

用C#语言描述单向循环列表,声明泛型类CircularLinkedList<T>如下:

public class CircularLinkList<T>:SingleLinkedList<T>
{
    private SingleLinkedNode<T> rear;
    public override SingleLinkedNode<T> Rear
    {
        get{return this.rear;}
    }
}

它继承的共有属性Head作为链表的头指针,指向链表中仅作为标志的头结点,头结点的链域则指向第一个数据节点

1)单向循环链表的初始化

用CircularLinkedList类的构造方法建立一条循环链表,算法如下:

public CircularLinkList():base()
{
    this.rear=this.Head;
}
public CircularLinkedList(<T[] itemArray>):this()
{
    SingleLinkedNode<T>q=null;
    for(int i=0;i<itemArray.Length;i++)
    {
        q=new SingleLinkedNode<T>(itemArray[i]);
        rear.Next=q;
        rear=q;
    }
    q.Next=Head;
}

2)返回链表长度

算法如下:

public override int Count
{
    get
    {
        int n=0;
        SingleLinkedNode<T>p=Head.Next;
        if(p==null)return 0;
        while(p!=Head)
        {
            n++;
            p=p.Next;
        }
        return n;
    }
}

3)判断单向链表是否为空

算法如下:

public override bool Empty
{
    get{
        return Head==rear;
    }
}

当尾结点指针rear等于头结点指针Head时,说明循环链表仅包含一个头结点,而没有数据节点。

 

4:双向链表

如果在结点结构中再增加一个链用于指向前驱结点,就会产生一种双向链表,它会极大的方便实现既向前又向后的操作

双向链表(Doubly Linked List)的每个结点除了保存数据的成员变量item之外,还有两个作为链的成员变量;prior指向前驱结点,next指向后继结点

1)双向链表的结点类

用C#语言声明DoubleLinkedNode<T>类如下:

public class DoubleLinkedNode<T>
{
    private T item;    //存放结点值
    private DoubleLinkedNode<T>prior,next;   前驱和后继结点的引用
    //构造值为K的结点
    public DoubleLinkedNode(T k)
    {
        item=k;
        prior=next=null;
    }
    //无参构造缺省值的结点
    public DoubleLinkedNode()
    {
        prior=next=null;
    }
    public T item
    {
        get{return item;}
        set{item=value;}
    }
    public DoubleLinkedNode<T>Next
    {
        get{return next;}
        set{next=value;}
    }
    public DoubleLinkedNode<T>prior
    {
        get{return prior;}
        set{prior=value;}
    }
}

2)双向链表类

用C#语言描述双向链表结构,声明DoubleLinkedList<T>类如下:

public class DoubleLinkedNode<T>
{
    private DoubleLinkedNode<T>head;//指向链表作为标志的头结点
    //构造空的双向链表
    public DoubleLinkedList()
    {
        head=new DoubleLinkedNode<T>();  //头结点是标志结点
    }
    protected DoubleLinkedNode<T>Head
    {
        get{return head;}
        set{head=value;}
    }
}

用DoubleLinkedList<T>构造的一个实例即可用来表示一条双向链表对象,它的缺省方法建立一条仅有头结点的空链表。

设P指向双向链表中的某一数据结点(尾结点除外),则双向链表具有下列本质特征:
(p.Prior).Next等于p;

(p.Next).Prior等于p;

3)双向链表的操作

①判断双向链表是否为空

public virtual bool Empty
{get{
    return head.Next==null;
}
}

②在双向链表中插入结点

生成值为k的新结点并做相应的准备工作如下:
 

DoubleLinkedNode<T> p,q,t=new DoubleLinkedNode<T>(k);

找到正确的插入位置后,设p指向链表中的某结点,在结点p之后插入结点t,形成新的链表,如下:

T.prior=p;
T.next=p.next;
(p.next).prior=t;
p.next=t;

完整的插入操作的实现代码如下:

public virtual void Insert(int i,T k)
{
    int j=0;
    DoubleLinkedNode<T>p=head;
    DoubleLinkedNode<T>q=p.next;
    if(i<0)i=0;
    DoubleLinkedNode<T>t=new DoubleLinkedNode<T>(k);
    while(q!=null){
        if(j==i)break;
        p=q;q=q.next;
        j++;
    }
        t.next=p.next;
        t.prior=p;
        if(q!=null)q.Prior=t;
}

③在双向链表中删除结点

在双向链表中栓除给定位置的结点,需要把该节点从链表中推出,并改变相邻结点的链接关系

执行下列语句将结点q从链表中推出:

p.Next=q.Next;

(p.Next).Prior=p;

执行上述语句之后,建立了新的链接关系,完整算法如下

public void RemoveAt(int i)
{
    int j=0;
    DoubleLinkedNode<T>p=head;
    DoubleLinkedNode<T>q=head.Next;
    while(q!=null)
    {
        if(j==i)
        {
            p.Next=q.Next;
            (p.Next).Prior=p;
            return;
        }
        p=q;
        q=q.Next;
        j++;
    }
    throw new IndexOutOfRangeException("Index of Range Exception in"+this.GetType());
}

4)双向循环链表

如果最后一个结点rear的next链指向链表的头结点(Head Node),而链表的头结点的prior链指向最后一个结点rear,则形成双向循环链表(Circula Double Linked list)即在双向循环链表中有下列关系成立:
rear.next等于head;

head.prior等于rear

当Head.Next等于null或者Head 等于rear时,循环链表为空

当head.next不等于null且head.prior等于head.next时,链表只有单个数据节点

双向循环链表类的查找,定义,插入删除等操作类似单向循环链表和双向链表类的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Laker404

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值