一.引入
上一篇的顺序表我们提到了,顺序表是存在它的局限性的,而且从更多角度上来说,顺序表使用起来并不是那么便捷,所以这一篇我们来讲解一下链表是如何使用的。
二.单链表概念及结构
1.概念
2.结构小复习
这里的next是一个struct SListNode类型的指针,不是结构体
上面//struct SListNode是类型名,下面的需要在}后SLTNode
3.结构详解
void SLTPrint(SLTNode * phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
cur指向的是地址而不是数据,cur先指向地址,如果不是空地址,则打印出数据,再指向下一个地址,就是先有地址,再有数据。
下面将顺序表与链表对比:
(1).
顺序表需要断言,而链表不需要断言:
因为链表即便是NULL,也可以打印,无非是空链表,但是顺序表必须断言,因为ps指针指向的不是结构体数据,而是a,size,capacity,也就是说即使没有数据,a可以空,但是ps指向的这块空间不能为空,况且顺序表取决于size的大小而并非a中的数据。
(2).
这里不可以cur++,因为链表不是顺序表,不是连续的,顺序表++是下一个位置但是链表不是。
(3).
这里不可以蓝框这么写,因为从结构示意图可以知道,在cur->next指向最后一个数据下一位的时候才是NULL,这样会导致最后一个数据无法被打印出。
三.单链表接口实现
1.头文件
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
//struct SListNode
//{
// SLTDataType data;
// struct SListNode* next;
//};
//
//typedef struct SListNode SLTNode;
void SLTPrint(SLTNode* phead);
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);
//
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
// pos֮ǰ
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// posλɾ
void SLTErase(SLTNode** pphead, SLTNode* pos);
// pos
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
// posλúɾ
void SLTEraseAfter(SLTNode* pos);
2.打印链表
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
3.扩容操作
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
}
4.尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
细节解说
1.链表和顺序表最大的不同就是链表必须是逻辑连接的,也就是必须有xxxx->next这样的写法连接起来,链表之间不是挨着的,必须逻辑上->next才能找到下一个目标,实现连接。
2.二级指针使用的目的:
下面是使用一级指针对应的效果
可以发现一级指针对单链表没有任何作用,这是因为我们只是传了形参,而没有把地址传过去,所以实际的内容没有发生改变。
我们大可以想象为:数据和地址存储在一个指针里,而这些指针存储在大的单链表里,因此改变数据要用一级指针,但是改变链表中的指针(如上图,删除链表中的最后一块数据,包含数据和地址)这就是改变整个链表,就必须要用二级指针了。
5.头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
6.尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
//暴力检查
assert(*pphead!=NULL);
//温柔检查
if ((*pphead) == NULL)
return;
//考虑单节点的情况
if ((*pphead)->next = NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
}
}
找尾细节解说
1.错误写法以及链表与顺序表的区别
可以发现,这个写法会出现bug,也就是说没有成功完成尾删,关键就在于红框内的内容tail=NULL,这个写法是有问题的,因为我们知道,链表不是连续的,如果要找到链表中节点的位置就必须使用xxxx->next查找,没有连接的话是无法进行操作的。
顺序表是一整块连续的空间,即使异地扩容,也是一整块的,连续的;链表是多个不连续的空间通过指针连接成的一块联合区域,物理上不连续,逻辑上连续。
2.正确写法
上面两个图原理上是一样的,都是正确写法。
7.头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* front = *pphead;
*pphead = front->next;
free(front);
front = NULL;
}
8.查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
9.插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pos);
assert(pphead);
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
插入原理:
pphead的断言
对每一个我们进行增删的,我们都要对pphead进行断言,因为pphead是plist的地址,我们的pphead使用的是二级指针,plist(链表)可以为空,但是pphead(地址)不能为空,*pphead是plist,所以对于引入链表,必须对pphead进行断言,而*pphead(plist)则在进行删除操作的时候必须断言。
10.删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
assert(*pphead);
if (pos = *pphead)
{
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
这个是最正常的一个思路,还有一个别样的方法:
交换法
这个只要不是尾结点就可以,不用头结点也可以。上图是插入,下图是删除,都是通过交换数据,改变链表链接完成的。
11.pos后面插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
12.pos后面删除
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
四.链表的分类
链表一共有8种
五.双向循环链表接口实现
1.头文件
#include<stdio.h>
#include<stdbool.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
LTNode* BuyListNode(LTDataType x);
LTNode* LTInit();
void LTDestroy(LTNode* phead);
void LTPrint(LTNode* phead);
bool LTEmpty(LTNode* phead);
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);
// posλ????
void LTInsert(LTNode* pos, LTDataType x);
void LTErase(LTNode* pos);
2.扩容操作
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
node->prev = NULL;
node->next = NULL;
node->data = x;
return node;
}
3.初始化链表
LTNode* LTInit()
{
LTNode* phead = BuyListNode(-1);
phead->next = phead;//尾的next指向哨兵位的头
phead->prev = phead;//哨兵位的头得prev指向链表的尾
return phead;
}
4.打印双向循环链表
void LTPrint(LTNode* phead)
{
assert(phead);
printf("<=haed=>");
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d=>", cur->data);
cur = cur->next;
}
printf("\n");
}
5.判断是否为空链表并返回bool值
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
6.双向循环链表的尾插
7.双向循环链表的尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
tailPrev->next = phead;
phead->prev = tailPrev;
free(tail);
tail = NULL;
}
8.双向循环链表的头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* first = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
}
9.双向循环链表的头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* first = phead->next;
LTNode* firstPrev = first->prev;
LTNode* firstNext = first->next;
firstPrev->next = firstNext;
firstNext->prev = firstPrev;
free(first);
}
10.双向循环链表的插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* Prev = pos->prev;
LTNode* newnode = BuyListNode(x);
Prev->next = newnode;
newnode->prev = Prev;
newnode->next = pos;
pos->prev = newnode;
}
11.双向循环链表的删除
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* Prev = pos->prev;
LTNode* Next = pos->next;
Prev->next = Next;
Next->prev = Prev;
free(pos);
}
12.双向循环链表操作规律
很明显,双向循环链表的操作关键是记录节点。插入节点就是把前一个节点和当前节点记录下来,删除节点就是把上一个节点和下一个节点记录下来,然后free掉对应的参数。