第二章 线性表

前言

线性表是其组成元素之间具有线性关系的一种线性结构。对线性表的基本操作主要有获得元素值、设置元素值、插入、删除、查找、替换和排序等,插入和删除可以在线性表的任何位置进行。线性表可采取线性存储和链式存储结构表示。

本章介绍线性表抽象数据类型,将线性表的线性存储和链式存储结构分为封装为顺序表类和链表类,比较这两种实现的特点以及各种基本操作算法的效率。重点是涉及顺序表类和单链表类;难点是使用指针实现单链表和双链表的各种基本操作。

2.1 线性表抽象数据类型

线性表(linear list) 是由n(n>=0)个类型相同的数据元素a0, a1, …, an-1组成的有限序列,记为: LinearList = (a0, a1, …, an-1)
其中,元素ai的数据类型可以是整数、浮点数、字符或类;n是线性表元素的个数,称为线性表的长度;若n=0,LinearList为空表;若n>0, ai(0<i<n-1)有且仅有一个前驱(predecessor)元素ai-1和一个后继(successor)元素ai+1, a0没有前驱元素,an-1没有后继元素。
线性表结构ILinearList声明如下,表示线性表抽象数据类型,描述线性表获取元素值、设置元素值、插入、删除等基本操作。

    public interface ILinearList<T> //线性表接口,泛型参数表示数据元素的数据类型
    {
        bool IsEmpty(); // 判断线性表是否为空
        int Length(); //返回线性表的长度
        T GET(int i); // 返回第i个元素
        void Set(int i, T x); // 设置第i个元素的值为x
        void Insert(int i, T x); // 在第i个位置插入元素i
        void Append(T x); // 在线性表最后插入x元素
        T Remove(int i); // 删除第i个元素并返回被删除的对象
        void RemoveAll(); // 删除线性表所有元素
       int Search(T key); // 查找,返回首次出现的关键字为key的元素索引
    }

由于线性表的数据元素之间具有顺序关系,所以可以为每个元素约定一个序号,因此线性表提供对指定序号元素进行操作的方法。
顺序存储和链式存储的线性表类(顺序表类和链表类)实现ILinearList接口,提供ILinearList结构中抽象方法的具体实现。

2.2 线性表的顺序表示和实现

1.线性表的顺序存储结构

线性表的顺序存储是用一组连续的内存单元依次存放线性表的数据元素,元素在内存中的物理存储次序与它们在线性表里的逻辑次序相同,即元素ai与其前驱元素ai-1和后继元素ai+1相邻。顺序存储的线性表也称为顺序表(sequential list)。
线性表的数据元素属于同一种数据类型,设a0的存储地址为Loc(a0),每个元素占用c字节,则ai的存储地址为:Loc(ai) = Loc(a0) + i × \times × c
顺序表ai的存储地址是它在线性表中位置i的线性函数,如图2-1所示,与线性表长度n无关;而计算一个元素地址所需时间是一个常量,与元素位置无关。因此,存取任何一个元素的时间复杂度是O(1)。换而言之,顺序表是一种随机存储结构。
在这里插入图片描述
顺序表通常采用数组存储数据元素。将线性表的数据元素顺序存储在数组中,数据元素在数组中的物理顺序与线性表中元素的顺序完全相同。
数组是顺序存储随机存取结构,占用一组连续的存储单元,通过下标识别元素,元素地址是下表的线性函数。一个下标能够确定唯一一个元素,存取任意一个元素所花费时间是O(1)。
在程序设计语言中,数组已经被实现为一种构造数据类型。数组一旦占用一片存储空间,这片存储空间的长度和地址就是确定的,不能更。因此,数组只能进行赋值、取值两种随机存取操作,不能进行插入、删除操作。

2.顺序表类

下面声明SeqList为顺序表类,实现ILinearList接口

   // 顺序表类
    public class SeqList<T> : ILinearList<T>
    {
        private object[] elements; // 存放线性表数据元素的一维数组
        private int len; //顺序表长度,记载实际元素个数

        // 构造函数,创建容量为size的空表
        public SeqList(int size)
        {
            this.elements = new object[size]; 
            this.len = 0;
        }

        // 默认构造函数,创建容量为10的空表
        public SeqList(): this(10)
        {
        }

        /// <summary>
        /// 在线性表最后插入x
        /// </summary>
        /// <param name="x">要插入的元素x</param>
        public void Append(T x)
        {
            this.Insert(this.len, x);
        }

        /// <summary>
        /// 返回第i个元素
        /// </summary>
        /// <param name="i">元素位置i</param>
        /// <returns></returns>
        public T GET(int i)
        {
            if (i >= 0 && i < this.len)
            {
                return (T)this.elements[i];
            }
            return default(T);
        }

        public bool IsEmpty()
        {
            return this.len == 0;
        }

        public int Length()
        {
            return this.len;
        }

        /// <summary>
        /// 删除线性表所有元素
        /// </summary>
        public void RemoveAll()
        {
           for (int i = 0; i < this.len; i++)
            {
                this.elements[i] = default(T);
            }
            this.len = 0;
        }

        /// <summary>
        /// 查找
        /// </summary>
        /// <param name="key">关键字key</param>
        /// <returns>返回首次出现的关键字为key的元素索引</returns>
        public int Search(T key)
        {
            if (key != null)
            {
                for (int i = 0; i < this.len; i++)
                {
                    if (key.Equals( this.elements[i]))
                    {
                        return i;
                    }
                }
                return -1;
            }
            return -1;
        }

        /// <summary>
        /// <summary>
        /// 设置第i个元素的值为x
        /// </summary>
        /// <param name="i">位置i</param>
        /// <param name="x">要设置的值x</param>
        public void Set(int i, T x)
        {
            if (x == null)
                return;
            if (i >= 0 && i < this.len)
            {
                this.elements[i] = x;
            }
            else
                throw new IndexOutOfRangeException(i +""); // 抛出索引越界异常
        }
    }

3.顺序表的插入

顺序表的插入和删除要移动数据元素。在顺序表ai位置插入元素 x x x,首先必须将ai, … \dots , an-1向后移动,空出ai所在的位置,然后将 x x x插入。元素移动过程如图2-2所示,其中length表示数组容量。
如果数组已满,则不能插入,称为数据溢出。解决溢出的办法就是申请一个容量更大的数组并且复制所有数据元素,这样句扩充了顺序表的容量。
在这里插入图片描述

图2-2
        public void Insert(int i, T x)
        {
            if (i < 0 || x == null)
                return;
            if (this.len == this.elements.Length) // 若数组满了,则扩充数组
            {
                object[] temp = this.elements;
                this.elements = new object[temp.Length * 2]; // 申请一个更大的数组
                for (int j = 0; j < temp.Length; j++)
                {
                    this.elements[i] = temp[i]; // 复制数组元素
                }
            }
            // 下标容错
            if (i < 0)
                i = 0;  // 如果i<0,则插入到最开始的位置
            if (i > this.len)
                i = this.len; //如果i>this.len,则插入到最后
            for (int k = this.len - 1; k >= i; k--)
            {
               this.elements[k+ 1] = this.elements[k];
            }
            this.len++;
            this.elements[i] = x;
        }

4.顺序表的删除

如果删除顺序表的ai元素,则需要将从ai+1开始的数据元素全部往前移。移动过程如图2-3所示:
在这里插入图片描述

图2-3
        public T Remove(int i)
        {
            if (this.len == 0 || i < 0 || i > this.len)
                return default(T);
            else
            {
                // 记住要删除的元素值
                T temp = (T)this.elements[i];

                //将i+1之后的元素往前移
                for (int j = i; i < this.len - 1; j++)
                {
                    elements[j] = elements[j + 1]; 
                }

                // 最后一个元素为null
                this.elements[this.len - 1] = null;

                // 数组长度减1
                this.len--;
                return temp;
            }
        }

5 顺序表操作的效率分析

