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

双向链表(Double Linked List) 虽然单向链表能够100%解决逻辑关系为“一对一”数据的存储问题,但在解决那些需要大量查找前趋节点的问题是,单向链表无疑是不能用了,因为单向链表适合“从前往后”查找,并不适合“从后往前”查找。如果要提高链表的查找效率,那双向链表(双链表)无疑是首选。双向链表字面上的意思是“双向”的链表,如图1所示。图1 - 双向链表示意图双向指各个节点之间的逻辑关系是双向的,该链表通常只有一个头节点。...... 阅读详情

目录

概念

带头双向循环链表的实现

前情提示

双向链表的结构体定义

双向链表的初始化

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

双向链表在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;
}

欢迎指正❀

 

数据结构-链表结构-双向链表 双向链表 阅读详情

相关推荐

双链表详解(初始化、插入、删除、遍历)(数据结构与算法)

同时,删除节点后必须确保释放相应的内存空间,以防止内存泄漏问题的发生。11. 双链表相较于单链表需要额外存储指向前一个节点的指针,因此会在空间上占用更多的内存。7. 双链表的每个节点包含三个部分:数据域、指向前一个节点的指针和指向下一个节点的指针。在双链表中删除节点的操作相对比较复杂,因为我们需要维护前驱节点和后继节点之间的指针连接。在双链表中插入节点需要更新前驱节点和后继节点的指针连接,操作相对比较复杂。将 p 结点的next指针,指向q结点的后继结点。程序设计如下:注意修改指针时要注意顺序!

AII_IIA的博客 4530

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

双向链表所需要头文件 首先重定义类型名 意义我前几篇讲过几次了,这里就不在赘述了,(顺序表,单链表的开头都有说明) 然后我们需要一个结构体 结构体包含 : 存储数据的 a 指向一个节点的指针 next 指向上一个节点的指针 prve 双向链表实现的函数功能有 :1.申请动态内存空间 2.初始化 3.双向链表的头,尾的插入与删除,在合法的任意位置进行插入和删除 4.释放链表,5.打印链表 1.申请动态内存空间 因为进行插入和初始化需要用到此函数 所以我们要先实现此功能 1.先申.

烁岩的博客 7821

安卓开发者必看:Win11最新Fastboot驱动安装避坑指南(含Android Bootloader Interface选择技巧)

本文为安卓开发者提供了Windows 11系统下Fastboot驱动安装的详细指南,特别针对Android Bootloader Interface选择技巧和多设备并发连接时的驱动冲突问题。通过解析Windows 11的特殊驱动验证机制,推荐使用Google官方USB驱动,并提供安装流程、版本选择建议及疑难问题排查方法,帮助开发者高效完成开发环境配置。

sea99的博客 1329

【数据结构】—C语言实现双向链表(超详细!)

本文是作者对于双向链表以及循环链表的学习记录,也是对于链表学习画上的一个句号。

B1ue还在专注 5047

电力系统实时仿真技术:从硬件在环到主流平台实践

实时仿真是一种基于高性能计算硬件,在严格时间约束下模拟物理系统动态行为的关键技术。其核心原理在于通过硬实时计算,确保仿真步长内的模型求解与输出时间严格同步,从而构建高保真的虚拟测试环境。这项技术的核心价值在于为电力电子、新能源并网等复杂系统的研发提供了无风险的“数字沙盘”,能够安全地验证控制器逻辑、保护装置动作及系统稳定性。在工程实践中,硬件在环测试通过连接虚拟模型与真实硬件,形成了控制器硬件在环和功率硬件在环两大主流应用场景,广泛应用于设备研发、电网安全分析和人员培训。本文聚焦于电力系统领域,深入解析了实

weixin_30952479的博客 420

【WorkBuddy专栏15】灵感被折叠到「更多」里,真的不重要了吗?——一个「被低估」功能的当下价值与未来演变

【WorkBuddy专栏15】灵感被折叠到「更多」里,真的不重要了吗?——一个「被低估」功能的当下价值与未来演变

AI人人懂 958

(C语言)双向链表(教程)(指针)(数据结构)

(C语言)双向链表(教程)(指针)(数据结构)

2302_78101238的博客 949

