① 在函数中判断结构体指针是否为空时采用assert函数,而不是用if语句判断。
② 函数的命名规则遵循:操作+结构类型名 的规则。例如 InitSqList 与DestroySqList。
③ 严蔚敏老师一书中很多运用了C++的语法,而我们是用C语言来实现,因此编写规则与书上会有很多不同,但是思路是一样的。例如用malloc代替new,free代替delete,引用与指针的区别等。
④ 本文没有采用bool变量以及自定义的Status作为返回值来判断是否操作成功。
线性表的特点是可以随意存取表中任意元素,但是在插入和删除时需要移动大量的元素,效率低下,而且因为数组的长度相对固定的静态特性,当表中数据元素个数多且变化大时操作过程相对复杂,必然导致空间的浪费。链表是线性表的链式表示,用于弥补线性表的缺陷。但是链表本身也有缺陷,我们后面再说。
线性链式表,简称链表,特点是用一组任意的存储单元存储线性表的数据元素。因此为了表示当前元素和逻辑上的下一个元素之间的关系,除了存储本身的数据之外,还要存储一个指示其直接后继的信息。由这两个信息组成的单元叫做结点。
通俗地讲,链表需要定义一个数据域和一个指针域,指针域用于存放逻辑上下一个元素的地址,即指向下一个元素。
typedef int LLDataType;
typedef struct LinkListNode
{
LLDataType data;
struct LinkListNode* next;//存储下一个结点的地址
}LLNode;
其中定义LLDataType便于后续修改元素类型。data是数据域,next是指针域。此处我们只定义了一个next指针,而没有pre,指向上一个结点的指针,因此是单向链表。链表最后的结点指向NULL。
//单链表不需要初始化
void PrintLinkList(LLNode** phead); 遍历打印链表
LLNode* CreateLinkNode(LLDataType i); 创建新的结点
void BackInsertLinkList(LLNode** pphead, LLDataType i);尾插
void BackDeleteLinkList(LLNode** pphead);尾删
void FrontInsertLinkList(LLNode** pphead, LLDataType i);头插
void FrontDeleteLinkList(LLNode** pphead);头删
LLNode* SearchLinkList_Address(LLNode** pphead, LLDataType i);
查找元素,返回的是该元素的地址
int SearchLinkList_Position(LLNode** pphead, LLDataType i);
查找元素,返回的是该元素的位置
void ModifyLinkList(LLNode** pphead, int pos, LLDataType i);
修改pos位置的元素
void InsertBeforeAddressLinkList(LLNode** pphead,
LLNode* pos, LLDataType i);
在pos地址前插入
void InsertAfterAddressLinkList(LLNode* pos, LLDataType i);
在pos地址后插入
void DeleteAtLinkList(LLNode** pphead, LLNode* pos);
删除pos地址处的结点
void DeleteAfterLinkList(LLNode* pos);
删除pos地址处之后的一个结点
void DestoryLinkList(LLNode** pphead); 摧毁链表
注意:
本文是带头结点(也叫根结点)的单向链表,因此每次需要定义一个头结点指向NULL,又传参的时候某些函数需要传二级指针,因为要修改头结点(头指针)的指向以便链接之后的结点。
LLNode* CreateLinkNode(LLDataType i)
{
LLNode* newnode = (LLNode*)malloc(sizeof(LLNode));
if (newnode == NULL)
{
printf("malloc失败!\n");
exit(-1);
}
else
{
newnode->data = i;
newnode->next = NULL;
}
return newnode;
}
在需要插入的时候定义一个newnode用于接收这个函数的返回值,之后用于插入操作。
void PrintLinkList(LLNode** pphead)
{
//不用assert,头结点可以没有下一个
LLNode* cur = *pphead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
//下一个结点的地址在next
}
printf("NULL\n");
}
一般是定义一个cur指针保存pphead,再通过cur来遍历。
void BackInsertLinkList(LLNode** pphead, LLDataType i)
{
assert(pphead);
LLNode* newnode = CreateLinkNode(i);
if (*pphead == NULL)//如果是空链表,直接插上去
{
*pphead = newnode;
}
else
{
//从头开始找尾结点
LLNode* tail = *pphead;
1 2 3 4 5
while (tail->next != NULL)//这里区别于打印
{
tail = tail->next;
}
tail->next = newnode;
}
}
pphead不能为空,因为pphead是目标结构体的地址,不能对一个空指针进行操作,因此要assert判断。定义一个尾指针来找最后一个结点。区别于打印,这里尾结点的下一个是空时就要停止之后插入,打印的判断条件是指针走到空便结束,要区分开来。
void BackDeleteLinkList(LLNode** pphead)
{
assert(pphead);
LLNode* tail = *pphead; 尾指针
if (*pphead != NULL) 先判断链表是否为空
{
if ((*pphead)->next == NULL) 再判断是只有一个结点还是多个
{
free(*pphead);
*pphead = NULL;
return;
}
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
return;
}
else
{
printf("链表为空,删除失败\n");
return;
}
}
void FrontInsertLinkList(LLNode** pphead, LLDataType i)
{
LLNode* newnode = CreateLinkNode(i);
//if (*pphead == NULL)
//{
// *pphead = newnode;
//}
//else 不用这一步
{
newnode->next = *pphead;
*pphead = newnode;
}
}
这里不用判断链表是否为空,空链表照样插入。
void FrontDeleteLinkList(LLNode** pphead)
{
assert(pphead);
LLNode* aim = *pphead;
if (aim == NULL)
{
printf("表为空,删除失败\n");
return;
}
else
{
//if (aim->next == NULL) 一个结点
//{
// free(aim);
// aim = NULL;
// *pphead = NULL;
//}
*pphead = aim->next;
free(aim);
}
}
这里同样不用判断是否只有一个结点,因为aim指向空的时候也能free aim。free(NULL)是合理的。
LLNode* SearchLinkList_Address(LLNode** pphead, LLDataType i)
{
assert(pphead);
LLNode* cur = *pphead;
while (cur != NULL)
{
if (cur->data == i)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
返回的是结点的地址,如果没找到就返回NULL。
int SearchLinkList_Position(LLNode** pphead, LLDataType i)
{
assert(pphead);
int pos = 0;
LLNode* cur = *pphead;
while (cur != NULL)
{
pos++; 进来先++一下
if (cur->data == i)
{
return pos;
}
cur = cur->next;
}
pos = 0; 置零
return pos;
}
找到了就返回在第几个位置,没找到就返回0
void ModifyLinkList_Add(LLNode* pos, LLDataType i)
{
assert(pos);
pos->data = i;
}
这太简单了
void ModifyLinkList_Pos(LLNode** pphead, int pos, LLDataType i)
{
assert(pphead);
LLNode* cur = *pphead;
if (pos > 0)
{
for (int i = 1; i < pos; i++)
{
if (cur == NULL)
{
printf("位置溢出,修改失败\n");
return;
}
cur = cur->next;
}
cur->data = i;
}
else
{
printf("位置输入不合法\n");
return;
}
}
这也没啥好说的
void InsertBeforeAddressLinkList(LLNode** pphead, LLNode* pos,
LLDataType i)
{
assert(pphead && pos);
if ((*pphead) == pos) 先判断是否头插 是的话直接复用头插函数
{
FrontInsertLinkList(pphead, i);
}
else
{
LLNode* pre = *pphead;
while (pre->next != pos)
{
pre = pre->next;
}
LLNode* newnode = CreateLinkNode(i);
newnode->next = pos;
pre->next = newnode;
}
}
void InsertAfterAddressLinkList(LLNode* pos, LLDataType i)
{
assert(pos);
LLNode* aim = (pos)->next;
LLNode* newnode = CreateLinkNode(i);
newnode->next = aim;
pos->next = newnode;
}
void DeleteAtLinkList(LLNode** pphead, LLNode* pos)
{
assert(pphead && pos);
if ((*pphead) == pos) 如果是第一个结点,同样复用头删函数
{
FrontDeleteLinkList(pphead);
}
else
{
LLNode* pre = *pphead;
while (pre->next != pos)
{
pre = pre->next;
}
pre->next = (pos)->next;
free(pos);
pos = NULL;
}
}
void DeleteAfterLinkList(LLNode* pos)
{
assert(pos);
LLNode* aim = pos->next;
if (aim != NULL)
{
pos->next = aim->next;
free(aim);
aim = NULL;
}
}
void DestoryLinkList(LLNode** pphead)
{
assert(pphead);
LLNode* cur = *pphead;
while (cur != NULL)
{
LLNode* aim = cur->next;
free(cur);
cur = aim;
}
*pphead = NULL;
}
效果展示
结语
单链表是一种使用指针来存储值的数据结构,其改善了线性表的缺点,但是也存在不足。单链表只能以一个方向进行遍历。为了把一个值插入链表中,首先需要找到插入的位置,之后进行断链和勾链。
在使用单链表时,通过指定下标或位置插入值的情况比较少见,使用结点地址更多一些。且链表的应用场景一般是作为复杂数据结构的子结构,因此单链表是今后学习复杂数据结构的基础。
单链表是链式数据结构的开端。
(本篇完)