双向链表(数据结构)(C语言)

news2025/1/16 8:00:22

目录

概念

带头双向循环链表的实现

前情提示

双向链表的结构体定义

双向链表的初始化

关于无头单向非循环链表无需初始化函数,顺序表、带头双向循环链表需要的思考

双向链表在pos位置之前插入x

双向链表的打印

双链表删除pos位置的结点

双向链表的尾插

关于单链表的尾插需要用到二级指针,双向链表不需要用到二级指针的思考

双向链表的判空

双向链表的尾删

双向链表的头插 

双向链表的头删

双向链表查找值为x的结点

双向链表的销毁 

双向链表的修改

双向链表删除值为x的结点

 双向链表计算结点总数(不计phead)

双向链表获取第i位置的结点

双向链表的清空

总代码(想直接看结果的可以看这里)


概念

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。我们一般构造双向循环链表。循环链表是一种链式存储结构,它的最后一个结点指向头结点,形成一个环。因此,从循环链表中的任何一个结点出发都能找到任何其它结点。


带头双向循环链表的实现

前情提示

List.h  用于  引用的头文件、双向链表的定义、函数的声明。

List.c  用于  函数的定义。

Test.c 用于  双向链表功能的测试。

双向链表的结构体定义

在List.h下

#pragma once//使同一个文件不会被包含(include)多次,不必担心宏名冲突

//先将可能使用到的头文件写上
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

typedef int LTDataType;//假设结点的数据域类型为 int
//给变量定义一个易于记忆且意义明确的新名字,并且便于以后存储其它类型时方便改动
//(比如我晚点想存double类型的数据,我就直接将 int 改为 double )

// 带哨兵位双向循环链表的结构体定义
typedef struct ListNode
{
	struct ListNode* prev;//前驱指针域:存放上一个结点的地址
	struct ListNode* next;//后继指针域:存放下一个结点的地址
	LTDataType data;//数据域
}LTNode;
//struct 关键字和 ListNode 一起构成了这个结构类型
//typedef 为这个结构起了一个别名,叫 LTNode,即:typedef struct ListNode LTNode 
//现在就可以像 int 和 double 那样直接使用 LTNode 来定义变量

双向链表的初始化

在List.h下

// 双向链表的初始化

// 如果是单链表直接给个空指针就行,不需要单独写一个函数进行初始化
// 即:LTNode* plist = NULL;
// 那为什么顺序表、带头双向循环链表有呢?
// 因为顺序表、带头双向循环链表的结构并不简单,
// 如:    顺序表顺序表为空size要为0,还要看capacity是否要开空间,
// 若不开空间capacity=0,指针要给空,若开空间,还要检查malloc是否成功
//	      带头双向循环链表要开个结点,检查malloc是否成功,然后让结点自己指向自己
// 顺序表和双向循环链表的初始化有点复杂,最好构建一个函数
LTNode* LTInit();

在List.c下

#include"List.h"//别忘了

//动态申请一个结点
LTNode* BuyListNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)//如果malloc失败
	{
		perror("malloc fail");
		return NULL;
	}
	//如果malloc成功
	//初始化一下,防止野指针,如果看到返回的是空指针,那逻辑可能有些错误
	node->next = NULL;
	node->prev = NULL;
	node->data = x;

	return node;
}

// 双向链表的初始化
LTNode* LTInit()
{
	LTNode* phead = BuyListNode(-1);//创建哨兵位
	//自己指向自己
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

关于无头单向非循环链表无需初始化函数,顺序表、带头双向循环链表需要的思考

无头单向非循环链表结构太简单了,初始化只需直接赋空指针,无需单独写一个函数进行初始化。

即:LTNode* plist = NULL;

那为什么顺序表、带头双向循环链表有单独写一个函数进行初始化呢?
因为顺序表、带头双向循环链表的结构并不简单。

如:

顺序表顺序表为空size要为0,还要看capacity是否要开空间,若不开空间capacity=0,指针要给空,若开空间,还要检查malloc是否成功。

带头双向循环链表要开个结点,检查malloc是否成功,然后让结点自己指向自己。

顺序表和双向循环链表的初始化有点复杂,最好构建一个函数。

双向链表在pos位置之前插入x

在List.h下

// 双向链表在pos位置之前进行插入
void LTInsert(LTNode* pos, LTDataType x);

在List.c下

// 双向链表在pos位置之前进行插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//pos肯定不为空

	LTNode* prev = pos->prev;
	LTNode* newnode = BuyListNode(x);//创建一个需要插入的结点

	prev->next = newnode;
	newnode->prev = prev;

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

双向链表的打印

在List.h下

// 双向链表打印
void LTPrint(LTNode* phead);

在List.c下

// 双向链表打印
void LTPrint(LTNode* phead)
{
	assert(phead);//有哨兵位
	printf("<=>phead<=>");
	LTNode* cur = phead->next;//cur指向第一个要打印的结点
	while (cur != phead)//cur等于头结点时打印就结束了
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

在Test.c下

#include"List.h"//别忘了

//测试函数
void TestList1()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 1);
	LTInsert(plist, 2);
	LTInsert(plist, 3);
	LTInsert(plist, 4);
	LTPrint(plist);
}

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

双链表删除pos位置的结点

在List.h下

// 双向链表删除pos位置的结点
void LTErase(LTNode* pos);

在List.c下

// 双向链表删除pos位置的结点
void LTErase(LTNode* pos)
{
	assert(pos);//pos肯定不为空

	LTNode* posprev = pos->prev;
	LTNode* posnext = pos->next;

	posprev->next = posnext;
	posnext->prev = posprev;

	free(pos);
	pos = NULL;
    //这个置空其实已经没有意义了,形参的改变不会改变实参
}

在Test.c下

//测试函数
void TestList1()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 1);
	LTInsert(plist, 2);
	LTInsert(plist, 3);
	LTInsert(plist, 4);
	LTPrint(plist);

	LTErase(plist->next);
	LTPrint(plist);

}

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

双向链表的尾插

在List.h下

//双向链表优于单链表的点——不需要找尾、二级指针
//(我们改的不是结构体的指针,改的是结构体的变量)
// 双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x);

在List.c下

法一:(便于新手更好地理解双向链表的尾插) 

// 双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);//有哨兵位
    
    //法一:(便于新手更好地理解双向链表的尾插)
	//一步就可完成链表为空/不为空的尾插——因为有哨兵位
	LTNode* newnode = BuyListNode(x);//创建一个要插入的结点
	LTNode* tail = phead->prev;

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

法二:函数复用(简单方便)

// 双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);//有哨兵位

	//法二:函数复用(简单方便)
	LTInsert(phead, x);
}

关于单链表的尾插需要用到二级指针,双向链表不需要用到二级指针的思考

单链表改变的是结构体的指针,双向链表改变的是结构体的变量

二级指针和一级指针的区别在于指针所指向变量的层级不同,一级指针指向的是结构体的变量,而二级指针指向的是结构体指针的地址
单链表中,在进行链表结点的删除或插入操作时,需要更新结点之间的指针指向。若使用一级指针,则操作会直接改变指向结点的指针,很难实现目标。因此需要传递二级指针,让函数能够修改指向结点指针的地址,也就是修改之前结点指针变量存放的地址
而双向链表中,每个结点除了保存指向下一结点的指针外,还有保存指向上一结点的指针,结点之间的双向指针关系使得结点的插入和删除操作更加方便。双向链表不需要传递二级指针,因为在结点的删除和插入操作中,只需要先修改当前结点前后结点的指针,无需直接改变前后结点指针变量存放的地址
综上所述,单链表只有指向下一结点的指针,通过传递二级指针来修改结点之间的指针关系,使得操作更加灵活;而双向链表的结点之间有双向指针关系,无需直接改变前后结点指针变量存放的地址,因此只需要传递一级指针即可

单链表(对比):

在Test.c下

//测试函数
void TestList2()
{
	LTNode* plist = LTInit();
	LTPushBack(plist, 5);
	LTPushBack(plist, 6);
	LTPushBack(plist, 7);
	LTPushBack(plist, 8);
	LTPrint(plist);
}

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

双向链表的判空

在尾删/头删之前,我们要先判断链表是否为空。

在List.h下

// 双向链表的判空
bool LTEmpty(LTNode* phead);

在List.c下

// 双向链表的判空
bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
	//两者相等就是空链表(返回真),两者不相等就不是空链表(返回假)
}

双向链表的尾删

在List.h下

// 双向链表的尾删
void LTPopBack(LTNode* phead);

在List.c下

法一:(便于新手更好地理解双向链表的尾删)

// 双向链表的尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);//有哨兵位
	assert(!LTEmpty(phead));//判空

    //法一:(便于新手更好地理解双向链表的尾删)
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

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

法二:函数复用(简单方便)

// 双向链表的尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);//有哨兵位
    assert(!LTEmpty(phead));//判空
	
    //法二:函数复用
	LTErase(phead->prev);
}

在Test.c下

//测试函数
void TestList2()
{
	LTNode* plist = LTInit();
	LTPushBack(plist, 5);
	LTPushBack(plist, 6);
	LTPushBack(plist, 7);
	LTPushBack(plist, 8);
	LTPrint(plist);

	LTPopBack(plist);
	LTPopBack(plist);
	LTPrint(plist);
}

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

