【初阶数据结构】——带头双向循环链表(C描述)

news2024/11/16 23:51:01

文章目录

    • 前言
    • 带头双向循环链表实现
      • 1. 结构介绍
      • 2. 结点创建
      • 3. 初始化
      • 4. 销毁
      • 5. 头插
      • 6. 头删
      • 7. 尾插
      • 8. 尾删
      • 9. 打印
      • 10. 查找
      • 11. 在pos之前插入数据
      • 12. 删除pos位置
      • 13. 判空
      • 14. 计算大小
    • 源码展示
      • 1. DoubleList.h
      • 2. DoubleList.c
      • 3. Test.c

前言

上一篇文章我们学习了单链表,同时我们提到了链表其实有很多种结构:带头或不带头,循环或不循环。

但其实,最常用的还是两种结构:

在这里插入图片描述
上一篇文章我们已经学了单链表(不带头),那这篇文章,我们就来学习一下带头双向循环链表

带头双向循环链表实现

1. 结构介绍

首先,从结构上来说,带头双向循环链表是结构最复杂的:
在这里插入图片描述

它带哨兵位的头结点,还是双向的,还循环。
带头双向循环链表一般用来单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。

对于带头双向循环链表来说:

首先它是带哨兵位的头结点的,也就是说,它是空表状态的时候,也是有一个头结点存在的(当然它不存储有效数据)。
对于它的每个结点来说,首先它要能存储一个数据,然后呢?它需要有两个指针,一个存它前驱的地址,另一个存它后继的地址。

typedef int DLDataType;
typedef struct DoubleListNode
{
	struct DoubleListNode* prev;
	DLDataType data;
	struct DoubleListNode* next;
}DLNode;

它的结构虽然复杂,但是呢,使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

接下来我们就来实现一下对应的接口函数。

2. 结点创建

带头双向循环链表的每个结点:一个数据域,两个指针域。