linux驱动由浅入深系列:基于高通平台分析触摸屏(TP)、虚拟按键驱动

触摸屏的触摸板(touch panel简称TP)驱动的基本架构和普通按键驱动基本一致,可以参考文章:linux驱动由浅入深系列:输入子系统之二(编写一个gpio_key驱动)。只是其功能稍稍复杂些,配置也多些,下面一起来看一下。 TP包括 触摸屏、虚拟按键、虚拟按键背光灯 TP驱动芯片厂商主要有:Goodix(汇顶),FocalTech(敦泰科技),cypress(赛普拉斯),synapt

Radia的专栏 4766

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

利用C语言实现双向链表的基本功能: 头文件: //LinkList.c #ifndef _LINKLIST_H #define _LINKLIST_H #define SUCCESS 10000 #define FAILURE 10001 #define TRUE 10002 #define FALSE 10003 typedef int Elemtype; typedef s...

x18261294286的博客 1364

什么是 SAP Fiori 的 Technical Catalog

笔者之前的文章曾经提到一个叫做本文介绍 Technical Catalog 这个概念在 SAP Fiori 技术体系中的作用。首先我们如何在系统里查看这个 Catalog SAP_BASIS_TCR_T 里面包含的内容呢?如果大家照着本文开头那篇文章里提到的步骤,那么在自己系统里会创建好名叫的 PFCG role.我们登录 ABAP 系统,启动事务码 PFCG,打开这个 role, 就能看到它。展开之后,发现它扮演着一个容器的角色,里面包含了很多 ABAP 对象。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 742

【100%通过率】华为OD机试真题 Python 实现【网上商城优惠活动 / 模拟商场优惠打折II】

某网上商场举办优惠活动,发布了满减、打折、无门槛3种 优惠券,分别为: 1:每满100元优惠10元,无使用数限制,如100~199元可以使用1张减10元,200~299可使用2张减20元,以此类推; 2:92折券,1次限使用1张,如100元,则优惠后为92元, 3:无门槛5元优惠券,无使用数限制,直接减5元。

MISAYAONE的博客 9369

【数据结构】之 链表 带头双向循环 (C语言)(含完整代码)

1、基本概念: 带头双向循环链表:一个节点有两个指针,一个指向前一个节点(前指针)一个指向下一个节点(后指针);有一个头结点,该节点不存储如何值,头结点的前指针指向该链表的最后一个节点。最后一个节点的后指针指向头结点。 基本逻辑结构: 2、代码实现 2.1 准备工作: 首先对头文件的引用和创建一个结构体。这个结构体就是链表的节点,一个节点包含数据项和两个指针项,一个指针指向前一个节点的地址,另一个指针指向下一个节点的地址。DLNode* phead=NULL表示创建一个节点的指...

weixin_53936115的博客 1376

【第一章】核心思维:那个在此刻“消失”的 CPU

摘要:RTOS本质是"有状态的抢占"而非"多任务",单核MCU通过时间片分时复用实现任务切换。裸机编程的线性思维在RTOS中不再适用,开发者需接受代码随时被打断的现实。RTOS通过保存/恢复CPU寄存器组(上下文)实现任务切换,每个任务拥有独立栈空间。非原子操作(如全局变量自增)会导致竞态条件,必须使用临界区或互斥锁保护。RTOS编程需要建立并发思维,关注代码原子性、可重入性和栈空间分配。

一路往蓝-Anbo 的博客 103

【数据结构】双向带头循环链表(c语言)(附源码)

双向带头循环链表与单链表的差异、双向带头循环链表的增删查改等功能实现

2301_80955819的博客 1630

CANbus收发器ADM3053BRWZ-REEL7应用原理图设计

ADM3053 在 CAN 协议控制器与物理层总线之间创建一个 完全隔离的接口。该器件具有限流和热关断特性,可防止输出短路。额定温 度范围为工业温度范围,提供 20 引脚、宽体 SOIC 封装。该器件采用 5 V 单电源供电, 提供完全隔离的 CAN 解决方案。ADM3053 是一款隔离式控制器区域网络 (CAN) 物理层收 发器,集成隔离 DC/DC 转换器,符合 ISO 11898 标准。该器件采用 ADI 公司的。Coupler® 技术,将双通道隔离器、 CAN 收发器和 ADI 公司的。