双向链表的头插 

在List.h下

// 双向链表头插
void LTPushFront(LTNode* phead, LTDataType x);

在List.c下

法一:只用phead和newnode两个指针(便于新手更好地理解双向链表的头插)

// 双向链表头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);//有哨兵位

	LTNode* newnode = BuyListNode(x);//创建一个要插入的结点
    
    //法一:只用phead和newnode两个指针(便于新手更好地理解双向链表的头插)
    //顺序很重要!!!
	newnode->next = phead->next;
	phead->next->prev = newnode;

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

法二:多用了first记录第一个结点的位置(便于新手更好地理解双向链表的头插)

// 双向链表头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);//哨兵位

	LTNode* newnode = BuyListNode(x);//创建一个要插入的结点

	//法二:多用了first先记住第一个结点(便于新手更好地理解双向链表的头插)
	LTNode* first = phead->next;
	phead->next = newnode;
	newnode->prev = phead;

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

法三:函数复用(简单方便)

// 双向链表头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);//有哨兵位
	
	//法三:函数复用(简单方便)
	LTInsert(phead->next, x);
}

在Test.c下

//测试函数
void TestList3()
{
	LTNode* plist = LTInit();
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
	LTPrint(plist);
}

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

双向链表的头删

在List.h下

// 双向链表头删
void LTPopFront(LTNode* phead);

在List.c下

法一:(便于新手更好地理解双向链表的头删)

// 双向链表头删
void LTPopFront(LTNode* phead)
{
	assert(phead);//有哨兵位
	assert(!LTEmpty(phead));//判空
	
    //法一:(便于新手更好地理解双向链表的头删)
	LTNode* head = phead->next;
	LTNode* headnext = head->next;

	phead->next = headnext;
	headnext->prev = phead;

	free(head);
	head = NULL;
}

法二:函数复用(简单方便) 

// 双向链表头删
void LTPopFront(LTNode* phead)
{
	assert(phead);//有哨兵位
    assert(!LTEmpty(phead));//判空
	
    //法二:函数复用(简单方便)
	LTErase(phead->next);
}

在Test.c下

//测试函数
void TestList3()
{
	LTNode* plist = LTInit();
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
	LTPrint(plist);

	LTPopFront(plist);
	LTPopFront(plist);
	LTPrint(plist);
}

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

双向链表查找值为x的结点

在List.h下

// 双向链表查找值为x的结点
LTNode* LTFind(LTNode* phead, LTDataType x);

在List.c下

// 双向链表查找值为x的结点
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);//有哨兵位

	LTNode* cur = phead->next;
	while (cur != phead)//让cur去遍历
	{
		if (cur->data == x)//如果找到
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;//如果没找到
}

在Test.c下

//测试函数
TestList4()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 1);
	LTInsert(plist, 2);
	LTInsert(plist, 3);
	LTInsert(plist, 4);
	LTPrint(plist);

	LTNode* pos = LTFind(plist, 3);
	if (pos)
	{
		LTErase(pos);
		pos = NULL;
	}

	LTPrint(plist);
}

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

双向链表的销毁 

在List.h下

// 双向链表的销毁
void LTDestory(LTNode* phead);

在List.c下

   

  

 

// 双向链表的销毁
void LTDestory(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;//让cur遍历
	while (cur != phead)
	{
		LTNode* curnext = cur->next;
		free(cur);
		cur = curnext;
	}
	free(phead);
	phead = NULL;
	//这个置空其实已经没有意义了,形参的改变不会改变实参
	//我们为了保持接口的一致性,不传二级指针,选择在测试的时候置空
}

在Test.c下

//测试函数
TestList4()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 1);
	LTInsert(plist, 2);
	LTInsert(plist, 3);
	LTInsert(plist, 4);
	LTPrint(plist);

	LTNode* pos = LTFind(plist, 3);
	if (pos)
	{
		LTErase(pos);
		pos = NULL;
	}
	LTPrint(plist);

	LTDestory(plist);
	plist = NULL;//在这里置空
}

双向链表的修改

在List.h下

// 双向链表的修改,修改pos位置的值为x
void LTModify(LTNode* pos, LTDataType x);

在List.c下

// 双向链表的修改,修改pos位置的值为x
void LTModify(LTNode* pos, LTDataType x)
{
	assert(pos);
	pos->data = x;
}

在Test.c下

//测试函数
TestList5()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 1);
	LTInsert(plist, 2);
	LTInsert(plist, 3);
	LTInsert(plist, 4);
	LTPrint(plist);

	LTModify(plist->next,5);
	LTPrint(plist);
}

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

