Android GPU纹理数据拷贝
在 Android 开发中读取纹理数据有以下几种方法:
- glReadPixels
- ImageReader
- PBO(Pixel BufferObject)
HardwareBuffer
1. glReadPixels
glReadPixels 是 OpenGL ES 的 API,通常用于从帧缓冲区中读取像素数据,OpenGL ES 2.0 和 3.0 均支持。使用非常方便,但是效率也是最低的。
- 当调用 glReadPixels 时,首先会影响 CPU 时钟周期,同时 GPU会等待当前帧绘制完成,读取像素完成之后,才开始下一帧的计算,造成渲染管线停滞。
- glReadPixels 读取的是当前绑定 FBO 的颜色缓冲区图像,所以当使用多个 FBO(帧缓冲区对象)时,需要确定好我们要读那个FBO 的颜色缓冲区。
- glReadPixels 性能瓶颈一般出现在大分辨率图像的读取,所以目前通用的优化方法是在 shader 中将处理完成的 RGBA 转成YUV(一般是 YUYV 格式),然后基于 RGBA 的格式读出 YUV 图像,这样传输数据量会降低一半,性能提升明显。
下面我们介绍两种使用 glReadPixels 来进行 RGBA 转换 NV21 的示例:
- 直接获取 RGBA 数据
这种方式 GPU 传输数据到 CPU 耗时比较长。
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, rgbaByteAddr);
libyuv::ABGRToNV21(rgbaByteAddr, width * 4, yByte, width, uvByte, width, width, height);;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
- OpenGL 扩展格式 YUV
// Draw Y
TextureAttributes textureAttriburesY = {.minFilter = GL_LINEAR,.magFilter = GL_LINEAR,.wrapS = GL_CLAMP_TO_EDGE,.wrapT = GL_CLAMP_TO_EDGE,.internalFormat = GL_RED_EXT,.format = GL_RED_EXT,.type = GL_UNSIGNED_BYTE
};varying vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main()
{vec4 color = texture2D(inputImageTexture,textureCoordinate);gl_FragColor.r = color.r*0.2990+color.g*0.5870+color.b*0.1140;
}// Draw UV
TextureAttributes textureAttriburesVU = {.minFilter = GL_LINEAR,.magFilter = GL_LINEAR,.wrapS = GL_CLAMP_TO_EDGE,.wrapT = GL_CLAMP_TO_EDGE,.internalFormat = GL_RG_EXT,.format = GL_RG_EXT,.type = GL_UNSIGNED_BYTE
};varying vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main()
{vec4 color = texture2D(inputImageTexture,textureCoordinate);gl_FragColor.rg = vec2(0.6150*color.r - 0.5150*color.g - 0.1000*color.b+0.5000,-0.1471*color.r - 0.2889*color.g + 0.4360*color.b+0.5000);
}glBindFramebuffer(GL_FRAMEBUFFER, yFbo);
glReadPixels(0, 0, width, height, GL_RED_EXT, GL_UNSIGNED_BYTE, yuv_byte);
glBindFramebuffer(GL_FRAMEBUFFER, 0);glBindFramebuffer(GL_FRAMEBUFFER, uvFbo);
glReadPixels(0, 0, width / 2, height / 2, GL_RG_EXT, GL_UNSIGNED_BYTE, yuv_byte + width * height);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
2. ImageReader
2.1 ImageReader 基础描述
ImageReader 是 Android 中的一个类,用于获取相机设备的图像数据。它可以用于捕获相机拍摄的静态图像或实时预览帧,并提供对图像数据的访问和处理。以下是一些 ImageReader 的特点和用法:
- 获取图像数据:通过创建一个 ImageReader 实例,可以指定要获取的图像的宽度、高度和图像格式。然后,可以使用ImageReader 的 acquireLatestImage() 或 acquireNextImage() 方法获取最新的图像或下一帧图像。这些方法返回一个 Image 对象,它包含了图像的数据和相关信息。
- 图像数据访问:通过 Image 对象,可以访问图像的像素数据。可以使用 getPlanes() 方法获取图像的平面数组,每个平面对应于图像的不同颜色通道。然后,可以使用 getBuffer() 方法获取每个平面的 ByteBuffer,从中读取或修改像素数据。
- 回收资源:使用完 Image 对象后,应调用其 close() 方法释放资源,以避免内存泄漏。
- 设置图像可用监听器:可以为 ImageReader 设置一个 OnImageAvailableListener 监听器,在新图像可用时收到通知,这样可以实现对图像数据的实时处理和分析。
- 配置图像输出:可以使用 ImageReader 的 setOnImageAvailableListener() 方法设置监听器,并通过 ImageReader 的 getSurface() 方法获取一个 Surface 对象,将其用于预览或拍照时的图像输出目标。
2.2 ImageReader 如何使用
我们可以使用 ImageReader 对象的 Surface 对象搭配 OpenGL 进行数据渲染。
mImageReader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 2);
mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mHandler);
mSurface = mImageReader.getSurface();
private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {@Overridepublic void onImageAvailable(ImageReader reader) {Image image = reader.acquireLatestImage();if (image != null) {image.close();}}
};
部分重要 API:
- acquireLatestImage() 从 ImageReader 队列中获取最新的一帧 Image ,并且将老的 Image 丢弃,如果没有新的可用的 Image 则返回 null 。此操作将会从 ImageReader 中获取所有可获取到的 Images ,并且关闭除了最新的 Image 之外的 Image 。此功能大多数情况下比 acquireNextImage 更推荐使用,更加适用于视频实时处理。需要注意的是 maxImages 应该至少为 2 ,因为丢弃除了最新的之外的所有帧需要至少两帧。换句话说,(maxImages - currentAcquiredImages < 2) 的情况下,丢帧将会不正常。
- acquireNextImage() 从 ImageReader 的队列中获取下一帧 Image ,如果没有新的则返回 null。Android 推荐我们使用 acquireLatestImage 来代替使用此方法,因为它会自动帮我们 close 掉旧的 Image,并且能让效率比较差的情况下能获取到最新的 Image 。acquireNextImage 更推荐在批处理或者后台程序中使用,不恰当的使用本方法将会导致得到的 images 出现不断增长的延迟。
- close() 释放所有跟此 ImageReader 关联的资源。调用此方法后,ImageReader 不会再被使用,再调用它的方法或者调用被 acquireLatestImage 或 acquireNextImage 返回的 Image 会抛出 IllegalStateException,尝试读取之前 Plane#getBuffer 返回的 ByteBuffers 将会导致不可预测的行为。
- newInstance(int width, int height, int format, int maxImages) 创建新的 reader 以获取期望的 size 和 format 的 Images。maxImages 决定了 ImageReader 能同步返回的最大的 Image 的数量,申请越多的 buffers 会耗费越多的内存空间,使用合适的数量很重要。
- newInstance(int width, int height, int format, int maxImages) 创建新的 reader 以获取期望的 size 和 format 的 Images。maxImages 决定了 ImageReader 能同步返回的最大的 Image 的数量,申请越多的 buffers 会耗费越多的内存空间,使用合适的数量很重要。
- maxImages:缓存的最大帧数,必须大于 0。
3. PBO(Pixel Buffer Object)
3.1 PBO 基础介绍
OpenGL PBO(Pixel Buffer Object),被称为像素缓冲区对象,主要被用于异步像素传输操作。PBO 仅用于执行像素传输,不连接到纹理,且与 FBO (帧缓冲区对象)无关。OpenGL PBO(像素缓冲区对象) 类似于 VBO(顶点缓冲区对象),PBO 开辟的也是 GPU 缓存,而存储的是图像数据。PBO 是 OpenGL ES 3.0 开始提供的一种方式,主要应用于从内存快速复制纹理到显存,或从显存复制像素数据到内存。
在使用 OpenGL 的时候经常需要在 GPU 和 CPU 之间传递数据,例如在使用 OpenGL 将 YUV 数据转换成 RGB 数据时就需要先将 YUV 数据上传到 GPU ,一般使用函数 glTexImage2D ,处理完毕后再将 RGB 结果数据读取到 CPU , 这时使用函数 glReadPixels 即可将数据取回。但是这两个函数都是比较缓慢的,特别是在数据量比较大的时候。PBO 就是为了解决这个访问慢的问题而产生的。
不使用 PBO 加载纹理:

使用 PBO 加载纹理:

3.2 PBO 如何使用?
int imgByteSize = m_Image.width * m_Image.height * 4;//RGBAglGenBuffers(1, &uploadPboId);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboId);
glBufferData(GL_PIXEL_UNPACK_BUFFER, imgByteSize, 0, GL_STREAM_DRAW);glGenBuffers(1, &downloadPboId);
glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadPboId);
glBufferData(GL_PIXEL_PACK_BUFFER, imgByteSize, 0, GL_STREAM_DRAW);
使用两个 PBO 从帧缓冲区读回图像数据:

如上图所示,利用 2 个 PBO 从帧缓冲区读回图像数据,使用 glReadPixels 通知 GPU 将图像数据从帧缓冲区读回到 PBO1 中,同时 CPU 可以直接处理 PBO2 中的图像数据。
// 交换 PBO
int index = m_FrameIndex % 2;
int nextIndex = (index + 1) % 2;// 将图像数据从帧缓冲区读回到 PBO 中
glBindBuffer(GL_PIXEL_PACK_BUFFER, m_DownloadPboIds[index]);
glReadPixels(0, 0, m_RenderImage.width, m_RenderImage.height, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);// glMapBufferRange 获取 PBO 缓冲区指针
glBindBuffer(GL_PIXEL_PACK_BUFFER, m_DownloadPboIds[nextIndex]);
GLubyte *bufPtr = static_cast<GLubyte *>(glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0,dataSize,GL_MAP_READ_BIT));
if (bufPtr) {nativeImage.ppPlane[0] = bufPtr;//NativeImageUtil::DumpNativeImage(&nativeImage, "/sdcard/DCIM", "PBO");glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
}
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
4. HardwareBuffer
4.1 HardwareBuffer 基础介绍
HardwareBuffer 官方介绍为一种底层的内存 buffer 对象,可在不同进程间共享,可映射到不同硬件系统,如 GPU、传感器等,从构造函数可以看出,其可以指定 format 和 usage,用来让底层选择最合适的实现,目前 format 主要是渲染相关的纹理格式,Android 11 之后支持了 BLOB 格式,可用来做 NN 相关的数据共享。
如果看一下 HardwareBuffer 的实现,会发现其只是 GraphicBuffer 的一个包装,只是 Android 低版本并没有开放 GraphicBuffer 相关 API,而前面提到的 Surface ,其底层就是基于 GraphicBuffer 来实现的,因此本质上是 Android 系统开放了更底层的 API,我们才可以有更高效的实现,接下来看具体如何基于 HardwareBuffer 跨进程传输纹理。

