文章目录


顺序表的问题

  1. 中间/头部的插入删除,时间复杂度为O(N)
  2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

链表



概念:链表是一种物理存储结构上非连续非顺序的存储结构,数据元素的逻辑顺序是通过链表 中的指针链接次序实现的 。


【C语言 数据结构】线性表 - 单向链表_数据结构

一、单向链表

1.1 创建及初始化基本结构 - 节点

【C语言 数据结构】线性表 - 单向链表_初始化_02

提到链表,我们可以将其形式上看成现实生活中的火车(理想模型),每节车厢代表了链表中的一个节点。唯一不同的是,链表在物理存储结构上是非连续的、非顺序的:


【C语言 数据结构】线性表 - 单向链表_链表_03

**在创建链表的时候,我们应当从每个基础节点开始,根据图示创建结构体​​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 单链表尾插


【C语言 数据结构】线性表 - 单向链表_初始化_04

注意点:二级指针的使用


【C语言 数据结构】线性表 - 单向链表_数据结构_05

单链表的尾插就是在创建的链表后面依次添加新的节点。

需要考虑两种情况:

  • 若原链表为空,也就是​​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);
}

测试结果如下:

【C语言 数据结构】线性表 - 单向链表_结点_06

返回顶部


1.4 单链表头插


【C语言 数据结构】线性表 - 单向链表_c语言_07

单链表的头插:

  • 首先我们需要根据传入的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);
}


【C语言 数据结构】线性表 - 单向链表_链表_08

返回顶部


1.5 单链表尾删

初步设计:

// 单链表的尾删
void SListPopBack(SListNode** pphead) {
SListNode* tail = *pphead;
// 循环找到尾结点
while (tail->next != NULL) {
tail = tail->next ;
}
// 释放尾结点
free(tail);
// tail下节点地址置空
tail->next = NULL;
}

野指针问题:

  • 若程序只写到上面,通过如下图所示的步骤后。只是将尾结释放掉了,但是前一个​​a2​​所在的指针依然指向删掉尾结点的地址(不存在),这时会出现野指针。

【C语言 数据结构】线性表 - 单向链表_结点_09

【C语言 数据结构】线性表 - 单向链表_c语言_10

【C语言 数据结构】线性表 - 单向链表_结点_11

  • 所以还需要将前一个节点的​​next​​指针置空

优化:


【C语言 数据结构】线性表 - 单向链表_初始化_12

定义一个pre节点,用于存放尾结点的前一个节点,使其在尾删操作后,指向尾结点的指针置空。

与此同时,还需要考虑的情况

  • 若为空链表(删完了),则直接assert断言直接报错。
  • 若只有一个节点,释放删除后直接置空:

【C语言 数据结构】线性表 - 单向链表_数据结构_13

// 单链表的尾删
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的链表,全部删除后,链表为空(请按任意键继续上面空行):


【C语言 数据结构】线性表 - 单向链表_c语言_14

再次进行删除,直接断言报错,退出程序:


【C语言 数据结构】线性表 - 单向链表_链表_15

整体测试:

// 测试链表尾删
void Test03() {
Test00(); // 初始化
Test01(); // 尾插
Test02(); // 头插
SListPopBack(&sl);
// 打印链表
printf("新的链表尾删一次结果为:\n");
SListPrint(sl);
SListPopBack(&sl);
// 打印链表
printf("新的链表尾删两次次结果为:\n");
SListPrint(sl);
}


【C语言 数据结构】线性表 - 单向链表_链表_16

返回顶部


1.6 单链表头删


【C语言 数据结构】线性表 - 单向链表_初始化_17

删除头节点,只需要选择当前链表的下一个节点做为新的头节点;将原来的头节点释放掉,最后将头指针指向新的节点即可。

最初考虑了三种情况:

  • 链表为空:断言报错退出
  • 链表只有一个节点:释放唯一节点、置空
  • 链表大于等于两个元素:释放头节点、置空

综上:代码可以进一步优化,将只有一个节点的情况归并到链表不为空的情况即可!

// 单链表头删
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);
}


【C语言 数据结构】线性表 - 单向链表_链表_18

返回顶部


1.7 单链表查找


【C语言 数据结构】线性表 - 单向链表_数据结构_19

单链表查找指定数据之后的链表:

  • 因为是查找,不涉及链表值的修改,所以这里我们不需要使用二级指针,使用​​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);
}

【C语言 数据结构】线性表 - 单向链表_初始化_20

返回顶部


1.8 单链表指定position插入

【C语言 数据结构】线性表 - 单向链表_c语言_21

// 单链表在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;
}
}

【C语言 数据结构】线性表 - 单向链表_c语言_22

// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x) {
// 创建新的插入节点
SListNode* newnode = SListNodeInit(x);
// 新节点后指pos后的节点
newnode->next = pos->next;
// pos后指新节点
pos->next = newnode;
}

由上述两种插入方式可以看出,显然在pos后插入新的节点要简单的多,省去了头插的情况讨论。

同时,pos后插的时候注意要从newnode先往后连接节点再向前连接节点的顺序,否则会发生后节点的丢失。

【C语言 数据结构】线性表 - 单向链表_c语言_23


优化:

// 单链表在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);
}


【C语言 数据结构】线性表 - 单向链表_数据结构_24

返回顶部


1.9 单链表指定position删除

【C语言 数据结构】线性表 - 单向链表_结点_25

// 单链表删除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;
}
}

【C语言 数据结构】线性表 - 单向链表_c语言_26

// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos) {
// 找到pos的后一个节点
SListNode* next = pos->next;
// pos指向next的下一个节点
pos->next = next->next;
// 释放pos后的节点
free(next);
next = NULL;
}


【C语言 数据结构】线性表 - 单向链表_初始化_27

优化:

// 单链表指定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);
}


【C语言 数据结构】线性表 - 单向链表_初始化_28

返回顶部


1.10 合并两个链表

【C语言 数据结构】线性表 - 单向链表_初始化_29

// 合并两个链表
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);
}

【C语言 数据结构】线性表 - 单向链表_结点_30

返回顶部


1.11 链表实现多项式求和

【C语言 数据结构】线性表 - 单向链表_c语言_31

多项式的求和类似于链表的合并,首先定义新的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; //返回新链表
}


【C语言 数据结构】线性表 - 单向链表_链表_32

返回顶部