数据结构–单链表
学习了顺序表,我们发现顺序表在向里面存放数据的时候很麻烦,比如我们要使用头插法存放一个数据到顺序表的时候,我们要将整个表都向后挪一位,这个操作就让人很难受。那么有没有一种结构可以让我们存放数据的操作变得简单一些呢?这就要用到线性结构的另一种–链表
链表的概念及结构
链表是一种物理存储结构上非连续、非顺序的的存储结构,数据元素的国际顺序是通过链表中的指针链接次序实现的。如下图:
注意:
- 从上图可以看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
- 现实中的结点一般都是从堆上申请出来的
- 从对上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
链表的分类
实际中链表的结构非常多,单链表、双向链表、带头单链表、带头双向链表、循环链表、不循环链表等等,虽然结构很多,但是我们实际最常用的还是无头单向非循环链表和带头双向循环链表
单向或者双向链表
带头链表
循环链表
链表的一些特点
**1.无头单向非循环链表:**结构简单,一般不会单独用来存数据。实际中更是多为其他数据结构的子结构。
**带头双向循环链表:**机构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。
链表的理解
链表的代码结是这样定义的:
struct Node{
int data; // 用来存放数据
struct Node* next; // 存放下一个节点的地址,用于找到下一个节点
}
我在学习链表的时候对于next的理解一直很困惑,后面突然顿悟,next就是指向下一个节点的地址。我们知道,指针是存放地址的,通过这个地址我们能访问到那个位置的数据。而这个next就是存放下一个节点地址的,通过这个地址,我们就能找到下一个节点。
单链表的实现
链表的操作和顺序表的操作是差不多的,无非是增删改查,这一系列操作。先看看我们要实现的功能
typedef int SLDataType;
// 定义链表的结构
typedef struct SListNode {
SLDataType data;
struct SListNode* next;
}SListNode;
// 创建一个新节点
SListNode* BuyNewNode(SLDataType x);
// 尾插
void SListPushBack(SListNode** pphead, SLDataType x);
// 头插
void SListPushFront(SListNode** pphead, SLDataType x);
// 尾删
void SListPopBack(SListNode** pphead);
// 头删
void SListPopFront(SListNode** pphead);
// 打印链表
void SListPrint(SListNode* pphead);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLDataType x);
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLDataType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
// 单链表的销毁
void SListDestory(SListNode* plist);
首先我们得先把链表的结构给定义出来,就是用结构,定义一个链表结点的结构。如下图,我们要把一个结点划分为两个部分,即data和next部分。data部分用来存储数据,next用来存放下一个结点的位置,也就是用来将结点“链接起来”。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xasr70N8-1636910284535)(D:\blogs\结点.png)]
// 链表结点的定义
typedef struct SListNode {
SLDataType data;
struct SListNode* next; // 因为结点是SListNode类型,所以next得是SListNode*
}SListNode;
在定义好结点的结构之后,就得创建结点,我们可以将它封装到一个函数中
SListNode* BuyNewNode(SLDataType x) {
SListNode* tmp = (SListNode*)malloc(sizeof(SListNode)); // 为结点申请一块空间
// 给成员变量赋值
tmp->data = x;
tmp->next = NULL;
return tmp;
}
// 使用创建结点的函数
int main(){
SListNode* node = BuyNewNode(2);
return 0;
}
这样我们就的到了单链表的第一个结点,接着就是不断地在这个结点后面插入数据 ,就形成的单链表。下面我们看看几种插入数据的方法。
// 尾插法
void SListPushBack(SListNode** pphead, SLDataType x) {
if (*pphead == NULL) {
*pphead = BuyNewNode(x);
}
else {
SListNode* tail = *pphead;
while (tail->next) {
tail = tail->next;
};
tail->next = BuyNewNode(x);
}
}
尾插法,故名思意就是每次都将数据插入到链表的尾部。所以,我们定义了一个tail指针,用它来找到链表的尾部。找的方式就是通过循环来遍历链表tail = tail->next;
,这段代码就是让tail指针指向tail的下一个结点,实现遍历。
头插法
// 头插
void SListPushFront(SListNode** pphead, SLDataType x) {
if (*pphead == NULL) {
*pphead = BuyNewNode(x);
}
else {
SListNode* tmp = BuyNewNode(x);
tmp->next = *pphead;
*pphead = tmp;
}
}
头插法,正好和尾插相反,头插是将新结点插入到链表的最前面,然后返回新节点的地址
尾删
// 尾删
void SListPopBack(SListNode** pphead) {
if (*pphead == NULL) {
return;
}
if ((*pphead)->next == NULL) {
free(*pphead);
*pphead = NULL;
}
else {
SListNode* tail = *pphead;
while (tail->next->next) {
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
尾删和尾插一样,都需要一个指针来遍历链表,让tail指向最后一个结点的前一个位置,先把最后一个结点free掉,然后再将tail的next指向空。
// 头删
void SListPopFront(SListNode** pphead) {
if (*pphead == NULL) {
return;
}
SListNode* tmp = *pphead;
*pphead = (*pphead)->next;
free(tmp);
tmp = NULL;
}
头删就是将最前面的一个结点删掉,所以我们需要先将头结点的地址保存起来,然后让头结点指向它的下一个结点,最后在把结点给free掉
// 单链表查找
SListNode* SListFind(SListNode* plist, SLDataType x) {
if (plist == NULL) {
return NULL;
}
SListNode* tmp = plist;
while (tmp) {
if (tmp->data == x) {
break;
}
else {
tmp = tmp->next;
}
}
return tmp;
}
单链表的查找就很简单了,就是把单链表遍历一下,看看有没有哪个结点的data部分和x是相等的,如果有就返回这个结点的地址,否则返回空。
接下来就是单链表的随机插入和删除,这里的随机不是说,听天由命的让电脑选一个位置插入到链表中,而是,我们自己想把它插到哪就插到哪。
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLDataType x) {
assert(pos);
SListNode* newnode = BuyNewNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos) {
assert(pos);
if (pos->next == NULL) {
return;
}
SListNode* tmp = pos->next;
pos->next = tmp->next;
free(tmp);
tmp = NULL;
}
当我们不用单链表的时候一定要将它销毁,把申请的空间给释放掉
// 单链表的销毁
void SListDestory(SListNode* plist) {
assert(plist);
SListNode* prev = plist;
SListNode* cur = plist->next;
while (cur) {
free(prev);
prev = cur;
cur = cur->next;
}
free(prev);
prev = NULL;
}
下次分享双向带头环形链表,那是一个很棒的结构。