【数据结构】C语言实现带头双向循环链表万字详解(附完整运行代码)

🦄个人主页:修修修也

🎏所属专栏:数据结构

⚙️操作环境:Visual Studio 2022


一.了解项目功能

在本次项目中我们的目标是实现一个带头双向循环链表:

带头双向循环链表使用动态内存分配空间,可以用来存储任意数量的同类型数据.

带头双向循环链表结点(Node)需要包含三个要素:前指针域prev,数据域data,后指针域next.

结点(Node)逻辑结构图示如下:

带头双向循环链表提供的功能有:

  1. 带头双向循环链表的初始化.
  2. 带头双向循环链表的新节点创建.
  3. 带头双向循环链表元素的尾插.
  4. 带头双向循环链表元素的头插.
  5. 带头双向循环链表的元素位置查找.
  6. 带头双向循环链表的任意指定元素前插入.
  7. 带头双向循环链表的尾删.
  8. 带头双向循环链表的头删.
  9. 带头双向循环链表的任意指定元素删除.
  10. 带头双向循环链表打印.
  11. 带头双向循环链表的销毁.

二.项目功能演示

要编写一个带头双向循环链表项目,首先要明确我们想要达到的效果是什么样,下面我将用vs2022编译器来为大家演示一下带头双向循环链表程序运行时的样子:


三.逐步实现项目功能模块及其逻辑详解

通过第二部分对项目功能的介绍,我们已经对带头双向循环链表的功能有了大致的了解,虽然看似需要实现的功能很多,貌似一时间不知该如何下手,但我们可以分步分模块来分析这个项目的流程,最后再将各部分进行整合,所以大家不用担心,跟着我一步一步分析吧!


!!!注意,该部分的代码只是为了详细介绍某一部分的项目实现逻辑,故可能会删减一些与该部分不相关的代码以便大家理解,需要查看或拷贝完整详细代码的朋友可以移步本文第四部分。


1.实现单链表程序菜单

菜单部分的逻辑比较简单,就是利用C语言printf函数打印出这个菜单界面即可。但要注意菜单的标序要和后续switch...case语句的分支相应,以免导致后续执行语句错乱的问题.基础问题就不过多赘述了,代码如下:

该部分功能实现代码如下: 

//菜单
void LTMenu()
{printf("**********************************************\n");printf("******请选择要进行的操作                ******\n");printf("******1.双向带头循环链表尾插            ******\n");printf("******2.双向带头循环链表头插            ******\n");printf("******3.双向带头循环链表指定元素前插入  ******\n");printf("******4.双向带头循环链表尾删            ******\n");printf("******5.双向带头循环链表头删            ******\n");printf("******6.双向带头循环链表指定元素删除    ******\n");printf("******7.双向带头循环链表打印            ******\n");printf("******0.退出双向带头循环链表程序        ******\n");printf("**********************************************\n");printf("请选择:>");
}

2.实现单链表程序功能可循环使用

由于我们要实现带头双向循环链表的功能可以反复使用的逻辑,且至少在一开始执行一次,因此我们选择do...while的循环语句来实现这一部分的逻辑.

该部分功能实现代码如下: 

int main()
{LTNode* plist = LTInit();//初始化带头双向循环链表int swi = 0;//创建变量swi作为do...while循环的终止条件,以及switch语句的运行条件do          //使用do...while实现{LTMenu();scanf("%d", &swi);switch (swi){case 0:// 释放链表内存LTDestroy(plist);plist = NULL;printf("您已退出程序:>\n");break;case 1:printf("请输入要尾插的数据:>");LTDataType pushback_data = 0;scanf("%d", &pushback_data);LTPushBack(plist, pushback_data);printf("已成功插入:>\n");break;case 2:printf("请输入要头插的数据:>");LTDataType pushfront_data = 0;scanf("%d", &pushfront_data);LTPushFront(plist, pushfront_data);printf("已成功插入:>\n");break;case 3:printf("请输入要插入的数据:>");LTDataType insert_data = 0;scanf("%d", &insert_data);printf("请输入要插入的位置上的数据:>");LTDataType insert_posdata = 0;scanf("%d", &insert_posdata);LTNode* insert_pos = LTFind(plist, insert_posdata);if (insert_pos != NULL){LTInsert(insert_pos, insert_data);printf("已成功在'%d'数据前插入'%d':>\n", insert_posdata, insert_data);}else{printf("该元素不存在,无法插入:<\n");}break;case 4:LTPopBack(plist);printf("尾删成功:>\n");break;case 5:LTPopFront(plist);printf("头删成功:>\n");break;case 6:printf("请输入要删除的数据:>");LTDataType erase_data = 0;scanf("%d", &erase_data);LTNode* erase_pos = LTFind(plist, erase_data);if (erase_pos == NULL){printf("要删除的元素不存在:<\n");}else{LTErase(erase_pos);printf("已成功删除:>\n");}break;case 7:printf("打印双向带头循环链表:>\n");LTPrint(plist);break;default:printf("输入错误,请重新输入\n");break;}} while (swi);return 0;
}

