数据结构------------线性表之链表(详细讲解)

简介: 数据结构------------线性表之链表(详细讲解)

一、链表是什么

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

下面我们用几种更容易理解的图片解释链表的物理结构和逻辑结构

tips:链表可分为8种

这里给出分法,自由组合即可

1.带头(哨兵位)链表/不带头链表

   注:头节点不存放任何数据,有些人可能说可以存放数据的数量阿,那么可以思考一个问题,如果我的链表存放数据是double,阁下又将如何应对。如果存放数据超过int的最大存储个数,阁下又当如何应对。

2.双向链表/单向链表

3.循环链表/非循环链表

本文将实现 单向不带头链表 和 双向循环带头链表(最常用)

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

2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都

是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带

来很多优势,实现反而简单了,后面我们代码实现了就知道了。(所以我们一定不要被复杂的数据结构吓到了,学就完了)

二、链表的接口代码实现

一.单向不带头链表

1.头文件的引入

#pragma once
#include <stdio.h>
#include <stdlib.h>

2. 链表元素的创建

typedef int SLTDataType;
struct SListNode
{
  int data;
  struct  SListNode* next;//指向下一个节点
};

3.开辟空间

由于头插尾插等操作多次使用,我们决定封装成一个函数来达到复用的效果。

SLTNode* BuySListNode(SLTDataType x)
{
  SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  newnode->data = x;
  newnode->next = NULL;
  return newnode;
}
typedef struct SListNode SLTNode;

4.各种接口的声明及实现

void SListPushBack(SLTNode** pphead, SLTDataType x);//尾插
void SListPushFront(SLTNode** pphead, SLTDataType x);//头插
void SListPrint(SLTNode* phead);
void SListPopFront(SLTNode** pphead);//头删
void SListPopBack(SLTNode** pphead);//尾删
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
void SListInsert(SLTNode** pphead,SLTNode* pos,SLTDataType x);//在pos的前面插入
void SListErase(SLTNode** pphead,SLTNode* pos);//删除pos位置的值
//在i前面插入x
// void SListInsert(SLTNode** head ,int i,SLTDataType x);
//删除i 位置的值
//  void SListErase(SLTNode** head ,int i);
//上述i表示下标
//
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));
  newnode->data = x;
  newnode->next = NULL;
  return newnode;
}
void SListPushBack(SLTNode** pphead, SLTDataType x)//也可以用引用来解决,*&,引用的意思是别名
{
  SLTNode* newnode = BuySListNode(x);
  if (*pphead == NULL)
  {
    *pphead = newnode;//形参是实参的临时拷贝,形参的改变不会影响实参
  }
  else
  {
    //找尾节点的指针
    SLTNode* tail = *pphead;
    while (tail->next != NULL)//phead是空指针,给tail也是空指针,再访问就会崩溃
    {
      tail = tail->next;
    }
    //尾节点链接新节点
    tail->next = newnode;
  }
}
//不会改变链表的头指针,传一级指针
//会改变链表的头指针,传二级指针
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
  SLTNode* newnode = BuySListNode(x);
  if (*pphead == NULL)//考虑头插如果只有一个节点
  {
    *pphead = newnode;
  }
  else 
  {
    newnode->next = *pphead;
    *pphead = newnode;
  }
}
void SListPopFront(SLTNode** pphead)//头删
{
  SLTNode* next = (*pphead)->next;
  free(*pphead);
  //如果直接一free直接找不到这个链表了
  *pphead = next;
}
void SListPopBack(SLTNode** pphead)
{
  //空
  //一个节点
  //一般情况
  if (*pphead == NULL)
  {
    return;
  }
  else if ((*pphead)->next==NULL)
  {
    free(*pphead);
    *pphead = NULL;
  }
  else
  {
    //找尾节点前一个的指针,再定义一个prev
    SLTNode* prev = NULL;
    SLTNode* tail = *pphead;
    while (tail->next != NULL)//phead是空指针,给tail也是空指针,再访问就会崩溃
    {
      prev = tail;//把自己的位置给到prev,自己在往下走
      tail = tail->next;
    }
    free(tail);
    prev->next = NULL;
  }
}
//寻找pos
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
  SLTNode* cur = phead;
  //while (cur != NULL)
  while (cur)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}
