【数据结构初阶】双向链表

各位读者老爷好,很高兴你又来读本鼠鼠的博客。鼠鼠我呀基于C语言实现一下双向链表,有兴趣的读者老爷可以瞅瞅哈!

目录

1.定义双向链表节点

2.初始化哨兵位

3.双向链表销毁

4.双向链表打印

5.双向链表在pos的前面进行插入

6.双向链表删除pos位置的节点 

7.双向链表尾插

8.双向链表尾删

9.双向链表头插 

10.双向链表头删 

11.双向链表查找

12.双向链表的小应用

12.1.list.h

12.2.list.c

12.3.test.c:

13.ending ​​​​​​​

好哈,我们上篇文章浅谈过链表的分类,其中讲到:

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

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

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

那咱们上篇文章实现过了无头单向非循环链表 ,那这篇博客自然要实现带头双向循环链表了。

对于带头双向循环链表,好像听起来挺难的,但其实实现起来比无头单向非循环链表简单不是一星半点儿,咱们可以在接下来的代码中体会到,我们可以先来看看带头双向循环链表的逻辑结构如下图:

咱们可以看到有一个指针plist指向哨兵位(也就是头),所有节点均有三个数据域,分别存储所需存储的数据、前一个节点的地址和后一个节点的地址,我们通过地址即可找到想要的节点。这里讲的前后节点只是逻辑上的“前后”,实际上在内存中这些个节点通过保存的地址形成了循环,没有所谓的前后之分,那这个链表改重哪里开始呢?

其实就是从哨兵位开始的,让plist指向哨兵位,让哨兵位当“头”,也就在逻辑上形成了“前后”。

带头双向循环链表带哨兵位是有优势的,虽然哨兵位不存储有效数据,好似浪费了一个节点的空间,但是因为无论什么时候该链表都不可能为空,对于该链表的增删改等操作不用另外考虑空链表的情况 。带头双向循环链表的哨兵位是不能删除的。

以下带头双向循环链表简称双向链表。我们实现双向链表以下功能:

typedef int LTDataType;

typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;


//初始化哨兵位
ListNode* LTInit(void);

// 双向链表销毁
void ListDestory(ListNode* pHead);

// 双向链表打印
void ListPrint(ListNode* pHead);

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);

// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);

// 双向链表尾删
void ListPopBack(ListNode* pHead);

// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);

// 双向链表头删
void ListPopFront(ListNode* pHead);

// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);

1.定义双向链表节点

typedef int LTDataType;

typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

由上分析可知,该链表节点需要包含三个数据域,所以我们定义结构体,结构体成员data用来存储需存储数据、结构体成员next用来存储后一个节点地址、结构体成员prev用来存储前一个节点地址。至于为什么要把int重命名成LTDataType,那是因为如果需存储数据的类型如果有所变化的话,我们只需更改此处的int更改成需存储数据的类型即可,极大方便后续的代码维护。

2.初始化哨兵位

