2线性表之链表

embedded/2025/3/11 8:38:13/

1.链表

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

关于顺序表的不足:

  1. 扩容有性能消耗且有可能存在空间浪费。
    扩容时,如果扩小了,大量插入数据时,频繁扩容,性能消耗较大;如果扩大了,又会存在空间浪费。
    例如当前容量为 100,满了以后增容到 200,我们再继续插入了 5 个数据,后面没有数据插入了,那么就浪费了 95 个数据空间。
  2. 头部或中间插入数据时,需要挪动数据,降低效率

链表优点

  1. 按需申请内存, 不存在空间浪费
  2. 任意位置 O(1)时间插入删除数据

链表缺点

  1. 不支持下标的随机访问

总结:顺序表和链表相辅相成,使用要看具体应用场景

2.单链表 Singly Linked List

在这里插入图片描述

struct SListNode
{int data;struct SListNode* next;
};
void Test()
{struct SListNode* node1 = (struct SListNode*)malloc(sizeof(struct SListNode));struct SListNode* node2 = (struct SListNode*)malloc(sizeof(struct SListNode));struct SListNode* node3 = (struct SListNode*)malloc(sizeof(struct SListNode));struct SListNode* node4 = (struct SListNode*)malloc(sizeof(struct SListNode));printf("%p\n", node1);printf("%p\n", node2);printf("%p\n", node3);printf("%p\n", node4);
}
//node1 等其实只是结点地址

在这里插入图片描述

地址均不连续,堆上使用的空间地址由高到低

逻辑结构&物理结构

链表逻辑结构线性,但物理结构是非线性的。

pList(头结点)是指针变量,存的是第一个节点的地址

当我们看到链表的实现都有箭头指向下一个节点,但实际上是没有箭头的,只不过是把下一个节点的地址存到了当前节点的 next 的值

在这里插入图片描述

链表组合

  1. 单向双向

在这里插入图片描述

  1. 带头不带头

    在这里插入图片描述

  1. 循环非循环
    在这里插入图片描述

最多有 8 种组合

虽然有这么的组合,但实际中最常用的只有 2 种:
在这里插入图片描述

  1. 无头单向非循环链表结构简单,一般不会单独用来存数据。实际中更多是作为 其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在 笔试面试 中出现很多。

  2. 带头双向循环链表结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

3.单链表实现

定义

typedef int SListDataType;
typedef struct SListNode // Single Link List
{SListDataType data;struct SListNode *next; //存储下一个节点的地址
} SListNode;

创建节点

SListNode *CreateNewNode(SListDataType x)
{SListNode *newNode = (SListNode *)malloc(sizeof(SListNode));if (newNode == NULL){printf("malloc newNode fail\n");exit(-1);}else{newNode->data = x;newNode->next = NULL;}return newNode;
}

二级指针

在单链表的操作函数中使用二级指针(指向指针的指针),主要是为了能够在函数内部修改调用者传入的指针变量本身的值。下面详细解释在单链表操作中使用二级指针的原因。

指针作为函数参数的基本概念:

在 C 语言里,函数参数传递是值传递。当把一个指针作为参数传递给函数时,实际上传递的是该指针的副本,也就是一个新的指针变量,它和原指针指向同一个内存地址,但它们是两个不同的变量。这意味着在函数内部修改这个指针副本的值,并不会影响到原指针的值。如果想在函数内修改这个指针,需要传递指针的地址,也就是二级指针。

打印

void SListPrint(SListNode* pList)
{//不需要assert(plist)SListNode* cur = pList;while (cur != NULL){printf("%d->", cur->data); cur = cur->next;//cur->next里面存的就是下一个结点的地址}printf("NULL\n");
}

提问:这里需要用断言吗?为什么不需要?

记住: 一定不能为空的才需要用断言 ,此处既是链表为空,那我们就什么也不打印就好了啊,断言太暴力了。

提问:这里需要传二级指针吗?为什么?