4.2 HardwareBuffer 如何使用?
AHardwareBuffer 创建纹理:
if(textureID == 0){AHardwareBuffer_Desc h_buffer_desc = {0};h_buffer_desc.stride = frameData->i32Width;h_buffer_desc.height = frameData->i32Height;h_buffer_desc.width = frameData->i32Width;h_buffer_desc.layers = 1;h_buffer_desc.format = 0x11;h_buffer_desc.usage = AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN | AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE;int ret = AHardwareBuffer_allocate(&h_buffer_desc, &inputHWBuffer);EGLint attr[] = {EGL_NONE};EGLDisplay edp;edp = (EGLDisplay)eglGetCurrentDisplay();inputEGLImage) = eglCreateImageKHR(edp, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, eglGetNativeClientBufferANDROID(inputHWBuffer), attr);glGenTextures(1, &textureID);glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureID);glTexParameteri(GL_TEXTURE_EXTERNAL_OES , GL_TEXTURE_MIN_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_EXTERNAL_OES , GL_TEXTURE_MAG_FILTER, GL_LINEAR);glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES , (GLeglImageOES)inputEGLImage);
}
AHardwareBuffer_Planes planes_info = {0}; int ret = AHardwareBuffer_lockPlanes(inputHWBuffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_MASK, -1,nullptr,&planes_info);
if (ret == 0) {memcpy(planes_info.planes[0].data,frameData->ppu8Plane[0],frameData->i32Width * frameData->i32Height*3/2);ret = AHardwareBuffer_unlock(inputHWBuffer, nullptr);
}
glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureID);
AHardwareBuffer 读取纹理图像数据:
unsigned char *ptrReader = nullptr;
ret = AHardwareBuffer_lock(inputHWBuffer, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, -1, nullptr, (void **) &ptrReader);
memcpy(dstBuffer, ptrReader, imgWidth * imgHeight * 3 / 2);
ret = AHardwareBuffer_unlock(inputHWBuffer, nullptr);
ImageReader、 PBO 和 HardwareBuffer 明显优于 glReadPixels 方式,HardwareBuffer、ImageReader 以及 PBO 三种方式性能相差不大,但是理论上 HardwareBuffer 性能最优。
补充:OPENGL NCNN GPU零拷贝实现

相关文章:
Android GPU纹理数据拷贝
在 Android 开发中读取纹理数据有以下几种方法: glReadPixelsImageReaderPBO(Pixel BufferObject) HardwareBuffer 1. glReadPixels glReadPixels 是 OpenGL ES 的 API,通常用于从帧缓冲区中读取像素数据,OpenGL ES…...
浏览器端直播推流实现——系统篇
浏览器端用vue3.5.12写,服务器端用php8.2+swoole5.1.4+thinkphp8写,流媒体服务器使用nginx-rtmp模块,拉流App端用uniapp(其他端各自实现吧,这里以App端为例) 操作系统基于opencloudos8,还用到了ffmpeg,该安装就安装,这里不啰嗦安装步骤 以下是vue的代码,比较简陋,各…...
HDFS和HBase跨集群数据迁移 源码
HDFS集群间数据迁移(hadoop distcp) hadoop distcp \ -pb \ hdfs://XX.14.36.205:8020/user/hive/warehouse/dp_fk_tmp.db/ph_cash_order \ hdfs://XX.18.32.21:8020/user/hive/warehouse/dp_fksx_mart.db/HBase集群间数据(hbase ExportSnap…...
opencv实时弯道检测
项目源码获取方式见文章末尾! 600多个深度学习项目资料,快来加入社群一起学习吧。 《------往期经典推荐------》 项目名称 1.【基于CNN-RNN的影像报告生成】 2.【卫星图像道路检测DeepLabV3Plus模型】 3.【GAN模型实现二次元头像生成】 4.【CNN模型实现…...
计算机网络综合题
IP数据报的划分 CRC差错检测 冗余码的计算 因此,余数是1110,传输的数为11010110111110。在传输过程中最后两位变成o,接收端能够发现,因为11010110111110除以10011余数不为0。 子网划分 暴力求解法 (定长子网划分大量…...
【ARM Linux 系统稳定性分析入门及渐进 1.2 -- Crash 工具依赖内容】
请阅读:【Linux 维测及Crash使用专栏】 文章目录 Prerequisites1. 内核对象文件2. 内存镜像3. 平台处理器类型4. Linux 内核版本 Prerequisites crash 工具需要依赖下面的内容: 1. 内核对象文件 vmlinux 文件:需要一个 vmlinux 内核对象文件ÿ…...
「C/C++」C++标准库 之 #include<exception> 异常处理库
✨博客主页何曾参静谧的博客📌文章专栏「C/C」C/C程序设计📚全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasoli…...
YOLOv7-0.1部分代码阅读笔记-experimental.py
experimental.py models\experimental.py 目录 experimental.py 1.所需的库和模块 2.class CrossConv(nn.Module): 3.class Sum(nn.Module): 4.class MixConv2d(nn.Module): 5.class Ensemble(nn.ModuleList): 6.def attempt_load(weights, map_locationNone): 1…...
【大数据学习 | kafka】简述kafka的消费者consumer
1. 消费者的结构 能够在kafka中拉取数据进行消费的组件或者程序都叫做消费者。 这里面要涉及到一个动作叫做拉取。 首先我们要知道kafka这个消息队列主要的功能就是起到缓冲的作用,比如flume采集数据然后交给spark或者flink进行计算分析,但是flume采用的…...
系统架构设计师论文:论湖仓一体架构及其应用
试题四 论湖仓一体架构及其应用 随着5G、大数据、人工智能、物联网等技术的不断成熟,各行各业的业务场景日益复杂,企业数据呈现出大规模、多样性的特点,特别是非结构化数据呈现出爆发式增长趋势。在这一背景下,企业数据管理不再局限于传统的结构化 OLTP (On-Line Transact…...
电磁兼容(EMC):GB 4343.1喀呖声 详解
目录 1. 喀呖声的危害 2. 喀呖声 Click定义 3. 中频参考电平 4. 开关操作 5. 最小观察时间 6. 喀呖声率 7. 喀呖声限值 8. 上四分位法 1. 喀呖声的危害 喀呖声作为一种电压骚扰,其危害主要体现在以下几个方面: 对电子设备的干扰:喀呖…...
纯血鸿蒙Native层支持说明
本文所有描述均参考鸿蒙官方文档:传送门 1.对C库的支持 C标准函数库在C语言程序设计中,提供符合标准的头文件,以及常用的库函数实现(如I/O输入输出和字符串控制)。 HarmonyOS采用musl作为C标准库,musl库…...
learn C++ NO.31——类型转换
C语言中的类型转换 在C语言中,当赋值符号两边的类型不匹配的时候,或者是形参类型和实参类型不匹配时,返回值类型与接受返回值类型不匹配时,都会需要类型转换。C语言的类型转换有两种:显示类型转换和隐式类型转换。 显…...
重学 Android 自定义 View 系列(三):自定义步数进度条
前言 本篇文章主要是实现仿QQ步数View,很老的一个View了,但技术永不落后,开搂! 最终效果如下: 1. 结构分析 QQStepView 主要由三个元素组成: 显示一个圆环进度条,通过外环和内环的角度变化来…...
海南华志亿星电子商务有限公司赋能抖音商家成长
在当今瞬息万变的电商时代,抖音凭借其短视频与直播电商的独特模式,迅速崛起并引领潮流。在这场电商变革中,海南华志亿星电子商务有限公司以其卓越的服务质量和创新的运营模式,在抖音电商领域大放异彩,成为众多商家的首…...
数据结构-并查集专题(1)
一、前言 因为要开始准备年底的校赛和明年年初的ACM、蓝桥杯、天梯赛,于是开始按专题梳理一下对应的知识点,先从简单入门又值得记录的内容开始,并查集首当其冲。 二、我的模板 虽然说是借用了jiangly鸽鸽的板子,但是自己也小做…...
共享汽车管理新纪元:SpringBoot框架应用
4系统概要设计 4.1概述 本系统采用B/S结构(Browser/Server,浏览器/服务器结构)和基于Web服务两种模式,是一个适用于Internet环境下的模型结构。只要用户能连上Internet,便可以在任何时间、任何地点使用。系统工作原理图如图4-1所示: 图4-1系统工作原理…...
道可云人工智能元宇宙每日资讯|《中国生成式人工智能应用与实践展望》白皮书发布
道可云元宇宙每日简报(2024年11月6日)讯,今日元宇宙新鲜事有: 《重庆市“机器人”应用行动计划(2024—2027年)》发布 近日,重庆市经济和信息化委员会、重庆市教育委员会等八部门印发《重庆市“…...
kaggle学习 eloData项目(1)-数据校验
文章目录 kaggle学习 eloData项目(1)-数据校验(1) 数据基本情况查看(2) 数据校验(3) 数据探究 小结 kaggle学习 eloData项目(1)-数据校验 不能懈怠࿰…...
ORACLE RAC用DNS服务器的配置
一、搭建本地YUM源 二、安装DNS全部组建 yum -y install bind* 三、规划您RAC集群所有IP #public 192.168.16.111 rac1.ntt.com rac1 192.168.16.112 rac2.ntt.com rac2 192.168.16.121 rac3.ntt.com rac3 192.168.16.122 rac4.ntt.com rac4 #private 10.10.10.111 rac1-pr…...
linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...
使用 SymPy 进行向量和矩阵的高级操作
在科学计算和工程领域,向量和矩阵操作是解决问题的核心技能之一。Python 的 SymPy 库提供了强大的符号计算功能,能够高效地处理向量和矩阵的各种操作。本文将深入探讨如何使用 SymPy 进行向量和矩阵的创建、合并以及维度拓展等操作,并通过具体…...
【从零学习JVM|第三篇】类的生命周期(高频面试题)
前言: 在Java编程中,类的生命周期是指类从被加载到内存中开始,到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。本文会深入探寻类的生命周期,让读者对此有深刻印象。 目录 …...
MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...
从 GreenPlum 到镜舟数据库:杭银消费金融湖仓一体转型实践
作者:吴岐诗,杭银消费金融大数据应用开发工程师 本文整理自杭银消费金融大数据应用开发工程师在StarRocks Summit Asia 2024的分享 引言:融合数据湖与数仓的创新之路 在数字金融时代,数据已成为金融机构的核心竞争力。杭银消费金…...
日常一水C
多态 言简意赅:就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过,当子类和父类的函数名相同时,会隐藏父类的同名函数转而调用子类的同名函数,如果要调用父类的同名函数,那么就需要对父类进行引用&#…...
【Linux】自动化构建-Make/Makefile
前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具:make/makfile 1.背景 在一个工程中源文件不计其数,其按类型、功能、模块分别放在若干个目录中,mak…...
Qt 事件处理中 return 的深入解析
Qt 事件处理中 return 的深入解析 在 Qt 事件处理中,return 语句的使用是另一个关键概念,它与 event->accept()/event->ignore() 密切相关但作用不同。让我们详细分析一下它们之间的关系和工作原理。 核心区别:不同层级的事件处理 方…...
WEB3全栈开发——面试专业技能点P7前端与链上集成
一、Next.js技术栈 ✅ 概念介绍 Next.js 是一个基于 React 的 服务端渲染(SSR)与静态网站生成(SSG) 框架,由 Vercel 开发。它简化了构建生产级 React 应用的过程,并内置了很多特性: ✅ 文件系…...
Java详解LeetCode 热题 100(26):LeetCode 142. 环形链表 II(Linked List Cycle II)详解
文章目录 1. 题目描述1.1 链表节点定义 2. 理解题目2.1 问题可视化2.2 核心挑战 3. 解法一:HashSet 标记访问法3.1 算法思路3.2 Java代码实现3.3 详细执行过程演示3.4 执行结果示例3.5 复杂度分析3.6 优缺点分析 4. 解法二:Floyd 快慢指针法(…...