//创建双向链表节点
ListNode* ListCreate(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail->");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

//初始化哨兵位
ListNode* LTInit(void)
{
	ListNode* head = ListCreate(-1);
	head->next = head;
	head->prev = head;
	return head;
}

创建双向链表的时候我们需要初始化哨兵位,初始化哨兵位就需要创建节点,所以我们需要调用ListCreate函数(LIstCreate函数参数是需存储数据,动态申请一个节点,返回这个节点地址。),将哨兵位的next和prev存储哨兵位自己的地址才符合双向带头循环链表的特点,哨兵位的data随便赋值即可。

大概这样式的啦!

3.双向链表销毁

//双向链表销毁
void ListDestory(ListNode*pHead)
{
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(pHead);
	pHead = NULL;
	cur = NULL;
}

当我们不在使用双向链表的时候,好习惯是主动释放双向链表的空间。我们从哨兵位后一个节点开始遍历释放节点空间,最后再单独释放哨兵位空间。

4.双向链表打印

//双向链表打印
void ListPrint(ListNode* pHead)
{
	ListNode* cur = pHead->next;
	printf("哨兵位<——>");
	while (cur != pHead)
	{
		printf("%d<——>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

大概思想是从哨兵位后一个节点开始遍历打印节点中需存储的数据即可(无需打印哨兵位的数据,因为那是无效数据,上面的写法很完美打印出了所有有效数据,当cur等于pHead时不进入循环内,自然不打印哨兵位的数据了。)。

5.双向链表在pos的前面进行插入

//创建双向链表节点
ListNode* ListCreate(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail->");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* newnode = ListCreate(x);
	newnode->prev = pos->prev;
	pos->prev->next = newnode;
	newnode->next = pos;
	pos->prev = newnode;
}

好好好,这个功能的实现来说,只需要通过pos->prev找到pos指向节点的前一个节点,并让新申请节点(调用ListCreate函数)的prev存储前一个节点的地址;让前一个节点的next存储新申请节点的地址;让新申请节点的next存储pos指向节点的地址;让pos指向节点的prev存储新申请节点的地址即可。

读者老爷请看图:

6.双向链表删除pos位置的节点 

// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos);
	if (pos->next == pos)
	{
		printf("不可删除哨兵位\n");
	}
	//防止删除哨兵位
	assert(pos->next != pos);
	ListNode* posnext = pos->next;
	ListNode* posprev = pos->prev;
	posnext->prev = posprev;
	posprev->next = posnext;
	free(pos);
	pos = NULL;
}

这个功能的实现也不难!我们只要分别通过pos->next和pos->prev找到pos指向节点的后一个节点和前一个节点;让后一个节点的prev存储前一个节点的地址;让前一个节点的next存储后一个节点的地址;再释放掉pos指向节点的空间。注意要断言防止删除哨兵位(当pos指向哨兵位时,pos->next等于pos,所以assert条件为假就报错了。)! 

读者老爷请看图:

7.双向链表尾插

//创建双向链表节点
ListNode* ListCreate(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail->");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	//法一
	ListNode* tail = pHead->prev;
	ListNode* newnode = ListCreate(x);
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = pHead;
	pHead->prev = newnode;

	//法二
	//ListInsert(pHead, x);
}

对于尾插,因为pHead指向哨兵位,所以通过pHead->prev找到双向链表尾节点;调用ListCreate函数申请一个新节点;让尾节点的next存储新申请节点的地址;让新申请节点的prev存储尾节点地址;让新申请节点的next存储pHead;让pHead的prev存储新申请节点地址即可。

也可以直接调用前面实现过的“双向链表再pos的前面进行插入”函数,将哨兵位的地址(pHead)和所需存储数据传参进去即可。

8.双向链表尾删

//双向链表尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	if (pHead->next == pHead)
	{
		printf("不可删除哨兵位\n");
	}
	//防止删除哨兵位
	assert(pHead->next != pHead);

	//法一
	/*ListNode* tail = pHead->prev;
	ListNode* tailprev = tail->prev;
	tailprev->next = pHead;
	pHead->prev = tailprev;
	free(tail);
	tail = NULL;*/

	//法二
	ListErase(pHead->prev);
}

对于双向链表尾删,要注意断言防止删除哨兵位!对于这个功能实现的思想大致来说就是通过pHead->prev找到尾节点;再通过尾节点的prev找到尾节点的前一个节点;让尾节点的前一个节点的next存储pHead;让pHead指向的节点(哨兵位)的prev存储位节点的前一个节点的地址;再释放掉pos指向的节点的空间即可。

也可以直接调用前面实现的“双向链表删除pos位置的节点”函数,传入pHead->prev(尾节点的地址)即可。

9.双向链表头插 

//创建双向链表节点
ListNode* ListCreate(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail->");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	//法一
	ListNode* newnode = ListCreate(x);
	ListNode* head = pHead->next;
	newnode->next = head;
	head->prev = newnode;
	newnode->prev = pHead;
	pHead->next = newnode;

	//法二
	//ListInsert(pHead->next, x);
}

对于双向链表头插,我们是往哨兵位的后一个节点的前面插入,使得插入的新申请节点成为哨兵位后一个节点。具体分析鼠鼠我就不分析了,很简单啦!也可以直接调用前面实现过的“双向链表再pos的前面进行插入”函数哈!

10.双向链表头删 

// 双向链表头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	if (pHead->next == pHead)
	{
		printf("不可删除哨兵位\n");
	}
	//防止删除哨兵位
	assert(pHead->prev != pHead);

	//法一
	ListNode* head = pHead->next;
	pHead->next = head->next;
	head->next->prev = pHead;
	free(head);
	head = NULL;

	//法二
	//ListErase(pHead->next);
}

头删的含义也是删除哨兵位后一个节点啦!我们只要做好相应节点的链接再释放哨兵位后一个节点就行了。也可以直接调用前面实现过的“双向链表删除pos位置的节点”函数。很简单,鼠鼠我偷个懒,就不分析了。

11.双向链表查找

// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

我们只要遍历查找除了哨兵位之外的所有节点的需存储数据即可,如果有需存储数据与需查找数据一致我们就返回该节点的地址,找不到就返回空指针。简简单单啦!对了,哨兵位的需存储数据本身是无效数据,当然不用查找了。

12.双向链表的小应用

老样子了,鼠鼠我呀写了三个文件,有兴趣的读者老爷可以将这三个文件放到同一个工程下面玩玩,可以更加深刻的理解上面代码的功能!

12.1.list.h

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>

typedef int LTDataType;

typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;


//初始化哨兵位
ListNode* LTInit(void);

// 双向链表销毁
void ListDestory(ListNode* pHead);

// 双向链表打印
void ListPrint(ListNode* pHead);

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);

// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);

// 双向链表尾删
void ListPopBack(ListNode* pHead);

// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);

// 双向链表头删
void ListPopFront(ListNode* pHead);

// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);

12.2.list.c

#define _CRT_SECURE_NO_WARNINGS
#include"list.h"

//创建双向链表节点
ListNode* ListCreate(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail->");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}


//初始化哨兵位
ListNode* LTInit(void)
{
	ListNode* head = ListCreate(-1);
	head->next = head;
	head->prev = head;
	return head;
}


//双向链表销毁
void ListDestory(ListNode*pHead)
{
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(pHead);
	pHead = NULL;
	cur = NULL;
}


//双向链表打印
void ListPrint(ListNode* pHead)
{
	ListNode* cur = pHead->next;
	printf("哨兵位<——>");
	while (cur != pHead)
	{
		printf("%d<——>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}


// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	//法一
	ListNode* tail = pHead->prev;
	ListNode* newnode = ListCreate(x);
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = pHead;
	pHead->prev = newnode;

	//法二
	//ListInsert(pHead, x);
}


//双向链表尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	if (pHead->next == pHead)
	{
		printf("不可删除哨兵位\n");
	}
	//防止删除哨兵位
	assert(pHead->next != pHead);

	//法一
	/*ListNode* tail = pHead->prev;
	ListNode* tailprev = tail->prev;
	tailprev->next = pHead;
	pHead->prev = tailprev;
	free(tail);
	tail = NULL;*/

	//法二
	ListErase(pHead->prev);
}


// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	//法一
	ListNode* newnode = ListCreate(x);
	ListNode* head = pHead->next;
	newnode->next = head;
	head->prev = newnode;
	newnode->prev = pHead;
	pHead->next = newnode;

	//法二
	//ListInsert(pHead->next, x);
}


// 双向链表头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	if (pHead->next == pHead)
	{
		printf("不可删除哨兵位\n");
	}
	//防止删除哨兵位
	assert(pHead->prev != pHead);

	//法一
	ListNode* head = pHead->next;
	pHead->next = head->next;
	head->next->prev = pHead;
	free(head);
	head = NULL;

	//法二
	//ListErase(pHead->next);
}


// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}


// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* newnode = ListCreate(x);
	newnode->prev = pos->prev;
	pos->prev->next = newnode;
	newnode->next = pos;
	pos->prev = newnode;
}


// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos);
	if (pos->next == pos)
	{
		printf("不可删除哨兵位\n");
	}
	//防止删除哨兵位
	assert(pos->next != pos);
	ListNode* posnext = pos->next;
	ListNode* posprev = pos->prev;
	posnext->prev = posprev;
	posprev->next = posnext;
	free(pos);
	pos = NULL;
}

12.3.test.c:

#define _CRT_SECURE_NO_WARNINGS
#include"list.h"
void menu()
{
	printf("******************************\n");
	printf("************0.退出************\n");
	printf("*******1.头插****2.尾插*******\n");
	printf("*******3.头删****4.尾删*******\n");
	printf("*******5.查找****6.打印*******\n");
	printf("*7.双向链表在pos的前面进行插入\n");
	printf("**8.双向链表删除pos位置的节点*\n");
}
int main()
{
	ListNode* plist = LTInit();
	int input = 0;
	do
	{
		menu();
		printf("请输入你想要的操作的代表数字:->");
		scanf("%d", &input);
		printf("\n");
		switch (input)
		{
		case 0:
		{
			ListDestory(plist);
			plist = NULL;
			printf("\n");
			break;
		}
		case 1:
		{
			int i = 0;
			printf("请输入你想头插的数据个数:->");
			scanf("%d", &i);
			int j = 0;
			printf("请输入你想头插的数据:->");
			for (j = 0; j < i; j++)
			{
				LTDataType x = 0;
				scanf("%d", &x);
				ListPushFront(plist, x);
			}
			printf("\n");
			break;
		}
		case 2:
		{

			int i = 0;
			printf("请输入你想尾插的数据个数:->");
			scanf("%d", &i);
			int j = 0;
			printf("请输入你想尾插的数据:->");
			for (j = 0; j < i; j++)
			{
				LTDataType x = 0;
				scanf("%d", &x);
				ListPushBack(plist, x);
			}
			printf("\n");
			break;
		}
		case 3:
		{
			ListPopFront(plist);
			printf("\n");
			break;
		}
		case 4:
		{
			ListPopBack(plist);
			printf("\n");
			break;
		}
		case 5:
		{
			LTDataType x = 0;
			printf("请输入你想查找的数据:->");
			scanf("%d", &x);
			ListNode* ret = ListFind(plist, x);
			if (ret != NULL)
			{
				printf("找到了,该节点地址是%p\n", ret);
			}
			else
			{
				printf("找不到\n");
			}
			printf("\n");
			break;
		}
		case 6:
		{
			ListPrint(plist);
			printf("\n");
			break;
		}
		case 7:
		{
			LTDataType n = 0, x = 0;
			printf("请分别输入你想插入的数据和pos指向节点的数据:->");
			scanf("%d %d", &x, &n);
			ListInsert(ListFind(plist,n), x);
			printf("\n");
			break;
		}
		case 8:
		{
			LTDataType x = 0;
			printf("请输入pos指向节点的数据:->");
			scanf("%d", &x);
			ListErase(ListFind(plist,x));
			printf("\n");
			break;
		}
		}
	} while (input);
	return 0;
}

13.ending 

好好好,看到这里的话,读者老爷如果觉得文章有不好的地方,恳请斧正,谢谢!

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

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

相关文章

Rocket-核心编程模型

RocketMQ的消息模型 深入理解RocketMQ的消息模型 RocketMQ客户端基本流程 RocketMQ基于Maven提供了客户端的核心依赖&#xff1a; <dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-client</artifactId><version&…

Adobe Acrobat DC 将PDF转曲步骤

1、编辑--更多--背景--添加 2、只需要将不透明度调为0即可。 3、工具--印刷制作 4、拼合器预览 5、只需要将下面标出来的地方勾选即可 6、可以另存为&#xff0c;不影响源文件 7、检查是否成功&#xff0c;文件--属性--字体为空&#xff0c;说明成功了 参考资料&#xff1a; …

实战系统玩转OpenGL和AI,助力实现各种高级酷炫视频特效几个技巧

随着计算机图形学和人工智能的发展&#xff0c;通过将OpenGL和AI相结合&#xff0c;我们可以实现各种令人印象深刻的高级酷炫视频特效。本文将介绍几个技巧&#xff0c;帮助您在实践中更好地应用这些技术&#xff0c;并附上相应的源码。 火焰效果: 利用OpenGL的纹理映射和着色器…

集成开发环境 PyCharm 的安装【侯小啾python领航班系列(二)】

集成开发环境PyCharm的安装【侯小啾python领航计划系列(二)】 大家好,我是博主侯小啾, 🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹…

ORA-00257: archiver error. Connect internal only, until freed 的解决方法

归档文件存储空间不足&#xff0c;导致出现该问题。 当我们将数据库的模式修改为归档模式的时候&#xff0c;如果没有指定归档目录&#xff0c;默认的归档文件就会放到Flash Recovery Area的目录&#xff0c;但是这个目录是有大小限制的&#xff0c;如果超过了这个大小&#x…

处理和分析人类语言数据-NLTK安装和使用

简介&#xff1a;NLTK&#xff08;Natural Language Toolkit&#xff09;是一个强大的Python库&#xff0c;用于处理和分析人类语言数据&#xff0c;是一个开源的项目&#xff0c;包含&#xff1a;Python模块&#xff0c;数据集和教程&#xff0c;用于NLP的研究和开发&#xff…

Python海绵宝宝

