数据结构初探:链表之双向链表篇

本文图皆为作者手绘,所有代码基于vs2022运行测试

系列文章目录

数据结构初探: 顺序表
数据结构初探:链表之单链表篇


文章目录

  • 系列文章目录
  • 前言
  • 一.双向链表的概念
  • 二.双向链表的存储结构
  • 三.准备工作
  • 四.双向链表的增删查改的实现
    • 1.List.h
    • 2.List.c
      • 2.1双向链表的可复用的节点创建函数
      • 2.2双向链表的初始化
      • 2.3双向链表的销毁
      • 2.4双向链表的打印
      • 2.5双向链表的是否为空判断
      • 2.6双向链表的节点查找
      • 2.7双向链表的尾插
      • 2.8双向链表的尾删
      • 2.9双向链表的头插
      • 2.10双向链表的头删
      • 2.11双向链表的中间插入
      • 2.12双向链表的中间删除
      • 2.13完整代码(使用了中间插入与中间删除的复用)
    • 3.test.c
  • 五.双向链表的优缺点
  • 六.顺序表与链表的比较
  • 链表总结


前言

在前文的单链表学习中,我们已经深刻的认识到单链表的方方面面,我们知道单链表适合用于其他数据结构的子结构,那双向链表又是怎么样的呢,现在我们继续来讲双向链表的强大之处.
双向带头循环链表:融合了双向指针、头节点以及循环特性,这三大特性相互交织,赋予了链表强大的功能。双向指针让我们能够在链表中自由往返,无论是向前追溯还是向后探寻,都能轻松实现;头节点的存在,为链表的操作提供了一个稳定的起点,简化了边界条件的处理;而循环特性,则让链表成为一个无始无终的环,使遍历操作更加流畅,也为一些复杂算法的实现提供了便利。

在这篇博客中,我将带你深入剖析双向带头循环链表的奥秘。从其精妙的结构设计,到具体的代码实现,再到实际应用场景的探讨,我会一步一步揭开它神秘的面纱,带你领略它在数据结构领域的独特魅力,让你掌握这一强大的数据结构工具。


以下所称双向链表,均为双向带头循环链表

一.双向链表的概念

定义

  • 双向带头循环链表是在双向链表的基础上,增加了头节点和循环特性。链表中存在一个特殊的头节点,它不存储实际数据,其前驱指针指向链表的最后一个节点,后继指针指向第一个真正存储数据的节点。而链表的最后一个节点的后继指针指向头节点,头节点的前驱指针也指向最后一个节点,形成一个循环的结构。

组成

  • 头节点:是链表的起始标志,不存储有效数据,主要用于标识链表的开始位置以及方便链表的操作,如插入、删除等。
  • 数据节点:用于存储实际的数据元素,每个数据节点都有一个指向前驱节点的指针和一个指向后继节点的指针,从而实现双向链接。

特点

  • 双向访问:与双向链表一样,能够从任意一个节点出发,方便地向前或向后遍历链表,访问前后节点的数据。
  • 循环特性:从链表中的任何一个节点出发,都可以通过指针遍历到链表中的其他所有节点,没有明显的起点和终点,在进行一些循环操作或需要反复遍历链表时非常方便。
  • 操作统一:由于有头节点的存在,在进行插入、删除等操作时,无需对头部和其他位置进行特殊处理,使操作更加统一和简单,减少了代码的复杂性和出错的可能性。

应用场景

  • 操作系统中的进程调度:可以将各个进程控制块用双向带头循环链表连接起来,方便按照一定的调度算法在各个进程之间进行切换,循环遍历查找合适的进程运行。
  • 循环缓冲区:在数据通信或数据处理中,用于实现循环缓冲区,数据可以在缓冲区中循环存储和读取,提高数据处理的效率和灵活性。
  • 实现一些复杂的数据结构:如哈希表中的链地址法解决冲突时,若采用双向带头循环链表作为链的结构,可以更方便地进行节点的插入、删除和查找操作。

