数据结构入门指南:单链表(附源码)

简介: 数据结构入门指南:单链表(附源码)

前言

       前边关于链表的基础如果已经理解透彻,那么接下来就是对链表各功能的实现,同时也希望大家能把这部分内容熟练于心,这部分内容对有关链表部分的刷题很有帮助。废话不多讲我们步入正题。


       前边已经实现了头插、尾插的操作,今天主要的内容是:头删、尾删、位置删除、位置后删除、查找、位置前插入、位置后插入。

尾删

       要想进行尾删,就要先找到链表的尾。

       我们知道单链表的缺点之一就是只可以单向遍历,所以要想删除最后一个节点,就要先找到倒数第二个节点,然后释放掉最后一个节点,将倒数第二个节点的next(指针域)置为NULL。

具体代码实现:

void SLPopBlack(SLNode** pphead)
{
  assert(*pphead);
  if ((*pphead)->next == NULL)
  {
    free(*pphead);
    *pphead = NULL;
  }
  SLNode* tail = *pphead;
  while (tail->next->next)
  {
    tail = tail->next;
  }
  free(tail->next);
  tail->next = NULL;
}

       这里为什么要传二级指针?有人可能会有这样的疑惑, 传二级指针就是为了防止链表被删空的情况,在链表的许多情节中都要考虑像链表删空等这种极端情况,要做到面面俱到。

       当然我们也可以选择不使用二级指针,而是直接返回头指针的地址。但这样函数的类型就变成了结构体指针类型,而要调用这个函数还需要相同类型的结构体指针变量接收,这种情况在刷题中经常遇到,但在写链表时不推荐,这样写调用函数会比较麻烦。

头删

头删大家可以先思考一下,需不需要使用二级指针。

有这样一个链表:

        想要删除第一个节点,只需要把头指针指向的位置改为指向第二个节点。把头指针修改,这是修改结构体指针,到这里想必大家已经清楚,需要使用二级指针。

       接下来我们理一下删除的逻辑,直接将头指针指向第二个节点,这样就会造成第一个节点丢失没办法释放掉空间。如果先将第一个节点释放就会使第二个节点丢失,头指针无法连接剩余节点。

       这要怎么解决呢?这里就需要创建一个新的变量来存储一下第二个节点的地址,然后再将第一个节点释放。

具体代码实现:

void SLPopFront(SLNode** pphead)
{
  assert(*pphead);
  SLNode* newhead = (*pphead)->next;
  free(*pphead);
  *pphead = newhead;
}

查找

       查找很简单,顺序表的查找返回的是下标,而链表返回的是节点的地址,后续的操作也是比较简单,我就不再画逻辑图。

SLNode* SLFind(SLNode* phead, Datatype x)
{
  SLNode* cur = phead;
  while (cur)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}

位置前插入

       位置前插入,如果是在第一个节点位置前插入,就是头插,其次是要想在位置前插入就必须要知道前有个节点,单链表是无法逆向遍历的,所以要想知道前一个节点就必须要传头指针。然后将前一个节点的next置为新节点的地址,新节点的next置为pos位置节点的地址。

void SLFrontInsert(SLNode** pphead, SLNode* pos, Datatype x)
{
  assert(pos);
  if (pos == *pphead)
  {
    SLPushFront(pphead, x);
  }
  else
  {
    SLNode* prev = *pphead;
    SLNode* newnode = NewNode(x);
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    newnode->next = pos;
    prev->next = newnode;
  }
}

位置后插入

       位置后插入,可以不需要头指针。操作也非常简单,把pos位置的下一个节点赋给新节点的next,把新节点的地址赋给pos位置节点的next。这里有人可能会有疑惑,不考虑极端情况吗?位置后插入是无法进行头插的,如果链表为空,传进来pos就为空,就会直接保错,至于尾插,这段代码也是可以解决的。

void SLAfterInsert(SLNode* pos, Datatype x)
{
  assert(pos);
  SLNode* newnode = NewNode(x);
  newnode->next = pos->next;
  pos->next = newnode;
}

位置删除

       位置删除,需要把pos位置前一个节点的next置为pos位置下一个节点的地址,同时还需要将删除的节点释放空间。考虑极端情况,如果删除位置是第一个节点,这种方法就失效了,因为没有第一个节点的前一个节点,这时也就是头删,我们可以调用前边已经实现的函数接口。

void SLErase(SLNode** pphead, SLNode* pos) {
  assert(pphead);
  assert(pos);
  if (pos == *pphead)
  {
    SLPopFront(pphead);
  }
  else
  {
    SLNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    prev->next = pos->next;
    free(pos);
    pos = NULL;
  }
}

