目录
概念
链表的概念及结构
概念:链表是一种物理存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针连接次序实现的。
现实中的链表:
数据结构中:
思考思考,实际中的链表会有非常多种
1.单向,双向
2.带头,不带头
3.循环,非循环
这样一列举就已经能组合出8种了qwq。
一、实现的功能
我们实现的是最基本的增删改查功能,通过列举,依次有以下几个接口需要我们实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
typedef int SListDataType;
typedef struct SListNode
{
SListDataType data;
struct SListNode *next;
} SListNode;
// 打印链表
void SListPrint(SListNode *phead);
// 尾插节点
void SListPushBack(SListNode **phead, SListDataType x);
// 获得一个新节点
SListNode *GetNewSListNode();
// 尾删节点
void SListPopBack(SListNode **pphead);
// 头插结点
void SListPushFront(SListNode **phead, SListDataType x);
// 头删结点
void SListPopFront(SListNode **phead);
//查找结点
SListNode *SListFind(SListNode *phead, SListDataType x);
//任意位置插入结点
void SListInsertAfter(SListNode *pos, SListDataType x);
//任意位置删除结点
void SListEraseAfter(SListNode *pos);
二、依次实现
1.打印链表
代码如下:
void SListPrint(SListNode *pphead)
{
SListNode *cur = pphead;
while (cur)
{
printf("%d->", cur->data);//遍历每个结点并且打印data
cur = cur->next;
}
printf("NULL\n");
}
2.尾插结点
2.尾插结点首先把头结点传入函数,接着通过指针遍历到最后一个结点,这之后向内存申请一个新结点,将这个新结点连接上。
代码如下:
SListNode *GetNewSListNode()
{
// 申请一个结点
SListNode *NewNode = (SListNode *)malloc(sizeof(SListNode));
// 申请失败,直接退出,简单粗暴
if (NewNode == NULL)
{
printf("%s", strerror(errno));
exit(-1);
}
return NewNode;
}
void SListPushBack(SListNode **pphead, SListDataType x)
{
if (*pphead == NULL)
{
*pphead = GetNewSListNode();
(*pphead)->data = x;
(*pphead)->next = NULL;
}
else
{
SListNode *NewNode = GetNewSListNode();
SListNode *tail = *pphead;
//循环找到一个节点,这个节点中的节点指针是NULL
//循环找到NULL
while (tail->next != NULL)
{
tail = tail->next;
}
NewNode->data = x;
NewNode->next = NULL;
tail->next = NewNode;
}
}
3.尾删结点
尾插结点就是把最后一个结点删除掉之后,要把新成为的最后一个结点中的next指针置NULL,
当然,我们要分三种情况讨论,第一种就是上面说的,第二种则是只要一个结点的时候,我们要把这个结点free掉,返回空指针,第三种则是没有结点的情况,直接返回空指针。
代码如下:
void SListPopBack(SListNode **pphead)
{
// 1.空
// 2.只有一个结点
// 3.一个以上结点
if (*pphead == NULL)
{
return;
}
else if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SListNode *tail = *pphead;
SListNode *pre = tail; //记录最后一个结点的前一个结点
//通过循环找到最后一个结点
while (tail->next != NULL)
{
pre = tail;
tail = tail->next;
}
//释放掉tail
free(tail);
tail = NULL;
// 之前倒数第二个结点成为了最后一个结点,置NULL
pre->next = NULL;
}
}
4.头插结点
头插结点首先要申请一个新结点,让这个结点的next指向原来的头,这之后在把头移到这个新结点上
代码实现:
void SListPushFront(SListNode **pphead, SListDataType x)
{
SListNode *NewNode = GetNewSListNode();
NewNode->data = x;
// 新插入结点中存放的地址为之前第一个结点的地址
NewNode->next = *pphead;
//把NewNode作为第一个结点
*pphead = NewNode;
}
5.头删结点
头删结点分两种情况,有结点和无结点,无结点时直接返回就好了,有结点的时候先让头指针指到下一个结点,之后再free掉前面那个结点
代码实现:
void SListPopFront(SListNode **pphead)
{
// 1.没有结点
// 2.有1个结点+ 3.有1个以上结点
if (*pphead == NULL)
{
//没有要删的
return;
}
else
{
// 1.保存头指针指向的下一个结点地址
// 2.free掉头指针
// 3.将头指针与下一个地址重新连接
SListNode *next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
}
6.查找结点
没什么难的,就是遍历,遍历到了就返回,不到就返回NULL。
SListNode *SListFind(SListNode *phead, SListDataType x)
{
SListNode *cur = phead;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
7.任意位置后一位的插入
传入位置后,首先要申请一个新结点,让结点指向位置的后一位,再让传入的位置指向新结点
代码实现:
void SListInsertAfter(SListNode *pos, SListDataType x)
{
assert(pos);
SListNode *NewNode = GetNewSListNode();
NewNode->data = x;
NewNode->next = pos->next;
pos->next = NewNode;
}
8.任意位置后一位的删除
首先把要删除的结点临时保存,再把传入位置的结点的next指针再往后指一位,最后free掉之前保存的结点。
代码实现:
void SListEraseAfter(SListNode *pos)
{
assert(pos);
if (pos->next)
{
SListNode *next = pos->next;
SListNode *nextnext = pos->next->next;
pos->next = nextnext;
free(next);
}
}
总结
由于做链表的时候一直要对内存进行操作,一出错容易导致整个程序崩溃,所以每完成一个函数加一次测试,确保没问题。要非常熟悉各种插入删除的步骤,做到烂熟于心,还有在写代码之前,不妨先分析有几种情况,比如之前的尾删就有3种情况,要是一开始没考虑清楚,后面程序出错了再来修改反而需要更长的时间。