从零开始实现一个双向循环链表:C语言实战

devtools/2025/2/6 13:07:12/

文章目录

  • 1链表的再次介绍
  • 2为什么选择双向循环链表
  • 3代码实现:从初始化到销毁
  • 4测试代码
  • 5链表种类介绍
  • 6链表与顺序表的区别
  • 7存储金字塔
    • L0: 寄存器
    • L1: 高速缓存(SRAM)
    • L2: 高速缓存(SRAM)
    • L3: 高速缓存(SRAM)
    • L4: 主存(DRAM)
    • L5: 本地二级存储(本地磁盘)
    • L6: 远程二级存储
    • 缓存利用率与局部性原理:
  • 8书籍推荐《深入理解计算机系统》
  • 9新年快乐,代码相伴

1链表的再次介绍

在计算机科学中,链表是一种常见的数据结构,它通过节点之间的指针连接来存储数据。链表有许多变种,其中双向循环链表因其独特的结构而备受关注。今天,我们将通过C语言实现一个双向循环链表,并探讨其核心操作。无论你是数据结构的新手,还是想巩固基础的老手,这篇文章都将为你提供实用的知识和代码示例。
链接: 单链表介绍

2为什么选择双向循环链表

双向循环链表是一种特殊的链表,它的每个节点都有两个指针:一个指向前一个节点,另一个指向后一个节点。与单向链表不同,双向链表可以从任意一个节点向前或向后遍历。而循环链表的特点是其尾节点指向头节点,形成一个闭环。这种结构在某些场景下非常有用,比如实现高效的队列或缓存机制。

3代码实现:从初始化到销毁

1. 定义链表节点

首先,我们需要定义链表节点的结构。每个节点包含三个部分:

data:存储节点的数据。

next:指向下一个节点的指针。

prev:指向前一个节点的指针。

typedef int LTDataType;typedef struct ListNode
{struct ListNode* next;struct ListNode* prev;LTDataType data;
} LTNode;

2. 初始化链表

链表的初始化是创建一个头节点,并将其next和prev指针指向自己,形成一个空的双向循环链表

LTNode* BuyListNode(LTDataType x)
{LTNode* New = (LTNode*)malloc(sizeof(LTNode));if (New == NULL){perror("malloc");exit(-1);}New->next = NULL;New->prev = NULL;New->data = x;return New;
}
LTNode* LTInit()
{LTNode* phead = BuyListNode(-1); // 创建头节点phead->next = phead;phead->prev = phead;return phead;
}

3. 插入和删除节点

在双向循环链表中,插入和删除节点是非常高效的操作。我们可以在任意位置插入或删除节点,只需调整相邻节点的指针即可。

插入节点

void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* prev = pos->prev;LTNode* new = BuyListNode(x);prev->next = new;new->prev = prev;new->next = pos;pos->prev = new;
}

删除节点

void LTErase(LTNode* pos)
{assert(pos);LTNode* prev = pos->prev;LTNode* next = pos->next;prev->next = next;next->prev = prev;free(pos);
}

4. 链表的其他操作

我们还实现了一些常见的链表操作,如LTPushBack(在链表尾部插入节点)、LTPopBack(删除链表尾部节点)、LTPushFront(在链表头部插入节点)和LTPopFront(删除链表头部节点)。这些操作都依赖于LTInsert和LTErase函数。

void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTInsert(phead, x);
}void LTPopBack(LTNode* phead)
{assert(phead);LTErase(phead->prev);
}void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTInsert(phead->next, x);
}void LTPopFront(LTNode* phead)
{assert(phead);LTErase(phead->next);
}

5. 打印链表和判断链表是否为空

为了方便调试和观察链表的状态,我们实现了LTPrint函数来打印链表中的所有节点数据。此外,LTEmpty函数用于判断链表是否为空。

void LTPrint(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;printf("<=phead=>");while (cur != phead){printf("%d<=>", cur->data);cur = cur->next;}puts("");
}bool LTEmpty(LTNode* phead)
{assert(phead);return phead == phead->next;
}

6. 销毁链表

在使用完链表后,我们需要释放所有节点的内存,避免内存泄漏。

void LTDestroy(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){LTNode* next = cur->next;free(cur);cur = next;}free(phead);
}

4测试代码

为了验证我们的双向循环链表实现是否正确,我们编写了一个简单的测试函数Test1。在这个函数中,我们进行了多次插入、删除操作,并打印链表的状态。

void Test1()
{LTNode* phead = LTInit();LTPushBack(phead, 2);LTPushBack(phead, 1);LTPushFront(phead, 5);LTPrint(phead);LTInsert(phead->next, 6);LTInsert(phead->next, 6);LTPushFront(phead, 6);LTPrint(phead);LTPopBack(phead);LTPopBack(phead);LTPopFront(phead);LTErase(phead->next);LTPrint(phead);LTPushBack(phead, 4);LTPushBack(phead, 3);LTPushBack(phead, 2);LTPushBack(phead, 1);LTPrint(phead);printf("%d\n", LTEmpty(phead));LTDestroy(phead);phead = LTInit();printf("%d\n", LTEmpty(phead));
}int main()
{Test1();return 0;
}

5链表种类介绍

在这里插入图片描述
在这里插入图片描述

6链表与顺序表的区别

在这里插入图片描述

7存储金字塔

在这里插入图片描述
这张图片展示了计算机存储体系的层级结构,通常被称为存储金字塔(Memory Hierarchy)。它描述了不同层级的存储设备从最快速但成本最高到最慢但成本最低的分布情况。每一层级的存储设备都具有不同的访问速度和成本,它们相互配合,以实现性能和成本的最佳平衡。
存储金字塔层级介绍:

L0: 寄存器

位于金字塔的顶端,是CPU内部的寄存器,提供最快的数据访问速度。
寄存器的数量有限,因此它们用于存储最频繁访问的数据。

L1: 高速缓存(SRAM)

位于寄存器下方,是CPU的一级缓存,使用静态随机存取存储器(SRAM)。
L1缓存分为两个部分:L1指令缓存(L1-I)和L1数据缓存(L1-D),分别用于存储指令和数据。

L2: 高速缓存(SRAM)

二级缓存通常集成在CPU芯片上,或者在某些设计中位于CPU芯片附近。
L2缓存比L1缓存大,但访问速度稍慢。

L3: 高速缓存(SRAM)

三级缓存是更大但速度较慢的缓存层级,通常位于CPU芯片外。
L3缓存为多个核心共享,用于减少核心之间的数据访问延迟。

L4: 主存(DRAM)

主存储器,即动态随机存取存储器(DRAM),是计算机的主要内存。
与缓存相比,主存的访问速度较慢,但容量更大,成本更低。

L5: 本地二级存储(本地磁盘)

本地磁盘,如硬盘驱动器(HDD)或固态硬盘(SSD),用于长期存储数据。
访问速度比主存慢得多,但存储容量更大,成本更低。

L6: 远程二级存储

远程存储,如分布式文件系统、网络附加存储(NAS)或Web服务器,提供更大的存储空间。
访问速度最慢,但可以提供几乎无限的存储容量。

缓存利用率与局部性原理:

缓存利用率:指的是缓存中存储的数据被访问的频率。高缓存利用率意味着更多的数据访问可以直接从缓存中获取,从而提高系统性能。
局部性原理:包括时间局部性和空间局部性。时间局部性指的是最近访问过的数据很可能在不久的将来再次被访问;空间局部性指的是如果一个数据被访问,那么它附近的数据也很可能被访问。缓存的设计就是基于这些原理,以提高缓存利用率。

8书籍推荐《深入理解计算机系统》

链接: 书籍介绍

9新年快乐,代码相伴

在这个充满希望的新年里,愿你的代码之旅充满乐趣和挑战。无论是探索数据结构的奥秘,还是深入理解计算机系统的运行机制,都希望你能收获满满。让我们一起在代码的世界里,继续探索、学习和成长,用代码书写属于自己的精彩篇章。


http://www.ppmy.cn/devtools/156537.html

相关文章

11.享元模式 (Flyweight)

定义 Flyweight 模式&#xff08;享元模式&#xff09; 是一种结构型设计模式&#xff0c;它旨在通过共享对象来有效支持大量细粒度对象的复用。该模式主要通过共享细节来减少内存使用&#xff0c;提升性能&#xff0c;尤其在需要大量对象时非常有效。 基本思想&#xff1a; …

【LeetCode 刷题】回溯算法(5)-棋盘问题

此博客为《代码随想录》二叉树章节的学习笔记&#xff0c;主要内容为回溯算法棋盘问题相关的题目解析。 文章目录 51. N皇后37. 解数独332.重新安排行程 51. N皇后 题目链接 class Solution:def solveNQueens(self, n: int) -> List[List[str]]:board [[. for _ in rang…

http和https的区别?

文章目录 一、安全性二、连接方式三、端口使用四、证书申请五、优缺点六、SSL&TLS协议6.1、SSL协议6.2、TLS协议6.3、SSL/TLS协议在HTTPS中的应用 总结 HTTP和HTTPS是两种常见的网络传输协议&#xff0c;它们在安全性、连接方式、端口使用以及证书申请等方面存在显著差异。…

【C语言】指针详细解读3

1. 数组名的理解 我们使用指针一般访问数组内容时&#xff0c;我们可能会这样写&#xff1a; int arr[10] {1,2,3,4,5,6,7,8,9,10}; int *p &arr[0]; 这⾥我们使⽤ &arr[0] 的⽅式拿到了数组第⼀个元素的地址&#xff0c;但是其实数组名本来就是地址&#xff0c;⽽…

React常见状态管理工具详解

了 解React常见的状态管理工具&#xff0c;需要详细解释。首先&#xff0c;我得回想一下React生态中常用的状态管理方案有哪些。React本身有useState和useContext&#xff0c;然后是第三方库比如Redux、MobX、Recoil、Zustand、Jotai、XState&#xff0c;可能还有Valtio。这些工…

Hackmyvm Deeper

简介 难度&#xff1a;简单 靶机&#xff1a;https://hackmyvm.eu/machines/?vdeeper 基本信息 kali&#xff1a;192.168.194.9 靶机&#xff1a;192.168.194.20 扫描 nmap基操 tcp扫描起手&#xff0c;nmap -sT -sV -A -T4 192.168.194.20 -Pn -p- 开启的tcp端口只有ss…

DeepSeek大模型介绍、本地化部署与使用!【AI大模型】

一、DeepSeek 是什么&#xff1f; 1.技术定位 专注大模型与AGI研究&#xff0c;开发高性能基座模型&#xff08;如 DeepSeek LLM 系列&#xff09;&#xff0c;支持长文本、多模态、代码生成等复杂任务。 提供开源模型&#xff08;如 DeepSeek-MoE、DeepSeek-V2&#xff09;…

Web - CSS3浮动定位与背景样式

概述 这篇文章主要介绍了 CSS3 中的浮动定位、背景样式、变形效果等内容。包括 BFC 规范与创建方法、浮动的功能与使用要点、定位的多种方式及特点、边框与圆角的设置、背景的颜色、图片等属性、多种变形效果及 3D 旋转等&#xff0c;还提到了浏览器私有前缀。 BFC规范与浏览…