数据结构从入门到实战————链表
前言
在计算机科学和数据结构的领域内,链表是一种灵活而重要的线性结构,它不仅支持动态的数据存储,还允许高效地进行插入和删除操作。链表通过节点之间的指针链接实现数据的线性组织,使得链表在实际应用中具有独特的优点。
链表的主要功能包括:
动态大小:链表能够根据需要动态增长和缩减,这意味着我们可以在运行时添加或删除元素,而不需要预先知道数据的确切大小。
插入和删除操作:尽管链表的插入和删除操作通常只需修改指针,因此这些操作的时间复杂度为O(1)(在已知位置的情况下)。
遍历操作:链表支持从头到尾顺序访问每个元素,但不支持随机访问,查找特定元素需要遍历整个列表。
空间效率:与数组相比,链表在内存使用上更加灵活,但每个节点需要额外存储指针,增加了内存开销。
链表的优点在于其灵活性和高效的插入删除操作,缺点是随机访问效率低、内存占用相对较高且不利于缓存局部性。本文将详细介绍链表的基本概念、主要功能及其优缺点,帮助读者更好地理解和应用这一重要数据结构。
正文开始
一、链表的概念及其结构
概念:链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的 。
链表的结构跟火车车厢相似,淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某街车厢去掉/加上,不会影响其他车厢,每节车厢都是独立存在的。
车厢是独立存在的,且每节车厢都有车门。想象⼀下这样的场景,假设每节车厢的车门都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带⼀把钥匙的情况下如何从车头走到车尾?
最简单的做法:每节车厢里都放⼀把下⼀节车厢的钥匙。
在链表里,每节“车厢”是什么样的呢?
与顺序表不同的是,链表里的每节"车厢"都是独立申请下来的空间,我们称之为“结点/节点”
节点的组成主要有两个部分:当前节点要保存的数据和保存下⼀个节点的地址(指针变量)。
图中指针变量 plist保存的是第⼀个节点的地址,我们称plist此时“指向”第⼀个节点,如果我们希
望plist“指向”第⼆个节点时,只需要修改plist保存的内容为0x0012FFA0。
二、链表的实现
为什么还需要指针变量来保存下⼀个节点的位置?
链表中每个节点都是独立申请的(即需要插⼊数据时才去申请⼀块节点的空间),我们需要通过指针变量来保存下⼀个节点位置才能从当前节点找到下⼀个节点。
结合前⾯学到的结构体知识,我们可以给出每个节点对应的结构体代码:
假设当前节点中的数据为整型
1.定义节点的结构
SList.h
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>//定义节点的结构
typedef int SLTDataType;
typedef struct SListNone
{//链表中存放的数据SLTDataType data;//下一个节点的地址struct SListNone* next;
}SLTNode;
当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数
据,也需要保存下⼀个节点的地址(当下⼀个节点为空时保存的地址为空)。
当我们想要从第⼀个节点走到最后⼀个节点时,只需要在前⼀个节点拿上下⼀个节点的地址(下⼀个节点的钥匙)就可以了。
给定的链表结构中,如何实现节点从头到尾的打印?
2.打印链表各个节点中data
SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"//链表数据的打印
void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"void SLTtest1()
{SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode*));node1->data = 1;SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode*));node2->data = 2;node1->next = node2;node2->next = NULL;//调用链表的打印SLTNode* plist = node1;SLTPrint(plist);}int main()
{SLTtest1();return 0;
}
测试结果如下:
3.尾插节点
SList.c
//尾插节点
SLTNode* SLTBuyNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//判断有没有创建成功if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->data = x;newnode->next = NULL;return newnode;
}
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);//创建一个新节点;SLTNode* newnode = SLTBuyNode(x);//首先需要判断链表中是否有节点if (*pphead == NULL){//如果没有节点就直接插入*pphead = newnode;}//如果有就需要找尾else{SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}ptail->next = newnode;}
}
ps:需要注意的点
- 首先是形参为什么要穿二级指针,而不是一级指针。因为我们需要通过形参来改变实参的值,在代码中也就是通过函数中的指针改变plist最终指向哪里。
- 第二需要断言pphead是否为空指针,因为我们无法对空指针进行解引用
- 第三需要判断链表是否为空链表。
test.c
void SLTtest2()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPrint(plist);}
int main()
{//SLTtest1();SLTtest2();return 0;
}
测试结果如下:
4.头插节点
SList.c
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{//判断传过来的是否为空指针assert(pphead);SLTNode* newnode = SLTBuyNode(x);newnode->next = *pphead;*pphead = newnode;
}
test.c
void SLTtest2()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPrint(plist);SLTPushFront(&plist,4);SLTPrint(plist);
}
int main()
{//SLTtest1();SLTtest2();return 0;
}
测试结果如下:
5.尾删节点
SList.c
//尾删节点
void SLTPopBack(SLTNode** pphead)
{//首先得先判断传过来得指针是否为空指针,同时判断链表是否为空链表assert(pphead);assert(*pphead);//如果只有一个节点,直接释放并且置空if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{SLTNode* prev = *pphead;SLTNode* ptail = *pphead;//开始找尾while (ptail->next){prev = ptail;ptail = ptail->next;}free(ptail);ptail = NULL;prev->next = NULL;}
}
ps:需要注意的点
- 首先得先判断传过来得指针是否为空指针,同时判断链表是否为空链表
- 如果只有一个节点,直接释放并且置空;如果有多个节点,则去找尾节点和倒数第二个节点,将最后一个节点释放,并将倒数第二个节点next=NULL。
test.c
void SLTTest2()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPrint(plist);SLTPushFront(&plist, 3);SLTPrint(plist);SLTPopBack(&plist);SLTPrint(plist);
}int main()
{/*SLTTest1();*/SLTTest2();return 0;
}
测试结果如下:
6. 根据data查找节点
SList.c
//查找数据,并返回节点
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* new_phead = phead;while (new_phead){if (x == new_phead->data){printf("找到了\n");return new_phead;}new_phead = new_phead->next;}printf("没找到!");return NULL;
}
test.c
void SLTtest2()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPrint(plist);SLTPushFront(&plist,4);SLTPrint(plist);SLTPopBack(&plist);SLTPrint(plist);//查找数据,并返回节点SLTNode* find = SLTFind(plist, 2);
}
int main()
{//SLTtest1();SLTtest2();return 0;
}
测试结果如下:
7.在指定位置之前插入节点
SList.c
//在指定位置之前插入节点
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos);assert(*pphead);//此时还要考虑头插的问题if (*pphead == pos){SLTNode* new_node = SLTBuyNode(x);new_node->next = pos;*pphead = new_node;}else{SLTNode* pcur = *pphead;while (pcur->next != pos){pcur = pcur->next;}//走到这pcur->pos = next//此时创建新的节点SLTNode* new_node = SLTBuyNode(x);pcur->next = new_node;new_node->next = pos;}
}
test.c
void SLTtest2()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPrint(plist);SLTPushFront(&plist,4);SLTPrint(plist);SLTPopBack(&plist);SLTPrint(plist);//查找数据,并返回节点SLTNode* find = SLTFind(plist, 4);//在指定位置之前插入数据SLTInsert(&plist, find, 33);SLTPrint(plist);
}
int main()
{//SLTtest1();SLTtest2();return 0;
}
测试结果如下
SList.c
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);//创建节点SLTNode* new_node = SLTBuyNode(x);new_node->next = pos->next;pos->next = new_node;
}
test.c
void SLTtest2()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPrint(plist);SLTPushFront(&plist,4);SLTPrint(plist);SLTPopBack(&plist);SLTPrint(plist);//查找数据,并返回节点SLTNode* find = SLTFind(plist, 2);////在指定位置之前插入数据SLTInsert(&plist, find, 33);SLTPrint(plist);//在指定位置之后插入数据SLTInsertAfter(find, 90);SLTPrint(plist);
}
int main()
{//SLTtest1();SLTtest2();return 0;
}
测试结果如下
8.删除pos位置的节点
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);assert(pos);//此时还是要判断是否为头删if (*pphead == pos){SLTNode* node = (*pphead)->next;free(*pphead);*pphead = node;}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}//此时prev->next = posprev->next = prev->next->next;free(pos);pos = NULL;}}
SList.c
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);assert(pos);//此时还是要判断是否为头删if (*pphead == pos){SLTNode* node = (*pphead)->next;free(*pphead);*pphead = node;}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}//此时prev->next = posprev->next = prev->next->next;free(pos);pos = NULL;}}
test.c
void SLTtest2()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPrint(plist);SLTPushFront(&plist,4);SLTPrint(plist);SLTPopBack(&plist);SLTPrint(plist);//查找数据,并返回节点SLTNode* find = SLTFind(plist, 4);//////在指定位置之前插入数据SLTInsert(&plist, find, 33);SLTPrint(plist);//在指定位置之后插入数据SLTInsertAfter(find, 90);SLTPrint(plist);//删除pos位置的节点SLTErase(&plist, find);SLTPrint(plist);
}
int main()
{//SLTtest1();SLTtest2();return 0;
}
测试结果如下
9.删除pos位置之后的节点
SList.c
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{assert(pos && pos->next);SLTNode* del = pos->next;pos->next = pos->next->next;free(del);del = NULL;
}
test.c
void SLTtest2()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPrint(plist);SLTPushFront(&plist,4);SLTPrint(plist);SLTPopBack(&plist);SLTPrint(plist);//查找数据,并返回节点SLTNode* find = SLTFind(plist, 4);//////在指定位置之前插入数据SLTInsert(&plist, find, 33);SLTPrint(plist);//在指定位置之后插入数据SLTInsertAfter(find, 90);SLTPrint(plist);//删除pos位置的节点//SLTErase(&plist, find);//SLTPrint(plist);//删除pos之后的节点SLTEraseAfter(find);SLTPrint(plist);
}
int main()
{//SLTtest1();SLTtest2();return 0;
}
测试结果如下