什么是链表

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表

中的指针链接次序实现的 。

链表的结构有八种,分别是单向带头循环链表,单向带头非循环链表,单向不带头循环链表,单向不带头非循环链表,双向带头循环链表,双向带头非循环链表,双向不带头循环链表,双向不带头非循环链表

其中无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结 构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带 来很多优势,实现反而简单了,后面我们代码实现了就知道了。

下面我会分别展示单向不带头非循环链表和双向带头循环链表的示意图:

双向带头循环链表的实现_链表

双向带头循环链表的实现_链表_02

对于无头单向非循环链表的实现我在我前面的博客已经写出来了,现在我来实现带头双向循环链表

双向带头循环链表要实现的接口函数

我先将我们要实现的接口写出来

// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType _data;
	struct ListNode* _next;
	struct ListNode* _prev;
}ListNode;

// 创建返回链表的头结点.
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

函数一:创建具有哨兵卫的链表

首先我们要知道哨兵卫里面要储存的是非有效数据,这里我就将这个数据值赋值为-1,

函数实现:

ListNode* ListCreate()
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->_data = -1;
	newnode->_next = newnode;//即使只有一个哨兵卫我们也要让哨兵卫的next和prev指向本身
	newnode->_prev = newnode;
	return newnode;
}

函数二:头插数据方式一

下面我们来学习头插数据的一种方式,为什么是方式一呢?在下面我们会学习到另外一个函数,而复用那一个函数也可以实现函数的头插。

既然要头插数据我们肯定要先创建一个节点,我这里就将创建节点的代码也写成一个函数了,叫做BuyNode

ListNode* BuyNode(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->_data = x;
	newnode->_prev = NULL;
	newnode->_next = NULL;
}//这里只是先创建一个节点所以我就先将prev和next指针置空了

下面我们来通过画图的方式理解如何头插一个数据

双向带头循环链表的实现_循环链表_03

这种写法我们不需要创建一个新变量去储存d1的地址,但是必须最后再修改head的next指针。

代码:

void ListPushFront(ListNode* pHead, LTDataType x)
{
  assert(pHead);
	ListNode* newnode = BuyNode(x);
	newnode->_next = pHead->_next;//第一步
	pHead->_next->_prev = newnode;//第二步
	newnode->_prev = pHead;//第三步
	pHead->_next = newnode;
}//这里最后才修改phead的next是因为我们在前面需要通过phead的next来找到d1

那么对于只有一个head节点的链表这种写法适用吗?答案当然是肯定的。

双向带头循环链表的实现_双向链表_04

这种写法需要严格注意最后才修改head的next,那么有没有写法能让我们不需要注意顺序呢?当然有那就是在之前就将head的next保存下来

void ListPushFront(ListNode* pHead, LTDataType x)
{
  assert(pHead);
  ListNode* newnode = BuyNode(x);
	ListNode* secend = pHead->_next;//先将phead后面的next节点记录下来
	pHead->_next = newnode;
	newnode->_prev = pHead;
	newnode->_next = secend;
	secend->_prev = newnode;
	//这里我们就不需要注意顺序了
  }

这一种写法也是完美符合上面说的只有一个哨兵卫和还有其它数据的情况的。我们就先完成链表数据的打印之后再来检测功能。

函数三:打印链表数据

void ListPrint(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->_next;
	printf("guard<=>");
	while (cur != pHead)//当cur再次指向哨兵卫的时候循环就结束了
	{
		printf("%d<=>", cur->_data);
		cur = cur->_next;
	}
	printf("\n");
}

测试函数一二三是否正常

void test1()
{
	ListNode* plist = ListCreate();
	ListPushFront(plist, 5);
	ListPushFront(plist, 6);
	ListPushFront(plist, 7);
	ListPrint(plist);
}

双向带头循环链表的实现_双向链表_05

结果正确正常。

函数四:尾插数据方式一