3.创建带头双向循环链表

创建带头双向循环链表成员的结构体应该包括:存储上一结点位置的前指针域prev,存储数据的数据域data,存储下一个结点位置的后指针域next.

图示如下:

因此我们创建ListNode结构体类型时应由一个数据成员类型及两个指向该结构体的结构体指针组成.

这里的第一行使用的typedef类定义的作用方便我们后续在使用带头双向循环链表时对存储的数据类型做更改,比如后续我们的带头双向循环链表不想存储int类型数据了,就可以很方便的在这里对带头双向循环链表数据域的存储数据类型做更改.比如改成char类型,或者double类型,甚至改成任意自己构造的结构类型.

在之前的实战项目通讯录中,我们就创建过类似的自定义结构体,如下图:

综上,该部分代码如下:

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

4.单链表的新节点创建

因为后续我们带头双向循环链表初始化,尾插,头插等插入操作时都需要先创建一个新结点,为了使代码的利用效率变高,我们不如将创建新节点这一操作封装成一个函数,后续当需要创建新节点时,直接调用该函数即可.

函数的参数需要接收新结点的数据域,至于新结点的指针域,在我们不清楚新结点的用途时,直接将其全部置为NULL即可.

该部分功能实现代码如下: 

LTNode* BuyListNode(LTDataType x)
{LTNode* node = (LTNode*)malloc(sizeof(LTNode));if (node == NULL){perror("malloc fail");return NULL;}node->next = NULL;node->prev = NULL;node->data = x;return node;
}

5.初始化带头双向循环链表

初始化带头双向循环链表部分和之前单链表中我们的处理方式不同,在无头单链表部分我们需要对链表的初始化的操作仅仅是创建一个指向NULL的头指针即可:

而在本次项目中,我们采用的是带头结点指针链表,因此在初始化的时候我们需要开辟一个头结点,并将它的prev指针和next指针都指向它自己:

该部分功能实现代码如下: 

LTNode* LTInit()
{LTNode* phead = BuyListNode(-1);//头结点数据域设为-1方便辨识phead->next = phead;phead->prev = phead;return phead;
}

6.带头双向循环链表元素的尾插

尾插示意图:

  

如图,我们在尾插时首先要找到原链表的尾,即head->prev,然后我们需要改变四个指针的指向关系:

  • 使旧尾的next连接上newnode
  • 使newnode的prev连接上旧尾
  • 使head的prev连接上newnode
  • 使newnode的next连接上head

然后尾插操作就完成了,该部分功能实现代码如下: 

void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);//带头结点的头指针必不为空.//单链表需要二级指针的原因是要改变头指针的指向,要改变指针只能用二级指针//要改变结构体的成员就不需要二级指针,只需要改变它存储的内容即可LTNode* newnode = BuyListNode(x);LTNode* tail = phead->prev;//链接新尾和旧尾tail->next = newnode;newnode->prev = tail;phead->prev = newnode;newnode->next = phead;}

7.带头双向循环链表元素的头插

头插示意图:

  

如图,我们在头插时首先需要找到旧头,即head->next,然后需要改变四个指针的指向关系:

  • 使newnode的next连接上旧头
  • 使旧头的prev连接上newnode
  • 使head的next连接上newnode
  • 使newnode的prev连接上head

 注意!在这部分,我们只使用head和nownode两个指针的情况下,一定要先让newnode和旧头链接起来,即图中的1操作,然后再将head和newnode连接起来,即图中的4操作.

因为,如果我们先更改了head的next指针的指向,后续想要找到旧头就只能再循环遍历一遍链表了,这样会非常麻烦.

