什么是单链表?
概念:链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
结点
链表里的每节都是独立申请下来的空间,称作“结点”。
链表中每个结点都是独⽴申请的(即需要插⼊数据时才去申请⼀块结点的空间),我们需要通过指针变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。
链表的性质
1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续。
2、结点⼀般是从堆上申请的。
3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不 连续。
实现单链表
1.SLT.h
创建头文件SLT.h,对单链表所需的函数进行声明,指明各个函数所代表的作用。
为结点创建结构体,其中要包含存储的数据,以及指向下一个结点的同类型结构体指针。
//SLT.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTdatatype;
typedef struct SListnode
{
SLTdatatype data;
struct SListnode* next;
}SLTnode;
//打印单链表
void SLTprint(SLTnode* phead);
//申请新结点
SLTnode* SLTnewnode(SLTdatatype x);
//尾插
void SLTPushBack(SLTnode** phead, SLTdatatype x);
//头插
void SLTPushFront(SLTnode** phead, SLTdatatype x);
//尾删
void SLTPopBack(SLTnode** phead);
//头删
void SLTPopFront(SLTnode** phead);
//查找
SLTnode* SLTFind(SLTnode* phead, SLTdatatype x);
//在指定位置之前插入数据
void SLTZhiBack(SLTnode** phead, SLTnode* pos, SLTdatatype x);
//在指定位置之后插入数据
void SLTZhiAfter(SLTnode* pos, SLTdatatype x);
//删除pos结点
void SLTErease(SLTnode** phead, SLTnode* pos);
//删除pos之后的结点
void SLTEreaseAfter(SLTnode* pos);
//销毁链表
void SLTdestroy(SLTnode** phead);
2.SLT.c
创建源文件SLT.c对声明的函数进行具体的定义。
1)打印单链表
因为在单链表中,每个指针都只能指向其所指向的下一个指针,而不能指向前一个指针。所以在打印单链表时,我们要创建一个新的结构体指针指向形参,通过这个新的指针来实现打印。
//创建一个新的结构体指针
SLTnode* pcur = phead;
利用while循环实现单链表的打印。
首先需要打印pcur所处在的第一个结点的数据,然后将pucr指向下一个结点,并进行打印,直到pcur遇到NULL。
需要注意的是,如果单链表本身为空,则直接打印NULL。
//打印单链表
void SLTprint(SLTnode* phead)
{
SLTnode* pcur = phead;//定义一个结构体指针,指向形参,保证形参指针仍在原位
while (pcur)
{
printf("%d->", pcur->data);//打印结点中的数据
pcur = pcur->next;//指向下一个结点
}
printf("NULL\n");//若本身为空,则直接打印NULL,否则在最后打印NULL
}
2)申请新结点
利用malloc函数动态开辟一个新的内存,内存大小为结构体指针大小。
如果内存开辟失败,则及时提示错误信息,并且退出函数。
内存开辟成功,则对数据信息进行完善,指针指向先置为NULL。
//申请新结点
SLTnode* SLTnewnode(SLTdatatype x)
{
SLTnode* node = (SLTnode*)malloc(sizeof(SLTnode));//创建一个结点
if (node == NULL)
{
perror("malloc");
exit(1);
}
node->data = x;//在结点中输入相应的数据
node->next = NULL;//将next置为空
}
3)尾插
在进行尾插的时候,我们需要对指向指针的指针进行修改,所以在传参时我们需要用到二级指针。(实参为指针,其中储存着变量,传参时需要实参的地址,需要用二级指针接受)
首先判断第一个形参地址是否为空。
申请一个新的结点,导入需要尾插的数据。
然后需要考虑到两种情况:1.当结点内data数据为空时,我们可以直接插入。
2.当结点内data数据不为空时,需要设置一个指针引导至结点尾 部插入。
//尾插
void SLTPushBack(SLTnode** phead, SLTdatatype x)
{
//二级指针,指向指针的指针
assert(phead);
//phead --> &plist
// *phead --> plist
SLTnode* newnode = SLTnewnode(x);//申请一个新的结点newnode,并且导入需要尾插的数据
if (*phead == NULL)//如果结点为空,则直接进行插入
{
*phead = newnode;
}
else
{
//pcur为指针指向phead的地址
SLTnode* pcur = *phead;//结点不为空,设置一个pcur指针引导至结点尾部进行插入,插入数据为newnode
while (pcur->next)//引导至最后一个结点位置
{
pcur = pcur->next;
}
pcur->next = newnode;//在NULL出插入新的结点,newnode指针指向NULL
}
}
4)头插
在进行头插的时候,我们需要对指向指针的指针进行修改,所以在传参时我们需要用到二级指针。(实参为指针,其中储存着变量,传参时需要实参的地址,需要用二级指针接受)
首先创建一个新的结点,将结点指向第一个结点,然后将首个结点更替为新添加的结点。
//头插
void SLTPushFront(SLTnode** phead, SLTdatatype x)
{
assert(phead);
//phead --> &plist
// *phead --> plist
SLTnode* newnode = SLTnewnode(x);
newnode->next = *phead;//指向plist
*phead = newnode;//首个结点更替为新添加的结点
}
5)尾删
在进行尾删的时候,我们需要对指向指针的指针进行修改,所以在传参时我们需要用到二级指针。(实参为指针,其中储存着变量,传参时需要实参的地址,需要用二级指针接受)
需要考虑两种情况:1.当只有一个结点时,即 (*phead)->next == NULL 时,直接释放 *phead 的空间,将其置为NULL。
2.不止一个结点,找到最后一个结点进行删除。设置两个指针,一个指 向第一个结点,一个置为NULL。利用while循环找到该链表最后一个 结点 while(ptail->next) ,通过第二个指针记录找到倒数第二个数 据,并且将下一个数据置为空,释放 ptail 内存,置为NULL。
//尾删
void SLTPopBack(SLTnode** phead)
{
//链表为空时,无法进行尾删
assert(phead);
//phead --> &plist
// *phead --> plist
if ((*phead)->next == NULL)//如果只有一个结点
{
free(*phead);
*phead = NULL;
}
else//找到最后一个结点进行删除
{
SLTnode* ptail = *phead;
SLTnode* prev = NULL;
while (ptail->next)
{
prev = ptail;//记录倒数第二个数据
ptail = ptail->next;//往后找到最后一个数据
}
prev->next = NULL;//将最后一个数据置为NULL
free(ptail);
ptail = NULL;
}
}
6)头删
在进行头删的时候,我们需要对指向指针的指针进行修改,所以在传参时我们需要用到二级指针。(实参为指针,其中储存着变量,传参时需要实参的地址,需要用二级指针接受)
需要判断实参的地址和内容是否为空。
定义一个结点指向第一个结点后的一个结点,释放第一个结点,而后将第一个节点的位置设置成刚刚定义的新结点。
//头删
void SLTPopFront(SLTnode** phead)
{
assert(phead && *phead);
//phead --> &plist
// *phead --> plist
SLTnode* next = (*phead)->next;//定义一个结点
free(*phead);
*phead = next;
}
7)查找
首先要判断链表是否为空。
定义一个结点指向第一个结点,利用while循环遍历每一个结点的内容,当内容符合时,返回对应结点,如果没有符合内容,跳出while循环,返回NULL。
/查找
SLTnode* SLTFind(SLTnode* phead, SLTdatatype x)
{
assert(phead);
SLTnode* pcur = phead;
while (pcur)//遍历链表
{
if (pcur->data == x)//找到了符合条件的数据
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
8)在指定位置之前插入数据
在进行插入的时候,我们需要对指向指针的指针进行修改,所以在传参时我们需要用到二级指针。(实参为指针,其中储存着变量,传参时需要实参的地址,需要用二级指针接受)
需要判断 链表 和 指定位置 是否为空。
分两种情况进行判断:1.如果指定位置为第一个结点,在第一个位置处进行头插操作。
2.随机位置。创建一个新的结点和新的指针prev,利用while循环遍历 链表查找pos的位置,当找到位置时,将newnode指向pos,并且将 prev指向newnode,完成链表的连接。
//在指定位置之前插入数据
void SLTZhiBack(SLTnode** phead, SLTnode* pos, SLTdatatype x)
{
assert(phead);
assert(pos);
if (pos == *phead)
{
SLTPushFront(phead, x);//如果指定位置为第一个,进行头插
}
else
{
SLTnode* newnode = SLTnewnode(x);//位于prev和pos之间的中间结点,接受prev指向,指向pos
SLTnode* prev = *phead;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;//数据插入后改变前后指针指向,保证链表逻辑连续
prev->next = newnode;
}
}
9)在指定位置之后插入数据
需要判断指定位置pos是否为空。
创建一个新结点,将newnode指向pos所指向的结点,将pos所指向的结点修改为newnode,完成在pos之后插入newnode。
//在指定位置之后插入数据
void SLTZhiAfter(SLTnode* pos, SLTdatatype x)
{
assert(pos);
SLTnode* newnode = SLTnewnode(x);
newnode->next = pos->next;
pos->next = newnode;
}
10)删除pos结点
在进行删除的时候,我们需要对指向指针的指针进行修改,所以在传参时我们需要用到二级指针。(实参为指针,其中储存着变量,传参时需要实参的地址,需要用二级指针接受)
需要判断 链表 、第一个结点的内容 和 pos结点是否为空。
分两种情况来判断:1.当pos结点为第一个结点时,对第一个结点进行头删操作。
2.当pos为随机结点时。设置prev结点,利用while循环遍历链表查找pos 的位置。找到后,将prev指向pos所指向的下一个结点,释放pos,将 pos置为空。
//删除pos结点
void SLTErease(SLTnode** phead, SLTnode* pos)
{
assert(phead && *phead);
assert(pos);
if (pos == *phead)
{
SLTPopFront(phead);//如果为第一个节点,则进行头删
}
else
{
SLTnode* prev = *phead;
while (prev->next != pos)//找到pos的位置
{
prev = prev->next;
}
prev->next = pos->next;//改变指针指向,保证连续
free(pos);
pos = NULL;
}
}
11)删除pos之后的结点
需要判断 pos 和 pos之后的结点是否为空。
创建一个新的指针指向pos之后的一个结点。将pos所指向的下一个结点修改为下下个结点,而后释放新指针的内存,将其置为NULL。
//删除pos之后的结点
void SLTEreaseAfterr(SLTnode* pos)
{
assert(pos && pos->next);
SLTnode* del = pos->next;
pos->next = pos->next->next;
free(del);
del = NULL;
}
12)销毁链表
在进行销毁的时候,我们需要对指向指针的指针进行修改,所以在传参时我们需要用到二级指针。(实参为指针,其中储存着变量,传参时需要实参的地址,需要用二级指针接受)
需要判断链表和第一个结点的内容是否为空。
创建一个 pcur 指针指向第一个结点。利用while循环遍历,创建一个新的 next 指针指向pcur的下一个结点,释放pcur的内存,然后将pcur指向next。以此为循环,一个个从头到尾销毁链表的每一个结点,最后将链表置为NULL。
//销毁链表
void SLTdestroy(SLTnode** phead)
{
assert(phead && *phead);
SLTnode* pcur = *phead;
while (pcur)//遍历链表,删除一个,指向下一个,再删除
{
SLTnode* next = pcur->next;
free(pcur);
pcur = next;
}
*phead = NULL;
}
3.test.c
对各个函数的功能进行具体的测试,分析出错位置,进行修改与完善。
#include"SLT.h"
void SLTtest01()
{
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;
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
SLTnode* plist = node1;
SLTprint(plist);
}
void SLTtest02()
{
SLTnode* plist = NULL;
//SLTPushBack(&plist, 1);
//SLTPushBack(&plist, 2);
//SLTPushBack(&plist, 3);
//SLTPushBack(&plist, 4);
//SLTprint(plist);//1->2->3->4->NULL
/*SLTPushBack(NULL, 3);*/
SLTPushFront(&plist, 1);
SLTPushFront(&plist, 2);
SLTPushFront(&plist, 3);
SLTPushFront(&plist, 4);
SLTprint(plist); //4->3->2->1->NULL
/*SLTPopBack(&plist);
SLTprint(plist);
SLTPopBack(&plist);
SLTprint(plist);
SLTPopBack(&plist);
SLTprint(plist);
SLTPopBack(&plist);
SLTprint(plist);*/
/*SLTPopFront(&plist);
SLTprint(plist);
SLTPopFront(&plist);
SLTprint(plist);
SLTPopFront(&plist);
SLTprint(plist);
SLTPopFront(&plist);
SLTprint(plist);*/
SLTnode* find = SLTFind(plist, 4);
if (find == NULL)
{
printf("δҵ\n");
}
else
{
printf("ҵˣ\n");
}
SLTZhiBack(&plist, find, 11);//4->3->2->11->1->NULL
//SLTZhiAfter(find, 11);
//SLTprint(plist);1->11->2->3->4->NULL
//SLTErease(&plist, find);// 1->2->3->NULL
//SLTEreaseAfter(find);
SLTprint(plist);
SLTdestroy(&plist);
SLTprint(plist);
}
int main()
{
/*SLTtest01();*/
SLTtest02();
return 0;
}
总结
对于一个刚刚学习单链表的c++菜鸟来说,其中各个函数的具体定义时而繁琐,时而抽象。每一个函数在具体定义完之后也需要进行及时的调试来判断是否成功,防止最后进行调试时工程过于庞大。一次两次的练习不足以将这些内容牢记于心,只有不断巩固完善,才能信手拈来。