位置后删除

       位置后删除,如果位置为最后一个节点,就不需要删除,且位置后删除无法进行头删,然后是正常情况,把pos位置节点的next置为pos位置后第二个节点的地址,就完成了。那是否可以这样写呢?

pos->next = pos->next->next

答案是不可以,这样会造成pos后一个节点丢失,无法释放。所以这里我们需要分成两步来写:

void SLEraseAfter(SLNode* pos)
{
  assert(pos);
  if (pos->next == NULL)
  {
    return;
  }
  else
  {
    SLNode* posnext = pos->next;//不可以写成一步,否则pos后一个节点就会丢失,无法释放
    pos->next = posnext->next;
    free(posnext);
    posnext = NULL;
  }
}

链表销毁

       执行完所有操作后,就需要将链表销毁了

void SLDestory(SLNode** pphead)
{
  assert(pphead);
  SLNode* cur = *pphead;
  while (cur)
  {
    SLNode* next =cur->next;
    free(cur);
    cur = next;
  }
  *pphead = NULL;
}

完整代码:

SList.c

#include"SList.h"
void SLprint(SLNode* phead)
{
  SLNode* cur = phead;
  while (cur)
  {
    printf("%d->", cur->data);
    cur = cur->next;
  }
  printf("NULL\n");
}
SLNode* NewNode(Datatype x)
{
  SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
  if (newnode == NULL)
  {
    perror("malloc");
    exit(-1);
  }
  newnode->data = x;
  newnode->next = NULL;
  return newnode;
}
void SLPushBlack(SLNode** pphead, Datatype x)
{
  assert(pphead);
  SLNode* newnode = NewNode(x);
  SLNode* tail = *pphead;
  if (*pphead == NULL)
  {
    *pphead = newnode;
  }
  else
  {
    while (tail->next)
    {
      tail = tail->next;
    }
    tail->next = newnode;
  }
}
void SLPushFront(SLNode** pphead, Datatype x)
{
  assert(pphead);
  SLNode* newnode = NewNode(x);
  newnode->next = *pphead;
  *pphead = newnode;
}
void SLPopBlack(SLNode** pphead)
{
  assert(pphead);
  assert(*pphead);
  if ((*pphead)->next == NULL)
  {
    free(*pphead);
    *pphead = NULL;
  }
  SLNode* tail = *pphead;
  while (tail->next->next)
  {
    tail = tail->next;
  }
  free(tail->next);
  tail->next = NULL;
}
void SLPopFront(SLNode** pphead)
{
  assert(pphead);
  assert(*pphead);
  SLNode* newhead = (*pphead)->next;
  free(*pphead);
  *pphead = newhead;
}
SLNode* SLFind(SLNode* phead, Datatype x)
{
  SLNode* cur = phead;
  while (cur)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}
void SLFrontInsert(SLNode** pphead, SLNode* pos, Datatype x)
{
  assert(pphead);
  assert(pos);
  if (pos == *pphead)
  {
    SLPushFront(pphead, x);
  }
  else
  {
    SLNode* prev = *pphead;
    SLNode* newnode = NewNode(x);
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    newnode->next = pos;
    prev->next = newnode;
  }
}
void SLAfterInsert(SLNode* pos, Datatype x)
{
  assert(pos);
  SLNode* newnode = NewNode(x);
  newnode->next = pos->next;
  pos->next = newnode;
}
void SLErase(SLNode** pphead, SLNode* pos) {
  assert(pphead);
  assert(pos);
  if (pos == *pphead)
  {
    SLPopFront(pphead);
  }
  else
  {
    SLNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    prev->next = pos->next;
    free(pos);
    pos = NULL;
  }
}
void SLEraseAfter(SLNode* pos)
{
  assert(pos);
  if (pos->next == NULL)
  {
    return;
  }
  else
  {
    SLNode* posnext = pos->next;
    pos->next = posnext->next;
    free(posnext);
    posnext = NULL;
  }
}
void SLDestory(SLNode** pphead)
{
  assert(pphead);
  SLNode* cur = *pphead;
  while (cur)
  {
    SLNode* next =cur->next;
    free(cur);
    cur = next;
  }
  *pphead = NULL;
}

