引例
顺序表的容量变化,对于增容,我们一般是以两倍的数量进行成倍增长,这样可能会出现空间的浪费,在这里,我们就要介绍一种新的线性表----链表。链表的逻辑结构是线性的,但它的物理空间是非线性的。
链表就像火车车厢,每节车厢都是独立的,火车是由一个一个车厢组成的,而链表是由一个一个节点组成的。
链表的组成
一.节点的组成
1.数据
2.指向下一个节点的地址的指针
我们利用结构体来定义链表
struct SListNode
{
int data;
struct SListNode* next;
};
通过有一个结构体指针来指向下一个链表的节点。
于是,我们来尝试创建几个节点
SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
node1->data = 1;
SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
node2->data = 2;
SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
node3->data = 3;
SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
node4->data = 4;
通过malloc函数创建node指针作为节点,然后,我们再通过结构体指针next将节点连接起来。
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
为了更直观地调试代码,这里我们写一个Print函数来将链表的值打印出来
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
SLTNode* plist = node1;
SLTPrint(plist);
如果链表为空则会打印"NULL"。
这样就将我们给节点所赋的值打印出来了。
二.链表的具体实现
对于链表的具体实现,我们要实现以下几个函数
SLTNode* SLTBuyNode(SLTDataType x);
void SLTPrint(SLTNode* phead);//定义形参phead执行链表的第一个节点
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDestroy(SLTNode** pphead);
1.节点创建函数
节点的创建,我们需要返回一个指向节点的指针,所以函数形式为SLTNode*:
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)//防止创建失败
{
perror("malloc fail!");
return;
}
newnode->data = x;//新节点中的值
newnode->next = NULL;//创建新节点后的值为空
return newnode;
}
2.尾插函数
尾插函数要注意的是,传到函数的实参应该是指针的函数,因为这里我们想要让形参影响形参,所以我们不传值,而是传地址(这里的地址指的是指针的地址,而不是值的地址)。
void SLTPushBack(SLTNode** pphead, SLTDataType x)//phead为头节点
{
assert(pphead);//防止传来的指针的地址为空
SLTNode* newnode = SLTBuyNode(x);
if (*pphead == NULL)//如果传来的指针为空
{
*pphead = newnode;//则直接将传来的指针指向newnode的值
}
else
{
SLTNode* ptail = *pphead;//定义ptail来进行指针的指向变化
while (ptail->next)
{
ptail = ptail->next;//ptail往后走
}
ptail->next = newnode;//此时ptail指向的就是尾节点,于是将新节点给这个尾节点
}
}
3.头插函数
头插则较为简单,先将要插入的节点与原来的头节点相连,再将newnode设为新的节点即可。
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;//直接传指针
*pphead = newnode;//将newnode设为新的头节点
}
4.尾删函数
尾删函数要求头节点与头节点的地址都不为空,并且要分为两种情况,链表只有一节点和链表有多节点,避免ptail变为野指针。
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
if ((*pphead)->next == NULL)//如果链表只有一个节点
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = *pphead;
SLTNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;//prev指向ptail
ptail = ptail->next;
}
free(ptail);
ptail == NULL;
prev->next = NULL;
}
}
5.头删函数
头删函数就要注意不能上来直接删除头节点,要先将头节点的下一个节点单独存起来
void SLTPopFront(SLTNode** pphead)//不能上来直接删头节点
{
//链表不为空
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
6.查找函数
查找函数不需要形参改变实参,所以不需要传头节点
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}//pcur == NULL
return NULL;//如果想要再遍历,则只让pcur改变,不改变phead
}
SLTNode* Find = SLTFind(plist, 3);
if (Find == NULL)
{
printf("没有找到!");
}
else
{
printf("找到了!");
}
7.在指定位置前插入数据函数
在指定位置前我们就要考虑第一个位置,又因为第一个位置前插入数据就是头插函数,所以这里我们直接调用即可。
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//需要利用头插函数,此处需要用到头节点
{
assert(pphead && *pphead);
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
SLTNode* prev = *pphead;
//pos == *pphead,说明是头插
if (pos == *pphead)
{
SLTPushFront(pphead,x);
}
else
{
while (prev->next != pos)
{
prev = prev->next;//遍历数组
}
newnode->next = pos;
prev->next = newnode;//prev找到新节点newnode,与newnode连接
}
}
8.在指定位置之后插入数据
在指定位置后插入数据相对简单,因为不需要找前一个节点了,也不需要遍历数组了。
void SLTInsertAfter(SLTNode* pos, SLTDataType x)//不需要遍历数组了,所以不需要头节点
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;//注意节点连接顺序
pos->next = newnode;
}
9.删除pos节点
此处仍然要考虑删除第一个节点的情况。
void SLTInsertAfter(SLTNode* pos, SLTDataType x)//不需要遍历数组了,所以不需要头节点
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;//注意节点连接顺序
pos->next = newnode;
}
10.删除pos之后的节点
此处要定义一个中间变量,防止pos->next指向的节点改变。
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);//pos后的节点为空不需要删除
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
11.销毁链表
销毁链表需要头节点向后遍历,所以需要用到头节点的二级指针。
void SListDestroy(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
这样,单链表的实现就完成了