文章目录
一、单链表
1.单链表的概念
链表是一种物理存储结构上非连续、非顺序的存储结构
,数据元素的逻辑顺序是通过链表中的指针链接在一起的
。
链表用节点的方式存储了数据以及其指向的下一个节点的地址,分别为存储有效数据数据的数据域,存储下一节点的地址的指针域。单链表只有一个指针域,而双向链表有两个。
2.单链表与顺序表的比较
结构上:
顺序表是用一段物理地址连续
的存储单元依次存储数据元素的线性结构,即顺序表相邻元素之间的物理位置和逻辑位置皆相邻
。
链表则是用一段物理地址非连续
的存储结构存储数据,即链表存储结构逻辑上是相邻的,但物理上不相邻
。
顺序表的优缺点:
优点:
1.空间连续,支持随机访问。
缺点:
1.中间或前面部分的插入删除时间复杂度O(N);
2.增容代价比较大;
单链表的优缺点:
优点:
1.进行头部插入和头部删除的时间复杂度为O(1);
2.没有增容问题,每次插入一个开辟一个空间;
缺点:
1.以节点为单位存储,不支持随机访问;
3.单链表的基本操作接口
以下实现的单链表皆无哨兵位头节点。
//单链表基本操作的接口
//动态申请一个节点
SListNode* BuySListNode(SLDataType val);
//单链表的打印
void SListPrint(SListNode* phead);
//单链表的尾插
void SListPushBack(SListNode** pphead, SLDataType val);
//单链表的尾删
void SListPopBack(SListNode** pphead);
//单链表的头插
void SListPushFront(SListNode** pphead, SLDataType val);
//单链表的头删
void SListPopFront(SListNode** pphead);
//单链表的查找
SListNode* SListFind(SListNode* phead, SLDataType val);
//单链表在pos位置之后插入
void SListInsertAfter(SListNode* pos, SLDataType val);
//单链表删除pos位置之后的节点
void SListEraseAfter(SListNode* pos);
//单链表的销毁
void SListDestory(SListNode** pphead);
4.单链表的结构定义
typedef int SLDataType;
typedef struct SListNode
{
struct SListNode* next;//存储下一个节点的地址
SLDataType val;//存储有效数据
}SListNode;
5.动态申请一个节点
//动态申请一个节点
SListNode* BuySListNode(SLDataType val)
{
//申请节点
SListNode* new_node = (SListNode*)malloc(sizeof(SListNode));
assert(new_node);//判断节点是否申请失败
//对数据域和指针域赋值
new_node->next = NULL;
new_node->val = val;
}
6.单链表的打印
//单链表的打印
void SListPrint(SListNode* phead)
{
SListNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
7.为什么要传送二级指针???
为什么要传送二级指针,是无头单链表的最难点,这考察你对指针和结构体的知识掌握是否熟练。
在部分情况下,如在链表为空的情况下插入节点,在链表只有一个节点时删除节点
,都需要你改变指向头节点的指针的指向
,但由于函数的形参的只是实参的一份临时拷贝
,改变形参不会影响实参的改变,所以你需要传送实参的指针,也就是头节点的指针的指针。
传送一级指针的作用:
如果你传送的是节点的指针,那么你可以修改节点中的指针域和数据域,但无法修改你传送的一级指针的指向;如:打印链表,在pos位置后插入数据;这些都不需要你修改头节点的指针的指向,所以使用一级指针便够了。
总结:
传送一级指针,允许你修改节点中的内容。
传送二级指针,允许你修改指向节点的指针的指向,并且也允许你修改节点中的内容。
8.单链表的增删查改
8.1单链表的尾插
注意:
无头单链表的尾插需要划分两种情况:1.链表为空;2.链表不为空;
当链表为空时,我们需要修改头节点指针的指向,让其指向新开辟的节点,所以需要使用二级指针;
当链表不为空时,我们需要遍历链表,直到链表的尾节点,再让其指针域指向新开辟的节点,此操作便与二级指针无关。
//单链表的尾插
void SListPushBack(SListNode** pphead, SLDataType val)
{
assert(pphead);//pphead不可能为空
//创建节点
SListNode* new_node = BuySListNode(val);
//尾插数据
//1.链表为空
if ((*pphead) == NULL)
{
//修改头节点的指向
*pphead = new_node;
}
else
{
//2.链表不为空
SListNode* tail = *pphead;
//寻找尾节点
while (tail->next != NULL)
{
tail = tail->next;
}
//插入数据
tail->next = new_node;
}
}
8.2 单链表的尾删
注意:
无头单链表的尾删划分为两种情况:1.链表中只有一个节点;2.链表中有两个及以上节点;
当链表只有一个节点时,先释放掉唯一的节点,再将头节点的指针指向NULL
,此时需要使用二级指针,才能将外部实参同样修改为指向NULL;
当链表有两个及两个以上的节点时,只需要遍历链表,寻找到尾节点的前一个节点
,然后释放掉尾节点,让尾节点的前一个节点的指针域指向NULL
。
void SListPopBack(SListNode** pphead)
{
assert(pphead);
assert(*pphead != NULL);//确保有节点可删
//删除节点
//链表只有一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//链表有两个及以上节点
//寻找尾节点的前一个节点
SListNode* cur = *pphead;
while (cur->next->next != NULL)
{
cur = cur->next;
}
//释放尾节点,让尾节点的前一个节点指针域指向NULL
SListNode* tail = cur->next;
cur->next = NULL;
free(tail);
}
}
8.3 单链表的头插
注意:在无头单链表中头插节点,任何情况下都需要改变头节点指针的指向,让其指向新的头节点;
//单链表的头插
void SListPushFront(SListNode** pphead, SLDataType val)
{
assert(pphead);
//创建新节点
SListNode* new_node = BuySListNode(val);
assert(new_node);//检查申请是否成功
SListNode* next = *pphead;//记录原链表的头节点
//在头节点前插入新节点
*pphead = new_node;//让头节点的指针指向新节点
new_node->next = next;
}
8.4 单链表的头删
注意:单链表的头删与头插一样,任何情况下都需要改变头节点指针的指向;让头节点指针指向原链表中头节点的下一个节点。
//单链表的头删
void SListPopFront(SListNode** pphead)
{
assert(pphead);
assert(*pphead != NULL);//确保有节点可删
SListNode* next = (*pphead)->next;//记录下头节点的下一个节点
free(*pphead);
*pphead = next;
}
8.5 单链表的查找
单链表的查找只需要依次遍历链表,找到值相等的节点便可访问该节点,找不到则返回NULL;
由于不需要改变头节点的指向,所以只需要传递一级指针。
//单链表的查找
SListNode* SListFind(SListNode* phead, SLDataType val)
{
assert(phead);
SListNode* pos = phead;
while (pos != NULL)
{
if (pos->val == val)
{
return pos;
}
pos = pos->next;
}
return