速通数据结构与算法第四站 双链表

系列文章目录

速通数据结构与算法系列

1   速通数据结构与算法第一站 复杂度          http://t.csdnimg.cn/sxEGF

2   速通数据结构与算法第二站 顺序表          http://t.csdnimg.cn/WVyDb

3   速通数据结构与算法第三站 单链表          http://t.csdnimg.cn/cDpcC

感谢佬们支持!


目录

系列文章目录

  • 前言
  • 一、双链表
  •    0 结构体
  •    1 接口声明
  •    2 增加节点
  •    3 初始化
  •    4 打印
  •    5 尾插
  •    6 尾删
  •    7 头插
  •    8 头删
  •    9 find
  •   10 insert
  •   11 erase
  •   12 销毁
  •   13 完整代码
  • 二、OJ题
  •    1 环形链表1
  •    2 环形链表2
  •    3 复杂链表的复制
  • 三、链表和顺序表的对比&&补充deque
  • 总结

前言

上篇博客我们探讨的是单链表,这篇博客将为大家带来双链表和3个更值得探讨的OJ题

双链表看似更复杂了,其实不然,代码写起来比单链表爽的多

我们要写的就是带头双向循环链表, 这也是STL(SGI版)中list相同的结构


一、双链表

我们还是先来搞一个结构体

结构体
typedef int LTDataType;typedef struct ListNode
{LTDataType data;struct ListNode* prev;struct ListNode* next;
}LTNode;

其中data表示数据,prev表示指向前一个结点,next表示指向后一个节点


接口声明
//增加节点
LTNode* BuyListNode(LTDataType x);//初始化
//void LTInit(LTNode**phead);
LTNode* LTInit();//打印
void LTPrint(LTNode* phead);//判空
bool LTEmpty(LTNode* plist);//销毁
void LTDestroy(LTNode* phead);//尾插
void LTNodePushBack(LTNode* phead, LTDataType x);//尾删
void LTNodePopBack(LTNode* phead);//头插
void LTNodePushFront(LTNode* phead, LTDataType x);//头删
void LTNodePopFront(LTNode* phead);//find
LTNode* LTNodeFind(LTNode* phead, LTDataType x);//pos前插
void LTNodeInsert(LTNode* pos, LTDataType x);//删pos
void LTNodeErase(LTNode* pos);

 增加节点

增加节点的逻辑简单,这里不再赘述

//创建节点
LTNode* BuyListNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));newnode->data = x;newnode->next = NULL;newnode->prev = NULL;return newnode;
}

初始化

显然在双链表这里我们的初始化是有事做的,而不像单链表在使用时给个空就行

我们要创建哨兵位的头节点,还要让他的next和prev都指向自己(毕竟是个循环链表)

按理说这样修改头节点我们是要传二级指针的,但是后面的接口实际上都传一级指针就行,毕竟有了头节点,之后的修改都不涉及整个链表。

如果用了二级指针的写法,会是这样……

 void LTInit(LTNode* *phead)
{//哨兵位*phead = BuyListNode(-1);(*phead)->next = *phead;(*phead)->prev = *phead;}

用起来就是这样……

LTNode* plist = NULL;LTInit(&plist);

为了接口的一致性,我们也可以用传一级指针,只要用返回值带出来即可

//初始化
LTNode* LTInit()
{//哨兵位LTNode*phead = BuyListNode(-1);phead->next = phead;phead->prev = phead;return phead;//返回头节点
}

用起来就是这样

	LTNode* plist = LTInit();

下来我们再写一下打印

打印

打印很简单,但是我们要控制好循环的逻辑,毕竟他是个循环链表,一不小心就会死循环

我可以定义一个cur指向phead->next,cur直到等于phead停下来

//打印
void LTPrint(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){printf("%d ", cur->data);cur = cur->next;}
}

尾插

双链表的尾插相比于单链表轻松很多,首先它不用找尾(phead的prev就是尾),拿到尾以后我们只需改4个指针即可

如图所示

而且好消息是,我们通过画图发现,如果链表为空,上述逻辑依然成立,所以我们不用特判链表为空的情况

直接上代码

