当前位置: 首页 > news >正文

一起学数据结构(3)——万字解析:链表的概念及单链表的实现

上篇文章介绍了数据结构的一些基本概念,以及顺序表的概念和实现,本文来介绍链表的概念和单链表的实现,在此之前,首先来回顾以下顺序表的特点:

1.顺序表特点回顾:

1. 顺序表是一组地址连续的存储单元依次存储的线性表的数据结构,逻辑上:顺序表中相邻的数据元素,其物理次序也是相邻的。

2. 顺序表的优点: 任一元素均可以随机存取

3.顺序表的缺点:进行插入和删除操作时,需要移动大量的元素,存储空间不灵活。

2. 链表的分类及概念:

2.1 链表的分类:

1.单链表:结点只有一个指针域的链表,称之为链式线性表或者单链表:

   

2.双链表:结点由两个指针域的链表:

3.循环链表:首尾相连的链表:

 本文将着重介绍单链表,下面给出单链表的概念及相关特点:

2.2 单链表的概念即特点:

单链表指的是链表的每个结点中只包含一个指针域,对于链表,下面给出定义:

线性表链式存储的结构是:用一组任意的存储单元 存储线性表的数据元素(这组存储单元可以连续,也可以不连续)。为了表示每个数据元素a_i与其后记数据元素^{a_{i+1}}之间的逻辑关系,所以,对于存储数据元素a_i的存储单元,还需要存储一个指示后记数据元素^{a_{i+1}}的相关信息,(即存储^{a_{i+1}}所在的地址)。这两部分信息构成了数据元素a_i的存储映像,称为结点。

对于结点,包括了两个域:存储数据元素信息的域称之为数据域,存储后记元素存储位置的域称之为指针域。指针域中存储的信息称作指针或者链。n个结点链成一个链表,即为线性表的链式存储结构

(注:对于链表更详细的定义可以参考人民邮电出版社出版的《数据结构》,本文不再过多说明)

3.单链表的代码实现:

3.1 链表的存储结构及结点的创建:

单链表的定义中提到了,链表是由n个结点构成的,每个结点中包含了两个与,一个是用于存储数据元素信息的数据域,另一个是用于存储下一个数据元素信息地址的指针域。不同类型的信息的保存,可以由结构体进行实现,所以,下面用图给出单个结点的结构:

其中,data表示存储的数据元素信息,next表示下一个数据元素信息的地址。并且,将每一个这种结点命名为newnode,对上述结点用代码表示:

typedef int SLTDataType;
typedef struct SListNode
{SLTDataType data;// 对应了存储的元素信息struct SListNode* next; // 对应了下一个数据元素信息的地址
}SLTNode;

对于链表,也和顺序表一样,可以实现增删查改各种功能,而实现这些功能的基础,就是如何创造新的结点,为了解决这个问题,可以专门定义一个函数BySListNode来实现。前面说到,链表各个结点之间的链接是通过某个结点存储下一个结点的地址来实现的。所以,对于函数BySListNode的返回类型,应该定义为SLTNode*型,即返回结构体的地址。

对于一个结点的创建,同样可以采用在顺序表中用动态内存函数malloc动态开辟内存的方式进行实现。具体代码如下:

SLTNode* BuySListNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}

 下面给出代码来测试函数BySListNode的功能:

为了测试函数的功能,首先需要针对链表来封装一个打印函数,将打印函数命名为SLTPrint,代码如下:

void SLTPrint(SLTNode* phead)
{SLTNode* cur = phead;while (cur != NULL){printf("%d->", cur->data);cur = cur->next;}
}

在链表的定义中说过,链表取任意元素必须从头节点开始向后依次寻找。所以,为了防止头指针的值被更改,后续无法找到第一个结点,所以,在上述代码中,创建了一个新的变量cur用来存放头指针phead中的地址。

对于cur = cur->next;这一行代码,是用于让cur保存下一个结点的地址,例如下图中cur的变化所示:

 下面,封装一个函数Testlist1来测试插入结点的功能,代码初步表示如下:

void Testlist()
{printf("请输入想要插入的元素的个数:");int n = 0;scanf("%d", &n);printf("请输入插入的元素:");for (size_t i = 0; i < n; i++){int val = 0;scanf("%d", &val);SLTNode* newnode = BuySListNode(val);}
}
int main()
{Testlist1();return 0;
}

但是对于上面给出的代码,并不能完成对多个结点的数据元素的打印,因为在创建结点后,并没有将前后的结点进行链接。同时,也并没有出现上面图中所说的头指针。

为了建立前后结点的链接。所以,在上面Testlist1函数中的代码的基础上,人为建立一个头指针,定义为plist,赋值为NULL

(注:下面为了方便进行演示,对于结点之间建立的过程采用头插的思想,但是并不等价于后面的头插)

对于链接各个结点,需要分以下两种情况:

1.头指针为NULL,即还未链接任何结点,但是已经创建了一个结点:

为了达到链接的效果,只需要将plist中存储的地址改为新结点newnode的地址即可。 

2.头指针不为空:

 此时,plist中已经通过存储第一结点的地址达到链接第一结点的目的,为了链接第二结点,需要将plist中存储的地址改为第二结点的地址。

注释中提到,为了方便演示采用头插的思想,对于头插,可以用下图进行表示:

例如,将地址为 0x0012ffa0的结点进行头插。为了完成头插,需要进行两步操作:

1.将地址为0x0012ffa0的结点(即新结点)中存储的地址改为0x0012ffb0(插入前的第一个结点)

2.将头指针中存储的地址改为0x0012ffa0(即新结点的地址)

对上述分析进行归纳,代码如下:

void Testlist()
{printf("请输入想要插入的元素的个数:");int n = 0;scanf("%d", &n);SLTNode* plist = NULL;printf("请输入插入的元素:");for (size_t i = 0; i < n; i++){int val = 0;scanf("%d", &val);SLTNode* newnode = BuySListNode(val);if (plist == NULL){plist = newnode;}else{newnode->next = plist;plist = newnode;}}SLTPrint(plist);
}

其中,newnode是一个结构体指针,所以需要用到->进行解引用。来改变新结点newnode中存储的下一个数据元素信息的地址。

测试效果如下:

3.2 链表功能实现——尾插: 

定义尾插函数SLTPushBack其内部参数如下面代码所示:

void SLTPushBack(SLTNode* phead, SLTDataType x);

对于尾插这一功能,首先需要找到链表的尾端。前面说到,头指针对于链表的各种功能来说都非常重要,所以,为了保证头指针不被更改,这里定义一个新的结构体指针tail来存储头指针中存储的地址。

对于如何找到尾端,下面给出一段示例的代码:

void SLTPushBack(SLTNode* phead, SLTDataType x)
{SLTNode* newnode = BuySListNode(x);SLTNode* tail = phead;while (tail != NULL){tail = tail->next;}tail = newnode;
}

上述代码给出的思路很明确。利用while循环不断检查指针tail是否为NULL,不为空,则tail存储下一个结点的指针。看似没有错误。但是如果将上述过程用图形表示,则上述代码会引起内存泄漏这一错误,具体如下:

一开始,指针tail存储了头指针phead中的地址,此时tail\neq NULL,于是执行循环内部的代码,此时,tail中存储的地址为0x0012ffb0,效果如下图所示:

依次循环上述步骤:

 再次循环上述步骤:

 到最后一步时,指针tail中存储的地址为NULL。到这里,while循环终止。已经寻找到尾端地址。假设,在循环之前新建立的结点如下图所示:

如果按照上面给出的代码来执行,即:tail = newnode;,则,执行结束后,最后一个结点和指针tail中存储的地址如下:

此时,便会出现一个问题,即,指针tail是只存在与函数内部的一个临时变量,出函数便会销毁。但是,最后一个结点中存储的地址仍然为NULL。最后一个结点和新的结点并未建立联系。造成了内存泄露的问题。 

因为,完成尾插的标志,便是最后一个结点存储的地址被更改为新结点的地址。通过上面的错误例子。可以得出一个结论。如果想要将最后一个结点存储的地址改为新结点的地址,则,不可以让临时指针tail赋值最后一个结点中存储的地址。应该赋值最后一个结点的前一个结点的存储的地址。

再通过这个结点存储的地址,来对最后一个结点存储的地址进行修改。所以,代码可以更改为:
 

void SLTPushBack(SLTNode* phead, SLTDataType x)
{SLTNode* newnode = BuySListNode(x);SLTNode* tail = phead;while (tail ->next != NULL){tail = tail->next;}tail -> next = newnode;
}

通过下面的测试来测试尾插的功能:

void Testlist1()
{printf("请输入想要插入的元素的个数:");int n = 0;scanf("%d", &n);SLTNode* plist = NULL;printf("\n请输入插入的元素:");for (size_t i = 0; i < n; i++){int val = 0;scanf("%d", &val);SLTNode* newnode = BuySListNode(val);if (plist == NULL){plist = newnode;}else{newnode->next = plist;plist = newnode;}}SLTPrint(plist);printf("\n");SLTPushBack(plist, 10000);SLTPrint(plist);
}

结果如下:

上面关于尾插的代码,只能是在有结点的情况下运行。对于头指针为空的情况下并不适用,下面将对上面的尾插代码进行完善:

给出下列代码:

void SLTPushBack(SLTNode* phead, SLTDataType x)
{SLTNode* newnode = BuySListNode(x);if (phead == NULL){phead = newnode;}SLTNode* tail = phead;while (tail ->next != NULL){tail = tail->next;}tail -> next = newnode;
}

这里给出的代码对比尾插,仅仅是多了一个对于头指针phead = NULL的情况的判定。中心思想就是在头指针phead = NULL时,将头指针保存的地址改为第一个结点的地址。但是这种做法并不正确。因为这里所说的头指针phead只是函数内部的一个形式参数。真正的头指针时上面定义的plist。此时,函数形式参数传递的时形参phead中保存的值,在前面关于C语言函数的文章中曾提到函数的两个传递参数的方式:传值和传址。对于传值调用,并不能改变外部变量的值。所以,这里虽然对头指针phead存储的地址进行改变。但是却没有真正的改变函数外部实际参数plist中保存的地址。

对于上面的错误,可以通过传址调用来改变头指针plist中保存的地址。在前面对于C语言函数的文章的介绍中曾写过一个用传址调用来实现交换函数。所以,通过函数来交换两个变量中的值,需要用到一级指针。相对于,对于头指针plist,想要通过函数来改变他的值,需采用二级指针。

所以,正确的尾插代码应为:

void SLTPushBack(SLTNode** phead, SLTDataType x)
{SLTNode* newnode = BuySListNode(x);if (*phead == NULL){*phead = newnode;}else{SLTNode* tail = *phead;while (tail ->next != NULL){tail = tail->next;}tail -> next = newnode;} 
}

运用下面的测试,来测试尾插的功能:

void Testlist2()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPushBack(&plist, 5);SLTPrint(plist);
}

结果如下:

3.3 链表功能实现——头插:

对于头插功能的实现,上面已经给出来了大体思路。但是上面的头插并不是通过函数实现的。根据刚才尾插的实现可以发现。每进行一次头插,都需要对头指针plist中存储的地址进行更改。所以。在函数传递参数时,也需要传递二级指针。

代码如下:

void SLTPushFront(SLTNode** phead, SLTDataType x)
{SLTNode* newnode = BuySListNode(x);newnode->next = *phead;*phead = newnode;
}

用下列代码对头插功能进行测试:

void Testlist3()
{SLTNode* plist = NULL;SLTPushFront(&plist, 1);SLTPushFront(&plist, 2);SLTPushFront(&plist, 3);SLTPushFront(&plist, 4);SLTPushFront(&plist, 5);SLTPushFront(&plist, 6);SLTPrint(plist);
}

结果如下:

3.4 链表功能实现——尾删:

对于尾删功能的实现,需要考虑下面的三种情况:

1. 链表整体为空:

2.链表内只有一个结点

3.链表有多个结点

对第一种情况,只需要在进行尾删功能前,检查一下地址是否合法即可。

对于第三种情况,需要两步。第一步是删除处于尾部的结点。第二种情况是将前一个结点中保存的地址改为NULL即:

 第二种情况也需要分为两步,首先删除尾部结点。再把头指针中存储的地址改为NULL。与第三步不同的时,第三步更改地址的对象是结构体中的一个成员。第二步中更改的对象时头指针。所以,在进行对于第二种情况的地址改动时,需要传递二级指针。

下面先给出针对第一、第二种情况下的代码:

//尾删
void SLTPopBack(SLTNode** phead)
{assert(*phead);if ((*phead)->next == NULL)//只有一个结点的情况{free(*phead);*phead = NULL;}}

对于第三种情况,假设此时有三个结点:

对于尾删功能,与尾插功能相同,第一步都是需要找到尾结点:

寻找尾结点时,采用与尾插寻找尾结点相同的方式,创建一个函数内部的指针变量SLTNode*tail来保存头指针保存的地址。当tail找到尾结点时:

如果此时就删除尾结点,还是会造成在讲解尾插原理中的错误。即,没能更改尾部结点前一个结点中存储的地址。如下图所示:

此时最后一个由malloc函数开辟的结点已经被free 。

为了解决上面的问题,可以在创建一个临时变量也用于保存phead中存储的地址。不过,这个变量的作用是用于更改尾结点前一个结点中存储的地址。这里将这个指针命名为tailprev,再有了这个指针后,当找到尾结点时,这两个指针的关系如下图所示:

先用代码表示指针tail

SLTNode* tail = *phead;SLTNode* tailprev = *phead;while (tail->next != NULL){tail = tail->next;}

 为了达到图中两个指针一前一后的关系,可以让循环中在执行tail = tail -> next之前,让tailprev存储一次tail中的地址。代码如下:

//尾删
void SLTPopBack(SLTNode** phead)
{assert(*phead);if ((*phead)->next == NULL)//只有一个结点的情况{free(*phead);*phead = NULL;}else{SLTNode* tail = *phead;SLTNode* tailprev = NULL;while (tail->next != NULL){tailprev = tail;tail = tail->next;}free(tail);tailprev->next = NULL;}
}

测试功能的代码如下:
 

void Testlist3()
{SLTNode* plist = NULL;SLTPushFront(&plist, 1);SLTPushFront(&plist, 2);SLTPushFront(&plist, 3);SLTPushFront(&plist, 4);SLTPushFront(&plist, 5);SLTPushFront(&plist, 6);SLTPrint(plist);printf("\n尾删");SLTPopBack(&plist);SLTPopBack(&plist);SLTPopBack(&plist);printf("\n");SLTPrint(plist);
}

结果如下:

3.5 链表功能实现——头删:

对于头删功能,依旧分为以下三种情况:

1. 链表整体为空:

2.链表内只有一个结点

3.链表有多个结点

对于第一种情况,直接检查头指针合法性即可。对于第三种情况,即多个结点,需要分为两步来完成头删:首先将头指针存储的地址改为第二个结点的地址。再把第一个结点free。对于第二种情况,和第三种情况相同,虽然只有一个结点。但是可以将NULL看作第二个结点的地址。代码如下:

//头删
void SLTPopFront(SLTNode** phead)
{assert(*phead);SLTNode* newhead = (*phead)->next;free(*phead);*phead = newhead;
}

测试函数如下:

void Testlist4()
{SLTNode* plist = NULL;SLTPushFront(&plist, 1);SLTPushFront(&plist, 2);SLTPushFront(&plist, 3);SLTPushFront(&plist, 4);SLTPushFront(&plist, 5);SLTPushFront(&plist, 6);printf("\n");printf("头删");printf("\n");SLTPrint(plist);SLTPopFront(&plist);SLTPopFront(&plist);SLTPopFront(&plist);printf("\n");SLTPrint(plist);}

 结果如下:

 4. 单链表的代码实现——针对某一元素对应的位置进行操作:

4.1 通过某一具体元素来找到特定位置:

例如给出下面所示的一个单链表:

如果需要找到元素2所对应的位置,只需要将整体单链表进行一次遍历,若某个结点中的元素= 想要寻找的元素。则返回这个元素对应的结点的坐标,具体代码如下:

//寻找某个元素所对应的节点的地址:
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* tail = phead;while (phead != NULL){if (tail->data == x){return tail;}else{tail = tail->next;}}return NULL;
}

4.2 在某一特定数据对应的结点前插入新结点

前面知道了如何找到一个特定数据对应的结点的位置后,如果需要在这个结点之前插入一个新的结点。

对于这种形式的插入,需要分为两种情况:

1. 头指针为空,此时无法插入,检查指针合法性即可

2. 链表中只有一个结点,此时的插入就等于头插

3. 链表中有多个结点

对于前两种情况,具体的代码如下:

void SLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x)
{assert(*phead);if (*phead == NULL){SLTPushFront(phead, x);}
}

对于第三种情况,需要考虑到,上面给出的查找函数返回的地址并不是插入新结点的地址,而是在这个地址对应的结点的前面进行插入。所以,此时的插入可以分为两步:

1.将新结点存储查找函数找到的结点的地址,这里将用查找函数找到的地址用指针pos存储。即:

2. 将原来pos对应的结点的前一个结点中存储的地址,改为存储新结点的地址,即:

 用代码表示上述过程,即:

void SLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x)
{assert(*phead);if (*phead == NULL){SLTPushFront(phead, x);}else{SLTNode* prev = *phead;while (prev->next != pos){prev = prev->next;}SLTNode* newnode = BuySListNode(x);prev->next = newnode;newnode->next = pos;}
}

用下面的函数测试前插的功能:

void Testlist5()
{SLTNode* plist = NULL;SLTPushFront(&plist, 1);SLTPushFront(&plist, 2);SLTPushFront(&plist, 3);SLTPushFront(&plist, 4);SLTPushFront(&plist, 5);SLTPrint(plist);printf("\n");int x = 0;printf("请输入想要查找的值");scanf("%d", &x);SLTNode* pos = SLTFind(plist, x);printf("插入后:");SLTInsert(&plist, pos, x*10);SLTPrint(plist);
}

结果如下:

4.3 在某一特定数据对应的结点后插入新结点:

原理与前插相似,这里不再叙述,只给出图形表示:

对应代码如下:

void SLTInsertafter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = BuySListNode(x);newnode->next = pos->next;pos->next = newnode;
}

测试函数如下:

void Testlist5()
{SLTNode* plist = NULL;SLTPushFront(&plist, 1);SLTPushFront(&plist, 2);SLTPushFront(&plist, 3);SLTPushFront(&plist, 4);SLTPushFront(&plist, 5);SLTPrint(plist);printf("\n");int x = 0;int y = 0;printf("请输入想要查找的值");scanf("%d %d", &x,&y);SLTNode* pos = SLTFind(plist, x);printf("插入后:");SLTInsert(&plist, pos, x*10);SLTPrint(plist);printf("\n");printf("前插\n");SLTInsertafter(pos, y * 10);SLTPrint(plist);
}

