之前结束结构体之后,就可以开始单链表的一些复习了,数据结构整个就是一个很重要的东西,所以有关数据结构的复习很重要。
什么是单链表
由于顺序表的插入删除操作需要移动大量的元素,影响了运行效率,因此引入了线性表的链式存储——单链表。单链表通过一组任意的存储单元来存储线性表中的数据元素,不需要使用地址连续的存储单元,因此它不要求在逻辑上相邻的两个元素在物理位置上也相邻。
就是说,虽然我两个数据在物理是不是相邻的,但是我可以通过某种方式让你在逻辑上是相邻的,所谓逻辑相邻就是,我的下一个就是你,用遍历的思想来说就是,访问了我之后下一个就要访问你,那么我们俩在物理在不是相邻的,所以我们的地址不是相邻的,要想做到逻辑相邻我们就可以用结构体来帮助实现。什么意思呢?如果一个整数,它携带着它下一个整数的地址,那么我们就可以通过这个地址去访问它的下一个数,这样就实现了逻辑相邻。那么这个时候很明显的,对于这个整数,它本身的属性就不只有整型(int)了,它还带有一个属性指针,两个属性怎么办?用结构体来定义就完事了,因为用了结构体所以在一个结点里面放了两个东西——数据类型和结构体指针。
为什么要在顺序表之后发明单链表
顺序表相对于单链表有着明显的缺陷: 顺序表在的空间在开辟后如果想变大就得扩容,而扩容是要付出代价的,扩容有两种,一个是原地扩容,就是说如果你需求的不多,接着这个连续的内存可以往后面接一块,那么就会原地扩容,这样代价较小,而如果说你的请求较大,那么他就会去异地扩容,就是在其他一个地方找一个连续的且满足你的内存大小的一块连续内存,然后把你之前的数据copy过去。
在扩容的时候最明显的代价就是-------内存浪费,比如你本来只是需要120的内存就够了,但是你为保险会直接开辟一个200的内存,所以剩下的80就浪费掉了,可能有的同学会说我用多少开多少,那万一120其实不够怎么办?你就得再去开辟,所以为了避免频繁开辟空间我们都不会去找恰好的内存大小,这就无法避免内存的浪费!
怎么定义
typeof struct node
{
int data;//保存数据的, "数据域"
struct node* next;//保存下一个元素的地址,"指针域"
}Node;
那么这里就是定义了一个新的类型“Node”,我们就可以用这个类型去定义多个结点
单链表的初始化
我们在定义了结点后,就要思考怎么样把这些结点给串起来,首先我们要知道两个东西,一个是头指针,一个是头节点。我们通常会用头指针来标识一个单链表,头指针为NULL时表示一个空表。但是,为了操作方便,会在单链表的第一个结点之前附加一个结点,称为头结点。头结点的数据域可以不设任何信息,也可以记录表长等信息。头结点的指针域指向线性表的第一个元素结点。这里要区别头结点和其他结点!不管带不带头结点,头指针始终指向单链表的第一个结点,而头结点是带头结点的单链表中的第一个结点,结点内通常不存储信息。
#include <stdio.h>
//typedef int ElemType;//数据元素的类型
typedef struct node
{
int data;//保存数据的, "数据域"
struct node * next;//保存下一个元素的地址,"指针域"
//struct node *prev;//保存上一个元素的地址
}Node;
int main()
{
//(1, 3, 5, 7, 2, 4)
Node a1,a2,a3,a4,a5,a6;
a1.data = 1;
a1.next = &a2;
a2.data = 3;
a2.next = &a3;
a3.data = 5;
a3.next = &a4;
a4.data = 7;
a4.next = &a5;
a5.data = 2;
a5.next = &a6;
a6.data = 4;
a6.next = NULL;
Node *p = &a1;
while(p)
{
printf("%d ",p->data);
p = p->next;
}
printf("\n");
return 0;
}
在上面的代码我们可以找到p就是头指针,p是一个结构体指针,它可以访问Node类型的结构体,在while里面,把a1结构体的地址给了p,所以p->data会去访问a1.data,然后p = p->next又把a1.next给了p,也就是a2的地址,这就实现了循环输出,因为最后的a6的指针域是指向空,所以到了a6就自动循环结束。以上就是一个简单的链表模型(不带头结点)。
单链表的基本结构就是这样,然后涉及单链表的操作就重新开一篇来写。