文章目录
顺序表的问题
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
链表
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表 中的指针链接次序实现的 。
一、单向链表
1.1 创建及初始化基本结构 - 节点
提到链表,我们可以将其形式上看成现实生活中的火车(理想模型),每节车厢代表了链表中的一个节点。唯一不同的是,链表在物理存储结构上是非连续的、非顺序的:
**在创建链表的时候,我们应当从每个基础节点开始,根据图示创建结构体SListNode
代表节点,并声明变量data
表示节点存储的数据、next
表示下一个节点的地址(指针)。 **
创建链表之前,需要确定结构的组成单位 — 节点。这里我们创建结构体SListNode
用来表示单个节点,并将其数据类型抽取出来自定义为SLTDateType
,这样一来方便后续存储内容的数据类型更换,若存储为char类型数据,则只需求改 typedef char SLTDateType;
即可。然后还需要定义一个next变量类型为SListNode*
节点指针变量,通过*
操作指针变量指向的内存,用于存储下一个节点的地址,从而使各节点关联起来。
// 自定义数据类型
typedef int SLTDateType;
// 定义链表节点结构体
typedef struct SListNode {
SLTDateType data; // 节点中存放的数据
struct SListNode* next; // 节点中存放指向下一个节点的指针
}SListNode;
接着就是链表的初始化操作,我们需要给链表的最初节点进行空间的开辟,使用malloc
函数即可:
// 单向链表初始化
SListNode* SListNodeInit(SLTDateType x){
// 开辟一个节点空间
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
// 开辟空间失败
if (newnode == NULL){
printf("无法给节点开辟空间!\n");
exit(-1);
}else{
// 开辟成功,初始化赋值,节点数据为传入的值
newnode->data = x;
newnode->next = NULL;
return newnode;
}
}
SListNode* sl;
// 创建新的链表 - 初始化
void Test00() {
sl = SListNodeInit(1);
}
返回顶部
1.2 单链表输出
链表的打印输出,就是遍历(迭代)链表的每个节点,获取每个节点的数据值进行输出:
// 单链表打印
void SListPrint(SListNode* phead){
SListNode* curr = phead; // 定义临时指针
while (curr != NULL){ // 循环遍历
printf("%c->", curr->data); // 输出每个节点的数据
curr = curr->next; // 指针指向下一个节点
}
}
返回顶部
1.3 单链表尾插
注意点:二级指针的使用
单链表的尾插就是在创建的链表后面依次添加新的节点。
需要考虑两种情况:
- 若原链表为空,也就是
SListNode* newnode = NULL;
,此时我们可直接将新创建的节点赋给原链表 - 若原链表不为空,我们需要在其尾部进行节点的插入操作
- 遍历循环获取尾结点
tail
- 将尾结点
tail.next
指向 新节点
// 单链表尾插
void SListPushBack(SListNode** pphead, SLTDateType x){
// 通过插入的新值创建新的节点
SListNode* newnode = SListNodeInit(x);
// 如果原链表为空
if ( *pphead == NULL){
// 直接将新节点作为链表的开始
*pphead = newnode;
}else{
// 如果不为空,循环找到给出链表的尾结点
SListNode* tail = *pphead;
while (tail->next != NULL){
tail = tail->next;
}
// 将尾节点指向通过插入的新值创建新的节点,实现节点的尾插
tail->next = newnode;
}
}
通过上面的叙述,我们测试一个链表的创建,代码如下:
// 创建新的链表 - 尾插、打印
void Test01() {
Test00(); // 初始化
// 尾插
SListPushBack(&sl, 2);
SListPushBack(&sl, 3);
SListPushBack(&sl, 4);
SListPushBack(&sl, 5);
// 打印链表
printf("创建新的链表为:\n");
SListPrint(sl);
}
测试结果如下:
返回顶部
1.4 单链表头插
单链表的头插:
- 首先我们需要根据传入的x值创建新的节点
- 接下来需要考虑插入节点时,原节点是否为空的情况
- 原节点不为空,此时获取链表的头节点,将
newnode.next
指向头节点(newnode
作为头节点),原头节点的指针指向新的节点 - 原节点为空,也就是根据不为空的情况,还是将
newnode
作为头节点,操作一样,所以包含在了上面的情况之中
// 单链表的头插
void SListPushFront(SListNode** pphead, SLTDateType x) {
// 通过插入的新值创建新的节点
SListNode* newnode = SListNodeInit(x);
// 如果不为空,获取当前链表的第一个节点
SListNode* head = *pphead;
// 将新节点作为链表的开始,新链表成为原链表的第一个节点
newnode->next = head;
*pphead = newnode;
}
// 测试链表头插
void Test02() {
Test00(); // 初始化
Test01(); // 尾插
// 头插
SListPushFront(&sl,0);
SListPushFront(&sl,-1);
// 打印链表
printf("新的链表头插结果为:\n");
SListPrint(sl);
}
返回顶部
1.5 单链表尾删
初步设计:
// 单链表的尾删
void SListPopBack(SListNode** pphead) {
SListNode* tail = *pphead;
// 循环找到尾结点
while (tail->next != NULL) {
tail = tail->next ;
}
// 释放尾结点
free(tail);
// tail下节点地址置空
tail->next = NULL;
}
野指针问题:
- 若程序只写到上面,通过如下图所示的步骤后。只是将尾结释放掉了,但是前一个
a2
所在的指针依然指向删掉尾结点的地址(不存在),这时会出现野指针。
优化:
定义一个pre
节点,用于存放尾结点的前一个节点,使其在尾删操作后,指向尾结点的指针置空。
与此同时,还需要考虑的情况
- 若为空链表(删完了),则直接
assert断言
直接报错。 - 若只有一个节点,释放删除后直接置空:
// 单链表的尾删
void SListPopBack(SListNode** pphead) {
// 断言:链表为空报错
assert(*pphead != NULL);
// 如果链表只有一个元素
if ((*pphead)->next == NULL) {
// 释放空间
free(*pphead);
// 置空
*pphead = NULL;
}else {
SListNode* tail = *pphead; // tail 尾结点
SListNode* pre = *pphead; // pre 尾结点前一个节点
// 循环
while (tail->next != NULL) {
// 记录尾结点的前一个节点
pre = tail;
// 尾结点随循环后移
tail = tail->next;
}
// 直到最后,释放尾结点
free(tail);
// tail下一节点地址置空
tail->next = NULL;
// pre下一节点地址置空
pre->next = NULL;
}
}
测试创建1-4的链表,全部删除后,链表为空(请按任意键继续上面空行):
再次进行删除,直接断言报错,退出程序:
整体测试:
// 测试链表尾删
void Test03() {
Test00(); // 初始化
Test01(); // 尾插
Test02(); // 头插
SListPopBack(&sl);
// 打印链表
printf("新的链表尾删一次结果为:\n");
SListPrint(sl);
SListPopBack(&sl);
// 打印链表
printf("新的链表尾删两次次结果为:\n");
SListPrint(sl);
}
返回顶部
1.6 单链表头删
删除头节点,只需要选择当前链表的下一个节点做为新的头节点;将原来的头节点释放掉,最后将头指针指向新的节点即可。
最初考虑了三种情况:
- 链表为空:断言报错退出
- 链表只有一个节点:释放唯一节点、置空
- 链表大于等于两个元素:释放头节点、置空
综上:代码可以进一步优化,将只有一个节点的情况归并到链表不为空的情况即可!
// 单链表头删
void SListPopFront(SListNode** pphead) {
// 断言:链表为空报错
assert(*pphead != NULL);
// 如果链表只有一个元素
if ((*pphead)->next == NULL) {
// 释放
free(*pphead);
// 置空
*pphead = NULL;
}else {
// 新头节点链表
SListNode* newhead = (*pphead)->next;
// 释放头结点
free(*pphead);
*pphead = newhead;
}
}
// 优化
// 单链表头删
void SListPopFront(SListNode** pphead) {
// 断言:链表为空报错
assert(*pphead != NULL);
// 新头节点链表
SListNode* newhead = (*pphead)->next;
// 释放头结点
free(*pphead);
*pphead = newhead;
}
// 测试链表头删
void Test04() {
Test00(); // 初始化
Test01(); // 尾插
Test02(); // 头插
Test03(); // 尾删
SListPopFront(&sl);
// 打印链表
printf("新的链表头删一次结果为:\n");
SListPrint(sl);
SListPopFront(&sl);
// 打印链表
printf("新的链表头删两次次结果为:\n");
SListPrint(sl);
}
返回顶部
1.7 单链表查找
单链表查找指定数据之后的链表:
- 因为是查找,不涉及链表值的修改,所以这里我们不需要使用二级指针,使用
SListNode*
即可 - 通过我们指定的数据值,在链表中循环比较值的大小
- 若值与当前
curr
节点的值相同,则直接输出当前的链表(值所在节点开始的链表) - 若没有找到,直接返回
NULL
// 单链表查找
SListNode* SListFind(SListNode* phead, SLTDateType x) {
// 定义游标节点
SListNode* curr = phead;
// 循环遍历每个节点
while (curr->next!=NULL){
// 如果节点值为要查找的数据
if (curr->data == x) {
// 返回当前链表
return curr;
}else {
// 否则继续移动到下一节点判断
curr = curr->next;
}
}
// 最终没有找到返回空
return NULL;
}
// 测试链表查找
void Test05() {
Test00(); // 初始化
Test01(); // 尾插
Test02(); // 头插
Test03(); // 尾删
Test04(); // 头删
SListNode * find = SListFind(sl,2);
// 打印链表
printf("查找链表中值为2的地方及其后续:\n");
SListPrint(find);
}
返回顶部
1.8 单链表指定position插入
// 单链表在pos位置之前插入x
void SListInsert(SListNode** pphead, SListNode* pos, SLTDateType x) {
// 创建新的插入节点
SListNode* newnode = SListNodeInit(x);
// 如果pos等于头节点,即为头插
if (*pphead==pos) {
newnode->next = *pphead;
*pphead = newnode;
}else {
// 找到pos的前一个位置
SListNode* pospre = *pphead;
// 循环找到指定位置节点的前一个节点
while (pospre->next != pos) {
pospre = pospre->next;
}
// 指定位置节点的前一个节点为新加入节点
pospre->next = newnode;
// 新加入节点的后一个节点为指定位置的节点
newnode->next = pos;
}
}
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x) {
// 创建新的插入节点
SListNode* newnode = SListNodeInit(x);
// 新节点后指pos后的节点
newnode->next = pos->next;
// pos后指新节点
pos->next = newnode;
}
由上述两种插入方式可以看出,显然在pos后插入新的节点要简单的多,省去了头插的情况讨论。
同时,pos后插的时候注意要从newnode先往后连接节点再向前连接节点的顺序,否则会发生后节点的丢失。
优化:
// 单链表在position位置之后插入x
void SListInsertAfterPosition(SListNode** pphead, int position, SLTDateType x) {
// 定义游标节点
SListNode* curr = *pphead;
// 循环遍历每个节点
int i = 0;
while (i < (position-1) && (*pphead)->next) {
// 否则继续移动到下一节点判断
curr = curr->next;
i++;
}
// 插入位置判断
if (!(*pphead)->next || i > position - 1) {
printf("插入位置不合理!");
exit(-1);
}
SListNode* newnode = SListNodeInit(x);// 创建新的插入节点
newnode->next = curr->next; // 新节点后指pos后的节点
curr->next = newnode; // pos后指新节点
}
// 测试链表指定位置插入
void Test06() {
Test00(); // 初始化
Test01(); // 尾插
Test02(); // 头插
Test03(); // 尾删
Test04(); // 头删
SListInsertAfterPosition(&sl,1,0);
// 打印链表
printf("指定1位置插入后的链表:\n");
SListPrint(sl);
}
返回顶部
1.9 单链表指定position删除
// 单链表删除pos位置的值
void SListErase(SListNode** pphead, SListNode* pos) {
// 头删
if (*pphead == pos) {
*pphead = pos->next;
free(pos);
}else {
// 找到pos的前一个位置
SListNode* pospre = *pphead;
// 循环找到指定位置节点的前一个节点
while (pospre->next != pos) {
pospre = pospre->next;
}
// pos前一个节点指向pos后一个节点
pospre->next = pos->next;
// 释放pos
free(pos);
pos = NULL;
}
}
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos) {
// 找到pos的后一个节点
SListNode* next = pos->next;
// pos指向next的下一个节点
pos->next = next->next;
// 释放pos后的节点
free(next);
next = NULL;
}
优化:
// 单链表指定position位置的节点
void SListEraseAfterPosition(SListNode** pphead, int position) {
// 找到pos的前一个位置
SListNode* curr = *pphead;
int i = 0;
// 循环找到指定位置节点的前一个节点
while (i < (position - 1) && (*pphead)->next) {
curr = curr->next;
i++;
}
// 删除位置判断
if (!(*pphead)->next || i > position - 1) {
printf("删除位置不合理!");
exit(-1);
}
// 找到position的后一个节点
SListNode* next = curr->next;
// position指向next的下一个节点
curr->next = next->next;
// 释放pos后的节点
free(next);
next = NULL;
}
// 测试链表指定位置删除
void Test07() {
Test00(); // 初始化
Test01(); // 尾插
Test02(); // 头插
Test03(); // 尾删
Test04(); // 头删
Test06(); //指定位置插入
SListEraseAfterPosition(&sl, 1);
// 打印链表
printf("指定1位置删除后的链表:\n");
SListPrint(sl);
}
返回顶部
1.10 合并两个链表
// 合并两个链表
struct SListNode* mergeTwoList(SListNode* l1, SListNode* l2) {
// 判断l1、l2,若其中一个为空,直接返回另一个
if (l1 == NULL) return l2;
if (l2 == NULL) return l1;
// 定义新链表head存储合并后的,tail记录尾结点
SListNode* head = NULL, * tail = NULL;
// 当两个链表都不为空时,循环比较
while (l1 && l2) {
// 如果l1的当前节点值小于l2的当前节点值
if ((l1->data) < (l2->data)) {
// 刚开始时,head、tail均为空,直接赋为较小的l1
if (head == NULL) {
head = tail = l1;
} else { // 若不为空tail后移一个节点
tail->next = l1;
tail = tail->next;
}
// 同时l1后移一个节点,准备下一次比较
l1 = l1->next;
} else { // 同理,如果l1的当前节点值大于l2的当前节点值
if (head == NULL) {
head = tail = l2;
} else {
tail->next = l2;
tail = tail->next;
}
l2 = l2->next;
}
}
// 如果l1、l2中任一为空,直接将另一个添加
if (l1) {
tail->next = l1;
}
if (l2) {
tail->next = l2;
}
return head;
}
void Test02() {
SListNode* l1 = SListNodeInit(3);
printf("合并前的两个链表:\n");
// 尾插
SListPushBack(&l1, 7);
SListPushBack(&l1, 8);
SListPushBack(&l1, 9);
SListPrint(l1);
SListNode* l2 = SListNodeInit(2);
// 尾插
SListPushBack(&l2, 5);
SListPushBack(&l2, 7);
SListPrint(l2);
// 合并链表
SListNode * merge = mergeTwoList(l1, l2);
// 打印
printf("合并后的两个链表:\n");
SListPrint(merge);
}
// 测试合并两个链表
void Test08() {
SListNode* l1 = SListNodeInit(3);
printf("合并前的两个链表:\n");
// 尾插
SListPushBack(&l1, 7);
SListPushBack(&l1, 8);
SListPushBack(&l1, 9);
SListPrint(l1);
SListNode* l2 = SListNodeInit(2);
// 尾插
SListPushBack(&l2, 5);
SListPushBack(&l2, 7);
SListPrint(l2);
// 合并链表
SListNode * merge = mergeTwoList(l1, l2);
// 打印
printf("合并后的两个链表:\n");
SListPrint(merge);
}
返回顶部
1.11 链表实现多项式求和
多项式的求和类似于链表的合并,首先定义新的front
链表存储合并结果,将链表的节点定义为三部分:a-多项式系数
,b-多项式指数
,next-指向下一个节点
。依次比较多项式链表的每一个节点中指数 b
的大小:
- 如果两多项式的指数相同,并且系数和不为零,对
front
进行新的节点尾插,p1、p2同时后移一个节点; - 若
p1.b > p2.b
,尾插p2
当前节点,p2
向后移一个节点; - 若
p1.b < p2.b
,尾插p1
当前节点,p1
向后移一个节点;
如果任一链表为空(遍历比较完成),则将另一链表剩余节点添加至front
// 多项式的尾插
void PolyPushBack(Poly** p, PolyDateType d1, PolyDateType d2) {
// 创建新的多项式节点
Poly* newPoly = createPolyn(d1, d2);
// 如果原链表为空,则直接令新的节点作为链表开始
if (*p == NULL) {
*p = newPoly;
}
else {
// 循环链表找到尾结点
Poly* tail = *p;
while (tail->next != NULL) {
tail = tail->next;
}
// 将尾结点指向新创的节点
tail->next = newPoly;
}
}
//比较两个结点的指数大小
int Compare(PolyDateType e1, PolyDateType e2)
{
if (e1 > e2)
return 1;
else if (e1 < e2)
return -1;
else
return 0;
}
// 两个多项式求和
Poly* AddTwoPolyn(Poly* p1, Poly* p2) {
Poly* front = NULL; // 相加后的链表的头指针
// 遍历两个多项式
while (p1 && p2) {
switch (Compare(p1->b, p2->b)) {
case 0: {
// 如果两个多项式的指数相同,则系数相加
PolyDateType sum = (p1->a) + (p2->a);
if (sum) {
// 尾插求和的多项式
PolyPushBack(&front, sum, p1->b);
}
// 多项式同时后移
p1 = p1->next;
p2 = p2->next;
break;
}
case 1:
PolyPushBack(&front, p2->a, p2->b);
p2 = p2->next;
break;
case -1:
PolyPushBack(&front, p1->a, p1->b);
p1 = p1->next;
break;
}
}
while (p1) { //将P1剩余项尾插到链表后面
PolyPushBack(&front, p1->a, p1->b);
p1 = p1->next;
}
while (p2) { //将P1剩余项尾插到链表后面
PolyPushBack(&front, p2->a, p2->b);
p2 = p2->next;
}
return front; //返回新链表
}
返回顶部