和头插数据一样,我们在下面也会适用复用的方式写出尾插数据的方式二。

这种链表结构极好的一点就是找尾非常的方便,因为尾就在pHead的prev指针。

因为尾插的理解和头插的差不多,我这里就不用图来解释了。

void ListPushBack(ListNode* pHead, LTDataType x)
{
	/*ListNode* newnode = BuyNode(x);
	//依旧有两者写法
	pHead->_prev->_next = newnode;//通过phead的prev找到尾节点修改尾节点的next
	newnode->_prev = pHead->_prev;//再修改newnode的prev节点,依旧是通过phead的prev找到尾节点
	newnode->_next = pHead;//修改newnode的next让其指向头节点
	pHead->_prev = newnode;//依旧是最后才修改pHead的prev*/
	//方式二:+
	ListNode* tail = pHead->_prev;//将尾部节点先记录下来
	ListNode* newnode = BuyNode(x);//然后按照要求修改哨兵卫的prev,尾节点的next以及插入节点的next
  //和prev即可
	pHead->_prev = newnode;
	newnode->_next = pHead;
	tail->_next = newnode;
	newnode->_prev = tail;
}

测试:

双向带头循环链表的实现_循环链表_06

函数五:头删数据

要删除数据我们首先就要考虑是否还有数据让我们去删除,如果只有一个哨兵卫了那肯定是不能删除了,所以我在这里就可以写一个函数用于判断是否只有哨兵卫了。

bool LTEmpty(ListNode* phead)
{
	return phead->_next == phead;//这里如果满足next指向的是自身那么返回的就是真,
  //如果不满足那么返回的就是假
}

那么我们如何适用呢?我会在下面的头删数据的代码里面详细解释

我们先来理解我们要怎么头删一个代码

双向带头循环链表的实现_双向链表_07

也就是变成下面这样

双向带头循环链表的实现_链表_08

那么对于只有1个数据的情况下这样还适用吗?答案是肯定的。

双向带头循环链表的实现_双向链表_09

写法:

void ListPopFront(ListNode* pHead)
{
	assert(!LTEmpty(pHead));//那么这里如果返回的是真!就会让其变成假那么断言就会发生,反之则不会发生
	//我们可以选则不记录头节点后面的那个节点也可以选择记录
	//这里我就写记录的那一种写法了。
	//ListNode* first = pHead->_next;//这是待会要删除的元素
	//ListNode* secend = first->_next;//这是要成为头节点的元素
	//pHead->_next = secend;
	//secend->_prev = pHead;
	//free(first);
	//也可以选则不记录第二个元素的写法
	ListNode* del = pHead->_next;
	del->_next->_prev = pHead;
	pHead->_next = del->_next;
	free(del);
}

测试:

双向带头循环链表的实现_双向链表_10

而当多删除一组的时候自然会报错。

函数六:尾删数据

void ListPopBack(ListNode* pHead)
{
	assert(!LTEmpty(pHead));//那么这里如果返回的是真!就会让其变成假那么断言就会发生,反之则不会发生
	//为了删除尾部节点所以我们必须要运用一个变量去储存尾节点用于最后的free
	ListNode* tail = pHead->_prev;
	tail->_prev->_next = pHead;//修改尾前节点的next
	pHead->_prev = tail->_prev;
	free(tail);
}

测试:

void test3()
{
	ListNode* plist = ListCreate();
	ListPushBack(plist, 6);
	ListPushBack(plist, 5);
	ListPushBack(plist, 4);
	ListPrint(plist);
	 
	ListPopBack(plist);
	ListPrint(plist);

	ListPopBack(plist);
	ListPrint(plist);

	ListPopBack(plist);
	ListPrint(plist);
	

	//ListPopBack(plist);
	//ListPrint(plist);
}

测试如果我们选择多删除一组就会出现下面的报错

双向带头循环链表的实现_循环链表_11

删除那一组多余的之后就不会报错

双向带头循环链表的实现_循环链表_12

函数七:双向链表查找

这个函数的目的也很明确的了,寻找链表里面特定的节点,找到了那就返回该节点的地址,没找到就返回NULL。

函数:

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	ListNode* cur = pHead;
	while (cur != pHead)
	{
		if (cur->_data == x)
			return cur;
		else
			cur = cur->_next;
	}
	return NULL;//进行到这一步自然也就证明了没有找到该节点
}

至于测试这个函数点的功能我们在下面的函数完成之后再进行测试

函数八:链表pos位置前插入数据

我们要在特定的pos位置前面插入数据,首先肯定要先找到pos位置,如何找到适用的就是我们的函数七。那么在找到后我们要怎么插入数据呢?肯定是要改变pos前面位置的next以及pos位置的prev和新节点的next和prev,理解图和上面的插入差不多这里就不画了,我们直接上代码。

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);//如果pos不存在则不能进行插入
	ListNode* Prev = pos->_prev;//记录下pos前面节点的位置
	ListNode* newnode = BuyNode(x);//创建新节点
	Prev->_next = newnode;//修改pos前面节点的next让其指向newnode
	newnode->_prev = Prev;//修改newnode的prev让其指向原本pos前面的节点
	pos->_prev = newnode;//修改pos的prev节点让其指向newnode
	newnode->_next = pos;//修改newnode的next节点让其指向pos
}

测试:

双向带头循环链表的实现_链表_13

下面我们来实现头插和尾插的方法二:

复用函数八实现头插

我们首先要知道头插自然也就是在原先的头节点的前面插入一个新节点,所以我们直接复用函数八自然也可以实现头插功能。

void ListPushFront(ListNode* pHead, LTDataType x)
{
     ListInsert(pHead, x);
}

复用函数八实现尾插

那么我们也要找到在哪一个节点的前面插入一个节点能让这个节点成为尾节点呢?

这个节点肯定不能是尾节点,这个节点自然只能是我们的哨兵卫,我们在哨兵卫的前面插入一个节点因为循环这个节点自然也就成为了尾节点。

void ListPushBack(ListNode* pHead, LTDataType x)
{
	ListInsert(pHead, x);
}

虽然这和我们所理解的一般的尾插不同,但是只要我们不修改外面的plist指向的第一个节点让其一直指向的都是哨兵卫那么就没有问题,究其原因便是这个链表是循环的。

函数九:删除pos位置的节点

要删除一个节点无非也就是修改这个节点前面和后面节点的指针最后free掉pos即可

代码实现:

void ListErase(ListNode* pos)
{
	//对于所有的删除操作我们都要进行一次判空
	assert(!LTEmpty(pos));
	ListNode* posPrev = pos->_prev;//储存pos位置前面位置的节点
	ListNode* posNext = pos->_next;//储存pos位置后面的节点
	posPrev->_next = posNext;
	posNext->_prev = posPrev;
	free(pos);
}

测试:

void test6()
{
	ListNode* plist = ListCreate();
	ListPushBack(plist, 6);
	ListPushBack(plist, 5);
	ListPushBack(plist, 4);
	ListPrint(plist);
	ListNode* pos = ListFind(plist, 5);
	ListErase(pos);
	ListPrint(plist);
}

双向带头循环链表的实现_双向链表_14

复用函数九实现头删和尾删

//头删:ListErase(pHead->_next);
//尾删:ListErase(pHead->_prev);

函数十:销毁链表

void ListDestory(ListNode* pHead)
{
	ListNode* cur = pHead->_next;
	while (cur != pHead)
	{
		ListNode* tmp = cur;
		cur = cur->_next;
		free(tmp);
	}
	free(pHead);
}

这里的测试我就使用调式监视的方式检测了。

void test7()
{
	ListNode* plist = ListCreate();
	ListPushBack(plist, 6);
	ListPushBack(plist, 5);
	ListPushBack(plist, 4);
	ListPrint(plist);
	ListDestory(plist);
}