//尾插
void LTNodePushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = BuyListNode(x);LTNode* tail = phead->prev;//连接关系tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode;//空链表尾插不用单独处理。奈斯}

尾删

尾删只需考虑链表是否为空的情况即可,即我们不能删哨兵位的头节点

依然是改4个指针的问题

//尾删
void LTNodePopBack(LTNode* phead)
{assert(phead);//链表为空就不能再删assert(!LTEmpty(phead));LTNode* tail = phead->prev;LTNode* newtail = tail->prev;phead->prev = newtail;newtail->next = phead;free(tail);tail = NULL;}

头插

头插同样是改4个指针的逻辑

直接上代码

//头插
void LTNodePushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = BuyListNode(x);LTNode* head = phead->next;phead->next = newnode;newnode->prev = phead;newnode->next = head;head = newnode;}

头删

头删同理,我们只需额外判断链表是否为空就行

//头删
void LTNodePopFront(LTNode* phead)
{assert(phead);//链表为空就不能再删assert(!LTEmpty(phead));LTNode* head = phead->next;LTNode* newhead = head->next;newhead->prev = phead;phead->next = newhead;free(head);head = NULL;}

我们简单的做一波测试

LTNode* plist = LTInit();LTNodePushBack(plist, 1);LTNodePushBack(plist, 1);LTNodePushBack(plist, 4);LTNodePushBack(plist, 2);//LTNodePopBack(plist);LTNodePopBack(plist);LTNodePushBack(plist, 1);LTNodePushFront(plist, 3);LTPrint(plist);LTDestroy(plist);

(没有问题)

这里我们发现双链表的头插头删尾插尾删都是O(1),确实挺不错的


find

find的原理不用多说了,直接上代码

//寻找
LTNode* LTNodeFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}//找不见return NULL;
}

insert

我们要写的pos位置前插,在写了前面的之后,写这个就是砍瓜切菜啦~

只需额外判断pos是否合法即可

//pos位置前插入
void LTNodeInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = BuyListNode(x);LTNode* prev = pos->prev;newnode->next = pos;pos->prev = newnode;newnode->prev = prev;prev->next = newnode;
}

erase
/删pos位置
void LTNodeErase(LTNode* pos)
{assert(pos);assert(!LTEmpty(pos));LTNode* next = pos->next;LTNode* prev = pos->prev;prev->next = next;next->prev = prev;free(pos);pos = NULL;
}

写了erase和insert之后,我们前面的代码就都不用写了,直接复用这个即可

尾插会是这样

//复用版本//LTNodeInsert(phead,x);

尾删

//复用版本//LTNodeErase(phead->prev);

头插

//复用版本//LTNodeInsert(phead->next,x);

头删

//复用版本//LTNodeErase(phead->next);

这样,如果面试官让你20min写一个链表,你直接写一个insert和erase就可以轻松搞定了


销毁

最后我们来搞定一下销毁就好啦!

//销毁
void LTDestroy(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){LTNode* next = cur->next;free(cur);cur = next;}free(phead);//由于传的是一级指针,记得要在外面手动置空
}

完整代码

完整代码是这样的…

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* prev;struct ListNode* next;
}LTNode;//增加节点
LTNode* BuyListNode(LTDataType x);//初始化
//void LTInit(LTNode**phead);
LTNode* LTInit();//打印
void LTPrint(LTNode* phead);//判空
bool LTEmpty(LTNode* plist);//销毁
void LTDestroy(LTNode* phead);//尾插
void LTNodePushBack(LTNode* phead, LTDataType x);//尾删
void LTNodePopBack(LTNode* phead);//头插
void LTNodePushFront(LTNode* phead, LTDataType x);//头删
void LTNodePopFront(LTNode* phead);//find
LTNode* LTNodeFind(LTNode* phead, LTDataType x);//pos前插
void LTNodeInsert(LTNode* pos, LTDataType x);//删pos
void LTNodeErase(LTNode* pos);

list.c