以下为结构图:

在这里插入图片描述

二.双向链表的存储结构

由上文我们可以知道,双向链表的节点是在包含数据的同时,存在着prev指针和next指针,让我们来看看具体的逻辑代码结构:

typedef int LTDataType;//方便修改变量类型;typedef struct ListNode
{LTDataType data;//数据存储struct ListNode* next;//指向下一个节点;struct ListNode* prev;//指向前一个节点;
}LTNode;

为了图片的美观,我就采取了横向的节点构成,这样在后面更方便能体现循环的特点,下面是节点的图结构:
在这里插入图片描述
现在我们已经了解了其节点的构成,该开始学习增删查改函数啦!

三.准备工作

创建对应的三个文件夹:
1.List.h:用于存储创建链表的节点结构体和增删查改函数的函数声明,以及对应的库函数,方便生成程序时的链接;
2.List.c:用于函数的实现;
3.test.c:用于测试和修改;
ps:2和3,均要包含头文件1,即 #include"List.h".

四.双向链表的增删查改的实现

1.List.h

以下是必要的函数声明:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>typedef int LTDataType;typedef struct ListNode
{LTDataType data;struct ListNode* next;struct ListNode* prev;
}LTNode;
//可复用的节点创建函数
LTNode* CreatListNode(LTDataType x);
//初始化
LTNode* LTInit();
//销毁
void LTDestroy(LTNode* phead);
//打印
void LTPrint(LTNode* phead);
//判断是否为空
bool LTEmpty(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//头删
void LTPopFront(LTNode* phead);
//中间插入`
void LTInsert(LTNode* pos, LTDataType x);
//中间删除
void LTErase(LTNode* pos);

在了解了这些不同函数的声明后,就来到我们的老环节啦—函数的实现!

2.List.c

2.1双向链表的可复用的节点创建函数

//因为需要返回一个新节点的地址,所以使用我们的结构体指针
LTNode* CreatListNode(LTDataType x)
{//使用动态内存开辟,开辟出结构体空间LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));if (newNode == NULL)//是否开辟空间失败{perror("malloc fail");//返回错误行return NULL;//结束}newNode->data = x;//存储数据newNode->next = NULL;//初始化置空newNode->prev = NULL;//初始化置空return newNode;//返回新节点地址;
}

我个人是比较建议每次都将创建新节点的函数单开出来,方便后续复用;

2.2双向链表的初始化

LTNode* LTInit()
{//利用-1创建新节点,即创建一个不带数据的头节点;LTNode* phead = CreatListNode(-1);phead->next = phead;//next指向自己phead->prev = phead;//prev指向自己
//体现循环的特点return phead;//返回一个头节点;
}

逻辑简图如下:
在这里插入图片描述

2.3双向链表的销毁

//思考:为什么这里又是传的一级指针?
void LTDestroy(LTNode* phead)
{assert(phead);//断言,防止传空LTNode* cur = phead->next;//给cur赋值第一个节点的地址while (phead->next != phead)//找尾{//因为是循环,所以当节点的下一个节点为头节点,此时节点为尾节点//说明已经走完,就结束;LTNode* next = cur->next;//记住当前节点的下一个节点free(cur);//释放cur = next;//更新循环条件}free(phead);//释放头节点;//注意头节点是头节点,第一个节点是第一个节点哦,不要混淆了哦;//可参考开头的结构图哦
}

思考题:为什么这里又是传的一级指针?
**后面传的都是一级指针,因为以下原因:

访问链表节点

  • 函数接收指向链表头节点的一级指针,就可通过该指针访问链表的各个节点,进而操作节点的数据和指针域。例如要遍历链表打印所有节点数据,只需通过传入的头指针依次访问每个节点。

修改节点内容

  • 通过一级指针可直接修改节点的数据内容。如要将链表中某个节点的数据值从10改为20,可利用一级指针找到该节点并修改其数据域。

保持操作的简洁性

  • 对于一些不改变链表结构,仅对节点数据或指针进行简单操作的函数,传一级指针能使代码简洁清晰。如获取链表节点个数的函数,仅需顺着一级指针遍历链表计数即可。

与其他数据结构操作统一

  • 在很多数据结构操作中,常使用一级指针来表示数据结构的起始位置或当前操作位置。使用一级指针操作双向带头循环链表,能与其他数据结构的操作方式保持一致,便于开发者理解和使用。

不过,在某些需要修改链表头指针本身,如创建新链表或进行链表合并等操作时,可能需要传递二级指针或使用指针的引用,以确保能正确修改头指针的指向。那都是特殊情况啦,我们目前可以就考虑传一级指针啦!

2.4双向链表的打印

void LTPrint(LTNode* phead)
{assert(phead);printf("<=head<=>");//加入头的表示LTNode* cur = phead->next;while (cur != phead)//挨个遍历,直到回到头节点{printf("%d<=>", cur->data);cur = cur->next;//往下走}printf("\n");//防止多次调用,数据连在一起
}

2.5双向链表的是否为空判断

bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;//如果头节点指向自己,说明为空
}