//创建结点
DLNode* CreateNewnode(DLDataType x)
{
	DLNode* newnode = (DLNode*)malloc(sizeof(DLNode));
	if (newnode == NULL)
	{
		perror("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

相信经过单链表的学习,这个函数就不用给大家过多解释了。

3. 初始化

大家如果看了上一篇单链表的文章会发现我们在实现单链表的时候其实没有搞初始化单链表的函数。

为什么没有搞呢?

因为不需要,我们实现的是单链表,而且不带头。
对于这样一个单链表只要有一个头指针就行了,空表怎么表示,是不是头指针指向空就行了啊,创建链表的时候是不是直接在头指针后面链接结点就行了。
根本不用写初始化的函数,定义一个头指针就完事了。

那为什么我们今天实现的带头双向循环链表还要搞一个初始化的函数呢?

因为它是带头的,所以我们应该先初始化一下,先搞一个头结点出来,就算是空的状态也应该有一个头存在的,后面要插入新结点是不是应该基于头结点进行操作啊。

那初始化函数应该怎么写?

搞一个头结点就完事了吗?首先哨兵位的头结点不存储有效数据,它的数据域我们可以随便给个值。
那指针域呢?
我们新创建的结点指针域默认赋值为NULL,是指向空的。那头结点的指针域我们需要改动吗?
我们来思考一下:
头结点的两个指针域(prev,next)
在这里插入图片描述
它的prev应该是指向尾结点的,那现在还没有有效结点,只有哨兵位自己,那它自己就是尾,那就让prev指向它自己
那next呢?
如果指向空的话,是不是好像没有循环起来啊,到空就断了。而且现在只有一个哨兵位的头结点,它自己就是尾,尾的next应该指向头,而现在头也是它自己,所以我们初始化的时候让next也指向自己
我们初始化的时候让头结点的两个指针域都指向自己,首先这样更符合循环,其次这样做在后面的操作中会带来很大的优势,我们到后面就能体会到。

所以,初始化的函数应该是这样的:

//初始化
DLNode* DLInit()
{
	DLNode* guard = CreateNewnode(-1);
	//创建哨兵位的头结点(不存储有效数据)
	guard->next = guard;
	guard->prev = guard;
	return guard;
}

另外这里再给大家提一点:

我们看到初始化函数我们搞了一个返回值,为什么这样做呢?
因为我们初始化之后,有一个哨兵位的头结点在这里,我们需要有一个头指针来指向这个头结点,以便我们来访问链表。
所以我们初始化的函数需要有一个返回值,返回哨兵位结点的地址,让我们自己的指针指向它,这样就能访问这个链表。
当然,除了返回值的方法,也可以用二级指针(传头指针的地址), 这里就不实现了。
但是传一级肯定是不行的,因为形参的改变并不会影响实参。

4. 销毁

和单链表一样,每个结点的空间是我们使用malloc动态开辟的,所以是需要我们手动去释放的。

很简单,还是对链表进行遍历,一一释放每个结点。
但是,对于带头双向循环链表,遍历结束的条件是什么呢?
这一点就和单链表不一样了,我们遍历单链表的时候,定义一个cur,走到空结束,因为单链表尾结点的指针域存的是NULL。
但是,对于循环链表来说,每个结点的指针域都没有空,那怎么判断遍历结束呢?
我们要知道循环链表是怎么循环起来的,是尾结点的next指针存了头结点的地址,头结点的prev指针存了尾结点的地址。

所以:

我们定义一个指针(cur ),从哨兵位后面的第一个有效结点开始向后走(cur =cur->next),直到cur == phead时循环结束,然后再把头结点释放一下:free(phead);,就销毁完了。
在这里插入图片描述

正常情况下,phead不可能为空(即使是空表,phead指向头结点,也不为空),所以我们进行一个断言:
assert(phead);

//销毁
void DLDestory(DLNode* phead)
{
	assert(phead);
	DLNode* cur = phead->next;
	while (cur != phead)
	{
		DLNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}

对空表的时候也适用,空的时候只有哨兵位的头结点,phead指向头结点,cur = phead->next,而我们初始化的时候头结点的next就指向自己。这样cur==phead,直接就不会进while循环,只销毁一下头结点,就完事了。
如果我们初始化的时候没有让头结点的next指向自己,这样cur = phead->next之后cur 就是NULL了,就进去while循环了,反而会出现问题了。
另外:
这里我们释放完哨兵头之后虽然phead = NULL;,把头指针置空了,但其实并不会影响外面真正的头指针,还是形参与实参的关系。
所以其实函数内部这句phead = NULL;加不加都无所谓,因为函数调用结束这个指针变量也就销毁了,当然加上是一个好习惯。
但是需要我们在函数外部手动将真正的头指针置一下空,不置的话就是一个空指针了

5. 头插

带头双向循环链表的头插也非常简单,给大家看图比较直观一下:
在这里插入图片描述

void DLPushFront(DLNode* phead, DLDataType x)
{
	assert(phead);
	//DLNode* newnode = CreateNewnode(x);

	//如果不保存第一个结点的地址,改变指针指向需要注意顺序
	/*newnode->next = phead->next;
	phead->next->prev = newnode;

	phead->next = newnode;
	newnode->prev = phead;*/

	//顺序无关
	DLNode* first = phead->next;//保存第一个结点的地址
	//phead newnode first
	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = first;
	first->prev = newnode;
}

6. 头删

在这里插入图片描述
头删的时候要注意进行一个判断:

如果链表为空了,就不能再删了,那怎么判断带头双向循环链表为空呢?
如果哨兵位的头结点的prev和next指针域都指向自己,是不是就是空表啊。
所以,我们可以加一个断言:
assert(phead->next != phead);
然后,改变对应的指针指向,释放结点就行了。

//头删
void DLPopFront(DLNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	DLNode* first = phead->next;
	DLNode* second = first->next;
	//phead first second
	phead->next = second;
	second->prev = phead;
	free(first);
	first = NULL;
}

7. 尾插

我们先来回忆一下,我们上一篇文章实现的单链表,它的尾插尾删其实是很麻烦的:

单链表尾插还要进行一个判断,因为我们实现的是没有头结点的,如果是对空表尾插,直接将要插入的结点赋值给头指针即可
但对于不是空表的尾插,还要要先遍历找尾,然后让尾结点的指针域存新结点的地址,使其成为新的尾。

但是带头双向循环链表的尾插需要这么麻烦吗?

不需要的,带头双向循环链表的尾插尾删实现起来就爽多了。
首先它是带头的,空表头插我们也不需要像单链表那样单独处理,其次,单链表尾插我们还需要遍历找尾,但是对于循环链表来说,找尾简单吗?
是不是so easy啊。头结点的prev指向的不就是尾嘛,找尾一句代码就搞定了:
DLNode* tail = phead->prev;
那接下来插入就很简单了,还是对指针的改变:
在这里插入图片描述
上代码:

//尾插
void DLPushBack(DLNode* phead, DLDataType x)
{
	assert(phead);
	DLNode* newnode = CreateNewnode(x);

	DLNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;

	newnode->next = phead;
	phead->prev = newnode;
}

8. 尾删

尾删呢也很简单:
在这里插入图片描述

//尾删
void DLPopBack(DLNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//如果为空,就不能再删了
	DLNode* tail = phead->prev;
	DLNode* newtail = tail->prev;

	phead->prev = newtail;
	newtail->next = phead;
	free(tail);
	tail = NULL;
}

我们单链表尾删的时候对于只剩一个结点的情况还需要单独判断,但是对于带头双向循环链表只有一个有效结点时头删也不需要单独判断,我们直接删就行了,删完就变成初始化的状态了。
在这里插入图片描述

9. 打印

打印呢也很好搞:

对链表进行遍历,打印每个结点中的数据就行了。
而遍历结束的条件,我们在实现销毁的时候是不是就讨论过了呀,定义一个指针(cur ),从哨兵位后面的第一个有效结点开始向后走(cur =cur->next),直到cur == phead时循环结束就行了。
在这里插入图片描述

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

10. 查找

查找是不是还是对链表进行遍历啊。

假设我们要查找的元素是X,那就遍历链表,将每个结点的值与X进行对比,相同的时候就是找到了,我们可以返回该结点的地址,如果找不到,我们就返回一个NULL。
在这里插入图片描述

//查找
DLNode* DLFind(DLNode* phead, DLDataType x)
{
	assert(phead);
	DLNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

11. 在pos之前插入数据

我们直接看图:

在这里插入图片描述
那实现起来也很简单,创建一个新结点,它的数据域赋值为我们要插入的数据,然后链接起来就行了.
那有没有什么需要注意的呢?
pos是不是得是个有效的位置啊,所以我们要加一个断言:
assert(pos);
我们这个函数是和查找函数结合使用的,find函数给我们返回一个地址,我们把它传给当前函数,在该位置前面插入.
如果pos为空,说明在链表中都找不到,那还往哪插呢?

//在pos之前插入(头插尾插可以复用)
void DLInsert(DLNode* pos, DLDataType x)
{
	assert(pos);
	DLNode* pos_prev = pos->prev;
	DLNode* newnode = CreateNewnode(x);
	//pos_prev newnode pos
	pos_prev->next = newnode;
	newnode->prev = pos_prev;
	newnode->next = pos;
	pos->prev = newnode;
}

这个函数实现好之后,我们会发现:

我们前面实现的头插尾插是不是可以复用这个函数啊,因为DLInsert函数是在pos位置之前插入,这个pos可以是链表中任意一个有效位置啊,那当然可以在头尾进行插入了.

那头插尾插就可以这样简化了.

头插:

DLInsert(phead->next, x);

尾插:

DLInsert(phead, x);

尾插为什么传的是phead呢?
phead是指向头结点的指针,而在循环链表中,头结点的前面位置不就是尾嘛.

12. 删除pos位置

直接来看图:

在这里插入图片描述
这里的pos位置有没有什么限制啊?
它不能是哨兵位的头结点,既然是带头的链表,那头结点我们肯定不能删.
不过如果我们是把find的返回值传给pos , pos也不会是头结点,因为我们遍历都是从头结点后面开始的.

//删除pos位置(头插头删可以复用)
void DLErase(DLNode* pos)
{
	assert(pos);
	DLNode* pos_prev = pos->prev;
	DLNode* pos_next = pos->next;
	//pos->prev pos pos->next
	pos_prev->next = pos_next;
	pos_next->prev = pos_prev;
	free(pos);
	pos = NULL;
}

那这个函数写好,头删尾删是不是也可以复用:

头删:

DLErase(phead->next);

尾删:

DLErase(phead->prev);

所以我们以后再写的时候可以先写这两个函数,然后头部尾部的插入删除就可以直接复用了.

13. 判空

判空的话呢,也很简单:

如果头结点的prev或next指针域指向的是自己,是不是就代表此时是空的状态啊。
在这里插入图片描述

//判空
bool DLEmpty(DLNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

当然,我们这里函数的返回值用的bool类型,需要包含一个头文件:

#include <stdbool.h>

14. 计算大小

计算大小,那就遍历一下链表,计算一下元素个数就行了.

//计算大小
int DLSize(DLNode* phead)
{
	assert(phead);
	DLNode* cur = phead->next;
	int size = 0;
	while (cur != phead)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

源码展示

1. DoubleList.h

(头文件的包含、结构定义和函数声明)

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

typedef int DLDataType;
typedef struct DoubleListNode
{
	struct DoubleListNode* prev;
	DLDataType data;
	struct DoubleListNode* next;
}DLNode;

//创建结点
DLNode* CreateNewnode(DLDataType x);
//初始化
DLNode* DLInit();
//销毁
void DLDestory(DLNode* phead);
//打印
void DLPrint(DLNode* phead);
//头插
void DLPushFront(DLNode* phead, DLDataType x);
//头删
void DLPopFront(DLNode* phead);
//尾插
void DLPushBack(DLNode* phead, DLDataType x);
//尾删
void DLPopBack(DLNode* phead);
//查找
DLNode* DLFind(DLNode* phead, DLDataType x);
//在pos之前插入
void DLInsert(DLNode* pos, DLDataType x);
//删除pos位置
void DLErase(DLNode* pos);
//判空
bool DLEmpty(DLNode* phead);
//计算大小
int DLSize(DLNode* phead);

2. DoubleList.c

(函数具体实现)

#define _CRT_SECURE_NO_WARNINGS
#include "DoubleList.h"

//创建结点
DLNode* CreateNewnode(DLDataType x)
{
	DLNode* newnode = (DLNode*)malloc(sizeof(DLNode));
	if (newnode == NULL)
	{
		perror("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}
//初始化
DLNode* DLInit()
{
	DLNode* guard = CreateNewnode(-1);//创建哨兵位的头结点(不存储有效数据)
	guard->next = guard;
	guard->prev = guard;
	return guard;
}
//销毁
void DLDestory(DLNode* phead)
{
	assert(phead);
	DLNode* cur = phead->next;
	while (cur != phead)
	{
		DLNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}
//打印
void DLPrint(DLNode* phead)
{
	assert(phead);
	DLNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}
//头插
void DLPushFront(DLNode* phead, DLDataType x)
{
	assert(phead);
	//DLNode* newnode = CreateNewnode(x);

	//如果不保存第一个结点的地址,改变指针指向需要注意顺序
	/*newnode->next = phead->next;
	phead->next->prev = newnode;

	phead->next = newnode;
	newnode->prev = phead;*/

	顺序无关
	//DLNode* first = phead->next;//保存第一个结点的地址
	phead newnode first
	//phead->next = newnode;
	//newnode->prev = phead;
	//newnode->next = first;
	//first->prev = newnode;

	DLInsert(phead->next, x);//复用DLInsert
}
//头删
void DLPopFront(DLNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	//DLNode* first = phead->next;
	//DLNode* second = first->next;
	phead first second
	//phead->next = second;
	//second->prev = phead;
	//free(first);
	//first = NULL;

	DLErase(phead->next);//复用DLErase
}
//尾插
void DLPushBack(DLNode* phead, DLDataType x)
{
	assert(phead);
	//DLNode* newnode = CreateNewnode(x);

	/*DLNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;

	newnode->next = phead;
	phead->prev = newnode;*/

	DLInsert(phead, x);//复用DLInsert
}
//尾删
void DLPopBack(DLNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//如果为空,就不能再删了
	/*DLNode* tail = phead->prev;
	DLNode* newtail = tail->prev;

	phead->prev = newtail;
	newtail->next = phead;
	free(tail);
	tail = NULL;*/

	DLErase(phead->prev);//复用DLErase
}
//查找
DLNode* DLFind(DLNode* phead, DLDataType x)
{
	assert(phead);
	DLNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
//在pos之前插入(头插尾插就可以复用了)
void DLInsert(DLNode* pos, DLDataType x)
{
	assert(pos);
	DLNode* pos_prev = pos->prev;
	DLNode* newnode = CreateNewnode(x);
	//pos_prev newnode pos
	pos_prev->next = newnode;
	newnode->prev = pos_prev;
	newnode->next = pos;
	pos->prev = newnode;
}
//删除pos位置(头插头删就可以复用了)
void DLErase(DLNode* pos)
{
	assert(pos);
	DLNode* pos_prev = pos->prev;
	DLNode* pos_next = pos->next;
	//pos->prev pos pos->next
	pos_prev->next = pos_next;
	pos_next->prev = pos_prev;
	free(pos);
	pos = NULL;
}
//判空
bool DLEmpty(DLNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
//计算大小
int DLSize(DLNode* phead)
{
	assert(phead);
	DLNode* cur = phead->next;
	int size = 0;
	while (cur != phead)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

3. Test.c

(对函数功能的测试)

#define _CRT_SECURE_NO_WARNINGS
#include "DoubleList.h"
#include <stdio.h>

void test1()
{
	DLNode* phead = DLInit();

	DLPushFront(phead, 1);
	DLPushFront(phead, 2);
	DLPushFront(phead, 3);
	DLPushFront(phead, 4);
	DLPushFront(phead, 5);
	DLPrint(phead);
	DLPopFront(phead);
	DLPrint(phead);
	DLPopFront(phead);
	DLPrint(phead);
	DLPopFront(phead);
	DLPrint(phead);
	DLPopFront(phead);
	DLPrint(phead);
	DLPopFront(phead);
	DLPrint(phead);

	DLDestory(phead);
	phead = NULL;
}
void test2()
{
	DLNode* phead = DLInit();

	DLPushBack(phead, 1);
	DLPushBack(phead, 2);
	DLPushBack(phead, 3);
	DLPushBack(phead, 4);
	DLPushBack(phead, 5);
	DLPrint(phead);

	DLPopBack(phead);
	DLPrint(phead);
	DLPopBack(phead);
	DLPrint(phead);
	DLPopBack(phead);
	DLPrint(phead);
	DLPopBack(phead);
	DLPrint(phead);
	DLPopBack(phead);
	DLPrint(phead);

	DLDestory(phead);
	phead = NULL;
}

void test3()
{
	DLNode* phead = DLInit();

	DLPushBack(phead, 1);
	DLPushBack(phead, 2);
	DLPushBack(phead, 3);
	DLPushBack(phead, 4);
	DLPushBack(phead, 5);
	DLPrint(phead);

	DLNode* pos = DLFind(phead, 3);
	/*if (pos)
	{
		pos->data = 10;
	}*/
	if (pos)
	{
		DLInsert(pos, 99);
	}
	DLPrint(phead);
	/*if (pos)
	{
		printf("找到了\n");
	}
	else
	{
		printf("找不到\n");
	}*/

	DLDestory(phead);
	phead = NULL;
}
void test4()
{
	DLNode* phead = DLInit();

	DLPushBack(phead, 1);
	DLPushBack(phead, 2);
	DLPushBack(phead, 3);
	DLPushBack(phead, 4);
	DLPushBack(phead, 5);
	DLPrint(phead);

	DLNode* pos = DLFind(phead, 3);
	if (pos)
	{
		DLErase(pos);
	}
	DLPrint(phead);

	DLDestory(phead);
	phead = NULL;
}
void test5()
{
	DLNode* phead = DLInit();

	DLPushBack(phead, 1);
	DLPushBack(phead, 2);
	DLPushBack(phead, 3);
	DLPushBack(phead, 4);
	DLPushBack(phead, 5);
	DLPrint(phead);
	printf("元素个数:%d\n", DLSize(phead));

	DLPopBack(phead);
	DLPopBack(phead);
	DLPopBack(phead);
	DLPopBack(phead);
	DLPrint(phead);
	if (DLEmpty(phead))
		printf("空表\n");
	DLPopBack(phead);
	DLPrint(phead);
	if (DLEmpty(phead))
		printf("空表\n");

	DLDestory(phead);
	phead = NULL;
}
int main()
{
	test5();
	return 0;
}

好了,对带头双向循环链表的讲解就到这里,欢迎大家指正!!!
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot自动装配原理

目录一、前言二、SpringBoot自动装配核心源码2.1、SpringBootApplication2.2、EnableAutoConfiguration2.3、Import(AutoConfigurationImportSelector.class)2.3.1、selectImports方法2.3.2、getAutoConfigurationEntry方法2.3.3、getCandidateConfigurations方法2.3.4、Spring…

在阿里云 ACK 上部署 EMQX MQTT 服务器集群

云进入以「应用为中心」的云原生阶段&#xff0c;Operator 模式的出现&#xff0c;则为 Kubernetes 中的自动化任务创建配置与管理提供了一套行之有效的标准规范。通过将运维知识固化成高级语言 Go/Java 代码&#xff0c;使得运维知识可以像普通软件一样交付&#xff0c;并能支…

欧姆龙NJ/NX基于Sysmac Studio的EIP通讯 方式

目录 Omorn - NJ301-1100 AND NX102-9000 EIP - Sysmac Studio 测试案例IP 创建变量类型 通讯配置 控制器程序下载 通讯测试 Omorn - NJ301-1100 AND NX102-9000 EIP - Sysmac Studio 测试案例IP 创建变量类型 通讯配置 控制器程序下载 通讯测试 Omorn - NJ301-1100…

Go语言快速入门笔记

文章目录import匿名导包和别名导包的方式defer语句数组和动态数组固定长度数组切片&#xff08;动态数组&#xff09;切片的容量追加和截取map面向对象struct继承多态interface空接口万能类型与类型断言机制变量的内置pair结构变量结构reflect包(反射)reflect反射解析结构体标签…

【Java毕设】基于idea Java的在线考试系统(附源码+课件)

项目介绍&#xff1a; 本系统是一个基于java的在线考试系统。它的用户由学生、教师和系统管理员组成。学生登陆系统可以进行在线测试和成绩查询。当学生登陆时&#xff0c;系统会随机地为学生选取试题组成考卷。当学生提交考卷后&#xff0c;系统会自动批改客观题&#xff0c;…

html实现爱情告白(附源码)

文章目录1.设计来源1.1 主界面1.2 执子之手&#xff0c;与子偕老1.3 死生契阔&#xff0c;与子成说1.4 生当复来归&#xff0c;死当长相思1.5 自君之出矣&#xff0c;明镜暗不治1.6 思君如流水&#xff0c;何有穷已时1.7 南有乔木&#xff0c;不可休思1.8 汉有游女&#xff0c;…

快递查询工具,一键查物流,派件时效怎么分析

快递发货后&#xff0c;该如何快速查询到物流信息、比如怎么分析派件时效呢&#xff1f;今天小编给大家分享一个新的技巧&#xff0c;它支持多家快递&#xff0c;一次能查询多个单号物流&#xff0c;还能对查询到的物流进行分析、导出以及筛选&#xff0c;下面一起来试试吧。 …

3000万人气的腾格尔,会和金鸡奖提名电影《巴林塔娜》合作吗

刚刚结束的2022年11月19日&#xff0c;对于“草原歌神”腾格尔来说&#xff0c;注定是要被载入史册的一天。2022年11月19日&#xff0c;是卡特尔世界杯开幕式的前一夜&#xff0c;腾格尔老师也通过某音平台&#xff0c;开启了自己的线上演唱会。 说起明星们的演唱会&#xff0c…

redis 登录案例

下图就是登录controller Controller public class LoginController {RequestMapping("/login")public String Login(String username, String password, HttpServletResponse response){System.out.println(username);System.out.println(password);//判断账号密码 …

微信小程序 | IM交友聊天功能大汇总

&#x1f4cc;个人主页&#xff1a;个人主页 ​&#x1f9c0; 推荐专栏&#xff1a;小程序开发成神之路 --【这是一个为想要入门和进阶小程序开发专门开启的精品专栏&#xff01;从个人到商业的全套开发教程&#xff0c;实打实的干货分享&#xff0c;确定不来看看&#xff1f; …

关系数据库系统中的 NULL 值及其用途

在数据库中&#xff0c;NULL值具有非常特殊的含义。因此&#xff0c;重要的是要理解NULL值不同于零值或包含空格的字段。在今天的博客中&#xff0c;我们将探讨 NULL 值的含义以及如何在 Navicat Premium 中使用NULL。 什么是NULL&#xff1f; 应该注意的是&#xff0c;NULL值…

Linux上部署Kubectl(k8s)

Linux上部署Kubectl(k8s) 1.k8s简介 1.1 Kubernetes 概念 在 k8s 上进行部署前&#xff0c;首先需要了解一个基本概念 Deployment Deployment 译名为 部署。在k8s中&#xff0c;通过发布 Deployment&#xff0c;可以创建应用程序 (docker image) 的实例 (docker container)…

跑步需要哪些运动装备?跑步爱好者者的装备推荐

一开始我认为跑步是不需要装备的&#xff0c;毕竟是基础运动&#xff0c;但问了一下身边的运动大神才明白在长期的跑步锻炼&#xff0c;特别是长跑的过程中好的装备不但可以保护你免受伤害&#xff0c;还能帮助你更好的掌握运动状态&#xff0c;进行合理的锻炼下面我就给大家列…

[附源码]java毕业设计网上书店的设计

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

vite+vue-router4.x配置动态路由

踩过的坑&#xff1a; import直接导入组件; router.addRoute 并不能一次性给你导入&#xff08;即不是vue-router3.x以下的addRoutes&#xff09;&#xff1b; addRoute后页面空白&#xff1b; 直接上才艺&#xff01; 我的设计思路是登录后获取token&#xff0c;并存入cookie…

bizlog通用操作日志组件(代码分析篇)

引言 在上篇博客中介绍了通用操作日志组件的使用方法&#xff0c;本篇博客将从源码出发&#xff0c;学习一下该组件是如何实现的。 代码结构 该组件主要是通过AOP拦截器实现的&#xff0c;整体上可分为四个模块&#xff1a;AOP模块、日志解析模块、日志保存模块、Starter模块…

企业小程序商城的推广方式有哪些_分享小程序商城的作用

其实搭建小程序商城比较容易&#xff0c;难的是后期的运营。要想办法进行引流&#xff0c;用户运营伙伴就给大家介绍一些引流推广的方法。 1、利用微信好友、微信群和朋友圈 可以让用户分享小程序给微信好友或微信群&#xff0c;这是吸引新用户的最快方法。除此之外&#xff0…

Kettle入门到实战

简介 Kettle是一个方便ETL(数据的抽取&#xff0c;装换&#xff0c;装载)开源框架。 官网 kettle下载、kettle源码下载 – Kettle中文网 百度网盘下载 链接&#xff1a;https://pan.baidu.com/s/1C-izMX_3KMkRb5hhdj66xg 提取码&#xff1a;yyds --来自百度网盘超级会员…

go radix tree

Radix Tree Search Insert Insert ‘water’ at the root Insert ‘slower’ while keeping ‘slow’ Insert ‘test’ which is a prefix of ‘tester’ Insert ‘team’ while splitting ‘test’ and creating a new edge label ‘st’ Insert ‘toast’ while splitti…

java 多线程()—— 线程同步=队列+锁

一、线程同步 队列 锁 同步就是多个线程同时访问一个资源。 那么如何实现&#xff1f; 队列锁。 想要访问同一资源的线程排成一个队列&#xff0c;按照排队的顺序访问。访问的时候加上一个锁&#xff08;参考卫生巾排队锁门&#xff09;&#xff0c;访问完释放锁。 二、 不…