深入理解数据结构(2):顺序表和链表详解

news2024/11/27 12:51:49

标头风景图片


  • 文章主题:顺序表和链表详解🌱
  • 所属专栏:深入理解数据结构📘
  • 作者简介:更新有关深入理解数据结构知识的博主一枚,记录分享自己对数据结构的深入解读。😄
  • 个人主页:[₽]的个人主页🔥🔥

顺序表和链表详解

  • 前言
  • 线性表
  • 顺序表
    • 概念及结构
    • 顺序表的实现
    • 顺序表的问题及思考
  • 链表
    • 链表的概念及结构
    • 链表的分类
    • 链表的实现
    • 链表是否有环的相关问题
    • 双向链表的实现
  • 顺序表和链表的区别
  • 结语

前言

顺序表和链表是数据结构的基础,也是最基本、最简单、最常用的数据结构——线性表的两种主要形式,以下是博主对于顺序表和链表这两种最基本数据结构的详解。


线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
线性表中的顺序表和链表


顺序表

概念及结构

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可分为:
1.静态顺序表:使用定长数组存储元素
静态顺序表
2.动态顺序表:使用动态开辟的数组存储。
动态顺序表

顺序表的实现

静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。

SList.h

// SeqList.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

typedef int SLDataType;
typedef struct SeqList
{
	SLDataType* a;
	int size;     // 有效数据
	int capacity; // 空间容量
}SeqList;

// 对数据的管理:增删查改 
void SeqListInit(SeqList* ps);
void SeqListDestroy(SeqList* ps);
static void SeqListCheckCapacity(SeqList* ps);
void SeqListPrint(SeqList* ps);
void SeqListPushBack(SeqList* ps, SLDataType x);
void SeqListPushFront(SeqList* ps, SLDataType x);
void SeqListPopBack(SeqList* ps);
void SeqListPopFront(SeqList* ps);

// 顺序表查找
int SeqListFind(SeqList* ps, SLDataType x);
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, int pos, SLDataType x);
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, int pos);

SList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void SeqListInit(SeqList* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->size = 0;
	ps->capacity = 0;
}
void SeqListDestroy(SeqList* ps)
{
	assert(ps);
	if (ps->a != NULL)
	{
		free(ps->a);
		ps->a = NULL;
		ps->size = 0;
		ps->capacity = 0;
	}
}
void SeqListPrint(SeqList* ps)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->a[i]);
	}
	printf("\n");
}
static void SeqListCheckCapacity(SeqList* ps)
{
	assert(ps);
	if (ps->size == ps->capacity)
	{
		int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * newcapacity);// realloc函数在对NULL扩容时功能会自动
		if (tmp == NULL)                                                               // 变成类似于malloc函数的功能直接在动态
		{                                                                              // 区开辟一块动态内存空间,但仅限于是具有
			perror("realloc fail");                                                    // 了初始化空指针的函数才行。
			return;
		}
		ps->a = tmp;
		ps->capacity = newcapacity;
	}
}
void SeqListPushBack(SeqList* ps, SLDataType x)
{     
	assert(ps);
	SeqListCheckCapacity(ps);
	ps->a[ps->size++] = x; 
}
void SeqListPushFront(SeqList* ps, SLDataType x)
{
	assert(ps);
	SeqListCheckCapacity(ps);
	//挪动腾空
	int end = ps->size - 1;
	while (end >= 0)
	{
		ps->a[end + 1] = ps->a[end];
		--end;
	}
	ps->a[0] = x;
	ps->size++;
}
void SeqListPopBack(SeqList* ps)
{
	assert(ps);
	//温柔的检查
	//if (ps->size == 0)
	//{
	//	printf("The size has already been set to 0.");
	//	return;
	//}
	//暴力检查
	assert(ps->size > 0);
	ps->size--;
}
void SeqListPopFront(SeqList* ps)
{
	assert(ps);
	assert(ps->size > 0);
	int begin = 1;
	//挪动覆盖
	while (begin < ps->size)
	{
		ps->a[begin - 1] = ps->a[begin++];
	}
	ps->size--;
}
//void SeqListFind()
//{
//
//}
void SeqListInsert(SeqList* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);
	SeqListCheckCapacity(ps);
	// 挪动腾空
	int end = ps->size - 1;
	while (end >= pos)
	{
		ps->a[end + 1] = ps->a[end--];
	}
	ps->a[pos] = x;
	ps->size++;
}
void SeqListErase(SeqList* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	SeqListCheckCapacity(ps);
	int begin = pos + 1;
	while (begin < ps->size)
	{
		ps->a[begin - 1] = ps->a[begin++];
	}
	ps->size--;
}
int SeqListFind(SeqList* ps, SLDataType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->a[i] == x)
			return i;
	}
	return -1;
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void TestSeqList1()
{
	SeqList S1;
	SeqListInit(&S1);
	SeqListPushBack(&S1, 1);
	SeqListPushBack(&S1, 3);
	SeqListPushBack(&S1, 4);
	SeqListPushBack(&S1, 5);
	SeqListPushBack(&S1, 6);
	SeqListPushBack(&S1, 7);
	SeqListPushBack(&S1, 8);
	SeqListPrint(&S1);
	SeqListPushFront(&S1, 10);
	SeqListPushFront(&S1, 20);
	SeqListPushFront(&S1, 30);
	SeqListPushFront(&S1, 40);
	SeqListPrint(&S1);
	SeqListDestroy(&S1);
}
void TestSeqList2()
{
	SeqList S1;
	SeqListInit(&S1);
	SeqListPushBack(&S1, 1);
	SeqListPushBack(&S1, 3);
	SeqListPushBack(&S1, 4);
	SeqListPushBack(&S1, 5);
	SeqListPushBack(&S1, 6);
	SeqListPushBack(&S1, 7);
	SeqListPushBack(&S1, 8);
	SeqListPrint(&S1);
	SeqListPopBack(&S1);
	SeqListPopBack(&S1);
	SeqListPrint(&S1);

	SeqListDestroy(&S1);
}
void TestSeqList3()
{
	SeqList S1;
	SeqListInit(&S1);
	SeqListPushBack(&S1, 1);
	SeqListPushBack(&S1, 3);
	SeqListPushBack(&S1, 4);
	SeqListPushBack(&S1, 5);
	SeqListPushBack(&S1, 6);
	SeqListPushBack(&S1, 7);
	SeqListPushBack(&S1, 8);
	SeqListPrint(&S1);

	SeqListPopFront(&S1);
	SeqListPrint(&S1);

	SeqListPopFront(&S1);
	SeqListPrint(&S1);

	SeqListPopFront(&S1);
	SeqListPrint(&S1);

	SeqListPopFront(&S1);
	SeqListPrint(&S1);

	SeqListPopFront(&S1);
	SeqListPrint(&S1);

	SeqListPopFront(&S1);
	SeqListPrint(&S1);

	SeqListPopFront(&S1);
	SeqListPrint(&S1);

	SeqListDestroy(&S1);
	//SeqListPopFront(&S1);// 顺序表为空仍在删除导致运行时断言报断言错误(严格来说并不属于编译器报错的一种,
	//SeqListPrint(&S1);   // 是一种防止程序发生运行错误及时止损的方法,报断言错误后直接回去改断言错误即可。
}
void TestSeqList4()
{
	SeqList S1;
	SeqListInit(&S1);
	SeqListPushBack(&S1, 1);
	SeqListPushBack(&S1, 3);
	SeqListPushBack(&S1, 4);
	SeqListPushBack(&S1, 5);
	SeqListPushBack(&S1, 6);
	SeqListPushBack(&S1, 7);
	SeqListPushBack(&S1, 8);
	SeqListPrint(&S1);

	SeqListInsert(&S1, 2, 40);
	SeqListPrint(&S1);

	SeqListDestroy(&S1);
}
void TestSeqList5()
{
	SeqList S1;
	SeqListInit(&S1);
	SeqListPushBack(&S1, 1);
	SeqListPushBack(&S1, 3);
	SeqListPushBack(&S1, 4);
	SeqListPushBack(&S1, 5);
	SeqListPushBack(&S1, 6);
	SeqListPushBack(&S1, 7);
	SeqListPushBack(&S1, 8);
	SeqListPrint(&S1);

	SeqListErase(&S1, 2, 40);
	SeqListPrint(&S1);

	SeqListDestroy(&S1);
}
void TestSeqList6()
{
	SeqList S1;
	SeqListInit(&S1);
	SeqListPushBack(&S1, 1);
	SeqListPushBack(&S1, 2);
	SeqListPushBack(&S1, 3);
	SeqListPushBack(&S1, 4);
	SeqListPushBack(&S1, 5);
	SeqListPushBack(&S1, 6);
	SeqListPushBack(&S1, 7);
	SeqListPushBack(&S1, 8);
	SeqListPrint(&S1);

	int pos = SeqListFind(&S1, 2);
	if (pos != -1)
	{
		SeqListErase(&S1, pos);
	}
	SeqListPrint(&S1);

	SeqListDestroy(&S1);
}
int main()
{
	TestSeqList6();
	return 0;
}

顺序表的问题及思考

问题

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

思考:如何解决以上问题呢?下面给出了链表的结构来看看。


链表

链表的概念及结构

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

链表的分类

实际中链表的结构非常多样,常见的有单向与双向、带头与不带头、循环与不循环,这几种常见情况组合起来就有8种链表结构:

  1. 单向或者双向单向或者双向

  2. 带头或者不带头带头或者不带头

  3. 循环或者不循环循环或者不循环
    虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构(其中后一种因为虽然节点与节点之间的逻辑结构上更复杂(本质实现起来也只是多个节点的区别,和单个节点实现后指前、前指后(循环的效果)上稍复杂一些,基本不会特别复杂),函数逻辑上实现却因为节点与节点之间的双向反倒会相比普通单链表(通常指无头单向不循环链表)起来更简单,时间复杂度上也会直接由O(N)到O(1)降阶)
    用得最多的无头单向非循环链表与带头双向循环链表
    (可直接简记成三种状况的对应前后两个状况的极端的链表用得最多,其中有的比不得用得更多,并且也更好些,时间上也会花更少。)

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为非单独使用的其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中单独使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

链表的实现

SList

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
// slist.h
typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SListNode;

