链表
1.链表的概念及结构
概念:连表示一种屋里存储结构上的非连续,非吮吸的储存结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
物理上:1.链式结构在逻辑上是连续的,但在物理上不一定连续。
2.现实中的节点一般都是从对上申请出来的。
3.从对上申请空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。
2.链表的分类
1.单向或者双向
2.带头或者不带头
3.循环或者非循环
共计8种
最常用的两种链表:无头单向非循环链表 和 带头双向循环链表
1.无头单向非循环链表:结构简单,一般不会单独用来存数据。实际更多是作为其他数据结构的子结构,如:哈希桶,图的邻链表。另外在笔试中出现很多。
2.带头双向循环链表:结构最复杂,一般用于单独存储数据。实际中使用的链表结构,都涉及带头双向循环链表。另外,这个结构看似复杂,实际使用代码实现会带来很多优势,实现反而更简单。
3.链表的实现
无头单向非循环链表
建立结构体变量
typedef int SLDataType;//方便更改数据类型
typedef struct SList
{
SLDataType data;
struct SList* next;//节点,存下一个链表的地址。
}SList;
链表不需要单独封装一个函数初始化
1.链表打印
基本思路:1)断言指针
2)为空直接打印NULL
3)创建临时变量,判断条件 cur!=NULL
void SListPrint(SList* phead)
{
if (phead == NULL)
printf("NULL\n");
else
{
SList* cur = phead;//一般情况下不要动头节点。
while (cur != NULL)//为空结束循环
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
}
2.创建一个新的节点
后续插入数据都需要创建新新节点,独立封装一个函数是最优解。
基本思路:1)malloc节点,判断节点是否成功创建,否就直接终止程序。
2)结构体成员初始化。
SList* BuySListNode(SLDataType x)
{
//创建一个节点
SList* tmp = (SList*)malloc(sizeof(SList));
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);//直接退出程序
}
else
{
SList* newnode = tmp;
//初始化
newnode->data = x;
newnode->next = NULL;
return newnode;
}
}
2.尾插数据
基本思路:1)创建一个新节点
2)传递指针要传递二级指针,创建变量创建的是结构体指针,修改数据要在地址上修改。
3)判断链表是否为空,为空直接赋值,不为空找到为节点,链接上即可。
配图:
代码实现:
void SListPushBack(SList** pphead, SLDataType x)
{
//不需要断言。
//创建一个节点。
SList* newnode = BuySListNode(x);
//如果为空,直接赋值
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//遍历链表,找到尾节点
SList* tail = *pphead;
while (tail->next != NULL)//下一个节点为空,上一个节点就是尾节点。
{
tail = tail->next;//找下一个节点
}
tail->next = newnode;
//新节点初始化时指针是指NULL,可以不用二次链接
}
}
3.头插数据
基本思路:1)创建新节点
2)判断空与非空,是否需要额外讨论。
画图:
1)非空
2)空
两者无区别
代码:
void SListPushFront(SList** pphead, SLDataType x)
{
SList* newnode = BuySListNode(x);
//plist非空
newnode->next = *pphead;
*pphead = newnode;
//为空时不需要额外讨论,画图解释。
}
4.尾删数据
基本思路:1)断言
2)链表为空,直接返回
3)找到尾节点,free掉
i.只有2个节点,尾删就是置空头节点。
ii.多个节点,采用双指针的方法prev,tail
画图:
1)链表为空 return;
2)链表有2个节点
3)链表有多个节点
prev->next = tail->next(NULL);
free(tail);
代码:
void SListPopBack(SList** pphead)
{
assert(pphead);
//空节点
if (*pphead == NULL)
{
return;
}
else if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//多个节点
else
{
SList* tail = (*pphead)->next;
SList* prev = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
prev = prev->next;
}
free(tail);
prev->next = NULL;
}
}
5.头删数据
基本思路:1)修改传递二级指针
2)如果是空节点直接返回
3)优良二或多个节点,创建一个next指针,保存头节点的
下一个节点,free掉头节点,将头节点换成next
画图:
当只有一个节点的时候,next= NULL,改变与多个节点的情况相同,不需要额外讨论。
代码:
void SListPopFront(SList** pphead)
{
assert(pphead);
//空节点
if (*pphead == NULL)
{
return;
}
else
{
//两个或多个节点
SList* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
}
6.查找x
基本思路:1)断言,防止传递空指针
2)遍历查找x,找到返回节点,找不到返回NULL
实现代码:
SList* SListFind(SList* phead, SLDataType x)
{
assert(phead);
SList* cur = phead;
while (phead != NULL)
{
if (cur->data == x)
{
return cur;
}
else
cur = cur->next;
}
return NULL;
}
7.在pos后插入x
基本思路:1)断言pos,以防传递空指针
2)创建新的节点newnode
3)进行插入的两种思路
思路一:创建临时变量tmp 保存pos->next节点,随便链接即可。
思路二:直接链接,但是要注意从后往前链接,防止出现连接未完成,pos找不到原先的pos->next。
void SistInsertAfter(SList* pos, SLDataType x)
{
assert(pos);
方法一:
//SList* tmp = pos->next;//保存节点,以防止pos指向原先的节点失效,引发bug
//SList* newnode = BuySListNode(x);//插入就要创建新的节点
//pos->next = newnode;//pos链接新的节点
//newnode->next = tmp;//实现插入
//方法二:
SList* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
8.在pos前插入x
基本思路:1)断言pos;
2)创建新的节点
3)分情况讨论
i若是pos指向的是头节点,那就调用头插函数接口。
ii一般情况,创建一个cur指针变量,找到pos前的节点,进行插入,与7相似的处理手段。
实现代码:
void SListInsertBefore(SList** pphead,SList* pos, SLDataType x)
{
assert(pos);
SList* newnode = BuySListNode(x);
if (pos == *pphead)
{
SListPushFront(pphead,x);
}
else
{
SList* cur = *pphead;
while (cur->next != pos)
{
cur = cur->next;
}
cur->next = newnode;
newnode->next = pos;
}
}
9.删除pos后的节点
基本思路:1)断言pos
2)创建临时节点next,记录pos->next,即要free的节点
3)进行链接pos 和 next->next
4)坑点,若pos后面为空,那么就会出现next->next访问出错,就需要处理下。
使用if语句进行处理。
画图:
代码实现:
void SListPopAfter(SList* pos)
{
assert(pos);
if (pos->next != NULL)//pos后为NULL就不执行代码
{
SList* next = pos->next;
pos->next = next->next;
free(next);
next = NULL;
}
/*else
printf("pos已经是尾节点了\n");*///单纯测试代码
}
10.删除pos前的节点
基本思路:1)断言
2)分情况讨论:
i.pos如果是头节点,直接返回
ii.如果是两个节点,那就直接调用头删函数解决
iii.如果是多个节点,创建cur节点,cur->next->next为pos时,就是要找的cur
free节点。
3)由于使用一般情况的节点数必须超过3个,所以<3的就需要额外讨论。
画图:
i
ii
iii
代码实现:
void SListPopBefore(SList* pos,SList** pphead)
{
assert(pos);
//一个
if (pos == *pphead)
{
return;
}
else if ((*pphead)->next == pos)
{
SListPopFront(pphead);
}
else
{//多个
SList* cur = *pphead;
while (cur->next->next != pos)
{
cur = cur->next;
}
SList* prev = cur->next;
cur->next = pos;
free(prev);
prev = NULL;
}
}
11.删除pos位置的值
基本思路:1)断言pos和pphead
2)分类讨论
i如果pos指向的是头节点,那就调用头删函数
ii创建临时变量cur遍历链表,找到pos前的节点,记为cur,链接cur和pos->next
free掉pos
画图:
代码实现:
void SListPopPos(SList** pphead, SList* pos)
{
assert(pphead&&pos);
if (pos == *pphead)
{
SListPopFront(pphead);
}
else
{
SList* cur = *pphead;
while (cur->next != pos)//找到pos前的节点
{
cur = cur->next;
}
cur->next = pos->next;//链接
free(pos);//释放pos
pos = NULL;
}
}
12.删除链表
基本思路:1)遍历链表,一个节点一个节点的删
2)free前创建临时变量next记录cur->next,循环条件就是cur!=NULL
3)头节点最后置空
void SListDestroy(SList** pphead)
{
SList* cur = *pphead;
while (cur)//一个节点一个节点的删
{
SList* next = cur->next;
free(cur);//释放
cur = next;//循环调整
}
*pphead = NULL;
}