结果如下:

4.4 删除某一特定元素对应位置的结点:

对于删除结点,也要分三种情况:

1. 链表整体为空,检查指针合法性即可

2.链表内只有一个结点,相当于头删 

3.链表有多个结点。

对于第三种情况。与前插相同,也需要创建一个指针来改变pos前面的结点中存储的地址,具体对应的代码如下:

void SLTErase(SLTNode** phead, SLTNode* pos)
{assert(pos);if (pos == *phead){SLTPopFront(phead);}else{SLTNode* prev = *phead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);}
}

测试函数如下:

void Testlist6()
{SLTNode* plist = NULL;SLTPushFront(&plist, 1);SLTPushFront(&plist, 2);SLTPushFront(&plist, 3);SLTPushFront(&plist, 4);SLTPushFront(&plist, 5);SLTPrint(plist);printf("\n");printf("请输入想要查找的值");int x = 0;scanf("%d", &x);SLTNode* pos = SLTFind(plist, x);SLTErase(&plist,pos);printf("\n");SLTPrint(plist);
}

结果如下:

 4.5删除某一特定元素对应位置后一个结点:

随机给出一个链表:

通过观察不难发现: 对于删除某一特定元素对应结点的后一个结点这个功能,对于两种情况是没有意义的:

1. 链表中没有结点。

2. 对应元素的结点恰好是最后一个结点。

所以,在进行删除之前,应该针对这两种情况进行地址的检查。

而对于删除,也需要创建一个新的指针,用来保存pos->nxet这个地址。如果不保存,则删除结点和链接结点之间会出现矛盾,例如:

如果不保存pos->nxet,若选择直接链接数据元素为3的结点,此时没有指针保存数据为2的结点的地址。如果先删除pos->nxet,也无法得到数据元素为3的结点的地址。所以应该创建一个新的指针,pos->nxet,用指针变量posNext保存这个地址。在进行链接数据元素为1,3这两个结点时,可以pos->next = posNext -> next来实现。删除数据元素为2的结点时,直接free(posNext),代码如下:

//删除对应位置后一个结点:
void SLTEraseAfter(SLTNode* pos)
{assert(pos);assert(pos->next);//检查是否是最后一个结点SLTNode* posNext = pos->next;pos->next = posNext->next;free(posNext);
}

测试函数如下:

void Testlist6()
{SLTNode* plist = NULL;SLTPushFront(&plist, 1);SLTPushFront(&plist, 2);SLTPushFront(&plist, 3);SLTPushFront(&plist, 4);SLTPushFront(&plist, 5);SLTPrint(plist);printf("\n");printf("请输入想要查找的值");int x = 0;scanf("%d", &x);SLTNode* pos = SLTFind(plist, x);/*SLTErase(&plist,pos);printf("\n");SLTPrint(plist);*/SLTEraseAfter(pos);printf("\n");SLTPrint(plist);}

结果如下:

 5.总体代码:

5.1 头文件 SList.h:

#include<assert.h>
#include<stdio.h>
#include<stdlib.h>
typedef int SLTDataType;
typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;//创建结点
SLTNode* BuySListNode(SLTDataType x);//打印各节点的信息
void SLTPprint(SLTNode* phead);//尾插
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 SLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x);//对应位置后插入:
void SLTInsertafter(SLTNode* pos, SLTDataType x);//对应位置前删除
void SLTErase(SLTNode** phead, SLTNode* pos);//删除对应位置后一个结点:
void SLTEraseAfter(SLTNode* pos);

5.1 函数功能实现——SLIst.c:

#include"SList.h"//创建结点
SLTNode* BuySListNode(SLTDataType  x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}//打印结点信息
void SLTPrint(SLTNode* phead)
{SLTNode* cur = phead;while (cur != NULL){printf("%d->", cur->data);cur = cur->next;}
}//尾插
void SLTPushBack(SLTNode** phead, SLTDataType x)
{SLTNode* newnode = BuySListNode(x);if (*phead == NULL){*phead = newnode;}else{SLTNode* tail = *phead;while (tail->next != NULL){tail = tail->next;}tail->next = newnode;}
}//头插:
void SLTPushFront(SLTNode** phead, SLTDataType x)
{SLTNode* newnode = BuySListNode(x);newnode->next = *phead;*phead = newnode;
}//尾删
void SLTPopBack(SLTNode** phead)
{assert(*phead);if ((*phead)->next == NULL)//只有一个结点的情况{free(*phead);*phead = NULL;}else{SLTNode* tail = *phead;SLTNode* tailprev = NULL;while (tail->next != NULL){tailprev = tail;tail = tail->next;}free(tail);tailprev->next = NULL;}
}//头删
void SLTPopFront(SLTNode** phead)
{assert(*phead);SLTNode* newhead = (*phead)->next;free(*phead);*phead = newhead;
}//寻找某个元素所对应的节点的地址:
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* tail = phead;while (phead != NULL){if (tail->data == x){return tail;}else{tail = tail->next;}}return NULL;
}//特定位置前插入:
void SLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x)
{assert(*phead);if (*phead == NULL){SLTPushFront(phead, x);}else{SLTNode* prev = *phead;while (prev->next != pos){prev = prev->next;}SLTNode* newnode = BuySListNode(x);prev->next = newnode;newnode->next = pos;}
}//特定位置后插入新结点
void SLTInsertafter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = BuySListNode(x);newnode->next = pos->next;pos->next = newnode;
}void SLTErase(SLTNode** phead, SLTNode* pos)
{assert(pos);if (pos == *phead){SLTPopFront(phead);}else{SLTNode* prev = *phead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);}
}//删除对应位置后一个结点:
void SLTEraseAfter(SLTNode* pos)
{assert(pos);assert(pos->next);//检查是否是最后一个结点SLTNode* posNext = pos->next;pos->next = posNext->next;free(posNext);
}

5.3 函数测试文件Test.c:

#include"SList.h"void Testlist1()
{printf("请输入想要插入的元素的个数:");int n = 0;scanf("%d", &n);SLTNode* plist = NULL;printf("\n请输入插入的元素:");for (size_t i = 0; i < n; i++){int val = 0;scanf("%d", &val);SLTNode* newnode = BuySListNode(val);if (plist == NULL){plist = newnode;}else{newnode->next = plist;plist = newnode;}}SLTPrint(plist);printf("\n");SLTPushBack(&plist, 10000);SLTPrint(plist);printf("\n");
}void Testlist2()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPushBack(&plist, 5);printf("尾插");printf("\n");SLTPrint(plist);printf("\n");
}void Testlist3()
{SLTNode* plist = NULL;SLTPushFront(&plist, 1);SLTPushFront(&plist, 2);SLTPushFront(&plist, 3);SLTPushFront(&plist, 4);SLTPushFront(&plist, 5);SLTPushFront(&plist, 6);SLTPrint(plist);printf("\n尾删");SLTPopBack(&plist);SLTPopBack(&plist);SLTPopBack(&plist);printf("\n");SLTPrint(plist);}
void Testlist4()
{SLTNode* plist = NULL;SLTPushFront(&plist, 1);SLTPushFront(&plist, 2);SLTPushFront(&plist, 3);SLTPushFront(&plist, 4);SLTPushFront(&plist, 5);SLTPushFront(&plist, 6);printf("\n");printf("头删");printf("\n");SLTPrint(plist);SLTPopFront(&plist);SLTPopFront(&plist);SLTPopFront(&plist);printf("\n");SLTPrint(plist);}void Testlist5()
{SLTNode* plist = NULL;SLTPushFront(&plist, 1);SLTPushFront(&plist, 2);SLTPushFront(&plist, 3);SLTPushFront(&plist, 4);SLTPushFront(&plist, 5);SLTPrint(plist);printf("\n");int x = 0;int y = 0;printf("请输入想要查找的值");scanf("%d %d", &x,&y);SLTNode* pos = SLTFind(plist, x);printf("插入后:");SLTInsert(&plist, pos, x*10);SLTPrint(plist);printf("\n");printf("前插\n");SLTInsertafter(pos, y * 10);SLTPrint(plist);printf("\n");
}void Testlist6()
{SLTNode* plist = NULL;SLTPushFront(&plist, 1);SLTPushFront(&plist, 2);SLTPushFront(&plist, 3);SLTPushFront(&plist, 4);SLTPushFront(&plist, 5);SLTPrint(plist);printf("\n");printf("请输入想要查找的值");int x = 0;scanf("%d", &x);SLTNode* pos = SLTFind(plist, x);/*SLTErase(&plist,pos);printf("\n");SLTPrint(plist);*/SLTEraseAfter(pos);printf("\n");SLTPrint(plist);}
int main()
{/*Testlist1();*//*Testlist2();*//*Testlist3();Testlist4();*//*Testlist5();*/Testlist6();return 0;
}

相关文章:

一起学数据结构(3)——万字解析:链表的概念及单链表的实现

上篇文章介绍了数据结构的一些基本概念&#xff0c;以及顺序表的概念和实现&#xff0c;本文来介绍链表的概念和单链表的实现&#xff0c;在此之前&#xff0c;首先来回顾以下顺序表的特点&#xff1a; 1.顺序表特点回顾&#xff1a; 1. 顺序表是一组地址连续的存储单元依次存…...

9.2.1Socket(UDP)

