前言
先给大家一个建议,学习数据结构去画图,不会的可以模仿一下我的gif;
欢迎指正错误;
一.什么是链表
链表由一个一个的节点组成,节点里面有两部分组成,数据域(date)和指针域(next),数据域呢就是用来储存数据的,而指针域(保存了下一个节点的地址这样就可以用指针访问到了)就是用来找到下一个节点的,一个一个节点链接起来便成了链表了。
链表就像这火车一样一节连着一节,就跑起来了。
什么是顺序表呢?这里不介绍我简单概括就是:对数组进去行动态管理。
链表和顺序表都是线性表,区别是顺序表在逻辑结构和空间结构上都是连续的,而链表则在逻辑结构上连续,在空间结构上一般都不连续。
什么是逻辑结构和空间结构呢?
我们看这个链表,它们中间都有一个箭头(指针抽象出来的)指向下一个节点,箭头将它节点串起来成了链表,这个箭头是我们抽象出来方便理解的,实际在内存中可没有个箭头指向下一个节点,这种叫逻辑结构。
实际上节点中存储的是下一个节点的指针,没有这个箭头,各个节点在空间中其实是独立的,没有关系的。
链表也分很多类别,我们先来讲一种结构最简单的链表:单链表。
二、单链表
1.静态链表
为了方便大家理解,先展示个简单的静态链表;
先定义一个结构体:
typedef struct SLTlistNode SLTNode;
//对结构体重新命名,方便下文使用;
typedef int SLTDateType;
//这里将int命名是为了方便以后更改存储数据的类型
//比如要储存浮点型在这里将int改为float就行了;
struct SLTlistNode {
SLTNode* next;//指针域保留下个节点的地址
SLTDateType date;//数据域存储数据;
};
这是运行的结果:
现在我们来详细解释一下这个打印函数
void SLTPrint(SLTNode* phead)
{
//这里的phead接收的是first的地址,也就是第一个节点的地址
SLTNode* cur = phead;
//实际使用中了我们不会直接去改变phead指针,因为很多情况下我们可能多次要用到头指针;
while(cur!=NULL)//当cur == NULL时说明我们的链表遍历完毕了,结束循环,常缩写为while(cur);
{
printf("%d->",cur->date);
cur = cur->next;
//这个代码是核心下面有详细解释;
}
printf("NULL\n");
}
好了,经过上面的讲解后应该对链表有大致了解了,现在我们正式开始动态单链表;
2.动态链表
(1)创建节点
有上面的铺垫后我们应该能知道创建一个节点是十分频繁的事情,我们将他封装成一个函数
//创建节点
SLTNode* SLTBuyNode(SLTDateType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
assert(newnode);
//这个是断言函数使用它得包含它的头文件<assert.h>
//当条件为假时报错,这里用来处理malloc失败的情况;
newnode->next = NULL;
newnode->date = x;
return newnode;
}
(2)头插和头删
头插顾名思义从链表的头部插入一个节点,头删则是从头部删除一个节点。
//头插
void SLTPushFront(SLTNode** pphead,SLTDateType x)
{
//看到二级指针先别急,等我下文详细解释;
//先来想一种极端情况,如果链表为空,那么这个节点便是我们的头;
//但我们发现链表为空下面也行的通,所以不用考虑那种情况了;
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
为什么使用二级指针呢?
首先我们知道函数传参有两种:传值和传址,传参改变的仅是形参不改变实参 。使用一级指针那么传过来的应该是一个指针变量(head为头节点):
SLTPushFront(head);
在回到头插上去看,发现我们对这个指针变量是进行了改变的,但它仅仅是一个临时创建的形式参数,出了头插函数后就销毁了,所以没有改变我们的head;
//举个例子
void Example1(int x)
{
x = 0;
}
void Example2(int* x)
{
*x = 0;
}
int main()
{
int a = 6;
Example1(a);
printf("%d ",a);
Example2(&a);
printf("%d",a);
return 0;
}
//上面这个程序第一次打印的a的值是6;
//第二次打印的值便为0了;
//这就是传值和传址的差别;
同样的我们要想改变head,那么就必须得传head的地址,也就是这样:
SLTPushFront(&head);
它们之间的关系是这样的
理解以后我们可以引入assert到头插,增强代码健壮性:
void SLTPushFront(SLTNode** pphead,SLTDateType x)
{
assert(pphead);
//为什么这里用assert判断因为pphead也就是&head不可能为空;
//那么*pphead呢,也就是head,它可以为空,空链表头插成为第一个节点呗~~;
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
理解了上面这些后你的单链表可以说完成一半了,我们接着往下走:
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
//*pphead不能为空,为空了还删上什么啊~~
SLTNode* Del = *pphead;//将要删除的节点保存下来,然后将头挪到下一个节点;
*pphead = *pphead->next;
free(Del);
Del = NULL;
//好习惯:释放置空避免野指针,尽管这里用处不大,但建议养成;
}
测试一下代码:
SLTNode* head = NULL;
SLTPushFront(&head,1);
SLTPushFront(&head, 2);
SLTPushFront(&head, 3);
SLTPushFront(&head, 4);
SLTPrint(head);
SLTPopFront(&head);
SLTPrint(head);
(3)尾插尾删
//尾插
void SLTPushFront(SLTNode** pphead,SLTDateType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
//考虑极端空链表情况
if(*pphead == NULL)
{
*pphead = newnode;
}
//循环遍历找到尾节点:
else
{
SLTNode* tail = *pphead;
//循环遍历找尾;
while(tail->next)
{
tail = tail->next;
}
tail->next = newnode;
}
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
//找尾
SLTNode* tail = *pphead;
SLTNode* prev = tail;
//这里得找到tail前一个节点才好删除;
while(tail->next)
{
prev =tail;
tail = tail->next;
}
prev->next = NULL;
free(tail);
tail = NULL;
}
测试代码:
SLTNode* head = NULL;
SLTPushFront(&head,1);
SLTPushFront(&head, 2);
SLTPushFront(&head, 3);
SLTPushFront(&head, 4);
SLTPrint(head);
SLTPopFront(&head);
SLTPrint(head);
SLTPushBack(&head, 2);
SLTPrint(head);
SLTPushBack(&head, 3);
SLTPushBack(&head, 4);
SLTPushBack(&head, 5);
SLTPrint(head);
SLTPopBack(&head);
SLTPrint(head);
(4)在任意位置的前面删除节点和删除任意位置节点
//在任意一个节点前插入新节点
void SLTInsert(SLTNode** pphead,SLTNode* pos,SLTDateType x)
{
assert(pphead && pos);
if(pos == *pphead)
{
SLTPushFront(pphead);
}
else
{
SLTNode* prev = *pphead;
SLTNode* newnode = SLTBuyNode(x);
while(prev->next!=pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
要找到那个节点可以和Find函数搭配使用;
//找到某位置
SLTNode* SLTFind(SLTNode* phead,SLTDateType x)
{
SLTNode* cur = phead;
while(cur)
{
//遍历链表数据域相同返回节点;
if(cur->date == x)
{
return cur;
}
cur = cur->next;
}
//还没找到说明没有返回空指针;
return NULL;
}
//删除任意位置的节点
void SLTErase(SLTNode** pphead,SLTNode* pos)
{
assert(pphead && pos);
//与上面一样考虑特殊情况
if(pos == *pphead)
{
SLTPopFront(pphead);
}
else
{
SLTNode* prev = pphead;
while(prev->next)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
测试代码
SLTNode* head = NULL;
SLTPushFront(&head,1);
SLTPushFront(&head, 2);
SLTPushFront(&head, 3);
SLTPushFront(&head, 4);
SLTPrint(head);
SLTPopFront(&head);
SLTPrint(head);
SLTPushBack(&head, 2);
SLTPrint(head);
SLTPushBack(&head, 3);
SLTPushBack(&head, 4);
SLTPushBack(&head, 5);
SLTPrint(head);
SLTPopBack(&head);
SLTPrint(head);
SLTInsert(&head, SLTFind(head, 1), 0);
SLTPrint(head);
SLTErase(&head, SLTFind(head, 3));
SLTPrint(head);
(5)销毁链表
//销毁链表
void SLTDestroy(SLTNode** pphead)
{
SLTNode* cur = *pphead;
SLTNode* Del = cur;
while(cur)
{
Del = cur;
cur = cur->next;
free(Del);
//这里得特别注意顺序,要不然会删少;
}
//别忘了将头置为空
*pphead = NULL;
}
测试代码:
SLTNode* head = NULL;
SLTPushFront(&head,1);
SLTPushFront(&head, 2);
SLTPushFront(&head, 3);
SLTPushFront(&head, 4);
SLTPrint(head);
SLTPopFront(&head);
SLTPrint(head);
SLTPushBack(&head, 2);
SLTPrint(head);
SLTPushBack(&head, 3);
SLTPushBack(&head, 4);
SLTPushBack(&head, 5);
SLTPrint(head);
SLTPopBack(&head);
SLTPrint(head);
SLTInsert(&head, SLTFind(head, 1), 0);
SLTPrint(head);
SLTErase(&head, SLTFind(head, 3));
SLTPrint(head);
SLTDestroy(&head);
SLTPrint(head);
好了单链表到此差不多完结了,现在我们来讲讲链表的分类
三、链表的分类
链表是否循环,是否带头,是否双向,这几个 一共组成了八种链表类型
双向:一个节点内包含两个指针,它可以找到前一个节点和后一个节点;
循环:可以是尾节点指向头节点这样的循环,但也有指向其中某个节点而形成了环状的,例如:
带头:指的是链表的头节点数据域为空,这样可以避免很多讨论:
我们把这个头叫做哨兵位;
举个列子:
/*void SLTPushBack(SLTNode** pphead, SLTDateType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
//考虑极端空链表情况
if (*pphead == NULL)
{
*pphead = newnode;
}
//循环遍历找到尾节点:
else
{
SLTNode* tail = *pphead;
//循环遍历找尾;
while (tail->next)
{
tail = tail->next;
}
tail->next = newnode;
}
}*/
//这是上面尾插的代码,我们需要去讨论它是否为空,但如果它是一个带头的链表的话,因为总存在一个
//哨兵位,链表不会为空,就可以略去链表是否为空的情况,但实际上哨兵位没有数据没有意义,所以
//一般我们打印链表时从head->next开始打印;
SLTNode* guard = SLTBuyNode(0);//这是哨兵位,这里数据无意义;
//尾插
SLTNode* newnode = SLTBuyNode(1);
SLTNode* tail = guard;
while (tail->next)
{
tail = tail->next;
}
tail->next = newnode;
SLTPrint(guard->next);
紧接着,我们来讲一个带头双向循环链表的实现;
四、带头双向循环链表
1.创建节点和初始化链表
//创造节点
LTNode* LTBuyNode(LTDateType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
assert(newnode);
newnode->date = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
//初始化节点
//为什么要有一个初始化函数呢?
//单链表都没有~~
//原因:单链表没写哨兵位,而且没有循环,直接创造一个节点就可以,而这里它作为一个循环链表,一开始
//我们head的prev和next都指向自己,方便设置而已;
LTNode* LTInit()
{
LTNode* head = LTBuyNode(0);
head->next = head;
head->prev = head;
return head;
}
2.头插和头删
//头插
void LTPushFront(LTNode* phead,LTDateType x)
{
assert(phead);
//传一级指针:与单链表不同,在双向循环链表中我们不用改变节点那个指针变量,改变的是节点内指针的信息;
//具体可以看我画的图
//这里要特别注意改变的顺序;
LTNode* newnode = LTBuyNode(x);
phead->next->prev = newnode;
newnode->next = phead->next;
phead->next = newnode;
}
//打印链表
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;//第一个是哨兵位从第二个开始开始遍历打印;
printf("<=>head<=>");
while (cur != phead)//作为一个循环链表它遍历完后会重新回到头节点;
{
printf("%d<=>", cur->date);
cur = cur->next;
}
printf("\n");
}
为什么使用一级指针?
//判断链表是否为空
bool LTEmpty(LTNode* phead)//使用bool要包含头文件<stdbool.h>
{
if (phead->next == phead)//当头指向自己说明链表为空;
{
return true;
}
return false;
}
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
//链表是否为空呢?写一个函数来判断;
assert(!LTEmpty(phead));
LTNode* Del = phead->next;
phead->next = Del->next;
Del->next->prev = phead;
free(Del);
Del = NULL;
}
测试一下
LTNode * head = LTInit();
LTPushFront(head, 1);
LTPushFront(head, 2);
LTPushFront(head, 3);
LTPushFront(head, 4);
LTPrint(head);
LTPopFront(head);
LTPrint(head);
3.尾插和尾删
//尾插
void LTPushBack(LTNode* phead, LTDateType x)
{
assert(phead);
//找尾:单链表找尾是要遍历,但双向循环不用,因为哨兵位的prev指向的是最后一个节点
LTNode* newnode = LTBuyNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* Del = phead->prev;
phead->prev = Del->prev;
Del->prev->next = phead;
free(Del);
Del = NULL;
}
上测试~~
LTNode * head = LTInit();
LTPushFront(head, 1);
LTPushFront(head, 2);
LTPushFront(head, 3);
LTPushFront(head, 4);
LTPrint(head);
LTPopFront(head);
LTPrint(head);
LTPushBack(head, 2);
LTPrint(head);
LTPopBack(head);
LTPrint(head);
4.在任意位置的前面删除节点和删除任意位置节点
//找到某节点
LTNode* LTFind(LTNode* phead, LTDateType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->date == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//任意位置之前插入
void LTInsert(LTNode* pos, LTDateType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
LTNode* prev = pos->prev;
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
//删除任意位置
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* prev = pos->prev;
prev->next = pos->next;
pos->next->prev = prev;
free(pos);
pos = NULL;
}
这里是测试:
LTNode * head = LTInit();
LTPushFront(head, 1);
LTPushFront(head, 2);
LTPushFront(head, 3);
LTPushFront(head, 4);
LTPrint(head);
LTPopFront(head);
LTPrint(head);
LTPushBack(head, 2);
LTPrint(head);
LTPopBack(head);
LTPrint(head);
LTInsert(LTFind(head, 2), 3);
LTPrint(head);
LTErase(LTFind(head, 2));
LTPrint(head);
5.销毁链表
//销毁链表
void LTDestroy(LTNode* phead)
{
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
五、结尾
累死了,第一次写这么长的文章,真累人,肝了10个多小时,写完已经头昏眼花了,所以有错位欢迎评论区指责,十分感谢,过几天应该会出链表的oj题.....