拿捏循环链表
目录:
一:单链表(不带头单向不循环)与循环链表(带头双向循环)区别
二:循环链表初始化
三:循环链表头插
四:循环链表尾插
五:循环链表头删
六:循环链表尾删
七:循环链表查找
八:循环链表指定pos 位置的删除
九:循环链表指定pos 位置之前的插入
十:循环链表销毁
十一:结语
1:单链表(不带头单向不循环)与循环链表(带头双向循环)区别
1)结构上
循环链表多了给 前驱指针 pre
2)链表增删查改
有了pre这个指针,效率大大提升
2:循环链表初始化
讲到初始化,这里主要就是对哨兵位 (暂时称为:phead)进行设置
注意:哨兵位只是占一个位置,并不存储任何有效的数据
循环链表初始状态是空的:phead 自己成环
对应代码:
phead -> next = phead ;
phead -> pre = phead ;
那么问题就来了,在设计这个初始化函数的时候用一级指针还是二级指针
想必,前期看过我的单链表的博客,自然会说二级指针呀:
因为是对phead 这个指针进行改变所以是传二级指针。没毛病!不知道大家在做OJ题的时候,我们也涉及到对一级指针的改变,但是我们也可以返回这个一级指针,即可实现
ListNode* ListNodeInit()
{/*哨兵位:val 没有实际意义(自行赋值)初始化的目的就是对哨兵位进行设置因为整个接口都是用一级指针,若是初始化用二级指针有点不顺眼此函数返回哨兵位地址即可实现对哨兵位的初始化*/ListNode* phead = NULL;ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));if (newnode == NULL){perror("malloc fail");return NULL;}phead = newnode;phead->val = -1;phead->pre = phead->next = phead;//够成环return phead;
}
3:循环链表头插
头插:即在哨兵位的后面进行头插(若是链表为空,此时的头插也即是尾插)
头插分析:显然我们只需改变指针走向即可
注意指针先后问题
各位老铁康康这样的代码是否之正确:
phead-> next = newnode;
newnode-> next = phead-> next;
newnode-> pre = phead;
newnode->next-> pre = newnode;
此代码结合画图,来看, 显然这样是不对的,因为newnode-> next = phead-> next;这句代码让newnode 这个节点自己成环了,所以又怎么可能头插进去呢?
实质性原因是:当我们头插进来newnode 时,应该先执行
newnode-> next = phead->next; //注意指针先后问题
phead->next->pre = newnode; //让原来头结点的 pre 指向newndoe
newnode-> pre = phead;
phead-> next = newnode;
ListNode* newnode = BuyNode(x);newnode->next = phead->next;newnode->pre = phead;phead->next->pre = newnode;//原来第一个节点pre与新的头结点连接phead->next = newnode;//新的头结点
对于以上问题还可以这样解决:定义一个 next指针来保留一下原来的头结点
4:循环链表尾插
尾差之前我们需要先思考一个问题:对于单链表(不循环,不带头,单向)而言,每次尾插之前都需要遍历链表,来找尾结点
但是对于循环链表而言我们就不需要:找尾结点直接一步到位 phead-> pre
真的是没有对比就没有伤害,所以在这块,咱循环链表还是比较好搞滴
尾插分析:
这里只需改变指针走向即可
代码:
void ListNodePushBack(ListNode* phead,DataType x)
{assert(phead);/*1:找尾结点 phead->pre2:指针连接 */ListNode* newnode = BuyNode(x);newnode->next = phead;newnode->pre = phead->pre;phead->pre->next = newnode;//原来尾结点与newnode进行连接phead->pre = newnode;//新的尾结点}
5:循环链表头删
依然如此,按照“国际惯例”:找头结点 phead -> next
删除之前先保留一下 第二个节点
当把链表所以节点删除后(除哨兵位),会自动保存一个循环链表
代码见下:
void ListNodePopFront(ListNode* phead)
{assert(phead->next != phead);//不相等说明不为空ListNode* newFirst = phead->next->next;newFirst->pre = phead;phead->next = newFirst;//成为新的头结点//以下写法也对,但可读性差phead->next = phead->next->next;phead->next->pre = phead;}
6:循环链表尾删
既然谈到尾删,咱这里不得不提一嘴,单链表的尾删
单链表尾删逻辑:
1:链表不为空
2:只有一个节点:传二级指针
3:多个节点:传一级指针
4:找尾结点: 条件 tail -> next != NULL;
咱就是说,是不是事很多。
相比较之下,循环链表就比较友好:找啥尾结点,直接一步到位 phead-> pre
直接改变指针走向即可。
老问题:保留一下尾结点的前一个节点 tailPre
void ListNodePopBack(ListNode* phead)
{assert(phead);/*空链表: 1:找尾结点 phead->pre2:成环: 改变新的尾结点与phead 直接的链接*/assert(phead->next != phead);//不相等说明链表不为空,为空不能删除ListNode* tail = phead->pre;ListNode* tailPre = tail->pre;phead->pre = tailPre;//新的尾结点tailPre->next = phead;free(tail);
}
7:循环链表查找
此函数可以实现2个功能:一个是查找;另一个是修改
逻辑:按值查找,若是存在,直接返回当前节点,否则返回 NULL
注意是从 第一个节点开始 而不是从哨兵位 开始
ListNode* Find(ListNode* phead, DataType x) //指定数据查找
{/*从第一个节点开始查找: phead->next依次遍历,若是存在返回节点否则返回NULL*/assert(phead);ListNode* cur = phead->next;while (cur != phead ) //phead 是哨兵位{if (x == cur->val){return cur;}cur = cur->next;}return NULL;//没有找到
}
8:循环链表指定pos 位置的删除
这个接口的逻辑其实说白了与尾删没啥不同
注意:pos 这个节点是查找函数返回的
void ListNodeErase(ListNode* pos)//指定位置删除 pos是查找函数返回的
{assert(pos);ListNode* posPre = pos->pre;ListNode* posNext = pos->next;posPre->next = posNext;posNext->pre = posPre;free(pos);
}
9:循环链表指定pos 位置之前的插入
void ListNodeInsert(ListNode* pos, DataType x)//指定位置之前插入
{/*1:找到pos 前一个节点 pos->pre2:注意避免节点找不到3:改变指针连接: posPre,newnode,pos*/assert(pos);//为空直接不玩了ListNode* newnode = BuyNode(x);ListNode* posPre = pos->pre;posPre->next = newnode;newnode->pre = posPre;newnode->next = pos;pos->pre = newnode;}
10:循环链表销毁
这里的销毁就是一个节点一个节点进行删除
注意:包括哨兵位在内
void ListNodeDestroy(ListNode* phead)
{/*注意哨兵位也需要删除一个节点一个节点删除*/assert(phead);ListNode* cur = phead->next;while (cur != phead){ListNode* next = cur->next;free(cur);cur = next;}free(phead);
}
各位老铁们,别走开,接下来的问题你值得一看,或许哪天自己面试会遇到类似问题呢?
前段时间看到过样一个问题:
有个求职者去面试:在他的简历上写着自己是比较熟练数据结构这个模块的。
面试官问了这样一个问题:你能否在10分钟之内,搞一个链表出来?
听到这,求职者心里多多少少是有点担忧“搞,是没有问题,但是这个时间能否在宽裕点……”
进过这种协商,时间定在15分钟
对于这个问题的答卷:很显然,这个求职者没有拿到100分
屏幕前的各位铁子们,假设你是那个求职者,你又会如何回答好这份答卷?
是的,要是我,我一定会用循环链表来搞呀(因为面试官有没有指定具体是8中链表的哪一种)
你仔细想想:循环链表效率多高呀
对于一个链表的基本操作无非不就是:
头删头插
尾删,尾插
任意位置的插入和删除
不知道你是否考虑过这样问题:
循环链表的尾插,头插其实就是任意位置插入的一个特例
尾删,头删其实就是任意位置的删除的一个特例
完整代码如下:
DList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
void ListNodePrint(ListNode* phead)
{assert(phead);ListNode* cur = phead->next;printf("guard<==>");while (cur != phead){printf("%d<==>", cur->val);cur = cur->next;}printf("\n");
}
ListNode* ListNodeInit()
{/*哨兵位:val 没有实际意义(自行赋值)初始化的目的就是对哨兵位进行设置因为整个接口都是用一级指针,若是初始化用二级指针有点不顺眼此函数返回哨兵位地址即可实现对哨兵位的初始化*/ListNode* phead = NULL;ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));if (newnode == NULL){perror("malloc fail");return NULL;}phead = newnode;phead->val = -1;phead->pre = phead->next = phead;//够成环return phead;
}ListNode* BuyNode(DataType x)
{ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));if (newnode == NULL){perror("malloc fail");return NULL;}newnode->val = x;newnode->next = NULL;newnode->pre = NULL;return newnode;
}
void ListNodePushBack(ListNode* phead,DataType x)
{assert(phead);/*1:找尾结点 phead->pre2:指针连接 *///ListNode* newnode = BuyNode(x);//newnode->next = phead;//newnode->pre = phead->pre;//phead->pre->next = newnode;//原来尾结点与newnode进行连接//phead->pre = newnode;//新的尾结点ListNodeInsert(phead, x);//注意ListNodeInsert 这个函数功能是在pos 之前插入,所以要想实现尾插,需要传phead ,而不是phead->pre }
void ListNodePushFront(ListNode* phead, DataType x)
{/*注意phead不是头结点(第一个节点),phead 只是一个哨兵位,只占位置第一个节点:phead->next考虑指针先后问题,避免找不到第一个节点(链表不为空)*/assert(phead);//ListNode* newnode = BuyNode(x);//newnode->next = phead->next;//newnode->pre = phead;//phead->next = newnode;//新的头结点以上代码不对//ListNode* newnode = BuyNode(x);//newnode->next = phead->next;//newnode->pre = phead;//phead->next->pre = newnode;//原来第一个节点pre与新的头结点连接//phead->next = newnode;//新的头结点ListNodeInsert(phead->next, x);}void ListNodePopBack(ListNode* phead)
{assert(phead);/*空链表: 1:找尾结点 phead->pre2:成环: 改变新的尾结点与phead 直接的链接*/assert(phead->next != phead);//不相等说明链表不为空,为空不能删除ListNodeErase(phead->pre);//ListNode* tail = phead->pre;//ListNode* tailPre = tail->pre;//phead->pre = tailPre;//新的尾结点// tailPre->next = phead;//free(tail);
}
void ListNodePopFront(ListNode* phead)
{assert(phead->next != phead);//不相等说明不为空ListNodeErase(phead->next);//ListNode* newFirst = phead->next->next;//newFirst->pre = phead;//phead->next = newFirst;//成为新的头结点以下写法也对,但可读性差//phead->next = phead->next->next;//phead->next->pre = phead;}
ListNode* Find(ListNode* phead, DataType x) //指定数据查找
{/*从第一个节点开始查找: phead->next依次遍历,若是存在返回节点否则返回NULL*/assert(phead);ListNode* cur = phead->next;while (cur != phead ) //phead 是哨兵位{if (x == cur->val){return cur;}cur = cur->next;}return NULL;//没有找到
}void ListNodeInsert(ListNode* pos, DataType x)//指定位置之前插入
{/*1:找到pos 前一个节点 pos->pre2:注意避免节点找不到3:改变指针连接: posPre,newnode,pos*/assert(pos);//为空直接不玩了ListNode* newnode = BuyNode(x);ListNode* posPre = pos->pre;posPre->next = newnode;newnode->pre = posPre;newnode->next = pos;pos->pre = newnode;}void ListNodeErase(ListNode* pos)//指定位置删除 pos是查找函数返回的
{assert(pos);ListNode* posPre = pos->pre;ListNode* posNext = pos->next;posPre->next = posNext;posNext->pre = posPre;free(pos);
}
void ListNodeDestroy(ListNode* phead)
{/*注意哨兵位也需要删除一个节点一个节点删除*/assert(phead);ListNode* cur = phead->next;while (cur != phead){ListNode* next = cur->next;free(cur);cur = next;}free(phead);
}
DList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int DataType;
typedef struct DListNode
{DataType val;struct DListNode* next;struct DListNode* pre;//前驱指针}ListNode;
void ListNodePrint(ListNode*phead);
ListNode* ListNodeInit();void ListNodePushBack(ListNode* phead,DataType x);
void ListNodePushFront(ListNode* phead, DataType x);void ListNodePopBack(ListNode* phead);
void ListNodePopFront(ListNode* phead);ListNode* Find(ListNode* phead,DataType x); //指定数据查找(此函数可以实现2个功能:查找 + 修改)void ListNodeInsert(ListNode* pos, DataType x);//指定位置之前插入,pos是查找函数返回的void ListNodeErase(ListNode* pos);//指定位置删除void ListNodeDestroy(ListNode* phead);
/*
面试题目: 10分钟之内写一个链表
注意:头删,尾删 都只是 ListNodeErase 这个函数一个特例
头插,尾插,同理,是ListNodeInsert 一个特例
所以 可以借用
*/
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"void TestPush()
{ListNode* plist = ListNodeInit();//ListNodeInit(&plist);//ListNodePushBack(plist, 1);//ListNodePushBack(plist, 2);//ListNodePushBack(plist, 3);ListNodePushFront(plist, 1);ListNodePushFront(plist, 2);ListNodePushFront(plist, 3);ListNodePushFront(plist, 4);ListNodePushFront(plist, 5);ListNodePushFront(plist, 6);ListNodePushFront(plist, 7);ListNodePrint(plist);ListNodePushBack(plist, 0);ListNodePrint(plist);ListNodeDestroy(plist);}
void TestPop()
{//ListNode* plist = (ListNode*)malloc(sizeof(ListNode));//if (plist == NULL)// return;//plist->val = -1;//plist->next = plist;//plist->pre = plist;//等价于以下代码ListNode* plist = ListNodeInit();ListNodePushBack(plist, 1);ListNodePushBack(plist, 2);ListNodePushBack(plist, 3);ListNodePrint(plist);//ListNodePopFront(plist);//ListNodePrint(plist);//ListNodePopFront(plist);//ListNodePrint(plist);//ListNodePopFront(plist);//ListNodePrint(plist);ListNodePopBack(plist);ListNodePrint(plist);ListNodePopBack(plist);ListNodePrint(plist);ListNodePopBack(plist);ListNodePrint(plist);ListNodeDestroy(plist);}
void Test()
{ListNode* plist = ListNodeInit();ListNodePushBack(plist, 1);ListNodePushBack(plist, 2);ListNodePushBack(plist, 3);ListNode* pos = Find(plist, 3);if (pos != NULL){ListNodeInsert(pos, 10);ListNodePrint(plist);}else{printf("操作失败\n");}ListNodeDestroy(plist);}void Test1()
{ListNode* plist = ListNodeInit();ListNodePushBack(plist, 1);ListNodePushBack(plist, 2);ListNodePushBack(plist, 3);ListNode* pos = Find(plist, 3);if (pos != NULL){ListNodeErase(pos);ListNodePrint(plist);}else{printf("操作失败\n");}ListNodeDestroy(plist);}int main()
{TestPush();//TestPop();//Test();//Test1();return 0;
}
11:结语
以上就是我今日要为大家share的内容。其实说白了,循环链表的结构看似复杂,实际操作起来,非常简单。(就是多了一个 pre这样的一个前驱指针)。希望各位老铁们能够从这篇博客中学到一些知识,同时欢迎大家随时指正,那咱话不多说,你懂滴!
相关文章:

拿捏循环链表
目录: 一:单链表(不带头单向不循环)与循环链表(带头双向循环)区别 二:循环链表初始化 三:循环链表头插 四:循环链表尾插 五:循环链表头删 六࿱…...
UMLChina公众号精选(20240207更新)
UMLChina服务 如何选择UMLChina服务 《软件方法》分步改进指南 做对《软件方法》强化自测题获得“软件方法建模师”称号 建模示范视频 [EA-029/石油钻井管理平台]35套UML/SysMLEA/StarUML的建模示范视频-全程字幕 UMLChina连EA经销商都不是,EA水平靠谱嘛?…...

东南亚手游市场攻略:出海前的关键准备与注意事项
随着全球游戏市场的日益繁荣,越来越多的手游企业开始将目光投向海外市场,其中东南亚地区因其庞大的用户基数和逐渐成熟的游戏市场环境,成为了不少企业的首选目标。然而,想要在东南亚市场取得成功,并非易事。本文Nox聚星…...

python二维数组初始化的一个极其隐蔽的bug(浅拷贝)
初始化一个三行三列的矩阵 m n 3初始化方式1 a [[0 for i in range(m)] for j in range(n)]初始化方式2 b [] row [0 for i in range(0,m)] for i in range(0,n):b.append(row)分别输出两个初始化的结果 for row in a:print(row) for row in b:print(row)当前的输出为…...

iOS 需求 多语言(国际化)App开发 源码
一直觉得自己写的不是技术,而是情怀,一个个的教程是自己这一路走来的痕迹。靠专业技能的成功是最具可复制性的,希望我的这条路能让你们少走弯路,希望我能帮你们抹去知识的蒙尘,希望我能帮你们理清知识的脉络࿰…...

YOLOv8改进 | 利用训练好权重文件计算YOLOv8的FPS、推理每张图片的平均时间(科研必备)
一、本文介绍 本文给大家带来的改进机制是利用我们训练好的权重文件计算FPS,同时打印每张图片所利用的平均时间,模型大小(以MB为单位),同时支持batch_size功能的选择,对于轻量化模型的读者来说,本文的内容对你一定有帮助,可以清晰帮你展示出模型速度性能的提升以及轻量…...
std::vector<cv::Mat>和unsigned char** in_pixels 互相转换
将std::vectorcv::Mat转换为unsigned char** in_pixels, std::vector<cv::Mat> matVector; // 假设已经有一个包含cv::Mat的vector// 创建一个二维数组,用于存储像素数据 unsigned char** in_pixels new unsigned char*[matVector.size()]; for …...

04-Java建造者模式 ( Builder Pattern )
建造者模式 摘要实现范例 建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象 一个Builder 类会一步一步构造最终的对象,该 Builder 类是独立于其他对象的 建造者模式属于创建型模式,它提供了一种创建对…...

使用PHPStudy搭建Cloudreve网盘服务
文章目录 1、前言2、本地网站搭建2.1 环境使用2.2 支持组件选择2.3 网页安装2.4 测试和使用2.5 问题解决 3、本地网页发布3.1 cpolar云端设置3.2 cpolar本地设置 4、公网访问测试5、结语 1、前言 自云存储概念兴起已经有段时间了,各互联网大厂也纷纷加入战局&#…...
Lua协程-coroutine
lua也有协程这个机制,用以完成非抢占式的多任务处理。 协程与线程 协程和线程类似,有自己的堆栈、局部变量、指令指针等等。但同时也有不一致的地方,其中最重要的地方在于多线程程序可以同一时间运行多个线程,而协程同一时间只能…...

HTML5+CSS3+移动web——HTML 基础
目录 一、标签语法 HTML的基本框架 1. 标题标签 2. 段落标签 3. 换行和水平线 4. 文本格式化标签 5. 图像标签 6. 路径 相对路径 绝对路径 7. 超链接标签 8. 音频 9. 视频 10. 注释 二、标签结构 一、标签语法 HTML 超文本标记语言——HyperText Markup Langua…...
Java中List接口的常用方法
列举一些List接口的常用方法 List接口是Java集合框架中的一个核心接口,它定义了一个有序的集合(也称为序列)。List接口继承自Collection接口,因此它包含了Collection接口中定义的所有方法,同时还增加了一些特定的方法…...
mysql基础从头到尾快速梳理
MYSQL数据库学习 mysql的启动 net start mysql net stop mysql MYSQL客户端的连接 mysql -h 127.0.0.1 -p 3306 -u root -p SQL SQL通用语法 SQL语句可以单行或者多行书写,以分号结尾SQL语句可以使用空格/缩进来增强语句的可读性Mysql数据库的SQL语句不区分大…...

MySQL用心总结
大家好,好久不见,今天笔者用心一步步写一份mysql的基础操作指南,欢迎各位点赞收藏 -- 启动MySQL net start mysql-- 创建Windows服务 sc create mysql binPath mysqld_bin_path(注意:等号与值之间有空格) mysql -h 地址 -…...

电路设计(13)——生产线易拉罐自动计数装置的proteus仿真
1.设计要求 使用指定元件,用模电、数电等有关知识,设计并制作一个易拉罐饮料计数自动化的模拟装置。生产单位常采用红外自动计数装置,将装有饮料的易拉罐放在马达带动的传动带上,在传动带运动的过程中让每个易拉罐依次同一方向地穿…...

微服务-微服务Alibaba-Nacos 源码分析 (源码流程图)-2.0.1
客户端注册临时实例,GRPC处理 客户端服务发现 及订阅处理 客户端数据变换,数据推送,服务端集群服务数据同步...

编码技巧——在项目中使用Alibaba Cloud Toolkit远程部署
背景 在新公司项目开发,当前项目为自建项目,意思是从开发到运维都需要自己负责,远程的服务器也是自己搭建的win操作系统; 之前在大厂工作时,一般提交代码之后,CICD流水线会自动的执行最新代码的拉取、构建打…...
深度学习如何入门?
深度学习是一个广泛且深入的领域,入门需要一些基础知识和学习资源。以下是一些推荐的步骤和资源: 数学基础:深度学习需要一些数学基础,包括线性代数、微积分、概率论和统计学。这些都是理解深度学习算法背后的原理的关键。 编程基…...
米哈游(原神)终面算法原题
恒大正式破产 准确来说,是中国恒大(恒大汽车、恒大物业已于 2024-01-30 复牌)。 恒大破产,注定成为历史的注目焦点。 作为首个宣布破产的房地产企业,恒大的破产规模也创历史新高。 房地产作为曾推动中国三分之一经济增…...
机器学习如何改变缺陷检测的格局?
机器学习在缺陷检测中扮演着重要的角色,它能够通过自动学习和识别各种缺陷的模式和特征,改变缺陷检测的格局。以下是机器学习在缺陷检测中的一些应用和优势: 自动化检测:机器学习技术可以自动化处理大量的数据,通过学…...
椭圆曲线密码学(ECC)
一、ECC算法概述 椭圆曲线密码学(Elliptic Curve Cryptography)是基于椭圆曲线数学理论的公钥密码系统,由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA,ECC在相同安全强度下密钥更短(256位ECC ≈ 3072位RSA…...
连锁超市冷库节能解决方案:如何实现超市降本增效
在连锁超市冷库运营中,高能耗、设备损耗快、人工管理低效等问题长期困扰企业。御控冷库节能解决方案通过智能控制化霜、按需化霜、实时监控、故障诊断、自动预警、远程控制开关六大核心技术,实现年省电费15%-60%,且不改动原有装备、安装快捷、…...

MODBUS TCP转CANopen 技术赋能高效协同作业
在现代工业自动化领域,MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步,这两种通讯协议也正在被逐步融合,形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...

听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...
在Ubuntu24上采用Wine打开SourceInsight
1. 安装wine sudo apt install wine 2. 安装32位库支持,SourceInsight是32位程序 sudo dpkg --add-architecture i386 sudo apt update sudo apt install wine32:i386 3. 验证安装 wine --version 4. 安装必要的字体和库(解决显示问题) sudo apt install fonts-wqy…...

并发编程 - go版
1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...

五子棋测试用例
一.项目背景 1.1 项目简介 传统棋类文化的推广 五子棋是一种古老的棋类游戏,有着深厚的文化底蕴。通过将五子棋制作成网页游戏,可以让更多的人了解和接触到这一传统棋类文化。无论是国内还是国外的玩家,都可以通过网页五子棋感受到东方棋类…...
在golang中如何将已安装的依赖降级处理,比如:将 go-ansible/v2@v2.2.0 更换为 go-ansible/@v1.1.7
在 Go 项目中降级 go-ansible 从 v2.2.0 到 v1.1.7 具体步骤: 第一步: 修改 go.mod 文件 // 原 v2 版本声明 require github.com/apenella/go-ansible/v2 v2.2.0 替换为: // 改为 v…...
深入浅出WebGL:在浏览器中解锁3D世界的魔法钥匙
WebGL:在浏览器中解锁3D世界的魔法钥匙 引言:网页的边界正在消失 在数字化浪潮的推动下,网页早已不再是静态信息的展示窗口。如今,我们可以在浏览器中体验逼真的3D游戏、交互式数据可视化、虚拟实验室,甚至沉浸式的V…...

CSS 工具对比:UnoCSS vs Tailwind CSS,谁是你的菜?
在现代前端开发中,Utility-First (功能优先) CSS 框架已经成为主流。其中,Tailwind CSS 无疑是市场的领导者和标杆。然而,一个名为 UnoCSS 的新星正以其惊人的性能和极致的灵活性迅速崛起。 这篇文章将深入探讨这两款工具的核心理念、技术差…...