// 动态申请一个节点
static SListNode* BuySListNode(SLTDataType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDataType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDataType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDataType x);
// 单链表在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?
// 答:因为单链表单向的性质在只给出了将要插入的位置的而不给出其前面的指针值的情况下无法用与单链表相反的逆向
// 找到该节点所对应的前一个节点的指针值的,一般此时会再给一个在其位置前的二级指针(原因是因为当单链表中一个节
// 点都没有时得开辟一个节点改变头指针的值(头指针没有对应的结构体使其解引用到动态内存中改变其值,所以只能通过指向指针变量的二级指针来正规的解引用来改变其值,节点中的结构体通过指针的嵌套本质上实现的就是另一种与二级指针类似的可以真正改变指针值的效果,只不过这种类型的指针本身就储存在它对应类型的结构体中才可以直接向外用一个同类型的一级指针采用结构体中的引用(用指针去引用本质就只一种解引用的方式)的访问到它内部的一个结构体成员而已。))才能实现这个效果
// 相当于这个结构体类型是这两个一级指针中的过渡层,让这两个一级指针通过结构体过渡引用的方式实现了一个一级指针指向结构体类型,再由结构体类型指向一个其内部同类型的一级指针等价于二级指针直接解引用得到一个一级指针的效果,结构体就是一种这么神奇的类型,别说一级指向一级,连与其等价的变量指向指针都能够实现,甚至一级指二级等(这都是结构体这种变量引用的性质,其中用对应这种结构体的指针去引用的方式本质就是一级指针的解引用,只不过指向的结构体中刚好可以编入任意
// 级数的指针类型成员而已。
void SListInsertAfter(SListNode* pos, SLTDataType x);
// 单链表删除pos位置之后的值
// 分析思考为什么不删除pos位置?
// 答:还是相同的原因因为单链表只能单向访问的限制,只给一个位置作参数还是无法知道单链表之前的节点坐标,而如果删除的是pos位置的节点的话又必须知道pos位置之前的节点指针所对应的指向下一个成员的指针变量的位置从而改变其指针变量指向下一个节点的指针值才能够将链表接上的,所以只传一个指针变量的参数是无法做到删除对应节点后再找到上一个节点的衔接指针从而衔接上删去了一个指针的断开的链表的。
void SListEraseAfter(SListNode* pos);

// 在pos的前面插入
void SLTInsert(SListNode** pplist, SListNode* pos, SLTDataType x);// 这些地方用了二级指针的原因是因为都会涉及到头指针内部数值的改变,因为单列表普通节点的数值的改变,直接让它的上一个节点指向的一级指针改变即可,是图纸就没有结构体给他直接用一级指针引用到他的头上去,所以只能通过普通的二级指针引用到一级的身上,才能切切实实改变一级指针,但是这个函数算法里面,所以就肯定得用一个二级指针的参数来改变这种头指针。
// 删除pos位置
void SLTErase(SListNode** pplist, SListNode* pos);
void SLTDestroy(SListNode** pplist);

