C语言实现双向链表

前言

       在讲双向链表之前,我会先总结一下前面的知识点,如需直接看双向链表的,可以直接跳转到双向链表的实现去阅读~~

链表的分类

       在上一篇的8道算法题,我提到了用哨兵位可以很好地进行插入,这个哨兵位就是头结点!还有在解决约瑟夫问题时,我提到了使用循环链表的概念,循环链表就是头尾相连,形成一个环。
链表的分类有这几种情况:带不带头(头结点==哨兵位),单向还是双向(就是一个节点只能找到下一个节点的就是单向,如果既能找到上一个节点又能找到下一个节点就是双向),循环还是不循环(头尾相连的就是循环,头尾不相连的就是不循环)

所以链表一共有八大类,那我们回顾一下什么是单链表,单链表实际上就是不带头单向不循环的链表,这里我要讲的双向链表实际上是带头双向循环的链表,只要我们会这两个链表的实现,其他的链表实现也是很简单的~~

链表的优势

       在C语言进阶的第一篇文章中,我带大家实现了动态顺序表,但是动态顺序表还是有存在空间浪费的出现,举个例子,我一共有101个数据需要保存,但是顺序表在第一百零一的时候会进行2倍或3倍扩容,假设扩2倍,那就是变成200容量,这时候就有99个空间浪费了,链表就可以实现一个数据一个数据的申请节点,不会有空间的浪费,但是单链表有一个缺陷,尾插尾删等操作时需要遍历所有节点导致效率不高,这时候我们就可以使用双向链表来大幅减少这种遍历的出现,也就是我会在下面提到的链表结构。

但是链表也有一个缺点,在数据量少的情况下,链表其实浪费的空间可能更大,因为链表结构是需要带一个或者两个指针的~~

任何事物都有两面性,我们需要根据实际情况来选择合适的数据结构来解决问题才是最perfect!!!

双向链表的实现

双向链表的含义

       双向链表是带头结点(哨兵位),每一个结点都能找到前一个和后一个节点,并且头尾是相连的。形成带头双向循环的链表,双向链表就是它的简称。

那我们来定义双向链表的结构体:

typedef int ListDataType;

