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

系列文章目录

速通数据结构与算法系列

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了嘛

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

有人用GPT来做日内交易,居然赚钱了!但是……

在我们还在烦恼会不会被AI替代时&#xff0c;已经有人在教ChatGPT去炒股票了。 在近年ChatGPT火速出圈后&#xff0c;围绕AI能取代什么职业的讨论持续受到大众关注。 从事客服、编程、法律合规以及内容创作等行业人员最早感受到这股AI带来的寒意。 那ChatGPT能不能替代交易员…

Lora 串口透传开发 5

1 简介 串口转usb、转wifi等很多应用 2 设计原理 2.1 设计需求 1将LoRa终端定义成两种角色:Master和Slave 2一个模块发送任意字节长度&#xff08;小于128Byte&#xff09;数据&#xff0c;另一模块都可以接收到 3PC机上通过串口调试助手实现接收和发送 4终端在LCD屏幕上显…

【Redis】分布式锁及其他常见问题

分布式锁及其他常见问题 1. 我看你的项目都用到了 Redis&#xff0c;你在最近的项目的哪些场景下用到了 Redis 呢&#xff1f; 一定要结合业务场景来回答问题&#xff01;要是没有不要硬讲&#xff0c;除非面试官问&#xff1b; 接下来面试官将深入发问。 你没用到的也可能会…

软考109-上午题-【计算机网络】-网络设备

一、网络设备 1-1、物理层的互联设备 物理层的设备&#xff1a;中继器、集线器 1、中继器 中继器&#xff0c;可以使得两个链路在物理层上互联。 可以使得信号再生&#xff0c;信号增强。因此&#xff0c;中继器使得接受用户&#xff0c;收到衰减很小的原始信号 2、集线器&a…

最优控制理论笔记 - 01数学准备

目录 一、向量和矩阵的微分 1. 向量对标量的导数 2. 矩阵对标量的导数 2.1 矩阵对标量的导数的运算公式 2.2 标量函数对向量的导数&#xff1a; 2.3 向量函数对向量的导数 二、函数极值的问题 三、有约束条件的函数极值问题 四、n元函数的Taylor 一、向量和矩阵的微分 …

23linux 自定义shell文件系统

打印环境变量&#xff0c;把当前子进程所有环境变量打印出来 环境变量也是一张表&#xff08;指针数组以null结尾&#xff0c;最后条件不满足就退出了 &#xff09; 用子进程调用 结论1 当我们进行程序替换的时候 &#xff0c;子进程对应的环境变量&#xff08;子进程的环境变…

51入门之LED

目录 1.配置文件 2.点亮一个LED 2.1单个端口操作点亮单个LED 2.2整体操作点亮LED 3.LED闪烁 4.LED实现流水灯 4.1使用for循环和移位实现 4.1.1移位操作符 4.1.2使用移位操作和for循环实现 4.2使用移位函数实现LED流水灯 众所周知&#xff0c;任何一个硬件工程师…

pycharm和Spyder多行注释快捷键

1.选取注释内容 2.pycharm&#xff1a;使用Ctrl/ 3.Spyder&#xff1a;使用Ctrl1 效果图

1.k8s架构

k8s集群架构 一个Kubernetes集群至少包含一个控制平面(control plane)&#xff0c;以及一个或多个工作节点(worker node)。控制平面(Control Plane) : 控制平面负责管理工作节点和维护集群状态。所有任务分配都来自于控制平面。工作节点(Worker Node) : 工作节点负责执行由控制…

Centos-Linux安装部署MySQL-8.0

linux搭建mysql 1.使用系统的root账户 2.切换到 /usr/local 目录下 3.下载mysql wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.31-linux-glibc2.12-x86_64.tar.xz也可以直接在官方下载最新版本 官方下载 选择linux 4.解压mysql tar xvJf mysql-8.0.21-l…

Redux Toolkit+TypeScript最佳实践