可以传,但没必要,Print 只是打印数据,不会修改数据,因此不需要传二级指针。

查找

查找不需要修改,也就不用传址调用,也就不需要传二级指针。

但如果传了也没问题。

//单链表查找
SListNode *SListFind(SListNode *plist, SListDataType x)
{SListNode *cur = plist;// while(cur != NULL)while (cur){if (cur->data == x){return cur;//查找兼具修改的作用}cur = cur->next;}return NULL;
}

尾删

  1. 没有节点,无法删除,直接 return

  2. 一个节点,直接 free 掉并置空即可,但注意 需要对实参进行操作,所以需要传实参地址,也就是传二级指针
    注意:free 之后要置空,因为 free 掉的是指针指向的内容,但指针还是指向那块空间的,因此要把指针置空

  3. 多个节点

    多个节点时,删除尾节点需要把 prev-> next 置为 NULL 再把 tail free 掉。

    在这里插入图片描述

如果用多个节点的代码去针对单个节点的情况,会产生解引用空指针的问题。

如果无需处理单个节点的情况,那其实也是不用传二级指针的,多个节点在已经有头结点的地址情况下是可以完成对原链表的删除操作的,只不过无法删除头结点而已。

void SListPopBack(SListNode** ppList)
{assert(ppList);//*ppList 为空时,ppList不为空,ppList是sList的地址//1.没有节点,无法删除,直接returnif(*ppList == NULL){return;//温柔检查}//2.单个节点else if((*ppList)->next == NULL){free(*ppList);*ppList = NULL;}//3.多个节点else{SListNode* prev = NULL;SListNode* tail = *ppList;while (tail->next != NULL){prev = tail;tail = tail->next;}free(tail);tail = NULL;//这里的tail是局部变量,置空了也没用,不过置了是个好习惯prev->next = NULL;//尾删时要将最后一个结点的上一个结点的next置为NULL才行}
}

传引用:

void SListPopBack(SListNode*& pList)
{//1.没有节点,无法删除,直接returnif(pList == NULL){return;}//2.单个节点else if((pList)->next == NULL){free(pList);pList = NULL;}//3.多个节点else{SListNode* prev = NULL;SListNode* tail = pList;while (tail->next != NULL){prev = tail;tail = tail->next;}free(tail);tail = NULL;prev->next = NULL;//尾删时要将最后一个结点的上一个结点的next置为NULL才行}
}

头删

  1. 没有节点:
    直接 return

  2. 单个节点:

    在这里插入图片描述

  1. 多个节点:
    在这里插入图片描述
先写多个节点的情况,再去比较单个节点能否适用,发现恰好可以匹配。
void SListPopFront(SListNode **ppList)
{assert(ppList);//ppList也有为空的可能// 1.没有节点if (*ppList == NULL){return;}// 2.单个节点// 3.多个节点//先写多个节点的情况,再去比较单个节点能否适用,发现恰好可以匹配。//保存plist->next,如果直接free plist就找不到后面的空间了SListNode *next = (*ppList)->next;free(*ppList);//这里只是释放了*ppList指向的那块空间内容,但*ppList还是指向那块空间的。*ppList = next;
}

传引用:

void SListPopFront(SListNode *&pList)
{// 1.没有节点if (pList == NULL){return;}// 2.单个节点// 3.多个节点//先写多个节点的情况,再去比较单个节点能否适用,发现恰好可以匹配。//保存plist->next,如果直接free plist就找不到后面的空间了SListNode *next = pList->next;free(pList); //这里只是释放了*ppList指向的那块空间内容,但*ppList还是指向那块空间的。pList = next;
}

头插

在这里插入图片描述

void SListPushFront(SListNode **pphead, SListDataType data)
{assert(pphead); // 如果链表为空,那就是*pphead为空SListNode *newNode = createNewNode(data);newNode->next = *pphead; // 新节点的next指向原头节点*pphead = newNode;		 // 更新头指针
}

