【数据结构与算法】单向链表的实现(上)

简介: 【数据结构与算法】单向链表的实现(上)

👉链表的引入👈


在上一篇博客中,我们已经讲到了顺序表。那现在再来总结一下顺序表的相关问题。


顺序表的问题及思考


问题:

  1. 中间/头部的插入删除,时间复杂度为O(N)
  2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  3. 增容一般是呈 2 倍的增长,势必会有一定的空间浪费。例如当前容量为 100,满了以后增容到 200,我们再继续插入了 5 个数据,后面没有数据插入了,那么就浪费了 95 个数据空间。

思考:如何解决以上问题呢?下面给出了链表的结构来看看。


👉链表👈


链表的概念及结构


概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表

中的指针链接次序实现的 。

2c27fd0bc689467298664410a98de3fb.png6e702c48e7cb48b99848b8fe4dbecb5d.png

物理结构:内存中实际的存储结构。

836d2a4ee179497598cd62d94fa6e0c9.png

逻辑结构:想象出来的存储结构。

2f49a0f906dc432495c7531559580956.png

a9c9e1f379fd407b97b1396db5cb6b23.png


链表的分类


实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

  1. 单向或者双向
  2. af820a540f264c82b7e43ca864e75c11.png
  3. 带头或者不带头
  4. c92b83f34e564673adfa28566bbd417a.png
  5. 循环或者非循环
  6. 99695cdfa32a46f283e85ef180352c96.png

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

a502655698e34c18b2d9b494da20a78d.png

无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。


👉单向链表的实现👈


链表和顺序表一样,都要实现增删查改的功能。链表需要实现的函数接口有:申请节点、打印链表、销毁链表、头插数据、尾插数据、尾删数据、头删数据、查找数据、在pos位置之前插入数据、在pos位置之后插入数据、删除pos位置的数据和删除pos位置之后的数据。由于要实现的函数接口比较多,所以我们还是需要采取三个模块的方式来写代码。SList.h源文件里面是头文件的包含、结构体的声明、类型的重命名以及函数接口的声明。SList.c源文件里面是函数接口的实现。Test.c源文件用来测试我们实现的函数接口是否正确。


现在来看一下每个模块的代码。


SList.h


#pragma once
#include<stdio.h>
#include<assert.h>
#include <stdlib.h>
typedef int SLTDataType;
typedef struct SListNode
{
  SLTDataType data;
  struct SListNode* next;
}SLTNode;
// 申请节点
SLTNode* BuySLTNode(SLTDataType x);
// 打印链表
void SListPrint(SLTNode* phead);
// 销毁链表
void SListDestory(SLTNode** pphead);
// 头插数据
void SListPushFront(SLTNode** pphead, SLTDataType x);
// 尾插数据
void SListPushBack(SLTNode** pphead, SLTDataType x);
// 尾删数据
void SListPopBack(SLTNode** pphead);
// 头删数据
void SListPopFront(SLTNode** pphead);
// 查找数据
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
// 在pos位置之前插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// 在pos位置之后插入
void SListInsertAfter(SLTNode* pos, SLTDataType x);
// 删除pos位置的数据
void SListErase(SLTNode** pphead, SLTNode* pos);
// 删除pos位置后面的数据
void SListEraseAfter(SLTNode* pos);


链表与顺序表的函数接口大多数是相同的,与顺序表不同的是,链表没有初始化的函数接口。那为什么链表不需要初始化呢?是因为我们只需要拿一个SLTNode*指针就能管理整个链表。


还有一点就是,除了打印链表和查找数据的函数接口的参数是一级指针,其它的函数接口的参数都是二级指针。这又是为什么呢?其实是因为打印链表和查找数据的函数不需要改变头节点,而其它函数有可能要改变头节点。这个知识点在接下来的内容将会跟大家讲解。

SList.c