一.传输层: 1.UDP:无连接,不可靠,面向数据报,全双工. 2.TCP:有连接,可靠,面向字节流,全双工. 注意:这里的可不可靠是相对的,并且和安不安全无关. 二.UDP数据报套接字编程: 1.socket文件:表示网卡的这类文件. 2.DatagramPacket:表示一个UDP数据报. 三.代码实现: 1.回显服务…...

9.1网络通信基础

一.基础概念: 1)IP地址:描述网络上的一个设备所在的位置. 2)端口号(port):区分一个主机上不同的进程,和pid一样的作用,但两者不同. 3)协议:网络通信传输数据的含义,协议表示一种约定,这种约定可以是任意的.协议分层之后,上层不需要知道下层协议的细节,可以灵活地调整,替换某…...

idea添加翻译插件并配置有道翻译

1、安装Translation插件 2、 创建有道云应用 有道智云控制台 3、设置idea 4、效果&#xff08;选中文本右键翻译&#xff0c;默认快捷键CtrlShiftY&#xff09;...

激光切割机的操作中蛙跳技术是什么意思

其实&#xff0c;蛙跳技术就是指在激光切割机运行的过程中&#xff0c;机器换位置的方式。打个比方&#xff0c;你刚刚在这儿把孔1切好了&#xff0c;接下来就得跑到那儿把孔2切了。 在这个过程中&#xff0c;激光切割机就像是一只青蛙&#xff0c;要从一个位置跳到另一个位置。…...

Typescript+React入门

初识Typescript 出现背景 Typescript&#xff08;以下简称TS&#xff09;实际上就是JavaScriptType&#xff0c;用数据类型的方式来约束了JS的变量定义 在JS的基础上增加了类型支持 在JS中大多数错误都是因为数据类型造成的&#xff0c;所以TS为了规避这个问题加入了类型限制…...

竞赛项目 酒店评价的情感倾向分析

前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 酒店评价的情感倾向分析 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f9ff; 更多资料, 项目分享&#xff1a; https://gitee.com/dancheng-senior/post…...

加载并绘制时间域内的心电图信号,并实施Q因子为1的陷波滤波器以去除50 Hz频率研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

瑞数信息《2023 API安全趋势报告》重磅发布: API攻击持续走高,Bots武器更聪明

如今API作为连接服务和传输数据的重要通道&#xff0c;已成为数字时代的新型基础设施&#xff0c;但随之而来的安全问题也日益凸显。为了让各个行业更好地应对API安全威胁挑战&#xff0c;瑞数信息作为国内首批具备“云原生API安全能力”认证的专业厂商&#xff0c;近年来持续输…...

HCIA静态路由与动态路由

目录 一、静态路由 定义&#xff1a; 适用环境 二、动态路由 定义&#xff1a; 特点&#xff1a; 动态路由协议: 三、缺点&#xff1a; 1&#xff09;静态路由缺点: 2&#xff09;动态路由的缺点: 四、静态路由与动态路由的区别 静态路由: 动态路由: 一、静态路…...

【前端 | CSS】flex布局

基本概念 Flexible模型&#xff0c;通常被称为 flexbox&#xff0c;是一种一维的布局模型。它给 flexbox 的子元素之间提供了强大的空间分布和对齐能力 我们说 flexbox 是一种一维的布局&#xff0c;是因为一个 flexbox 一次只能处理一个维度上的元素布局&#xff0c;一行或者…...

YoloV8优化:感受野注意力卷积运算(RFAConv),效果秒杀CBAM和CA等 | 即插即用系列

💡💡💡本文改进:感受野注意力卷积运算(RFAConv),解决卷积块注意力模块(CBAM)和协调注意力模块(CA)只关注空间特征,不能完全解决卷积核参数共享的问题 RFAConv| 亲测在多个数据集能够实现大幅涨点,有的数据集达到3个点以上 💡💡💡Yolov8魔术师,独家首…...

面对AI冲击,技术人才该如何考核?

一天下午&#xff0c;在与知名企业的技术交流会议室里&#xff0c;一位兄弟企业的CTO 小力苦笑着&#xff0c;分享了一个技术招聘的故事&#xff1a; “我们有个高级工程师&#xff0c;为了搞定MySQL三个表Join的问题&#xff0c;搞了一整天都研究不出来。结果他尝试将表结构扔…...

放弃51单片机,直接学习STM32开发可能会面临的问题

学习51单片机并非仅仅是为了学习51本身&#xff0c;而是通过它学习一种方法&#xff0c;即如何仅仅依靠Datasheet和例程来学习一种新的芯片。51单片机相对较简单&#xff0c;是这个过程中最容易上手的选择&#xff0c;而AVR单片机则更为复杂。虽然您已经学习了大约十天的51单片…...

windows安装git并初始化

