数据结构修炼——顺序表和链表的区别与联系?从入门到进阶!

news2024/11/15 17:41:26

目录

  • 一、线性表
  • 二、顺序表
    • 2.1 概念及结构
    • 2.2 接口实现
    • 2.3 一些思考以及顺序表的缺点
  • 三、链表
    • 3.1 概念及结构
    • 3.2 链表的分类
    • 3.3 链表的实现
      • 3.3.1 无头单向非循环链表
      • 3.3.2 带头双向循环链表
  • 四、顺序表和链表的区别

一、线性表

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

二、顺序表

2.1 概念及结构

顺序表(Sequence List)是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。

顺序表一般可以分为静态顺序表动态顺序表
静态顺序表:使用定长数组存储数据元素。
动态顺序表:使用动态开辟的数组存储数据元素。

例如:

//静态顺序表的实现
#define CAPACITY 10//宏定义数组长度 CAPACITY
typedef int SLDataType//数组元素类型重命名为 SLDataType
typedef struct SeqList
{
	SLDataType array[NUMSSIZE];//定长数组
	size_t size;//有效数据的个数
}SeqList;

首先是宏定义#define,为了方便统一修改与重定义定长数组的长度,通常会使用宏定义一个变量作为数组长度,这样变量就可以用来定义数组长度了。相似的,为了方便统一修改与重定义数组元素的类型,通常会使用typedef将数组元素类型重命名为SLDataType。其次是在顺序表的结构体定义中,通常除了数组这个结构体成员外,还有一个size_t类型的结构体成员用于记录数组的有效元素个数,同时也记录了数组最后一个元素的下标即有效数据个数减1。

//动态顺序表的实现
typedef int SLDataType//数组元素类型重命名为SLDataType
typedef struct SeqList
{
	SLDataType* array;//指向动态数组地址的指针
	size_t size;//有效数据个数
	size_t capacity;//动态数组最大容量空间的大小
}SeqList;

与静态顺序表一样,为了方便修改动态数组元素类型,对数组元素类型进行了重命名。在顺序表的结构体内部,动态顺序表定义了一个数组元素类型的指针来指向数组所在空间(在C语言修炼的指针篇中,我们详细介绍过了数组与指针本质上的联系——传送门)。动态顺序表相较静态顺序表增加了一个变量,用于记录动态开辟出来的数组大小/容量,毕竟调用相关函数开辟空间也是有时间空间上的消耗的,所以不可能存储一个就开辟一个,而是按一定规律提前开辟好空间与扩容

2.2 接口实现

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

动态顺序表增删查改的实现:

typedef int SLDataType;//根据需要修改数组元素类型
typedef struct SeqList
{
    SLDataType* arr;
    size_t size;//有效数据个数
    size_t capacity;//空间容量
}SL;

//初始化和销毁
void SLInit(SL* ps);
void SLDestroy(SL* ps);
//数组空间扩容
void SLCheckCapacity(SL* ps);
//顺序表打印
void SLPrint(SL s);
//头部插入删除 / 尾部插入删除
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
//指定位置之前插入数据
void SLInsert(SL* ps, int pos, SLDataType x);
//删除指定位置的数据
void SLErase(SL* ps, int pos);
//查找顺序表中的数据
SLDataType SLFind(SL s, SLDataType x);

后面需要用到的头文件有:

#include<stdio.h>
#include<assert.h>//提供assert断言函数,帮助判断指针是否为空或数据是否有效
#include<stdlib.h>//提供free,realloc函数

顺序表的初始化与销毁:
当我们定义了顺序表的结构体后,我们就可以定义结构体变量了,一个结构体变量SL s代表一个顺序表。但定义了变量后,还需要进行顺序表的初始化。顺序表不用之后也需要销毁并释放动态开辟的空间。
代码示例如下:

void SLInit(SL* ps)//顺序表初始化
{
	assert(ps);//对 ps 指针判空
	//顺序表结构体变量的成员变量初始化
	ps->arr = NULL;
	ps->capacity = ps->size = 0;
}
void SLDestroy(SL* ps)//顺序表销毁
{
	assert(ps);
	if (ps->arr)//判断arr数组是否开辟了空间
	{
		free(ps->arr);//释放所开辟空间,防止内存泄漏
	}
	ps->arr = NULL;
	ps->capacity = ps->size = 0;
}

一定要注意函数的参数问题,首先传递过来的一定是顺序表变量SL s的地址,传值调用是无法改变实参——顺序表变量的,只有传址调用才能改变实参的值。其次是对传递过来的指针参数SL* ps进行判空,空指针是无法解引用访问的。
在顺序表的销毁中,需要注意对程序申请的空间进行释放,程序在关闭时会主动进行内存空间的释放,但如果程序一直不结束,就会造成内存泄漏问题。

数组空间的扩容:
动态顺序表的重点就是动态的空间分配,因此扩容函数的实现非常关键。
代码如下:

void SLCheckCapacity(SL* ps)//检查空间容量与扩容
{
	assert(ps);//判空
	if (ps->size == ps->capacity)//判断数组大小是否等于容量
	{
		//数组大小等于空间容量,则空间不足,直接扩容
		int newCapacity = ps->capacity == 0 ? 4: 2 * 
												ps->capacity;
		SLDataType* tmp = (SLDataType*)realloc(ps->arr, 
			   newCapacity * sizeof(SLDataType));//重新申请空间
		if (!tmp)//判断空间是否申请成功
		{
			//申请失败,报错并退出程序
			perror("realloc fail!");
			exit(1);//直接退出程序
		}
		//空间申请成功
		ps->arr = tmp;//更新arr指针
		ps->capacity = newCapacity;//更新空间容量
	}
}

扩容要注意的是一定不能直接用内存函数realloc对原数组空间进行调整,因为存在空间申请失败的可能,所以需要先判断是否申请空间成功,再将有效的新空间给数组。
在扩容数量的选择上,通常按二倍二倍的扩容,经过数学统计与分析这是最高效且节省资源的扩容方式。但我们不能直接int newCapacity = 2 * ps->capacity;,因为在顺序表的初始化中空间容量的初始值是0,而0乘以任何数为0,所以这里取巧使用了一个三目操作符ps->capacity == 0? 4: 2 * ps->capacity,如果ps->capacity为0则返回4,如果不为0则返回它的2倍,三目操作符的返回值又赋值给了newCapacity

头删尾删,头插尾插:
数据的头删头插,即删除数组的第一个元素或在所有元素之前插入数据。尾删尾插同理,删除数组的最后一个有效元素或在所有元素之后插入数据。
代码示例如下:

void SLPushBack(SL* ps, SLDataType x)//尾插
{
	assert(ps);//判空
	SLCheckCapacity(ps);//检查空间容量
	ps->arr[ps->size++] = x;//从后面插入数据
}
void SLPushFront(SL* ps, SLDataType x)//头插
{
	assert(ps);
	SLCheckCapacity(ps);
	for (int i = ps->size; i > 0; i--)//当前有效元素整体后移一位
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[0] = x;//头插赋值
	ps->size++;//更新有效数据个数
}
void SLPopBack(SL* ps)//尾删
{
	assert(ps);
	assert(ps->size);//判断元素有效个数是否为0,为0报错警告
	ps->size--;//元素有效个数不为0,直接有效元素个数减1即可
}
void SLPopFront(SL* ps)//头删
{
	assert(ps);
	for (int i = 0; i < ps->size - 1; i++)
	{
		//除了第一个有效元素,后面元素整体前移一位覆盖第一个元素
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;//有效元素个数减1
}

指定位置插入新数据:

void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);//确保pos坐标合法
	SLCheckCapacity(ps);
	for (int i = ps->size; i > pos; i--)
	{
		//从下标pos这个位置到最后一个有效元素整体后移一位,空出pos位置
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos] = x;//赋值插入数据
	ps->size++;//有效元素个数加1
}

删除指定位置的数据:

