通过研究证明,怎么学好数据结构?怎么入门?需要学些什么东西?链表是数据结构的重要部分,学好用好链表,在解题的过程中,思路将更加清晰,链表作为数据结果的基础之一,本篇将会通过图文和代码展示的形式系统的介绍。

什么是链表?

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。


顺序存储和链式存储

(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)链表还有删除、获取、查找等操作,这些都相对简单,大家可以在做题中理解。


点赞(0)

C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:

一点编程也不会写的:零基础C语言学练课程

解决困扰你多年的C语言疑难杂症特性的C语言进阶课程

从零到写出一个爬虫的Python编程课程

只会语法写不出代码?手把手带你写100个编程真题的编程百练课程

信息学奥赛或C++选手的 必学C++课程

蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程

手把手讲解近五年真题的蓝桥杯辅导课程

Dotcpp在线编译      (登录可减少运行等待时间)