这四个指针的顺序是可以更改的,但必须要保证第1步操作在第4步操作的前面,只要满足这个条件,剩下的顺序都是可以随意更改的.

然后头插操作就完成了,该部分功能实现代码如下: 

void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = BuyListNode(x);//双指针:先连后再连前newnode->next = phead->next;phead->next->prev = newnode;phead->next = newnode;newnode->prev = phead;//三指针顺序可以随便换
}

8.带头双向循环链表的元素位置查找

因为后续我们要使用的带头双向循环链表按位插入和按位删除需要知道用户传入的链表元素在链表中的位置在哪,因此我们把查找链表元素位置的操作封装成一个单独的函数,后续需要查找某一链表元素的位置直接调用这个函数就行.

函数的参数应该接收待查找的结点的数据域,以便我们在遍历链表的过程中能够找到它.

函数的返回值是链表结点指针型(LTNode*),这样可以方便我们在找到要查找的指针后直接对齐进行相关操作,而不需要再在函数外再遍历一遍链表了.

对于带头双向循环链表的遍历,我们要特别注意,因为是循环链表,所以我们一般的遍历方式都是从头结点的后一个结点开始,直到走到头结点的时候才算结束.

该部分功能实现代码如下: 

LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* cur = phead->next;  //从头节点的后一个结点开始while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL;       //没找到,返回空
}

9.带头双向循环链表的任意指定元素前插入

因为我们只要知道某一结点的位置,就可以通过访问它的prev和next指针访问它的上一个或下一个结点,所以在指定元素前插入函数中我们只需要两个参数,一个是指定元素的位置,一个是新结点的数据域的数据值.

任意指定位置前插入示意图:

  

如图,我们这次创建一个指针p来记录下pos的前一个结点的位置,即使用三指针的方式来插入新数据,这时四个指针的修改顺序就可以任意书写了,我们可以先更改p指针的next指针了,我们改变下面四个指针的指向关系即可:

  • p的next连接上newnode
  • newnode的prev连接上p
  • newnode的next连接上pos
  • pos的prev连接上newnode

更改完我们的插入操作也就完成了,该部分代码实现如下:

//在pos位置之前插入一个值
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = BuyListNode(x);LTNode* p = pos->prev;p->next = newnode;newnode->prev = p;newnode->next = pos;pos->prev = newnode;//尾插pos传head//头插pos传head->next}

当我们拥有了任意位置前插入函数后, 其实就不需要再单独写尾插或头插函数了,当我们想要尾插时我们只需要给LTInsert函数传入head的地址就可以了,而当我们想要头插时我们只需要给LTInsert函数传入head->next的地址就可以了.

所以尾插函数可以直接写成:

void LTPushBack(LTNode* phead, LTDataType x)
{LTInsert(phead,x);
}

头插函数可以直接写成:

void LTPushFront(LTNode* phead, LTDataType x)
{LTInsert(phead->next,x);
}

10.带头双向循环链表的判空

因为我们在删除链表元素前都需要先判断一下链表当前是不是为空,如果链表为空那就不要再删了,因为尾删,头删,指定元素删除都需要判空操作,所以我们不如封装一个函数,在调用时判断当前链表是否为空,如果为空返回假,不为空返回真.

 该部分功能实现代码如下: 

bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next != phead;   //简洁写法//完整写法,即phead的next不等于phead返回真(即链表不为空)//phead的next等于phead返回假(即链表为空)/*if (phead->next != phead){return true;}else{return false;}*/}

11.带头双向循环链表的尾删

尾删示意图:

  

如图,我们尾删前判断一下链表不为空的话就要找到尾结点,然后就可以开始尾删了.我们先创建一个指针tail记录下尾结点的位置,再创建一个指针tailPrev记录下尾结点的前一个结点(即新尾)的位置.

在删除时我们只需要改变两个指针的指向就可以了,即:

  •  使phead的prev指针指向新尾(tailPrev)
  • 使新尾(tailPrev)的next指针指向phead

 更改完我们的尾删操作也就完成了,该部分代码实现如下:

void LTPopBack(LTNode* phead)
{assert(phead);assert(LTEmpty(phead));LTNode* tail = phead->prev;LTNode* tailPrev = tail->prev;phead->prev = tailPrev;tailPrev->next = phead;free(tail);tail = NULL;
}

12.带头双向循环链表的头删

头删示意图:

如图,我们头删前判断一下链表不为空的话就要找到首结点,然后就可以开始头删了.我们先创建一个指针tail记录下首结点的位置.然后就可以开始删除了.

在删除时我们只需要改变两个指针的指向就可以了,即:

  •  使phead的next指针指向新首(tail->next)
  • 使新首(tail->next)的prev指针指向phead

 更改完我们的头删操作也就完成了,该部分代码实现如下:

void LTPopFront(LTNode* phead)
{assert(phead);assert(LTEmpty(phead));LTNode* tail = phead->next;phead->next = tail->next;tail->next->prev = phead;free(tail);tail = NULL;
}

13.带头双向循环链表的任意指定元素删除

删除任意指定元素示意图:

如图,我们任意位置删前判断一下链表不为空的话就要找到待删结点的前驱结点和后继结点,然后就可以开始删除了.我们创建一个指针p记录下待删结点的前驱结点的位置.再创建一个指针n记录下待删结点的后继结点的位置,然后就可以开始删除了.

在删除时我们只需要改变两个指针的指向就可以了,即:

  • 使前驱结点p的next指针指向后继结点n
  • 使后继结点n的prev指针指向前驱结点p

 更改完我们的删除操作也就完成了,该部分代码实现如下:

//删除pos位置的值
void LTErase(LTNode* pos)
{assert(pos);LTNode* p = pos->prev;LTNode* n = pos->next;n->prev = p;p->next = n;free(pos);pos = NULL;//头删传phead->next//尾删传phead->prev}

当我们拥有了任意位置前删除函数后, 其实就不需要再单独写尾删或头删函数了,当我们想要尾删时我们只需要给LTErase函数传入phead->prev的地址就可以了,而当我们想要头删时我们只需要给LTErase函数传入phead->next的地址就可以了.

所以尾删函数可以直接写成:

void LTPopBack(LTNode* phead, LTDataType x)
{LTErase(phead->prev,x);
}

头删函数可以直接写成:

void LTPopFront(LTNode* phead, LTDataType x)
{LTErase(phead->next,x);
}

14.带头双向循环链表打印

在打印部分我们要注意的是:循环链表和单链表的主要差异就在于循环遍历时的判断条件上,原来是判断p->next是否为NULL,现在则是p->next不等于头结点,则循环遍历未结束.

了解了这点后,带头双向循环链表的打印逻辑很简单,顺着头指针的后一个结点向后循环遍历打印整个链表结点的数据域即可,当遍历指针再次走到head结点时,则代表已经遍历打印完链表的所有元素,这时跳出循环即可.

该部分功能实现代码如下: 

void LTPrint(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;printf("<->Head<->");while (cur != phead){printf("%d<->",cur->data);cur = cur->next;}printf("\n");
}

15.带头双向循环链表的销毁

当我们使用完双向带头循环链表想要退出程序时,就应该将之前动态开辟的内存释放掉,还给操作系统.即销毁双向带头循环链表操作.

我们使用free()函数将前面开辟的结点的内存逐一释放,释放完将头指针置为空即可.

要注意,这里循环链表的遍历从头结点的后一个结点开始,然后循环遍历时的判断条件p->next不等于头结点,则循环遍历未结束.

 该部分功能实现代码如下:

void LTDestroy(LTNode*phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){LTNode* next = cur->next;free(cur);cur = next;}free(phead);//让调用的人置空
}

四.项目完整代码

我们将程序运行的代码分别在三个工程文件中编辑,完整代码如下:

test.c文件

