文章目录
1. 链表是什么?
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
画一张易于理解的简图如下,矩形代表存放的是数据,椭圆代表存放的是指针,链表的头必须是一个指针。
使用链表结构可以克服数组需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。
上面的图说明了链表的原理,下面是链表的存储方式,链表在内存中是非连续、非顺序的,就是说链表结点在内存中的存放并不是一个接一个,按顺序存放的。
这里我们可以发现,如果想要访问链表中某一结点的话,必须从head开始查找,并不像数组那样方便。
2. 队列和栈
队列是限定只能在表的一端进行插入和在另一端进行删除操作的线性表;
栈是限定只能在表的一端进行插入和删除操作的线性表。
简而言之队列就是数据先进先出(first in first out,FIFO);而栈则是先进后出(first in last out,FILO)
3. 头插法和尾插法
头插法和尾插法顾名思义,就是在链表加入新结点的时候是在头部插入还是在尾部插入。
简易的示意图如下。黑色箭头是插入新结点之前的链表顺序,红线是插入新结点后的链表顺序。
可以看到尾插法不改变链表的原本顺序,在遍历链表的时候,正好就对应了队列的先进先出的特性;头插法会改变链表原本的顺序,在遍历链表的时候也是正好对应了栈的先进后出的特性。
所以我们就用头插法的链表来实现栈;尾插法的链表来实现队列。理论存在,实践开始!
4.实践创建栈和队列(各部分代码)
4.1 定义一个链表的结构体
typedef struct node_s //定义链表的结构
{
int data; //数据域
struct node_s *next //指针域
}node_t;
4.2 在main函数中定义链表的一系类指针
node_t *head = NULL; //初始化头指针为空指针
node_t *new_node; //创建一个新结点的指针
node_t *tail; //创建一个链表的尾指针
node_t *front; //创建一个链表的头指针
int count; //计数器
4.3 for循环实现尾插法建立链表
for(count = 0; count < 10; count++) //用一个for循环来循环创建新的结点。
{
new_node = malloc(sizeof(node_t));//malloc申请一块内存给新结点并存在指针中
memset(new_node, 0, sizeof(*new_node));//清理新结点内存
new_node->next = NULL; //新结点的指针域设为空
new_node->data = i+1; //新结点的数据域放入数值
/*到这里新结点的建立就完成了,下面是尾插法的逻辑实现*/
if( head == NULL)
{
head = new_node;//若头指针为空,则是第一个结点,直接用头指针指向
}
else
{
tail->next = new_node;//否则尾结点的指针域指向新结点
}
tail = new_node; //尾指针指向新插入的结点
}
4.4 for循环实现头插法建立链表
for(i = 0; i < 10; i++) //用一个for循环来循环创建新的结点。
{
new_node = malloc(sizeof(node_t));//malloc申请一块内存给新结点并存在指针中
memset(new_node, 0, sizeof(*new_node));//清理新结点内存
new_node->next = NULL; //新结点的指针域设为空
new_node->data = i+1; //新结点的数据域放入数值
/*到这里新结点的建立就完成了,下面是头插法的逻辑实现*/
if( head == NULL)
{
head = new_node;//若头指针为空,则是第一个结点,直接用头指针指向它
}
else
{
front = head; //用front保留住上一个链表的地址
new_node->next = front; //新链表的尾部指向上一个链表的头
head = new_node; //head指向新建链表的头
}
}
4.5 for循环实现遍历链表
for(prt = head; prt != NULL; prt = prt->next) //循环实现遍历
{
printf("%d\n", prt->data); //打印每个节点的数据
}
5.实践创建栈和队列(完整代码)
整合上面各部分的代码,包含malloc、memset和printf的头文件,再用上条件编译,完整的代码如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define INSERT_BEHIND //定义了这行就用尾插擦,注释这行就用头插法
typedef struct node_s //定义链表结构
{
int data; //链表的数据域
struct node_s *next; //链表的指针域
} node_t; //重定义结构体node_s为node_t
int main (int argc, char **argv)
{
node_t *head = NULL; //初始化链表的头指向nowhere
node_t *new_node; //新结点的指针
node_t *tail; //链表的尾指针
node_t *front; //链表的头指针
int MagnetoF; // 计数器(author of this passage: MagnetoF)
#ifdef INSERT_BEHIND //条件编译:定义了INSTER_BEHIND就使用尾插法实现队列的数据结构
for( MagnetoF = 0; MagnetoF < 8; MagnetoF++) //循环创建链表
{
new_node = malloc(sizeof(node_t) ); //创建一块内存,并用new_node指针标记
memset(new_node, 0, sizeof(*new_node) ); //清空申请的内存
new_node->next = NULL; //新链表的尾部指向nowhere
new_node->data = MagnetoF+1; //把数据域放入相应的值
if(head == NULL)
{
head = new_node; //若头指针为空,则是第一个结点,头指针指向新结点
}
else
{
tail->next = new_node; //非首个结点,尾结点的指针域指向新结点
}
tail = new_node; //尾指针指向新的结点
}
#else //未定义INSERT_BEHIND时,使用头插法
for( MagnetoF = 0; MagnetoF < 8; MagnetoF++) //这里创建新链表的for循环和尾插法一样
{
new_node = malloc(sizeof(node_t) );
memset(new_node, 0, sizeof(*new_node) );
new_node->next = NULL;
new_node->data = MagnetoF+1;
if(head == NULL) //这里开始是头插法的关键
{
head = new_node;
}
else
{
front = head; //用front保留住上一个链表的地址
new_node->next = front; //新链表的尾部指向上一个链表的头
head = new_node; //head指向新建链表的头
}
}
#endif
node_t *prt;
for(prt = head; prt != NULL; prt = prt->next) //循环实现遍历
printf("%d\n", prt->data);
return 0;
}
在Ubuntu18.04下用gcc编译分别生成“list_FIFO”(尾插法实现队列)和“list_FILO”(头插法实现栈)。两者运行的结果如下: