最新资讯

  • 《数据结构初阶》【顺序表 + 单链表 + 双向链表】

《数据结构初阶》【顺序表 + 单链表 + 双向链表】

2025-05-01 12:00:09 1 阅读

《数据结构初阶》【顺序表 + 单链表 + 顺序表】

  • 前言:
    • 先聊些其他的东西!!!
    • 什么是线性表?
    • 什么是顺序表?
      • 顺序表的种类有哪些?
    • 什么是链表?
      • 链表的种类有哪些?
  • ---------------顺序表---------------
  • 动态顺序表的实现
    • 头文件
    • 实现文件
    • 测试文件
    • 运行结果
  • ---------------单链表---------------
  • 无头单向非循环链表的实现
    • 头文件
    • 实现文件
    • 测试文件
    • 运行结果
    • 心得总结
      • 哪些操作使用了断言?都使用了哪些断言?
      • 哪些操作是需要分情况处理的?都分为哪些情况?
  • ---------------双向链表---------------
  • 带头双向循环链表的实现
    • 头文件
    • 实现文件
    • 测试文件
    • 运行结果
    • 心得总结
  • 顺序表和链表的区别有哪些?

往期《数据结构初阶》回顾:
【时间复杂度 + 空间复杂度】

前言:

先聊些其他的东西!!!

在之前的博客中博主向大家信誓旦旦地宣布博主之后将持续更新的《数据结构初阶》这个系列的博客。
博客内容主要划分为:数据结构的介绍 + 数据结构的实现 + 数据结构的OJ练习,这三大板块的内容。
结果一动手发现——好家伙!三部分加起来有2万字,要是把OJ练习也塞进来,怕是要写成《数据结构从入门到放弃》了!
所以博主这里选择先将前两个板块的内容写成一篇博客,至于数据结构的OJ练习这个板块就单独成文。


温馨提示:这篇博客中的主要内容是代码,每个代码块中的代码都有非常详细的注释,相信各位勇士一定能征服这些数据结构!✨ (毕竟博主的注释写得比情书还用心💘)

什么是线性表?

线性表(Linear List):是具有相同数据类型的n(n≥0)个数据元素的有限序列。

  • 线性表是数据结构中最基本、最简单的一种结构。

  • 线性表是一种在实际中广泛使用的数据结构。

  • 常见的线性表:顺序表、链表、栈、队列、字符串…

线性表在逻辑结构上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以 顺序结构链式结构 的形式存储。

线性表有两种主要的存储结构:

  1. 顺序存储结构(顺序表)

    • 用一组地址连续的存储单元依次存储线性表的元素
    • 可以通过数组实现
  2. 链式存储结构(链表)

    • 用一组任意的存储单元存储线性表的元素

    • 每个元素除了存储数据外,还需要存储指向后继元素的指针

特性顺序表 (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种链表结构:

单向或者双向

带头或者不带头

循环或者非循环

虽然链表有这么多的结构,但是我们实际中最常用的只有以下两种结构:

  • 无头单向非循环链表:又名为 单链表
    • 结构最简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构
    • 如:哈希桶、图的邻接表等等。
  • 带头双向循环链表:又名为 双向链表
    • 结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表

1. 单链表:

typedef int SLTDataType;

typedef struct singleListNode
{
	//1.记录链表中节点的值 ---> 一个SLTDataType类型的变量
	//2.记录下一个节点的地址 ---> 一个struct singleListNode*类型的指针
	SLTDataType data;
	struct singleListNode* next;
}SLTNode;

1. 双向链表:

typedef int DLTDataType;

typedef struct DoubleListNode
{
	//1.存储双向链表中的节点的值 --> 一个DLTDataType类型的变量
	//2.记录节点的前驱节点的位置 --> 一个struct DoubleListNode*类型的指针
	//3.记录节点的后继节点的位置 --> 一个struct DoubleListNode*类型的指针
	DLTDataType data;
	struct DoubleListNode* prev;
	struct DoubleListNode* next;
}DLTNode;

---------------顺序表---------------

动态顺序表的实现

头文件

-------------------------------SeqList.h--------------------------------
    
#pragma once


//任务1:包含需要使用的头文件
#include 
#include 
#include 


//任务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("
");
}




//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.将顺序表中当前元素的数量+1
	ps->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.将顺序表中当前元素的数量+1
	ps->size++;

}



//6.实现:“顺序表的尾删”操作
/**
 * @brief 顺序表尾部删除元素
 * @param ps 指向顺序表结构的指针
 * @note 只需减小size,不实际释放内存
 *       时间复杂度O(1)
 */
