03/20 数据结构之单链表
单链表的增删查改
22/07/20,这篇记录自己的学习历程吧,这篇没涉及到思维层面的东西,就不用过多的文字就描述,注释写在代码旁边了,希望将来的我能看的懂
1.顺序表与链表特点:
顺序表:可动态增长的数组,数据在数组中存储必须是连续的。
优点:
1.可以随机访问。(排序,二分查找)
2.与链式结构对比,缓存命中率比较高。(本质由于物理空间连续)
缺点:
1.中间或者头部的插入很慢,需要挪动数据,时间复杂度是O(N)。
2.存在空间不够时,增容会有一定的消耗和空间浪费。
2.单链表的实现:
头文件SList.h的实现(各个接口函数):
分开放能让整个工程更有条理,更方便。
其中的函数分别是打印,尾插,尾删,头插,头删,查找及修改的声明。
#pragma once
#include <stdio.h>
#include <stdlib.h>
typedef int SListDataType;
//结点
typedef struct SListNode
{
SListDataType data;
struct SListNode* next;
}SListNode;
void SListPrint(SListNode* phead);
void SListPushBack(SListNode** pphead, SListDataType x);
void SListPopBack(SListNode** pphead);
void SListPushFront(SListNode** pphead, SListDataType x);
void SListPopFront(SListNode* phead);
SListNode* SListFind(SListNode* phead, SListDataType x);
SListNode* BuySListNode(SListDataType x);
void SListInsertAfter(SListNode* pos, SListDataType x);
源文件test.c的实现:
主要是要养成好的习惯,写的时候考虑多一点,否则经常出现内存问题,一写多一点代码程序就崩了。
申请一个新结点:
SListNode* BuySListNode(SListDataType x)
{
SListNode* newNode = (SListNode*)malloc(sizeof(SListNode)); // 为新结点分配内存,并将其强制转换为SListNode*类型
if (newNode == NULL) // 检查内存分配是否成功
{
printf("申请结点失败\n"); // 打印错误信息
exit(-1); // 以错误码终止程序
}
newNode->data = x; // 将参数x赋值给新结点的数据域
newNode->next = NULL; // 将新结点的指针域设为NULL
return newNode; // 返回新结点的指针
}
打印一个单链表:
void SListPrint(SListNode* phead)
{
SListNode* cur = phead;
while (cur != NULL)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
尾插:
//注意!此处需要二级指针!才可以改变函数外的一级指针的值
void SListPushBack(SListNode** pphead, SListDataType x)
{
SListNode* newNode = BuySListNode(x); // 调用BuySListNode函数,创建一个新结点
//如果一开始就为空链表
if (*pphead == NULL) // 检查链表是否为空
{
*pphead = newNode; // 如果为空,直接将新结点作为头结点
}
else // 如果不为空
{
//首先要找到尾巴
SListNode* tail = *pphead; // 定义一个指针,指向头结点
while (tail->next != NULL) // 循环遍历链表,直到找到尾结点
{
tail = tail->next; // 指针后移
}
tail->next = newNode; // 将尾结点的指针域指向新结点
}
}
尾删:
void SListPopBack(SListNode** pphead)
{
//1.空
//2.一个结点
//3.一个以上结点
if (*pphead == NULL) // 检查链表是否为空
{
return; // 如果为空,直接返回
}
else if ((*pphead)->next == NULL) // 检查链表是否只有一个结点
{
free(*pphead); // 如果是,释放头结点的内存
*pphead = NULL; // 将头指针设为NULL
}
else // 如果链表有多个结点
{
SListNode* prev = NULL; // 定义一个指针,指向尾结点的前一个结点
SListNode* tail = *pphead; // 定义一个指针,指向尾结点
while (tail->next != NULL) // 循环遍历链表,直到找到尾结点
{
prev = tail; // 指针后移
tail = tail->next; // 指针后移
}
free(tail); // 释放尾结点的内存
prev->next = NULL; // 将前一个结点的指针域设为NULL
}
}
头插:
void SListPushFront(SListNode** pphead, SListDataType x)
{
SListNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
头删:
void SListPopFront(SListNode** pphead)
{
//1.空
//2.一个节点 + 一个以上节点
if (*pphead = NULL)
{
return;
}//如果没有这一段代码下一段可能会出错
else
{
SListNode* next = (*pphead)->next;//先存储再释放
free(*pphead);
*pphead = next;
}
}
查找修改:
在单链表中只要找到了数值,即可找到所对应的地址,利用地址可直接将数值修改,所以在这里查找修改放在一起。不过查找的代码段需要放在SList.c的源文件,修改的代码段需要放在test.c的源文件。
查找:
SListNode* SListFind(SListNode* phead, SListDataType x)
{
SListNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
修改:
假设要查找3,并且把3改成30:
SListNode* pos = SListFind(pList, 3);
if (pos)
{
pos->data = 30;
}
拓展:
例如对于删除功能来说,想删除这个位置的节点,需要找到前一个节点,用双向链表更方便。所以一般用链表存数据,比较少用单链表,更多的是用双向链表。但是Oj题几乎都是单链表,而且后面的哈希和图的临接表都要用到单链表,也不是一无是处啦。
所以在这里由于单链表的缺陷都是删除位置之后的节点。
pos位置之后插入:
void SListInsertAfter(SListNode* pos, SListDataType x)
{
assert(pos);
SListNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
pos位置之后删除x:
void SListEraseAfter(SListNode* pos)
{
assert(pos);
if (pos->next)
{
SListNode* next = pos->next;
SListNode* nextnext = next->next;
pos->next = nextnext;
free(next);
}
}
战损配图:
3.补充知识(存储体系):
CPU进行计算需要拿到数据,数据存放在内存当中,内存的速度有点跟不上CPU,这时候有L1,L2,L3的三级缓存,都比内存快,比三级缓存更快一点的是寄存器,越快的越贵,越慢的空间越大。缓存命中率实际上是由系统的预加载机制导致的,当是顺序表的时候,预加载的刚好就是想要的,是链表的时候,预加载的可能是你想要的但是大部分都是你不想要的。