如果头插时链表为空,也是可以的。
pList == NULL,newNode-> next 指向 NULL,然后再让 pList 指向 newNode,完美解决。

传引用的写法:

void SListPushFront(SListNode *&plist, SListDataType x)
{//即使传进来的是NULL也能解决SListNode *newNode = CreateNewNode(x);newNode->next = plist; //*pplist 其实就是 plistplist = newNode;
}

尾插

  1. 链表本身为空,直接插入就好。

  2. 链表不为空,遍历找到尾。

    在这里插入图片描述

  1. 注意需要用二级指针,因为在判断空链表的情况时,需要对实参 sList 进行操作,才需要传址调用。
    *ppList 其实就是 pList
void SListPushBack(SListNode **ppList, SListDataType x)
{//同样不需要断言空,因为本来就有可能传空链表SListNode* newNode = CreateNewNode(x);//1.空链表if(*ppList == NULL){*ppList = newNode;//也就是把newNode的地址覆盖掉sList原来的NULL地址//要修改sList必须传址调用}//2.正常链表,去找尾else{SListNode* tail = *ppList;//不能直接修改plist,plist一改就找不到链表while (tail->next != NULL){tail = tail->next;}//出来时tail->next 指向的是NULLtail->next = newNode;}
}

带上头节点可以不用传址调用, 因为不需要修改 plist

如果不想用二级指针,也可以传引用。

void SListPushBack(SListNode *&pList, SListDataType x)
{//同样不需要断言空,因为本来就有可能传空链表SListNode* newNode = CreateNewNode(x);//1.空链表if(pList == NULL){pList = newNode;//也就是把newNode的地址覆盖掉pList原来的NULL地址//传进来空链表,要修改plist必须传址调用}//2.正常链表,去找尾else{SListNode* tail = pList;//不能直接修改plist,plist一改就找不到链表while (tail->next != NULL){tail = tail->next;}//出来时tail->next 指向的是NULLtail->next = newNode;}
}

pos 后插入

2 种插入方式:

在这里插入图片描述

注意要操作顺序,如果先让 pos-> next 指向了 newNode 之后就找不到 pos 的下一个了。

第二种:

将 pos 的下一个临时保存起来就行,这样顺序就没所谓了。

//在pos后面插入
void SListInserAfter(SListNode *pos, SListDataType x)
{assert(pos);SListNode *newNode = CreateNewNode(x);//注意顺序不要反了newNode->next = pos->next;pos->next = newNode;
}
//或者临时保存 pos->next
//在pos后面插入
void SListInserAfter(SListNode *pos, SListDataType x)
{assert(pos);SListNode *newNode = CreateNewNode(x);SListNode* next = pos->next;//这样就无需关心顺序问题了pos->next = newNode;newNode->next = next;
}

pos 前插入

除了要让 newNode 指向 pos,还需 pos 之前的节点指向 newNode ,因此需要找到 pos 的前一个位置,只能从头开始遍历,非常麻烦且不实用。

  1. 多个节点的情况:
    在这里插入图片描述
  2. 如果 pos 是第一个节点呢?
    如果还让 prev 指向 newNode,那就发生了空指针解引用的问题。可以直接用 if 过滤掉这种情况。
    pos 是第一个节点的,其实就相当于是头插,但是头插的话,需要改变实参的 sList,要让传进来的 pList 指向 newNode,因此还需要传二级指针或者传引用。
    在这里插入图片描述
  3. pos 是最后一个节点呢?
    和多个节点的情况相同
void SListInserBefore(SListNode** ppList, SListNode* pos, SListDataType x)
{assert(pos);assert(ppList);SListNode* newNode = CreateNewNode(x);if(*ppList == pos)//相当于头插{newNode->next = pos;*ppList = newNode;}else{SListNode* prev = NULL;SListNode* cur = *ppList;while (cur != pos){prev = cur;cur = cur->next;}prev->next = newNode;newNode->next = cur;}
}

传引用的写法:

void SListInserBefore(SListNode *&pList, SListNode *pos, SListDataType x)
{assert(pos);SListNode *newNode = CreateNewNode(x);if (pList == pos) //相当于头插{newNode->next = pos;pList = newNode;}else{SListNode *prev = NULL;SListNode *cur = pList;while (cur != pos){prev = cur;cur = cur->next;}prev->next = newNode;newNode->next = cur;}
}

提问:

在一个无头(不告诉头指针)单链表的某一个节点前面插入一个值 x, 怎么插?

这里没告诉头,就不能从头开始遍历去找 pos 的前一个了。
其实我们可以先后插,然后交换 data

在这里插入图片描述

pos 后擦除

  1. 只有一个节点:
    没有可删除的,直接 return

  2. 多个节点
    在这里插入图片描述

    先记录 pos 的下一个节点,如何让 pos 指向它的下一个节点的下一个节点,再 free 之前记录的 pos 的下一个节点并置空。

  3. 后一个为空时,同样适用。

void SListEraseAfter(SListNode *pos)
{assert(pos);//只有一个节点的情况if (pos->next == NULL){return;}else{SListNode *next = pos->next;pos->next = next->next;free(next);next = NULL;}
}

pos 擦除

  1. 多个节点,需要找 pos 位置的前一个节点 prev,然后 free pos,让 prev 指向 pos 后面的那个
    在这里插入图片描述

  2. pos 指向的是第一个节点,其实就相当于头删,需要改变实参,因此传二级指针或传引用。需要先保存 pList 的下一个节点,然后 free pos,再让 pList 指向 next

    在这里插入图片描述

void SListEraseCur(SListNode** ppList, SListNode* pos)
{//pos指向第一个节点,相当于头删if(pos == *ppList){SListNode* next = (*ppList)->next;free(*ppList);*ppList = next;}else{SListNode* prev = NULL;SListNode* cur = *ppList;while (cur != pos){prev = cur;cur = cur->next;}//出来时cur指向的pos,prev指向pos前一个prev->next = cur->next;free(cur);cur = NULL;}
}

传引用:

void SListEraseCur(SListNode *&pList, SListNode *pos)
{// pos指向第一个节点,相当于头删if (pos == pList){SListNode *next = pList->next;free(pList);pList = next;}else{SListNode *prev = NULL;SListNode *cur = pList;while (cur != pos){prev = cur;cur = cur->next;}//出来时cur指向的pos,prev指向pos前一个prev->next = cur->next;free(cur);cur = NULL;}
}

注意: 在某个位置插入删除,需要配合 Find 一起使用。

销毁

void SListDestroy(SListNode** ppList)
{assert(ppList);SListNode* cur = *ppList;while (cur){SListNode* next = cur->next;free(cur);cur = next;}*ppList = NULL;//结束之后还要让pList指向空
}

传引用:

void SListDestroy(SListNode *&pList)
{assert(pList);SListNode *cur = pList;while (cur){SListNode *next = cur->next;free(cur);cur = next;}pList = NULL; //结束之后还要让pList指向空
}

4.带头双向循环链表实现

定义

// 定义链表存储的数据类型,方便后续修改
typedef int DCListDataType;// 定义链表节点结构体
typedef struct ListNode
{DCListDataType data;struct ListNode *prev;struct ListNode *next;
} ListNode;// 定义链表结构体,包含头节点指针和链表大小
typedef struct DoublyCircularList
{ListNode *head;int size;
} DoublyCircularList;

创建节点

// 创建新节点
ListNode *DCListCreateNode(DCListDataType data)
{ListNode *newNode = (ListNode *)malloc(sizeof(ListNode));if (newNode == NULL){fprintf(stderr, "Memory allocation failed\n");exit(EXIT_FAILURE);}newNode->data = data;newNode->prev = NULL;newNode->next = NULL;return newNode;
}

初始化