目录 系列文章 写在前面 海绵宝宝 写在后面 系列文章 序号文章目录直达链接表白系列1浪漫520表白代码https://want595.blog.csdn.net/article/details/1306668812满屏表白代码https://want595.blog.csdn.net/article/details/1297945183跳动的爱心https://want595.blog.cs…

MySQL数据库从小白到入门(一)

MySQL概述&#xff1a; MySQL连接&#xff1a; 打开cmd窗口 window r 输入 cmd输入mysql -u用户名 -p密码&#xff1b; 示例&#xff1a;mysql -uroot -p1234&#xff1b; 这种方式登录mysql&#xff0c;会出现警告&#xff0c;建议使用下面这种。mysql -uroot -p 然后回车…

sharding-jdbc实现分库分表

shigen日更文章的博客写手&#xff0c;擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。记录成长&#xff0c;分享认知&#xff0c;留住感动。 &#x1f605;&#x1f605;最近几天的状态有点不对&#xff0c;所以有几天没有更新了。 当我们的数据量比较大…

7.6 Windows驱动开发:内核监控FileObject文件回调

本篇文章与上一篇文章《内核注册并监控对象回调》所使用的方式是一样的都是使用ObRegisterCallbacks注册回调事件&#xff0c;只不过上一篇博文中LyShark将回调结构体OB_OPERATION_REGISTRATION中的ObjectType填充为了PsProcessType和PsThreadType格式从而实现监控进程与线程&a…

node.js-连接SQLserver数据库

1.在自己的项目JS文件夹中建文件&#xff1a;config.js、mssql.js和server.js以及api文件夹下的user.js 2.在config.js中封装数据库信息 let app {user: sa, //这里写你的数据库的用户名password: ,//这里写数据库的密码server: localhost,database: medicineSystem, // 数据…

C# WPF上位机开发(绘图软件)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 本身c# wpf可以看成是生产力工具&#xff0c;它的意义在于可以快速根据业务的情况&#xff0c;把产品模型搭建出来。这一点不像c/c&#xff0c;需要…

Python容器——字典

Key——Value 键值对

Nacos源码解读04——服务发现

SpringBoot自动注入 项目启动的时候会通过自动注入的机制将 NacosDiscoveryClientConfiguration注入 当注入NacosDiscoveryClientConfiguration的时候会将DiscoveryClient一起注入Bean DiscoveryClient实现了SpringCloud的DiscoveryClient接口&#xff0c;重点是getInstances和…

观察者设计模式

package com.jmj.pattern.observer;/*抽象观察者类*/ public interface Observer {void update(String message);}package com.jmj.pattern.observer;/*** 抽象主题角色*/ public interface Subject {//添加观察者对象void attach(Observer observer);//删除订阅者void detach(…

Docker的常用基本命令(基础命令)

文章目录 1. Docker简介2. Docker环境安装Linux安装 3. 配置镜像加速4. Docker镜像常用命令列出镜像列表搜索镜像下载镜像查看镜像版本删除镜像构建镜像推送镜像 5. Docker容器常用命令新建并启动容器列出容器停止容器启动容器进入容器删除容器&#xff08;慎用&#xff09;查看…

AI创作ChatGPT源码+AI绘画(Midjourney绘画)+DALL-E3文生图+思维导图生成

一、AI创作系统 SparkAi创作系统是基于ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI…

安装selenium+chrome详解

1、创建yaml文件 创建yaml文件,命名为:docker-compose-chrome.yaml,具体内容如下: version: "3.9" services:spiderdriver:image: selenium/standalone-chrome:114.0restart: alwayshostname: spiderdrivercontainer_name: spiderdriverdeploy:resources:limit…

为何全球电商都在拼“质价比”?

远在西雅图的希拉里&#xff0c;在著名的“黑色星期五”大促开始之前&#xff0c;她就已经准备好了一份购物清单。 然而&#xff0c;她发现身边的朋友们总是拉她组团购物。 在朋友和社交媒体的持续轰炸下&#xff0c;希拉里决定尝试一下这个让人贼上头的Temu。 最终&#xf…

4.OpenResty系列之Nginx负载均衡

1. 负载均衡配置 上篇文章中&#xff0c;代理仅仅指向一个服务器。但是&#xff0c;网站在实际运营过程中&#xff0c;大部分都是以集群的方式运行&#xff0c;这时需要使用负载均衡来分流。nginx 也可以实现简单的负载均衡功能。 假设这样一个应用场景&#xff1a;将应用部署…