#include"List.h"int main()
{LTNode* plist = LTInit();int swi = 0;//创建变量swi作为do...while循环的终止条件,以及switch语句的运行条件do          //使用do...while实现{LTMenu();scanf("%d", &swi);switch (swi){case 0:// 释放链表内存LTDestroy(plist);plist = NULL;printf("您已退出程序:>\n");break;case 1:printf("请输入要尾插的数据:>");LTDataType pushback_data = 0;scanf("%d", &pushback_data);LTPushBack(plist, pushback_data);printf("已成功插入:>\n");break;case 2:printf("请输入要头插的数据:>");LTDataType pushfront_data = 0;scanf("%d", &pushfront_data);LTPushFront(plist, pushfront_data);printf("已成功插入:>\n");break;case 3:printf("请输入要插入的数据:>");LTDataType insert_data = 0;scanf("%d", &insert_data);printf("请输入要插入的位置上的数据:>");LTDataType insert_posdata = 0;scanf("%d", &insert_posdata);LTNode* insert_pos = LTFind(plist, insert_posdata);if (insert_pos != NULL){LTInsert(insert_pos, insert_data);printf("已成功在'%d'数据前插入'%d':>\n", insert_posdata, insert_data);}else{printf("该元素不存在,无法插入:<\n");}break;case 4:LTPopBack(plist);printf("尾删成功:>\n");break;case 5:LTPopFront(plist);printf("头删成功:>\n");break;case 6:printf("请输入要删除的数据:>");LTDataType erase_data = 0;scanf("%d", &erase_data);LTNode* erase_pos = LTFind(plist, erase_data);if (erase_pos == NULL){printf("要删除的元素不存在:<\n");}else{LTErase(erase_pos);printf("已成功删除:>\n");}break;case 7:printf("打印双向带头循环链表:>\n");LTPrint(plist);break;default:printf("输入错误,请重新输入\n");break;}} while (swi);return 0;
}

List.c文件