void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	for (int i = pos; i < ps->size - 1; i++)
	{
		//pos后的元素整体前移一位覆盖pos位置
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;//有效元素个数减1
}

查找指定数据的位置:
查找数据只需要返回数据的位置信息即可,不需要改变顺序表信息,所以顺序表变量的传参为传值调用即可。

SLDataType SLFind(SL s, SLDataType x)
{
	for (int i = 0; i < s.size; i++)//遍历数组
	{
		if (s.arr[i] == x)//找到第一个指定数据直接返回
		{
			return i;
		}
	}
	return -1;//遍历完数组未找到指定数据则返回特定值(视情况设置)
}

2.3 一些思考以及顺序表的缺点

缺点:

  1. 中间/头部的插入删除操作不方便,需要遍历顺序表,时间复杂度为O(N)。
  2. 增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗。
  3. 增容一般是呈二倍的增长,无法避免会存在一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间,而且这种浪费现象有可能随着扩容次数的增加越来越严重。

思考:如何解决以上问题呢?接下来我们进入链表的学习或许能找到答案。

三、链表

3.1 概念及结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
在这里插入图片描述
从上图可以看出:

  1. 链式结构在逻辑上是连续的,但物理存储上并不一定连续。
  2. 链表结点一般都是从上申请的空间。
  3. 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。
  4. 链表一般分为数据域指针域两个部分,数据域存储了数据信息,指针域存储了相关结点的地址信息。

3.2 链表的分类

链表分为:带头 / 不带头,循环 / 不循环,单向 / 双向。可以分别构成8种链表结构。
但实际中最常用的还是 无头单向不循环 / 带头双向循环 链表。
在这里插入图片描述

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

3.3 链表的实现

3.3.1 无头单向非循环链表

//实现单链表 Single Linked List 的增删查改
typedef int SLTDataType;
typedef struct SListNode//定义链表结点的结构体
{
	SLTDataType data;
	struct SListNode* next;//下一个结点为结构体类型,不是SLTDataType
}SLTNode;

//输出单链表数据
void SLTPrint(SLTNode* phead);
//动态申请一个结点
SLTNode* SLBuyNode(SLTDataType x);
//头部插入删除/尾部插入删除
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);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);

需要用到的头文件:

#include<stdio.h>
#include<stdlib.h>//提供free,malloc函数
#include<assert.h>//提供assert函数

头插尾插,头删尾删:

//动态申请一个结点
SLTNode* SLBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//动态申请一块空间
	if (!newnode)//申请失败则报错并退出
	{
		perror("malloc fail!\n");
		exit(1);
	}
	newnode->data = x;//初始化结点数据域的信息
	newnode->next = NULL;//初始化结点指针域的信息
	return newnode;//返回新结点
}

需要注意的是单向不带头链表的头结点即有效结点,创建时需要在堆上动态申请空间,所以先实现一个结点申请函数便于后续操作。接着我们就可以借助结点申请函数来创建单链表了。单链表的创建需要创建一个单链表结点结构体类型的指针变量,例如SLTNode* phead = SLBuyNode(x);。需要知道的是像这样的一个指针作为头节点就可以指向一整个链表,这就是单链表的初始化。

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	//要改变指向头节点的指针就需要传递头节点的地址,即&phead,则形参pphead类型为二级指针SLTNode**
	assert(pphead);//后续需要通过二级指针pphead访问头节点,先检查形参的有无有效性,即判空
	SLTNode* newnode = SLBuyNode(x);//申请需要尾插的新结点
	//尾插有两种情况
	if (!*pphead)
	{//头节点为空
		*pphead = newnode;
	}
	else
	{//头节点不为空
		SLTNode* ptmp = *pphead;
		while (ptmp->next) //找单链表表尾,即最后一个结点ptmp
		{
			ptmp = ptmp->next;
		}
		ptmp->next = newnode;//将尾结点的next指向新结点
	}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLBuyNode(x);
	newnode->next = *pphead;//直接将新结点的next指向原头节点即可
	*pphead = newnode;//更新头节点指针信息,指向新头节点
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);//删除操作不仅要保证pphead不为空,还要保证链表不为空,即头节点不为空
	
	//如果链表有多个结点,尾删需要先找到尾结点以及尾结点的前一结点
	SLTNode* pcur = *pphead;//找尾结点
	SLTNode* prev = *pphead;//找尾结点前一结点
	if (!pcur->next)
	{
		//一个结点,直接释放头节点,同时头结点置为空
		free(pcur);
		*pphead = NULL;
	}
	else
	{
		//多个结点,遍历链表
		while (pcur->next)
		{
			prev = pcur;
			pcur = pcur->next;
		}
		prev->next = NULL;//前结点的next置为空
		free(pcur);//释放尾结点
	}
}
//头删
void SLTPopFront(SLTNode** pphead)
{//删除操作需要保证pphead不为空且链表不为空
	assert(pphead && *pphead);
	SLTNode* del = *pphead;//保存需要删除的头结点
	*pphead = (*pphead)->next;//头结点置为原头结点的下一结点
	free(del);//释放原头结点
}

