对于链表这部分,在数据结构里面是非常重要的。链表的学习周期长一些,博客的内容会多一些,所以对于链表的描述我将分为多篇。基本概念、基本实现、基本习题、进阶习题我都会通过博客向大家分享。那么本篇是对链表有一个初步的认识,并且能够掌握最基本的单链表的增删查改。
在学习链表之前,我们回顾以下顺序表,那么我们来思考下面几个问题:
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请空间,拷贝数据,释放旧空间。这就会造成不小的空间消耗
- 增容一般是呈 2 倍的增长,势必会有一定的空间浪费。例如当前容量为 100,满了以后增容到 200 ,我们再继续插入 5 个数据,那么就浪费了 95 个数据空间。
那么我们要解决上述问题,就必须学习链表。所以带着问题去学习,那么将事半功倍。
基本概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
如何理解物理存储结构是不连续的呢?
链表与顺序表不一样的是,顺序表它是一个数组,但是链表不是,所有的连续数据都是通过指针来链接的。那么现在又引出了一个问题,链表的每个数据都要链接下一个数据,而链接的方式是通过指针,那么如何做到呢?我们可以运用结构体来解决这个问题,并且提出结点的概念。
可以看到,每个结点都有数据域和指针域,指针是指向下一个结点的地址,那么需要补充的是,第一个结点的地址需要用一个指针来保存,最后一个结点的指针是指向空指针。 保存第一个结点的地址的意义在于:可以找到链表的头结点,从而进一步访问结点之后的结点。最后一个结点的指针指向空指针的意义在于:避免野指针的问题,客观上增加一种链表的结束条件,例如字符串的结束标志 '\0' 。
那么我们就得出了链表的三个特征:
- 链表的结构在逻辑上是连续的,但是在物理上不一定连续
- 现实中的结点一般都是从堆上申请出来的
- 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
链表的结构是有多种的,单向、双向、循环等等,但我们主要的侧重点在单向的不带哨兵卫的链表。
结点的声明:
//结点的声明 typedef int LLDataType; typedef struct LinkList { LLDataType val; struct LinkList* next; }LL;存储头结点的指针在主函数中定义:
#include "LinkList.h" int main() { LL* head = NULL; return 0; }
头插的实现:
//创建新结点 static LL* CreateNode(LLDataType x) { LL* RetNode = (LL*)calloc(1, sizeof(LL)); assert(RetNode); RetNode->val = x; RetNode->next = NULL; return RetNode; } //头插 void ListPushHead(LL** phead, LLDataType x) { assert(phead); //此时我们还没有结点,需要创建一个结点 LL* NewNode = CreateNode(x); //新结点的指针要指向以前的头结点 NewNode->next = *phead; //头指针的内容更新为新结点的地址 *phead = NewNode; }需要注意,即使存储头结点的指针是一个指针,但我们在函数内操作时,需要改变指针的指向位置,所以需要使用二级指针。我们可能对最后两条代码存在疑问,我在这里解释一下:
链表打印实现:
//打印 void ListPrint(LL* phead) { LL* cur = phead;//使用临时变量来操作链表,但因为是传址调用,不使用也是可以的 while (cur) { printf("%d->", cur->val); cur = cur->next; } printf("NULL\n"); }
测试:头插一个数据以及打印:
#include "LinkList.h" int main() { LL* head = NULL; ListPushHead(&head, 5); ListPrint(head); return 0; }
尾插的实现:
//尾插 void LIstPushBack(LL** phead, LLDataType x) { assert(phead); LL* newnode = CreateNode(x); //如果链表没有结点,新结点直接作为头结点 if (*phead == NULL) { *phead = newnode; } else { LL* tail = *phead; //向后寻找链表的最后一个结点 while (tail->next != NULL) { tail = tail->next; } tail->next = newnode; } }可以看到,我们尾插的情况是有两个的,一个链表里面没有结点,那么就让这个要尾插的结点直接为头。而另一种则是链表中存在结点,我们需要遍历找到最后一个结点,并让这个结点的指针指向新的结点。同时需要注意,我们传的参数是二级指针,我们要解引用才能找到存放头结点地址的指针。
我们思考一个问题:我们能否像头插一样定义一个指针,专门用来存储最后一个结点的地址呢?当然可以,不过我们需要对函数的参数做出一些改变。
头插更改:
//头插 void ListPushHead(LL** phead, LL** ptail,LLDataType x) { //assert(phead); 此时我们还没有结点,需要创建一个结点 //LL* NewNode = CreateNode(x); 新结点的指针要指向以前的头结点 //NewNode->next = *phead; 头指针的内容更新为新结点的地址 //*phead = NewNode; assert(phead); //此时我们还没有结点,需要创建一个结点 LL* NewNode = CreateNode(x); //链表为空时,第一个头插的结点就是尾结点 if (*phead == NULL) { *ptail = NewNode; } //新结点的指针要指向以前的头结点 NewNode->next = *phead; //头指针的内容更新为新结点的地址 *phead = NewNode; }尾插更改:
//尾插 void LIstPushBack(LL** phead,LL** ptail, LLDataType x) { //assert(phead); //LL* NewNode = CreateNode(x); 如果链表没有结点,新结点直接作为头结点 //if (*phead == NULL) //{ // *phead = NewNode; //} //else //{ // LL* tail = *phead; // //向后寻找链表的最后一个结点 // while (tail->next != NULL) // { // tail = tail->next; // } // tail->next = NewNode; //} assert(phead && ptail); LL* NewNode = CreateNode(x); //如果链表为空,那么头尾指针都指向这个新结点 if (*phead == NULL) { *ptail = *phead = NewNode; } else { (*ptail)->next = NewNode;//尾结点的指针指向新结点 (*ptail) = (*ptail)->next;//尾指针指向新结点 } }我们对修改之后的代码进行数据测试:
#include "LinkList.h" int main() { LL* head = NULL; LL* tail = NULL;//专门用来存储最后一个结点的地址 ListPushHead(&head,&tail, 5); ListPushHead(&head, &tail, 6); ListPushHead(&head, &tail, 7); ListPrint(head); LIstPushBack(&head,&tail, 3); LIstPushBack(&head, &tail, 4); LIstPushBack(&head, &tail, 5); ListPrint(head); return 0; }可以看到,输出结果正是我们想要的。那么修改之后的代码有什么优势呢?其一就是减少了时间复杂度,用循环的方式去找尾,如果数据量足够大,是非常消耗时间的。其二就是我们掌握了这个方法以后,在一些题目上面,我们能取得一些优势。
头删的实现:
//头删 void ListPopHead(LL** phead, LL** ptail) { assert(phead && ptail); assert(*phead);//链表为空不需要删除,强制退出 LL* cur = *phead; //如果链表仅有一个结点 if (cur->next == NULL) { free(cur);//释放结点 *phead = *ptail = NULL;//存储头尾结点地址的指针置空 } //链表有两个或两个以上结点时 else { *phead = cur->next; free(cur); //尾指针不需要变 } }
尾删的实现:
//尾删 void ListPopBack(LL** phead, LL** ptail) { assert(phead && ptail); assert(*phead);//链表里面没有结点不需要删除 LL* cur = *ptail; LL* NewPtail = *phead; //找到倒数第二个结点 while (NewPtail->next != *ptail) { NewPtail = NewPtail->next; } //对删除之前的尾结点释放 free(cur); //存储尾结点地址的指针更新 *ptail = NewPtail; (*ptail)->next = NULL; }
测试:头删和尾删:
#include "LinkList.h" int main() { LL* head = NULL; LL* tail = NULL;//专门用来存储最后一个结点的地址 ListPushHead(&head,&tail, 5); ListPushHead(&head, &tail, 6); ListPushHead(&head, &tail, 7); ListPrint(head); LIstPushBack(&head,&tail, 3); LIstPushBack(&head, &tail, 4); LIstPushBack(&head, &tail, 5); ListPrint(head); ListPopHead(&head, &tail); ListPrint(head); ListPopBack(&head, &tail); ListPrint(head); return 0; }
数据的查找:
//查找 LL* ListFind(LL* phead,LLDataType x) { LL* cur = phead; while (cur) { if (cur->val == x) return cur; cur = cur->next; } return NULL; }我们的目的是找到数据的结点地址,如果确实有结点存储这个数据,那么就返回结点地址,否则返回空。如果有多个结点存储同一个数据,那么返回第一个结点的地址。这是符合常理的设计方法。
在 pos 位置插入数据:
//在 pos 位置插入 void ListInsert(LL** phead, LL** ptail, LL* pos, LLDataType x) { assert(phead && ptail); assert(*phead);//确保链表有结点 //如果 pos 位置与头结点位置重合就头插 if (pos == *phead) { ListPushHead(phead, ptail, x); } else { //在 pos 位置之前插入 LL* cur = *phead; while (cur->next != pos) { cur = cur->next; } LL* NewNode = CreateNode(x); cur->next = NewNode; NewNode->next = pos; } }
测试:在 pos 位置插入数据(与查找配合):
#include "LinkList.h" int main() { LL* head = NULL; LL* tail = NULL;//专门用来存储最后一个结点的地址 ListPushHead(&head,&tail, 5); ListPushHead(&head, &tail, 6); ListPushHead(&head, &tail, 7); ListPrint(head); LIstPushBack(&head,&tail, 3); LIstPushBack(&head, &tail, 4); LIstPushBack(&head, &tail, 5); ListPrint(head); ListPopHead(&head, &tail); ListPrint(head); ListPopBack(&head, &tail); ListPrint(head); ListInsert(&head, &tail, ListFind(head,5), 50); ListPrint(head); return 0; }
在 pos 位置删除结点:
//在 pos 位置删除 void ListPopInsert(LL** phead, LL** ptail, LL* pos) { assert(phead && ptail); assert(*phead);//确保链表有结点 //如果 pos 位置是头结点就头删 if (pos == *phead) { ListPopHead(phead, ptail); } //如果 pos 位置是尾结点就尾删 else if (pos == *ptail) { ListPopBack(phead, ptail); } else { LL* cur = *phead; while (cur->next != pos) { cur = cur->next; } cur->next = pos->next; free(pos); } }
测试:在 pos 位置删除结点:
#include "LinkList.h" int main() { LL* head = NULL; LL* tail = NULL;//专门用来存储最后一个结点的地址 ListPushHead(&head,&tail, 5); ListPushHead(&head, &tail, 6); ListPushHead(&head, &tail, 7); ListPrint(head); LIstPushBack(&head,&tail, 3); LIstPushBack(&head, &tail, 4); LIstPushBack(&head, &tail, 5); ListPrint(head); ListPopHead(&head, &tail); ListPrint(head); ListPopBack(&head, &tail); ListPrint(head); ListInsert(&head, &tail, ListFind(head,5), 50); ListPrint(head); ListPopInsert(&head, &tail, ListFind(head, 5)); ListPrint(head); return 0; }
- #define _CRT_SECURE_NO_WARNINGS 1
-
- #include "LinkList.h"
- int main()
- {
- LL* head = NULL;
- LL* tail = NULL;//专门用来存储最后一个结点的地址
- ListPushHead(&head,&tail, 5);
- ListPushHead(&head, &tail, 6);
- ListPushHead(&head, &tail, 7);
- ListPrint(head);
-
- LIstPushBack(&head,&tail, 3);
- LIstPushBack(&head, &tail, 4);
- LIstPushBack(&head, &tail, 5);
- ListPrint(head);
-
- ListPopHead(&head, &tail);
- ListPrint(head);
- ListPopBack(&head, &tail);
- ListPrint(head);
-
- ListInsert(&head, &tail, ListFind(head,5), 50);
- ListPrint(head);
-
- ListPopInsert(&head, &tail, ListFind(head, 5));
- ListPrint(head);
-
-
- return 0;
- }
- #define _CRT_SECURE_NO_WARNINGS 1
- #include "LinkList.h"
-
- //创建新结点
- static LL* CreateNode(LLDataType x)
- {
- LL* RetNode = (LL*)calloc(1, sizeof(LL));
- assert(RetNode);
- RetNode->val = x;
- RetNode->next = NULL;
- return RetNode;
- }
- //头插
- void ListPushHead(LL** phead, LL** ptail,LLDataType x)
- {
- //assert(phead);
- 此时我们还没有结点,需要创建一个结点
- //LL* NewNode = CreateNode(x);
- 新结点的指针要指向以前的头结点
- //NewNode->next = *phead;
- 头指针的内容更新为新结点的地址
- //*phead = NewNode;
-
- assert(phead);
- //此时我们还没有结点,需要创建一个结点
- LL* NewNode = CreateNode(x);
- //链表为空时,第一个头插的结点就是尾结点
- if (*phead == NULL)
- {
- *ptail = NewNode;
- }
- //新结点的指针要指向以前的头结点
- NewNode->next = *phead;
- //头指针的内容更新为新结点的地址
- *phead = NewNode;
- }
- //打印
- void ListPrint(LL* phead)
- {
- LL* cur = phead;//使用临时变量来操作链表,但因为是传址调用,不使用也是可以的
- while (cur)
- {
- printf("%d->", cur->val);
- cur = cur->next;
- }
- printf("NULL\n");
- }
- //尾插
- void LIstPushBack(LL** phead,LL** ptail, LLDataType x)
- {
- //assert(phead);
- //LL* NewNode = CreateNode(x);
- 如果链表没有结点,新结点直接作为头结点
- //if (*phead == NULL)
- //{
- // *phead = NewNode;
- //}
- //else
- //{
- // LL* tail = *phead;
- // //向后寻找链表的最后一个结点
- // while (tail->next != NULL)
- // {
- // tail = tail->next;
- // }
- // tail->next = NewNode;
- //}
-
- assert(phead && ptail);
- LL* NewNode = CreateNode(x);
- //如果链表为空,那么头尾指针都指向这个新结点
- if (*phead == NULL)
- {
- *ptail = *phead = NewNode;
- }
- else
- {
- (*ptail)->next = NewNode;//尾结点的指针指向新结点
- (*ptail) = (*ptail)->next;//尾指针指向新结点
- }
- }
- //头删
- void ListPopHead(LL** phead, LL** ptail)
- {
- assert(phead && ptail);
- assert(*phead);//链表为空不需要删除,强制退出
- LL* cur = *phead;
- //如果链表仅有一个结点
- if (cur->next == NULL)
- {
- free(cur);//释放结点
- *phead = *ptail = NULL;//存储头尾结点地址的指针置空
- }
- //链表有两个或两个以上结点时
- else
- {
- *phead = cur->next;
- free(cur);
- //尾指针不需要变
- }
- }
- //尾删
- void ListPopBack(LL** phead, LL** ptail)
- {
- assert(phead && ptail);
- assert(*phead);//链表里面没有结点不需要删除
- LL* cur = *ptail;
- LL* NewPtail = *phead;
- //找到倒数第二个结点
- while (NewPtail->next != *ptail)
- {
- NewPtail = NewPtail->next;
- }
- //对删除之前的尾结点释放
- free(cur);
- //存储尾结点地址的指针更新
- *ptail = NewPtail;
- (*ptail)->next = NULL;
- }
- //查找
- LL* ListFind(LL* phead,LLDataType x)
- {
- LL* cur = phead;
- while (cur)
- {
- if (cur->val == x)
- return cur;
- cur = cur->next;
- }
- return NULL;
- }
- //在 pos 位置插入
- void ListInsert(LL** phead, LL** ptail, LL* pos, LLDataType x)
- {
- assert(phead && ptail);
- assert(*phead);//确保链表有结点
- //如果 pos 位置与头结点位置重合就头插
- if (pos == *phead)
- {
- ListPushHead(phead, ptail, x);
- }
- else
- {
- //在 pos 位置之前插入
- LL* cur = *phead;
- while (cur->next != pos)
- {
- cur = cur->next;
- }
- LL* NewNode = CreateNode(x);
- cur->next = NewNode;
- NewNode->next = pos;
- }
- }
- //在 pos 位置删除
- void ListPopInsert(LL** phead, LL** ptail, LL* pos)
- {
- assert(phead && ptail);
- assert(*phead);//确保链表有结点
- //如果 pos 位置是头结点就头删
- if (pos == *phead)
- {
- ListPopHead(phead, ptail);
- }
- //如果 pos 位置是尾结点就尾删
- else if (pos == *ptail)
- {
- ListPopBack(phead, ptail);
- }
- else
- {
- LL* cur = *phead;
- while (cur->next != pos)
- {
- cur = cur->next;
- }
- cur->next = pos->next;
- free(pos);
- }
- }
- #define _CRT_SECURE_NO_WARNINGS 1
-
- #include
- #include
- #include
- //结点的声明
- typedef int LLDataType;
- typedef struct LinkList
- {
- LLDataType val;
- struct LinkList* next;
- }LL;
- //存放头结点地址的指针
-
-
- void ListPushHead(LL** phead, LL** ptail,LLDataType x);//头插
- void ListPrint(LL* phead);//打印
- void LIstPushBack(LL** phead,LL** ptail ,LLDataType x);//尾插
- void ListPopHead(LL** phead, LL** ptail);//头删
- void ListPopBack(LL** phead, LL** ptail);//尾删
- LL* ListFind(LL* phead,LLDataType x);//查找
- void ListInsert(LL** phead, LL** ptail, LL* pos, LLDataType x);//在 pos 位置插入
- void ListPopInsert(LL** phead, LL** ptail, LL* pos);//删除 pos 位置的数据