双向链表删除值为x的结点

在List.h下

// 双向链表删除值为x的结点
void LTRemove(LTNode* phead,LTDataType x);

在List.c下

// 双向链表删除值为x的结点
void LTRemove(LTNode* phead, LTDataType x)
{
	assert(phead);//有哨兵位
	LTNode* pos = phead->next;
	while (pos != phead)
	{
		pos = LTFind(phead, x);
		if (pos == NULL)//如果遍历完
		{
			return NULL;
		}
		LTErase(pos);
		pos = pos->next;
	}
}

在Test.c下

TestList6()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 1);
	LTInsert(plist, 3);
	LTInsert(plist, 3);
	LTInsert(plist, 4);
	LTInsert(plist, 3);
	LTPrint(plist);

	LTRemove(plist, 3);
	LTPrint(plist);
}

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

 双向链表计算结点总数(不计phead)

在List.h下

// 双向链表计算结点总数(不计phead)
int LTTotal(LTNode* phead);

在List.c下

// 双向链表计算结点总数(不计phead)
int LTTotal(LTNode* phead)
{
	assert(phead);//有哨兵位

	int count = 0;//count来计数
	LTNode* cur = phead->next;//让cur去遍历
	while (cur != phead)
	{
		count++;
		cur = cur->next;
	}
	return count;
}

在Test.c下

TestList6()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 1);
	LTInsert(plist, 3);
	LTInsert(plist, 3);
	LTInsert(plist, 4);
	LTInsert(plist, 3);
	LTPrint(plist);

	LTRemove(plist, 3);
	LTPrint(plist);

	printf("%d\n", LTTotal(plist));
}

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

双向链表获取第i位置的结点

在List.h下

// 双向链表获取第i位置的结点
LTNode* LTGet(LTNode* phead, int i);

在List.c下

// 双向链表获取第i位置的结点
LTNode* LTGet(LTNode* phead, int i)
{
	assert(phead);//有哨兵位

	int length = LTTotal(phead);
	LTNode* cur = phead->next;
	if (i == 0)
	{
		return phead;
	}
	else if (i<0 || i>length)//位置不合法
	{
		return NULL;
	}
	else if (i <= (length / 2))//从表头开始遍历
	{
		cur = phead->next;
		for (int count = 1; count < i; count++)
		{
			cur = cur->next;
		}
	}
	else//从表尾开始遍历
	{
		cur = phead->prev;
		for (int count = 1; count <= length - i; count++)
		{
			cur = cur->prev;
		}
	}
	return cur;
}

在Test.c下

//测试函数
TestList7()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 5);
	LTInsert(plist, 6);
	LTInsert(plist, 7);
	LTInsert(plist, 8);
	LTInsert(plist, 9);
	LTPrint(plist);

	LTNode* pos = LTGet(plist,3);
	if (pos)
	{
		LTErase(pos);
		pos = NULL;
	}
	LTPrint(plist);
}

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

双向链表的清空

在List.h下

// 双向链表的清空
void LTClean(LTNode* phead);

在List.c下

// 双向链表的清空
void LTClear(LTNode* phead)
{
	assert(phead);//有哨兵位

	while (!LTEmpty(phead))//如果不为空就一直头删
	{
		LTPopFront(phead);
	}
}

在Test.c下

//测试函数
TestList8()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 5);
	LTInsert(plist, 6);
	LTInsert(plist, 7);
	LTInsert(plist, 8);
	LTInsert(plist, 9);
	LTPrint(plist);

	LTClear(plist);
	LTPrint(plist);
}

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

总代码(想直接看结果的可以看这里)

在List.h下

#pragma once//使同一个文件不会被包含(include)多次,不必担心宏名冲突


//先将可能使用到的头文件写上
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

typedef int LTDataType;//假设结点的数据域类型为 int
//给变量定义一个易于记忆且意义明确的新名字,并且便于以后存储其它类型时方便改动
//(比如我晚点想存double类型的数据,我就直接将 int 改为 double )

// 带哨兵位双向循环链表的结构体定义
typedef struct ListNode
{
	struct ListNode* prev;//前驱指针域:存放上一个结点的地址
	struct ListNode* next;//后继指针域:存放下一个结点的地址
	LTDataType data;//数据域
}LTNode;
//struct 关键字和 ListNode 一起构成了这个结构类型
//typedef 为这个结构起了一个别名,叫 LTNode,即:typedef struct ListNode LTNode 
//现在就可以像 int 和 double 那样直接使用 LTNode 来定义变量



// 双向链表的初始化

