文章目录
1、前言
了解过顺序表的缺陷后,我们知道必要的优化在于:
1.按需申请释放;
2.不要挪动数据。
那么,链表又如何能做到真正的按需扩容?
2、链表简介
链表是一种物理存储上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
表现形式:单链表、双链表和循环链表。
主要特征:单向、双向、带头、不带头、循环、不循环。
链表的八种结构:带头单向循环链表、带头单向非循环链表、带头双向循环链表、带头双向非循环链表、无头单向循环链表、无头单向非循环链表、无头双向循环链表、无头双向非循环链表。
2.1组成
链表:由若干个同一结构类型的结点依次串联而成,每个结点保存下一个结点的地址。
结点:数据域和指针域。(结点类型为结构体,方便存储不同类型的数据和指针)
数据域:存储数据元素。
指针域:存储下一个结点的地址。
2.2逻辑结构
每个结点分为两个区域:数据域和指针域。(一般逻辑结构是现象出来便于理解学习的)
2.3物理结构
反映内存中结构的存储以及结点之间的关系。
与顺序表不同的是,内存地址完全是随机的,不一定相邻。
3、创建结构体
//创建单链表的结构体(即结点类型,包含组成链表的数据和指针)
typedef int DataType;
typedef struct ListNode
{
DataType data;//数据域:存储数据
struct ListNode* next;//指针域:存储下一个结点的地址
}LTNode;
4、链表接口
//接口
//创建结点
LTNode* BuyLTNode(DataType x);
//创建单链表(可以不写)
LTNode* CreatList(int n);
//打印
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode** pphead, DataType x);//找尾
//尾删
void LTPopBack(LTNode* phead);
//头插
void LTPushFront(LTNode** pphead, DataType x);
//头删
void LTPopFront(LTNode** pphead);
//查找
LTNode* LTFind(LTNode* phead, DataType x);
//先find后insert/erase
//在pos结点之后插入数据
void LTInsertAfter(LTNode* pos, DataType x);
//pos结点之前插入数据
void LTInsert(LTNode** pphead, LTNode* pos, DataType x);
//删除pos结点之后的一个数据
void LTEraseAfter(LTNode* pos);
//删除pos结点位置数据
void LTErase(LTNode** pphead, LTNode* pos);
//销毁
void LTDestroy(LTNode** pphead);
为什么部分接口函数在实现时需要传递二级指针参数?
//测试
void test()
{
DataType* plist = NULL;
}
这是因为实参作为一个一级指针指向内存空间地址,如果想对头结点进行操作,就需要使用二级指针将改变的数据传递回实参所在的函数内。另外,二级指针用在这个地方可以作为最优解(C语言)看待,摒弃了许多麻烦的操作。
5、创建结点
//创建结点
//由于链表在内存中的物理地址是随机的,所以创建结点时无需使用扩容
LTNode* BuyLTNode(DataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x; //确定数据
newnode->next = NULL; //确定下一个结点的地址指针
//创建新结点时置空,方便后续操作
return newnode;
}
6、创建单链表
//创建单链表
LTNode* CreatList(int n)
{
LTNode* phead = NULL;
LTNode* ptail = NULL;
int x = 0;
for (int i = 0; i < n; i++)
{
scanf("%d", &x);
LTNode* newnode = BuyLTNode(x);
if (phead == NULL)
{
ptail = phead = newnode;
}
else
{
ptail->next = newnode;
ptail = newnode;
}
}
//ptail->next = NULL;
return phead; //返回phead避免丢失
}
为什么链表的物理结构中地址是随机的,不一定连续的?
因为结点在申请空间时是根据循环一个一个申请的,所以这些结点在内存中分配的空间并不是连续的,而是随机分配的空间,所以链表并不能像顺序表一样可以随机下标访问元素。
7、打印单链表
//打印单链表
void LTPrint(LTNode* phead)
{
LTNode* cur = phead;
while (cur != NULL)
{
//printf("%d->", cur->data);
printf("[%d|%p]->", cur->data, cur->next);
cur = cur->next;
}
printf("NULL\n");
}
打印单链表时,从头指针位置开始依次向后打印,当(打印)指针指向NULL时结束。
8、增加结点
8.1单链表的头插
//头插
//单链表的头插插入方便
void LTPushFront(LTNode** pphead, DataType x)
{
LTNode* newnode = BuyLTNode(x); //申请新节点
//先让新结点记住头结点的位置,然后将头指针指向新结点
newnode->next = *pphead;
*pphead = newnode;
}
头插结点:首先申请新结点,然后将新结点的指针域指向头结点,最后让头指针指向新结点即可。
8.2单链表的尾插
//尾插
void LTPushBack(LTNode** pphead, DataType x)//找尾
{
LTNode* newnode = BuyLTNode(x); //申请新结点
if (*pphead == NULL) //判断头结点
{
*pphead = newnode;
}
else
{
LTNode* tail = *pphead;
while (tail->next) //利用结点存储的地址寻找结束条件
{
tail = tail->next;
}
tail->next = newnode;
}
}
尾插结点:核心任务就是找尾,注意头结点为空需要另外判断。
8.3单链表指定位置插入结点
//指定位置之后插入结点
void LTInsertAfter(LTNode* pos, DataType x)
{
assert(pos);
LTNode* newnode = BuyLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//指定位置之前插入结点
void LTInsert(LTNode** pphead, LTNode* pos, DataType x)
{
assert(pos);
if (*pphead == pos)
{
LTPushFront(pphead, x);
}
else
{
LTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
LTNode* newnode = BuyLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
9、删除结点
9.1单链表的头删
//头删
void LTPopFront(LTNode** pphead)
{
assert(*pphead);
LTNode* tail = *pphead; //记录第一个节点的位置
*pphead = tail->next; //头节点存入下一个节点
free(tail); //释放临时空间
tail = NULL; //记录节点置空
}
如果是空表,可以无需做处理;如果不是空表,可以直接让头指针指向第二个结点,然后释放第一个结点的内存空间。
//头删
void SListPopBack(SLTNode** phead)
{
assert(*phead);
//*和->优先级相同,使用时需注意括号应用
SLTNode* next = (*phead)->next; //找到第二个节点
free(*phead); //释放头节点
*phead = next; //将第二个节点改为头节点
}
9.2单链表的尾删
//尾删1
void LTPopBack(LTNode** pphead)
{
assert(*pphead);
//双指针操作
LTNode* prev = *pphead;
LTNode* tail = *pphead;
//tail-最后一个节点
//prev-倒数第二个节点
while (tail->next)
{
prev = tail;
tail = tail->next;
}
if (prev == tail) //特殊情况,链表只有一个节点
{
*pphead = NULL; //头指针置空
free(prev); //释放prev
}
else
{
prev->next = NULL;//置空倒数第二个节点的指针域
free(tail); //释放最后一个节点
tail = NULL; //及时置空
}
}
尾删需要注意不同情况下的单链表删除不同:
- 单链表为空;
- 单链表只有一个元素;
- 单链表有多个元素。
//尾删2
void LTPopBack(LTNode** pphead)
{
assert(*pphead);//列表为空不能删除
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
LTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
9.3单链表指定位置删除结点
//删除指定位置之后的结点
void LTEraseAfter(LTNode* pos)
{
assert(pos);
if (pos->next == NULL) //判断指定位置是否为最后一个结点的地址
{
return;
}
else
{
LTNode* cur = pos->next; //待删除结点(需要保留方便后续的释放)
pos->next = cur->next; //pos结点指向待删除结点的下一个结点,完成单链表的删除
free(cur); //释放临时变量
}
}
//删除指定位置的结点
void LTErase(LTNode** pphead, LTNode* pos)
{
assert(pos);
assert(*pphead);
if (pos == *pphead) //特殊情况,删除头结点
{
LTPopFront(pphead); //复用头删
}
else
{
LTNode* prev = *pphead; //使用头指针
while (prev->next != pos) //找到待删除结点的前一个节点地址
{
prev = prev->next;
}
prev->next = pos->next; //prev指向pos下一个节点位置
free(pos); //释放pos
}
}
10、查找
//查找结点(根据数据)
LTNode* LTFind(LTNode* phead, LTDataType x)
{
LTNode* cur = phead;
//遍历单链表(根据值查找)
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}