Redux-Toolkit是为了简化使用Redux繁琐的步骤&#xff0c;可以j降低使用useReducer与useContext管理状态的频率&#xff0c;而且起到项目中状态管理规范和约束化的效果。 阅读本文需要的前置知识&#xff1a;React、Redux、Typescript、Redux hooks。 Redux-Toolkit使用步骤 …

【GO语言卵细胞级别教程】11.探索Go语言的面向对象编程之美(含源码仅此一份,先到先得)

【GO语言卵细胞级别教程】11.探索Go语言的面向对象编程之美&#xff08;含源码仅此一份&#xff0c;先到先得&#xff09; 目录 【GO语言卵细胞级别教程】11.探索Go语言的面向对象编程之美&#xff08;含源码仅此一份&#xff0c;先到先得&#xff09;1.面向对象的引用1.1简介1…

Day79:服务攻防-中间件安全IISApacheTomcatNginx弱口令不安全配置CVE

目录 中间件-IIS-短文件&文件解析&蓝屏&写权限 HTTP.SYS&#xff08;CVE-2015-1635&#xff09;主要用作蓝屏破坏&#xff0c;跟权限不挂钩 IIS短文件(iis全版本都可能有这个问题) IIS文件解析 IIS写权限 中间件-Nginx-文件解析&目录穿越漏洞&CRLF …

C++ //练习 11.14 扩展你在11.2.1节练习(第378页)中编写的孩子姓到名的map,添加一个pair的vector,保存孩子的名和生日。

C Primer&#xff08;第5版&#xff09; 练习 11.14 练习 11.14 扩展你在11.2.1节练习&#xff08;第378页&#xff09;中编写的孩子姓到名的map&#xff0c;添加一个pair的vector&#xff0c;保存孩子的名和生日。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#x…

【大数据存储】实验七 Spark RDD

Spark RDD操作实验 一、实验目的 &#xff08;1&#xff09;掌握使用Spark访问本地文件和HDFS文件的方法 &#xff08;2&#xff09;熟练掌握在Spark Shell中对Spark RDD的操作方法 &#xff08;3&#xff09;掌握Spark应用程序的编写、编译打包和运行方法 二、.实验平台 …

自动化测试框架Robot Framework入门

什么是RF RF是一个基于 Python 的、可扩展的关键字驱动的自动化 验收测试框架、验收测试驱动开发 &#xff08;ATDD&#xff09;、 行为驱动开发 &#xff08;BDD&#xff09; 和机器人流程自动化 &#xff08;RPA&#xff09;。它 可用于分布式、异构环境&#xff0c;其中自动…

VUE3和SpringBoot实现ChatGPT页面打字效果SSE流式数据展示

在做这个功能之前&#xff0c;本人也是走了很多弯路&#xff08;花了好几天才搞好&#xff09;&#xff0c;你能看到本篇博文&#xff0c;那你就是找对地方了。百度上很多都是使用SseEmitter这种方式&#xff0c;这种方式使用的是websocket&#xff0c;使用这种方式就搞复杂了&…

STM32 PWM方式读取AS5600磁编码器数据

HAL STM32 PWM方式读取AS5600磁编码器获取角度例程 &#x1f4cd;相关篇《STM32 软件I2C方式读取AS5600磁编码器获取角度例程》 &#x1f4cc;《HAL STM32 硬件I2C方式读取AS5600磁编码器获取角度例程》 &#x1f341;先通过IIC读取的角度值和逻辑分析仪抓取的AS5600 OUT引脚…

MySQL - 基础二

6、表的增删改查 CRUD : Create(创建), Retrieve(读取)&#xff0c;Update(更新)&#xff0c;Delete&#xff08;删除&#xff09; 6.1、Create 语法&#xff1a; INSERT [INTO] table_name[(column [, column] ...)]VALUES (value_list) [, (value_list)] ...value_list: v…

SQL注入---盲注

文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 一.盲注概述 注是一种SQL注入攻击的形式&#xff0c;在这种攻击中&#xff0c;攻击者向目标应用程序发送恶意注入代码&#xff0c;然后通过观察应用程序的响应来推断出数据库中的信息。与常规的…