判空逻辑如图:
在这里插入图片描述

2.6双向链表的节点查找

LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);
//简单的查找LTNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}

前面的几篇已经多次讲述了查找啦,这里就不过多赘述啦!

2.7双向链表的尾插

void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);//逻辑代码LTNode* newNode = CreatListNode(x);LTNode* tail = phead->prev;//找尾//双向链表的好处,非常简单的找尾tail->next = newNode;newNode->prev = tail;newNode->next = phead;//双向链接phead->prev = newNode;//使用代码复用;//LTInsert(phead, x);//后面我们会讲
}

结合图进行理解
在这里插入图片描述

2.8双向链表的尾删

void LTPopBack(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));//逻辑代码LTNode* tail = phead->prev;//找尾LTNode* tailPrev = tail->prev;//找尾的前一个tailPrev->next = phead;//将头和尾的前一个链接phead->prev = tailPrev;free(tail);//释放尾tail = NULL;//置空,完成尾删//使用代码复用;//LTErase(phead->prev);
}

在这里插入图片描述

2.9双向链表的头插

void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);//逻辑代码LTNode* newNode = CreatListNode(x);//双向链接方法一//newNode->next = phead->next;//思考一下这个是如何实现头插的//phead->next->prev = newNode;//phead->next = newNode;//newNode->prev = phead;//双向链接方法二LTNode* first = phead->next;//先记第一个节点phead->next = newNode;//将第一个节点更新为新节点newNode->prev = phead;//双向链接newNode->next = first;//新第一个节点与旧第一个节点双向链接;first->prev = newNode;//使用代码复用;//LTInsert(phead->next, x);
}

图示如下:
在这里插入图片描述

2.10双向链表的头删

void LTPopFront(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));//逻辑代码LTNode* first = phead->next;//记下第一个节点LTNode* firstNext = first->next;//记下第二个节点phead->next = firstNext;//头节点与第二个节点双向链接firstNext->prev = phead;free(first);//释放first = NULL;//置空,完成头删//使用代码复用;//LTErase(phead->next);
}

可以结合头插的图示分析一下,理顺逻辑;

2.11双向链表的中间插入

void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newNode = CreatListNode(x);//先将newNode的prev指针连接上pos的前一个,//此时有两个指针指向pos的前一个节点newNode->prev = pos->prev;pos->prev->next = newNode;//此时再断掉其与pos的链接,转而链接上newNodenewNode->next = pos;//再将newNode与pos双向链接;pos->prev = newNode;//LTNode* prev = pos->prev;//newNode->next = pos;//pos->prev = newNode;//newNode->prev = prev;//prev->next = newNode;
}

