距离上次写博客已经是一个月前了......为什么会这样呢,当时学到栈和队列来着,现在已经学完了数据结构了,寻思着写博客直接从头写复习一遍,估计效果可能不如边学边写好,但总是要写的,以后给自己复习看看。
一. 单链表的定义
单链表是单向,非连续,但逻辑线性的数据结构。。
单链表由一个个节点串联而成,仅能从头节点向后遍历。
节点组成:数据域(存储数据) + 指针域(存储下一个节点地址)
核心特征:
-
尾节点指针指向 null
-
内存不连续,动态分配空间
-
无随机访问能力,只能顺序遍历
与数组的区别:
数组:连续内存,支持随机访问,查询快,增删慢,需提前分配空间,易浪费/溢出
单链表 :非连续内存,不支持随机访问,查询慢,增删极快,动态扩容无浪费
基础代码
1.结构体定义;
cpp
typedef struct SListNode
{
SLTypeData data;
struct SListNode* next;
}SLNode;
这就是单链表的结构体定义,很简单,就是一个节点数据+下一个节点的地址,就这么一个个链起来。所以叫单链表。
2.初始化;
cpp
void InitList(SLNode** head)
{
*head = NULL;
}
是的,就这么简单,唯一需要注意的点是函数传递的是二级指针,因为需要改变节点本身。
举个例子,比如:
cpp
SLNode *L;
InitList(&L);
外部传递过来的就是指针的指针,也就是二级指针。如果只传InitList(L),相当于只传了一个拷贝的地址给函数,并不会改变L自身。只有传它的指针才能改变自身。
3.单链表的判空;
cpp
bool SLEmpty(SLNode* phead)
{
return *phead==NULL;
}
很简单,没啥说的。
核心基础操作
核心基础操作包括插入,删除,遍历,查找。我们一个个来看。


在写之前,需要先写一个申请新节点的函数SLByNode;
cpp
SLNode* SLByNode(SLTypeData x)//申请空间
{
SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));//使用malloc申请动态空间
if (newnode == NULL)
{
perror("malloc fail");//打印错误信息
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
1.插入(尾插)
cpp
void SLpushback(SLNode** phead, SLTypeData x)//尾插
{
SLNode* newnode = SLByNode(x);//新节点申请空间
if (*phead == NULL)//如果链表是空链表
{
*phead = newnode;//直接插入即可
}
else//非空链表
{
SLNode* tail = *phead;
while (tail->next)
{
tail = tail->next;//寻找链表尾部
}
//tail此时就是链表尾部节点
tail->next = newnode;//将新节点插入链表尾部节点
}
}
这还是当初过年的时候写的,我这注释写得也是真详细,毕竟当时理解起来有点困难。
现在看起来真是简单。
2.头插
cpp
void SLHeadInsert(SLNode** phead, SLTypeData x)//头插
{
assert(phead);
SLNode* newnode = SLByNode(x);//给新节点申请空间
newnode->next = *phead;//让新节点的下一个节点指向头节点
*phead = newnode;//更新头指针,使其指向新节点,新节点成为新的链表头
}
对比起尾插,头插是简单得多了。
3.尾删
cpp
void SLdelback(SLNode** phead)//尾删
{
assert(phead && *phead);
//链表只有一个节点
if ((*phead)->next == NULL)
{
free(*phead);
*phead = NULL;
}
else//链表有多个节点
{
SLNode* tail = *phead;//用来找尾节点
SLNode* prev = *phead;//保存尾结点的前驱
while (tail->next)//
{
prev = tail;
tail = tail->next;
}
//此时tail是尾节点,prev是倒数第二个节点
free(tail);//删除尾节点
tail = NULL;
prev->next = NULL;//让倒数第二个节点成为新的尾结点
}
}
我觉得现在看懂这些代码很轻松了,更需要注意的是细节问题了。比如要不要考虑只有一个节点的问题,要怎么设计代码更好等。
4.头删
cpp
void SLdelhead(SLNode** phead)//头删
{
assert(phead&&*phead);
SLNode*next=(*phead)->next;
free(*phead);
*phead=next;
}
5.查找
cpp
SLNode* SListFind(SLNode* phead, SLTypeData x)//查找
{
SLNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
简单易懂,但如果现在我写不知道能不能记住最后要加一个return NULL;
6.在指定位置之后删除
cpp
void SLInsert(SLNode** phead, SLNode* pos, int x)//指定位置之前插入
{
assert(phead && *phead);
assert(pos);
SLNode* newnode = SLByNode(x);
if (pos == *phead)//如果链表只有一个节点
{
SLHeadInsert(phead, x);//直接头插
}
else
{
SLNode* newnode = SLByNode(x);
SLNode* prev = *phead;
while (prev != NULL && prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
这代码有点难度了,并不是说难看懂,而是细节有点多,刚才我自己尝试写过一次,漏了好几个细节,首先就是assert断言,我居然忘记了还需要断言pos,接着是没考虑到链表只有一个节点的情况,不过写完了检查一下还是发现了。
7.在指定位置之后插入
cpp
//指定位置之后插入
void SLTInsertAfter(SLNode* pos, SLTypeData x)
{
assert(pos);
SLNode* newnode = SLByNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
这比起刚才的那个指定位置头插简单多了。
8.指定位置删除
cpp
//删除指定位置节点
void SLTErase(SLNode** phead, SLNode* pos)
{
assert(*phead && phead);
assert(pos);
if (pos == *phead)
{
SLdelhead(phead);//直接头删
}
else
{
SLNode* prev = *phead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
还行,应该没问题。
9.链表的销毁
cpp
//销毁链表
void SListDestroy(SLNode** phead)
{
assert(*phead && phead);
SLNode* pcur = *phead;
while (pcur)
{
SLNode* next = pcur->next;
free(pcur);
pcur = next;
}
*phead = NULL;
}
这个就需要重点注意了,我都已经忘记了需要循环销毁了。
结语
以上就是单链表的复习情况了,看起来还挺简单的,没有刚开始学的那种困难的感觉了。
我觉得比顺序表简单。^-^。