void SLPopBack(SL* ps)
{
	assert(ps);
	assert(ps->size > 0);

	//1.直接顺序表中当前的元素的数量-1
	ps->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.将顺序表中当前的元素的数量-1
	ps->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.将顺序表中当前元素的数量+1
	ps->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.将顺序表中当前元素的数量-1
	ps->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 7

    SLDestroy(&sl);
    printf("
");
}

/**
 * @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("没有找到!
");  
    }
    else {
        printf("找到了!下标为%d
", find);
    }

    SLDestroy(&sl);      
}

int main()
{
    test01();
    test02();

    return 0;
}

运行结果

---------------单链表---------------

无头单向非循环链表的实现

头文件

-----------------------------SingleList.h-------------------------------
    
#pragma once

//任务1:包含要使用的头文件
#include 
#include 
#include 

//任务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
");
}


//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("
========== 测试1:创建和打印 ==========
");
    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("
========== 测试2:删除操作 ==========
");
    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("
========== 测试3:查找和插入 ==========
");
    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
");
        SLTInsertAfter(pos, 3);
        SLTPrint(plist);  // 1->2->3->4->NULL
    }

    pos = SLTFind(plist, 1);
    if (pos) 
    {
        printf("找到节点1,在其前插入0
");
        SLTInsert(&plist, pos, 0);
        SLTPrint(plist);  // 0->1->2->3->4->NULL
    }
}

void TestSLT4() 
{
    printf("
========== 测试4:删除指定节点 ==========
");
    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
");
        SLTErase(&plist, pos);
        SLTPrint(plist);  // 1->3->4->NULL
    }

    // 测试删除后继节点
    pos = SLTFind(plist, 3);
    if (pos) 
    {
        printf("删除节点3的后继
");
        SLTEraseAfter(pos);
        SLTPrint(plist);  // 1->3->NULL
    }
}

void TestSLT5() 
{
    printf("
========== 测试5:销毁链表 ==========
");
    SLTNode* plist = NULL;
    SLTPushBack(&plist, 1);
    SLTPushBack(&plist, 2);
    SLTPushBack(&plist, 3);
    printf("销毁前: ");
    SLTPrint(plist);  // 1->2->3->NULL

    SLTDestroy(&plist);
    printf("销毁后: ");
    SLTPrint(plist);  // NULL

    // 测试销毁后能否继续操作
    SLTPushBack(&plist, 5);
    printf("重新插入后: ");
    SLTPrint(plist);  // 5->NULL
    SLTDestroy(&plist);
}

void TestSLT6() 
{
    printf("
========== 测试6:边界测试 ==========
");
    SLTNode* plist = NULL;

    // 测试空链表操作
    printf("尝试对空链表头删: ");
    //SLTPopFront(&plist);  // 应该触发断言

    printf("尝试对空链表尾删: ");
    //SLTPopBack(&plist);   // 应该触发断言

    // 测试单节点操作
    SLTPushFront(&plist, 1);
    printf("单节点链表: ");
    SLTPrint(plist);  // 1->NULL

    SLTPopBack(&plist);
    printf("删除后: ");
    SLTPrint(plist);  // NULL
}

int main() 
{
    TestSLT1();  // 基本插入测试
    TestSLT2();  // 基本删除测试
    TestSLT3();  // 查找和插入测试
    TestSLT4();  // 指定位置删除测试
    TestSLT5();  // 销毁测试
    //TestSLT6();  // 边界测试

    printf("
所有测试完成!
");
    return 0;
}

运行结果

心得总结

0.单链表的打印

1.单链表的尾插
2.单链表的头插
3.单链表的尾删
4.单链表的头删

5.单链表的查找
6.单链表的指定节点的前驱节点插入
7.单链表的指定节点的后继节点插入
8.单链表的指定节点的删除
9.单链表的指定节点的后继节点的删除
10.单链表的销毁

哪些操作使用了断言?都使用了哪些断言?

  1. 除了 0.单链表的打印5.单链表的查找 操作没有使用断言,其余的操作都使用了断言
  2. 只要是指定节点的操作,都要添加这一条断言:assert(pos); //断言检查1:确保pos指针有效
  3. 只要是涉及删除的操作都使用了这一条断言:assert(*pphead); //断言检查2:确保单链表是非空单链表
  4. 除了 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 
#include 
#include 

//任务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("
");
}



//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 
#include 

// 打印分隔线,用于区分不同的测试环节
void print_separator() 
{
    printf("------------------------
");
}

// 测试双向链表的初始化、尾插、头插和打印功能
void test01() 
{
    printf("开始测试双向链表的初始化、尾插、头插和打印功能
");
    /*
         第一代双向链表的初始化方式:
         DLTNode* head = NULL;
         DLTInit(&head);
    */

    DLTNode* head = DLTInit();
    printf("双向链表已初始化
");

    printf("执行尾插操作,插入 1
");
    DLTPushBack(head, 1);
    printf("当前双向链表内容为:");
    DLTPrint(head);

    printf("执行尾插操作,插入 2
");
    DLTPushBack(head, 2);
    printf("当前双向链表内容为:");
    DLTPrint(head);

    printf("执行头插操作,插入 3
");
    DLTPushFront(head, 3);
    printf("当前双向链表内容为:");
    DLTPrint(head);

    printf("执行头插操作,插入 4
");
    DLTPushFront(head, 4);
    printf("当前双向链表内容为:");
    DLTPrint(head);

    DLTDestroy(head);
    printf("双向链表已销毁
");
    print_separator();
}

// 测试双向链表的尾删和头删功能
void test02() 
{
    printf("开始测试双向链表的尾删和头删功能
");
    DLTNode* head = DLTInit();

    printf("执行尾插操作,插入 1
");
    DLTPushBack(head, 1);
    printf("执行尾插操作,插入 2
");
    DLTPushBack(head, 2);
    printf("执行尾插操作,插入 3
");
    DLTPushBack(head, 3);
    printf("插入元素后,当前双向链表内容为:");
    DLTPrint(head);

    printf("执行尾删操作
");
    DLTPopBack(head);
    printf("尾删操作后,当前双向链表内容为:");
    DLTPrint(head);

    printf("执行头删操作
");
    DLTPopFront(head);
    printf("头删操作后,当前双向链表内容为:");
    DLTPrint(head);

    DLTDestroy(head);
    printf("双向链表已销毁
");
    print_separator();
}

// 测试双向链表的查找、指定节点后插入和指定节点删除功能
void test03() 
{
    printf("开始测试双向链表的查找、指定节点后插入和指定节点删除功能
");
    DLTNode* head = DLTInit();

    printf("执行尾插操作,插入 1
");
    DLTPushBack(head, 1);
    printf("执行尾插操作,插入 3
");
    DLTPushBack(head, 3);
    printf("插入元素后,当前双向链表内容为:");
    DLTPrint(head);

    printf("查找值为 1 的节点
");
    DLTNode* pos = DLTFind(head, 1);
    if (pos != NULL) 
    {
        printf("已找到值为 1 的节点,执行在该节点后插入 2 的操作
");
        DLTInsert(pos, 2);
        printf("插入操作后,当前双向链表内容为:");
        DLTPrint(head);

        printf("删除值为 1 的节点
");
        DLTErase(pos);
        printf("删除操作后,当前双向链表内容为:");
        DLTPrint(head);
    }
    else 
    {
        printf("未找到值为 1 的节点
");
    }

    DLTDestroy(head);
    printf("双向链表已销毁
");
    print_separator();
}

int main() 
{
    test01();
    test02();
    test03();

    printf("所有双向链表接口函数测试完成
");
    return 0;
}

运行结果

心得总结

链表类型空链表判断断言示例
单链表*pphead == NULLassert(*pphead);
双向带头链表phead->next == pheadassert(phead->next != phead);

顺序表和链表的区别有哪些?

对比维度顺序表(数组实现)链表
存储结构物理存储连续逻辑连续,物理存储不连续(通过指针链接)
随机访问支持,O(1) 时间复杂度不支持,需遍历,O(n) 时间复杂度
插入/删除效率可能需要搬移元素,平均 O(n)只需修改指针,已知位置时 O(1)
空间开销只需存储数据,无额外开销每个结点需额外存储指针(存储密度较低)
扩容方式动态顺序表需重新分配内存并拷贝数据(代价高)无容量限制,随时插入新结点
内存碎片可能产生碎片(频繁动态分配释放)
缓存命中率高(空间局部性好)低(结点分散存储)
适用场景1. 频繁访问
2. 数据量可预估
3. 强调存储效率
1. 频繁插入/删除
2. 数据规模变化大
3. 内存灵活性要求高

本文地址:https://www.vps345.com/6733.html

搜索文章

Tags

PV计算 带宽计算 流量带宽 服务器带宽 上行带宽 上行速率 什么是上行带宽? CC攻击 攻击怎么办 流量攻击 DDOS攻击 服务器被攻击怎么办 源IP 服务器 linux 运维 游戏 云计算 javascript 前端 chrome edge llama 算法 opencv 自然语言处理 神经网络 语言模型 数据库 centos oracle 关系型 安全 分布式 ssh 阿里云 网络 网络安全 网络协议 ubuntu deepseek Ollama 模型联网 API CherryStudio 进程 操作系统 进程控制 Ubuntu RTSP xop RTP RTSPServer 推流 视频 科技 ai java 人工智能 个人开发 python MCP ssl EtherCAT转Modbus ECT转Modbus协议 EtherCAT转485网关 ECT转Modbus串口网关 EtherCAT转485协议 ECT转Modbus网关 fastapi mcp mcp-proxy mcp-inspector fastapi-mcp agent sse 物联网 tcp/ip mcu iot 信息与通信 filezilla 无法连接服务器 连接被服务器拒绝 vsftpd 331/530 智能路由器 外网访问 内网穿透 端口映射 嵌入式 linux驱动开发 arm开发 嵌入式硬件 asm numpy rust http 开发语言 android harmonyos typescript 鸿蒙 华为 HarmonyOS Next 运维开发 云原生 YOLO efficientVIT YOLOv8替换主干网络 TOLOv8 Flask FastAPI Waitress Gunicorn uWSGI Uvicorn 面试 性能优化 jdk intellij-idea 架构 宝塔面板访问不了 宝塔面板网站访问不了 宝塔面板怎么配置网站能访问 宝塔面板配置ip访问 宝塔面板配置域名访问教程 宝塔面板配置教程 计算机网络 macos adb vue.js audio vue音乐播放器 vue播放音频文件 Audio音频播放器自定义样式 播放暂停进度条音量调节快进快退 自定义audio覆盖默认样式 mysql C# MQTTS 双向认证 emqx vscode ide 编辑器 后端 目标检测 计算机视觉 深度学习 YOLOv12 jar gradle ESP32 camera Arduino 电子信息 sqlserver kubernetes 容器 websocket .net c++ 单片机 数据结构 学习 c语言 笔记 经验分享 学习方法 docker 豆瓣 追剧助手 迅雷 nas 微信 前端框架 golang aws googlecloud vSphere vCenter 软件定义数据中心 sddc jmeter 软件测试 HCIE 数通 自动化 开源 windows rag ragflow ragflow 源码启动 rpc 命名管道 客户端与服务端通信 gateway Clion Nova ResharperC++引擎 Centos7 远程开发 电脑 cuda cudnn anaconda word图片自动上传 word一键转存 复制word图片 复制word图文 复制word公式 粘贴word图文 粘贴word公式 微服务 springcloud 向日葵 pycharm conda pillow unix live555 rtsp rtp node.js json html5 firefox 机器学习 WSL win11 无法解析服务器的名称或地址 rust腐蚀 C 环境变量 进程地址空间 spring boot tomcat 迁移指南 ffmpeg 音视频 vue3 HTML audio 控件组件 vue3 audio音乐播放器 Audio标签自定义样式默认 vue3播放音频文件音效音乐 自定义audio播放器样式 播放暂停调整声音大小下载文件 程序人生 ESXi 僵尸进程 DeepSeek-R1 API接口 源码剖析 rtsp实现步骤 流媒体开发 udp 客户端 java-ee ue5 vr Qwen2.5-coder 离线部署 ollama 大模型 mac flutter Hyper-V WinRM TrustedHosts Dify ipython pip jenkins mount挂载磁盘 wrong fs type LVM挂载磁盘 Centos7.9 AI 爬虫 数据集 ui 华为云 华为od c# 安装教程 GPU环境配置 Ubuntu22 CUDA PyTorch Anaconda安装 debian PVE .netcore github 创意 社区 dell服务器 go 代理模式 cpu 内存 实时 使用 WSL2 git elasticsearch react.js 前端面试题 持续部署 YOLOv8 NPU Atlas800 A300I pro asi_bench ai小智 语音助手 ai小智配网 ai小智教程 智能硬件 esp32语音助手 diy语音助手 MacOS录屏软件 统信UOS 麒麟 bonding 链路聚合 gpu算力 chatgpt llama3 Chatglm 开源大模型 ddos qt stm32项目 stm32 GaN HEMT 氮化镓 单粒子烧毁 辐射损伤 辐照效应 zotero WebDAV 同步失败 Dell R750XS 大数据 数据挖掘 网络用户购物行为分析可视化平台 大数据毕业设计 1024程序员节 温湿度数据上传到服务器 Arduino HTTP unity protobuf 序列化和反序列化 安装 flask AI编程 AIGC sql KingBase 鸿蒙系统 oceanbase rc.local 开机自启 systemd uni-app 政务 分布式系统 监控运维 Prometheus Grafana 计算机外设 软件需求 银河麒麟 kylin v10 麒麟 v10 php tcpdump vim 智能手机 NAS Termux Samba Linux postman mock mock server 模拟服务器 mock服务器 Postman内置变量 Postman随机数据 Agent LDAP VMware安装mocOS VMware macOS系统安装 maven intellij idea pytorch Windsurf nginx dubbo 产品经理 agi microsoft 微信分享 Image wxopensdk 高效远程协作 TrustViewer体验 跨设备操作便利 智能远程控制 spring cloud kafka hibernate lio-sam SLAM windwos防火墙 defender防火墙 win防火墙白名单 防火墙白名单效果 防火墙只允许指定应用上网 防火墙允许指定上网其它禁止 宝塔面板 同步 备份 建站 安全威胁分析 多进程 小艺 Pura X AI大模型 大模型入门 Deepseek 大模型教程 excel IPMI Node-Red 编程工具 流编程 W5500 OLED u8g2 TCP服务器 KylinV10 麒麟操作系统 虚拟机 Vmware 漏洞 bash kind unity3d ecmascript KVM 中间件 web安全 可信计算技术 安全架构 网络攻击模型 网络穿透 云服务器 SSH Xterminal C语言 apache pdf n8n 工作流 workflow linux安装配置 firewalld bug 负载均衡 其他 远程工作 弹性计算 裸金属服务器 弹性裸金属服务器 虚拟化 微信小程序 小程序 iBMC UltraISO openEuler Cookie 蓝桥杯 gitlab express okhttp CORS 跨域 雨云 NPS arcgis 飞书 dns wireshark 测试工具 DeepSeek 程序员 恒源云 致远OA OA服务器 服务器磁盘扩容 ue4 着色器 虚幻 dify pyautogui kylin arm 能力提升 面试宝典 技术 IT信息化 oneapi mongodb 大模型微调 网络结构图 开机自启动 实时音视频 webrtc Portainer搭建 Portainer使用 Portainer使用详解 Portainer详解 Portainer portainer 大语言模型 LLMs virtualenv django sqlite 华为认证 网络工程师 交换机 MS Materials openssl 密码学 监控 自动化运维 远程 命令 执行 sshpass 操作 redis Redis Desktop code-server yum源切换 更换国内yum源 MQTT mosquitto 消息队列 QQ 机器人 bot Docker 媒体 rabbitmq DevEco Studio 硬件架构 低代码 ArcTS 登录 ArcUI GridItem https AISphereButler arkUI 课程设计 shell kamailio sip VoIP mariadb 大数据平台 echarts 信息可视化 数据分析 网页设计 AP配网 AK配网 小程序AP配网和AK配网教程 WIFI设备配网小程序UDP开 spring Java Applet URL操作 服务器建立 Socket编程 网络文件读取 进程信号 remote-ssh matplotlib 国产操作系统 ukui 麒麟kylinos openeuler 统信 虚拟机安装 图像处理 框架搭建 vue RustDesk自建服务器 rustdesk服务器 docker rustdesk web3.py ollama下载加速 服务器繁忙 VPS pyqt k8s 微信小程序域名配置 微信小程序服务器域名 微信小程序合法域名 小程序配置业务域名 微信小程序需要域名吗 微信小程序添加域名 safari Mac 系统 系统架构 EasyConnect linux 命令 sed 命令 hadoop 系统安全 pygame 小游戏 五子棋 服务器主板 AI芯片 devops springboot 孤岛惊魂4 WebRTC gpt openwrt ux 多线程 nvidia zabbix RTMP 应用层 jupyter Python 网络编程 聊天服务器 套接字 TCP Socket IPMITOOL BMC 硬件管理 权限 opcua opcda KEPServer安装 npm 游戏服务器 TrinityCore 魔兽世界 asp.net大文件上传 asp.net大文件上传源码 ASP.NET断点续传 asp.net上传文件夹 asp.net上传大文件 .net core断点续传 .net mvc断点续传 软件工程 职场和发展 代码调试 ipdb adobe 传统数据库升级 银行 mysql离线安装 ubuntu22.04 mysql8.0 iis 源码 毕业设计 多线程服务器 Linux网络编程 云服务 雨云服务器 fd 文件描述符 springsecurity6 oauth2 授权服务器 token sas 蓝耘科技 元生代平台工作流 ComfyUI FTP 服务器 混合开发 环境安装 JDK log4j 矩阵 服务器管理 配置教程 服务器安装 网站管理 NFS redhat list 模拟实现 AI写作 prompt VMware安装Ubuntu Ubuntu安装k8s 群晖 文件分享 threejs 3D hive Hive环境搭建 hive3环境 Hive远程模式 SenseVoice 服务器数据恢复 数据恢复 存储数据恢复 raid5数据恢复 磁盘阵列数据恢复 远程控制 远程看看 远程协助 visualstudio centos-root /dev/mapper yum clean all df -h / du -sh prometheus 银河麒麟操作系统 国产化 工业4.0 京东云 rsyslog 命令行 基础入门 编程 硬件工程 微信开放平台 微信公众平台 微信公众号配置 chrome 浏览器下载 chrome 下载安装 谷歌浏览器下载 三级等保 服务器审计日志备份 缓存 r语言 数据可视化 串口服务器 Trae IDE AI 原生集成开发环境 Trae AI 驱动开发 嵌入式实习 黑客技术 Open WebUI 流式接口 html css llm 本地部署 api eureka k8s集群资源管理 云原生开发 DigitalOcean GPU服务器购买 GPU服务器哪里有 GPU服务器 Kylin-Server matlab bootstrap Cline Kali Linux 黑客 渗透测试 信息收集 nextjs react reactjs CPU 主板 电源 网卡 ci/cd 压测 ECS 飞牛NAS 飞牛OS MacBook Pro ssrf 失效的访问控制 RoboVLM 通用机器人策略 VLA设计哲学 vlm fot robot 视觉语言动作模型 具身智能 Ubuntu Server Ubuntu 22.04.5 Docker Hub docker pull 镜像源 daemon.json 企业微信 Linux24.04 deepin Reactor 设计模式 C++ xrdp 远程桌面 远程连接 LLM 大模型面经 大模型学习 string模拟实现 深拷贝 浅拷贝 经典的string类问题 三个swap 开发环境 SSL证书 半虚拟化 硬件虚拟化 Hypervisor 环境迁移 rime thingsboard postgresql iftop 网络流量监控 毕设 jellyfin outlook IIS .net core Hosting Bundle .NET Framework vs2022 直播推流 并查集 leetcode svn 信号处理 网络药理学 生信 生物信息学 gromacs 分子动力学模拟 MD 动力学模拟 状态管理的 UDP 服务器 Arduino RTOS 安卓 yum openvpn server openvpn配置教程 centos安装openvpn gitea ssh漏洞 ssh9.9p2 CVE-2025-23419 Invalid Host allowedHosts rdp 实验 RAGFLOW fpga开发 vmware 卡死 Wi-Fi 自动化编程 DNS mamba Vmamba 压力测试 UOS 统信操作系统 mybatis 云电竞 云电脑 todesk OD机试真题 华为OD机试真题 服务器能耗统计 ros2 moveit 机器人运动 gitee telnet 远程登录 transformer gpt-3 文心一言 selenium ping++ CH340 串口驱动 CH341 uart 485 显示过滤器 ICMP Wireshark安装 智能音箱 智能家居 dba 实战案例 k8s资源监控 annotations自动化 自动化监控 监控service 监控jvm 医疗APP开发 app开发 交叉编译 部署 服务器配置 next.js 部署next.js 拓扑图 ocr visual studio code 无人机 线程 测试用例 功能测试 磁盘监控 ansible playbook 剧本 P2P HDLC 思科 银河麒麟服务器操作系统 系统激活 kali 共享文件夹 TRAE RAID RAID技术 磁盘 存储 自动驾驶 博客 强制清理 强制删除 mac废纸篓 王者荣耀 Google pay Apple pay 单元测试 集成测试 实时互动 Linux无人智慧超市 LInux多线程服务器 QT项目 LInux项目 单片机项目 android studio 交互 grafana 灵办AI kvm AI代码编辑器 wps wsl Ark-TS语言 硬件 设备 GPU PCI-Express cmos 深度优先 图论 并集查找 换根法 树上倍增 p2p 视觉检测 trae 腾讯云大模型知识引擎 游戏机 VMware创建虚拟机 SWAT 配置文件 服务管理 网络共享 etcd 数据安全 RBAC gaussdb 重启 排查 系统重启 日志 原因 ruoyi 金融 DeepSeek行业应用 Heroku 网站部署 wsl2 IIS服务器 IIS性能 日志监控 腾讯云 报错 Java 多层架构 解耦 micropython esp32 mqtt nuxt3 产测工具框架 IMX6ULL 管理框架 分析解读 JAVA 开发 docker搭建nacos详解 docker部署nacos docker安装nacos 腾讯云搭建nacos centos7搭建nacos 算力 springboot远程调试 java项目远程debug docker远程debug java项目远程调试 springboot远程 Linux PID EMQX 通信协议 本地部署AI大模型 uv glibc LInux 银河麒麟高级服务器 外接硬盘 Kylin 物联网开发 社交电子 数据库系统 宠物 免费学习 宠物领养 宠物平台 vscode 1.86 软负载 HiCar CarLife+ CarPlay QT RK3588 yolov8 直流充电桩 充电桩 CLion web SEO chfs ubuntu 16.04 MacMini 迷你主机 mini Apple curl wget 火绒安全 Nuxt.js frp 内网服务器 内网代理 内网通信 GCC aarch64 编译安装 HPC CentOS Stream CentOS idm rnn 程序员创富 高级IO epoll 树莓派 VNC VR手套 数据手套 动捕手套 动捕数据手套 uniapp 域名服务 DHCP 符号链接 配置 备选 网站 调用 示例 AD域 av1 电视盒子 机顶盒ROM 魔百盒刷机 目标跟踪 OpenVINO 推理应用 3d 数学建模 ShenTong easyui langchain 运维监控 HarmonyOS 边缘计算 输入法 代理 飞牛nas fnos keepalived sonoma 自动更新 业界资讯 鲲鹏 xshell termius iterm2 neo4j 数据仓库 数据库开发 数据库架构 database 自定义客户端 SAS 链表 rclone AList webdav fnOS OpenManus 系统开发 binder 车载系统 framework 源码环境 chrome devtools chromedriver 华为机试 skynet 宝塔 大大通 第三代半导体 碳化硅 指令 回显服务器 UDP的API使用 x64 SIGSEGV SSE xmm0 做raid 装系统 armbian u-boot miniapp 真机调试 调试 debug 断点 网络API请求调试方法 LORA NLP elk AI-native Docker Desktop sublime text 游戏程序 游戏引擎 Cursor 办公自动化 自动化生成 pdf教程 URL ftp 升级 CVE-2024-7347 VM搭建win2012 win2012应急响应靶机搭建 攻击者获取服务器权限 上传wakaung病毒 应急响应并溯源 挖矿病毒处置 应急响应综合性靶场 历史版本 下载 h.264 etl 图形渲染 LLM Web APP Streamlit 集成学习 big data 黑苹果 opensearch helm sdkman crosstool-ng sequoiaDB MI300x 设置代理 实用教程 web3 tcp vscode1.86 1.86版本 ssh远程连接 open Euler dde Ubuntu DeepSeek DeepSeek Ubuntu DeepSeek 本地部署 DeepSeek 知识库 DeepSeek 私有化知识库 本地部署 DeepSeek DeepSeek 私有化部署 ip命令 新增网卡 新增IP 启动网卡 相机 SSH 服务 SSH Server OpenSSH Server prometheus数据采集 prometheus数据模型 prometheus特点 open webui IMM 自动化任务管理 WebUI DeepSeek V3 nlp alias unalias 别名 单一职责原则 minicom 串口调试工具 VSCode 移动云 推荐算法 cnn DenseNet 崖山数据库 YashanDB spark HistoryServer Spark YARN jobhistory 文件系统 用户缓冲区 Ubuntu 24.04.1 轻量级服务器 ArkTs ArkUI asp.net大文件上传下载 Xinference RAGFlow UOS1070e ruby 计算机 考研 zookeeper nfs 服务器部署ai模型 sqlite3 SSL 域名 Anolis nginx安装 linux插件下载 高效日志打印 串口通信日志 服务器日志 系统状态监控日志 异常记录日志 DocFlow mq rocketmq 私有化 Windows ai工具 v10 软件 ldap 版本 gcc 小智AI服务端 xiaozhi ASR TTS GIS 遥感 WebGIS minio docker命令大全 AD 域管理 flash-attention Kali 渗透 网站搭建 serv00 怎么卸载MySQL MySQL怎么卸载干净 MySQL卸载重新安装教程 MySQL5.7卸载 Linux卸载MySQL8.0 如何卸载MySQL教程 MySQL卸载与安装 MySql 联想开天P90Z装win10 多个客户端访问 IO多路复用 TCP相关API Linux的权限 网卡的名称修改 eth0 ens33 deep learning 大文件分片上传断点续传及进度条 如何批量上传超大文件并显示进度 axios大文件切片上传详细教 node服务器合并切片 vue3大文件上传报错提示错误 大文件秒传跨域报错cors 监控k8s 监控kubernetes 网工 Ubuntu22.04 开发人员主页 搜索引擎 健康医疗 互信 邮件APP 免费软件 上传视频至服务器代码 vue3批量上传多个视频并预览 如何实现将本地视频上传到网页 element plu视频上传 ant design vue vue3本地上传视频及预览移除 Ubuntu 24 常用命令 Ubuntu 24 Ubuntu vi 异常处理 vue-i18n 国际化多语言 vue2中英文切换详细教程 如何动态加载i18n语言包 把语言json放到服务器调用 前端调用api获取语言配置文件 Claude AnythingLLM AnythingLLM安装 SRS 流媒体 直播 c 数据管理 数据治理 数据编织 数据虚拟化 Deepseek-R1 私有化部署 推理模型 端口测试 自动化测试 性能测试 田俊楠 odoo 服务器动作 Server action 反向代理 CrewAI 常用命令 文本命令 目录命令 python3.11 qemu libvirt 视频编解码 dash 正则表达式 pgpool deepseek r1 键盘 金仓数据库 2025 征文 数据库平替用金仓 相差8小时 UTC 时间 netty make命令 makefile文件 GoogLeNet 远程过程调用 Windows环境 es jvm ios iphone Jellyfin swoole 云桌面 微软 AD域控 证书服务器 FTP服务器 5G 3GPP 卫星通信 Linux awk awk函数 awk结构 awk内置变量 awk参数 awk脚本 awk详解 USB网络共享 镜像 无桌面 监控k8s集群 集群内prometheus risc-v ssh远程登录 技术共享 我的世界 我的世界联机 数码 软考 远程服务 rustdesk 路径解析 centos 7 docker run 数据卷挂载 交互模式 执法记录仪 智能安全帽 smarteye SysBench 基准测试 ecm bpm selete 昇腾 npu Attention 宕机切换 服务器宕机 技能大赛 稳定性 看门狗 RAG 检索增强生成 文档解析 大模型垂直应用 linux环境变量 音乐服务器 Navidrome 音流 MCP server C/S windows日志 Minecraft dity make DOIT 四博智联 北亚数据恢复 oracle数据恢复 eNSP 网络规划 VLAN 企业网络 freebsd searxng PPI String Cytoscape CytoHubba Docker引擎已经停止 Docker无法使用 WSL进度一直是0 镜像加速地址 XCC Lenovo 人工智能生成内容 DBeaver kerberos bcompare Beyond Compare 繁忙 解决办法 替代网站 汇总推荐 AI推理 MQTT协议 消息服务器 代码 H3C Dell HPE 联想 浪潮 iDRAC R720xd cursor embedding 聊天室 Docker Compose docker compose docker-compose 前后端分离 can 线程池 服务器无法访问 ip地址无法访问 无法访问宝塔面板 宝塔面板打不开 openstack Xen IM即时通讯 剪切板对通 HTML FORMAT XFS xfs文件系统损坏 I_O error saltstack file server http server web server 阿里云ECS EMUI 回退 降级 gnu Playwright muduo 个人博客 X11 Xming webstorm nac 802.1 portal QT 5.12.12 QT开发环境 Ubuntu18.04 uni-file-picker 拍摄从相册选择 uni.uploadFile H5上传图片 微信小程序上传图片 linux上传下载 FunASR apt 计算虚拟化 弹性裸金属 edge浏览器 ROS 图形化界面 聚类 程序 性能分析 yaml Ultralytics 可视化 阻塞队列 生产者消费者模型 服务器崩坏原因 jetty undertow grub 版本升级 扩容 xml 虚拟局域网 ceph less ISO镜像作为本地源 显示管理器 lightdm gdm 小番茄C盘清理 便捷易用C盘清理工具 小番茄C盘清理的优势尽显何处? 教你深度体验小番茄C盘清理 C盘变红?!不知所措? C盘瘦身后电脑会发生什么变化? 虚拟显示器 ip 备份SQL Server数据库 数据库备份 傲梅企业备份网络版 深度求索 私域 知识库 磁盘镜像 服务器镜像 服务器实时复制 实时文件备份 换源 国内源 Debian 显卡驱动 Erlang OTP gen_server 热代码交换 事务语义 大模型应用 MNN Qwen tensorflow pppoe radius hugo 序列化反序列化 Netty 即时通信 NIO ros SSH 密钥生成 SSH 公钥 私钥 生成 tidb GLIBC HTTP 服务器控制 ESP32 DeepSeek jina 匿名管道 dns是什么 如何设置电脑dns dns应该如何设置 银河麒麟桌面操作系统 Kylin OS 在线预览 xlsx xls文件 在浏览器直接打开解析xls表格 前端实现vue3打开excel 文件地址url或接口文档流二进 seatunnel seleium AutoDL Linux的基础指令 AI作画 信创 信创终端 中科方德 AI agent composer 思科模拟器 Cisco vasp安装 qt项目 qt项目实战 qt教程 模拟退火算法 国标28181 视频监控 监控接入 语音广播 流程 SIP SDP 英语 xcode Radius wordpress 无法访问wordpess后台 打开网站页面错乱 linux宝塔面板 wordpress更换服务器 HAProxy 根服务器 clickhouse 信号 ubuntu24.04.1 自学笔记 小米 澎湃OS Android c/c++ 串口 laravel IO模型 react native junit 上传视频文件到服务器 uniApp本地上传视频并预览 uniapp移动端h5网页 uniapp微信小程序上传视频 uniapp app端视频上传 uniapp uview组件库 端口 查看 ss fast 读写锁 需求分析 规格说明书 AI Agent 字节智能运维 DIFY lua autodl 音乐库 飞牛 g++ g++13 语音识别 IMX317 MIPI H265 VCU 热榜 yolov5 7z k8s二次开发 集群管理 CDN onlyoffice EtherNet/IP串口网关 EIP转RS485 EIP转Modbus EtherNet/IP网关协议 EIP转RS485网关 EIP串口服务器 Headless Linux wpf 服务网格 istio DeepSeek r1 Qwen2.5-VL vllm cd 目录切换 AzureDataStudio 语法 flink NLP模型 java-rocketmq 策略模式 单例模式 eclipse 网络建设与运维 xpath定位元素 OpenHarmony bat 项目部署到linux服务器 项目部署过程 鸿蒙开发 移动开发 李心怡 Doris搭建 docker搭建Doris Doris搭建过程 linux搭建Doris Doris搭建详细步骤 Doris部署 离线部署dify cpp-httplib docker部署Python llama.cpp 大模型部署 docker部署翻译组件 docker部署deepl docker搭建deepl java对接deepl 翻译组件使用 捆绑 链接 谷歌浏览器 youtube google gmail 强化学习 WSL2 上安装 Ubuntu GameFramework HybridCLR Unity编辑器扩展 自动化工具 vpn 企业网络规划 华为eNSP 软链接 硬链接 sysctl.conf vm.nr_hugepages PX4 regedit 开机启动 支付 微信支付 开放平台 进程优先级 调度队列 进程切换 webgl 本地化部署 VS Code 僵尸世界大战 游戏服务器搭建 代码托管服务 Mac内存不够用怎么办 在线office opengl cocoapods fonts-noto-cjk ubuntu24 vivado24 免费域名 域名解析 玩机技巧 软件分享 软件图标 perl 架构与原理 banner 双系统 内网环境 triton 模型分析 移动魔百盒 影刀 #影刀RPA# USB转串口 harmonyOS面试题 trea idea Python基础 Python教程 Python技巧 axure 富文本编辑器 毕昇JDK WLAN IDEA su sudo 实习 Mermaid 可视化图表 防火墙 NAT转发 NAT Server Unity Dedicated Server Host Client 无头主机 ubuntu20.04 开机黑屏 figma WebVM 基础环境 流水线 脚本式流水线 安防软件 midjourney 增强现实 沉浸式体验 应用场景 技术实现 案例分析 AR 嵌入式系统开发 css3 代理服务器 sentinel 知识图谱 Zoertier 内网组网 沙盒 区块链 存储维护 NetApp存储 EMC存储 Typore TrueLicense 虚幻引擎 virtualbox 佛山戴尔服务器维修 佛山三水服务器维修 超融合 perf linux内核 浏览器开发 AI浏览器 conda配置 conda镜像源 我的世界服务器搭建 加解密 Yakit yaklang 干货分享 黑客工具 密码爆破 烟花代码 烟花 元旦 tailscale derp derper 中转 线性代数 电商平台 服务器时间 C++软件实战问题排查经验分享 0xfeeefeee 0xcdcdcdcd 动态库加载失败 程序启动失败 程序运行权限 标准用户权限与管理员权限 本地知识库部署 DeepSeek R1 模型 lsb_release /etc/issue /proc/version uname -r 查看ubuntu版本 安卓模拟器 像素流送api 像素流送UE4 像素流送卡顿 像素流送并发支持 带外管理 rancher 磁盘清理 powerpoint ArtTS 环境配置 本地环回 bind 模拟器 教程 TCP协议 抗锯齿 nftables word Linux环境 iventoy VmWare OpenEuler firewall deekseek cfssl 大模型技术 本地部署大模型 GRUB引导 Linux技巧 互联网医院 Logstash 日志采集 嵌入式Linux IPC Spring Security MVS 海康威视相机 rtsp服务器 rtsp server android rtsp服务 安卓rtsp服务器 移动端rtsp服务 大牛直播SDK 中兴光猫 换光猫 网络桥接 自己换光猫 多端开发 智慧分发 应用生态 鸿蒙OS 游戏开发 lb 协议 dock 加速 hosts 大模型推理 OpenSSH 元服务 应用上架 软件构建 minecraft db IPv4 子网掩码 公网IP 私有IP 主从复制 xss 计算生物学 生物信息 基因组 搭建个人相关服务器 Ubuntu共享文件夹 共享目录 Linux共享文件夹 iperf3 带宽测试 K8S k8s管理系统 HarmonyOS NEXT 原生鸿蒙 查询数据库服务IP地址 SQL Server 分布式训练 虚拟现实 智能电视 SVN Server tortoise svn ros1 Noetic 20.04 apt 安装 云耀服务器 风扇控制软件 top Linux top top命令详解 top命令重点 top常用参数 System V共享内存 进程通信 React Next.js 开源框架 docker desktop image deployment daemonset statefulset cronjob 图片增强 增强数据 欧标 OCPP powerbi 国产数据库 瀚高数据库 数据迁移 下载安装 IO maxkb ARG 开源软件 话题通信 服务通信 notepad rpa tar 隐藏文件 隐藏目录 管理器 通配符 Linux find grep 钉钉 oracle fusion oracle中间件 硅基流动 ChatBox wsgiref Web 服务器网关接口 抓包工具 nosql mysql安装报错 windows拒绝安装 合成模型 扩散模型 图像生成 ardunio BLE WebServer 智慧农业 开源鸿蒙 团队开发 跨平台 软件卸载 系统清理 Linux权限 权限命令 特殊权限 CPU 使用率 系统监控工具 linux 命令 AI员工 MDK 嵌入式开发工具 论文笔记 MAC SecureCRT win服务器架设 windows server 服务器扩容没有扩容成功 UEFI Legacy MBR GPT U盘安装操作系统 MacOS 视频平台 录像 视频转发 视频流 授时服务 北斗授时 状态模式 lvm 磁盘挂载 磁盘分区 ELF加载 hexo cmake kotlin 服务器部署 本地拉取打包 kernel CosyVoice visual studio 华为证书 HarmonyOS认证 华为证书考试 浪潮信息 AI服务器 MAVROS 四旋翼无人机 Helm k8s集群 burp suite 抓包 Web服务器 多线程下载工具 PYTHON fork wait waitpid exit vnc ranger MySQL8.0 windows 服务器安装 工具 zip unzip Web应用服务器 ebpf uprobe python2 ubuntu24.04 Linux 维护模式 shell脚本免交互 expect linux免交互 网络爬虫 ABAP scapy 粘包问题 电视剧收视率分析与可视化平台 内核 问题解决 极限编程 java-rabbitmq 安装MySQL Carla 智能驾驶 通信工程 毕业 UDP 多产物 navicat Sealos 论文阅读 Reactor反应堆 proxy模式 性能调优 安全代理 samba 流量运营 网络文件系统 scikit-learn Apache Beam 批流统一 案例展示 数据分区 容错机制 网页服务器 web服务器 Nginx 查看显卡进程 fuser 网络搭建 神州数码 神州数码云平台 云平台 copilot mcp服务器 client close ip协议 nvm whistle export import save load 迁移镜像 Unity插件 docker搭建pg docker搭建pgsql pg授权 postgresql使用 postgresql搭建 输入系统 多路转接 milvus ROS2 浏览器自动化 Mac软件 项目部署 ubuntu 18.04 mm-wiki搭建 linux搭建mm-wiki mm-wiki搭建与使用 mm-wiki使用 mm-wiki详解 Alexnet 宝塔面板无法访问 vu大文件秒传跨域报错cors 容器技术 cron crontab日志 机柜 1U 2U 对比 meld DiffMerge 服务器安全 网络安全策略 防御服务器攻击 安全威胁和解决方案 程序员博客保护 数据保护 安全最佳实践 端口聚合 windows11 达梦 DM8 fstab 搜狗输入法 中文输入法 录音麦克风权限判断检测 录音功能 录音文件mp3播放 小程序实现录音及播放功能 RecorderManager 解决录音报错播放没声音问题 生活 接口优化 联机 僵尸毁灭工程 游戏联机 开服 服务器正确解析请求体 数字证书 签署证书 Docker快速入门 DevOps 软件交付 数据驱动 解决方案 js 镜像下载 qt5 客户端开发 显示器 VMware Tools vmware tools安装 vmwaretools安装步骤 vmwaretools安装失败 vmware tool安装步骤 vm tools安装步骤 vm tools安装后不能拖 vmware tools安装步骤 MobaXterm 文件传输 lighttpd安装 Ubuntu配置 Windows安装 服务器优化 蓝牙 弹性服务器 零售 yum换源 大屏端 WINCC csrutil mac恢复模式进入方法 恢复模式 服务器ssl异常解决 stable diffusion 西门子PLC 通讯 netlink libnl3 代码规范 macOS 工具分享 ubuntu安装 linux入门小白 zerotier