// 初始化链表
DoublyCircularList *DCListInit()
{DoublyCircularList *list = (DoublyCircularList *)malloc(sizeof(DoublyCircularList));if (list == NULL){fprintf(stderr, "Memory allocation failed\n");exit(EXIT_FAILURE);}list->head = DCListCreateNode(0); // 创建头节点list->head->prev = list->head;list->head->next = list->head;list->size = 0;return list;
}

头插尾插

// 在链表头部插入节点
void DCListPushFront(DoublyCircularList *list, DCListDataType data)
{assert(list);ListNode *newNode = DCListCreateNode(data);ListNode *next = list->head->next;// head -> newNode -> nextnewNode->next = next;newNode->prev = list->head;next->prev = newNode;list->head->next = newNode;list->size++;
}// 在链表尾部插入节点
void DCListPushBack(DoublyCircularList *list, DCListDataType data)
{assert(list);ListNode *newNode = DCListCreateNode(data);// head   tail   newNodeListNode *tail = list->head->prev;newNode->prev = tail;newNode->next = list->head;tail->next = newNode;list->head->prev = newNode;list->size++;
}

头删尾删

// 删除链表头部节点
void DCListPopFront(DoublyCircularList *list)
{assert(list);assert(list->size > 0);// head toDelete secondListNode *toDelete = list->head->next;ListNode *second = toDelete->next;list->head->next = second;second->prev = list->head;free(toDelete);list->size--;
}// 删除链表尾部节点
void DCListPopBack(DoublyCircularList *list)
{assert(list);assert(list->size > 0);// head tailPrev tailListNode *tail = list->head->prev;ListNode *tailPrev = tail->prev;tailPrev->next = list->head;list->head->prev = tailPrev;free(tail);list->size--;
}

插入删除

// 在指定位置之前插入节点
void DCListInsert(ListNode *pos, DCListDataType data)
{assert(pos);// prev newNode posListNode *newNode = DCListCreateNode(data);ListNode *prev = pos->prev;newNode->next = pos;newNode->prev = prev;prev->next = newNode;pos->prev = newNode;
}// 删除指定位置的节点
void DCListErase(ListNode *pos)
{assert(pos);// prev pos nextListNode *prev = pos->prev;ListNode *next = pos->next;prev->next = next;next->prev = prev;free(pos);
}

复用

// 尾插 头插 尾删 头删 都可以复用 Insert 和 Erase
// 头插
void DCListPushFront(DoublyCircularList *list, DCListDataType data)
{// 复用DCListInsert函数assert(list);DCListInsert(list->head->next, data);
}
// 尾插
void DCListPushBack(DoublyCircularList *list, DCListDataType data)
{// 复用DCListInsert函数assert(list);DCListInsert(list->head, data);
}
// 头删
void DCListPopFront(DoublyCircularList *list)
{// 复用DCListErase函数assert(list);DCListErase(list->head->next);
}
// 尾删
void DCListPopBack(DoublyCircularList *list)
{// 复用DCListErase函数assert(list);DCListErase(list->head->prev);
}

查找

// 查找指定值的节点
ListNode *DCListFind(DoublyCircularList *list, DCListDataType data)
{assert(list);ListNode *current = list->head->next;while (current != list->head){if (current->data == data){return current;}current = current->next;}return NULL;
}

其他

// 获取链表的大小
int DCListSize(DoublyCircularList *list)
{assert(list);return list->size;
}// 判断链表是否为空
int DCListEmpty(DoublyCircularList *list)
{if (list == NULL)return 1;return list->size == 0;
}// 打印链表元素
void DCListPrint(DoublyCircularList *list)
{assert(list);printf("head<=>");ListNode *current = list->head->next;while (current != list->head){printf("%d<=>", current->data);current = current->next;}printf("\n");
}

摧毁

// 销毁链表
void DCListDestroy(DoublyCircularList *list)
{assert(list);ListNode *current = list->head->next;ListNode *next;while (current != list->head){next = current->next;free(current);current = next;}free(list->head);free(list);
}