#include"List.h"//创建节点
LTNode* BuyListNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));newnode->data = x;newnode->next = NULL;newnode->prev = NULL;return newnode;
}#if 0
//初始化(二级指针z)
void LTInit(LTNode* *phead)
{//哨兵位*phead = BuyListNode(-1);(*phead)->next = *phead;(*phead)->prev = *phead;//return phead;//返回头节点}
#endif//初始化
LTNode* LTInit()
{//哨兵位LTNode*phead = BuyListNode(-1);phead->next = phead;phead->prev = phead;return phead;//返回头节点
}
//打印
void LTPrint(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){printf("%d ", cur->data);cur = cur->next;}
}//判空
bool LTEmpty(LTNode* phead)
{return phead->next == phead;
}//销毁
void LTDestroy(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){LTNode* next = cur->next;free(cur);cur = next;}free(phead);//由于传的是一级指针,记得要在外面手动置空
}//尾插
void LTNodePushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = BuyListNode(x);LTNode* tail = phead->prev;//连接关系tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode;//空链表尾插不用单独处理。奈斯//复用版本//LTNodeInsert(phead,x);
}//尾删
void LTNodePopBack(LTNode* phead)
{assert(phead);//链表为空就不能再删assert(!LTEmpty(phead));LTNode* tail = phead->prev;LTNode* newtail = tail->prev;phead->prev = newtail;newtail->next = phead;free(tail);tail = NULL;//复用版本//LTNodeErase(phead->prev);
}//头插
void LTNodePushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = BuyListNode(x);LTNode* head = phead->next;phead->next = newnode;newnode->prev = phead;newnode->next = head;head = newnode;//复用版本//LTNodeInsert(phead->next,x);
}//头删
void LTNodePopFront(LTNode* phead)
{assert(phead);//链表为空就不能再删assert(!LTEmpty(phead));LTNode* head = phead->next;LTNode* newhead = head->next;newhead->prev = phead;phead->next = newhead;free(head);head = NULL;//复用版本//LTNodeErase(phead->next);
}//寻找
LTNode* LTNodeFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}//找不见return NULL;
}//pos位置前插入
void LTNodeInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = BuyListNode(x);LTNode* prev = pos->prev;newnode->next = pos;pos->prev = newnode;newnode->prev = prev;prev->next = newnode;
}//删pos位置
void LTNodeErase(LTNode* pos)
{assert(pos);assert(!LTEmpty(pos));LTNode* next = pos->next;LTNode* prev = pos->prev;prev->next = next;next->prev = prev;free(pos);pos = NULL;
}

test.c

#include"List.h"void test1()
{//LTNode* plist = NULL;// LTInit(&plist);LTNode* plist = LTInit();LTNodePushBack(plist, 1);LTNodePushBack(plist, 1);LTNodePushBack(plist, 4);LTNodePushBack(plist, 1);LTNodePushBack(plist, 2);//LTNodePopBack(plist);//LTNodePopBack(plist);LTNodePushFront(plist, 3);LTNode* ret = LTNodeFind(plist, 4);LTNodeErase(ret);LTPrint(plist);LTDestroy(plist);plist = NULL;
}void test2()
{LTNode* plist = LTInit();LTNodePushBack(plist, 1);LTNodePushBack(plist, 1);LTNodePushBack(plist, 4);LTNodePushBack(plist, 2);//LTNodePopBack(plist);LTNodePopBack(plist);LTNodePushBack(plist, 1);LTNodePushFront(plist, 3);LTPrint(plist);LTDestroy(plist);
}int main()
{test2();return 0;
}

二、OJ题

这次我们来看几道相对较难的OJ题


1 环形链表1

题目链接: . - 力扣(LeetCode)

首先 这个题最好不要遍历,不然会很容易死循环

最简单的方法是快慢指针,让slow一次走一步,fast一次走两步,如果他们最终相遇

说明有环

为什么呢?

1 显然,slow和fast的相对速度是1步(1个节点),由物理学知识我们知道,如果有环,他们最终一定相遇,不会错过

2 如果fast一次走3步呢?相对速度就会是2步,suppose slow进环后fast开始追击。如果他们之间的距离是偶数,他们就会相遇;如果是奇数便会错过。以此类推。

代码还是简单的,我们很轻松就能写出来啦~

