数据结构修炼——顺序表和链表的区别与联系
目录
- 一、线性表
- 二、顺序表
- 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 一些思考以及顺序表的缺点
缺点:
- 中间/头部的插入删除操作不方便,需要遍历顺序表,时间复杂度为O(N)。
- 增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗。
- 增容一般是呈二倍的增长,无法避免会存在一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间,而且这种浪费现象有可能随着扩容次数的增加越来越严重。
思考:如何解决以上问题呢?接下来我们进入链表的学习或许能找到答案。
三、链表
3.1 概念及结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

从上图可以看出:
- 链式结构在逻辑上是连续的,但物理存储上并不一定连续。
- 链表结点一般都是从堆上申请的空间。
- 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。
- 链表一般分为数据域与指针域两个部分,数据域存储了数据信息,指针域存储了相关结点的地址信息。
3.2 链表的分类
链表分为:带头 / 不带头,循环 / 不循环,单向 / 双向。可以分别构成8种链表结构。
但实际中最常用的还是 无头单向不循环 / 带头双向循环 链表。

- 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等。另外这种结构在笔试面试中出现很多。
- 带头双向循环链表:结构最复杂,一般用于单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现其带来了很多优势,实现反而简单了。
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题分享,用于学习与巩固,做更深一步的提升。
相关文章:
数据结构修炼——顺序表和链表的区别与联系
目录 一、线性表二、顺序表2.1 概念及结构2.2 接口实现2.3 一些思考以及顺序表的缺点 三、链表3.1 概念及结构3.2 链表的分类3.3 链表的实现3.3.1 无头单向非循环链表3.3.2 带头双向循环链表 四、顺序表和链表的区别 一、线性表 线性表(linear list)是n…...
AD9854 为什么输出波形幅度受限??
🏆本文收录于《全栈Bug调优(实战版)》专栏,主要记录项目实战过程中所遇到的Bug或因后果及提供真实有效的解决方案,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&am…...
在grafana上配置显示全部node资源信息概览
在grafana上配置显示全部node资源信息概览,便于巡检 1,注册grafana官网账号:Grafana dashboards | Grafana Labs 2、寻找可以展示所有node资源概览信息的dashboard,并下载支持prometheus数据源的dashboardÿ…...
MySQL —— 索引
索引的概念 MySQL的索引是⼀种数据结构,它可以帮助数据库高效地查询、更新数据表中的数据。索引通过 ⼀定的规则排列数据表中的记录,使得对表的查询可以通过对索引的搜索来加快速度。 MySQL索引类似于书籍的目录,通过指向数据行的位置&…...
(已解决)vscode如何选择python解释器
文章目录 前言解决方案 前言 有的时候可能有不同版本的编译器,以适用不同年份的项目。所以,怎么在vscode中换python解释器呢? 解决方案 对着要运行的python文件进行右键,比如我是要运行main文件,点击那个命令选项版…...
sql刷题常用函数
ROW_NUMBER() ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) 是一个窗口函数,用于生成每个分组内的唯一行号。这个函数非常适合在分组数据中进行排序,并为每一行分配一个序号。下面是对你的具体示例的详细解释: ROW_NUMBER() OVER (…...
Golang | Leetcode Golang题解之第417题太平洋大西洋水流问题
题目: 题解: type pair struct{ x, y int } var dirs []pair{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}func pacificAtlantic(heights [][]int) (ans [][]int) {m, n : len(heights), len(heights[0])pacific : make([][]bool, m)atlantic : make([][]bool, …...
Acwing Hash表
哈希表的作用:把一个比较大的空间,通过一个函数映射到一个比较小的空间 一般做哈希运算时,取一个质数作为模,会使得冲突的概率降低。 哈希表的冲突解决方法: 拉链法开放寻址法 下面详细介绍这两种方法的原理及其实现…...
大健康裂变分销小程序开发
大健康裂变分销小程序的开发是一个涉及技术、市场策略、用户体验和合规性等多个方面的综合项目。这类小程序旨在通过分销机制促进大健康产品的销售和品牌推广,同时利用社交网络的裂变效应扩大市场影响力。以下是大健康裂变分销小程序开发的主要步骤和考虑因素&#…...
js取出一个对象中指定的字段(封装公共方法)
需求:在一个对象里面挑选出所需要的一个或多个字段 例子:在{ a: 1, b: 2, c: 3, d: 4 }里面挑选出b和d字段 封装公共方法 const pick (obj, keys) > {return Object.keys(obj).filter(key > keys.includes(key)).reduce((result, key) > {if …...
【黑马点评】已解决java.lang.NullPointerException异常
Redis学习Day3——黑马点评项目工程开发-CSDN博客 问题发现及描述 在黑马点评项目中,进行到使用Redis提供的Stream消息队列优化异步秒杀问题时,我在进行jmeter测试时遇到了重大的错误 发现无论怎么测试,一定会进入到catch中,又由…...
计算机专业的就业方向
计算机专业的就业方向 亲爱的新生们,欢迎你们踏上计算机科学的旅程!作为一名计算机专业的学生,你们即将进入一个充满无限可能的领域。今天,我将为大家介绍计算机专业的一些主要就业方向,帮助你们了解未来的职业选择。…...
VSCode C++ Tasks.json中的变量
前言 上文介绍了在VSCode中创建C项目和编译多文件的情况。本文将介绍Tasks.json中一些变量的含义; 内容 tasks.json文件 下文参考VSCode文档:Visual Studio Code 变量参考 预定义标量 ${userHome} - 用户主文件夹的路径${workspaceFolder} - 在 VS Co…...
第一次安装Pytorch
1、新版本的Anaconda内置的python版本是3.12, 目前 Windows 上的 PyTorch 仅支持 Python 3.8-3.11;不支持 Python 2.x。 1、创建运行环境 在不创建虚拟环境的情况下,不建议使用最新的Python和Anaconda。 在几次失败后,我使用的是Anaconda3-2…...
Python数据分析-Steam 收入排名前 1500 的游戏
一、研究背景 随着全球数字化进程的加速,电子游戏产业已成为全球娱乐产业的重要组成部分,吸引了越来越多的资本与消费者关注。特别是基于互联网的游戏平台,如Steam,已成为全球范围内发行和销售游戏的重要渠道。Steam平台不仅为玩…...
Android14请求动态申请存储权限
Android14请求动态申请存储权限 Android14和Android15存储权限有增加多了选择部分,还是全部。一个小小的存储权限真的被它玩出了花来。本来Android13就将存储权限进行了3个细分,是图片,音频还是视频文件。 步骤一:AndroidManife…...
Doris:数据库建表最佳实践
目录 一、表模型推荐归约 二、字段推荐归约 三、建表推荐归约 四、建表强制归约 五、最佳实践 Doris 数据表模型上目前分为三类:DUPLICATE KEY, UNIQUE KEY, AGGREGATE KEY。因为数据模型在建表时就已经确定,且无法修改。所以,选择一个合…...
Parallels Desktop 20(Mac虚拟机) v20.0.0 for Mac 最新破解版(支持M系列)
Parallels Desktop 20 for Mac 正式发布,完全支持 macOS Sequoia 和 Windows 11 24H2,并且在企业版中引入了全新的管理门户。 据介绍,新版本针对 Windows、macOS 和 Linux 虚拟机进行了大量更新,最大的亮点是全新推出的 Parallels…...
【已解决】华为AR100-S路由器 恢复出厂后,找不到5G wifi的设置
前两帖讨论了华为AR100-S路由器: 一是用电脑浏览器访问web管理界面报错的解决,详情点这里! https://blog.csdn.net/weixin_62598385/article/details/142215136 再就是如何回复出厂,也即如何复位, 详情点这里ÿ…...
【MongoDB】--MongoDB批量操作
目录 一、批量更新 一、批量更新 /*** 批量更新的操作* return*/public int batchUpdate(){List<StudentDo> list new ArrayList<>(); //要修改的一批数据List<Pair<Query, Update>> updateList new ArrayList<>(list.size());BulkOperations …...
黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 使用React Native…...
Java - Mysql数据类型对应
Mysql数据类型java数据类型备注整型INT/INTEGERint / java.lang.Integer–BIGINTlong/java.lang.Long–––浮点型FLOATfloat/java.lang.FloatDOUBLEdouble/java.lang.Double–DECIMAL/NUMERICjava.math.BigDecimal字符串型CHARjava.lang.String固定长度字符串VARCHARjava.lang…...
【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】
1. BluetoothProperties介绍 libsysprop/srcs/android/sysprop/BluetoothProperties.sysprop BluetoothProperties.sysprop 是 Android AOSP 中的一种 系统属性定义文件(System Property Definition File),用于声明和管理 Bluetooth 模块相…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
Python+ZeroMQ实战:智能车辆状态监控与模拟模式自动切换
目录 关键点 技术实现1 技术实现2 摘要: 本文将介绍如何利用Python和ZeroMQ消息队列构建一个智能车辆状态监控系统。系统能够根据时间策略自动切换驾驶模式(自动驾驶、人工驾驶、远程驾驶、主动安全),并通过实时消息推送更新车…...
【Linux手册】探秘系统世界:从用户交互到硬件底层的全链路工作之旅
目录 前言 操作系统与驱动程序 是什么,为什么 怎么做 system call 用户操作接口 总结 前言 日常生活中,我们在使用电子设备时,我们所输入执行的每一条指令最终大多都会作用到硬件上,比如下载一款软件最终会下载到硬盘上&am…...
Linux中《基础IO》详细介绍
目录 理解"文件"狭义理解广义理解文件操作的归类认知系统角度文件类别 回顾C文件接口打开文件写文件读文件稍作修改,实现简单cat命令 输出信息到显示器,你有哪些方法stdin & stdout & stderr打开文件的方式 系统⽂件I/O⼀种传递标志位…...
python可视化:俄乌战争时间线关键节点与深层原因
俄乌战争时间线可视化分析:关键节点与深层原因 俄乌战争是21世纪欧洲最具影响力的地缘政治冲突之一,自2022年2月爆发以来已持续超过3年。 本文将通过Python可视化工具,系统分析这场战争的时间线、关键节点及其背后的深层原因,全面…...
Linux入门(十五)安装java安装tomcat安装dotnet安装mysql
安装java yum install java-17-openjdk-devel查找安装地址 update-alternatives --config java设置环境变量 vi /etc/profile #在文档后面追加 JAVA_HOME"通过查找安装地址命令显示的路径" #注意一定要加$PATH不然路径就只剩下新加的路径了,系统很多命…...