为什么说可以复用呢?
只要我传的位置是第一个节点的位置,是不是就是在第一个节点前插入,也就是头插,
只要我传的位置是头节点的位置,是不是就是在头节点前插入,也就是尾插,(因为是循环,所以头节点的前一个就是尾节点)

2.12双向链表的中间删除

//复用理由同上
void LTErase(LTNode* pos)
{assert(pos);LTNode* Prev = pos->prev;//找到pos前一个LTNode* Next = pos->next;//找到pos后一个Prev->next = Next;Next->prev = Prev;//两个双向链接free(pos);释放//pos = NULL;形参无法改变实参,pos置不置空都可以;//但是需要在test.c,即在使用中进行置空; 
}

2.13完整代码(使用了中间插入与中间删除的复用)

#define _CRT_SECURE_NO_WARNINGS 1#include"List.h"//双向链表的创建;
LTNode* CreatListNode(LTDataType x)
{LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));if (newNode == NULL){perror("malloc fail");return NULL;}newNode->data = x;newNode->next = NULL;newNode->prev = NULL;return newNode;
}//双向链表的初始化;
LTNode* LTInit()
{LTNode* phead = CreatListNode(-1);phead->next = phead;phead->prev = phead;return phead;
}//双向链表的销毁;
void LTDestroy(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;while (phead->next != phead){LTNode* next = cur->next;free(cur);cur = next;}free(phead);
}//双向链表的打印
void LTPrint(LTNode* phead)
{assert(phead);printf("<=head<=>");LTNode* cur = phead->next;while (cur != phead){printf("%d<=>", cur->data);cur = cur->next;}printf("\n");
}//双向链表的是否为空判断;
bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;
}//双向链表的节点查找;
LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}//双向链表的尾插;
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);//逻辑代码//LTNode* newNode = CreatListNode(x);//LTNode* tail = phead->prev;//tail->next = newNode;//newNode->prev = tail;//newNode->next = phead;//phead->prev = newNode;//使用代码复用;LTInsert(phead, x);
}//双向链表的尾删;
void LTPopBack(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));//逻辑代码//LTNode* tail = phead->prev;//LTNode* tailPrev = tail->prev;//tailPrev->next = phead;//phead->prev = tailPrev;//free(tail);//tail = NULL;//使用代码复用;LTErase(phead->prev);
}//双向链表的头插;
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);//逻辑代码//LTNode* newNode = CreatListNode(x);//newNode->next = phead->next;//phead->next->prev = newNode;//phead->next = newNode;//newNode->prev = phead;//LTNode* first = phead->next;//phead->next = newNode;//newNode->prev = phead;//newNode->next = first;//first->prev = newNode;//使用代码复用;LTInsert(phead->next, x);
}//双向链表的头删;
void LTPopFront(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));//逻辑代码//LTNode* first = phead->next;//LTNode* firstNext = first->next;//phead->next = firstNext;//firstNext->prev = phead;//free(first);//first = NULL;//使用代码复用;LTErase(phead->next);
}void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newNode = CreatListNode(x);newNode->prev = pos->prev;pos->prev->next = newNode;newNode->next = pos;pos->prev = newNode;//LTNode* prev = pos->prev;//newNode->next = pos;//pos->prev = newNode;//newNode->prev = prev;//prev->next = newNode;
}void LTErase(LTNode* pos)
{assert(pos);LTNode* Prev = pos->prev;LTNode* Next = pos->next;Prev->next = Next;Next->prev = Prev;free(pos);//pos = NULL;形参无法改变实参,pos置不置空都可以;
}

那么接下来我们来看看测试吧

3.test.c

直接上完整代码:

#define _CRT_SECURE_NO_WARNINGS 1#include"List.h"void TestList1()
{LTNode* plist = LTInit();LTPushBack(plist, 1);LTPushBack(plist, 2);LTPushBack(plist, 3);LTPushBack(plist, 4);LTPrint(plist);LTPopBack(plist);LTPopBack(plist);LTPrint(plist);LTPushFront(plist, 20);LTPushFront(plist, 40);LTPrint(plist);LTPopFront(plist);LTPopFront(plist);LTPrint(plist);LTPushFront(plist, 20);LTPushFront(plist, 40);LTNode* ret = LTFind(plist, 20);LTInsert(ret, 4000);LTPrint(plist);if (ret){LTErase(ret);ret = NULL;//自己来置空}LTPrint(plist);LTDestroy(plist);plist = NULL;//使用者来置空;}int main()
{TestList1();return 0;
}

五.双向链表的优缺点

优点

  • 遍历灵活:既可以正向遍历,也可以反向遍历,能方便地访问当前节点的前驱和后继节点,在处理需要双向访问数据的问题时效率较高,比如实现双向链表的迭代器、文本编辑器中的撤销和重做功能等。
  • 插入和删除高效:在已知节点位置的情况下,插入和删除操作的时间复杂度为O(1)。因为只需修改相关节点的指针,就能完成插入或删除,无需像顺序表那样移动大量元素,适用于频繁进行插入和删除操作的场景,如操作系统的进程调度、内存管理等。
  • 内存利用率高:节点按需动态分配内存,可根据实际需求灵活调整链表长度,不会像数组那样预先分配大量连续内存空间而可能导致浪费,在处理数据量不确定的情况时优势明显,如网络数据包的存储和处理。
  • 安全性好:有头节点作为链表的标识,且循环结构使链表形成一个闭环,在遍历或操作链表时,可有效避免出现空指针引用等错误,增强了程序的稳定性和可靠性。

缺点

  • 结构复杂:每个节点需要额外存储前驱和后继指针,相比单链表,增加了实现和理解的难度,在编写代码时,对指针的操作和维护更复杂,容易出现指针操作错误,导致程序出现难以排查的bug。
  • 空间开销大:除数据本身外,每个节点都要存储两个指针,对于大量数据存储,指针所占用的空间不容忽视,可能会降低内存的有效利用率,在内存资源紧张的环境中,可能会成为性能瓶颈。
  • 随机访问效率低:要访问第n个节点,需从头部或尾部开始逐个节点遍历,时间复杂度为O(n),远低于数组的随机访问效率O(1),不适用于需要频繁随机访问元素的场景,如数组下标访问很方便的矩阵运算等。

六.顺序表与链表的比较

如图:
在这里插入图片描述


链表总结

  • 链表作为基础且重要的数据结构,在程序设计中占据关键地位。本次博客对链表进行了深入剖析,从单链表出发,其通过节点间的指针连接实现数据存储,虽结构简单但在插入和删除操作上展现出O(1)的时间复杂度优势,无需像数组那样大量移动元素。

  • 随着讨论深入,引入双向链表,它为每个节点增设前驱指针,赋予链表双向遍历能力,为特定场景提供了便利。而循环链表则通过让尾节点指向头节点,形成闭环,在循环处理数据等场景中有独特应用。

  • 当然,链表也并非完美无缺,其随机访问效率低,需从头遍历查找元素,时间复杂度达O(n) ,且每个节点需额外存储指针,带来一定空间开销。理解链表的特性、操作方式及优缺点,能帮助我们在不同编程需求下做出明智的数据结构选择,为构建高效、稳定的程序奠定坚实基础。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

尝试把clang-tidy集成到AWTK项目

前言 项目经过一段时间的耕耘终于进入了团队开发阶段&#xff0c;期间出现了很多问题&#xff0c;其中一个就是开会讨论团队的代码风格规范&#xff0c;目前项目代码风格比较混乱&#xff0c;有的模块是驼峰&#xff0c;有的模块是匈牙利&#xff0c;后面经过讨论&#xff0c;…

ES面试题

1、Elasticsearch的基本构成&#xff1a; &#xff08;1&#xff09;index 索引&#xff1a; 索引类似于mysql 中的数据库&#xff0c;Elasticesearch 中的索引是存在数据的地方&#xff0c;包含了一堆有相似结构的文档数据。 &#xff08;2&#xff09;type 类型&#xff1a…

硬件电路基础

目录 1. 电学基础 1.1 原子 1.2 电压 1.3 电流 1.电流方向&#xff1a; 正极->负极,正电荷定向移动方向为电流方向&#xff0c;与电子定向移动方向相反。 2.电荷&#xff08;这里表示负电荷&#xff09;运动方向&#xff1a; 与电流方向相反 1.4 测电压的时候 2. 地线…

【VM】VirtualBox安装ubuntu22.04虚拟机

阅读本文之前&#xff0c;请先根据 安装virtualbox 教程安装virtulbox虚拟机软件。 1.下载Ubuntu系统镜像 打开阿里云的镜像站点&#xff1a;https://developer.aliyun.com/mirror/ 找到如图所示位置&#xff0c;选择Ubuntu 22.04.3(destop-amd64)系统 Ubuntu 22.04.3(desto…

零代码搭建个人博客—Zblog结合内网穿透发布公网

目录 一、准备工作二、Z-blog 网站搭建1. XAMPP 环境设置2. Z-blog 安装3. Z-blog 网页测试 三、内网穿透工具 Cpolar 的安装和配置1. Cpolar 安装2. Cpolar 云端设置3. Cpolar 本地设置 四、本地网页发布五、注意六、本次经历总结 大家好&#xff0c;我是学问小小谢。 最近心血…

动静态库的学习

动静态库中&#xff0c;不需要包含main函数 文件分为内存级(被打开的)文件和磁盘级文件 库 每个程序都要依赖很多基础的底层库&#xff0c;本质上来说库是一种可执行代码的二进制形式&#xff0c;可以被载入内存执行 静态库 linux .a windows .lib 动态库 linux .…

esp32如何接入豆包

要在 ESP32 上接入豆包工具&#xff0c;本质上是让 ESP32 设备与豆包的 API 进行通信&#xff0c;以获取相关服务。以下是具体步骤&#xff1a; 1. 准备工作 硬件准备 ESP32 开发板&#xff1a;确保你的 ESP32 开发板能正常工作&#xff0c;可以使用 USB 线连接到电脑上。电…

neo4j-在Linux中安装neo4j

目录 切换jdk 安装neo4j 配置neo4j以便其他电脑可以访问 切换jdk 因为我安装的jdk是1.8版本的&#xff0c;而我安装的neo4j版本为5.15,Neo4j Community 5.15.0 不支持 Java 1.8&#xff0c;它要求 Java 17 或更高版本。 所以我需要升级Java到17 安装 OpenJDK 17 sudo yu…

Docker使用教程

文章目录 Docker启动和校验镜像和容器常见命令 存储目录挂载卷映射数据卷命令 网络网络命令自定义网络实现redis主从同步集群 DockerCompose语法 Docker启动和校验 # 启动Docker systemctl start docker# 查看Docker运行状态 systemctl status docker# 停止Docker systemctl s…

Mac M1 ComfyUI 中 AnyText插件安装问题汇总?

Q1&#xff1a;NameError: name ‘PreTrainedTokenizer’ is not defined ? 该项目最近更新日期为2024年12月&#xff0c;该时间段的transformers 版本由PyPI 上的 transformers 页面 可知为4.47.1. A1: transformers 版本不满足要求&#xff0c;必须降级transformors &#…

大语言模型的个性化综述 ——《Personalization of Large Language Models: A Survey》

摘要&#xff1a; 本文深入解读了论文“Personalization of Large Language Models: A Survey”&#xff0c;对大语言模型&#xff08;LLMs&#xff09;的个性化领域进行了全面剖析。通过详细阐述个性化的基础概念、分类体系、技术方法、评估指标以及应用实践&#xff0c;揭示了…

SpringBoot+Dubbo+zookeeper 急速入门案例

项目目录结构&#xff1a; 第一步&#xff1a;创建一个SpringBoot项目&#xff0c;这里选择Maven项目或者Spring Initializer都可以&#xff0c;这里创建了一个Maven项目&#xff08;SpringBoot-Dubbo&#xff09;&#xff0c;pom.xml文件如下&#xff1a; <?xml versio…

算法:线性同余法(LCG,Linear Congruential Generator)

1. 线性同余法&#xff08;LCG&#xff09;是什么&#xff1f; 线性同余法&#xff08;LCG&#xff0c;Linear Congruential Generator&#xff09; 是一种最简单、最常见的伪随机数生成算法。它使用一个递推公式&#xff0c;通过线性变换生成一系列的伪随机数。 LCG 的特点&…

分析用户请求K8S里ingress-nginx提供的ingress流量路径

前言 本文是个人的小小见解&#xff0c;欢迎大佬指出我文章的问题&#xff0c;一起讨论进步~ 我个人的疑问点 进入的流量是如何自动判断进入iptables的四表&#xff1f;k8s nodeport模式的原理&#xff1f; 一 本机环境介绍 节点名节点IPK8S版本CNI插件Master192.168.44.1…

CommonAPI学习笔记-2

一. 概述 ​ 这篇文章主要是想整理并且分析CommonAPI代码生成工具根据fidl和fdepl配置文件生成出来的代码的结构和作用。 二. fidl ​ 用户根据业务需求在fidl文件中定义业务服务接口的结构以及自定义数据类型&#xff0c;然后使用core生成工具传入fidl文件生成该fidl的核心…

什么叫DeepSeek-V3,以及与GPT-4o的区别

1. DeepSeek 的故事 1.1 DeepSeek 是什么&#xff1f; DeepSeek 是一家专注于人工智能技术研发的公司&#xff0c;致力于打造高性能、低成本的 AI 模型。它的目标是让 AI 技术更加普惠&#xff0c;让更多人能够用上强大的 AI 工具。 1.2 DeepSeek-V3 的问世 DeepSeek-V3 是…

数据结构:队列篇

图均为手绘,代码基于vs2022实现 系列文章目录 数据结构初探: 顺序表 数据结构初探:链表之单链表篇 数据结构初探:链表之双向链表篇 链表特别篇:链表经典算法问题 数据结构:栈篇 文章目录 系列文章目录前言一.队列的概念和结构1.1概念一、动态内存管理优势二、操作效率与安全性…

MySQL

二进制方式&#xff1a; 下载并上传安装包到设备 创建组与用户 [rootlocalhost ~]# groupadd mysql [rootlocalhost ~]# useradd -r -g mysql -s /bin/false mysql解压安装包&#xff1a; [rootlocalhost ~]# tar xf mysql-8.0.36-linux-glibc2.28-x86_64.tar.xz -C /usr/l…

Windows电脑本地部署运行DeepSeek R1大模型(基于Ollama和Chatbox)

文章目录 一、环境准备二、安装Ollama2.1 访问Ollama官方网站2.2 下载适用于Windows的安装包2.3 安装Ollama安装包2.4 指定Ollama安装目录2.5 指定Ollama的大模型的存储目录 三、选择DeepSeek R1模型四、下载并运行DeepSeek R1模型五、常见问题解答六、使用Chatbox进行交互6.1 …

洛谷网站: P3029 [USACO11NOV] Cow Lineup S 题解

题目传送门&#xff1a; P3029 [USACO11NOV] Cow Lineup S - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 前言&#xff1a; 这道题的核心问题是在一条直线上分布着不同品种的牛&#xff0c;要找出一个连续区间&#xff0c;使得这个区间内包含所有不同品种的牛&#xff0c;…