顺序表存取任何一个元素的get(), set()方法时间复杂度为O(1).
对顺序表进行插入或者删除操作时,算法所花费的时间主要用于移动元素。设表长度为n,若在第一个位置插入元素,则需要移动n个元素;若在表最后插入元素,则需要移动0个元素。
设在第i个位置插入元素的概率为pi,则插入一个元素的平均移动次数为
∑ i = 0 n \sum\limits_{i = 0}^n i=0n(n-1) × \times × pi
如果在各个位置插入元素的概率相同,即p0 = p1 = ⋯ \cdots = pn-1 = 1 n \frac{1}{n} n1,则
∑ i = 0 n \sum\limits_{i = 0}^n i=0n(n-1) × \times × pi = ∑ i = 0 n \sum\limits_{i = 0}^n i=0n(n-i) × \times × 1 n \frac{1}{n} n1 = ( 0 + n ) n 2 \frac{(0+n)n}{2} 2(0+n)n × \times × 1 n \frac{1}{n} n1 = n 2 \frac{n}{2} 2n = O(n)

换而言之,在等概率下,插入一个元素平均需要移动一半的元素,时间复杂度是O(n)。删除同理。
综上所述,顺序表的静态特性很好,动态特性很差,具体说明如下。

  1. 顺序表元素的物理存储顺序直接体现了元素的逻辑顺序,顺序表是一种随机存取结构。顺序表实现了线性表抽象数据类型的基本操作,不仅存取元素的时间复杂度是O(1),获取元素的前驱元素和后继元素的时间复杂度也是O(1)。
  2. 插入和删除效率很低。每次插入或删除元素,可能需要移动大量元素,其平均移动次数是顺序表长度的一半。再者,数组长度不可变,存在因容量过小造成数据溢出或容量过大造成内存资源浪费的问题。解决溢出的办法是重新申请一个容量更大的数组,并进行数组元素复制,但插入操作效率更低。

6 顺序表的浅拷贝与深拷贝

一个类的构造方法,如果其参数是该类对象,称为拷贝构造方法,声明格式如下:
类(类 对象)
构造方法的作用是复制对象,以形式参数的实例值初始化当前新创建的对象。

  1. 顺序表的浅拷贝
    如果一个类将拷贝构造方法实现为逐域拷贝,即将当前对象的各成员变量赋值为实际参数对应各成员变量值,称为浅拷贝。

SeqList的浅拷贝构造函数如下:

        public SeqList(SeqList<T> seqList)
        {
            this.elements = seqList.elements;
            this.len = seqList.len;
        }

当成员变量的数据类型时基本数据类型时,浅拷贝能实现对象复制功能。但当变量的数据类型时引用类型时,浅拷贝只复制了对象引用,并没有真正实现对象复制功能。下面我们在main函数里调用一下,举例说明。

main函数做了以下几件事情:
1 先声明里一个有5个元素的seqList,然后调用浅拷贝构造函数初始化listA
2 打印两个顺序表的长度和元素
3 删除seqList的第一个元素
4 打印两个顺序表的长度
5 打印两个顺序表的元素

private static void Main(string[] args)
    {
        // 声明一个顺序表类
        SeqList<string> seqList = new SeqList<string>();
        seqList.Insert(0, "test0");
        seqList.Insert(1, "test1");
        seqList.Insert(2, "test2");
        seqList.Insert(3, "test3");
        
        SeqList<string> listA = new SeqList<string>(seqList);
        Console.WriteLine("seqList's length {0} ", seqList.Length());
        Console.WriteLine("seqList's elements {0} ", seqList.ToString());
        Console.WriteLine("listA's length {0} ", listA.Length());
        Console.WriteLine("listA's elements {0} ", listA.ToString());
        Console.WriteLine();
        
        seqList.Remove(0);
        Console.WriteLine("after removing seqList's first element:");
        Console.WriteLine("seqList's length is {0}", seqList.Length());
        Console.WriteLine("listA's length is {0}", listA.Length());
        Console.WriteLine();

        Console.Write("seqList's elements: ");
        Console.WriteLine(seqList.ToString());
        Console.WriteLine();

        Console.Write("listA's elements: ");
        Console.WriteLine(listA.ToString());
        Console.WriteLine();

        Console.ReadKey();
    }

结果如下
在这里插入图片描述

从结果可知,初始化时,listA和seqList的长度以及元素都相同;删除seqList之后,seqList的len减1,而listA的len没有变,导致遍历时报错。
这是因为,浅拷贝只复制数组的引用,使得两个对象的elements只有一个数组,新对象没有申请自己的数组空间。两个对象拥有同一个数组,做出删除、修改、插入等操作结果互相影响,这是错误的。
在这里插入图片描述
因此,拷贝构造函数,不仅要复制对象的所有成员变量值,还要重新申请一个数组并复制所有数组元素,实现深度复制功能。

  1. 顺序表的深拷贝
    当一个类包含引用类型成员变量时,该类声明的拷贝构造函数,不仅要复制对象的所有基本类型成员变量,还要重新申请引用类型变量占用的存储空间,并复制其中所有对象,这种复制方式称为深拷贝。

SeqList类的深拷贝构造函数如下:

        public SeqList(SeqList<T> list)
        {
            this.len = list.len;
            this.elements = new object[list.elements.Length]; // 申请一个新的数组
            for (int i = 0; i < list.elements.Length; i++) // 复制数组元素
            {
                this.elements[i] = list.elements[i];
            }
        }

此时在此运行main方法,运行结果如下
在这里插入图片描述
此时,删除seqList的元素对listA是没有影响的

7 顺序表比较相等

比较两个对象顺序表是否相等,是指它们的长度相等并且各对应元素相等。SeqList声明equal(Object obj)方法如下,它重写了父类的equal(obj)方法

        /// <summary>
        /// 判断顺序表对象是否相等,重写了object类的equal方法
        /// </summary>
        /// <param name="obj">比较对象</param>
        /// <returns>是否相等</returns>
        public override bool Equals(object? obj)
        {
            if (this == obj)
                return true;
            if (obj is SeqList<T>)
            {
                SeqList<T> list = (SeqList<T>)obj;
                if (this.Length() == list.Length())
                {
                    for (int i = 0; i < this.Length(); i++)
                    {
                        if (!(this.Get(i).Equals(list.Get(i))))
                            return false;
                    }
                    return true;
                }
            }
            return false;
        }

我们在main中调用一下

    private static void Main(string[] args)
    {
        // 声明一个顺序表类
        SeqList<string> seqList = new SeqList<string>();

        seqList.Insert(0, "test0");
        seqList.Insert(1, "test1");
        seqList.Insert(2, "test2");
        seqList.Insert(3, "test3");
        
        SeqList<string> listA = new SeqList<string>(seqList);
        Console.WriteLine("seqList's length {0} ", seqList.Length());
        Console.WriteLine("seqList's elements {0} ", seqList.ToString());
        Console.WriteLine("listA's length {0} ", listA.Length());
        Console.WriteLine("listA's elements {0} ", listA.ToString());
        *Console.WriteLine("equal or not: {0}", listA.Equals(seqList));*
        Console.WriteLine();
        
        seqList.Remove(0);
        Console.WriteLine("after removing seqList's first element:");
        Console.WriteLine("seqList's length is {0}", seqList.Length());
        Console.WriteLine("listA's length is {0}", listA.Length());
        Console.WriteLine();

        Console.Write("seqList's elements: ");
        Console.WriteLine(seqList.ToString());
        Console.WriteLine();

        Console.Write("listA's elements: ");
        Console.WriteLine(listA.ToString());
        Console.WriteLine();

        Console.WriteLine("equal or not: {0}", listA.Equals(seqList));


        Console.ReadKey();
    }

运行结果如下
在这里插入图片描述

2.3 线性表的链式表示和实现

线性表的链式存储是用若干地址分散的存储单元存储数据元素,逻辑上相邻的数据元素在物理位置上不一定相邻,必须采用附加信息表示数据元素之间的顺序关系。因此,存储一个数据元素的存储单元至少包含两部分——数据域和地址域,结构如下:
在这里插入图片描述
其中,data存储数据元素值,称为数据域;next存储后继元素地址,称为地址域或链(link),上述结构通常称为结点(node)。一个结点表示一个数据元素,通过结点中的地址域把结点链接起来,结点间的链接关系体现了数据元素之间的顺序关系。采用这种结构表示的线性表称为线性链表(linked list),如图2-8所示。
在这里插入图片描述
在图2-8中,head存放线性链表的第一个结点的地址,称为头指针,最后一个结点的地址域为空(null,图中用“$ ∧ \wedge ”)表示,表示其后不再有结点。每个结点只有一个地址域的线性链表称为单链表(singly linked list),地址域通常指向后继结点;每个结点有两个地址域的线性链表称为双链表(doubly linked list),地址域分别指向前驱结点和后继结点。从head开始,沿着链的方向,可以遍历单链表或双链表,顺序地访问每个结点。