typedef struct ListNode
{
	ListDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

初始化和创建新节点

       注意双向链表是一个带头的循环的链表,带头意味着我们在初始化要创建一个头结点,那头结点的两个指针怎么处理?由于这是双向链表,需要头尾相连,因此我们将头节点的两个指针都指向自己!既然如此,我们就写一个函数来创建新节点,让每个新节点的指针开始都指向自己~~

//创建新节点
ListNode* CreatNewnode(ListDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail"); exit(1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = newnode;

	return newnode;
}

注意了,由于创建新节点的函数我们需要传入一个值,那我们的头结点就随便传入一个值,但是我们自己要知道头结点知识一个站岗的,不会存放有效值。
会不会有人会问,为什么不专门写一个函数来创建头结点,你可以自己尝试,我觉得没有必要,多写一个和创建新节点的代码感觉很浪费也没有很大必要~~

//初始化
void ListInit(ListNode** pphead)
{
	*pphead = CreatNewnode(-1);
}

头指针的传参问题

我们要知道初始化链表的时候就创建好头结点了,头指针就是指向这个头结点,所以我们不需要改变头结点,它是一个放哨的,与链表是共存亡的,所以我们一般情况下传一级指针就可以了~~
简单来说:头指针是指向头结点的,只要头结点还存在,头指针就没有必要发生改变~~

尾插

尾插操作,我们需要改变三个节点,分别是newnode,head,还有原来的尾节点head->prev。

//尾插
void ListPushBack(ListNode* phead, ListDataType x)
{
	ListNode* newnode = CreatNewnode(x);

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

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

这里建议大家先将newnode 的next和prev两个指针先连接好,毕竟改变newnode的指向不会影响到另外两个节点,这时候我们就要考虑剩下两个节点怎么连接,为了避免找不到d3这个节点,所以我先改变d3这个节点,再改变head的节点~~

头插

头插,我们需要改变三个节点,newnode,head,d1;我们还是先改变newnode(不会对另外两个产生影响),再改变d1(防止改变head的时候找不到d1),最后改变head。

//头插
void ListPushFront(ListNode* phead, ListDataType x)
{
	ListNode* newnode = CreatNewnode(x);
	
	newnode->prev = phead;
	newnode->next = phead->next;

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

打印

注意头结点存放的值是无效的,所以从头结点的下一个结点开始打印,由于链表是循环的,所以当回到头结点的时候就要停止打印(循环停止的条件)

//打印
void ListPrint(ListNode* phead)
{
	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

尾删

为了更好的进行删除操作,我使用一个变量保存要删除的节点,便于要删除的节点的前一个节点与头结点进行连接~~

//尾删
void ListPopBack(ListNode* phead)
{
	assert(phead && phead->next != phead);

	ListNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;
	free(del);
	del = NULL;
}

头删

注意这里的头删是删除第一个有效的节点,不是我们的哨兵位!!!

还是一样,使用一个变量保存要删除的节点

//头删
void ListPopFront(ListNode* phead)
{
	assert(phead && phead->next != phead);

	ListNode* del = phead->next;
	phead->next = del->next;
	del->next->prev = phead;
	free(del);
	del = NULL;
}

查找

从第一个有效节点开始遍历链表进行查找即可~~

//查找
ListNode* ListFind(ListNode* phead, ListDataType x)
{
	ListNode* pcur = phead->next;

	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}

	return NULL;
}

写这一个查找函数是为了方便我们后续指定位置的相关操作~~
还有要注意如果找不到就会放回NULL,所以后面的指定位置操作是记得判断pos是否有效!!!

指定位置删除

需要改变三个节点pos,pos->prev,pos->next这三个节点~~

//指定位置删除
void ListPopPos(ListNode* pos)
{
	assert(pos);
	
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

这里要注意了pos 如果就是头指针的话,是不可以进行删除操作的,由于我没有传头指针,所以没有判断这个条件~~

删除指定位置之前的数据

这里要注意,指定位置之前需要有有效的节点才能进行删除!!!所以这里你可以判断一下,传入头指针是为了更好地进行判断处理~~

//删除指定位置前的数据
void ListPopPosFront(ListNode* phead, ListNode* pos)
{
	assert(phead);
	assert(pos && pos->prev != phead);

	ListNode* del = pos->prev;
	del->prev->next = pos;
	pos->prev = del->prev;
	free(del);
	del = NULL;
}

删除指定位置之后的数据

这里要注意,指定位置之后需要有有效的节点才能进行删除!!!所以这里你可以判断一下,传入头指针是为了更好地进行判断处理~~

//删除指定位置之后的数据
void ListPopPosAfter(ListNode* phead, ListNode* pos)
{
	assert(phead);
	assert(pos && pos->next != phead);

	ListNode* del = pos->next;
	del->next->prev = pos;
	pos->next = del->next;
	free(del);
	del = NULL;
}

在指定位置之前插入数据

//在指定位置之前插入数据
void ListPushPosFront(ListNode* pos, ListDataType x)
{
	assert(pos);

	ListNode* newnode = CreatNewnode(x);
	newnode->next = pos;
	newnode->prev = pos->prev;

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

在指定位置之后插入数据

//在指定位置之后插入数据
void ListPushPosAfter(ListNode* pos, ListDataType x)
{
	assert(pos);

	ListNode* newnode = CreatNewnode(x);
	newnode->prev = pos;
	newnode->next = pos->next;

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

销毁链表

销毁链表我们需要传入头结点的二级指针了,因为头结点也需要进行销毁,头指针要置为NULL
但是这里我却使用了一级指针,是为了保持接口的一致性~~ 因为上面的函数除了初始化都是传一级指针,也是为了方便别人来使用我们的接口函数,减少使用者的记忆负担~~

//销毁链表
void ListDestroy(ListNode* phead)
{
	assert(phead);

	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		ListNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(phead);
	phead = NULL;
}

如果你还想让接口函数更加完美,我们可以改变一下初始化函数的:

//初始化
ListNode* ListInit()
{
	ListNode* phead = CreatNewnode(-1);
	return phead;
}

小结

在实现双向链表的时候,我们可以通过画图理解的方式进行理解和书写相应的代码~~

封装函数

List.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int ListDataType;

typedef struct ListNode
{
	ListDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

//初始化
void ListInit(ListNode** pphead);

//打印
void ListPrint(ListNode* phead);

//插入
void ListPushBack(ListNode* phead, ListDataType x);
void ListPushFront(ListNode* phead, ListDataType x);

//删除
void ListPopBack(ListNode* phead);
void ListPopFront(ListNode* phead);

//查找
ListNode* ListFind(ListNode* phead, ListDataType x);

//指定位置删除
void ListPopPos(ListNode* pos);

//删除指定位置前的数据
void ListPopPosFront(ListNode* phead, ListNode* pos);

//删除指定位置之后的数据
void ListPopPosAfter(ListNode* phead, ListNode* pos);

//在指定位置前插入数据
void ListPushPosFront(ListNode* pos, ListDataType x);

//在指定位置之后插入数据
void ListPushPosAfter(ListNode* pos, ListDataType x);

//销毁链表
void ListDestroy(ListNode* phead);

List.c

#include "List.h"

//创建新节点
ListNode* CreatNewnode(ListDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail"); exit(1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = newnode;

	return newnode;
}

//初始化
void ListInit(ListNode** pphead)
{
	*pphead = CreatNewnode(-1);
}

//尾插
void ListPushBack(ListNode* phead, ListDataType x)
{
	ListNode* newnode = CreatNewnode(x);

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

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

//打印
void ListPrint(ListNode* phead)
{
	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

//头插
void ListPushFront(ListNode* phead, ListDataType x)
{
	ListNode* newnode = CreatNewnode(x);
	
	newnode->prev = phead;
	newnode->next = phead->next;

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

//尾删
void ListPopBack(ListNode* phead)
{
	assert(phead && phead->next != phead);

	ListNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;
	free(del);
	del = NULL;
}

//头删
void ListPopFront(ListNode* phead)
{
	assert(phead && phead->next != phead);

	ListNode* del = phead->next;
	phead->next = del->next;
	del->next->prev = phead;
	free(del);
	del = NULL;
}

//查找
ListNode* ListFind(ListNode* phead, ListDataType x)
{
	ListNode* pcur = phead->next;

	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}

	return NULL;
}

//指定位置删除
void ListPopPos(ListNode* pos)
{
	assert(pos);
	
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

//删除指定位置前的数据
void ListPopPosFront(ListNode* phead, ListNode* pos)
{
	assert(phead);
	assert(pos && pos->prev != phead);

	ListNode* del = pos->prev;
	del->prev->next = pos;
	pos->prev = del->prev;
	free(del);
	del = NULL;
}

//删除指定位置之后的数据
void ListPopPosAfter(ListNode* phead, ListNode* pos)
{
	assert(phead);
	assert(pos && pos->next != phead);

	ListNode* del = pos->next;
	del->next->prev = pos;
	pos->next = del->next;
	free(del);
	del = NULL;
}

//在指定位置前插入数据
void ListPushPosFront(ListNode* pos, ListDataType x)
{
	assert(pos);

	ListNode* newnode = CreatNewnode(x);
	newnode->next = pos;
	newnode->prev = pos->prev;

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

//在指定位置之后插入数据
void ListPushPosAfter(ListNode* pos, ListDataType x)
{
	assert(pos);

	ListNode* newnode = CreatNewnode(x);
	newnode->prev = pos;
	newnode->next = pos->next;

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

//销毁链表
void ListDestroy(ListNode* phead)
{
	assert(phead);

	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		ListNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(phead);
	phead = NULL;
}

双向链表完结撒花~~

  • 53
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 42
    评论
双向链表(Doubly Linked List)是一种数据结构,它与单向链表相似,但是每个节点除了指向下一个节点的指针之外,还有一个指向上一个节点的指针。这使得在双向链表中可以方便地向前或向后遍历节点。以下是使用C语言实现双向链表的全部功能的代码: ```c #include <stdio.h> #include <stdlib.h> // 定义双向链表的节点结构体 typedef struct Node { int data; struct Node* prev; struct Node* next; } Node; // 初始化双向链表 Node* init() { Node* head = (Node*)malloc(sizeof(Node)); head->data = -1; head->prev = NULL; head->next = NULL; return head; } // 在双向链表末尾添加节点 void append(Node* head, int data) { Node* new_node = (Node*)malloc(sizeof(Node)); new_node->data = data; new_node->prev = head; new_node->next = NULL; Node* p = head; while (p->next != NULL) { p = p->next; } p->next = new_node; } // 在双向链表指定位置插入节点 void insert(Node* head, int pos, int data) { Node* new_node = (Node*)malloc(sizeof(Node)); new_node->data = data; Node* p = head; for (int i = 0; i < pos; i++) { p = p->next; if (p == NULL) { printf("Error: position out of range\n"); return; } } new_node->prev = p->prev; new_node->next = p; p->prev->next = new_node; p->prev = new_node; } // 删除双向链表指定位置的节点 void remove_node(Node* head, int pos) { Node* p = head; for (int i = 0; i < pos; i++) { p = p->next; if (p == NULL) { printf("Error: position out of range\n"); return; } } p->prev->next = p->next; if (p->next != NULL) { p->next->prev = p->prev; } free(p); } // 获取双向链表指定位置的节点的数据 int get(Node* head, int pos) { Node* p = head; for (int i = 0; i < pos; i++) { p = p->next; if (p == NULL) { printf("Error: position out of range\n"); return -1; } } return p->data; } // 修改双向链表指定位置的节点的数据 void set(Node* head, int pos, int data) { Node* p = head; for (int i = 0; i < pos; i++) { p = p->next; if (p == NULL) { printf("Error: position out of range\n"); return; } } p->data = data; } // 获取双向链表的长度 int length(Node* head) { Node* p = head; int len = 0; while (p->next != NULL) { len++; p = p->next; } return len; } // 打印双向链表的所有节点的数据 void print(Node* head) { Node* p = head->next; while (p != NULL) { printf("%d ", p->data); p = p->next; } printf("\n"); } // 反向打印双向链表的所有节点的数据 void reverse_print(Node* head) { Node* p = head; while (p->next != NULL) { p = p->next; } while (p->prev != NULL) { printf("%d ", p->data); p = p->prev; } printf("\n"); } // 释放双向链表的所有节点的内存 void destroy(Node* head) { Node* p = head->next; while (p != NULL) { Node* q = p; p = p->next; free(q); } free(head); } int main() { Node* head = init(); append(head, 1); append(head, 2); append(head, 3); insert(head, 1, 4); remove_node(head, 2); set(head, 2, 5); printf("Length: %d\n", length(head)); print(head); reverse_print(head); destroy(head); return 0; } ``` 在上面的代码中,`init`函数用于初始化双向链表,返回一个头节点;`append`函数用于在双向链表末尾添加一个节点;`insert`函数用于在双向链表指定位置插入一个节点;`remove_node`函数用于删除双向链表指定位置的节点;`get`函数用于获取双向链表指定位置的节点的数据;`set`函数用于修改双向链表指定位置的节点的数据;`length`函数用于获取双向链表的长度;`print`函数用于打印双向链表的所有节点的数据;`reverse_print`函数用于反向打印双向链表的所有节点的数据;`destroy`函数用于释放双向链表的所有节点的内存。在`main`函数中,我们对双向链表进行了一些操作,例如添加节点、插入节点、删除节点、修改节点、获取长度、打印节点等等。最后,我们使用`destroy`函数释放了双向链表的内存。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 42
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值