C语言实现单链表
if ((*pphead)->next == NULL)//除了单链表为空这种情况以外,还有一种情况需要我们考虑一下,就是单链表中有几个节点,如果单链表中只有一个节点,那么最后一个节点就是第一个节点,将最后一个节点的空间释放掉之后,相应的头指针就会变成一个野指针,有一定的风险,因此,这种情况下,要将头指针置为NULL,反之,如果单链表中的节点在两个及其以上的化,就不需要将头指针置空。//因为是要删
目录
这部分内容建议大家最好在电脑上看。
OK,各位未来的高级程序员们,今天,我们来讲一下单链表相关的内容,相信肯定有同学对于这部分相关的知识是非常期待的,那么,话不多说,精彩如下所示:
链表介绍:
链表{ 物理结构:不一定线性
{ 逻辑结构:一定线性
链表本质上是由1个或者多个节点依靠指针连接在一起的,如下图所示:
链表它在本质上和顺序表的区别是在于是使用哪一种结构实现的,顺序表它是通过数组来实现的,而链表本质上是通过指针来指向下一块空间的位置,就是通过指针的这种连接来将一块块在同一个内存中的不同位置所开创的一样大小的空间连接到一起。这样就形成了上面那副图的结构(上图仅限在逻辑方面上),而在实际的内存中,这些相同大小的空间是分布在一块空间中的不同的地方的,这样就使得在连接时会显得有点乱。
出现这种情况的原因还是因为我们的操作系统在进行空间创建和分配的时候时随机的,我们无法知道它创建的空间在内存中的具体位置(非要知道的话,可以通过地址知道位置)。
一.单链表专题:
单链表属于一种特殊的线性表,它在逻辑结构上是连续的,但是在物理结构上是单链表中的各个节点分布在内存中的不同的地方,各个节点之间仅仅只是靠着指针相连的。
1.实现:
(1).单链表节点的创建
(我们将一个节点中应有的数据存放在一个结构体中,方便传送):
typedef int SLTDataType;//单链表中存放的数据它不只能存放固定一种类型的元素,还有可能存放char类型等等一些其他不同的类型,因此我们这里将int这个类型改一个名字,方便我们下次想再次使用该单链表存放一些其他类型的数据时容易替换,否则,假如说你建立了一个拥有10000个节点的单链表,里面的数据类型全部都是int类型,这时突然想存放char类型的数据,就需要将int 全部换为char类型,太过于麻烦。
typedef struct SListNode
{
SLTDataType val;//该节点中存放的值。
struct SListNode* next;//指向下一个节点的指针。
}SLTNode;
(2).单链表的初始化:
void SLTinit(SLTNode** pphead)//这里我们是将结构体的地址传过来,既然我们传过来的是地址,那么我们就必须要使用指针来接收地址,这里我们来解释一下为什么是使用二级指针来接收地址呢,因为这里我们传过来的是一级指针的地址,我们在建立好一个单链表之后,就需要再使用一个指针来指向第一个节点的位置,这个指针我们叫头指针,我们将头指针的地址传过来,对其解引用后,那么此时就相当于是得到的了头指针,那么,这样的话,我们后面对参数进行的一系列操作,就相当于是在实参中进行的一系列操作,其实这里的传址主要是针对于刚开始还没有建立单链表的时候,也就是头指针指向NULL的时候,如果这时候我们只是传值过去,也就是将NULL传过去了,那么这样的话,形参就会在另一块地址创建一块空间,将传过来的NULL放到里面,那么,这个时候在调用函数里实现的一些事情,就是在形参开创的那块空间实现的,实参不改变,因此,为了防止这种情况的发生,我们直接将头指针的地址传过去就可以了,那么这样的话对二级指针解引用的话,我们得到的就是头指针。
{
*pphead = NULL;//对pphead二级指针解引用之后得到的就是头指针,因此,我们可以直接将其置为NULL。
}
(3).单链表的打印:
因为我们这里是直接将单链表中的值一一给打印出来,不需要对单链表进行一些修改,因此这里不需要传一级指针的地址。
void SLTPrint(SLTNode* phead)
{
assert(phead);
SLTNode* pcur = phead;//我们一般在进行一系列有关单链表的遍历的相关操作时,一般不会使用头指针直接对单链表进行遍历,而是另外定义一个pcur指针指向头指针指向的地址,让新定义的这个pcur指针去遍历单链表。
while (pcur)//当pcur指针指向NULL时,说明单链表已经遍历完了,结束循环。
{
printf("%d ", pcur->val);
pcur = pcur->next;//当打印出当前节点中所存储的数据时,让pcur指针指向当前节点的下一个节点的位置即可。
}
printf("\n");
}
(4).开创一个节点:
单链表是由多个节点用指针连接起来的一个线性表,因此,为了方便我们后续在进行插入元素操作时建立新的节点,因此,我们这里写一个创建节点的函数。
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//创建节点我们使用malloc函数去创建一个节点。
if (newnode == NULL)
{
perror(newnode);
return 1;
}//这一步是判断节点有没有创建成功。
//以下操作是为新节点中相对应的变量进行赋值操作。
newnode->val = x;
newnode->next = NULL;
return newnode;//返回新节点的地址。
}
(5).尾部删除:
删除单链表最后以一个节点。
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);//这里我们得确定pphead指针是否为NULL,如果为NULL则该单链表没有节点。
if ((*pphead)->next == NULL)//除了单链表为空这种情况以外,还有一种情况需要我们考虑一下,就是单链表中有几个节点,如果单链表中只有一个节点,那么最后一个节点就是第一个节点,将最后一个节点的空间释放掉之后,相应的头指针就会变成一个野指针,有一定的风险,因此,这种情况下,要将头指针置为NULL,反之,如果单链表中的节点在两个及其以上的化,就不需要将头指针置空。
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = *pphead;//我们在这里定义两个指针都先指向头节点,prev这个指针到时候是指向尾结点的前一个节点。
SLTNode* pail = *pphead;//pail指针到时候是指向尾节点。
//为什么要设立两个节点呢,因为我们通过前面对单链表的介绍可知,单链表是单向的链表,要想找到某一个节点如果不知道这个节点的地址的话,就只能一遍一遍的通过遍历去寻找了,当你将最后一个链表删除了之后,倒数第二个节点的next指针还指向的是这个位置,必须将倒数第二个节点的next指针置为NULL,否则有风险。
while (pail->next)
{
prev = pail;
pail = pail->next;
}循环遍历数组,让pail指针指向单链表的最后一个节点,prev指针指向单链表的倒数第二个节点。
free(pail);//释放最后一个节点所开创的空间。
pail = NULL;
prev->next = NULL;//将倒数第二个节点的next指针置为NULL。
}
}
(6).头部插入:
在单链表的头部再插入一个节点,让新插入的这个节点成为单链表新的头,并让头指针指向这个新的头。
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);//判断ppheaad指针是否接收到地址。
SLTNode* newnode = SLTBuyNode(x);//创建一个新的节点,让这个新开创的节点作为新的头插入到单链表中。
newnode->next = *pphead;//新节点的next指针指向头指针指向的位置,这样,原单链表中的头就变成了新单链表的第二个节点。
*pphead = newnode;//由于头指针是指向单链表的第一个元素,因此,这里还需要让头指针指向新单节点的位置。
}
(7).尾部插入:
就是在单链表的最后再插入一个节点。
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);//判断ppheaad指针是否接收到地址。
SLTNode* newnode = SLTBuyNode(x);//创建一个新的节点,让这个新开创的节点作为新的尾节点插入到单链表中。
if (*pphead == NULL)
{
*pphead = newnode;
}//这里我们需要其判断一下单链表是否为NULL,若为NULL,则说明单链表中没有节点,那么,这个新建的的节点就是首节点,这时直接让头指针指向这个新节点就可以了。
else
{
SLTNode* pail = *pphead;//定义一个尾指针,到时候让其指向单链表中最后一个节点。
while (pail->next)
{
pail = pail->next;//将pail指针指向单链表的最后一个节点。
}
pail->next = newnode;//这时再让pail指针的next指针指向新开创的节点即可。
}
}
(8).头部删除:
删除第一个节点
void SLTPopFront(SLTNode** pphead)
{
assert(pphead&&*pphead);//因为是要删除第一个节点,我们就得保证单链表中至少有一个节点,因此,我们也要对*pphead执行一下断言操作。
SLTNode* next = (*pphead)->next;//定义一个节点指针,让其指向单链表中的第二个节点,当这里我们将头指针指向的第一个节点删除了以后,如果我们步定义一个指针指向单链表的第二个节点,那么,就会找不到第二个节点的地址,因此,我们这里必须定义一个结点指针指向单链表中的第二个节点的位置。
free(*pphead);//释放头指针指向的那块空间。
*pphead = next;//头指针指向原单链表的第二个节点的位置,使其成为新的头节点。
}
(9).查找某一元素:
给定一个元素,让我i们到单链表中去找这个元素,若有,返回这个元素在单链表中对应的节点的地址。
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
assert(phead);//判断ppheaad指针是否接收到地址。
SLTNode* pcur = phead;//定义一个指针,指向头指针指向的位置
while (pcur)//遍历一遍单链表
{
if (pcur->val == x)//判断pcur指针指向的那个节点中存储的值是否与x相同
{
return pcur;//若相同,则返回当前节点的地址
}
pcur = pcur->next;//若不相同,则让pcur指针指向下一个节点的位置
}
return NULL;//若程序运行到这里,则说明单链表中没有一个节点中存储的元素是x
}
(10).在指定位置之前插入数据:
在pos这个指定位置之前插入一个节点
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//前提得保证这个要插入元素的这个地址必须在单链表范围内
{
assert(pphead && *pphead);//判断ppheaad指针是否接收到地址,原因这里是要某个指定的位置之前插入元素,那么我们就得保证单链表中必须不为空,也就是必须有节点。
assert(pos);//这里是要某个指定的位置之前插入元素,所以我们得保证pos这个位置必须不能为空,。
SLTNode* newnode = SLTBuyNode(x);//开创一个新节点
if (pos == *pphead)//因为我们这里是要在指定位置之前插入一个新的节点,所以我们得让找到指定位置之前的那个地址,但我们在写这里的代码时,我们需要注意一件事情,就是pos指针指向的位置是不是头指针的位置,因为我们后面在找指定位置之前的地址时,要定义一个指针pcur去遍历单链表,因此,我们要找pcur指针指向的next指针是否指向pos,如果这里pos指针指向的位置是在头指针的话,那么,我们在执行上面的过程的话,就会错过pos指针指向的位置从而就会导致一种结果的出现,就是找不到pos指针指向的位置。
{
SLTPushFront(pphead, x);//头插操作。
}
else//如果pos指针指向的位置不是头指针指向的位置,通过遍历来找到pos指针指向的位置的前一个位置。
{
SLTNode* pcur = *pphead;//定义一个指针用来遍历单链表来找到pos指针指向的位置的前一个位置。
while (pcur->next != pos)
{
pcur = pcur->next;
}//结束此循环的话,就说明找到pos指针指向的位置的前一个位置了。
newnode->next = pos;//让新指针的next指针指向pos指针指向的节点。
pcur->next = newnode;pcur指针的next指针指向新节点。
}
}
(11).删除pos节点:
删除特定位置的节点。
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);//判断ppheaad指针是否接收到地址,原因这里是要删除某个位置的节点,那么我们就得保证单链表中必须不为空,也就是必须有节点,我们后面要让pos指针指向的节点的前面的那个节点的next指针指向pos指针指向的位置的后面那个节点的地址。
assert(pos);//这里是要删除某个指定的位置的节点,所以我们得保证pos这个位置必须不能为空。
if (pos == *pphead)//在我们进行删除操作之前,要注意一件事情,就是pos指针指向的位置的那个节点是不是头节点,如果是的话,我们在对头节点进行释放之后,那么此时头指针指向位置还是头节点的位置,但是此时的头节点已经被释放,那头指针就变成了一个野指针,为了安全考虑,这里需要将头指针置空,如果pos指针指向的位置不是头节点,就无需置空。
{
SLTPopFront(pphead);//头删。
}
else
{
SLTNode* prev = *pphead;//定义一个指针用来遍历单链表来找到pos指针指向的位置的前一个位置。
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);//释放pos指针指向的节点。
pos = NULL;//为了程序的安全,将其置空。
}
}
(12).删除pos之后的节点:
void SLTEraseAfter(SLTNode* pos)
{
assert(pos&&pos->next);//判断ppheaad指针是否接收到地址,原因这里是要删除某个位置之后的节点,那么我们就得保证单链表中必须不为空,也就是必须有节点,我们后面要让pos指针指向的节点的next指针指向pos指针指向的位置的后面那个节点的地址。
SLTNode* pcur = pos->next;//定义一个指针指向pos指针在这些的节点后面的那个节点。
pos->next = pcur->next;//让pos指针指向的节点的next指针指向pos指针指向的位置的后面那个节点的地址。
free(pcur);//释放pos指针指向的节点的下一个节点。
pcur = NULL;//将其置空。
}
(13) 销毁单链表:
将单链表中所有的节点全部都释放掉。
void SListDesTroy(SLTNode** pphead)
{
assert(pphead&&*pphead);//与前面的解释类似。
SLTNode* pcur = *pphead;//定义一个指针用来遍历单链表一次将单链表中的节点一一释放掉。
while (pcur)//通过循环来实现对节点的一一释放。
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;//释放结束后,头指针就变成了野指针,出于安全考虑,将其置空。
}
OK,以上内容就是单链表的所有内容了,大家若有疑问,请在评论区告诉我呦。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)