这是还未销毁前的

双向带头循环链表的实现_双向链表_15

销毁后的:

双向带头循环链表的实现_循环链表_16

下面我就将三个文件的所有代码都写下面

test.c

#include"Listnode.h"
void test1()
{
	ListNode* plist = ListCreate();
	ListPushFront(plist, 5);
	ListPushFront(plist, 6);
	ListPushFront(plist, 7);
	ListPrint(plist);
}
void test2()
{
	ListNode* plist = ListCreate();
	ListPushBack(plist, 6);
	ListPushBack(plist, 5);
	ListPushBack(plist, 4);
	ListPrint(plist);
}
void test3()
{
	ListNode* plist = ListCreate();
	ListPushBack(plist, 6);
	ListPushBack(plist, 5);
	ListPushBack(plist, 4);
	ListPrint(plist);
	 
	ListPopBack(plist);
	ListPrint(plist);

	ListPopBack(plist);
	ListPrint(plist);

	ListPopBack(plist);
	ListPrint(plist);
	

	//ListPopBack(plist);
	//ListPrint(plist);
}
void test4()
{
	ListNode* plist = ListCreate();
	ListPushBack(plist, 6);
	ListPushBack(plist, 5);
	ListPushBack(plist, 4);
	ListPrint(plist);
	
	
	ListPopFront(plist);
	ListPrint(plist);
	
	
	ListPopFront(plist);
	ListPrint(plist);


	ListPopFront(plist);
	ListPrint(plist);
}
void test5()
{
	ListNode* plist = ListCreate();
	ListPushBack(plist, 6);
	ListPushBack(plist, 5);
	ListPushBack(plist, 4);
	ListPrint(plist);
	ListNode* pos =  ListFind(plist, 5);
	ListInsert(pos, 50);//这里就是在5前面插入50
	ListPrint(plist);
}
void test6()
{
	ListNode* plist = ListCreate();
	ListPushBack(plist, 6);
	ListPushBack(plist, 5);
	ListPushBack(plist, 4);
	ListPrint(plist);
	ListNode* pos = ListFind(plist, 5);
	ListErase(pos);
	ListPrint(plist);
}
void test7()
{
	ListNode* plist = ListCreate();
	ListPushBack(plist, 6);
	ListPushBack(plist, 5);
	ListPushBack(plist, 4);
	ListPrint(plist);
	ListDestory(plist);
}
int main()
{
	//test1();//检测头插打印和初始化功能
	//test2();//检测尾插功能
	//test3();//检测尾删功能
	//test4();//检测头删功能
	//test5();//检测特定位置前的插入
	//test6();//检测特定位置的删除
	test7();
}

ListNode.h

// 带头+双向+循环链表增删查改实现
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType _data;
	struct ListNode* _next;
	struct ListNode* _prev;
}ListNode;

// 创建返回链表的头结点.
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

ListNode.c

