今天,我们来学习,数据结构中的链表
链表,就是多个结构体变量之间,通过结构体指针连接在一起的一种数据结构
提示:
本篇文章主要讲解动态链表,对于静态链表不做过多介绍
链表可分为静态链表和动态链表
只是初步了解,更详细的操作(比如插入节点、删除节点)在这篇文章中不做说明
静态链表,实际上就是一个结构体数组
分配一整片连续的内存空间,各个结构体变量集中安置,逻辑结构上相邻的数据元素,存储在指定的一块内存空间中,数据元素只允许在这块内存空间中随机存放,这样的存储结构生成的链表称为静态链表。也就是说静态链表是用数组来实现链式存储结构
下面,给出一段例子
这就是静态链表的定义和初始化
#include
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
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的话,只需要在创建节点和插入节点那里修改一下就可以了
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);清空缓冲区函数
(在此只是介绍一下,作者也不会)
算是初步把链表中的静态和动态链表了解和学习了一下,希望这篇文章对你有帮助,我们下篇文章见~~