输出链表数据:

void SLTPrint(SLTNode* phead)
{
	//一个简单的遍历即可
	SLTNode* pcur = phead;
	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

查找特定数据:

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	while (pcur)//简单的遍历比较判断
	{
		if (pcur->data == x)
		{//找到第一个符合条件的结点直接返回该结点即可
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;//遍历链表未找到则返回空指针
}

指定位置插入删除:
需要注意,什么是指定位置呢?它又是如何表示的呢?由于链表不同于顺序表,可以通过下标找到指定位置的数据,因此这个指定位置其实是某个结点——一整个结点,这里可以配合上面的查找接口完成整个操作逻辑。在插入或删除指定位置操作时,我们需要注意保证链表不会断开,仔细斟酌前、后结点与指定结点的next。

//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{//在指定位置插入亦需要保证pphead,链表,以及位置有效,不为空
	assert(pphead && *pphead && pos);
	SLTNode* newnode = SLBuyNode(x);
	//结点在指定位置前的插入有两种情况
	if (*pphead == pos)
	{//头节点为指定位置
		newnode->next = *pphead;//新结点的next指向头节点
		*pphead = newnode;//头节点置为新结点
	}
	else
	{//指定位置不为头节点
		SLTNode* prev = *pphead;//遍历找到指定位置结点的前一结点
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		newnode->next = pos;//新节点的next指向指定结点
		prev->next = newnode;//指定结点前一结点的next指向新结点
	}
}
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{//修改结点结构体内的信息,传参结构体类型指针变量即可,同时需要确保pos的有效性
	assert(pos);
	SLTNode* newnode = SLBuyNode(x);
	newnode->next = pos->next;//新结点的next指向pos的下一结点
	pos->next = newnode;//pos的next指向新结点
}
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{//删除整个结点需要确保结点的地址pphead有效,链表不为空以及pos有效
	assert(pphead && *pphead && pos);
	//指定结点的删除有两种情况
	if (*pphead == pos)
	{//指定结点为头节点
		*pphead = (*pphead)->next;//新头节点为原头节点的下一结点
		free(pos);//释放原头节点
	}
	else
	{//指定结点不为头结点
		SLTNode* prev = *pphead;//找到指定结点前一结点
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = prev->next->next;//指定结点前一结点的next指向pos后一结点
		free(pos);//释放指定结点
	}
}
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	SLTNode* del = pos->next;//保存需要删除的结点
	pos->next = del->next;//pos的next指向删除结点的下一结点
	free(del);
}

销毁链表:

void SListDesTroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* pcur = *pphead;
	while (pcur)//遍历链表
	{
		SLTNode* del = pcur;//先保存删除结点
		pcur = pcur->next;//再找到下一结点
		free(del);//释放删除结点
	}
	*pphead = NULL;//链表置为空
}

3.3.2 带头双向循环链表