bool hasCycle(struct ListNode* head)
{//快慢指针,追击相遇struct ListNode* fast = head;struct ListNode* slow = head;//fast一次两步,slow一次一步while (fast && fast->next)//如果不断言fast,测试用例会有空链表,如果不想断言fast,可以加上面的那个判断{slow = slow->next;fast = fast->next->next;if (slow == fast){return true;}}return false;
}

2 环形链表2

题目链接: . - 力扣(LeetCode)

下来恶心的是环形链表2,它建立在环形链表1的基础上

我们需要在有环的基础上找到环的入口节点

先说结论,再证明:在环形链表1中我们可以得到最后slow和fast的相遇点(如果有环的话)

现在是这样,我们再给两个指针,一个从链表开始走,另一个从相遇点走,他们最后相遇的节点

就是我们要的入口节点。

证明:

suppose入口点到环的起点距离为N,环的长度为C,入口点至相遇点的距离为x

也就是这样……

首先在相遇时slow走的距离为:N+X

fast走的距离为:N+X+n*C(n为fast走的圈数)

由于fast的距离==slow的两倍

即2*(N+X)=N+X+n*C

化简一下就会得到

N+X=n*C

N=n*C-X

由此便得:此时再用两个指针,一个从链表开始走,另一个从相遇点走,他们最后相遇的节点

就是我们要的入口节点。

代码如下……

