【数据结构】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.mfbz.cn/a/170257.html

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

相关文章

全志H616开发版

开发板介绍&#xff1a; 二、开发板刷机 SDFormatter TF卡的格式化工具、Win32Diskimager 刷机工具 刷机镜像为&#xff1a;Orangepizero2_2.2.0_ubuntu_bionic_desktop_linux4.9.170.img 使用MobaXterm_Personal_20.3连接使用 网络配置&#xff1a;nmcli dev wifi 命令接入网…

【机器学习】034_多层感知机Part.2_从零实现多层感知机

一、解决XOR问题 1. 回顾XOR问题&#xff1a; 如图&#xff0c;如何对XOR面进行分割以划分四个输入 对应的输出 呢&#xff1f; 思路&#xff1a;采用两个分类器分类&#xff0c;每次分出两个输入 &#xff0c;再借助这两个分类从而分出 。 即采用同或运算&#xff0c;当两…

Redis轻松添加从节点:零阻塞、零烦恼,系统性能再飙升

点击上方蓝字关注我 生成环境的Redis有时需要替换或添加从节点&#xff0c;如果此时主库较大&#xff0c;添加从节点时将可能因为主节点在做bgsave数据备份时使得主库压力大&#xff0c;从而引起其他操作变慢&#xff0c;进而出现阻塞等操作。那么有什么方法可以尽最大程度地减…

C语言中的指针(上)

目录 一、基本概念 1.变量的存储空间 2.定义指针 3.引用与解引用 二、指针的算术运算、类型以及通用指针 1.指针的算数运算 2.指针类型以及通用型指针 三、指向指针的指针&#xff08;pointers to pointers&#xff09; 四、函数传值以及传引用 1.局部变量 2.从存储地…

【ctfshow】web入门-信息搜集-web21~28

SSS web21_爆破什么的&#xff0c;都是基操web22_域名也可以爆破的&#xff0c;试试爆破这个ctf.show的子域名web23_还爆破&#xff1f;这么多代码&#xff0c;告辞&#xff01;web24_爆个&#x1f528;web25_爆个&#x1f528;&#xff0c;不爆了web26_这个可以爆web27_CTFsho…

软件测试之测试用例的设计

1. 测试用例的概念 软件测试人员向被测试系统提供的一组数据的集合&#xff0c;包括 测试环境、测试步骤、测试数据、预期结果 2. 为什么在测试前要设计测试用例 测试用例是执行测试的依据 在回归测试的时候可以进行复用 是自动化测试编写测试脚本的依据 衡量需求的覆盖率…

C#开发的OpenRA游戏之属性QuantizeFacingsFromSequence(7)

C#开发的OpenRA游戏之属性QuantizeFacingsFromSequence(7) 前面分析了身体的方向,在这里继续QuantizeFacingsFromSequence属性,这个属性就是通过序列定义文件里获取身体的方向。 根据前面分析可知,同样有一个信息类QuantizeFacingsFromSequenceInfo: [Desc("Deriv…

基于安卓android微信小程序美容理发店预约系统app

项目介绍 为美容院设计一个系统以减少员工的工作量就成为了想法的初始状态。紧接着对美容院进行进一步的调查发现我的想法已然落后。基本上每个美容院都以有了自己的信息系统&#xff0c;并且做的已经较完善了。 在这时我突然想到&#xff0c;现在关注美容养生的人越来越多&am…

Windows 安装 Docker Compose

目录 前言什么是 Docker Compose &#xff1f;安装 Docker Compose配置环境变量结语开源项目 前言 在当今软件开发和部署领域&#xff0c;容器化技术的应用已成为提高效率和系统可移植性的关键手段。Docker&#xff0c;作为领先的容器化平台&#xff0c;为开发人员提供了轻松构…

MobaXterm如何连接CentOS7的Linux虚拟机?Redis可视化客户端工具如何连接Linux版Redis?

一、打开Lunix虚拟机,进入虚拟机中,在终端中输入ifconfig,得到以下信息&#xff0c;红框中为ip地址 二、打开MobaXterm&#xff0c;点击session 选择SSH&#xff0c;在Remote host中输入linux得到的IP地址&#xff0c;Specify username中可起一个任意的连接名称。 输入密码 四、…