测试

#include "doubly_circular_list.h"// 测试函数:测试链表的初始化和插入操作
void testInitializationAndInsertion()
{DoublyCircularList *list = DCListInit();// 测试在头部插入元素DCListPushFront(list, 10);DCListPushFront(list, 20);printf("在头部插入元素后的链表: ");DCListPrint(list);// 测试在尾部插入元素DCListPushBack(list, 30);DCListPushBack(list, 40);printf("在尾部插入元素后的链表: ");DCListPrint(list);// 测试链表大小printf("当前链表大小: %d\n", DCListSize(list));DCListDestroy(list);
}// 测试函数:测试链表的删除操作
void testDeletion()
{DoublyCircularList *list = DCListInit();DCListPushBack(list, 1);DCListPushBack(list, 2);DCListPushBack(list, 3);DCListPushBack(list, 4);printf("原始链表: ");DCListPrint(list);// 测试删除头部元素DCListPopFront(list);printf("删除头部元素后的链表: ");DCListPrint(list);// 测试删除尾部元素DCListPopBack(list);printf("删除尾部元素后的链表: ");DCListPrint(list);// 测试删除指定位置元素ListNode *pos = DCListFind(list, 2);if (pos != NULL){DCListErase(pos);}printf("删除元素 2 后的链表: ");DCListPrint(list);DCListDestroy(list);
}// 测试函数:测试链表的查找操作
void testFinding()
{DoublyCircularList *list = DCListInit();DCListPushBack(list, 100);DCListPushBack(list, 200);DCListPushBack(list, 300);printf("当前链表: ");DCListPrint(list);ListNode *found = DCListFind(list, 200);if (found != NULL){printf("找到元素 200\n");}else{printf("未找到元素 200\n");}found = DCListFind(list, 400);if (found != NULL){printf("找到元素 400\n");}else{printf("未找到元素 400\n");}DCListDestroy(list);
}// 测试函数:测试在指定位置插入元素
void testInsertAtPosition()
{DoublyCircularList *list = DCListInit();DCListPushBack(list, 1);DCListPushBack(list, 3);printf("原始链表: ");DCListPrint(list);ListNode *pos = DCListFind(list, 3);if (pos != NULL){DCListInsert(pos, 2);}printf("在元素 3 前插入元素 2 后的链表: ");DCListPrint(list);DCListDestroy(list);
}// 测试函数:测试链表是否为空
void testEmpty()
{DoublyCircularList *list = DCListInit();if (DCListEmpty(list)){printf("链表初始为空\n");}else{printf("链表初始不为空,测试错误\n");}DCListPushBack(list, 1);if (DCListEmpty(list)){printf("插入元素后链表仍为空,测试错误\n");}else{printf("插入元素后链表不为空\n");}DCListDestroy(list);
}int main()
{testInitializationAndInsertion();printf("\n");testDeletion();printf("\n");testFinding();printf("\n");testInsertAtPosition();printf("\n");testEmpty();return 0;
}

测试结果:

(base) PS C:\Users\yzq0207\OneDrive\learn\programming\dataStructure>  & 'c:\Users\yzq0207\.vscode\extensions\ms-vscode.cpptools-1.24.2-win32-x64\debugAdapters\bin\WindowsDebugLauncher.exe' '--stdin=Microsoft-MIEngine-In-i5hnyga5.grr' '--stdout=Microsoft-MIEngine-Out-oxcfklgl.m5e' '--stderr=Microsoft-MIEngine-Error-z01squvk.zmt' '--pid=Microsoft-MIEngine-Pid-prir2imd.yj0' '--dbgExe=C:\Program Files\mingw64\bin\gdb.exe' '--interpreter=mi' 
在头部插入元素后的链表: head<=>20<=>10<=>
在尾部插入元素后的链表: head<=>20<=>10<=>30<=>40<=>
当前链表大小: 4原始链表: head<=>1<=>2<=>3<=>4<=>
删除头部元素后的链表: head<=>2<=>3<=>4<=>
删除尾部元素后的链表: head<=>2<=>3<=>
删除元素 2 后的链表: head<=>3<=>当前链表: head<=>100<=>200<=>300<=>
找到元素 200
未找到元素 400原始链表: head<=>1<=>3<=>
在元素 3 前插入元素 2 后的链表: head<=>1<=>2<=>3<=>链表初始为空
插入元素后链表不为空

