数据结构之双向链表(赋源码)

news2025/1/11 19:46:29

数据结构之双向链表(源码)

线性表

双向链表是线性表链式存储结构的一种,若对链式存储结构进行分类可以分为八种。

  • 带头、不带头:指的是该连链表有无头节点,头节点不存放任何内容,它不一定是链表必备的元素,而一个链表拥有一个头节点就可以对后续的插入、删除等操作进行统一而不需要判空等情况。
  • 单向、双向:双向链表与单向链表的区别在于,它多了一个指针用来存放上一个节点的指针,这样就可以通过一个节点任意的向前、向后遍历。
  • 循环、不循环:链表的循环结构是将不循环链表的尾节点从指向空指针改为指向头指针。完成链表的自循环。

在这里插入图片描述

本篇所述:带头双向循环链表,简称双链表,双链表和单链表(不带头单向不循环链表)是八种链表中常用的两个链表,

双向链表

双链表结构

双链表是一个带头双向循环链表,更据它的特性不能想出,在一个双链表的一个节点里它的指针域用来存放前一个节点的地址和存放下一个节点的地址

typedef int ListNodeDataType;
typedef struct ListNode
{
	ListNodeDataType data;
	struct ListNode* prev;
	struct ListNode* next;
}ListNode;

prev是前驱指针存放前一个节点的地址,next是后继指针存放下一个节点的指针,而双链表是循环链表,尾节点没有下一个节点,它是指向头节点,头节点的prec指针指向尾节点。

功能实现

//初始化
void ListNodeInit2(ListNode** pphead);
ListNode* LTInit();
//打印
void ListPrint(ListNode* phead);
//尾插
void ListNodePushBack(ListNode* phead, ListNodeDataType x);
//头插
void ListNodePushFront(ListNode* phead, ListNodeDataType x);
//尾删
void ListNodePopBack(ListNode* phead);
//头删
void ListNodePopFront(ListNode* phead);
//查找
ListNode* ListNodeFind(ListNode* phead, ListNodeDataType x);
//指定位置删除
void ListNodeErase(ListNode* pos);
//指定位置插入(之后)
void ListNodeInsert(ListNode* pos, ListNodeDataType x);
//销毁
void ListDestory(ListNode* phead);
void ListDestory2(ListNode** pphead);

创建节点、初始化、打印

创建节点

//创建节点
ListNode* ListBuyNode(ListNodeDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc :");
		exit(1);
	}
	newnode->data = x;
	newnode->next = newnode;
	newnode->prev = newnode;
}

我们说双链表是由一个一个的节点组成,而节点里存放的有数据,前驱指针用来存放上一个节点的地址和后继指针

用来存放下一个节点的地址,在创建节点时,使用malloc函数开辟空间即可,realloc适合对一块连续存储的空间开辟新的更大的空间,而节点用不是realloc函数,它们在物理空间上不一定是连续的。

使用malloc函数开辟完空间后别忘了判断是否开辟成功,这是为了养成一个良好的习惯,如今的计算机想要开辟空间是否,一般都是内存被用完了 。

最后,将x值放入节点里,由于它只是一个节点,没有前驱节点,和后继节点,而双向链表是循环链表所以让这两个指针指向自己,实现自循环,而不会去指向空指针。

初始化

//初始化
ListNode* LTInit()
{
	ListNode* phead = ListBuyNode(-1);
	return phead;
}
//  初始化
void ListNodeInit2(ListNode** pphead)
{
	*pphead = ListBuyNode(-1);
}

初始化头节点,使双链表带头,为了后续插入、删除等操作同意而设置的,数据域一般无意义,在链表里不一定有头节点。

初始化双链表有两种写法:

  • 创建头节点让将其地址返回

    • 不需要传递参数,调用创建节点函数后将开辟的空间返回即可
    • 头节点不需要存储有效信息,所以在调用ListBuyNode函数是任意传递了一个整形变量。
  • 将头节点声明后传递它的地址给ListNodeInit2初始化函数,在函数内完成空间的开辟。

    • 需要传递参数,且是二级指针,指向双链表的变量已经是一级指针,想要改变一级指针的内容必须取出其地址,使用二级指针接收。
    • 传递的是二级指针在函数内对形参的开辟,影响到了实参,不需要将地址返回。

再重述一遍,指向双链表的变量是一级指针,也就是头指针它是链表不可缺少的元素,链表里有头节点他就指向头节点,没有头节点它就指向第一个节点。想要对头指针修改,得传递它的地址,才能使函数内对形参的改变影响到实参。而一级指针的地址需要二级指针接收。

打印

//打印
void ListPrint(ListNode* phead)
{
	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

提前将打印功能的实现,是为了方便后续对双链表,插入数据、删除数据进行更方便的观察,而不是每一次调用插入、删除等函数都使用vs的调试功能。

首先、在打印单链表时,同样使用了循环,创建了一个临时指针来指向第一个节点,打印单链表数据的接收条件是pcur指向空时停止。而双链表是一个循环链表它没有一个节点的指针是指向空的。

所以这里以pcur指向头节点时为循环结束条件 pcur != phead, 它并不存放有效数据,不用打印它。

在打印时需要从第一个节点开始打印所以pcur指针的起始位置是 ListNode* pcur = phead->next;,通过将pcur的下一个节点的地址赋给它,完成自循环 pcur = pcur->next;

查找

//查找
ListNode* ListNodeFind(ListNode* phead, ListNodeDataType x)
{
	assert(phead);
	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

查找,数据有着比较重要的地位,涉及到了指定位置(之前)插入,和指定位置删除等。

双链表的查找很好理解,想要查找一个元素,只需要将链表里每一个数据一一比较大小,若两者刚好相等,这就是需要查找的数据,然后将其地址返回即可,若跳出循环后并没有找到对应的数据,返回空指针。

使用assert断言,防止传递的头指针为空,并在函数内对空指针解引用从而引发一系列报错。

循环遍历每一个节点时,并不会查找头节点,将临时指针的起始位置设为第一个节点 ListNode* pcur = phead->next;,在循环内使用if语句进行判断,形参x和pcur内的data大小是否相等,相等将其地址返回,若不等继续循环判断,出了循环后没有找到,返回空指针即可。

插入

插入函数,传递的都是一级指针,为啥不传递二级指针捏,执行对节点的插入并不会影响到头指针的指向,传递一级指针即可。

插入分为:

  • 头插、尾插
  • 指定位置之前插入、指定位置之后插入

在实现节点的插入时实现需要考虑的是,我插入如这个节点会影响到那些节点,该如何选择插入的顺序将影响最小化。

尾插

//尾插
void ListNodePushBack(ListNode* phead, ListNodeDataType x)
{
	assert(phead);
	ListNode* newnode = ListBuyNode(x);
	newnode->next = phead;
	newnode->prev = phead->prev;
	phead->prev->next = newnode;
	phead->prev = newnode;
}

在这里插入图片描述

第一步:使用assert断言判断,头指针是否为空,为空运行程序就会报错。

第二步:如图,想要在尾节点插入一个新的节点,那就需要创建新的节点调用 ListBuyNode函数。

第三步:首先尾插一个节点会影响到的节点有:phead(头节点)、phead->prev

以及待插入的newnode新节点,会影响到phead的prev指针、phead->next的next指针,newnode的next、prev指针,

其中影响最小的是新节点newnode的两个指针,先改变它们的指向,让其prev指向最后一个节点,next指向头指针
在这里插入图片描述

然后再改变phead->prev指向的节点,也就是尾节点,先改变尾节点是由于提前将尾节点的prev指针改为newnode,那就无法通过头指针找到尾节点了。phead->prev->next = newnode;,这里让尾节点的next指针的指向从头节点改为newnode。

所以先改变头指针之外的节点,然后再改变头指针。

改变头指针里prev的指向,让其指向新的节点,这样就完成了所有节点的改变,成功插入了新的节点。

头插

//头插
void ListNodePushFront(ListNode* phead, ListNodeDataType x)
{
	assert(phead);
	ListNode* newnode = ListBuyNode(x);
	newnode->next = phead->next;
	newnode->prev = phead;
	phead->next->prev = newnode;
	phead->next = newnode;
}

头插与尾插及其类似,只是改变了插入如新节点的位置。

第一步:使用assert断言判断,头指针是否为空,为空运行程序就会报错。

第二步:第二步:如图,想要在尾节点插入一个新的节点,那就需要创建新的节点调用 ListBuyNode函数。

通过代码观察可以发现,再头插和尾插里的第一步和第二步一模一样,当然指定位置之后插入的逻辑也是一样的。

最主要的区别还是它们影响的节点不同。对新节点进行头插,主要影响到头节点phead和头节点的下一个节点(第一个节点)phead->next

有了尾插的经验,插入头节点时。首先改变新节点的next、prev指针的指向让prev指向头节点 newnode->prev = phead;,和next指针指向第一个节点 newnode->next = phead->next;,完成了头节点的改变,接着就改变,第一个节点的prev指针,让它指向newnode,最后改变头节点的next指针让其指向newnode。
在这里插入图片描述

指定位置之后插入

//指定位置插入(之后)
void ListNodeInsert(ListNode* pos, ListNodeDataType x)
{
	assert(pos);
	//pos newnode pos->next
	ListNode* newnode = ListBuyNode(x);
	newnode->next = pos->next;
	newnode->prev = pos;
	pos->next->prev = newnode;
	pos->next = newnode;
}

指定位置之后插入,一开始的断言的创建新节点的操作理解是一样的,最主要的区别是,指定位置插入影响的节点不同,而指定位置之后插入的实现,我只能说与头插基本上没有区别,在代码上只是将phead指针改为了pos指针。

首先,在pos之后插入新的节点newnode,它会影响到pos节点,pso->next两个节点,其中需要改变pos的next指针的指向,以及pos->next->prev指针的指向,需要它们指向新的节点newnode,而newnode的next指针需要指向pos的下一个节点,pos->next,和newnode的prev指针需要指向pos节点。这样就完成了对新节点的插入。其逻辑与头插无异。

删除

判空

bool ListEmpty(ListNode* phead)
{
	assert(phead); 
	return phead->next == phead;
}

在对删除双链表时有一种特殊情况,就是只有一个头节点,这时双链表被看作是空链表,这与前文实现的单链表不同,单链表是需要将所有节点删除才为空,因为它没有头节点,这才将所有节点删除。

对于这种特殊情况做出了判断,当头节点的next指针指向自己的时候说明双链表里只有一个头节点,此时双链表为空,返回true,双链表不为空的话返回false。

再删除函数里都需要调用这种方法,以免错误的将头节点删除。

使用assert断言,进行判断 assert(!ListEmpty(phead));,若双链表为空返回true,!true,而为假,运行代码时直接报警告。

尾删

//尾删
void ListNodePopBack(ListNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));
	//phead phead->prev  phead->prev->prev
	ListNode* del = phead->prev;
	phead->prev = del->prev;
	del->prev->next = phead;
	free(del);
	del = NULL;
}

尾删,首先考虑的是删除尾节点会影响到那些节点,而这些节点有需要做出那些改变,改变的顺序

影响的节点:删除尾节点会影响到头节点 phead,待删除的节点phead->perv,尾节点的前一个节点phead->prev->prev.

为了避免出现过多的结构体访问操作符从而降低代码的可读性,以及方边后续的删除操作,这里将 ListNode* del = phead->prev;

删除节点重命名,也就是del节点。这样 phead->prev->prev,又为 del->prev
在这里插入图片描述

如图需要对del节点进行删除,首先需要改变 del->prev节点的next指针,然后再改变头节点的prev指针,这是为了先将头节点的prev指针修改后而找不到del节点,但不过我们以及提前将del节点保存的所以不用担心它的发生。

接下来就是修改指针的指向了,先修改 del前一个节点的指向del->prev,删除尾节点后他就是新的尾,将它的next指向头节点 del->prev->next = phead;,然后修改头节点的prec指针让其指向新的尾节点,最后将del节点释放即可,释放完别忘了将其置为空,他此时可是一个野指针~。

头删

//头删
void ListNodePopFront(ListNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));
	//phead phead->next phead->next->next
	ListNode* del = phead->next;
	phead->next = del->next;
	del->next->prev = phead;
	free(del);
	del = NULL;
}

头删的逻辑与尾删类似,首先删除头节点会影响到那些节点:pheadphead->nextphead->next->next,这三个节点,同样为了避免出现过多的结构体访问操作符,以及后续的操作,将 phead->next,保存下来赋给del指针。
在这里插入图片描述

将这两个指针撇开重新指向新的节点
在这里插入图片描述

按照图示,需要将 del->next,的prev指针指向头节点,将头节点的next指针指向新的next节点。

将指针的指向修改完后,将del释放即完成了头删操作,别忘了将del置空。

指定位置删除

//指定位置删除
void ListNodeErase(ListNode* pos)
{
	assert(pos);
	assert(!ListEmpty(pos));
	//pos->prev pos pos->next
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;

}

指定位置删除的思考逻辑与头删尾删一样,首先考虑删除指定的节点pos会影响到那些节点:pos->prev pos pos->next,这三个节点

在这里插入图片描述

pos->prev 节点的next指针撇开和, pos->next的prec指针撇开,这样该是容易理解的多。

  • 然后改变其指向,让pos->prev 节点的next指针指向 pos->next节点。

  • pos->next节点的prev指针指向 pos->prev 节点。

  • 最后将pos释放,置空即可。

在这里插入图片描述

销毁

//销毁
void ListDestory(ListNode* phead)
{
	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		ListNode* nextnode = pcur->next;
		free(pcur);
		pcur = nextnode;
	}
	phead = NULL;
	pcur = NULL;
}

void ListDestory2(ListNode** pphead)
{
	ListNode* pcur = (*pphead)->next;
	while(pcur != *pphead)
	{
		ListNode* nextnode = pcur->next;
		free(pcur);
		pcur = nextnode;
	}
	*pphead = NULL;
	pcur = NULL;
}

接口:

销毁函数的功能是将双链表所有内容全部清除,包括头节点。所以不许要调用判空函数。而销毁函数的两种方法一是传递二级指针,而是传递一级指针,这里与之前的初始化函数类似,分为了两种情况。而更推荐使用一级指针。

其原因是为了保证双链表接口的一致性,接口:现阶段不考虑吧过多的话,接口就是指函数。

保证接口的一致性是为了当实现的函数功能过多时,一些函数传递一级指针,一些函数传递二级指针,为了防止调用者的混淆,所以这里将双链表的所有功能均采用一级指针来实现。

销毁:

在进行销毁时需要创建一个临时变量用来遍历所有节点进行销毁,这里销毁的逻辑与单链表的销毁的逻辑时一样的,然后再循环内遍历销毁时需要使用一个next指针一便于将pcur释放后重新找到下一个节点释放,循环结束条件为 pcur != *pphead

最后将节点释放完后将pcur置为空,此时它为空指针。

缺陷

前文说过,需要对头节点进行更改就需要传递它的地址,也就是二级指针,而这里使用一级指针来实现对双链表的销毁就丢失了这种特性,所以在销毁完双链表后,还需要手动的将头节点置为空。

总结

总的来说,在实现双链表的算法时,在插入和删除上优先考虑的是插入一个节点会影响到那些节点、删除一个节点又会影响到那些节点,以及被影响节点的指针的指向。这里最好画图加以理解。

在插入、删除、查找等功能里均使用assert断言,这样做的目的是提高函数的健壮性、而不是在传递空指针时函数无法解决而产生一系列未知异常的情况。

还强调了在实现函数功能是统一双链表的一致性、这样虽然保证了所有函数传递的都是一级指针,但不可否认的是这样又会丢失一些功能,需要手动去实现,如传递一级指针,将一个双链表销毁后需要手动置空。

源码

List.h

#pragma once

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

typedef int ListNodeDataType;
typedef struct ListNode
{
	ListNodeDataType data;
	struct ListNode* prev;
	struct ListNode* next;
}ListNode;
//初始化
void ListNodeInit2(ListNode** pphead);
ListNode* LTInit();
//打印
void ListPrint(ListNode* phead);
//尾插
void ListNodePushBack(ListNode* phead, ListNodeDataType x);
//头插
void ListNodePushFront(ListNode* phead, ListNodeDataType x);
//尾删
void ListNodePopBack(ListNode* phead);
//头删
void ListNodePopFront(ListNode* phead);
//查找
ListNode* ListNodeFind(ListNode* phead, ListNodeDataType x);
//指定位置删除
void ListNodeErase(ListNode* pos);
//指定位置插入(之后)
void ListNodeInsert(ListNode* pos, ListNodeDataType x);
//销毁
void ListDestory(ListNode* phead);
void ListDestory2(ListNode** pphead);