1 单链表

由于单链表是由一个个结点链接而成,以下定义单链表结点类和单链表类描述单链表。

1. 单链表结点

单链表结点类Node声明如下:

    // 链表结点类
    public class Node<T>
    {
        public T data;
        public Node<T>? next; // 声明为public,方便我们修改

        public Node(T data, Node<T> ?next)
        {
            this.data = data; // 数据域,保存数据元素值
            this.next = next; //地址域,引用后继结点
        }

        // 构建一个空链表
        public Node(): this(default(T), null) 
        {
        }
    }

Node类有两个成员变量,data和next,data表示结点的数据域,保存数据元素,数据类型为T;next表示地址域,保存后继结点的引用信息。Node类是自引用类,它的成员变量next的类型是类本身。
自引用类(self-referential class)指一个类声明中包含一个引用当前类的对象的成员变量。
Node类的一个对象表示单链表中的一个结点。通过next链,将两个结点链接起来。
在C#中,调用new运算符创建实例,为之分配内存空间并初始化各成员变量,再将实例引用赋值给对象。建立并链接两个结点的语句如下:

        //方法1
        Node<string> a = new Node<string>("a", null); // 创建Node类的一个实例,由a引用
        Node<string> b = new Node<string>("b", null); // 创建Node类的一个实例,由b引用
        a.next = b; //链接,使b结点成为a结点的后继结点

        //方法2
        Node<string> d = new Node<string>("d", null); // 创建d结点
        Node<string> c = new Node<string>("c", d); //使d结点成为c结点的后继结点
        b.next = c;

建立的链表如下图所示
在这里插入图片描述
若干结点通过next链接指定相互之间的顺序关系,形成一条单链表。为了方便更改结点间的链接关系,将node类的两个成员变量声明成了public,允许其他类访问。如果将其更改为privarte,则需要提供共有的Set(), GetData(), GetNext()等方法。
单链表的头指针head也是一个结点引用,声明如下:
Node head = null;
当head = null时,表示空链表。

2 单链表的遍历操作

遍历单链表是指从单链表的第一个结点开始,沿着结点的next链,依次访问单链表中的每个结点,并且每个结点只访问一次。
遍历单链表不能改变头指针head,因此要声明一个变量p指向当前访问的结点。p从head指向的结点开始,再沿着next链到达后继结点,逐个访问,直到最后一个结点,完成一次遍历操作。单链表的遍历算法描述如下,如果单链表为空,则循环体不执行。

    private static void Main(string[] args)
    {
        // 建立并链接链表的语句
        //方法1
        Node<string> a = new Node<string>("a", null); // 创建Node类的一个实例,由a引用
        Node<string> b = new Node<string>("b", null); // 创建Node类的一个实例,由b引用
        a.next = b; //链接,使b结点成为a结点的后继结点
        a.next = b;

        //方法2
        Node<string> d = new Node<string>("d", null); // 创建d结点
        Node<string> c = new Node<string>("c", d); //使d结点成为c结点的后继结点
        b.next = c;

        // 遍历单链表
        Node<string> p = a; // 声明变量p,指向第一个结点,从第一个结点开始遍历
        while (p != null)
        {
            Console.Write("{0} ", p.data);
            p = p.next; // 指向下一个结点
           
        }
        Console.ReadKey();
    }

运行结果如下
在这里插入图片描述
注意:语句p = p.next 使p移到下一个结点,并没有改变结点间的链接关系。如果语句写成
p.next = p,则使p指向了自己,改变了结点间的链接关系,丢失了后继结点,如下图所示,遍历算法也变成了死循环。

在这里插入图片描述

3 单链表的插入操作

对单链表的插入操作,只需要改变结点的链接关系,不需要移动元素。
在单链表中插入元素,根据插入的问题值不同,可分为4种情况,如图2-12所示。
在这里插入图片描述
1 空表插入

    private static void Main(string[] args)
    {
      // 插入结点test,test.data = "insert"
        Node<string> head = new Node<string>(); // 声明头指针

        // 空表插入
        Node<string> test = new Node<string>("insert", null); // 声明结点test
        head = test; // 将test复制给头结点head
      
       // 遍历
        Node<string> p = head; 
        while (p != null)
        {
            Console.Write("{0} ", p.data);
            p = p.next; // 指向下一个结点
        }
        Console.ReadKey();
    }

结果如下图
在这里插入图片描述
2 头插入

    private static void Main(string[] args)
    {
      // 插入结点test,test.data = "insert"
        Node<string> head = new Node<string>(); // 声明头指针
        Node<string> test = new Node<string>("insert", null); // 声明结点test
        
        // 头插入
        test.next = head; // 将test的后继结点设为head
        head = test;  // 将test变为head
      
       // 遍历
        Node<string> p = head; 
        while (p != null)
        {
            Console.Write("{0} ", p.data);
            p = p.next; // 指向下一个结点
        }
        Console.ReadKey();
    }

结果如下图
在这里插入图片描述

从上面代码可以看出,这两种情况可以合并为一句代码

    private static void Main(string[] args)
    {
      	// 插入结点test,test.data = "insert"
        Node<string> head = new Node<string>(); // 声明头指针
        
        //空表插入和头插入可以合并为一句
         head = new Node<string>("insert",head);
    }

3 中间插入

    private static void Main(string[] args)
    {
        Node<string> a = new Node<string>("a", null); 
        Node<string> b = new Node<string>("b", null); 
        a.next = b; 

        Node<string> d = new Node<string>("d", null); // 创建d结点
        Node<string> c = new Node<string>("c", d); //使d结点成为c结点的后继结点
        b.next = c;
        
      // 插入结点test,test.data = "insert"
        Node<string> head = new Node<string>(); // 声明头指针
        Node<string> test = new Node<string>("insert", null); // 声明结点test
        
        // 中间插入
        // 假设在b结点之后插入test结点
        test.next = b.next;
        b.next = test;        
      
       // 遍历
        Node<string> p = head; 
        head = a;
        while (p != null)
        {
            Console.Write("{0} ", p.data);
            p = p.next; // 指向下一个结点
        }
        Console.ReadKey();
    }

运行结果
在这里插入图片描述
注意: test.next = d.next 和 d.next = test 这两天语句的顺序不能颠倒,否则会产生下面的错误在这里插入图片描述
4 尾插入

    private static void Main(string[] args)
    {
        Node<string> a = new Node<string>("a", null); 
        Node<string> b = new Node<string>("b", null); 
        a.next = b; 

        Node<string> d = new Node<string>("d", null); // 创建d结点
        Node<string> c = new Node<string>("c", d); //使d结点成为c结点的后继结点
        b.next = c;
        
      // 插入结点test,test.data = "insert"
        Node<string> head = new Node<string>(); // 声明头指针
        Node<string> test = new Node<string>("insert", null); // 声明结点test
        
        // 尾插入,假设在d结点之后插入test结点
        test.next = d.next;
        d.next = test;     
      
       // 遍历
        Node<string> p = head; 
        head = a;
        while (p != null)
        {
            Console.Write("{0} ", p.data);
            p = p.next; // 指向下一个结点
        }
        Console.ReadKey();
    }

在这里插入图片描述