5.源代码

顺序表

链表


http://www.ppmy.cn/embedded/171713.html

相关文章

Python大数据可视化:基于大数据技术的共享单车数据分析与辅助管理系统_flask+hadoop+spider

开发语言&#xff1a;Python框架&#xff1a;flaskPython版本&#xff1a;python3.7.7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 管理员登录 管理员功能界面 场地信息界面 单车信息界面 归还信息界面 共享单车界面 系…

Unity 通用UI界面逻辑总结

概述 在游戏开发中&#xff0c;常常会遇到一些通用的界面逻辑&#xff0c;它不论在什么类型的游戏中都会出现。为了避免重复造轮子&#xff0c;本文总结并提供了一些常用UI界面的实现逻辑。希望可以帮助大家快速开发通用界面模块&#xff0c;也可以在次基础上进行扩展修改&…

STM32如何精准控制步进电机?

在工业自动化、机器人控制等场合&#xff0c;步进电机以其高精度、开环控制的特性得到了广泛应用。而在嵌入式系统中&#xff0c;使用STM32进行步进电机的精确控制&#xff0c;已成为开发者的首选方案之一。 本文将从嵌入式开发者的角度&#xff0c;深入探讨如何基于STM32 MCU…

【JavaEE】SpringBoot快速上手,探秘 Spring Boot,搭建 Java 项目的智慧脚手架

1.Spring Boot介绍 在学习SpringBoot之前, 我们先来认识⼀下Spring &#xff0c;我们看下Spring官⽅的介绍 可以看到&#xff0c;Spring让Java程序更加快速, 简单和安全。 Spring对于速度、简单性和⽣产⼒的关注使其成为世界上最流⾏的Java框架。 Spring官⽅提供了很多开源的…

ERROR:L6002U 一种解决方案

1.问题描述 前面的文章也讲到过有一次出现&#xff0c;这次再看一下为什么 打开这个文件&#xff0c;以及它的头文件 源文件报错信息&#xff1a; 2、原因查找 这是由于不同的人上传不同的工程&#xff0c;拉取更新导致的&#xff0c;可能的原因是keil文件夹的组织方式。 很…

Spring Boot/Spring Cloud 整合 ELK(Elasticsearch、Logstash、Kibana)详细避坑指南

我们在开发中经常会写日志&#xff0c;所以需要有个日志可视化界面管理&#xff0c;使用ELK可以实现高效集中化的日志管理与分析&#xff0c;提升性能稳定性&#xff0c;满足安全合规要求&#xff0c;支持开发运维工作。 下述是我在搭建ELK时遇到的许许多多的坑&#xff0c;希望…

【FPGA】导航

注&#xff1a;有的地方可能会有点问题&#xff0c;忘记传的是哪个版本了 目录 I型 R型 J型 lw、sw 扩展 MIPS CPU设计-CSDN博客 【FPGA】参考设计文档-CSDN博客 I型 【计组】实验三 ORI指令设计实验-CSDN博客 R型 【计组】实验四 R型指令设计实验-CSDN博客 J型 【…

AI日报 - 2025年3月11日

AI日报 - 2025年3月11日 &#x1f31f; 今日概览&#xff08;60秒速览&#xff09; ▎&#x1f916; AGI突破 | OpenAI发布神秘预告引发AGI猜测 &#x1f52c; 中国AI代理「Manus」实现50项任务自动化引伦理争议 ▎&#x1f4bc; 商业动向 | ServiceNow拟30亿美元收购Movework…