【数据结构】链表 (4000+字超级详细 图文结合)C语言

简介: 链表是多个节点通过结构体的next指针实现逻辑结构的链接次序的,我们通常表示链表是用一个指针进行表示的,这个指针指向的就是链表的头结点。通过这个头结点地址,我们可以实现对链表的遍历等其它操作。

ઇଓ 欢迎来阅读子豪的文章,大家有什么宝贵的意见或建议可以在留言区留言


☾ ⋆ 如果你喜欢我的文章,欢迎 点赞 关注 收藏


ღღ 我的码云仓库:补集王子 (YZH_skr) - Gitee.com


❣ฅ 不要偷偷拿走我的小火车哦~嘿嘿


33c00c59d5ba4e7288e08d14ab09d219.gif


顺序表对比


以前学习了的顺序表


优点


动态物理空间下标连续存放访问


缺点


1,空间不够,要扩容,扩容有一定的内存消耗,其次一般扩容是扩二倍,会存在一定的空间浪费


2.头部或中间插入效率低(要挪动数据)


1e0e3620afb54311826b9044ea9c09d6.png


改善方案:        链表


1.按需申请释放空间


2.头部或者中间插入删除就不需要挪动数据(新增然后去掉原来的)


跟这个小火车类似


3688bc9af676469aa91a0810c9ae2513.png


单链表链表很重要


链表笔试面试题基本都是单链表结构,单链表会成为以后更复杂数据结构子结构(哈希桶,图)


下图中,每个方框代表一个结点,箭头代表链接next关系


1的next存2的地址,2的next存3的地址,以此类推


d527fb87d78d4a48b35de0b028cfce68.png

96fa3409e53b4256b6df890b154096bf.png


习惯


一般需要遍历的时候就设置cur(指向当前位置)指针


单独弄个指针,不让原来的指针动(cur )


01e863ed3bde4df993ba1119c75c205f.png

01e863ed3bde4df993ba1119c75c205f.png


循环一直更新cur直到遇到NULL


while (cur != NULL)
  {
    cur = cur->next;
  }


难点:


cur = cur->next        指针 = 指针里的next所指向的值        (更新指针)


基础链表操作


打印


void SListPrint(SLTNode* phead)
{
  SLTNode* cur = phead;
  while (cur != NULL)
  {
    printf("%d->", cur->data);
    cur = cur->next;
  }
  printf("NULL\n");
}


开辟新结点


SLTNode* BuySListNode(SLTdataType x) 
{
  SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  assert(newnode);
  newnode->data = x;
  newnode->next = NULL;
  return newnode;
}


链表增删查改


其实只需要懂下面两张图就能轻松理解增删查改的步骤了


c5dfdcfc92924a22b48dcd8d0514ef93.png


尾插


void SListPushBack(SLTNode** pphead, SLTdataType x) // 尾插 只要改了第一个结点就得用二级指针 传地址
{
  SLTNode* newnode = BuySListNode(x);
  if (*pphead == NULL)
  {
    *pphead = newnode;
  }
  else
  {
    // 找尾节点
    SLTNode* tail = *pphead;
    while (tail->next != NULL)
    {
      tail = tail->next;
    }
    tail->next = newnode;
  }
}


现在只有头结点的地址(phead)


尾巴要不空就是最后一个


判断 while (tail->next!=NULL)


把新的地值給最后一个然后把自己变为空


这里出现问题 plist是实参 phead 是形参 形参改变是对实参无影响的        原函数plist还是空


所以在可能会改变头接点的情况下,都要传址调用


链表是多个节点通过结构体的next指针实现逻辑结构的链接次序的,我们通常表示链表是用一个指针进行表示的,这个指针指向的就是链表的头结点。通过这个头结点地址,我们可以实现对链表的遍历等其它操作。


以尾插为例,这里我们在链表的最后一个节点的后面插入一个节点,我们并不影响plist和phead的指向 这个时候一级指针就可以实现


但如果我现在是空链表想插入一个节点,我这里就需要改变plist的指向,但是如果是传值的话,我们只能修改实参的拷贝phead的指向,而不能真正操作修改plist的指向。我们调用完尾插的函数之后,我们的plist还是指向null,这块就有问题了