#include"Listnode.h"
ListNode* ListCreate()
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->_data = -1;
	newnode->_next = newnode;//即使只有一个哨兵卫我们也要让哨兵卫的next和prev指向本身
	newnode->_prev = newnode;
	return newnode;
}
ListNode* BuyNode(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->_data = x;
	newnode->_prev = NULL;
	newnode->_next = NULL;
	return newnode;
}//这里只是先创建一个节点所以我就先将prev和next指针置空了
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	/*ListNode* newnode = BuyNode(x);
	newnode->_next = pHead->_next;//第一步
	pHead->_next->_prev = newnode;//第二步
	newnode->_prev = pHead;//第三步
	pHead->_next = newnode;//这里最后才修改phead的next是因为我们在前面需要通过phead的next来找到d1
	//不需要严格注意顺序的写法*/
	//ListNode* newnode = BuyNode(x);
	//ListNode* secend = pHead->_next;//先将phead后面的next节点记录下来
	//pHead->_next = newnode;
	//newnode->_prev = pHead;
	//newnode->_next = secend;
	//secend->_prev = newnode;
	//这里我们就不需要注意顺序了
	ListInsert(pHead->_next, x);
}
// 双向链表打印
void ListPrint(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->_next;
	printf("guard<=>");
	while (cur != pHead)
	{
		printf("%d<=>", cur->_data);
		cur = cur->_next;
	}
	printf("\n");
}
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	/*ListNode* newnode = BuyNode(x);
	//依旧有两者写法
	pHead->_prev->_next = newnode;
	newnode->_prev = pHead->_prev;
	newnode->_next = pHead;
	pHead->_prev = newnode;//依旧是最后才修改pHead的prev*/
	//方式二:+
	//ListNode* tail = pHead->_prev;//将尾部节点先记录下来
	//ListNode* newnode = BuyNode(x);
	//pHead->_prev = newnode;
	//newnode->_next = pHead;
	//tail->_next = newnode;
	//newnode->_prev = tail;
	ListInsert(pHead, x);
}
bool LTEmpty(ListNode* phead)
{
	return phead->_next == phead;//这里如果满足next指向的是自身那么返回的就是真,如果不满足那么返回的就是假
}
// 双向链表尾删
void ListPopBack(ListNode* pHead)
{
	assert(!LTEmpty(pHead));//那么这里如果返回的是真!就会让其变成假那么断言就会发生,反之则不会发生
	//为了删除尾部节点所以我们必须要运用一个变量去储存尾节点用于最后的free
	//ListNode* tail = pHead->_prev;
	//tail->_prev->_next = pHead;//修改尾前节点的next
	//pHead->_prev = tail->_prev;
	//free(tail);
	ListErase(pHead->_prev);
}
// 双向链表头删
void ListPopFront(ListNode* pHead)
{
	assert(!LTEmpty(pHead));//那么这里如果返回的是真!就会让其变成假那么断言就会发生,反之则不会发生
	//我们可以选则不记录头节点后面的那个节点也可以选择记录
	//这里我就写记录的那一种写法了。
	//ListNode* first = pHead->_next;//这是待会要删除的元素
	//ListNode* secend = first->_next;//这是要成为头节点的元素
	//pHead->_next = secend;
	//secend->_prev = pHead;
	//free(first);
	//也可以选则不记录第二个元素的写法
	//ListNode* del = pHead->_next;
	//del->_next->_prev = pHead;
	//pHead->_next = del->_next;
	//free(del);
	ListErase(pHead->_next);
}
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	ListNode* cur = pHead->_next;
	while (cur != pHead)
	{
		if (cur->_data == x)
			return cur;
		else
			cur = cur->_next;
	}
	return NULL;//进行到这一步自然也就证明了没有找到该节点
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);//如果pos不存在则不能进行插入
	ListNode* Prev = pos->_prev;//记录下pos前面节点的位置
	ListNode* newnode = BuyNode(x);//创建新节点
	Prev->_next = newnode;//修改pos前面节点的next让其指向newnode
	newnode->_prev = Prev;//修改newnode的prev让其指向原本pos前面的节点
	pos->_prev = newnode;//修改pos的prev节点让其指向newnode
	newnode->_next = pos;//修改newnode的next节点让其指向pos
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	//对于所有的删除操作我们都要进行一次判空
	assert(!LTEmpty(pos));
	ListNode* posPrev = pos->_prev;//储存pos位置前面位置的节点
	ListNode* posNext = pos->_next;//储存pos位置后面的节点
	posPrev->_next = posNext;
	posNext->_prev = posPrev;
	free(pos);
}
// 双向链表销毁
void ListDestory(ListNode* pHead)
{
	ListNode* cur = pHead->_next;
	while (cur != pHead)
	{
		ListNode* tmp = cur;
		cur = cur->_next;
		free(tmp);
	}
	free(pHead);
}

希望能对你有所帮助。如果有错误请严厉指出。我一定改正