// 如果是单链表直接给个空指针就行,不需要单独写一个函数进行初始化
// 即:LTNode* plist = NULL;
// 那为什么顺序表、带头双向循环链表有呢?
// 因为顺序表、带头双向循环链表的结构并不简单,
// 如:顺序表顺序表为空size要为0,还要看capacity是否要开空间,
//若不开空间capacity=0,指针要给空,若开空间,还要检查malloc是否成功
// 带头双向循环链表要开个结点,检查malloc是否成功,然后让结点自己指向自己
// 顺序表和双向循环链表的初始化有点复杂,最好构建一个函数
LTNode* LTInit();

// 双向链表在pos位置之前进行插入x
void LTInsert(LTNode* pos, LTDataType x);

// 双向链表的打印
void LTPrint(LTNode* phead);

// 双向链表删除pos位置的结点
void LTErase(LTNode* pos);

//双向链表优于单链表的点——不需要找尾、二级指针
// (我们改的不是结构体的指针,改的是结构体的变量)
// 双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x);

// 双向链表的判空
bool LTEmpty(LTNode* phead);

// 双向链表的尾删
void LTPopBack(LTNode* phead);

// 双向链表头插
void LTPushFront(LTNode* phead, LTDataType x);

// 双向链表头删
void LTPopFront(LTNode* phead);

// 双向链表查找值为x的结点
LTNode* LTFind(LTNode* phead, LTDataType x);

// 双向链表的销毁
void LTDestory(LTNode* phead);

// 双向链表的修改,修改pos位置的值为x
void LTModify(LTNode* pos, LTDataType x);

// 双向链表删除值为x的结点
void LTRemove(LTNode* phead, LTDataType x);

// 双向链表计算结点总数(不计phead)
int LTTotal(LTNode* phead);

// 双向链表获取第i位置的结点
LTNode* LTGet(LTNode* phead, int i);

// 双向链表的清空
void LTClear(LTNode* phead);

在List.c下

#include"List.h"

//动态申请一个结点
LTNode* BuyListNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)//如果malloc失败
	{
		perror("malloc fail");
		return NULL;
	}
	//如果malloc成功
	//初始化一下,防止野指针,如果看到返回的是空指针,那逻辑可能有些错误
	node->next = NULL;
	node->prev = NULL;
	node->data = x;

	return node;
}

// 双向链表的初始化
LTNode* LTInit()
{
	LTNode* phead = BuyListNode(-1);
	//自己指向自己
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

// 双向链表在pos位置之前进行插入x
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//pos肯定不为空

	LTNode* prev = pos->prev;
	LTNode* newnode = BuyListNode(x);//创建一个需要插入的结点

	prev->next = newnode;
	newnode->prev = prev;

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

// 双向链表的打印
void LTPrint(LTNode* phead)
{
	assert(phead);//有哨兵位
	printf("<=>phead<=>");
	LTNode* cur = phead->next;//cur指向第一个要打印的结点
	while (cur != phead)//cur等于头结点时打印就结束了
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

// 双向链表删除pos位置的结点
void LTErase(LTNode* pos)
{
	assert(pos);//pos肯定不为空

	LTNode* posprev = pos->prev;
	LTNode* posnext = pos->next;

	posprev->next = posnext;
	posnext->prev = posprev;

	free(pos);
	pos = NULL;
	//这个置空其实已经没有意义了,形参的改变不会改变实参
}

// 双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);//有哨兵位

	//法一:(便于新手更好地理解双向链表的尾插)
	//一步就可完成链表为空/不为空的尾插
	//LTNode* newnode = BuyListNode(x);
	//LTNode* tail = phead->prev;

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

	//法二:函数复用(简单方便)
	LTInsert(phead, x);
}

// 双向链表的判空
bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
	//两者相等就是空链表(返回真),两者不相等就不是空链表(返回假)
}

// 双向链表的尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);//有哨兵位

	//法一:(便于新手更好地理解双向链表的尾删)
	//assert(!LTEmpty(phead));//判空

	//LTNode* tail = phead->prev;
	//LTNode* tailPrev = tail->prev;

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

	//法二:函数复用
	LTErase(phead->prev);
}

// 双向链表头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);//有哨兵位

	//LTNode* newnode = BuyListNode(x);//创建一个要插入的结点

	//法一:只用phead和newnode两个指针(便于新手更好地理解双向链表的头插)
	//newnode->next = phead->next;
	//phead->next->prev = newnode;

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

	//法二:多用了first先记住第一个结点(便于新手更好地理解双向链表的头插)
	//LTNode* first = phead->next;
	//phead->next = newnode;
	//newnode->prev = phead;

	//newnode->next = first;
	//first->prev = newnode;

	//法三:函数复用(简单方便)
	LTInsert(phead->next, x);
}

