《数据结构初阶》【顺序表 + 单链表 + 双向链表】
《数据结构初阶》【顺序表 + 单链表 + 顺序表】
- 前言:
- 先聊些其他的东西!!!
- 什么是线性表?
- 什么是顺序表?
- 顺序表的种类有哪些?
- 什么是链表?
- 链表的种类有哪些?
- ---------------顺序表---------------
- 动态顺序表的实现
- 头文件
- 实现文件
- 测试文件
- 运行结果
- ---------------单链表---------------
- 无头单向非循环链表的实现
- 头文件
- 实现文件
- 测试文件
- 运行结果
- 心得总结
- 哪些操作使用了断言?都使用了哪些断言?
- 哪些操作是需要分情况处理的?都分为哪些情况?
- ---------------双向链表---------------
- 带头双向循环链表的实现
- 头文件
- 实现文件
- 测试文件
- 运行结果
- 心得总结
- 顺序表和链表的区别有哪些?
往期《数据结构初阶》回顾:
【时间复杂度 + 空间复杂度】
前言:
先聊些其他的东西!!!
在之前的博客中博主向大家信誓旦旦地宣布博主之后将持续更新的《数据结构初阶》这个系列的博客。
博客内容主要划分为:数据结构的介绍 + 数据结构的实现 + 数据结构的OJ练习,这三大板块的内容。
结果一动手发现——好家伙!三部分加起来有2万字,要是把OJ练习也塞进来,怕是要写成《数据结构从入门到放弃》了!
所以博主这里选择先将前两个板块的内容写成一篇博客,至于数据结构的OJ练习这个板块就单独成文。
温馨提示
:这篇博客中的主要内容是代码,每个代码块中的代码都有非常详细的注释,相信各位勇士一定能征服这些数据结构!✨(毕竟博主的注释写得比情书还用心💘)
什么是线性表?
线性表(Linear List)
:是具有相同数据类型的n(n≥0)个数据元素的有限序列。
线性表是数据结构中最基本、最简单的一种结构。
线性表是一种在实际中广泛使用的数据结构。
常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑结构上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以
顺序结构
和链式结构
的形式存储。线性表有两种主要的存储结构:
顺序存储结构(顺序表)
- 用一组地址连续的存储单元依次存储线性表的元素
- 可以通过数组实现
链式存储结构(链表)
用一组任意的存储单元存储线性表的元素
每个元素除了存储数据外,还需要存储指向后继元素的指针
特性 | 顺序表 (Array List) | 链表 (Linked List) |
---|---|---|
逻辑结构 | 1. 线性结构,元素按顺序排列 2. 通过下标直接表示逻辑关系 | 1. 线性结构,元素通过指针链接 2. 逻辑顺序由指针决定 |
物理结构 | 1. 连续内存空间存储 2. 物理顺序 = 逻辑顺序 | 1. 非连续内存存储(节点分散) 2. 物理顺序 != 逻辑顺序 |
什么是顺序表?
顺序表(Sequential List)
:是线性表的顺序存储结构,即用一组地址连续的存储单元依次存储线性表中的数据元素。顺序表在内存中的物理结构与逻辑结构一致,元素之间的顺序关系由存储位置决定。
顺序表的种类有哪些?
顺序表一般可以分为:
1. 静态顺序表:使用定长数组存储元素
--------------------------顺序表的静态存储实现-----------------------------// 定义顺序表的最大容量为7
#define N 7// 定义顺序表存储的数据类型为int(便于后续灵活修改数据类型)
typedef int SLDataType;// 定义静态顺序表的结构体
typedef struct SeqList
{size_t size; //1.记录当前顺序表中有效数据的个数(即:表长)SLDataType array[N]; //2.静态分配的定长数组,用于存储顺序表元素
} SeqList;
2. 动态顺序表:使用动态开辟的数组存储
--------------------------顺序表的动态存储实现-----------------------------// 定义顺序表存储的数据类型(默认为int,可通过修改此处改变整个表的数据类型)
typedef int SLDataType;// 定义动态顺序表结构体
typedef struct SeqList
{size_t size; //1.当前顺序表中实际存储的有效元素个数 size_t capacity; //2.当前动态数组的总容量大小SLDataType* array; //3.指向动态开辟的数组空间的首地址
} SeqList;
什么是链表?
链表(Linked List)
:是一种线性表的 链式存储结构,它通过 指针(或引用) 将一组 零散的内存块(结点)串联起来,形成逻辑上的线性序列。
链表的种类有哪些?
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
单向或者双向
带头或者不带头
循环或者非循环
虽然链表有这么多的结构,但是我们实际中最常用的只有以下两种结构:
无头单向非循环链表
:又名为单链表
- 结构最简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构
- 如:哈希桶、图的邻接表等等。
带头双向循环链表
:又名为双向链表
- 结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表
---------------顺序表---------------
动态顺序表的实现
头文件
-------------------------------SeqList.h--------------------------------#pragma once//任务1:包含需要使用的头文件
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>//任务2:定义顺序表的存储结构
typedef int SLDataType;
typedef struct SeqList
{//1.动态顺序的底层使用动态数组实现 ---> 一个SLDataType类型的指针(代表动态数组的首元素地址)//2.记录当前动态顺序中元素的数量 ---> 一个int类型的变量//3.记录动态顺序表的容量 ---> 一个int类型的变量SLDataType* a;int size;int capacity;
}SL;//任务3:声明动态顺表使用的工具函数
//1.扩容函数
void SLCheckCapacity(SL* ps);//任务4:声明顺序表的接口函数/*--------------------- 基础操作 ---------------------*/
//1.顺序表的初始化
//2.顺序表的销毁
//3.顺序表的打印/*--------------------- 插入删除操作 ---------------------*/
//4.顺序表的头插
//5.顺序表的尾插
//6.顺序表的头删
//7.顺序表的尾删/*--------------------- 指定位置操作 ---------------------*/
//8.顺序表的指定位置插入
//9.顺序表的指定位置删除
//10.顺序表的查找某个元素void SLInit(SL* ps);
void SLDestroy(SL* ps);
void SLPrint(SL ps);void SLPushBack(SL* ps, SLDataType x);
void SLPushFront(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPopFront(SL* ps);void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);
int SLFind(SL* ps, SLDataType x);
实现文件
-------------------------------SeqList.c--------------------------------#include "SeqList.h"/*--------------------- 工具函数的实现 ---------------------*/
//1.实现:“动态顺序表的扩容”的工具函数
/*** @brief 检查并扩容顺序表* @param ps 指向顺序表结构的指针* @note 当size == capacity时自动扩容* 初始容量为4,后续每次扩容为原来的2倍*/void SLCheckCapacity(SL* ps)
{if (ps->size == ps->capacity){//1.先判断需要扩容的数量int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;//2.再使用realloc进行空间的扩容SLDataType* tmp = (SLDataType*)realloc(ps->a, newCapacity * sizeof(SL)); //注意:这里先使用一个临时的指针指向开辟的这片空间,因为开辟空间可能开辟失败//2.1:使用if判断:扩容是否成功if (tmp == NULL){perror("realloc fail");return;}//3.最后更新指针和容量(扩容成功)ps->a = tmp;ps->capacity = newCapacity;}
}/*--------------------- 顺序表接口函数的实现 ---------------------*///1.实现:“顺序表的初始化”操作
/*** @brief 初始化顺序表* @param ps 指向顺序表结构的指针* @note 将顺序表置为空表,a指针置NULL* size和capacity初始化为0*/
void SLInit(SL* ps)
{assert(ps);ps->a = NULL;ps->size = 0;ps->capacity = 0;
}//2.实现:“顺序表的销毁”操作
/*** @brief 销毁顺序表* @param ps 指向顺序表结构的指针* @note 释放动态分配的数组内存* 并将所有成员重置为初始状态*/
void SLDestroy(SL* ps)
{assert(ps);free(ps->a); //顺序表的销毁相较于初始化唯一的不同在于销毁时需要将ps->a指向的动态开辟的空间释放掉;//同时我们也要注意我们的初始化操作中没有动态开辟空间ps->a = NULL;ps->size = 0;ps->capacity = 0;
}//3.实现:“顺序表的打印”操作
/*** @brief 打印顺序表内容* @param s 顺序表结构(传值)* @note 遍历打印所有有效元素*/
void SLPrint(SL s)
{for (int i = 0; i < s.size; i++){printf("%d ", s.a[i]);}printf("\n");
}//4.实现:“顺序表的尾插”操作
/*** @brief 顺序表尾部插入元素* @param ps 指向顺序表结构的指针* @param x 要插入的元素值* @note 先检查容量,不足则自动扩容* 时间复杂度O(1)(不考虑扩容)*/
void SLPushBack(SL* ps, SLDataType x)
{assert(ps);SLCheckCapacity(ps);//1.直接在数组的尾部添加要插入的元素ps->a[ps->size] = x;//2.将顺序表中当前元素的数量+1ps->size++;
}//5.实现:“顺序表的头插”操作
void SLPushFront(SL* ps, SLDataType x)
{assert(ps);SLCheckCapacity(ps);//1.将数组中的所有元素都向后挪动一位(从后向前处理元素)for (int i = ps->size - 1; i >= 0; i--){ps->a[i + 1] = ps->a[i];}//2.直接在数组的头部添加要插入的元素ps->a[0] = x;//3.将顺序表中当前元素的数量+1ps->size++;}//6.实现:“顺序表的尾删”操作
/*** @brief 顺序表尾部删除元素* @param ps 指向顺序表结构的指针* @note 只需减小size,不实际释放内存* 时间复杂度O(1)*/
void SLPopBack(SL* ps)
{assert(ps);assert(ps->size > 0);//1.直接顺序表中当前的元素的数量-1ps->size--;
}//7.实现:“顺序表的头删”操作void SLPopFront(SL* ps)
{assert(ps);assert(ps->size > 0);//1.将数组中的所有的元素都向前移动一位(从前往后处理元素)for (int i = 1; i <= ps->size - 1; i++){ps->a[i - 1] = ps->a[i];}//2.将顺序表中当前的元素的数量-1ps->size--;
}//8.实现:“顺序表的指定位置的前面插入”操作
/*** @brief 在指定位置前面插入元素* @param ps 指向顺序表结构的指针* @param pos 插入位置(0-based)* @param x 要插入的元素值* @note 位置必须合法(0 <= pos <= size)* 自动检查扩容,时间复杂度O(n)*/
void SLInsert(SL* ps, int pos, SLDataType x)
{assert(ps);assert(pos >= 0 && pos <= ps->size);SLCheckCapacity(ps);//1.将指定位置及其之后的所有的元素都向后挪动一个位置(从后往前处理元素)for (int i = ps->size - 1; i >= pos; i--){ps->a[i + 1] = ps->a[i];}//2.直接在数组的pos位置上添加想要插入的元素ps->a[pos] = x;//3.将顺序表中当前元素的数量+1ps->size++;
}//9.实现:“顺序表的指定位置的删除”操作
/*** @brief 删除指定位置元素* @param ps 指向顺序表结构的指针* @param pos 删除位置(0-based)* @note 位置必须合法(0 <= pos < size)* 时间复杂度O(n)*/void SLErase(SL* ps, int pos)
{assert(ps);assert(pos >= 0 && pos < ps->size);//1.将指定位置之后的所有的元素都向前挪动一个位置(从前向后处理元素)for (int i = pos + 1; i <= ps->size - 1; i++){ps->a[i - 1] = ps->a[i];}//2.将顺序表中当前元素的数量-1ps->size--;
}//10.实现:“顺序表的查找某个元素”操作
int SLFind(SL* ps, SLDataType x)
{assert(ps);for (int i = 0; i < ps->size; i++){if (ps->a[i] == x)return i;}return -1;
}
测试文件
--------------------------------Test.c---------------------------------#include "SeqList.h"/*** @brief 测试顺序表基础功能* @note 包含初始化、销毁、尾部插入、打印等基础测试*/
void test01()
{SL sl;SLInit(&sl);// 测试头插SLPushFront(&sl, 5);SLPushFront(&sl, 3);printf("头插2个元素后: ");SLPrint(sl); // 预期输出:3 5// 测试尾插SLPushBack(&sl, 7);SLPushBack(&sl, 9);printf("尾插2个元素后: ");SLPrint(sl); // 预期输出:3 5 7 9// 测试头删SLPopFront(&sl);printf("头删1次后: ");SLPrint(sl); // 预期输出:5 7 9// 测试尾删SLPopBack(&sl);printf("尾删1次后: ");SLPrint(sl); // 预期输出:5 7SLDestroy(&sl);printf("\n");
}/*** @brief 测试顺序表高级功能* @note 测试指定位置插入/删除、查找等功能* 验证边界条件处理是否正确*/
void test02()
{SL sl; // 声明顺序表变量SLInit(&sl); //准备测试数据 SLPushBack(&sl, 1); SLPushBack(&sl, 2); SLPushBack(&sl, 3); SLPushBack(&sl, 4); printf("初始数据: ");SLPrint(sl); // 预期输出:1 2 3 4///测试指定位置插入SLInsert(&sl, 1, 99); SLInsert(&sl, sl.size, 88); printf("插入后数据: ");SLPrint(sl); // 预期输出:1 99 2 3 4 88//测试指定位置删除 SLErase(&sl, 1); printf("删除后数据: ");SLPrint(sl); // 预期输出:1 2 3 4 88///测试查找功能 int find = SLFind(&sl, 40); if (find < 0) {printf("没有找到!\n"); }else {printf("找到了!下标为%d\n", find);}SLDestroy(&sl);
}int main()
{test01();test02();return 0;
}
运行结果
---------------单链表---------------
无头单向非循环链表的实现
头文件
-----------------------------SingleList.h-------------------------------#pragma once//任务1:包含要使用的头文件
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>//任务2:定义单链表的存储结构
typedef int SLTDataType;typedef struct singleListNode
{//1.记录链表中节点的值 ---> 一个SLTDataType类型的变量//2.记录下一个节点的地址 ---> 一个struct singleListNode*类型的指针SLTDataType data;struct singleListNode* next;
}SLTNode;//任务3:声明单链表使用的工具函数
SLTNode* SLTCreateNode(SLTDataType x);//任务4:声明单链表的接口函数//0.单链表的打印//1.单链表的尾插
//2.单链表的头插
//3.单链表的尾删
//4.单链表的头删//5.单链表的查找
//6.单链表的指定节点的前驱节点插入
//7.单链表的指定节点的后继节点插入
//8.单链表的指定节点的删除
//9.单链表的指定节点的后继节点的删除
//10.单链表的销毁void SLTPrint(SLTNode* phead);void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
void SLTErase(SLTNode** pphead, SLTNode* pos);
void SLTEraseAfter(SLTNode* pos);
void SLTDestroy(SLTNode** pphead);
实现文件
-----------------------------SingleList.c-------------------------------#include "SingleList.h"//0.实现:“单边表的节点创建”工具函数
/*** @brief 动态创建一个新的链表节点并初始化* @param x 要存储在新节点中的数据* @return 返回指向新创建节点的指针* @note 1. 使用malloc动态分配内存* 2. 检查内存分配是否成功* 3. 初始化节点的data和next成员*/SLTNode* SLTCreateNode(SLTDataType x)
{//1.节点空间的创建SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));//1.1:判断空间是否开辟成功if (newNode == NULL){perror("malloc fail");return NULL;}//2.节点参数的初始化newNode->data = x;newNode->next = NULL;//3.节点地址的返回return newNode;
}//1.实现:“单链表的打印”操作
/*** @brief 打印单链表的所有元素* @param phead 指向单链表头节点的指针* @note 遍历链表并打印每个节点的数据,最后以NULL结尾*/
void SLTPrint(SLTNode* phead)
{//1.定义一个临时的指针代替phead指针遍历整个链表SLTNode* pcur = phead;//2.进行循环遍历while (pcur != NULL){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}//2.实现:“单链表的尾插”操作
/*** @brief 在单链表的尾部插入新节点* @param pphead 指向头节点指针的指针(二级指针,用于修改头节点)* @param x 要插入的数据* @note 1. 如果链表为空(*pphead == NULL),新节点成为头节点* 2. 如果链表非空,遍历找到尾节点,并在其后插入新节点*/
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead); //断言检查1:确保传入的二级指针pphead是有效的,防止对空指针进行解引用的操作//1.创建一个新节点并将其初始化SLTNode* newNode = SLTCreateNode(x);//2.情况1:处理单链表是空链表的情况if (*pphead == NULL){//1.1:更新头指针*pphead = newNode;}//3.情况2:处理单链表是非空链表的情况else{//3.1:遍历链表找到尾节点的位置SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}//3.2:将新节点链接到链表的尾部ptail->next = newNode;}
}//3.实现:“单链表的头插”操作
/*** @brief 在单链表的头部插入新节点* @param pphead 指向头节点指针的指针(二级指针,用于修改头节点)* @param x 要插入的数据* @note 1. 新节点会成为新的头节点* 2. 无论链表是否为空都适用*/
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead); //断言检查1:确保传入的二级指针pphead是有效的,防止对空指针进行解引用的操作//1.创建一个新节点并将其初始化SLTNode* newNode = SLTCreateNode(x);//2.将新节点链接到链表的头部 (注意:这里无论链表的是空链表还是非空链表都是符合)newNode->next = *pphead;//3.更新头指针*pphead = newNode;
}//4.实现:“单链表的尾删”操作
/*** @brief 删除单链表的尾节点* @param pphead 指向头节点指针的指针(二级指针)* @note 1. 链表不能为空* 2. 处理单节点和多节点不同情况* 3. 释放尾节点内存并维护链表结构*/void SLTPopBack(SLTNode** pphead)
{assert(pphead); //断言检查1:确保传入的二级指针pphead是有效的,防止对空指针进行解引用的操作assert(*pphead); //断言检查2:确保单链表是非空单链表,防止对空链表进行删除节点的操作//情况1:处理单链表中只有一个节点的情况if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}//情况2:处理单链表中节点不止一个的情况else{//1.找到尾节点前面的那个节点的位置SLTNode* prev = *pphead;while (prev->next->next != NULL){prev = prev->next;}//2.断开尾节点的链接 + 释放尾节点的内存//2.1:定义指针指向要删除的节点SLTNode* del = prev->next;//2.2:断开要删除的节点的链接prev->next = prev->next->next;//2.3:释放要删除的节点的内存free(del);//2.4:将指向被删除节点的指针都置空del = NULL;}
}//5.实现:“单链表的头删”操作
/*** @brief 删除单链表的头节点* @param pphead 指向头节点指针的指针(二级指针)* @note 1. 链表不能为空* 2. 释放原头节点内存* 3. 更新头指针指向下一个节点*/
void SLTPopFront(SLTNode** pphead)
{assert(pphead); //断言检查1:确保传入的二级指针pphead是有效的,防止对空指针进行解引用的操作assert(*pphead); //断言检查2:确保单链表是非空单链表,防止对空链表进行删除节点的操作//1.定义指向头节点的下一个节点的指针SLTNode* next = (*pphead)->next;//2.释放头指针指向的头节点的内存free(*pphead);//3.更新头指针 (注意:这里并没有将指向被删除节点的指针*pphead置空,原因是:*pphead会被更新为next指针所在的位置并未变成野指针)*pphead = next;
}//6.实现:“单链表的查找”操作
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* pcur = phead;while (pcur != NULL){if (pcur->data == x) return pcur;pcur = pcur->next;}return NULL;
}//7.实现:“单链表的指定节点的前驱节点插入”操作
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead); //断言检查1:确保传入的二级指针pphead是有效的,防止对空指针进行解引用的操作assert(*pphead); //断言检查2:确保单链表是非空单链表,防止对空链表进行指定节点之前插入的操作assert(pos); //断言检查3:确保pos指针有效,防止对空节点之前插入节点SLTNode* newNode = SLTCreateNode(x);//情况1:处理pos是头节点的情况 --> 相当于头插if (pos == *pphead){SLTPushFront(pphead, x);}//情况2:处理pos不是头节点的情况else{//1.找到pos节点前面那个节点的位置SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}//2.链接新节点:prev -> newNode -> pos (新插入的节点的前后节点有独立的指针指向,所以这里的链接随意)prev->next = newNode;newNode->next = pos;}}//8.实现:“单链表的指定节点的后继节点插入”操作
/*** @brief 在单链表指定节点后插入新节点* @param pos 要在其后插入新节点的目标节点指针* @param x 要插入的新数据* @note 1. 不需要头指针,直接操作pos节点* 2. 时间复杂度O(1)* 3. 新节点插入在pos和原pos->next之间*/void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos); //断言检查1:确保pos指针有效,防止对空节点之后插入节点SLTNode* newNode = SLTCreateNode(x);//链接新节点:pos -> newNode -> pos->next 新插入的节点的后一个节点没有独立的指针指向,所以这里的链接顺序必须是下面的这个顺序//同时这也是为什么我们传参数的时候之传入一个指针即可,因为一个指针就可以管控newNode节点前后的两个节点//1.先链接新节点的下一个节点newNode->next = pos->next;//2.再链接新节点的上一个节点pos->next = newNode;
}//9.实现:“单链表的指定节点的删除”操作
/*** @brief 删除单链表中的指定节点* @param pphead 指向头节点指针的指针(二级指针)* @param pos 要删除的目标节点指针* @note 1. 处理pos是头节点和非头节点两种情况* 2. 需要维护链表结构完整性* 3. 释放被删除节点的内存*/
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead); //断言检查1:确保传入的二级指针pphead是有效的,防止对空指针进行解引用的操作assert(*pphead); //断言检查2:确保单链表是非空单链表,防止对空链表进行指定节点的删除的操作assert(pos); //断言检查3:确保pos指针有效,防止对空节点进行删除//情况1:处理pos头节点的情况 ---> 相当于头删if (pos == *pphead){SLTPopFront(pphead);}//情况2:处理pos非头节点的情况else{//1.找到要删除节点pos之前的节点位置SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}//2.断开pos节点的链接 + 释放pos节点的内存//2.1:断开链接prev->next = prev->next->next;//2.2:释放内存free(pos);//2.3:将指向被删除节点的指针置为空pos = NULL;}
}//10.实现:“单链表的指定节点的后继节点删除”操作
/*** @brief 删除指定节点后的节点* @param pos 指定节点指针(要删除其后的节点)* @note 1. 直接操作pos节点的next指针* 2. 时间复杂度O(1)* 3. 需要确保pos->next存在(不能是尾节点)*/
void SLTEraseAfter(SLTNode* pos)
{assert(pos); //断言检查1:确保pos指针有效,防止对空节点之后进行删除//1.定义指针指向要删除的节点SLTNode* del = pos->next;//2.断开要删除的节点的链接pos->next = pos->next->next;//3.释放要删除的节点的内存free(del);//4.将指向被删除的节点的中指针置空del = NULL;
}//11.实现:“单链表的销毁”操作
/*** @brief 销毁整个单链表,释放所有节点内存* @param pphead 指向头节点指针的指针(二级指针)* @note 1. 遍历链表逐个释放节点* 2. 最后将头指针置NULL* 3. 时间复杂度O(n)*/
void SLTDestroy(SLTNode** pphead)
{assert(pphead); //断言检查1:确保传入的二级指针pphead是有效的,防止对空指针进行解引用的操作//1.定义临时指针代替*pphead进行单链表的遍历SLTNode* pcur = *pphead;while (pcur != NULL){//2.定义指针存储临时指针的下一个遍历的位置SLTNode* next = pcur->next;//3.释放要删除的节点的内存free(pcur);//4.更新临时指针pcur = next; //指针指向空间被释放后并没有进行置空来防止其为野指针,因为我们更新了指针}//5.将链表的头指针置空防止其为野指针*pphead = NULL;
}
测试文件
---------------------------------Test.c----------------------------------#include "SingleList.h"void TestSLT1()
{printf("\n========== 测试1:创建和打印 ==========\n");SLTNode* plist = NULL;SLTPrint(plist); // 预期输出:NULL// 测试尾插SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);printf("尾插1,2,3后: ");SLTPrint(plist); // 预期输出:1->2->3->NULL// 测试头插SLTPushFront(&plist, 0);SLTPushFront(&plist, -1);printf("头插0,-1后: ");SLTPrint(plist); // 预期输出:-1->0->1->2->3->NULL
}void TestSLT2()
{printf("\n========== 测试2:删除操作 ==========\n");SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);printf("初始链表: ");SLTPrint(plist); // 1->2->3->NULL// 测试尾删SLTPopBack(&plist);printf("尾删后: ");SLTPrint(plist); // 1->2->NULL// 测试头删SLTPopFront(&plist);printf("头删后: ");SLTPrint(plist); // 2->NULL// 删除最后一个节点SLTPopBack(&plist);printf("删除最后一个节点后: ");SLTPrint(plist); // NULL
}void TestSLT3()
{printf("\n========== 测试3:查找和插入 ==========\n");SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 4);printf("初始链表: ");SLTPrint(plist); // 1->2->4->NULL// 测试查找SLTNode* pos = SLTFind(plist, 2);if (pos) {printf("找到节点2,在其后插入3\n");SLTInsertAfter(pos, 3);SLTPrint(plist); // 1->2->3->4->NULL}pos = SLTFind(plist, 1);if (pos) {printf("找到节点1,在其前插入0\n");SLTInsert(&plist, pos, 0);SLTPrint(plist); // 0->1->2->3->4->NULL}
}void TestSLT4()
{printf("\n========== 测试4:删除指定节点 ==========\n");SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);printf("初始链表: ");SLTPrint(plist); // 1->2->3->4->NULL// 测试删除中间节点SLTNode* pos = SLTFind(plist, 2);if (pos) {printf("删除节点2\n");SLTErase(&plist, pos);SLTPrint(plist); // 1->3->4->NULL}// 测试删除后继节点pos = SLTFind(plist, 3);if (pos) {printf("删除节点3的后继\n");SLTEraseAfter(pos);SLTPrint(plist); // 1->3->NULL}
}void TestSLT5()
{printf("\n========== 测试5:销毁链表 ==========\n");SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);printf("销毁前: ");SLTPrint(plist); // 1->2->3->NULLSLTDestroy(&plist);printf("销毁后: ");SLTPrint(plist); // NULL// 测试销毁后能否继续操作SLTPushBack(&plist, 5);printf("重新插入后: ");SLTPrint(plist); // 5->NULLSLTDestroy(&plist);
}void TestSLT6()
{printf("\n========== 测试6:边界测试 ==========\n");SLTNode* plist = NULL;// 测试空链表操作printf("尝试对空链表头删: ");//SLTPopFront(&plist); // 应该触发断言printf("尝试对空链表尾删: ");//SLTPopBack(&plist); // 应该触发断言// 测试单节点操作SLTPushFront(&plist, 1);printf("单节点链表: ");SLTPrint(plist); // 1->NULLSLTPopBack(&plist);printf("删除后: ");SLTPrint(plist); // NULL
}int main()
{TestSLT1(); // 基本插入测试TestSLT2(); // 基本删除测试TestSLT3(); // 查找和插入测试TestSLT4(); // 指定位置删除测试TestSLT5(); // 销毁测试//TestSLT6(); // 边界测试printf("\n所有测试完成!\n");return 0;
}
运行结果
心得总结
0.单链表的打印1.单链表的尾插
2.单链表的头插
3.单链表的尾删
4.单链表的头删5.单链表的查找
6.单链表的指定节点的前驱节点插入
7.单链表的指定节点的后继节点插入
8.单链表的指定节点的删除
9.单链表的指定节点的后继节点的删除
10.单链表的销毁
哪些操作使用了断言?都使用了哪些断言?
- 除了
0.单链表的打印
和5.单链表的查找
操作没有使用断言,其余的操作都使用了断言- 只要是指定节点的操作,都要添加这一条断言:
assert(pos); //断言检查1:确保pos指针有效
- 只要是涉及
删除
的操作都使用了这一条断言:assert(*pphead); //断言检查2:确保单链表是非空单链表
- 除了
7.单链表的指定节点的后继节点插入
和9.单链表的指定节点的后继节点的删除
这两操作的接口函数的形参中没有SLTNode** pphead
,导致断言中没有assert(pphead); //断言检查1:确保传入的二级指针pphead是有效的,防止对空指针进行解引用的操作
,其他有断言的函数中都有这个断言。并且这两个函数中且只有这一个断言:assert(pos); //断言检查1:确保pos指针有效
1.单链表的尾插
2.单链表的头插
assert(pphead); //断言检查1:确保传入的二级指针pphead是有效的,防止对空指针进行解引用的操作
------------------------------------------------------------------------3.单链表的尾删
4.单链表的头删
10.单链表的销毁
assert(pphead); //断言检查1:确保传入的二级指针pphead是有效的,防止对空指针进行解引用的操作
assert(*pphead); //断言检查2:确保单链表是非空单链表,防止对空链表进行删除节点的操作------------------------------------------------------------------------6.单链表的指定节点的前驱节点插入
assert(pphead); //断言检查1:确保传入的二级指针pphead是有效的,防止对空指针进行解引用的操作
assert(*pphead); //断言检查2:确保单链表是非空单链表,防止对空链表进行指定节点之前插入的操作
assert(pos); //断言检查3:确保pos指针有效,防止对空节点之前插入节点8.单链表的指定节点的删除
assert(pphead); //断言检查1:确保传入的二级指针pphead是有效的,防止对空指针进行解引用的操作
assert(*pphead); //断言检查2:确保单链表是非空单链表,防止对空链表进行指定节点的删除的操作
assert(pos); //断言检查3:确保pos指针有效,防止对空节点进行删除------------------------------------------------------------------------7.单链表的指定节点的后继节点插入
assert(pos); //断言检查1:确保pos指针有效,防止对空节点之后插入节点9.单链表的指定节点的后继节点的删除
assert(pos); //断言检查1:确保pos指针有效,防止对空节点之后进行删除
哪些操作是需要分情况处理的?都分为哪些情况?
1.单链表的尾插
- 情况1:处理单链表是空链表的情况
- 情况2:处理单链表是非空链表的情况
3.单链表的尾删
- 情况1:处理单链表中只有一个节点的情况
- 情况2:处理单链表中节点不止一个的情况
6.单链表的指定节点的前驱节点插入
- 情况1:处理pos是头节点的情况 --> 相当于头插
- 情况2:处理pos不是头节点的情况
8.单链表的指定节点的删除
- 情况1:处理pos头节点的情况 —> 相当于头删
- 情况2:处理pos非头节点的情况
---------------双向链表---------------
带头双向循环链表的实现
头文件
-----------------------------DoubleList.h--------------------------------#pragma once//任务1:包含要使用的头文件
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>//任务2:定义双向链表的存储结构
typedef int DLTDataType;typedef struct DoubleListNode
{//1.存储双向链表中的节点的值 --> 一个DLTDataType类型的变量//2.记录节点的前驱节点的位置 --> 一个struct DoubleListNode*类型的指针//3.记录节点的后继节点的位置 --> 一个struct DoubleListNode*类型的指针DLTDataType data;struct DoubleListNode* prev;struct DoubleListNode* next;
}DLTNode;//任务3:声明双向链表需要使用辅助工具函数
//1.用于创建双向链表的节点
DLTNode* DLTCreateNode(DLTDataType x);//任务4:声明双向链表的接口函数
//1.双向链表的初始化
//2.双向链表的销毁
//3.双向链表打印//3.双向链表的尾插
//4.双向链表的头插
//5.双向链表的尾删
//6.双向链表的头删//7.双向链表的查找
//8.双向链表的指定节点之后插入
//9.双向链表的指定节点的删除//void DLTInit(DLTNode** pphead);
DLTNode* DLTInit();
void DLTDestroy(DLTNode* phead);
void DLTPrint(DLTNode* phead);void DLTPushBack(DLTNode* phead, DLTDataType x);
void DLTPushFront(DLTNode* phead, DLTDataType x);
void DLTPopBack(DLTNode* phead);
void DLTPopFront(DLTNode* phead);DLTNode* DLTFind(DLTNode* phead, DLTDataType x);
void DLTInsert(DLTNode* pos, DLTDataType x);
void DLTErase(DLTNode* pos);
实现文件
-----------------------------DoubleList.c--------------------------------#include "DoubleList.h"//0.实现:“用于创建双向链表的节点”的工具函数
/*** @brief 申请一个新节点并初始化* @param x 节点存储的数据* @return 返回新节点的指针* @note 1. 动态分配内存* 2. 初始化前后指针都指向自己*/
DLTNode* DLTCreateNode(DLTDataType x)
{DLTNode* newNode = (DLTNode*)malloc(sizeof(DLTNode));if (newNode == NULL){perror("malloc fail");return NULL;}newNode->data = x;newNode->prev = newNode;newNode->next = newNode;return newNode;
}//1.实现:“双向链表的初始化”操作
/*** @brief 初始化双向链表* @return 返回哨兵位的指针* @note 创建一个值为-1的哨兵位节点*//*
void DLTInit(DLTNode** pphead)
{//双向链表的初始化本质就是:给双向链表创建一个哨兵节点*pphead = DLTCreateNode(-1);//注意:双向链表的哨兵节点中存储的值并无实际的意义,所以这里我们将其赋值为-1
}*/
//由于双向链表的其他的接口函数的形式参数的中都是使用的一个*的值传递
//为了保持一致性,这里我们重写DLTInint函数
DLTNode* DLTInit()
{DLTNode* phead = DLTCreateNode(-1);return phead;
}//2.实现:“双向链表的销毁”操作
/*** @brief 销毁双向链表* @param phead 哨兵位指针* @note 释放所有节点包括哨兵位*/
void DLTDestroy(DLTNode* phead) //注意:这里我们传参的时候只是用了一个*,是值传递:所以调用完DLTDestroy函数之后我们还要手动的将phead指针置空
{assert(phead); //作用:保证传入的哨兵节点的有效性,防止对空指针进行解引用DLTNode* pcur = phead->next;while (pcur != phead){DLTNode* next = pcur->next;free(pcur);pcur = next;}// 注意:相较于单链表双向链表还需要将哨兵节点置空free(phead);//注意:这里我们并没用将哨兵节点置为空,原因是:此处phead是函数的局部变量,对其置NULL不会影响外部实参//所以:调用者必须自行处理外部指针
}//3.实现:“双向链表的打印”操作
/*** @brief 打印双向链表的所有元素(不打印哨兵位)* @param phead 指向双向链表哨兵位的指针* @note 从哨兵位的下一个节点开始遍历,直到回到哨兵位*/
void DLTPrint(DLTNode* phead)
{assert(phead); //作用:保证传入的哨兵节点的有效性,防止对空指针进行解引用DLTNode* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("\n");
}//4.实现:“双向链表的尾插”操作
/*** @brief 双向链表尾插* @param phead 哨兵位指针* @param x 要插入的数据* @note 将新节点插入到哨兵位之前*/
void DLTPushBack(DLTNode* phead, DLTDataType x)
{assert(phead); //作用:保证传入的哨兵节点的有效性,防止对空指针进行解引用DLTNode* newNode = DLTCreateNode(x);//双向链表的尾插涉及到三个节点://1.哨兵节点:phead//2.尾节点:phead->prev//3.要插入的节点:newNode//总共要出连接四条线才能完成插入//1.将“要插入的节点”和其他的节点产生联系newNode->prev = phead->prev;newNode->next = phead;//2.将“哨兵节点 + 尾节点”和要插入的节点产生联系phead->prev->next = newNode;phead->prev = newNode;
}//5.实现:“双向链表的头插”操作
/*** @brief 双向链表头插* @param phead 哨兵位指针* @param x 要插入的数据* @note 将新节点插入到哨兵位之后*/
void DLTPushFront(DLTNode* phead, DLTDataType x)
{assert(phead); //作用:保证传入的哨兵节点的有效性,防止对空指针进行解引用DLTNode* newNode = DLTCreateNode(x);//双向链表的头插涉及到三个节点://1.哨兵节点:phead//2.首元节点:phead->next//3.要插入的节点:newNode//总共要出连接四条线才能完成插入//1.将“要插入的节点”和其他节点建立连接newNode->prev = phead;newNode->next = phead->next;//2.将“哨兵节点 + 首元节点”和要插入的节点之间建立连接phead->next = newNode;phead->next->next->prev = newNode;//这里一般大家会交换一下这两个连接的顺序,这样的话不用写这么多的箭头phead->next->next->prev//又或者有一部分人会将phead->next替换为newNode,这样也可以省去一个箭头//这里我没有:1.交换连接的顺序 2.使用newNode进行替换 //只是为告诉大家:这里的连接正常连就行,仅仅使用phead即可完成}//6.实现:“双向链表的尾删”操作
void DLTPopBack(DLTNode* phead)
{assert(phead);//作用:保证传入的哨兵节点的有效性,防止对空指针进行解引用assert(phead->next != phead); //作用:确保双向链表非空,防止对空链表进行删除操作(双向链表为空的判断依据:phead->next == phead)//双向链表的尾删涉及到三个节点://1.哨兵节点:phead//2.尾节点的前一个节点:phead->prev->prev//3.要插入的节点(尾节点):phead->prev//总共要出调整两条线才能完成删除//链表删除一个节点的步骤://1.定义一个指针指向要删除的节点//2.重新调整节点的连接//3.将要删除的节点的空间释放 + 该指针置空//1.DLTNode* del = phead->prev;//2.phead->prev = phead->prev->prev;phead->prev->next = phead;//注意:上面的这两个连接的顺序交不交换完全没有影响(既不会出现错误,也不会带来简化)//但是绝大多数人在调整节点的连接的时候会使用上之前已经定义的指针del来简化连接的箭头//但是这里我还是没有进行简化,因为还是想明确未删除只是使用phead并且不需要考虑连接的顺序就可以实现//我们定义del指针只是用来释放删除的节点而已//3.free(del);del = NULL;
}//7.实现:“双向链表的头删”操作
/*** @brief 双向链表头删* @param phead 哨兵位指针* @note 删除哨兵位后的一个节点*/
void DLTPopFront(DLTNode* phead)
{assert(phead);//作用:保证传入的哨兵节点的有效性,防止对空指针进行解引用assert(phead->next != phead); //作用:确保双向链表非空,防止对空链表进行删除操作(双向链表为空的判断依据:phead->next == phead)//双向链表的头删涉及到三个节点://1.哨兵节点:phead//2.首元节点的下一个节点:phead->next->next//3.要插入的节点(首元节点):phead->next//总共要出调整两条线才能完成删除//链表删除一个节点的步骤://1.定义一个指针指向要删除的节点//2.重新调整节点的连接//3.将要删除的节点的空间释放 + 该指针置空//1.DLTNode* del = phead->next;//2.phead->next = phead->next->next;phead->next->prev = phead;//3.free(del);del = NULL;
}//8.实现:“双向链表的查找”操作
/*** @brief 在双向链表中查找值为x的节点* @param phead 哨兵位指针* @param x 要查找的值* @return 找到返回节点指针,否则返回NULL*/
DLTNode* DLTFind(DLTNode* phead, DLTDataType x)
{DLTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}//9.实现:“双向链表的指定节点之后插入”操作
void DLTInsert(DLTNode* pos, DLTDataType x)
{assert(pos); //作用:保证传入的节点的有效性,防止对空指针进行解引用DLTNode* newNode = DLTCreateNode(x);//双向链表的插入涉及到三个节点://1.插入节点的前一个节点:pos//2.插入节点的后一个节点:pos->next//3.要插入的节点:newNode//总共要出调整四条线才能完成插入操作//1.将“要插入的节点”和其他节点建立连接newNode->prev = pos;newNode->next = pos->next;//2.将“插入节点的前一个节点 + 插入节点的前一个节点” 和要插入的节点建立连接pos->next = newNode;pos->next->next->prev = newNode;
}//10.实现:“双向链表的指定节点的删除”操作
/*** @brief 删除pos节点* @param pos 要删除的节点指针* @note 不能删除哨兵位*/
void DLTErase(DLTNode* pos)
{assert(pos); //作用:保证传入的节点的有效性,防止对空指针进行解引用//双向链表的删除涉及到三个节点://1.删除节点的前一个节点:pos->prev//2.删除节点的后一个节点:pos->next//3.要删除的节点:pos//总共要调整两条线才能完成删除//链表删除一个节点的步骤://1.定义一个指针指向要删除的节点//2.重新调整节点的连接//3.将要删除的节点的空间释放 + 该指针置空//1.//2.pos->prev->next = pos->next;pos->next->prev = pos->prev;//注:交换连接顺序没有任何影响,只能这么写//3.free(pos);//pos = NULL; 外面置空
}
测试文件
---------------------------------Test.c----------------------------------#include "DoubleList.h"
#include <stdio.h>
#include <assert.h>// 打印分隔线,用于区分不同的测试环节
void print_separator()
{printf("------------------------\n");
}// 测试双向链表的初始化、尾插、头插和打印功能
void test01()
{printf("开始测试双向链表的初始化、尾插、头插和打印功能\n");/*第一代双向链表的初始化方式:DLTNode* head = NULL;DLTInit(&head);*/DLTNode* head = DLTInit();printf("双向链表已初始化\n");printf("执行尾插操作,插入 1\n");DLTPushBack(head, 1);printf("当前双向链表内容为:");DLTPrint(head);printf("执行尾插操作,插入 2\n");DLTPushBack(head, 2);printf("当前双向链表内容为:");DLTPrint(head);printf("执行头插操作,插入 3\n");DLTPushFront(head, 3);printf("当前双向链表内容为:");DLTPrint(head);printf("执行头插操作,插入 4\n");DLTPushFront(head, 4);printf("当前双向链表内容为:");DLTPrint(head);DLTDestroy(head);printf("双向链表已销毁\n");print_separator();
}// 测试双向链表的尾删和头删功能
void test02()
{printf("开始测试双向链表的尾删和头删功能\n");DLTNode* head = DLTInit();printf("执行尾插操作,插入 1\n");DLTPushBack(head, 1);printf("执行尾插操作,插入 2\n");DLTPushBack(head, 2);printf("执行尾插操作,插入 3\n");DLTPushBack(head, 3);printf("插入元素后,当前双向链表内容为:");DLTPrint(head);printf("执行尾删操作\n");DLTPopBack(head);printf("尾删操作后,当前双向链表内容为:");DLTPrint(head);printf("执行头删操作\n");DLTPopFront(head);printf("头删操作后,当前双向链表内容为:");DLTPrint(head);DLTDestroy(head);printf("双向链表已销毁\n");print_separator();
}// 测试双向链表的查找、指定节点后插入和指定节点删除功能
void test03()
{printf("开始测试双向链表的查找、指定节点后插入和指定节点删除功能\n");DLTNode* head = DLTInit();printf("执行尾插操作,插入 1\n");DLTPushBack(head, 1);printf("执行尾插操作,插入 3\n");DLTPushBack(head, 3);printf("插入元素后,当前双向链表内容为:");DLTPrint(head);printf("查找值为 1 的节点\n");DLTNode* pos = DLTFind(head, 1);if (pos != NULL) {printf("已找到值为 1 的节点,执行在该节点后插入 2 的操作\n");DLTInsert(pos, 2);printf("插入操作后,当前双向链表内容为:");DLTPrint(head);printf("删除值为 1 的节点\n");DLTErase(pos);printf("删除操作后,当前双向链表内容为:");DLTPrint(head);}else {printf("未找到值为 1 的节点\n");}DLTDestroy(head);printf("双向链表已销毁\n");print_separator();
}int main()
{test01();test02();test03();printf("所有双向链表接口函数测试完成\n");return 0;
}
运行结果
心得总结
链表类型 | 空链表判断 | 断言示例 |
---|---|---|
单链表 | *pphead == NULL | assert(*pphead); |
双向带头链表 | phead->next == phead | assert(phead->next != phead); |
顺序表和链表的区别有哪些?
对比维度 | 顺序表(数组实现) | 链表 |
---|---|---|
存储结构 | 物理存储连续 | 逻辑连续,物理存储不连续(通过指针链接) |
随机访问 | 支持,O(1) 时间复杂度 | 不支持,需遍历,O(n) 时间复杂度 |
插入/删除效率 | 可能需要搬移元素,平均 O(n) | 只需修改指针,已知位置时 O(1) |
空间开销 | 只需存储数据,无额外开销 | 每个结点需额外存储指针(存储密度较低) |
扩容方式 | 动态顺序表需重新分配内存并拷贝数据(代价高) | 无容量限制,随时插入新结点 |
内存碎片 | 无 | 可能产生碎片(频繁动态分配释放) |
缓存命中率 | 高(空间局部性好) | 低(结点分散存储) |
适用场景 | 1. 频繁访问 2. 数据量可预估 3. 强调存储效率 | 1. 频繁插入/删除 2. 数据规模变化大 3. 内存灵活性要求高 |
相关文章:
《数据结构初阶》【顺序表 + 单链表 + 双向链表】
《数据结构初阶》【顺序表 单链表 顺序表】 前言:先聊些其他的东西!!!什么是线性表?什么是顺序表?顺序表的种类有哪些? 什么是链表?链表的种类有哪些? ---------------…...
【JS-Leetcode】2621睡眠函数|2629复合函数|2665计数器||
文章目录 2621睡眠函数2629复合函数2665计数器|| 这三个题目涉及setTimeout、promise、数组reduce方法,闭包。 2621睡眠函数 请你编写一个异步函数,它接收一个正整数参数 millis ,并休眠 millis 毫秒。要求此函数可以解析任何值。 原理&am…...
全国各地级城市月度平均房价统计数据2009-2021年
全国各地级城市月度平均房价统计数据2009-2021年.ziphttps://download.csdn.net/download/2401_84585615/90259770 https://download.csdn.net/download/2401_84585615/90259770 来源:安居客,本数据以excel格式展示,列举2.5万多条样本数据。总…...
ElasticSearch从入门到精通-覆盖DSL操作和Java实战
一、ElasticSearch基础概念 1.1 认识elasticSearch ElasticSearch(简称ES)是一款开源的、分布式的搜索引擎,它建立在Apache Lucene之上。简单来说,ElasticSearch就是一个能让你以极快速度进行数据搜索、存储和分析的系统。它不仅…...
SHCTF-REVERSE
前言 之前写的,一直没发,留个记录吧,万一哪天记录掉了起码在csdn有个念想 1.ezapk 反编译 快速定位关键函数 package com.mycheck.ezjv;import adrt.ADRTLogCatReader; import android.app.Activity; import android.content.Context; impo…...
C++学习:六个月从基础到就业——模板编程:模板特化
C学习:六个月从基础到就业——模板编程:模板特化 本文是我C学习之旅系列的第三十四篇技术文章,也是第二阶段"C进阶特性"的第十二篇,主要介绍C中的模板特化技术。查看完整系列目录了解更多内容。 目录 引言模板特化基础…...
【中级软件设计师】编译和解释程序的翻译阶段、符号表 (附软考真题)
【中级软件设计师】编译和解释程序的翻译阶段、符号表 (附软考真题) 目录 【中级软件设计师】编译和解释程序的翻译阶段、符号表 (附软考真题)一、历年真题二、考点:编译和解释程序的翻译阶段1、解释2、编译3、解释和编译的异同之处4、符号表 三、真题的答案与解析答…...
G1(Garbage-First)垃圾回收器与JVM内存
G1垃圾回收器简介 G1(Garbage-First)是Java虚拟机(JVM)中的一种垃圾回收器,它是针对服务器端应用设计的,旨在提供高吞吐量和低延迟的垃圾回收性能。G1垃圾回收器的主要目标是高效地管理JVM的堆内存,同时尽量减少垃圾回收(GC)过程对应用程序性能的影响。 特点 分区回收…...
STM32 驱动 INA226 测量电流电压功率
文章目录 一、INA226简介二、引脚功能三、寄存器介绍1.配置寄存器 0x002.分流电压寄存器 0x013.总线电压寄存器 0x024.功率寄存器 0x035.电流寄存器 0x046.基准寄存器 0x05 四、IIC 时序说明1.写时序2.读时序 五、程序六、实验现象1.线路图2.输出数据 一、INA226简介 INA226 是…...
解决新搭建的centos虚拟器,yum下载不了的问题
1. 检查网络连接 确保虚拟机可以访问互联网: ping 8.8.8.8 # 测试基础网络连通性若不通: 检查网卡 IP 配置(参考之前的 IP 恢复步骤)。 确认虚拟机网络模式(如 NAT 或桥接模式)是否允许访问外网。 检查网…...
python连接Elasticsearch并完成增删改查
python库提供了elasticsearch模块,可以通过以下命令进行快速安装,但是有个细节需要注意一下,安装的模块版本要跟es软件版本一致,此处举例:7.8.1 pip install elasticsearch==7.8.1 首先连接elasticsearch,以下是免密示例 from elasticsearch import Elasticsearch# El…...
Python爬虫(7)Python数据存储实战:CSV文件读写与复杂数据处理指南
目录 一、背景与核心价值二、CSV基础与核心应用场景2.1 CSV文件结构解析2.2 适用场景 三、Python csv模块核心操作3.1 安装与基础读写3.2 高级功能:字典读写与自定义格式 四、处理复杂数据场景4.1 含特殊字符的字段4.2 嵌套数据(如JSO…...
Spring Boot 中的条件注解
Spring Boot条件注解的汇总: 注解作用判断依据使用场景ConditionalOnBean容器中存在指定Bean时,被注解的配置或Bean定义生效指定Bean在容器中存在依赖其他已存在Bean时配置相关功能ConditionalOnCheckpointRestore在特定检查点恢复相关条件满足时生效满…...
Java 字符串分解技术:substring、tokenizing 和 trimming 方法详解
关键点 Java 字符串处理是开发中不可或缺的一部分,广泛用于数据解析和格式化。substring() 方法能够精确提取字符串的子串,需注意索引范围以避免异常。String.split() 是分词的首选方法,支持正则表达式,灵活性高。trim() 和 stri…...
OpenCV进阶操作:图像金字塔
文章目录 前言一、图像金字塔1、什么是图像金字塔2、金字塔类型1) 高斯金字塔 (Gaussian Pyramid)2)拉普拉斯金字塔 (Laplacian Pyramid) 3、图像金字塔的作用 二、图像金字塔中的操作1、向下采样步骤 2、向上采样步骤 3、拉普拉斯金字塔4、结论 三、代码…...
Rust游戏开发全栈指南:从理论到实践的革新之路
一、Rust游戏开发生态全景 1.1 核心引擎框架 Rust游戏生态已形成多层级工具链,覆盖从轻量级2D到3A级项目的开发需求: Bevy:采用ECS架构的模块化引擎,提供优雅的API设计和活跃社区支持,支持实时热重载和跨平台部署Fy…...
[GXYCTF2019]Ping Ping Ping
解题步骤 1、先使用 内敛执行 查看当前的php文件 执行 命令执行 发现空格被过滤 ?ip127.0.0.1$IFS|$IFSwhomi 还有一个点就是这个 执行的命令是不能进行拼接的 可能就是被过滤了 | 所以我们使用 ; 进行绕过一下 空格过滤代替 $IFS ${IFS} ${IFS}$9 //这里$1到$9都可以 $IFS$1…...
马哥教育Linux云计算运维课程
课程大小:19.1G 课程下载:https://download.csdn.net/download/m0_66047725/90640128 更多资源下载:关注我 你是否找了很多资料看了很多视频聊了很多群友,却发现自己技术仍然原地踏步?本教程联合BAT一线导师倾囊相授…...
科技打头阵,创新赢未来——中科视界携千眼狼超高速摄像机亮相第三届科交会
2025年4月26日,合肥,第三届中国(安徽)科技创新成果转化交易会国际合作板块展区,中科视界及其旗下品牌“千眼狼”高速摄像机成为展会焦点。作为国内科学仪器的领军企业,中科视界以“科技打头阵,创…...
【Flutter】Unity 三端封装方案:Android / iOS / Web
关联文档:【方案分享】Flutter Unity 跨平台三维渲染架构设计全解:插件封装、通信机制与热更新机制—— 支持 Android/iOS/Web 的 3D 内容嵌入与远程资源管理,助力 XR 项目落地 —— 支持 Android/iOS/Web 的 3D 内容嵌入与远程资源管理&…...
高能效计算:破解算力增长与能源约束的科技密码
引言 在人工智能和大模型技术迅猛发展的今天,全球算力需求正以每年50%的速度激增[3]。然而,传统计算范式已逼近物理极限——国际能源署预测,到2030年数据中心的全球电力消耗占比可能突破3%[3]。面对这场"算力革命"与"能源危机…...
【质量管理】TRIZ(萃智)的工程系统进化法则
在文章【质量管理】现代TRIZ(萃智)理论概述-CSDN博客 我们谈到到现代TRIZ的理论、TRIZ与传统创新的差异等。在文章中我们有说到TRIZ的创始人阿奇舒勒发现其实技术的进化是有规律可循的。 那到底技术进步有什么规律呢? 技术进化发展趋势和路径…...
FastAPI系列07:“请求-响应”过程高阶技巧
“请求-响应”过程高阶技巧 1、自定义 Request自定义 Request的用途如何自定义 Request 2、自定义APIRouteAPIRoute的用途自定义 APIRoute的用途如何自定义 APIRoute 3、使用BackgroundTasks(后台任务)BackgroundTasks的用途如何使用BackgroundTasksBack…...
游戏服务器不加防护能活多久?
游戏服务器若不加防护,其存活时间受多种因素影响,但通常面临极高的安全风险,可能在数小时至数天内因攻击或漏洞利用而崩溃。以下是具体分析: 1. DDoS攻击与勒索风险 未加防护的服务器极易成为黑客攻击目标,尤其是DDoS…...
Embedding入门概述
概述 Embedding,嵌入,一种将离散的符号数据(如单词、句子、图像等)映射到连续的向量空间中的技术,这些向量能够捕捉数据之间的语义、结构等关系。就是把原本难以直接处理的符号数据,转换成计算机更容易理解…...
革新桌面自动化:微软UFO²操作系统深度解析与未来展望
一、系统架构:多智能体协同的OS级创新 微软UFO(Unified Framework for Operations)是首个深度集成于Windows底层的多智能体操作系统,其核心架构由HostAgent控制中枢与模块化AppAgent执行单元构成。 HostAgent作为系统级调度器…...
【Java】分布式事务解决方案
分布式事务是指在分布式系统中,为了保证多个节点上的操作要么全部成功提交,要么全部失败回滚,所采取的一系列技术手段和协议。 CAP理论 在一个分布式系统中以下三个基本属性无法被同时满足: C(一致性):一致性是指写…...
es数据导出
有大数据量导出的需求 整体思路:分页查询es,一页查询2000条,下一页查询的截止时间取上一页最后一条记录的创建时间(因为分页是按照创建时间逆序排列的),组装最后导出的list,利用EasyExcel导出到…...
chrony服务器(2)
安装与配置 [rootserver ~]# systemctl status ntp # 查看ntp状态 安装 # 默认已安装,若需要安装则可执行: [rootserver ~]# yum install chrony -y [rootserver ~]# systemctl start chronyd [rootserver ~]# systemctl enable chronyd Chrony配置文…...
C++入门小馆: STL 之queue和stack
嘿,各位技术潮人!好久不见甚是想念。生活就像一场奇妙冒险,而编程就是那把超酷的万能钥匙。此刻,阳光洒在键盘上,灵感在指尖跳跃,让我们抛开一切束缚,给平淡日子加点料,注入满满的pa…...
从零搭建云原生后端系统 —— 一次真实项目实践分享
一、引言:为什么选择云原生技术打造后端? 在当今数字化加速的时代,业务需求变化频繁,应用需要快速开发、快速上线、快速迭代。传统单体应用后端架构在灵活性、扩展性和稳定性方面越来越难以满足需求。 而云原生(Clou…...
东田数码科技前端面经
东田数码科技有限公司前端面经 一个月三次面试,也是逐渐积攒了许多经验,也有遇到面试官问到的重复的问题,也有一些我不懂的问题,以下是4.27东田前端面经,希望给大家做参考。 1-自我介绍 我是ac鸽,就读与…...
【音视频】SDL窗口显示
SDL视频显示函数简介 SDL_Init(): 初始化SDL系统SDL_CreateWindow():创建窗口SDL_WindowSDL_CreateRenderer():创建渲染器SDL_RendererSDL_CreateTexture():创建纹理SDL_TextureSDL_UpdateTexture(): 设置纹理的数据S…...
小球在摆线上下落的物理过程MATLAB代码
物理建模: 使用摆线参数方程定义轨迹:x r(θ - sinθ), y r(1 - cosθ)通过微分方程求解角度θ随时间变化关系,考虑能量守恒定律计算实时速度分量和切向加速度 可视化特性: 灰色虚线显示完整摆线轨迹红色小球实时显示当…...
【设计模式】享元模式
享元模式属于结构型设计模式 核心思想是通过共享技术,实现相似对象的高效复用。用 1%的资源支撑100%的需求——通过对象状态的分离与共享,用最小内存支持海量对象 内部状态:对象中不变的部分共享 外部状态:对象中变化的部分非共享…...
R中实现数值求导的包numDeriv
介绍 numDeriv 是一个用于数值求导的 R 包,它提供了计算函数导数的简单方法,支持一阶导数和高阶导数的计算。 计算一阶导数 grad(func, x, method"Richardson", sideNULL, eps1e-4, method.argslist(), ...) 参数: func&#x…...
常用的多传感器数据融合方法
1. 概述 根据具体需求(实时性、计算资源、噪声特性)选择合适的方法,实际应用中常结合多种方法(如UKF与神经网络结合)。 传统方法 (KF/EKF/UKF/PF)依赖数学模型,适合动态系统&#…...
[Lc_week] 447 | 155 | Q1 | hash | pair {}调用
447_Q1 题解 class Solution {typedef pair<int,int> PII;// 自定义哈希函数struct HashPII {size_t operator()(const PII& p) const {return hash<int>()(p.first) ^ (hash<int>()(p.second) << 1);}};public:int countCoveredBuildings(int n,…...
HTML5 新特性详解:语义化标签、表单与音视频嵌入
前言 HTML5作为当前Web开发的核心技术,为开发者提供了更强大、更语义化的工具集。本文将深入探讨HTML5的三大核心特性:语义化标签、增强的表单功能以及原生的音视频支持,帮助开发者构建更现代化、更易维护的网页应用。 一、HTML5语义化标签…...
关于 React Fiber 架构、Hooks 原理
下面将详细介绍你提到的关于 React Fiber 架构、Hooks 原理等相关知识点: React Fiber 架构概述 1. 架构演变 在 React 16 版本之前,采用的是栈调和(Stack Reconciler),流程是 JSX 经过 render 函数转换为虚拟 DOM&…...
音视频之H.265/HEVC熵编码
H.265/HEVC系列文章: 1、音视频之H.265/HEVC编码框架及编码视频格式 2、音视频之H.265码流分析及解析 3、音视频之H.265/HEVC预测编码 4、音视频之H.265/HEVC变换编码 5、音视频之H.265/HEVC量化 6、音视频之H.265/HEVC环路后处理 7、音视频之H.265/HEVC熵编…...
【视频生成模型】通义万相Wan2.1模型本地部署和LoRA微调
目录 1 简介2 本地部署2.1 配置环境2.2 下载模型 3 文生视频3.1 运行命令3.2 生成结果 4 图生视频4.1 运行命令4.2 生成结果 5 首尾帧生成视频5.1 运行命令5.2 生成结果 6 提示词扩展7 LoRA微调 1 简介 2 本地部署 2.1 配置环境 将Wan2.1工程克隆到本地: git cl…...
Java高频面试之并发编程-09
hello啊,各位观众姥爷们!!!本baby今天来报道了!哈哈哈哈哈嗝🐶 面试官:详细说说ThreadLocal ThreadLocal 是 Java 中用于实现线程本地变量的工具类,主要解决多线程环境下共享变量的…...
[Vulfocus解题系列]Apache HugeGraph JWT Token硬编码导致权限绕过(CVE-2024-43441)
[Vulfocus解题系列]Apache HugeGraph JWT Token硬编码导致权限绕过(CVE-2024-43441) Apache HugeGraph 是一款快速、高度可扩展的图数据库。它提供了完整的图数据库功能,具有出色的性能和企业级的可靠性。 Apache HugeGraph 存在一个 JWT t…...
MySQL最新安装、连接、卸载教程(Windows下)
文章目录 MySQL最新安装、连接、卸载教程(Windows下)1.MySQL安装2.MySQL连接2.1 命令行连接2.2 图形化连接(推荐) 3.MySQL卸载参考 MySQL最新安装、连接、卸载教程(Windows下) 1.MySQL安装 MySQL 一共可以…...
Scala 函数柯里化及闭包
一、柯里化 1.1 定义 柯里化是将一个接受多个参数的函数转换为一系列接受单个参数的函数的过程。每个函数返回一个新函数,直到所有参数都被收集完毕,最终返回结果。 1.2 示例 非柯里化函数(普通多参数函数) def add(a: Int, b…...
EasyRTC嵌入式音视频通信SDK助力视频客服,开启智能服务新时代
一、背景 在数字化服务浪潮下,客户对服务体验的要求日益提升,传统语音及文字客服在复杂业务沟通、可视化指导等场景下渐显不足。视频客服虽成为企业服务升级的关键方向,但普遍面临音视频延迟高、画质模糊、多端适配难、功能扩展性差等问题&a…...
OceanBase数据库-学习笔记1-概论
多租户概念 集群和分布式 随着互联网、物联网和大数据技术的发展,数据量呈指数级增长,单机数据库难以存储和处理如此庞大的数据。现代应用通常需要支持大量用户同时访问,单机数据库在高并发场景下容易成为性能瓶颈。单点故障是单机数据库的…...
Android 理清 Gradle、AGP、Groovy 和构建文件之间的关系
在 Android 开发中,我们常常会接触到一系列看似相近却各有分工的名词,比如:Gradle、Groovy、AGP、gradle-wrapper.properties、build.gradle、settings.gradle 等等。 它们彼此之间到底是什么关系?各自又承担了什么角色࿱…...
ubuntu 安装ollama后,如何让外网访问?
官网下载linux版本:https://ollama.com/download/linux 1、一键安装和运行 curl -fsSL https://ollama.com/install.sh | sh 2、下载和启动deepseek-r1大模型 ollama run deepseek-r1 这种方式的ollama是systemd形式的服务,会随即启动。默认开启了 …...