//在pos的前面插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
  if (pos == *pphead)
  {
    SListPushFront(pphead, x);
  }
  SLTNode* newnode = BuySListNode(x);
  SLTNode* prev = pphead;
  while(prev->next!=pos)
  {
    prev = prev->next;
  }
  prev->next = newnode;
  newnode->next = pos;
}
void SListErase(SLTNode** pphead,SLTNode* pos)
{
  if (pos == *pphead)
  {
    SListPopFront(pphead);
  }
  SLTNode* prev = pphead;
  while (prev->next != pos)
  {
    prev = prev->next;
  }
  prev->next = pos->next;
  free(pos);
}
//删除pos位置的值

这里注意一点,形参只是实参的临时拷贝,我们有两种选择方式,第一种,让函数的返回值为结构体指针,不然如果是无返回值的函数则无法真正的改变实参本身,所以我们传指针的地址来修改实参。

聪明的小伙伴可能又发现了,我们erase和insert就可以实现头插尾插的全部内容,如果要快速实现完整的链表,可千万不要一个一个实现奥,我们可以利用函数的复用实现更精简的代码效果。

一.双向带头循环链表

这里直接给出完整的代码

List.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
  struct ListNode* next;
  struct ListNode* prev;//前驱指针
  int data;
}ListNode;
ListNode* ListInit();
void ListDestory(ListNode* phead);
void ListPrint(ListNode* phead);
void ListPushBack(ListNode* phead,LTDataType x);
void ListPushFront(ListNode* phead, LTDataType x);
void ListPopBack(ListNode* phead);
void ListPopFront(ListNode* phead);
ListNode* ListFind(ListNode* phead, LTDataType x);
void ListInsert(ListNode* pos, LTDataType x);
void ListErase(ListNode* pos, LTDataType x);
bool ListEmpty(ListNode* phead);
int ListSize(ListNode* phead);

List.c

void ListPrint(ListNode* phead)
{
  ListNode* cur = phead->next;
  while (cur != phead)
  {
    printf("%d ", cur->data);
    cur = cur->next;
  }
  printf("\n");
}
ListNode* BuyListNode(LTDataType x)
{
  ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
  newnode->data = x;
  newnode->next = NULL;
  newnode->prev = NULL;
  return newnode;
}
ListNode* ListInit()//这个时候仍然存在形参改变不了实参的问题,所以我们使用返回结构体指针来解决这个问题
{
  ListNode* phead = BuyListNode(0);
  phead->next = phead;
  phead->prev = phead;
  return phead;
}
void ListDestory(ListNode* phead)
{
  assert(phead);
  ListNode* cur = phead->next;
  while (cur != phead)
  {
    ListNode* next = cur->next;
    free(cur);
    cur = next;
  }
  free(phead);
  phead = NULL;
}
void ListPushBack(ListNode* phead,LTDataType x)
{
  assert(phead);
  ListNode* tail=phead->prev;
  ListNode* newnode = BuyListNode(x);
  tail->next = newnode;
  newnode->prev = tail;
  newnode->next = phead;
  phead->prev = newnode;
}
void ListPushFront(ListNode* phead, LTDataType x)
{
  assert(phead);
  ListNode* first = phead->next;
  ListNode* newnode = BuyListNode(x);
  //接下来就是三个节点的链接
  phead->next = newnode;
  newnode->next = first;
  newnode->prev = phead;
  first->prev = newnode;
}
void ListPopFront(ListNode* phead)
{
  assert(phead);
  assert(phead->next != phead);//防止只剩一个哨兵节点也删掉
  ListNode* first = phead->next;
  ListNode* second = first->next;
  phead->next = second;
  second->prev = phead;
  free(first);
  first = NULL;
}
void ListPopBack(ListNode* phead)
{
  assert(phead);
  assert(phead->next != phead);//防止只剩一个哨兵节点也删掉
  ListNode* tail = phead->prev;
  ListNode* prev = tail->prev;
  prev->next = phead;
  phead->prev = prev;
  free(tail);
  tail = NULL;
}
ListNode* ListFind(ListNode* phead, LTDataType x)
{
  assert(phead);
  ListNode* cur = phead->next;
  while (cur != phead)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}