3d28aba2b4d2404e9ac80f8a89a2dc7d.png


指针的地址就是二级指针


61fe4bb6bf3b4dcb9ef3cdc6197723d8.png


pphead是存的plist的地址 想改plist就要对pphead解引用


头插


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

3dc1c35223044092863a736bbb57b9f0.png


960734989a9745dba7e2cda6606fc756.png


若头为空,亦是如此插


头删


void SListPopFront(SLTNode** pphead)
{
  assert(*pphead != NULL);
  //if (*pphead == NULL)
  //  return;
  SLTNode* next = (*pphead)->next;
  free(*pphead);
  *pphead = next;
}


必须要先存一下头结点信息,不然free掉了就找不到下一个指针了


5f62b11cabe247d8905cb9baa118f852.png


传二级问题


要改变plist(头结点),就要传plist的地址,需要传二级指针


处理空指针 或者 删成空指针还在删的问题


指针检查(断言debug版本才有 release会忽略)


c0efddfe25064cf7bce9bc5a7d1df3c6.png


尾删


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


直接找尾删掉不行


这样倒数第二个就成了野指针


把尾部的前一个给他记录一下,设置一个prev指针指向cur的前一个结点


a73dc095fcf045f3971afcef948cfa25.png


tail走一下prev走一下


b8035d2fafa9475c8e2f6b23f0211fa4.png


删到只有一个结点的时候要单独考虑一下


if ((*pphead)->next == NULL)
  {
    free(*pphead);
    *pphead = NULL;
    }


查找(返回此结点)


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(pos);
  assert(pphead);
  // 头插
  if (pos == *pphead)
  {
    SListPushFront(pphead, x);
  }
  else
  {
    SLTNode* prev = *pphead;  //记录前面一个结点
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    SLTNode* newnode = BuySListNode(x); //插入
    prev->next = newnode;
    newnode->next = pos;
  }
}


删除pos位置结点


// 删除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;
    }
    prev->next = pos->next;   //删除
    free(pos);          //释放指针
    pos = NULL;
  }
}


插入pos后


要注意顺序 ,要先指向后一个再,让pos的next指向newnode (删除)


还有一种就是用一个指针标识他,就不在乎链接顺序


951ab68cdadd4857982b4a50ed55d1c9.png


// 单链表在pos位置之后插入x
void SListInsertAfter(SLTNode* pos, SLTdataType x)
{
  assert(pos);
  //SLTNode* newnode = BuySListNode(x); 先指向下一个
  //newnode->next = pos->next;      再链接 删除原来那根线
  //pos->next = newnode;          不能反
  // 不在乎链接顺序
  SLTNode* newnode = BuySListNode(x);
  SLTNode* next = pos->next;  // 记录下一个结点
  // pos newnode next
  pos->next = newnode; 
  newnode->next = next;
}


删除pos后的结点


// 删除pos后面的接点
void SListEraseAfter(SLTNode* pos)
{
  assert(pos);
  if (pos->next == NULL)
    return;
  SLTNode* del = pos->next;
  //pos->next = pos->next->next;
  pos->next = del->next;
  free(del);
  del = NULL;
}


总结


一:指针检查


当要对指针进行修改操作时应该考虑空指针野指针问题应该对指针进行检查


1. 温柔的检查(if判断)


if (*pphead == NULL)
        return;


2. 暴力的检查(断言)


assert ( *pphead)


二:释放结点(free)


当结点不用了,(next线断了应该进行释放)


free(cur);


cur指向空间的内容已经被释放了 但是可以对cur重新赋值 改变cur的指向


所以对指针先free,再赋值


总之学习还是得多画图,凭空想象很困难


数据结构链表这一块知识内容毋庸置疑非常重要


学习起来肯定有一定的难度,但是不要怕


第一遍不会就看第二遍,软磨硬泡必须给他拿下!


如果你喜欢我的文章,别忘了 素质三连:关注,收藏,点赞!