// 双向链表头删
void LTPopFront(LTNode* phead)
{
	assert(phead);//有哨兵位
	assert(!LTEmpty(phead));//判空

	//法一:(便于新手更好地理解双向链表的头删)
	//LTNode* head = phead->next;
	//LTNode* headnext = head->next;

	//phead->next = headnext;
	//headnext->prev = phead;

	//free(head);
	//head = NULL;

	//法二:函数复用(简单方便)
	LTErase(phead->next);
}

// 双向链表查找值为x的结点
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);//有哨兵位

	LTNode* cur = phead->next;
	while (cur != phead)//让cur去遍历
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

// 双向链表的销毁
void LTDestory(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* curnext = cur->next;
		free(cur);
		cur = curnext;
	}
	free(phead);
	phead = NULL;
	//这个置空其实已经没有意义了,形参的改变不会改变实参
	//我们为了保持接口的一致性,不传二级指针,选择在测试的时候置空
}

// 双向链表的修改,修改pos位置的值为x
void LTModify(LTNode* pos, LTDataType x)
{
	assert(pos);//pos肯定不为空
	pos->data = x;
}

// 双向链表删除值为x的结点
void LTRemove(LTNode* phead, LTDataType x)
{
	assert(phead);//有哨兵位
	LTNode* pos = phead->next;
	while (pos != phead)
	{
		pos = LTFind(phead, x);
		if (pos == NULL)//如果遍历完
		{
			return NULL;
		}
		LTErase(pos);
		pos = pos->next;
	}
}

// 双向链表计算结点总数(不计phead)
int LTTotal(LTNode* phead)
{
	assert(phead);//有哨兵位

	int count = 0;//count来计数
	LTNode* cur = phead->next;//让cur去遍历
	while (cur != phead)
	{
		count++;
		cur = cur->next;
	}
	return count;
}

// 双向链表获取第i位置的结点
LTNode* LTGet(LTNode* phead, int i)
{
	assert(phead);//有哨兵位

	int length = LTTotal(phead);
	LTNode* cur = phead->next;
	if (i == 0)
	{
		return phead;
	}
	else if (i<0 || i>length)//位置不合法
	{
		return NULL;
	}
	else if (i <= (length / 2))//从表头开始遍历
	{
		cur = phead->next;
		for (int count = 1; count < i; count++)
		{
			cur = cur->next;
		}
	}
	else//从表尾开始遍历
	{
		cur = phead->prev;
		for (int count = 1; count <= length - i; count++)
		{
			cur = cur->prev;
		}
	}
	return cur;
}

// 双向链表的清空
void LTClear(LTNode* phead)
{
	assert(phead);//有哨兵位

	while (!LTEmpty(phead))//如果不为空就一直头删
	{
		LTPopFront(phead);
	}
}

在Test.c下

#include"List.h"

//测试函数
void TestList1()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 1);
	LTInsert(plist, 2);
	LTInsert(plist, 3);
	LTInsert(plist, 4);
	LTPrint(plist);

	LTErase(plist->next);
	LTPrint(plist);

}

//测试函数
void TestList2()
{
	LTNode* plist = LTInit();
	LTPushBack(plist, 5);
	LTPushBack(plist, 6);
	LTPushBack(plist, 7);
	LTPushBack(plist, 8);
	LTPrint(plist);

	LTPopBack(plist);
	LTPopBack(plist);
	LTPrint(plist);

}

//测试函数
void TestList3()
{
	LTNode* plist = LTInit();
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
	LTPrint(plist);

	LTPopFront(plist);
	LTPopFront(plist);
	LTPrint(plist);
}

//测试函数
TestList4()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 1);
	LTInsert(plist, 2);
	LTInsert(plist, 3);
	LTInsert(plist, 4);
	LTPrint(plist);

	LTNode* pos = LTFind(plist, 3);
	if (pos)
	{
		LTErase(pos);
		pos = NULL;
	}
	LTPrint(plist);

	LTDestory(plist);
	plist = NULL;//在这里置空
}

//测试函数
TestList5()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 1);
	LTInsert(plist, 2);
	LTInsert(plist, 3);
	LTInsert(plist, 4);
	LTPrint(plist);

	LTModify(plist->next, 5);
	LTPrint(plist);

}

TestList6()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 1);
	LTInsert(plist, 3);
	LTInsert(plist, 3);
	LTInsert(plist, 4);
	LTInsert(plist, 3);
	LTPrint(plist);

	LTRemove(plist, 3);
	LTPrint(plist);

	printf("%d\n", LTTotal(plist));
}