SList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SListPrint(SListNode* plist)
{// 此处不用加空指针的断言,因为在单链表中空指针的效果指的就是一个数据都没有,如果加了断言就无法反映单链表中没有数据的情况了,
 // 顺序表加了空指针断言的原因是因为就算其数据为0时它所对应的结构体变量早创建好了(可理解成是进顺序表的那扇门),哪怕初始化
 // 也是将其指向真正储存的表中内容的指针变成空指针,而这扇门因为创建好了其指针值在数据个数为0的情况下也是不会变成0的。
	printf("phead");
	if (plist == NULL)
	{
		printf("(NULL)->");
	}
	else
	{
		printf("->");
		SListNode* cur = plist;
		while (cur)
		{
			printf("%d", cur->data);
			if (cur->next == NULL)
				printf("(NULL)->");
			else
				printf("->");
			cur = cur->next;
		}
	}
	printf("void\n");
}
static SListNode* BuySListNode(SLTDataType x)
{
	SListNode* pnewnode = (SListNode*)malloc(sizeof(SListNode));
	if (pnewnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	pnewnode->data = x;
	pnewnode->next = NULL;
	return pnewnode;
}
void SListPushBack(SListNode** pplist, SLTDataType x)
{
	assert(pplist);
	if (*pplist == NULL)
	{
		*pplist = BuySListNode(x);// 是链表中没有一个变量时的正经情况,所以不会等于温柔的检查逻辑,因为此处是和后面等价的一种情况
	}                             // 而不是对一种错误参数的迅速止损逻辑,虽然两者很相似,但是还是不能等价,所以这种情况下没有去采
	                              // return;的格式去表示及时止损的特点,因为其不是对一个错误信息的检查站的特点,而是一个和后面情
								  // 况等价的都有可能存在的逻辑,所以是用的非独立而是和后面一种情况等价的条件双分支语句的形式表示
								  // 的。
	// 找尾
	else
	{
		SListNode* tail = *pplist;
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = BuySListNode(x);
	}
}
void SListPushFront(SListNode** pplist, SLTDataType x)
{
	assert(pplist);
	SListNode* pnewnode = BuySListNode(x);
	pnewnode->next = *pplist;
	*pplist = pnewnode;
}
void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	// 1、零个节点
	assert(*pplist);
	// 2、一个节点
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	// 多个节点
	else
	{
		SListNode* tail = *pplist, * prev = NULL;
		// 找尾
		while (tail->next)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;
	}
}
void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	assert(*pplist);
	SListNode* prev = *pplist;// 这里的prev理解的是删除节点后的之前位置的节点
	*pplist = prev->next;
	free(prev);
}
SListNode* SListFind(SListNode* plist, SLTDataType x)
{
	SListNode* cur = plist;
	while (cur && cur->data != x)// 没找到就返回NULL,且先写判断空指针的条件是利用了逻辑操作符&&可以控制求值顺序的特性,在空指针引用之前就可以先判断了空指针,再直接结束循环,从而在判断住第一个错误之后就退出循环改变两边操作目的求值顺序后,因为第一个操作目的结果为假直接跳过了后一个操作目的判断直接跳出了循环,防止后续判断下一节点的数据值时发生野指针访问错误的报错。
	{
		cur = cur->next;
	}
	return cur;
}
void SListInsertAfter(SListNode* pos, SLTDataType x)
{
	assert(pos);
	SListNode* pnewnode = BuySListNode(x);
	pnewnode->next = pos->next;
	pos->next = pnewnode;
}
void SListEraseAfter(SListNode* pos)
{
	// 1、零/一个节点
	assert(pos && pos->next);
	// 2、多个节点
	SListNode* erasednode = pos->next;
	pos->next = erasednode->next;
	free(erasednode);
}
void SLTInsert(SListNode** pplist, SListNode* pos, SLTDataType x)
{
	assert(pplist);
	// 严格限定pos一定是链表里的一个有效节点(即不会使其为指向链表末端非节点处的处于最后一个节点的防止其指针变量为野指针的空指针)
	//assert(*pplist);// 严格情况即用有长度情况的非尾插算法即可,即去除尾插分支的简单双分支
	//assert(pos);
	// 要么都是空,要么都不是空,要么头指针不为空,pos为空
	// 灵活的限定pos的位置————可以在头在尾插入
	assert((!pos && *pplist) || (!pos && !(*pplist)) || (pos && *pplist));
	// 头插(长度为零时可理解为长度为零时的尾插)(因为前面不是prev,而是一个头指针,所以只能用头指针那边的算法)
	if (*pplist == pos)
	{
		SListPushFront(pplist, x);
	}
	// 有长度时的尾插(降低时间复杂度,本身也可用else中的逻辑处理)
	else if (!pos)
		{
			SListPushFront(pplist, x);
		}
	// 有长度时的中间插入
	else
	{
		SListNode* prev = *pplist;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SListNode* pnewnode = BuySListNode(x);
		pnewnode->next = prev->next;
		prev->next = pnewnode;
	}
}
void SLTErase(SListNode** pplist, SListNode* pos)
{
	assert(pplist);
	assert(*pplist);
	assert(pos);
	// 1、头删(长度为一时可理解成尾删)(能头删尾删的积极采用,降低时间复杂度)
	if (*pplist == pos)
	{
		SListPopFront(pplist);
	}
	// 2、长度大于一时更简单的利用前一个节点的逻辑进行的中间删除
	else
	{// 本质和头指针一样也是用改变链表中的指针值,只不过要借用节点才能找到,既然用节点就能找到也就没必要用二级指针了,逻辑上顺水推舟,无需画蛇添足的多增加几步了
		SListNode* prev = *pplist;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}
void SLTDestroy(SListNode** pplist)
{
	assert(pplist);
	// 温柔的检查
	// 1、无节点
	if (*pplist == NULL)
	{
		return;
	}
	// 2、有节点
	else
	{
		SListNode* cur = *pplist, * next = NULL;
		while (cur)
		{
			next = cur->next;
			free(cur);
			cur = next;
		}
		// 小细节:销毁当前链表后指向当前链表的头指针赋回空指针,1、代表当前链表销毁后长度变回0。2、防止头指针变成野指针在后续被错误引用重新赋成空指针。
		*pplist = NULL;
	}
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SListTest1()
{
	SListNode* phead = NULL;
	SListPushBack(&phead, 1);
	SListPushBack(&phead, 2);
	SListPushBack(&phead, 3);
	SListPushBack(&phead, 4);
	SListPushBack(&phead, 5);
	SListPrint(phead);

	SLTDestroy(&phead);
}
void SListTest2()
{
	SListNode* phead = NULL;
	SListPushBack(&phead, 1);
	SListPushBack(&phead, 2);
	SListPushBack(&phead, 3);
	SListPushBack(&phead, 4);
	SListPushBack(&phead, 5);
	SListPrint(phead);

	SListPushFront(&phead, 10);
	SListPrint(phead);

	SListPushFront(&phead, 20);
	SListPrint(phead);

	SListPushFront(&phead, 30);
	SListPrint(phead);

	SListPushFront(&phead, 40);
	SListPrint(phead);

	SLTDestroy(&phead);
}
void SListTest3()
{
	SListNode* phead = NULL;
	SListPushBack(&phead, 1);
	SListPushBack(&phead, 2);
	SListPushBack(&phead, 3);
	SListPushBack(&phead, 4);
	SListPushBack(&phead, 5);
	SListPrint(phead);

	SListPopBack(&phead);
	SListPrint(phead);

	SListPopBack(&phead);
	SListPrint(phead);

	SListPopBack(&phead);
	SListPrint(phead);

	SListPopBack(&phead);
	SListPrint(phead);

	SListPopBack(&phead);
	SListPrint(phead);

	//SListPopBack(&phead);
	//SListPrint(phead);

	SLTDestroy(&phead);
}
void SListTest4()
{
	SListNode* phead = NULL;
	SListPushBack(&phead, 1);
	SListPushBack(&phead, 2);
	SListPushBack(&phead, 3);
	SListPushBack(&phead, 4);
	SListPushBack(&phead, 5);
	SListPrint(phead);

	SListPopFront(&phead);
	SListPrint(phead);

	SListPopFront(&phead);
	SListPrint(phead);

	SListPopFront(&phead);
	SListPrint(phead);

	SListPopFront(&phead);
	SListPrint(phead);

	SListPopFront(&phead);
	SListPrint(phead);

	SLTDestroy(&phead);
}
void SListTest5()
{
	SListNode* phead = NULL;
	SListPushBack(&phead, 1);
	SListPushBack(&phead, 2);
	SListPushBack(&phead, 3);
	SListPushBack(&phead, 4);
	SListPushBack(&phead, 5);
	SListPrint(phead);

	SListPopBack(&phead);
	SListPrint(phead);

	SListPopBack(&phead);
	SListPrint(phead);

	SListPopBack(&phead);
	SListPrint(phead);

	SListPopBack(&phead);
	SListPrint(phead);

	SListPopBack(&phead);
	SListPrint(phead);

	SLTDestroy(&phead);
}
void SListTest6()
{
	SListNode* phead = NULL;
	SListPushBack(&phead, 1);
	SListPushBack(&phead, 2);
	SListPushBack(&phead, 3);
	SListPushBack(&phead, 4);
	SListPushBack(&phead, 5);
	SListPrint(phead);

	SListPopFront(&phead);
	SListPrint(phead);

	SListPopFront(&phead);
	SListPrint(phead);

	SListPopFront(&phead);
	SListPrint(phead);

	SListPopFront(&phead);
	SListPrint(phead);

	SListPopFront(&phead);
	SListPrint(phead);

	SLTDestroy(&phead);
}
void SListTest7()
{
	SListNode* phead = NULL;
	SListPushBack(&phead, 1);
	SListPushBack(&phead, 2);
	SListPushBack(&phead, 3);
	SListPushBack(&phead, 4);
	SListPushBack(&phead, 5);
	SListPrint(phead);

	SListNode* pos = SListFind(phead, 6);
	if (pos == NULL)
		printf("没找到%d。\n", 6);
	else
		printf("找到了%d!\n", 6);

	pos = SListFind(phead, 5);;
	if (pos == NULL)
		printf("没找到%d。\n", 5);
	else
		printf("找到了%d!\n", 5);

	SLTDestroy(&phead);
}
void SListTest8()
{
	SListNode* phead = NULL;
	SListPushBack(&phead, 5);
	SListPrint(phead);

	SListNode* pos = SListFind(phead, 5);
	SListInsertAfter(pos, 90);
	SListPrint(phead);

	SLTDestroy(&phead);
}
void SListTest9()
{
	SListNode* phead = NULL;
	SListPushBack(&phead, 5);
	SListPushBack(&phead, 6);
	SListPrint(phead);

	SListNode* pos = SListFind(phead, 5);
	SListEraseAfter(pos);
	SListPrint(phead);

	SLTDestroy(&phead);
}
void SListTest10()
{
	SListNode* phead = NULL;
	SListPrint(phead);

	SListNode* pos = phead;
	SLTInsert(&phead, pos, 100);
	SLTInsert(&phead, NULL, 100);
	SListPrint(phead);

	SLTDestroy(&phead);
}
void SListTest11()
{
	SListNode* phead = NULL;
	SListPushBack(&phead, 90);
	SListPrint(phead);

	SListNode* pos = SListFind(phead, 90);
	SLTErase(&phead, pos);
	SListPrint(phead);

	SLTDestroy(&phead);
}
void SListTest12()
{
	SListNode* phead = NULL;
	SListPushBack(&phead, 1);
	SListPushBack(&phead, 2);
	SListPushBack(&phead, 3);
	SListPushBack(&phead, 4);
	SListPushBack(&phead, 5);
	SListPrint(phead);

	SLTDestroy(&phead);

	if (SListFind(phead, 1) == NULL && phead == NULL)
		printf("已清空,且头指针不是野指针,赋回了空指针。\n");
}
int main()
{
	SListTest11();
	return 0;
}

链表是否有环的相关问题

给定一个链表,如何判断链表中是否有环呢?

思路:快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表起始位置开始运行,如果链表带环则一定会在环中相遇,否则快指针率先走到链表的末尾。
扩展问题

  • 为什么快指针每次走两步,慢指针走一步可以?
    假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度减一。
    此时,两个指针每移动一次,之间的距离就缩小一步,不会出现每次刚好是套圈(一个循环内刚要相遇时快指针一次又会多走几步又超过慢指针又要重新追还不一定会相遇的情况)的情况,因此:在慢指针走到一圈之前,快指针肯定是可以追上慢指针的,即相遇。
  • 快指针一次走3步,4步,···n步行吗?
    与步数差的奇偶性,非环直链的长度,慢指针第一次入环相对于快指针在运动方向一边的相对位置的奇偶性(非环直链与整个圆环长度的关系共同决定,可由其两项推出),以及整个圆环的长度有关,无固定规律,需分具体情况讨论,只有快指针一次走2步时这一种情况才能保证不管什么情况都能在有环链表中一次就相遇,来判断是否有环。
  • 在快指针一次走两步的通用情况下会得出一个结论
    让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,且在环外指针刚好入环时,肯定会与环内指针在入环点处相遇(运用通过证明得出的该结论,可比在确定为环内位置的相交点将环剪断通过判断相交链表的相交点来判断入环点位置,更快更巧妙一些地解决带环链表判断入环点位置的问题(都先需要快慢指针确定环内位置的点,但后续前者只要两次O(N)的遍历,而后者需要四次,并且后者也为设置更多的变量的暴力拆解法))。
  • 证明
    确定圆环入环点的证明

双向链表的实现

List.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType _data;
	struct ListNode* _next;
	struct ListNode* _prev;
}ListNode;

// 创建返回链表的头结点
ListNode* ListCreate(LTDataType x);
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pHead, ListNode* pos);