带头双向循环链表的“头”即哨兵位,也称头节点,但这里的头节点就没有实际意义了,仅仅作为一个标记,也不会存储数据信息,只会存储前一结点与后一结点的地址信息。

//带头双向循环链表 Linked List 增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
   LTDataType data;
   struct ListNode* next;
   struct ListNode* prev;
}LTNode;

//申请结点
LTNode* LTBuyNode(LTDataType x);
//双向循环链表的初始化
LTNode* LTInit();
// 双向链表销毁
void LTDestory(LTNode* phead);
// 双向链表打印
void LTPrint(LTNode* phead);
// 头部插入删除/尾部插入删除
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);
// 双向链表查找
LTNode* LTFind(LTNode* phead, LTDataType x);
// 双向链表在pos的前面进行插入
void LTInsert(LTNode* pos, LTDataType x);
// 双向链表删除pos位置的结点
void LTErase(LTNode* pos);

需要用到的头文件:与单链表相同。

双向循环链表的初始化与销毁:

//申请结点
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (!node)
	{
		perror("malloc fail!\n");
		exit(1);
	}
	node->data = x;
	node->next = node->prev = node;//只有一个结点的双向循环链表,前后结点都是自己
	return node;
}
//双向循环链表的初始化
LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);//哨兵位取任意值即可
	return phead;//返回哨兵位结点
}
// 双向链表销毁
void LTDestory(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;//跳过哨兵位找下一有效结点
	//销毁结点
	while (pcur != phead)//遍历链表
	{//先保存下一结点再销毁上一结点
		LTNode* ptmp = pcur->next;
		free(pcur);
		pcur = ptmp;
	}
	//销毁哨兵位
	free(phead);
	pcur = phead = NULL;
}

双向链表打印:

void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

头部插入删除/尾部插入删除:

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	//先修改新尾结点的指针域信息
	newnode->next = phead;//尾插next指向哨兵位
	newnode->prev = phead->prev;//尾插prev指向原尾结点
	//再修改哨兵位与原哨兵位前结点
	phead->prev->next = newnode;//原哨兵位前结点的next指向新尾结点
	phead->prev = newnode;//哨兵位的prev指向新尾结点
}
//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead && phead->next != phead);//确保双向循环链表存在有效结点
	LTNode* del = phead->prev;//先保存尾结点
	//修改尾结点前后结点的指向
	del->prev->next = phead;//尾结点前一结点变为新尾结点,next指向哨兵位
	phead->prev = del->prev;//哨兵位的prev指向新尾结点
	free(del);//删除原尾结点
	del = NULL;
}
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	//先修改新结点的指针域信息
	newnode->prev = phead;//头插prev指向哨兵位
	newnode->next = phead->next;//头插next指向哨兵位下一结点
	//再修改哨兵位与原哨兵位下一结点指向
	phead->next->prev = newnode;//原哨兵位下一结点的prev指向新结点
	phead->next = newnode;//哨兵位的next指向新结点
}
//头删
void LTPopFront(LTNode* phead)
{
	assert(phead && phead->next != phead);
	LTNode* del = phead->next;
	del->next->prev = phead;
	phead->next = del->next;
	free(del);
	del = NULL;
}

