概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
- 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
- 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
我们在了解线性结构最重要的就是明白这个结构是如何定义的,是怎样的一种结构。这里单链表的定义,我们考虑的是我们需要这个结构中包含什么,这里我们需要的是一个数据域和一个指针域,数据域用于存储数据,指针域用于链接每个单链表节点。行,我们来rua一下:
typedef int SLTDateType;//typedef 一下,方便我们更改所需存储的类型
typedef struct SListNode
{
SLTDateType data;//数据域,用于存储数据
struct SListNode* next;//指针域,用于链接下一个结点
}SLTNode;
单链表一般不直接提供初始化接口,是因为单链表的初始化不需要额外的方法来完成,可以通过其他操作来实现。
一般而言,单链表的初始化可以分为两种方式:
SLTNode* plist = NULL;//后续只需要不断插入新的结点就可以
因此,为了简化接口设计和操作复杂度,单链表通常不提供单独的初始化接口,而是通过其他操作来实现链表的构建和初始化。
这里就可以看到我们在为一个指针开辟空间后就立马初始化了,和上述第二种初始化的方法相同。
SLTNode* SLTNewNode(SLTDateType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
//开辟一个链表结点大小的空间
if (node == NULL)//检测是否开辟成功
{
perror("malloc fail");
return NULL;
}
node->data = x;
node->next = NULL;
return node;
}
为了方便测试一般我们会使用链表打印接口来查看检测。
void SLTPrint(SLTNode* pa)
{
SLTNode* cur = pa;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
这里涉及到单链表的一个难点,就是这里需要二级指针,我们需要更改一级指针内的元素数据,所以需要用二级指针来更改。就例如我们要改变一个数据类型的值,需要一级指针一样。
在链表第一个结点前插入。
void SLTPushFront(SLTNode** ppa, SLTDateType x)
{
assert(ppa);//&plist(ppa)不可能为空,所以需要检测
SLTNode* newnode = SLTNewNode(x);
newnode->next = *ppa;//新节点的next指针指向链表开头(*ppa)
*ppa = newnode;//更新头指针
}
在链表最后结点后插入
void SLTPushBack(SLTNode** ppa, SLTDateType x)
{
assert(ppa);
SLTNode* newnode = SLTNewNode(x);
if (*ppa == NULL) //判断链表是否为空
{
*ppa = newnode;
}
else
{
//找尾操作,这里单链表我们只能使用遍历来实现,当找到一个节点的next指针指向null时,就代表找到尾了。
SLTNode* tail = *ppa;
while (tail->next)
{
tail = tail->next;
}
tail->next = newnode;
}
}
pos前位置插入,我们知道了尾插,这里就相当于把pos位置当做尾,去找pos位置,然后再用头插的思路完成。
//任意位置插入(pos前插入)
void SLTInsertF(SLTNode** ppa, SLTNode* pos, SLTDateType x)
{
assert(ppa);
assert(pos);
if (pos == *ppa)
{
SLTPushFront(ppa, x);
}
else
{
SLTNode* tmp = SLTNewNode(x);
SLTNode* prev = *ppa;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = tmp;
tmp->next = pos;
}
}
pos位置后插入:学习了任意位置插入,我们其实可以把头插和尾插的实现过程改成调用任意位置插入接口
void SLTInsertB(SLTNode** ppa, SLTNode* pos, SLTDateType x)
{
assert(ppa);
if (pos == NULL)
{
SLTPushBack(ppa, x);
}
else
{
SLTNode* newnode = SLTNewNode(x);
SLTNode* cur = *ppa;
while (cur != pos->next)
{
cur = cur->next;
}
pos->next = newnode;
newnode->next = cur;
}
}
头删的思路就是把头指针指向下一个结点,然后释放开始的结点。
void SLTPopFront(SLTNode** ppa)
{
assert(ppa);
assert(*ppa);
SLTNode* tmp = *ppa;
*ppa = tmp->next;
free(tmp);
tmp = NULL;
}
尾删道理是相通的,找到尾指针,但也要保留尾指针的前一个指针,因为需要在删除尾指针后,把前一个尾指针的next指向空。
void SLTPopBack(SLTNode** ppa)
{
assert(ppa);
assert(*ppa);
if ((*ppa)->next == NULL)
{
free(*ppa);
*ppa = NULL;
}
else
{
SLTNode* prev = *ppa;
SLTNode* tail = prev->next;
while (tail->next)
{
prev = prev->next;
tail = tail->next;
}
prev->next = tail->next;
free(tail);
tail = NULL;
}
}
意思相近与尾删。
void SLTEraser(SLTNode** ppa, SLTNode* pos)
{
assert(ppa);
assert(*ppa);
if ((*ppa)->next == NULL||pos==*ppa)
{
SLTPopFront(ppa);
}
else
{
SLTNode* prev = *ppa;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
链表查找就是对应data,然后遍历返回比较简单,修改的话就是根据返回访问data进行赋值修改
SLTNode* SLTFind(SLTNode* pa, SLTDateType x)
{
SLTNode* cur = pa;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}