【数据结构】链表的学习和介绍

简介: 【数据结构】链表的学习和介绍

前言

今天,我们来学习,数据结构中的链表

链表是什么

链表,就是多个结构体变量之间,通过结构体指针连接在一起的一种数据结构

提示:

本篇文章主要讲解动态链表,对于静态链表不做过多介绍

链表的分类

链表可分为静态链表动态链表

静态链表

只是初步了解,更详细的操作(比如插入节点、删除节点)在这篇文章中不做说明

静态链表,实际上就是一个结构体数组

分配一整片连续的内存空间,各个结构体变量集中安置,逻辑结构上相邻的数据元素,存储在指定的一块内存空间中,数据元素只允许在这块内存空间中随机存放,这样的存储结构生成的链表称为静态链表。也就是说静态链表是用数组来实现链式存储结构

下面,给出一段例子

这就是静态链表的定义和初始化

#include<iostream>
using namespace std;
struct Node
{
  int data;//数据域
  int next;//指针域
  //注释1
  //静态链表的节点是索引值 不是结构体指针
};
int main()
{
  struct Node n[10];
  for (int i = 0; i < 10; i++)
  {
    n[i].data = i;
    n[i].next = i + 1;//每个节点的下一个节点位当前索引值+1
  }
  n[9].next = -1;
  //注释2
  return 0;
}

注释1:

相信大家都知道,链表这种数据结构,前面一部分存储的是数据,后面一部分存储的是指向下一个变量(一般称为节点)的指针

存储数据的区域,就称为数据域

存储指针的区域,就称为指针域

注释2:

n[9].next = -1;

这行代码的意思是:将这个链表的最后一个节点 的指针(索引值)赋为-1

为什么要这么做呢?

首先:

当我们已经遍历到最后一个节点时,没有下一个节点了,那么此时的next指针指向的就是一个随机值,这可能会导致发生不可预期的行为,比如程序崩溃

其次,将最后一个节点的指针赋为-1,有便于我们检查对于链表的遍历是否结束,这一点在后面会详细说明

最后,当我们在处理链表的最后一个字节时,可以通过-1来直接找到它

静态链表的特点

静态链表需要预先开辟一块空间来存储,并且,静态链表的大小是固定的,(毕竟它没用到动态内存分配 没法变大),以及,静态链表在存储时,在内存中是连续存储的

静态链表也有一些好处,比如:

静态链表可以直接通过索引访问,不需要指针(好像这二者差不多)

动态链表

接下来,就进入到我们这篇文章的重头戏:动态链表了

要实现动态链表,有两个东西比较重要:

动态内存的申请,是动态链表之所以被称为动态链表的原因

模块化设计,可以使代码可读性和简洁性提升(当然,都写在一起也不是不行…)

下文为了便于理解,对于数据域只使用一个int类型的变量

动态链表的实现思路

本篇文章将动态链表的实现分为五个步骤,

1.创建链表表头
2.创建节点
3.插入节点
4.删除节点
5.打印链表

前四步比较关键,最后一步一般用于测试自己创建的链表是否符合自己的预期

创建链表表头

要创建链表,我们首先需要创建一个表头,来指向整个链表,可以将表头理解为第一个节点,

想指向一个结构体变量(也就是节点),那表头应该是结构体指针c

问题来了,如何把一个结构体指针,变成结构体变量呢

这时候,我们就要学习动态内存申请,即malloc函数

(注意:使用new也可以,会在本节的末尾附上使用new创建动态链表的代码)

给出一个示例来说明如何创建头节点

struct Node* createList()
{
  struct Node* headNode = (struct Node*)malloc
  (sizeof(struct Node));
  //
  //用malloc申请内存
  headNode->next = nullptr;
  //注释3
  return headNode;
}

注释3

有的同学可能会有疑问,为什么要将头节点初始化为空指针

首先,现在链表里只有一个变量(节点),不初始化成空指针,那不就变成野指针了

其次,初始化成空指针的话,当我们后续无论是查找头节点还是继续插入新的节点的时候,都是能很轻松的找到头节点

创建节点(一个)
/*

创建节点的这个函数

/*/

下面给出一个例子:

将创建节点的内容模块化城createNode函数

struct Node* createNode(int data)
{
  struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
  newNode->data = data;
  //传入数据
  newNode->next = nullptr;
  //注释4
  return newNode;
}

注释4:

类似的,在创建新的节点时,将next赋值为空指针,

是因为这个新节点并不知道链表中下一个节点是什么,所以只能赋值为空指针

插入节点

对于插入,有三种方式:头插法,尾插法,指定位置插入

下面给出一个使用头插法例子:

将创建节点的内容模块化城insertNodeByHead函数

(代码中给出了相应的注释)

void insertNodeByHead(struct Node* headNode, int data)
{
  struct Node* newNode = createNode(data);
  //创建插入的节点
  newNode->next = headNode->next;
  //修改指针域
  headNode->next = newNode;
  //指向要插入的数据
}

注释5:

在编写创建节点和插入节点的时候,我感觉这两步有一些联系,在此说明一下:

这两个步骤通常是相互关联的,因为通常我们需要先创建一个新的节点,然后将其插入到链表中

删除节点

下面给出一个例子:

将创建节点的内容模块化城deleteNodeByAppoin函数

(代码中给出了相应的注释)

