在编程领域,数据结构与算法向来都是提升编程能力的重点。而一般常见的数据结构是链表,栈,队列,树等。事实上C#也已经封装好了这些数据结构,在头文件 System.Collections.Generic 中,直接创建并调用其成员方法就行。不过我们学习当然要知其然,亦知其所以然。
本文实现的是链表中的单链表和双向链表,并且实现了一些基本方法
一. 定义一个链表接口 MyList
接口里声明了我们要实现的方法:
interface MyList<T> { int GetLength(); //获取链表长度 void Clear(); //清空链表 bool IsEmpty(); //判断链表是否为空 void Add(T item); //在链表尾部添加新节点 void AddPre(T item,int index); //在指定节点前添加新节点 void AddPost(T item,int index); //在指定节点后添加新节点 T Delete(int index); //按索引删除节点 T Delete(T item,bool isSecond = true); //按内容删除节点,如果有多个内容相同点,则删除第一个 T this[int index] { get; } //实现下标访问 T GetElem(int index); //根据索引返回元素 int GetPos(T item); //根据元素返回索引地址 void Print(); //打印 }
二. 实现单链表
2.1 节点类
先定义一个单链表所用的节点类,Node。而且我们要实现泛型
先定义一个数据域和下一节点(“Next”),并进行封装,然后给出数个重载构造器。这一步比较简单,这里直接给出代码
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 线性表 { /// <summary> /// 单向链表节点 /// </summary> /// <typeparam name="T"></typeparam> class Node<T> { private T data; //内容域 private Node<T> next; //下一节点 public Node() { this.data = default(T); this.next = null; } public Node(T value) { this.data = value; this.next = null; } public Node(T value,Node<T> next) { this.data = value; this.next = next; } public T Data { get { return data; } set { data = value; } } public Node<T> Next { get { return next; } set { next = value; } } } }
2.2 链表类
创建一个链表类,命名为 LinkList 并继承 MyList。
先定义一个头结点,尾节点和一个 count;
其中,head 表示该链表的头部,不包含数据;
tail 表示尾节点,指向该链表最后一个节点,当链表中只有 head 时,tail 指向 head。定义 tail 会方便接下来的操作
count 用来表示该链表中除了 head 以外的节点个数
构造函数:
/// <summary> /// 构造器 /// </summary> public LinkList() { head = new Node<T>(); tail = head; count = 0; }
在我们实现成员函数之前,先实现两个特别的方法,因为在许多的成员方法中都要做两个操作:
- 判断索引 index 是否合法,即是否小于0或者大于当前链表的节点个数
- 寻找到 index 所代表的节点
①. 判断索引是否合法,然后可以根据其返回的数值进行判断操作
②. 寻找节点。
定义这两个方法主要是它们的重复使用率高,所以把它们的代码抽出来。
相对于数组,链表的插入与删除更方便,而查找却更加费时,一般都是从头结点开始遍历链表,时间复杂度为 O(n) ,而跳跃链表则会对查询进行优化,当然这会在下一篇中详述。现在继续来实现成员方法。
1. 获取链表长度
![]()
这个方法实际上是比较简单的,因为 count 会随着添加,删除等操作自动增减,所以直接返回 count 就相当于 链表长度。
需要注意的是,本文中的 count 是不计算空头结点的,即 head 不会计算入内
2. 清空链表
这里要注意对 tail 的操作,而 head.Next 原本所指的节点不再被引用后,会被GC自动回收
3. 判断链表是否为空
因为本文实现的链表是带空头结点的,所以这里认为,当除了头结点外没有别的节点时,则为空链表