前置知识:C语言基础语法,指针,结构体,动态内存申请
前言:链表作为一种非常基础的数据结构,在C程序当中也是应用广泛(比如大学生作业要做的管理系统)。链表就像是一辆火车,而每一个节点就是火车的车厢,所以大家学习链表的时候不妨把链表就当作是一辆火车去看待,方便想象。
请细心阅读每一个字并记住上下文(我也会适当的提醒滴)
一个单链表最主要包含两个部分:数据域 和 指针域
数据域:即为各种数据,可以是int、char等基础数据类型,也可以是结构体等的自定义数据类型
指针域:用于指向下一个节点的指针,链表就是通过指针来把各个节点连接到一起,形成一个有逻辑的顺序表。
为了方便,这里就不用结构体作为数据了,直接用一个int类型作为链表的数据域
单链表的设计:
#include <stdio.h> //这里的头文件下面的代码中会用上,所以一起写上了
#include <stdlib.h>
#include <assert.h>
typedef struct Node
{
int data;
struct Node* next;
}Node, List;
如上图所示,这里我们设计的链表由一个int data作为数据域。而Node* next作为指向下一节点的指针。至于为什么要起一个别名叫做List呢?请接着往下看(如果不知道typedef在结构体中的作用可以去搜一下哦)
接下来就到了单链表的创建了
我们知道,一辆火车最主要的就是火车头,通过火车头才能把后面的车厢连接起来,才能群龙有首,火车头一般是不用来装货物的,装货物的一般是车厢。因此我这里起了个别名叫做List,来表示List节点就代表了头节点,即火车头。而Node用来代表装数据用的车厢。
链表的创建:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef struct Node
{
int data;
struct Node* next;
}Node, List;
List* createList()
{
List* list = (List*)calloc(1, sizeof(List));
assert(list);
return list;
}
int main()
{
List* list = createList(); //创建一个链表
return 0;
}
有人可能会有疑问,为啥要把创建一个链表封装成一个函数捏?还有为啥List* list = createList();就相当于创建了一个链表捏?
回答1:封装成一个函数是为了简便,而且通过调用这个函数,我们可以快速的创建多个链表,就不用复制粘贴前面的代码了。
回答2:前面说到了,一节火车最重要的就是火车头,而有了火车头,后面的节点,我们只要用指针连接上去就行了,所以,这其实就相当于创建了一个链表,只是火车头的后面目前还没有车厢而已。
节点的创建:
请移步到 Node* createNode(int data)这个函数
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef struct Node
{
int data;
struct Node* next;
}Node, List;
List* createList()
{
List* list = (List*)calloc(1, sizeof(List));
assert(list);
return list;
}
Node* createNode(int data)
{
Node* newNode = (Node*)calloc(1, sizeof(Node));
assert(newNode);
newNode->data = data;
return newNode;
}
同样为了后续插入数据的代码的简便,这里createNode函数封装的创建节点的过程,这里传入的参数int data,则代表了这个节点存储的数据。
---------------------------------------------------------------------------------------------------------------------------------
那么接下来就到了重头戏,也就是链表的插入、删除、展示。
链表的插入:
尾插法:
尾插法,顾名思义,就是在链表的最后面插入节点。即在一辆火车的后面,接入新的车厢。这里我们需要先知道一件事情,一个单链表的尾节点的指向总是指向NULL的。这个很好理解,因为火车尾的后面也是没有车厢的,尾部接的是空气嘛。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//为了避免代码太长了,所以省略的前面的代码
void push_back(List* list, int data)
{
Node* newNode = createNode(data);
Node* curNode = list;
while (curNode->next != NULL)
{
curNode = curNode->next;
}
curNode->next = newNode;
}
这里我们分步骤给大家逐一讲解:
- push_back函数的参数指明了我们需要插入到哪个链表,以及需要插入的数据是什么。
- 在函数体当中,我们先是调用了createNode函数,来创建了一个需要插入的节点,newNode即为需要插入的节点。
- 然后,既然是尾插法,那么我们就需要先找到链表的尾部,即找到火车最后的一节车厢。(注意,当链表只有头节点的时候,那么这个时候头节点 == 尾节点)
- 我们用创建了一个Node*的指针curNode,代表的是当前节点。我们前面说过,尾节点是肯定指向空的,根据这个特性,我们可以很容易的找到尾节点,因此while循环里面的条件即为curNode->next != NULL
- 可能很多人看到curNode = curNode->next就一脸懵逼了,不懂是什么意思,这里curNode是代表当前遍历到的节点,而curNode->next代表的是下一个节点嘛。那么curNode = curNode->next,就相当于我们让 当前节点 = 下一个节点 这样我们不就实现了遍历每一个节点了嘛。因为curNode = curNode->next 是放在while循环当中的,curNode和curNode->next都是在不停的变化的。你只要记住:curNode->next 永远表示的是当前节点的下一个节点就行了。
- 最后,当循环结束了,curNode就变成了尾节点了,然后让curNode指向newNode(curNode->next = newNode),至此链表连接成功。
由于篇幅原因,接下来的头插法、任意插法,节点的删除等会在后面的文章写上,可以查看数据结构专栏哦