//测试函数
TestList7()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 5);
	LTInsert(plist, 6);
	LTInsert(plist, 7);
	LTInsert(plist, 8);
	LTInsert(plist, 9);
	LTPrint(plist);

	LTNode* pos = LTGet(plist, 3);
	if (pos)
	{
		LTErase(pos);
		pos = NULL;
	}
	LTPrint(plist);

	LTClear(plist);
	LTPrint(plist);
}

//测试函数
TestList8()
{
	LTNode* plist = LTInit();
	LTInsert(plist, 5);
	LTInsert(plist, 6);
	LTInsert(plist, 7);
	LTInsert(plist, 8);
	LTInsert(plist, 9);
	LTPrint(plist);

	LTClear(plist);
	LTPrint(plist);
}

int main()
{
	//TestList1();
	//TestList2();
	//TestList3();
	//TestList4();
	//TestList5();
	//TestList6();
	//TestList7();
	TestList8();
	return 0;
}

欢迎指正

 

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

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

相关文章

Windows命令提示行使用指南一

命令提示行使用指南 前言一、起源和发展二、和DOS的关系三、常用命令 前言 cmd 是 Windows 操作系统中的命令行界面&#xff08;CLI&#xff09;&#xff0c;也称为命令提示符&#xff08;CMD&#xff09;或批处理文件。它是 Windows 命令行界面的主要组成部分&#xff0c;用于…

《Linux 内核设计与实现》07. 中断和中断处理

文章目录 注册中断处理程序释放中断处理程序编写中断处理程序共享的中断处理程序中断例程实例 中断上下文/proc/interrupts中断控制禁止和激活中断禁用指定中断线中断系统的状态 注册中断处理程序 // 分配一条给定的 irq 中断线 request_irq(unsigned int irq, irq_handler_t …

『python爬虫』13. 视频地址防盗链实战案例(保姆级图文)

目录 1. 寻找视频真实url地址&#xff08;视频地址被加密了&#xff09;2. 防盗链中的来源判断完整代码总结 欢迎关注 『python爬虫』 专栏&#xff0c;持续更新中 欢迎关注 『python爬虫』 专栏&#xff0c;持续更新中 1. 寻找视频真实url地址&#xff08;视频地址被加密了&am…

【刷题笔记】不要二+把字符串转换为整数

一、不要二 题目&#xff1a; 牛客网链接&#xff1a;不要二_牛客题霸_牛客网 描述 二货小易有一个W*H的网格盒子&#xff0c;网格的行编号为0~W-1&#xff0c;网格的列编号为0~H-1。每个格子至多可以放一块蛋糕&#xff0c;任意两块蛋糕的欧几里得距离不能等于2。 对…

camunda表达式如何使用

在Camunda中&#xff0c;表达式是一种灵活的方式&#xff0c;可以用于在流程定义和表单中计算和处理数据。表达式可以在Camunda的各个环节中使用&#xff0c;例如服务任务、网关、表单、条件等。 以下是Camunda表达式的一些常见用途&#xff1a; 1、计算值&#xff1a;表达式可…

腾讯云轻量16核32G28M带宽服务器CPU流量性能测评

腾讯云轻量16核32G28M服务器3468元15个月&#xff0c;折合每月231元&#xff0c;28M公网带宽下载速度峰值可达3584KB/s&#xff0c;折合3.5M/秒&#xff0c;系统盘为380GB SSD盘&#xff0c;6000GB月流量&#xff0c;折合每天200GB流量。腾讯云百科来详细说下腾讯云轻量应用服务…

SeaweedFS学习笔记:架构和快速入门

目录 1. 介绍1.1 Components1.2 Master 服务1.3 Volume 服务1.4 Filer服务1.5 S3服务1.6 Volume的概念1.7 Collection的概念 2. 快速入门2.1 安装 SeaweedFS2.2 启动 Master 服务2.3 启动 Volume 服务2.4 快速启动一个Master服务和一个Volume服务2.5 测试 3. 参考 1. 介绍 1.1 …

asp.net汽车保养美容店维修管理系统

本系统汽车预约美容系统分为前台和后台两部分&#xff0c;具体功能如下 前台部分功能 1.注册登录&#xff0c;用户通过注册登录之后可以进行保养和美容项目的预约 2.新闻资讯&#xff0c;查看和汽车保养&#xff0c;美容相关的新闻资讯信息 3.美容查看&#xff0c;查看汽车美容…

6年测开经验,从功能测试到测试开发,每一步都深思熟虑...

蓦然回首&#xff0c;软件测试风风雨雨的这几年&#xff0c;起初每天的工作只是鼠标点点点&#xff0c;我还真不知道怎么办&#xff0c;闲的时候真的怀疑自己的存在价值&#xff0c;每天拿着7000的工资&#xff0c;飘荡在繁华的深圳&#xff0c;吃不饱也饿不死&#xff0c;未来…