#include "SList.h"
// 申请节点
SLTNode* BuySLTNode(SLTDataType x)
{
  SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  if (newnode == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  newnode->data = x;
  newnode->next = NULL;
  return newnode;
}
// 打印链表
void SListPrint(SLTNode* phead)
{
  //assert(phead);
  SLTNode* cur = phead;
  while (cur)
  {
    printf("%d->", cur->data);
    cur = cur->next;
  }
  printf("NULL\n");
}
// 销毁链表
void SListDestory(SLTNode** pphead)
{
  assert(pphead);
  SLTNode* cur = *pphead;
  while (cur)
  {
    SLTNode* next = cur->next;
    free(cur);
    cur = next;
  }
  *pphead = NULL;
}
// 头插数据
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
  assert(pphead);
  SLTNode* newnode = BuySLTNode(x);
  newnode->next = *pphead;
  *pphead = newnode;
}
// 尾插数据
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
  assert(pphead);
  SLTNode* newnode = BuySLTNode(x);
  // 链表为空
  if (*pphead == NULL)
  {
    *pphead = newnode;
  }
  else
  {
    // 找尾节点
    SLTNode* tail = *pphead;
    while (tail->next)
    {
      tail = tail->next;
    }
    tail->next = newnode;
  }
}
// 尾删数据
void SListPopBack(SLTNode** pphead)
{
  assert(pphead);
  assert(*pphead);// 判断是否为空链表
  // 只有一个节点
  if ((*pphead)->next == NULL)
  {
    free(*pphead);
    *pphead = NULL;
  }
  else
  {
    SLTNode* tail = *pphead;
    SLTNode* prev = NULL;
    while (tail->next)
    {
      prev = tail;
      tail = tail->next;
    }
    free(tail);
    tail = NULL;
    prev->next = NULL;
    //SLTNode* tail = *pphead;
    //while (tail->next->next)
    //{
    //  tail = tail->next;
    //}
    //free(tail->next);
    //tail->next = NULL;
  }
}
// 头删数据
void SListPopFront(SLTNode** pphead)
{
  assert(pphead);
  assert(*pphead); // 判断是否为空链表
  SLTNode* newHead = (*pphead)->next; // 新的头节点
  free(*pphead); // 释放旧的头节点
  *pphead = newHead;
}
// 查找数据
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
  SLTNode* cur = phead;
  // 循环遍历链表
  while (cur)
  {
    if (cur->data == x)
    {
      return cur; // 找到了
    }
    cur = cur->next; // 继续往后找
  }
  return NULL; // 没找到
}
// 在pos位置之前插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
  assert(pphead);
  assert(pos);
  // 头插
  if (*pphead == pos)
  {
    SListPushFront(pphead, x);
  }
  else
  {
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
      assert(prev);
      // 暴力检查,如果prev为空,那么就说明pos不在链表中,参数pos传错了
    }
    SLTNode* newnode = BuySLTNode(x);
    prev->next = newnode;
    newnode->next = pos;
  }
}
// 在pos位置之后插入
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
  assert(pos);
  SLTNode* newnode = BuySLTNode(x);
  newnode->next = pos->next;
  pos->next = newnode;
}
// 删除pos位置的数据
void SListErase(SLTNode** pphead, SLTNode* pos)
{
  assert(pphead);
  assert(pos);
  // 头删数据
  if (*pphead == pos)
  {
    SListPopFront(pphead);
  }
  else
  {
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
      assert(prev);
      // 暴力检查,如果prev为空,那么就说明pos不在链表中,参数pos传错了
    }
    prev->next = pos->next;
    free(pos);
  }
}
// 删除pos位置后面的数据
void SListEraseAfter(SLTNode* pos)
{
  assert(pos);
  // pos位置执行NULL
  if (pos->next == NULL)
  {
    return;
  }
  else
  {
    SLTNode* next = pos->next;
    pos->next = next->next;
    free(next);
  }
}


以上就是SList.c源文件实现的函数接口,大家可以先看一下,在下面再来详细讲解每一个函数接口的实现。


申请节点


用来存储数据的节点是在插入数据时,一个一个地向堆区申请空间的。如果申请节点失败,那就直接结束程序,没有必要继续往下执行代码了。如果申请节点成功,那么newnode->data = x, newnode->next = NULL,最后将newnode的值返回。


// 申请节点
SLTNode* BuySLTNode(SLTDataType x)
{
  SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  if (newnode == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  newnode->data = x;
  newnode->next = NULL;
  return newnode;
}


打印链表


ae26f443a09b42b58b212c41a3d73acf.png

打印链表就是利用while循环将整个链表的数据打印出来。因为每个节点都存储着数据和下一个节点的地址,所以我们可以通过该地址找到下一个节点,依次类推就能遍历整个链表了。所以需要定义一个指针SLTNode* cur = head,当cur = NULL时,遍历链表结束。


// 打印链表
void SListPrint(SLTNode* phead)
{
  //assert(phead); //不需要断言phead
  SLTNode* cur = phead;
  while (cur)
  {
    printf("%d->", cur->data);
    cur = cur->next;
  }
  printf("NULL\n");
}

销毁链表


可以发现销毁链表函数的参数是二级指针SLTNode** pphead,即头指针SLTNode* plist的地址,该地址是不可能为空指针NULL的,所以要对pphead进行断言。销毁链表后,plist的值要置为NULL。如果函数的参数是一级指针SLNode* phead的话,将无法将plist的值置为NULL。因为形参只是实参的一份临时拷贝,对形参的修改不会影响实参。



如果还是不能理解的话,请看下面的例子:


#include <stdio.h>
//交换x、y的值
void Swap1(int* px, int* py)
{
  int tmp = *px;
  *px = *py;
  *py = tmp;
}
//交换px、py的值
void Swap2(int** ppx, int** ppy)
{
  int* tmp = *ppx;
  *ppx = *ppy;
  *ppy = tmp;
}
int main()
{
  int x = 10;
  int y = 20;
  int* px = &x;
  int* py = &y;
  printf("x:%d y:%d\n", x, y);
  Swap1(&x, &y);
  printf("x:%d y:%d\n", x, y);
  printf("px:%p py:%p\n", px, py);
  Swap2(&px, &py);
  printf("px:%p py:%p\n", px, py);
  return 0;
}

8bb5f2ca21484c49bd660ee3dbc14a9a.png


从上面的例子可以看出,如果想要改变int类型变量的值,函数的参数就要设置为int*;如果要改变int*类型变量的值,函数的参数就要设置为int**。因为插入和删除数据都有可能影响头节点,所以要传二级指针SLTNode** pphead。只有传二级指针SLTNode** pphead,才能够改变头指针SLTNode* plist的值。


// 销毁链表
void SListDestory(SLTNode** pphead)
{
  assert(pphead);
  SLTNode* cur = *pphead;
  while (cur)
  {
    SLTNode* next = cur->next;
    free(cur);
    cur = next;
  }
  *pphead = NULL;
}














相关文章
|
2月前
|
算法
【❤️算法笔记❤️】-每日一刷-19、删除链表的倒数第 N个结点
【❤️算法笔记❤️】-每日一刷-19、删除链表的倒数第 N个结点
69 1
|
2月前
|
算法 索引
❤️算法笔记❤️-(每日一刷-141、环形链表)
❤️算法笔记❤️-(每日一刷-141、环形链表)
50 0
|
2月前
|
算法
【❤️算法笔记❤️】-(每日一刷-876、单链表的中点)
【❤️算法笔记❤️】-(每日一刷-876、单链表的中点)
44 0
|
2月前
|
算法
【❤️算法笔记❤️】-每日一刷-23、合并 K 个升序链表
【❤️算法笔记❤️】-每日一刷-23、合并 K 个升序链表
32 0
|
2月前
|
存储 算法
【❤️算法笔记❤️】-每日一刷-21、合并两个有序链表
【❤️算法笔记❤️】-每日一刷-21、合并两个有序链表
102 0
|
13天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
32 5
|
1月前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
62 4
|
1月前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
2月前
|
存储 缓存 算法
经典算法之链表篇(三)
经典算法之链表篇(三)