单向链表的实现
实现不带头、不循环单向链表的代码多种多样,不过大体思路一致,我们提供一个思路,重在理解,才会应对其他有所区别的代码。
我们将代码分为三个文件实现,分别为:
SeqList.h: 包含所有需要的头文件,定义以及接口函数的声明。
SeqList: 保存所有接口函数的定义和实现
test.c: 主函数,我们不实现主函数,仅将关键的头文件和接口函数实现。
头文件
//SList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
typedef int SLTDateType;//便于修改节点存储的数据类型
struct SListNode
{
SLTDateType date;//每个节点存储的的数据
struct SListNode* next;//指向下一个节点
};
typedef struct SListNode SLTNode;
接口函数实现
单向链表的节点是在插入新数据时创建的,我们不需要初始化函数。
-
我们直接实现打印函数,形参phead是我们的第一个节点的地址,我们在调用接口函数时,要单独创建一个SLTNode*类型的指针,将它的值作为我们的头指针。
void SListPrint(SLTNode* phead) { //创建一个变量接收我们的头指针,每次判断此变量不为NULL,就将当前节点的数据打印并将当前节点中存储的下一节点的地址赋给此变量,由此打印出所有节点存储的数据 SLTNode* cur = phead; while(cur != NULL) { printf("%d->",cur->date); cur = cur->next; } printf("NULL\n"); } //假如我们的链表有1,2,3,4,5这五个节点元素,那么调用打印函数的效果为:1->2->3->4->5->NULL,这样更加直观形象。
-
前面提到,链表的节点是在插入新数据时创建的,每次我们插入都需要创建一个新节点,方便起见,我们把创建节点的操作分装成一个函数,同时可以把要插入的数据传给这个函数,让新节点存储我们的新数据。
SLTNode* BuySListNode(SLTDateType x) { SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//为新节点开辟一块新的内存空间 newnode->next = NULL;//初始化 newnode->date = x;//存放新数据 return newnode;//返回开辟的新空间的地址 }
-
以上函数准备好后,我们实现一个尾插函数
void SListPushBack(SLTNode** pphead, SLTDateType x) { //思路:我们先用创建节点函数创建新空间,并将其地址接收 //我们需要拿到最后一个节点的地址,让它的next指向我们的新节点。怎样拿到最后一个节点的地址呢?我们在实现打印函数时写了一个循环,循环继续的条件是cur != NULL,以此循环我们cur最终不是最后一个节点的地址,而是最后一个节点的next存放的地址。 //基于此,我们可以改变循环继续的条件,让cur指针(这里我们起名tail,表明是末节点)最终指向最后一个节点,最终我们的循环条件是tail->next != NULL //不过,我们还要考虑一种情况,当我们的链表没有元素时,也就是phead == NULL时,tail->next会报错,因为->操作符实际上是一种解引用操作,对空指针解引用,当然会报错。 //解决方案是:在循环开始前,判断phead是否为NULL,如果为NULL,我们就将newnode赋值给我们的phead。 //此时又出现了一个问题,我们本意是改变头指针的值,我们如果写phead == newnode,phead只是形参,形参只是实参的一份临时拷贝,我们改变形参不会改变实参,所以我们要给函数传一个二级指针,对我们的二级指针解引用,再赋值,就能达到我们对头指针修改的目的。 //此时,再看我们的代码,就很明了了。 SLTDateType* newnode = BuySListNode(x); if(*pphead == NULL) { *pphead = newnode; } else { SLTNode* tail = *pphead; while(tail->next != NULL) { tail = tail->next; } tail-> = newnode; } }
-
头插函数
void SListPushFront(SLTNode** pphead, SLTDateType x) { //思路:同样先开辟新空间并接收,要让我们的头指针指向我们的新节点,再让我们的新节点的next指向我们的原首节点。首先考虑到要对头指针修改,参数就要二级指针。 SLTNode* newnode = BuySListNode(x); newnode->next = *pphead; *pphead = newnode; //我们考虑一下原链表没有元素的情况,以这一情景走一遍代码: //首先我们开辟了一块新空间,并拿到了它的地址,按照情景*pphead == NULL,newnode->next赋值为NULL,然后头指针存放了呢哇node的地址。这种情景下头插一个元素,需要让头指针指向我们的新节点,由于原链表没有元素,所以当插入新节点时,新节点后面没有节点了,newnode->next需要为NULL。这与我们的代码结果一致,所以我们头插函数不需要额外修改什么。 }
-
尾删函数
void SListPopBack(SLTNode** pphead) { //思路:我们需要拿到最后一个节点的地址,将这块空间释放掉;同时我们需要拿到原来链表的倒数第二个节点的地址,将它的next置成NULL。 //怎样拿到我们倒数两个节点的地址呢?我们实现这样一个思路:创建两个节点指针变量prev和tail,当tail指向一个节点时,prev指向前一个节点,这样,当我们的tail指向最后一个节点时,prev就指向了倒数第二个节点,如此我们便拿到了倒数两个指针变量的地址。 //之前实现尾插函数时,我们实现过拿到最后一个节点的代码。我们对此加以修改,保留while循环的条件tail->next != NULL,在循环体内部增加一条语句:prev = tail,每次tail赋新值前,用prev接收现在的值,这样便得到了上述思路的结果。 //每次实现接口函数都要考虑极端情况,我们考虑这样一个情景:原链表没有元素,即我们的头指针的值为NULL。在没有元素可删的情境下,我们直接退出函数就好了。 //还有一种容易忽略的情景:我们的原链表只有一个元素。我们走一遍循环附近的代码:我们创建了两个指针变量prev和tail分别赋值NULL和头指针的值,判断循环条件tail->next != NULL,不满足循环条件,不进入循环体,prev和tail的值不变,接着,运行循环后面的代码prev->next = NULL,prev此时为NULL,对NULL解引用会报错。至此,我们发现了问题。 if(*pphead == NULL) { return; } else if((*pphead)->next == NULL) { free(*pphead); *pphead = NULL; } else { SLTNode* prev = NULL; SLTNode* tail = *pphead; while(tail->next != NULL) { prev = tail; tail = tail->next; } prev->next = NULL; free(tail); } }
-
头删函数
void SListPopFront(SLTNode** pphead) { //思路:让头指针指向第二个节点,然后将第一个节点释放掉。我们的头指针存放的是第一个节点的地址,第一个节点的next存放的就是第二个节点的地址。 //考虑链表没有元素的情况,这个容易解决。同时考虑只有一个元素的情况,我们在这种情景下走一下最后三个语句,首先我们创建一个变量存放链表第一个节点的next的值,也就是第二个节点的地址,此时tmp内存储的是NULL,释放第一个节点的空间,将tmp(NULL)赋给头指针*pphead,结果是删除了这一个元素,没有出现问题,所以我们不需要修改。 if(*pphead == NULL) { return; } SLTNode* tmp = (*pphead)->next; free(*pphead); *pphead = tmp; }
-
查找函数
SLTNode* SListFind(SLTNode* phead, SLTDateType x) { //传入想要寻找的数据,查找函数会返回这个数据所在节点的地址 SLTNode* cur = phead; while(cur != NULL) { if(cur->date == x) { return cur; } cur = cur->next; } return NULL; }
-
插入函数
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x) { //要在pos这个位置插入一个新节点,需要让新节点的next指向pos节点,让pos前一个节点的next指向新节点。 //我们需要拿到pos位置前一个节点的地址 //如果我们想在第一个位置插入新节点呢?pos的值等于*pphead,在寻找pos位置时会直接跳过第一个节点的位置,所以我们写if语句判断pos,pos指向第一个节点,相当于头插。 if(*pphead == pos) { SListPushFront(pphead, x); } else { SLTNode* newnode = BuyListNode(x); SLTNode* cur = *pphead; while(cur->next != pos) { cur = cur->next; } cur->next = newnode; newnode->next = pos; } }
我们在想在某个数据的位置插入新数据时怎么办?我们实现的函数需要的是要插入的节点的地址,我们可以这么做:
//假如头指针为plist,我们想在数据5的位置插入一个新数据100 SLTNode* pos = SListFind(plist, 5); //查找函数若没有找到5,会返回NULL,所以我们先判断 if(pos != NULL) { SListInsert(&plist, pos, 100); }
以上是我们实现插入函数以及使用方法。
-
删除函数
void SListErase(SLTNode** pphead, SLTNode* pos) { //思路:我们利用pos拿到它的下一个节点的位置,然后让pos前一个节点的next指向pos下一个节点的位置,最终将pos释放掉。 //如果我们想删除第一个节点,pos的值等于*pphead,在寻找pos位置时会直接跳过第一个节点的位置,所以我们写if语句判断pos,而pos指向第一个节点,相当于头删。 if(pos == *pphead) { SListPopFront(pphead); } else { SLTNode* cur = *pphead; while(cur->next != pos) { cur = cur->next; } cur->next = pos->next; free(pos); } }
我们完成了一些常见的接口函数,当然还有其他的接口函数,不过都是建立在理解这些基本的接口函数的基础上。
完整代码
//SList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
typedef int SLTDateType;
struct SListNode
{
SLTDateType date;
struct SListNode* next;
};
typedef struct SListNode SLTNode;
//不改变链表的头指针,传一级指针
void SListPrint(SLTNode* phead);
SLTNode* BuySListNode(SLTDateType x);
//可能改链表的头指针,传二级指针
void SListPushBack(SLTNode** pphead, SLTDateType x);
void SListPushFront(SLTNode** pphead, SLTDateType x);
void SListPopBack(SLTNode** pphead);
void SListPopFront(SLTNode** pphead);
SLTNode* SListFind(SLTNode* phead, SLTDateType x);
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x);
void SListErase(SLTNode** pphead, SLTNode* pos);
//SList.c
#include "SList.h"
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->date);
cur = cur->next;
}
printf("NULL\n");
}
SLTNode* BuySListNode(SLTDateType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->date = x;
newnode->next = NULL;
return newnode;
}
void SListPushBack(SLTNode** pphead, SLTDateType x)
{
SLTNode* newnode = BuySListNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SListPushFront(SLTNode** pphead, SLTDateType x)
{
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SListPopBack(SLTNode** pphead)
{
//1.链表为空
if (*pphead == NULL)
{
return;
}
//链表只有一个节点
else if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//链表至少有两个节点
else
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
void SListPopFront(SLTNode** pphead)
{
if (*pphead == NULL)
{
return;
}
SLTNode* tmp = (*pphead)->next;
free(*pphead);
*pphead = tmp;
}
SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
SLTNode* cur = phead;
while (cur != NULL)
{
if (cur->date == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
//如果要在第一个位置插入新数据,相当于头插,调用头插函数
if (pos == *pphead)
{
SListPushFront(pphead, x);
}
else
{
SLTNode* newnode = BuySListNode(x);
newnode->next = pos;
SLTNode* cur = *pphead;
while (cur->next != pos)
{
cur = cur->next;
}
cur->next = newnode;
}
}
void SListErase(SLTNode** pphead, SLTNode* pos)
{
if (pos == *pphead)
{
SListPopFront(pphead);
}
else
{
SLTNode* cur = *pphead;
while (cur->next != pos)
{
cur = cur->next;
}
cur->next = pos->next;
free(pos);
}
}