List.c

#define _CRT_SECURE_NO_WARNINGS
#include "List.h"

ListNode* ListBuyNode(ListNodeDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc :");
		exit(1);
	}
	newnode->data = x;
	newnode->next = newnode;
	newnode->prev = newnode;
}
ListNode* LTInit()
{
	ListNode* phead = ListBuyNode(-1);
	return phead;
}
void ListNodeInit2(ListNode** pphead)
{
	*pphead = ListBuyNode(9);
}
//打印
void ListPrint(ListNode* phead)
{
	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

//尾插
void ListNodePushBack(ListNode* phead, ListNodeDataType x)
{
	assert(phead);
	ListNode* newnode = ListBuyNode(x);
	newnode->next = phead;
	newnode->prev = phead->prev;
	phead->prev->next = newnode;
	phead->prev = newnode;
}
//头插
void ListNodePushFront(ListNode* phead, ListNodeDataType x)
{
	assert(phead);
	ListNode* newnode = ListBuyNode(x);
	newnode->next = phead->next;
	newnode->prev = phead;
	phead->next->prev = newnode;
	phead->next = newnode;
}

bool ListEmpty(ListNode* phead)
{
	assert(phead); 
	return phead->next == phead;
}

//尾删
void ListNodePopBack(ListNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));
	//phead phead->prev  phead->prev->prev
	ListNode* del = phead->prev;
	phead->prev = del->prev;
	del->prev->next = phead;
	free(del);
	del = NULL;
}
//头删
void ListNodePopFront(ListNode* phead)
{
	assert(phead);
	assert(!ListEmpty(phead));
	//phead phead->next phead->next->next
	ListNode* del = phead->next;
	phead->next = del->next;
	del->next->prev = phead;
	free(del);
	del = NULL;
}

//查找
ListNode* ListNodeFind(ListNode* phead, ListNodeDataType x)
{
	assert(phead);
	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

//指定位置删除
void ListNodeErase(ListNode* pos)
{
	assert(pos);
	assert(!ListEmpty(pos));
	//pos->prev pos pos->next
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;

}
//指定位置插入(之后)
void ListNodeInsert(ListNode* pos, ListNodeDataType x)
{
	assert(pos);
	//pos newnode pos->next
	ListNode* newnode = ListBuyNode(x);
	newnode->next = pos->next;
	newnode->prev = pos;
	pos->next->prev = newnode;
	pos->next = newnode;
}

//销毁
void ListDestory(ListNode* phead)
{
	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		ListNode* nextnode = pcur->next;
		free(pcur);
		pcur = nextnode;
	}
	phead = NULL;
	pcur = NULL;
}
void ListDestory2(ListNode** pphead)
{
	ListNode* pcur = (*pphead)->next;
	while(pcur != *pphead)
	{
		ListNode* nextnode = pcur->next;
		free(pcur);
		pcur = nextnode;
	}
	*pphead = NULL;
	pcur = NULL;
}

test.c

#define _CRT_SECURE_NO_WARNINGS
#include "List.h"
void rest()
{
	ListNode* head = NULL;
	//ListNodeInit2(&head);
	head = LTInit();
	//尾插
	ListNodePushBack(head, 1);
	ListNodePushBack(head, 2);
	ListNodePushBack(head, 3);
	ListNodePushBack(head, 4);
	ListPrint(head);
	//头插
	//ListNodePushFront(head, 1);
	//ListNodePushFront(head, 2);
	//ListNodePushFront(head, 3);
	//ListNodePushFront(head, 4);
	//ListPrint(head);

	//尾删
	//ListNodePopBack(head);
	//ListPrint(head);
	//ListNodePopBack(head);
	//ListPrint(head);
	//ListNodePopBack(head);
	//ListPrint(head);
	//ListNodePopBack(head);
	//ListPrint(head);
	
	//头删
	//ListNodePopFront(head);
	//ListPrint(head);
	//ListNodePopFront(head);
	//ListPrint(head);
	//ListNodePopFront(head);
	//ListPrint(head);
	//ListNodePopFront(head);
	//ListPrint(head);
	ListNode* ret = ListNodeFind(head,4);
	if (ret != NULL)
	{
		printf("有了!\n");
	}
	else
	{
		printf("没有!\n");
	}
	//ListNodeErase(ret);
	//ListNodeInsert(ret, 6);
	ListPrint(head);

	//ListDestory2(&head);
	ListDestory(head);
	head = NULL;

}
int main()
{
	rest();
	return 0;
}


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

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

相关文章

ros2--launch

是什么 ros2的多节点启动工具。 作用 通过launch工具执行launch文件&#xff0c;可以启动launch文件中配置的多个节点&#xff0c;以及这些启动的节点配置数据。 launch文件的实现 ROS2的launch文件有三种格式&#xff0c;python、xml、yaml。 链接 python语言创建launch文…

Elasticsearch 批量更新

Elasticsearch 批量更新 准备条件查询数据批量更新 准备条件 以下查询操作都基于索引crm_flow_info来操作&#xff0c;索引已经建过了&#xff0c;本文主要讲Elasticsearch批量更新指定字段语句&#xff0c;下面开始写更新语句执行更新啦&#xff01; 查询数据 查询指定shif…

微服务实战系列之玩转Docker(三)

前言 镜像&#xff08;Image&#xff09;作为Docker的“水源”&#xff0c;取之于它&#xff0c;用之于它。这对于立志成为运维管理的撒手锏——Docker而言&#xff0c;重要性不言而喻。 我们在虚拟机时代&#xff08;当然现在依然ing…&#xff09;&#xff0c;如何快速完成…

硅谷裸机云多IP服务器怎么样?

硅谷裸机云多IP服务器是一种在硅谷地区提供的、具有多个IP地址的裸机云服务器。这种服务器结合了裸机服务器的高性能和云服务器的灵活性&#xff0c;同时提供了多个IP地址&#xff0c;为用户的各种需求提供了支持。以下是关于硅谷裸机云多IP服务器的一些详细信息&#xff0c;ra…

3.RabbitMQ安装-Centos7

官方网址&#xff1a;gInstalling RabbitMQ | RabbitMQ 安装前提&#xff0c;需要一个erlang语言环境。 下载 erlang: Releases rabbitmq/erlang-rpm GitHub rabbitmq-server: 3.8.8 Releases rabbitmq/rabbitmq-server GitHub 安装命令 (说明i表示安装&#xff…

华清数据结构day2 24-7-17

1> 完成班级的创建&#xff0c;创建时&#xff0c;需要传递班级实际人数 2> 完成班级学生的信息录入工作 3> 完成将班级学生按成绩进行降序排序工作 4> 输出班级中成绩最好和最差学生的信息 5> 完成信息的输出工作 6> 完成班级的销毁工作 要求&#xff1a;班…

基于springboot+vue+uniapp的驾校预约平台小程序

开发语言&#xff1a;Java框架&#xff1a;springbootuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#…

小程序图片下载保存方法,图片源文件保存!

引言 现在很多时候我们在观看到小程序中的图片的时候&#xff0c;想保存图片的原文件格式的话&#xff0c;很多小程序是禁止保存的&#xff0c;即使是让保存的话&#xff0c;很多小程序也会限制不让保存原文件&#xff0c;只让保存一些分辨率很低的&#xff0c;非常模糊的图片…

leetcode简单题27 N.119 杨辉三角II rust描述

// 直接生成杨辉三角当前行 pub fn get_row(row_index: i32) -> Vec<i32> {let mut row vec![1; (row_index 1) as usize];for i in 1..row_index as usize {for j in (1..i).rev() {row[j] row[j] row[j - 1];}}row } // 空间优化的方法 pub fn get_row2(row_ind…

数据结构——线性表(循环链表)

一、循环链表定义 将单链表中终端结点的指针端由空指针改为指向头结点&#xff0c;就使整个单链表形成一 个环&#xff0c;这种头尾相接的单链表称为单循环链表&#xff0c;简称循环链表(circular linked list)。 循环链表解决了一个很麻烦的问题。如何从当中一 个结点出发&am…

stm32学习:(寄存器3)系统架构

时钟系统 时钟树 在STM32中有3种不同的时钟源用来驱动系统时钟(SYSCLK)&#xff1a; HSI振荡器时钟&#xff08;High Speed Internal oscillator&#xff0c;高速内部时钟&#xff09;HSE振荡器时钟&#xff08;High Speed External&#xff08;Oscillator / Clock&#xff…

RK3588读取不到显示器edid

问题描述 3588HDMIout接老的显示器或者HDMI转DVI接DVI显示器显示不了或者显示内容是彩色条纹,但是这种显示器测试过如果接笔记本或者主机是可以直接显示的。这一类问题是HDMI下的i2c与显示器通讯没成功,读取不到设备的edid。问题包括全志的H3 、AML的S905都有遇到 测试环境…

基于Web的特产美食销售系统的设计与实现

&#x1f497;博主介绍&#x1f497;&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 温馨提示&#xff1a;文末有 CSDN 平台官方提供的老师 Wechat / QQ 名片 :) Java精品实战案例《700套》 2025最新毕业设计选题推荐…

用太空办公桌spacedesk把废旧平板利用起来

正文共&#xff1a;1500 字 15 图&#xff0c;预估阅读时间&#xff1a;2 分钟 这些年积攒了不少电子设备&#xff0c;比如我现在手头上还有6部手机、4台电脑、2个平板。手机的话&#xff0c;之前研究过作为Linux服务器来使用&#xff08;使用UserLAnd给华为平板装个Linux系统&…

网络安全(含面试题版)

一、网络概念 网络&#xff1a;一组相互连接的计算机&#xff0c;多台计算机组成&#xff0c;使用物理线路进行连接 作用&#xff1a; 数据交换 资源共享 二、网络分类 计算机网络覆盖的地理区域决定了它的类型。一般分为局域网(LAN)、城域网(MAN)、广域网(WAN)。 三、www万维网…

分享 .NET EF6 查询并返回树形结构数据的 2 个思路和具体实现方法

前言 树形结构是一种很常见的数据结构&#xff0c;类似于现实生活中的树的结构&#xff0c;具有根节点、父子关系和层级结构。 所谓根节点&#xff0c;就是整个树的起始节点。 节点则是树中的元素&#xff0c;每个节点可以有零个或多个子节点&#xff0c;节点按照层级排列&a…

js | Core

http://dmitrysoshnikov.com/ecmascript/javascript-the-core/ Object 是什么&#xff1f; 属性[[prototype]]对象。 例如&#xff0c;下面的&#xff0c;son是对象&#xff0c;foo不是对象。打印出来的son&#xff0c;能看到有一个prototype 对象。 prototype vs _proto_ v…

水利行业的智慧革命:深度剖析智慧水利解决方案,看其如何以科技力量提升水资源管理效率,保障水生态安全

目录 一、智慧水利的概念与内涵 二、智慧水利解决方案的核心要素 1. 感知层&#xff1a;全面监测&#xff0c;精准感知 2. 网络层&#xff1a;互联互通&#xff0c;信息共享 3. 平台层&#xff1a;数据分析&#xff0c;智能决策 4. 应用层&#xff1a;精准施策&#xff0…

django创建子应用、追加导包路径、默认用户模型类、自定义用户模型类、

一、创建用户模块子应用 1.准备apps包&#xff0c;用于管理所有应用 2.在apps包下创建应用users 查看项目导包路径 提示&#xff1a;若要知道如何导入users应用并完成注册&#xff0c;需要知道项目导包路径 已知导包路径&#xff1a;project/meiduo已知 users应用所在目录&…

好用的AI搜索引擎

1. 360AI 搜索 访问 360AI 搜索: https://www.huntagi.com/sites/1706642948656.html 360AI 搜索介绍&#xff1a; 360AI 搜索&#xff0c;新一代智能答案引擎&#xff0c;值得信赖的智能搜索伙伴&#xff0c;为复杂搜索提供专业支持&#xff0c;解锁更相关、更全面的答案。AI…