当前位置:   article > 正文

【数据结构】C语言实现双向链表(带头结点、循环)_构建空链表(30分),依次完成下述内容(双向链表,且带头结点): (1) 使用头插法,插入4

构建空链表(30分),依次完成下述内容(双向链表,且带头结点): (1) 使用头插法,插入4


一、带头结点的循环双向链表

在这里插入图片描述

二、结点与接口定义

结点定义:

typedef int ListDataType;
typedef struct LinkedListNode
{
	struct LinkedListNode* next;
	struct LinkedListNode* prev;
	ListDataType data;
}LinkedListNode;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

接口定义:

LinkedListNode* LinkedListInit();
void LinkedListPrint(LinkedListNode* phead);

bool LinkedListEmpty(LinkedListNode* phead);
void LinkedListPushBack(LinkedListNode* phead, ListDataType x);
void LinkedListPushFront(LinkedListNode* phead, ListDataType x);
void LinkedListPopBack(LinkedListNode* phead);
void LinkedListPopFront(LinkedListNode* phead);

LinkedListNode* LinkedListFind(LinkedListNode* phead, ListDataType x);

// 在pos之前插入
void LinkedListInsert(LinkedListNode* pos, ListDataType x);
// 删除pos位置的值
void LinkedListErase(LinkedListNode* pos);

void LinkedListDestroy(LinkedListNode* phead);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

三、实现

3.1 申请节点

我们将申请结点的代码封装成函数,方便后续使用