List.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"
ListNode* ListCreate(LTDataType x)
{
	struct ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	// 创建自己构成循环的链表节点(在将其用于创建哨兵位时尤其见效(只有一个节点,必须自己指自己))
	newnode->_data = x;
	newnode->_next = newnode;
	newnode->_prev = newnode;
	return newnode;
}

void ListDestory(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->_next;
	// 清理节点
	while (cur != pHead)
	{
		ListNode* tmp = cur;
		cur = cur->_next;
		free(tmp);
	}
	// 清理哨兵位
	free(pHead);
}

void ListPrint(ListNode* pHead)
{
	assert(pHead);
	printf("哨兵位<=>");
	ListNode* cur = pHead->_next;
	while (cur != pHead)
	{
		printf("%d<=>", cur->_data);
		cur = cur->_next;
	}
	printf("\n");
}

void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	// 独立版本:
	ListNode* newnode = ListCreate(x);
	ListNode* tail = pHead->_prev;
	//pHead                    tail   newnode
	tail->_next = newnode;
	newnode->_prev = tail;
	newnode->_next = pHead;
	pHead->_prev = newnode;
	// Insert替换版本(在带头双向循环链表的结构中逻辑与该函数完全相同):
	//ListInsert(pHead, x);
}

void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->_next != pHead);
	// 独立版本:
	ListNode* tail = pHead->_prev;
	ListNode* tailPrev = tail->_prev;
	//pHead                    tailprev   tail
	tailPrev->_next = pHead;
	pHead->_prev = tailPrev;
	free(tail);
	// Erase替换版本(在带头双向循环链表的结构中逻辑与该函数完全相同):
	//ListErase(pHead, pHead->_prev);
}

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	// 独立版本:
	ListNode* newnode = ListCreate(x);
	ListNode* first = pHead->_next;
	// pHead   first
	newnode->_next = first;
	first->_prev = newnode;
	pHead->_next = newnode;
	newnode->_prev = pHead;
	// Insert替换版本(在带头双向循环链表的结构中逻辑与该函数完全相同):
	//ListInsert(pHead->_next, x);
}