//删除节点
void deleteNodeByAppoin(struct Node* headNode, int posData)
//headNode只是一个传入的参数名,不是真的头节点 不要弄混
//posData是要删除的数据
{
  struct Node* posNode = headNode->next;
  struct Node* posNodeFront = headNode;
  //分别对于链表是否为空和posData是否存在进行判断
  if (posNode == nullptr)
  {
    cout << "链表为空 操作错误" << endl;
  }
  else
  {
    while (posNode->data != posData)
    //遍历查找
    {
      posNodeFront = posNode;
      posNode = posNodeFront->next;
      if (posNode == nullptr)
      {
        cout << "未找到要删除的数据" << endl;
        return;
      }
    }
    posNodeFront->next = posNode->next;
    //用free函数释放内存
    free(posNode);
  }
}
遍历打印链表

通过遍历打印链表

void PrintList(struct Node* headNode)
{
  struct Node* pMove = headNode->next;
  while (pMove)
  //当pmove不为空指针时 持续遍历
  //这里就可以联系到创建节点时,为什么要将next初始化为空指针
  //巧妙 妙
  //while这里还能这么写 学到了 学到了
  {
    cout << pMove->data;
    pMove = pMove->next;
  }
  cout << endl;
}
主函数
int main()
{
  struct Node* list = createList();
  insertNodeByHead(list, 1);
  insertNodeByHead(list, 2);
  insertNodeByHead(list, 3);
  PrintList(list);
  return 0;
}

运行结果:

注释6:

因为用的是头插法,所以先插入的变量在遍历输出的时候,在后面

完整代码
#include<iostream>
using namespace std;
struct Node
{
  int data;
  struct Node* next;//静态链表的节点是索引值 不是结构体指针
};
//创建表头
struct Node* createList()
{
  struct Node* headNode = (struct Node*)malloc
  (sizeof(struct Node));
  //用malloc申请内存
  headNode->next = nullptr;
  //注释3
  return headNode;
}
//创建节点(一个)
struct Node* createNode(int data)
{
  struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
  newNode->data = data;
  //传入数据
  newNode->next = nullptr;
  return newNode;
}
//插入节点
void insertNodeByHead(struct Node* headNode, int data)
{
  struct Node* newNode = createNode(data);
  //创建插入的节点
  newNode->next = headNode->next;
  //修改指针域
  headNode->next = newNode;
  //指向要插入的数据
}
//删除节点
void deleteNodeByAppoin(struct Node* headNode, int posData)
//headNode只是一个传入的参数名,不是真的头节点 不要弄混
//posData是要删除的数据
{
  struct Node* posNode = headNode->next;
  struct Node* posNodeFront = headNode;
  //分别对于链表是否为空和posData是否存在进行判断
  if (posNode == nullptr)
  {
    cout << "链表为空 操作错误" << endl;
  }
  else
  {
    while (posNode->data != posData)
    {
      posNodeFront = posNode;
      posNode = posNodeFront->next;
      if (posNode == nullptr)
      {
        cout << "未找到要删除的数据" << endl;
        return;
      }
    }
    posNodeFront->next = posNode->next;
    //用free函数释放内存
    free(posNode);
  }
}
//遍历打印链表
void PrintList(struct Node* headNode)
{
  struct Node* pMove = headNode->next;
  while (pMove)
  //当pmove不为空指针时 持续遍历
  //这里就可以联系到创建节点时,为什么要将next初始化为空指针
  //巧妙 妙
  //while这里还能这么写 学到了 学到了
  {
    cout << pMove->data;
    pMove = pMove->next;
  }
  cout << endl;
}
int main()
{
  struct Node* list = createList();
  insertNodeByHead(list, 1);
  insertNodeByHead(list, 2);
  insertNodeByHead(list, 3);
  PrintList(list);
  return 0;
}
使用new

使用new的话,只需要在创建节点和插入节点那里修改一下就可以了

Node* createList()  
{  
 Node* headNode = new Node;  
 headNode->next = nullptr;  
 return headNode;  
}  
Node* createNode(int data)  
{  
 Node* newNode = new Node;  
 newNode->data = data;  
 newNode->next = nullptr;  
 return newNode;  
}  
实际项目中可能会遇到的小问题

在编写如通讯录、图书馆信息管理系统的时候,data一般都是结构体,那么在输入的时候,就可能会遇到输入完数字,再输入字符串,

那么就存在需要清空缓冲区的问题

此时,我们可以用setbuf函数

使用格式如下:

setbuf(stdin,NULL);清空缓冲区函数

(在此只是介绍一下,作者也不会)

结语

算是初步把链表中的静态和动态链表了解和学习了一下,希望这篇文章对你有帮助,我们下篇文章见~~



相关文章
|
4天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
25 5
|
29天前
|
存储 算法 安全
2024重生之回溯数据结构与算法系列学习之串(12)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丟脸好嘛?】
数据结构与算法系列学习之串的定义和基本操作、串的储存结构、基本操作的实现、朴素模式匹配算法、KMP算法等代码举例及图解说明;【含常见的报错问题及其对应的解决方法】你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
2024重生之回溯数据结构与算法系列学习之串(12)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丟脸好嘛?】
|
28天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
54 4
|
29天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习(8)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
29天前
|
存储 算法 安全
2024重生之回溯数据结构与算法系列学习之顺序表【无论是王道考研人还真爱粉都能包会的;不然别给我家鸽鸽丢脸好嘛?】
顺序表的定义和基本操作之插入;删除;按值查找;按位查找等具体详解步骤以及举例说明
|
29天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
29天前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
29天前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
29天前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之顺序表习题精讲【无论是王道考研人还真爱粉都能包会的;不然别给我家鸽鸽丢脸好嘛?】
顺序表的定义和基本操作之插入;删除;按值查找;按位查找习题精讲等具体详解步骤以及举例说明
|
28天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
41 0