前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >数据结构初步(六)- 复杂链表的分析与C语言实现

数据结构初步(六)- 复杂链表的分析与C语言实现

作者头像
怠惰的未禾
发布于 2023-04-27 13:32:20
发布于 2023-04-27 13:32:20
39200
代码可运行
举报
文章被收录于专栏:Linux之越战越勇Linux之越战越勇
运行总次数:0
代码可运行
前言

本节继续探讨对链表的学习,介绍结构更加复杂的链表的结构以及实现!


1. 链表的分类

首先我们来看链表结构的分类,以下三类链表两两组合就有8中结构。

1.1 单向链表与双向链表

1.2 不带头节点(哨兵头)与带头结点(哨兵头)的链表

1.3 循环链表与不循环链表

无头单向不循环链表:结构简单,一般不会单独用来储存数据。实际中更多的是作为其他数据结构的子结构,如哈希桶等; 带头双向循环链表:结构复杂,一般用于单独储存数据。实际中使用的链表数据结构,都是带头双向循环链表。这个结构虽然复杂,但是使用代码实现时反而简单,这是优势的结构所带来的便利。


2. 带头双向循环链表的分析与实现

2.1 说明

带头双向循环链表基本结构相对于单向链表来说结构十分复杂,实际上正是因为其复杂的结构(已知条件变多)才使得头插头删尾插尾删操作的时间复杂度都是O(1),并且真正的代码层面反而更加简单。 单链表解决了动态顺序表

  1. 头插、头删操作时时间复杂度较高;
  2. 申请的空间存在浪费情况;
  3. 申请空间对于系统开销越来越大等问题。

没有解决动态顺序表

  1. 尾插、尾删操作时间复杂度较高O(n)问题

带头双向循环链表完美解决了动态顺序表的遇到的问题,这都得益于其有优势的结构


2.2 带头双向循环链表结构分析

在链表开头就有一个头节点哨兵头,所以链表一定不会为空至少有一个头节点,只不过这个头节点并不存放任何数据; 双向指对于每一个节点结构体来说,都有一个数据成员变量,一个节点指针next指向下一个节点,一个节点指针prev指向上一个节点; 循环是指链表的尾节点指针成员next指向了链表头结点head哨兵头guard,而链表头节点哨兵头指针成员prev指向了尾节点


2.3 功能分析

1. 防止头文件被重复包含

方法1: 条件编译指令

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#ifndef __SeqList__
#define __SeqList__
//....
#endif

方法2:在头文件最开始的位置加上一行代码

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#pragma once

2. 头文件的包含

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>

3. 定义节点结构体类型

一个节点包括储存数据的变量指向下一个节点的指针指向上一个节点的指针

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
typedef int DLTDataType;
typedef struct DListNode {
	DLTDataType data;
	struct DListNode* next;
	struct DListNdoe* prev;
}DLTNode;

重定义一个通用数据类型的名字,这样以后需要按修改案数据类型时只需要在开头修改一次。 把结构体类型名重定义一个较短且有含义的新名字,这样敲打方便。


4. 初始化链表

由于链表有头结点哨兵头链表只要存在就有一个哨兵头,所以我们需要对链表进行初始化:创建一个哨兵头并使哨兵头两个指针成员nextprev指向自己(哨兵头),以此达成在没有有效数据(节点)时链表也是一个闭环。