相关文章
|
21天前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
33 1
|
1月前
|
存储 算法 搜索推荐
【趣学C语言和数据结构100例】91-95
本文涵盖多个经典算法问题的C语言实现,包括堆排序、归并排序、从长整型变量中提取偶数位数、工人信息排序及无向图是否为树的判断。通过这些问题,读者可以深入了解排序算法、数据处理方法和图论基础知识,提升编程能力和算法理解。
45 4
|
1月前
|
存储 机器学习/深度学习 搜索推荐
【趣学C语言和数据结构100例】86-90
本文介绍并用C语言实现了五种经典排序算法:直接插入排序、折半插入排序、冒泡排序、快速排序和简单选择排序。每种算法都有其特点和适用场景,如直接插入排序适合小规模或基本有序的数据,快速排序则适用于大规模数据集,具有较高的效率。通过学习这些算法,读者可以加深对数据结构和算法设计的理解,提升解决实际问题的能力。
43 4
|
1月前
|
存储 算法 数据处理
【趣学C语言和数据结构100例】81-85
本文介绍了五个经典算法问题及其C语言实现,涵盖图论与树结构的基础知识。包括使用BFS求解单源最短路径、统计有向图中入度或出度为0的点数、统计无向无权图各顶点的度、折半查找及二叉排序树的查找。这些算法不仅理论意义重大,且在实际应用中极为广泛,有助于提升编程能力和数据结构理解。
38 4
|
1月前
|
算法 数据可视化 数据建模
【趣学C语言和数据结构100例】76-80
本文介绍了五种图论算法的C语言实现,涵盖二叉树的层次遍历及广度优先搜索(BFS)和深度优先搜索(DFS)的邻接表与邻接矩阵实现。层次遍历使用队列按层访问二叉树节点;BFS利用队列从源节点逐层遍历图节点,适用于最短路径等问题;DFS通过递归或栈深入图的分支,适合拓扑排序等场景。这些算法是数据结构和算法学习的基础,对提升编程能力和解决实际问题至关重要。
46 4
|
1月前
|
存储 算法 vr&ar
【趣学C语言和数据结构100例】71-75
本文介绍了五个C语言数据结构问题及其实现,涵盖链表与二叉树操作,包括按奇偶分解链表、交换二叉树左右子树、查找节点的双亲节点、计算二叉树深度及求最大关键值。通过递归和遍历等方法,解决了理论与实际应用中的常见问题,有助于提升编程能力和数据结构理解。
37 4
|
1月前
|
存储 算法 C语言
【趣学C语言和数据结构100例】66-70
本书《趣学C语言和数据结构100例》精选了5个典型的数据结构问题及C语言实现,涵盖链表与数组操作,如有序集合的集合运算、有序序列表的合并、数组中两顺序表位置互换、三递增序列公共元素查找及奇偶数重排。通过详细解析与代码示例,帮助读者深入理解数据结构与算法设计的核心思想,提升编程技能。
32 4
|
1月前
|
存储 算法 C语言
【趣学C语言和数据结构100例】51-55
本文介绍了五个关于链表操作的C语言实现案例,包括删除单链表中的重复元素、从两个有序链表中查找公共元素、判断一个链表是否为另一链表的连续子序列、判断循环双链表是否对称及合并两个循环单链表。每个案例都详细解析了算法思路与实现方法,涵盖了链表操作的多种场景,旨在帮助读者深入理解链表数据结构的应用,提升算法设计与编程能力。
39 4
|
1月前
|
存储 算法 C语言
【趣学C语言和数据结构100例】26-30
本文精选五个编程问题,涵盖递归、数字处理、字符串操作、组合数学和数论等领域,通过C语言实现,旨在提升编程能力和算法理解。包括递归逆序打印字符、正整数位数及逆序打印、回文数判断、0-7组成奇数个数计算及偶数分解为两素数之和。
38 4
【趣学C语言和数据结构100例】26-30
|
19天前
|
存储 算法 C语言
【C语言】深入浅出:C语言链表的全面解析
链表是一种重要的基础数据结构,适用于频繁的插入和删除操作。通过本篇详细讲解了单链表、双向链表和循环链表的概念和实现,以及各类常用操作的示例代码。掌握链表的使用对于理解更复杂的数据结构和算法具有重要意义。
144 6