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

前言

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

链表是什么

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

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

链表的分类

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

静态链表

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

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

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

下面,给出一段例子
这就是静态链表的定义和初始化

#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;
}

运行结果:
【数据结构】链表的学习和介绍_第1张图片
注释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

使用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);清空缓冲区函数

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

结语

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

你可能感兴趣的:(数据结构,数据结构,链表,学习)