SList.h

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int Datatype;
typedef struct SLNode
{
  Datatype data;
  struct SLNode* next;
}SLNode;
//打印链表
void SLprint(SLNode* phead);
//创建新节点
SLNode* NewNode(Datatype x);
//尾插
void SLPushBlack(SLNode** phead, Datatype x);
//头插
void SLPushFront(SLNode** pphead, Datatype x);
//尾删
void SLPopBlack(SLNode** pphead);
//头删
void SLPopFront(SLNode** pphead);
//查找
SLNode* SLFind(SLNode* phead, Datatype x);
//pos位置前插入
void SLFrontInsert(SLNode** pphead, SLNode* pos, Datatype x);
//pos位置后插入
void SLAfterInsert(SLNode* pos, Datatype x);
//pos位置后删除
void SLEraseAfter(SLNode* pos);
//pos位置删除
void SLErase(SLNode** pphead, SLNode* pos);
void SLDestory(SLNode** pphead);

test.c

这里基本都是测试接口,没有什么太大的参考价值,代码如下,便于大家调试。

#include"SLNode.h"
void test1()
{
  SLNode* plist = NULL;
  int n = 0;
  printf("请输入链表的长度\n");
  scanf("%d", &n);
  printf("请输入数据\n");
  for (int i = 0; i < n; i++)
  {
    int val = 0;
    scanf("%d", &val);
    SLNode *newnode= NewNode(val);
    newnode->next = plist;
    plist = newnode;
  }
  SLNode* pos= SLFind(plist, 2);
  if (pos)
  {
    pos->data *= 10;
  }
  SLFrontInsert(&plist, pos, 10);
  SLprint(plist);
  SLPushBlack(&plist,100);
  SLprint(plist);
  SLPushFront(&plist, 200);
  SLprint(plist);
  SLPopBlack(&plist);
  SLprint(plist);
  SLPopFront(&plist);
  SLprint(plist);
}
void test2()
{
  SLNode* plist = NULL;
  SLPushBlack(&plist, 1);
  SLPushBlack(&plist, 2);
  SLPushBlack(&plist, 3);
  SLPushBlack(&plist, 4);
  SLPushBlack(&plist, 5);
  SLprint(plist);
  SLNode* pos = SLFind(plist, 5);
  SLAfterInsert(pos, 20);
  SLprint(plist);
  SLFrontInsert(&plist, pos, 10);
  SLprint(plist);
}
void test3()
{
  SLNode* plist = NULL;
  SLPushBlack(&plist, 1);
  SLPushBlack(&plist, 2);
  SLPushBlack(&plist, 3);
  SLPushBlack(&plist, 4);
  SLPushBlack(&plist, 5);
  SLprint(plist);
  SLNode* pos = SLFind(plist, 1);
  //SLErase(&plist, pos);
  SLEraseAfter(pos);
  SLprint(plist);
  SLDestory(&plist);
}
int main()
{
  test2();
  return 0;
}

总结

       好的,内容到这里就要结束了,这部分内容或许看来很繁琐,但在刷链表相关的题时就会惊奇的发现,题解都是这些操作的变形。熟练这部分内容,可以让你在刷链表相关的题时会感觉非常的爽,刷题也会更加顺利。最后,感谢阅读!

相关文章
|
2月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
244 9
|
2月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
111 16
|
2月前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
148 8
|
3月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
56 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
2月前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
119 4
|
2月前
|
存储 C语言
【数据结构】顺序表(c语言实现)(附源码)
本文介绍了线性表和顺序表的基本概念及其实现。线性表是一种有限序列,常见的线性表有顺序表、链表、栈、队列等。顺序表是一种基于连续内存地址存储数据的数据结构,其底层逻辑是数组。文章详细讲解了静态顺序表和动态顺序表的区别,并重点介绍了动态顺序表的实现,包括初始化、销毁、打印、增删查改等操作。最后,文章总结了顺序表的时间复杂度和局限性,并预告了后续关于链表的内容。
86 3
|
3月前
|
存储
[数据结构] -- 单链表
[数据结构] -- 单链表
29 1
|
3月前
|
Java C++ 索引
让星星⭐月亮告诉你,LinkedList和ArrayList底层数据结构及方法源码说明
`LinkedList` 和 `ArrayList` 是 Java 中两种常见的列表实现。`LinkedList` 基于双向链表,适合频繁的插入和删除操作,但按索引访问元素效率较低。`ArrayList` 基于动态数组,支持快速随机访问,但在中间位置插入或删除元素时性能较差。两者均实现了 `List` 接口,`LinkedList` 还额外实现了 `Deque` 接口,提供了更多队列操作。
32 3
|
3月前
|
存储
【数据结构】——单链表实现
【数据结构】——单链表实现
|
2月前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
79 0