<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Kiramux</title><description>kira⭐萌！</description><link>https://notes.kiramux.com</link><item><title>美术资源备忘录</title><link>https://notes.kiramux.com/posts/001-artbooks</link><guid isPermaLink="true">https://notes.kiramux.com/posts/001-artbooks</guid><pubDate>Wed, 07 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是一个我个人整理的美术资源备忘录，用来记录我曾经觉得很有用但没来得及整理的美术资源。&lt;/p&gt;
&lt;p&gt;通常来说这些资源我浏览过之后会觉得很有用或是“之后一定用得到”，于是我把它们放进了b站 / 浏览器收藏夹，或是在 To Do 列表中贴一个地址。今天我偶然打开我的某个 To Do 列表，我竟然无法得知曾经的我是为什么记下它们。放在B站收藏夹里的那些视频，等到我真正需要它们时去翻找它们，费事费力，同时也会发现收藏夹里多了一些 “已失效的视频”，然而翻来找去却找不到自己想要找的那条视频，或许它就藏身于这些“已失效的视频”里……于是我想借此机会把自己曾经接触到的一些美术资源列出来作备忘，以免自己在有某些需求急需实现时忘了他们，而又焦头烂额甚至病急乱投医。这里会有一些大大的心得笔记、一些个人博客或是一些课程推荐的记录，也有一些书籍推荐，同时会附上推荐理由，虽然可能会没有时间去做整理，只是堆在这里，避免我自己忘记它们而已。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我该列个书单了。。&lt;/p&gt;
&lt;h2&gt;目录系列&lt;/h2&gt;
&lt;h3&gt;MadlineCG专题推送目录&lt;/h3&gt;
&lt;p&gt;https://mp.weixin.qq.com/s/PWuffjACVugobNiwsdrI4A&lt;/p&gt;
&lt;p&gt;里面有很多很有价值的文章和教学，包括创作思路分析等等&lt;/p&gt;
&lt;h3&gt;技术美术 百人计划目录 by May&lt;/h3&gt;
&lt;p&gt;https://docs.qq.com/doc/DUFlzT3ByV2tHanpT&lt;/p&gt;
&lt;p&gt;分为 &lt;strong&gt;图形部分&lt;/strong&gt;  、 &lt;strong&gt;美术部分&lt;/strong&gt; 、 以及一些其他的建议。&lt;/p&gt;
&lt;h3&gt;以求职为导向的入门级Unity攻略&lt;/h3&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/644800944&lt;/p&gt;
&lt;h2&gt;课程 / 视频合集系列&lt;/h2&gt;
&lt;h3&gt;合集·【美感提升】角色设计 by. &lt;a href=&quot;https://space.bilibili.com/431071234&quot;&gt;不要小白兔&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;https://space.bilibili.com/431071234/channel/collectiondetail?sid=2896715&lt;/p&gt;
&lt;h3&gt;合集·【美感提升】场景设计 by. &lt;a href=&quot;https://space.bilibili.com/431071234&quot;&gt;不要小白兔&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;https://space.bilibili.com/431071234/channel/collectiondetail?sid=3098951&lt;/p&gt;
&lt;h3&gt;【绘画教程】皮克斯搬运！色彩与光影。外网大佬视频，包看包会，记得收藏&lt;/h3&gt;
&lt;p&gt;https://www.bilibili.com/video/BV1UW411o7PJ/&lt;/p&gt;
&lt;h3&gt;Blender4.0建模入门教程_超细节100集课程 新手必学 by. &lt;a href=&quot;https://space.bilibili.com/6708009&quot;&gt;阿发你好&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;https://www.bilibili.com/video/BV1fb4y1e7PD/&lt;/p&gt;
&lt;h3&gt;Unity2024新手入门教程_超细节100集课程 by. &lt;a href=&quot;https://space.bilibili.com/6708009&quot;&gt;阿发你好&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;https://www.bilibili.com/video/BV1TZ4y1o76s/&lt;/p&gt;
&lt;p&gt;注意，这部合集是我从知乎上找 unity 特效时找到的。&lt;/p&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/483450692&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tips&lt;/strong&gt;: 又上知乎别的帖子得知 “永远的孤月”“魔王虾”是割韭菜之神，最好闭坑。&lt;/p&gt;
&lt;p&gt;割韭菜详情见：https://www.zhihu.com/question/616249171&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实名反对 魔王虾/皮皮虾/永远的孤月 的回答，开发 b站有很多视频 千峰黑马啥的都有免费课，ta建议看霜狼的百人计划，特效目前公开课都是基础教程，熟悉完粒子系统参数后，找何sir（moba） 李可 浮生（二次元手绘） sin（写实） 西西（moba） 捷导（二次元手绘）除了皮皮虾/火星时代 都可以学，b站有锤大佬皮皮虾的视频，可以看下。
我做特效的 说一下特效需要的软件，unity/ue引擎（二选一） ps处理贴图 max/blend/maya做模型（大部分还是max。
不知道你是不是美术生，做特效需要美术，确实是不会可以学，但是有没有审美天差地别，就像皮皮虾做的东西，你看看他做的，然后去 魔法盒 这个网站，看一下首页作品，这就是审美差距。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而这部合集就是他推荐的特效视频，因此对于 Unity 特效有没有帮助存疑。不过 up 主 阿发你好 是我曾经就关注过的，但是他的视频我还没有看过。&lt;/p&gt;
&lt;p&gt;但是原文提供的链接时旧版，这位 up 主已经将其下线了并重新上线了新版。&lt;/p&gt;
&lt;p&gt;旧版的原链接：https://bilibili.com/video/BV1WK411V7dn&lt;/p&gt;
&lt;h3&gt;【缺课】黄捷Unity3d二次元手绘与CG特效制作第48期【画质高清有大部分素材】&lt;/h3&gt;
&lt;p&gt;https://chenwenb.com/hju3d2cyshcgtx.html&lt;/p&gt;
&lt;p&gt;二次元手绘风格特效。简单看了一下，淘宝上也是117节课的缺课版本，因此大元资源网上的版本应是和那边一样的。&lt;/p&gt;
&lt;p&gt;网盘会有备份。&lt;/p&gt;
&lt;h3&gt;西西MOBA游戏特效课程基础篇【画质不错有素材】&lt;/h3&gt;
&lt;p&gt;https://chenwenb.com/xiximobayxtx.html&lt;/p&gt;
&lt;p&gt;网盘会有备份。&lt;/p&gt;
&lt;h2&gt;单集视频系列&lt;/h2&gt;
&lt;h3&gt;二次元，但写实！首次详解个人作画流程+基础科普 by. &lt;a href=&quot;https://space.bilibili.com/22759614&quot;&gt;mikasamu &lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;https://www.bilibili.com/video/BV1wi421Y7vB/&lt;/p&gt;
&lt;h3&gt;Giant Buffalo-Photoshop Manipulation Speed Art Tutorial&lt;/h3&gt;
&lt;p&gt;https://youtu.be/TN_vWXLu9YQ&lt;/p&gt;
&lt;h2&gt;个人博客系列&lt;/h2&gt;
&lt;h3&gt;大空昴单推人 的个人图书馆&lt;/h3&gt;
&lt;p&gt;http://www.360doc.com/userhome.aspx?userid=64191223&lt;/p&gt;
&lt;h2&gt;网站列表&lt;/h2&gt;
&lt;h3&gt;魔法盒&lt;/h3&gt;
&lt;p&gt;https://www.magesbox.com/&lt;/p&gt;
&lt;p&gt;貌似是一个很厉害的特效社区，可以进去膜拜一波。&lt;/p&gt;
&lt;h2&gt;书籍推荐&lt;/h2&gt;
&lt;h3&gt;[OCR] ARTISTS&apos; MASTER SERIES - Color&amp;amp;Light&lt;/h3&gt;
&lt;p&gt;部分汉化： https://www.bilibili.com/opus/841142548650000389&lt;/p&gt;
&lt;p&gt;网盘有备份&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- &amp;lt;video controls preload=&quot;metadata&quot; width=&apos;100%&apos; poster=&quot;https://graph.sayafx.top/2024/08/accaf92280b15e66575c2978deeda98a.webp&quot;&amp;gt;
&amp;lt;source src=&quot;https://pan.oharasaya.one/d/JAVFX/%E5%86%99%E7%9C%9F/MBRAU-013%20-%20Rina%20-%20An%20Nyoung%20Rina.mp4&quot; type=&quot;video/webm&quot;&amp;gt;
&amp;lt;/video&amp;gt; --&amp;gt;&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>【转载】 Unity Shader 毛发实现</title><link>https://notes.kiramux.com/posts/reprint-unity-shader-fur</link><guid isPermaLink="true">https://notes.kiramux.com/posts/reprint-unity-shader-fur</guid><pubDate>Wed, 07 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;原标题：真香预警！新版“峡谷第一美”妲己尾巴毛发制作分享&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/aIWMEO5Qa2gNn2yCmnHbOg&quot;&gt;原文链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;原创&lt;/code&gt; 腾讯游戏学院 腾讯游戏学堂 2019-02-20 19:44&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关于作者：梁家斌，腾讯互动娱乐天美工作室群高级游戏美术师。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;之前有很多人来询问新版妲己宝宝&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;毛茸茸的尾巴&lt;/strong&gt; 做法，&lt;/p&gt;
&lt;p&gt;先谢谢大家对这个毛发效果的认可，&lt;/p&gt;
&lt;p&gt;我在这里就简单的分享一下，&lt;/p&gt;
&lt;p&gt;毛发的实现思路和制作方法。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;毛绒材质在生活中出现的频率非常高，&lt;/p&gt;
&lt;p&gt;但是在各种游戏中，&lt;/p&gt;
&lt;p&gt;我们却很少看到这种材质效果的良好表现，&lt;/p&gt;
&lt;p&gt;原因在于它们的制作与渲染成本都太高了。&lt;/p&gt;
&lt;p&gt;所以， &lt;strong&gt;实时毛发渲染&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;是业内最为期待的次世代特效之一。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;我们先看看这些材质的特征：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655364-1.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 毛茸茸的衣服 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655365-2.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 可爱的毛绒玩具 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655365-3.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 毛茸茸的动物 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;他们有着柔软的丰富的外轮廓、&lt;/p&gt;
&lt;p&gt;柔和的光影、温暖，&lt;/p&gt;
&lt;p&gt;这种材质表现一直是女孩子所喜爱的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655365-4.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 微观的毛发，其实可以理解为一根根的圆柱体，越细毛发的质感越柔软 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655365-5.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 无数细小的圆柱体拼合在一起，就形成了我们看到的皮毛效果 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;因为毛发复杂性与庞大的计算量&lt;/p&gt;
&lt;p&gt;（每根毛发产生的阴影遮挡），&lt;/p&gt;
&lt;p&gt;所以在毛发渲染方面，&lt;/p&gt;
&lt;p&gt;基本一直都是离线渲染的专利。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655365-6.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ MAYA毛发渲染 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655365-7.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 迪士尼《疯狂动物城》毛发渲染 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;直到最近，&lt;/p&gt;
&lt;p&gt;GPU处理能力才达到实时计算毛发的标准，&lt;/p&gt;
&lt;p&gt;当然那也是针对PC平台而言，&lt;/p&gt;
&lt;p&gt;我们手机暂时还望尘莫及。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655365-8.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ Square Enix 夜光引擎的毛发 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655365-9.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 插片-毛发制作 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;我们一般游戏的毛发渲染，&lt;/p&gt;
&lt;p&gt;都是将毛发纹理放到模型面片上面，&lt;/p&gt;
&lt;p&gt;用AlphaBlend或者AlphaTest剔除镂空区。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;但这两者也都无法尽善尽美：&lt;/p&gt;
&lt;p&gt;-AlphaBlend没有深度会有模型穿插问题。&lt;/p&gt;
&lt;p&gt;-AlphaTest有顶点深度但边缘锯齿感严重，&lt;/p&gt;
&lt;p&gt;需要在SSAA下面才不会有明显的锯齿。&lt;/p&gt;
&lt;p&gt;-两者结合使用效果比较好，&lt;/p&gt;
&lt;p&gt;但会带来更高的性能消耗。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;所以至今为止，&lt;/p&gt;
&lt;p&gt;游戏上面都很难做到满意的毛发表现，&lt;/p&gt;
&lt;p&gt;尤其是手游，&lt;/p&gt;
&lt;p&gt;往往我们只能从设计层面去规避这些问题。&lt;/p&gt;
&lt;h2&gt;毛发的特征&lt;/h2&gt;
&lt;p&gt;要制作一个东西，&lt;/p&gt;
&lt;p&gt;我们必须抓住它的特点。&lt;/p&gt;
&lt;p&gt;那么毛发有哪些特征呢？&lt;/p&gt;
&lt;p&gt;我稍微归纳了一下：&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;通透性-次表面散射：&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;毛发是非常的细小的，&lt;/p&gt;
&lt;p&gt;光线很容易就可以穿透它的表面，&lt;/p&gt;
&lt;p&gt;尤其是浅色毛发，&lt;/p&gt;
&lt;p&gt;颜色越深吸收光线的能力越强，&lt;/p&gt;
&lt;p&gt;所以深色发毛的透光性相对较低。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655365-11.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 不同颜色的毛发通透性差异 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655365-12.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 逆光下的深色头发与浅色头发 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;这也是为啥很多女孩子&lt;/p&gt;
&lt;p&gt;比较喜欢染发、烫发的原因之一。&lt;/p&gt;
&lt;p&gt;因为浅色比黑色的毛发，&lt;/p&gt;
&lt;p&gt;在逆光下的效果要漂亮很多。&lt;/p&gt;
&lt;p&gt;烫发可以使头发蓬松，&lt;/p&gt;
&lt;p&gt;让更多的光线穿透与反弹，&lt;/p&gt;
&lt;p&gt;这样整体的层次感会变得很丰富。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;复杂的遮蔽性：&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;毛发又非常的密集，&lt;/p&gt;
&lt;p&gt;毛发相互之间的阴影遮挡造成毛发。&lt;/p&gt;
&lt;p&gt;密集的地方光线也很难继续渗透进去，&lt;/p&gt;
&lt;p&gt;就形成了发根到发梢的阴影渐变。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655365-13.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 毛发的Occlusion ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;特殊的高光-各项异性高光：&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;头发的表面并不是光滑的，&lt;/p&gt;
&lt;p&gt;上面有一层被称之为毛鳞片的结构，&lt;/p&gt;
&lt;p&gt;产生的高光散射性很高，&lt;/p&gt;
&lt;p&gt;很多人造毛很难做到这一点。&lt;/p&gt;
&lt;p&gt;这也是为啥很多廉价的假发的质感&lt;/p&gt;
&lt;p&gt;让人一眼就看出来不真实的原因。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655365-14.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 显微镜下的头发 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;上面我们说了头发，&lt;/p&gt;
&lt;p&gt;是排列在一起的一个个细小圆柱体。&lt;/p&gt;
&lt;p&gt;很多人不理解为什么&lt;/p&gt;
&lt;p&gt;我们要把头发的高光叫做各项异性高光，&lt;/p&gt;
&lt;p&gt;我用一张图简单说明下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-15.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 各项异性高光的产生 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;我们这里提到的所谓各向异性高光，&lt;/p&gt;
&lt;p&gt;其实是我们没办法真的将头发&lt;/p&gt;
&lt;p&gt;用一个个圆柱体来渲染，&lt;/p&gt;
&lt;p&gt;而使用一种基于观察近似的算法模拟结果。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;而动物的短毛皮比人类头发更加复杂，&lt;/p&gt;
&lt;p&gt;可以大致分为两类，&lt;/p&gt;
&lt;p&gt;一种是表层的长毛（被毛），&lt;/p&gt;
&lt;p&gt;另一种是内层的绒毛（更加细小、弯曲）。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;丝绸同理但更加复杂，&lt;/p&gt;
&lt;p&gt;不同的丝本身质感就不一样，&lt;/p&gt;
&lt;p&gt;加上不同针织结构，&lt;/p&gt;
&lt;p&gt;如纱、绮、绢、锦、罗、绸、缎等10多类，&lt;/p&gt;
&lt;p&gt;造成丝绸效果最终质感有很大的差异性，&lt;/p&gt;
&lt;p&gt;有兴趣可以深入了解不同丝绸的微观结构。&lt;/p&gt;
&lt;h2&gt;制作思路&lt;/h2&gt;
&lt;p&gt;说了这么多。&lt;/p&gt;
&lt;p&gt;在手机游戏里怎么实现这些效果呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-17.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用顶点细分来制作不现实，&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这么大量的毛发，&lt;/p&gt;
&lt;p&gt;又是移动端平台，&lt;/p&gt;
&lt;p&gt;机器的计算性能达不到要求。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;而用大量面片插片的方式，&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;会占用大量3D美术的人力成本，&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;尽管效果是很好，&lt;/p&gt;
&lt;p&gt;相对效果来说也不太划算。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最后选择了美术制作上最简单的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多pass渲染的方案。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为这套方案在美术资源制作上，&lt;/p&gt;
&lt;p&gt;基本上和普通角色资源没有差别，&lt;/p&gt;
&lt;p&gt;唯一的限制就是skin多边形数量上的限制。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;而且这套方案在PC端游上已经比较成熟，&lt;/p&gt;
&lt;p&gt;比如《剑灵》。&lt;/p&gt;
&lt;p&gt;多pass的方法局限性也很大，&lt;/p&gt;
&lt;p&gt;不过我们可以拆分开来研究和优化。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;layer实现方式&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-18.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 多pass的实现方式 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;根据模型使用层 (layer) 来渲染毛发长度，&lt;/p&gt;
&lt;p&gt;在 Unity Shader 中，&lt;/p&gt;
&lt;p&gt;每一个 Pass 即表示一层。&lt;/p&gt;
&lt;p&gt;当渲染每一层时，&lt;/p&gt;
&lt;p&gt;使用法线将顶点位置挤出模型表面 。&lt;/p&gt;
&lt;p&gt;Pass及使用的层数越多渲染效果越好，&lt;/p&gt;
&lt;p&gt;当然开销也越大。&lt;/p&gt;
&lt;p&gt;这种做法如果想要很好的表现，&lt;/p&gt;
&lt;p&gt;就需要大量的pass。&lt;/p&gt;
&lt;p&gt;如何用少量的pass实现更好的效果，&lt;/p&gt;
&lt;p&gt;后面我们再一步步解决。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-19.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float3 aNormal = (v.normal.xyz);

aNormal.xyz += FUR_OFFSET;

float3 n = aNormal * FUR_OFFSET * (FUR_OFFSET * saturate( v.color.a )); //顶点色alpha通道控制毛发扩展范围
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后将Noise贴图根据layer做衰减，&lt;/p&gt;
&lt;p&gt;来当做alpha值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-20.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ Noise贴图 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_UVoffset (&quot;UV偏移：XY=UV偏移;ZW=UV扰动&quot;, Vector) = (0, 0, 0.2, 0.2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一定加入基于layer层高度的UV偏移。&lt;/p&gt;
&lt;p&gt;• 没有UV偏移效果的毛怎么看都会像刺猬。&lt;/p&gt;
&lt;p&gt;• 记得对毛发做UV偏移的时候，&lt;/p&gt;
&lt;p&gt;Diffuse贴图的UV也要跟着一起计算哦。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float2 uvoffset= _UVoffset.xy  * FUR_OFFSET;

uvoffset *=  0.1 ; //尺寸太大不好调整 缩小精度。

float2 uv1= TRANSFORM_TEX(v.texcoord.xy, _MainTex ) + uvoffset * (float2(1,1)/_SubTexUV.xy);

float2 uv2= TRANSFORM_TEX(v.texcoord.xy, _MainTex )*_SubTexUV.xy   + uvoffset;

o.uv = float4(uv1,uv2);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;half3 NoiseTex = tex2D(_SubTex, i.uv.zw).rgb;

half Noise = NoiseTex.r;

color.rgb = lerp(_Color,_BaseColor,FUR_OFFSET) ;

color.a = saturate(Noise-FUR_OFFSET) ;

return color;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以加入风力、重力等顶点控制项。&lt;/p&gt;
&lt;p&gt;根据不同性能的机器来选择是否开启。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-21.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 默认：顶点重力：UV偏移 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-22.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ Noise混合 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;也可以将多层Noise&lt;/p&gt;
&lt;p&gt;混合到一起来做一些不同的毛发，&lt;/p&gt;
&lt;p&gt;将他们分别放到R、G、B不同的通道里，&lt;/p&gt;
&lt;p&gt;可以减少贴图量。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-23.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺点：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• 这样大家能看出来，&lt;/p&gt;
&lt;p&gt;多pass的制作方式，&lt;/p&gt;
&lt;p&gt;无法制作头发这样的长毛，&lt;/p&gt;
&lt;p&gt;只能制作较短的毛发。&lt;/p&gt;
&lt;p&gt;不过我们这次的目标也是制作短毛，&lt;/p&gt;
&lt;p&gt;所以长毛与头发可以先抛开。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;• 要有比较好的效果，&lt;/p&gt;
&lt;p&gt;就需要非常多的pass来进行计算，&lt;/p&gt;
&lt;p&gt;这也是我们不希望的。&lt;/p&gt;
&lt;p&gt;因为移动平台对大量Overdraw这样的&lt;/p&gt;
&lt;p&gt;像素级处理是非常大的一笔开销。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-24.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ layer30 : layer10 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;layer的层数是越少效率越高的，&lt;/p&gt;
&lt;p&gt;在低layer上得到更好效果是我们的目标。&lt;/p&gt;
&lt;p&gt;我将控制半透明毛发的曲线做了一些优化，&lt;/p&gt;
&lt;p&gt;勉强可以在低layer上面达到多layer的效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-25.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 对Alpha的衰减曲线做调整 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-26.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ layer30 : layer10 : 拟合曲线后的layer10 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;优化Layer数量&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;一般的做法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alpha = Noise -FUR_OFFSET;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优化后：同时加入了可控的变量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alpha = (Noise*2-(FUR_OFFSET *FUR_OFFSET +(FUR_OFFSET*FurMask*5)))*_tming ;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;灯光&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;到现在为止，&lt;/p&gt;
&lt;p&gt;在外形上基本接近了我们想要的毛发效果。&lt;/p&gt;
&lt;p&gt;我们还需要将毛发的渲染特征也加上去。&lt;/p&gt;
&lt;p&gt;这里用3个部分来实现：&lt;/p&gt;
&lt;p&gt;环境光、轮廓光、太阳光&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;环境光：&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;环境光可以是一个单色，&lt;/p&gt;
&lt;p&gt;也可以是一个微弱的顶底渐变，&lt;/p&gt;
&lt;p&gt;或者球协光照（提取Hdir贴图低频数据）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-27.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 这里可以根据不同项目的需求来定义 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;这里以简单的顶底颜色为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float3 normal = normalize(mul(UNITY_MATRIX_MV, float4(v.normal,0)).xyz);

half3 SH = saturate(normal.y *0.25+0.35) ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-28.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 无环境光遮，与真实效果差异很大 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;前面讲了毛发的特点之一&lt;/p&gt;
&lt;p&gt;就是环境光遮蔽与自阴影，&lt;/p&gt;
&lt;p&gt;缺少了环境光遮蔽的效果只能打20分。&lt;/p&gt;
&lt;p&gt;环境光遮蔽形成的散射是带有颜色的，&lt;/p&gt;
&lt;p&gt;会根据物体的颜色不同产生不同颜色。&lt;/p&gt;
&lt;p&gt;我很懒，就没将颜色与环境光遮蔽&lt;/p&gt;
&lt;p&gt;之间的关系公式写进去，&lt;/p&gt;
&lt;p&gt;直接开放一个颜色手动设置反弹的颜色。&lt;/p&gt;
&lt;p&gt;（也能节约一些计算不是~）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;half Occlusion =FUR_OFFSET*FUR_OFFSET; //伽马转线性最精简版

Occlusion +=0.04 ;

half3 SHL = lerp (_OcclusionColor*SH,SH,Occlusion) ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-29.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 无环境光遮 vs 加上环境光遮蔽 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;但要记住固有色越浅，&lt;/p&gt;
&lt;p&gt;反弹光就越强，&lt;/p&gt;
&lt;p&gt;Visibility的影响就越弱；&lt;/p&gt;
&lt;p&gt;反之颜色越深，&lt;/p&gt;
&lt;p&gt;反弹光就越弱，&lt;/p&gt;
&lt;p&gt;Visibility的影响就越强；&lt;/p&gt;
&lt;p&gt;简单的说就是：&lt;/p&gt;
&lt;p&gt;**物体的颜色越浅，**&lt;strong&gt;AO颜色越浅；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**反之颜色越深，**&lt;strong&gt;AO颜色越深。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;轮廓光：&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;轮廓光其实也是环境光的一部分。&lt;/p&gt;
&lt;p&gt;这里单独给轮廓光计算，&lt;/p&gt;
&lt;p&gt;也只是弥补环境反光的不足，&lt;/p&gt;
&lt;p&gt;同时加一些可控项，&lt;/p&gt;
&lt;p&gt;也可以调出一些特殊的不一样的效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-30.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 毛发轮廓光 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;同时也和上面的一样，&lt;/p&gt;
&lt;p&gt;物体的颜色越浅，&lt;/p&gt;
&lt;p&gt;轮廓光穿透率越强；&lt;/p&gt;
&lt;p&gt;反之颜色越深，&lt;/p&gt;
&lt;p&gt;轮廓光穿透率越弱。&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;这里也一定要加入环境光遮蔽的遮挡，&lt;/p&gt;
&lt;p&gt;因为毛发的透光性，&lt;/p&gt;
&lt;p&gt;边缘稀疏的部分光线穿透率更高。&lt;/p&gt;
&lt;p&gt;同时模拟了在环境光下次表面散射效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-31.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ Fresnel ：Fresnel+Visilibity ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;差异性在低多边形下会更加明显。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;half Occlusion =FUR_OFFSET*FUR_OFFSET; //伽马转线性最精简版

Occlusion +=0.04 ;

half Fresnel = 1-max(0,dot(N,V));//pow (1-max(0,dot(N,V)),2.2);

half RimLight =Fresnel * Occlusion; //AO的深度剔除 很重要

RimLight *=RimLight; //fresnel~pow简化版

RimLight *=_FresnelLV *SH; //加上环境光因数

   SHL +=RimLight；//与环境光结合
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655366-32.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 模型线框：Occlusion平方：Occlusion4四次方 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;将Occlusion的计算&lt;/p&gt;
&lt;p&gt;放到RimLight平方的前面，&lt;/p&gt;
&lt;p&gt;是因为模型的多边形数量低，&lt;/p&gt;
&lt;p&gt;轮廓会比较明显。&lt;/p&gt;
&lt;p&gt;将Occlusion4次方，&lt;/p&gt;
&lt;p&gt;能更好的减弱低多边形的影响。&lt;/p&gt;
&lt;p&gt;因为我们用于毛发材质的模型面数，&lt;/p&gt;
&lt;p&gt;是非常非常低的，&lt;/p&gt;
&lt;p&gt;如果模型面数相对高一些的模型，&lt;/p&gt;
&lt;p&gt;可以放到后面。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-33.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 环境光最终渲染结果 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;太阳光：&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;我们平常说的太阳光，&lt;/p&gt;
&lt;p&gt;其实可以把它看成是平行光。&lt;/p&gt;
&lt;p&gt;太阳其实是个点光源，&lt;/p&gt;
&lt;p&gt;不过因为它过于庞大，&lt;/p&gt;
&lt;p&gt;光线到地球上面的夹角非常非常小，&lt;/p&gt;
&lt;p&gt;再到我们的可视物体的时候，&lt;/p&gt;
&lt;p&gt;就完全可以忽略掉了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-34.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 太阳为什么是平行光 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;我们就取最简单的公式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;half3 lightDir = -_SGameShadowParams.xyz; //外部传入的灯光方向

half NoL =dot(lightDir,normal);

half DirLight =NoL * _FurDirLightExposure*_DirLightColor;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-35.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 正测光：背光 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;普通光照模型这样计算没有问题，&lt;/p&gt;
&lt;p&gt;但完全没有毛发的特性：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺少太阳光在边缘的穿透性，&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;也没有逆光下的毛发次表面散射效果。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缺少每根毛产生的复杂的阴影表现。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-36.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 阴影与光线边缘的穿透 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;用一个最简单的拟合就可以得到这个效果。&lt;/p&gt;
&lt;p&gt;主要利用了NdotL（-1~1）的特性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_LightFilter(&quot;平行光毛发穿透&quot;,  Range(-0.5,0.5)) = 0.0

half3 lightDir = -_SGameShadowParams.xyz;

half NoL =dot(lightDir,normal);

half DirLight= saturate (NoL+_LightFilter+ FUR_OFFSET ) ;

DirLight *=_FurDirLightExposure*_DirLightColor;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-37.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 正测光：背光 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;最后将所有光照合并到一起：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-38.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-39.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 佩奇陪你过大年 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;• 为了节省性能，所有灯光与颜色计算全部在顶点空间完成；&lt;/p&gt;
&lt;p&gt;• 像素空间只用来计算贴图采样；&lt;/p&gt;
&lt;p&gt;• 很多需要贴图一起计算颜色值的地方都优化省略掉了，比较遗憾。&lt;/p&gt;
&lt;p&gt;• 所有颜色计算都是在线性空间进行的所以最后要转换到伽马空间。这一步也可以去掉，也可以再加上简单的tommping，让画面颜色变得更好。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;高光 - Anisotropic（各项异性）&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-40.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;前面讲毛发特性的时候，&lt;/p&gt;
&lt;p&gt;对各项异性高光做了个简单的介绍。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-41.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ Anisotropic（各项异性） ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;学术上Anisotropic（各项异性）的解释：&lt;/p&gt;
&lt;p&gt;• 某些材质上有一些微观上有方向的细丝，这些细丝在宏观角度来看是不易察觉的，典型的有光盘的背面或者是头发。&lt;/p&gt;
&lt;p&gt;• strand based anisotropy是 对上述光照情形的一种建模。&lt;/p&gt;
&lt;p&gt;（http://www.bluevoid.com/opengl/sig00/advanced00/notes/node159.html）&lt;/p&gt;
&lt;p&gt;一些Anisotropic表现的例子：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-42.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 《崩坏3》 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-43.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 《爱丽丝惊魂记：疯狂再临 (Alice: Madness Returns)》 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;各向异性制作的各种具体实现方式&lt;/p&gt;
&lt;p&gt;我就不一一细说了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-44.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 沿着法线方向去偏移切线 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float3 TShift(float3 tangent,float3 normal,float shift)
           {
               return normalize(tangent + shift * normal);
           }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-45.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ Anisotropic高光 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float StrandSpecular(fixed3 T,fixed3 V,fixed3 L,fixed exponent)
   {
       float3 H = normalize(L+V);
       float dotTH = dot(T,H);
       float sinTH = sqrt(1- dotTH * dotTH);
       float dirAtten = smoothstep(-1,0,dotTH);    
       return dirAtten*pow(sinTH,exponent);
   }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用抖动忒图来弥补头发细节。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-46.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 抖动贴图制作头发高光细节 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;头发比较特殊&lt;/strong&gt;
观察头发的高光会发现，&lt;/p&gt;
&lt;p&gt;其中一层高光是有颜色的，&lt;/p&gt;
&lt;p&gt;另外一层高光是没有颜色的，&lt;/p&gt;
&lt;p&gt;且两层高光的相互错开一点点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-47.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 观察头发高光 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-48.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 头发双层高光公式 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;我们根据公式在VS里计算出高光效果。
这时候会发现高光效果会很粗糙，&lt;/p&gt;
&lt;p&gt;简直有点不堪入目。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-49.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 顶点 VS 逐像素 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;一步一步来优化。&lt;/p&gt;
&lt;p&gt;• 依然使用上面已经使用老套的方法，&lt;/p&gt;
&lt;p&gt;用FUR_OFFSET做高光的遮蔽后，&lt;/p&gt;
&lt;p&gt;因为VS渲染精度方面过低的问题缓解了，&lt;/p&gt;
&lt;p&gt;但效果方面依然不是太好。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fixed SPec1 =StrandSpecular (T1,V,L,_specExp.x)*FUR_OFFSET;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-50.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 高光加入FUR_OFFSET对比 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;• 因为毛束是一个个细小的单个圆柱体，&lt;/p&gt;
&lt;p&gt;它的高光也并不是一个平面连续的表现。&lt;/p&gt;
&lt;p&gt;我想了很多方法来弥补毛发体积与细节。
最终下面的结果相对来说，&lt;/p&gt;
&lt;p&gt;得出的结果是目前得出的最好的。&lt;/p&gt;
&lt;p&gt;大家猜一下——下面这张图是怎么得到的？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655367-51.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ color = Alpha平方 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其实这就是我们目前的alpha当做色彩输出的结果。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• 越靠近透明的区域越黑，中间区域偏亮。&lt;/p&gt;
&lt;p&gt;• 它刚好能达到我们高光缺少的细节部分的要求。&lt;/p&gt;
&lt;p&gt;我就用它来当做单根毛发边缘体积的遮挡。&lt;/p&gt;
&lt;p&gt;得到的结果很不错。
同时这个也替代了抖动贴图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;color.rgb +=i.Specular  * (Noise*Noise);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655368-52.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 加入Noise遮罩,代替抖动贴图方案 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;加入低频与高频2层高光后的效果更加自然。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655368-53.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ 双层高光效果 ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fixed3 T1 = normalize(_specExp.z*normalWorld+binormalWorld);
fixed3 T2 = normalize(_specExp.w*normalWorld+binormalWorld);
fixed SPec1 =StrandSpecular (T1,V,L,_specExp.x) *FUR_OFFSET;
fixed SPec2 =StrandSpecular (T2,V,L,_specExp.y) *FUR_OFFSET;
o.Specular  = SPec1*_SPColor1  + SPec2*_SPColor2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;高光部分我测试了很多方案，
目前效果只能说还能凑合着用，
因为都是在VS进行的计算，&lt;/p&gt;
&lt;p&gt;效率上面应该还行。&lt;/p&gt;
&lt;p&gt;高光参考资料：&lt;/p&gt;
&lt;p&gt;https://developer.amd.com/wordpress/media/2012/10/Scheuermann_HairRendering.pdf&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;第一次写分享，&lt;/p&gt;
&lt;p&gt;罗里吧嗦的写了一大堆。&lt;/p&gt;
&lt;p&gt;效果方面肯定还有非常大的优化空间。&lt;/p&gt;
&lt;p&gt;欢迎大家一起讨论和指正。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一些个人遇到的问题\：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• 毛发UV控制部分我加入了flowmap，&lt;/p&gt;
&lt;p&gt;但是FlowTex的绘制太困难，&lt;/p&gt;
&lt;p&gt;而且也不直观。&lt;/p&gt;
&lt;p&gt;可以做个工具实时直观看到flowmap&lt;/p&gt;
&lt;p&gt;在毛发上面的绘制效果。&lt;/p&gt;
&lt;p&gt;甚至可以直接在unity绘制到模型顶点色&lt;/p&gt;
&lt;p&gt;XY2个值上面控制毛发方向。&lt;/p&gt;
&lt;p&gt;• 多模型重叠的时候还是有深度穿插。&lt;/p&gt;
&lt;p&gt;分享一个16年的国外视频，&lt;/p&gt;
&lt;p&gt;用了另一种方案来控制毛发长度与方向。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/furry-tail/640-1723010655368-55.webp&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;[ https://www.youtube.com/watch?v=bl61E2j_q-U ]&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;·END·&amp;lt;/center&amp;gt;&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>Unity Shader 入门笔记vol1--第一束光</title><link>https://notes.kiramux.com/posts/003-technical-artist-learning-vol1</link><guid isPermaLink="true">https://notes.kiramux.com/posts/003-technical-artist-learning-vol1</guid><pubDate>Tue, 08 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前面的碎碎念&lt;/h2&gt;
&lt;p&gt;本人从 2024 年 3 月初识技术美术，然后一边烦恼一边在各大平台乱逛，想要了解这个职位究竟如何。期间也断断续续学了一点点，就这么犹犹豫豫半年过去了，直至前阵子总算清除了所有的迷惘，并且学过了那些基础的光照模型。&lt;/p&gt;
&lt;p&gt;本人是按照 &lt;a href=&quot;https://www.bilibili.com/video/BV1BE411N74b/&quot;&gt;AP01&lt;/a&gt; 的课程来入门的，目前刚好处于学过光照模型，然后做过第一个 DOTA2 大作业的状态，勉勉强强可以说是入了半个门了。接下来用我的节奏讲讲我的一些理解。&lt;/p&gt;
&lt;p&gt;本人在学习技术美术的过程始终贯彻一个原则——“装懂”，听的懂的很好，听不懂的先装作懂了先用着，秉持一个能跑就行，先跟着走，之后再尝试理解。因此直到现在那些 输入结构 、 输出结构 、 像素 Shader 之类的东西我还没弄懂。&lt;/p&gt;
&lt;p&gt;首先是渲染流程，我还没弄明白，只是在脑中有一个懵懂的概念，因此先贴上PPT。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008201244509.png&quot; alt=&quot;image-20241008201244509&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;第一束光-Lambert&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;向量&lt;/strong&gt; ( Vector )：既有大小 也有方向的量；如：力，速度等。&lt;/p&gt;
&lt;p&gt;理论上不存在没有大小，只有方向的量；但存在大小无现实意义的量，我们多把它归一化处理，在 Shader 中如果出现大于 1 的量会比较危险。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;点乘/点积/Dot&lt;/strong&gt; ：两向量间的一种运算方式，结果为一标量，具有良好的几何含义。&lt;/p&gt;
&lt;p&gt;结果的&lt;strong&gt;几何意义&lt;/strong&gt;为：一个向量在另一向量上的投影长度；&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结果的图形学表现为：两向量 &lt;strong&gt;方向同&lt;/strong&gt; 结果= 1 （白色）； &lt;strong&gt;方向反&lt;/strong&gt; 结果= 1 （黑色）； &lt;strong&gt;垂直&lt;/strong&gt; 结果= 0 （黑色）&lt;/p&gt;
&lt;p&gt;根据向量点乘的图形学含义 我们令：&lt;/p&gt;
&lt;p&gt;• 模型表面的垂直方向为向量 &lt;strong&gt;nDir&lt;/strong&gt; 即：法线方向（ NormalDirection ）;
• 令光照方向的反方向为向量 &lt;strong&gt;lDir&lt;/strong&gt; 即： LightDir ，光方向（由物体表面指向光源）;
• 令 nDir · lDir (两者点乘) 结果为像素输出；&lt;/p&gt;
&lt;p&gt;则有下图的光照表现&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008173452496.png&quot; alt=&quot;image-20241008173452496&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;最亮处：值为1，纯白；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;明暗交界处：值为0，纯黑；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;暗部：值为负数，亦为黑；&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008173636516.png&quot; alt=&quot;image-20241008173636516&quot; /&gt;&lt;/p&gt;
&lt;p&gt;负数是个无意义的亮度，所以我们通常把结果为负数的值截断，都改为 0 ：&lt;/p&gt;
&lt;p&gt;Max(0 , nDir · IDir);&lt;/p&gt;
&lt;p&gt;即为 &lt;strong&gt;兰伯特光照模型 ( Lambert )&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008173818184.png&quot; alt=&quot;image-20241008173818184&quot; /&gt;&lt;/p&gt;
&lt;p&gt;截断负值的做法还可以用 Clamp 0-1 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shader Forge 实现&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们在 Shader Forge 中新建一个 Unlit 模型作为基础，我们所有的效果都靠自己写，不涉及 Unity 的黑盒的东西。&lt;/p&gt;
&lt;p&gt;此处观察 Shader Forge 中的 Light Dir. 的图片就可以理解光方向其实是 “光方向的反方向”&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008174142424.png&quot; alt=&quot;image-20241008174142424&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们先给场景内添加个球，然后对这个 Shader 右键 &amp;gt;&amp;gt; Create &amp;gt;&amp;gt; Material ，用它创建一个材质。然后拖动这个材质到球上，就把这个材质附给这只球了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008174309616.png&quot; alt=&quot;image-20241008174309616&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;HalfLambert&lt;/h2&gt;
&lt;p&gt;我们现在有了 兰伯特光照模型，感觉这个 兰伯特 的暗部全是一片死黑。那么有一种改进方法。&lt;/p&gt;
&lt;p&gt;如下图，对兰伯特的结果 [-1,1] * 0.5 + 0.5 ，得到一个 [0,1] 的结果，这就是半兰伯特（ HalfLambert ）。它的优缺点如图所示，虽然比 Lambert 更透气，但是显然它不符合我们常见的光照情况，因为半兰伯特是一个经验模型，他并不是由物理公式推理而来的。实际上之前版本的 cs 就使用了半兰伯特模型。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008175019657.png&quot; alt=&quot;image-20241008175019657&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shader Forge 实现&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008202154128.png&quot; alt=&quot;image-20241008202154128&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008175532969.png&quot; alt=&quot;image-20241008175532969&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里的 Clamp 0-1 等效于之前所做的 Max0&lt;/p&gt;
&lt;p&gt;半兰伯特 放到这个球上还是挺漂亮的。&lt;/p&gt;
&lt;h2&gt;HalfLambert 调子映射&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008175725954.png&quot; alt=&quot;image-20241008175725954&quot; /&gt;&lt;/p&gt;
&lt;p&gt;考虑对半兰伯特做映射。上面讲点积时提到，点积的结果是一维的，但是要对一张贴图（RampTex）采样需要一套 UV （2维），就是说必须要有2个值才能去采样一张贴图。想像那些模型展 UV 时，把模型拆开然后摊大饼，在U坐标V坐标摊开，然后去做贴图之类的东西。这不可能只摊成一条线，那样采样贴图是不可能的。&lt;/p&gt;
&lt;p&gt;我们现在有了 HalfLambert 这个一维的常量，那么如何得到一个二维向量呢？&lt;/p&gt;
&lt;p&gt;这里就要用到 Append 节点，我们把 2 个 标量/一维向量 &lt;strong&gt;Append&lt;/strong&gt; ，就变成了一个 二维向量（即有2个分量的向量） ，一个 二维向量 和一个 标量/一维向量 &lt;strong&gt;Append&lt;/strong&gt; 就变成了三维向量，最多到四维，理解为 RGBA 4个通道。（另外 Component Mask 节点是拆分量的）&lt;/p&gt;
&lt;p&gt;那么我们可以把 HalfLambert 和一个常量 &lt;strong&gt;Append&lt;/strong&gt; 成一个二维向量，把它作为 UV 去采样一张贴图（RampTex），采样的结果连接到 Shader Forge 里的 Emission 就完成了。&lt;/p&gt;
&lt;p&gt;这里一般来说贴图的渐变是横坐标变换，所以把半兰伯特作为横坐标，另外给的常量作为纵坐标。&lt;/p&gt;
&lt;p&gt;然后我们可以用 HalfLambert 做一个简单的卡通渲染（简称”卡渲“）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;贴图的特点&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;3 阶明度，过渡卡硬。&lt;/p&gt;
&lt;p&gt;暗部做色相变化，少明度变化。&lt;/p&gt;
&lt;p&gt;明暗交界线附近由于有偏红的 SSS 效果，&lt;strong&gt;光能守恒&lt;/strong&gt;的原因可能在明暗交界线附近的亮部，会些许泛蓝。&lt;/p&gt;
&lt;p&gt;这张贴图尺寸不用很大，采样贴图是一个比较费的操作，一般128*128即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008200032605.png&quot; alt=&quot;image-20241008200032605&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008195655554.png&quot; alt=&quot;image-20241008195655554&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008195728422.png&quot; alt=&quot;image-20241008195728422&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可能遇到的问题&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最亮处有暗点：把渐变纹理的 Wrap Mode 从Repeat 改为 Clamp 模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是，我们需要把渐变纹理的 Wrap Mode 设为 Clamp 模式，以防止对纹理进行采样时由于浮点数精度而造成的问题。图 7.19给出了 Wrap Mode分别为 Repeat和 Clamp模式的效果对比。 可以看出，左图(使用 Repeat 模式)中在高光区域有一些黑点。这是由浮点精度造成的，当我们使用 fixed2(halfLamberthalfLambert)对渐变纹理进行采样时，虽然理论上 halfLambert 的值在【0，1】之间，但可能会有 1.000 01 这样的值出现。如果我们使用的是 Repeat 模式，此时就会舍弃整数部分，只保留小数部，得到的值就是 0.000 01，对应了渐变图中最左边的值，即黑色。因此，就会出现图中这样在高光区域反而有黑点的情况。我们只需要把渐变纹理的 Wrap Mode 设为 Clamp 模式就可以解决这种问题。&lt;/p&gt;
&lt;p&gt;——《Unity Shader 入门精要》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;描边有锯齿：卡渲一般标配抗锯齿后处理，暂时不用考虑。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;附带一些卡渲的&lt;strong&gt;不同风格&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008200709106.png&quot; alt=&quot;image-20241008200709106&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008200718689.png&quot; alt=&quot;image-20241008200718689&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008200722701.png&quot; alt=&quot;image-20241008200722701&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008200740445.png&quot; alt=&quot;image-20241008200740445&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008200743790.png&quot; alt=&quot;image-20241008200743790&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008200821427.png&quot; alt=&quot;image-20241008200821427&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008200834362.png&quot; alt=&quot;image-20241008200834362&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008200843577.png&quot; alt=&quot;image-20241008200843577&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol1/image-20241008200852196.png&quot; alt=&quot;image-20241008200852196&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>Onedrive 迁移方案记录</title><link>https://notes.kiramux.com/posts/002-onedrive-transfer</link><guid isPermaLink="true">https://notes.kiramux.com/posts/002-onedrive-transfer</guid><pubDate>Tue, 08 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文所提到的 onedrive 包含 onedrive 和 sharepoint 两种，它们本质上都是 sharepoint ，同时文中提到的 od 都是指它们。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;迁移 OneDrive 时应当考虑 api 限制问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;目前未知 clouddrive 是否会重试失败任务。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;本文只考虑向较为稳定的网盘迁移，国内网盘如百度、阿里、123盘不考虑。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;本次记录从 od E5 到 od E5/家庭版 、 pikpak 、 115 的迁移方案。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;秒传情况&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;秒传&lt;/strong&gt; 就是两个云盘之间的文件可以快速相互上传，不用等待。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;左侧&lt;/strong&gt;为来源网盘、 &lt;strong&gt;顶部&lt;/strong&gt;为目标网盘&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;onedrive/sharepoint&lt;/th&gt;
&lt;th&gt;pikpak&lt;/th&gt;
&lt;th&gt;115&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;onedrive/sharepoint&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;td&gt;√&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;onedrive/sharepoint &amp;gt;&amp;gt; onedrive/sharepoint&lt;/h2&gt;
&lt;p&gt;rclone 是最优解，其他的一概不推荐。&lt;/p&gt;
&lt;p&gt;一般来说直接用 rclone 复制即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rclone copy &quot;backup01:/汉化会社合集部分 part8/&quot; &quot;s1:/汉化会社合集部分 part8/&quot;  -P
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;rclone copy &quot;backup01:/汉化会社合集部分 part8/&quot; &quot;s1:/汉化会社合集部分 part8/&quot;  -P --onedrive-chunk-size 100M
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加上 &lt;code&gt;-P&lt;/code&gt; 可以持续输出日志。如果有报错，可以再运行下面这行，输出错误日志，看看哪些文件出错了。一般是文件报毒，没加密压缩上传导致的。这种情况在网页版 od 仔细翻翻就可以取回了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rclone copy &quot;backup01:/汉化会社合集部分 part5/&quot; &quot;s0:/汉化会社合集部分 part5/&quot;  --log-level ERROR --log-file hh5.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想同步（删掉目标文件夹的一些文件实现同步），可以用 sync 命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rclone sync -v R2:/img sayafx02:/img-backup  -P
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外再记录一种情况，想要批量复制文件夹到另一个盘中。&lt;/p&gt;
&lt;p&gt;例如最近我要把 E5  od 的 2T 文件夹分开迁移到家庭版 2 个 1t 的 od 中。如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;E5/Folder1 300g
E5/Folder2 300g
E5/Folder3 300g
E5/Folder4 300g
E5/Folder5 300g
E5/Folder6 300g
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Personal-1/Folder1 300g
Personal-1/Folder2 300g
Personal-1/Folder3 300g

Personal-2/Folder4 300g
Personal-2/Folder5 300g
Personal-2/Folder6 300g
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于我没找到 rclone 有类似的命令，等上一条复制完再添加下一个，有些麻烦而且说实话有点蠢。貌似有把需要复制的文件放在一个文本文件里再读取复制的方法，但由于家庭版的 od 号实在是太多了，这么做也很麻烦。&lt;/p&gt;
&lt;p&gt;于是我用了 1panel 的计划任务，如果不设置 &lt;code&gt;--onedrive-chunk-size&lt;/code&gt; 大约是 4h 搬运完一个文件夹（100g上下）。设置好命令和时间，剩下的只有等待了。&lt;/p&gt;
&lt;p&gt;由于个人版 od 不能申请自己的 api 了，所以会慢一些。另外如果调高 &lt;code&gt;--onedrive-chunk-size&lt;/code&gt; 的值，内存消耗会急剧增加，省事省力还是使用默认的吧，正好内存占用小，还可以多运行几条命令多线程搬运。&lt;/p&gt;
&lt;p&gt;最后把运行成功的计划任务 停用 或者删除就OK了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/od-transfer/image-20241008141544577.png&quot; alt=&quot;image-20241008141544577&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;onedrive &amp;gt;&amp;gt; pikpak&lt;/h2&gt;
&lt;h3&gt;1T 以内&lt;/h3&gt;
&lt;p&gt;把 onedrive 和 pikpak 都挂载到 Alist ，然后用 Alist 复制。（现在的会员每个账号每月只有1t的上传流量，用工具复制算在上传流量里面）&lt;/p&gt;
&lt;p&gt;用 Alist 从 od 到 pikpak 是可以秒传的。&lt;/p&gt;
&lt;h3&gt;超过 1T&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方案一&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以分批用 rclone 向多个会员账号迁移，最后汇总到一个号上。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方案二&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把 onedrive 挂载到 onemanager 或 Alist ，然后用直链在 pikpak 云下载。&lt;/p&gt;
&lt;p&gt;推荐用 cmd 或其他神秘方法生成 onedrive 的目录列表，然后加上域名和路径，拼接出直链批量云下载。&lt;/p&gt;
&lt;p&gt;云下载时要注意 onedrive 的 api 限制，具体没测试，大概 1h 50个文件，不然等到碰到 onedrive 限制了，pikpak 传输列表会一堆报错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PS&lt;/strong&gt;: 目前 pikpak 挂载到 Alist 需要获取 刷新令牌、验证码和设备id，并且时不时会掉登录，挂载方式不是很稳定，可能下次官方再整次活，挂载方式又变了。&lt;/p&gt;
&lt;h2&gt;onedrive &amp;gt;&amp;gt; 115&lt;/h2&gt;
&lt;h3&gt;探索&lt;/h3&gt;
&lt;p&gt;115 的登录 Cookie 可以用 &lt;a href=&quot;https://alist.nn.ci/zh/guide/drivers/115.html&quot;&gt;Alist&lt;/a&gt; 官方文档提供的方法，我用的是 &lt;a href=&quot;https://alist.nn.ci/zh/guide/drivers/115.html#_3-%E4%BD%BF%E7%94%A8python%E8%84%9A%E6%9C%AC%E8%8E%B7%E5%8F%96-cookie&quot;&gt;使用Python脚本获取 Cookie&lt;/a&gt; 。&lt;/p&gt;
&lt;p&gt;登录时使用 微信/支付宝 小程序的 Cookie，日常很少会用到且较为稳定。如果用 Android 或者 IOS 的 Cookie 挂载，当你用手机登录时，Alist 那边对应的登录会掉。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;网上搜到的那个支持 115 的改版 rclone 不能用 copy 命令。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;直接用 Alist 迁移会在本地留下和文件同大小的缓存文件，迁移大文件不合适，会直接把 vps 的硬盘撑爆。不过支持秒传这点很好，迁移起来很快。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用 Alist 挂载 onedrive 和 115 ，然后把 Alist 用 WebDAV 挂载到 rclone ，用 rclone 的 copy 命令貌似无法写入 115 。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;实际使用方案&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.clouddrive2.com/&quot;&gt;clouddrive2&lt;/a&gt; 可以使用，还算稳定，没有缓存文件，内存占用小&lt;/p&gt;
&lt;p&gt;本来想把 Alist 用 WebDAV 添加进去，就不用去一个个添加 onedrive 了。没想到只能读到文件列表，迁移时发现疑似无法从 WebDAV 下载。&lt;/p&gt;
&lt;p&gt;clouddrive 需要购买会员，由于我只是偶尔用一下，开了 5￥ 的月度会员，价格不算贵。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;就在我已经迁移了 5t 的时候，用 115 的 cookie 登录挂载上传的方法又挂逼了，疑似偶尔才可以用。无法秒传的文件的迁移计划基本寄掉了。并且听说 115 高峰期时限速很严重，所以暂时不打算向 115 备份了，目前迁移了 5t 之后就再没动过了。&lt;/p&gt;
&lt;h2&gt;本文用到的工具&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://alist.nn.ci/zh/&quot;&gt;Alist&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/qkqpttgf/OneManager-php&quot;&gt;Onemanager&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://rclone.org/&quot;&gt;Rclone&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.clouddrive2.com/&quot;&gt;clouddrive2&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>Kiramux</author></item><item><title>Unity Shader 入门笔记vol2--符文·筑基·照</title><link>https://notes.kiramux.com/posts/004-technical-artist-learning-vol2</link><guid isPermaLink="true">https://notes.kiramux.com/posts/004-technical-artist-learning-vol2</guid><pubDate>Tue, 08 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;解惑&lt;/h2&gt;
&lt;p&gt;我现在学习的&lt;a href=&quot;https://www.bilibili.com/video/BV1BE411N74b/&quot;&gt;这门课程&lt;/a&gt;是在 2020 年开始录制的，使用的是 Unity 2019，有些年代久远了。课程作者是一位资深 TA ，他也在 B 站个人简介标出：此课程已过赏味期 。&lt;/p&gt;
&lt;p&gt;在4年后的今天，Shader Forge 的版本也定格在了 2019 ，有更多方便好用的工具可做为连连看替代。我在听过一点课之后，某次和朋友交流时，不经意间看到他在用另一款既美观又看起来很吊的插件（忘记名字了），当时有深深地怀疑过，自己在 2024 年去听这样一门课疑似”过时“的课是否值得。&lt;/p&gt;
&lt;p&gt;技术美术最终是要落实到 Shader 代码的。当我接触到这门课程的代码部分时，之前的疑问全都烟消云散了。&lt;/p&gt;
&lt;p&gt;以 Shader Forge 作为 Unity Shader 的开始仍是有意义的。Shader Forge 程序化生成的代码很简洁，可以很容易地删减出 Unity Shader 版的 HelloWorld ，辅助思路实现的同时也很好拿来做写代码的参考。Shader 代码的部分是相通的，会写 Shader 之后，我们可以把 SD 、 SP 里的 Shader 拿出来参考，把它们移植到 Unity 里或者别的地方，也可以比葫芦画瓢把写好的 Unity Shader 移植到其他地方。&lt;/p&gt;
&lt;p&gt;连连看终究是一个辅助，不是最终答案，最终还是要丢掉的。目前 Unity 和 UE 有连连看，然而到一些陌生的平台，有没有这样的连连看工具还难说，也不会有人为了你想向这个平台移植 Shader ，专门为你开发一款工具。学会 Shader 是一个学会 “渔” 的过程。&lt;/p&gt;
&lt;p&gt;综上， Shader Forge 只是一个工具，很适合拿来入门，等到熟练之后要脱离它。很多情况下写代码比连连看更容易实现效果，现在因为纠结工具问题而犹犹豫豫就输了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PS&lt;/strong&gt;：不同的 Unity 版本使用起来还是有区别的，比如某个变量在 2019 版里还叫这个名字，有可能之后某个版本就改了。同时有一些旧版本官方文档推荐的做法，在之后的新版本就不推荐了。还是要多查官方文档，这也是一名技美必备的基本素养。&lt;/p&gt;
&lt;h2&gt;HelloWorld （ FlatCol ）&lt;/h2&gt;
&lt;p&gt;我们在 Shader Forge 中新建一个 Unlit Shader ，自带一个颜色连到了 Emission 上，这就是一个最简单的 Shader 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol2/image-20241009100254724.png&quot; alt=&quot;image-20241009100254724&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个颜色我们可以任意更改，比如我们可以把它改成绿色。效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol2/image-20241009110821958.png&quot; alt=&quot;image-20241009110821958&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol2/image-20241009103255422.png&quot; alt=&quot;image-20241009103255422&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们用 VSCode 打开这个 Shader ，删除掉 Shader Forge 自带的内容，最大程度上地精简它，得到一个 Unity Shader 版的 Hello World —— FlatCol （ Flat Color ，最简的 Shader ）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Shader &quot;VS/FlatCol&quot; {
    Properties {
    }
    SubShader {
        Tags {
            &quot;RenderType&quot;=&quot;Opaque&quot;
        }
        Pass {
            Name &quot;FORWARD&quot;
            Tags {
                &quot;LightMode&quot;=&quot;ForwardBase&quot;
            }


            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include &quot;UnityCG.cginc&quot;
            #pragma multi_compile_fwdbase_fullshadows
            #pragma target 3.0
            // 输入结构
            struct VertexInput {
                float4 vertex : POSITION;   // 将模型的顶点信息输入进来
            };
            // 输出结构
            struct VertexOutput {
                float4 pos : SV_POSITION;   // 由模型顶点信息换算而来的顶点屏幕位置
            };
            // 输入结构&amp;gt;&amp;gt;&amp;gt;顶点Shader&amp;gt;&amp;gt;&amp;gt;输出结构
            VertexOutput vert (VertexInput v) {
                VertexOutput o = (VertexOutput)0;           // 新建一个输出结构
                o.pos = UnityObjectToClipPos( v.vertex );   // 变换顶点信息 并将其塞给输出结构
                return o;                                   // 将输出结构 输出
            }
            // 输出结构&amp;gt;&amp;gt;&amp;gt;像素
            float4 frag(VertexOutput i) : COLOR {
                return float4(0.0, 1.0, 0.0, 1.0);
            }
            ENDCG
        }
    }
    FallBack &quot;Diffuse&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;我们目前需要关注的只有以下这些部分，其他部分可以当做“无用”的形式段。知道含义自然更好，不知道也无所谓，就把它当做一种仪式感。可以把它们当做发动魔术之前吟唱的咒文。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 材质面板上暴露出的参数
   Properties {
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// CGPROGRAM 前面的部分，目前是空白，这个位置用来为材质面板上的参数声明变量	
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
// #include 用来引用库文件
            #include &quot;UnityCG.cginc&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 输入结构
            struct VertexInput {
                float4 vertex : POSITION;   // 将模型的顶点信息输入进来
            };
// 输出结构
            struct VertexOutput {
                float4 pos : SV_POSITION;   // 由模型顶点信息换算而来的顶点屏幕位置
            };
// 输入结构&amp;gt;&amp;gt;&amp;gt;顶点Shader&amp;gt;&amp;gt;&amp;gt;输出结构
            VertexOutput vert (VertexInput v) {
                VertexOutput o = (VertexOutput)0;           // 新建一个输出结构
                o.pos = UnityObjectToClipPos( v.vertex );   // 变换顶点信息 并将其塞给输出结构
                return o;                                   // 将输出结构 输出
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以及最重要的也是我们主要写的部分——&lt;strong&gt;像素 Shader&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 输出结构&amp;gt;&amp;gt;&amp;gt;像素
            float4 frag(VertexOutput i) : COLOR {
                return float4(0.0, 1.0, 0.0, 1.0);
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;需要注意的地方&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个 Shader 都应该有一个独一无二的路径名，位置在开头的 &lt;code&gt;Shader &quot;VS/FlatCol&quot;&lt;/code&gt; ，引号内的就是路径名。Shader Forge 中的路径在左栏的 Shader Settings &amp;gt;&amp;gt; Path 。&lt;/li&gt;
&lt;li&gt;只有从 &lt;code&gt;CGPROGRAM&lt;/code&gt; 到 &lt;code&gt;ENDCG&lt;/code&gt; 之间的内容，每行末尾需要加 &lt;code&gt;;&lt;/code&gt; ，其余的地方是不用加分号的，相当于是在 Shader 代码中插入了一段 cg 代码。&lt;/li&gt;
&lt;li&gt;注意&lt;strong&gt;半角符号&lt;/strong&gt;！&lt;/li&gt;
&lt;li&gt;最后 return 的值必须是一个四维的（有时会自动转换）&lt;/li&gt;
&lt;li&gt;在写 Shader 时数字最好保留一位小数，如 0.0 ， 1.0 这种，虽说引擎可能自动转换，但是在某些情况下转换又会很邪门地出问题，尤其在一些老设备上，所以最好养成这个习惯。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打开 Unity 的 Console&lt;/strong&gt; 放在窗口下部，这样每次 Shader 报错出现 bug 红 的时候，可以看到报错信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当我们精简好之后，我们用 FlatCol 创建一个材质附给一个模型，不出意外的话，会和 Shadow Forge 中连出来的效果一样。如果有些地方不慎写错，会出现以下的 &lt;strong&gt;bug 红&lt;/strong&gt; 和 Console 报错：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol2/image-20241009103048045.png&quot; alt=&quot;image-20241009103048045&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;代码 Lambert&lt;/h2&gt;
&lt;p&gt;有了 Hello World （ FlatCol ），我们已经可以输出一个纯色了&lt;/p&gt;
&lt;p&gt;我们离写出人生中写的第一个 Shader 只有一步之遥了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol2/image-20241009103514468.png&quot; alt=&quot;image-20241009103514468&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们先观察在 Shader Forge 连出来的 Lambert 节点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol2/image-20241009104150694.png&quot; alt=&quot;image-20241009104150694&quot; /&gt;&lt;/p&gt;
&lt;p&gt;FlatCol 中已经有了顶点信息。&lt;/p&gt;
&lt;p&gt;我们在 Lambert 又用到了 &lt;code&gt;法线信息&lt;/code&gt; ，因此在 &lt;code&gt;输入结构&lt;/code&gt; 、 &lt;code&gt;输出结构&lt;/code&gt; 、以及 &lt;code&gt;输入结构 &amp;gt;&amp;gt;顶点 Shader &amp;gt;&amp;gt; 输出结构&lt;/code&gt; 这三个部分内追加法线信息。&lt;/p&gt;
&lt;p&gt;看不懂是正常现象，直接从 Shader Forge 那边先复制过来，之后用的多了慢慢就会懂了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;            // 输入结构
            struct VertexInput {
                float4 vertex : POSITION;   // 输入模型的顶点信息
                float3 normal : NORMAL;     // 输入模型的法线信息
            };

            // 输出结构
            struct VertexOutput {
                float4 pos : SV_POSITION;       // 由模型顶点信息换算而来的顶点屏幕位置
                float3 nDirWS : TEXCOORD0;      // 由模型法线信息换算而来的世界空间法线信息
            };
            // 输入结构&amp;gt;&amp;gt;&amp;gt;顶点Shader&amp;gt;&amp;gt;&amp;gt;输出结构
            VertexOutput vert (VertexInput v) {
                VertexOutput o = (VertexOutput)0;                   //新建一个输出结构
                o.pos = UnityObjectToClipPos( v.vertex );           //变换顶点信息，并将它塞给数据结构
                o.nDirWS = UnityObjectToWorldNormal( v.normal );    //变换法线信息，并将它塞给数据结构
                return o;                                           //输出数据结构
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们准备向量 nDIr 和 lDir ，可以先不用理解，依旧直接从 Shader Forge 连好的代码里粘贴过来。&lt;/p&gt;
&lt;p&gt;由于  nDIr 和 lDir 都是三维的，我们用 float3 。&lt;/p&gt;
&lt;p&gt;点积的结果是一个一维的量，用 float ，然后截断负值。&lt;/p&gt;
&lt;p&gt;我们把计算结果补充成 float4 （ RGBA ），最后输出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;            // 输出结构&amp;gt;&amp;gt;&amp;gt;像素Shader
            float4 frag(VertexOutput i) : COLOR {
            
                float3 nDir = i.nDirWS;                             //获取nDir
                float3 lDir = _WorldSpaceLightPos0.xyz;             //获取lDir
                
                float nDotl = dot(nDir,lDir);                       //nDir点积lDir
                
                float Lambert = max(0.0,nDotl);                     //截断负值
                
                return float4(Lambert,Lambert,Lambert,1.0);         //最终输出颜色，只能是float4，float等也可以只不过会自行转换
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol2/image-20241009114909119.png&quot; alt=&quot;image-20241009114909119&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;展开查看完整代码&amp;lt;/summary&amp;gt;
&amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;
Shader &quot;VS/Lambert&quot;
{
Properties {
}
SubShader {
Tags {
&quot;RenderType&quot;=&quot;Opaque&quot;
}
Pass {
Name &quot;FORWARD&quot;
Tags {
&quot;LightMode&quot;=&quot;ForwardBase&quot;
}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include &quot;UnityCG.cginc&quot;
        #pragma multi_compile_fwdbase_fullshadows
        #pragma target 3.0

        // 输入结构
        struct VertexInput {
            float4 vertex : POSITION;   // 输入模型的顶点信息
            float3 normal : NORMAL;     // 输入模型的法线信息
        };

        // 输出结构
        struct VertexOutput {
            float4 pos : SV_POSITION;       // 由模型顶点信息换算而来的顶点屏幕位置
            float3 nDirWS : TEXCOORD0;      // 由模型法线信息换算而来的世界空间法线信息
        };
        // 输入结构&amp;gt;&amp;gt;&amp;gt;顶点Shader&amp;gt;&amp;gt;&amp;gt;输出结构
        VertexOutput vert (VertexInput v) {
            VertexOutput o = (VertexOutput)0;                   //新建一个输出结构
            o.pos = UnityObjectToClipPos( v.vertex );           //变换顶点信息，并将它塞给数据结构
            o.nDirWS = UnityObjectToWorldNormal( v.normal );    //变换法线信息，并将它塞给数据结构
            return o;                                           //输出数据结构
        }
        // 输出结构&amp;gt;&amp;gt;&amp;gt;像素Shader
        float4 frag(VertexOutput i) : COLOR {                   //只能是float4或half4
            float3 nDir = i.nDirWS;                             //获取nDir
            float3 lDir = _WorldSpaceLightPos0.xyz;             //获取lDir
            float nDotl = dot(nDir,lDir);                       //nDir点积lDir
            float Lambert = max(0.0,nDotl);                     //截断负值
            return float4(Lambert,Lambert,Lambert,1.0);         //最终输出颜色，只能是float4，float等也可以只不过会自行转换
        }
        ENDCG
    }
}
FallBack &quot;Diffuse&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}
&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol2/image-20241009105733738.png&quot; alt=&quot;image-20241009105733738&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;代码 HalfLambert&lt;/h2&gt;
&lt;p&gt;我们只需要在 像素Shader 部分，对 Lambert 的结果做一下处理即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;            // 输出结构&amp;gt;&amp;gt;&amp;gt;像素Shader
            float4 frag(VertexOutput i) : COLOR {                                    //只能是float4或half4
                float3 nDir = i.nDirWS;                                              //获取nDir
                float3 lDir = _WorldSpaceLightPos0.xyz;                              //获取lDir
                float nDotl = dot( nDir, lDir );                                     //nDir点积lDir
                float HalfLambert = max( 0.0, nDotl * 0.5 + 0.5 );                   //转换为半兰伯特并截断负值
                return float4( HalfLambert, HalfLambert, HalfLambert, 1.0 );         //最终输出颜色，只能是float4，float等也可以只不过会自行转换
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;委托任务&lt;/h2&gt;
&lt;p&gt;如 Gif 图创建一个材质，面板上提供一个 Slider 可以平滑调整猴子的明暗调子；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/technical-artist-learning-vol2/20241009112922.gif&quot; alt=&quot;20241009112922 &quot; /&gt;&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>浑浑噩噩的每一天</title><link>https://notes.kiramux.com/posts/diary-wonderful-everyday</link><guid isPermaLink="true">https://notes.kiramux.com/posts/diary-wonderful-everyday</guid><pubDate>Fri, 25 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这些文章属于较为私人的个人日记，拿来随手记录的。俺的生活终于要回归正轨了。（一个月限定）&lt;/p&gt;
&lt;p&gt;这篇文章是昨天创建的，本来昨天下午创建好，想趁着有难得的空闲写一篇 blog 记录的，走在路上连名字都想好了，英文明明就叫做 “ wonderful everyday ” 。然而到了专业教室，旁边坐着的两位仁兄看上去要准备连麦打劫，我顿感身心俱疲，收拾电脑光速滚回宿舍了。&lt;/p&gt;
&lt;p&gt;这篇文章我是随心写的，想到什么就记下什么。标题叫做浑浑噩噩每一天。实际上我已经被课设折磨了连续 10 天了，实际上应该更久，但由于课设题目简单 + 老师很亚撒西 + 我自己刻意逃避，我自己作死把 9 周的课设压缩到 10 天来做。其实这次的课设真的很简单，城市园林景观设计，说白了随便去知末上搜一下案例，直接贴过来就好了。然而俺心里总有个倔强的想法，既然是个课设俺就要做做试试，做出自己的东西（当然也有老师引导的成分，那位老师确实是位不错的老师）。于是就开始做课设，但又不会给它分配太多时间，因为知道自己志不在此，想在随便做做的同时顺便做出点自己的东西来，就是这样任性、倔强的想法。再加上有休假，所以趁机整理一下网站，迁移一下文件，然后画点画顺便做做课设，这期间还萌生了好多别的想法，例如学一下 py 做 tg 频道文件补档之类的，于是国庆七天就过去了。刚好国庆过去交了二草，然后家里表哥结婚，于是回家 4 天，顺便把要交草模的那节课给请假了（实际上是俺故意逃的，只不过有假条，事实证明那个东西只是纯粹浪费时间）。等休假回来，发现由于学校要被评估，设计周被砍了，交图也提前了，然而俺的设计甚至连草图还没画好。然而俺还怀着之前的那种想法彻底藐视这个设计，景观设计又没啥规范限制，随便做都可以，只要自己说的通，圆的过来就没人会说咱的设计有问题。事实上确实很简单，当俺每次上课都不给老师看图的时候，看到其他的同学已经渲染都渲完了开始排版了，俺的草图还没落实，此时我的战略腚力依旧很强，在上次二草俺的方案已经基本确定了，就差那些水滴的形状的具体处理了，自己来看进度还是可以的，于是依旧不慌，而且已经有了初步的想法了。就在俺这么慢悠悠地做的时候，时间来到了上周六（20241019），俺正在和 Galgamer 的群友玩世界第一好玩的你画我猜。正在画着的时候，后面那个哥们王者打完了（没错就是昨天打劫的那个，每次吵得要死），然后看到俺在用数位板画画，然后就开始当爹了。“这是手绘板吗？”等下又直接问“现在在画画，想必你的设计进度肯定做的很快吧。”&lt;/p&gt;
&lt;p&gt;我当时还毫不在意，另一个哥们在说“有个爱好挺好的”所以也没放在心上，然而这句话已经印在心里了。当时咱的计划是周二把方案和模型出完，然而周六白天几乎什么都没干，出去和室友吃了大锅台然后睡了一下午，晚上回去随便干点啥，晚上又和群友玩画猜了。其实还有周日也还好，毕竟还有一天。然而不知道怎么回事，具体情况也给忘了，结果就是周日俺睡了一天，然后越是迫近周日的晚上，我就越寝食难安。当我每次想到“这会儿在干这个，想必你进度一定很快吧”这句话的时候，我就会被干碎一次。这种心态已经完全做不成设计了，玩游戏睡觉都要没心思了，时间又那么紧迫（在当时的心理下看来是这样，毕竟自己定的 ddl 是周二）。周一也有一天课，我在周六周日什么课都没的情况下进度几乎为 0，难道指望在满课的周一将设计进度推进突飞猛进吗。&lt;/p&gt;
&lt;p&gt;没错，这时候俺破防了。想了想好想也没什么人能安慰自己，但还是试着给她发了发信息，毕竟之前她也老对着我发电来着，俺对她稍微撒撒娇怎么了。。于是给她发完消息之后，先给我妈通了一通视频电话，完全没提自己心里的堵，就是日常聊天，聊到表哥要去新婚旅行了，然后家里想买台电车日常开之类的。。当妈妈在揉眼睛的时候，俺看到她的眼袋和眼角有深深地皱纹，俺的眼泪立马就要涌出来了。但妈妈就是很普通地跟我在聊天，我也不想就这么哭出来，还好忍住了，然后那位打过来了微信电话，只不过我这边仍在占线中。&lt;/p&gt;
&lt;p&gt;过了 10 分钟，和她接通了。一开始谁都没说话，持续了好几分钟，也忘了是谁先开的口，于是就聊了起来。她帮我分析问题，然后安慰我。虽然她说的道理我都知道，而且大家的问题和焦虑其实都很相似，大家都心里很难受，自己有啥理由让别人安慰我呢，她大概也在面临同样的压力同样的问题，或许比我还严重，然后还要她克制住自己的痛苦然后再来安慰咱。实际上更现实的问题是，她或许也没有能解决这个问题的思路，或者说比我更焦虑，然后说着一些咱都懂的话，如果不是完全站在某个人的立场下，只是说“你这样那样不就好了吗”，这些都是不负责任的话。&lt;/p&gt;
&lt;p&gt;其实周日那天，我重读了《挪威的森林》第一章，里面渡边君对直子说：“直子你就是神经崩的太紧，日常应当放松一些。”原话有些记不清了，但大致意思是这样。直子回答的大意是，你无法站在我的立场上，也就无法理解我的痛苦，现在我的自己只要稍微放松一丝，我的精神就会分崩离析，然后你却让我日常放松一些，不就好了。说出这种话是十分不负责任的。&lt;/p&gt;
&lt;p&gt;不过我还是和她有一搭没一搭地聊了很久，然后她问了我地址，说要给我买东西吃。和她聊了聊自己感觉好多了，焦虑也少了好多。过了几天我明白了，其实和她聊天的各种内容都无所谓，只要自己能感觉到，有人还在关心着自己，然后愿意来安慰自己，这件事本身，就是对我最大的抚慰了，是俺在焦虑万分时的灵丹妙药。&lt;/p&gt;
&lt;p&gt;以上都是这周周一之前的故事了。周日那晚和她通完电话已经很晚了，于是就休息了。周一上课。在周二周三周四，我拼命赶设计进度，然后感觉自己果然是 deadline 领域大神，周二周三的进度推动依旧是龟速，然后在周四肝一整天，顺带肝到凌晨 4 点，居然把图出完了。然后是上午做 PPT ，等待答辩，下午上课--补觉--然后上绘画的构成课。可能是因为自己这次真的太藐视这次课设了，或者说自己已经不怎么把重心放在建筑学上了，这次完成课设之后也没有那种一释重负的感觉，只是想到&lt;strong&gt;俺的生活终于要回归正轨了&lt;/strong&gt;。毕竟之后还有一大堆事留着没做，开了太多坑了，需要自己慢慢填，而且前几天在高压状态下，我又萌生出了想自制周边（甚至去学 zb 雕手办）的危险想法。总之我仍是范围巨广的三分钟杂食动物。&lt;/p&gt;
&lt;p&gt;接下来打算先把 技术美术 稍微放一放，专心学 2d 绘画，然后辅以学 python ，看看能不能写一个 tgbot 出来，然后刚好技术美术学到一个节点（虽然都快 20 多天没碰了），打算写 blog 总结以下之前学到的东西，算复习让自己回想一遍不至于忘掉。大致上就是这些安排，之间穿插着各种学校的课程和作业什么的，偶尔看看能不能偷点时间玩玩游戏打打 gal 什么的。今天大概就写到这里了。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;其实那晚还有一位女生来安慰我，只不过被我略过没写了。附上那天去吃的大锅台&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/diary-wonderful-everyday/food.jpg&quot; alt=&quot;大锅台&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后她给俺买的糖果和鸡爪。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/diary-wonderful-everyday/feed.jpg&quot; alt=&quot;她的投喂&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>初见德加的舞女</title><link>https://notes.kiramux.com/posts/005-first-sight-of-the-dancing-girl-painted-by-degas</link><guid isPermaLink="true">https://notes.kiramux.com/posts/005-first-sight-of-the-dancing-girl-painted-by-degas</guid><pubDate>Sun, 10 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;../images/005/00130.jpg&quot; alt=&quot;《芭蕾舞团在巴黎歌剧院》 Ballet at the Paris Opéra 1877年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我在昨天读过 《西方绘画大师经典佳作 德加（高清细节版）》 后，萌生了起草这篇 blog 的想法。然而读过之后我已经很累了，于是零零碎碎记录下了当时看每一幅画时的第一想法，打算之后再记录。然而昨晚和今天想要写点什么的时候，发觉当时那些画给我留下的感受，和我之前想写的细节内容，在我脑中已经荡然无存了。这篇文章就是我事后凭借印象写出大致感想，再拼贴当时的记录而成的，所以可能会有文章上下文思路跳跃过大，或出现破碎言语片段的情况。此篇文章&lt;strong&gt;仍待日后再整理&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;读过这篇文章，你就相当于读过 《西方绘画大师经典佳作 德加（高清细节版）》 的 50%之多了。因为本文贴了大量这本书中的画作。&lt;/p&gt;
&lt;p&gt;本文左右的图名都是针对&lt;strong&gt;图名上方的画作&lt;/strong&gt;的标注。&lt;/p&gt;
&lt;p&gt;另外需要注意的是，尽管本文贴出的 德加 的画作几乎都是人物油画，但 德加 的画作远不止 人物油画 这一个种类，他还很喜欢风景油画和人物素描等。本文算是对看过 德加笔下的舞女 的感想总结。&lt;/p&gt;
&lt;p&gt;本人只是一个非科班出身热爱美术的普通人，请理性看待本文内容。&lt;/p&gt;
&lt;p&gt;zlib 地址: &lt;a href=&quot;https://z-lib.gd/book/11731581/c0b68a/&quot;&gt;《西方绘画大师经典佳作 德加（高清细节版）》&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;初见&lt;/h2&gt;
&lt;p&gt;昨天在 &lt;a href=&quot;https://z-lib.gd/&quot;&gt;zlib&lt;/a&gt; 翻找书的时候，偶然发现了一本带着芭蕾舞者画的封面的书，瞬间被封面上的舞裙吸引了。我很喜欢芭蕾舞者（实际上更喜欢舞裙），于是打开阅读。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/image-20241110104851771.png&quot; alt=&quot;image-20241110104851771&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://z-lib.gd/book/11731581/c0b68a/&quot;&gt;《西方绘画大师经典佳作 德加（高清细节版）》&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这本书一开始从 德加 的生平讲起，接着是沿着德加的人生历程展示画作。&lt;/p&gt;
&lt;p&gt;在 Chapter 1 德加生平概述 和 Chapter 2 特立独行的青年时期（1857年-1861年） 两个章节中，展示的画作绝大多数是自画像、肖像画或者说风景画，只有偶然的一两张画舞女。于是我在想，这本书大概也是因为这位叫做 德加 的画家，他画的几张舞女十分出名，然而数量很少，还是在画什么人物和风景画居多，只是因为画的舞女很出名，于是作为这本书的封面了。&lt;/p&gt;
&lt;p&gt;我就带着这样的想法接着看下去。不得不赞赏这些风景画的构图真的很优美很和谐，最近我也在学画面构成相关的内容，所以深有体会，看这些画的时候就在一直想，或许把这些人物换成二次元美少女，也会是很不错的画面。或者说我可以直接从这些经典作品中提取那些动态线，然后 copy 到我的作品中来用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00362.jpg&quot; alt=&quot;自画像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《自画像》 Self-Portrait 1856 年—1858 年 布面油画&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00367.jpg&quot; alt=&quot;《意大利老女人》 The Old Italian Woman 1857年 布面油画 宽749mm 高610mm&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《意大利老女人》 The Old Italian Woman 1857年 布面油画 宽749mm 高610mm&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00386.jpg&quot; alt=&quot;《海边长廊》 Promenade by the Sea 1860 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《海边长廊》 Promenade by the Sea 1860 年 布面油画&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00083.jpg&quot; alt=&quot;《亚历山大和布塞弗勒斯》 Alexander and Bucephalus 1861年—1862年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《亚历山大和布塞弗勒斯》 Alexander and Bucephalus 1861年—1862年 布面油画&lt;/p&gt;
&lt;p&gt;Chapter 1 2 和 3 的前半部分我还在走马观花地浏览，很随意地看着想着，这张图不错，那张图没准可以在 xx 用到。不过当我注意到，即使在我看过上面的全幅作品后，再看到一些画作的细节放大图时仍是很陌生，甚至根本没有印象，就好像是一副全新的画作。我这时才意识到我犯的错误。我欣赏这些画作的方式不对。&lt;/p&gt;
&lt;p&gt;我从来不懂怎么去看这些名家的画作，可能我是个俗人。无论是看照片，或是去美术馆看原作 / 复制品，自己看到的始终是“整幅画的一部分”。直到这时我才发现，这些细节部分，如果用画框框住画面，就已经是一副新的作品了。这本书的细节展示部分的图片边缘，正是一个个图框，把这部分细节和整个画面隔离开，才能使人更容易欣赏到它的细节之美。&lt;/p&gt;
&lt;p&gt;通常我自己在看这些画的时候，或许脑中或者眼中应当有一个无形的画框，或者干脆手持一个实体的画框，通过它来观察。否则，我的注意力大概只会被画作上的笔触、藏色吸引，而忽略构图等要素。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00088.jpg&quot; alt=&quot;《花瓶旁的女人》 A Woman Seated beside a Vase of Flowers 1865年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《花瓶旁的女人》 A Woman Seated beside a Vase of Flowers 1865年 布面油画&lt;/p&gt;
&lt;p&gt;例如这张画，我看待它时只会看到它的构图和大致的色彩，构图上中间有好大一簇花之类的，甚至都没注意到画面左下角有个透明瓶，也没注意到花球的边缘和女人衣服的背景是如何处理的。这些方面只有我看到编者特意在下方提供的画面细节时才注意到。原来我已经错过了这么多重要的信息。&lt;/p&gt;
&lt;p&gt;《花瓶旁的女人》局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00203.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00091.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00221.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;单独地看，每一处的细节裁切都是一副精妙绝伦的景物画，我甚至开始怀疑它们真的是从上面那张我认为“平平无奇”的画上裁切出来的吗？这真的不是另一幅画？然而当我返回再看时，事实每次都狠狠地打了自己的脸。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00320.jpg&quot; alt=&quot;《版画收藏者》 The Collector of Prints 1866年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《版画收藏者》 The Collector of Prints 1866年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00381.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00297.jpg&quot; alt=&quot;《菲奥克尔小姐在芭蕾剧〈泉〉中》 Mlle Fiocre in the Ballet - La Source 1867年—1868年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《菲奥克尔小姐在芭蕾剧〈泉〉中》 Mlle Fiocre in the Ballet - La Source 1867年—1868年 布面油画&lt;/p&gt;
&lt;p&gt;作品说明：&lt;/p&gt;
&lt;p&gt;受菲奥克尔之约创作的《菲奥克尔小姐在芭蕾剧〈泉〉中》， 他在前景中添画了一匹正在饮水的马。此画是德加创作的一个转折点，标志着他在创作中反映当代社会的日常生活。&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00305.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我意识到这些画的内容不可小觑，即使从我这个俗人眼中看来是“平平无奇”的画，也值得细细揣摩。于是接着读 Chapter 3，已经渐渐出现许多有关 芭蕾剧 内容的画作了。&lt;/p&gt;
&lt;p&gt;写到这里我忽然在想，既然要出画作系列的话，那么是否可以用 德加 的画当作参考，然后把人物替换为日系二次元或者其他形式，创作出一系列插画。这样就保证了灵感足够多、构图流畅和谐、不缺乏故事性，同时还有戏谑的意味，也是对经典作品的一种”致敬“。会有些像那些国画山水画风的 “猫和老鼠图”的感觉。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00079.jpg&quot; alt=&quot;《沙滩上的船》 The Ship on the Beach 1869 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这张画让我想到了 island 里的那只小船。&lt;/p&gt;
&lt;h2&gt;舞女&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00292.jpg&quot; alt=&quot;《舞蹈课》 The Dancing Class 1870 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《舞蹈课》 The Dancing Class 1870 年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00025.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00062.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00101.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;[PS: 被这张镜中舞女的表现再次惊艳到了]&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00152.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从这幅画开始，描绘舞女的作品数量极度增多，使我对这部画集的认知再次改观。&lt;/p&gt;
&lt;p&gt;以这幅画为分界线，之前的部分虽然让我认识到了 德加 的厉害之处，同时端正了自己欣赏画作的态度，但是我一直在寻觅那些自己想看到的东西。我想看到那些身姿婀娜、翩翩起舞的舞女，我想看到蓬松绽放的芭蕾裙。&lt;/p&gt;
&lt;p&gt;于是在这幅画之后的篇幅，大量的舞女画作呈现出来，目不暇接。&lt;/p&gt;
&lt;p&gt;我从书中摘录了一些放在这里，请欣赏这些动人的画作。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00351.jpg&quot; alt=&quot;《歌剧院的乐队席》 The Orchestra at the Opera 1870年 巴黎奥赛博物馆 布面油画 宽 565mm 高462mm&quot; /&gt;&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00003.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;折叠了一些男性音乐家的细节展示&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00032.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00122.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00123.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00124.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00064.jpg&quot; alt=&quot;《舞蹈课》 Dance Classes 1871年—1874年 奥赛美术馆 法国 巴黎 布面油画 宽850mm 高750mm&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《舞蹈课》 Dance Classes 1871年—1874年 奥赛美术馆 法国 巴黎 布面油画 宽850mm 高750mm&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00207.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00155.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00093.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00119.jpg&quot; alt=&quot;《在舞台上排练》 The Rehearsal on the Stage 1874年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《在舞台上排练》 The Rehearsal on the Stage 1874年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00045.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00082.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00129.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00170.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00125.jpg&quot; alt=&quot;《芭蕾舞台上的排练》 The Rehearsal of the Ballet Onstage 1874年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《芭蕾舞台上的排练》 The Rehearsal of the Ballet Onstage 1874年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00228.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00033.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00132.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00392.jpg&quot; alt=&quot;《芭蕾舞台上的排练》 The Ballet Rehearsal on Stage 1874年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《芭蕾舞台上的排练》 The Ballet Rehearsal on Stage 1874年 布面油画&lt;/p&gt;
&lt;p&gt;“人们称我是画舞女的画家，他们不知道，舞女之于我只是描绘美丽的纺织品和表现运动的媒介罢了。”&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00020.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00054.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00071.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;三张同名的画作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00119.jpg&quot; alt=&quot;《在舞台上排练》 The Rehearsal on the Stage 1874年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00125.jpg&quot; alt=&quot;《芭蕾舞台上的排练》 The Rehearsal of the Ballet Onstage 1874年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00392.jpg&quot; alt=&quot;《芭蕾舞台上的排练》 The Ballet Rehearsal on Stage 1874年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;三张同名的画作，相同的构图，几乎相同的人物和场景，却用了三种的表现方式。&lt;/p&gt;
&lt;p&gt;第一幅更偏写实一些，体现的是写实美。第二幅则是通过用色和场景形态传达出了一种疯狂、一种痴迷。第三幅更偏黑白的表现，此时指导的老师消失了，只剩后方一个男人，我猜想可能是想表达更客观的视点画面，或者是所谓的 &quot; Other view &quot; ，又或者是这些舞者内心中的芭蕾舞台的表现。&lt;/p&gt;
&lt;p&gt;其实我第一次看到这三幅画时有些吃惊，认为这些画可能是在同一空间上的不同时间的表达，现在看来又或者是同一场景的同一段时间内不同“视点”的表达。&lt;/p&gt;
&lt;p&gt;1 &amp;gt;&amp;gt; 所希望的画面，2 &amp;gt;&amp;gt; 不知是谁心中的疯狂、痴迷的体现，3 &amp;gt;&amp;gt; 舞者内心的体现 / 舞台的客观体现。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00307.jpg&quot; alt=&quot;《芭蕾舞课》 Ballet lessons 1874年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《芭蕾舞课》 Ballet lessons 1874年 布面油画&lt;/p&gt;
&lt;p&gt;这张的人物位置安排，也是我最想模仿的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00131.jpg&quot; alt=&quot;《练舞者》 Practice the Dancers 1876年—1878年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《练舞者》 Practice the Dancers 1876年—1878年 布面油画&lt;/p&gt;
&lt;p&gt;我被舞者身上的那一抹红深深吸引了。&lt;/p&gt;
&lt;p&gt;我很在意这幅画中的冷暖对比，衣服上的红色过于鲜艳，背景很灰暗很危险的绿灰色，然而画面却很和谐？应当如何把控？&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00330.jpg&quot; alt=&quot;《跳舞者》 Dancers 1876年—1878年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00340.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;[PS: 有一种锈蚀感]&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00355.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00327.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00395.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这幅画中的笔触已经体现出些许疯狂了。让我联想到 沙耶之歌 的男主的家，房间内刷满了油漆……&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00021.jpg&quot; alt=&quot;《两个舞女，半幅》 Two Dancers, Half-length 1876 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《两个舞女，半幅》 Two Dancers, Half-length 1876 年 布面油画&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00410.jpg&quot; alt=&quot;《杜乐丽花园里撑阳伞的女人》 In the tuileries garden umbrella woman 1877年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《杜乐丽花园里撑阳伞的女人》 In the tuileries garden umbrella woman 1877年 布面油画&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00094.jpg&quot; alt=&quot;《舞者在练习芭蕾》 Dancers Practicing at the Barre 1877年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《舞者在练习芭蕾》 Dancers Practicing at the Barre 1877年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00145.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00175.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;[PS:把签名融入画面中是一个不错的选择，既能补充画面，有些情况下人为去掉签名还会导致画面动态缺失，算是“硬编码”署名了]&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00196.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00248.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;《芭蕾舞团在巴黎歌剧院》&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00130.jpg&quot; alt=&quot;《芭蕾舞团在巴黎歌剧院》 Ballet at the Paris Opéra 1877年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《芭蕾舞团在巴黎歌剧院》 Ballet at the Paris Opéra 1877年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00341.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00134.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00028.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00138.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00140.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00156.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我很喜欢偏冷色的皮肤质感表达，无论是摄影还是绘画。我在高中二年级时在教室后黑板画黑板报时，无意间画出了类似的感觉。当时是在画水下的初音，阴差阳错画出了水下冷色皮肤的感觉，之后我便喜欢上了这种感觉，偏灰偏冷。应该说很喜欢冷灰白。&lt;/p&gt;
&lt;p&gt;我很喜欢这幅画中的裙子四肢等白色阴影部分的表达。&lt;/p&gt;
&lt;p&gt;头发、裙子的表达，肢体的安排和颜色，冷色皮肤，舞台上的舞者和台下秃顶的仰头拼命看舞者的观众对比……&lt;/p&gt;
&lt;h2&gt;舞女的画家&lt;/h2&gt;
&lt;p&gt;去拍摄的编者，他们肯定是满心激动去拍摄的。同一幅画，通过不同的画布尺寸裁剪，就可以“发掘”出一件新的作品，一边为大师的作品之精美所惊叹，一边为同一幅画以不同的构图拍摄不断涌现新的惊喜而欣喜若狂。这同时也是我的真实感受。&lt;/p&gt;
&lt;p&gt;“人们称我是画舞女的画家，他们不知道，舞女之于我只是描绘美丽的纺织品和表现运动的媒介罢了。”德加这样说道。他确实不愧被称为 “舞女的画家” 。&lt;/p&gt;
&lt;p&gt;无论是从结构和构图，光线和色彩的运用，还有画面所表达的故事这些方面来看，都无懈可击。细节明暗色彩的表达，同时和笔触的结合……十分完美，同时能精确而强烈地传达出人物的情绪。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00107.jpg&quot; alt=&quot;《等待》 Waiting 1882 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《等待》 Waiting 1882 年 布面油画&lt;/p&gt;
&lt;p&gt;作品说明：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;德加的画里根本没有故事。他之所以对芭蕾舞女演员感兴趣，并非因为她们是漂亮少女。他似乎不关心她们的心情，他以印象主义者观察周围风景那种冷漠客观的态度去观察她们。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00269.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00160.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00273.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;这本书前面的部分是画家早期的作品，大多是些风景和肖像。一开始只是觉得构图有意思，色彩和传达的故事感也很棒，可以尝试临摹一下。就是这样简单的想法。&lt;/p&gt;
&lt;p&gt;然而一旦舞者的画作一幅幅出现下页时，看过一张画的总览，再一张张看细节图，我就越来越难以抑制住心中的激动之情。&lt;/p&gt;
&lt;p&gt;从心底里觉得这才是舞者，这才是绘画和艺术。光影和用色，构图的故事性，以及舞者的神态情绪的表达，给我带来了极大冲击，我立刻有一种想要临摹试试的冲动。&lt;/p&gt;
&lt;p&gt;如果不是这本书的编者安排细节图在书中，引导我去细看这些作品，我可能永远都不会注意到这些作品的精绝之处。
可能是因为我是个俗人，领悟不到要如何去阅读这些作品，并且无法无师自通，也没有过人来引导我来怎么欣赏这些画作吧。&lt;/p&gt;
&lt;p&gt;上文说到，我很在意 德加 的皮肤的用色，他画出了我很喜欢的冷色调的皮肤色，或者即使是暖色也会有一种我说不上来的可以打动人的地方。光影的形状，阴影的边缘控制，颜色的倾向……&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00063.jpg&quot; alt=&quot;《绿裙歌手》 The Green Singer 1884 年 布面油画 宽603mm 高464mm&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《绿裙歌手》 The Green Singer 1884 年 布面油画 宽603mm 高464mm&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00358.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00154.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00387.jpg&quot; alt=&quot;《舞蹈者》 Dancers 1884 年—1885 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《舞蹈者》 Dancers 1884 年—1885 年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00202.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00261.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;对舞女的芭蕾裙的表达也是 德加 的一绝，也是最打动我的地方。我一开始点进这本书，就是被芭蕾裙的表达所惊艳到了。我个人对芭蕾舞者和芭蕾裙/服装是很喜爱的，看到书中有这么多芭蕾裙不同的表达，感动的快要热泪盈眶。&lt;/p&gt;
&lt;p&gt;这次阅读这本画集，很久违地被画深深地触动了。真的好久好久没有这种感觉了，上次应该还是在看到 void_0 的画和一些构成参考时有被触动到，已经有整整 1 年了。&lt;/p&gt;
&lt;p&gt;一直觉得我现在的生活太功利了，总是想着我去做这个事能给我带来什么，情绪价值或者能提升画技，或者说为准备工作能有什么直接的帮助吗。昨天上午我被这些画吸引，阅读然后感触颇深，我真的应该慢下来去欣赏这些诉说着故事的画作，也能引发一些自己的思考和感动。&lt;/p&gt;
&lt;p&gt;太功利做事或许会时间收获双双失去。&lt;/p&gt;
&lt;h2&gt;透过舞女的情感传达&lt;/h2&gt;
&lt;p&gt;读到 Chapter 4 时，体会已经由 好厉害 的心情变得很累很疲劳了，画面太过冲击，并且有很多诡异、匪夷所思、用意很深的表达，感觉似乎在隐喻着 舞女 在德加眼中究竟&lt;strong&gt;是什么&lt;/strong&gt;，和舞女当时的社会地位和问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00168.jpg&quot; alt=&quot;《女帽店》 Millinery 1882 年—1895 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《女帽店》 Millinery 1882 年—1895 年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00174.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00040.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00193.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00241.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;德加在晚年一转之前的写实画风转到写意，并且用色越来越诡异。颜色传达了情绪，在作者眼中观测到的这些人物是什么样子的，是怎样的情绪或者在传达一种怎样的情绪/氛围。&lt;/p&gt;
&lt;p&gt;请先看下面这幅作品。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00237.jpg&quot; alt=&quot;《芭蕾舞团》 The Ballet 1885 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《芭蕾舞团》 The Ballet 1885 年 布面油画&lt;/p&gt;
&lt;p&gt;德加 越到后期的画作越透露出了一种癫狂，有些像被致幻的感觉。构图依旧是哪些构图，依旧是那些舞者，只是用色更加癫狂起来，在那些场景中几乎不可能出现的大片的绿色蓝色，同时画面中充满了线条在扭动着。皮肤的颜色也怪异了起来。&lt;/p&gt;
&lt;p&gt;我觉得他是在表达自己眼中的舞女，在描绘他观测到的舞女的存在。举一个不恰当的例子，可能有点像是色盲的世界，或者说是那些写意的画面是 类似 “沙耶之歌” 男主这样的视点的描绘，或者是某种环境气氛的感觉，又像或者素晴日里“三位一体”带着极度主观色彩的客观世界的视点画面。&lt;/p&gt;
&lt;p&gt;书中说到，德加只是冷眼观测舞女，他的创作就是他眼中舞女的样子的表达，是他眼中的世界，是他观测到的世界。&lt;/p&gt;
&lt;p&gt;“她们是我所知最接近天堂的形象，尽管她们早已远离了伊甸园。”&lt;/p&gt;
&lt;p&gt;尽管说是 德加 在冷眼旁观这些舞者，但是这些舞者在 德加 眼中究竟算什么呢，是怎样的存在呢？他对 舞者 如此痴迷，为何最终演变成了这种癫狂的画风？或许是因为他在晚年察觉到了自己有失明的风险。我看到这些画的时候，觉得这些舞女给人一种仙子或者精灵的感觉，但是是处在底层的精灵或仙子。她们美好而轻盈，却也容易被压迫被惊吓。特别是 德加 在许多舞女的创作中，都很喜欢用树林 / 丛林作背景，这就更为舞女和画面的故事带上神秘的色彩。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00194.jpg&quot; alt=&quot;《一群舞者》 A Group of Dancers 1890 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00246.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00200.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00050.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;这里舞女的表达感觉更像是 Ghosts 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00317.jpg&quot; alt=&quot;《芭蕾舞舞者》 Ballet Dancers 1890 年—1900 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《芭蕾舞舞者》 Ballet Dancers 1890 年—1900 年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00216.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00218.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;或者说他们是在被什么追赶着，驱逐着。注意到脚和手臂的颜色已经不一样。&lt;/p&gt;
&lt;p&gt;是否和当时舞者的生存环境和社会地位有关？&lt;/p&gt;
&lt;p&gt;这些舞者她们生活的环境条件是怎样的，她们的下场最终会如何？&lt;/p&gt;
&lt;p&gt;她们一生是辛苦幸福吗，还是遭到冷眼或抵触或歧视？？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1880年，德加开始创作其晚期代表作品“浴女”系列。对于这些纯属隐私、不宜见人的妇女形象，画家只是冷眼画出妇女们丑陋的抬腿，在浴盆前半近半出的样子。还有就是浴后擦身体的不雅动作，或蹲着、或弓着背、或伸直着腿。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可能是德加在尝试表达光鲜亮丽的舞女私下生活那些纯粹隐私的地方。她们不只有舞台上光鲜亮丽的那一刻，她们的还有舞台下的另一面。更真实的舞女，还包括她们私下的生活。&lt;/p&gt;
&lt;p&gt;“丑陋的抬腿，在浴盆前半近半出的样子。还有就是浴后擦身体的不雅动作，或蹲着、或弓着背、或伸直着腿。”无论是台上的优雅或是台下生活洗浴时各种不宜见人 / “丑陋”的动作，这都是舞女的全部，都会让人们对“舞女”这个存在的认识更加立体。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00255.jpg&quot; alt=&quot;《浴者踏入浴桶》 Step Into the Bath Bucket Bath 1890 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《浴者踏入浴桶》 Step Into the Bath Bucket Bath 1890 年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00073.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00262.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00089.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;德加 在“冷眼”观察舞女的同时，也在舞女的创作中投入了自己更多的感情。那些画作更加疯狂，富有进攻型和侵略性。&lt;/p&gt;
&lt;p&gt;整幅画作都变成了德加输送情感的途径，黄绿和蓝灰的色调使人想到某种邪恶的秘境，或是使人联想到噩梦的场景。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=../images/005/00027.jpg&quot; alt=&quot;《浴后擦拭身子的女人》缩略图&quot; style=&quot;zoom:25%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;《浴后擦拭身子的女人》 zoom:25%&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00027.jpg&quot; alt=&quot;《浴后擦拭身子的女人》 After Shower Wipe the Body of A Woman 1890 年—1895 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《浴后擦拭身子的女人》 After Shower Wipe the Body of A Woman 1890 年—1895 年 布面油画&lt;/p&gt;
&lt;p&gt;如果眯着眼或者缩小来看 《浴后擦拭身子的女人》 时，会觉得这不过是一张背景略显”嘈杂“的写实裸女画罢了，甚至有点像那种被压缩失真很严重的 GIF 图的感觉。然而一旦放大这张图看，你就会发现画面中充斥着交错的”刺眼“的线。画面十分”吵闹“，并且用色基本和科学和事实无关，纯粹是情感的表达，传递着偏执。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00197.jpg&quot; alt=&quot;《用毛巾的女人》 Woman with a Towel 1894 年—1895 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《用毛巾的女人》 Woman with a Towel 1894 年—1895 年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00250.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00299.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00344.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00149.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00121.jpg&quot; alt=&quot;《舞者》 Dancers 1894 年—1904 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《舞者》 Dancers 1894 年—1904 年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00147.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00304.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00291.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;被锈蚀或者被蚕食的感觉，让人细看之后很不舒服。然而这种大胆的用色却没有让画面显得很脏。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00108.jpg&quot; alt=&quot;《浴后》 After the Bath 1895 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《浴后》 After the Bath 1895 年 布面油画&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00158.jpg&quot; alt=&quot;《浴后》 After the Bath 1895 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《浴后》 After the Bath 1895 年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00183.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00205.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00267.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00308.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00006.jpg&quot; alt=&quot;《梳发》 Combing the Hair 1895年-1898年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《梳发》 Combing the Hair 1895年-1898年 布面油画&lt;/p&gt;
&lt;p&gt;《梳发》把传统绘画中女子&lt;strong&gt;梳头&lt;/strong&gt;或&lt;strong&gt;被梳头&lt;/strong&gt;的经典主题引入了现代社会。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00037.jpg&quot; alt=&quot;《演出前》 Before the Performance 1896年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《演出前》 Before the Performance 1896年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00075.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00236.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00293.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;看到这些画，都有一种吃了云南菌子的感觉&lt;/s&gt;。真的像是在凝视幻觉。&lt;/p&gt;
&lt;p&gt;隐约感觉到德加似乎在为她们控诉着什么……&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00331.jpg&quot; alt=&quot;《演出前》 Before the Performance 1896 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《演出前》 Before the Performance 1896 年 布面油画&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00400.jpg&quot; alt=&quot;《舞者的身躯》 Torso of a Dancer 1896 年—1899 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00141.jpg&quot; alt=&quot;《穿红色服装的三个舞者》 Three Dancers in Red Costume 1896 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《穿红色服装的三个舞者》 Three Dancers in Red Costume 1896 年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00336.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00153.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00352.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00179.jpg&quot; alt=&quot;《三个舞者》 Three Dancers 1896 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00068.jpg&quot; alt=&quot;《两个舞者》 Two Dancers 1898 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《两个舞者》 Two Dancers 1898 年 布面油画&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;到此时，德加 笔下的舞女风格已经和之前的大相径庭。回头看看上面的这些写意画，你还记得 《芭蕾舞团在巴黎歌剧院》 那些梦幻朦胧的舞女吗？&lt;/p&gt;
&lt;p&gt;德加 对舞女在舞台灯光下的描绘也十分精彩。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;阿·沃拉尔援引过德加一句颇有意思的话：“人们从一幅画中呼吸到的空气与人们在画外呼吸到的空气不是一回事”。他不想去发现大自然即兴安排的大胆的美，而更钟爱由人类创造出来的美。德加对人工舞台灯光的热爱要甚于阳光。他喜欢表现人，但从不以其他印象派画家那种随便、勿忙去勾勒人物的身影。因此，他的艺术不是对安格尔激发起来的古典主义的拒绝，而是以新的贡献扩大和丰富了这种绘画形式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00366.jpg&quot; alt=&quot;《舞者》 The Dancers 1899 年 布面油画&quot; /&gt;&lt;/p&gt;
&lt;p&gt;《舞者》 The Dancers 1899 年 布面油画&lt;/p&gt;
&lt;p&gt;局部细节展示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00008.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/005/00038.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;结尾&lt;/h2&gt;
&lt;p&gt;实际上我在看到这些部分时，已经感到十分疲惫了。这些画作对我的冲击也很大，同时给我的情绪和思绪带来了很大的压力。&lt;/p&gt;
&lt;p&gt;我在看 《芭蕾舞团在巴黎歌剧院》 那部分的画作时，每幅都会停下来细细观看，分析那些打动我的地方。然而下面的画作却渐渐染上了疯狂的色彩。我隐隐感觉这些画所传达的信息和主题越来越沉重，我不得不转变之前欣赏画的策略，快速浏览看过就作罢。&lt;/p&gt;
&lt;p&gt;我想你读到这里也肯定有相同的感受吧。&lt;/p&gt;
&lt;p&gt;其实我欠这篇文章一个结尾。然而此时的我已经无力写出什么结尾了。&lt;/p&gt;
&lt;p&gt;读过这本 《西方绘画大师经典佳作 德加（高清细节版）》 ，我对这些名家大师的作品，以及其他美术类书籍感兴趣了起来。或许我之后可以读一下 &lt;a href=&quot;https://z-lib.gd/book/5412007/c157ab/&quot;&gt;《如何看一幅画 = How to Look at a Painting》&lt;/a&gt; 。&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>使用虚拟信用卡注册 Mapbox 过程记录</title><link>https://notes.kiramux.com/posts/006-how-to-sign-up-for-mapbox</link><guid isPermaLink="true">https://notes.kiramux.com/posts/006-how-to-sign-up-for-mapbox</guid><pubDate>Sat, 23 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;https://www.mapbox.com/&lt;/p&gt;
&lt;p&gt;Mapbox 现在注册要信用卡验证了&lt;/p&gt;
&lt;p&gt;前阵子是只要用美国 IP 和 proton 邮箱就不要信用卡信息的，疑似被国人薅烂了，现在也需要信用卡验证了。我想上车已经晚了，因此一直没能注册到账号。&lt;/p&gt;
&lt;p&gt;前几天发现了一篇帖子，看起来刚好可以用来注册 Mapbox。&lt;s&gt;要不是课设要用谁会注册这个网站啊。&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nodeseek.com/post-84328-1&quot;&gt;【教程】免信用卡为cloudflare添加付款方式&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我复制一些过来存档。&amp;lt;这个括号里是我之后编辑的信息&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;summary&amp;gt;展开查看&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;Cloudflare是一家全球云平台服务提供商，面向全球不同规模的企业和个人用户提供广泛的网络服务。该公司提供多项免费网络服务，包括CDN、Workers and Pages、R2、Zero Trust等，深获用户好评。然而，部分免费服务如R2、Zero Trust需要绑定支付方式方可使用。鉴于一些用户没有外币信用卡、担心意外超额扣费、或因隐私考虑不愿绑定真实信用卡，Paypal信息等原因，无法使用R2、Tunnel、SaaS等服务。本教程将介绍一种通过&lt;strong&gt;生成信用卡信息&lt;/strong&gt;为Cloudflare添加付款方式的方法。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;需要准备：&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;一个信用卡号生成网站，这里以&lt;a href=&quot;https://www.nodeseek.com/jump?to=https%3A%2F%2Fnamso-gen.com%2F&quot;&gt;Namso-gen&lt;/a&gt;为例&lt;/li&gt;
&lt;li&gt;一个信用卡校验测活网站，&lt;s&gt;这里以&lt;a href=&quot;https://www.nodeseek.com/jump?to=https%3A%2F%2Fwww.mrchecker.net%2Fcard-checker%2Fccn2%2F&quot;&gt;MrChecker&lt;/a&gt;为例&lt;/s&gt;，&amp;lt;失效，更换为 https://checkertop.com/&amp;gt;&lt;/li&gt;
&lt;li&gt;一个卡BIN查询网站，这里以&lt;a href=&quot;https://www.nodeseek.com/jump?to=https%3A%2F%2Fbincheck.io%2Fzh&quot;&gt;BINCheck&lt;/a&gt;为例&lt;/li&gt;
&lt;li&gt;一个虚拟地址生成网站，这里以&lt;a href=&quot;https://www.nodeseek.com/jump?to=https%3A%2F%2Fwww.meiguodizhi.com%2F&quot;&gt;美国地址生成器&lt;/a&gt;为例。&amp;lt;推荐使用真实地址，随便搜点就可以，生成的地址有些网站可能不认&amp;gt;&lt;/li&gt;
&lt;li&gt;一个可以通过cf支付网关验证的卡段，这里我为大家提供几个：
&lt;code&gt;601121255660xxxx&lt;/code&gt;
&lt;code&gt;546775142533xxxx&lt;/code&gt;
&lt;code&gt;622759531087xxxx&lt;/code&gt;
&lt;code&gt;479229938031xxxx&lt;/code&gt;
（用其他卡段测试成功的mjj也可在楼下补充）&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：
一部分网站的支付网关在初步验证信用卡信息时仅检查卡号的格式是否符合标准（如&lt;a href=&quot;https://www.nodeseek.com/jump?to=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2F%E5%8D%A2%E6%81%A9%E7%AE%97%E6%B3%95&quot;&gt;Luhn算法&lt;/a&gt;）以及卡种、发卡行等基本信息，而不进行实际的资金预授权或小额扣款验证。也就是说，在绑定信用卡时，这些网站可能只进行基本的格式检查，而不与银行或信用卡网络进行实时验证来确认卡号的真实性和有效性。这意味着，只要卡号格式正确，就可能可以通过验证。然而，这并不意味着可以使用这些随机生成的信用卡信息进行实际消费或支付，因为在进行交易时，支付系统会进行更深入的验证，包括资金的实际可用性和账户的有效性。因此，当一些网站采用更严格的验证措施，如要求验证卡的有效性或进行小额扣款验证时，这时随机生成的信用卡信息就无法通过验证。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;操作步骤&lt;/strong&gt;：
a.在Cloudflare Dashboard点击右上角的头像--&amp;gt;账单--&amp;gt;付款信息，添加付款方式。
b.使用信用卡号生成网站&lt;a href=&quot;https://www.nodeseek.com/jump?to=https%3A%2F%2Fnamso-gen.com%2F&quot;&gt;Namso-gen&lt;/a&gt;，在对应的卡段下生成一定数量的信用卡信息
&lt;img src=&quot;https://imgz.pp.ua/file/e670e88553491b105870e.png&quot; alt=&quot;img&quot; /&gt;
c.将随机生成得到的信用卡信息复制到&lt;a href=&quot;https://www.nodeseek.com/jump?to=https%3A%2F%2Fwww.mrchecker.net%2Fcard-checker%2Fccn2%2F&quot;&gt;MrChecker&lt;/a&gt;进行校验
&lt;img src=&quot;https://imgz.pp.ua/file/ddf07dff0ec3c15ae49e7.png&quot; alt=&quot;img&quot; /&gt;
d.挑选一个通过校验的信用卡信息，将卡号，有效期，cvv填写到cf中，其中的地址信息最好要和信用卡段所在国家一致，比如我提供的信用卡卡段是美国的，就填写美国的地址信息。卡bin查询和虚拟地址生成可以使用我上面提供的网站。
&lt;img src=&quot;https://imgz.pp.ua/file/81ccd6aac96dc25046f48.png&quot; alt=&quot;img&quot; /&gt;
e.最后点击确定即可通过验证，如果不能通过，可以选取另外的信用卡信息再次进行尝试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后话&lt;/strong&gt;
生成的虚假信用卡信息远不止绑定cf这一用处。比如可以通过绑定特定卡段的虚假信用卡信息，实现免费试用github copilot服务，白嫖perplexity ai pro等，一般把这种行为称为C号（C即CVV，Crack之意），这里因为篇幅和防止滥用等一些原因不再深入。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;❌&lt;strong&gt;在某种意义上，尝试使用随机生成的信用卡信息进行绑定或交易属于非法行为，可能会导致法律后果，包括被网站禁止、面临欺诈指控等。因此如非必要，即便在技术上可能存在这样的漏洞，也强烈建议遵守法律和道德规范，不要尝试使用非法手段进行操作。本教程仅供学习参考&lt;/strong&gt;
❌&lt;strong&gt;在某种意义上，尝试使用随机生成的信用卡信息进行绑定或交易属于非法行为，可能会导致法律后果，包括被网站禁止、面临欺诈指控等。因此如非必要，即便在技术上可能存在这样的漏洞，也强烈建议遵守法律和道德规范，不要尝试使用非法手段进行操作。本教程仅供学习参考&lt;/strong&gt;
❌&lt;strong&gt;在某种意义上，尝试使用随机生成的信用卡信息进行绑定或交易属于非法行为，可能会导致法律后果，包括被网站禁止、面临欺诈指控等。因此如非必要，即便在技术上可能存在这样的漏洞，也强烈建议遵守法律和道德规范，不要尝试使用非法手段进行操作。本教程仅供学习参考&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;具体注册方法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用美国 IP ，账单地址使用美国真实地址（疑似非必要）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;a href=&quot;https://namso-gen.com/?tab=advance&amp;amp;network=random&quot;&gt;namso-gen&lt;/a&gt; 随机生成信用卡，选用网友分享的可用的卡头， format 选 PIPE ，方便校验卡信息。我使用的是第二个卡头。&lt;/p&gt;
&lt;p&gt;5154620012 、 515462001608 、 5154 6200 1822 7221&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 https://checkertop.com/ 校验卡信息，把 &lt;a href=&quot;https://namso-gen.com/?tab=advance&amp;amp;network=random&quot;&gt;namso-gen&lt;/a&gt; 生成的那一堆信用卡信息粘贴进去检测。使用 &lt;strong&gt;Live&lt;/strong&gt; 状态的信用卡。最好去 https://bincheck.io/ BIN Check 一下。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 Mapbox 关闭 ADBlock 等广告插件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用 Gmail 和美国节点注册。注册信息填完之后会提示输入信用卡信息，把刚才验证过的信用卡信息填入。日期的格式例如 10/32 ，CVV 是3位（或4位数字）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果失败就换其他验证过的信用卡信息重试，没有过信用卡的账号是没注册的，因此需要重新填注册信息，再填一次信用卡。如果一直失败，就考虑换卡头。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;信用卡过了之后，要验证邮箱。邮箱验证之后去搜一个&lt;a href=&quot;https://zhuanlan.zhihu.com/p/653302438&quot;&gt;真实美国地址&lt;/a&gt;，填账单信息，这步之后就注册好了。&lt;/p&gt;
&lt;p&gt;![]../images/006/result.png)&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>Arcgis 三维模型导入 SketchUp</title><link>https://notes.kiramux.com/posts/007-shp-to-skp</link><guid isPermaLink="true">https://notes.kiramux.com/posts/007-shp-to-skp</guid><pubDate>Mon, 25 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;此方法来自：&lt;a href=&quot;https://www.bilibili.com/video/BV1tV411H7YW/?p=8&quot;&gt;07ArcGIS三维模型导入Sketchup&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在导出模型之后，我们发现了模型底面与 Arcmap 中显示不一样，Arcmap 中的显示显然更扁。当时一直不知道怎么解决，现在了解到是坐标系的问题，把它改为投影坐标系就可以了，详情见 &lt;a href=&quot;/008-block-development-intensity/&quot;&gt;008&lt;/a&gt; 的开头部分。&lt;/p&gt;
&lt;p&gt;由于我导出模型时用了 FME 等工具进行数据转换，可能这些操作在什么地方自动解决了投影坐标系不一致的情况，所以模型是准确的，而未调整坐标系的 Arcmap 中显示的底图图形是错误的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;[TOC]&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;获取数据&lt;/h2&gt;
&lt;p&gt;一般来说可以调用某些地图平台 api 等迂回曲折的方式获取到 POI 数据，再根据 POI 数据搞到 AOI 数据。另外貌似可以水经注下载。最方便的还是去某鱼找代下，十几块钱一个市，价格也不贵。&lt;/p&gt;
&lt;h2&gt;在 Arcmap 中处理数据&lt;/h2&gt;
&lt;p&gt;把数据导入 Arcmap 中筛选数据，导出为shp。&lt;/p&gt;
&lt;p&gt;可以对图层右键 &amp;gt;&amp;gt; 数据 &amp;gt;&amp;gt; 导出数据 。选择 &lt;code&gt;视图范围中的所有要素&lt;/code&gt; 可以免去选择数据的步骤。输出要素类是输出路径，输出类型要改为 shp 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125195625377.png&quot; alt=&quot;image-20241125195625377&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;在 Arcscene 处理模型&lt;/h2&gt;
&lt;p&gt;把 Arcmap 导出的 shp 数据导入 Arcscene 处理。对图层 右键 &amp;gt;&amp;gt; 属性 &amp;gt;&amp;gt; 拉伸设定为 height (或者 floor * &lt;a href=&quot;https://www.bilibili.com/video/BV1tV411H7YW/?p=8&quot;&gt;层高3.6&lt;/a&gt;)。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125175102558.png&quot; alt=&quot;image-20241125175102558&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125175138859.png&quot; alt=&quot;image-20241125175138859&quot; /&gt;&lt;/p&gt;
&lt;p&gt;会出现高度问题。对左栏 Scene 图层右键 &amp;gt;&amp;gt; 场景属性 &amp;gt;&amp;gt; 垂直夸大 &amp;gt;&amp;gt; 地理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125175356305.png&quot; alt=&quot;image-20241125175356305&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125175437192.png&quot; alt=&quot;image-20241125175437192&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125175517093.png&quot; alt=&quot;image-20241125175517093&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125175540318.png&quot; alt=&quot;image-20241125175540318&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在还不是 3d 模型（多面体），需要转成 3d 模型&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125175620814.png&quot; alt=&quot;image-20241125175620814&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125175717629.png&quot; alt=&quot;image-20241125175717629&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在每个建筑都是一个体块，直接导出会变成一堆文件，需要合并成一个“组”&lt;/p&gt;
&lt;p&gt;右键 编辑要素 开始编辑&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125175820144.png&quot; alt=&quot;image-20241125175820144&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打开属性表 &amp;gt;&amp;gt; 全选 ，选择之后直接关闭窗口。3D编辑器 &amp;gt;&amp;gt; 合并 &amp;gt;&amp;gt; 停止编辑 &amp;gt;&amp;gt; 保存&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125175843735.png&quot; alt=&quot;image-20241125175843735&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125175959709.png&quot; alt=&quot;image-20241125175959709&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125180030724.png&quot; alt=&quot;image-20241125180030724&quot; /&gt;&lt;/p&gt;
&lt;p&gt;默认即可。此时打开 建筑体块_cut 的属性表，就会只有一个多面体了。效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125194803046.png&quot; alt=&quot;image-20241125194803046&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;使用 FMEx64 转换为 SketchUp 格式&lt;/h2&gt;
&lt;p&gt;如果按网传的方法（见下文）直接把输出的 ID_0.dae 导入 SketchUp 会出错，疑似是因为是 32 位软件。&lt;/p&gt;
&lt;p&gt;因此需要使用 &lt;strong&gt;FME（64位版）&lt;/strong&gt; 把 shp 数据直接转为 skp&lt;/p&gt;
&lt;p&gt;FME 新建一个 Blank Workspace。&lt;/p&gt;
&lt;p&gt;在菜单栏处找到 Reader 。添加一个 Reader 。&lt;/p&gt;
&lt;p&gt;Format 里如果没有 Esri Shapefile ，需要下拉在更多中找。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125194141574.png&quot; alt=&quot;image-20241125194141574&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125194210230.png&quot; alt=&quot;image-20241125194210230&quot; /&gt;&lt;/p&gt;
&lt;p&gt;添加一个 Writer&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125194250390.png&quot; alt=&quot;image-20241125194250390&quot; /&gt;&lt;/p&gt;
&lt;p&gt;把两个节点连起来， Run 一下就可以了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125194321777.png&quot; alt=&quot;image-20241125194321777&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下方 log 栏出现 &lt;code&gt;Translation was SUCCESSFUL&lt;/code&gt; 就可以去看输出文件了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125194521510.png&quot; alt=&quot;image-20241125194521510&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;其他可用的方法&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125195221551.png&quot; alt=&quot;image-20241125195221551&quot; /&gt;&lt;/p&gt;
&lt;p&gt;直接使用 arcgis pro ，只有 64 位版的，所以可以直接用自带的 FME 转换插件。&lt;/p&gt;
&lt;h2&gt;疑似失效的方法记录&lt;/h2&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;展开查看&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;网上传的方法，一般按照他们说的流程，导入 su 会卡在 10% 然后失败。方法记录。&lt;/p&gt;
&lt;p&gt;首先要把合并好的多面体转 Collada 导出 dae 文件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125192956145.png&quot; alt=&quot;image-20241125192956145&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125193037659.png&quot; alt=&quot;image-20241125193037659&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果直接把输出的 ID_0.dae 导入 SketchUp 会出错，疑似是因为是 32 位软件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125113441602.png&quot; alt=&quot;image-20241125113441602&quot; /&gt;&lt;/p&gt;
&lt;p&gt;评论区说把数据存储在 GDB 中再转为 dae 可以用 su 导入，但是实测导入 dae 慢的要死，一直卡在 10%，可能这次选的区域太大了。&lt;/p&gt;
&lt;p&gt;后续。。&lt;strong&gt;依旧导入失败&lt;/strong&gt;，&lt;strong&gt;此方法无效&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125113936236.png&quot; alt=&quot;image-20241125113936236&quot; /&gt;&lt;/p&gt;
&lt;p&gt;想要把数据存为 GDB 需要先新建一个 文件地理数据库 ，然后导出的时候选择这个新建的 gdb 文件即可。&lt;/p&gt;
&lt;p&gt;当然也有可能是我把数据存进 GDB 的方式不对，导致这个方法没复现成功。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/007/image-20241125113814656.png&quot; alt=&quot;image-20241125113814656&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>Arcmap 计算街区开发强度</title><link>https://notes.kiramux.com/posts/008-block-development-intensity</link><guid isPermaLink="true">https://notes.kiramux.com/posts/008-block-development-intensity</guid><pubDate>Wed, 04 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文整理自 【街区开发强度】：&lt;a href=&quot;https://www.bilibili.com/video/BV1HG4y167Qn/&quot;&gt;GIS统计街区建筑密度、容积率&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由于本文中使用的都是 shp ，故给字段命名时有长度要求，因此都用了英文的简写来辨识。&lt;/p&gt;
&lt;p&gt;另外需要注意，默认导入数据用的是地理坐标系，需要转换为&lt;strong&gt;投影坐标系&lt;/strong&gt;才可以显示正常，否则图形会比较扁。可以看文中和文末的图片区别。&lt;/p&gt;
&lt;p&gt;菜单栏 &amp;gt;&amp;gt; 视图 &amp;gt;&amp;gt; 数据框属性 &amp;gt;&amp;gt; 坐标系，选择投影坐标系下的 World &amp;gt;&amp;gt; WGS_1984_World_Mercator ，确定即可显示正常。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本文的目的是通过已有的数据，对该街区开发强度进行计算（容积率和建筑密度），因此需要根据公式计算并获取需要的数据。&lt;/p&gt;
&lt;p&gt;计算公式：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;建筑密度 = 总基底面积/总用地面积*100&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;容积率 = 总建筑面积/总用地面积&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因此需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;总用地面积（街区的面积）&lt;/li&gt;
&lt;li&gt;总基底面积和总建筑面积（基底面积 * 层数）&lt;/li&gt;
&lt;li&gt;代入公式计算&lt;/li&gt;
&lt;li&gt;通过图的形式表现结果（调整符号系统）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;[TOC]&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;创建街区范围&lt;/h2&gt;
&lt;p&gt;仍旧是自行获取路网和建筑的数据。我获取的建筑数据里没有 Shape_area ，要自己用字段计算器的 Python 函数 &lt;code&gt;!Shape.Area!&lt;/code&gt;计算一下，下文有记录.&lt;/p&gt;
&lt;p&gt;先对 路网 要素转面 通过道路围合生成街区面。系统工具箱 &amp;gt;&amp;gt; Data Management Tools.tbx  &amp;gt;&amp;gt; 要素  &amp;gt;&amp;gt; 要素转面 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204194839701.png&quot; alt=&quot;image-20241204194839701&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204195242701.png&quot; alt=&quot;image-20241204195242701&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204195734341.png&quot; alt=&quot;image-20241204195734341&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;给每个街区添加街区编号&lt;/strong&gt;。对 街区.shp 右键打开属性表，添加新字段 &lt;code&gt;街区id&lt;/code&gt;，通过字段计算器将其赋值为编号 FID 的值（有可能是 OBJECTID 等）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204200253609.png&quot; alt=&quot;image-20241204200253609&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204200345862.png&quot; alt=&quot;image-20241204200345862&quot; /&gt;&lt;/p&gt;
&lt;p&gt;计算&lt;strong&gt;建筑&lt;/strong&gt;的 shape area 。添加新字段 shape_area ，字段编辑器使用 Python 的 !Shape.Area! 函数计算面积。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204222828531.png&quot; alt=&quot;image-20241204222828531&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;连接街区与建筑数据&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204200537849.png&quot; alt=&quot;image-20241204200537849&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204204651391.png&quot; alt=&quot;image-20241204204651391&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;计算街区建筑基底面积及建筑总面积&lt;/h2&gt;
&lt;p&gt;建筑基底面积 = shape_area&lt;/p&gt;
&lt;p&gt;层数 height / 3，因为数据的 height 都是 3 的倍数，图方便就直接除以3得到大概的数据了。&lt;/p&gt;
&lt;p&gt;建筑总面积 = 基底面积 * 层数&lt;/p&gt;
&lt;p&gt;对刚刚输出的 建筑交街区.shp 右键 &amp;gt;&amp;gt; 打开属性表，分别添加 基底area （建筑基底面积）和 Total_area （建筑总面积） 字段，分别计算 建筑基底面积 和 建筑总面积。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204205753071.png&quot; alt=&quot;image-20241204205753071&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;汇总街区建筑基底面积&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;基底area 右键汇总，导出dbf表（dBASE 表），命名为 街区建筑面积计算.dbf 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204205954998.png&quot; alt=&quot;image-20241204205954998&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204210210190.png&quot; alt=&quot;image-20241204210210190&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204210426406.png&quot; alt=&quot;image-20241204210426406&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里已经导出过了，截图演示注意一下保存类型为 dBASE 表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;汇总街区建筑总面积&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;汇总 建筑总面积 条目&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204210358499.png&quot; alt=&quot;image-20241204210358499&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204210553445.png&quot; alt=&quot;image-20241204210553445&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;连接统计结果表与街区图层数据&lt;/h2&gt;
&lt;p&gt;就是一个操作：分别连接，添加新字段，赋值，移除连接&lt;/p&gt;
&lt;p&gt;总结就是分别连接两个表，在 街区 这个表中新建字段，把刚才连接的表里 用到的数据赋值给这个新建的，然后断开连接（为了不干扰下一个表的连接和赋值），重复下一个。&lt;/p&gt;
&lt;p&gt;右键打开刚才保存的这两个表，再打开 街区 的属性表&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204210644133.png&quot; alt=&quot;image-20241204210644133&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204210853346.png&quot; alt=&quot;image-20241204210853346&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过连接与关联，通过 &lt;code&gt;街区id&lt;/code&gt; 进行连接。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204210910937.png&quot; alt=&quot;image-20241204210910937&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204210953277.png&quot; alt=&quot;image-20241204210953277&quot; /&gt;&lt;/p&gt;
&lt;p&gt;降序排列 Sum_基底area&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204211056794.png&quot; alt=&quot;image-20241204211056794&quot; /&gt;&lt;/p&gt;
&lt;p&gt;添加新字段 Sum_base （建筑基底面积汇总），得到 街区.Sum_base&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204211156397.png&quot; alt=&quot;image-20241204211156397&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204211610361.png&quot; alt=&quot;image-20241204211610361&quot; /&gt;&lt;/p&gt;
&lt;p&gt;移除连接。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204211709218.png&quot; alt=&quot;image-20241204211709218&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204211738348.png&quot; alt=&quot;image-20241204211738348&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再连接 街区建筑总面积计算 ，依旧对 Sum_Total_area 降序排列，添加字段，建筑总面积汇总，之后赋值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204211815900.png&quot; alt=&quot;image-20241204211815900&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204212113204.png&quot; alt=&quot;image-20241204212113204&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204212231950.png&quot; alt=&quot;image-20241204212231950&quot; /&gt;&lt;/p&gt;
&lt;p&gt;移除连接&lt;/p&gt;
&lt;h2&gt;计算容积率和建筑密度&lt;/h2&gt;
&lt;p&gt;添加字段 建筑密度 Density 和 容积率 Ratio （双精度）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204212641574.png&quot; alt=&quot;image-20241204212641574&quot; /&gt;&lt;/p&gt;
&lt;p&gt;计算公式：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;建筑密度 = 总基底面积/总用地面积*100&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;容积率 = 总建筑面积/总用地面积&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;这里要用到总用地面积，即每个街区的面积，为了不和上面的建筑 Shape_area 混淆，就放到这里计算。&lt;/p&gt;
&lt;p&gt;先计算出 街区的 Shape_area，依旧是用 Python 的 &lt;code&gt;!Shape.Area!&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后用 字段计算器 分别计算 建筑密度 和 容积率 并赋值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;建筑密度&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204213437524.png&quot; alt=&quot;image-20241204213437524&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;容积率&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204213731748.png&quot; alt=&quot;image-20241204213731748&quot; /&gt;&lt;/p&gt;
&lt;p&gt;筛选并剔除空值：按住 Shift 去选表左侧的表头，选中之后关闭表，对 街区 右键 导出数据，命名为 街区开发强度.shp 。（视频中导出的是 文件和个人地理数据库要素类，我这里导出了 Shapefile）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204213917057.png&quot; alt=&quot;image-20241204213917057&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204214213979.png&quot; alt=&quot;image-20241204214213979&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后在 属性 &amp;gt;&amp;gt; 符号系统 里调整&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204214355488.png&quot; alt=&quot;image-20241204214355488&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204214828190.png&quot; alt=&quot;image-20241204214828190&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/image-20241204214756689.png&quot; alt=&quot;image-20241204214756689&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;成品图&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/%E5%BB%BA%E7%AD%91%E5%AF%86%E5%BA%A6%E5%88%86%E6%9E%90.png&quot; alt=&quot;建筑密度&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/008/%E5%AE%B9%E7%A7%AF%E7%8E%87%E5%88%86%E6%9E%90.png&quot; alt=&quot;容积率&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这两张成品图按照出图要求做了处理：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;转换为地理坐标系出图，图形形状准确与实际相符。&lt;/li&gt;
&lt;li&gt;更换与整张图纸相符的配色，并对色块图层创建剪辑蒙版，下面是该地块的建筑底图。&lt;/li&gt;
&lt;li&gt;加了图例和图名，成功转换为有信息量的水图。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Kiramux</author></item><item><title>2024年内总结</title><link>https://notes.kiramux.com/posts/009-2024</link><guid isPermaLink="true">https://notes.kiramux.com/posts/009-2024</guid><pubDate>Mon, 30 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;随便开个头写年内总结。&lt;/p&gt;
&lt;p&gt;2024 年可以分阶段用不同的词语来概括。&lt;/p&gt;
&lt;p&gt;1 月-2 月休假期间，是&lt;strong&gt;接洽&lt;/strong&gt;了自己，好好休息，也是这时候加入了 galgamer 的群聊，认识了很多群友。&lt;/p&gt;
&lt;p&gt;3 月-6 月，是在&lt;strong&gt;迷茫&lt;/strong&gt;和浑浑噩噩，在 3 月尝试过学习一点技美入门，但是学校课总会有一种无形的压力，即使任务没多少，但是总会感觉被 PUA 了。3 月一直在白天磨洋工，晚上和室友干农活。于是 4 月为了转换心情，出去旅游玩，去南京散心。一直待在小城市，那次到了南京才第一次会坐地铁。然后和朋友玩了 2 天，他先回了，然后第 3 天我车欠了，稍微玩了下就回去了。之后就忙忙碌碌到期末了。&lt;/p&gt;
&lt;p&gt;7 月留校 1 个月，原计划打算好好入门技美，但是每天只能面对一个电脑屏幕，没人可以说话，整个人精神状态堪忧，期间还由于这个和家里人起了点摩擦。家里人想让我回家，不想让我留到学校，不知道是什么神秘理由，我认为他们无法理解我，甚至要给我断掉生活费。于是撑了 1 个月回家了，仍旧和自己和解了，暑假休息了一个月。&lt;/p&gt;
&lt;p&gt;8 月，休息。在暑假即将结束的那个夜晚，我和几位友人在小酒馆彻夜长谈，解决了我目标不明确和焦虑问题，让我确定了&lt;strong&gt;方向&lt;/strong&gt;，并不动摇地走下去。那个夜晚真的对我来说很重要。如今有人问我今后的打算，我会毫不犹豫地说我在准备别的工作，不再瞻前顾后了。&lt;/p&gt;
&lt;p&gt;9 月初，测绘实习，体验了无人机测绘，学会了如何用无人机拍的照片跑正投影图，跑三维重建。与此同时，我重启了技美的&lt;strong&gt;学习&lt;/strong&gt;，和设计课同步进行。同时也在用 blog 记录的方式复习之前的内容，虽然只有一点点。设计课老师是一位很好的老师，他很直白地问我们，如果谁想转行在准备其他工作可以和他讲，他不会要求那么严苛。然而我在这门课设中也有想学到的东西，也有自己的倔强，有点不想认输，于是最终也没有跟他讲过。虽然最后出的成果图也是差强人意，但是有好好地把我想表达的表达出来了。&lt;/p&gt;
&lt;p&gt;10 月，节奏&lt;strong&gt;中断&lt;/strong&gt;了。国庆假期，2 天给 TA ，2 天画 Q 版，中间穿插休息，剩下的时间给了设计课和测绘画图。中旬家里人结婚，请假回家休息，于是不知不觉到了 10 月 17 日，krenz 的构成课开始了。原计划打算在 17 号之前把庄懂的课程学完，但是因为 课程内容涉及到大作业有些难/上半月大量休假 ，导致进度停滞，不过刚好课程到达一个节点，于是技美的学习暂停，开始 krenz 的构成绘画课学习。&lt;/p&gt;
&lt;p&gt;11 月，&lt;strong&gt;投入&lt;/strong&gt; krenz 的课程学习。中间涉及到课程作业的成果提交，于是延缓了 2 - 3 周的作业没做，课程还是跟着听。给 12 月埋了很多的雷。&lt;/p&gt;
&lt;p&gt;12 月，&lt;strong&gt;忙碌&lt;/strong&gt; + 疲劳。新课设改为了小组作业，本来以为会比之前任务量小很多，没想到其实也不少。我不擅长做设计，于是承接了前期的技术相关工作和周围场地的建模工作。12 月写的那些 Arcgis 的文章就是学习过程中的记录，只不过有些地方需要勘误，之前一些疑惑的地方现在已经可以解决了。本以为只用画图工作量还可以，但是由于是小组作业，作图的标准大大提高了，这次画的图是我画过最密的一次。之前一张的图幅现在需要放四张图，可以理解为工作量几乎多了 3 倍，唯一轻松的点是我可以完全把设计抛给她们做。整个 12 月最要命的时期还是在课设预答辩前夕， krenz 的 L8 作业截止日期和预答辩日期完全重叠，我不得不在画图压力很大的情况下，抽出时间来做 L8 的创作作业。好在 krenz 的课程设计很精妙，他已经将前期我最不擅长的路给铺好了，于是我顺风顺水地完成了 L8 ，每个节点给助教发过去检查 &amp;gt;&amp;gt; 通过 &amp;gt;&amp;gt; 下一节点 &amp;gt;&amp;gt; 检查 &amp;gt;&amp;gt; 最终通过。就这么顺利地完成并通过了，有些超乎我的想象，本以为会卡更多时间。不过拜熬夜画图所赐，再加上周日喝了点酒，周日那晚我发烧病倒了，周末浪费过去了，甚至那天出去玩还 A 了钱，然后玩一半回去看病（草好心疼。12 月末我还在还上次生病欠下的债。&lt;/p&gt;
&lt;p&gt;总结看来，其实直到 9 月，我今年才算真的步入正轨，其他时间其实都不知不觉流过去了，而且自己其实也并不开心，一直在被焦虑裹挟。说到底其实还是就业形式不好，还有国内建筑学本身教育安排就在 PUA 学生，不给学生留空隙。休息的空隙就只刚好够休息，一旦再加另一件要完成的事，都要透支自己的睡眠去完成，很畸形。不过今年其实已经还算轻松了，去年还一直在补修大一的课，现在看来当时的自己简直是超人了，用 1 年的时间几乎修完了 2 年的课。&lt;/p&gt;
&lt;p&gt;其实我之前是没有写年内总结的习惯的，去年甚至写了一般然后烂尾了。今年是因为原计划的元旦贺图难以推进，于是把那个鸽了来写这个。&lt;/p&gt;
&lt;p&gt;经过去年年末以及今年的尝试，我发现我确实不适合做创意工作，我没有创作的才能，但是比较擅长 copy 。而且现在还对前端感兴趣（虽然一直没有开始正经学），但是还是保持着想学的心态，这很好。然后在今年也萌生了很多想做的事，有短期难以实现需要长期坚持的，也有平时随手可以做得到的小事。例如我想有生之年自己搓出来一个自己设计的手办，这个要自己先画原画，慢慢学 zbrush 然后做成 GK 白模再上色，是个长期的目标。也有日常可以拿来陶冶情操的活动可以做。例如我之前想过自己画画的意义以及如何能让他人记住自己，于是我买过空白书签，再上面画上自己喜欢的人物，然后在上课时送给陌生人（当然是美少女），然后还画了一些送给社团的朋友，招新时也单独送了一些，现在再考虑画一些明信片给友人寄过去。做这些是想有机会能让自己的存在以另一种形式存在与他人的生活中，即使是只见过一面的陌生人，没准她偶然间翻到那张书签，然后会尝试回忆这张书签是从哪里来的呢？即使她想不到，这页由我画的书签仍然是留在了她的生活中。假如不幸被丢掉了，它可以说是也曾经留在过她的记忆中过。我觉得这种实体存在的画，比深藏在某个文件夹下的 psd 文件更有意义一点。&lt;/p&gt;
&lt;p&gt;其实这一年整体看来过的蛮糟糕的，不过还好在最后努力了，还认识了很多很重要的群友，这一年也很有意义。&lt;/p&gt;
&lt;p&gt;接下来就要准备考试，然后把作业和那些报告都做完，寒假回家好好休息，假期多玩点 jrpg ，争取过年入一台 macmini ，有空的话可以画点明信片给群友或者朋友邮寄，假期或者等开学再二度重启 TA 学习。这就是我接下来的安排和愿景。&lt;/p&gt;
&lt;p&gt;虽然今年乱糟糟的，但生活中还是有很多美好，比如我还可以随手画一点小东西做记录，毕竟还有很多不错的朋友和群友能听俺讲话，偶尔和家里人通一通视频也挺好的。希望我的下一年没那么累，然后顺利找到实习。&lt;/p&gt;
&lt;p&gt;今天先写到这里，明天有空的话再看一眼可能再写点。&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>低成本网盘存储方案记录</title><link>https://notes.kiramux.com/posts/010-low-cost-netdisk-storage-solution</link><guid isPermaLink="true">https://notes.kiramux.com/posts/010-low-cost-netdisk-storage-solution</guid><pubDate>Thu, 06 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;❗❗前排提示❗❗
本文于深夜撰写，内容输出完毕但顺序有待日后优化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;最近闲暇在家折腾网盘。作为穷学生一名，又想用廉价的云存储，那就只有网盘这一个选择了。&lt;/p&gt;
&lt;p&gt;首先说明我的需求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;文件存储。游戏、图片、书籍、课程等。&lt;/li&gt;
&lt;li&gt;影视。转存动漫、电影资源，通过程序挂载 + 302 / 反代 获取网盘视频文件和字幕，在第三方播放器播放。&lt;/li&gt;
&lt;li&gt;大空间不限速，安全稳定，价格相对便宜，短期内不会跑路，不用费心频繁迁移。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但是话又说回来，稳定、速度、便宜永远在一个「不可能三角」上，所以肯定会有不少坑和限制。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/010/%E4%B8%8D%E5%8F%AF%E8%83%BD%E4%B8%89%E8%A7%92.svg&quot; alt=&quot;不可能三角&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们可以参考 &lt;a href=&quot;https://alist.nn.ci/zh/guide/drivers/&quot;&gt;Alist 官方文档&lt;/a&gt; 所列出支持的网盘存储。&lt;/p&gt;
&lt;p&gt;首先排除一些网盘：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阿里云盘：永久空间不大，曾经的承诺说改就改，没会员限速，PASS。&lt;/li&gt;
&lt;li&gt;百度网盘：SVIP 用户官方客户端体验勉强还行，限设备严重，挂载复杂且使用不便，离开官方客户端使用限制很大。&lt;/li&gt;
&lt;li&gt;夸克网盘：要开会员，88VIP被砍废了限制很多，大空间疑似需要做任务。&lt;/li&gt;
&lt;li&gt;中国移动/联通网盘：了解不多，据说限制比较多，资源较少玩的人也少，运营商网盘，暂时不考虑。&lt;/li&gt;
&lt;li&gt;谷歌云盘：没稳定的账号，国内无法直连，空间不便宜，暂时跳过。&lt;/li&gt;
&lt;li&gt;还有一些其他的网盘也不在考虑范围内，不再赘述原因。Dropbox、MEGA 、蓝奏云、迅雷云盘。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;排除之后，有以下表格对比：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;挂载可用性&lt;/th&gt;
&lt;th&gt;挂载API风控限制&lt;/th&gt;
&lt;th&gt;上传速度&lt;/th&gt;
&lt;th&gt;挂载下载&lt;/th&gt;
&lt;th&gt;价格&lt;/th&gt;
&lt;th&gt;用户数/讨论度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OneDrive&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;小/中&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Free/5一月&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;天翼网盘&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;小&lt;/td&gt;
&lt;td&gt;家庭盘or会员✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;买账号便宜&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;115&lt;/td&gt;
&lt;td&gt;本地代理✅/302❓&lt;/td&gt;
&lt;td&gt;中/高&lt;/td&gt;
&lt;td&gt;会员+官方✅/挂载❓&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;年费158&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;123&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❓&lt;/td&gt;
&lt;td&gt;❓&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;99一年/228三年&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6盘&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❓&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pikpak&lt;/td&gt;
&lt;td&gt;✅/❌&lt;/td&gt;
&lt;td&gt;易掉线&lt;/td&gt;
&lt;td&gt;1T/云下载✅&lt;/td&gt;
&lt;td&gt;✅/WebDAV⍻&lt;/td&gt;
&lt;td&gt;贵且不等/邀请白嫖&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terabox&lt;/td&gt;
&lt;td&gt;❓&lt;/td&gt;
&lt;td&gt;❓&lt;/td&gt;
&lt;td&gt;❓&lt;/td&gt;
&lt;td&gt;❓&lt;/td&gt;
&lt;td&gt;Free？&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FebBox&lt;/td&gt;
&lt;td&gt;❓&lt;/td&gt;
&lt;td&gt;❓&lt;/td&gt;
&lt;td&gt;❓&lt;/td&gt;
&lt;td&gt;❓&lt;/td&gt;
&lt;td&gt;Free？&lt;/td&gt;
&lt;td&gt;❓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一些可能可用的网盘，没有过多尝试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;115：原石会员高峰期限速，挂载使用不当疑似会风控。好在原石会员便宜，当时买号时送了3天体验会员，迁移了部分文件，体验不错，后来 115 改了上传接口，当时无法挂载上传文件了。现在 API 上传已恢复，但是账号是没会员状态，我只需要它的大空间，月费会员过于昂贵，为了传几t文件花一百多充年费疑似不值得。由于有人用它来开影视站，官方目前限制卡的很死，用 cookies 挂载掉线是玄学问题。据说之后会出 API ，先观望。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;123：会员和空间挺便宜，了解不多，听说挂载使用体验还不错。分享链接会封文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;6盘：国内的一家基于 IPFS 的云盘，现在仍在整改调整中，计费计划还没出。前几天可以注册体验，注册有 12T 空间，上传流量少的可怜只有 10g ，只能用过磁力上传。现在使用的空间，等计费计划确定之后要发账单的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FebBox：空间大，之前看的时候官方客户端做的烂，且 WebDAV 不支持上传，不知道现在如何。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pikpak：会员贵，主打磁力下载，登录挂载容易掉，WebDAV 挂载有时速度很烂，会员上传和下载流量有限制，不适合高强度挂载使用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Terabox(海外百度)：听说送很多空间，还可以刷很多空间，不知道是否限速，再加上是百度加的产品，没有上手尝试。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大致上&lt;strong&gt;符合需求的网盘&lt;/strong&gt;，实际大量使用过：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OneDrive：E5 和家庭版。免费 or 便宜，挂载使用体验极好，限制不多。&lt;/li&gt;
&lt;li&gt;天翼网盘：便宜，大空间，基本不限速，资源多。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还是因为「不可能三角」原则，目前（打算）主力使用的网盘只有 OneDrive 和 天翼网盘。这两个网盘限制最少，价格最便宜，使用体验最好。可能其他网盘也很不错，考虑到文件迁移，目前精力有限只主力用过这两款。
接下来我就对这二者的使用体验和注意事项方面逐一说明我的个人经验。&lt;/p&gt;
&lt;h2&gt;OneDrive&lt;/h2&gt;
&lt;p&gt;Onedrive 分很多版本。A1 相关的 Onedrive 基本已经死绝了，E3 只有少部分人有订阅，使用最广门槛最低的只有 E5 试用和家庭版订阅了，下面详细说明这两个。&lt;/p&gt;
&lt;h3&gt;购买价格&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;E5 试用：5T *25 。现已无法免费注册，注册条件见：&lt;a href=&quot;https://learn.microsoft.com/zh-cn/office/developer-program/microsoft-365-developer-program-faq#who-qualifies-for-a-microsoft-365-e5-developer-subscription-&quot;&gt;谁有资格获得 Microsoft 365 E5 开发人员订阅？&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;家庭版：1T*6 。5 CNY/月。注册土耳其区 AppleID 并充值礼品卡，通过 App Store 订阅月付的 Microsoft 365 家庭版。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;下载和上传相关&lt;/h3&gt;
&lt;p&gt;下载体验从左至右依次降低⬇️：挂载/反代下载、rclone 配合前者 &amp;gt; 网页+多线程 &amp;gt; 客户端&lt;/p&gt;
&lt;p&gt;上传只推荐：客户端 / rclone 。（极少时候用到 本地部署 Alist，分别挂载本机存储和 od 再 copy ，下面会讲）&lt;/p&gt;
&lt;h4&gt;E5 &lt;s&gt;灵车🚗&lt;/s&gt;&lt;/h4&gt;
&lt;p&gt;现在除了无法免费注册，时不时还会有被&lt;s&gt;无差别&lt;/s&gt;封禁的风险。因此虽然有免费的大空间，但随时可能丢失数据，已经成为名副其实的&lt;strong&gt;灵车&lt;/strong&gt;了。&lt;/p&gt;
&lt;p&gt;E5 的 OneDrive 挂载到 &lt;a href=&quot;https://alist.nn.ci/zh/&quot;&gt;Alist&lt;/a&gt; 、 &lt;a href=&quot;https://github.com/qkqpttgf/OneManager-php&quot;&gt;Onemanager&lt;/a&gt; 等网盘程序获取下载直链，搭配 IDM 等多线程下载工具体验很不错。短时间内下载文件过多会报 &lt;code&gt;activityLimitReached&lt;/code&gt; ，等待 1-2h 会恢复。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;E5 只推荐挂载下载&lt;/strong&gt;。E5 的客户端直连下载很烂，用浏览器直连下载获取的直链体验也很烂。&lt;/p&gt;
&lt;p&gt;E5 OneDrive 一般不审查文件，只审查病毒/炼铜屁内容。审查 ban 掉的「疑似」含毒文件可以通过网页版取回本地，入口有些隐蔽。&lt;/p&gt;
&lt;p&gt;一些特殊拓展名文件有时也不被允许上传（极少情况一般遇不到）&lt;/p&gt;
&lt;h4&gt;家庭版🏠&lt;/h4&gt;
&lt;p&gt;购买订阅后获得一个账号的订阅以及5个订阅席位，每个账号有 1t OneDrive 。你可以注册5个小号分配席位，然后通过 &lt;a href=&quot;https://alist.nn.ci/zh/guide/drivers/alias.html&quot;&gt;Alist 别名&lt;/a&gt; 实现 6T 一样的效果。&lt;/p&gt;
&lt;p&gt;听说 API 限制比 E5 严重很多，动不动会限制。虽然不支持创建应用程序，但也可登录挂载到 Alist / rclone 等程序。偶尔会小封一下账号，手机验证码验证一下即可。&lt;/p&gt;
&lt;p&gt;家庭版疑似审查色情敏感内容，因此不要对这些文件创建分享链接，否则可能会面临很严重的封禁，不是收收验证码就可以解决的。&lt;/p&gt;
&lt;p&gt;虽然限制不少，但是比起国内网盘的文件审查可以说是几乎没有了。每月 5 元的价格提供 6T 的空间，即使是作为冷备份空间也是很不错的选择。&lt;/p&gt;
&lt;h3&gt;Cloudflare 反代下载&lt;/h3&gt;
&lt;p&gt;详细教程请参见：&lt;a href=&quot;https://anwen-anyi.github.io/index/11-durl.html&quot;&gt;AList如何使用代理下载链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;按照教程走就OK，这里记录一些你可能踩到的坑。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Cloudflare 只代理 80/443 端口。cf 反代隐写端口号域名无法访问。&lt;/p&gt;
&lt;p&gt;可能你会因为 80/443 的特殊性，单独为了部署 Alist cf 反代购买一台 vps 。这时假如你把 Alist 的端口设置到 443 ，然后又恰巧通过 cf 绑定域名并用页面规则反代隐写端口号，这时你会发现域名无法访问了，用 ip 却可以正常访问。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方法&lt;/strong&gt; ：Alist 端口改为 80 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/AlistGo/alist-proxy/blob/main/alist-proxy.js&quot;&gt;alist-proxy.js&lt;/a&gt; 相关问题。&lt;/p&gt;
&lt;p&gt;var ADDRESS = &quot;YOUR_ADDRESS&quot;;&lt;/p&gt;
&lt;p&gt;这里的 Alist 地址如果填写 ip 可能无法反代。&lt;/p&gt;
&lt;p&gt;前阵子新增了&lt;code&gt;WORKER_ADDRESS&lt;/code&gt;，填写 Worker 的地址即可。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优选域名和 IP 会影响反代下载速度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Crypt 驱动不能反代&lt;/strong&gt;，Pikpak 的 WebDAV 挂载疑似也不能反代。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;确实可以单独准备一台 vps 用来跑 cf 反代，然后通过 Alist V3 协议挂载到主力站点就OK。&lt;/p&gt;
&lt;h3&gt;OneDrive 补充&lt;/h3&gt;
&lt;p&gt;如果你试过用 Rclone 在两个 OneDrive 之间迁移文件，会发现最近 API 限速严重，无论是上传还是下载，甚至到无法使用。在我没用反代之前，只能被迫把 rclone copy 的参数改为 &lt;code&gt;--onedrive-chunk-size 150M --transfers=1&lt;/code&gt; 才能勉强使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rclone copy -v &quot;storage01:/folderA/&quot; &quot;storage02:/folderA/&quot;  -P --onedrive-chunk-size 150M --transfers=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后发现海外 VPS + cf 反代刚好解决了 OneDrive 的 API 限速问题。但是由于上传还有限制，所以速度也只是在 100Mb/s 左右，勉强能看。&lt;/p&gt;
&lt;p&gt;Rclone + OneDrive + Cloudflare 反向代理，Copy 文件到本机存储截图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/010/rclone-onedrive-proxy.png&quot; alt=&quot;rclone-onedrive-proxy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图为国内联通 300M 家宽， rclone 下载 cf 反代的 OneDrive 文件速度。&lt;/p&gt;
&lt;h2&gt;天翼云盘&lt;/h2&gt;
&lt;h3&gt;账号的选择和购买&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;空间容量&lt;/th&gt;
&lt;th&gt;传输速度&lt;/th&gt;
&lt;th&gt;单日上传总量（文件大小）&lt;/th&gt;
&lt;th&gt;单文件上传大小&lt;/th&gt;
&lt;th&gt;单次转存文件数&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;普通账号&lt;/td&gt;
&lt;td&gt;30g + 30g&lt;/td&gt;
&lt;td&gt;高峰期限速❓&lt;/td&gt;
&lt;td&gt;≤2G（可家庭中转）&lt;/td&gt;
&lt;td&gt;≤2G（可家庭中转❓）&lt;/td&gt;
&lt;td&gt;≤1000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;黄金会员&lt;/td&gt;
&lt;td&gt;2t + 2t&lt;/td&gt;
&lt;td&gt;不限速✅/❓&lt;/td&gt;
&lt;td&gt;≤200G（可家庭中转）&lt;/td&gt;
&lt;td&gt;≤200G（可家庭中转❓）&lt;/td&gt;
&lt;td&gt;≤10000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;铂金会员&lt;/td&gt;
&lt;td&gt;4t + 4t&lt;/td&gt;
&lt;td&gt;不限速✅/❓&lt;/td&gt;
&lt;td&gt;不限制&lt;/td&gt;
&lt;td&gt;不限制&lt;/td&gt;
&lt;td&gt;≤50000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;备注&lt;/td&gt;
&lt;td&gt;可买大空间账号&lt;/td&gt;
&lt;td&gt;使用不多没遇到，听说限速&lt;/td&gt;
&lt;td&gt;买号不推荐共享家庭&lt;/td&gt;
&lt;td&gt;未测试&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我目前的账号（闲鱼购买）：&lt;/p&gt;
&lt;p&gt;铂金会员 4+4 ：4T 个人 + 4T 家庭，主力盘，用来上传存文件。配合 Crypt 加密上传了很多「重要文件」。&lt;/p&gt;
&lt;p&gt;普通账号 20t ：20T 个人 + 30g 家庭。用来转存影视资源挂载播放，或者转存会员号的分享文件。&lt;/p&gt;
&lt;h3&gt;选择天翼云盘的原因&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;电信运营商网盘不怕跑路，也不像联通和移动抠抠搜搜的，网盘权益很多。&lt;/li&gt;
&lt;li&gt;有大空间和会员账号可以低价购买，不用担心实名问题。&lt;/li&gt;
&lt;li&gt;绝大多数情况下不限速，可以挂载 Alist。&lt;/li&gt;
&lt;li&gt;配合 Crypt 加密可以实现文件的安全存储，给自己的游戏和小姐姐一个安全的家🏠。&lt;/li&gt;
&lt;li&gt;可以把文件分享给其他账号，避免某个账号突然暴毙。&lt;/li&gt;
&lt;li&gt;影视资源较为丰富，挂载看视频体验很好。&lt;/li&gt;
&lt;li&gt;正常使用不乱折腾，账号很稳定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与此同时也有些许&lt;strong&gt;遗憾&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分享文件时需要先审核才能分享，不过不影响 Crypt 加密文件分享。&lt;/li&gt;
&lt;li&gt;非电信（多是移动）可能会使用速度不佳。我这边联通家宽无影响。&lt;/li&gt;
&lt;li&gt;低价购买的账号不能修改密码。❗❗&lt;strong&gt;触发验证码是高危操作&lt;/strong&gt;❗❗ 后果是账号可能会突然无法登录。&lt;/li&gt;
&lt;li&gt;家庭空间文件可以不经过审核转移到个人空间。而天翼网盘有家庭共享功能。原本可以把会员号的大家庭空间和普通账号绑定，实现会员上传再转移到普通号的大空间内。这个操作会涉及到验证码高危操作，所以尽量避开❗❗ 已经有人去试这个然后账号无法使用的例子了。当然也有人试过这个无事发生。&lt;/li&gt;
&lt;li&gt;Rclone 没有支持天翼网盘存储，几乎无法使用 Rclone 写入大文件。&lt;/li&gt;
&lt;li&gt;太多设备登录也许会掉登录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Crypt 相关&lt;/h3&gt;
&lt;p&gt;使用 Crypt 加密可以安全地在网盘放文件，但是一定要牢记 密码和盐值 ❗❗ 否则数据丢失可不是闹着玩的。&lt;/p&gt;
&lt;p&gt;Alist 的 Crypt 有性能问题，以下是我遇到的情况和问题，至今仍未找到解决方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Alist 复制文件到 Crypt 存储（网盘上传）速度有时过慢，任务有时会失败而浪费 vps 流量。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/010/Crypt%E4%B8%8A%E4%BC%A0.png&quot; alt=&quot;Crypt上传&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;本地 PC 部署 Alist 本机存储，用 rclone WebDAV 挂载 alist 复制文件到 crypt （上传）也会有性能问题。PC 的性能肯定够，但是多数上传 80 KB/s。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;VPS 上部署的 Crypt 存储下载有时会遇到性能问题，即使看不到一点点 CPU 和内存占用，有时下载速度也是几百KB/s 的龟速。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;本地 PC 部署 Alist 挂载该Crypt 存储，下载速度飞起，疑似只适合挂载到本地 PC 用。&lt;/p&gt;
&lt;p&gt;&lt;s&gt;这也相当于一种另类的网盘客户端了。&lt;/s&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Crypt 上传&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Rclone 不支持天翼网盘，而通过 Alist 复制文件到 Crypt 存储实在很难用，还是用的国内运营商网盘，本身使用移动等家宽就有可能会有网络问题，海外 vps 就更不必说了。因此我十分想通过 Rclone + Alist WebDAV 实现上传文件到 Crypt &amp;gt;&amp;gt; 天翼网盘。&lt;/p&gt;
&lt;p&gt;然而只实现了1%。&lt;/p&gt;
&lt;p&gt;以下操作除非提及使用 PC ，其余都在海外 VPS 操作。&lt;/p&gt;
&lt;p&gt;我的思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Rclone 直接挂载 Alist WebDAV，直接从其中的网盘A 复制到 其中的 Crypt，相当于从 Alist WebDAV &lt;strong&gt;网盘&lt;/strong&gt; &amp;gt;&amp;gt; Alist WebDAV &lt;strong&gt;网盘&lt;/strong&gt; ，通过 Alist 的 Crypt加密。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;WebDAV 挂载时不能挂载托管在 Cloudflare 的域名，否则会只空跑流量不传输文件，返回 Cloudflare 相关的错误，因此之后的 WebDAV 都用 ip+端口 挂载。&lt;/p&gt;
&lt;p&gt;结果：失败❌&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;WebDAV 用 ip+端口 挂载到 rclone ，仍使用上面的方案，文件有时传输到 100% 后长时间没反应，有时干脆根本不传输。&lt;/p&gt;
&lt;p&gt;结果：失败❌&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时我意识到不能直接通过 Alist 的 Crypt 加密，这个 Buff 叠的太多了，加密还要走一层 WebDAV 协议。直接在 rclone 这边加密相当于在 VPS 本机操作，更近一点应该更合理。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Rclone 直接挂载 Alist WebDAV， Alist WebDAV 里的网盘作为文件源A，在 rclone 添加一个天翼网盘的 Crypt 加密存储B，然后rclone copy A &amp;gt;&amp;gt; B，相当于从 Alist WebDAV &lt;strong&gt;网盘&lt;/strong&gt; &amp;gt;&amp;gt; Alist WebDAV &lt;strong&gt;网盘&lt;/strong&gt; ，通过 rclone 的 Crypt 加密。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;结果：失败❌&lt;/p&gt;
&lt;p&gt;这时我搜到相关帖子，想到可能是文件源的问题，可能是 Alist WebDAV 的问题，于是发现就算不走 Crypt 加密， Alist 存储间通过 WebDAV 复制都有极大概率出错。&lt;/p&gt;
&lt;p&gt;那么不走 Alist WebDAV ，而直接挂载 OneDrive 或者直接用本机存储作为文件源试试如何呢？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Rclone 挂载 OneDrive 作为文件源A，同时挂载 Alist WebDAV ，在 rclone 添加 WebDAV 里的一个天翼网盘 Crypt 加密存储B，然后 rclone copy A &amp;gt;&amp;gt; B，相当于从 OneDrive &lt;strong&gt;网盘&lt;/strong&gt; &amp;gt;&amp;gt; Alist WebDAV **网盘 **，通过 rclone 的 Crypt 加密。&lt;/p&gt;
&lt;p&gt;结果：失败❌，只有一个几百兆的小文件成功了一次，之后速度就几乎为0。此时我意识到 Alist WebDAV 用 rclone 复制，只能走 本机存储 &amp;gt;&amp;gt; 网盘。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PC / VPS Rclone 挂载 本机存储A，同时 挂载 Alist WebDAV ，rclone 添加天翼网盘的 Crypt 加密存储B，然后 rclone copy A &amp;gt;&amp;gt; B。相当于从 &lt;strong&gt;本机&lt;/strong&gt; &amp;gt;&amp;gt; Alist WebDAV **网盘 **，通过 rclone 的 Crypt 加密。&lt;/p&gt;
&lt;p&gt;结果：大文件成功✅&lt;/p&gt;
&lt;p&gt;此时我意识到本机存储上传是可行的。&lt;/p&gt;
&lt;p&gt;于是我在自己的 PC 上搭了个 Alist 又来测试，从本机存储复制到 Crypt 加密的天翼网盘，发现不会报错全部成功，但是也会出现文件传输 100% 后需要等半天才上传成功的情况，真的要等好久。我上传总大小 2g 数量为 1,294 的图片，居然花了 2.5h 。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;经过以上历程，我已经彻底放弃用 rclone Crypt 加密上传天翼网盘了，还是老老实实用 Alist 的复制功能吧。&lt;/p&gt;
&lt;h2&gt;意外收获&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;WebDAV 可能没有我之前想象中的那么好，经过深入使用，它的阴暗面已经渐渐地暴露出来了，各种权限问题真的层出不穷。例如有的云盘给 WebDAV 权限只有读取不给上传，还有各种限速问题……网盘支持挂载或者 WebDAV 速度不限制的话，肯定会被用来建影视站，网盘被薅怕了就会改政策。目前看来多家网盘都是这个结果，天翼网盘是电信家的应该不怕，现在带宽这么贵，不知道 123 网盘能撑多久。&lt;/li&gt;
&lt;li&gt;如果想通过 Alist 把本地文件上传到挂载的网盘，那么挂载本机存储，用 Alist 复制到那个网盘是更好的选择。这是除用官方客户端上传外最好的方法了，同时可以用到 Crypt 加密。使用网页端上传的同学应该深有体会，本地通过上传接口上传到 Crypt 存储，稍微大点的 4-5g 的文件几乎没有不出错的时候，而使用复制功能可以极大提高文件传输成功率。&lt;/li&gt;
&lt;li&gt;搭建好 Cloudflare 反向代理之后，发现真的很好用，几乎万物都可反代，通常在海外有非常客观的速度，可以拿来突破 OneDrive API 的下载限速问题。同时也体会了本机代理的好，像 115 这样风控严重的网盘，使用本机代理可以大大提高挂载稳定性，不会因为 302 给全球各地的 IP 而掉登录。&lt;/li&gt;
&lt;li&gt;另一种文件分享的思路。通过 Crypt 加密上传到天翼等网盘，然后分享加密后的文件，附上加密和盐值。拿到文件的用户，自行解密使用。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Kiramux</author></item><item><title>前端笔记 - Form Input Wave</title><link>https://notes.kiramux.com/posts/011-flontend-form-input-wave</link><guid isPermaLink="true">https://notes.kiramux.com/posts/011-flontend-form-input-wave</guid><pubDate>Sat, 15 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;效果&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../images/011/formInputWave.png&quot; alt=&quot;formInputWave&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Sayafx/50-projects-50-days/tree/main/Form_Input_Wave&quot;&gt;代码地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://flosover.vercel.app/Form_Input_Wave/&quot;&gt;Live Demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;实现思路以及用到的东西&lt;/h2&gt;
&lt;p&gt;本身是 flex 布局的一个小表单项目，记录一下某些细节的实现方式。&lt;/p&gt;
&lt;h3&gt;Input 的样式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 点击 input 或焦点在 input 上时，去掉描边并更改颜色，用两个伪类实现 */

.form-container input:focus,
.form-container input:valid {
    outline: 0;
    border-bottom: 2px lightblue solid;
}

.form-container input {
    display: block;
    background-color: transparent;
    width: 100%;
    border: 0;
    border-bottom: 2px #fff solid;
    padding: 15px 0;
    font-size: 18px;
    color: #fff;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;:focus&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;表示获得焦点的元素（如表单输入）。当用户点击或轻触一个元素或使用键盘的 Tab 键选择它时，它会被触发。&lt;/p&gt;
&lt;p&gt;https://developer.mozilla.org/zh-CN/docs/Web/CSS/:focus&lt;/p&gt;
&lt;p&gt;&lt;code&gt;:valid&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;表示内容验证正确的&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input&quot;&gt;&lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;&lt;/a&gt; 或其他 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/form&quot;&gt;&lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;&lt;/a&gt; 元素。这能简单地将校验字段展示为一种能让用户辨别出其输入数据的正确性的样式。&lt;/p&gt;
&lt;h3&gt;Label  文字波动上移&lt;/h3&gt;
&lt;p&gt;部分 HTML 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form&amp;gt;
    &amp;lt;div class=&quot;form-container&quot;&amp;gt;
        &amp;lt;input type=&quot;text&quot; required&amp;gt;
        &amp;lt;label&amp;gt;Email&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;form-container&quot;&amp;gt;
        &amp;lt;input type=&quot;password&quot; required&amp;gt;
        &amp;lt;label&amp;gt;Password&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;button class=&quot;btn&quot;&amp;gt;Login&amp;lt;/button&amp;gt;
    &amp;lt;p class=&quot;text&quot;&amp;gt;
        Don&apos;t have an account? 
        &amp;lt;a href=&quot;#&quot;&amp;gt;Register&amp;lt;/a&amp;gt;
    &amp;lt;/p&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;部分 CSS 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.form-container label {
    position: absolute;
    top: 15px;
    pointer-events: none;
    /* 加了上面这行，鼠标才能穿透label点击input */
}

.form-container label span {
    display: inline-block;
    font-size: 18px;
    min-width: 5px;
    transition: 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
/* 对 span 设置了 inline-block 后，下面的 transform 才生效 */

.form-container input:focus ~ label span,
.form-container input:valid ~ label span {
    transform: translateY(-30px);
    color: lightblue;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JavaScript 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const labels = document.querySelectorAll(&apos;.form-container label&apos;)

labels.forEach(label =&amp;gt; {
    label.innerHTML = label.innerText
        .split(&apos;&apos;)
        .map( (letter,idx) =&amp;gt; `&amp;lt;span style=&quot;transition-delay:${idx * 30}ms&quot;&amp;gt;${letter}&amp;lt;/span&amp;gt;`)
        .join(&apos;&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码为表单 label 标签中的每个字母创建独立动画效果：&lt;/p&gt;
&lt;p&gt;将标签文本拆分为单个字符，每个字符包裹在带有递增过渡延迟的&lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;中，最终配合 CSS 形成波浪式动画效果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JavaScript 笔记&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DOM选择&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;document.querySelectorAll(&apos;.form-container label&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特性&lt;/strong&gt;：&lt;code&gt;querySelectorAll&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：CSS选择器获取节点列表&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文档&lt;/strong&gt;：&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Document/querySelectorAll&quot;&gt;querySelectorAll - MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;遍历元素&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;labels.forEach(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特性&lt;/strong&gt;：&lt;code&gt;NodeList.prototype.forEach&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：遍历类数组对象&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文档&lt;/strong&gt;：&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/NodeList/forEach&quot;&gt;NodeList.forEach - MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;文本处理&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;label.innerText.split(&apos;&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特性&lt;/strong&gt;：&lt;code&gt;String.prototype.split&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：字符串转字符数组&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文档&lt;/strong&gt;：&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/split&quot;&gt;String.split - MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;引号内是用于分割的 separator ，省略 separator （即：&lt;code&gt;‘’&lt;/code&gt;）或者传递 &lt;code&gt;undefined&lt;/code&gt; 会返回只包含所调用字符串的数组，比如这个项目的效果是 [&quot;E&quot;, &quot;m&quot;, &quot;a&quot;, &quot;i&quot;, &quot;l&quot;]。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数组转换&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;.map((letter,idx) =&amp;gt; `...`)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特性&lt;/strong&gt;：&lt;code&gt;Array.prototype.map&lt;/code&gt; + &lt;strong&gt;箭头函数&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：数组元素批量转换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文档&lt;/strong&gt;：&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/map&quot;&gt;Array.map - MDN&lt;/a&gt; | &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions&quot;&gt;箭头函数 - MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 正确写法（保留前两个参数占位）
.map((element, index, originalArray) =&amp;gt; {
    console.log(originalArray) // 原始数组的引用
    return element * originalArray.length
})

// 或
// 只使用元素
[1,2,3].map(n =&amp;gt; n*2) 

// 使用元素和索引
[1,2,3].map((n, i) =&amp;gt; n + i)

// 使用全部三个参数
[1,2,3].map((n, i, arr) =&amp;gt; n + arr.length)

// 如果不需要某些参数
.map((_, _, arr) =&amp;gt; arr.join(&apos;&apos;)) // 用下划线占位
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Array.prototype.map&lt;/strong&gt; 里的三个参数不能换位置，后面两个参数可选，但是如果用数组参数，就必须要留 index 的位置，用 &lt;code&gt;_&lt;/code&gt; 占位。&lt;/p&gt;
&lt;p&gt;箭头函数用 &lt;code&gt;{}&lt;/code&gt; 就必须有 return
只有一个变量时可以省略 &lt;code&gt;()&lt;/code&gt;
只有一条表达式时可以省略 &lt;code&gt;{}&lt;/code&gt; ，且不要 return&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;模板字符串&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;`&amp;lt;span&amp;gt;${letter}&amp;lt;/span&amp;gt;`
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特性&lt;/strong&gt;：&lt;strong&gt;模板字面量&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;作用&lt;/strong&gt;：动态字符串构建&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;文档&lt;/strong&gt;：&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literals&quot;&gt;模板字符串 - MDN&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：&lt;code&gt;${}&lt;/code&gt; 中可以写哪些内容？&lt;/strong&gt;
A：任何合法 JavaScript 表达式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;`计算：${2 + 3 * 4}` // &quot;计算：14&quot;
`时间：${new Date().toLocaleTimeString()}`
`嵌套：${`子模板：${value}`}`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Q：如何输出反引号？&lt;/strong&gt;
A：使用转义符：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;`这是一个\`反引号\`` // &quot;这是一个`反引号`&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Q：模板字符串会执行HTML转义吗？&lt;/strong&gt;
A：不会！需注意XSS防护：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 危险示例
const userInput = &apos;&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&apos;;
document.body.innerHTML = `${userInput}`; // 会执行脚本

// 正确做法：使用textContent替代innerHTML
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 危险！用户输入可能包含恶意脚本
element.innerHTML = `&amp;lt;div&amp;gt;${userInput}&amp;lt;/div&amp;gt;`;

// 正确做法：转义特殊字符
const safeHTML = `&amp;lt;div&amp;gt;${escapeHTML(userInput)}&amp;lt;/div&amp;gt;`;

function escapeHTML(str) {
    return str.replace(/&amp;amp;/g, &apos;&amp;amp;amp;&apos;)
             .replace(/&amp;lt;/g, &apos;&amp;amp;lt;&apos;)
             .replace(/&amp;gt;/g, &apos;&amp;amp;gt;&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数组合并&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;.join(&apos;&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特性&lt;/strong&gt;：&lt;code&gt;Array.prototype.join&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：数组转字符串&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文档&lt;/strong&gt;：&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/join&quot;&gt;Array.join - MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;[1,2,3].join(&apos;-&apos;) // &quot;1-2-3&quot;
[1,2,3].join(&apos;💡&apos;) // &quot;1💡2💡3&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DOM更新&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;label.innerHTML = ...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特性&lt;/strong&gt;：&lt;code&gt;Element.innerHTML&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：解析HTML字符串&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文档&lt;/strong&gt;：&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Element/innerHTML&quot;&gt;Element.innerHTML - MDN&lt;/a&gt; | &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/innerText&quot;&gt;Element.innerText - MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;innerHTML 包含 HTML 标签，而 innerText 剔除 HTML 标签，只保留文本，表示一个节点及其后代所渲染文本的内容。&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>随笔一则-250416</title><link>https://notes.kiramux.com/posts/012-diary-250416</link><guid isPermaLink="true">https://notes.kiramux.com/posts/012-diary-250416</guid><pubDate>Wed, 16 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近过的十分混乱十分魔幻，这一周通了不少宵，短短一周一共七天，居然通宵四次 😱，直到现在也没缓过来。一直想找个时间写点东西，没想到过了两天，又有更多想写的东西了。&lt;/p&gt;
&lt;p&gt;自从月初从清明假期结束后，在家恣意休息，落下很多进度，于是回到学校之后开始恶补，特别是设计课的项目。&lt;/p&gt;
&lt;p&gt;由于想写的太多了，先把想写的话题分开写。&lt;/p&gt;
&lt;h2&gt;//关于生活和学习/工作的界限&lt;/h2&gt;
&lt;p&gt;高中毕业后，我最先想到的就是购买一部属于自己的电脑，然后得到之后，开始了我的电脑人之旅。本身对于 PC 比较感兴趣，于是找来各种各样感兴趣的地方研究。同时也比较喜欢画画，于是经常在休息的时候看一些绘画视频/教程观摩学习。因为这些玩的过程很有意思，于是经常不感到疲倦，同时感到很快乐，时间全花在上面，然后游戏也没啥时间玩了，或者说不怎么想到去玩。&lt;/p&gt;
&lt;p&gt;自己杂七杂八的研究这个研究那个，可能确实是懂一些，然后日常各种工作都更轻松更舒服一些，但是直到最近，我突然意识到，自己好像没什么能拿得出手的项目。或者换句话说，无法证明自己的能力。最简单的体现，除了自己的主修课程，其余方向上没有任何一个方向“有所成就”，连半份简历都凑不出来。最近越来越接近毕业，也是要提早考虑就业问题，于是想找方向主要学习，然后做些项目出来。&lt;/p&gt;
&lt;p&gt;然而这么想着开始学习时，忽然发现这种学习侵占自己的生活太多，几乎没有生活的存留空间了。我现在还是没办法很好地划分学习和生活的界限。之前是觉得“这个不错啊，来做吧！”，于是废寝忘食地去做。然后有些累了或者疲倦了，又遇到其他方面的不错的东西，于是抛下之前的事去做别的。有点朝三暮四的感觉。&lt;/p&gt;
&lt;p&gt;学了建筑学之后，我在一学期内就领悟到了一个道理：在 deadline 之前，工作是永远做不完的，除非到了临近日期不得不提交成果，否则工作甚至可能会越做越多。于是我决定一定要将学习和生活分得开。前阵子 C 来到我们专业教室，评价我们上学像上班……每个人坐一个工位，然后来了就开始对着一块屏幕做自己的工作。&lt;/p&gt;
&lt;h2&gt;//关于认真生活&lt;/h2&gt;
&lt;p&gt;我曾经做了许多计划，几月中旬、几月初什么工作做到什么程度，什么内容学到什么进度，然而总是计划赶不上变化，于是我寻找到了原因：这些计划与我的休息和生活冲突，特别是在假期的时候。&lt;/p&gt;
&lt;p&gt;于是我假期用来休息。&lt;/p&gt;
&lt;p&gt;我的家乡在某高考大省，从小被灌输的观念都是学习第一重要，休息什么的都靠边站。以至于我在转专业之后，面对课业压力，被迫也在以这种方式推着自己走。最近越来越注意到，认真体会生活，甚至学习生活中各种技能，也是生活中非常重要的一部分。&lt;/p&gt;
&lt;p&gt;前面提到，我在假期休息。放在曾经被灌输的观念里，“假期才应该多学习、弯道超车”等等，但我觉得假期休息是必要的，学会生活在人生中也是非常重要的。或许我也还是在自己推着自己走，才会有“假期不学习是不是浪费”这种想法，我在潜意识里可能还是在这么想。但是又从另一个角度来看，我在这个假期陪伴家人，探访亲戚，练习开车，这些也很重要。现在就业形势那么严峻，面向就职多学一些未必会和其他应届生做更多区别，未必会有更好的工作选择，但是给自己疲惫的身心休息时间，以及给家人朋友多点陪伴或许更有价值。毕竟家人会陪自己一辈子，朋友也会陪自己走尽可能久，再看看那&lt;strong&gt;沟槽的工作&lt;/strong&gt;呢，它值得你为了它而疏于生活，疏于陪伴家人朋友吗？&lt;/p&gt;
&lt;h2&gt;//关于恋爱剧情&lt;/h2&gt;
&lt;p&gt;上文说到 C 来我这边找我了。在她来我这边找我的前一天，其实我刚跟朋友出去通宵唱 K 去了，睡到中午准备开始下午的画图。进入状态没多久，忽然一条信息弹出，“来不来喝酒”。她怎么来我这边了我去。于是光速收拾好去见她。关于她来的理由我其实挺不能接受的，她其实是“失恋”了顺便来找我的，只是因为我在同一个城市。&lt;/p&gt;
&lt;p&gt;我当然是心情很复杂，但还是决定一起去附近的景区逛逛。一开始空气真的有点安静，不知道彼此之间要说些什么，刚好碰到了那个景区举办小游戏活动，我们一人一本小册子，去游景区做小游戏集印章。我们两人在一起手捧着本小册子，一边看前面的人表演一遍排队，真的很有约会的感觉。&lt;/p&gt;
&lt;p&gt;多亏了这个集印章小游戏，让我们有了游园的理由和目标，不再有那种尴尬的气氛，玩了小游戏也比较放的开了。我们写了毛笔字，抢粮票，还给她录了她跳舞的视频。然后在一遍找集印章地点时，我们还同时在找可以出片的场景，陪她拍照。当时我有些恍惚，在其他人眼里，我们是不是很像情侣。不过拜她所赐，经过她的指导，之前没什么拍照经验的我也能拍出很不错的照片，她看起来很高兴。&lt;/p&gt;
&lt;p&gt;景区闭园之后，我们找了一家小酒馆，顺便叫了另一个人，于是我们三人开始喝酒，玩骰子。喝着喝着，他们可能喝醉了，于是开始大聊情感话题，我就在一边装睡一边听，同时还要注意不让他们上头接着点酒喝，中途她居然还把她房间号告诉了我。&lt;/p&gt;
&lt;p&gt;这些都结束之后，我们出门走走，在河边看到了很圆的月亮，我想起了《I/O》和《月姬》的剧情，同时也在回忆今天的经历。我真的很喜欢月亮，不仅仅是那处风景，也很喜欢月亮虚无缥缈的意象。于是我拍下了一张照片留念。之后 C 又点了点东西吃，我们在河边又聊了很久，这种感觉真的很久违，我突然想到高中的时候，我唯一一次生病请假，我妹妹邀请我去外面散步走走，真的很突然，她从来没有做过类似的事，当然我也没经历过。那天也是圆圆的月亮，路上几乎没有行人，我们在路灯下走着不断聊天……我忽然又恍惚自己是在当年。这些经历给我的感受真的很相像，应该会让我永生难忘。&lt;/p&gt;
&lt;p&gt;那天我为她拍了很多照片，凌晨我翻着相册，看着手机荧幕中的她，觉得挺可爱，第一次萌生了想要主动备份相册的念头。&lt;/p&gt;
&lt;h2&gt;//GalGame 的价值&lt;/h2&gt;
&lt;p&gt;从所谓“第三人称视点”来看前几天发生的事，是不是可以这样总结：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;某天我在疲于工作要死要活时，突然收到了失恋女孩的一则消息，她突然来到了我在的城市，问我要不要出来喝酒。&lt;/p&gt;
&lt;p&gt;接着我们莫名其妙地像情侣一样在景区给对方拍照、合影、做游戏集印章……接着又去喝酒，互相倾诉情感事迹，甚至她还告诉了我那个神秘数字……&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这情节修改一下甚至完全可以当作某废萌 GalGame 作品的共通线剧情了，甚至很俗套。&lt;/p&gt;
&lt;p&gt;事实上陪 C 出门那天，我其实特别忙，因为我已经花掉一天去陪朋友出去场 K，然后第二天其实要课设预答辩，而我的进度几乎没有推进……然后我犹豫了一小会儿，还是决定陪她出去玩。不涉及男女之情，我还是觉得朋友比较重要，狗屁课设作业算什么。&lt;/p&gt;
&lt;p&gt;之前看到一篇文章标题是“GalGame 的价值”，虽然那个作者写了什么内容我已经记不得了，但我经常会问自己，玩了很多 GalGame，对于我来说，什么是 GalGame 的价值。在那天我和 C 出门约会归来之后，我又开始思考这个问题。我觉得 GalGame 的价值最大在于，它让我认识到应该以真诚的态度对待自己身边的人。我玩过一些比较“黑深残”的作品，你会发现在这些表面看上去很猎奇很残忍的故事背后，绝大多数的作者都还是在宣扬倡导最朴素的正能量价值观。例如《素晴日》、《寒蝉》、《海猫》、《月姬》等。特别是在被以“血池轮回”称号冠之的十二神器之作《3days》，在最后抵达真实之扉时，仍然是为了友情和爱情，在与邪恶斗争。也是玩过这个游戏之后，我不知不觉中把 C 看作自己人生中的“藤见环”。她是亲密的朋友，是青梅竹马，是朋友之上无所不言的关系，却并非一定要是男女朋友，只不过我不能忍受她受委屈。&lt;/p&gt;
&lt;h2&gt;//有能力有水平的老师，真心向我们传授经验&lt;/h2&gt;
&lt;p&gt;这学期我有一个很严重的失误：有一门很重要很精彩的课，我用没选课的借口少上了三节课。&lt;/p&gt;
&lt;p&gt;哪门课是一门实务课，我本以为也是水课，直到去了才知道这门课真的很有用，是请了一位相对于设计院老板的女士来给我们讲课。我前阵子在 B 站上听了雷军班班会的讲话，深有体会并且受益匪浅。像这种级别的老师肯平下心来平等并无保留地对我们讲授经验，真的是很难得的机会。她的这门课会给我们讲几乎所有建筑学在工作单位会遇到的实务问题，还有相关考研考证的推荐。同时每位同学还要去讲自己对于建筑学的见解，以及自己将来的打算。我很重视这次机会，于是好好准备了一下，这也是我第一次如此重视如此认真地去准备一次 presentation ，同时也是一个了解同学们想法的很好的机会。&lt;/p&gt;
&lt;p&gt;这位老师在昨天最后一节课上，对我们讲了很多她想对当年的自己讲的一些话和经验，如果当年有人能对她讲这一番话，她肯定会比现在更好一些。我最近其实也有点丢失方向，也真的很期望有人能对我讲一些这样的话。刚好这位老师有水平、有经验、不摆架子。&lt;/p&gt;
&lt;p&gt;其中提到几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要不断学习并舍得对自我投资&lt;/li&gt;
&lt;li&gt;学习从书和知识中寻找解决方案，历史/经济/心理学&lt;/li&gt;
&lt;li&gt;合理安排时间&lt;/li&gt;
&lt;li&gt;投资的一些建议&lt;/li&gt;
&lt;li&gt;多一些准备，多一些退路，没有一个事业可以干 30 年之久&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我想等我消化一下之后，再专门找个时间整理一下。&lt;/p&gt;
&lt;p&gt;在课堂中，这位老师是真的为我们好，希望我们能听进去，未来发展的更好，看待事物也不带偏见，指出她同期的同事不思进取的桎梏所在。我又想到之前测量学的老师，他也是一位很能抓得住机会和风口的老师，很有水平，只不过他现在在我们学校执教“养老”，看他也活的很豁达。他也是很真诚地为我们做建议、指路。说实话能遇到这样的老师真的很好，而且也可以算是我的引路人，不至于让自己彻底迷失方向。其实在高中毕业之后，我就很少有明确的目标和憧憬的人了，即使有憧憬的人（或者说是当作目标的人），在和他们熟识之后，也会发现他们是普通人，也有擅长和不擅长的事，甚至还会想，自己努力到他们那种程度，又如何呢……&lt;/p&gt;
&lt;p&gt;不过有引路人已经是一件很好的事了，总好过周围一堆烂人。&lt;/p&gt;
&lt;p&gt;有时候看着这个沟槽的世界，本来都觉得一篇黑暗了，什么傻逼，但是想想自己周围的朋友，他们都是很纯粹的人，又觉得世界上还是有很多不错的好人的，也不至于那么遭。这也是我经常给自己充电的方式。&lt;/p&gt;
&lt;p&gt;//时间安排&lt;/p&gt;
&lt;p&gt;这点之后在写吧，时间有些不够了。&lt;/p&gt;
&lt;h2&gt;//画画&lt;/h2&gt;
&lt;p&gt;最近突然又重拾画笔，又有了画画的想法、很想设计自己的人物了。今天画了&lt;a href=&quot;https://vndb.org/v96&quot;&gt;《I/O》&lt;/a&gt;中的角色&lt;strong&gt;朔夜&lt;/strong&gt;，把&lt;strong&gt;赛博世界&lt;/strong&gt;中的角色留在了&lt;strong&gt;现实世界&lt;/strong&gt;中。打算再找个时间用冷裱膜和胶带把它做成色纸 🥰。&lt;/p&gt;
&lt;p&gt;画画之魂又重燃了 🥰&lt;/p&gt;
&lt;p&gt;![sakuya]../images/012/sakuya.jpg)&lt;/p&gt;
&lt;p&gt;![sakuya]../images/012/sakuyaPaint@0.5x.jpg)&lt;/p&gt;
&lt;p&gt;&lt;em&gt;左侧重颜色不小心加了太多 TT&lt;/em&gt;&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>绘画总结（上）</title><link>https://notes.kiramux.com/posts/013-sketch-learning-in-30days</link><guid isPermaLink="true">https://notes.kiramux.com/posts/013-sketch-learning-in-30days</guid><pubDate>Wed, 13 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前阵子报名了毛厂速写绘画的速写团练60天。经过30天的训练，已经卓有成效。&lt;/p&gt;
&lt;p&gt;训练之前：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/013/image-20250814170534305.png&quot; alt=&quot;image-20250814170534305&quot; /&gt;&lt;/p&gt;
&lt;p&gt;训练之后：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/013/0813-day34.jpg&quot; alt=&quot;0813-day34&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在看看30天之前画的画真的很愚蠢……&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;“课程”介绍&lt;/h2&gt;
&lt;h3&gt;特色和优点&lt;/h3&gt;
&lt;p&gt;以下评价仅针对于 3 期，个人观点仅供参考。&lt;/p&gt;
&lt;p&gt;不同于另一家「xx班长」的速写团练只有录播课，毛厂的速写团练可以算成是一门课程。&lt;/p&gt;
&lt;p&gt;课程十分便宜只要299，性价比很高，我认为薄纱了很多几千块报名费的课程。团练每天都有直播带画，会讲很多知识（人体、如何画角色、黑白灰关系、画面构成设计）。每周六是图包拍摄，可以看到真实动作是如何来的，与摆拍有什么区别。周日是很牛的嘉宾分享课~~（我一般当成鉴赏课来听）~~。&lt;/p&gt;
&lt;p&gt;课程结束后会有奖金瓜分和纪念品。人太多了奖金应该只有一点点，所以可以忽略。打卡满60天会送一个很有意思的吧唧。&lt;/p&gt;
&lt;p&gt;老师讲的内容和绘画理念很好，不会误人子弟。&lt;/p&gt;
&lt;h3&gt;一些不足&lt;/h3&gt;
&lt;p&gt;团队管理运营人手不足，但是不影响上课。我在报3期进课程群时审核很慢，可能是报名人数是在太多了。上课使用的是「小鹅通」这个平台，发展还不太成熟，界面逻辑有待优化，不过网络还可以（如果平台没有更新的话）。&lt;/p&gt;
&lt;p&gt;有一点点学生饭圈化的感觉。&lt;/p&gt;
&lt;h2&gt;学习心得&lt;/h2&gt;
&lt;h3&gt;准备工作&lt;/h3&gt;
&lt;p&gt;软件等配置和老师尽可能完全一致。绘画软件、数位板、软件布局，为的是能100%复刻老师的各种操作，降低软件操作的学习成本。同时也顺便学习到老师使用软件的习惯、工作区安排和软件操作，可以参考老师平时的绘画流程。&lt;/p&gt;
&lt;p&gt;一台PC | 数位板、数位屏 | sai2 | pureRef（参考图软件）&lt;/p&gt;
&lt;p&gt;调整 sai2 ，分别为以下工具设置合适的快捷键。&lt;/p&gt;
&lt;p&gt;由于我已经习惯了 [A]减小画笔尺寸、[D]增大画笔尺寸，所以我的快捷键可能有些奇怪。&lt;/p&gt;
&lt;p&gt;使用最基本的 3 种笔刷：&lt;/p&gt;
&lt;p&gt;[B]铅笔、[Z]喷枪、[C]水彩笔（涂抹用）。&lt;/p&gt;
&lt;p&gt;[S]选区笔、[F]选区擦、[W]魔棒&lt;/p&gt;
&lt;p&gt;[R]扩大选区1px、[T]缩小选区1px&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/013/image-20250814171720695.png&quot; alt=&quot;image-20250814171720695&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为什么要设置快捷键？&lt;/p&gt;
&lt;p&gt;当熟悉快捷键之后，操作效率是非常高的，可以在1s内迅速选区然后用喷枪图几笔。如果不为经常使用的工具设置快捷键，画几笔就需要去选其他工具或笔刷，会经常性地打断绘画，浪费了时间，甚至绘画思路也会丢掉，所以要保证自己的笔一直在画画！！&lt;/p&gt;
&lt;p&gt;下图我做了以下操作：
&lt;strong&gt;[S]选区笔&lt;/strong&gt;封闭线稿 -&amp;gt; &lt;strong&gt;[W]魔棒&lt;/strong&gt;选择区域 -&amp;gt; **[R]**扩大选区 -&amp;gt; &lt;strong&gt;[Z]喷枪&lt;/strong&gt;填充&lt;/p&gt;
&lt;p&gt;如果非常熟练的话，这个过程可以很快完成，效率很高！&lt;/p&gt;
&lt;p&gt;把喷枪调大，填充非常方便，可以很好的控制深浅，也可以在选区边缘轻轻蹭，实现渐变效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/013/paint.gif&quot; alt=&quot;GIF&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;基础训练&lt;/h3&gt;
&lt;p&gt;起稿不用太在意型准，但需要特别注意面积和比例正确。需要特别注意整体！所有的「正确」和细节都需要为整体让步。&lt;/p&gt;
&lt;p&gt;随意画的草稿比仔细勾线的线稿更好看，很大程度上是因为勾线时追求精致，放大到局部去画，忽略了整体。因此忘掉勾线这个步骤，换为“细化造型”。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;选 sai2 最普通的铅笔笔刷，把笔刷调整到和躯干、四肢差不多的粗细。观察整体，用色块起稿画出相似的趋势。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;补上轮廓线，这一步需要注意各个体块的前后关系（谁在谁的前面？），依旧不用注意型准。可以先重叠起来画，再把被遮挡的部分擦掉。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​	[每一步都需要检查一下比例和趋势（斜度），选区后用 Ctrl + T 调整]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;新建一层，重新画一遍&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果参考图的人物不是失衡状态，则人物的重心一般在两脚之间。从头顶向下拉一条竖直的线可以找到重心。&lt;/li&gt;
&lt;li&gt;一定要观察并检查画面的整体比例、面积、以及各部位的前后遮挡、穿插关系。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以下截图来自老师直播带画。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/013/image-20250813164645702.png&quot; alt=&quot;基础绘画步骤1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/013/image-20250813163213113.png&quot; alt=&quot;基础绘画步骤2&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;纠结型准？&lt;/h3&gt;
&lt;p&gt;什么是型准？为什么要追求型准？型准的目的是什么？&lt;/p&gt;
&lt;p&gt;我们所有的练习最终都是为画面和创作服务。我们可以拥有画得像能力，但不需要苛求每次画画都做到和原图 90% 甚至 99% 相似，这只是平面思维。再换个角度想想，我们创作找参考时绝对找不到一模一样的参考图，我们拿到参考之后一定会对它做处理，旋转角度或是增减零件，所以追求从平面上画得一模一样对创作来说意义不大。追求平面型准只是一种能力，但不是那么地必要，除非你想要把自己变成一台照相机。然而现在相机已经足够多了，甚至现在 AI 文生图、图生图的能力越来越强，我们或许真的应该想想自己追求的是什么。&lt;/p&gt;
&lt;p&gt;我们需要引入&lt;strong&gt;平面思维&lt;/strong&gt;和&lt;strong&gt;三维思维&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/013/image-20250813164333193.png&quot; alt=&quot;image-20250813164333193&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/013/80488bbdbf76e6d23bde1a722cb7a84c.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;感性&lt;/strong&gt;： 我们可以把眼睛看到的画面看作是2d图形，它是感性的。我们看到上图中某个图形（剪影）会想到什么？上图的每一个作品都没有画出任何 3d 结构，但我们的大脑仍然大致可以看出（我们认为的）物体，可能是人物、楼梯、城市，或者某个场景。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;理性&lt;/strong&gt;： 我们学习透视，或者 3d 美术，或者游玩 3d 游戏，会了解到三维的模型经过渲染器渲染，最终会输出为屏幕上的 2d 画面，所以理论上来说一切画面都可以通过 3d 的思维来实现。&lt;/p&gt;
&lt;p&gt;用纯图形（感性）的思想，上手简单直观，但易遇到瓶颈。有人经常会发现自己可能已经画得很不错了，但仍缺些什么。信息量不足？如何添加细节才更合理？什么是按照结构来画，结构要怎么确定？同时我们也会发现有些人物或物体角度、有些场景很难画出来，或者画出来不太真实。&lt;/p&gt;
&lt;p&gt;3d 透视有标准答案，它是理性的，它绝对「正确」，但不灵活，它不像我们手持一支铅笔就可以在纸上很快地涂鸦出画面，3d 需要我们摆模型、设置摄影机和光源……只用三维的思想很难快速地表达我们的创作想法，它在限制我们的表达，用三维的思想直接构图对人的要求特别高。而且一般来说，直接用 3d 方式出图，效果总没有平面出图来的自然且有灵性。通过「绝对」正确的计算出来的内容「绝对」正确，十分经得起检验，奈何它一般都不好看。&lt;/p&gt;
&lt;p&gt;所以我们自然而然地想到：可以先通过感性（平面）画出草稿框架，再给其中填充透视。这条路径是过度十分顺滑且低耗的，也同时继承了这两种思维的优点。&lt;/p&gt;
&lt;h3&gt;基础进阶&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../images/013/b874f50ddc82d90e1a5e5ea8381ceacb.jpeg&quot; alt=&quot;b874f50ddc82d90e1a5e5ea8381ceacb&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一些补充说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;观察上图，小臂可以理解为一段圆柱和一段长方体的平滑过渡，不要去研究小臂的肌肉和骨骼，纯纯浪费时间，只要记住这个形状即可。&lt;/li&gt;
&lt;li&gt;一定要记得画“护腕”，给手腕留位置。手掌通常会和小臂末端稍微错开~~（往哪边错开我忘记了）~~。&lt;/li&gt;
&lt;li&gt;不要在意胯部和大腿根部如何连接，先画重合在一起，再补成圆柱，最后根据遮挡关系擦掉多余的线，再把没接上的地方接上，就已经很逼真了。&lt;/li&gt;
&lt;li&gt;在画腿部形状的时候，连接处最好留出一些位置，并且大小腿也会向某个方向错开~~（不好意思我又忘掉了）~~，具体观察照片。&lt;/li&gt;
&lt;li&gt;人体四肢的边线通常是一段曲一段直&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你难以判断手臂或者腿的截面的俯仰关系，或者很难画出来它的透视线，那么这说明你可能不会画各个角度的圆柱体。这是绘画的基本功，应当多加练习。&lt;/p&gt;
&lt;h3&gt;观察方法&lt;/h3&gt;
&lt;p&gt;&amp;lt;img src=&quot;../images/013/image-20250813171150657.png&quot; alt=&quot;image-20250813171150657&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;不推荐使用九宫格计算检查画面！画得一模一样没有意义。&lt;/p&gt;
&lt;p&gt;观察方式，&lt;strong&gt;多点位对比&lt;/strong&gt;。不要绝对的准确，只要相对关系准确。&lt;/p&gt;
&lt;p&gt;比如下图，我们只要注意观察的点位与其他点位的&lt;strong&gt;相对位置关系&lt;/strong&gt;正确即可。&lt;/p&gt;
&lt;p&gt;戴手表的手腕在另一只手腕的偏左侧一点、绿色的肘部比蓝色的肩膀偏左一点、抬起的脚底面在粉色手腕的左边……人体的趋势和比例正确，重心在两腿之间，再通过许多点位快速检查位置关系，你就画出照片中的动作了。&lt;strong&gt;不需要一模一样。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;../images/013/image-20250813171641557.png&quot; alt=&quot;image-20250813171641557&quot; style=&quot;zoom:67%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/013/image-20250813171321639.png&quot; alt=&quot;image-20250813171321639&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;层次关系大于体积关系&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;物体之间的层次关系一定要画对，哪怕不画透视只画几个矩形，也要比“层次画错的、完成度高的画”要好。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/013/image-20250813172647056.png&quot; alt=&quot;image-20250813172647056&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;其他起稿方式&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;油烟管道“螺旋升天”&lt;/strong&gt;：通过不断画圆来暗示透视线，更方便画一些比较刁钻的透视。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/013/image-20250814173951468.png&quot; alt=&quot;image-20250814173951468&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;T字&lt;/strong&gt;：适合常规一些的透视。找到肩线和胯线，再找到中线（或者脊柱线）。不太适合画大透视。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/013/a640f52e2b57bc1bcf538ea4bb34ac0e.jpeg&quot; alt=&quot;a640f52e2b57bc1bcf538ea4bb34ac0e&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;”势“&lt;/strong&gt;：我理解的是一种 gesture drawing，先忽略具体的透视和形状，去感受整个人体的走势，用线画出来，一般比较奔放粗犷，动感很强，是我喜欢的风格。根据“势”可以更方便地做出夸张。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/013/09b9f6018edc2441ccab1ad0c8a96dc8.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/013/15f683f72a973fb865ea0d3e234a5a16.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/013/a3aff8bda50dbd1bb6fb6cf8802c2d92.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/013/299055889217d6c4def494b1c4bdd886.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;众多起稿方式都很不错，不能评判孰优孰劣，也可以混合使用或者互相检查。只有在合适的场景使用它们才能发挥最大作用。&lt;/p&gt;
&lt;h3&gt;重要绘画小技巧&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;参考图不需要很清晰，甚至很模糊更好。画速写看的是整体关系，不需要看细节，关注细节只会影响整体的观察和绘画进度。&lt;/li&gt;
&lt;li&gt;不用在意胸腔和盆腔的连接，大致的比例和位置对了，直接用线连上即可。因为腹部都是软的肉，本身就可以变形，只要不是太离谱都说的过去。&lt;/li&gt;
&lt;li&gt;膝盖方向通常和脚尖方向一致&lt;/li&gt;
&lt;li&gt;在画手腕时，最好留一个”护腕（手表）“的位置，这是一个好习惯&lt;/li&gt;
&lt;li&gt;画手其实和画人物整体的方式一样，先画手掌「方形」平面，然后手指使用宽度相当的色块起稿，最后勾线时手指当作2节来画。大拇指需要先在手掌一侧加一个三角形，再加一节大拇指。&lt;/li&gt;
&lt;li&gt;画脚时先画脚的两个底面（圆形），然后再和脚踝连接&lt;/li&gt;
&lt;li&gt;宁脏勿净，多画&lt;strong&gt;结构素描&lt;/strong&gt;，虽然画完不能当作作品，但是可以练到超级多的东西。要敢于去画，不要担心画错，只有参考一条错的线，才可能画出一条更正确的线，所以一开始肯定画的线没有一条是正确的。少按撤回键，多用橡皮。把透明度调低调出一个软一些的橡皮轻轻擦，在它的基础上修正虽然画面可能脏脏的，但是结构全都画清晰了，干净的线可能很难有这种练习的效果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./../images/013/9130d55ab42fed27c732c597db168dab.jpeg&quot; alt=&quot;9130d55ab42fed27c732c597db168dab&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;其他思考&lt;/h2&gt;
&lt;p&gt;筛选好老师，会教课的老师，不要听垃圾课浪费时间。每个老师教的都有特色，但也有局限性，会有一点“偏见”和糟粕。不能只跟一个老师学，他说的答案不一定是最优解，也有可能是一条歧路，可能会对自己产生误导。要多听其他老师的课，多听其他绘画观点。&lt;/p&gt;
&lt;p&gt;例如大毛和 krenz 的某些观点会有冲突，因此听绘画课需要多思考。&lt;/p&gt;
&lt;p&gt;同时我感觉到这些教的很好的老师，学生太多，于是有了很多饭圈化粉丝学生。我觉得这对学画来说不是一件好事情吧。&lt;/p&gt;
&lt;p&gt;最后，感谢你看到这里，祝你越画越好🥰。&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>Go数组及相关算法（上）</title><link>https://notes.kiramux.com/posts/014-go-arrayalgorithm</link><guid isPermaLink="true">https://notes.kiramux.com/posts/014-go-arrayalgorithm</guid><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;对 Go 数组和相关算法进行了学习，并做了笔记。
相关链接：&lt;a href=&quot;https://programmercarl.com/%E6%95%B0%E7%BB%84%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html&quot;&gt;代码随想录&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;Go 数组&lt;/h2&gt;
&lt;h3&gt;定义相关&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;数组是存放在连续内存空间上的相同类型数据的集合。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;数组是具有相同类型的一组已编号且长度固定的数据项序列。这种类型可以是任意的原始类型（int、string或者自定义类型）。&lt;/p&gt;
&lt;p&gt;数组元素可以通过索引来读取或修改。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/014/goarray.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;数组定义：&lt;code&gt; var a [len]int&lt;/code&gt;，例如 var a [5]int&lt;/p&gt;
&lt;p&gt;数组的大小&lt;code&gt;len&lt;/code&gt;必须是常量，且是类型的组成部分（数组的类型不仅由 &lt;strong&gt;元素类型&lt;/strong&gt; 决定，还由 &lt;strong&gt;长度&lt;/strong&gt; 决定，例如&lt;code&gt;[5]int&lt;/code&gt; 和 &lt;code&gt;[10]int&lt;/code&gt; 是 &lt;strong&gt;两种完全不同的类型&lt;/strong&gt;）。&lt;/p&gt;
&lt;p&gt;数组大小一旦定义后不可变。&lt;/p&gt;
&lt;p&gt;内置函数 &lt;code&gt;len&lt;/code&gt; 和 &lt;code&gt;cap&lt;/code&gt; 都会返回数组大小&lt;/p&gt;
&lt;p&gt;需要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组的下标都是从0开始的&lt;/li&gt;
&lt;li&gt;数组内存空间的地址是连续的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正是因为数组在内存空间的地址是连续的，所以在“删除”元素时需要移动其他元素的地址。&lt;strong&gt;数组的元素不能删，只能覆盖&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/014/%E7%AE%97%E6%B3%95%E9%80%9A%E5%85%B3%E6%95%B0%E7%BB%841.png&quot; alt=&quot;算法通关数组1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;遍历数组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    for i := 0; i &amp;lt; len(a); i++ {
        //...
    }
    for index, v := range a {
        //...
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;访问越界&lt;/strong&gt;，如果下标在数组合法范围之外，则触发访问越界，会panic&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数组是值类型&lt;/strong&gt;，赋值和传参会复制整个数组，而不是指针。因此改变副本的值，不会改变本身的值。&lt;/p&gt;
&lt;p&gt;值拷贝会出现性能问题，通常会使用 slice 或数组指针。&lt;/p&gt;
&lt;p&gt;Go 数组支持 &quot;==&quot;、&quot;!=&quot; 操作符，因为数组是&lt;strong&gt;值类型&lt;/strong&gt;，&lt;strong&gt;内存总是被初始化&lt;/strong&gt;为对应类型的0值。&lt;/p&gt;
&lt;p&gt;指针数组 &lt;code&gt;[n]*T&lt;/code&gt;，数组指针 &lt;code&gt;*[n]T&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;数组的初始化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 全局:
var numbers [5]int //初始值为0
var arr0[5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}	// 使用初始化列表来初始化数组的元素
var arr2 = [...]int{1, 2, 3, 4, 5, 6}	// 数组长度不确定，使用...代替数组长度，编译器会根据元素个数自行推断数组长度
var str = [5]string{3: &quot;hello world&quot;, 4: &quot;kira&quot;}	// 如果设置了数组长度，可以通过指定下标来初始化元素

// 局部:
a := [3]int{1, 2}	// 未初始化的元素值为0
b := [...]int{1, 2, 3, 4}	// 通过初始化值来确定数组长度
c := [5]int{2:100, 4:200}	// 使用下标来初始化元素
d := [...]struct {
    name string
    age string
    // 这里是数组元素类型struct
}{
    {&quot;user1&quot;, 10},
    {&quot;user2&quot;, 20},	// 2个元素
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go 向函数中传递数组&lt;/h3&gt;
&lt;p&gt;Go 语言中的数组是&lt;strong&gt;值类型&lt;/strong&gt;，因此将数组传递给函数时，实际上传递的是数组的副本。&lt;/p&gt;
&lt;p&gt;向函数传递数组参数，需要在函数定义时声明形参为数组。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func aFunc(arr [10]int) {
    ...
}
// 或者
func bFunc(arr []int) {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想要在函数内修改原始数组，需要传递数组的&lt;strong&gt;指针&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

// 函数接受一个数组作为参数
func modifyArray(arr [5]int) {
    for i := 0; i &amp;lt; len(arr); i++ {
        arr[i] = arr[i] * 2
    }
}

// 函数接受一个数组的指针作为参数
func modifyArrayWithPointer(arr *[5]int) {
    for i := 0; i &amp;lt; len(*arr); i++ {
        (*arr)[i] = (*arr)[i] * 2
    }
}

func main() {
    // 创建一个包含5个元素的整数数组
    myArray := [5]int{1, 2, 3, 4, 5}

    fmt.Println(&quot;Original Array:&quot;, myArray)

    // 传递数组给函数，但不会修改原始数组的值
    modifyArray(myArray)
    fmt.Println(&quot;Array after modifyArray:&quot;, myArray)

    // 传递数组的指针给函数，可以修改原始数组的值
    modifyArrayWithPointer(&amp;amp;myArray)
    fmt.Println(&quot;Array after modifyArrayWithPointer:&quot;, myArray)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Original Array: [1 2 3 4 5]
Array after modifyArray: [1 2 3 4 5]
Array after modifyArrayWithPointer: [2 4 6 8 10]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;多维数组&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../images/014/go-array-2021-01-19.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一个二维数组 a 为三行四列，二维数组中的元素可通过 &lt;strong&gt;a[ i ][ j ]&lt;/strong&gt; 来访问。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    // 全局
    var arr0 [5][3]int
    var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
    // 局部
    a := [3][4]int{  
        {0, 1, 2, 3} ,   /*  第一行索引为 0 */
        {4, 5, 6, 7} ,   /*  第二行索引为 1 */
        {8, 9, 10, 11},   /* 第三行索引为 2 */
    }
    b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 &quot;...&quot;。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;多维数组遍历&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;fmt&quot;
)

func main() {

    var f [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}

    for k1, v1 := range f {
        for k2, v2 := range v1 {
            fmt.Printf(&quot;(%d,%d)=%d &quot;, k1, k2, v2)
        }
        fmt.Println()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    (0,0)=1 (0,1)=2 (0,2)=3 
    (1,0)=7 (1,1)=8 (1,2)=9
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;704.二分查找&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/binary-search/&quot;&gt;704. 二分查找&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给定一个 &lt;code&gt;n&lt;/code&gt; 个元素有序的（升序）整型数组 &lt;code&gt;nums&lt;/code&gt; 和一个目标值 &lt;code&gt;target&lt;/code&gt; ，写一个函数搜索 &lt;code&gt;nums&lt;/code&gt; 中的 &lt;code&gt;target&lt;/code&gt;，如果 &lt;code&gt;target&lt;/code&gt; 存在返回下标，否则返回 &lt;code&gt;-1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;你必须编写一个具有 &lt;code&gt;O(log n)&lt;/code&gt; 时间复杂度的算法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;你可以假设 &lt;code&gt;nums&lt;/code&gt; 中的所有元素是不重复的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt; 将在 &lt;code&gt;[1, 10000]&lt;/code&gt;之间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nums&lt;/code&gt; 的每个元素都将在 &lt;code&gt;[-9999, 9999]&lt;/code&gt;之间。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用二分查找的前提：有序数组；无重复元素。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;边界处理&lt;/strong&gt;是重要问题。&lt;/p&gt;
&lt;p&gt;主流写法有2类，左闭右闭&lt;code&gt;[left, right]&lt;/code&gt;和左闭右开&lt;code&gt;[left, right)&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;左闭右闭区间&lt;code&gt;[left, right]&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;x &amp;gt;&amp;gt; n&lt;/code&gt;相当于除以 2^n 并向下取整。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mid := left + (right-left)&amp;gt;&amp;gt;1&lt;/code&gt;是常见的二分中点写法，直接相加除以二可能会导致数值溢出。&lt;/p&gt;
&lt;p&gt;left := 0&lt;/p&gt;
&lt;p&gt;right := len(num) - 1&lt;/p&gt;
&lt;p&gt;*判断 left &amp;lt; right 时有没有&lt;code&gt;=&lt;/code&gt;，需要看区间&lt;code&gt;[left, right]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;左闭右闭，left == right 在这个区间内有意义，所以需要 &amp;lt;=&lt;/p&gt;
&lt;p&gt;*更新区间边界，以下是伪代码&lt;/p&gt;
&lt;p&gt;if num[mid] == target&lt;/p&gt;
&lt;p&gt;​	return mid&lt;/p&gt;
&lt;p&gt;else if num[mid] &amp;gt; target&lt;/p&gt;
&lt;p&gt;​	更新右边界，如下图所示。因为区间&lt;code&gt;[left, right]&lt;/code&gt;右闭包含 right，并且上面的判断条件 mid ！= target，所以&lt;/p&gt;
&lt;p&gt;​	right = mid - 1 。&lt;/p&gt;
&lt;p&gt;else // num[mid] &amp;lt; target&lt;/p&gt;
&lt;p&gt;​	更新左边界，左闭包含 left，left != target， 所以&lt;/p&gt;
&lt;p&gt;​	left = mid + 1&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/014/20210311153055723.jpg&quot; alt=&quot;704.二分查找&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func search(nums []int, target int) int {
    // 左闭右闭 [left, right]
    // 初始化左右边界
    left := 0
    right := len(nums) - 1

    for left &amp;lt;= right {
        mid := left + (right - left) &amp;gt;&amp;gt; 1
        if nums[mid] == target {
            return mid
        } else if nums[mid] &amp;lt; target {
            // 更新左边界，mid 不是 target，所以left应该为 mid+1
            left = mid + 1
        } else {
            // 更新右边界， mid &amp;gt; targer,所以right 应该为 mid-1 
            right = mid - 1
        }
    }
    return -1
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;左闭右开区间&lt;code&gt;[left, right)&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;left := 0&lt;/p&gt;
&lt;p&gt;right := len(num)&lt;/p&gt;
&lt;p&gt;由于右开， right 应当是数组元素综述+1才不会漏元素&lt;/p&gt;
&lt;p&gt;*条件判断时，left &amp;lt; right ，因为 left == right 在区间&lt;code&gt;[left, right)&lt;/code&gt;中没有意义。&lt;/p&gt;
&lt;p&gt;*更新区间边界&lt;/p&gt;
&lt;p&gt;if num[mid]  == target&lt;/p&gt;
&lt;p&gt;​	return mid&lt;/p&gt;
&lt;p&gt;else if num[mid] &amp;gt; target&lt;/p&gt;
&lt;p&gt;​	更新右边界。num[mid] != target，又因为右开，所以&lt;/p&gt;
&lt;p&gt;​	right = mid&lt;/p&gt;
&lt;p&gt;else // num[mid] &amp;lt; target&lt;/p&gt;
&lt;p&gt;​	更新左边界， 因为左闭，应当包含边界，而 num[mid] != target 所以&lt;/p&gt;
&lt;p&gt;​	left = mid + 1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func search(nums []int, target int) int {
    // 左闭右开 [left, right)
    // 初始化左右边界
    left := 0
    right := len(nums)

    for left &amp;lt; right {
        mid := left + (right - left) &amp;gt;&amp;gt; 1
        if nums[mid] == target {
            return mid
        } else if nums[mid] &amp;lt; target {
            // 更新左边界，mid 不是 target，所以left应该为 mid+1
            left = mid + 1
        } else {
            // 更新右边界， mid &amp;gt; targer,所以right 应该为 mid-1 
            right = mid
        }
    }
    return -1
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;遗留：什么是循环不变量？&lt;/p&gt;
&lt;h2&gt;27. 移除元素&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/remove-element/&quot;&gt;27. 移除元素&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给你一个数组 &lt;code&gt;nums&lt;/code&gt; 和一个值 &lt;code&gt;val&lt;/code&gt;，你需要 &lt;strong&gt;&lt;a href=&quot;https://baike.baidu.com/item/%E5%8E%9F%E5%9C%B0%E7%AE%97%E6%B3%95&quot;&gt;原地&lt;/a&gt;&lt;/strong&gt; 移除所有数值等于 &lt;code&gt;val&lt;/code&gt; 的元素。元素的顺序可能发生改变。然后返回 &lt;code&gt;nums&lt;/code&gt; 中与 &lt;code&gt;val&lt;/code&gt; 不同的元素的数量。&lt;/p&gt;
&lt;p&gt;假设 &lt;code&gt;nums&lt;/code&gt; 中不等于 &lt;code&gt;val&lt;/code&gt; 的元素数量为 &lt;code&gt;k&lt;/code&gt;，要通过此题，您需要执行以下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更改 &lt;code&gt;nums&lt;/code&gt; 数组，使 &lt;code&gt;nums&lt;/code&gt; 的前 &lt;code&gt;k&lt;/code&gt; 个元素包含不等于 &lt;code&gt;val&lt;/code&gt; 的元素。&lt;code&gt;nums&lt;/code&gt; 的其余元素和 &lt;code&gt;nums&lt;/code&gt; 的大小并不重要。&lt;/li&gt;
&lt;li&gt;返回 &lt;code&gt;k&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;用户评测：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;评测机将使用以下代码测试您的解决方案：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int[] nums = [...]; // 输入数组
int val = ...; // 要移除的值
int[] expectedNums = [...]; // 长度正确的预期答案。
                           // 它以不等于 val 的值排序。

int k = removeElement(nums, val); // 调用你的实现

assert k == expectedNums.length;
sort(nums, 0, k); // 排序 nums 的前 k 个元素
for (int i = 0; i &amp;lt; actualLength; i++) {
   assert nums[i] == expectedNums[i];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果所有的断言都通过，你的解决方案将会 &lt;strong&gt;通过&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [3,2,2,3], val = 3
输出：2, nums = [2,2,_,_]
解释：你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。
你在返回的 k 个元素之外留下了什么并不重要（因此它们并不计入评测）。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [0,1,2,2,3,0,4,2], val = 2
输出：5, nums = [0,1,4,0,3,_,_,_]
解释：你的函数应该返回 k = 5，并且 nums 中的前五个元素为 0,0,1,3,4。
注意这五个元素可以任意顺序返回。
你在返回的 k 个元素之外留下了什么并不重要（因此它们并不计入评测）。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= nums.length &amp;lt;= 100&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= nums[i] &amp;lt;= 50&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= val &amp;lt;= 100&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;暴力解法&lt;/h3&gt;
&lt;p&gt;时间复杂度是O(n^2)&lt;/p&gt;
&lt;p&gt;两层 for 循环，一个for循环遍历数组元素 ，第二个for循环更新数组。&lt;/p&gt;
&lt;p&gt;一旦第一层 for 循环找到 val ，就用其后面的元素把它往前一位覆盖掉，并且数组大小 len-1&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/014/27.%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0-%E6%9A%B4%E5%8A%9B%E8%A7%A3%E6%B3%95.gif&quot; alt=&quot;27.移除元素-暴力解法&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func removeElement(nums []int, val int) int {
    len := len(nums)
    for i := 0; i &amp;lt; len; i++ {
        if nums[i] == val {
            for j := i + 1; j &amp;lt; len; j++ {
                nums[j-1] = nums[j]
            }
            i--
            len--
        }
    }
    return len
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;快慢指针法&lt;/h3&gt;
&lt;p&gt;时间复杂度：O(n)；空间复杂度：O(1)&lt;/p&gt;
&lt;p&gt;没有改变元素的相对位置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快指针：寻找新数组元素（非val的元素）&lt;/li&gt;
&lt;li&gt;慢指针：指向要更新的元素，新数组的下标。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;快指针一旦找到新数组元素，就更新慢指针的元素，直到找完就得到了全部是非 val 元素的新数组。&lt;/p&gt;
&lt;p&gt;slowIndex 刚好就是新数组大小。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/014/27.%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0-%E5%8F%8C%E6%8C%87%E9%92%88%E6%B3%95.gif&quot; alt=&quot;27.移除元素-双指针法&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func removeElement(nums []int, val int) int {
    var slowIndex int = 0
    len := len(nums)
    for fastIndex := 0; fastIndex &amp;lt; len ; fastIndex++ {
        if nums[fastIndex] != val {
            nums[slowIndex] = nums[fastIndex]
            slowIndex++
        }
    }
    return slowIndex
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;双向指针法&lt;/h3&gt;
&lt;p&gt;有点像二分查找，这里按照左闭右闭来写。&lt;/p&gt;
&lt;p&gt;双向指针法，left找 val，right找非 val，一旦都找到，就 nums[left] = nums[right]
随后 left++ ; right-- (都向中间靠拢)
当 left &amp;gt; right 时，说明left左侧没有 val，left 指针左侧即是最终的数组，left 就是数组大小。&lt;/p&gt;
&lt;p&gt;过程：&lt;/p&gt;
&lt;p&gt;&amp;lt;video src=&quot;../images/014/移除元素-相向双指针法.mp4&quot;&amp;gt;&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func removeElement(nums []int, val int) int {
    left := 0
    right := len(nums) - 1
    // 左闭右闭区间，所以是 &amp;lt;=
    for left &amp;lt;= right {
        // 寻找左侧val，找到退出循环
        for left &amp;lt;= right &amp;amp;&amp;amp; nums[left] != val {
            left++
        }
        // 寻找右侧非val，找到退出循环
        for left &amp;lt;= right &amp;amp;&amp;amp; nums[right] == val {
            right--
        }
        // 各自都找到后开始覆盖
        if left &amp;lt; right {
            nums[left] = nums[right]
            left++
            right--
        }
    }
    return left
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;977.有序数组的平方&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/squares-of-a-sorted-array/&quot;&gt;977. 有序数组的平方&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给你一个按 &lt;strong&gt;非递减顺序&lt;/strong&gt; 排序的整数数组 &lt;code&gt;nums&lt;/code&gt;，返回 &lt;strong&gt;每个数字的平方&lt;/strong&gt; 组成的新数组，要求也按 &lt;strong&gt;非递减顺序&lt;/strong&gt; 排序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [-4,-1,0,3,10]
输出：[0,1,9,16,100]
解释：平方后，数组变为 [16,1,0,9,100]
排序后，数组变为 [0,1,9,16,100]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [-7,-3,2,3,11]
输出：[4,9,9,49,121]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= nums.length &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-104 &amp;lt;= nums[i] &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nums&lt;/code&gt; 已按 &lt;strong&gt;非递减顺序&lt;/strong&gt; 排序&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;进阶：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请你设计时间复杂度为 &lt;code&gt;O(n)&lt;/code&gt; 的算法解决本问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;暴力解法 直接平方排序&lt;/h3&gt;
&lt;p&gt;O(n + nlogn) 或是 O(nlogn)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func sortedSquares(nums []int) []int {
    for i, val := range(nums) {
        nums[i] *= val 
    }
    sort.Ints(nums)
    return nums
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;双指针法&lt;/h3&gt;
&lt;p&gt;O(n)&lt;/p&gt;
&lt;p&gt;数组是有序的，平方之后的数组，最大的数分别在两端，越往中间越小。&lt;/p&gt;
&lt;p&gt;创建新数组 ans，以及k（数组的最大下标）。&lt;/p&gt;
&lt;p&gt;两端的下标 left, right（左闭右闭）。nums[left]^2 和 nums[right]^2 做比较。&lt;/p&gt;
&lt;p&gt;大的填入 ans[k]，然后大的指针--，k--，另一个不动留着下一次比较。即：&lt;/p&gt;
&lt;p&gt;如果&lt;code&gt;A[i] * A[i] &amp;lt; A[j] * A[j]&lt;/code&gt; 那么&lt;code&gt;ans[k--] = A[j] * A[j];&lt;/code&gt; &lt;code&gt;j--&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果&lt;code&gt;A[i] * A[i] &amp;gt;= A[j] * A[j]&lt;/code&gt; 那么&lt;code&gt;ans[k--] = A[i] * A[i];&lt;/code&gt; &lt;code&gt;i++&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;下图在最后应该是 i 和 j 相等，都到元素0再判断平方大小，动画可能少了一张图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/014/977.%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E7%9A%84%E5%B9%B3%E6%96%B9.gif&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func sortedSquares(nums []int) []int {
    len := len(nums)
    var left, right, k int = 0, len-1, len-1
    ans := make([]int, len)
    for left &amp;lt;= right {
        lsq, rsq := nums[left]*nums[left], nums[right]*nums[right]
        if lsq &amp;gt; rsq {
            left++
            ans[k] = lsq
            k--
        } else {
            right--
            ans[k] = rsq
            k--
        }
    }
    return ans
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Kiramux</author></item><item><title>Go数组及相关算法（下）+ Go 基础笔记</title><link>https://notes.kiramux.com/posts/015-go-arrayalgorithm2-and-go-base</link><guid isPermaLink="true">https://notes.kiramux.com/posts/015-go-arrayalgorithm2-and-go-base</guid><pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;对 Go 数组和相关算法继续学习，并做了笔记。
做了一部分 Go 基础的笔记。
相关链接：&lt;a href=&quot;https://programmercarl.com/%E6%95%B0%E7%BB%84%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html&quot;&gt;代码随想录&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;Go 基础笔记&lt;/h2&gt;
&lt;p&gt;Go 思想：&lt;strong&gt;Less can be more.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Go 优点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自带 gc&lt;/li&gt;
&lt;li&gt;静态编译，编译好后，扔服务器直接运行&lt;/li&gt;
&lt;li&gt;简单的思想，没有继承、多态、类等&lt;/li&gt;
&lt;li&gt;丰富的库和详细的开发文档&lt;/li&gt;
&lt;li&gt;语法层支持并发，和拥有同步并发的 channel 类型，使并发开发变得非常方便&lt;/li&gt;
&lt;li&gt;简洁的语法、提高开发效率，同时提高代码的阅读性和可维护性&lt;/li&gt;
&lt;li&gt;超级简单的交叉编译，仅需更改环境变量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Go 语言的主要特性：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;自动立即回收&lt;/li&gt;
&lt;li&gt;更丰富的内置类型&lt;/li&gt;
&lt;li&gt;函数多返回值&lt;/li&gt;
&lt;li&gt;错误处理&lt;/li&gt;
&lt;li&gt;匿名函数和闭包&lt;/li&gt;
&lt;li&gt;类型和接口&lt;/li&gt;
&lt;li&gt;并发编程&lt;/li&gt;
&lt;li&gt;反射&lt;/li&gt;
&lt;li&gt;语言交互性&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Go 基本类型&lt;/h3&gt;
&lt;p&gt;置顶变量类型，如果没有初始化，则变量默认为零值。&lt;/p&gt;
&lt;p&gt;各种类型各自的零值&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;长度（字节）&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;无法参与数值运算，无法转换为其他类型，也不允许将整型强制转为 bool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;byte&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;byt e是 uint8 的别名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rune&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Unicode Code Point, 底层是 int32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int, uint&lt;/td&gt;
&lt;td&gt;4或8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;32位或64位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int8, uint8&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;-128~127; 0~255; uint8-&amp;gt;byte&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int16, uint16&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;-32768~32767; 0~65535&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int32, uint32&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;-21亿~21亿; 0~42亿;  rune 是 int32 的别名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int64, uint64&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;float32&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;float64&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;complex64&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;实部和虚部为32位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;complex128&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;实部和虚部为64位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uintptr&lt;/td&gt;
&lt;td&gt;4或8&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;用以存储指针的 uint32 或 uint64 整数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;array&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;值类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;struct&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;值类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;“”（空字符串）&lt;/td&gt;
&lt;td&gt;UTF-8字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;slice&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;nil&lt;/td&gt;
&lt;td&gt;引用类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;map&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;nil&lt;/td&gt;
&lt;td&gt;引用类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;channel&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;nil&lt;/td&gt;
&lt;td&gt;引用类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;interface&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;nil&lt;/td&gt;
&lt;td&gt;接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;function&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;nil&lt;/td&gt;
&lt;td&gt;函数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;值类型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;值类型是指变量直接存储了实际的数据，并且每个变量都拥有独立的存储空间。当一个值类型的变量被复制给另一个变量时，会进行值拷贝。对其中一个变量的修改不会影响到原始变量。&lt;/p&gt;
&lt;p&gt;Go 中值类型包含：整型、浮点型、复数、布尔型、字符串型、数组、结构体&lt;/p&gt;
&lt;p&gt;值类型有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接存储值，不存储地址&lt;/li&gt;
&lt;li&gt;变量间赋值时、作为参数传递时 进行值复制&lt;/li&gt;
&lt;li&gt;值类型的变量副本是独立的，修改一个变量的副本不会影响另一个&lt;/li&gt;
&lt;li&gt;值类型通常在栈上分配，除非是通过 new 函数分配的，或者是作为闭包中的变量被分配到堆上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;引用类型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;引用类型并不直接存储数据本身，而是存储指向数据的指针。&lt;/p&gt;
&lt;p&gt;当复制一个引用类型的变量时，复制的是指针，新旧变量指向的是相同的底层数据。&lt;/p&gt;
&lt;p&gt;Go 中的引用类型包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;切片 slice ： 切片是对数组的封装。当修改切片中的元素时，实际上是在修改底层数组的相应元素&lt;/li&gt;
&lt;li&gt;映射 map ： 将 map 传递给一个函数、赋值给一个变量时，任何对映射的修改都会反映在所有引用了这个 map 的地方&lt;/li&gt;
&lt;li&gt;通道 channel&lt;/li&gt;
&lt;li&gt;接口 interface ： 接口类型是一种抽象类型，定义了一组方法，但不会实现这些方法。接口内部存储的是 指向实现了接口方法的值的指针 和 指向该类型信息的指针&lt;/li&gt;
&lt;li&gt;函数 function ：当把一个函数赋给另一个变量时，实际上是在复制一个指向该函数的引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;引用类型有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;存储的是指向该数据的地址，而不是数据本身&lt;/li&gt;
&lt;li&gt;当引用类型的变量被赋值 或 作为函数参数传递时，实际上是将该地址复制一份，因此多个变量可能共享同一份数据&lt;/li&gt;
&lt;li&gt;引用类型的数据通常在堆上分配，即使变量本身在栈上&lt;/li&gt;
&lt;li&gt;引用类型的零值是 nil ，一个未初始化的引用类型的变量值是 nil ，不指向任何内存地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;整型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;uint8&lt;/code&gt; 对应 &lt;code&gt;byte&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;int16&lt;/code&gt; 对应 C 中的 &lt;code&gt;short&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;int64&lt;/code&gt; 对应 C 中的 &lt;code&gt;long&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;int 和 uint 的长度自动根据平台大小调整（uintptr: 用于存储指针值的无符号整型）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;字符串&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;定义多行字符串，使用反引号&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    s1 := `第一行
    第二行
    第三行
    `
    fmt.Println(s1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;反引号间换行将被作为字符串中的换行，但是所有的&lt;strong&gt;转义字符均无效&lt;/strong&gt;，文本将会原样输出。&lt;/p&gt;
&lt;p&gt;字符串常见操作&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;len(str)&lt;/td&gt;
&lt;td&gt;求长度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ 或 fmt.Sprintf&lt;/td&gt;
&lt;td&gt;拼接字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;strings.Split&lt;/td&gt;
&lt;td&gt;分割&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;strings.Contains&lt;/td&gt;
&lt;td&gt;判断是否包含&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;strings.HasPrefix, strings.HasSuffix&lt;/td&gt;
&lt;td&gt;前缀/后缀判断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;strings.Index(), strings.LastIndex()&lt;/td&gt;
&lt;td&gt;子串出现的位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;strings.Join(a[]string, sep string)&lt;/td&gt;
&lt;td&gt;join 操作&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;byte和rune类型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;组成每个字符串的元素叫做“字符”，可以通过遍历或者获取单个字符串元素获得字符。字符用单引号&lt;code&gt;&apos;&lt;/code&gt;包裹起来，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a := &apos;中
var b := &apos;x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go 语言的字符有两种&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;uint8 类型，或者叫 byte 型，代表了 ASCII 码的一个字符&lt;/li&gt;
&lt;li&gt;rune 类型，代表一个 UTF-8 字符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当需要处理中文、日文或者其他复合字符时，则需要用到 &lt;code&gt;rune&lt;/code&gt; 类型。&lt;code&gt;rune&lt;/code&gt; 类型实际上是一个&lt;code&gt;int32&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Go 使用了特殊的 &lt;code&gt;rune&lt;/code&gt; 类型来处理 Unicode，让基于 Unicode 的文本处理更为方便，也可以使用 byte 型进行默认字符串处理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    // 遍历字符串
    func traversalString() {
        s := &quot;pprof.cn博客&quot;
        for i := 0; i &amp;lt; len(s); i++ { //byte
            fmt.Printf(&quot;%v(%c) &quot;, s[i], s[i])
        }
        fmt.Println()
        for _, r := range s { //rune
            fmt.Printf(&quot;%v(%c) &quot;, r, r)
        }
        fmt.Println()
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 229(å) 141() 154() 229(å) 174(®) 162(¢)
112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 21338(博) 23458(客)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;for i := 0; i &amp;lt; len(s); i++ { //byte }&lt;/code&gt; 是以字节为单位遍历的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;for range&lt;/code&gt; 是以 Unicode 码点（rune）遍历字符串的。&lt;/p&gt;
&lt;p&gt;字符串底层是一个 byte 数组，所以可以和 []byte 类型相互转换。字符串是不能直接修改的。&lt;/p&gt;
&lt;p&gt;字符串有 byte 字节组成，所以 len(s) 是 byte 字节的长度。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rune&lt;/code&gt; 类型用来表示 UTF-8 字符。因为 UTF-8 编码下，一个中文由 3~4 个字节组成，所以不能按照字节去遍历一个包含中文的字符串，否则就会出现上面输出中第一行的结果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;修改字符串&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;要修改字符串，需要将其先转成 &lt;code&gt;[]rune&lt;/code&gt; 或 &lt;code&gt;[]byte&lt;/code&gt; ，完成后再转换为 &lt;code&gt;string&lt;/code&gt; 。&lt;strong&gt;无论哪种转换，都会重新分配内存，并复制字节数组&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    func changeString() {
        s1 := &quot;hello&quot;
        // 强制类型转换
        byteS1 := []byte(s1)
        byteS1[0] = &apos;H&apos;
        fmt.Println(string(byteS1))

        s2 := &quot;博客&quot;
        runeS2 := []rune(s2)
        runeS2[0] = &apos;狗&apos;
        fmt.Println(string(runeS2))
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go 变量声明&lt;/h3&gt;
&lt;p&gt;Go 声明可见性&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;声明在函数内部，是函数的本地值，类似 private&lt;/li&gt;
&lt;li&gt;声明在函数外部，是对当前包可见（包内所有 .go 文件都可见）的全局值，类似 protect&lt;/li&gt;
&lt;li&gt;声明在函数外部且首字母大写是所有包可见的全局值，类似 public&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Go 语言在声明变量的时候，会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值。（参考上文的类型表格）&lt;/p&gt;
&lt;p&gt;不同声明方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var name string

// 声明时初始化
var name2 string = &quot;kiramux&quot;
var num int = 1

// 批量声明
var (
    a string
    b int
    c bool
    d float32
)

// 批量声明+初始化
var (
    color string = &quot;blue&quot;
    x int = 10
    is_X bool = true
)

// 类型推导
var domain = &quot;kiramux.com&quot;

// 一次初始化多个变量
var name3, y = &quot;kiramux&quot;, 1

// ---
// 全局变量m
var m = 100

// 函数内部的短变量声明
func main() {
    n := 10
    m := 100 // 此处声明局部变量m
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在使用多重赋值时，如果想要忽略某个值，可以使用匿名变量，用下划线&lt;code&gt;_&lt;/code&gt;表示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for _, val := range nums {
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;匿名变量&lt;/strong&gt;不占用命名空间，不会分配内存，所以匿名变量之间不存在重复声明。&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数外的每个语句都必须以关键字开始（var、const、func 等）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:=&lt;/code&gt; 不能在函数外使用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_&lt;/code&gt; 多用于占位，表示忽略值&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Go 下划线&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;下划线在 import 中&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在导入一个包时，该包下的文件里所有的 init() 函数都会被执行。&lt;/p&gt;
&lt;p&gt;当我们仅希望它执行 init() 函数，不需要把整个包都倒进来，就可以用 &lt;code&gt;import&lt;/code&gt; 引入该包。即 &lt;code&gt;import _ 包路径&lt;/code&gt; 只是引用该包，仅仅为了调用 init() 函数，所以无法通过包名来调用包中的其他函数。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;代码结构
    src 
    |
    +--- main.go            
    |
    +--- hello
           |
            +--- hello.go
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package main

import _ &quot;./hello&quot;

func main() {
    // hello.Print() 
    // 启用上一行代码会编译报错：./main.go:6:5: undefined: hello
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;hello.go&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package hello

import &quot;fmt&quot;

func init() {
    fmt.Println(&quot;imp-init() come here.&quot;)
}

func Print() {
    fmt.Println(&quot;Hello!&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    imp-init() come here.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;init 函数和 main 函数&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;init 函数&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;init 函数是用于程序执行前做包的初始化的函数，比如初始化包里的变量等&lt;/li&gt;
&lt;li&gt;每个包可以有多个 init 函数&lt;/li&gt;
&lt;li&gt;包的每个源文件也可以拥有多个 init 函数&lt;/li&gt;
&lt;li&gt;同一个包中多个 init 函数的执行顺序 go 语言没有明确的定义（说明）&lt;/li&gt;
&lt;li&gt;不同包的 init 函数按照包导入的依赖关系决定该初始化函数的执行顺序&lt;/li&gt;
&lt;li&gt;init 函数不能被其他函数调用，而会在 main 函数执行之前自动被调用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;init 函数 和 main 函数的异同&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;相同点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;两个函数在定义时不能有任何的参数和返回值，且 Go 程序自动调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不同点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;init 可以应用于任意包中，且可以重复定义多个。&lt;/li&gt;
&lt;li&gt;main 函数只能用于 main 包中，且只能定义一个。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;init 函数和 main 函数的执行顺序&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对同一个 go 文件的 init 函数调用顺序是从上到下的。&lt;/p&gt;
&lt;p&gt;对同一个 package 中不同文件是按照文件名字符串比较“从小到大”顺序调用各文件中的 init 函数&lt;/p&gt;
&lt;p&gt;对于不同的 package ，如果不相互依赖的话，按照 main 包中“先 import 后调用其包中的 init 函数”。如果 package 存在依赖，则先调用最早被依赖的 package 中的 init 函数。&lt;/p&gt;
&lt;p&gt;最后调用 main 函数。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;如果 init 函数中使用了 &lt;code&gt;println()&lt;/code&gt; 或者 &lt;code&gt;print()&lt;/code&gt; ，会发现在执行过程中二者不会按照想象中的顺序执行。&lt;/p&gt;
&lt;p&gt;这两个函数官方只推荐在测试环境中使用，在正式环境不要使用。&lt;/p&gt;
&lt;h3&gt;Go 类型别名和自定义类型&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;自定义类型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Go 中有整型、string 等基本的数据类型，可以使用 type 关键字来自定义类型。&lt;/p&gt;
&lt;p&gt;自定义类型是一个全新的类型。可以基于内置的基本类型定义，也可以通过 struct 定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //将MyInt定义为int类型
    type MyInt int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MyInt 就是一种新类型，它具有 int 的特性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类型别名&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;TypeAlias 只是 Type 的别名，本质上两者是同一个类型。&lt;/p&gt;
&lt;p&gt;就像一个人小时候有小名、乳名、中文名、英文名，但这些名字都是指它一个人。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    type TypeAlias = Type

    // rune和byte就是类型别名
    type byte = uint8
    type rune = int32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;类型定义和类型别名的区别&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//类型定义
type NewInt int

//类型别名
type MyInt = int

func main() {
    var a NewInt
    var b MyInt

    fmt.Printf(&quot;type of a:%T\n&quot;, a) //type of a:main.NewInt
    fmt.Printf(&quot;type of b:%T\n&quot;, b) //type of b:int
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果显示&lt;/p&gt;
&lt;p&gt;a 的类型是 main.NewInt ，表示 main 包下定义的 NewInt 类型；&lt;/p&gt;
&lt;p&gt;b 的类型是 Int 。MyInt 类型只会在代码中存在，编译完成时并不会有 MyInt 类型。&lt;/p&gt;
&lt;p&gt;如果怕记错的话，想一想 struct 的定义， struct 就是一种新类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type cats struct {
    name string
    age int
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;209.长度最小的子数组&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-size-subarray-sum/&quot;&gt;209. 长度最小的子数组&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给定一个含有 &lt;code&gt;n&lt;/code&gt; 个正整数的数组和一个正整数 &lt;code&gt;target&lt;/code&gt; &lt;strong&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;找出该数组中满足其总和大于等于 &lt;code&gt;target&lt;/code&gt; 的长度最小的 &lt;strong&gt;子数组&lt;/strong&gt; &lt;code&gt;[numsl, numsl+1, ..., numsr-1, numsr]&lt;/code&gt; ，并返回其长度**。**如果不存在符合条件的子数组，返回 &lt;code&gt;0&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：target = 7, nums = [2,3,1,2,4,3]
输出：2
解释：子数组 [4,3] 是该条件下的长度最小的子数组。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：target = 4, nums = [1,4,4]
输出：1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：target = 11, nums = [1,1,1,1,1,1,1,1]
输出：0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= target &amp;lt;= 109&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= nums.length &amp;lt;= 105&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= nums[i] &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;进阶：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你已经实现 &lt;code&gt;O(n)&lt;/code&gt; 时间复杂度的解法, 请尝试设计一个 &lt;code&gt;O(n log(n))&lt;/code&gt; 时间复杂度的解法。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;暴力解法（力扣超时）&lt;/h3&gt;
&lt;p&gt;时间复杂度：&lt;strong&gt;O(n^2)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;直接遍历所有子数组，寻找符合条件的情况。&lt;/p&gt;
&lt;p&gt;相当于 &lt;code&gt;i&lt;/code&gt; 是头， &lt;code&gt;j&lt;/code&gt; 是尾。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func minSubArrayLen(target int, nums []int) int {
    len := len(nums)
    var sum int = 0
    var result int = len + 1
    for i := 0; i &amp;lt; len; i++ {
        sum = 0
        for j := i; j &amp;lt; len; j++ {
            sum += nums[j]
            if sum &amp;gt;= target {
                subLen := j - i + 1
                if subLen &amp;lt; result {
                    result = subLen
                break
                } 
            }
        }
    }
    if result == len + 1 {
        return 0
    } else {
        return result
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;滑动窗口法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(1)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所谓滑动窗口，&lt;strong&gt;就是不断的调节子序列的起始位置和终止位置，从而得出我们想要的结果&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;也可以理解成一种双指针法。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;在暴力解法中，是一个for循环滑动窗口的起始位置，一个for循环为滑动窗口的终止位置，用两个for循环 完成了一个不断搜索区间的过程。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;尝试使用一个 for 循环：&lt;/p&gt;
&lt;p&gt;以题目中的示例来举例，target=7， 数组是 2，3，1，2，4，3，查找过程：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/015/209.%E9%95%BF%E5%BA%A6%E6%9C%80%E5%B0%8F%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84.gif&quot; alt=&quot;209.长度最小的子数组&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后找到 4，3 是最短距离。&lt;/p&gt;
&lt;p&gt;j代表滑动窗口的“尾”，先找到总和大于 target 的子数组，再移动头部缩小子数组。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果 &lt;code&gt;j&lt;/code&gt; 代表起始位置的话，就和暴力解法没区别了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;func minSubArrayLen(target int, nums []int) int {
    len := len(nums)
    var sum int = 0
    var result int = len + 1
    i := 0

    // j 是滑动窗口的终止位置，如果代表起始位置的话，和暴力解法没区别
    for j := 0; j &amp;lt; len; j++ {
        sum += nums[j]
        for sum &amp;gt;= target {
            subLen := j - i + 1 // 求长度所以+1 
            if subLen &amp;lt; result {
                result = subLen
            }
            // 起始位置移动
            sum -= nums[i]
            i++
        }
    }

    if result == len + 1 {
        return 0
    } else {
        return result
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;本题实现滑动窗口，主要确定以下三点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;窗口内的是什么？&lt;/li&gt;
&lt;li&gt;如何移动窗口的初始位置？&lt;/li&gt;
&lt;li&gt;如何移动窗口的结束位置？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本题的窗口： 满足其和≥target 的最小的&lt;strong&gt;连续&lt;/strong&gt;子数组；&lt;/p&gt;
&lt;p&gt;本题的起始位置如何移动：如果当前窗口的值≥target ，窗口就要向右移动了（窗口该缩小了）；&lt;/p&gt;
&lt;p&gt;本题的终止位置如何移动：窗口的结束位置就是数组的遍历指针，也就是 for 循环中的索引。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;解题的关键在于，窗口的起始位置如何移动：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/015/20210312160441942.png&quot; alt=&quot;leetcode_209&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;滑动窗口的精妙之处在于：根据当前子序列和大小的情况，不断调整子序列的起始位置，从而将O(n^2)暴力解法降为O(n)。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**时间复杂度是O(n)**的原因：&lt;/p&gt;
&lt;p&gt;不是两个 for 循环嵌套就是O(n^2)，主要看每个元素被操作的次数。&lt;/p&gt;
&lt;p&gt;每个元素在滑动窗口进来操作一次，出去操作一次，每个元素都是被操作两次。所以时间复杂度是 &lt;code&gt;2*n&lt;/code&gt; 也就是 O(n)。&lt;/p&gt;
&lt;h2&gt;59.螺旋矩阵II&lt;/h2&gt;
&lt;h3&gt;循环不变量1&lt;/h3&gt;
&lt;p&gt;模拟顺时针画矩阵的过程:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;填充上行从左到右&lt;/li&gt;
&lt;li&gt;填充右列从上到下&lt;/li&gt;
&lt;li&gt;填充下行从右到左&lt;/li&gt;
&lt;li&gt;填充左列从下到上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以发现这里的边界条件非常之多，在一个循环中，如此多的边界条件，如果不按照固定的规则来遍历，上来就i:=固定值。那么就是 &lt;strong&gt;一进循环深似海，从此offer是路人&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;方法1: 每一边都坚持&lt;strong&gt;左闭右开&lt;/strong&gt;原则*（或者左开右闭原则）*&lt;/p&gt;
&lt;p&gt;每一个拐角处都不处理，交给下一条边来处理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/015/20220922102236.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/015/image-20250917155630618.png&quot; alt=&quot;image-20250917155630618&quot; /&gt;&lt;/p&gt;
&lt;p&gt;圈数 loop == n / 2，如果n是奇数，那么就把中间的值赋值 n*n 即可。&lt;/p&gt;
&lt;p&gt;定好每一行开始的位置和结束的位置，每完成一圈，offset++&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func generateMatrix(n int) [][]int {
    var (
        loop = n / 2
        startx = 0
        starty = 0
        offset = 1
        counter = 1
    )

    var nums = make([][]int, n)
    for i := 0; i &amp;lt; n; i++ {
        nums[i] = make([]int, n)
    }
    for loop &amp;gt; 0 {
        i,j  := startx, starty
        for j = starty; j &amp;lt; n - offset; j++ {
            nums[startx][j] = counter
            counter++
        }
        for i = startx; i&amp;lt; n - offset; i++ {
            nums[i][j] = counter
            counter++
        }
        for ;j &amp;gt; starty; j-- {
            nums[i][j] = counter
            counter++
        }
        for ;i &amp;gt; startx; i-- {
            nums[i][starty] = counter
            counter++
        }
        loop--
        startx++
        starty++
        offset++
    }
    if n % 2 == 1 {
    nums[n/2][n/2] = n*n 
    }
    return nums
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;循环不变量2&lt;/h3&gt;
&lt;p&gt;这个思路是：规定未处理的范围 left, right, top, bottom, 坚持左闭右闭原则。&lt;/p&gt;
&lt;p&gt;每处理完一条边，就把未处理的范围减去一条边，明确下一条边要处理的范围。例如处理完红色的第一行，那么就&lt;code&gt;top++&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这个思路优点是代码简单，思路容易想到。缺点是每一条边的具体操作不一致，可能会容易出错。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/015/image-20250917155544383.png&quot; alt=&quot;image-20250917155544383&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func generateMatrix(n int) [][]int {
    left, right := 0, n-1
    top, bottom := 0, n-1
    counter := 1
    tar := n * n

    var matrix = make([][]int, n)
    for i := 0; i &amp;lt; n; i++ {
        matrix[i] = make([]int, n)
    }

    for counter &amp;lt;= tar {
        for i := left; i &amp;lt;= right; i++ {
            matrix[top][i] = counter
            counter++
        }
        top++
        for i := top; i &amp;lt;= bottom; i++ {
            matrix[i][right] = counter
            counter++
        }
        right--
        for i := right; i &amp;gt;= left; i-- {
            matrix[bottom][i] = counter
            counter++
        }
        bottom--
        for i := bottom; i &amp;gt;= top; i-- {
            matrix[i][left] = counter
            counter++
        }
        left++
    }

    return matrix
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Kiramux</author></item><item><title>链表算法（1）</title><link>https://notes.kiramux.com/posts/016-linkedlist-algorithm-1</link><guid isPermaLink="true">https://notes.kiramux.com/posts/016-linkedlist-algorithm-1</guid><pubDate>Thu, 18 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;对 Go 链表相关算法继续学习，并做了笔记。
相关链接：&lt;a href=&quot;https://programmercarl.com/0203.%E7%A7%BB%E9%99%A4%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0.html&quot;&gt;代码随想录&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;203.移除链表元素&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/remove-linked-list-elements/&quot;&gt;203. 移除链表元素&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给你一个链表的头节点 &lt;code&gt;head&lt;/code&gt; 和一个整数 &lt;code&gt;val&lt;/code&gt; ，请你删除链表中所有满足 &lt;code&gt;Node.val == val&lt;/code&gt; 的节点，并返回 &lt;strong&gt;新的头节点&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/016/removelinked-list.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [1,2,6,3,4,5,6], val = 6
输出：[1,2,3,4,5]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [], val = 1
输出：[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [7,7,7,7], val = 7
输出：[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;列表中的节点数目在范围 &lt;code&gt;[0, 104]&lt;/code&gt; 内&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= Node.val &amp;lt;= 50&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= val &amp;lt;= 50&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;使用原链表（力扣超时）&lt;/h3&gt;
&lt;p&gt;这里以链表 1 4 2 4 来举例，移除元素4。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/016/20210316095351161.png&quot; alt=&quot;203_链表删除元素1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/016/image-20250918103627592.png&quot; alt=&quot;image-20250918103627592&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果首元节点的值就是要移除的值，例如 head = [7,7,7,7,6], val = 7&lt;/p&gt;
&lt;p&gt;直接移除首元节点&lt;/p&gt;
&lt;p&gt;for head != nil &amp;amp;&amp;amp; head.Val == val {&lt;/p&gt;
&lt;p&gt;​	head = head.Next&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func removeElements(head *ListNode, val int) *ListNode {
    // 先删除含 val 的首元节点，可能有多个所以循环
    for head != nil &amp;amp;&amp;amp; head.Val == val{// head.Next != nil 不用考虑，因为最后一个节点就是指向nil的
            // head 指向下一个节点，相当于删除了原来的首元节点
            head = head.Next
    }

    // struct(节点)是值类型，传递的是副本
    cur := head
    for cur != nil &amp;amp;&amp;amp; cur.Next != nil {
        if cur.Next.Val == val {
            cur.Next = head.Next.Next
        } else {
            cur = cur.Next
        }
    }
    return head
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;虚拟头结点&lt;/h3&gt;
&lt;p&gt;第一种方法中，移除首元节点和移除之后节点的方法不同。&lt;/p&gt;
&lt;p&gt;如果添加一个虚拟头结点，那么对节点的移除操作就一致了。&lt;/p&gt;
&lt;p&gt;最后 return dummyHead.Next&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/016/20210316095619221.png&quot; alt=&quot;203_链表删除元素6&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func removeElements(head *ListNode, val int) *ListNode {
    dummyHead := &amp;amp;ListNode{}
    dummyHead.Next = head

    cur := dummyHead
    for cur != nil &amp;amp;&amp;amp; cur.Next != nil {
        if cur.Next.Val == val {
            cur.Next = cur.Next.Next
        } else {
            cur = cur.Next
        }
    }
    return dummyHead.Next
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;递归（未完成）&lt;/h3&gt;
&lt;p&gt;java思路&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 时间复杂度 O(n)
 * 空间复杂度 O(n)
 * @param head
 * @param val
 * @return
 */
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        if (head == null) {
            return head;
        }

        // 假设 removeElements() 返回后面完整的已经去掉val节点的子链表
        // 在当前递归层用当前节点接住后面的子链表
        // 随后判断当前层的node是否需要被删除，如果是，就返回
        // 也可以先判断是否需要删除当前node，但是这样条件语句会比较不好想
        head.next = removeElements(head.next, val);
        if (head.val == val) {
            return head.next;
        }
        return head;

        // 实际上就是还原一个从尾部开始重新构建链表的过程
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;707.设计链表&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/design-linked-list/&quot;&gt;707. 设计链表&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;中等&lt;/p&gt;
&lt;p&gt;你可以选择使用单链表或者双链表，设计并实现自己的链表。&lt;/p&gt;
&lt;p&gt;单链表中的节点应该具备两个属性：&lt;code&gt;val&lt;/code&gt; 和 &lt;code&gt;next&lt;/code&gt; 。&lt;code&gt;val&lt;/code&gt; 是当前节点的值，&lt;code&gt;next&lt;/code&gt; 是指向下一个节点的指针/引用。&lt;/p&gt;
&lt;p&gt;如果是双向链表，则还需要属性 &lt;code&gt;prev&lt;/code&gt; 以指示链表中的上一个节点。假设链表中的所有节点下标从 &lt;strong&gt;0&lt;/strong&gt; 开始。&lt;/p&gt;
&lt;p&gt;实现 &lt;code&gt;MyLinkedList&lt;/code&gt; 类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MyLinkedList()&lt;/code&gt; 初始化 &lt;code&gt;MyLinkedList&lt;/code&gt; 对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int get(int index)&lt;/code&gt; 获取链表中下标为 &lt;code&gt;index&lt;/code&gt; 的节点的值。如果下标无效，则返回 &lt;code&gt;-1&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void addAtHead(int val)&lt;/code&gt; 将一个值为 &lt;code&gt;val&lt;/code&gt; 的节点插入到链表中第一个元素之前。在插入完成后，新节点会成为链表的第一个节点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void addAtTail(int val)&lt;/code&gt; 将一个值为 &lt;code&gt;val&lt;/code&gt; 的节点追加到链表中作为链表的最后一个元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void addAtIndex(int index, int val)&lt;/code&gt; 将一个值为 &lt;code&gt;val&lt;/code&gt; 的节点插入到链表中下标为 &lt;code&gt;index&lt;/code&gt; 的节点之前。如果 &lt;code&gt;index&lt;/code&gt; 等于链表的长度，那么该节点会被追加到链表的末尾。如果 &lt;code&gt;index&lt;/code&gt; 比长度更大，该节点将 &lt;strong&gt;不会插入&lt;/strong&gt; 到链表中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void deleteAtIndex(int index)&lt;/code&gt; 如果下标有效，则删除链表中下标为 &lt;code&gt;index&lt;/code&gt; 的节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入
[&quot;MyLinkedList&quot;, &quot;addAtHead&quot;, &quot;addAtTail&quot;, &quot;addAtIndex&quot;, &quot;get&quot;, &quot;deleteAtIndex&quot;, &quot;get&quot;]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]

解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2);    // 链表变为 1-&amp;gt;2-&amp;gt;3
myLinkedList.get(1);              // 返回 2
myLinkedList.deleteAtIndex(1);    // 现在，链表变为 1-&amp;gt;3
myLinkedList.get(1);              // 返回 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= index, val &amp;lt;= 1000&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;请不要使用内置的 LinkedList 库。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;get&lt;/code&gt;、&lt;code&gt;addAtHead&lt;/code&gt;、&lt;code&gt;addAtTail&lt;/code&gt;、&lt;code&gt;addAtIndex&lt;/code&gt; 和 &lt;code&gt;deleteAtIndex&lt;/code&gt; 的次数不超过 &lt;code&gt;2000&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;单链表优化&lt;/h3&gt;
&lt;p&gt;链表设计及其初始化。链表中有链表大小 Size。在实现链表操作方法的时候，不要忘记 &lt;code&gt;l.Size++&lt;/code&gt; 和 &lt;code&gt;l.Size--&lt;/code&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type SingleNode struct {
    Val int
    Next *SingleNode
}

type MyLinkedList struct {
    DummyHead *SingleNode
    Size int
}

func Constructor() MyLinkedList {
    NewDummyHead := &amp;amp;SingleNode {
        Val: -1,
        Next: nil,
    }

    return MyLinkedList{
        DummyHead: NewDummyHead,
        Size: 0,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体对链表的操作均使用&lt;code&gt;虚拟头结点&lt;/code&gt;的方法，具体思路看下面。&lt;/p&gt;
&lt;p&gt;一开始只觉得删除节点可以用这个思想，其他方法的思路都是梦到哪写哪，写的很糟糕。&lt;/p&gt;
&lt;p&gt;针对自己写的版本的一些优化：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;遍历链表优化：&lt;/p&gt;
&lt;p&gt;只遍历到 &lt;code&gt;index&lt;/code&gt; 即可。这时 cur 到达 index 的前一个节点。&lt;/p&gt;
&lt;p&gt;如果不确定范围对不对，可以代入极端情况，例如 index = 0 的情况。具体看第三点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        for i := 0; i &amp;lt; index; i++ {
            cur = cur.Next
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对 &lt;code&gt;index&lt;/code&gt; 是否合法的判断也进行了优化。&lt;/p&gt;
&lt;p&gt;之前自己写的时候，总想把 &lt;code&gt;插入到 index 前&lt;/code&gt; 和&lt;code&gt;插入到末尾&lt;/code&gt;的情况单独讨论，分别调用 &lt;code&gt;AddAtHead()&lt;/code&gt; 和 &lt;code&gt;AddAtTail&lt;/code&gt; 。但实际上可以整合到一起处理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (l *MyLinkedList) AddAtIndex(index int, val int)  {
    if index &amp;lt; 0 {
        index = 0
    } else if index &amp;gt; l.Size {// 题中 index == l.Size 会将该节点添加到链表末尾，所以是 l.Size 不是 l.Size -
        return
    }
    newNode := &amp;amp;SingleNode{Val: val}
    cur := l.DummyHead
    // cur 指向 index 的前一个节点
    for i := 0; i &amp;lt; index; i++ {
        cur = cur.Next
    }
    newNode.Next = cur.Next
    cur.Next = newNode
    l.Size++
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;统一使用&lt;code&gt;虚拟头结点&lt;/code&gt;的思想，规范了变量名。如上面的代码所示。&lt;/p&gt;
&lt;p&gt;统一遍历到 cur 指向 index 的前一个节点，然后通过 cur 进行操作。&lt;/p&gt;
&lt;p&gt;例如下图的 AddAtIndex() 操作&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/016/image-20250918182049395.png&quot; alt=&quot;image-20250918182049395&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这时 cur 已经指向了 index 的前一个节点，接下来只要添加节点就好了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/016/image-20250918182127646.png&quot; alt=&quot;image-20250918182127646&quot; /&gt;&lt;/p&gt;
&lt;p&gt;添加节点时， &lt;code&gt;newNode&lt;/code&gt; 先指向 &lt;code&gt;indexNode&lt;/code&gt; ，&lt;code&gt;cur&lt;/code&gt; 再指向 &lt;code&gt;newNode&lt;/code&gt; 。如果搞反了，后面的节点就丢失了。&lt;/p&gt;
&lt;p&gt;如果不知道这么遍历对不对，可以代入极端情况，例如 index = 0 的情况。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;index == 0&lt;/code&gt;，不进入循环。&lt;code&gt;cur = DemmyHead&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/016/image-20250918182448589.png&quot; alt=&quot;image-20250918182448589&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;完整代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type SingleNode struct {
    Val int
    Next *SingleNode
}

type MyLinkedList struct {
    DummyHead *SingleNode
    Size int
}


func Constructor() MyLinkedList {
    NewDummyHead := &amp;amp;SingleNode {
        Val: -1,
        Next: nil,
    }

    return MyLinkedList{
        DummyHead: NewDummyHead,
        Size: 0,
    }
}


func (l *MyLinkedList) Get(index int) int {
    if l.Size &amp;lt; 1 {
        return -1
    } 
    if l == nil || index &amp;gt; l.Size - 1 || index &amp;lt; 0 {// 加上了 l == nil 的判断
        return -1
    } else {
        cur := l.DummyHead.Next
        for i := 0; i &amp;lt; index; i++ {
            cur = cur.Next
        }
        return cur.Val
    }
    return -1
}


func (l *MyLinkedList) AddAtHead(val int)  {
    newNode := &amp;amp;SingleNode{Val: val}
    newNode.Next = l.DummyHead.Next
    l.DummyHead.Next = newNode
    l.Size++
}


func (l *MyLinkedList) AddAtTail(val int)  {
    newNode := &amp;amp;SingleNode{Val: val}
    cur := l.DummyHead
    for cur.Next != nil {// 遍历到最后一个节点
        cur = cur.Next
    }
    newNode.Next = cur.Next
    cur.Next = newNode
    l.Size++
}


func (l *MyLinkedList) AddAtIndex(index int, val int)  {
    if index &amp;lt; 0 {
        index = 0
    } else if index &amp;gt; l.Size {// 题中 index == l.Size 会将该节点添加到链表末尾，所以是 l.Size 不是 l.Size -
        return
    }
    newNode := &amp;amp;SingleNode{Val: val}
    cur := l.DummyHead
    // cur 指向 index 的前一个节点
    for i := 0; i &amp;lt; index; i++ {
        cur = cur.Next
    }
    newNode.Next = cur.Next
    cur.Next = newNode
    l.Size++
}


func (l *MyLinkedList) DeleteAtIndex(index int)  {
    if index &amp;lt; 0 || index &amp;gt; l.Size - 1 {
        return
    }
    cur := l.DummyHead
    // 遍历到 index 的前一个节点
    for i := 0; i &amp;lt; index; i++ {
        cur = cur.Next
    }
    if cur.Next != nil {
        cur.Next = cur.Next.Next // 当前节点直接指向下下个节点，即删除了下一个节点
        l.Size--
    } else { // 链表中没有节点，DummyHead.Next == nil
        return
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自己思路写的单链表（烂）&lt;/h3&gt;
&lt;p&gt;思路很乱。虽然用到了虚拟头结点的思想，但是没有统一写法，每个函数想到哪写哪，逻辑不清晰，导致代码冗长。&lt;/p&gt;
&lt;p&gt;特别体现在链表的遍历、循环边界、index合法边界上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type SingleNode struct {
    Val int
    Next *SingleNode
}

type MyLinkedList struct {
    DummyHead *SingleNode
    Size int
}


func Constructor() MyLinkedList {
    NewDummyHead := &amp;amp;SingleNode {
        Val: -1,
        Next: nil,
    }

    return MyLinkedList{
        DummyHead: NewDummyHead,
        Size: 0,
    }
}


func (l *MyLinkedList) Get(index int) int {
    if l.Size &amp;lt; 1 {
        return -1
    } 
    if index &amp;gt; l.Size - 1 || index &amp;lt; 0 {
        return -1
    } else {
        tmpNode := l.DummyHead
        for i := 0; i &amp;lt; l.Size; i++ {
            tmpNode = tmpNode.Next
            if i == index {
                return tmpNode.Val
            }
        }
    }
    return -1
}


func (l *MyLinkedList) AddAtHead(val int)  {
    newNode := &amp;amp;SingleNode{
    Val: val,
    Next: nil,
    }
    if l.Size == 0 {
        l.DummyHead.Next = newNode
        l.Size += 1
    } else if l.Size &amp;gt; 0 {
        // 找到第一个节点
        originalHead := l.DummyHead.Next
        newNode.Next = originalHead
        l.DummyHead.Next = newNode
        l.Size += 1
    }
}


func (l *MyLinkedList) AddAtTail(val int)  {
    newNode := &amp;amp;SingleNode{
    Val: val,
    Next: nil,
    }

    if l.Size == 0 {
        l.DummyHead.Next = newNode
        l.Size += 1
    } else if l.Size &amp;gt; 0 {
        tmpNode := l.DummyHead
        for i := 0; i &amp;lt; l.Size; i++ {
            tmpNode = tmpNode.Next
        }
        tmpNode.Next = newNode
        l.Size += 1
    }
}


func (l *MyLinkedList) AddAtIndex(index int, val int)  {
    if index == 0 {
        l.AddAtHead(val)
    } else if index == l.Size {
        l.AddAtTail(val)
    }else if index &amp;gt; 0 &amp;amp;&amp;amp; index &amp;lt; l.Size {
        newNode := &amp;amp;SingleNode{
        Val: val,
        Next: nil,
        }
        // 找到 index 下标的节点
        var prevNode *SingleNode
        indexNode := l.DummyHead
        for i := 0; i &amp;lt; l.Size; i++ {
            indexNode = indexNode.Next
            if i == index - 1 {
                prevNode = indexNode
            }
            if i == index {
                break
            }
        }
        // 把 newNode 插入到 indexNode 前面
        // 先把 newNode 指向 indexNode
        newNode.Next = indexNode
        // 再把 prevNode 指向 newNode
        prevNode.Next = newNode
        l.Size += 1
    } else {// 非法 index 值
        return
    }
}


func (l *MyLinkedList) DeleteAtIndex(index int)  {
    // 如何避免操作空指针？
    if l.Size &amp;lt; 1 {
        return
    } 
    if index == 0 {
        // 找到 indexNode, prevNode 就是 DummyHead
        // indexNode := l.DummyHead.Next
        l.DummyHead.Next = l.DummyHead.Next.Next
        l.Size -= 1
    } else if index &amp;gt; 0 &amp;amp;&amp;amp; index &amp;lt;= l.Size - 1 {
        // 找到 prevNode 和 indexNode
        var prevNode *SingleNode
        indexNode := l.DummyHead
        for i := 0; i &amp;lt; l.Size; i++ {
            indexNode = indexNode.Next
            if i == index - 1 {
                prevNode = indexNode
            }
            if i == index {
                break
            }
        }
        prevNode.Next = indexNode.Next
        l.Size -= 1
    }
}


/**
 * Your MyLinkedList object will be instantiated and called as such:
 * obj := Constructor();
 * param_1 := obj.Get(index);
 * obj.AddAtHead(val);
 * obj.AddAtTail(val);
 * obj.AddAtIndex(index,val);
 * obj.DeleteAtIndex(index);
 */

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;双链表（未完成）&lt;/h3&gt;
&lt;p&gt;代码随想录上的参考代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//循环双链表
type MyLinkedList struct {
    dummy *Node
}

type Node struct {
    Val  int
    Next *Node
    Pre  *Node
}

//仅保存哑节点，pre-&amp;gt; rear, next-&amp;gt; head
/** Initialize your data structure here. */
func Constructor() MyLinkedList {
    rear := &amp;amp;Node{
        Val:  -1,
        Next: nil,
        Pre:  nil,
    }
    rear.Next = rear
    rear.Pre = rear
    return MyLinkedList{rear}
}

/** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */
func (this *MyLinkedList) Get(index int) int {
    head := this.dummy.Next
    //head == this, 遍历完全
    for head != this.dummy &amp;amp;&amp;amp; index &amp;gt; 0 {
        index--
        head = head.Next
    }
    //否则, head == this, 索引无效
    if 0 != index {
        return -1
    }
    return head.Val
}

/** Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. */
func (this *MyLinkedList) AddAtHead(val int) {
    dummy := this.dummy
    node := &amp;amp;Node{
        Val: val,
        //head.Next指向原头节点
        Next: dummy.Next,
        //head.Pre 指向哑节点
        Pre: dummy,
    }

    //更新原头节点
    dummy.Next.Pre = node
    //更新哑节点
    dummy.Next = node
    //以上两步不能反
}

/** Append a node of value val to the last element of the linked list. */
func (this *MyLinkedList) AddAtTail(val int) {
    dummy := this.dummy
    rear := &amp;amp;Node{
        Val: val,
        //rear.Next = dummy(哑节点)
        Next: dummy,
        //rear.Pre = ori_rear
        Pre: dummy.Pre,
    }

    //ori_rear.Next = rear
    dummy.Pre.Next = rear
    //update dummy
    dummy.Pre = rear
    //以上两步不能反
}

/** Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. */
func (this *MyLinkedList) AddAtIndex(index int, val int) {
    head := this.dummy.Next
    //head = MyLinkedList[index]
    for head != this.dummy &amp;amp;&amp;amp; index &amp;gt; 0 {
        head = head.Next
        index--
    }
    if index &amp;gt; 0 {
        return
    }
    node := &amp;amp;Node{
        Val: val,
        //node.Next = MyLinkedList[index]
        Next: head,
        //node.Pre = MyLinkedList[index-1]
        Pre: head.Pre,
    }
    //MyLinkedList[index-1].Next = node
    head.Pre.Next = node
    //MyLinkedList[index].Pre = node
    head.Pre = node
    //以上两步不能反
}

/** Delete the index-th node in the linked list, if the index is valid. */
func (this *MyLinkedList) DeleteAtIndex(index int) {
    //链表为空
    if this.dummy.Next == this.dummy {
        return
    }
    head := this.dummy.Next
    //head = MyLinkedList[index]
    for head.Next != this.dummy &amp;amp;&amp;amp; index &amp;gt; 0 {
        head = head.Next
        index--
    }
    //验证index有效
    if index == 0 {
        //MyLinkedList[index].Pre = index[index-2]
        head.Next.Pre = head.Pre
        //MyLinedList[index-2].Next = index[index]
        head.Pre.Next = head.Next
        //以上两步顺序无所谓
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Kiramux</author></item><item><title>链表算法（2）</title><link>https://notes.kiramux.com/posts/017-linkedlist-algorithm-2</link><guid isPermaLink="true">https://notes.kiramux.com/posts/017-linkedlist-algorithm-2</guid><pubDate>Sat, 20 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;25-09-19 反转链表、两数之和
25-09-20 两两交换链表中的节点
昨天和今天在准备速写带画教学内容，学习进度放缓...
相关链接：代码随想录 |&amp;gt; &lt;a href=&quot;https://programmercarl.com/0206.%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8.html&quot;&gt;206.反转链表&lt;/a&gt;|&lt;a href=&quot;https://programmercarl.com/0024.%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9.html&quot;&gt;24. 两两交换链表中的节点&lt;/a&gt;|&lt;a href=&quot;https://programmercarl.com/0001.%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C.html&quot;&gt;1. 两数之和&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;206.反转链表&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/reverse-linked-list/&quot;&gt;206. 反转链表&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;给你单链表的头节点 &lt;code&gt;head&lt;/code&gt; ，请你反转链表，并返回反转后的链表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/017/rev1ex1.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [1,2,3,4,5]
输出：[5,4,3,2,1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/017/rev1ex2.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [1,2]
输出：[2,1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = []
输出：[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;链表中节点的数目范围是 &lt;code&gt;[0, 5000]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-5000 &amp;lt;= Node.val &amp;lt;= 5000&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**进阶：**链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;双指针法&lt;/h3&gt;
&lt;p&gt;写的时候犯了个小错误，由于基础不牢，不知道怎么直接给 prev nil，然后直接写了 &lt;code&gt;prev := &amp;amp;ListNode{}&lt;/code&gt; 再赋值成 nil ，然后这个节点被丢弃掉，等待 GC 回收。多了一次无意义的分配。&lt;/p&gt;
&lt;p&gt;应该是： &lt;code&gt;var prev *ListNode&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;含义：声明了一个指针，初始状态是「空指针」，不指向任何元素。&lt;/p&gt;
&lt;p&gt;这证明我对结构体的传递还不熟悉。&lt;/p&gt;
&lt;p&gt;大致过程：&lt;/p&gt;
&lt;p&gt;prev = nil&lt;/p&gt;
&lt;p&gt;cur = head&lt;/p&gt;
&lt;p&gt;tmp = cur.Next // tmp 存储下一个节点，防止指针反向后丢失&lt;/p&gt;
&lt;p&gt;cur.Next = prev // 指针反向&lt;/p&gt;
&lt;p&gt;以上就是一次反向的过程，遍历的条件是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for cur != nil {
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/017/image-20250919211706978.png&quot; alt=&quot;image-20250919211706978&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/017/image-20250919211721024.png&quot; alt=&quot;image-20250919211721024&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;多次操作后...到达最后一次&lt;/p&gt;
&lt;p&gt;当 cur == tail 时是最后一次迭代，接着 cur = nil ，不进循环，tmp 也是 nil。此时 &lt;code&gt;return prev&lt;/code&gt; 就是反转后的链表&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/017/image-20250919211924684.png&quot; alt=&quot;image-20250919211924684&quot; /&gt;&lt;/p&gt;
&lt;p&gt;代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func reverseList(head *ListNode) *ListNode {
    // prev := &amp;amp;ListNode{}
    // prev = nil
    var prev *ListNode
    cur := head

    for cur != nil {
        tmp := cur.Next // 存储下一个节点/nil
        cur.Next = prev // 反转指向
        // 向后移动
        prev = cur
        cur = tmp
    }
    return prev
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;24. 两两交换链表中的节点&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/swap-nodes-in-pairs/&quot;&gt;24. 两两交换链表中的节点&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;中等&lt;/p&gt;
&lt;p&gt;给你一个链表，两两交换其中相邻的节点，并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题（即，只能进行节点交换）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/017/swap_ex1.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [1,2,3,4]
输出：[2,1,4,3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = []
输出：[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [1]
输出：[1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;链表中节点的数目在范围 &lt;code&gt;[0, 100]&lt;/code&gt; 内&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= Node.val &amp;lt;= 100&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;虚拟头结点&lt;/h3&gt;
&lt;p&gt;一定要画图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/017/image-20250920174456760.png&quot; alt=&quot;image-20250920174456760&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/017/image-20250920174517629.png&quot; alt=&quot;image-20250920174517629&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/017/image-20250920174532533.png&quot; alt=&quot;image-20250920174532533&quot; /&gt;&lt;/p&gt;
&lt;p&gt;设一个虚拟头结点指向 &lt;code&gt;head&lt;/code&gt; ，然后按照上图的步骤操作。注意这里需要设一个 tmp 存一下节点1。&lt;/p&gt;
&lt;p&gt;同时需要注意循环条件。&lt;/p&gt;
&lt;p&gt;当链表节点数为偶数时，&lt;code&gt;cur.Next != nil&lt;/code&gt; （空链表时也满足）；&lt;/p&gt;
&lt;p&gt;当链表节点数为奇数时，&lt;code&gt;cur.Next.Next != nil&lt;/code&gt; ；&lt;/p&gt;
&lt;p&gt;写循环条件时，&lt;code&gt;cur.Next != nil&lt;/code&gt; 应该写在 &lt;code&gt;cur.Next.Next != nil&lt;/code&gt; 前面，否则可能会操作空指针。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for cur.Next != nil &amp;amp;&amp;amp; cur.Next.Next != nil {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func swapPairs(head *ListNode) *ListNode {
    DummyHead := &amp;amp;ListNode{Next: head}
    cur := DummyHead

    for cur.Next != nil &amp;amp;&amp;amp; cur.Next.Next != nil {
        tmp := cur.Next
        cur.Next = tmp.Next
        tmp.Next = cur.Next.Next
        cur.Next.Next = tmp

        // 移动 cur
        cur = cur.Next.Next
    }
    return DummyHead.Next
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Kiramux</author></item><item><title>链表算法（3）</title><link>https://notes.kiramux.com/posts/018-linkedlist-algorithm-3</link><guid isPermaLink="true">https://notes.kiramux.com/posts/018-linkedlist-algorithm-3</guid><pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;25-09-21 删除链表的倒数第 N 个结点
25-09-22 链表相交、环形链表 II
相关链接：&lt;a href=&quot;https://programmercarl.com/%E9%93%BE%E8%A1%A8%E6%80%BB%E7%BB%93%E7%AF%87.html#%E9%93%BE%E8%A1%A8%E7%BB%8F%E5%85%B8%E9%A2%98%E7%9B%AE&quot;&gt;代码随想录_链表&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;19. 删除链表的倒数第 N 个结点&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/remove-nth-node-from-end-of-list/&quot;&gt;19. 删除链表的倒数第 N 个结点&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;中等&lt;/p&gt;
&lt;p&gt;提示&lt;/p&gt;
&lt;p&gt;给你一个链表，删除链表的倒数第 &lt;code&gt;n&lt;/code&gt; 个结点，并且返回链表的头结点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/018/remove_ex1.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [1,2,3,4,5], n = 2
输出：[1,2,3,5]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [1], n = 1
输出：[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [1,2], n = 1
输出：[1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;链表中结点的数目为 &lt;code&gt;sz&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= sz &amp;lt;= 30&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= Node.val &amp;lt;= 100&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= n &amp;lt;= sz&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**进阶：**你能尝试使用一趟扫描实现吗？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;题目中标注了 &lt;code&gt;1 &amp;lt;= n &amp;lt;= sz&lt;/code&gt; ，所以不用对&lt;code&gt;n&lt;/code&gt;做不合法判断。&lt;/p&gt;
&lt;p&gt;这道题适合用&lt;strong&gt;虚拟头结点&lt;/strong&gt;，统一节点操作，不需要对头结点特殊处理。&lt;/p&gt;
&lt;h3&gt;快慢指针法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;n 是要删除的倒数第 n 个节点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;要求第 size-n 个节点，快指针先走 n ，然后快慢指针一起走。&lt;/p&gt;
&lt;p&gt;则 快==慢+n 。当 快 == size 时，慢 == len - n （即要删除的节点）。&lt;/p&gt;
&lt;p&gt;要处理某个节点，需要获取它前面的一个节点，那么我们应该找到 len-n 前面的一个节点。这时让&lt;strong&gt;快指针多走一步&lt;/strong&gt;，那么 慢 == len-n-1。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func removeNthFromEnd(head *ListNode, n int) *ListNode {
    DummyHead := &amp;amp;ListNode{Next: head}
    fast, slow := DummyHead, DummyHead
    // 要求删除第 size-n 个节点，快指针先走 n ，再快慢一起走，则 fast == slow + n; 
    // 当 fast == len 时， slow == size - n （需要删除的节点）
    // 要删除某个节点，需要得到前一个节点，所以快指针多走一步(n+1)，slow == size - n - 1;
    for i := 0; i &amp;lt; n + 1; i++ {
        fast = fast.Next
    }
    
    for fast != nil { // fast == nil 时判断不通过，此时 fast 刚好走了 size 步
        fast = fast.Next
        slow = slow.Next
    }
    slow.Next = slow.Next.Next
    return DummyHead.Next
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;双循环法&lt;/h3&gt;
&lt;p&gt;先遍历链表获得 size，在遍历得到“前一个节点”，从而删除倒数第n个节点。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;当时没想到用虚拟头结点，单独处理了头结点。&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func removeNthFromEnd(head *ListNode, n int) *ListNode {
    var size int
    cur := head
    // 获取链表长度
    for cur != nil {
        size++
        cur = cur.Next
    }
    if n == size {
        return head.Next
    } else {
        cur = head
        for i := 0; i &amp;lt; size - n - 1; i++ {
            cur = cur.Next
        }
        cur.Next = cur.Next.Next
        return head
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;面试题 02.07. 链表相交&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/&quot;&gt;面试题 02.07. 链表相交&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;提示&lt;/p&gt;
&lt;p&gt;给你两个单链表的头节点 &lt;code&gt;headA&lt;/code&gt; 和 &lt;code&gt;headB&lt;/code&gt; ，请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点，返回 &lt;code&gt;null&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;图示两个链表在节点 &lt;code&gt;c1&lt;/code&gt; 开始相交**：**&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/14/160_statement.png&quot;&gt;&lt;img src=&quot;../images/018/160_statement.png&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;题目数据 &lt;strong&gt;保证&lt;/strong&gt; 整个链式结构中不存在环。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;，函数返回结果后，链表必须 &lt;strong&gt;保持其原始结构&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://assets.leetcode.com/uploads/2018/12/13/160_example_1.png&quot;&gt;&lt;img src=&quot;../images/018/160_example_1.png&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出：Intersected at &apos;8&apos;
解释：相交节点的值为 8 （注意，如果两个链表相交则不能为 0）。
从各自的表头开始算起，链表 A 为 [4,1,8,4,5]，链表 B 为 [5,0,1,8,4,5]。
在 A 中，相交节点前有 2 个节点；在 B 中，相交节点前有 3 个节点。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://assets.leetcode.com/uploads/2018/12/13/160_example_2.png&quot;&gt;&lt;img src=&quot;../images/018/160_example_2.png&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出：Intersected at &apos;2&apos;
解释：相交节点的值为 2 （注意，如果两个链表相交则不能为 0）。
从各自的表头开始算起，链表 A 为 [0,9,1,2,4]，链表 B 为 [3,2,4]。
在 A 中，相交节点前有 3 个节点；在 B 中，相交节点前有 1 个节点。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://assets.leetcode.com/uploads/2018/12/13/160_example_3.png&quot;&gt;&lt;img src=&quot;../images/018/160_example_3.png&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出：null
解释：从各自的表头开始算起，链表 A 为 [2,6,4]，链表 B 为 [1,5]。
由于这两个链表不相交，所以 intersectVal 必须为 0，而 skipA 和 skipB 可以是任意值。
这两个链表不相交，因此返回 null 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;listA&lt;/code&gt; 中节点数目为 &lt;code&gt;m&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;listB&lt;/code&gt; 中节点数目为 &lt;code&gt;n&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= m, n &amp;lt;= 3 * 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= Node.val &amp;lt;= 105&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= skipA &amp;lt;= m&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= skipB &amp;lt;= n&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;listA&lt;/code&gt; 和 &lt;code&gt;listB&lt;/code&gt; 没有交点，&lt;code&gt;intersectVal&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;listA&lt;/code&gt; 和 &lt;code&gt;listB&lt;/code&gt; 有交点，&lt;code&gt;intersectVal == listA[skipA + 1] == listB[skipB + 1]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**进阶：**你能否设计一个时间复杂度 &lt;code&gt;O(n)&lt;/code&gt; 、仅用 &lt;code&gt;O(1)&lt;/code&gt; 内存的解决方案？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;思路&lt;/h3&gt;
&lt;p&gt;简单来说，就是求两个链表交点节点的指针。这里要注意，&lt;strong&gt;交点不是数值相等，而是指针相等。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了方便举例，假设节点元素数值相等，则节点指针相等。&lt;/p&gt;
&lt;p&gt;看如下两个链表，目前curA指向链表A的头结点，curB指向链表B的头结点：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/018/%E9%9D%A2%E8%AF%95%E9%A2%9802.07.%E9%93%BE%E8%A1%A8%E7%9B%B8%E4%BA%A4_1.png&quot; alt=&quot;面试题02.07.链表相交_1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们求出两个链表的长度，并求出两个链表长度的差值，然后让curA移动到，和curB 末尾对齐的位置，如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/018/%E9%9D%A2%E8%AF%95%E9%A2%9802.07.%E9%93%BE%E8%A1%A8%E7%9B%B8%E4%BA%A4_2.png&quot; alt=&quot;面试题02.07.链表相交_2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时我们就可以比较 curA 和 curB 是否相同。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果不相同：&lt;/strong&gt; 同时向后移动 curA 和 curB；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果相同：&lt;/strong&gt; 则找到交点&lt;/p&gt;
&lt;p&gt;否则退出循环，返回空指针。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func getIntersectionNode(headA, headB *ListNode) *ListNode {
    // 分别遍历到最后一个节点，获取到链表长度
    curA := headA
    curB := headB
    lenA, lenB := 0, 0
    for curA != nil {
        lenA++
        curA = curA.Next
    }
    for curB != nil {
        lenB++
        curB = curB.Next
    }

    step := 0
    var fast, slow *ListNode
    if lenA &amp;gt; lenB {
        step = lenA - lenB
        fast, slow = headA, headB
    } else {
        step = lenB - lenA
        fast, slow = headB, headA
    }

    // 先让 fast 走 step 步，将链表对齐
    for i := 0; i &amp;lt; step; i++ {
        fast = fast.Next
    }

    for fast != slow {
        fast = fast.Next
        slow = slow.Next
    }
    return fast
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;go 中两个 nil 可能会不相等吗&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;同一类型的 nil 值是相等的&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var p1 *int = nil
var p2 *int = nil
fmt.Println(p1 == p2) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不同类型的 nil 不能直接比较&lt;/strong&gt;，编译会报错&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var p *int = nil
var s []int = nil
fmt.Println(p == s) // ❌ 编译错误：mismatched types *int and []int
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接口中存的 nil 可能和直接的 nil 不相等&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var x interface{} = nil
var y interface{} = (*int)(nil)

fmt.Println(x == nil) // true
fmt.Println(y == nil) // false，因为 y 的动态类型是 *int，动态值是 nil
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;142. 环形链表 II&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/linked-list-cycle-ii/&quot;&gt;142. 环形链表 II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;中等&lt;/p&gt;
&lt;p&gt;给定一个链表的头节点  &lt;code&gt;head&lt;/code&gt; ，返回链表开始入环的第一个节点。 &lt;em&gt;如果链表无环，则返回 &lt;code&gt;null&lt;/code&gt;。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;如果链表中有某个节点，可以通过连续跟踪 &lt;code&gt;next&lt;/code&gt; 指针再次到达，则链表中存在环。 为了表示给定链表中的环，评测系统内部使用整数 &lt;code&gt;pos&lt;/code&gt; 来表示链表尾连接到链表中的位置（&lt;strong&gt;索引从 0 开始&lt;/strong&gt;）。如果 &lt;code&gt;pos&lt;/code&gt; 是 &lt;code&gt;-1&lt;/code&gt;，则在该链表中没有环。&lt;strong&gt;注意：&lt;code&gt;pos&lt;/code&gt; 不作为参数进行传递&lt;/strong&gt;，仅仅是为了标识链表的实际情况。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不允许修改&lt;/strong&gt; 链表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/018/circularlinkedlist.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [3,2,0,-4], pos = 1
输出：返回索引为 1 的链表节点
解释：链表中有一个环，其尾部连接到第二个节点。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/018/circularlinkedlist_test2.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [1,2], pos = 0
输出：返回索引为 0 的链表节点
解释：链表中有一个环，其尾部连接到第一个节点。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/018/circularlinkedlist_test3.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：head = [1], pos = -1
输出：返回 null
解释：链表中没有环。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;链表中节点的数目范围在范围 &lt;code&gt;[0, 104]&lt;/code&gt; 内&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-105 &amp;lt;= Node.val &amp;lt;= 105&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pos&lt;/code&gt; 的值为 &lt;code&gt;-1&lt;/code&gt; 或者链表中的一个有效索引&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**进阶：**你是否可以使用 &lt;code&gt;O(1)&lt;/code&gt; 空间解决此题？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;快慢指针法&lt;/h3&gt;
&lt;h4&gt;判断链表是否有环&lt;/h4&gt;
&lt;p&gt;可以用快慢指针法，分别定义 fast 和 slow 从头结点出发，fast 每次移动2个节点，slow 每次移动1个节点。如果 fast 和 slow 在途中相遇，说明这个链表有环。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么 fast 走2个节点， slow 走一个节点，有环的话，一定在环内相遇呢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;为什么 fast 走2个节点，slow 走一个节点，有环的话一定会在环内相遇呢，而不是永远的错开呢？&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;首先，&lt;strong&gt;fast 一定先进入环中&lt;/strong&gt;，如果 fast 和 slow 相遇，&lt;strong&gt;一定是在环中相遇&lt;/strong&gt;。（fast 转一圈和 slow 在入口也算在环中相遇）&lt;/p&gt;
&lt;p&gt;为什么 fast 和 slow 一定会相遇呢？&lt;/p&gt;
&lt;p&gt;相对与 slow 来说，fast 是一个节点一个节点地靠近 slow 的，所以 fast 一定可以和 slow 重合。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/018/141.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8.gif&quot; alt=&quot;141.环形链表&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;如果有环，如何找到这个环的入口&lt;/h4&gt;
&lt;p&gt;假设从&lt;code&gt;头结点&lt;/code&gt;到&lt;code&gt;环形入口节点&lt;/code&gt;的节点数为 &lt;code&gt;x&lt;/code&gt; ；&lt;/p&gt;
&lt;p&gt;从&lt;code&gt;环形入口节点&lt;/code&gt; 到 fast 与 slow &lt;code&gt;相遇节点&lt;/code&gt; 的节点数为 &lt;code&gt;y&lt;/code&gt;；&lt;/p&gt;
&lt;p&gt;从&lt;code&gt;相遇节点&lt;/code&gt; 再到 &lt;code&gt;环形节点入口&lt;/code&gt; 的节点数为 &lt;code&gt;z&lt;/code&gt;。如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/018/20220925103433.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么相遇时，slow 走过的节点数为 &lt;code&gt;x+y&lt;/code&gt;，fast 走过的节点数为 &lt;code&gt;x+y + n(y+z)&lt;/code&gt; ， n 为 fast 在环内走了 n 圈才遇到 slow，&lt;code&gt;(y+z)&lt;/code&gt; 为一圈内节点的个数。&lt;/p&gt;
&lt;p&gt;因为 fast 一次走2个节点，slow 一次走1个节点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;slow走过的节点数 *2 == fast; 即：
(x + y)*2 == x+y + n*(y+z)
两边消掉1个(x+y)
x + y == n*(y+z)
需要求的是x
x == n*(y+z) - y
再提出一个 (y+z) 
x == (n-1)*(y+z) + z
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;n一定是大于等于1的，因为 fast 一定要多走一圈才能遇到 slow 。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;x == (n-1)*(y+z) + z&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当 n == 1 时&lt;/strong&gt;（fast 多走1圈遇到 slow），&lt;code&gt;x == z&lt;/code&gt;。这意味着：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;从头结点出发一个指针，同时从相遇节点也出发一个指针，这两个指针每次只走一个节点，那么当这两个节点相遇时就找到了&lt;code&gt;环形入口&lt;/code&gt;的节点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/018/142.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II%EF%BC%88%E6%B1%82%E5%85%A5%E5%8F%A3%EF%BC%89.gif&quot; alt=&quot;142.环形链表II（求入口）&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当 n &amp;gt; 1 时&lt;/strong&gt;，就是 fast 在环形转n圈后才遇到的 slow 指针，然后两个 index 开始走，和 n == 1 的情况效果是一样的。&lt;/p&gt;
&lt;p&gt;详细代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func detectCycle(head *ListNode) *ListNode {
    slow, fast := head, head
    for fast != nil &amp;amp;&amp;amp; fast.Next != nil {
        // 因为下一行要保证 fast.Next 不是空指针，从而也需要保证 fast 不是空指针
        fast = fast.Next.Next
        slow = slow.Next
        if fast == slow {
            for slow != head {
                slow = slow.Next
                head = head.Next
            }
            return head
        }
    }
    return nil
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;哈希表&lt;/h3&gt;
&lt;p&gt;遍历链表时，把遍历到的节点存进 map 中。&lt;/p&gt;
&lt;p&gt;每次遍历节点时，查询下个节点是否在 map 中，如果在说明就找到了环入口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func detectCycle(head *ListNode) *ListNode {
    existNodes := make(map[*ListNode]int)
    index := 0
    cur := head
    for cur != nil {
        existNodes[cur] = index
        index++
        cur = cur.Next
        for node, _ := range existNodes {
            if cur == node {// 找到了入口
            return cur
            }
        }
    }
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../images/018/%E9%93%BE%E8%A1%A8%E6%80%BB%E7%BB%93.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>哈希表算法（1）</title><link>https://notes.kiramux.com/posts/019-hash-table-algorithm-1</link><guid isPermaLink="true">https://notes.kiramux.com/posts/019-hash-table-algorithm-1</guid><pubDate>Tue, 23 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;哈希表相关算法题 | &lt;a href=&quot;https://programmercarl.com/%E5%93%88%E5%B8%8C%E8%A1%A8%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html#%E5%93%88%E5%B8%8C%E8%A1%A8&quot;&gt;代码随想录_哈希表&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;什么时候可能会用到哈希表？&lt;/h2&gt;
&lt;p&gt;给一个元素，&lt;strong&gt;需要判断它是否在一个集合中出现过&lt;/strong&gt;，通常就需要用哈希表了。&lt;/p&gt;
&lt;p&gt;在 Go 语言中，可以使用数组或者 map 来解题。（例如数组下标存元素、map 的 key 存数据……）&lt;/p&gt;
&lt;p&gt;数据量小用数组；数据量散用 map 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;例如要处理 0, 5, 1000000 这三个数，如果用数组下标需要开辟很大的数组，但要处理的数据只有3个，会造成很大的空间浪费，这时使用 map 合适。
像 &lt;code&gt;242.有效的字母异位词&lt;/code&gt; 中用数组就比较方便。如果用 map 的话还需要进行哈希运算，用数组比较省。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;242.有效的字母异位词&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/valid-anagram/&quot;&gt;242. 有效的字母异位词&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;给定两个字符串 &lt;code&gt;s&lt;/code&gt; 和 &lt;code&gt;t&lt;/code&gt; ，编写一个函数来判断 &lt;code&gt;t&lt;/code&gt; 是否是 &lt;code&gt;s&lt;/code&gt; 的 字母异位词。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入: s = &quot;anagram&quot;, t = &quot;nagaram&quot;
输出: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入: s = &quot;rat&quot;, t = &quot;car&quot;
输出: false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= s.length, t.length &amp;lt;= 5 * 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 和 &lt;code&gt;t&lt;/code&gt; 仅包含小写字母&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;进阶:&lt;/strong&gt; 如果输入字符串包含 unicode 字符怎么办？你能否调整你的解法来应对这种情况？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;哈希表&lt;/h3&gt;
&lt;p&gt;数据比较小用数组，&lt;s&gt;数据比较大用set&lt;/s&gt;，数据比较散用map&lt;/p&gt;
&lt;p&gt;构建一个大小为26的数组 hash ，遍历第一个单词 s ，出现一个字母，对应位置+1，a对应0。&lt;/p&gt;
&lt;p&gt;再遍历第二个单词 t ，并对对应位置元素-1。&lt;/p&gt;
&lt;p&gt;最后遍历 hash，如果出现非0元素，那么这两个单词一定不是字母异位词。（力扣上认为 &lt;code&gt;abc&lt;/code&gt; 和 &lt;code&gt;abc&lt;/code&gt; 也是字母异位词）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func isAnagram(s string, t string) bool {
    hash := [26]int{}
    for i := 0; i &amp;lt; len(s); i++ {
        hash[s[i] - &apos;a&apos;]++
    }
    for i := 0; i &amp;lt; len(t); i++ {
        hash[t[i] - &apos;a&apos;]--
    }
    for i := 0; i &amp;lt; 26; i++ {
        if hash[i] != 0 {
            return false
        }
    }
    return true
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;349. 两个数组的交集&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/intersection-of-two-arrays/&quot;&gt;349. 两个数组的交集&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;给定两个数组 &lt;code&gt;nums1&lt;/code&gt; 和 &lt;code&gt;nums2&lt;/code&gt; ，返回 &lt;em&gt;它们的 交集&lt;/em&gt; 。输出结果中的每个元素一定是 &lt;strong&gt;唯一&lt;/strong&gt; 的。我们可以 &lt;strong&gt;不考虑输出结果的顺序&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums1 = [1,2,2,1], nums2 = [2,2]
输出：[2]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出：[9,4]
解释：[4,9] 也是可通过的
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= nums1.length, nums2.length &amp;lt;= 1000&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= nums1[i], nums2[i] &amp;lt;= 1000&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;思路：哈希表&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../images/019/image-20250923100957914.png&quot; alt=&quot;image-20250923100957914&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;哈希表--map&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func intersection(nums1 []int, nums2 []int) []int {
    hash := make(map[int]int)
    index := 0
    var result []int
    for i := 0; i &amp;lt; len(nums1); i++ {
        hash[nums1[i]] = index
        index++
    }
    for i := 0; i &amp;lt; len(nums2); i++ {
        if _, ok := hash[nums2[i]]; ok {
            // 先写入 result
            result = append(result, nums2[i])
            // 删除 hash 中这个数值，避免之后 result 元素重复
            delete(hash, nums2[i])
        }
    }
    return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实一开始写的版本是，每次循环判断 hash 中是否出现过 nums[2]，逻辑很不清晰。&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;展开查看&amp;lt;/summary&amp;gt;
&amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;
func intersection(nums1 []int, nums2 []int) []int {
hash := make(map[int]int)
index := 0
var result []int
for i := 0; i &amp;lt; len(nums1); i++ {
hash[nums1[i]] = index
index++
}
for i := 0; i &amp;lt; len(nums2); i++ {
if _, ok := hash[nums2[i]]; ok {
// 写入切片前先判断是否会重复
exist := false
for j := 0; j &amp;lt; len(result); j++ {
if result[j] == nums2[i] {
exist = true
// 也可以检测到在 hash 中存在，就删掉 hash 中的这个值。就是这一步遍历的可能多一点。
break
}
}
if !exist {
result = append(result, nums2[i])
}
}
}
return result
}
&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h3&gt;哈希表--数组&lt;/h3&gt;
&lt;p&gt;力扣上新增了以下条件，现在可以用数组求解了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= nums1.length, nums2.length &amp;lt;= 1000&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= nums1[i], nums2[i] &amp;lt;= 1000&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用数组的下标来存储 nums1 的元素，如果该元素出现过则 hash 中该下标对应的值设为1（顺便可以去重）。&lt;/p&gt;
&lt;p&gt;然后用 nums2 中的元素去查询 hash，如果查到了就放进 result，顺便把 hash 对应的值改为 0 实现 result 去重。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func intersection(nums1 []int, nums2 []int) []int {
    hash := [1001]int{}
    result := []int{}
    for i := 0; i &amp;lt; len(nums1); i++ {
        // 用下标存储 nums1 的元素
        hash[nums1[i]] = 1
    }

    for i := 0; i &amp;lt; len(nums2); i++ {
        // 判断 hash 中是否出现过 nums2[i]
        if hash[nums2[i]] == 1 {
            result = append(result, nums2[i])
            hash[nums2[i]] = 0
        }
    }
    return result
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Kiramux</author></item><item><title>哈希表算法（2）</title><link>https://notes.kiramux.com/posts/020-hash-table-algorithm-2</link><guid isPermaLink="true">https://notes.kiramux.com/posts/020-hash-table-algorithm-2</guid><pubDate>Thu, 25 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;202. 快乐数&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/happy-number/&quot;&gt;202. 快乐数&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;编写一个算法来判断一个数 &lt;code&gt;n&lt;/code&gt; 是不是快乐数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「快乐数」&lt;/strong&gt; 定义为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于一个正整数，每一次将该数替换为它每个位置上的数字的平方和。&lt;/li&gt;
&lt;li&gt;然后重复这个过程直到这个数变为 1，也可能是 &lt;strong&gt;无限循环&lt;/strong&gt; 但始终变不到 1。&lt;/li&gt;
&lt;li&gt;如果这个过程 &lt;strong&gt;结果为&lt;/strong&gt; 1，那么这个数就是快乐数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果 &lt;code&gt;n&lt;/code&gt; 是 &lt;em&gt;快乐数&lt;/em&gt; 就返回 &lt;code&gt;true&lt;/code&gt; ；不是，则返回 &lt;code&gt;false&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：n = 19
输出：true
解释：
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：n = 2
输出：false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= n &amp;lt;= 231 - 1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;思路&lt;/h3&gt;
&lt;p&gt;这道题看上去貌似是一道数学问题，其实并不是！&lt;/p&gt;
&lt;p&gt;题中说了会 &lt;strong&gt;无限循环&lt;/strong&gt;，也就是说求和的过程中，&lt;strong&gt;sum 会反复出现&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;而当我们遇到了要快速判断一个元素是否出现在集合里的时候，就要考虑哈希法了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以这道题用哈希法来判断 sum 是否重复出现。如果重复了就 return false， 否则一直找到 sum 为止。&lt;/p&gt;
&lt;p&gt;判断 sum 是否重复出现可以用 map[int][bool].&lt;/p&gt;
&lt;p&gt;还有一个难点是求和的过程，要对 取数值各个位上的单数 很熟悉。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这道题还学到一点，&lt;code&gt;return n == 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这里的 n == 1 是个比较表达式，它的值是 bool，并不是把整数当作 bool 来用。等价于&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if n == 0 {

	return true

} else {

	return false

}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;详细代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func isHappy(n int) bool {
    hash := make(map[int]bool)
    for n != 1 &amp;amp;&amp;amp; !hash[n] {
        hash[n] = true
        n = getsum(n)
    }
    // 这里是一个比较表达式，值是bool，而不是把整数当作 bool 值
    return n == 1
}

func getsum(n int) int {
    sum := 0
    for n &amp;gt; 0 {
        sum += (n % 10) * (n % 10)
        n /= 10
    }
    return sum
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1. 两数之和&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/two-sum/&quot;&gt;1. 两数之和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;提示&lt;/p&gt;
&lt;p&gt;给定一个整数数组 &lt;code&gt;nums&lt;/code&gt; 和一个整数目标值 &lt;code&gt;target&lt;/code&gt;，请你在该数组中找出 &lt;strong&gt;和为目标值&lt;/strong&gt; &lt;em&gt;&lt;code&gt;target&lt;/code&gt;&lt;/em&gt; 的那 &lt;strong&gt;两个&lt;/strong&gt; 整数，并返回它们的数组下标。&lt;/p&gt;
&lt;p&gt;你可以假设每种输入只会对应一个答案，并且你不能使用两次相同的元素。&lt;/p&gt;
&lt;p&gt;你可以按任意顺序返回答案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [2,7,11,15], target = 9
输出：[0,1]
解释：因为 nums[0] + nums[1] == 9 ，返回 [0, 1] 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [3,2,4], target = 6
输出：[1,2]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [3,3], target = 6
输出：[0,1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;2 &amp;lt;= nums.length &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-109 &amp;lt;= nums[i] &amp;lt;= 109&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-109 &amp;lt;= target &amp;lt;= 109&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只会存在一个有效答案&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**进阶：**你可以想出一个时间复杂度小于 &lt;code&gt;O(n2)&lt;/code&gt; 的算法吗？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;力扣第一题却难度不低，属于哈希表的题。&lt;/p&gt;
&lt;h3&gt;暴力解法，2层 for 循环&lt;/h3&gt;
&lt;p&gt;噩梦开始的地方。。。&lt;/p&gt;
&lt;p&gt;第一次写用了两层 for 循环。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func twoSum(nums []int, target int) []int {
    len := len(nums)
    for i, num := range nums {
        for j := i + 1; j &amp;lt; len; j++ {
            if tar := num + nums[j]; tar == target {
                return []int{i,j}
            }
        }
    }
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;哈希表（最优解）&lt;/h3&gt;
&lt;p&gt;map key 存数组的值，value 存数组的下标，来记录出现过的元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func twoSum(nums []int, target int) []int {
    m := make(map[int]int) // 值 -&amp;gt; 下标
    for i, num := range nums {
        if j, ok := m[target-num]; ok {
            return []int{j, i}
        }
        m[num] = i
    }
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四数相加 II&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/4sum-ii/&quot;&gt;454. 四数相加 II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;中等&lt;/p&gt;
&lt;p&gt;给你四个整数数组 &lt;code&gt;nums1&lt;/code&gt;、&lt;code&gt;nums2&lt;/code&gt;、&lt;code&gt;nums3&lt;/code&gt; 和 &lt;code&gt;nums4&lt;/code&gt; ，数组长度都是 &lt;code&gt;n&lt;/code&gt; ，请你计算有多少个元组 &lt;code&gt;(i, j, k, l)&lt;/code&gt; 能满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= i, j, k, l &amp;lt; n&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出：2
解释：
两个元组如下：
1. (0, 0, 0, 1) -&amp;gt; nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -&amp;gt; nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出：1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;n == nums1.length&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n == nums2.length&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n == nums3.length&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n == nums4.length&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= n &amp;lt;= 200&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-228 &amp;lt;= nums1[i], nums2[i], nums3[i], nums4[i] &amp;lt;= 228&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;思路--哈希表&lt;/h3&gt;
&lt;p&gt;先遍历数组 A 和数组 B，用 map 记录 &lt;code&gt;a+b&lt;/code&gt; 出现的次数。&lt;/p&gt;
&lt;p&gt;再遍历数组 C 和数组 D， 查询 map 中是否出现 &lt;code&gt;0-(c+d)&lt;/code&gt;， 如果出现，counter += value。&lt;/p&gt;
&lt;p&gt;相当于转化为了&lt;strong&gt;两数之和&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func fourSumCount(nums1 []int, nums2 []int, nums3 []int, nums4 []int) int {
    hash := make(map[int]int)
    counter := 0
    for _, a := range nums1 {
        for _, b := range nums2 {
            hash[a+b]++
        }
    }

    for _, c := range nums3 {
        for _, d := range nums4 {
            if v, ok := hash[0 - (c + d)]; ok {
                counter += v
            }
        }
    }
    return counter
}

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么遍历 A、B 再遍历 C、D？为什么不能先遍历 A 再遍历 B、C、D？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;两两遍历，时间复杂度是 n^2 ；后者的时间复杂度是 n^3。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;赎金信&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/ransom-note/&quot;&gt;383. 赎金信&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;给你两个字符串：&lt;code&gt;ransomNote&lt;/code&gt; 和 &lt;code&gt;magazine&lt;/code&gt; ，判断 &lt;code&gt;ransomNote&lt;/code&gt; 能不能由 &lt;code&gt;magazine&lt;/code&gt; 里面的字符构成。&lt;/p&gt;
&lt;p&gt;如果可以，返回 &lt;code&gt;true&lt;/code&gt; ；否则返回 &lt;code&gt;false&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;magazine&lt;/code&gt; 中的每个字符只能在 &lt;code&gt;ransomNote&lt;/code&gt; 中使用一次。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：ransomNote = &quot;a&quot;, magazine = &quot;b&quot;
输出：false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：ransomNote = &quot;aa&quot;, magazine = &quot;ab&quot;
输出：false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：ransomNote = &quot;aa&quot;, magazine = &quot;aab&quot;
输出：true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= ransomNote.length, magazine.length &amp;lt;= 105&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ransomNote&lt;/code&gt; 和 &lt;code&gt;magazine&lt;/code&gt; 由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;思路&lt;/h3&gt;
&lt;p&gt;这道题和 &lt;a href=&quot;https://programmercarl.com/0242.%E6%9C%89%E6%95%88%E7%9A%84%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D.html&quot;&gt;242.有效的字母异位词&lt;/a&gt; 很像。242 相当于求&lt;code&gt;字符串a&lt;/code&gt;和&lt;code&gt;字符串b&lt;/code&gt;是否可以相互组成，而这道题是求&lt;code&gt;字符串a&lt;/code&gt;能否组成&lt;code&gt;字符串b&lt;/code&gt;，而不用管&lt;code&gt;字符串b&lt;/code&gt;能不能组成&lt;code&gt;字符串a&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;magazine&lt;/code&gt; 中的每个字符只能在 &lt;code&gt;ransomNote&lt;/code&gt; 中使用一次。&lt;/li&gt;
&lt;li&gt;由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;思路就是用哈希表来存每个字母出现的次数。&lt;/p&gt;
&lt;h3&gt;哈希法--map&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func canConstruct(ransomNote string, magazine string) bool {
    hash := make(map[rune]int)
    for _, c := range magazine {
        hash[c]++
    }
    for _, c := range ransomNote {
        if hash[c] == 0 {
            return false
        }
        hash[c]--
    }
    return true
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;哈希法--数组&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func canConstruct(ransomNote string, magazine string) bool {
    hash := [26]int{}
    for _, c := range magazine {
        index := c - &apos;a&apos;
        hash[index]++
    }
    for _, c := range ransomNote {
        index := c - &apos;a&apos;
        if hash[index] &amp;gt; 0 {
            hash[index]--
        } else if hash[index] == 0 {
            return false
        }
    }
    return true
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Kiramux</author></item><item><title>哈希表算法（3）</title><link>https://notes.kiramux.com/posts/021-hash-table-algorithm-3</link><guid isPermaLink="true">https://notes.kiramux.com/posts/021-hash-table-algorithm-3</guid><pubDate>Sat, 27 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;三数之和和四数之和被放在哈希表章节中，但是其实不适合用哈希表来做，剪枝操作很麻烦。这里主要用双指针法来做。双指针法的好处是可以省一次 for 循环。
代码随想录 | &lt;a href=&quot;https://programmercarl.com/0015.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C.html&quot;&gt;三数之和&lt;/a&gt;、&lt;a href=&quot;https://programmercarl.com/0018.%E5%9B%9B%E6%95%B0%E4%B9%8B%E5%92%8C.html&quot;&gt;四数之和&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;15. 三数之和&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/3sum/&quot;&gt;15. 三数之和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;中等&lt;/p&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; ，判断是否存在三元组 &lt;code&gt;[nums[i], nums[j], nums[k]]&lt;/code&gt; 满足 &lt;code&gt;i != j&lt;/code&gt;、&lt;code&gt;i != k&lt;/code&gt; 且 &lt;code&gt;j != k&lt;/code&gt; ，同时还满足 &lt;code&gt;nums[i] + nums[j] + nums[k] == 0&lt;/code&gt; 。请你返回所有和为 &lt;code&gt;0&lt;/code&gt; 且不重复的三元组。&lt;/p&gt;
&lt;p&gt;**注意：**答案中不可以包含重复的三元组。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [-1,0,1,2,-1,-4]
输出：[[-1,-1,2],[-1,0,1]]
解释：
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意，输出的顺序和三元组的顺序并不重要。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [0,1,1]
输出：[]
解释：唯一可能的三元组和不为 0 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [0,0,0]
输出：[[0,0,0]]
解释：唯一可能的三元组和为 0 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;3 &amp;lt;= nums.length &amp;lt;= 3000&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-105 &amp;lt;= nums[i] &amp;lt;= 105&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;双指针法&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../images/021/15.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C.gif&quot; alt=&quot;15.三数之和&quot; /&gt;&lt;/p&gt;
&lt;p&gt;首先将数组排序，然后有一层for循环，&lt;code&gt;i&lt;/code&gt; 从下标0的地方开始，&lt;code&gt;left&lt;/code&gt; 在 &lt;code&gt;i+1&lt;/code&gt; 的位置上， &lt;code&gt;right&lt;/code&gt; 在 &lt;code&gt;len-1&lt;/code&gt; 的位置上。&lt;/p&gt;
&lt;p&gt;在数组中找到abc使得 &lt;code&gt;a+b+c==0&lt;/code&gt;，这里 a = nums[i], b = nums[left]， c = nums[right]。&lt;/p&gt;
&lt;p&gt;首先判断 nums[i] 是否大于 0， 如果 a 大于0，那么 a+b+c 一定大于0。那么如何移动 left 和 right 呢？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里需要注意 target &amp;gt; 0 才可以这样写。如果 target &amp;lt; 0，a &amp;lt; 0 也是可以找到三元组的。
例如 [-4, -1, 0]， target == 5&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果 &lt;code&gt;a+b+c &amp;gt; 0&lt;/code&gt; 说明三数之和大了，因为数组排序过，所以 right 应该向左移动，这样三数之和才能小一点。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;a+b+c &amp;lt; 0&lt;/code&gt; 说明三数之和小了，应该让 left 向右移动，这样三数之和才能大一点。&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;a+b+c == 0&lt;/code&gt; 时开始收集结果，在收集到一个结果之后去重，right 也是一样。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for left &amp;lt; right &amp;amp;&amp;amp; nums[left] == nums[left+1] {
    left++
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 nums[left] 和 下一个数不同时，&lt;strong&gt;至少保证一次指针移动&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;完整代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func threeSum(nums []int) [][]int {
    sort.Ints(nums)
    len := len(nums)
    result := [][]int{}
    for i := 0; i &amp;lt; len - 2; i++ {
        if nums[0] &amp;gt; 0 {
           break
        }
        // 如果 nums[i] 和前一个重复，则进行下一次循环
        if i &amp;gt; 0 &amp;amp;&amp;amp; nums[i] == nums[i - 1] {
            continue
        }
        left := i + 1
        right := len - 1
        for left &amp;lt; right { // 没有=，因为 left == right 时是一个数，不是三数之和了
            if nums[i] + nums[left] + nums[right] == 0 {
                result = append(result, []int{nums[i], nums[left], nums[right]})
                // 在收集到一个结果后去重
                for left &amp;lt; right &amp;amp;&amp;amp; nums[left] == nums[left+1] {
                    left++
                }
                for left &amp;lt; right &amp;amp;&amp;amp; nums[right] == nums[right-1] {
                    right--
                }
                // 在 nums[left] 和 下一个数不同时，至少保证一次指针移动
                left++
                right--
            } else if nums[i] + nums[left] + nums[right] &amp;gt; 0 {
                right--
            } else {
                left++
            }
        }
    } 
    return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;哈希解法（未完成）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func threeSum(nums []int) [][]int {
    res := make([][]int, 0)
    sort.Ints(nums)
    // 找出a + b + c = 0
    // a = nums[i], b = nums[j], c = -(a + b)
    for i := 0; i &amp;lt; len(nums); i++ {
        // 排序之后如果第一个元素已经大于零，那么不可能凑成三元组
        if nums[i] &amp;gt; 0 {
            break
        }
        // 三元组元素a去重
        if i &amp;gt; 0 &amp;amp;&amp;amp; nums[i] == nums[i-1] {
            continue
        }
        set := make(map[int]struct{})
        for j := i + 1; j &amp;lt; len(nums); j++ {
            // 三元组元素b去重
            if j &amp;gt; i + 2 &amp;amp;&amp;amp; nums[j] == nums[j-1] &amp;amp;&amp;amp; nums[j-1] == nums[j-2] {
                continue
            }
            c := -nums[i] - nums[j]
            if _, ok := set[c]; ok {
                res = append(res, []int{nums[i], nums[j], c})
                // 三元组元素c去重
                delete(set, c)
            } else {
                set[nums[j]] = struct{}{}
            }
        }
    }
    return res
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;18.四数之和&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/4sum/&quot;&gt;18. 四数之和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;中等&lt;/p&gt;
&lt;p&gt;给你一个由 &lt;code&gt;n&lt;/code&gt; 个整数组成的数组 &lt;code&gt;nums&lt;/code&gt; ，和一个目标值 &lt;code&gt;target&lt;/code&gt; 。请你找出并返回满足下述全部条件且&lt;strong&gt;不重复&lt;/strong&gt;的四元组 &lt;code&gt;[nums[a], nums[b], nums[c], nums[d]]&lt;/code&gt; （若两个四元组元素一一对应，则认为两个四元组重复）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= a, b, c, d &amp;lt; n&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt;、&lt;code&gt;c&lt;/code&gt; 和 &lt;code&gt;d&lt;/code&gt; &lt;strong&gt;互不相同&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nums[a] + nums[b] + nums[c] + nums[d] == target&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以按 &lt;strong&gt;任意顺序&lt;/strong&gt; 返回答案 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [1,0,-1,0,-2,2], target = 0
输出：[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [2,2,2,2,2], target = 8
输出：[[2,2,2,2]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= nums.length &amp;lt;= 200&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-109 &amp;lt;= nums[i] &amp;lt;= 109&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-109 &amp;lt;= target &amp;lt;= 109&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;双指针法&lt;/h3&gt;
&lt;p&gt;这道题同样使用双指针的思路，就是在 &lt;code&gt;三数之和&lt;/code&gt; 的基础上再套一层 for 循环。但是需要注意，&lt;code&gt;四数之和&lt;/code&gt; 的 target 不是 0 是可以变的，这时就不能直接根据 &lt;code&gt;n1 &amp;gt; target&lt;/code&gt; 来确定没有符合要求的四元组了。因为 target 可能为负数，所以四数之和也可能是越加越小的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果 target &amp;lt; 0，a &amp;lt; 0 也是可以找到三元组的。
例如 [-4, -1, 0]， target == 5&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以把 a + b 当成一个整体来剪枝，但是剪枝是非必要操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (nums[k] + nums[i] &amp;gt; target &amp;amp;&amp;amp; nums[k] + nums[i] &amp;gt;= 0) {
    break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外在去重的时候，边界也有所更改。例如对 b 去重时，（i := k + 1）需要 i &amp;gt; k+1，因为要取 nums[i - 1]， i 必须大于 k 。&lt;/p&gt;
&lt;p&gt;完整代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func fourSum(nums []int, target int) [][]int {
    len := len(nums)
    res := [][]int{}
    if len &amp;lt; 4 {
        return nil
    }
    sort.Ints(nums)
    for k := 0; k &amp;lt; len - 3; k++ {
        n1 := nums[k]
        // if n1 &amp;gt; target { // 不能这样写，因为 target 可能是负数
        //     break
        // }
        if k &amp;gt; 0 &amp;amp;&amp;amp; n1 == nums[k-1] { // 对 nums[k] 去重
            continue
        }
        for i := k + 1; i &amp;lt; len - 2; i++ {
            n2 := nums[i]
            if i &amp;gt; k + 1 &amp;amp;&amp;amp; n2 == nums[i-1] { // 对 nums[i] 去重
                continue
            }
            left := i + 1
            right := len - 1
            for left &amp;lt; right {
                n3 := nums[left]
                n4 := nums[right]
                sum := n1 + n2 + n3 + n4
                if sum &amp;lt; target {
                    left++
                } else if sum &amp;gt; target {
                    right--
                } else {
                    res = append(res, []int{n1, n2, n3, n4})
                    for left &amp;lt; right &amp;amp;&amp;amp; n3 == nums[left+1] { // left 去重
                        left++
                    }
                    for left &amp;lt; right &amp;amp;&amp;amp; n4 == nums[right-1] { // right 去重
                        right--
                    }
                    // 找到答案时，双指针同时靠近
                    left++
                    right--
                }
            }
        }
    }
    return res
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;哈希表章节总结&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;../images/021/hash.xmind&quot;&gt;xmind&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/021/hash.png&quot; alt=&quot;hash&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>字符串算法（2）「初探KMP算法」</title><link>https://notes.kiramux.com/posts/023-string-algorithm-2</link><guid isPermaLink="true">https://notes.kiramux.com/posts/023-string-algorithm-2</guid><pubDate>Sun, 05 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;28. 找出字符串中第一个匹配项的下标&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/&quot;&gt;28. 找出字符串中第一个匹配项的下标&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;给你两个字符串 &lt;code&gt;haystack&lt;/code&gt; 和 &lt;code&gt;needle&lt;/code&gt; ，请你在 &lt;code&gt;haystack&lt;/code&gt; 字符串中找出 &lt;code&gt;needle&lt;/code&gt; 字符串的第一个匹配项的下标（下标从 0 开始）。如果 &lt;code&gt;needle&lt;/code&gt; 不是 &lt;code&gt;haystack&lt;/code&gt; 的一部分，则返回 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：haystack = &quot;sadbutsad&quot;, needle = &quot;sad&quot;
输出：0
解释：&quot;sad&quot; 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ，所以返回 0 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：haystack = &quot;leetcode&quot;, needle = &quot;leeto&quot;
输出：-1
解释：&quot;leeto&quot; 没有在 &quot;leetcode&quot; 中出现，所以返回 -1 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= haystack.length, needle.length &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;haystack&lt;/code&gt; 和 &lt;code&gt;needle&lt;/code&gt; 仅由小写英文字符组成&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;什么是KMP&lt;/h3&gt;
&lt;p&gt;KMP 由 Knuth，Morris、Pratt 三位学者发明，取他们名字的首字母命名，即KMP算法。&lt;/p&gt;
&lt;h3&gt;KMP有什么用&lt;/h3&gt;
&lt;p&gt;假设现在有一个&lt;code&gt;文本字符串&lt;/code&gt; &lt;strong&gt;aabaabaaf&lt;/strong&gt; 和一个&lt;code&gt;模板字符串&lt;/code&gt; &lt;strong&gt;aabaaf&lt;/strong&gt;，现在要求在&lt;code&gt;文本串&lt;/code&gt;中找出&lt;code&gt;模板串&lt;/code&gt;出现的第一个位置 (从0开始)。如果不存在，则返回 -1。&lt;/p&gt;
&lt;p&gt;如果不想用双重 for 循环的暴力解法，就需要用到 KMP 算法思想。&lt;/p&gt;
&lt;p&gt;KMP的主要思想是&lt;strong&gt;当出现字符串不匹配时，可以知道一部分已经匹配的文本内容，从而避免从头重新做匹配。&lt;/strong&gt; 所以 KMP 的重点是&lt;strong&gt;如何记录已经匹配的文本内容&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;什么是前缀表&lt;/h3&gt;
&lt;p&gt;什么是前缀和后缀？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;前缀&lt;/strong&gt;： &lt;strong&gt;必须包含第一个字母&lt;/strong&gt;但&lt;strong&gt;不包含最后一个字母&lt;/strong&gt;的&lt;strong&gt;连续&lt;/strong&gt;子串&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;后缀&lt;/strong&gt;：&lt;strong&gt;必须包含最后一个字母&lt;/strong&gt;但&lt;strong&gt;不包含第一个字母&lt;/strong&gt;的&lt;strong&gt;连续&lt;/strong&gt;子串&lt;/p&gt;
&lt;p&gt;aabaaf 的所有前缀：&lt;code&gt;a&lt;/code&gt;; &lt;code&gt;aa&lt;/code&gt;; &lt;code&gt;aab&lt;/code&gt;; &lt;code&gt;aaba&lt;/code&gt;; &lt;code&gt;aabaa&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;aabaaf 的所有后缀：&lt;code&gt;f&lt;/code&gt;; &lt;code&gt;af&lt;/code&gt;; &lt;code&gt;aaf&lt;/code&gt;; &lt;code&gt;baaf&lt;/code&gt;; &lt;code&gt;abaaf&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;后缀也是从左往右读的。&lt;/p&gt;
&lt;p&gt;此时就可以引入「最长公共前后缀」（也叫「最长相等前后缀」）的概念了，即一个字符串的前缀集和后缀集中最长的相等的字符串。&lt;/p&gt;
&lt;p&gt;前缀表：&lt;strong&gt;记录下标i之前（包括i）的字符串中，有多大长度的相同前缀后缀。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;为什么要使用前缀表？&lt;/h3&gt;
&lt;p&gt;前缀表的任务是当前位置匹配失败，找到上次已经匹配上的位置，再重新匹配。也就是说在某个字符匹配失败时，前缀表会告诉你下一步匹配时模式串应该跳到哪个位置。这就涉及到计算模式串的完整前缀表。next 数组也是一种前缀表。&lt;/p&gt;
&lt;p&gt;只用文字描述可能有点抽象，用下面的图来说明：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;要在文本串：aabaabaafa 中查找是否出现过一个模式串：aabaaf。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/KMP%E7%B2%BE%E8%AE%B21.gif&quot; alt=&quot;KMP详解1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看出，当第六个字符&lt;code&gt;b&lt;/code&gt;和&lt;code&gt;f&lt;/code&gt;不匹配时，如果使用暴力匹配方法，这时需要从头匹配。如果&lt;strong&gt;使用前缀表&lt;/strong&gt;，就不会从头匹配，而是从上次已经匹配的内容&lt;code&gt;aa&lt;/code&gt;后开始匹配。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/image-20251004222455936.png&quot; alt=&quot;image-20251004222455936&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为不匹配时，前面的子串&lt;code&gt;aabaa&lt;/code&gt;，后缀 aa 和前缀 aa 相等，aa 既是前缀又是后缀，下次匹配时前缀aa一定相等，就不用匹配了，直接从aa的下一位字符&lt;code&gt;b&lt;/code&gt;开始匹配就OK了。这里就用到了前五位子串&lt;code&gt;aabaa&lt;/code&gt;的&lt;strong&gt;最长相等前后缀的长度&lt;/strong&gt;为2。&lt;/p&gt;
&lt;p&gt;所以如果有前缀表，匹配失败时就可以跳到已经匹配过的地方，不用再从头匹配了。&lt;/p&gt;
&lt;h3&gt;如何计算前缀表&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/KMP%E7%B2%BE%E8%AE%B25.png&quot; alt=&quot;KMP精讲5&quot; /&gt;&lt;/p&gt;
&lt;p&gt;⬆️长度为前1个字符的子串&lt;code&gt;a&lt;/code&gt;，最长相同前后缀的长度为0。（注意字符串的&lt;strong&gt;前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串&lt;/strong&gt;；&lt;strong&gt;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串&lt;/strong&gt;。）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/KMP%E7%B2%BE%E8%AE%B26.png&quot; alt=&quot;KMP精讲6&quot; /&gt;&lt;/p&gt;
&lt;p&gt;⬆️长度为前2个字符的子串&lt;code&gt;aa&lt;/code&gt;，最长相同前后缀的长度为1。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/KMP%E7%B2%BE%E8%AE%B27.png&quot; alt=&quot;KMP精讲7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;⬆️长度为前3个字符的子串&lt;code&gt;aab&lt;/code&gt;，最长相同前后缀的长度为0。&lt;/p&gt;
&lt;p&gt;以此类推：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;长度为前4个字符的子串&lt;code&gt;aaba&lt;/code&gt;，最长相同前后缀的长度为1；&lt;/li&gt;
&lt;li&gt;长度为前5个字符的子串&lt;code&gt;aabaa&lt;/code&gt;，最长相同前后缀的长度为2；&lt;/li&gt;
&lt;li&gt;长度为前6个字符的子串&lt;code&gt;aabaaf&lt;/code&gt;，最长相同前后缀的长度为0。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/KMP%E7%B2%BE%E8%AE%B28.png&quot; alt=&quot;KMP精讲8&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么把求得的最长相同前后缀的长度组成一个数组，就是对应的前缀表。&lt;/p&gt;
&lt;p&gt;可以看出模式串与前缀表对应位置的数字表示的就是：&lt;strong&gt;下标i之前（包括i）的字符串中，有多大长度的相同前缀后缀。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;使用前缀表来匹配&lt;/h3&gt;
&lt;p&gt;再来看一下如何利用前缀表，在找到不匹配的字符时，指针应该移动到的位置。如动画所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/KMP%E7%B2%BE%E8%AE%B22.gif&quot; alt=&quot;KMP精讲2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;找到不匹配的位置，需要看它的前一个字符的前缀表数值是多少（因为要找前面字符串的&lt;strong&gt;最长相等前后缀&lt;/strong&gt;的长度）。前一个字符的前缀表数值是2，所以把下标移动到&lt;code&gt;2&lt;/code&gt;的位置继续匹配。&lt;/p&gt;
&lt;h3&gt;前缀表与next数组&lt;/h3&gt;
&lt;p&gt;next 数组可以是前缀表，也可以是对前缀表做过处理的数组。&lt;/p&gt;
&lt;p&gt;有的人会把前缀表整体右移，首位赋值为-1。因为之前直接使用前缀表的话，需要找模式串匹配失败位置的前一位，整体右移之后可以直接找当前位置，比较方便。&lt;/p&gt;
&lt;p&gt;还有一种是先把前缀表都-1，之后再+1，这个思路我还没有理解。&lt;/p&gt;
&lt;h3&gt;时间复杂度分析&lt;/h3&gt;
&lt;p&gt;m为模式串长度，n为文本串长度。&lt;/p&gt;
&lt;p&gt;暴力解法显然是 O(n × m)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KMP&lt;/strong&gt;：根据前缀表不断调整匹配的位置，可以看出匹配的过程是O(n)。计算生成next数组，时间复杂度是O(m)。所以整个KMP算法的时间复杂度是&lt;strong&gt;O(n+m)&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;构造next数组&lt;/h3&gt;
&lt;p&gt;构造 next 数组其实就是计算&lt;code&gt;模式串s&lt;/code&gt;的前缀表的过程，主要有3步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始化&lt;/li&gt;
&lt;li&gt;处理前后缀不相同的情况&lt;/li&gt;
&lt;li&gt;处理前后缀相同的情况&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面先写了从逻辑上易于理解的思路，先处理相同再处理不同。实际上应当是先处理&lt;strong&gt;前后缀不相同的情况&lt;/strong&gt;，再处理&lt;strong&gt;前后缀相同的情况&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;定义两个指针 i 和 j 。j 指向&lt;strong&gt;前缀末尾位置&lt;/strong&gt;，i 指向&lt;strong&gt;后缀末尾位置&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// params:
//  	next 前缀表数组
//		s 模式串
j := 0
next[0] = j
for i := 1; i &amp;lt; len(s); i++ {
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/image-20251004230849883.png&quot; alt=&quot;image-20251004230849883&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;处理前后缀相同的情况&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/image-20251004231734680.png&quot; alt=&quot;image-20251004231734680&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/image-20251004231915895.png&quot; alt=&quot;image-20251004231915895&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时 s[j] == s[i] -&amp;gt; &lt;code&gt;next[i] = next[j] + 1&lt;/code&gt;，i和j同时右移。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if  s[j] == s[i] {
    j++
}
next[i] = j
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;此时这个求出来的1有什么意义呢？看下面的图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/image-20251004232046496.png&quot; alt=&quot;image-20251004232046496&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在匹配到x和b不相等了，要找它的前一位的前缀表的值，即a对应的值是1，这代表着已经有前缀和后缀a相同的情况，所以已经匹配了a，下一个点需要检查x和b。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/image-20251004232247156.png&quot; alt=&quot;image-20251004232247156&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/image-20251004232255511.png&quot; alt=&quot;image-20251004232255511&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/image-20251004232428762.png&quot; alt=&quot;image-20251004232428762&quot; /&gt;&lt;/p&gt;
&lt;p&gt;⬆️当下一个 s[j] == s[i] 时， &lt;code&gt;s[i] = s[j] + 1&lt;/code&gt;，i和j同时右移，所以b对应的是2，c对应的是3。当比较到 d 和 a 时，就是前后缀不相同的情况了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;处理前后缀不相同的情况&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/image-20251004231549778.png&quot; alt=&quot;image-20251004231549778&quot; /&gt;&lt;/p&gt;
&lt;p&gt;s[j] != s[i]，此时j需要&lt;strong&gt;移动到前一个字符&lt;/strong&gt;对应的值（即c对应的值），所以j移动到0，开始比较 s[j] 和 s [i]，发现相等，所以 next[i] = next[j] + 1&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么要移动到前一个字符？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/image-20251004234024964.png&quot; alt=&quot;image-20251004234024964&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;ABACABA&lt;/code&gt; 中 &lt;code&gt;ABA&lt;/code&gt; 已经构成了最长相等前后缀。&lt;/p&gt;
&lt;p&gt;此时 &lt;code&gt;ABA&lt;/code&gt; 已经无法与下一个字符构成更长的相等前后缀，那么我们就寻找一下是否存在更短的，比如 A 是可能与下个字符构成相等前后缀的。此时就要判断开头的A和红B前的A是否是相等前后缀。这一步不用暴力求解&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/image-20251004234618106.png&quot; alt=&quot;image-20251004234618106&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/image-20251004235312113.png&quot; alt=&quot;image-20251004235312113&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/image-20251004235348805.png&quot; alt=&quot;image-20251004235348805&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其实不用暴力求解，因为我们得知左右两边的 &lt;code&gt;ABA&lt;/code&gt; 是相等的，要判断上图两个A是否相同，&lt;strong&gt;相当于直接在左边的 &lt;code&gt;ABA&lt;/code&gt; 找相等前后缀&lt;/strong&gt;，而左边的前后缀之前已经计算过了，也就是&lt;strong&gt;看 j 的前一位的前缀表的值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;之后就是检查下一位字符是否相同，如果相同则可以构成一个更长的相等前后缀，长度+1即可。&lt;/p&gt;
&lt;p&gt;求前缀表全过程的GIF：&lt;/p&gt;
&lt;p&gt;![QQ2025105-0017-HD 00_00_00-00_00_30](../images/023/QQ2025105-0017-HD 00_00_00-00_00_30.gif)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里如果没明白为什么要移动到前一个字符，可以从&lt;code&gt;06:11&lt;/code&gt;看这个视频：&lt;a href=&quot;https://www.bilibili.com/video/BV1AY4y157yL/?share_source=t=369&quot;&gt;🔗【最浅显易懂的 KMP 算法讲解】&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/023/image-20251004233326195.png&quot; alt=&quot;image-20251004233326195&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for j &amp;gt; 0 &amp;amp;&amp;amp; s[j] != s[i] {
    j = next[j-1]
}
if  s[j] == s[i] {
    j++
}
next[i] = j
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以经过优化，实际上应当是先处理&lt;strong&gt;前后缀不相同的情况&lt;/strong&gt;，再处理&lt;strong&gt;前后缀相同的情况&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 前缀表无减一或者右移

// getNext 构造前缀表next
// params:
//		  next 前缀表数组
//		  s 模式串
func getNext(next []int, s string) {
    j := 0
    next[0] = j
    for i := 1; i &amp;lt; len(s); i++ {
        for j &amp;gt; 0 &amp;amp;&amp;amp; s[i] != s[j] {
            j = next[j-1]
        }
        if s[i] == s[j] {
            j++
        }
        next[i] = j
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用next数组来做匹配&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func strStr(haystack string, needle string) int {
    n := len(needle)
    if n == 0 {
        return 0
    }
    j := 0
    next := make([]int, n)
    getNext(next, needle)
    for i := 0; i &amp;lt; len(haystack); i++ {
        for j &amp;gt; 0 &amp;amp;&amp;amp; haystack[i] != needle[j] {
            j = next[j-1]      // 回退到j的前一位
        }
        if haystack[i] == needle[j] {
            j++
        }
        if j == n {
            return i - n + 1
        }
    }
    return -1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;完整代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func strStr(haystack string, needle string) int {
    n := len(needle) // 模式串的长度
    if n == 0 {
        return 0
    }
    j := 0 // 模式串中的指针
    next := make([]int, n)
    getNext(next, needle)
    for i := 0; i &amp;lt; len(haystack); i++ {
        if j &amp;gt; 0 &amp;amp;&amp;amp; haystack[i] != needle[j] {
            j = next[j - 1]
        }
        if haystack[i] == needle[j] {
            j++
        }
        if j == n {
            return i - j + 1 // 返回第一个匹配的下标所以+1
        }
    }
    // 遍历完 haystack 都没有和 needle 完全匹配（j != n），说明 needle 不是 haystack 的一部分
    return -1
}

func getNext(next []int, s string) {
    j := 0
    next[0] = j
    for i := 1; i &amp;lt; len(s); i++ {
        for j &amp;gt; 0 &amp;amp;&amp;amp; s[j] != s[i] { // 前后缀不相等的情况
            j = next[j-1]
        }
        if s[j] == s[i] {
            j++
        }
        next[i] = j
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Kiramux</author></item><item><title>字符串算法（3）</title><link>https://notes.kiramux.com/posts/024-string-algorithm-3</link><guid isPermaLink="true">https://notes.kiramux.com/posts/024-string-algorithm-3</guid><pubDate>Sun, 05 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;459. 重复的子字符串&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/repeated-substring-pattern/&quot;&gt;459. 重复的子字符串&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;给定一个非空的字符串 &lt;code&gt;s&lt;/code&gt; ，检查是否可以通过由它的一个子串重复多次构成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入: s = &quot;abab&quot;
输出: true
解释: 可由子串 &quot;ab&quot; 重复两次构成。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入: s = &quot;aba&quot;
输出: false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入: s = &quot;abcabcabcabc&quot;
输出: true
解释: 可由子串 &quot;abc&quot; 重复四次构成。 (或子串 &quot;abcabc&quot; 重复两次构成。)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= s.length &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 由小写英文字母组成&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;暴力解法&lt;/h3&gt;
&lt;p&gt;s 如果是周期串，要找的 substr 一定满足以下要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;substr 包括 s 的第一个字符和最后一个字符，所以只需遍历 substr 的结尾字符，不用遍历开头。&lt;/li&gt;
&lt;li&gt;substr 的长度不得超过 len(s) 的一半，否则s不可能是周期串。&lt;/li&gt;
&lt;li&gt;substr 的长度必须被 len(s) 整除&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然是暴力解法，但也做了相应的剪枝操作，时间复杂度是 O(n^2)。&lt;/p&gt;
&lt;p&gt;is_match := true 这个初始值虽然方便，但是总感觉初始值设为 true 不太好。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func repeatedSubstringPattern(s string) bool {
    // 寻找子串，包含开头，结尾不超过中间，否则不可能由子串重复构成
    n := len(s)
    for l := 1; l &amp;lt;= n/2; l++ {
        if n % l != 0 { // 必须整除，否则不可能由该子串重复构成
            continue
        }
        substr := s[:l]
        is_match := true
        // 检查每一段是不是和 substr 相等
        for i := l; i &amp;lt; n; i = i + l {
            if s[i:i+l] != substr {
                is_match = false
                break
            }
        }
        if is_match {
            return true
        }
    }
    return false
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;双倍字符串法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;把字符串翻倍，掐头去尾，如果原字符串在其中，那么原字符串就是周期串 。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这里直接使用库函数来查找字符串，实际上就是力扣 28 题。&lt;/p&gt;
&lt;p&gt;具体代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func repeatedSubstringPattern(s string) bool {
    // 如果2个s拼起来掐头去尾，可以在其中找到s，就说明s是周期串
    t := s + s
    return strings.Contains(t[1:len(t)-1], s)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体解释可以看 &lt;a href=&quot;https://writings.sh/post/algorithm-repeated-string-pattern&quot;&gt;🔗周期字符串问题（两种方法）&lt;/a&gt;，以下内容为搬运。&lt;/p&gt;
&lt;blockquote&gt;
&lt;h4&gt;双倍字符串方法 &lt;a href=&quot;https://writings.sh/post/algorithm-repeated-string-pattern#%E5%8F%8C%E5%80%8D%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%96%B9%E6%B3%95&quot;&gt;¶&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;把字符串翻倍，掐头去尾，如果原字符串在其中，那么原字符串就是周期串 。&lt;/p&gt;
&lt;p&gt;假设字符串是 &lt;code&gt;s&lt;/code&gt; ，把它的头尾字符分别染上黄色和蓝色：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/024/repeated-string-pattern-double-str-1.jpeg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;把字符串 &lt;code&gt;s&lt;/code&gt; 接到自身后面，然后掐头去尾，形成新字符串 &lt;code&gt;s&apos;&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/024/repeated-string-pattern-double-str-2.jpeg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;匹配意味着周期性&lt;/h4&gt;
&lt;p&gt;下面讨论原字符串 &lt;code&gt;s&lt;/code&gt; 在新字符串 &lt;code&gt;s&apos;&lt;/code&gt; 中存在的情况。&lt;/p&gt;
&lt;p&gt;一步一步对各部分涂色，使得相等的字符串颜色一样 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/024/repeated-string-pattern-double-str-3.jpeg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;经过几轮的染色，可以看到最终 &lt;code&gt;s&lt;/code&gt; 确实是一个周期串。&lt;/p&gt;
&lt;p&gt;是否巧合？可以做一般性说明。&lt;/p&gt;
&lt;p&gt;下图，不妨设右边匹配的少一些。对其中的任一字符 &lt;code&gt;A&lt;/code&gt; ，可以按照如下的规则推演：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/024/repeated-string-pattern-double-str-4.jpeg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;推演的细致说明：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;① 号推演，由于上下字符串匹配。&lt;/li&gt;
&lt;li&gt;② 号推演，由于和左上方的自身相等。&lt;/li&gt;
&lt;li&gt;如此反复推演。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如此，任一此区间上的字符 &lt;code&gt;A&lt;/code&gt; 会在 &lt;code&gt;s&lt;/code&gt; 中周期性出现。&lt;/p&gt;
&lt;p&gt;即说明字符串 &lt;code&gt;s&lt;/code&gt; 是周期串。&lt;/p&gt;
&lt;h4&gt;周期性意味着匹配&lt;/h4&gt;
&lt;p&gt;反过来，如果一个字符串 &lt;code&gt;s&lt;/code&gt; 是周期串，那么它一定在对应的 &lt;code&gt;s&apos;&lt;/code&gt; 中吗？&lt;/p&gt;
&lt;p&gt;任何一个周期串可以表达为： 由某个模式子串的重复多次构成 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/024/repeated-string-pattern-double-str-5.jpeg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;将周期串 &lt;code&gt;s&lt;/code&gt; 的头字符对齐在第一个模式串后面， 每次右移一个模式串的长度。&lt;/p&gt;
&lt;p&gt;可知，&lt;code&gt;s&lt;/code&gt; 会在 &lt;code&gt;s&apos;&lt;/code&gt; 中有匹配，且可以有多个匹配。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/024/repeated-string-pattern-double-str-6.jpeg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图中可看出， 因为模式串重复 &lt;code&gt;n&lt;/code&gt; 次，所以会有 &lt;code&gt;n&lt;/code&gt; 次匹配 。&lt;/p&gt;
&lt;p&gt;构造双倍串 &lt;code&gt;s&apos;&lt;/code&gt; 时，移除头尾字符， 正是为了剔除最左和最右的两次必然匹配。 只有中间的 &lt;code&gt;n&lt;/code&gt; 次匹配才用到了周期串重复模式串的性质。&lt;/p&gt;
&lt;h4&gt;结论&lt;/h4&gt;
&lt;p&gt;综上两方面说明了充分性和必要性，结论：&lt;/p&gt;
&lt;p&gt;如果字符串在其掐头去尾的双倍字符串中，它就是周期串 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;KMP算法&lt;/h3&gt;
&lt;p&gt;如果一个字符串 s 是由重复子串组成，那么&lt;strong&gt;最长相等前后缀不包含的子串&lt;/strong&gt;一定是字符串 s 的最小重复子串。&lt;/p&gt;
&lt;p&gt;具体原因看 &lt;a href=&quot;https://programmercarl.com/0459.%E9%87%8D%E5%A4%8D%E7%9A%84%E5%AD%90%E5%AD%97%E7%AC%A6%E4%B8%B2.html&quot;&gt;🔗代码随想录_重复的子字符串&lt;/a&gt; 和 &lt;a href=&quot;https://www.bilibili.com/video/BV1cg41127fw&quot;&gt;🔗bilibili_字符串这么玩，可有点难度！ | LeetCode：459.重复的子字符串&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;具体代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func repeatedSubstringPattern(s string) bool {
    // 求 next 数组
    n := len(s)
    next := make([]int, n)
    j := 0
    next[0] = j
    for i := 1; i &amp;lt; n; i++ {
        for j &amp;gt; 0 &amp;amp;&amp;amp; s[j] != s[i] {
            j = next[j-1]
        }
        if s[j] == s[i] {
            j++
        }
        next[i] = j
    }
    // next[n-1] 是s最长相等前后缀的长度
    if next[n-1] != 0 &amp;amp;&amp;amp; n % (n - next[n-1]) == 0 {
        return true
    }
    return false
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Xmind 总结&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;../images/024/string.xmind&quot;&gt;🔗Xmind 源文件&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/024/string.png&quot; alt=&quot;string&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>字符串算法（1）</title><link>https://notes.kiramux.com/posts/022-string-algorithm-1</link><guid isPermaLink="true">https://notes.kiramux.com/posts/022-string-algorithm-1</guid><pubDate>Sun, 05 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;344. 反转字符串&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/reverse-string/&quot;&gt;344. 反转字符串&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;编写一个函数，其作用是将输入的字符串反转过来。输入字符串以字符数组 &lt;code&gt;s&lt;/code&gt; 的形式给出。&lt;/p&gt;
&lt;p&gt;不要给另外的数组分配额外的空间，你必须**&lt;a href=&quot;https://baike.baidu.com/item/%E5%8E%9F%E5%9C%B0%E7%AE%97%E6%B3%95&quot;&gt;原地&lt;/a&gt;修改输入数组**、使用 O(1) 的额外空间解决这一问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：s = [&quot;h&quot;,&quot;e&quot;,&quot;l&quot;,&quot;l&quot;,&quot;o&quot;]
输出：[&quot;o&quot;,&quot;l&quot;,&quot;l&quot;,&quot;e&quot;,&quot;h&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：s = [&quot;H&quot;,&quot;a&quot;,&quot;n&quot;,&quot;n&quot;,&quot;a&quot;,&quot;h&quot;]
输出：[&quot;h&quot;,&quot;a&quot;,&quot;n&quot;,&quot;n&quot;,&quot;a&quot;,&quot;H&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= s.length &amp;lt;= 105&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s[i]&lt;/code&gt; 都是 &lt;a href=&quot;https://baike.baidu.com/item/ASCII&quot;&gt;ASCII&lt;/a&gt; 码表中的可打印字符&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;用 left 和 right 双指针， for left &amp;lt; right {} 循环直接反转字符串即可。不要习惯性地 for i:=0; i&amp;lt;len; i++ {}。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func reverseString(s []byte)  {
    left := 0
    right := len(s) - 1
    for left &amp;lt; right {
        s[left], s[right] = s[right], s[left]
        left++
        right--
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;541.反转字符串 II&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/reverse-string-ii/&quot;&gt;541. 反转字符串 II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;给定一个字符串 &lt;code&gt;s&lt;/code&gt; 和一个整数 &lt;code&gt;k&lt;/code&gt;，从字符串开头算起，每计数至 &lt;code&gt;2k&lt;/code&gt; 个字符，就反转这 &lt;code&gt;2k&lt;/code&gt; 字符中的前 &lt;code&gt;k&lt;/code&gt; 个字符。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果剩余字符少于 &lt;code&gt;k&lt;/code&gt; 个，则将剩余字符全部反转。&lt;/li&gt;
&lt;li&gt;如果剩余字符小于 &lt;code&gt;2k&lt;/code&gt; 但大于或等于 &lt;code&gt;k&lt;/code&gt; 个，则反转前 &lt;code&gt;k&lt;/code&gt; 个字符，其余字符保持原样。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：s = &quot;abcdefg&quot;, k = 2
输出：&quot;bacdfeg&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：s = &quot;abcd&quot;, k = 2
输出：&quot;bacd&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= s.length &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 仅由小写英文组成&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= k &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;思路&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;每隔 2k 个字符的前 k 个字符进行反转&lt;/li&gt;
&lt;li&gt;剩余字符小于 2k 但大于或等于 k 个，则反转前 k 个字符&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;PS：反转函数和&lt;code&gt;344.反转字符串&lt;/code&gt;一样，直接使用库函数也可以。许多库函数的区间都是左闭右开的。&lt;/p&gt;
&lt;p&gt;这道题用Go写可以对 s 进行切片，直接对底层数组进行反转操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func reverseStr(s string, k int) string {
    ss := []byte(s)
    len := len(ss)
    for i := 0; i &amp;lt; len; i += 2*k {
        if i + k &amp;lt;= len { // 1,2,3 k==2
            // 反转 i ~ i+k （前k个），凑足k个反转
            reverse(ss[i:i+k]) // 左闭右开
        } else {
            // 剩余的不足k个，剩下的都反转
            reverse(ss[i:len])
        }
    }
    return string(ss)
}

func reverse(s []byte) []byte{
    left := 0
    right := len(s)-1
    for left &amp;lt; right {
        s[left], s[right] = s[right], s[left]
        left++
        right--
    }
    return s
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;151. 反转字符串中的单词&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/reverse-words-in-a-string/&quot;&gt;151. 反转字符串中的单词&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;中等&lt;/p&gt;
&lt;p&gt;给你一个字符串 &lt;code&gt;s&lt;/code&gt; ，请你反转字符串中 &lt;strong&gt;单词&lt;/strong&gt; 的顺序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;单词&lt;/strong&gt; 是由非空格字符组成的字符串。&lt;code&gt;s&lt;/code&gt; 中使用至少一个空格将字符串中的 &lt;strong&gt;单词&lt;/strong&gt; 分隔开。&lt;/p&gt;
&lt;p&gt;返回 &lt;strong&gt;单词&lt;/strong&gt; 顺序颠倒且 &lt;strong&gt;单词&lt;/strong&gt; 之间用单个空格连接的结果字符串。&lt;/p&gt;
&lt;p&gt;**注意：**输入字符串 &lt;code&gt;s&lt;/code&gt;中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中，单词间应当仅用单个空格分隔，且不包含任何额外的空格。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：s = &quot;the sky is blue&quot;
输出：&quot;blue is sky the&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：s = &quot;  hello world  &quot;
输出：&quot;world hello&quot;
解释：反转后的字符串中不能存在前导空格和尾随空格。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：s = &quot;a good   example&quot;
输出：&quot;example good a&quot;
解释：如果两个单词间有多余的空格，反转后的字符串需要将单词间的空格减少到仅有一个。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= s.length &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 包含英文大小写字母、数字和空格 &lt;code&gt;&apos; &apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 中 &lt;strong&gt;至少存在一个&lt;/strong&gt; 单词&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**进阶：**如果字符串在你使用的编程语言中是一种可变数据类型，请尝试使用 &lt;code&gt;O(1)&lt;/code&gt; 额外空间复杂度的 &lt;strong&gt;原地&lt;/strong&gt; 解法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本题增加难度，在原地进行操作。在 go 语言中将字符串转换为 &lt;code&gt;[]byte&lt;/code&gt; 来操作。&lt;/p&gt;
&lt;h3&gt;1. 双指针法（快慢指针）来去除多余的空格。&lt;/h3&gt;
&lt;p&gt;当快指针 &lt;code&gt;i&lt;/code&gt; 读到字母（非空格字符时），开始处理需要写入的内容。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在每个单词前加一个空格（首个单词除外）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 单词间加上空格
if slow != 0 {
	ss[slow] = &apos; &apos;
	slow++
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;把需要复制的字符，复制到慢指针，随后快慢指针向后移动（即原地覆盖字符串），截断字符串到有效部分。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 翻转字符串&lt;/h3&gt;
&lt;p&gt;先翻转整个字符串，再翻转每个单词，就实现了题目的要求。&lt;/p&gt;
&lt;p&gt;翻转单词的思路就是记录 单词的开头位置，同时找到&lt;strong&gt;下一个空格&lt;/strong&gt;或&lt;strong&gt;字符串末尾+1的索引&lt;/strong&gt; &lt;code&gt;i&lt;/code&gt;，reverse(s[start, i])，实现对底层数组的修改。&lt;/p&gt;
&lt;p&gt;for 循环的判断条件是 i &amp;lt;= len(s)，因为到 i&amp;lt;len 是遍历到字符串末尾，切片操作是左闭右开，想要包含最后一个字符，就要把 i 向后多移一位。&lt;/p&gt;
&lt;p&gt;接着 start = i + 1 （因为第 i 位是空格，下一位才是新的字母）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func reverseWords(s string) string {
    // 1. 双指针去除多余空格
    ss := []byte(s)
    slow := 0
    // 如果 slow == 0，开始复制字符串直到 i 遇到空格，下一次循环
    // 这时 slow != 0，先加个空格，再继续复制字符串
    // 这样就实现了去除多余空格
    for i := 0; i &amp;lt; len(ss); i++ {
        if ss[i] != &apos; &apos; {
            // 单词间加上空格
            if slow != 0 {
                ss[slow] = &apos; &apos;
                slow++
            }
            // 复制逻辑
            for i &amp;lt; len(ss) &amp;amp;&amp;amp; ss[i] != &apos; &apos; {
                ss[slow] = ss[i]
                slow++
                i++
            }
        }
    } 
    // 截断字符串到实际位置
    ss = ss[0:slow]

    // 2. 反转整个字符串
    reverse(ss)
    // // 3. 反转每个单词
    start := 0
    for i := 0; i &amp;lt;= len(ss); i++ {
        if i == len(ss) || ss[i] == &apos; &apos; {
            reverse(ss[start:i])
            // start 是单词的开头，而此时i指向空格或字符串末尾，i+1 才是下一个单词的开头
            start = i + 1
        }
    }
    return string(ss)
}

func reverse(s []byte){
    left := 0
    right := len(s) - 1
    for left &amp;lt; right {
        s[left], s[right] = s[right], s[left]
        left++
        right--
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Kiramux</author></item><item><title>栈与队列算法（1）——用栈实现队列</title><link>https://notes.kiramux.com/posts/025-stackqueue-algorithm-1</link><guid isPermaLink="true">https://notes.kiramux.com/posts/025-stackqueue-algorithm-1</guid><pubDate>Tue, 07 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;参考资料：
&lt;a href=&quot;https://programmercarl.com/0232.%E7%94%A8%E6%A0%88%E5%AE%9E%E7%8E%B0%E9%98%9F%E5%88%97.html&quot;&gt;🔗代码随想录_用栈实现队列&lt;/a&gt;
&lt;a href=&quot;https://juejin.cn/post/7288628985322356751&quot;&gt;🔗稀土掘金_golang 栈数据结构的实现和应用&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;[toc]&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;232. 用栈实现队列&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/implement-queue-using-stacks/&quot;&gt;232. 用栈实现队列&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作（&lt;code&gt;push&lt;/code&gt;、&lt;code&gt;pop&lt;/code&gt;、&lt;code&gt;peek&lt;/code&gt;、&lt;code&gt;empty&lt;/code&gt;）：&lt;/p&gt;
&lt;p&gt;实现 &lt;code&gt;MyQueue&lt;/code&gt; 类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;void push(int x)&lt;/code&gt; 将元素 x 推到队列的末尾&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int pop()&lt;/code&gt; 从队列的开头移除并返回元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int peek()&lt;/code&gt; 返回队列开头的元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean empty()&lt;/code&gt; 如果队列为空，返回 &lt;code&gt;true&lt;/code&gt; ；否则，返回 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;说明：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你 &lt;strong&gt;只能&lt;/strong&gt; 使用标准的栈操作 —— 也就是只有 &lt;code&gt;push to top&lt;/code&gt;, &lt;code&gt;peek/pop from top&lt;/code&gt;, &lt;code&gt;size&lt;/code&gt;, 和 &lt;code&gt;is empty&lt;/code&gt; 操作是合法的。&lt;/li&gt;
&lt;li&gt;你所使用的语言也许不支持栈。你可以使用 list 或者 deque（双端队列）来模拟一个栈，只要是标准的栈操作即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：
[&quot;MyQueue&quot;, &quot;push&quot;, &quot;push&quot;, &quot;peek&quot;, &quot;pop&quot;, &quot;empty&quot;]
[[], [1], [2], [], [], []]
输出：
[null, null, null, 1, 1, false]

解释：
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= x &amp;lt;= 9&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最多调用 &lt;code&gt;100&lt;/code&gt; 次 &lt;code&gt;push&lt;/code&gt;、&lt;code&gt;pop&lt;/code&gt;、&lt;code&gt;peek&lt;/code&gt; 和 &lt;code&gt;empty&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;假设所有操作都是有效的 （例如，一个空的队列不会调用 &lt;code&gt;pop&lt;/code&gt; 或者 &lt;code&gt;peek&lt;/code&gt; 操作）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;进阶：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你能否实现每个操作均摊时间复杂度为 &lt;code&gt;O(1)&lt;/code&gt; 的队列？换句话说，执行 &lt;code&gt;n&lt;/code&gt; 个操作的总时间复杂度为 &lt;code&gt;O(n)&lt;/code&gt; ，即使其中一个操作可能花费较长时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;用栈实现队列，需要&lt;strong&gt;一个输入栈，一个输出栈&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;需要先用 Go 实现一个栈（这里使用 slice），然后再用该栈模拟队列。&lt;/p&gt;
&lt;p&gt;原本需要在 stack.Len() 中加读锁，但在 LeetCode 中再多加一个读锁就会时间超限。。&lt;/p&gt;
&lt;p&gt;下面动画模拟以下队列的执行过程：&lt;/p&gt;
&lt;p&gt;执行语句：
queue.push(1);
queue.push(2);
queue.pop(); &lt;strong&gt;注意此时的输出栈的操作&lt;/strong&gt;
queue.push(3);
queue.push(4);
queue.pop();
queue.pop();&lt;strong&gt;注意此时的输出栈的操作&lt;/strong&gt;
queue.pop();
queue.empty();&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/025/232.%E7%94%A8%E6%A0%88%E5%AE%9E%E7%8E%B0%E9%98%9F%E5%88%97%E7%89%88%E6%9C%AC2.gif&quot; alt=&quot;232.用栈实现队列版本2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;push 数据，只要把数据放进输入栈即可。&lt;/p&gt;
&lt;p&gt;pop 数据：如果输出栈为空，就把&lt;strong&gt;输入栈&lt;/strong&gt;的数据&lt;strong&gt;全部导入输出栈&lt;/strong&gt;，再从输出栈 Pop 数据。&lt;/p&gt;
&lt;p&gt;fillStackOut： 将数据全部导入输出栈写成一个方法&lt;/p&gt;
&lt;p&gt;peek 和 empty： 如果输出栈为空，就把&lt;strong&gt;输入栈&lt;/strong&gt;的数据&lt;strong&gt;全部导入输出栈&lt;/strong&gt;，再查看 stackOut 的栈顶元素、判断 stackOut 是否为空。&lt;/p&gt;
&lt;p&gt;见详细代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 用 slice 实现栈
type myStack struct {
    Ints []int
    Lock sync.RWMutex
}

func NewStack() *myStack {
    return &amp;amp;myStack{
        Ints: []int{}, // 字面量 []int{} 已经分配好内存空间
    }
}

// Push 入栈
func (s *myStack) Push(t int) {
    s.Lock.Lock()
    defer s.Lock.Unlock()
    s.Ints = append(s.Ints, t)
}

// Pop 出栈
func (s *myStack) Pop() int {
    s.Lock.Lock()
    defer s.Lock.Unlock()
    if s.Empty() {
        return 0
    }
    val := s.Ints[s.Len()-1]
    s.Ints = s.Ints[:s.Len()-1]
    return val
}

// Len 栈内存储的元素数量
func (s *myStack) Len() int {
    return len(s.Ints)
}

// Peek 查看栈顶元素
func (s *myStack) Peek() int {
    if s.Empty() {
        return 0
    }
    return s.Ints[s.Len()-1]
}

// Empty 检查栈是否为空
func (s *myStack) Empty() bool {
    return s.Len() == 0
}


// ==========分割线==========

type MyQueue struct {
    stackIn *myStack
    stackOut *myStack
}


func Constructor() MyQueue {
    return MyQueue {
        stackIn: NewStack(),
        stackOut: NewStack(),
    }
}


func (q *MyQueue) Push(x int)  {
    q.stackIn.Push(x)
}


func (q *MyQueue) Pop() int {
    // 如果 q.stackOut 中没有元素，把 stackIn 元素全部 Push 到 stackOut 中
    q.fillStackOut()
    // 如果 q.stackOut 中有元素则直接 Pop 元素
    return q.stackOut.Pop()

}


func (q *MyQueue) Peek() int {
    q.fillStackOut()
    return q.stackOut.Peek()
}


func (q *MyQueue) Empty() bool {
    q.fillStackOut()
    return q.stackOut.Empty()
}

func (q *MyQueue) fillStackOut() {
    if q.stackOut.Empty() {
        for !q.stackIn.Empty() {
            val := q.stackIn.Pop()
            q.stackOut.Push(val)
        }
    }
}


/**
 * Your MyQueue object will be instantiated and called as such:
 * obj := Constructor();
 * obj.Push(x);
 * param_2 := obj.Pop();
 * param_3 := obj.Peek();
 * param_4 := obj.Empty();
 */
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Kiramux</author></item><item><title>栈与队列算法（2）——用队列实现栈</title><link>https://notes.kiramux.com/posts/026-stackqueue-algorithm-2</link><guid isPermaLink="true">https://notes.kiramux.com/posts/026-stackqueue-algorithm-2</guid><pubDate>Wed, 08 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;225. 用队列实现栈&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/implement-stack-using-queues/&quot;&gt;225. 用队列实现栈&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;请你仅使用两个队列实现一个后入先出（LIFO）的栈，并支持普通栈的全部四种操作（&lt;code&gt;push&lt;/code&gt;、&lt;code&gt;top&lt;/code&gt;、&lt;code&gt;pop&lt;/code&gt; 和 &lt;code&gt;empty&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;实现 &lt;code&gt;MyStack&lt;/code&gt; 类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;void push(int x)&lt;/code&gt; 将元素 x 压入栈顶。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int pop()&lt;/code&gt; 移除并返回栈顶元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int top()&lt;/code&gt; 返回栈顶元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean empty()&lt;/code&gt; 如果栈是空的，返回 &lt;code&gt;true&lt;/code&gt; ；否则，返回 &lt;code&gt;false&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你只能使用队列的标准操作 —— 也就是 &lt;code&gt;push to back&lt;/code&gt;、&lt;code&gt;peek/pop from front&lt;/code&gt;、&lt;code&gt;size&lt;/code&gt; 和 &lt;code&gt;is empty&lt;/code&gt; 这些操作。&lt;/li&gt;
&lt;li&gt;你所使用的语言也许不支持队列。 你可以使用 list （列表）或者 deque（双端队列）来模拟一个队列 , 只要是标准的队列操作即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：
[&quot;MyStack&quot;, &quot;push&quot;, &quot;push&quot;, &quot;top&quot;, &quot;pop&quot;, &quot;empty&quot;]
[[], [1], [2], [], [], []]
输出：
[null, null, null, 2, 2, false]

解释：
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= x &amp;lt;= 9&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最多调用&lt;code&gt;100&lt;/code&gt; 次 &lt;code&gt;push&lt;/code&gt;、&lt;code&gt;pop&lt;/code&gt;、&lt;code&gt;top&lt;/code&gt; 和 &lt;code&gt;empty&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;每次调用 &lt;code&gt;pop&lt;/code&gt; 和 &lt;code&gt;top&lt;/code&gt; 都保证栈不为空&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**进阶：**你能否仅用一个队列来实现栈。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实感觉用1个队列实现栈，比用2个队列更简单一点。&lt;/p&gt;
&lt;h3&gt;用2个队列实现栈&lt;/h3&gt;
&lt;p&gt;push： 把元素加到 queue1 中&lt;/p&gt;
&lt;p&gt;pop/top：用 MoveWithoutLast() 将 queue1 中除最后一个外所有元素移入 queue2 ，就可以单独对栈顶元素操作了。pop: 返回栈顶元素并删除它；top: 返回栈顶元素，并将其加入 queue2 。最后 queue1 为空，queue2 包含所有元素，交换 queue1 和 queue2&lt;/p&gt;
&lt;p&gt;empty： 判断 queue1 和 queue2 是否同时为空&lt;/p&gt;
&lt;p&gt;动画演示：&lt;/p&gt;
&lt;p&gt;模拟的队列执行语句如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;queue.push(1);        
queue.push(2);        
queue.pop();   // 注意弹出的操作       
queue.push(3);        
queue.push(4);       
queue.pop();  // 注意弹出的操作    
queue.pop();    
queue.pop();    
queue.empty();   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../images/026/225.%E7%94%A8%E9%98%9F%E5%88%97%E5%AE%9E%E7%8E%B0%E6%A0%88.gif&quot; alt=&quot;225.用队列实现栈&quot; /&gt;&lt;/p&gt;
&lt;p&gt;完整代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type MyStack struct {
    // 2个队列
    queue1 []int
    queue2 []int
}


func Constructor() MyStack {
    return MyStack {
        queue1: []int{},
        queue2: []int{},
    }
}


func (s *MyStack) Push(x int)  {
    s.queue1 = append(s.queue1, x)
}


func (s *MyStack) Pop() int {
    s.MoveWithoutLast()
    val := s.queue1[0]
    s.queue1 = s.queue1[1:] // 去除第一个元素，即变成空切片
    s.queue1, s.queue2 = s.queue2, s.queue1 // 交换，把 queue1 置为 queue2， queue2 置为空
    return val
}

func (s *MyStack) Top() int {
    s.MoveWithoutLast()
    val := s.queue1[0]
    s.queue2 = append(s.queue2, val)
    s.queue1 = s.queue1[1:] // 去除第一个元素，即变成空切片
    s.queue1, s.queue2 = s.queue2, s.queue1 // 交换，把 queue1 置为 queue2， queue2 置为空
    return val
}


func (s *MyStack) Empty() bool {
    return len(s.queue1) == 0 &amp;amp;&amp;amp; len(s.queue2) == 0
}

func (s *MyStack) MoveWithoutLast() {
    // 除 queue1 最后一个元素，其余元素转移至 queue2
    for i := 0; i &amp;lt; len(s.queue1) - 1; i++ {
        s.queue2 = append(s.queue2, s.queue1[i])
    }
    // queue1 中只保留最后一个元素
    s.queue1 = s.queue1[len(s.queue1)-1:]
}


/**
 * Your MyStack object will be instantiated and called as such:
 * obj := Constructor();
 * obj.Push(x);
 * param_2 := obj.Pop();
 * param_3 := obj.Top();
 * param_4 := obj.Empty();
 */

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;用1个队列实现栈&lt;/h3&gt;
&lt;p&gt;push： 直接加到队列末尾&lt;/p&gt;
&lt;p&gt;pop/top： 把除栈顶元素（最后一个元素）以外的所有元素，一个一个移到队列尾，就可以对栈顶元素操作了。pop： 读取并删除它；top： 读取并将其添加到 queue 末尾。&lt;/p&gt;
&lt;p&gt;empty： 判断 queue 是否为空&lt;/p&gt;
&lt;p&gt;可以用以下方法快速找到栈顶元素，并将其他元素放到 queue 末尾：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    for range len(s.queue) - 1 {
        first := s.queue[0]
        // 把第一个元素移到queue末尾
        s.queue = append(s.queue, first)
        s.queue = s.queue[1:] // 移除第一个元素
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type MyStack struct {
    queue []int
}


func Constructor() MyStack {
    return MyStack {
        queue: []int{},
    }
}


func (s *MyStack) Push(x int)  {
   s.queue = append(s.queue, x) 
}


func (s *MyStack) Pop() int {
    s.MoveWithoutLast()
    val := s.queue[0]
    s.queue = s.queue[1:]
    return val
}


func (s *MyStack) Top() int {
    s.MoveWithoutLast()
    val := s.queue[0]
    s.queue = append(s.queue, val)
    s.queue = s.queue[1:]
    return val
}


func (s *MyStack) Empty() bool {
    return len(s.queue) == 0
}

func (s *MyStack) MoveWithoutLast() {
    for range len(s.queue) - 1 {
        first := s.queue[0]
        // 把第一个元素移到queue末尾
        s.queue = append(s.queue, first)
        s.queue = s.queue[1:]
    }
}


/**
 * Your MyStack object will be instantiated and called as such:
 * obj := Constructor();
 * obj.Push(x);
 * param_2 := obj.Pop();
 * param_3 := obj.Top();
 * param_4 := obj.Empty();
 */

&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Kiramux</author></item><item><title>栈与队列算法（3）——总结</title><link>https://notes.kiramux.com/posts/027-stackqueue-algorithm-3</link><guid isPermaLink="true">https://notes.kiramux.com/posts/027-stackqueue-algorithm-3</guid><pubDate>Tue, 14 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;20. 有效的括号&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/valid-parentheses/&quot;&gt;20. 有效的括号&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;给定一个只包括 &lt;code&gt;&apos;(&apos;&lt;/code&gt;，&lt;code&gt;&apos;)&apos;&lt;/code&gt;，&lt;code&gt;&apos;{&apos;&lt;/code&gt;，&lt;code&gt;&apos;}&apos;&lt;/code&gt;，&lt;code&gt;&apos;[&apos;&lt;/code&gt;，&lt;code&gt;&apos;]&apos;&lt;/code&gt; 的字符串 &lt;code&gt;s&lt;/code&gt; ，判断字符串是否有效。&lt;/p&gt;
&lt;p&gt;有效字符串需满足：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;左括号必须用相同类型的右括号闭合。&lt;/li&gt;
&lt;li&gt;左括号必须以正确的顺序闭合。&lt;/li&gt;
&lt;li&gt;每个右括号都有一个对应的相同类型的左括号。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**输入：**s = &quot;()&quot;&lt;/p&gt;
&lt;p&gt;**输出：**true&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**输入：**s = &quot;()[]{}&quot;&lt;/p&gt;
&lt;p&gt;**输出：**true&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**输入：**s = &quot;(]&quot;&lt;/p&gt;
&lt;p&gt;**输出：**false&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 4：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**输入：**s = &quot;([])&quot;&lt;/p&gt;
&lt;p&gt;**输出：**true&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 5：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**输入：**s = &quot;([)]&quot;&lt;/p&gt;
&lt;p&gt;**输出：**false&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= s.length &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 仅由括号 &lt;code&gt;&apos;()[]{}&apos;&lt;/code&gt; 组成&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;栈&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../images/027/image-20251009115610957.png&quot; alt=&quot;image-20251009115610957&quot; /&gt;&lt;/p&gt;
&lt;p&gt;用 map 来存括号的对应关系。&lt;/p&gt;
&lt;p&gt;遍历到的左括号存入栈中，当遇到右括号时，与栈顶元素对应的右括号做匹配，若相等则说明括号有效，同时弹出栈顶元素，不等则括号无效。&lt;/p&gt;
&lt;p&gt;另外需要注意，如果 len(s) 为奇数则一定不匹配，如果遇到右括号时栈中无元素，则说明多了右括号，不匹配返回false&lt;/p&gt;
&lt;p&gt;如果匹配完毕，栈中无元素，说明所有括号都有效匹配。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func isValid(s string) bool {
    if len(s) % 2 != 0 {
        return false
    }
    // 用切片模拟栈
    stack := make([]rune, 0)

    // 哈希表 m 记录右括号对应的左括号
    m := make(map[rune]rune)
    m[&apos;)&apos;] = &apos;(&apos;
    m[&apos;}&apos;] = &apos;{&apos;
    m[&apos;]&apos;] = &apos;[&apos;

    for _, v := range s {
        if v == &apos;(&apos; || v == &apos;{&apos; || v == &apos;[&apos; {
            stack = append(stack, v)
        } else {
            // 如果是右括号，先判断栈内是否有元素
            if len(stack) == 0 {
                return false
            }
            peek := stack[len(stack)-1]
            if peek != m[v] {
                return false
            } else {
                // 弹出栈顶元素
                stack = stack[:len(stack)-1]
            }
        }
    }
    // 如果栈中不包含元素，则代表完全匹配
    return len(stack) == 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1047. 删除字符串中的所有相邻重复项&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string/&quot;&gt;1047. 删除字符串中的所有相邻重复项&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单&lt;/p&gt;
&lt;p&gt;给出由小写字母组成的字符串 &lt;code&gt;s&lt;/code&gt;，&lt;strong&gt;重复项删除操作&lt;/strong&gt;会选择两个相邻且相同的字母，并删除它们。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;s&lt;/code&gt; 上反复执行重复项删除操作，直到无法继续删除。&lt;/p&gt;
&lt;p&gt;在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：&quot;abbaca&quot;
输出：&quot;ca&quot;
解释：
例如，在 &quot;abbaca&quot; 中，我们可以删除 &quot;bb&quot; 由于两字母相邻且相同，这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 &quot;aaca&quot;，其中又只有 &quot;aa&quot; 可以执行重复项删除操作，所以最后的字符串为 &quot;ca&quot;。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= s.length &amp;lt;= 105&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; 仅由小写英文字母组成。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;func removeDuplicates(s string) string {
    var stack []byte
    // 用字符串模拟栈，字符串尾部是栈入口，字符串头部是栈底部

    for i := 0; i &amp;lt; len(s); i++ {
        // 栈不空且与栈顶元素相等，弹出栈顶元素
        if len(stack) != 0 &amp;amp;&amp;amp; s[i] == stack[len(stack)-1] {
            stack = stack[:len(stack)-1]
        } else {
            // 把元素放入栈
            stack = append(stack, s[i])
        }
    }
    return string(stack)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;150. 逆波兰表达式求值&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/evaluate-reverse-polish-notation/&quot;&gt;150. 逆波兰表达式求值&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;中等&lt;/p&gt;
&lt;p&gt;给你一个字符串数组 &lt;code&gt;tokens&lt;/code&gt; ，表示一个根据 &lt;a href=&quot;https://baike.baidu.com/item/%E9%80%86%E6%B3%A2%E5%85%B0%E5%BC%8F/128437&quot;&gt;逆波兰表示法&lt;/a&gt; 表示的算术表达式。&lt;/p&gt;
&lt;p&gt;请你计算该表达式。返回一个表示表达式值的整数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有效的算符为 &lt;code&gt;&apos;+&apos;&lt;/code&gt;、&lt;code&gt;&apos;-&apos;&lt;/code&gt;、&lt;code&gt;&apos;*&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;/&apos;&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;每个操作数（运算对象）都可以是一个整数或者另一个表达式。&lt;/li&gt;
&lt;li&gt;两个整数之间的除法总是 &lt;strong&gt;向零截断&lt;/strong&gt; 。&lt;/li&gt;
&lt;li&gt;表达式中不含除零运算。&lt;/li&gt;
&lt;li&gt;输入是一个根据逆波兰表示法表示的算术表达式。&lt;/li&gt;
&lt;li&gt;答案及所有中间计算结果可以用 &lt;strong&gt;32 位&lt;/strong&gt; 整数表示。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：tokens = [&quot;2&quot;,&quot;1&quot;,&quot;+&quot;,&quot;3&quot;,&quot;*&quot;]
输出：9
解释：该算式转化为常见的中缀算术表达式为：((2 + 1) * 3) = 9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：tokens = [&quot;4&quot;,&quot;13&quot;,&quot;5&quot;,&quot;/&quot;,&quot;+&quot;]
输出：6
解释：该算式转化为常见的中缀算术表达式为：(4 + (13 / 5)) = 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：tokens = [&quot;10&quot;,&quot;6&quot;,&quot;9&quot;,&quot;3&quot;,&quot;+&quot;,&quot;-11&quot;,&quot;*&quot;,&quot;/&quot;,&quot;*&quot;,&quot;17&quot;,&quot;+&quot;,&quot;5&quot;,&quot;+&quot;]
输出：22
解释：该算式转化为常见的中缀算术表达式为：
  ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= tokens.length &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tokens[i]&lt;/code&gt; 是一个算符（&lt;code&gt;&quot;+&quot;&lt;/code&gt;、&lt;code&gt;&quot;-&quot;&lt;/code&gt;、&lt;code&gt;&quot;*&quot;&lt;/code&gt; 或 &lt;code&gt;&quot;/&quot;&lt;/code&gt;），或是在范围 &lt;code&gt;[-200, 200]&lt;/code&gt; 内的一个整数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;逆波兰表达式：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;逆波兰表达式是一种后缀表达式，所谓后缀就是指算符写在后面。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;平常使用的算式则是一种中缀表达式，如 &lt;code&gt;( 1 + 2 ) * ( 3 + 4 )&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;该算式的逆波兰表达式写法为 &lt;code&gt;( ( 1 2 + ) ( 3 4 + ) * )&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;逆波兰表达式主要有以下两个优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;去掉括号后表达式无歧义，上式即便写成 &lt;code&gt;1 2 + 3 4 + * &lt;/code&gt;也可以依据次序计算出正确结果。&lt;/li&gt;
&lt;li&gt;适合用栈操作运算：遇到数字则入栈；遇到算符则取出栈顶两个数字进行计算，并将结果压入栈中&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;栈&lt;/h3&gt;
&lt;p&gt;遍历 tokens 读到数字放入栈中，读到运算符就弹出栈顶的2个元素做运算，需要&lt;strong&gt;注意相减和相除时的元素顺序&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在判断元素&lt;strong&gt;是否是运算符&lt;/strong&gt;，第一想到的是用哈希表。然而在用哈希表时遇到很多类型方面的问题。&lt;/p&gt;
&lt;p&gt;首先是要判断元素是否在map中，最省空间的是用空struct，初始化时用 &lt;code&gt;&quot;+&quot;: {},&lt;/code&gt;，对map中元素赋值时用 &lt;code&gt; set[&quot;+&quot;] = struct{}{}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;遍历的 token 是字符串，遇到例如 token“12” 时，不是单个字符 &lt;code&gt;&apos;1&apos;&lt;/code&gt; 或 &lt;code&gt;&apos;2&apos;&lt;/code&gt;，而是整体的 &lt;code&gt;&quot;12&quot;&lt;/code&gt;字符串，长度为2。因此不能用 map[byte]struct{}，它只适合处理单字符场景。&lt;/p&gt;
&lt;p&gt;把数字放入栈中，需要提前将 string 转为 int&lt;/p&gt;
&lt;p&gt;下面的代码几乎没有用到库函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func evalRPN(tokens []string) int {
    stack := []int{}
    set := map[string]struct{} {
        &quot;+&quot;: {},
        &quot;-&quot;: {},
        &quot;*&quot;: {},
        &quot;/&quot;: {},
    }
    for i := 0; i &amp;lt; len(tokens); i++ {
        if _, ok := set[tokens[i]]; ok {
            // 从栈中取值做运算
            b := stack[len(stack)-1]
            a := stack[len(stack)-2]
            c := tokens[i]
            stack = stack[:len(stack)-2]
            switch {
                case c == &quot;+&quot;:
                    stack = append(stack, a+b)
                case c == &quot;-&quot;:
                    stack = append(stack, a-b)
                case c == &quot;*&quot;:
                    stack = append(stack, a*b)
                case c == &quot;/&quot;:
                    stack = append(stack, a/b) 
            }
        } else {
            // 把数字放入栈中
            num, _ := strconv.Atoi(tokens[i])
            stack = append(stack, num)
        }
    }
    return stack[len(stack)-1]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用库函数，判断元素&lt;strong&gt;是否是数字&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func evalRPN(tokens []string) int {
	stack := []int{}
	for _, token := range tokens {
		val, err := strconv.Atoi(token)
		if err == nil {
			stack = append(stack, val)
		} else {   // 如果err不为nil说明不是数字
			num1, num2 := stack[len(stack)-2], stack[(len(stack))-1]
			stack = stack[:len(stack)-2]
			switch token {
			case &quot;+&quot;:
				stack = append(stack, num1+num2)
			case &quot;-&quot;:
				stack = append(stack, num1-num2)
			case &quot;*&quot;:
				stack = append(stack, num1*num2)
			case &quot;/&quot;:
				stack = append(stack, num1/num2)
			}
		}
	}
	return stack[0]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;239.滑动窗口最大值&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/sliding-window-maximum/&quot;&gt;239. 滑动窗口最大值&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;困难&lt;/p&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt;，有一个大小为 &lt;code&gt;k&lt;/code&gt; 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 &lt;code&gt;k&lt;/code&gt; 个数字。滑动窗口每次只向右移动一位。&lt;/p&gt;
&lt;p&gt;返回 &lt;em&gt;滑动窗口中的最大值&lt;/em&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [1,3,-1,-3,5,3,6,7], k = 3
输出：[3,3,5,5,6,7]
解释：
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [1], k = 1
输出：[1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= nums.length &amp;lt;= 105&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-104 &amp;lt;= nums[i] &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= k &amp;lt;= nums.length&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;单调队列（仅适用于本题）&lt;/h3&gt;
&lt;p&gt;队列没有必要维护窗口里的所有元素，只需要&lt;strong&gt;维护有可能成为窗口里最大值的元素&lt;/strong&gt;就可以了，同时保证队列里的元素数值是由大到小的。&lt;/p&gt;
&lt;p&gt;设计单调队列的时候，pop，和push操作要保持如下规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;pop(value)：如果窗口移除的元素value等于单调队列的出口元素，那么队列弹出元素，否则不用任何操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (m *MyQueue) Pop(val int) {
    if !m.Empty() &amp;amp;&amp;amp; val == m.Front() {
        m.queue = m.queue[1:]
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;push(value)：如果push的元素value大于入口元素的数值，那么就将队列入口的元素弹出，直到push元素的数值小于等于队列入口元素的数值为止&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (m *MyQueue) Push(val int) {
    for !m.Empty() &amp;amp;&amp;amp; val &amp;gt; m.Back() {
        m.queue = m.queue[:len(m.queue)-1]
    }
    m.queue = append(m.queue, val)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;动画演示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/027/239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC-2.gif&quot; alt=&quot;239.滑动窗口最大值-2&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 封装单调队列的方式解题
type MyQueue struct {
    queue []int
}

func NewMyQueue() *MyQueue {
    return &amp;amp;MyQueue{
        queue: make([]int, 0),
    }
}

func (m *MyQueue) Front() int {
    return m.queue[0]
}

func (m *MyQueue) Back() int {
    return m.queue[len(m.queue)-1]
}

func (m *MyQueue) Empty() bool {
    return len(m.queue) == 0
}

func (m *MyQueue) Push(val int) {
    for !m.Empty() &amp;amp;&amp;amp; val &amp;gt; m.Back() {
        m.queue = m.queue[:len(m.queue)-1]
    }
    m.queue = append(m.queue, val)
}

func (m *MyQueue) Pop(val int) {
    if !m.Empty() &amp;amp;&amp;amp; val == m.Front() {
        m.queue = m.queue[1:]
    }
}

func maxSlidingWindow(nums []int, k int) []int {
    queue := NewMyQueue()
    length := len(nums)
    res := make([]int, 0)
    // 先将前k个元素放入队列
    for i := 0; i &amp;lt; k; i++ {
        queue.Push(nums[i])
    }
    // 记录前k个元素的最大值
    res = append(res, queue.Front())

    for i := k; i &amp;lt; length; i++ {
        // 滑动窗口移除最前面的元素
        queue.Pop(nums[i-k])
        // 滑动窗口添加最后面的元素
        queue.Push(nums[i])
        // 记录最大值
        res = append(res, queue.Front())
    }
    return res
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;347. 前 K 个高频元素&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/top-k-frequent-elements/&quot;&gt;347. 前 K 个高频元素&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;中等&lt;/p&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; 和一个整数 &lt;code&gt;k&lt;/code&gt; ，请你返回其中出现频率前 &lt;code&gt;k&lt;/code&gt; 高的元素。你可以按 &lt;strong&gt;任意顺序&lt;/strong&gt; 返回答案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**输入：**nums = [1,1,1,2,2,3], k = 2&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输出：&lt;/strong&gt;[1,2]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**输入：**nums = [1], k = 1&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输出：&lt;/strong&gt;[1]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**输入：**nums = [1,2,1,2,1,2,3,1,3,2], k = 2&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输出：&lt;/strong&gt;[1,2]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= nums.length &amp;lt;= 105&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;k&lt;/code&gt; 的取值范围是 &lt;code&gt;[1, 数组中不相同的元素的个数]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;题目数据保证答案唯一，换句话说，数组中前 &lt;code&gt;k&lt;/code&gt; 个高频元素的集合是唯一的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**进阶：**你所设计算法的时间复杂度 &lt;strong&gt;必须&lt;/strong&gt; 优于 &lt;code&gt;O(n log n)&lt;/code&gt; ，其中 &lt;code&gt;n&lt;/code&gt; 是数组大小。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;map+排序&lt;/h3&gt;
&lt;p&gt;先用 map 统计元素出现次数，再根据出现次数排序。&lt;/p&gt;
&lt;p&gt;使用了 &lt;a href=&quot;https://before80.github.io/go_docs/stdLib/sort/#func-slice----go18&quot;&gt;sort.Slice()&lt;/a&gt; 库函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func topKFrequent(nums []int, k int) []int {
    ans := []int{}
    map_num := map[int]int{}
    // 统计元素出现次数
    for _, v := range nums {
        map_num[v]++
    }
    // 统计出现过的元素（去重）
    for k, _ := range map_num {
        ans = append(ans, k)
    }

    // 根据出现频率排序元素
    sort.Slice(ans, func(i, j int) bool {
        return map_num[ans[i]] &amp;gt; map_num[ans[j]]
    })
    return ans[:k]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;xmind 总结&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;../images/027/stack&amp;amp;queue.xmind&quot;&gt;🔗xmind 源文件&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/027/stack&amp;amp;queue.png&quot; alt=&quot;stack&amp;amp;queue&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>二叉树的前/中/后序遍历（深度优先遍历）</title><link>https://notes.kiramux.com/posts/028-binary-tree-depth-first-traversal</link><guid isPermaLink="true">https://notes.kiramux.com/posts/028-binary-tree-depth-first-traversal</guid><pubDate>Fri, 17 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;力扣题目链接&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/binary-tree-preorder-traversal/&quot;&gt;🔗144.二叉树的前序遍历(opens new window)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/binary-tree-postorder-traversal/&quot;&gt;🔗145.二叉树的后序遍历(opens new window)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/binary-tree-inorder-traversal/&quot;&gt;🔗94.二叉树的中序遍历&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;[TOC]&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/028/image-20251015102826679.png&quot; alt=&quot;image-20251015102826679&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;递归遍历&lt;/h2&gt;
&lt;p&gt;写递归遍历一定要确定以下3点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;确定递归函数的参数和返回值&lt;/strong&gt;：确定哪些参数是递归的工程中需要处理的，就在递归函数中加上这个参数；明确每次递归的返回值，从而确定递归函数的返回类型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确定终止条件&lt;/strong&gt;：运行递归时遇到栈溢出错误，一般就是没写终止条件或终止条件写的不对。&lt;em&gt;操作系统是用一个栈结构来保存每一层的递归信息，如果递归没有终止，操作系统的内存栈必然会溢出&lt;/em&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确定单层递归的逻辑&lt;/strong&gt;：确定每一层递归需要处理的信息，在这里也就会重复调用自己来实现递归的过程。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;前序遍历&lt;/h3&gt;
&lt;p&gt;在 Go 中如果使用&lt;strong&gt;命名返回值&lt;/strong&gt;（给返回值起了名字 &lt;code&gt;res&lt;/code&gt;，并且类型是 &lt;code&gt;[]int&lt;/code&gt;），相当于在函数内部自动声明了一个变量 res ，类型为 []int ，自动分配内存，并初始化为对应类型的零值 nil。&lt;/p&gt;
&lt;p&gt;函数变量声明 &lt;code&gt;var traversal func(node *TreeNode)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;声明一个变量 &lt;code&gt;traversal&lt;/code&gt; ，它的类型是 &lt;code&gt;func(node *TreeNode)&lt;/code&gt;，&lt;em&gt;也就是“接受一个&lt;code&gt;*TreeNode&lt;/code&gt;参数，没有返回值”的函数类型&lt;/em&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;后续再给它赋值一个具体的函数实现。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通常在&lt;strong&gt;匿名函数递归&lt;/strong&gt;中使用，因为 Go 中的匿名函数不能在函数内调用自己，因此要先声明一个函数变量，再赋值匿名函数。&lt;/p&gt;
&lt;p&gt;这样不用在外部声明递归函数，也不需要传指针了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func preorderTraversal(root *TreeNode) (res []int) {
    var traversal func(node *TreeNode)
    traversal = func(node *TreeNode) {
        // 确定终止条件
        if node == nil {
            return
        }
        // 确定单层递归的逻辑
        res = append(res, node.Val)// 中
        // 左
        traversal(node.Left)
        // 右
        traversal(node.Right)
    }
    traversal(root)
    return res
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;后序遍历&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func postorderTraversal(root *TreeNode) (res []int) {
    var traversal func(node *TreeNode)
    traversal = func(node *TreeNode) {
        // 确定终止条件
        if node == nil {
            return
        }
        // 确定单层递归的逻辑
        // 左
        traversal(node.Left)
        // 右
        traversal(node.Right)
        res = append(res, node.Val)// 中
    }
    traversal(root)
    return res
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;中序遍历（尝试外部递归函数）&lt;/h3&gt;
&lt;p&gt;三道题目一样都可以用匿名递归函数解决，这里尝试使用外部定义的递归函数来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func inorderTraversal(root *TreeNode) []int {
    res := []int{}
    traversal(root, &amp;amp;res)
    return res
}

func traversal(cur *TreeNode, res *[]int) {
    // 确定终止条件
    if cur == nil {
        return
    }
    // 确定每层递归逻辑
    traversal(cur.Left, res) // 左
    *res = append(*res, cur.Val) // 中
    traversal(cur.Right, res) // 右
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;迭代遍历&lt;/h2&gt;
&lt;p&gt;在计算机中递归就是用栈实现的：&lt;strong&gt;递归的实现就是：每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中&lt;/strong&gt;，然后递归返回的时候，从栈顶弹出上一次递归的各项参数，所以这就是递归为什么可以返回上一层位置的原因。&lt;/p&gt;
&lt;p&gt;因此用栈就可以实现二叉树的前后中序遍历了。&lt;/p&gt;
&lt;h3&gt;前序遍历--迭代法&lt;/h3&gt;
&lt;p&gt;前序遍历是「中左右」，每次先处理根节点，在处理左、右。那么就可以先把根节点放入栈中，再把右孩子加入栈，再加入左孩子。因为栈是先入后出，先加入右孩子才能保证出栈的时候是「中左右」的顺序&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/028/%E4%BA%8C%E5%8F%89%E6%A0%91%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%EF%BC%88%E8%BF%AD%E4%BB%A3%E6%B3%95%EF%BC%89.gif&quot; alt=&quot;二叉树前序遍历（迭代法）&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意空节点不入栈，因为要对左右孩子操作，如果空节点入栈就操作空指针了。&lt;/p&gt;
&lt;p&gt;用到了 &lt;a href=&quot;https://before80.github.io/go_docs/stdLib/container/list/&quot;&gt;List&lt;/a&gt; 包来作为栈。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func preorderTraversal(root *TreeNode) []int {
    res := []int{}
    if root == nil {
        return res
    }
    st := list.New()
    st.PushBack(root)
    for st.Len() &amp;gt; 0 {
        // func (l *List) Remove(e *Element) any
        // 如果e是列表l的一个元素，Remove方法将e从l中移除，并返回元素值e.Value。该元素不能是nil。
        // any 需要断言成需要的类型
        node := st.Remove(st.Back()).(*TreeNode) // pop 栈顶元素
        res = append(res, node.Val)

        // 把右、左孩子放入栈
        if node.Right != nil {
            st.PushBack(node.Right)
        }
        if node.Left != nil {
            st.PushBack(node.Left)
        }
    }
    return res
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;后序遍历--迭代法&lt;/h3&gt;
&lt;p&gt;前序遍历最后数组的顺序是 「中左右」，那么把左右调换位置「中右左」，再把res数组 reverse 一下就变成了后序遍历「左右中」的输出顺序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func postorderTraversal(root *TreeNode) []int {
    res := []int{}
    if root == nil {
        return res
    }
    st := list.New()
    st.PushBack(root)
    for st.Len() &amp;gt; 0 {
        // func (l *List) Remove(e *Element) any
        // 如果e是列表l的一个元素，Remove方法将e从l中移除，并返回元素值e.Value。该元素不能是nil。any 需要断言成需要的类型
        node := st.Remove(st.Back()).(*TreeNode) // pop 栈顶元素
        res = append(res, node.Val)

        // 前序遍历最后数组的顺序是 中左右，那么把左右调换位置「中右左」，再把res数组 reverse 一下就变成了后序遍历「左右中」 
        if node.Left != nil {
            st.PushBack(node.Left)
        }
        if node.Right != nil {
            st.PushBack(node.Right)
        }
        
    }
    return reverse(res)
}

func reverse(s []int) []int {
    left := 0
    right := len(s) - 1
    for left &amp;lt; right {
        s[left], s[right] = s[right], s[left]
        left++
        right--
    }
    return s
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;中序遍历--迭代法&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../images/028/%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%EF%BC%88%E8%BF%AD%E4%BB%A3%E6%B3%95%EF%BC%89.gif&quot; alt=&quot;二叉树中序遍历（迭代法）&quot; /&gt;&lt;/p&gt;
&lt;p&gt;和迭代法前、后序遍历不同，迭代法中序遍历需要&lt;strong&gt;遍历过程&lt;/strong&gt;和&lt;strong&gt;处理过程&lt;/strong&gt;分开。&lt;/p&gt;
&lt;p&gt;声明一个 cur TreeNode ，把它当指针遍历二叉树的左孩子，一边遍历一边把它放进栈中，一路向左~~（一路向西）~~走到黑，直到 cur.Left == nil。&lt;/p&gt;
&lt;p&gt;此时 cur 就是「中」，弹出 cur 写入 result 数组（处理元素），cur = cur.Right，再继续遍历 cur 的左孩子……&lt;/p&gt;
&lt;p&gt;（当 cur.Left == nil 时，左节点已经在上一步处理过了，此时 cur 就是中节点，需要处理的就是中节点。cur.Right 相当于一个「新的二叉树」，需要从它的左孩子遍历）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func inorderTraversal(root *TreeNode) []int {
    res := []int{}
    if root == nil {
        return res
    }
    st := list.New()
    cur := root
    
    for cur != nil || st.Len() &amp;gt; 0 {
        if cur != nil {
            st.PushBack(cur)
            cur = cur.Left
        } else {
            cur = st.Remove(st.Back()).(*TreeNode)
            res = append(res, cur.Val)
            cur = cur.Right
        }
    }
    return res
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Kiramux</author></item><item><title>二叉树的层序遍历（广度优先遍历）</title><link>https://notes.kiramux.com/posts/029-binary-tree-level-order-traversal</link><guid isPermaLink="true">https://notes.kiramux.com/posts/029-binary-tree-level-order-traversal</guid><pubDate>Sat, 01 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- toc --&amp;gt;&lt;/p&gt;
&lt;h2&gt;二叉树的层序遍历——使用队列&lt;/h2&gt;
&lt;p&gt;层序遍历一个二叉树，就是从左到右一层一层地遍历二叉树，需要用队列来实现。&lt;/p&gt;
&lt;p&gt;可以用 slice 或 &lt;a href=&quot;https://before80.github.io/go_docs/stdLib/container/list/&quot;&gt;list&lt;/a&gt; 来模拟队列。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;队列先进先出，符合一层一层遍历的逻辑，广度优先逻辑。&lt;/p&gt;
&lt;p&gt;栈先进后出，适合模拟深度优先遍历，也就是递归的逻辑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;../images/029/102%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86.gif&quot; alt=&quot;102二叉树的层序遍历&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;102.二叉树的层序遍历&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/binary-tree-level-order-traversal/&quot;&gt;🔗102.二叉树的层序遍历_LeetCode&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给你二叉树的根节点 &lt;code&gt;root&lt;/code&gt; ，返回其节点值的 &lt;strong&gt;层序遍历&lt;/strong&gt; 。 （即逐层地，从左到右访问所有节点）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;用切片模拟队列&lt;/p&gt;
&lt;p&gt;先判断 root 如果为空则返回 nil；root 不为空则把 root 放入 queue 中。&lt;/p&gt;
&lt;p&gt;第一层 for 循环在队列中没有元素时终止&lt;/p&gt;
&lt;p&gt;第二层 for 循环需要提前确定 size 的数值，而不能直接用 len(queue)，因为 queue 中的元素是实时在变化的。其实这里用 for range size 更合适，之后的题目大多用了这个。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;处理队列中的元素:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;确定好该层的元素个数，循环弹出队列中第一个元素，记录它的值（并做相关操作），如果 node 有 Left 和 Right 则放入队列中。&lt;/p&gt;
&lt;p&gt;最后把每层收集到的数组放入 result 中返回。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func levelOrder(root *TreeNode) [][]int {
    result := [][]int{}
    if root == nil {
        return result
    }
    queue := []*TreeNode{}
    queue = append(queue, root)

    for len(queue) &amp;gt; 0 {
        size := len(queue)
        curLevelVal := []int{}
        for size &amp;gt; 0 {
            // 弹出队列第一个元素
            a := queue[0]
            curLevelVal = append(curLevelVal, a.Val)
            queue = queue[1:]
            if a.Left != nil {
                queue = append(queue, a.Left)
            }
            if a.Right != nil {
                queue = append(queue, a.Right)
            }
            size--
        }
        result = append(result, curLevelVal)
    }
    return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;107.二叉树的层次遍历II&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/binary-tree-level-order-traversal-ii/&quot;&gt;🔗107.二叉树的层次遍历II_LeetCode&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给你二叉树的根节点 &lt;code&gt;root&lt;/code&gt; ，返回其节点值 &lt;strong&gt;自底向上的层序遍历&lt;/strong&gt; 。 （即按从叶子节点所在层到根节点所在的层，逐层从左向右遍历）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;就是上面的 102 题的结果反转一下。&lt;/p&gt;
&lt;p&gt;这次用 container 中的 list 来模拟队列。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func levelOrderBottom(root *TreeNode) [][]int {
    result := [][]int{}

    if root == nil {
        return nil
    }
    queue := list.New() //这里的list是包名，不是List类型
    queue.PushBack(root)

    for queue.Len() &amp;gt; 0 {
        curLevelVal := []int{} // 用于存储当前层节点值
        size := queue.Len() // 保存当前层的节点个数，再循环操作该层节点
        for range size { // 这个循环结束也就代表处理完该层元素了
            // Element结构体是链表List中的一个元素，里面的 Val 才是存的值，所以在 Remove() 时传入的是元素指针，不会出现相等元素删除错误的情况
            // 弹出队列第一个元素
            node := queue.Remove(queue.Front()).(*TreeNode)
            curLevelVal = append(curLevelVal, node.Val)
            if node.Left != nil {
                queue.PushBack(node.Left)
            }
            if node.Right != nil {
                queue.PushBack(node.Right)
            }
        }
        // 这层处理完了，把这层的节点值数组放进 result 中
        result = append(result, curLevelVal)
    }

    return reverse(result)
}

func reverse(s [][]int) [][]int {
    left := 0
    right := len(s)-1
    for left &amp;lt; right {
        s[left], s[right] = s[right], s[left]
        left++
        right--
    }
    return s
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;199.二叉树的右视图&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/binary-tree-right-side-view/&quot;&gt;🔗199.二叉树的右视图_LeetCode&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个二叉树的 &lt;strong&gt;根节点&lt;/strong&gt; &lt;code&gt;root&lt;/code&gt;，想象自己站在它的右侧，按照从顶部到底部的顺序，返回从右侧所能看到的节点值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;相同的遍历思路，只记录队列中的该层元素的最后一个（最右侧的）节点值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func rightSideView(root *TreeNode) []int {
    result := []int{}
    if root == nil {
        return nil
    }
    queue := list.New()
    queue.PushBack(root)

    for queue.Len() &amp;gt; 0 {
        size := queue.Len()
        for i := range size { // 遍历该层元素
            node := queue.Remove(queue.Front()).(*TreeNode)
            if node.Left != nil {
                queue.PushBack(node.Left)
            }
            if node.Right != nil {
                queue.PushBack(node.Right)
            }
            // 只记录队列中的该层元素的最后一个（最右侧的）节点值
            if i == size - 1 {
                result = append(result, node.Val)
            }
        }
    }
    return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;637.二叉树的层平均值&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/average-of-levels-in-binary-tree/&quot;&gt;🔗637.二叉树的层平均值_LeetCode&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个非空二叉树的根节点 &lt;code&gt;root&lt;/code&gt; , 以数组的形式返回每一层节点的平均值。与实际答案相差 &lt;code&gt;10-5&lt;/code&gt; 以内的答案可以被接受。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;同 102 思路，给每层的结果分别计算一下平均值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func averageOfLevels(root *TreeNode) []float64 {
    result := []float64{}
    if root == nil {
        return nil
    }

    queue := list.New()
    queue.PushBack(root)

    for queue.Len() &amp;gt; 0 {
        size := queue.Len()
        curLevelVals := []int{}
        for range size {
            node := queue.Remove(queue.Front()).(*TreeNode)
            curLevelVals = append(curLevelVals, node.Val)
            if node.Left != nil {
                queue.PushBack(node.Left)
            }
            if node.Right != nil {
                queue.PushBack(node.Right)
            }
        }
        // 计算该层数值平均值
        sum := 0
        for i := range len(curLevelVals) {
            sum += curLevelVals[i]
        }
        var avg float64
        avg = float64(sum) / float64(len(curLevelVals))
        result = append(result, avg)
    }
    return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;429.N叉树的层序遍历&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/n-ary-tree-level-order-traversal/&quot;&gt;🔗429.N叉树的层序遍历_LeetCode&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个 N 叉树，返回其节点值的&lt;em&gt;层序遍历&lt;/em&gt;。（即从左到右，逐层遍历）。&lt;/p&gt;
&lt;p&gt;树的序列化输入是用层序遍历，每组子节点都由 null 值分隔（参见示例）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/029/narytreeexample.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：root = [1,null,3,2,4,null,5,6]
输出：[[1],[3,2,4],[5,6]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/029/sample_4_964.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]
输出：[[1],[2,3,4,5],[6,7,8,9,10],[11,12,13],[14]]
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;处理节点时，用遍历切片的方式代替之前的 Left、Right 判断。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func levelOrder(root *Node) [][]int {
    if root == nil {
        return nil
    }
    result := [][]int{}
    queue := list.New()
    queue.PushBack(root)

    for queue.Len() &amp;gt; 0 {
        size := queue.Len()
        curLevelVals := []int{}
        for range size {
            node := queue.Remove(queue.Front()).(*Node)
            curLevelVals = append(curLevelVals, node.Val)
            if node.Children != nil {
                for i := range node.Children {
                    queue.PushBack(node.Children[i])
                }
            }
        }
        result = append(result, curLevelVals)
    }
    return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;515.在每个树行中找最大值&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-largest-value-in-each-tree-row/&quot;&gt;🔗515.在每个树行中找最大值_LeetCode&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一棵二叉树的根节点 &lt;code&gt;root&lt;/code&gt; ，请找出该二叉树中每一层的最大值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在遍历每一层的 node 时顺便做最大值的判断。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;也可以在 102 的结果上做最大值判断，只不过会多一次 for 循环。&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func largestValues(root *TreeNode) []int {
    if root == nil {
        return nil
    }
    result := []int{}
    queue := list.New()
    queue.PushBack(root)
    

    for queue.Len() &amp;gt; 0 {
        temp := math.MinInt64
        size := queue.Len()
        curLevelVals := []int{}
        for range size {
            node := queue.Remove(queue.Front()).(*TreeNode)
            curLevelVals = append(curLevelVals, node.Val)
            temp = max(temp, node.Val)

            if node.Left != nil {
                queue.PushBack(node.Left)
            }
            if node.Right != nil {
                queue.PushBack(node.Right)
            }
        }
        // 取最大值
        result = append(result, temp)
    }
    return result
}

func max(x, y int) int {
    if x &amp;gt; y {
        return x
    } 
    return y
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;116.填充每个节点的下一个右侧节点指针&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/populating-next-right-pointers-in-each-node/&quot;&gt;🔗116.填充每个节点的下一个右侧节点指针_LeetCode&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个 &lt;strong&gt;完美二叉树&lt;/strong&gt; ，其所有叶子节点都在同一层，每个父节点都有两个子节点。二叉树定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Node {
  int val;
  Node *left;
  Node *right;
  Node *next;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;填充它的每个 next 指针，让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点，则将 next 指针设置为 &lt;code&gt;NULL&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;初始状态下，所有 next 指针都被设置为 &lt;code&gt;NULL&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/029/116_sample.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：root = [1,2,3,4,5,6,7]
输出：[1,#,2,3,#,4,5,6,7,#]
解释：给定二叉树如图 A 所示，你的函数应该填充它的每个 next 指针，以指向其下一个右侧节点，如图 B 所示。序列化的输出按层序遍历排列，同一层节点由 next 指针连接，&apos;#&apos; 标志着每一层的结束。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：root = []
输出：[]
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;处理每一层元素时：&lt;/p&gt;
&lt;p&gt;if i &amp;lt; size - 1 即此时的 node.Next 还有节点，不为 nil，把 node.Next 指向后面那个节点。这时 node 已经被弹出队列了，因此它原先的下一个节点，就是 queue 现在的第一个节点，读取它并类型断言为 *Node。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node.Next = queue.Front().Value.(*Node)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;否则（i &amp;gt;= size -1），此时的 node.Next 为 nil。 i == size -1 时，这个 node 是 queue 中仅有的一个元素，因此 node.Next == nil。&lt;/p&gt;
&lt;p&gt;就这样把每一层的节点都指向它队列中的下一个节点，末尾节点指向 nil 即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func connect(root *Node) *Node {
	if root == nil {
        return nil
    }
    queue := list.New()
    queue.PushBack(root)
    for queue.Len() &amp;gt; 0 {
        size := queue.Len()
        for i := range size {
            node := queue.Remove(queue.Front()).(*Node)
            if i &amp;lt; size - 1 {
                node.Next = queue.Front().Value.(*Node)
            } else {
                node.Next = nil
            }

            if node.Left != nil {
                queue.PushBack(node.Left)
            }
            if node.Right != nil {
                queue.PushBack(node.Right)
            }
        }
    }
    return root
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;117.填充每个节点的下一个右侧节点指针II&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii/&quot;&gt;🔗117.填充每个节点的下一个右侧节点指针II_LeetCode&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个二叉树：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Node {
  int val;
  Node *left;
  Node *right;
  Node *next;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;填充它的每个 next 指针，让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点，则将 next 指针设置为 &lt;code&gt;NULL&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;初始状态下，所有 next 指针都被设置为 &lt;code&gt;NULL&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/029/117_sample.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：root = [1,2,3,4,5,null,7]
输出：[1,#,2,3,#,4,5,7,#]
解释：给定二叉树如图 A 所示，你的函数应该填充它的每个 next 指针，以指向其下一个右侧节点，如图 B 所示。序列化输出按层序遍历顺序（由 next 指针连接），&apos;#&apos; 表示每层的末尾。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：root = []
输出：[]
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;同116。&lt;/p&gt;
&lt;h2&gt;104.二叉树的最大深度&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-depth-of-binary-tree/&quot;&gt;🔗104.二叉树的最大深度_LeetCode&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个二叉树 &lt;code&gt;root&lt;/code&gt; ，返回其最大深度。&lt;/p&gt;
&lt;p&gt;二叉树的 &lt;strong&gt;最大深度&lt;/strong&gt; 是指从根节点到最远叶子节点的最长路径上的节点数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/029/tmp-tree.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：root = [3,9,20,null,null,15,7]
输出：3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：root = [1,null,2]
输出：2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;树中节点的数量在 &lt;code&gt;[0, 104]&lt;/code&gt; 区间内。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-100 &amp;lt;= Node.val &amp;lt;= 100&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;初始化一个计数器为0，每处理完二叉树的一层就+1，最后返回它。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func maxDepth(root *TreeNode) int {
    if root == nil {
        return 0
    }
    queue := list.New()
    queue.PushBack(root)
    counter := 0
    for queue.Len() &amp;gt; 0 {
        size := queue.Len()
        for range size {
            node := queue.Remove(queue.Front()).(*TreeNode)
            if node.Left != nil {
                queue.PushBack(node.Left)
            }
            if node.Right != nil {
                queue.PushBack(node.Right)
            }
        }
        counter++
    }
    return counter
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;111.二叉树的最小深度&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-depth-of-binary-tree/&quot;&gt;🔗111.二叉树的最小深度_LeetCode&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个二叉树，找出其最小深度。&lt;/p&gt;
&lt;p&gt;最小深度是从根节点到最近叶子节点的最短路径上的节点数量。&lt;/p&gt;
&lt;p&gt;**说明：**叶子节点是指没有子节点的节点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/029/ex_depth.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：root = [3,9,20,null,null,15,7]
输出：2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：root = [2,null,3,null,4,null,5,null,6]
输出：5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;树中节点数的范围在 &lt;code&gt;[0, 105]&lt;/code&gt; 内&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-1000 &amp;lt;= Node.val &amp;lt;= 1000&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;思路同 104 ，每遍历完一层，counter++&lt;/p&gt;
&lt;p&gt;在层序遍历二叉树时，同时判断 node 是否有左右孩子，如果没有可以直接返回 counter+1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func minDepth(root *TreeNode) int {
    if root == nil {
        return 0
    }
    queue := list.New()
    queue.PushBack(root)
    counter := 0
    for queue.Len() &amp;gt; 0 {
        size := queue.Len()
        for range size {
            node := queue.Remove(queue.Front()).(*TreeNode)
            if node.Left != nil {
                queue.PushBack(node.Left)
            }
            if node.Right != nil {
                queue.PushBack(node.Right)
            }
            if node.Left == nil &amp;amp;&amp;amp; node.Right == nil {
                return counter+1
            }
        }
        counter++
    }
    return counter
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Kiramux</author></item><item><title>560. 和为 K 的子数组 题解</title><link>https://notes.kiramux.com/posts/030-leetcodehot100-560</link><guid isPermaLink="true">https://notes.kiramux.com/posts/030-leetcodehot100-560</guid><pubDate>Mon, 02 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/subarray-sum-equals-k/&quot;&gt;560. 和为 K 的子数组&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;中等&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;nums&lt;/code&gt; 和一个整数 &lt;code&gt;k&lt;/code&gt; ，请你统计并返回 &lt;em&gt;该数组中和为 &lt;code&gt;k&lt;/code&gt; 的子数组的个数&lt;/em&gt; 。&lt;/p&gt;
&lt;p&gt;子数组是数组中元素的连续非空序列。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [1,1,1], k = 2
输出：2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：nums = [1,2,3], k = 3
输出：2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= nums.length &amp;lt;= 2 * 104&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-1000 &amp;lt;= nums[i] &amp;lt;= 1000&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-107 &amp;lt;= k &amp;lt;= 107&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;前排提示：由于数组 nums 不单调且存在负数，不适合用滑动窗口来做。&lt;/p&gt;
&lt;p&gt;本题是&lt;code&gt;连续子数组和&lt;/code&gt;问题，可以用&lt;code&gt;前缀和&lt;/code&gt;转化。因此想要解决这道题，需要先理解如何使用前缀和转化问题，这就要先来看看前缀和模板题了。&lt;/p&gt;
&lt;h2&gt;前缀和模板题&lt;/h2&gt;
&lt;p&gt;这里有一道整数数组前缀和模板题，可以帮助我们很好地理解前缀和。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/range-sum-query-immutable/description/&quot;&gt;303. 区域和检索 - 数组不可变&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下面用 a 来代指数组 nums&lt;/p&gt;
&lt;p&gt;这道题需要我们设计一个类型 NumArray，实现它的初始化函数 Constructor 和一个用来求 a[i] ~ a[j] 的元素和的 SumRange 方法，。&lt;/p&gt;
&lt;h3&gt;关键思路&lt;/h3&gt;
&lt;p&gt;这里有一个数组 a = [1, 2, 3, 4, 5, 6]，要想计算子数组 [3, 4, 5] 的元素和，可以用前缀 [1, 2, 3, 4, 5] 的元素和 减去前缀 [1, 2] 的元素和。即:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;3+4+5 = (1+2+3+4+5) - (1+2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相当于前缀 [1, 2, 3, 4, 5] 去掉前缀 [1, 2] 就得到了子数组 [3, 4, 5]&lt;/p&gt;
&lt;p&gt;一般地，任意子数组都是一个前缀去掉前缀后的结果。所以&lt;strong&gt;任意子数组的和，都可以表示为两个前缀和的差。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;a 的子数组有 O(n^2) 个  （n*(n+1)/2），但只有 O(n) 个前缀。那么预处理 a 的所有前缀和，就可以 O(1) 计算任意子数组的元素和。&lt;/p&gt;
&lt;h3&gt;具体思路&lt;/h3&gt;
&lt;p&gt;nums 记作 &lt;em&gt;a&lt;/em&gt;，其长度为n&lt;/p&gt;
&lt;p&gt;对于数组 &lt;em&gt;a&lt;/em&gt; ，计算它的长为 n+1 的前缀和数组 &lt;em&gt;s&lt;/em&gt;，即计算 「a 的前 0 个数的和」， 「前 1 个数的和」，「前 2 个数的和」 …… 「前 n 个数的和」，这些和组成一个数组 s。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注意这里 s[0] = 0
s[0] = 0
s[1] = a[0]
s[2] = a[0] + a[1]
  ⋮
s[i] = a[0] + a[1] + ⋯ + a[i−1]
s[i+1] =a[0] + a[1] + ⋯ + a[i−1] + a[i]
  ⋮
s[n] = a[0] + a[1] + ⋯ + a[n−1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过前缀和，我们可以把子数组的元素和 转化成两个前缀和的差。&lt;/p&gt;
&lt;p&gt;下标区间 [left, right] 的元素和 等于前缀 [0, right] 的元素和 减去 [0, left-1] 的元素和，即&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s[right+1] - s[left]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例数组 [−2,0,3,−5,2,−1]&lt;/p&gt;
&lt;p&gt;对应的前缀和数组 &lt;em&gt;s&lt;/em&gt; = [0,−2,−2,1,−4,−2,−3]&lt;/p&gt;
&lt;p&gt;需要求子数组 [3,−5,2,−1] 的和&lt;/p&gt;
&lt;p&gt;有了这个式子，我们就可以 O(1) 计算出：s[6] - s[2] = -3 -(-2) = -1&lt;/p&gt;
&lt;h3&gt;为什么要定义 s[0] = 0，这样做有什么好处&lt;/h3&gt;
&lt;p&gt;当 left == 0 时，要计算的子数组其实是一个前缀（从 a[0] 到 a[right] ）。&lt;/p&gt;
&lt;p&gt;如果不定义 s[0] = 0&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无法只用一个公式 &lt;code&gt;s[i] = s[i-1] + x&lt;/code&gt; 来计算前缀和，此时需要特判 i = 0 的情况了。&lt;/li&gt;
&lt;li&gt;同时计算差值时 &lt;code&gt;s[right] - s[left-1]&lt;/code&gt; 也需要对 left = 0 进行特判。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过定义 s[0] = 0, 任意子数组（包括前缀）都可以表示为两个前缀和的差 &lt;code&gt;s[right+1] - s[left]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;此外，如果 a 是空数组，定义 s[0] = 0 可以兼容这种情况，体现在计算前缀和数组 s ：&lt;/p&gt;
&lt;p&gt;如果不定义 s[0] = 0，那么当遇到空数组时，s 也会是空数组，需要对空数组的情况进行特判。&lt;/p&gt;
&lt;p&gt;这里有点像链表章节题目里常用的虚拟头结点，通过添加虚拟头结点，统一对链表各元素的操作。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;另外一种涉及到子数组的时候，前缀和为什么要添加0的解释：&lt;/p&gt;
&lt;p&gt;对于范围 [i, j]子数组而言，其和为：
s[j] - s[i - 1]&lt;/p&gt;
&lt;p&gt;类似于动态规划，为了避免i - 1是负数，我们可以选择把表达式改为：
s[j + 1] - s[i]
并在最前面添加一个状态0&lt;/p&gt;
&lt;p&gt;https://leetcode.cn/problems/range-sum-query-immutable/solutions/2693498/qian-zhui-he-ji-qi-kuo-zhan-fu-ti-dan-py-vaar/comments/3000111/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;代码实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type NumArray []int

// 构造前缀和数组
func Constructor(nums []int) NumArray {
    s := make(NumArray, len(nums)+1)
    for i, x := range nums {
        s[i+1] = s[i] +x
    }
    return s
}

// 计算区间内前缀和
func (s NumArray) SumRange(left int, right int) int {
    return s[right+1] - s[left]
}


/**
 * Your NumArray object will be instantiated and called as such:
 * obj := Constructor(nums);
 * param_1 := obj.SumRange(left,right);
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;560. 和为 K 的子数组 题解&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/subarray-sum-equals-k/&quot;&gt;560. 和为 K 的子数组&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;关键思路&lt;/h3&gt;
&lt;p&gt;求整数数组 nums 中和为 k 的子数组的个数（子数组是数组中元素的连续非空序列）。&lt;/p&gt;
&lt;p&gt;根据 303 题的前缀和的定义：&lt;/p&gt;
&lt;p&gt;nums 是一个长度为 n 的数组。&lt;/p&gt;
&lt;p&gt;s 是 nums 的前缀和数字，长为 n+1，第一项 s[0] 设为0，最后一项的下标为 n。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s[0] = 0
s[i] = nums[0] + nums[1] + ⋯ + nums[i-1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设 i &amp;lt; j，如果 nums[i] 到 nums[j-1] 的元素和等于 k ，用前缀和表示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s[j] - s[i] = k
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题转化为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;s 中有多少对下标 (i, j) 满足 0≤i&amp;lt;j≤n 且 s[j] - s[i] = k&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;即 s[j] + (-s[i]) = k，转化为了两数之和的问题。只不过两数之和只需找到一对下标，本题需要计算所有满足条件的下标对的个数。&lt;/p&gt;
&lt;p&gt;如果用二重循环暴力枚举满足的下标对，时间复杂度是 O(n^2)。如何加速？&lt;/p&gt;
&lt;p&gt;可以根据两数之和的思路，&lt;code&gt; s[j] - s[i] = k&lt;/code&gt; 移项得&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s[i] = s[j] - k
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;代码实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func subarraySum(nums []int, k int) int {
    s := make([]int, len(nums)+1)   // 前缀和数组
    for i, x := range nums {        // 计算前缀和
        s[i+1] = s[i] + x           // s[0] == 0
    }
    // 设 i &amp;lt; j，求 nums[i] 到 nums[j-1] 的元素和等于k
    // 用前缀和表示： s[j] - s[i] == k     (两数之和)
    // s[i] == s[j] - k
    cnt := make(map[int]int, len(s))
    ans := 0
    for _, sj := range s {          // 遍历 s[j] 的时候顺便把出现过的元素（即需要判断的元素s[i] ）存入哈希表了
        ans += cnt[sj-k]            // 寻找有没有符合条件是 s[i]，如果有计数+1
        cnt[sj]++
    }
    return ans
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;遍历 s[j] 的时候顺便把出现过的元素（即需要判断的元素s[i] ）存入哈希表了。相当两次遍历，先遍历了一遍 s[j]，又遍历一遍 s[i]，只不过是把 s[i] 存入哈希表，使遍历时间复杂度降为了 O(1).&lt;/p&gt;
&lt;h3&gt;答疑&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;问&lt;/strong&gt;：为什么这样做可以不重不漏地计算？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答&lt;/strong&gt;：暴力做法是，外层循环枚举 &lt;em&gt;j&lt;/em&gt;，内层循环枚举 &lt;em&gt;i&lt;/em&gt;，如果 &lt;em&gt;s&lt;/em&gt;[&lt;em&gt;j&lt;/em&gt;]−&lt;em&gt;s&lt;/em&gt;[&lt;em&gt;i&lt;/em&gt;]=&lt;em&gt;k&lt;/em&gt;，那么答案加一。我们保留了「外层循环枚举 &lt;em&gt;j&lt;/em&gt;」这个过程，把内层循环用哈希表优化成了 O(1)，所以本质是对暴力算法的哈希表优化。既然暴力算法是不重不漏地计算，那么优化做法也是不重不漏地计算。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问&lt;/strong&gt;：为什么要把 &lt;em&gt;s&lt;/em&gt;[0]=0 也加到哈希表中？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答&lt;/strong&gt;：举个最简单的例子，&lt;em&gt;nums&lt;/em&gt;=[1], &lt;em&gt;k&lt;/em&gt;=1。如果不把 &lt;em&gt;s&lt;/em&gt;[0]=0 加到哈希表中，按照我们的算法，没法算出这里有 1 个符合要求的子数组。也可以这样理解，要想把任意子数组都表示成两个前缀和的差，必须添加 &lt;em&gt;s&lt;/em&gt;[0]=0，否则当子数组是前缀时，没法减去一个数，具体见 &lt;a href=&quot;https://leetcode.cn/problems/range-sum-query-immutable/solution/qian-zhui-he-ji-qi-kuo-zhan-fu-ti-dan-py-vaar/&quot;&gt;前缀和及其扩展&lt;/a&gt; 中的讲解。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问&lt;/strong&gt;：为什么代码中要先更新 &lt;em&gt;ans&lt;/em&gt;，再更新 &lt;em&gt;cnt&lt;/em&gt;？这两行代码能否交换？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答&lt;/strong&gt;：不行，这会在 &lt;em&gt;k&lt;/em&gt;=0 的时候算错。例如 &lt;em&gt;nums&lt;/em&gt;=[2], &lt;em&gt;k&lt;/em&gt;=0，正确答案应该是 0，但如果先把 &lt;em&gt;cnt&lt;/em&gt;[2] 加一，再把 &lt;em&gt;cnt&lt;/em&gt;[2] 加到 &lt;em&gt;ans&lt;/em&gt; 中，最后返回的 &lt;em&gt;ans&lt;/em&gt; 就不是 0 了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问&lt;/strong&gt;：为什么这题不适合用&lt;strong&gt;滑动窗口&lt;/strong&gt;做？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答&lt;/strong&gt;：滑动窗口需要满足&lt;strong&gt;单调性&lt;/strong&gt;，当右端点元素进入窗口时，窗口元素和是不能减少的。本题 &lt;em&gt;nums&lt;/em&gt; 包含&lt;strong&gt;负数&lt;/strong&gt;，当负数进入窗口时，窗口左端点该往哪个方向移动？无法确定。如果没有负数的话，则可以用滑动窗口（恰好型滑动窗口），见 &lt;a href=&quot;https://leetcode-cn.com/problems/binary-subarrays-with-sum/&quot;&gt;930. 和相同的二元子数组&lt;/a&gt;。&lt;/p&gt;
</content:encoded><author>Kiramux</author></item><item><title>一个乙女游戏资源站</title><link>https://notes.kiramux.com/posts/031-otomegame</link><guid isPermaLink="true">https://notes.kiramux.com/posts/031-otomegame</guid><pubDate>Tue, 24 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;不知道什么原因，我建了一个乙女游戏资源站。&lt;/p&gt;
&lt;p&gt;我觉得一切的一切的开端，是我偶然看到了&lt;a href=&quot;https://vndb.org/v41412&quot;&gt;一款乙女游戏&lt;/a&gt;，觉得 CG 很漂亮，于是下载体验了一下。&lt;/p&gt;
&lt;p&gt;其实我会被吸引还是猎奇心理在作祟。明明是一部乙女游戏，&lt;a href=&quot;https://t.vndb.org/sf/65/181365.jpg&quot;&gt;女主角的🐻画的那么大，cg 也画的那么涩♥️&lt;/a&gt;，我倒要看看这游戏到底怎么样。然后我就体验了一个女性希望被理解和需要的故事，并且感受到了很多共鸣。&lt;/p&gt;
&lt;p&gt;从那款游戏开始，我意识到自己是被游戏的 CG 吸引了。我关注到很多乙女游戏的 CG 画得非常漂亮，总结为以下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;华丽的服饰：&lt;/p&gt;
&lt;p&gt;例如乙女游戏中的主角，通常会身着及其华丽的服饰或可爱的服装，满足少女心玩家。众所周知少女心并不是少女独有的，&lt;s&gt;猛男也有少女心&lt;/s&gt;。女主人公穿着华丽，在故事中和人物进行着一件又一件的互动事件，这是在 GalGame 中无法体验到的特殊感受。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CG 的互动性：&lt;/p&gt;
&lt;p&gt;GalGame 中的 event CG 多为男主人公视角。作为一名画画人，人体早已司空见惯，仅仅看着一张 CG 上的人躺着摆个姿势，没有任何互动性，人物动作全靠人物描写，一张图嗯嗯啊啊水过去的事件看了只会犯困，我觉得这类 cg 是没什么意思的，h场景没什么互动性，十分无聊，但是又想看那么一眼 cg，否则会觉得这家游戏厂商也太没诚意了。这也可能是很多 gal 玩家持「我不看 h 剧情，但你不能没有」观点的原因。&lt;/p&gt;
&lt;p&gt;OtomeGame 中的 CG 大多表现男女主2人的互动，并且由于游戏性质，男主人公颜值也非常高。因此没有什么比看郎才女貌的主人公涩涩更赏心悦目的事情了。（这也可能是我偶尔喜欢看女性向AV的原因）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;「不太」魅宅的画面表现&lt;/p&gt;
&lt;p&gt;这里的不太魅宅是针对 GalGame 对比的，或许应该说女主不会突然化身为一只榨汁姬？曾经在玩 YuzuSoft 的一款游戏，角色设定很可爱我很喜欢，日常也十分有趣，然而进线之后，可爱的人物设定不知道哪里去了，只知道天天h。我也因为这个原因弃坑了很多 GalGame，个人线后面的内容实在是无聊至极。&lt;/p&gt;
&lt;p&gt;OtomeGame 的 h 场景之后通常会描写女主人公的心理描写，在男性玩家视角中常见的游戏情节，在女性主人公的心理活动中又是另一番镜像。可能是这种体验比较新鲜，目前我更喜欢这种感觉。&lt;/p&gt;
&lt;p&gt;(当然我现在玩的日乙不多，仅仅针对我玩过的部分游戏评价，不代表所有的游戏)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;许多 GalGame 优质作品中的 CG 画的也非常漂亮，但通常只描绘女主角，在花1分钟欣赏过这张 CG 之后呢？大多 h 场景的文本枯燥，并且很多作品到 h 部分就把人物塑造丢掉了，非常有工业味。其实我被日乙吸引，更多的还是 CG 方面，想要看到&lt;strong&gt;人物互动&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;有点扯远了。。总之我之后开始找其他日乙的资源，然后发现日乙分享资源有点像活在10年前。举个例子来说，现在我想玩某款乙女游戏，我现在有几个选择：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Steam、DLsite 等网站购入官方中文版/日文版&lt;/li&gt;
&lt;li&gt;购入 Switch 并购买游戏&lt;/li&gt;
&lt;li&gt;在初音的青葱免费下载部分 PC 版盗版资源，在其他站点下载 ns 游戏用模拟器游玩&lt;/li&gt;
&lt;li&gt;给某论坛捐钱，冒着被封论坛账号的风险下载盗版资源&lt;/li&gt;
&lt;li&gt;去司马 tb 店铺花钱购买盗版游戏&lt;/li&gt;
&lt;li&gt;去贴吧淘资源，很可能会遇到删帖 / 资源到期&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;目前看起来获取游戏的途径还蛮多的，但是一旦想要去玩比较久远的游戏，就会面临正版售罄、自己不懂日语、汉化版被论坛或店铺垄断的情况，唯一有希望能玩到这款游戏的方式，就是去翻贴吧的陈年老帖，或是向大佬求资源。接下来还有可能面临网盘链接失效的情况，想玩一款游戏的难度可想而知。因此我才会觉得这种分享资源的方式像是 10 年前，游戏资源被垄断，社区封闭，几乎没有公益站分享，打开帖子和网盘链接完全是在开盲盒。&lt;/p&gt;
&lt;p&gt;可能因为我比较喜欢建站玩，前阵子也不知道该干嘛了，于是就在 kun-touchgal-next 的基础上简单修改了一下，搭建了一个乙女游戏的公益分享站，照搬 touchgal 的模式（网站 + cloudreve 自建盘）。如果你在 touchgal 仓库的 fork 列表中翻一下，很容易找到我现在使用的仓库。
经过近3个月的整理，现在站内已有几百部游戏，也结识了一位做 AI 翻译的朋友，现在正在稳定发展中。&lt;/p&gt;
&lt;p&gt;现在上传游戏的热情也没那么高了，随手写下这一篇文章记录一下吧。前阵子有群友关心我最近在做什么，在这里汇报一下😋。网址就不贴了，在 GitHub 仓库上挂着，避免有不喜欢这类游戏的朋友误入了。&lt;/p&gt;
&lt;p&gt;有个题外话。那位做 AI 翻译的朋友，ta 的 blog 最近把 AI 翻译补丁都下线了，并联系我把补丁全放在我的站点。我有点担心是不是因为我的站点，导致 ta 有了下架补丁的想法。因为我的站点，让一个 blog 少了很大篇幅的内容，我心里还是很过意不去的。还好刚刚询问 ta ，原来是 xhs 上有小警察天天出警导致的，尽管 ta 并没有 xhs 账号。这种危险的人确实得避一避。&lt;/p&gt;
&lt;p&gt;今天就是随意写写，没什么固定的主题。最后放点我喜欢的 CG 插图。有些是直接从网站上摘下来的，所以会有水印。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/031/2.jpg&quot; alt=&quot;2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/031/3.jpg&quot; alt=&quot;3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/031/E077ED2E5108AD7D7D017CC0F1133E8B.jpg&quot; alt=&quot;E077ED2E5108AD7D7D017CC0F1133E8B.jpg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/031/004_a.jpg&quot; alt=&quot;004_a&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/031/013.jpg&quot; alt=&quot;013&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/031/092.jpg&quot; alt=&quot;092&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/031/2a5e9cb7526f7ad92c7f4b20c1e4f3ec7b6d853417782b4ea1100086ab79d7ea.avif&quot; alt=&quot;2a5e9cb7526f7ad92c7f4b20c1e4f3ec7b6d853417782b4ea1100086ab79d7ea.avif&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/031/8d2bec93374f4d414d11a7b731d919520286b024cc99819acee85caea988683f.avif&quot; alt=&quot;8d2bec93374f4d414d11a7b731d919520286b024cc99819acee85caea988683f.avif&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/031/75479d677ab4bbd69bb1f4ffbb9f0efb8e64931b97b770ff9c0784e18b0da49d.avif&quot; alt=&quot;75479d677ab4bbd69bb1f4ffbb9f0efb8e64931b97b770ff9c0784e18b0da49d.avif&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/031/386925de49068433388da60ce4d6fb6cb531b87cf85ef3a5d75e44d71dd1c597.avif&quot; alt=&quot;386925de49068433388da60ce4d6fb6cb531b87cf85ef3a5d75e44d71dd1c597.avif&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/031/b40b9d69e4b3805e5f973d09ba1498704926a624eb742090a7a6af418f482cdb.avif&quot; alt=&quot;b40b9d69e4b3805e5f973d09ba1498704926a624eb742090a7a6af418f482cdb.avif&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/031/f904bb31bdad6e8c9f4cb1323024470e697b607735a0fdf140d1674ee85f0ca7.avif&quot; alt=&quot;f904bb31bdad6e8c9f4cb1323024470e697b607735a0fdf140d1674ee85f0ca7.avif&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/031/f3d5e1f6812987fd6716bfb8b80855cca7e92045f28ec63c59a7bb11b294b311.avif&quot; alt=&quot;f3d5e1f6812987fd6716bfb8b80855cca7e92045f28ec63c59a7bb11b294b311.avif&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./../images/031/feb785eccd47da217bdffadc83e572039377ac2dab23cf5a24b92cd45fd7fe90.avif&quot; alt=&quot;feb785eccd47da217bdffadc83e572039377ac2dab23cf5a24b92cd45fd7fe90.avif&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Kiramux</author></item></channel></rss>