【PWN · ret2libc】ret2libc1

ret2libc的第一题 目录 前言 一、动态链接 二、ret2libc原理 三、exp编写 干货 干货一&#xff1a;python下的ELF 干货二&#xff1a;strings看看有没有待选字符串 编写exp 总结 前言 本来是和学习ret2text\ret2shellcode\ret2syscall一样在网上找文字资源&#x…

28从零开始学Java之面向对象和面向过程到底有哪些区别?

作者&#xff1a;孙玉昌&#xff0c;昵称【一一哥】&#xff0c;另外【壹壹哥】也是我哦 千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者 前言 壹哥相信&#xff0c;经过你对前面文章中技术点的学习&#xff0c;现在的你应该已经对Java具备了初步…

【UE】直升机沿样条线移动

效果 步骤 1. 将虚幻商城中的免费资产导入工程 下载完毕后可以看到如下文件 2. 新建一个Actor蓝图类&#xff0c;命名为“Track”&#xff0c;这个蓝图就是用来画样条线的 打开“Track”&#xff0c;添加样条组件 3. 打开“BP_West_Heli_AH64D” 在事件图表中先新建一个时间轴…

Unity冷知识:读取用户输入应该写在Update还是FixedUpdate里?

Unity冷知识&#xff1a;读取用户输入应该写在Update还是FixedUpdate里&#xff1f; 版权声明&#xff1a; 本文为“优梦创客”原创文章&#xff0c;您可以自由转载&#xff0c;但必须加入完整的版权声明文章内容&#xff0c;不得删减、修改、演绎相关学习资源见文末 一些人…

1.SpringCloud技术

SpringCloud01 1.认识微服务 随着互联网行业的发展&#xff0c;对服务的要求也越来越高&#xff0c;服务架构也从单体架构逐渐演变为现在流行的微服务架构。这些架构之间有怎样的差别呢&#xff1f; 1.0.学习目标 了解微服务架构的优缺点 1.1.单体架构 单体架构&#xff…

初识C++之线程库

目录 一、C中的线程使用 二、C的线程安全问题 1. 加锁 2. 变为原子操作 3. 递归里面的锁 4. 定时锁 5. RAII的锁 三、条件变量 1. 为什么需要条件变量 2. 条件变量的使用 2.1 条件变量的相关函数 2.2 wait函数 一、C中的线程使用 线程的概念在linux中的线程栏已经…

9个加密货币交易所被查封,交易所安全审计后仍不安全

美国联邦调查局和乌克兰警方查封了九个加密货币交易网站&#xff0c;这些网站为包括勒索软件参与者在内的诈骗者和网络犯罪分子洗钱提供了便利。 联邦调查局 FBI 在其公告中表示&#xff0c;该行动是在虚拟货币响应小组、乌克兰国家警察和该国法律检察官的帮助下进行的。 此次…

ai皮带跑偏撕裂监测算法 yolov7

ai皮带跑偏撕裂监测系统算法基于yolov7网络模型人工智能视觉技术&#xff0c;ai皮带跑偏撕裂监测算法模型自动识别现场画面中传送皮带撕裂、跑偏、偏移等情况&#xff0c;立即告警抓拍存档同步回传后台。YOLO 的核心思想就是把目标检测转变成一个回归问题&#xff0c;利用整张图…

Git入门学习

Git是什么&#xff1f; 是一种免费开源的分布式版本控制系统&#xff0c;区别于集中式挂历系统&#xff08;SVN,CVS&#xff09;的是分布式每人都有一个“档案馆”&#xff0c;而集中式是只有一个“档案馆”。 这样的话&#xff0c;如果你使用git进行开发&#xff0c;感觉自己…

多层PCB层叠结构

在设计多层PCB电路板之前&#xff0c;设计者需要首先根据电路的规模、电路板的尺寸和电磁兼容&#xff08;EMC&#xff09;的要求来确定所采用的电路板结构&#xff0c;也就是决定采用4层&#xff0c;6层&#xff0c;还是更多层数的电路板。确定层数之后&#xff0c;再确定内电…

全景丨0基础学习VR全景制作,平台篇第18章:热点功能-音频

大家好&#xff0c;欢迎观看蛙色VR官方——后台使用系列课程&#xff01; 功能说明 应用场景 热点&#xff0c;指在全景作品中添加各种类型图标的按钮&#xff0c;引导用户通过按钮产生更多的交互&#xff0c;增加用户的多元化体验。 音频热点&#xff0c;即点击热点后会直接播…