#include"List.h"//菜单
void LTMenu()
{printf("**********************************************\n");printf("******请选择要进行的操作                ******\n");printf("******1.双向带头循环链表尾插            ******\n");printf("******2.双向带头循环链表头插            ******\n");printf("******3.双向带头循环链表指定元素前插入  ******\n");printf("******4.双向带头循环链表尾删            ******\n");printf("******5.双向带头循环链表头删            ******\n");printf("******6.双向带头循环链表指定元素删除    ******\n");printf("******7.双向带头循环链表打印            ******\n");printf("******0.退出双向带头循环链表程序        ******\n");printf("**********************************************\n");printf("请选择:>");
}LTNode* LTInit()
{LTNode* phead = BuyListNode(-1);phead->next = phead;phead->prev = phead;return phead;
}void LTDestroy(LTNode*phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){LTNode* next = cur->next;free(cur);cur = next;}free(phead);//让调用的人置空
}LTNode* BuyListNode(LTDataType x)
{LTNode* node = (LTNode*)malloc(sizeof(LTNode));if (node == NULL){perror("malloc fail");return NULL;}node->next = NULL;node->prev = NULL;node->data = x;return node;
}void LTPrint(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;printf("<->Head<->");while (cur != phead){printf("%d<->",cur->data);cur = cur->next;}printf("\n");
}bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next != phead;/*if (phead->next != phead){return true;}else{return false;}*/}void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);//带头结点的头指针必不为空.//单链表需要二级指针的原因是要改变头指针的指向,要改变指针只能用二级指针//要改变结构体的成员就不需要二级指针,只需要改变它存储的内容即可LTNode* newnode = BuyListNode(x);LTNode* tail = phead->prev;//链接新尾和旧尾tail->next = newnode;newnode->prev = tail;phead->prev = newnode;newnode->next = phead;}void LTPopBack(LTNode* phead)
{assert(phead);assert(LTEmpty(phead));LTNode* tail = phead->prev;LTNode* tailPrev = tail->prev;phead->prev = tailPrev;tailPrev->next = phead;free(tail);tail = NULL;
}void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = BuyListNode(x);//双指针:先连后再连前newnode->next = phead->next;phead->next->prev = newnode;phead->next = newnode;newnode->prev = phead;//三指针顺序随便换
}void LTPopFront(LTNode* phead)
{assert(phead);assert(LTEmpty(phead));LTNode* tail = phead->next;phead->next = tail->next;tail->next->prev = phead;free(tail);tail = NULL;
}LTNode* LTFind(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 LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = BuyListNode(x);LTNode* p = pos->prev;p->next = newnode;newnode->prev = p;newnode->next = pos;pos->prev = newnode;//尾插pos传head//头插pos传head->next}//删除pos位置的值
void LTErase(LTNode* pos)
{assert(pos);LTNode* p = pos->prev;LTNode* n = pos->next;n->prev = p;p->next = n;free(pos);pos = NULL;//头删传phead->next//尾删传phead->prev}

List.h文件

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
#include<stdbool.h>typedef int LTDataType;typedef struct ListNode
{struct ListNode* next;struct ListNode* prev;LTDataType data;}LTNode;void LTMenu();LTNode* LTInit();LTNode* BuyListNode(LTDataType x);LTNode* LTFind(LTNode* phead, LTDataType x);void LTDestroy(LTNode* phead);void LTPrint(LTNode* phead);bool LTEmpty(LTNode* phead);void LTPushBack(LTNode* phead, LTDataType x);void LTPopBack(LTNode* phead);void LTPushFront(LTNode* phead, LTDataType x);void LTPopFront(LTNode* phead);void LTInsert(LTNode* pos,LTDataType x);void LTErase(LTNode* pos);

结语

希望这篇双向带头循环表的实现详解能对大家有所帮助,欢迎大佬们留言或私信与我交流.

学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!

相关文章推荐

【数据结构】什么是线性表?

【数据结构】线性表的链式存储结构

【C语言】free()函数详解(动态内存释放函数)

【C语言】malloc()函数详解(动态内存开辟函数)
【数据结构】链表的八种形态
【数据结构】C语言实现单链表万字详解(附完整运行代码)

【实用编程技巧】不想改bug?初学者必须学会使用的报错函数assert!(断言函数详解)



数据结构线性篇思维导图:

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

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

相关文章

ISP--Black Level Correction(黑电平矫正)

图像的每一个像素点都是由一个光电二极管控制的&#xff0c;由二极管将电信号&#xff0c;转换为数字信号。 那么&#xff0c;我们知道了&#xff0c;图像的像素值是与电信号强度相关的。但是&#xff0c;我们得知道&#xff0c;每一个光电二极管要想工作&#xff0c;都得有一定…

2023年亚太杯数学建模亚太赛ABC题思路资料汇总贴

下文包含&#xff1a;2023年亚太杯数学建模亚太赛A- C题思路解析、选题建议、代码可视化及如何准备数学建模竞赛&#xff08;23号发&#xff09; C君将会第一时间发布选题建议、所有题目的思路解析、相关代码、参考文献、参考论文等多项资料&#xff0c;帮助大家取得好成绩。2…

【python】直方图正则化详解和示例

直方图正则化&#xff08;Histogram Normalization&#xff09;是一种图像增强技术&#xff0c;目的是改变图像的直方图以改善图像的质量。具体来说&#xff0c;它通过将图像的直方图调整为指定的形状&#xff0c;以增强图像的对比度和亮度。 直方图正则化的基本步骤如下&…

Unity中Shader法线贴图(下)理论篇

文章目录 前言一、采样出错的原因二、切线空间是什么&#xff1f;切线空间图解&#xff1a; 三、计算方式1、统一变换到切线空间下进行计算2、统一变换到世界空间下进行计算 四、一般统一变换到世界空间下的坐标进行计算1、求M^-1^2、求出n~w~ 前言 这篇文章&#xff0c;主要解…

【腾讯云云上实验室-向量数据库】探索腾讯云向量数据库:全方位管理与高效利用多维向量数据的引领者

目录 前言1 腾讯云向量数据库介绍2 向量数据库信息及设置2.1 向量数据库实例信息2.2 实例监控2.3 密钥管理2.4 安全组2.5 Embedding2.6 可视化界面 3 可视化界面4 Embedding4.1 embedding_coll精确查询4.2 unenabled_embedding_coll精确查询 5 数据库5.1 创建数据库5.2 插入数据…

openGauss学习笔记-128 openGauss 数据库管理-设置透明数据加密(TDE)

文章目录 openGauss学习笔记-128 openGauss 数据库管理-设置透明数据加密&#xff08;TDE&#xff09;128.1 概述128.2 前提条件128.3 背景信息128.4 密钥管理机制128.5 表级加密方案128.6 创建加密表128.7 切换加密表加密开关128.8 对加密表进行密钥轮转 openGauss学习笔记-12…

2023.11.17使用flask将多个图片文件上传至服务器

2023.11.17使用flask将多个图片文件上传至服务器 实现功能&#xff1a; 1、同时上传多个图片文件 2、验证文件扩展名 3、显示上传文件的文件名 4、显示文件上传结果 程序结构 main.py from flask import Flask, request, jsonify, render_template import osapp Flask(__n…

C#,数值计算——插值和外推,分段线性插值(Linear_interp)的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { /// <summary> /// 分段线性插值 /// Piecewise linear interpolation object. /// Construct with x and y vectors, then call interp for interpolated values. /// </summary> …

矩阵的QR分解

矩阵的QR分解 GramSchmidt 设存在 B { x 1 , x 2 , … , x n } \mathcal{B}\left\{\mathbf{x}_{1},\mathbf{x}_{2},\ldots,\mathbf{x}_{n}\right\} B{x1​,x2​,…,xn​}在施密特正交化过程中 q 1 x 1 ∣ ∣ x 1 ∣ ∣ q_1\frac{x_1}{||x_1||} q1​∣∣x1​∣∣x1​​ q k …

Apache Airflow (十一) :HiveOperator及调度HQL

&#x1f3e1; 个人主页&#xff1a;IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 &#x1f6a9; 私聊博主&#xff1a;加入大数据技术讨论群聊&#xff0c;获取更多大数据资料。 &#x1f514; 博主个人B栈地址&#xff1a;豹哥教你大数据的个人空间-豹…

【Flask使用】全知识md文档,4大部分60页第3篇:状态cookie和session保持

本文的主要内容&#xff1a;flask视图&路由、虚拟环境安装、路由各种定义、状态保持、cookie、session、模板基本使用、过滤器&自定义过滤器、模板代码复用&#xff1a;宏、继承/包含、模板中特有变量和函数、Flask-WTF 表单、CSRF、数据库操作、ORM、Flask-SQLAlchemy…

汇编-loop循环指令

LOOP指令是根据ECX计数器循环&#xff0c;将语句块重复执行特定次数。 ECX自动作为计数器&#xff0c; 每重复循环一次就递减1。 语法如下所示&#xff1a; 循环目的地址必须在距离当前位置计数器的-128到127字节范围内 LOOP指令的执行有两个步骤&#xff1a; 第一步&…

Pandas数据集的合并与连接merge()方法_Python数据分析与可视化

数据集的合并与连接 merge()解析merge()的主要参数 merge()解析 merge()可根据一个或者多个键将不同的DataFrame连接在一起&#xff0c;类似于SQL数据库中的合并操作。 数据连接的类型 一对一的连接&#xff1a; df1 pd.DataFrame({employee: [Bob, Jake, Lisa, Sue], grou…

电容的耐压值是什么意思呢?

电容是什么&#xff1f; 电容是一种能以电荷的形式储存能量的装置。与同样大小的电池相比&#xff0c;电容能储存的能量要小得多&#xff0c;大约1w个电容存储的能量才顶一节电池存储的能量&#xff0c;但对于许多电路设计来说却足够使用了。 看下图的直插式电容&#xff0c;…

[github配置] 远程访问仓库以及问题解决

作者&#xff1a;20岁爱吃必胜客&#xff08;坤制作人&#xff09;&#xff0c;近十年开发经验, 跨域学习者&#xff0c;目前于新西兰奥克兰大学攻读IT硕士学位。荣誉&#xff1a;阿里云博客专家认证、腾讯开发者社区优质创作者&#xff0c;在CTF省赛校赛多次取得好成绩。跨领域…

V100 GPU服务器安装CUDA教程

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

Pycharm中添加Python库指南

一、介绍 Pycharm是一款为Python开发者提供的集成开发环境&#xff08;IDE&#xff09;&#xff0c;支持执行、调试Python代码&#xff0c;并提供了许多有用的工具和功能&#xff0c;其中之一就是在Pycharm中添加Python库。 添加Python库有许多好处&#xff0c;比如能够增加开…

【Django-DRF用法】多年积累md笔记,第3篇:Django-DRF的序列化和反序列化详解

本文从分析现在流行的前后端分离Web应用模式说起&#xff0c;然后介绍如何设计REST API&#xff0c;通过使用Django来实现一个REST API为例&#xff0c;明确后端开发REST API要做的最核心工作&#xff0c;然后介绍Django REST framework能帮助我们简化开发REST API的工作。 全…

从0开始学习JavaScript--深入探究JavaScript类型化数组

JavaScript类型化数组是一种特殊的数组类型&#xff0c;引入了对二进制数据的更底层的操作。这种数组提供了对内存中的二进制数据直接进行读写的能力&#xff0c;为处理图形、音频、视频等大规模数据提供了高效的手段。本文将深入探讨JavaScript类型化数组的基本概念、常见类型…

python的socket模块以及通信相关学习笔记

Socket又称"套接字"&#xff0c;应用程序通常通过"套接字"向网络发出请求或者应答网络请求&#xff0c;使主机间或者一台计算机上的进程间可以通讯(最初设计是为了是使同一台计算机中的不同进程进行信息传递通信)&#xff0c;最后拓展到可以使网络上两台计…