通过研究证明,怎么学好数据结构?怎么入门?需要学些什么东西?链表是数据结构的重要部分,学好用好链表,在解题的过程中,思路将更加清晰,链表作为数据结果的基础之一,本篇将会通过图文和代码展示的形式系统的介绍。
什么是链表?
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
顺序存储和链式存储
(1)数组—顺序存储
数组作为一个顺序储存方式的数据结构,可是有大作为的,它的灵活使用为我们的程序设计带来了大量的便利;
但数组最大的缺点就是我们的插入和删除时需要移动大量的元素,所以,很多人想到需要大量的消耗时间,以及冗余度就想放弃或寻求别的方法。
以C语言数组插入一个元素为例,当我们需要在一个数组{1,2,3,4}的第1个元素后的位置插入一个’A’时,我们需要做的有:
1. 将第1个元素后的整体元素后移,形成新的数组{1,2,2,3,4}
2. 再将第2个元素位置的元素替换为我们所需要的元素’A’
3. 最终形成我们的预期,这需要很多的操作喔。

上图可以看出,使用数组都有这两大缺点:
1. 插入删除操作所需要移动的元素很多,浪费算力。
2. 必须为数组开足够的空间,否则有溢出风险。
(2)链表—链式存储
由于数组的这些缺点,自然而然的就产生链表的思想。
链表通过不连续的储存方式,自适应内存大小,以及指针的灵活使用,巧妙的简化了上述的内容。
链表的基本思维是,利用结构体的设置,额外开辟出一份内存空间去作指针,它总是指向下一个结点,一个个结点通过NEXT指针相互串联,就形成了链表。

其中DATA为自定义的数据类型,NEXT为指向下一个链表结点的指针,通过访问NEXT,可以引导我们去访问链表的下一个结点。
对于一连串的结点而言,就形成了链表如下图:

相比起数组,链表解决了数组不方便移动,插入,删除元素的弊端,但相应的,链表付出了更加大的内存牺牲换来的这些功能的实现。
链表概述
包含单链表,双链表,循环单链表,实际应用中的功能不同,但实现方式都差不多。

链表与数组的区别
回忆下数组的概念 ,所谓数组,是相同数据类型的元素按一定顺序排列的集合。根据概念我们可以知道数组在内存中连续,链表不连续;由于不同的存储方式导致数组静态分配内存,链表动态分配内存,数组元素在栈区,链表元素在堆区;由于数组在内存中连续,我们可以利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n);但是由于数组的连续性数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)。总结一下,数组和链表的区别如下:
1. 数组静态分配内存,链表动态分配内存
2. 数组在内存中连续,链表不连续
3. 数组元素在栈区,链表元素在堆区
4. 数组利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n);
5. 数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)。
C#实现链表的基本操作
以单链表为例,单链表是一种链式存取的数据结构,链表中的数据是以结点来表示的,每个结点由元素和指针构成。元素表示数据元素的映象,就是存储数据的存储单元;指针指示出后继元素存储位置,就是连接每个结点的地址数据。
以结点的序列表示的线性表称作线性链表,也就是单链表,单链表是链式存取的结构。
public class Node<T>
{
private T data;
private Node<T> next;
//有参构造函数
//主要用例实例化需要处理的节点用
public Node(T item, Node<T> next)
{
data = item;
this.next = next;
}
//无参构造函数,用例实例化Node节点
public Node()
{
data = default(T);
next = null;
}
public Node<T> Next
{
get { return next; }
set { this.next = value; }
}
public T Data
{
get { return data; }
set { this.data = value; }
}
}接下来我们来实现链表的操作,构造一个链表,在构造链表里我们定一个头结点的对象,头结点是个很有用的节点,在后续代码中就可以慢慢体会到
public class MyLinkList<T>
{
public Node<T> Head { get; set; }
//构造器
public MyLinkList()
{
Head = null;
}
}(1)求链表的长度,思路:从头结点向后访问,直到最后一个节点,代码展示:
public int Length()
{
var p = Head;
int len = 0;
while (p != null)
{
++len;
p = p.Next;
}
return len;
}(2)清空链表,这个就比较奥简单了,直接将头结点置为null 即可,代码展示:
public void Clear()
{
Head = null;
}(3)同理判断链表是否为空也要用的头结点,代码展示:
public bool IsEmpty()
{
if (Head == null)
{
return true;
}
else
{
return false;
}
}(4)在链表的末尾添加新元素,添加新元素,需要先判断链表是否为空,如果为空我们要给头结点赋值,如果不为空需要修改最后一个节点的next指向,代码展示:
public void Append(T item)
{
if (Head == null)
{
Head = new Node<T>(item, null);
return;
}
var p = new Node<T>();
p = Head;
while (p.Next != null)
{
p = p.Next;
}
p.Next = new Node<T>(item, null);
}(5)在单链表的第i个结点的位置前插入一个指定结点,首先需要找到插入的位置,然后修改相邻节点的指向即可,代码展示:
public void Insert(T item, int i)
{
if (IsEmpty() || i < 1 || i > GetLength())
{
return;
}
//如果在第一个位置插入 则只需要将该节点的next 指向head即可
if (i == 1)
{
var first = new Node<T>(item, null);
first.Next = Head;
Head = first;
return;
}
var p = new Node<T>();
p = Head;
var left = new Node<T>();
var right = new Node<T>();
int j = 1;
while (p.Next != null && j < i)
{
left = p;
right = p.Next;
++j;
}
var q = new Node<T>(item, null);
left.Next = q;
q.Next = right;
}(6)删除指定节点,先找到要删除的前一个结点,然后修改该结点的next指向即可。
(7)链表还有删除、获取、查找等操作,这些都相对简单,大家可以在做题中理解。
C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:
一点编程也不会写的:零基础C语言学练课程
解决困扰你多年的C语言疑难杂症特性的C语言进阶课程
从零到写出一个爬虫的Python编程课程
只会语法写不出代码?手把手带你写100个编程真题的编程百练课程
信息学奥赛或C++选手的 必学C++课程
蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程
手把手讲解近五年真题的蓝桥杯辅导课程