zhusl6688的博客 1235

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

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

 C语言实现双向循环链表的基本功能与调试: //DoubleCircleLinkLst.h #ifndef _LINKLIST_H #define _LINKLIST_H #include &lt;stdio.h&gt; #include &lt;stdlib.h&gt; #define FAILURE 10000 #define SUCCESS 10001 #define T...

x18261294286的博客 2498

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

线性表之带头双向循环链表(数据结构)(VS)(C语言)(DoubleCircularGuardList)

1.初始化链表 2.创建返回链表的头结点 3.双向链表销毁 4.带头双向循环链表打印 5.双向链表尾插 6.双向链表头插 7.判断链表是否为空 8.双向链表尾删 9.双向链表头删 10.返回链表中的数据个数 11.双向链表查找 12.双向链表在pos的前面进行插入 13.双向链表删除pos位置的节点..................

zhanghgh的博客 411

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

数据结构——双向链表(C语言版)(带虚拟头节点)

该链表循环,所以在初始化的时候(只有虚拟头节点这一个节点),这个虚拟头节点的next指向自己,prev也指向自己。在pos位置之前插入,首先pos不能为空(不然不知在哪插入元素),否则报错(使用断言能更快发现错误)。循环链表打印的结束标志不像单向链表(NULL),它的结束标志是不等于虚拟头节点。创建一个元素为x的新节点,并把该节点与pos和pos->prev相连接。链表可以分为三大类:循环与不循环,双向和单向,带头与不带头。上期更新了不循环不带头的单向链表,这期为循环带头的双向链表。

Yair_的博客 315

线性表(双向链表)基本功能实现(C语言)(西工大数据结构)

这里写目录标题双向链表定义基本操作结构定义尾插法建立双向链表在第i个结点后插入e删除第i个结点并用e返回其值输出链表主代码 今天想写写双向链表的基本操作,其实和单链表差不多,所以就不做太多解释啦 双向链表定义 双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。 基本操作 结构定义 #include<stdio.h> #include<stdlib.h> typ

Lucky Home 398

数据结构——单向链表和双向链表的实现(C语言版)

这篇文章关于链表的介绍,还有单向链表和双向链表的C语言实现,内容干货满满,建议边看边上手敲代码!通过这篇文章,相信你已经对链表这个数据结构有了一定的了解,可以开始刷一些链表的OJ题目。如果只是看了一遍,建议上手敲敲代码,实践出真知。创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连哦,你的支持的我最大的动力!!!

2301_79171011的博客 3798

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

数据结构双向链表简单直白讲解

2301_80220607的博客 2008

C语言实现双向链表

本章使用C语言实现了数据结构中的双向链表,对于数据结构的初学者来说有一定难度,但对于提升对链表的理解来说意义非凡,值得我们亲自去实现。

2401_86557724的博客 2718

【C语言数据结构】之双向链表

本文主要介绍C语言双向链表的增删改查。我们主要手搓代码,以及容易出现的问题的强调。最主要我们还是要敲代码,而不是看和听。

weixin_61276540的博客 2543

C语言双向链表讲解

lfylcj C语言双向链表讲解 一、双向链表的概念 双向链表基于单链表。单链表是单向的,有一个头结点,一个尾结点,要访问任何结点,都必须知道头结点,不能逆着进行。而双链表添加了一个指针域,通过两个指针域,分别指向结点的前结点和后结点。这样的话,可以通过双链表的任何结点,访问到它的前结点和后结点。 在双向链表中,结点除含有数据域外,还有两个链域,一个存储直接后继结点的地址,...

疾风剑豪 6439

数据结构之C语言实现双向链表

C语言实现双向链表 双向链表主要实现以下功能: 双向链表创建 节点头插 节点尾插 指定位置插入节点 节点删除 链表排序 链表求长 /****************************************************************************************************** * function:自定义一个...

煮酒论英雄 8082
上一篇: 单链表(数据结构)(C语言)
outdated_socks
博客等级 码龄4年 185粉丝 17原创
评论 117
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值