void ListInsert(ListNode* pos, LTDataType x)
{
  assert(pos);
  ListNode* prev = pos->prev;
  ListNode* newnode = BuyListNode(x);
  prev->next = newnode;
  newnode->next = pos;
  pos->prev = newnode;
  newnode->prev = prev;
}
void ListErase(ListNode* pos, LTDataType x)
{
  assert(pos);
  ListNode* prev = pos->prev;
  ListNode* next = pos->next;
  prev->next = next;
  next->prev = prev;
  free(pos);
  pos = NULL;
}

有人可能问了,这里为啥头插尾插可以只传一级指针呢?因为这里有哨兵位置,就不必修改传过来的指针了,通过哨兵位去修改即可。

附上小题几道,将在下期讲解。

1.反转链表

2.链表的中间节点

3.环形链表

4.合并两个有序链表

总结

以上就是今天要讲的内容,本文仅仅介绍了链表的两种,剩下的链表结构小伙伴们可以自由探索,最后附上的力扣真题兄弟们可以尝试尝试,如果本篇文章对你的数据结构有帮助,能不能点个免费的三连呢,这对博主的创作能起到极大的助力,最后愿诸君都能学有所成。

相关文章
|
8月前
|
存储 算法 Perl
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
134 4
|
5月前
|
存储 机器学习/深度学习 算法
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
147 30
|
5月前
|
存储 算法 C语言
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
210 25
|
6月前
|
存储 算法 测试技术
【C++数据结构——线性表】求集合的并、交和差运算(头歌实践教学平台习题)【合集】
本任务要求编写程序求两个集合的并集、交集和差集。主要内容包括: 1. **单链表表示集合**:使用单链表存储集合元素,确保元素唯一且无序。 2. **求并集**:遍历两个集合,将所有不同元素加入新链表。 3. **求交集**:遍历集合A,检查元素是否在集合B中存在,若存在则加入结果链表。 4. **求差集**:遍历集合A,检查元素是否不在集合B中,若满足条件则加入结果链表。 通过C++代码实现上述操作,并提供测试用例验证结果。测试输入为两个集合的元素,输出为有序集合A、B,以及它们的并集、交集和差集。 示例测试输入: ``` a c e f a b d e h i ``` 预期输出:
182 7
|
6月前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
277 5
|
6月前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】顺序表的基本运算(头歌实践教学平台习题)【合集】
本文档介绍了线性表的基本运算任务,涵盖顺序表和链表的初始化、销毁、判定是否为空、求长度、输出、查找元素、插入和删除元素等内容。通过C++代码示例详细展示了每一步骤的具体实现方法,并提供了测试说明和通关代码。 主要内容包括: - **任务描述**:实现顺序表的基本运算。 - **相关知识**:介绍线性表的基本概念及操作,如初始化、销毁、判定是否为空表等。 - **具体操作**:详述顺序表和链表的初始化、求长度、输出、查找、插入和删除元素的方法,并附有代码示例。 - **测试说明**:提供测试输入和预期输出,确保代码正确性。 - **通关代码**:给出完整的C++代码实现,帮助完成任务。 文档
164 5
|
7月前
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
8月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
195 5
|
8月前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
180 0
|
8月前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
249 0