LinkedListNode* CreateLinkedListNode(ListDataType x)
{
	LinkedListNode* newnode = (LinkedListNode*)malloc(sizeof(LinkedListNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3.2 初始化

由于是带头结点的双向链表,因此在使用链表前,我们需要对链表进行初始化。

LinkedListNode* LinkedListInit()
{
	LinkedListNode* phead = CreateLinkedListNode(230510);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.3 打印

遍历链表,值得说的是,带头结点的双向链表的循环结束条件是 cur != phead

void LinkedListPrint(LinkedListNode* phead)
{
	assert(phead);

	LinkedListNode* cur = phead->next;

	printf("guard<->");
	while (cur != phead)
	{
		printf("%d<->", cur->data);
		cur = cur->next;
	}
	printf("\n");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3.4 尾插

尾插时,需要先找到尾结点,然后将新结点插入到尾结点后面。

void LinkedListPushBack(LinkedListNode* phead, ListDataType x)
{
	assert(phead);

	// 1.找到尾结点
	LinkedListNode* tail = phead->prev;

	// 2.插入到尾结点后面
	LinkedListNode* newnode = CreateLinkedListNode(x);
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3.5 头插

第一种写法,要注意现将newnode后面的结点进行链接,然后再讲newnode链接到phead后面。

void LinkedListPushFront(LinkedListNode* phead, ListDataType x)
{
	assert(phead);

	LinkedListNode* newnode = CreateLinkedListNode(x);
	// 与原来的第一个数据结点链接
	newnode->next = phead->next;
	phead->next->prev = newnode; // newnode->next->prev = newnode;

	// newnode变为新的第一个数据结点
	phead->next = newnode;
	newnode->prev = phead;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

第二种写法(推荐写法),我们使用变量记录phead的next,记为first,这样newnode插入到phead和first之间,这样逻辑比较清楚。

void LinkedListPushFront(LinkedListNode* phead, ListDataType x)
{
	assert(phead);

	LinkedListNode* newnode = CreateLinkedListNode(x);
	// 用变量记录第一个结点
	LinkedListNode* first = phead->next;

	// 与原来的第一个数据结点链接
	newnode->next = first;
	first->prev = newnode; 

	// newnode变为新的第一个数据结点
	phead->next = newnode;
	newnode->prev = phead;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

3.6 尾删

phead的prev是尾tail,tail的prev是tailPrev。

void LinkedListPopBack(LinkedListNode* phead)
{
	assert(phead);

	LinkedListNode* tail = phead->prev;
	LinkedListNode* tailPrev = tail->prev;

	free(tail);
	tailPrev->next = phead;
	phead->prev = tailPrev;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

上面代码的问题是链表为空的情况报错,于是我们在该函数内部对空链表进行断言:

void LinkedListPopBack(LinkedListNode* phead)
{
	assert(phead);
	assert(!LinkedListEmpty(phead)); // 删除时链表不能为空

	LinkedListNode* tail = phead->prev;
	LinkedListNode* tailPrev = tail->prev;

	free(tail);
	tailPrev->next = phead;
	phead->prev = tailPrev;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

3.7 判断链表为空断言

判断链表为空逻辑:

bool LinkedListEmpty(LinkedListNode* phead)
{
	assert(phead);

	return phead->next == phead;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

使用链表为空的断言:

assert(!LinkedListEmpty(phead)); // 链表为空时error
  • 1

3.8 头删

头删时需要将第一个结点删除,很容易便想到以下代码:

void LinkedListPopFront(LinkedListNode* phead)
{
	assert(phead);
	assert(!LinkedListEmpty(phead)); // 删除时链表不能为空

	LinkedListNode* next = phead->next;

	phead->next = next->next;
	phead->next->prev = phead; // next->next->prev = phead;
	free(next); 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

为了提高可读性,推荐使用以下代码,定义first和second两个变量指向第一个和第二个:

void LinkedListPopFront(LinkedListNode* phead)
{
	assert(phead);
	assert(!LinkedListEmpty(phead)); // 删除时链表不能为空

	LinkedListNode* first = phead->next;
	LinkedListNode* second = first->next;

	phead->next = second;
	second->prev = phead;
	free(first);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

3.9 查找find

查找的本质就是遍历链表

LinkedListNode* LinkedListFind(LinkedListNode* phead, ListDataType x)
{
	assert(phead);
	
	LinkedListNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3.10 插入insert-在pos之前插入

pos的来源一般是find的结果

void LinkedListInsert(LinkedListNode* pos, ListDataType x)
{
	assert(pos);

	LinkedListNode* prev = pos->prev;
	LinkedListNode* newnode = CreateLinkedListNode(x);

	// prev newnode pos
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

3.11 头插尾插复用insert

有了上面的insert在任意位置插入,我们可以修改尾插代码:

void LinkedListPushBack(LinkedListNode* phead, ListDataType x)
{
	assert(phead);

	// 在phead之前插入,也就是尾插
	LinkedListInsert(phead, x);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

同理也可以修改头插代码:

void LinkedListPushFront(LinkedListNode* phead, ListDataType x)
{
	assert(phead);

	LinkedListInsert(phead->next, x);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.12 删除erase-删除pos位置

同insert一样,pos也应该是调用者通过find返回的结果。

void LinkedListErase(LinkedListNode* pos)
{
	assert(pos);

	LinkedListNode* posPrev = pos->prev;
	LinkedListNode* posNext = pos->next;

	posPrev->next = posNext;
	posNext->prev = posPrev;
	free(pos);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

3.13 头删尾删复用erase

有了上面的erase在任意位置删除,我们可以修改尾删的代码:

void LinkedListPopBack(LinkedListNode* phead)
{
	assert(phead);
	assert(!LinkedListEmpty(phead));
	LinkedListErase(phead->prev);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

同理也可以修改头删的代码:

void LinkedListPopFront(LinkedListNode* phead)
{
	assert(phead);
	assert(!LinkedListEmpty(phead));

	LinkedListErase(phead->next);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3.14 销毁destroy

记得释放头结点phead:

void LinkedListDestroy(LinkedListNode* phead)
{
	assert(phead);

	LinkedListNode* cur = phead->next;
	while (cur != phead)
	{
		LinkedListNode* next = cur->next;
		free(cur);
		cur = next;
	}

	free(phead);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

源码

gitee-LinkedList

总结

带头结点、双向、循环链表的实现都非常的简单,需要注意判空条件与遍历终止的条件。

在代码写法上,对于某个节点的前一个或后一个的问题,我们最好分别使用变量去记录,这样代码的逻辑更清晰,可读性更高。例如尾插时的tail,尾删时的tail和tailPrev,以及头删时的first与second,erase中的posPrev与posNext,这些变量的使用,提高了代码的可读性。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/码创造者/article/detail/789541
推荐阅读
相关标签
  

闽ICP备14008679号