git官网下载地址&#xff1a; https://git-scm.com/downloads 安装步骤&#xff0c;一直点击下一步即可 git初始化 1、用户签名 git config --global user.email 2734542837qq.com#设置全局用户邮箱git config --global user.name "zoujiahao"# 设置全局用户使用人…...

SpringBoot集成websocket(3)|(websocket调用websocket采用回调方式实现数据互传)

SpringBoot集成websocket&#xff08;3&#xff09;|&#xff08;websocket调用websocket采用回调方式实现数据互传&#xff09; 文章目录 SpringBoot集成websocket&#xff08;3&#xff09;|&#xff08;websocket调用websocket采用回调方式实现数据互传&#xff09;[TOC] 前…...

基于Doris实时数据开发的一些注意事项

300万字&#xff01;全网最全大数据学习面试社区等你来&#xff01; 最近Doris的发展大家是有目共睹的。例如冷热分离等新特性的持续增加。使得Doris在易用和成本上都有大幅提升。 基于Doris的一些存储实时数仓在越来越多的场景中开始有一些实践。大家也看到了这种方案频繁出现…...

竞赛项目 深度学习疲劳驾驶检测 opencv python

文章目录 0 前言1 课题背景2 实现目标3 当前市面上疲劳驾驶检测的方法4 相关数据集5 基于头部姿态的驾驶疲劳检测5.1 如何确定疲劳状态5.2 算法步骤5.3 打瞌睡判断 6 基于CNN与SVM的疲劳检测方法6.1 网络结构6.2 疲劳图像分类训练6.3 训练结果 7 最后 0 前言 &#x1f525; 优…...

20.4 HTML 表单

1. form表单 <form>标签: 用于创建一个表单, 通过表单, 用户可以向网站提交数据. 表单可以包含文本输入字段, 复选框, 单选按钮, 下拉列表, 提交按钮等等. 当用户提交表单时, 表单数据会发送到服务器进行处理.action属性: 应指向一个能够处理表单数据的服务器端脚本或UR…...

Linux——基础IO(1)

目录 0. 文件先前理解 1. C文件接口 1.1 写文件 1.2 读文件 1.3 输出信息到显示器 1.4 总结 and stdin & stdout & stderr 2. 系统调用文件I/O 2.1 系统接口使用示例 2.2 接口介绍 2.3 open函数返回值 3. 文件描述符fd及重定向 3.1 0 & 1 & 2 3.2…...

java_网络服务相关_gateway_nacos_feign区别联系

1. spring-cloud-starter-gateway 作用&#xff1a;作为微服务架构的网关&#xff0c;统一入口&#xff0c;处理所有外部请求。 核心能力&#xff1a; 路由转发&#xff08;基于路径、服务名等&#xff09;过滤器&#xff08;鉴权、限流、日志、Header 处理&#xff09;支持负…...

Leetcode 3576. Transform Array to All Equal Elements

Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接&#xff1a;3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到&#xf…...

uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖

在前面的练习中&#xff0c;每个页面需要使用ref&#xff0c;onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入&#xff0c;需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...

SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现

摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序&#xff0c;以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务&#xff0c;提供稳定高效的数据处理与业务逻辑支持&#xff1b;利用 uniapp 实现跨平台前…...

NFT模式:数字资产确权与链游经济系统构建

NFT模式&#xff1a;数字资产确权与链游经济系统构建 ——从技术架构到可持续生态的范式革命 一、确权技术革新&#xff1a;构建可信数字资产基石 1. 区块链底层架构的进化 跨链互操作协议&#xff1a;基于LayerZero协议实现以太坊、Solana等公链资产互通&#xff0c;通过零知…...

CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云

目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...

使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度

文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...

VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP

编辑-虚拟网络编辑器-更改设置 选择桥接模式&#xff0c;然后找到相应的网卡&#xff08;可以查看自己本机的网络连接&#xff09; windows连接的网络点击查看属性 编辑虚拟机设置更改网络配置&#xff0c;选择刚才配置的桥接模式 静态ip设置&#xff1a; 我用的ubuntu24桌…...

如何应对敏捷转型中的团队阻力

应对敏捷转型中的团队阻力需要明确沟通敏捷转型目的、提升团队参与感、提供充分的培训与支持、逐步推进敏捷实践、建立清晰的奖励和反馈机制。其中&#xff0c;明确沟通敏捷转型目的尤为关键&#xff0c;团队成员只有清晰理解转型背后的原因和利益&#xff0c;才能降低对变化的…...

comfyui 工作流中 图生视频 如何增加视频的长度到5秒

comfyUI 工作流怎么可以生成更长的视频。除了硬件显存要求之外还有别的方法吗&#xff1f; 在ComfyUI中实现图生视频并延长到5秒&#xff0c;需要结合多个扩展和技巧。以下是完整解决方案&#xff1a; 核心工作流配置&#xff08;24fps下5秒120帧&#xff09; #mermaid-svg-yP…...