void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->_next != pHead);
	// 独立版本:
	ListNode* first = pHead->_next;
	ListNode* second = first->_next;
	// pHead   first   second
	pHead->_next = second;
	second->_prev = pHead;
	free(first);
	// Erase替换版本(在带头双向循环链表的结构中逻辑与该函数完全相同):
	//ListErase(pHead, pHead->_next);
}

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* cur = pHead->_next;
	// 从第一个数据节点查找至最后一个
	while (cur != pHead)
	{
		// 找到返回值
		if (cur->_data == x)
			return cur;
		cur = cur->_next;
	}
	// 未找到返回NULL
	return NULL;
}

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* newnode = ListCreate(x);
	ListNode* posPrev = pos->_prev;
	//pHead          posPrev   pos   ...
	newnode->_next = pos;
	pos->_prev = newnode;
	posPrev->_next = newnode;
	newnode->_prev = posPrev;
}

void ListErase(ListNode* pHead, ListNode* pos)
{
	assert(pos);
	assert(pos != pHead);// 专门传一个头指针的参数,为了防止将链表的哨兵位给清理了,使链表的头指针变成野指针,从而直接导致整个链表的数据不能通过头指针当作钥匙给访问到具体内存,直接造成整个链表数据的内存泄漏
	ListNode* posNext = pos->_next;
	ListNode* posPrev = pos->_prev;
	// posPrev   pos   posNext
	posPrev->_next = posNext;
	posNext->_prev = posPrev;
	free(pos);
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"
void ListTest1()
{
	// 初始化链表(创造链表的头节点(哨兵位)的同时将链表头节点的地址赋值给头指针)
	ListNode* pHead = ListCreate(-1);

	// 插入链表数据节点
	ListPushBack(pHead, 1);
	ListPushBack(pHead, 2);
	ListPushBack(pHead, 3);
	ListPushBack(pHead, 4);
	ListPushBack(pHead, 5);
	ListPrint(pHead);

	// 销毁链表
	ListDestory(pHead);
	pHead = NULL;
}

void ListTest2()
{
	// 初始化链表(创造链表的头节点(哨兵位)的同时将链表头节点的地址赋值给头指针)
	ListNode* pHead = ListCreate(-1);

	// 尾插链表数据节点
	ListPushBack(pHead, 1);
	ListPushBack(pHead, 2);
	ListPushBack(pHead, 3);
	ListPushBack(pHead, 4);
	ListPushBack(pHead, 5);
	ListPrint(pHead);

	// 尾删链表数据节点
	ListPopBack(pHead);
	ListPrint(pHead);

	ListPopBack(pHead);
	ListPrint(pHead);

	ListPopBack(pHead);
	ListPrint(pHead);

	ListPopBack(pHead);
	ListPrint(pHead);

	ListPopBack(pHead);
	ListPrint(pHead);

	//ListPopBack(pHead);
	//ListPrint(pHead);

	// 销毁链表
	ListDestory(pHead);
	pHead = NULL;
}

void ListTest3()
{
	// 初始化链表(创造链表的头节点(哨兵位)的同时将链表头节点的地址赋值给头指针)
	ListNode* pHead = ListCreate(-1);

	// 插入链表数据节点
	ListPushFront(pHead, 1);
	ListPushFront(pHead, 2);
	ListPushFront(pHead, 3);
	ListPushFront(pHead, 4);
	ListPushFront(pHead, 5);
	ListPrint(pHead);

	// 销毁链表
	ListDestory(pHead);
	pHead = NULL;
}

void ListTest4()
{
	// 初始化链表(创造链表的头节点(哨兵位)的同时将链表头节点的地址赋值给头指针)
	ListNode* pHead = ListCreate(-1);

	// 插入链表数据节点
	ListPushFront(pHead, 1);
	ListPushFront(pHead, 2);
	ListPushFront(pHead, 3);
	ListPushFront(pHead, 4);
	ListPushFront(pHead, 5);
	ListPrint(pHead);

	// 插入链表数据节点
	ListPopFront(pHead);
	ListPrint(pHead);

	ListPopFront(pHead);
	ListPrint(pHead);

	ListPopFront(pHead);
	ListPrint(pHead);

	ListPopFront(pHead);
	ListPrint(pHead);

	ListPopFront(pHead);
	ListPrint(pHead);

	//ListPopFront(pHead);
	//ListPrint(pHead);

	// 销毁链表
	ListDestory(pHead);
	pHead = NULL;
}

void ListTest5()
{
	// 初始化链表(创造链表的头节点(哨兵位)的同时将链表头节点的地址赋值给头指针)
	ListNode* pHead = ListCreate(-1);

	// 插入链表数据节点
	ListPushFront(pHead, 1);
	ListPushFront(pHead, 2);
	ListPushFront(pHead, 3);
	ListPushFront(pHead, 4);
	ListPushFront(pHead, 5);
	ListPrint(pHead);

	// 查找节点->插入节点
	ListNode* pos = ListFind(pHead, 3);
	ListInsert(pos, 30);
	ListPrint(pHead);

	// 销毁链表
	ListDestory(pHead);
	pHead = NULL;
}

void ListTest6()
{
	// 初始化链表(创造链表的头节点(哨兵位)的同时将链表头节点的地址赋值给头指针)
	ListNode* pHead = ListCreate(-1);

	// 插入链表数据节点
	ListPushFront(pHead, 1);
	ListPushFront(pHead, 2);
	ListPushFront(pHead, 3);
	ListPushFront(pHead, 4);
	ListPushFront(pHead, 5);
	ListPrint(pHead);

	// 查找节点->删除节点
	LTDataType n = 3;
	printf("删除%d:\n", n);
	ListNode* pos = ListFind(pHead, n);
	if (pos == NULL)
		printf("没找到。\n");
	else
	{
		ListErase(pHead, pos);
		ListPrint(pHead);
	}
	n = 6;
	printf("删除%d:\n", n);
	pos = ListFind(pHead, n);
	if (pos == NULL)
		printf("没找到。\n");
	else
	{
		ListErase(pHead, pos);
		ListPrint(pHead);
	}

	// 销毁链表
	ListDestory(pHead);
	pHead = NULL;
}
int main()
{
	ListTest6();
	return 0;
}

顺序表和链表的区别

不同点顺序表链表
存储空间上物理上一定连续逻辑上连续,但物理上不一定连续
随机访问支持O(1)不支持:O(N)
任意位置插入或者删除元素可能需要搬移元素,效率低O(N)只需修改指针指向
容量动态顺序表,空间不够时需要扩容没有容量的概念
应用场景元素高效存储 + 频繁访问任意位置插入和删除频繁
缓存利用率

备注:缓存利用率参考存储体系结构 以及 局部原理性。
系统的存储层次结构


结语

以上就是博主对顺序表和链表的详解,😄希望对你的数据结构的学习有所帮助!看都看到这了,点个小小的赞或者关注一下吧(当然三连也可以~),你的支持就是博主更新最大的动力!让我们一起成长,共同进步!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1558442.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构——lesson12排序之归并排序

&#x1f49e;&#x1f49e; 前言 hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#x…

揭秘五力模型:轻松掌控企业竞争命脉,决策不再迷茫!

五力分析模型又成为波特五力模型是由著名的管理学者迈克尔波特(Michael Porter)在20世纪80年代初提出的一种理论框架&#xff0c;它对企业营销中的战略制定产生了全球性的深远影响。这一模型被广泛应用于企业竞争战略的分析&#xff0c;可以帮助企业有效地分析企业在营销环境中…

Java实验报告2

一、实验目的 本实验为Java课程的第二次实验&#xff0c;其主要目的如下&#xff1a; 理解继承和多态的概念&#xff1b; 掌握域和方法在继承中的特点&#xff1b; 掌握构造函数的继承和重载&#xff1b; 掌握this和super的用法&#xff1b; 二、实验原理 ​ 继承性是面…

上市公司-动态能力数据集(2008-2022年)

01、数据介绍 上市公司动态能力是指企业在不断变化的外部环境中&#xff0c;通过整合、创建和重构内外部资源&#xff0c;寻求和利用机会的能力。这种能力有助于企业重新构建、调配和使用其核心竞争力&#xff0c;从而保持与时俱进&#xff0c;应对市场挑战。具体来说&#xf…

Chrome DevTools中的骚操作

今天来分享 Chrome DevTools 中一些非常实用的功能和调试技巧&#xff01; 保留日志 当我们刷新完页面之后&#xff0c;通常控制台的 Console 面板就会被清空。如果想保留控制台的日志&#xff0c;就可以在设置中勾选 Preserve log 选项以保留控制台中的日志。 代码覆盖率 我…

快讯!TiDB v8 发版!超硬核 v8 引擎!

TiDB 是 PingCAP 公司自主设计、研发的开源分布式关系型数据库&#xff0c;是一款同时支持在线事务处理与在线分析处理 (Hybrid Transactional and Analytical Processing, HTAP) 的融合型分布式数据库产品。 具备水平扩容或者缩容、金融级高可用、实时 HTAP、云原生的分布式数…

【Docker】Windows中打包dockerfile镜像导入到Linux

【Docker】Windows中打包dockerfile镜像导入到Linux 大家好 我是寸铁&#x1f44a; 总结了一篇【Docker】Windows中打包dockerfile镜像导入到Linux✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 前言 今天遇到一个新需求&#xff0c;如何将Windows中打包好的dockerfile镜像给迁移…

【Linux】进程程序替换 做一个简易的shell

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 文章目录 前言 进程程序替换 替换原理 先看代码和现象 替换函数 第一个execl()&#xff1a; 第二个execv()&#xff1a; 第三个execvp()&#xff1a; 第四个execvpe()&a…

android WMS服务

android WMS服务 WMS的定义 窗口的分类 WMS的启动 WindowManager Activity、Window、DecorView、ViewRootImpl 之间的关系 WindowToken WMS的定义 WMS是WindowManagerService的简称&#xff0c;它是android系统的核心服务之一&#xff0c;它在android的显示功能中扮演着…

YOLOv9改进策略 :卷积魔改 | 感受野注意力卷积运算(RFAConv)

💡💡💡本文改进内容:感受野注意力卷积运算(RFAConv),解决卷积块注意力模块(CBAM)和协调注意力模块(CA)只关注空间特征,不能完全解决卷积核参数共享的问题 💡💡💡使用方法:代替YOLOv9中的卷积,使得更加关注感受野注意力,提升性能 💡💡💡RFAConv…

vue3:通过【自定义指令】实现自定义的不同样式的tooltip

一、效果展示 vue3自定义不同样式的tooltip 二、实现思路 1.ts文件 在ts文件中创建一个全局容器 import一个容器组件&#xff0c;用于存放自定义的各式组件 创建一个指令并获取到指令传递的数据&#xff0c;并为容器组件传值 2.容器组件 用于存放自定义Tooltip样式的组件…

最新2024年增强现实(AR)营销指南(完整版)

AR营销是新的最好的东西&#xff0c;就像元宇宙和VR营销一样。利用AR技术开展营销活动可以带来广泛的利润优势。更不用说&#xff0c;客户也喜欢AR营销&#xff01; 如果企业使用AR&#xff0c;71%的买家会更多地购物。40%的购物者准备在他们可以在AR定制的产品上花更多的钱。…

详解Java线程的状态

一、观察线程的所有状态 线程的状态是⼀个枚举类型 Thread.State public class ThreadState {public static void main(String[] args) {for (Thread.State state : Thread.State.values()) {System.out.println(state);}} } NEW: 安排了⼯作, 还未开始⾏动 RUNNABLE: 可⼯…

JavaSE day16笔记 - string

第十六天课堂笔记 学习任务 Comparable接口★★★★ 接口 : 功能的封装 > 一组操作规范 一个抽象方法 -> 某一个功能的封装多个抽象方法 -> 一组操作规范 接口与抽象类的区别 1本质不同 接口是功能的封装 , 具有什么功能 > 对象能干什么抽象类是事物本质的抽象 &…

MYSQL——索引概念索引结构

索引 索引是帮助数据库高效获取数据的排好序的数据结构。 有无索引时&#xff0c;查询的区别 主要区别在于查询速度和系统资源的消耗。 查询速度&#xff1a; 在没有索引的情况下&#xff0c;数据库需要对表中的所有记录进行扫描&#xff0c;以找到符合查询条件的记录&#…

《深入理解计算机系统》学习(9):链接和执行

目录 一、链接1.1 编译器驱动程序1.2 链接任务 二、目标文件2.1 目标文件三种形式2.2 可重定位目标文件 三、符号3.1 符号表3.2 符号解析3.3 链接器解析多重定义的全局符号 四、重定位4.1 重定位条目4.2 重定位符号引用 五、可执行目标文件5.1 可执行文件结构5.2 加载可执行目标…

设置asp.net core WebApi函数请求参数可空的两种方式

以下面定义的asp.net core WebApi函数为例&#xff0c;客户端发送申请时&#xff0c;默认三个参数均为必填项&#xff0c;不填会报错&#xff0c;如下图所示&#xff1a; [HttpGet] public string GetSpecifyValue(string param1,string param2,string param3) {return $"…

C++格式化输入和输出

格式化输入与输出 除了条件状态外&#xff0c;每个iostream对象还维护一个格式状态来控制IO如何格式化的细节。 格式状态控制格式化的某些方面&#xff0c;如整型值是几进制、浮点值的精度、一个输出元素的宽度等。 标准库定义了一组操纵符来修改流的格式状态。 一个操纵符…

【进程IO】详细讲解文件描述符fd

文章目录 前言什么叫文件描述符FILE与fd的关系 再次理解文件为什么要有文件的方法列表呢&#xff1f; 进程和struct file的关系再次理解open操作 前言 C语言的关于文件操作的各种函数实际上是对系统调用的封装。那么从进程的角度看&#xff0c;一个文件到底是如何被描述的呢&a…

Postwoman 安装

Postwoman作为Postman的女朋友&#xff0c;具有免费开源、轻量级、快速且美观等特性&#xff0c;是一款非常好用的API调试工具。能帮助程序员节省时间&#xff0c;提升工作效率。 Github地址&#xff1a;GitHub - hoppscotch/hoppscotch: &#x1f47d; Open source API devel…