首先我们要明白他的基本概念:
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。就像一个火车一样,连接每一个车厢。
我们会发现链表的特点:
1.从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续。
2.现实中的结点一般都是从堆上申请出来的
3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
介绍完链表的基本概念和特点后,我们将会学习到许多不同类型的链表。
1.单向或双向链表
2.带头不带头链表
3.循环不循环链表
我们平时学习到的链表就是将这些分别组合起来,形成一个个链表结构。
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
现在我们先进行单链表的实现:
1.我们先进行节点的设置,链表结构的设置。
typedef int SLTDatetype;//将int类型重定义为SLTDatetype,方便之后如果类型变了,只需变一下重定义类型
typedef struct SListNode
{
SLTDatetype date;//链表节点中的数据域,储存节点中的数据
struct SListNode* next;//链表节点的指针域,储存节点中的指针,指向下一个节点。
}SLTNode;
2.实现链表中的各项功能。
//动态申请一个节点。
SLTNode* buyListNode(SLTDatetype x);
//头插节点
void SLTpushfront(SLTNode** pphead, SLTDatetype x);
//尾插节点
void SLTpushback(SLTNode* phead,SLTDatetype x);
//尾删节点
void SLTpopback(SLTNode** pphead, SLTDatetype x);
//头删节点
void SLTpopfront(SLTNode** pphead, SLTDatetype x);
//pos位置前插void SLTInsert(SLTNode**pphead,SLTNode*pos, SLTDatetype x);
//pos位置后插
void SLTInsertAfter(SLTNode* pos, SLTDatetype x);//pos位置之前删除
void SLTerase(SLTNode** pphead, SLTNode* pos);
//pos位置后删除
void SLTeraseAfter(SLTNode** pphead, SLTNode* pos);
1.动态分配申请一个节点,这里我们将用到malloc函数,进行动态内存分配,在推中开辟一个空间,用来储存链表节点中的信息数据。
C语言:辨析malloc,realloc,calloc三个动态内存函数_阿威昂的博客-CSDN博客
SLTNode* buyListNode(SLTDatetype x) { SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//malloc动态分配函数 if (newnode== NULL) { perror("malloc failed"); } newnode->date = x;//将数据域置于x newnode->next == NULL;//下一节点指向空 return newnode;//返回节点,因为返回类型是结构体变量 }
2.头插节点。(头插法)
void SLTpushfront(SLTNode** pphead, SLTDatetype x) { SLTNode* newnode = buyListNode(x);//先创建一个节点 newnode->next = *pphead;//将那一个个节点指向头节点 *pphead = newnode;//重新将那个节点置于头节点 }
函数形参中为啥是**pphead?
是因为如果要改变指针的内容,就用一级指针,如果要改变指针指向,要用二级指针。头插了,头结点变了,第一个节点变了,就要二级指针。
3.尾插节点
void SLTpushback(SLTNode** pphead, SLTDatetype x) { SLTNode* newnode = buyListNode(x); if (*pphead == NULL)//如果头节点为空,直接将新节点置于头节点 { //改变的是结构体指针 *pphead = newnode; } SLTNode* tail=*pphead;//创建一个tail尾节点,先置于头节点 while (tail->next != NULL)//遍历到最后一个尾节点 { tail = tail->next; } tail->next = newnode;//将链表最后一个节点指向新节点,实现尾插 }
4.头删节点
void SLTpopfront(SLTNode** pphead) { //空 assert(*pphead); //非空 SLTNode* newnode = (*pphead)->next;//创建一个节点newhead表示pphead的下一个节点 free(*pphead);//删除头节点 *pphead = newnode;//重新将newhead置于头节点 }
5.尾删节点
void SLTpopback(SLTNode** pphead, SLTDatetype x) { assert(*pphead); if ((*pphead)->next==NULL)//只有头节点时 { free(*pphead); *pphead = NULL; } else { SLTNode* tailprev = NULL;//创建一个节点表示尾节点的上一个节点 SLTNode* tail = *pphead;//尾节点 while (tail->next)//遍历 { tailprev = tail; tail = tail->next; } free(tail);//删除尾节点 tailprev->next = NULL;//让尾节点的上一个节点指向空 } }
6.pos前插节点
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDatetype x) { assert(pos); if (pos == *pphead) { SLTpushfront(pphead, x);//如果pos为头节点则为需要头插。 } else { SLTNode* prev = *pphead; while (prev->next !=pos )//遍历到pos前一个位置 { prev = prev->next; } SLTNode* newnode = buyListNode(x); prev->next = newnode;//将上一个节点指向newnode新节点,然后指向pos节点 newnode->next = pos; } }
7.pos后插入节点
void SLTInsertAfter( SLTNode* pos, SLTDatetype x) { assert(pos); SLTNode* newnode = buyListNode(x); newnode->next = pos->next;//直接加入 pos->next = newnode; }
8.pos前删除节点
void SLTerase(SLTNode** pphead, SLTNode* pos) { assert(pos); if (pos == *pphead)//当pos为头节点时 { SLTpopfront(pphead); } else { SLTNode* prev = *pphead; while (prev->next != pos)//遍历到pos前一个位置 { prev = prev->next; } //删除操作 prev->next = pos->next; free(pos); } }
9.pos后删除节点
void SLTeraseAfter(SLTNode** pphead, SLTNode* pos) { assert(pos->next); SLTNode* posnext=pos->next; pos->next = posnext->next;//pos下一个节点指向pos下一个节点的下一个 free(posnext); posnext = NULL; }
通过上面的这些操作,我们就已经学会掌握了单链表的基本操作,基本原理。学完之后,我们也可以去尝试做一下链表oj题。