什么是链表,如何实现?(单链表篇)

简介: 什么是链表,如何实现?(单链表篇)

 9dd29798538247eaaf2a70d49bffb4bb.png

“仅仅活着是不够的,还需要有阳光,自由和花的芬芳。”


前言:

在日常使用的网站和软件中,列表属于最常见的一种东西了,其实现形式有顺序表,链表等,主要功能有增删查改,那么链表具体是什么,如何实现?这篇博客为你解答。

注:

这篇博客属于数据结构内容,建议学习完C语言的小伙伴食用~


目录

🥰Part1: 何为链表

1.链表的概念

2.链表的结构

💗Part2: 链表的实现

1.开工准备

1.1创建项目

1.2建立结点

1.3动态申请结点

2.相关功能实现

2.1打印链表

2.2尾部插入

2.3头部插入

2.4尾部删除

2.5头部删除

2.6查找位置

2.7在pos位置之后插入

2.8删除pos之后的值

2.9pos之前插入

2.10删除pos结点



Part1: 何为链表


1.链表的概念

概念:链表是一种物理存储结构上非连续非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。

简单理解下这个概念,就是一个存储单元中有 存储的数据 下一个存储单元的地址 ,经过这个存储单元可利用地址找到下一个存储单元。

下面讲到结构就会更加明白:


2.链表的结构

d6fe6c164fe659153723ccce384e76a5_844e778f4c99492180ec65482cbd3d53.png

一个简单链表的逻辑图 


注意:

• 链式结构在逻辑上连续,在物理上不一定连续(存储的地址不连续);

• 现实中的结点一般是从上申请出来的;

• 从堆上申请的空间,按照一定策略分配,两次申请的空间不一定连续


Part2: 链表的实现


1.开工准备


1.1创建项目


按照惯例,在 Single linked list 解决方案中创建三个项:

SList.h:头文件,声明所有函数;

SList.c:源文件,实现各函数;

test.c:  源文件,主函数所在项,可调用各函数。


1.2建立结点


这里创建一个结点的结构体,包含要储存的数据和下一个结点的地址:

typedef int SLTDateType;
typedef struct SListNode
{
  SLTDateType data;
  struct SListNode* next;//指向结构体的指针,可通过它找到下一个结构体
}SLTNode;//将此结点简易命名,方便引用


1.3动态申请结点


关于为什么要动态申请:

动态申请按需分配内存,既不会空间多余,也不会浪费

SLTNode* BuySListNode(SLTDateType x)
{
  //动态申请
  SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  if (newnode == NULL)
  {
    perror("malloc failed");
    return;
  }
  //初始化
  newnode->data = x;
  newnode->next = NULL;
  return newnode;
}

注意动态申请后要进行判断和初始化。


2.相关功能实现


2.1打印链表


我们打印的是一个结点中的数据

void SListPrint(SLTNode* phead)
{
  SLTNode* cur = phead;
  while (cur)
  {
    printf("%d->", cur->data);
    cur = cur->next;//地址不一定连续,不可++;
  }
  printf("NULL\n");
}

这里值得注意的是 遍历过程中是如何前进的:

47b2e27df8b1493fe810d5e98d7d3f58_964f74fc67074c36985e23117feb2e7f.png

因为地址不一定连续,所以要进行赋值,不可直接++,这一点很重要 


2.2尾部插入


尾部插入,在申请一个新的结点后,要做的最重要的一点就是 末端节点 中的 结构体指针 要指向   新的结点

尾部插入有两种情况:

• phead 为NULL;

• phead 不为NULL。

我们一个个分析:

第一种情况 比较简单,phead 为空

要让 phead 指向 newnode 指向的结点 ,直接赋值即可

e7efdef70a02a1ca26cd1f97b9fdb98d_cbdf9255f83c40548a3629505bf70763.png

*pphead = newnode;//pphead 是指向 phead 的指针,后面会讲利用二级指针的原因

第二种情况 就比较复杂,

因为要实现上下的链接

bbf7bd7660170a5dd2c369fbb041ea78_5000ee30eb6b423cb48d863ec3a9f832.png

再寻找一个 tail 用来遍历整个链表,遇到 NULL 时停止。

void SListPushBack(SLTNode** pphead, SLTDateType x)
{
  assert(pphead);
    SLTNode* newnode = BuySListNode(x);
  if (*pphead == NULL)
  {
    *pphead = newnode;
  }
  else
  {
    //查找尾部
    SLTNode* tail = *pphead;
    while (tail->next != NULL)
    {
      tail = tail->next;
    }
    tail->next = newnode;//改变的是结构体成员,用结构体的指针当然可以
  }
}

需要注意的是

形参的改变不影响实参,需要通过二级指针来改变 phead.

如要改变 int* ,就需要通过 int** 来改变。


2.3头部插入


头部插入,包括前后两个方向的链接:一是 phead 要指向新的结点 ,二是 新结点中的结构体指针 指向原链表的第一个结点

80d2b6f8b53a6da2ca218553df4ae356_85dcff9db6d54c6aa42ca11fcced4a9d.png

我是图示

void SListPushFront(SLTNode** pphead, SLTDateType x)
{
  assert(pphead);
    SLTNode* newnode = BuySListNode(x);
  newnode->next = *pphead;
  *pphead = newnode;
}


