链表-增删查改
1、链表的概念及结构
概念:链表是一种物理结构上非连续、非顺序的存储结构,数据元素的逻辑是通过链表中的指针链接次序实现的。链表通过结构体实现,结构体成员分为数据域与指针域,这部分内容可以查看文章《c语言自定义类型:结构体、枚举、联合》中的结构体的自引用部分。
链表的物理连接如下:
每个节点(结构体)有两个区域,一是数据域,用来储存数据,如存储的1、2、3、4,二是指针域,用来存储下一个节点的地址。
注意:
- 从上图可以看出,链式结构在逻辑上是连续的,但是在物理上不一定连续。
- 现实中的节点一般都是从堆上申请出来的。
- 从堆上申请的空间(动态内存开辟的空间),是按照一定策略来分配的,两次申请的空间可能连续,也可能不连续。
假设在32位系统上,节点的数据域为 int 类型,那么一个节点的大小一共为8个字节。
2、链表的分类
实际中链表的结构非常多样,第一节中展示的是单向不带头不循环链表,以下情况组合起来有8种链表结构:
2×2×2=8,共有8种链表结构。
虽然有这么么的链表结构,但是我们实际种最常用的是下面两种结构:
- 无头单向非循环链表:结构简单,一般不会单独用来存储数据。实际中更多是作为其他数据结构的的子结构,如哈希桶、图的邻接表等等。
- 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现这个结构会带来很多优势,实现反而简单了。
3、无头单向非循环链表的实现
本节主要实现无头单向非循环链表
3.1 链表的创建
typedef int SLTDataType; //int类型重定义,方便以后更改为其他数据类型 typedef struct SListNode { SLTDataType data; struct SListNode* next; //该结构体成员为结构体指针,指向的是下一个结构体的位置 }SLTNode; //结构体struct SListNode重命名为SLTNode
- 先定义一个结构体 struct SListNode,里面有数据域,用来存放整型数据,指针域 struct SListNode* next,存放下一个节点(下一个结构体)的位置,所以指针域是结构体指针。并且结构体类型都是 struct SListNode,属于结构体的自引用。
- 为了方便以后更改节点中的数据域为其他数据类型,将int 类型用 typedef重命名。
- 为了书写方便,将结构体类型 struct SListNode重命名为 SLTNode。
链表创建完成后不需要初始化,链表过于简单,就是由一个个的结构体通过指针域串起来的,在不存储数据的时候 ,可以没有任何节点,即没有结构体,在主函数中将指向结构体的指针置为空就行了。
int main() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 return 0; }
结构体创建一个结构体指针 plist,当作第一个节点的地址(即第一个结构体的地址),此时链表中没有数据的存储,将第一节点的地址置为空。
3.2 链表的尾插函数
在一个链表的尾部插入数据可以分为两种情况,一种是链表为空,即没有任何数据,头节点指向空指针。第二种是链表存储了数据。
尾插函数代码展示
//动态申请一个节点 SLTNode* BuySLTNode(SLTDataType x) { SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode)); //为插入的数据创建新的结构体空间 node->data = x; node->next = NULL; return node; } //尾插数据 void SListPushBack(SLTNode** pplist, SLTDataType x) { SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将>数据放入这个空间,返回的是这个空间的地址 if (*pplist == NULL)//如果这个链表为空,直接把新创建的结构体当作第一个节点 { *pplist = newnode; } else //如果不为空,在链表的最后一个节点的后面插入新的节点 { SLTNode* tail = *pplist; // 尾插数据先找到尾 while (tail->next != NULL) { tail = tail->next; //一直往下一个结构体遍历,直到找到存储的是NULL的结构体,这个结构体即是最后一个结构体 } tail->next = newnode; //将新的节点的位置储存在之前的最后一个节点处 } }
- 插入数据首先要开辟一个新的节点,先自定义一个新节点开辟函数,SLTNode* BuySLTNode(SLTDataType x);
参数为给这块开辟的空间要插入的数。
为要插入的数据开辟空间,并将数据放入这个空间(节点),在不确定这个节点是作为尾节点或者是链表中其他位置的节点时,将这个节点的指针域置空,函数返回的是这个空间的地址- void SListPushBack(SLTNode** pplist, SLTDataType x);尾插函数。
第一个参数为头节点的地址,第二个参数是要插入的值。
在链表为空的时候,头节点指向空指针,当创建好一个节点放进来时,要将之前的头节点 plist 指向这个这个节点的位置,因此头节点 plist 本身的值要改变,函数参数本身要改变,因此传递的是实参的地址,plist自身就是一个指针,指针的地址要用二级指针来接收---->SLTNode** pplist。- 当链表为空的时候,直接将头节点指向新节点的地址。
- 当链表不为空时,先找到链表的尾部(即最后一个节点),找到尾节点后,将尾节点的指针域存储新节点的地址。
- SLTNode* tail = *pplist;
tail = tail->next; ,头节点指向指针域,并将起赋值给自身,通过循环条件tail->next != NULL;,就能找到最后一个节点的位置。
在主函数中调用尾插函数,查看监视
void TestSList1() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 SListPushBack(&plist, 1); SListPushBack(&plist, 2); SListPushBack(&plist, 3); SListPushBack(&plist, 4); } int main() { TestSList1(); return 0; }
监视结果
调用四次尾插函数,分别存储1、2、3、4,可以看到要存储的值已经存进去了。
3.3 链表的打印函数
void SListPrint(SLTNode* plist) { SLTNode* cur = plist; //plist保持不变,创建一个第三方结构体指针cur代替plist职责 while (cur != NULL) { printf("%d ", cur->data); cur = cur->next; //将当前节点的数据打印后,访问到下一个结构体的位置,赋值给自身,即跳到下一个结构体的位置 } printf("\n"); }
- 通过头节点指向指针域,将链表中的所有节点都遍历,直到遍访问到cur->next==NULL,即访问到了尾节点。
- 每访问一个节点就将节点的中数据域打印一遍,所有节点访问完毕,数据打印完毕
在主函中调用打印函数
void TestSList2() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 SListPushBack(&plist, 1); SListPushBack(&plist, 2); SListPushBack(&plist, 3); SListPushBack(&plist, 4); SListPrint(plist); } int main() { TestSList2(); return 0; }
打印结果
3.4 链表的头插函数
头插函数代码
void SListPushFront(SLTNode** pplist, SLTDataType x) { SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将数据放入这个空间,返回的是这个空间的地址 newnode->next = *pplist; *pplist = newnode; //将开辟的空间的地址作为头地址 }
- 插入数据首先要开辟一个新的节点,调用新节点开辟函数
- 头节点要指向新开辟节点的地址,即头节点 plist 作为参数本身要改变,传址调用,plist本身就是指针,指针的地址,形参用二级指针接收。
- 之前的头节点此时成为第二个节点,新开辟节点的指针域要储存第二个头节点的地址。
在主函数中调用头插函数并调用打印函数
void TestSList3() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 SListPushFront(&plist, 10); SListPushFront(&plist, 20); SListPushFront(&plist, 30); SListPushFront(&plist, 40); SListPrint(plist); } int main() { TestSList3(); return 0; }
打印函数
3.5 链表的尾删函数
//尾删数据 void SListPopBack(SLTNode** pplist) { //1、没有节点 //2、一个节点 //3、多个节点 if (*pplist == NULL) { return; //链表为空,没得删,直接返回 } else if ((*pplist) ->next == NULL) { free(*pplist); *pplist = NULL; } else { SLTNode* prev = NULL; //尾删数据先找到尾 SLTNode* tail = *pplist; while (tail->next != NULL) { prev = tail; //通过不断循环,找最后一个的节点的同时,找到倒数第二个节点 tail = tail->next; //找到尾 } free(tail); //将最后一个结构体free掉,这个结构体储存的数据就没了。 tail = NULL;//及时置空 prev->next = NULL; //此时倒数第二个节点成为最后一个节点,要将最后一个结构体的指针域置为空。 } }
- 链表为空时,没得删,直接返回。
- 链表只有一个节点时,将这个节点删掉后,头节点 plist 要指向空指针,即头节点本身要改变,需要传值调用,头节点 plist 本身是指针,所以形参用二级指针接收。
- 链表有多个节点,尾删数据先要找到尾节点,将尾节点直接free掉,尾节点中的内容就清空了,尾节点的内容清空了。
- 尾节点内容清空后,但是尾节点的地址依然存在,倒数第二个节点中的指针域依然指向这里,倒数第二个节点此时变为尾节点,根据单向不循环链表的性质,所以要将倒数第二个节点的指针域置空。
- 将倒数第二个节点的指针域置空,要先找到倒数第二个节点。
SLTNode* prev = NULL;
SLTNode* tail = *pplist;
while (tail->next != NULL)
{
prev = tail;//找到倒数第二个节点
tail = tail->next; //找到尾
}
在主函数中调用尾删函数,并且打印
void TestSList4() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 SListPushBack(&plist, 1); SListPushBack(&plist, 2); SListPushBack(&plist, 3); SListPushBack(&plist, 4); SListPrint(plist); SListPopBack(&plist); SListPopBack(&plist); SListPrint(plist); } int main() { TestSList4(); return 0; }
打印结果
- 调用两次尾删函数,被删掉了两个数据。
3.6 链表的头删函数
void SListPopFront(SLTNode** pplist) { if (*pplist == NULL) { return; } else { SLTNode* next = (*pplist)->next; //先将第二个节点保存起来,免得找不到 free(*pplist); //将第一个节点中的内容释放掉 *pplist = next; //这时第二个节点变为第一个节点 } }
- 链表为空时,没得删,直接返回
- 链表不为空,直接将第一个节点free掉,第一个节点的内容直接清空。
- 头节点被删除后,此时第二个节点变为头节点,根据单向不循环链表的性质,要将第二个节点的地址给头节点 plist,即头节点本身要改变,需要传值调用,头节点 plist 本身是指针,所以形参用二级指针接收。
- 在free头节点的内容之前,要先将第二个节点的地址给记录下来,以便第二个节点赋值给头节点。如果没有实现记录下来,在free的时候,第二个节点作为头节点中的成员(指针域),就清空了,那么再也找不到第二个节点的地址了。
在主函数中调用头删函数并打印
void TestSList5() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 SListPushBack(&plist, 1); SListPushBack(&plist, 2); SListPushBack(&plist, 3); SListPushBack(&plist, 4); SListPrint(plist); SListPopFront(&plist); SListPopFront(&plist); SListPopFront(&plist); SListPrint(plist); } int main() { TestSList5(); return 0; }
打印结果
- 头删函数调用了三次,链表中储存好的1、2、3、4中的1、2、3都被删掉了。
3.7 链表的查找函数
SLTNode* SListFind(SLTNode* plist, SLTDataType x) { SLTNode* cur = plist; while (cur != NULL) { if (cur->data == x) { return cur; } cur = cur->next; } return NULL; //链表走完了还没找到,返回NULL }
- 给定一个数x,去找这个数在链表中存不存在
- 函数的参数是头节点的地址 plist 和要找的数 x。返回值是要找的数 x 的地址。
- 跟找节点尾的逻辑是一样的,只不过这里在等于x时就返回其地址了
- 当链表从头遍历到节点尾还没找到要找的数x,那么链表中这个数不存在,返回空指针。
在主函数中调用查找函数
void TestSList6() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 SListPushBack(&plist, 1); SListPushBack(&plist, 2); SListPushBack(&plist, 3); SListPushBack(&plist, 4); SListPrint(plist); SLTNode* pos = SListFind(plist, 3); if (pos) { printf("找到了\n"); } else { printf("找不到\n"); } } int main() { TestSList6(); return 0; }
打印结果
3.8 在给定位置pos之后插入x
void SListInsertAfter(SLTNode* pos, SLTDataType x) { assert(pos); SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将数据放入这个空间,返回的是这个空间的地址 newnode->next = pos->next; pos->next = newnode; }
- 在pos节点后插入数据,首先要保证pos节点的位置不为空,也即链表不能为空链表,对形参 pos进行断言。
- 函数参数为要插入的位置,要插入的具体数据。
- 插入数据首先要开辟一个新的节点,调用新节点开辟函数。
- 将 pos 节点原本后面的节点的位置赋值给新开辟节点的指针域,再将新开辟节点的地址赋值给 pos 节点的指针域。
在主函数中调用此函数
void TestSList7() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 SListPushBack(&plist, 1); SListPushBack(&plist, 2); SListPushBack(&plist, 3); SListPushBack(&plist, 4); SListPrint(plist); SLTNode* pos = SListFind(plist, 3); SListInsertAfter(pos, 30); //在3之后插一个30 SListPrint(plist); } int main() { TestSList7(); return 0; }
打印结果
- 在3的后面插入了一个30。
3.9 在给定位置pos之前插入x
void SListInsertBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x) { assert(pos); SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将数据放入这个空间,返回的是这个空间的地址 if (pos == *pplist) //这种情况是头插 { newnode->next = pos; //或者写成 newnode->next = *pplist; *pplist = newnode; } else { SLTNode* prev = NULL; SLTNode* cur = *pplist; while (cur != pos) //找pos节点与pos节点的上一个节点 { prev = cur; cur = cur->next; } prev->next = newnode; newnode->next = pos; } }
- 在pos节点前插入数据,首先要保证pos节点的位置不为空,也即链表不能为空链表,对形参 pos进行断言。
- 函数参数为要插入的位置,要插入的具体数据。
- 插入数据首先要开辟一个新的节点,调用新节点开辟函数。
- 在第一个节点前面插入数据,情况变为头插,头插要改变头节点的地址,即头节点 plist 作为参数本身要改变,传址调用,plist本身就是指针,指针的地址,形参用二级指针接收。
- 当不是头插时,要找到 pos 节点前一个节点的地址。因为要将新开辟的节点的的地址赋值给 pos 节点前一个节点的指针域
- 此时pos节点变为新开辟节点的后一个节点,所以将新开辟节点的地址赋值给 pos 节点的指针域。
在主函数中调用此函数
void TestSList8() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 SListPushBack(&plist, 1); SListPushBack(&plist, 2); SListPushBack(&plist, 3); SListPushBack(&plist, 4); SListPrint(plist); SLTNode* pos = SListFind(plist, 3); SListInsertBefore(&plist, pos, 300); //在3之前插一个300 SListPrint(plist); } int main() { TestSList8(); return 0; }
打印结果
- 在3之前插入了一个300。
3.10 删除给定位置pos处的数据
void SListErase(SLTNode** pplist, SLTNode* pos) { assert(pplist); assert(pos); if (*pplist == pos) { SListPopFront(pplist); //这种情况是头删,直接调用头删函数 } else { SLTNode* prev = *pplist; while (prev->next != pos) //找pos节点的上一个节点 { prev = prev->next; assert(prev); //走完了还没有,说明没有pos这个节点,检查pos不是链表中的节点,即参数传错了 } prev->next = pos->next; free(pos); pos = NULL; } }
- 如果删除的是第一个节点处的数据,那么是头删的情况,直接调用头删函数。
- 除了头删,正常删除链表中的一个节点时,要找到被删除节点的上一个节点,以便被删除节点被删除后,上一个节点的地址能指向被删除节点的下一个节点。
- 将要删除的节点free掉,这个节点中的内容就被删除了。
在主函数中调用该函数
void TestSList9() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 SListPushBack(&plist, 1); SListPushBack(&plist, 2); SListPushBack(&plist, 3); SListPushBack(&plist, 4); SListPrint(plist); SLTNode* pos = SListFind(plist, 3); if (pos) { SListErase(&plist, pos);//将3删掉 } SListPrint(plist); } int main() { TestSList9(); return 0; }
打印结果
- 新找到 3 的位置,在将3的位置传递给删除函数,从打印结果看,3被删除了。
3.11 删除给定位置pos之后的数据
void SLisEraseAfter(SLTNode* pos) { assert(pos); if (pos->next == NULL) { return; } else { SLTNode* next = pos->next; //先将pos的下一个节点记录下来 pos->next = next->next; free(next); next = NULL;//及时置空 } }
- 先将pos位置后二个节点的地址通过指针域找到并存储在pos位置的指针域中。
- 然后free掉 pos 位置后面的节点。
- 实现这个函数的时间复杂度是O(1)。因为pos位置是确定的参数,那么他后面两个节点的位置也是确定的了,整个函数没有遍历链表这个操作,所以时间复杂度是O(1)。
在主函数中调用
void TestSList10() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 SListPushBack(&plist, 1); SListPushBack(&plist, 2); SListPushBack(&plist, 3); SListPushBack(&plist, 4); SListPrint(plist); SLTNode* pos = SListFind(plist, 1); if (pos) { SLisEraseAfter(pos);//将2删掉 } SListPrint(plist); } int main() { TestSList10(); return 0; }
打印结果
- 1后面的2被删掉了。
3.12 删除给定位置pos处的数据—时间复杂度为O(1)的实现方式
void SListErase1(SLTNode* pos) { assert(pos); SLTDataType tmp = pos->data; //交换pos节点与他下一个节点中的数据域 pos->data = pos->next->data; pos->next->data = tmp; SLisEraseAfter(pos); //再调用删除pos后面节点的函数 }
- 删除pos节点的数据,可以先采用将pos节点与他的下一个节点中的数据域互相交换,那么删除pos节点的下一个节点,就可以达到本来的目的了。
- 这样不用去遍历找pos节点的上一个节点,并且调用的删下一个节点的函数SLisEraseAfter(pos);时间复杂度也是O(1),所以这个函数的时间复杂度是O(1)。
在主函数中调用此函数
void TestSList11() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 SListPushBack(&plist, 1); SListPushBack(&plist, 2); SListPushBack(&plist, 3); SListPushBack(&plist, 4); SListPrint(plist); SLTNode* pos = SListFind(plist, 3); if (pos) { SListErase1(pos);//将3删掉 } SListPrint(plist); } int main() { TestSList11(); return 0; }
打印结果
3.13 删除给定位置pos之前的数据
void SListEraseBefore(SLTNode** pplist, SLTNode* pos) { assert(pplist); assert(pos); SLTNode* prev = *pplist; while (prev->next != pos) { prev = prev->next; assert(prev); } SLTDataType tmp = prev->data; prev->data = pos->data; pos->data = tmp; SLisEraseAfter(prev); }
- 将pos节点之前的数据,可以采用将pos节点与pos节点前一个节点prev的数据互相交换,再删除prev的后一个节点(即pos节点),就可以达到目的。
在主函数中调用此函数
void TestSList12() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 SListPushBack(&plist, 1); SListPushBack(&plist, 2); SListPushBack(&plist, 3); SListPushBack(&plist, 4); SListPrint(plist); SLTNode* pos = SListFind(plist, 3); if (pos) { SListEraseBefore(&plist, pos); } SListPrint(plist); } int main() { TestSList12(); return 0; }
打印结果
- 3前面的2被删掉了。
3.14 在给定位置pos之前插入x—时间复杂度为O(1)的实现方式
void SListInsertBefore1(SLTNode* pos, SLTDataType x) { SListInsertAfter(pos, x); //在pos之后插入 SLTNode* newnode = pos->next; SLTDataType tmp = newnode->data; newnode->data = pos->data; pos->data = tmp; }
- 在pos位置之前插入数据x,可以采用先将在pos位置之后插入x,在将x与pos位置的数据交换,那么可以达到目的。
- 这样不用去遍历找pos节点的上一节点,并且调用的(给定位置pos之后插入x)的函数的时间复杂度也是O(1),所以这个函数的时间复杂度是O(1)。
在主函中调用该函数
void TestSList13() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 SListPushBack(&plist, 1); SListPushBack(&plist, 2); SListPushBack(&plist, 3); SListPushBack(&plist, 4); SListPrint(plist); SLTNode* pos = SListFind(plist, 3); if (pos) { SListInsertBefore1(pos, 300); } SListPrint(plist); } int main() { TestSList13(); return 0; }
打印结果
- 在3的前面插入了300。
3.15 链表的销毁函数
void SListDestory(SLTNode** pplist) { assert(pplist); SLTNode* cur = *pplist; while (cur != NULL) { SLTNode* next = cur->next; free(cur); cur = next; } *pplist = NULL; }
- 从头节点开始,一个一个完后面的节点遍历,遍历一个节点销毁一个节点,再全部销毁完毕后,再将头节点置空,那么整个链表也就空了。
在主函数中调用销毁函数
void TestSList14() { SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址 SListPushBack(&plist, 1); SListPushBack(&plist, 2); SListPushBack(&plist, 3); SListPushBack(&plist, 4); SListPrint(plist); SListDestory(&plist); SListPrint(plist); } int main() { TestSList14(); return 0; }
打印结果
1 存储好后打印一次,链表中的数据是 1 2 3 4,调用销毁函数后再打印一次,什么也么打印出来。
3.16 无头单向非循环链表的所有代码
SList.h文件:写函数的声明,函数的头文件,其他 .c文件只要包含SList.h文件—>#include “SList.h”,就相当于写了函数的声明与头文件。如下:
#pragma once
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
//单向+不带头+不循环 链表
typedef int SLTDataType; //int类型重定义,方便以后更改为其他数据类型
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next; //该结构体成员为结构体指针,指向的是下一个结构体的位置
}SLTNode; //结构体struct SListNode重命名为SLTNode
//链表打印函数
void SListPrint(SLTNode* plist);
//尾插数据
void SListPushBack(SLTNode** pplist, SLTDataType x); //形参传的是plist的地址,因为如果链表为空的话,要将开辟的空间作为链表
//第一个节点的地址,即第一个节点plist本身要改变,自身要改变,就要传自身的地址
//plist储存的是第一个SLTNode的位置,所以实参&plist传的是一个二级结构体指针。
//形参用二级结构体指针接收。
//头插数据
void SListPushFront(SLTNode** pplist, SLTDataType x); //头插也要将头节点plist本身改变,所以传plist的地址
//尾删数据
void SListPopBack(SLTNode** pplist);
//头删数据
void SListPopFront(SLTNode** pplist);
//单链表查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x);
//单链表在pos之后插入x
void SListInsertAfter(SLTNode* pos, SLTDataType);
//单链表在pos之前插入x(很麻烦,不合适)
void SListInsertBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x);
//删除pos位置
void SListErase(SLTNode** pplist, SLTNode* pos);
//删除pos之后的x
void SLisEraseAfter(SLTNode* pos);
//删除pos位置的数据的第二种写法,这种时间复杂度是 1,缺点是这种写法要删除的pos位置不能是尾节点
void SListErase1(SLTNode* pos);
//删除pos位置之前的数据
void SListEraseBefore(SLTNode** pplist, SLTNode* pos);
//单链表在pos之前插入x,要求时间复杂度为 1
void SListInsertBefore1(SLTNode* pos, SLTDataType x);
//链表的销毁
void SListDestory(SLTNode** pplist);
SList.c文件:用来实现链表的各种功能接口函数。如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
//链表的打印
void SListPrint(SLTNode* plist)
{
SLTNode* cur = plist; //plist保持不变,创建一个第三方结构体指针cur代替plist职责
while (cur != NULL)
{
printf("%d ", cur->data);
cur = cur->next; //将当前节点的数据打印后,访问到下一个结构体的位置,赋值给自身,即跳到下一个结构体的位置
}
printf("\n");
}
//动态申请一个节点
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode)); //为插入的数据创建新的结构体空间
node->data = x;
node->next = NULL;
return node;
}
//尾插数据
void SListPushBack(SLTNode** pplist, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将数据放入这个空间,返回的是这个空间的地址
if (*pplist == NULL)//如果这个链表为空,直接把新创建的结构体当作第一个节点
{
*pplist = newnode;
}
else //如果不为空,在链表的最后一个节点的后面插入新的节点
{
SLTNode* tail = *pplist; // 尾插数据先找到尾
while (tail->next != NULL)
{
tail = tail->next; //一直往下一个结构体遍历,直到找到存储的是NULL的结构体,这个结构体即是最后一个结构体
}
tail->next = newnode; //将新的节点的位置储存在之前的最后一个节点处
}
}
//头插数据
void SListPushFront(SLTNode** pplist, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将数据放入这个空间,返回的是这个空间的地址
newnode->next = *pplist;
*pplist = newnode; //将开辟的空间的地址作为头地址
}
//尾删数据
void SListPopBack(SLTNode** pplist)
{
//1、没有节点
//2、一个节点
//3、多个节点
if (*pplist == NULL)
{
return; //链表为空,没得删,直接返回
}
else if ((*pplist) ->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
SLTNode* prev = NULL;
//尾删数据先找到尾
SLTNode* tail = *pplist;
while (tail->next != NULL)
{
prev = tail; //通过不断循环,找最后一个的节点的同时,找到倒数第二个节点
tail = tail->next; //找到尾
}
free(tail); //将最后一个结构体free掉,这个结构体储存的数据就没了。
tail = NULL;//及时置空
prev->next = NULL; //此时倒数第二个节点成为最后一个节点,要将最后一个结构体的指针域置为空。
}
}
//头删
void SListPopFront(SLTNode** pplist)
{
if (*pplist == NULL)
{
return;
}
else
{
SLTNode* next = (*pplist)->next; //先将第二个节点保存起来,免得找不到
free(*pplist); //将第一个节点中的内容释放掉
*pplist = next; //这时第二个节点变为第一个节点
}
}
//单链表查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
SLTNode* cur = plist;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL; //链表走完了还没找到,返回NULL
}
//单链表在pos之后插入x
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将数据放入这个空间,返回的是这个空间的地址
newnode->next = pos->next;
pos->next = newnode;
}
//单链表在pos之前插入x
void SListInsertBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将数据放入这个空间,返回的是这个空间的地址
if (pos == *pplist) //这种情况是头插
{
newnode->next = pos; //或者写成 newnode->next = *pplist;
*pplist = newnode;
}
else
{
SLTNode* prev = NULL;
SLTNode* cur = *pplist;
while (cur != pos) //找pos节点与pos节点的上一个节点
{
prev = cur;
cur = cur->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
//删除pos位置的数据
void SListErase(SLTNode** pplist, SLTNode* pos)
{
assert(pplist);
assert(pos);
if (*pplist == pos)
{
SListPopFront(pplist); //这种情况是头删,直接调用头删函数
}
else
{
SLTNode* prev = *pplist;
while (prev->next != pos) //找pos节点的上一个节点
{
prev = prev->next;
assert(prev); //走完了还没有,说明没有pos这个节点,检查pos不是链表中的节点,即参数传错了
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除pos之后的x
void SLisEraseAfter(SLTNode* pos)
{
assert(pos);
if (pos->next == NULL)
{
return;
}
else
{
SLTNode* next = pos->next; //先将pos的下一个节点记录下来
pos->next = next->next;
free(next);
next = NULL;//及时置空
}
}
//删除pos位置的数据的第二种写法,这种时间复杂度是 1,缺点是这种写法要删除的pos位置不能是尾节点
void SListErase1(SLTNode* pos)
{
assert(pos);
SLTDataType tmp = pos->data; //交换pos节点与他下一个节点中的数据域
pos->data = pos->next->data;
pos->next->data = tmp;
SLisEraseAfter(pos); //再调用删除pos后面节点的函数
}
//删除pos位置之前的数据
void SListEraseBefore(SLTNode** pplist, SLTNode* pos)
{
assert(pplist);
assert(pos);
SLTNode* prev = *pplist;
while (prev->next != pos)
{
prev = prev->next;
assert(prev);
}
SLTDataType tmp = prev->data;
prev->data = pos->data;
pos->data = tmp;
SLisEraseAfter(prev);
}
//单链表在pos之前插入x,要求时间复杂度为 1
void SListInsertBefore1(SLTNode* pos, SLTDataType x)
{
SListInsertAfter(pos, x); //在pos之后插入
SLTNode* newnode = pos->next;//找到在pos后插入的新节点newnode的地址
SLTDataType tmp = newnode->data;
newnode->data = pos->data;
pos->data = tmp;
}
//链表的销毁
void SListDestory(SLTNode** pplist)
{
assert(pplist);
SLTNode* cur = *pplist;
while (cur != NULL)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pplist = NULL;
}
test.c文件:主函数写在这,在主函数中可以调用 SList.c文件中实现的各种接口。如下:
int main()
{
SLTNode* plist = NULL; //plist为头节点的地址,即第一个结构体的地址
return 0;
}
以上就是无头单向非循环链表链表的所有代码,在SList.c文件中实现的函数接口有以下注意:
- 在函数中如果需要改变链表中的头节点地址,则传参的时候需要传址调用,结合参数已经是指针了,形参用二级指针接收。
- 如果函数对指定位置pos的之前的节点prev进行操作,需要对链表进行遍历找到prev,这样这个函数的时间复杂度至少是O(n)。
- 对指定的地址要进行断言,避免传过来的是空指针。