初始化就需要修改外部的头指针phead方法一: 传头指针本身,然后函数返回新的头指针,使用一级结构体指针

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
DLTNode* DListInit() {
	DLTNode* guard = (DLTNode*)malloc(sizeof(DLTNode));
    //malloc()申请空间失败
	if (!newnode) {
	if (!guard) {
		perror("DListInit");
		exit(-1);
	}
	guard->next = guard;
	guard->prev = guard;
	return guard;
}

方法二: 传外部头指针的地址在函数内部修改头指针,使用二级结构体指针

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void DListInit(DLTNode** pphead) {
	assert(pphead);
	DLTNode* guard = (DLTNode*)malloc(sizeof(DLTNode));
	if (!guard) {
		perror("DListInit");
		exit(-1);
	}
	*pphead = guard;
	guard->next = guard;
	guard->prev = guard;
}

头指针的地址时,头指针的地址一定不为空NULL,所以需要对其进行断言判断。


5. 遍历链表并输出节点储存的数据

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//遍历链表,输出数据
void DListPrint(DLTNode* phead) {
	assert(phead);
	DLTNode* cur = phead->next;
	while (cur != phead) {
		printf("%d<==>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

注意点:

  1. 由于头节点哨兵头的存在,除了初始化链表接口函数,其他不涉及头节点本身改变的情况的函数都可以不使用二级指针作为参数。
  2. 与不带头的链表相比,带哨兵头的链表由于不存放数据,虽然我们传参传的是哨兵头的地址,但是遍历链表实际是从哨兵头的下一个节点开始的。
  3. 与头尾不相连的链表相比尾节点指向空,头尾相连的链表的遍历结束条件也有所不同:从头节点的下一个节点开始遍历到头节点结束。

6. 申请一个新节点并赋值和初始化

本函数是为了与头插数据、尾插数据、某个节点前插数据提供便利,需要新增节点时直接调用本函数,将返回新节点的地址。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//申请一个新节点
DLTNode* BuyDLTNode(DLTDataType x) {
	DLTNode* newnode = (DLTNode*)malloc(sizeof(DLTNode));
    //malloc()申请空间失败
	if (!newnode) {
		perror("BuyDLTNode");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = NULL;
	return newnode;
}

注意malloc()函数可能申请空间失败,需要判断一下。


7. 尾插数据

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void DListPushBack(DLTNode* phead, DLTDataType x) {
	assert(phead);
	DLTNode* newnode = BuyDLTNode(x);
	DLTNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;
    
	newnode->next = phead;
	phead->prev = newnode;
}

尾插数据,需要先找到尾节点tail:借助头指针副本``phead通过头节点哨兵头内部指针找到尾节点。 再断开哨兵头与尾节点的链接,使新节点newnode与尾节点tail进行链接,再与头结点哨兵头进行链接。


8. 头插数据

头插数据,需要注意的并不是在头节点哨兵头之前插入数据,而是应该在头节点哨兵头的下一个节点之前插入数据,因为头节点的下一个节点才是链表第一个真正储存数据的节点。

这样需要断开头节点头节点下一个节点的链接,再使头节点与新节点链接,新节点与头节点下一个节点链接。 在链接操作中有两种思路: 思路1: 不借助额外指针变量来储存头节点下一个节点的地址,需要慎重、仔细考虑新节点分别与头结点头结点下一个节点之间的链接顺序。 新节点头节点下一个节点链接,头节点链接。 如果新节点先与头节点链接,又没有额外指针变量记录头节点下一个节点的地址,这样头节点下一个节点的地址就无法找到了,也就无法使新节点newnode与其链接了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void DListPushFront(DLTNode* phead, DLTDataType x) {
	assert(phead);
	DLTNode* newnode = BuyDLTNode(x);
	//方法1
	newnode->next = phead->next;
	phead->next->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;

	方法二
	//DLTNode* first = phead->next;
	//phead->next = newnode;
	//newnode->prev = phead;
	//
	//newnode->next = first;
	//first->prev = newnode;
}

思路2: 借助额外结构体指针变量first记录头结点的下一个节点的地址。 这样就不需要考虑新节点与头节点、新节点与头结点下一个节点链接的顺序。 随便你怎样的链接顺序。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void DListPushFront(DLTNode* phead, DLTDataType x) {
	assert(phead);
	DLTNode* newnode = BuyDLTNode(x);

	//方法二
	DLTNode* first = phead->next;
	phead->next = newnode;
	newnode->prev = phead;
	
	newnode->next = first;
	first->prev = newnode;
}

9. 判断链表是否为空

若本链表只有一个头结点哨兵头时对应链表为空,此时头节点哨兵头两个指针成员都指向了自己(头节点);否则头节点的两个指针成员都不会指向自己(头节点)。 故判断头节点的两个指针之一的指向是否等于指向头节点的指针即可。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
bool DListEmpty(DLTNode* phead) {
	assert(phead);
	return phead->next == phead;
    //return phead->prev == phead;
}

链表为空就返回true,不为空就返回false 返回类型时bool类型,当然也可以用int整型。 注意传入的头指针副本的值一定不为NULL

链表为空:

链表不为空:


10. 尾删数据

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void DListPopBack(DLTNode* phead) {
	assert(phead);
	assert(!DListEmpty(phead));
    
	DLTNode* tail = phead->prev;
	DLTNode* last = tail->prev;
	last->next = phead;
	phead->prev = last;
	free(tail);
	//tail = NULL;
}

尾删数据,首先判断链表是否为空; 然后需要先找到尾节点tail尾节点的上一个节点last。 可以通过头节点内部指针成员prev直接找到尾节点结构体指针tail指向;再通过tail尾节点内部指针成员prev直接找到尾节点的上一个节点结构体指针last指向。 断开尾节点tail与头结点、last节点的链接,建立last节点与头结点的链接; 最后手动释放free()尾节点tail指向的空间。


11. 头删数据

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void DListPopFront(DLTNode* phead) {
	assert(phead);
	assert(!DListEmpty(phead));
	DLTNode* first = phead->next;
	DLTNode* second = first->next;

	phead->next = second;
	second->prev = phead;
	free(first);
	//first = NULL;	
}

已知本链表至少有一个头节点哨兵头,所以传入的头指针副本phead一定不为空NULL; 头删数据,链表至少有一个储存有效数据的节点,所以需要先判断链表是否为空NULL; 需要注意的是:删除的不是头节点哨兵头,而是头节点的下一个节点。 通过头指针phead找到头节点,然后通过头节点内部指针成员next找到头节点的下一个节点借助结构体指针first指向;再通过first节点内部指针成员next找到first的下一个节点借助结构体指针second记录。 删除first节点,即断开first节点与头节点、second节点的链接,然后使头节点second节点相链接。


12. 查找数据并返回节点地址

从头节点的的下一个节点开始遍历(使用while循环)链表中的所有存放数据的节点,直到头节点为止。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//查找数据并返回节点地址
DLTNode* DListFind(DLTNode* phead, DLTDataType x) {
	assert(phead);
	DLTNode* cur = phead->next;
	while(cur){
		if (cur->data == x) {
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

13. 在pos节点之前插入一个储存数据的节点

重点接口函数之一来了,实现该函数之后可以替换头插函数、尾插函数接口。 头插函数接口、尾插函数接口可以直接调用本函数接口实现相应功能。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//在pos节点插入一个节点
void DListInsert(DLTNode* pos, DLTDataType x){
	assert(pos);
	DLTNode* newnode = BuyDLTNode(x);
	DLTNode* last = pos->prev;
    
	last->next = newnode;
	newnode->prev = last;
	newnode->next = pos;
	pos->prev = newnode;
}

在函数一开始需要判断传入节点地址副本pos是否为空,正常传入的地址不应该是NULL,并且该节点地址应该属于本链表内。 判断pos是否是空,对其进行断言暴力判断就可以;而判断其是否是本链表内的节点则需要头结点的地址来遍历链表判断,需要传入头结点的地址,但是传入的节点是否属于本链表的功能不应该是本函数的功能,应该有外部调用者判断。 在pos节点前插入节点需要得到pos节点的上一个节点的地址,以便于pos的上一个节点与newnode节点进行链接。 不同于单向链表需要借助头节点遍历整个链表用于找到pos节点的上一个节点;带头双向循环链表则不需要头节点的帮助,因为pos节点不仅包含着下一个节点的的地址,还包含着上一个节点的地址,所以只需要pos节点就可以找到pos的上一个节点了。 借助结构体指针变量last记录pos节点的上一个节点; 分别建立节点lastnewnode之间的链接和节点newnodepos之间的链接。此情况下,这两个操作没有先后顺序,可以任意进行。


14. 删除pos节点

重点接口函数之二来了,实现该函数之后可以替换头删函数、尾删函数接口。 头删函数接口、尾删函数接口可以直接调用本函数接口实现相应功能。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//删除pos节点
void DListErase(DLTNode* pos) {
	assert(pos);
	DLTNode* last = pos->prev;
	DLTNode* later = pos->next;
    
	last->next = later;
	later->prev = last;
	free(pos);
	//pos = NULL;
}

在函数一开始需要判断传入节点地址副本``pos是否为空,正常传入的地址不应该是NULL,并且该节点地址应该属于本链表内的节点。 判断pos是否是空,对其进行断言暴力判断就可以;而判断其是否是本链表内的节点则需要头结点的地址来遍历链表判断,需要传入头结点的地址,但是传入的节点是否属于本链表的功能不应该是本函数的功能,应该有外部调用者判断。 删除pos节点后需要将pos节点前一个节点和pos节点后一个节点建立链接关系。因此需要得到pos节点的上一个节点的地址。 借助结构体指针变量last记录pos节点上一个节点的地址; 借助结构体指针变量later记录pos节点下一个节点的地址。 需要建立节点lastlater之间的链接关系,然后手动释放free指针pos指向的空间即可。


15. 销毁链表

链表使用结束,链表中会有一个或多个节点向内存申请的空间未被释放,放着不管的就是内存泄露的情况。虽然程序运行结束时操作系统会自动回收分配给程序的空间,不会出现内存泄漏的情况。但是我们不能永远依靠着操作系统解决内存泄漏的问题,我们得自己注意。有许多程序是不会轻易停止运行的,如果这样的程序存在内存泄漏将会是一个不小的危机。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//销毁链表,一级指针或者二级指针都可以,只不过一级指针下头指针需要在外部被置空
void DListDestroy(DLTNode* phead) {
	assert(phead);
	DLTNode* cur = phead->next;
	while (cur != phead) {
		DLTNode* later = cur->next;
		free(cur);
		cur = later;
	}
	free(phead);
	//phead = NULL;
	//外部头指针需要调用者在外部置NULL
}

同输出节点数据相似,链表节点也将通过while循环达到节点的空间顺序释放。 我们从头节点的下一个节点开始进行,依次释放节点,直到循环走到头节点时停止循环,在循环结束之后在释放头节点的空间即可。 本函数接受的是外部头指针,得到的是外部头指针的副本phead,所以我们并不能在函数内部修改外部头指针使其置为NULL,而是需要在外部调用者手动将其置空。


16. 头插数据、尾插数据操作借助插入节点函数接口快速实现

尾插接口函数 phead的前一个节点相当于尾。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void DListPushBack(DLTNode* phead, DLTDataType x) {
	assert(phead);
	DListInsert(phead, x);
}

头插接口函数 phead的下一个节点相当于真正意义上头。(是储存数据节点的头节点)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void DListPushFront(DLTNode* phead, DLTDataType x) {
	assert(phead);
	DListInsert(phead->next, x);
}

17. 头删数据、尾删数据操作借助删除节点函数快速实现

尾删接口函数 phead的前一个节点相当于尾。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void DListPopBack(DLTNode* phead) {
	assert(phead);
    DListErase(phead->prev);
}

头删接口函数 phead的下一个节点相当于真正意义上头。(是储存数据节点的头节点)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void DListPopFront(DLTNode* phead) {
	assert(phead);
	DListErase(phead->next);
}

2.4 C语言实现

仍然是分文件实现: 头文件DList.h

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#pragma once

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>

typedef int DLTDataType;
typedef struct DListNode {
	DLTDataType data;
	struct DLTDataType* next;
	struct DLTDataType* prev;
}DLTNode;

//初始化
//void DListInit(DLTNode** pphead);
DLTNode* DListInit();

//遍历链表,输出数据
void DListPrint(DLTNode* phead);

//销毁链表,一级指针或者二级指针都可以,只不过一级指针下头指针需要在外部被置空
void DListDestroy(DLTNode* phead);

//头插尾插 头删尾删
void DListPushBack(DLTNode* phead, DLTDataType x);
void DListPushFront(DLTNode* phead, DLTDataType x);
void DListPopBack(DLTNode* phead);
void DListPopFront(DLTNode* phead);

//查找数据并返回节点地址
DLTNode* DListFind(DLTNode* phead, DLTDataType x);

//在pos节点插入一个节点
void DListInsert(DLTNode* pos, DLTDataType x);

//删除pos节点
void DListErase(DLTNode* pos);

函数定义源文件DList.c

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include "DList.h"

//初始化方法1:
//void DListInit(DLTNode** pphead) {
//	assert(pphead);
//	DLTNode* guard = (DLTNode*)malloc(sizeof(DLTNode));
//	if (!guard) {
//		perror("DListInit");
//		exit(-1);
//	}
//	*pphead = guard;
//	guard->next = guard;
//	guard->prev = guard;
//}
//初始化方法2:
DLTNode* DListInit() {
	DLTNode* guard = (DLTNode*)malloc(sizeof(DLTNode));
	if (!guard) {
		perror("DListInit");
		exit(-1);
	}
	guard->next = guard;
	guard->prev = guard;
	return guard;
}

//遍历链表,输出数据
void DListPrint(DLTNode* phead) {
	assert(phead);
	DLTNode* cur = phead->next;
	while (cur != phead) {
		printf("%d<==>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

//销毁链表,一级指针或者二级指针都可以,只不过一级指针下头指针需要在外部被置空
void DListDestroy(DLTNode* phead) {
	assert(phead);
	DLTNode* cur = phead->next;
	while (cur != phead) {
		DLTNode* later = cur->next;
		free(cur);
		cur = later;
	}
	free(phead);
	//phead = NULL;
	//外部头指针需要调用者在外部置NULL
}

//申请一个新节点
DLTNode* BuyDLTNode(DLTDataType x) {
	DLTNode* newnode = (DLTNode*)malloc(sizeof(DLTNode));
	if (!newnode) {
		perror("BuyDLTNode");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = NULL;
	return newnode;
}

//头插尾插 头删尾删
void DListPushBack(DLTNode* phead, DLTDataType x) {
	assert(phead);
	DLTNode* newnode = BuyDLTNode(x);
	DLTNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
	
	//调用插入函数
	//DListInsert(phead, x);
}

void DListPushFront(DLTNode* phead, DLTDataType x) {
	assert(phead);
	DLTNode* newnode = BuyDLTNode(x);
	//方法1
	newnode->next = phead->next;
	phead->next->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;

	方法二
	//DLTNode* first = phead->next;
	//phead->next = newnode;
	//newnode->prev = phead;
	//
	//newnode->next = first;
	//first->prev = newnode;

	//方法三
	//DListInsert(phead->next, x);
}

bool DListEmpty(DLTNode* phead) {
	assert(phead);
	return phead->next == phead;
}

void DListPopBack(DLTNode* phead) {
	assert(phead);
	DLTNode* tail = phead->prev;
	DLTNode* last = tail->prev;
	last->next = phead;
	phead->prev = last;
	free(tail);
	//DListErase(phead->prev);
}

void DListPopFront(DLTNode* phead) {
	assert(phead);
	assert(!DListEmpty(phead));
	DLTNode* first = phead->next;
	DLTNode* second = first->next;

	phead->next = second;
	second->prev = phead;
	free(first);
	//first = NULL;	
	DListErase(phead->next);
}

//查找数据并返回节点地址
DLTNode* DListFind(DLTNode* phead, DLTDataType x) {
	assert(phead);
	DLTNode* cur = phead->next;
	while(cur){
		if (cur->data == x) {
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

//在pos节点插入一个节点
void DListInsert(DLTNode* pos, DLTDataType x){
	assert(pos);
	DLTNode* newnode = BuyDLTNode(x);
	DLTNode* last = pos->prev;
	last->next = newnode;
	newnode->prev = last;
	newnode->next = pos;
	pos->prev = newnode;
}

//删除pos节点
void DListErase(DLTNode* pos) {
	assert(pos);
	DLTNode* last = pos->prev;
	DLTNode* later = pos->next;
	last->next = later;
	later->prev = last;
	free(pos);
	//pos = NULL;
}

结语

本节到这里就差不多结束了,主要内容介绍了链表的种类;其中详细介绍了带头双向循环链表的功能并完成了 C语言代码实现,希望本文的一些内容能够帮助到在看的你!!! 下次再见!


END

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-09-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
【初阶数据结构】——带头双向循环链表(C描述)
上一篇文章我们已经学了单链表(不带头),那这篇文章,我们就来学习一下带头双向循环链表。
YIN_尹
2024/01/23
1200
【初阶数据结构】——带头双向循环链表(C描述)
【数据结构初阶】双向带头循环链表原来是纸老虎,结构复杂,操作简单
目录 0.结构体定义 1.初始化 2.尾插 3.打印 4.头插 5.任意位置插入前面位置 6.尾删 7.头删 8.链表长度 9.任意位置删除当前位置 10. 销毁 ---- 双向带头循环链表:结构复杂,操作简单 0.结构体定义 这里方便浏览,特地没有将int类型重命名为TLDateType  #define _CRT_SECURE_NO_WARNINGS 1 #pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h
MicroFrank
2023/01/16
1620
数据结构初步(五)- 线性表之单链表的分析与C语言实现
上节介绍了顺序表,本节将继续数据结构的学习:介绍链表的有关概念与知识,并着重于分析单链表的具体实现。 本节多组动图预警!!!
怠惰的未禾
2023/04/27
9000
数据结构初步(五)- 线性表之单链表的分析与C语言实现
【数据结构】链表最强结构-带头双向循环链表(超详解)
目录 前言 写在前面的话 链表类型区别 带头+双向+循环链表增删查改实现 接口展示 构建节点类型 创建链表及初始化 节点开辟 链表摧毁 链表打印 链表尾插 链表尾删 链表头插 链表头删 链表查找 链表pos位置前插 链表pos删除 总结 ---- 前言 ---- 本章将带你们走进带头双向循环链表的实现与讲解 写在前面的话 ---- 在前一章我们学习实现了单链表(无头单向不循环链表),这里我们引入带头双向循环链表 很明显这两种结构截然不同,但都是作为链表最常使用链表结构 前者因其结构上的缺点
用户9645905
2022/11/30
3390
【数据结构】链表最强结构-带头双向循环链表(超详解)
链接未来:深入理解链表数据结构(二.c语言实现带头双向循环链表)
上篇文章简述讲解了链表的基本概念并且实现了无头单向不循环链表:链接未来:深入理解链表数据结构(一.c语言实现无头单向非循环链表)-CSDN博
是Nero哦
2024/01/18
1390
链接未来:深入理解链表数据结构(二.c语言实现带头双向循环链表)
【数据结构】C语言实现带头双向循环链表万字详解(附完整运行代码)
要编写一个带头双向循环链表项目,首先要明确我们想要达到的效果是什么样,下面我将用vs2022编译器来为大家演示一下带头双向循环链表程序运行时的样子:
修修修也
2024/04/01
2420
【数据结构】C语言实现带头双向循环链表万字详解(附完整运行代码)
数据结构——带头双向循环链表
Eternity._
2024/06/14
840
数据结构——带头双向循环链表
数据结构---双向链表
单向链表:一块内存指向下一个内存。 单链表存在一些缺陷: 1.查找速度慢。 2.不能从后往前找。 3.找不到前驱。 链表的结构分为8种: 1.单向和双向 2.带头和不带头 带头的链表有一个带哨兵位的头结点,这个节点不存储有效数据。 好处:尾插更方便,不需要二级指针了,带头结点不需要改变传过来的指针,,也就意味着不需要传二级指针 3.循环和不循环 不循环:尾是指向空的 循环:尾是指针头的 一、无头单向肺循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的领接表等等。 另外这种结构在笔试面试中出现很多。 二、带头双向循环链表:结构复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表,另外这个结构虽然结构复杂, 但是使用代码实现以后会发现带来很多优势,实现反而简单了。
用户11305458
2024/10/09
710
【数据结构】双向链表
顺序表的优点: 1.尾插尾删的效率很高 2.可以用下标随机访问 3.相比链表结构 CPU高速缓存命中率更高 顺序表的缺点: 1.头部和中部插入效率低——O(N) 2.扩容时的性能消耗+扩容时的空间浪费 链表的优点: 1.任意位置插入删除效率很高——O(1) 2.按需申请释放 链表的缺点: 1.不支持随机访问 注:三级缓存被称为CPU周围的禁卫军 CPU执行指令不会直接访问内存  1.先看数据在不在三级缓存,在(命中),直接访问 2.不在(不命中),先加载到缓存,再访问 注:加载到缓存时,会将需要加载的位置开始的一段都加载进缓存,(加载多少取决于硬件) 由于顺序表的数据彼此之间的地址紧密联系 所以加载到高速缓存时命中率高 但链表不然 更可能会导致缓存污染 
IT编程爱好者
2023/04/12
2380
【数据结构】双向链表
[数据结构]—带头双向循环链表——超详解
注意,该函数的前提条件是链表中至少存在一个节点,否则会因为assert函数判断失败而终止程序。在使用该函数时需要注意链表的状态。
小李很执着
2024/06/15
1190
[数据结构]—带头双向循环链表——超详解
【初探数据结构】线性表——链表(二)带头双向循环链表(详解与实现)
在初始化时,创建一个头节点,并将其next和prev指针都指向自身,这样链表初始时是空的,并且形成了一个循环结构。
我想吃余
2025/03/31
1060
【链表】双向循环带头链表-增-删-查(C语言)
---- ---- 单链表存在的缺陷: 不能从后往前走, 找不到他的前驱, 指定位置 删除 增加 尾删 都要找前一个,时间复杂度都是O(n) ---- 针对上面的这些缺陷的解决方案——双向链表。 ---- 实际中要实现的链表的结构非常多样,以下情况组合起来就有8种链表结构: 单向、双向 带头、不带头——带哨兵位的头结点,这个结点不存储有效数据,好处是什么?尾插的判断更方便简单,带头就不需要二级指针了,(带头结点,不需要改变穿过来的指针,也就是意味着不需要传二级指针了。) 循环、非循环 ---- 无头单向非循
半生瓜的blog
2023/05/12
3220
【链表】双向循环带头链表-增-删-查(C语言)
数据结构——lesson4带头双向循环链表实现
大耳朵土土垚
2024/03/13
1520
数据结构——lesson4带头双向循环链表实现
带头双向循环链表增删查改实现(C语言)
带头双向循环链表,那么,结点的结构就要有两个指针域,分别指向前一个结点和后一个结点。
有礼貌的灰绅士
2023/03/28
5990
带头双向循环链表增删查改实现(C语言)
【数据结构】顺序表和链表详解&&顺序表和链表的实现
线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串..
用户10925563
2024/06/04
2370
【数据结构】顺序表和链表详解&&顺序表和链表的实现
【数据结构】双向带头循环链表(c语言)(附源码)
而对于单链表,由于不具备这三个特性,所以在运行效率上要低于双向链表。那么我们来看看它的结构定义:
ephemerals__
2024/10/24
1660
【数据结构】双向带头循环链表(c语言)(附源码)
【数据结构与算法】带头双向循环链表
关于程序的三个部分前面已经说了很多次了,这里就不展开说明了,直接说一说我们要实现的功能:
平凡的人1
2022/11/15
2040
【数据结构与算法】带头双向循环链表
单循环链表-带头双向循环链表的实现
  虽然名字听上去比较复杂单循环链表,但是实现起来比单链表(全名:不带头、不循环、单向链表)更加简单,也不需要过多考虑特殊情况;
宜轩
2022/12/29
6470
【数据结构初阶】单链表补充内容+又双叒叕刷链表题
综合而言,两个各有优缺,相辅相成,具体用谁看场景,但是总体而言顺序表使用的频率更高一点
MicroFrank
2023/01/16
3360
【数据结构初阶】复杂链表复制+带头双向循环链表+缓存级知识
我们下面的讲解顺序是先给大家将最后一道链表题,本题难度较大,所以在大家还没看困的基础下,我们先讲解一下这道题目。然后博主在详细得用图文方式给大家讲一下链表的另一经典结构:带头双向循环链表。最后我们利用一小段时间再给大家补充一下缓存级部分的知识,由于偏硬件,仅供了解即可。
举杯邀明月
2023/04/12
2990
【数据结构初阶】复杂链表复制+带头双向循环链表+缓存级知识
推荐阅读
相关推荐
【初阶数据结构】——带头双向循环链表(C描述)
更多 >
LV.1
这个人很懒,什么都没有留下~
目录
  • 1. 链表的分类
    • 1.1 单向链表与双向链表
    • 1.2 不带头节点(哨兵头)与带头结点(哨兵头)的链表
    • 1.3 循环链表与不循环链表
  • 2. 带头双向循环链表的分析与实现
    • 2.1 说明
    • 2.2 带头双向循环链表结构分析
    • 2.3 功能分析
      • 1. 防止头文件被重复包含
      • 2. 头文件的包含
      • 3. 定义节点结构体类型
      • 4. 初始化链表
      • 5. 遍历链表并输出节点储存的数据
      • 6. 申请一个新节点并赋值和初始化
      • 7. 尾插数据
      • 8. 头插数据
      • 9. 判断链表是否为空
      • 10. 尾删数据
      • 11. 头删数据
      • 12. 查找数据并返回节点地址
      • 13. 在pos节点之前插入一个储存数据的节点
      • 14. 删除pos节点
      • 15. 销毁链表
      • 16. 头插数据、尾插数据操作借助插入节点函数接口快速实现
      • 17. 头删数据、尾删数据操作借助删除节点函数快速实现
    • 2.4 C语言实现
  • 结语
加入讨论
的问答专区 >
1合伙人擅长4个领域
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档