2.4尾部删除


我的思路是找到倒数第二个结点,但还有只有一个结点的情况。

只有一个结点时,不用查找,直接释放并置空该节点;

有多个结点时,先找到倒数第二个结点,将该节点的下一个结点释放并置空即可。

void SListPopBack(SLTNode** pphead)
{
  assert(pphead);
    if ((*pphead)->next == NULL)
  {
    free(*pphead);
    *pphead = NULL;
  }
  else
  {
    SLTNode* tail = *pphead;
    while (tail->next->next != NULL)
    {
      tail = tail->next;
    }
    free(tail->next);
    tail->next = NULL;
  }
}


2.5头部删除


要头部删除,令 phead 指向第二个结点后释放第一个结点即可。

void SListPopFront(SLTNode** pphead)
{
  assert(pphead);
  SLTNode* first = *pphead;
  *pphead = first->next;
  free(first);
  first = NULL;
}


2.6查找位置


查找特定数值所在的位置,思路是通过头指针遍历,最多遍历一遍链表

SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
  SLTNode* cur = plist;
  while (cur)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return cur;
}

最后返回的是 结构体指针 ,也就是数值所在结构体的地址。


2.7在pos位置之后插入


在创建 newnode 之后,改变指针指向即可

730f2e6a68440de327eb1d80d956ef01_9f91f2218f894001b2faf00253975f75.png

我是图示

void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
  assert(pos);
  SLTNode* newnode = BuySListNode(x);
  newnode->next = pos->next;
  pos->next = newnode;
}

注意:要先改变 newnode.next 的值,再改变 pos.next 的值,否则会被覆盖。


2.8删除pos之后的值


可以先找到要删除的结点,也就是pos的下一个结点,再将该节点释放置空

6ce8880fa221a105b85ab0c2ae283c6b_3442055b3b664b6fa60ef80b28c8428e.png

我是图示

void SListEraseAfter(SLTNode* pos)
{
  assert(pos);
  assert(pos->next);
  SLTNode* del = pos->next;
  pos = del->next;
  free(del);
  del = NULL;
}


2.9pos之前插入


如果pos指向第一个结点,就相当于前插

其他情况下,需要找到pos的前一个,再进行修改。

137c67d4e855134f9ffd33b178beec21_7cb2af4af62e46b885c2a17b9cbc2b21.png

我是图示


void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
  assert(pphead);
  assert(pos);
  if (pos == *pphead)
  {
    SListPushFront(pphead, x);
  }
  else
  {
    //查找pos前一个
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    SLTNode* newnode = BuySListNode(x);
    prev->next = newnode;
    newnode->next = pos;
  }
}


2.10删除pos结点


寻找pos的前一个,修改其指向,pos释放置空即可

6cfee5d7d5d021a1106abddad5af9cf9_16f28bb7888e41bfaee1a5d6175f995b.png

我是图示

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
  assert(pphead);
  assert(*pphead);
  assert(pos);
  //查找pos前一个
  SLTNode* prev = *pphead;
  while (prev->next != pos)
  {
    prev = prev->next;
  }
  prev->next = pos->next;
  free(pos);
  pos = NULL;
}

总结:

这篇博客介绍了最简单的链表 -- 单链表的概念和结构,并进行了相关功能的实现,第一次接触可能会难很消化,建议充分理解结构后进行实现呐~

目录
相关文章
|
19小时前
|
存储
【单链表】数据结构单链表的实现
【单链表】数据结构单链表的实现
|
19小时前
|
存储 缓存 算法
链表全景:探索单链表、双向链表与循环链表【实战演练】
链表全景:探索单链表、双向链表与循环链表【实战演练】
34 3
|
19小时前
leetcode:203. 移除链表元素(有哨兵位的单链表和无哨兵位的单链表)
leetcode:203. 移除链表元素(有哨兵位的单链表和无哨兵位的单链表)
20 0
|
19小时前
特殊链表(循环单链表,循环双链表,静态链表)
特殊链表(循环单链表,循环双链表,静态链表)
12 3
|
19小时前
|
存储
数据结构:4、链表之单链表
数据结构:4、链表之单链表
12 0
|
19小时前
|
存储
数据结构:图文详解单链表的各种操作(头插法,尾插法,任意位置插入,删除节点,查询节点,求链表的长度,清空链表)
数据结构:图文详解单链表的各种操作(头插法,尾插法,任意位置插入,删除节点,查询节点,求链表的长度,清空链表)
280 0
|
19小时前
|
缓存 算法 搜索推荐
【数据结构】链表(单链表与双链表实现+原理+源码)
【数据结构】链表(单链表与双链表实现+原理+源码)
|
19小时前
|
存储 C语言
C语言之单链表的实现以及链表的介绍
C语言之单链表的实现以及链表的介绍
|
19小时前
|
JavaScript 算法 前端开发
TypeScript算法专题 - blog2 - 单链表节点的索引、结点删除与链表反转
TypeScript算法专题 - blog2 - 单链表节点的索引、结点删除与链表反转
31 0
|
19小时前
数据结构实验之链表七:单链表中重复元素的删除
数据结构实验之链表七:单链表中重复元素的删除