双向链表查找:

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)//遍历双向链表
	{
		if (pcur->data == x)
		{//找到第一个符合条件的结点直接返回结点
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;//未找到数据返回空
}

双向链表指定位置插入删除:

// 双向链表在pos的前面进行插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos && pos->prev);
	LTNode* newnode = LTBuyNode(x);
	newnode->prev = pos->prev;
	newnode->next = pos;
	pos->prev->next = newnode;
	pos->prev = newnode;
}
// 双向链表删除pos位置的结点
void LTErase(LTNode* pos)
{
	assert(pos && pos->prev && pos->next);
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

四、顺序表和链表的区别

在这里插入图片描述
顺序表的优点有:连续存储,访问方便,高效存储,缓存利用率高等。
链表的优点在于:方便插入删除操作,没有容量限制,动态申请创建结点。
注:缓存利用率参考存储体系结构以及局部原理性。

这里附上一个介绍缓存相关知识的佳文——与程序员相关的CPU缓存知识。
在这里插入图片描述


本篇到此结束,下一篇将会带来顺序表以及链表相关的OJ题分享,用于学习与巩固,做更深一步的提升。

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

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

相关文章

初级练习[3]:Hive SQL子查询应用

目录 环境准备看如下链接 子查询 查询所有课程成绩均小于60分的学生的学号、姓名 查询没有学全所有课的学生的学号、姓名 解释: 没有学全所有课,也就是该学生选修的课程数 < 总的课程数。 查询出只选修了三门课程的全部学生的学号和姓名 环境准备看如下链接 环境准备h…

【蓝桥杯省赛真题53】Scratch游乐场 蓝桥杯scratch图形化编程 中小学生蓝桥杯省赛真题讲解

目录 scratch游乐场 一、题目要求 编程实现 二、案例分析 1、角色分析 2、背景分析 3、前期准备 三、解题思路 1、思路分析 2、详细过程 四、程序编写 五、考点分析 六、推荐资料 1、入门基础 2、蓝桥杯比赛 3、考级资料 4、视频课程 5、python资料 scratch游…

Upstage 将发布新一代 LLM “Solar Pro “预览版

Solar Pro 是最智能的 LLM&#xff0c;经过优化可在单 GPU 上运行&#xff0c;性能超过微软、Meta 和谷歌等科技巨头的模型。 加州圣何塞2024年9月11日电 /美通社/ – Upstage 今天宣布发布其下一代大型语言模型 (LLM) Solar Pro 的预览版。加州圣何塞2024年9月11日电 /美通社…

ElementUI大坑Notification修改样式

默认<style lang"scss" scoped>局部样式&#xff0c;尝试用deep透传也无效 实践成功方法&#xff1a;单独写一个style <style> .el-notification{position: absolute !important;top: 40% !important;left: 40% !important; } </style> 也支持自…

无头服务(Headless Service)

无头服务 ​ 无头服务&#xff08;Headless Service&#xff09;是 Kubernetes 中的一种特殊服务类型&#xff0c;主要用于提供稳定的网络标识&#xff0c;而不需要通过负载均衡来分配流量。它允许直接访问 Pod&#xff0c;而不经过集群内的负载均衡器&#xff0c;并且通常用于…

C# net跨平台上位机开发(avalonia)附demo源码

介绍: 目前微软还没有跨平台桌面程序的开发框架。github上有一个团队开始自行研发跨平台桌面框架,其中一款叫avalonia。avalonia 采用 Xaml+C#,类似于wpf,可运行于.netframework,.netcore,是相对比较成熟的.net跨平台桌面应用技术。下面介绍如何创建 avalonia项目;如何在…

mysql_getshell的几种方法

mysql_getshell 一、mysql的--os-shell 利用原理 --os-shell就是使用udf提权获取WebShell。也是通过into oufile向服务器写入两个文件&#xff0c;一个可以直接执行系统命令&#xff0c;一个进行上传文件。此为sqlmap的一个命令&#xff0c;利用这条命令的先决条件&#xff1a;…

PMP--一模--解题--41-50

文章目录 14.敏捷--方法--回顾--回顾是最重要的一个实践&#xff0c;原因是它能让团队学习、改进和调整其过程。41、 [单选] 新项目中的所有团队成员都希望通过尽快交付价值来获得客户的信任。项目经理了解到一个资源已经在其他项目中与发起人一起工作。某资源似乎在使用个人影…

ICM20948 DMP代码详解(20)

接前一篇文章&#xff1a;ICM20948 DMP代码详解&#xff08;19&#xff09; 本回继续对inv_icm20948_read_mems_reg函数的其余内容进行解析。为了便于理解和回顾&#xff0c;再次贴出inv_icm20948_read_mems_reg函数源码&#xff0c;在EMD-Core\sources\Invn\Devices\Drivers\I…

在docker中安装 zendesk/maxwell 失败,解决方法

文章目录 1、拉取镜像失败2、一键设置镜像加速&#xff1a;修改文件 /etc/docker/daemon.json&#xff08;如果不存在则创建&#xff09;3、保存好之后 执行以下两条命令 1、拉取镜像失败 [rootlocalhost docker]# docker pull zendesk/maxwell Using default tag: latest Err…

有奖直播 | onsemi IPM 助力汽车电气革命及电子化时代冷热管理

在全球汽车行业向电气化和智能化转型的浪潮中&#xff0c;功率管理技术的创新和应用成为了关键驱动力。作为全球领先的半导体解决方案供应商&#xff0c;onsemi&#xff08;安森美&#xff09;致力于通过其先进的智能功率模块&#xff08;IPM&#xff09;技术&#xff0c;推动汽…

Java许可政策再变,Oracle JDK 17 免费期将结束!

原文地址&#xff1a;https://www.infoworld.com/article/3478122/get-ready-for-more-java-licensing-changes.html Oracle JDK 17的许可协议将于9月变更回Oracle Technology Network License Agreement&#xff0c;这将迫使用户重新评估他们的使用策略。 有句老话说&#xf…

【MyBatis---快速学习和复习】

学习视频&#xff08;强推&#xff09;&#xff1a;【MyBatis视频零基础入门到进阶&#xff0c;MyBatis全套视频教程源码级深入详解】 https://www.bilibili.com/video/BV1JP4y1Z73S/?p134&share_sourcecopy_web&vd_source4d877b7310d01a59f27364f1080e3382 MyBatis中…

【算法】-单调队列

目录 什么是单调队列 区域内最大值 区域内最小值 什么是单调队列 说到单调队列&#xff0c;其实就是一个双端队列&#xff0c; 顾名思义&#xff0c;单调队列的重点分为「单调」和「队列」。「单调」指的是元素的「规律」——递增&#xff08;或递减&#xff09;。「队列」指…

Python精选200Tips:126-130

Those who know are not as good as those who love, and those who love are not as good as those who enjoy 126 PyInstaller - 将 Python 程序打包成独立可执行文件的工具示例:图像变为灰度图像项目结构代码文件打包步骤运行可执行文件127 PyYAML - YAML 解析和生成工具示…

【机器学习(六)】分类和回归任务-LightGBM算法-Sentosa_DSML社区版

文章目录 一、算法概念二、算法原理&#xff08;一&#xff09;Histogram&#xff08;二&#xff09;GOSS1、信息增益2、近似误差 &#xff08;三&#xff09;EFB 三、算法优缺点&#xff08;一&#xff09;优点&#xff08;二&#xff09;缺点 四、LightGBM分类任务实现对比&a…

计算机毕业设计 财会信息管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

【机器学习(四)】分类和回归任务-梯度提升决策树(GBDT)-Sentosa_DSML社区版

文章目录 一、算法概念一、算法原理&#xff08;一&#xff09; GBDT 及负梯度拟合原理&#xff08;二&#xff09; GBDT 回归和分类1、GBDT回归1、GBDT分类二元分类多元分类 &#xff08;三&#xff09;损失函数1、回归问题的损失函数2. 分类问题的损失函数&#xff1a; 三、G…

ThreeJS入门(002):学习思维路径

查看本专栏目录 - 本文是第 002篇入门文章 文章目录 如何使用这个思维导图 Three.js 学习思维导图可以帮助你系统地了解 Three.js 的各个组成部分及其关系。下面是一个简化的 Three.js 学习路径思维导图概述&#xff0c;它包含了学习 Three.js 的主要概念和组件。你可以根据这个…

CSP-J 之计算机基本结构

文章目录 前言计算机的宏观结构计算机的微观结构硬件部分软件部分 计算机硬件系统介绍主存储器与辅助存储器1. 主存储器&#xff08;Main Memory&#xff09;2. 辅助存储器&#xff08;Secondary Storage&#xff09;Cache&#xff08;缓存&#xff09;总线&#xff08;Bus&…