这两种情况也可以合并成为一条语句

    private static void Main(string[] args)
    {
        Node<string> a = new Node<string>("a", null); 
        Node<string> b = new Node<string>("b", null); 
        a.next = b; 

        Node<string> d = new Node<string>("d", null); // 创建d结点
        Node<string> c = new Node<string>("c", d); //使d结点成为c结点的后继结点
        b.next = c;
        
      // 插入结点test,test.data = "insert"
        Node<string> head = new Node<string>(); // 声明头指针
        Node<string> test = new Node<string>("insert", null); // 声明结点test
        
        // 中间插入和尾插入可以合并为一种情况,假如要插入到d结点之后
        d.next = new Node<string>("insert", d.next);   

中间插入和尾插入都不回改变链表的头指针head。

4 单链表的删除

单链表的删除指定结点不需要移动元素,只需要改变结点的next域即可。根据被删除结点的位置不同,分两种情况讨论,如图2-14所示
在这里插入图片描述

  1. 头删除
    删除单链表的第一个结点,只要十head指向其后继结点即可,语句如下:
    private static void Main(string[] args)
    {
        Node<string> a = new Node<string>("a", null); 
        Node<string> b = new Node<string>("b", null); 
        a.next = b; 

        Node<string> d = new Node<string>("d", null); // 创建d结点
        Node<string> c = new Node<string>("c", d); //使d结点成为c结点的后继结点
        b.next = c;
        
        Node<string> head = new Node<string>(); // 声明头指针
        
        // 单链表的删除,假设删除第一个结点
        head = a;
        head = head.next; //将head指向其后继结点
        
        Node<string> p = head; // 声明变量p,指向第一个结点,从第一个结点开始遍历
        while (p != null)
        {
            Console.Write("{0} ", p.data);
            p = p.next; // 指向下一个结点
        }
   }
  1. 中间/尾删除
    private static void Main(string[] args)
    {
        Node<string> a = new Node<string>("a", null); 
        Node<string> b = new Node<string>("b", null); 
        a.next = b; 

        Node<string> d = new Node<string>("d", null); // 创建d结点
        Node<string> c = new Node<string>("c", d); //使d结点成为c结点的后继结点
        b.next = c;
        
        Node<string> head = new Node<string>(); // 声明头指针
        
        // 单链表的删除,假设删除a的后继结点
        head = a;
        a.next = a.next.next;

        Node<string> p = head; // 声明变量p,指向第一个结点,从第一个结点开始遍历
        while (p != null)
        {
            Console.Write("{0} ", p.data);
            p = p.next; // 指向下一个结点
        }
   }

5 带头结点的单链表

带头结点的单链表是指,在单链表的第一个结点之前增加一个特殊的结点,称为头结点。头结点的作用是使所有链表(包括空表)的头指针不为空,并使对单链表的插入、删除操作不需要区分是否为空表或是否在第一个位置进行,从而与其他位置的插入、删除操作一致,如图2-15所示。
在这里插入图片描述
带头结点的单链表类SinglyLinkedList声明如下,其中成员变量head表示单链表的头指针,引用单链表Node类

    public class SinglyLinkedList<T> : ILinearList<T>
    {
        protected Node<T> head; // 头指针,指向头结点

        // 默认构造函数 构造空链表。 创建头结点,data和next都为null
        public SinglyLinkedList()
        {
            this.head = new Node<T>();
        }

        // 由指定数组中的多个对象构造单链表,采用尾插入法构造单链表
        public SinglyLinkedList(T[] elements): this() // 构造空链表
        {
            Node<T> rear = this.head; //rear 指向单链表最后一个结点 此时链表只有一个头结点,所以指向头结点就是指向最后一个结点
            for (int i = 0; i < elements.Length; i++)
            {
                rear.next = new Node<T>(elements[i], null); // 尾插入,创建结点并链入到rear之后
                rear = rear.next;  //rear指向新的链尾结点
            }
        }

        /// <summary>
        /// 在单链表最后插入x
        /// </summary>
        /// <param name="x">元素x</param>
        public void Append(T x)
        {
            if (x == null)
                return;

                Node<T> p = this.head; // p指向头结点 
               while (p.next != null)
                    p = p.next; //让p指向最后一个结点
                p.next = new Node<T>(x, null); //让p的后继结点指向q
        }

        /// <summary>
        /// 获取第i个元素的值,如果i无效,则返回T的默认值
        /// </summary>
        /// <param name="i">元素的索引(i>=0)</param>
        /// <returns>第i个元素</returns>
        public T Get(int i)
        {
            if (i > 0)
            {
                Node<T>? p = this.head.next; // p指向第一个结点
                for (int j = 0; j < i && p != null; j++)
                    p = p.next; //让p指向第i个元素
                if (p != null)
                    return p.data; //返回第i个元素的值
            }
            return default(T);
        }

        /// <summary>
        /// 在指定位置插入元素
        /// </summary>
        /// <param name="i">要插入元素的位置(i>=0)</param>
        /// <param name="x">要插入的元素</param>
        public void Insert(int i, T x)
        {
            if (x == null)
                return;
            if (i > 0)
            {
                Node<T> p = this.head; // p指向头结点
                for (int j = 0; j < i && p.next != null; j++)
                    p = p.next; //让p指向第i的前驱结点
 
                p.next = new Node<T>(x, p.next); //让p的后继结点指向q,让q的后继结点指向p的后继结点
            }
        }

        public bool IsEmpty()
        {
            return this.head.next == null;
        }

        /// <summary>
        /// 获取单链表的长度
        /// </summary>
        /// <returns>单链表的长度</returns>
        public int Length()
        {
            Node<T>? p = this.head.next; // p指向第一个结点
            int i = 0;
            while (p != null)
            {
                p = p.next;
                i++;
            }
            return i;
        }

        /// <summary>
        /// 删除指定位置的元素
        /// </summary>
        /// <param name="i">要删除元素的索引(i>=0)/param>
        /// <returns><删除掉的元素值/returns>
        public T Remove(int i)
        {
            if (i < 0)
                return default(T);

            Node<T> p = this.head;
            for (int j = 0; j < i && p.next != null; j++)
            {
                p = p.next; // 定位到第i-1个元素
            }
            if (p.next != null)
            {
                T old = p.next.data;
                p.next = p.next.next;
                return old;
            }
            return default(T);
        }

        /// <summary>
        /// 删除所有链表元素
        /// </summary>
        public void RemoveAll()
        {
            this.head.next = null;
        }

        /// <summary>
        /// 在链表里查找指定元素
        /// </summary>
        /// <param name="key">要查找的元素值</param>
        /// <returns>首次出现该元素的索引(从0算起)</returns>
        /// <exception cref="NotImplementedException"></exception>
        public int Search(T key)
        {
            if (key == null)
                return -1;
            Node<T> ?p = this.head.next;
            int count = 0;
            while (p != null && !key.Equals(p.data))
            {
                p = p.next;
                count++;
            }

            return count;
        }

        public void Set(int i, T x)
        {
            if (x ==null) //不能设置空对象
                return;
            if (i > 0)
            {
                Node<T> p = this.head.next;
                for (int j = 0; j <= i && p.next != null; j++)
                {
                    p = p.next;
                }
                if (p != null)
                    p.data = x;
            }
            else
                throw new IndexOutOfRangeException(i+"");
        }
    }
        /// <summary>
        /// 重写父类的ToString()方法
        /// </summary>
        /// <returns><返回链表里所有的元素/returns>
        public override string ToString()
        {
            Node<T> ?p = this.head.next; // 指向第一个结点
            string str = "(";
            while (p != null)
            {
                str += p.data;
                if (p.next != null)
                    str += ",";
                p = p.next;
            }
            return str+")";
        }

main 方法调用单链表类

  private static void Main(string[] args)
    {
		string[] names = { "a", "b", "c", "d"};
        SinglyLinkedList<string> singlyLinkedList = new SinglyLinkedList<string>(names);
        Console.WriteLine("链表初始化:链表的元素有:{0}, 链表的长度为:{1}", singlyLinkedList.ToString(), singlyLinkedList.Length());
        Console.WriteLine();

        // 插入
        singlyLinkedList.Insert(0, "aa");
        Console.WriteLine("链表第一个位置插入aa后:{0}, 链表的长度为:{1}", singlyLinkedList.ToString() ,singlyLinkedList.Length());
        Console.WriteLine();

        //获取链表的第3个元素
        Console.WriteLine("链表第三个元素是{0}", singlyLinkedList.Get(2));
        Console.WriteLine();

        //设置链表的第三个元素为bb
        Console.WriteLine("设置链表的第三个元素为bb");
        singlyLinkedList.Set(2, "bb");
        Console.WriteLine("链表第三个元素是{0}", singlyLinkedList.Get(2));
        Console.WriteLine();

        // 在链表里查找元素a
        Console.WriteLine("元素a的位置是:{0}", singlyLinkedList.Search("a"));
        Console.WriteLine();

        // 在链表最后插入last
        singlyLinkedList.Append("last");
        Console.WriteLine("append last之后,链表的元素有:{0}, 链表的长度为:{1}", singlyLinkedList.ToString(), singlyLinkedList.Length());
        Console.WriteLine();

        // 删除第1个元素
        Console.WriteLine("删除第一个元素之前,链表的元素有:{0}, 链表的长度为:{1}", singlyLinkedList.ToString(), singlyLinkedList.Length());
        singlyLinkedList.Remove(0);
        Console.WriteLine("删除第一个元素之后,链表的元素有:{0}, 链表的长度为:{1}", singlyLinkedList.ToString(), singlyLinkedList.Length());
        Console.WriteLine();

        // 删除所有元素
        Console.WriteLine("删除所有元素之前,链表的元素有:{0}, 链表的长度为:{1}", singlyLinkedList.ToString(), singlyLinkedList.Length());
        singlyLinkedList.RemoveAll();
        Console.WriteLine("删除所有元素之后,链表的元素有:{0}, 链表的长度为:{1}", singlyLinkedList.ToString(), singlyLinkedList.Length());
        Console.WriteLine();
   }

6 单链表操作效率分析

IsEmpty()方法的时间复杂度是O(1);Length()方法要遍历单链表,时间复杂度是O(n)。
单链表是一种顺序存储结构,不是随机存储结构。虽然可以直接访问第一个结点,但要访问其他结点,必须从head开始,沿着链的方向逐个结点寻找,知道找到所需的结点。访问第i(0<=i<n)个结点,需要进行i次p=p.next操作,平均移动n/2次,所以Get(i),Set(i)的时间复杂度为O(n)。
Insert(i,x)方法插入x作为第i个结点,时间复杂度是O(n);InsertAfter(p,x)将x插入到p结点之后,时间复杂度为O(1);InsertBefore(p,x)将x插入到p结点之前,必须先找到p的前驱结点font,其实是将x插入到font之后。这个过程需要遍历部分单链表,花费时间视插入位置而定,若在单链表最后插入,则时间复杂度是O(n)。
RemoveAfter§删除p结点的后继结点,时间复杂度为O(1);Remove(i),时间复杂度是O(n)。
如果要删除指定结点p,需要找到p的前驱结点front,然后删除front的后继结点,时间复杂度是O(n)。

对单链表的删除和插入操作只需改变少量结点的链,不需要移动元素。单链表中结构的存储空间是在插入和删除时动态申请和释放的,不需要事先给单链表分配存储空间,这就可以避免顺序表因空间不足而扩充空间和复制元素的过程,提供了运行效率和存储空间的利用率。

7 提高单链表操作效率的措施

由于单链表的Length()方法需要遍历单链表,所以,在某些需要使用长度的情况下,应该注意避免两次遍历单链表。例如,如果我们将单链表的Append()方法声明如下:

        public void Append(T x)
        {
           	//这样实现append也可以,但是效率较低。
            //因为Length()方法需要遍历单链表,这样写就需要遍历两次单链表
			this.Insert(this.Length(),x);
        }

这样就需要遍历两次单链表,效率较低。
类似的,如果我们这样声明ToString()方法:

        public override string ToString()
        {
            string str = "(";
            if (this.Length() > 0)
            {
                for (int i = 0; i < this.Length(); i++)
                {
                    str += this.Get(i) + ",";
                }
                str += this.Get(this.Length() - 1) + ")";
            }
            return str;
        }

这回导致多次遍历单链表。该算法作用于顺序表的时间复杂度是O(n),但作用于单链表的时间复杂度是O( n 2 n^2 n2)

此外,如果在单链表类增加某些私有成员变量,则可提高某些操作效率。例如,增加成员变量len表示单链表的长度,当插入一个元素时,len++,删除一个元素时,len–,则可使Length()的时间复杂度为O(1)。同理,如果增加成员变量rear作为单链表的尾指针,指向单链表的最后一个结点,则在单链表最后插入元素的时间复杂度为O(1)。这些措施虽然能提高效率,但是增加了维护的困难,尤其是在子类中。

下面我们来做两个练习题。

  1. 求整数单链表的平均值
       /// <summary>
        /// 求整数链表的平均值
        /// </summary>
        /// <returns><平均值/returns>
        public int Average(SinglyLinkedList<int> list)
        {
            if (list == null)
                throw new ArgumentNullException("不能对空表计算平均值!");
            int avg = 0,sum = 0, i = 0;
            Node<int> p = list.head.next; // 指向第一个结点,同时要求head的权限是public
            while (p != null)
            {
                sum += p.data; // 元素值累加
                p = p.next; // 移动到下一个结点
                i++; // 我没这里不用list.Length(),因为它会遍历链表,效率比较低
            }

            avg = sum / i;

            return avg;
        }

        /// <summary>
        /// 获取随机产生整数的数组
        /// </summary>
        /// <param name="n">随机数的数量</param>
        /// <returns>整数数组</returns>
        public int[] GetRandom(int n)
        {
            int[] array = new int[n];
            for (int i = 0; i < n; i++)
            {
                array[i] = new Random().Next(); //每次产生一个1-100之间不同的随机正整数,包含1,不包含100
            }
            return array;
        }

main中调用

  private static void Main(string[] args)
    {
        int [] array = SinglyLinkedListAverage.GetRandom(10);
        Console.WriteLine("随机数组为:");
        for (int i = 0; i < array.Length; i++)
        {
            Console.Write("{0} ", array[i]);
        }
        Console.WriteLine();
        SinglyLinkedList<int> list = new SinglyLinkedList<int>(array);
        Console.WriteLine("由数组构造的单链表为:{0}", list.ToString());
        Console.WriteLine("单链表的平均值为:{0}", SinglyLinkedListAverage.Average(list));
    }

运行结果
在这里插入图片描述
2. 反转单链表
反转单链表是指,将单链表中各结点的next域改为指向其前驱结点。逆转算法描述如下。
设p指向单链表中的某个节点,front指向p的前驱结点,successor指向p的后继结点。反转后,p应该指向font,语句为: p.next = font。执行该语句,将导致p指向successor的链断开。这三个结点同时向后移动。具体过程如图2-19所示
在这里插入图片描述
代码实现如下

    public class SinglyLinkedListReverse<T>
    {
        public static void Reverse(SinglyLinkedList<T> list)
        {
            Node<T> front = null; // 前驱结点
            Node<T> succ = null; //后继结点
            Node<T> p = list.head.next; // 第一个结点
            while (p != null)
            {
                succ = p.next; // p的后继结点
                p.next = front; // p指向其前驱结点
                front = p; //front移动到下一个结点
                p = succ; // p移动到下一个结点
            }
            list.head.next = front;
        }
    }

main调用

  private static void Main(string[] args)
    {
        string[] str = { "a", "b", "c", "d"};
        SinglyLinkedList<string> list = new SinglyLinkedList<string>(str);
        Console.WriteLine("链表逆转前为:{0}", list.ToString());
        Console.WriteLine();
        Console.Write("链表逆转后为:");
        SinglyLinkedListReverse<string>.Reverse(list);
        Console.WriteLine("{0}", list.ToString());
    }

运行结果
在这里插入图片描述
单链表的逆转没有什么实际使用价值,逆转不是单链表的基本操作,上述Reverse()方法不能声明为SinglyLinkedList类的成员方法。本例目的有二:其一,演示通过改变结点间的链接关系对结点进行操作;其二,演示在SinglyLinkedList之外声明静态方为其增加功能,方法参数类型为SinglyLinkedList,这样声明的方法称为泛型方法。

8 单链表的浅拷贝和深拷贝

浅拷贝

        /// <summary>
        /// 浅拷贝构造函数
        /// </summary>
        /// <param name="list">要拷贝的链表</param>
        public SinglyLinkedList(SinglyLinkedList<T> list)
        {
            this.head = list.head; //只对头指针进行复制,导致两个单链表对象引用同一条单链表
        }

main调用

  private static void Main(string[] args)
    {
        string[] str = { "a", "b", "c", "d"};
        SinglyLinkedList<string> list1 = new SinglyLinkedList<string>(str);
        Console.WriteLine("链表list1为:{0}", list1.ToString());
        SinglyLinkedList<string> list2 = new SinglyLinkedList<string>(list1);
        Console.WriteLine("链表list2为:{0}", list2.ToString());

        list1.Remove(0);
        Console.WriteLine("链表list1删除第一个元素后为:{0}", list1.ToString());
        Console.WriteLine("链表list1删除第一个元素后list2为:{0}", list1.ToString());
    }

运行结果
在这里插入图片描述
由结果可知,对list1的操作会影响到list2,它没有实现单链表的复制功能。
在这里插入图片描述
深拷贝

        /// <summary>
        /// 深拷贝构造函数
        /// </summary>
        /// <param name="list">要拷贝的链表</param>
        public SinglyLinkedList(SinglyLinkedList<T> list) : this()
        {
            Node<T> p = list.head.next;
            Node<T> rear = this.head;
            while (p != null)
            {
                rear.next = new Node<T>(p.data, null);
                rear = rear.next;
                p = p.next;
            }
        }

main调用

  private static void Main(string[] args)
    {
        string[] str = { "a", "b", "c", "d"};
        SinglyLinkedList<string> list1 = new SinglyLinkedList<string>(str);
        Console.WriteLine("链表list1为:{0}", list1.ToString());
        SinglyLinkedList<string> list2 = new SinglyLinkedList<string>(list1);
        Console.WriteLine("链表list2为:{0}", list2.ToString());

        list1.Remove(0);
        Console.WriteLine("链表list1删除第一个元素后为:{0}", list1.ToString());
        Console.WriteLine("链表list1删除第一个元素后list2为:{0}", list2.ToString());
    }

运行结果
在这里插入图片描述
由结果可知,list1的操作不会影响到list2。
在这里插入图片描述

9 单链表比较相等

SinglyLinkedList类比较两条单链表是否相等的方法声明如下:

        public override bool Equals(object? obj)
        {
            if (obj == this)
                return true;
            if (obj is SinglyLinkedList<T>)
            {
                Node<T> p = this.head.next;
                Node<T> q = ((SinglyLinkedList<T>)obj).head.next;
                while (p != null && q != null && p.data.Equals(q.data))
                {
                    p = p.next;
                    q = q.next;
                }
                return p == null && q == null;
            }
            else
                return false;
        }

思考题
SeqList类的Equal()是否适用于单链表类?为什么?
【答】不适用。因为顺序表类的equal方法里面适用length和get(i)来判断,而这些方法在链表类中是需要作遍历操作的,这对链表来说效率不高。

10 排序单链表

排序单链表是指,各结点按照data域值递增或递减顺序链接。排序单链表与无序单链表操作的注意区别是,插入操作步指定插入位置,而是由各结点的data域值大小决定。
在一个排序单链表中插入data域值为x的元素,元素的位置由x值域与各结点data域值大小决定。确定插入位置的过程是查找过程,即从单链表的第一个结点开始,将元素x依次与当前结点的data值比较大小,一旦找到一个比x大的结点p,则应将x插入在p结点之前,此时需要记住p结点的前驱结点front,将结点插入到它之后。
按升序排列的单链表类SortedSinglyLinkedList声明如下。它继承单链表类,因为涉及到比较大小,所以必须实现IComparer接口,提供比较大小的方法。

    public class SortedSinglyLinkedList<T>: SinglyLinkedList<T>,IComparer<T>
    {
        
        public SortedSinglyLinkedList() // 默认构造函数 调用默认构造函数时,会自动先去调用父类的构造函数
        {     
        }

        public SortedSinglyLinkedList(T[] elements)
        {
            if (elements != null)
            {
                for (int i = 0; i < elements.Length; i++)
                {
                    this.Insert(elements[i]);
                }
            }
        }

        /// <summary>
        /// 深拷贝构造函数
        /// </summary>
        /// <param name="list">要复制的排序单链表</param>
        public SortedSinglyLinkedList(SortedSinglyLinkedList<T> list):base(list) // 因为拷贝构造函数不是默认构造函数,所以得显示调用父类的拷贝构造函数
        {

        }

        public int Compare(T? x, T? y)
        {
            if (x == null || y == null)
                return -1;
            if (x is int && y is int)
            {
                int a = Convert.ToInt32(x);
                int b = Convert.ToInt32(y);
                return a.CompareTo(b);
            }
            if (x is string && y is string)
            {
                string a = x as string;
                string b = y as string;
                if (a != null)
                    return a.CompareTo(b);
            }
            return 0;
        }

        /// <summary>
        /// 插入x
        /// </summary>
        /// <param name="x">数据域值x</param>
        public void Insert(T x) // 重载父类的insert方法
        {
            if (x == null) // 不可以插入空对象
                return;
            Node<T> front = this.head; // 头结点 front是p的前驱结点
            Node<T> p = this.head.next; // 第一个结点
            while (p != null && this.Compare(p.data, x) < 0) //如果当前结点的data值<x,则继续向后找
            {
                front = p;
                p = p.next;
            }
            front.next = new Node<T>(x, p); // 创建结点在front之后,p之前
        }

        //隐藏父类的Insert(int i, T x)方法,因为排序单链表不支持在指定位置插入元素
        public new void Insert(int i, T x)
        {
            throw new Exception("不支持该方法");
        }

        //隐藏父类的Append(T x)方法,因为排序单链表不支持在指定位置插入元素
        public new void Append(T x)
        {
            throw new Exception("不支持该方法");
        }

        /// <summary>
        /// 删除首次出现的值为x的结点,若找不到,就不删除
        /// </summary>
        /// <param name="x">要删除的数据元素</param>
        public void Remove(T x) //重载父类的remove方法 时间复杂度为 O(n)
        {
            if (x == null)
                return;
            Node<T> p = this.head.next;
            Node<T> front = this.head;
            while (p != null && this.Compare(p.data,x) < 0) //因为链表本身是按升序排序的,所以只要找到比x小的data值中最大的那个结点即可
            {
                front = p;
                p = p.next;
            }
            if (p != null && this.Compare(p.data, x) == 0)
                front.next = p.next; // 删除p结点
        }
    }

其中insert(x)方法和remove(x)方法,没插入或删除一个结点,都要根据元素值x在单链表中进行查找操作,确定插入或删除位置并进行插入或删除操作,两者都要借助p的前驱结点front。
main方法里面调用

  private static void Main(string[] args)
    {
        string[] element = {"b", "c", "d", "f" };
        //SortedSinglyLinkedList<int> list1 = new SortedSinglyLinkedList<int>(SinglyLinkedListAverage.GetRandom(10));
        SortedSinglyLinkedList<string> list1 = new SortedSinglyLinkedList<string>(element);
        Console.WriteLine("list1 为:{0}", list1.ToString());
        Console.WriteLine();

        //SortedSinglyLinkedList<int> list2 = new SortedSinglyLinkedList<int>(list1);
        SortedSinglyLinkedList<string> list2 = new SortedSinglyLinkedList<string>(list1);
        Console.WriteLine("list2 为:{0}", list2.ToString());
        Console.WriteLine();

        list1.Insert("a");
        //Console.WriteLine("list1插入-2后");
        Console.WriteLine("list1插入a后");
        Console.WriteLine("list1 为:{0}", list1.ToString());
        Console.WriteLine("list2 为:{0}", list2.ToString());
        Console.WriteLine();

        // 这两句删除的代码体现了多态,如果结点的data值为int类型,就不能很好的体现出多态度,因此本例子中结点data的数据类型为string
        list1.Remove(list1.Get(0)); // 删除第一个结点 调用排序单链表类的remove(T x)方法,删除指定元素
        list1.Remove(list1.Length()-1); //删除最后一个结点 调用父类(单链表)的remove(int i)方法,删除指定位置元素
        Console.WriteLine("list1删除第一个和最后一个元素后");
        Console.WriteLine("list1 为:{0}", list1.ToString());
        Console.WriteLine("list2 为:{0}", list2.ToString());
        Console.WriteLine();
    }

11 循环单链表

如果单链表最后一个结点的next链保存单链表头结点head值,则该单链表成为环形结构,称为循环单链表(circular linked list)。带头结点的循环单链表如图2-22所示。
在这里插入图片描述
循环单链表类CircularLinkedList声明如下,仍然适用Node类,其基本操作和单链表类似,部分成员方法声明省略。

    public class CircularLinkedList<T> : ILinearList<T>
    {
        public Node<T> head; // 头指针,指向头结点

        // 默认构造函数 构造空循环单链表
        public CircularLinkedList()
        {
            this.head = new Node<T>(); // 创建头结点
            this.head.next = this.head;
        }
        /// <summary>
        /// 重写父类的ToString()方法
        /// </summary>
        /// <returns><返回链表里所有的元素/returns>
        public override string ToString()
        {
            Node<T> p = this.head.next; // 指向第一个结点
            string str = "(";
            while (p != this.head) // 注意这里的判断条件
            {
                str += p.data;
                if (p.next != this.head) // 注意这里的判断条件
                    str += ",";
                p = p.next;
            }
            return str + ")";
        }
   }

2. 双链表

1 双链表结点

双链表的每个结点有两个地址域,一个指向其前驱结点,一个指向其后继结点,结构如下:
在这里插入图片描述
双链表类DLinkNode类的声明如下:

    // 双链表结点类
    public class DLinkNode<T>
    {
        public T data; // 数据元素
        public DLinkNode<T> prev, next; // prev指向前驱结点, next指向后继结点

        public DLinkNode():this(default(T), null, null)
        {
        }

        // 构造结点
        public DLinkNode(T data,DLinkNode<T> prev, DLinkNode<T> next)
        {
            this.data = data;
            this.prev = prev;
            this.next = next;
        }
    }

2 双链表

带头结点的双链表结构如图2-23所示,空双链表只有头结点,且head.next = null && head.prev = null。
在这里插入图片描述
设p指向双链表中非两端的的某个节点,p.next指向其后继结点,p.prev指向其前驱结点,有下列关系成立: p.next.prev= p.prev.next = p。

1 双链表的插入操作

在双链表中插入一个结点,既可插入在指定结点之前,也可插入在指定结点之后。
设p指向双链表某结点,在p之后插入值为x的结点语句如下:

DLinkNode q = new DLinkNode(x, p, p.next);
if(p.next!=null) // p.next ==null时,尾插入
    p.next.prev = q;
p.next = q;

在这里插入图片描述
在p之前插入插入值为x的结点语句如下:

DLinkNode q = new DLinkNode(x, p.prev, p);
p.prev.next = q;
p.prev = q;

在这里插入图片描述

2 双链表的删除操作

删除p指向的结点,语句如下:

p.prev.next = p.next;
if(p.next!=null) 
	p.next.prev = p.prev;

在这里插入图片描述
双链表类DoublyLinkedList声明如下:

    // 双链表类
    public class DoublyLinkedList<T> : ILinearList<T>
    {
        public DLinkNode<T> head; // 头结点

        /// <summary>
        /// 默认构造函数,构造空双链表
        /// </summary>
        public DoublyLinkedList() 
        {
            this.head = new DLinkNode<T>(); 
        }

        /// <summary>
        /// 根据数组构造循环双链表
        /// </summary>
        /// <param name="elements">数组</param>
        public DoublyLinkedList(T[] elements):this()// 采用尾查法构建双链表
        {
            DLinkNode<T> rear = this.head; // rear指向最后一个结点
            for (int i = 0; i < elements.Length; i++)
            {
                rear.next = new DLinkNode<T>(elements[i], rear, null);
                rear = rear.next;
            }
        }

        /// <summary>
        /// 深拷贝构造函数
        /// </summary>
        /// <param name="list">要拷贝的链表</param>
        public DoublyLinkedList(DoublyLinkedList<T> list) : this() // 构造空双链表
        {
            DLinkNode<T> rear = this.head; // rear指向最后一个结点
            DLinkNode<T> p = list.head.next; // p指向list的第一个结点
            while (p != null)
            {
                rear.next = new DLinkNode<T>(p.data, rear, null) ;
                rear = rear.next;
                p = p.next;
            }
        }

        /// <summary>
        /// 在链表最后插入x
        /// </summary>
        /// <param name="x">要插入的数据元素值</param>
        public void Append(T x) // O(n)
        {
            if (x == null)
                return; // 不可插入空对象
            DLinkNode<T> p = this.head.next; // p指向第一个结点
            while (p.next != null)
            {
                p = p.next; // p最后指向最后一个结点
            }
            p.next = new DLinkNode<T>(x, p.prev, null);
        }

        /// <summary>
        /// 获取指定位置的元素值
        /// </summary>
        /// <param name="i">要获取元素值的索引(从0开始)</param>
        /// <returns>元素值</returns>
        /// <exception cref="IndexOutOfRangeException"></exception>
        public T Get(int i)
        {
            if (i >= 0)
            {
                DLinkNode<T> p = this.head.next; // p指向第一个结点
                for (int j = 0; j < i && p.next!=null; j++)
                {
                    p = p.next; // p最后指向i的位置
                }
                return p.data;
            }
            else
                throw new IndexOutOfRangeException(i.ToString());
        }

        /// <summary>
        /// 在指定位置插入指定值
        /// </summary>
        /// <param name="i">要插入的位置索引(从0开始)</param>
        /// <param name="x">要插入的元素值</param>
        /// <exception cref="IndexOutOfRangeException"></exception>
        public void Insert(int i, T x)
        {
            if (x == null)
                return; // 不能插入空对象
            if (i >= 0)
            {
                DLinkNode<T> p = this.head; // p指向第一个结点
                for (int j = 0; j < i && p.next != null; j++)
                {
                    p = p.next; // p最后指向i-1的位置
                }

                DLinkNode<T> q = new DLinkNode<T>(x, p, p.next);
                p.next.prev = q;
                p.next = q;
            }
            else
                throw new IndexOutOfRangeException(i.ToString());
        }

        /// <summary>
        /// 双链表是否为空
        /// </summary>
        /// <returns>是否为空</returns>
        public bool IsEmpty()
        {
            return this.head.prev == null && this.head.next == null;
        }

        /// <summary>
        /// 获取双链表的长度
        /// </summary>
        /// <returns>双链表的长度</returns>
        public int Length()
        {
            DLinkNode<T> p = this.head; // p指向头结点
            int length = 0;
            while (p.next != null)
            {
                p = p.next;
                length++;
            }
            return length;
        }

        /// <summary>
        /// 删除指定位置的元素
        /// </summary>
        /// <param name="i">要删除元素的位置索引(从0开始)</param>
        /// <returns>删除的元素值</returns>
        /// <exception cref="IndexOutOfRangeException"></exception>
        public T Remove(int i)
        {
            if (i >= 0)
            {
                DLinkNode<T> p = this.head.next;
                for (int j = 0; j < i && p.next!=null; j++)
                {
                    p = p.next; // p最后指向i的位置
                }
                T old = p.data;
                if (p.next != null)
                {
                    p.prev.next = p.next;
                    p.next.prev = p.prev;
                }
                return old;
           
            }
            else
                throw new IndexOutOfRangeException(i.ToString());
        }

        /// <summary>
        /// 删除链表所有元素
        /// </summary>
        public void RemoveAll()
        {
            this.head.prev = null;
            this.head.next = null;
        }

        /// <summary>
        /// 查找元素
        /// </summary>
        /// <param name="key">要查找的元素值</param>
        /// <returns><元素的位置索引(从0开始)/returns>
        public int Search(T key)
        {
                if (key == null)
                return -1;
            DLinkNode<T>? p = this.head.next;
            int count = 0;
            while (p != null && !key.Equals(p.data))
            {
                p = p.next;
                count++;
            }
            return count;
        }

        /// <summary>
        /// 设置指定位置的元素值为指定值
        /// </summary>
        /// <param name="i">指定的位置索引(从0开始)</param>
        /// <param name="x">要设置的元素值</param>
        /// <exception cref="IndexOutOfRangeException"></exception>
        public void Set(int i, T x)
        {
            if (x == null)
                return; // 不可以设置空对象
            if (i >= 0)
            {
                DLinkNode<T> p = this.head.next; // p指向第一个结点
                for (int j = 0; j < i && p.next!=null; j++)
                {
                    p = p.next; // p最后指向i的位置
                }
                p.data = x;
            }
            else
            throw new IndexOutOfRangeException(i.ToString());
        }

        /// <summary>
        /// 重写父类的ToString()方法
        /// </summary>
        /// <returns><返回链表里所有的元素/returns>
        public override string ToString()
        {
            DLinkNode<T>? p = this.head.next; // 指向第一个结点
            string str = "(";
            while (p != null)
            {
                str += p.data;
                if (p.next != null)
                    str += ",";
                p = p.next;
            }
            return str + ")";
        }
    }

3 循环双链表

如果双链表最后一个结点的next链指向头结点,头结点的prev链指向最后u一个结点,则构成循环双链表。如下图所示,空循环双链表head.next = head&& head.prev = head。
在这里插入图片描述
循环双链表类CirDoublyLinkedList声明如下,循环双链表类继承DoublyLinkedList类,部分方法的实现和DoublyLinkedList不同,具体请参考下列代码

    public class CirDoublyLinkedList<T>: DoublyLinkedList<T>
    {
        public CirDoublyLinkedList() // 默认构造函数 构造空循环双链表
        {
            this.head.next = head;
            this.head.prev = head;
        }

        public CirDoublyLinkedList(T[]elements) 
        {
            DLinkNode < T > rear = this.head; // rear指向最后一个结点
            for (int i = 0; i < elements.Length; i++) // 采用尾插法构造循环双链表
            {
                DLinkNode < T > q = new DLinkNode<T>(elements[i], rear, head);
                rear.next = q;
                rear = rear.next;
                this.head.prev = q;
            }
        }

        /// <summary>
        /// 深拷贝构造函数
        /// </summary>
        /// <param name="list">要拷贝的链表</param>
        public CirDoublyLinkedList(CirDoublyLinkedList<T> list) : this () // 构造空循环单链表
        {
            DLinkNode<T> rear = this.head; // rear指向最后一个结点
            DLinkNode<T> p = list.head.next; // p指向list的第一个结点
            while (p.next != list.head.next)
            {
                DLinkNode<T> q = new DLinkNode<T>(p.data, rear, head);
                rear.next = q;
                rear = rear.next;
                p = p.next;
                this.head.prev = q;
            }
        }

        public new void Append(T x) //O(1) 这个和双链表的实现方式不同,覆盖父类的Append(T x)
        {
            if (x == null)
                return;
            DLinkNode<T> q = new DLinkNode<T>(x, this.head.prev, head);
            this.head.prev.next = q;
            head.prev = q;
        }

        public new bool IsEmpty() // 和父类的IsEmpty()判断方法不一致,覆盖父类该方法
        {
            return this.head.next ==this.head;
        }

        public new int Length()// 和父类的Length判断方法不一致,覆盖父类该方法
        {
            DLinkNode<T>? p = this.head.next; // p指向第一个结点
            int i = 0;
            while (p.next != this.head)
            {
                p = p.next;
                i++;
            }
            return i;
        }

        public new void RemoveAll() // 和父类的RemoveAll()判断方法不一致,覆盖父类该方法
        {
            this.head.prev = head;
            this.head.next = head;
        }

        /// <summary>
        /// 重写基类的ToString()方法
        /// </summary>
        /// <returns><返回链表里所有的元素/returns>
        public override string ToString() // 重写基类的方法
        {
            DLinkNode<T>? p = this.head.next; // 指向第一个结点
            string str = "(";
            while (p.data!=null) // head头结点的data值为null  这里不能用p.next != this.head来判断,会丢失最后一个数据元素
            {
                str += p.data;
                if (p.next != this.head)
                    str += ",";
                p = p.next;
            }
            return str + ")";
        }
    }

4 排序循环双链表

循环双链表的data值按照递增或递减的次序排列,就称为排序循环双链表。
在排序循环双链表中插入一个值为x的结点,首先要查找插入位置,从双链表的第一个结点开始,将元素x依次与当前结点的data值比较,一旦找到一个比x大或等值结点p,将x插入在p之前。
插入x时,如果x比排序循环双链表里面所有元素的data值都大,此时我没就不用循环遍历了,直接插入到最后即可,此时时间复杂度就是O(1);否则,还是需要从第一结点开始遍历,此时时间复杂度时O(n)。
在这里插入图片描述
排序循环双链表类SortedCirDoublyLinkedList声明如下:

    public class SortedCirDoublyLinkedList<T> : CirDoublyLinkedList<T>, IComparer<T>
    {
        public SortedCirDoublyLinkedList()
        {
        }

        public SortedCirDoublyLinkedList(T[] elements)
        {
            for (int i = 0; i < elements.Length; i++)
            {
                this.Insert(elements[i]);
            }
        }

        /// <summary>
        /// 深拷贝构造函数
        /// </summary>
        /// <param name="list">要拷贝的链表</param>
        public SortedCirDoublyLinkedList(SortedCirDoublyLinkedList<T> list) : base(list) // 构造空循环单链表
        {
        }

        public int Compare(T? x, T? y)
        {
            {
                if (x == null || y == null)
                    return -1;
                if (x is int && y is int)
                {
                    int a = Convert.ToInt32(x);
                    int b = Convert.ToInt32(y);
                    return a.CompareTo(b);
                }
                if (x is string && y is string)
                {
                    string a = x as string;
                    string b = y as string;
                    if (a != null)
                        return a.CompareTo(b);
                }
                return 0;
            }
        }

        /// <summary>
        /// 插入指定值
        /// </summary>
        /// <param name="x">要插入的元素值</param>
        public void Insert(T x) 
        {
            if (x == null)
                return; // 不允许插入空对象

            // 插入如果在末端,可以直接插入,不用遍历 O(1)
            if (this.head.prev != this.head && this.Compare(this.head.prev.data, x) < 0)
            {
                DLinkNode<T> m = new DLinkNode<T>(x, this.head.prev, this.head);
                this.head.prev.next = m;
                this.head.prev = m;
            }
            else
            {
                // 插入在中间
                DLinkNode<T> p = this.head.next; // p指向第一个结点
                while (p != head && this.Compare(p.data, x) < 0)
                {
                    p = p.next; //p最后指向第一个data值大于x的结点,将x插入到p之前
                }
                DLinkNode<T> q = new DLinkNode<T>(x, p.prev, p);
                p.prev.next = q;
                p.prev = q;
            }
        }

        /// <summary>
        /// 删除指定值
        /// </summary>
        /// <param name="x">要删除的元素值</param>
        public void Remove(T x)
        {
            if (x == null)
                return; 
             
                DLinkNode<T> p = this.head.next; // p指向第一个结点
                while (p != head && this.Compare(p.data, x) < 0)
                {
                    p = p.next; //p最后指向第一个data值大于或等于x的结点,将x插入到p之前
                }
            if (p != head && this.Compare(p.data, x) == 0)
            {
                p.prev.next = p.next;
                p.next.prev = p.prev;
            }            
        }

        /// <summary>
        /// 在指定位置插入指定值
        /// </summary>
        /// <param name="i">要插入的位置索引(从0开始)</param>
        /// <param name="x">要插入的元素值</param>
        /// <exception cref="Exception"></exception>
        public new void Insert(int i, T x)
        {
            throw new Exception("不支持该方法");
        }

        /// <summary>
        /// 设置指定位置的元素值为指定值
        /// </summary>
        /// <param name="i">指定的位置索引(从0开始)</param>
        /// <param name="x">要设置的元素值</param>
        /// <exception cref="IndexOutOfRangeException"></exception>
        public new void Set(int i, T x)
        {
            throw new Exception("不支持该方法");
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值