RoCE、IB和TCP等网络的基本知识及差异对比

目前有三种RDMA网络&#xff0c;分别是Infiniband、RoCE(RDMA over Converged Ethernet)、iWARP。 其中&#xff0c;Infiniband是一种专为RDMA设计的网络&#xff0c;从硬件级别保证可靠传输 &#xff0c;技术先进&#xff0c;但是成本高昂。 而RoCE 和 iWARP都是基于以太网的…

leetcode数据结构与算法刷题(三)

目录 第一题 交叉链表 思想&#xff1a; 注意点 第一步先求两个链表的长度 第二步 让长的先走&#xff0c;当长短一样时一起走。 犯错点 第二题 判断是有环 思想&#xff1a; 注意 错误分享 第三题&#xff08;重点面试题&#xff09; 思路&#xff1a; 这题面试问题&a…

InnoDB 的一次更新事务是怎么实现的?

大体流程&#xff1a; 步骤: 1.加载数据到缓存中&#xff08;Buffer Pool&#xff09;&#xff1a; 在进行数据更新时&#xff0c;InnoDB首先会在缓冲池&#xff08;Buffer Pool&#xff09;中查找该记录是否已经在内存中。如果记录不在内存中&#xff0c;会将需要更新的数据…

java学习part06数组工具类

1比较内容 2输出信息 3值填充 4快速排序 5二分查找 负数没找到&#xff0c;其他表示下标

Redis常用的八种场景

作为一名 Java后端人员&#xff0c;对 Redis肯定并不陌生&#xff0c;Redis作为一种内存数据库&#xff0c;以其速度之快在编程的舞台上纵横多年&#xff0c;那么&#xff0c;Redis到底适合哪些业务场景&#xff1f;今天就来聊一聊。 1. 缓存/数据库 缓存&#xff08;Cache&am…

ESP32 http 请求

目录 参考教程1.使用的http连接2.使用Vscode-IDF创建http_request例程3.修改http_request_example_main.c函数4.已经获取到响应的数据 参考教程 ESP-IDF HTTP获取网络时间 1.使用的http连接 http://api.m.taobao.com/rest/api3.do?apimtop.common.getTimestamp请求可以得到…

校园报修抢修小程序系统开发 物业小区报修预约上门维修工单系统

开发的功能模块有&#xff1a; 1.报修工单提交&#xff1a;学生、教职员工等可以使用小程序提交报修请求。这通常包括选择报修的问题类型&#xff08;如水漏、电器故障、照明问题等&#xff09;&#xff0c;地点&#xff0c;报修联系人&#xff0c;联系电话等&#xff0c;并提供…

C语言——I /深入理解指针(一)

一、内存和地址 1byte&#xff08;字节&#xff09; 8bit&#xff08;比特位&#xff09; 1KB 1024byte 1MB 1024KB 1GB 1024MB 1TB 1024GB 1PB 1024TB一个比特位可以存放二进制的0/1的一位 ⽣活中我们把⻔牌号也叫地址&#xff0c;在计算机中我们把内存单元的编号也称为…

Java学习day14:权限修饰符,集合(知识点+例题详解)

声明&#xff1a;该专栏本人重新过一遍java知识点时候的笔记汇总&#xff0c;主要是每天的知识点题解&#xff0c;算是让自己巩固复习&#xff0c;也希望能给初学的朋友们一点帮助&#xff0c;大佬们不喜勿喷(抱拳了老铁&#xff01;) 往期回顾 Java学习day13&#xff1a;泛型&…

kettle创建数据库资源库kettle repository manager

数据库资源库是将作业和转换相关的信息存储在数据库中&#xff0c;执行的时候直接去数据库读取信息&#xff0c;很容易跨平台使用。 创建数据库资源库&#xff0c;如图 1.点击Connect 2.点击Repository Manager 3.点击Other Repository 4.点击Database Repository 在选择Ot…
最新文章