bool hasCycle(struct ListNode* head)
{//快慢指针,追击相遇struct ListNode* fast = head;struct ListNode* slow = head;//fast一次两步,slow一次一步while (fast && fast->next)//如果不断言fast,测试用例会有空链表,如果不想断言fast,可以加上面的那个判断{slow = slow->next;fast = fast->next->next;if (slow == fast){return true;}}return false;
}struct ListNode* detectCycle(struct ListNode* head)
{if (hasCycle(head) == NULL){return NULL;}else{//一个从相遇点开始走,一个从头走,它们会在环的入口相遇struct ListNode* meet = hasCycle(head);while (meet != head){meet = meet->next;head = head->next;}return meet;}
}

我们还有第二种思路

在相遇点处断开,我们会得到两条链表,如图

我们可以求这两条链表的相交节点(具体相交链表的逻辑请大家移步我的上一篇博客

(  http://t.csdnimg.cn/cDpcC)

//链表相交
struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB)
{int lenA = 1;int lenB = 1;struct ListNode* tailA = headA;struct ListNode* tailB = headB;while (tailA){lenA++;tailA = tailA->next;}while (tailB){lenB++;tailB = tailB->next;}//如果相交,尾节点一定相同  //这个条件很有必要if (tailA != tailB){return NULL;}//快的先走差距步,再一起走struct ListNode* fast = lenA > lenB ? headA : headB;struct ListNode* slow = lenA > lenB ? headB : headA;int gap = abs(lenA - lenB);while (gap--){fast = fast->next;}while (fast != slow){fast = fast->next;slow = slow->next;}return fast;}
struct ListNode* hasCycle(struct ListNode* head)
{//快慢指针,追击相遇struct ListNode* fast = head;struct ListNode* slow = head;//fast一次两步,slow一次一步while (fast && fast->next)//如果不断言fast,测试用例会有空链表,如果不想断言fast,可以加上面的那个判断{slow = slow->next;fast = fast->next->next;if (slow == fast){return slow;}}return NULL;
}struct ListNode* detectCycle(struct ListNode* head)
{if (hasCycle(head) == NULL){return NULL;}else{struct ListNode* newhead = hasCycle(head)->next;hasCycle(head)->next = NULL;return getIntersectionNode(newhead, head);}
}

3 复杂链表的复制

题目链接:. - 力扣(LeetCode)

首先,这个题很离谱,很难,算是大家目前链表学习的最后一块天花板

我们要复制一个带有random指针得链表,其中random会指向任意节点,这道题的关键就是

如何让新链表的random指针指向新链表的节点而非老链表的节点。

学过C++的兄弟们肯定就爽了,直接哈希 启动!

构建源节点和新节点的映射关系就行,确实,哈希真的很香,这里放一份参考代码

class Solution {
public:Node* copyRandomList(Node* head) {if(head==nullptr)return nullptr;unordered_map<Node*,Node*> m;Node*cur=head;while(cur){//建立旧结点和新节点的链接m[cur]=new Node(cur->val);cur=cur->next;}cur=head;while(cur){m[cur]->next=m[cur->next];m[cur]->random=m[cur->random];cur=cur->next;}return m[head];}};

但是现在我们在学数据结构,没有哈希表,这怎么办?

这个时候有大佬就想了一种办法,在每个原节点的后面拷贝这个节点,并链接至源节点之后

就像这样……

这有什么用呢?

别忘了我们的核心问题是解决新节点random的问题,此时我们会发现,

新节点的random==老节点random的next,

这波还是非常神奇的,一气呵成,行云流水,让人拍案叫绝。

接下来就是将这些新节点串成一个新链表,并将老链表恢复原样的工作,看似简单,但代码其实并不好写

代码如下~

struct Node* copyRandomList(struct Node* head)
{if (head == NULL)return NULL;struct Node* cur = head;//1 每个节点后连一个拷贝后的节点while (cur){struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));newnode->val = cur->val;struct Node* next = cur->next;cur->next = newnode;newnode->next = next;cur = next;}cur = head;while (cur){struct Node* newnode = cur->next;if (cur->random == NULL){newnode->random = NULL;}else{newnode->random = cur->random->next;}//一次走两步!cur = cur->next->next;}//链接成新链表,cur = head;struct  Node* newhead;struct  Node* tail;while (cur){struct Node* copy = cur->next;struct Node* next = copy->next;if (NULL == newhead){tail = newhead = copy;}else{tail->next = copy;tail = tail->next;}//恢复原链表cur->next - next;cur = next;}return newhead;
}

三、链表和顺序表的对比&&补充deque

话不多说我们直接上一个表格

不同点顺序表链表
存储空间物理上一定连续物理上不一定连续
随机访问O(1)O(n)
随意位置插入删除元素可能要挪元素,O(n)只需修改指针指向
插入动态顺序表须扩容没有容量的概念
迭代器失效
应用场景随机访问和元素高效存储任意位置插入删除频繁
缓存利用率

补充两点:

1 迭代器失效的问题是由于顺序表某次插入后可能刚好扩容了,而我们在用的时候不知道,所以之前指向原空间的迭代器(指针)由于空间已经销毁了,当我们再次使用时,读取到的就算随机数据;显然,由于链表无须扩容,所以没有迭代器失效的问题

2 有关缓存利用率的问题

首先我们要明白计算机的存储体系结构

 

首先,冯诺依曼体系告诉我们,CPU如果直接和外设打交道,就太慢了,所以提出了所谓存储器的概念:由外设将数据load至主存,再让CPU从主存中读取数据。

但是主存的速度也太慢了,所以设立了所谓3级缓存,CPU拿数据时会直接去缓存拿,如果缓存中刚好有所需的数据,称为命中;如果没有所需的数据,会load新一批数据至缓存中。

而在计算机中有一个所谓的局部性原理,当你用到某个地址空间的数据,那大概率他周围的数据你也会用到。所以当这一段内存空间load到缓存时,如果你是顺序表,你的存储是连续的,所以你缓存中的数据用到的概率就会增加。而如果你是链表,你的缓存中大概率都是一些用不到的数据

此时甚至会有所谓"缓存污染"的问题。


另外,有趣的是,有人为了结合链表和顺序表的优点,发明出了一个叫deque的东西

deque,双端队列

1 相较于vector的扩容-拷贝原数据-释放旧空间的做法,他的操作是搞一个指针数组map,由指针数组来指向一块一块空间(缓冲区)

而map的扩容也有自己的策略,但总归括的次数会少很多,这算是保留了list的优点

2 第二个好消息是相较于vector的头插头删效率低而list的尾插尾删效率低,deque的头插头删尾插尾删都是O(1),这算是集中了vector和list的优点

3 deque支持随机访问,即像数组一样可以用下标访问,只不过底层需要一些除运算模运算

不够极致

4 deque是分段的连续空间,所以缓存利用率也还可以

看似deque真的很不错,但是其实他也有缺点

虽然deque头插头删尾插尾删都很奈斯,但是他的insert、erase(中间插入删除)更拉了

另外还有其设计的逆天的迭代器机制,这个我们到C++再说

总结:看似是集成了vector和list的优点,但实际上很鸡肋,用的人很少。


总结

 做总结,这篇博客结束了链表的学习,大家需要牢记链表和顺序表的区别,这是面试中常常问到的。下一篇博客我们将开启栈和队列的学习。

水平有限,还请各位大佬指正。如果觉得对你有帮助的话,还请三连关注一波。希望大家都能拿到心仪的offer哦。

每日gitee侠:今天你交gitee了嘛

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

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

相关文章

WebGIS 之 vue3+vite+ceisum

1.项目搭建node版本在16以上 1.1创建项目 npm create vite 项目名 1.2选择框架 vuejavaScript 1.3进入项目安装依赖 cd 项目名 npm install 1.4安装cesium依赖 pnpm i cesium vite-plugin-cesium 1.5修改vite.config.js文件 import { defineConfig } from vite import vue fr…

07-app端文章搜索

app端文章搜索 1) 今日内容介绍 1.1)App端搜索-效果图 1.2)今日内容 文章搜索 ElasticSearch环境搭建 索引库创建 文章搜索多条件复合查询 索引数据同步 搜索历史记录 Mongodb环境搭建 异步保存搜索历史 查看搜索历史列表 删除搜索历史 联想词查询 联想词的来源 联…

基于单片机的全自动洗衣机系统仿真设计

**单片机设计介绍&#xff0c;基于单片机的全自动洗衣机系统仿真设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的全自动洗衣机系统仿真设计概要是关于利用单片机技术实现全自动洗衣机控制功能的系统设计概述。以…

【CSS】浮动笔记及案例

CSS浮动 1. 认识浮动 float属性可以指定一个元素沿着左侧或者是右侧放置&#xff0c;允许文本和内联元素环绕它 float属性最初只使用文字环绕图片但却是早起CSS最好用的左右布局方案 绝对定位、浮动都会让元素脱标&#xff0c;以达到灵活布局的目的可以通过float属性让元素脱…

Unix信号处理

信号的基本概念我已经在上一节中简单介绍了&#xff0c;大家可以去看我的上一篇博客&#xff1a; Unix中的进程和线程-2-CSDN博客 1.信号的产生 kill函数&#xff1a; #include <signal.h> #include <fcntl.h> #include<t_stdio.h> //自定义信号处理函数,n为…

第十一届蓝桥杯物联网试题(省赛)

对于通信方面&#xff0c;还是终端A、B都保持接收状态&#xff0c;当要发送的数组不为空再发送数据&#xff0c;发送完后立即清除&#xff0c;接收数据的数组不为空则处理&#xff0c;处理完后立即清除&#xff0c;分工明确 继电器不亮一般可能是电压不够 将数据加空格再加\r…

线段树练习

1.单点修改区间查询 P3374 【模板】树状数组 1 题目描述 如题&#xff0c;已知一个数列&#xff0c;你需要进行下面两种操作&#xff1a; 将某一个数加上 x 求出某区间每一个数的和 输入格式 第一行包含两个正整数 n,m&#xff0c;分别表示该数列数字的个数和操作的总个…

C++之函数提高(HM)

目录 1.函数默认参数&#xff08;缺省参数&#xff09; 2.占位参数 3.函数重载 4.类和对象--封装 &#xff08;1&#xff09;圆类&#xff1a; &#xff08;2&#xff09;访问权限 &#xff08;3&#xff09;struct&&class &#xff08;4&#xff09;立方体类的…

C++利用键值对计算某一个数对应的最值及其索引位置

目录 一、算法概述二、代码实现1、计算最值2、计算最值及其索引 三、结果展示 本文由CSDN点云侠原创&#xff0c;原文链接。如果你不是在点云侠的博客中看到该文章&#xff0c;那么此处便是不要脸的爬虫与GPT。 一、算法概述 类似下图所示&#xff0c;计算第一列中1或2对应的最…

C#学生信息管理系统

一、引言 学生信息管理系统是现代学校管理的重要组成部分&#xff0c;它能够有效地管理学生的基本信息、课程信息、成绩信息等&#xff0c;提高学校管理的效率和质量。本文将介绍如何使用SQL Server数据库和C#语言在.NET平台上开发一个学生信息管理系统的课程设计项目。 二、项…

单细胞RNA测序(scRNA-seq)SRA数据下载及fastq-dumq数据拆分

单细胞RNA测序&#xff08;scRNA-seq&#xff09;入门可查看以下文章&#xff1a; 单细胞RNA测序&#xff08;scRNA-seq&#xff09;工作流程入门 单细胞RNA测序&#xff08;scRNA-seq&#xff09;细胞分离与扩增 1. NCBI查询scRNA-seq SRA数据 NCBI地址&#xff1a; https…

C++之类

目录 一&#xff1a;面向过程和面向对象的初步认识 二&#xff1a;类的引入 三&#xff1a;类的定义 3.1类的两种定义方式&#xff1a; 3.2成员变量命名的建议 四&#xff1a;类的访问限定符及封装 4.1类的访问限定符 4.2封装 一&#xff1a;面向过程和面向对象的初步认…

C#项目引用解决方案中其他项目dll时,出现黄色感叹号的解决方案

问题引入 今天拿着老师傅的老项目&#xff0c;需要做通讯调试&#xff0c;说测试一下&#xff0c;便添加了一个项目A来编写结构体&#xff0c;然后在窗体程序项目B中引用A&#xff0c;发现B一引用A&#xff0c;在B项目的引用下面A就多了个黄色感叹号&#xff0c;一编译B项目&am…

基于k8s的高性能综合web服务器搭建

目录 基于k8s的高性能综合web服务器搭建 项目描述&#xff1a; 项目规划图&#xff1a; 项目环境&#xff1a; k8s&#xff0c; docker centos7.9 nginx prometheus grafana flask ansible Jenkins等 1.规划设计整个集群的架构&#xff0c;k8s单master的集群环境&…

PyTorch深度学习——张量及其运算

深度学习框架的张量 张量的运算是深度学习的核心&#xff0c;如一张图片可以看作是四维的张量&#xff0c;一个迷你批次的文本可以看作是二维张量&#xff0c;基本上所有的深度学习模型都可以表示为张量的操作&#xff0c;梯度、反向传播算法也可以表示为张量和张量的运算 张…

scoped原理及使用

一、什么是scoped&#xff0c;为什么要用 在vue文件中的style标签上&#xff0c;有一个特殊的属性&#xff1a;scoped。 当一个style标签拥有scoped属性时&#xff0c;它的CSS样式就只能作用于当前的组件&#xff0c;通过该属性&#xff0c;可以使得组件之间的样式不互相污染。…

LLM大模型可视化-以nano-gpt为例

内容整理自&#xff1a;LLM 可视化 --- LLM Visualization (bbycroft.net)https://bbycroft.net/llm Introduction 介绍 Welcome to the walkthrough of the GPT large language model! Here well explore the model nano-gpt, with a mere 85,000 parameters. 欢迎来到 GPT 大…

深入浅出 -- 系统架构之分布式CAP理论和BASE理论

科技进步离不开理论支撑&#xff0c;而当下大行其道的分布式架构&#xff0c;透过繁荣昌盛表象&#xff0c;底层同样离不开诸多分布式理论撑持。当然&#xff0c;相信诸位在学习分布式相关技术时&#xff0c;必然学到过两个分布式领域中的基础理论&#xff0c;即&#xff1a;CA…

9.set容器的使用

文章目录 set容器1.构造和赋值代码工程运行结果 2.大小和交换代码工程运行结果 4.插入和删除代码工程运行结果 5.查找和统计工程代码运行结果 6.multset代码工程运行结果 7.指定排序规则代码工程运行结果 8.自定义数据类型排序代码工程运行结果 set容器 所有元素都会在插入时&a…

理解Three.js的相机

大家都知道我们生活中的相机&#xff0c;可以留下美好瞬间。那Three.js的相机是什么呢&#xff1f;Three.js创建的场景是三维的&#xff0c;而我们使用的显示器显然是二维的&#xff0c;相机就是抽象的定义了三维空间到二维显示器的投影方式。Three.js常见的相机有两类&#xf…