
[{"content":" 本教程使用 TensorFlow 搭建深度卷积生成对抗网络（DCGAN），在 FromSoftware 魂系游戏（Dark Souls）截图数据集上训练，实现从随机噪声生成黑暗之魂的游戏画面。\n1. 环境准备 # 导入所需的库：\nimport tensorflow as tf from tensorflow.keras import layers from pathlib import Path import matplotlib.pyplot as plt from IPython import display import random import os import time from datetime import datetime from dateutil import tz import seaborn as sns sns.set() time_zone = tz.gettz(\u0026#39;Asia/Shanghai\u0026#39;) 2. 数据集下载 # 数据集来自 Kaggle 的 FromSoftware 魂系游戏图片集：\nURL = \u0026#39;https://www.kaggle.com/api/v1/datasets/download/fraxle/images-from-fromsoftware-soulslikes?datasetVersionNumber=1\u0026#39; 设置工作目录：\n!test ! -d /content/GAN_DS1 \u0026amp;\u0026amp; mkdir /content/GAN_DS1 %cd /content/GAN_DS1 /content/GAN_DS1 数据集包含 24,218 张 PNG 图片，均为 Dark Souls 等魂系游戏的截图：\ndataset_path = Path.cwd()/\u0026#39;../datasets/FromSoftwareImages/dark souls\u0026#39; len(list(dataset_path.glob(\u0026#39;*.png\u0026#39;))) 24218 3. 数据清洗 # 检查并移除损坏文件。运行过程中因数据量过大被中断，未完成全部检查。\ndef check(file_dir, format, channels): cnt = 0 bad_files =[] for file_path in sorted(file_dir.glob(f\u0026#39;*.{format}\u0026#39;)): cnt += 1 try: img = tf.io.read_file(str(file_path)) img = tf.image.decode_image(img) if img.ndim != 3: raise Exception(\u0026#39;invalid\u0026#39;) if img.shape[-1] != channels: raise Exception(\u0026#39;invalid\u0026#39;) except Exception as e: if str(e) == \u0026#39;invalid\u0026#39;: bad_files.append(str(file_path)) print(f\u0026#39;checked {cnt} files, found {len(bad_files)} bad files\u0026#39;) return bad_files bad_files = check(dataset_path, \u0026#39;png\u0026#39;, 3) 4. 数据预览与预处理 # 4.1 查看样本 # 原始图片尺寸为 360×640 像素：\nfor file_path in sorted(dataset_path.glob(\u0026#39;*.png\u0026#39;))[:10]: img = tf.io.read_file(str(file_path)) img = tf.image.decode_image(img) print(img.shape) plt.figure() plt.imshow(img) plt.axis(\u0026#39;off\u0026#39;) break (360, 640, 3) 4.2 参数设置 # 将所有图片缩放到 128×256：\nBUFFER_SIZE = 24218 BATCH_SIZE = 256 # 640 360 IMG_WIDTH = 128 IMG_HEIGHT = 256 4.3 图片加载函数 # def get_image(path, width, height): img = tf.io.read_file(str(path)) img = tf.image.decode_image(img) img = tf.image.resize(img, [width, height]) img = img / 255.0 return img 4.4 自定义数据提供器 # 支持限制读取数量，训练时随机打乱顺序：\nclass ImageProvider: def __init__(self, path, nums, width, height, format, training=False): self.path = path self.nums = nums self.format = format self.width = width self.height = height self.training = training def __call__(self): files = sorted(list(self.path.glob(f\u0026#39;*.{self.format}\u0026#39;))[:self.nums]) if self.training: random.shuffle(files) for f in files: yield get_image(f, self.width, self.height) 验证数据提供器：\nimage_provider = ImageProvider(dataset_path, 10, IMG_WIDTH, IMG_HEIGHT, \u0026#39;png\u0026#39;) img = next(image_provider()) print(img.shape) plt.imshow(img) plt.axis(\u0026#39;off\u0026#39;) (128, 256, 3) 4.5 构建 tf.data 流水线 # train_ds = tf.data.Dataset.from_generator( ImageProvider(dataset_path, BUFFER_SIZE, IMG_WIDTH, IMG_HEIGHT, \u0026#39;png\u0026#39;, True), output_signature = (tf.TensorSpec( shape = (IMG_WIDTH, IMG_HEIGHT, 3), dtype = tf.float32 )) ) AUTOTUNE = tf.data.AUTOTUNE train_ds = train_ds.cache().shuffle(BUFFER_SIZE).prefetch(buffer_size = AUTOTUNE).batch(BATCH_SIZE) 验证批次形状：\nfor images in train_ds.take(1): print(images.shape) plt.imshow(images[0]) plt.axis(\u0026#39;off\u0026#39;) (256, 128, 256, 3) 每个 batch 包含 256 张 128×256×3 的图片。\n5. 构建生成器（Generator） # 5.1 噪声维度 # seed_dim = 100 5.2 生成器网络结构 # 接收 100 维随机噪声，通过多层转置卷积（Conv2DTranspose）逐步上采样到 128×256×3 的输出：\n# 128 256 # 64 128 # 32 64 # 16 32 # 8 16 def make_generator_model(): model = tf.keras.Sequential() model.add(layers.Input(shape=(seed_dim, ))) model.add(layers.Dense(8*16*256, use_bias=False)) model.add(layers.BatchNormalization()) model.add(layers.LeakyReLU()) model.add(layers.Reshape((8, 16, 256))) model.add(layers.Conv2DTranspose(128, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;, use_bias=False)) model.add(layers.BatchNormalization()) model.add(layers.LeakyReLU()) model.add(layers.Conv2DTranspose(64, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;, use_bias=False)) model.add(layers.BatchNormalization()) model.add(layers.LeakyReLU()) model.add(layers.Conv2DTranspose(128, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;, use_bias=False)) model.add(layers.BatchNormalization()) model.add(layers.LeakyReLU()) model.add(layers.Conv2DTranspose(3, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;, use_bias=False, activation=\u0026#39;tanh\u0026#39;)) return model 5.3 测试生成器 # seed = tf.random.normal([1, seed_dim]) generator = make_generator_model() generated_images = generator(seed, training=False) print(generated_images.shape) plt.imshow(generated_images[0, :, :, 0]) plt.axis(\u0026#39;off\u0026#39;) (1, 128, 256, 3) 输出形状为 (1, 128, 256, 3)，与预处理后的真实图片尺寸一致。\n6. 构建判别器（Discriminator） # 判别器接收 128×256×3 的图片，通过多层卷积逐步下采样，最终输出一个标量 logit：\ndef make_discriminator_model(): model = tf.keras.Sequential() model.add(layers.Input(shape = (IMG_WIDTH, IMG_HEIGHT, 3))) model.add(layers.Conv2D(64, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;)) model.add(layers.LeakyReLU()) model.add(layers.Dropout(0.3)) model.add(layers.Conv2D(128, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;)) model.add(layers.LeakyReLU()) model.add(layers.Dropout(0.3)) model.add(layers.Conv2D(256, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;)) model.add(layers.LeakyReLU()) model.add(layers.Dropout(0.3)) model.add(layers.Conv2D(128, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;)) model.add(layers.LeakyReLU()) model.add(layers.Dropout(0.3)) model.add(layers.Conv2D(64, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;)) model.add(layers.LeakyReLU()) model.add(layers.Dropout(0.3)) model.add(layers.Flatten()) model.add(layers.Dense(1)) return model 测试未训练判别器对生成图片的判断：\ndiscriminator = make_discriminator_model() decision = discriminator(generated_images) print(decision) tf.Tensor([[-0.00015949]], shape=(1, 1), dtype=float32) 输出接近 0，表示模型未经训练时对图片真假判断不明确。\n7. 损失函数与优化器 # 7.1 损失函数 # cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True) def discriminator_loss(real_output, fake_output): real_loss = cross_entropy(tf.ones_like(real_output), real_output) fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output) return real_loss + fake_loss def generator_loss(fake_output): return cross_entropy(tf.ones_like(fake_output), fake_output) 判别器损失：希望真实图片被判为真（标签=1），生成图片被判为假（标签=0） 生成器损失：希望生成图片被判别器判为真（标签=1） 7.2 优化器与训练参数 # EPOCHS = 800 generator_optimizer = tf.keras.optimizers.Adam(1e-4) discriminator_optimizer = tf.keras.optimizers.Adam(1e-4) 800 个 epoch，两个网络均使用 Adam 优化器，学习率 1e-4。\n8. 训练循环 # 8.1 单步训练 # 每个 batch 的对抗训练步骤：\n@tf.function() def train_step(images): seed = tf.random.normal([BATCH_SIZE, seed_dim]) with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape: generated_images = generator(seed, training=True) real_output = discriminator(images, training=True) fake_output = discriminator(generated_images, training=True) gen_loss = generator_loss(fake_output) disc_loss = discriminator_loss(real_output, fake_output) gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables) gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables) generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables)) discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables)) 8.2 图片保存 # 每个 epoch 结束后保存当前生成器输出的图片：\n!test ! -d generated_images \u0026amp;\u0026amp; mkdir generated_images def generate_and_save_images(model, epoch, test_input): predictions = model(test_input, training=False) generated_images = predictions * 255 generated_images = generated_images.numpy() plt.figure() plt.imshow(generated_images[0].astype(\u0026#39;uint8\u0026#39;)) plt.axis(\u0026#39;off\u0026#39;) plt.savefig(\u0026#39;./generated_images/image_at_epoch_{:04d}.png\u0026#39;.format(epoch)) 8.3 检查点与完整训练流程 # checkpoint_dir = \u0026#39;./training_checkpoints\u0026#39; checkpoint_prefix = os.path.join(checkpoint_dir, \u0026#39;ckpt\u0026#39;) checkpoint = tf.train.Checkpoint( generator_optimizer = generator_optimizer, discriminator_optimizer = discriminator_optimizer, generator = generator, discriminator = discriminator ) seed = tf.random.normal([BATCH_SIZE, seed_dim]) def train(dataset, epochs): for epoch in range(epochs): print(\u0026#39;epoch: \u0026#39;, epoch) start_time = time.time() batch_idx = 0 for image_batch in dataset: train_step(image_batch) batch_idx += 1 display.clear_output(wait=True) generate_and_save_images(generator, epoch+1, seed) print(f\u0026#39;Time for epoch {epoch+1} is {time.time() - start_time} sec\u0026#39;) checkpoint.save(file_prefix = checkpoint_prefix) 启动训练：\ntrain(train_ds, EPOCHS) 9. 最终生成 # 训练完成后，使用生成器生成并保存一张带有时间戳的游戏画面：\n!test ! -d final_generated \u0026amp;\u0026amp; mkdir final_generated time_str = datetime.now().astimezone(time_zone).strftime(\u0026#39;%Y-%m-%d_%H:%M:%S\u0026#39;) seed = tf.random.normal([1, seed_dim]) generated_image = generator(seed)[0] generated_image *= 255 generated_image = generated_image.numpy() generated_image = tf.keras.preprocessing.image.array_to_img(generated_image) generated_image.save(f\u0026#39;./final_generated/image_{time_str}.png\u0026#39;) plt.imshow(generated_image) plt.axis(\u0026#39;off\u0026#39;) 10. 总结 # 项目 说明 模型类型 DCGAN（深度卷积生成对抗网络） 数据集 FromSoftware 魂系游戏截图 — 24,218 张 PNG 输入 100 维随机正态噪声 输出尺寸 128×256×3（宽幅游戏画面） 生成器结构 Dense + Conv2DTranspose + BatchNorm + LeakyReLU 判别器结构 Conv2D + LeakyReLU + Dropout 堆叠，最终输出单标量 损失函数 BinaryCrossentropy (from_logits=True) 优化器 Adam (lr=1e-4) 训练轮数 800 epochs Batch size 256 原始图片尺寸 360×640 像素 本教程从数据下载、预处理到模型构建和训练，完整实现了 DCGAN 在魂系游戏画面生成任务中的应用。\n","date":"8 April 2025","externalUrl":null,"permalink":"/ai/gan_%E9%BB%91%E9%AD%82%E6%B8%B8%E6%88%8F%E7%94%BB%E9%9D%A2%E7%94%9F%E6%88%90/","section":"AI 相关内容","summary":"","title":"GAN 模型实践-黑暗之魂游戏画面生成","type":"ai"},{"content":"sRenderer 是一个纯软件渲染器——每一个像素都完全在 CPU 上计算完成，不依赖任何 GPU。没有 OpenGL，没有 Vulkan，没有 DirectX。唯一的外部依赖是 SFML，它仅用于创建窗口和显示画面。从向量运算到透视投影再到纹理映射，其他所有功能都是从零开始构建的。\n项目地址： https://github.com/Oh-noodles/sRenderer\nAfrican Head 模型 鲨鱼 树木 技术栈 # 类别 技术 说明 编程语言 C++11 不依赖任何外部数学库（Eigen、glm 等） 构建系统 CMake 3.11+ 使用 FetchContent 自动获取 SFML 界面 / 窗口 SFML 3.0.0 仅用于创建窗口和纹理显示 模型格式 Wavefront .obj 解析顶点、纹理坐标、法线和面 纹理格式 TGA (Targa) 手写的 TGA 加载器，支持 RGBA 编译器 g++ / clang++ 在 Linux 和 macOS 上测试 特性 # 核心渲染 # 纯软件光栅化器 —— 每个像素在 CPU 上着色，无需 GPU 透视投影（Perspective Projection） —— 完整的 4x4 矩阵管线：视图矩阵（View）→ 投影矩阵（Project）→ 视口矩阵（Viewport） Z 缓冲深度测试（Z-Buffer） —— 逐像素深度比较以处理遮挡关系 透视正确插值（Perspective-Correct Interpolation） —— 使用 1/z 插值技术实现正确的纹理映射 漫反射光照（Diffuse / Lambertian Lighting） —— 表面法线与光线方向的点积，附带环境光项 纹理映射（Texture Mapping） —— 带透视校正的 UV 坐标插值 线框渲染（Wireframe） —— 类似 Bresenham 的三角线绘制算法 着色器抽象（Shader Abstraction） —— IShader 接口，包含顶点阶段和片段阶段（类似 GPU 着色器模型） 数学库（从零实现） # Vec\u0026lt;T, DIM\u0026gt; —— 泛型 N 维向量，支持运算符（+、-、*、点积、叉积、归一化） Matrix44f —— 完整 4x4 浮点矩阵，支持乘法、逆矩阵和转置 lookAt() —— 相机变换矩阵 edgeFunction() —— 三角形光栅化辅助函数 embed\u0026lt;\u0026gt;() —— 齐次坐标的维度嵌入 模型与图像加载 # Wavefront .obj 解析器（顶点、纹理坐标、法线、三角形/四边形面） 四边形到三角形的扇形转换（支持任意多边形面） TGA 图像加载器，支持 RGBA 内置模型切换器（运行时可选择 african_head、shark 或 tree） 实时渲染循环 # 自动旋转轨道相机（基于时间的动画） 使用 SFML 的 60 FPS 渲染循环 通过 SFML RenderTexture + Sprite 管线实时显示 渲染管线 # 渲染管线遵循经典 GPU 管线的简化版。以下是每个三角形的处理流程：\nOBJ 模型 │ ▼ 顶点处理（Vertex Processing） │ 将顶点嵌入为 Vec4f（齐次坐标） │ 依次应用 View × Project × ViewPort 矩阵变换 │ 执行透视除法（w-divide） │ ▼ 光栅化（Rasterization） │ 计算三角形包围盒 │ 逐像素进行点在三角形内测试（边函数） │ 透视正确插值（1/z 方法） │ ▼ 片段着色（Fragment Shading） │ 在插值后的 UV 坐标处采样纹理 │ 计算漫反射光照（法线 · 光线方向 + 环境光） │ 将光照应用到纹理颜色 │ ▼ 深度测试与写入（Depth Test \u0026amp; Write） │ 将插值后的 z 值与 Z 缓冲进行比较 │ 将像素写入帧缓冲 │ ▼ SFML 显示 将帧缓冲上传为 GPU 纹理（通过 SFML） 渲染精灵到窗口 矩阵变换 # 三个主要矩阵的构建方式如下：\n视图矩阵（View Matrix） —— lookAt(eye, target, up) 的逆矩阵 将世界空间坐标转换为相机空间 投影矩阵（Projection Matrix） —— 对称透视投影 将相机空间映射到裁剪空间：near=1、far=30、视场角约 45° 视口矩阵（Viewport Matrix） —— 将归一化设备坐标映射到屏幕像素 按帧缓冲维度进行缩放和平移 组合变换为：ViewPort × Project × View × vertex\n透视正确插值 # 这是避免纹理映射中常见\u0026quot;扭曲\u0026quot;伪影的关键技术。算法不是直接线性插值 UV 坐标，而是先对 UV/z 值进行插值，然后除以插值后的 1/z 来获得透视正确的结果：\n1/z = w0/z0 + w1/z1 + w2/z2 uv_corrected = z * (w0*uv0/z0 + w1*uv1/z1 + w2*uv2/z2) 项目结构 # sRenderer/ ├── inc/ # 头文件 │ ├── geometry.hpp # Vec\u0026lt;T,DIM\u0026gt;、Matrix44f、数学运算符 │ ├── model.hpp # OBJ 模型加载器（顶点、纹理、法线） │ ├── renderer.hpp # 主 Renderer 类（光栅化循环） │ ├── shader.hpp # IShader 接口 + SimpleShader 实现 │ ├── tgaImage.hpp # TGA 图像加载器 + TGAColor + TGAImage │ └── SFML/ # SFML 头文件（vendored） ├── src/ # 源文件 │ ├── main.cpp # 入口点、模型选择、渲染循环 │ ├── geometry.cpp # 矩阵实现、lookAt、直线数学 │ ├── model.cpp # OBJ 文件解析器 │ ├── renderer.cpp # Renderer::render() —— 完整光栅化管线 │ ├── shader.cpp # 增量式直线绘制 │ └── tgaImage.cpp # TGA 读写（含 RLE 支持） ├── obj/ # Wavefront 模型文件 │ ├── african_head.obj │ ├── shark.obj │ ├── tree.obj │ ├── blender_cube.obj │ ├── cube.obj │ ├── frustum.obj │ └── teapot.obj ├── texture/ # TGA 纹理贴图 │ ├── african_head_diffuse.tga │ ├── shark_diffuse.tga │ ├── tree_diffuse.tga │ └── blender_cube_diffuse.tga ├── doc/assets/ # 演示 GIF │ ├── head.gif │ ├── shark.gif │ └── tree.gif ├── CMakeLists.txt # CMake 构建配置 ├── Makefile # 使用 make 的备选构建方式 ├── compile_flags.txt # 用于 clangd 的编译选项 ├── README.md # 本文件（英文版） └── README_zh.md # 中文版 编译与运行 # 环境要求 # Linux 或 macOS（Windows 可能可以工作但未经测试） g++ 或 clang++，需支持 C++11 cmake（版本 3.11 或更高） 编译步骤 # # 1. 克隆仓库（或进入项目目录） cd sRenderer # 2. 配置构建（CMake 会自动获取 SFML 3.0.0） cmake -B build # 3. 编译 cmake --build build # 4. 运行程序 ./build/sRenderer 使用说明 # 启动后，程序会提示选择一个模型：\nChoose a model below: 0. head 1. shark 2. tree Your choice [0-2]: 选择后，模型将以自动旋转的相机轨道进行渲染。\n","date":"3 April 2023","externalUrl":null,"permalink":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6/3d%E8%BD%AF%E6%B8%B2%E6%9F%93%E7%A8%8B%E5%BA%8Fsrenderer/","section":"计算机图形学","summary":"","title":"3D 软渲染程序 sRenderer","type":"计算机图形学"},{"content":"KMP 匹配算法用于快速从字符串中查找某子串。其由 Knuth、Morris、Pratt 三人于 1977 年联合发表（1974 年 Knuth 和 Pratt 共同构思出，同一时间 Morris 也独立设计出该算法，最终三人于 1977 年联合发表），因此被称为 KMP 算法。\n朴素匹配算法及其问题 # 在讲解 KMP 算法之前，我们先看看朴素匹配算法。所谓朴素匹配算法，就是在没有 KMP 算法的情况下，大多数人在第一时间会想到的最直观计算方法。KMP 算法也就是在朴素匹配法的基础上，所进行的改进。\n朴素匹配算法图示\n如上图所示为朴素匹配算法的匹配步骤。图中黄色的为主串「abcdabcdef」，粉色的为子串「abcde」，我们可以把主串和子串看作是两把尺子，而匹配过程就是固定主串不动，逐步移动子串，然后比较它们的重叠部分，如果重叠部分对应字符全部相等的话，则匹配成功，否则的话子串继续往后移一位，然后重新比较重叠部分。\n我们分步来描述一下运算步骤：\n子串处于位置 0，我们欣喜地发现前 4 个字符都相等，但是等我们比较到第 5 个字符的时候，遗憾发现这一位并不相等，匹配失败。 子串移动到位置 1，首字符即不相等。 子串移动到位置 2，首字符即不相等。 子串移动到位置 3，首字符即不相等。 子串移动到位置 4，发现重叠部分所有字符都相等，匹配成功！ 以上就是朴素匹配算法，如它的名字指出的那样，它很朴素很直观，且有效，但这也往往意味着其在效率上会存在存在很大的问题。\n在步骤 1 中，我们已经比较发现主串和子串的前 4 位是相等的，而第 5 位不相等。在之后的步骤中，我们不断地进行移位和比较。大家有没有想过，我们已经将主串的前 5 位和子串的前 5 位逐一比较过了，我们是不是可以更好地来利用这个比较结果，达到减少步骤的目的呢。\n具体来说，我们看步骤 2，此时子串移动到位置 1，然后我们将子串的第 1 位和主串的第 2 位进行比较，从步骤 1 的比较中我们已知主串的第 2 位和子串的第 2 位是相等的，因而步骤 2 中的操作也就等同于将子串的第 1 位和子串的第 2 位进行比较。同理，步骤 3 是将子串的 1、3 位相比较，步骤 4 是将子串的 1、4 位相比较。假如说，我们已经预先已经知道了子串「abcde」中就没有重复字符，那么像步骤 2 到步骤 4 的比较就不用做了，我们直接把子串移动到位置 4，进行后面的比较即可。\n这样的话，步骤就直接简化到如下图所示的两步。\n朴素匹配算法优化后的步骤\n当然，以上举的是一个极端情况，子串「abcde」中没有重复字符，那么如果子串中有重复字符又怎么说呢？我们看如下图的例子：\n另一种情况下的优化\n子串为「ababe」，是有重复字符的，显然，第2 步中我们不能直接把子串移动到位置 4。因为在子串的前 4 位字符中，前两位和末两位是相等的，我们只能将子串移动到位置 2，将子串的前两位和主串的第 3、4 位对齐（此时我们已经知道它们是相等的了），然后来比较主串的第 5 位和子串的第 3 位，希冀于后面的几位都能匹配成功。\n通过以上对朴素匹配算法问题的分析，对于如何解决其问题，我们可以总结出：\n比较过程中，主串的游标没必要往回走，比如过现在主串的前 4 位都已经跟子串比较过了，那就没必要再将这前面 4 位再去跟子串比较了。主串的游标只往右走，不往回走。 既然确定主串的游标只要往右移动就行了，剩下来要确认的就是子串的游标问题。我们可以看到，当子串已比较字符中没有重复字符时，我们直接将子串的头部移动到当前主串游标所在位置，又从子串的第一位开始比较；而当子串已比较字符的头尾存在重复的话，我们则没必要回到第一位，而是回到重复字符的下一位继续比较。这个子串游标应该回到第几位的问题，我们称之为回溯。研究清了回溯问题，KMP 算法也就自然出来了。 next 数组 # 我们已经了解到，KMP 的关键在于子串的回溯，确切来说是根据已比较的那部分子串来回溯。具体回溯到哪一位，跟主串并没有关系，它只取决于已比较的那部分子串的首位字符重复度。因此，对于给定的一个子串，比如「abcabcef」，我们关心的是：如果子串的前 3 位「abc」已经匹配到跟主串相等了，匹配第 4 位的时候没相等，这个时候我的子串应该回溯到哪一位。如果已经匹配了 4 位、5 位呢？显然，我们应该为此构建一个数组。\n由于这个数组解决的是下一步比较从哪一位开始的问题，因此我们把它命名为 next 数组。\n上图演示了对字符串「ababae」求 next 数组的过程。描述如下。\nnext[0]\na 前面没有字符，因而 next[0] = 0\nnext[1]\nb 前面只有一个字符 a，因而 next[1] = 0 。注意，咱们计算 next[i]，是根据第 i 位之前的字符来计算的，不包括第 i 位本身，即图中蓝色背景的部分。\nnext[2]\nb 前面有两个字符「ab」，没有相等的，因而 next[2] = 0\nnext[3]\nb 前面有 3 个字符「aba」，其中首部的 a 和尾部的 a 是相等的，因而 next[3] = 1\nnext[4]\n「abab」中，首部的 ab 和尾部的 ab 重合，因而 next[4] = 2\nnext[5]\n「ababa」中，首部的 aba 和尾部的 aba 重合，因而 next[5] = 3\nnext[6]\n「ababab」中，首部的 abab 和尾部的 abab 重合，因而 next[6] = 4\nnext[7]\n「abababe」首尾没有重合，因而 next[7] = 0\n通过以上的步骤讲解，大家对于 next 数组的概念，即字符串首位重合度，已经理解了。但是如何如何通过程序来计算呢？其实 next 数组的计算过程，也是字符串和字符串的比较过程，只不过比较的双方是自己和自己。合着绕了一圈，还是回到两个字符串的比较上来了，不过不要急， 「两个相同的串比较」和「两个陌生的串比较」毕竟是不一样的，耐心把这个问题解决了，咱们的 KMP 算法就出来了。\n如上图右侧所示，我们把每次步骤中两个串（蓝串和粉串）的位置关系画了出来。整个匹配过程仍然是蓝串不动，粉串往右移。由于我们要比较的是首和尾，至少要达到两个字符，因而前两步可以跳过，其 next 值必然等于零。达到两个字符的长度后，我们把粉串的头放在蓝串的尾，发现这两个字符不相等，因而 next 值为 0。之后粉串和蓝串的长度都加一，同时我们把粉串继续往后移一位，发现首尾字符相等，next 值为 1。两个串的长度再加一，此时我们没必要移动粉串，因为上一步的两个字符已经匹配上了，我们继续匹配下一个字符，果然这个字符也匹配上了，next 的值又比之前加一，变成了 2。之后两步由于一直匹配成功，所以我们继续保持粉串不动。知道最后一步，上一步的匹配失败了，这个时候我们 next 值继续增长的美梦破灭了，我们只得将粉串往右移。但是应该往后移几位呢？我们自然是希望少移一点，多匹配几位，但是如果一位一位移的话，也未免效率太低了。这个时候智慧就体现出来了。\n我聚焦到这一步来看：\n已经有四位匹配成功了，如红框中所示。此时我们要将粉串往右移动，面对的问题是该移多少位。朋友们，有没有点似曾相识的感觉了？这又是一个 KMP 啊，这是一个递归有没有！关键是，红框中串的 next 数组我们已经有了啊！还等什么，赶紧拿出来用。我们取出 next[4] = 2，说明我们应该再往后移 2 位。同时蓝串和子串长度加一，就是如下所示了：\n若比较仍是不相等，则按照刚刚的方法继续递归，直到匹配成功或 next 的值为零。\n整个过程其实很简单，只有最后递归这一步是有点绕的。\n无论怎样，我们已经得到了 next 数组，离成功不远了。喘口气，把这部分的代码实现了。\n//------ 计算字符串的回溯下标数组 -------- // str: 要计算的字符串 // next：用于存储回溯下标的数组 //---------------------------------------- void getNextArr(char * str, int *next) { int length = strlen(str); int index1 = 1; // 固定串的游标 int index2 = 0; // 移动串的游标 next[0] = 0; while (index1 \u0026lt; length) { if (index2 == 0 || str[index1] == str[index2]) { next[index1] = index2; ++index1; ++index2; } else { index2 = next[index2]; } } } KMP 算法的实现 # 其实到这里，KMP 算法的核心我们已经掌握了。\n我们已经得到了 next 数组，因而接下来的代码实现也变得非常简单。实现思路也就如咱们上面所说的，固定主串不动，移动子串去比较。在这个过程中，主串的游标只增大不减小，而子串的游标则会在匹配失败时减小，至于减少到多少，取决于 next 数组。\n好，下面我们便贴上代码。\n//---------------------------- // KMP 字符匹配算法 // author: huangqiang // date: 2020-09-13 //---------------------------- #include\u0026lt;stdio.h\u0026gt; #include\u0026lt;string.h\u0026gt; #include\u0026lt;stdbool.h\u0026gt; //------ 计算字符串的回溯下标数组 -------- // str: 要计算的字符串 // next：用于存储回溯下标的数组 //---------------------------------------- void getNextArr(char * str, int *next) { int length = strlen(str); int index1 = 1; // 固定串的比较下标 int index2 = 0; // 移动串的比较下标 next[0] = 0; while (index1 \u0026lt; length) { if (index2 == 0 || str[index1] == str[index2]) { next[index1] = index2; ++index1; ++index2; } else { index2 = next[index2]; } } } void main() { // 主串 char *mainStr = \u0026#34;abaaabababebabf\u0026#34;; int mainStrLen = strlen(mainStr); // 子串 char *subStr = \u0026#34;abababe\u0026#34;; int subStrLen = strlen(subStr); // 回溯下标数组 int next[subStrLen]; // 调用 getNextArr 方法计算回溯下标数组 getNextArr(subStr, next); // matched 变量用于存储匹配结果 bool matched = false; int mainIndex = 0, subIndex = 0; // 配对生成了，退出; 到主串尾了，退出。 while(!matched \u0026amp;\u0026amp; mainIndex \u0026lt; mainStrLen) { // 对当前字符进行匹配 if (mainStr[mainIndex] == subStr[subIndex]) { // 子串游标到尾了，说明匹配成功了 if (subIndex \u0026gt;= subStrLen-1) { matched = true; } else { subIndex++; mainIndex++; } } else { if(subIndex \u0026gt; 0) { // 字串回溯的时候，主串的游标保持不动 subIndex = next[subIndex]; } else { subIndex = 0; mainIndex++; } } } printf(\u0026#34;主串: %s\\n子串: %s\\n\u0026#34;, mainStr, subStr); if (matched) { printf(\u0026#34;匹配成功，起始下标: %d\\n\u0026#34;, mainIndex-subStrLen+1); } else { printf(\u0026#34;匹配失败\\n\u0026#34;); } return; } KMP 算法的改进 # 上面的 KMP 算法其实还是有改进空间的。我们来看下面一种情况。\n在第 1 步中，已经匹配了主子串的前 4 位，匹配第 5 位时发现不相等。此时主串和子串的游标（数组下标）均等于 4。\n这个时候我们要回溯了，回溯到多少呢？next[4] = 3，即图中步骤 2 的位置，然后继续回溯 next[3] = 2，next[2] = 1，next[1] = 0。然而你看前几位都是 a，咱们忙活了半天其实就是反复地在将 a 和 b 进行比较，这显然是多余的。\n如何改进呢？方法也很直接。在我们计算 next 数组的时候，若发现回溯到的那个位置的字母和当前的字母是一样的，那意味着比较结果仍将是不相等，继续往前回溯吧。根据这个规则，我们重新来改写 next 数组。\n在此我们只贴上修改后的 getNextArr 函数，代码的其他部分不变。\n//------ 计算字符串的回溯下标数组 -------- // str: 要计算的字符串 // next：用于存储回溯下标的数组 //---------------------------------------- void getNextArr(char * str, int *next) { int length = strlen(str); int index1 = 1; // 固定串的比较下标 int index2 = 0; // 移动串的比较下标 next[0] = 0; while (index1 \u0026lt; length) { if (index2 == 0 || str[index1] == str[index2] \u0026amp;\u0026amp; str[next[index2] != str[index2]] ) { next[index1] = index2; ++index1; ++index2; } else { index2 = next[index2]; } } } ","date":"3 November 2022","externalUrl":null,"permalink":"/%E7%AE%97%E6%B3%95/kmp%E5%8C%B9%E9%85%8D%E7%AE%97%E6%B3%95%E5%85%A8%E8%A7%A3/","section":"算法","summary":"","title":"KMP 匹配算法全解","type":"算法"},{"content":"本教程使用 TensorFlow 搭建一个深度卷积生成对抗网络（DCGAN），在 CelebA 人脸数据集上训练，实现从随机噪声生成人脸图像。\n1. 环境准备 # 导入所需的库：\nimport tensorflow as tf import glob import imageio import matplotlib.pyplot as plt import numpy as np import os import PIL from tensorflow.keras import layers import time from IPython import display import pathlib import random import seaborn as sns import gdown sns.set() 2. 数据集下载 # 从 Google Drive 下载 CelebA 人脸数据集（约 20 万张人脸图片）。\n!test ! -d /content/GAN_face \u0026amp;\u0026amp; mkdir /content/GAN_face %cd /content/GAN_face /content/GAN_face url = \u0026#34;https://drive.google.com/uc?id=1O7m1010EJjLE5QxLZiM9Fpjs7Oj6e684\u0026#34; gdown.download(url, \u0026#34;dataset.zip\u0026#34;, quiet=True) \u0026#39;dataset.zip\u0026#39; 解压数据集：\n!test ! -d dataset \u0026amp;\u0026amp; mkdir dataset !unzip -q -d dataset dataset.zip dataset_path = pathlib.Path.cwd()/\u0026#39;dataset/img_align_celeba\u0026#39; 查看前 10 张图片的文件名和原始尺寸：\n!ls dataset/img_align_celeba/ | head -n 10 img = tf.io.read_file(\u0026#39;dataset/img_align_celeba/000001.jpg\u0026#39;) img = tf.image.decode_image(img) print(img.shape) plt.figure() plt.imshow(img) plt.axis(\u0026#39;off\u0026#39;) 000001.jpg 000002.jpg 000003.jpg 000004.jpg 000005.jpg 000006.jpg 000007.jpg 000008.jpg 000009.jpg 000010.jpg (218, 178, 3) (-0.5, 177.5, 217.5, -0.5) 数据集总量：\nlen(list(pathlib.Path.cwd().glob(\u0026#39;dataset/*/*.jpg\u0026#39;))) 202599 共约 202,599 张人脸图片，原始尺寸为 218×178 像素。\n3. 数据清洗 # 检查并移除损坏图片：\nfrom pathlib import Path from tensorflow.io import read_file from tensorflow.image import decode_image, decode_jpeg def check(data_dir, channels): cnt = 0 for image in sorted(data_dir.glob(\u0026#39;*\u0026#39;)): cnt += 1 try: img = read_file(str(image)) img = decode_jpeg(img) if img.ndim != 3: raise Exception(\u0026#39;invalid\u0026#39;) if img.shape[-1] != channels: raise Exception(\u0026#39;invalid\u0026#39;) except Exception as e: if str(e) == \u0026#39;invalid\u0026#39;: print(\u0026#39;removed bad file: \u0026#39;, str(image)) image.unlink() print(\u0026#39;checked {} files\u0026#39;.format(cnt)) removed bad file: ... checked 4117 files 4. 数据预处理与加载 # 将图片统一缩放到 56×56 像素，归一化到 [0,1] 区间。\n4.1 参数设置 # BUFFER_SIZE = 6000 BATCH_SIZE = 256 IMG_WIDTH = 56 IMG_HEIGHT = 56 4.2 图片加载函数 # def get_image(path, width, height, channels): img = tf.io.read_file(str(path)) img = tf.io.decode_jpeg(img, channels) img = tf.image.resize(img, [width, height], method=tf.image.ResizeMethod.BILINEAR) img = img / 255.0 return img 4.3 自定义数据提供器 # 使用 generator 逐张读取图片，训练时随机打乱顺序：\nclass ImageProvider: def __init__(self, path, width, height, channels, format, training=False): self.path = path self.format = format self.training = training self.width = width self.height = height self.channels = channels def __call__(self): files = sorted(list(self.path.glob(f\u0026#39;*.{self.format}\u0026#39;))) if self.training: random.shuffle(files) for f in files: yield get_image(f, self.width, self.height, self.channels) 4.4 构建 tf.data 流水线 # 利用 cache、shuffle、prefetch、batch 优化数据加载性能：\ntrain_ds_ = tf.data.Dataset.from_generator( ImageProvider(dataset_path, IMG_WIDTH, IMG_HEIGHT, 3, \u0026#39;jpg\u0026#39;, training=True), output_signature=(tf.TensorSpec(shape=(IMG_WIDTH, IMG_HEIGHT, 3), dtype=tf.float32)) ) AUTOTUNE = tf.data.AUTOTUNE train_ds = train_ds_.cache().shuffle(BUFFER_SIZE).prefetch(buffer_size=AUTOTUNE).batch(BATCH_SIZE) 验证数据形状：\nfor images in train_ds.take(1): print(images.shape) plt.figure() plt.imshow(images[0]) plt.axis(\u0026#39;off\u0026#39;) (256, 56, 56, 3) 每个 batch 包含 256 张 56×56×3 的图片。\n5. 构建生成器（Generator） # 生成器的作用是接收随机噪声向量，通过转置卷积（反卷积）逐步上采样，最终输出一张 56×56×3 的图片。\n5.1 噪声维度设定 # noise_dim = 100 使用 100 维的随机正态分布向量作为输入。\n5.2 生成器网络结构 # def make_generator_model(): model = tf.keras.Sequential() model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(noise_dim, ))) model.add(layers.BatchNormalization()) model.add(layers.LeakyReLU()) model.add(layers.Reshape((7, 7, 256))) model.add(layers.Conv2DTranspose(128, kernel_size=5, strides=1, padding=\u0026#39;same\u0026#39;, use_bias=False)) model.add(layers.BatchNormalization()) model.add(layers.LeakyReLU()) model.add(layers.Conv2DTranspose(64, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;, use_bias=False)) model.add(layers.BatchNormalization()) model.add(layers.LeakyReLU()) model.add(layers.Conv2DTranspose(128, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;, use_bias=False)) model.add(layers.BatchNormalization()) model.add(layers.LeakyReLU()) model.add(layers.Conv2DTranspose(3, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;, use_bias=False, activation=\u0026#39;tanh\u0026#39;)) return model 5.3 备选生成器结构 # 另一个更简洁的生成器变体，使用更小的卷积核（3×3）：\ndef make_generator_model_2(): model = tf.keras.Sequential() model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(noise_dim, ), activation=\u0026#39;relu\u0026#39;)) model.add(layers.Reshape((7, 7, 256))) model.add(layers.Conv2DTranspose(256, kernel_size=3, strides=2, padding=\u0026#39;same\u0026#39;)) model.add(layers.BatchNormalization(momentum=0.8)) model.add(layers.ReLU()) model.add(layers.Conv2DTranspose(256, kernel_size=3, strides=2, padding=\u0026#39;same\u0026#39;)) model.add(layers.BatchNormalization(momentum=0.8)) model.add(layers.ReLU()) model.add(layers.Conv2DTranspose(3, kernel_size=3, strides=2, padding=\u0026#39;same\u0026#39;, activation=\u0026#39;tanh\u0026#39;)) return model 5.4 测试生成器 # 用未训练的生成器测试一次前向传播，验证输出形状：\nnoise = tf.random.normal([1, noise_dim]) generator = make_generator_model() generated_image = generator(noise, training=False) print(generated_image[0].shape) plt.imshow(generated_image[0, :, :, 0]) plt.axis(\u0026#39;off\u0026#39;) (56, 56, 3) 输出 56×56×3 的图片，与预处理后的真实图片尺寸一致。\n6. 构建判别器（Discriminator） # 判别器是一个二分类卷积网络，接收 56×56×3 的图片，输出一个标量 logit，表示图片为真实图片的置信度。\ndef make_discriminator_model(): model = tf.keras.Sequential() model.add(layers.Input([IMG_WIDTH, IMG_HEIGHT, 3])) model.add(layers.Conv2D(64, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;)) model.add(layers.LeakyReLU()) model.add(layers.Dropout(0.3)) model.add(layers.Conv2D(128, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;)) model.add(layers.LeakyReLU()) model.add(layers.Dropout(0.3)) model.add(layers.Conv2D(256, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;)) model.add(layers.LeakyReLU()) model.add(layers.Dropout(0.3)) model.add(layers.Conv2D(128, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;)) model.add(layers.LeakyReLU()) model.add(layers.Dropout(0.3)) model.add(layers.Conv2D(64, kernel_size=5, strides=2, padding=\u0026#39;same\u0026#39;)) model.add(layers.LeakyReLU()) model.add(layers.Dropout(0.3)) model.add(layers.Flatten()) model.add(layers.Dense(1)) return model 测试未训练的判别器对生成图片的判断：\ndiscriminator = make_discriminator_model() decision = discriminator(generated_image) print(decision) tf.Tensor([[-0.00022056]], shape=(1, 1), dtype=float32) 由于模型未经训练，输出接近于 0（即对真假判断不明确）。\n7. 损失函数与优化器 # 7.1 损失函数 # 使用 BinaryCrossentropy(from_logits=True)，将判别器的输出直接作为 logits 计算损失：\n判别器损失： real_loss：真实图片被误判为假的损失（标签=1） fake_loss：生成图片被误判为真的损失（标签=0） 总损失 = real_loss + fake_loss 生成器损失：生成图片被判别器判为真的能力（标签=1） cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True) def discriminator_loss(real_output, fake_output): real_loss = cross_entropy(tf.ones_like(real_output), real_output) fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output) total_loss = real_loss + fake_loss return total_loss def generator_loss(fake_output): return cross_entropy(tf.ones_like(fake_output), fake_output) 7.2 优化器 # 生成器和判别器均使用 Adam 优化器，学习率 1e-4：\ngenerator_optimizer = tf.keras.optimizers.Adam(1e-4) discriminator_optimizer = tf.keras.optimizers.Adam(1e-4) 7.3 检查点 # 设置模型保存路径，每轮训练后可恢复：\ncheckpoint_dir = \u0026#39;./training_checkpoints\u0026#39; checkpoint_prefix = os.path.join(checkpoint_dir, \u0026#39;ckpt\u0026#39;) checkpoint = tf.train.Checkpoint( generator_optimizer=generator_optimizer, discriminator_optimizer=discriminator_optimizer, generator=generator, discriminator=discriminator ) 8. 训练循环 # 8.1 参数设置 # 训练 500 个 epoch，每次生成 16 张样例图片：\nEPOCHS = 500 num_examples_to_generate = 16 seed = tf.random.normal([num_examples_to_generate, noise_dim]) 8.2 单步训练 # 每个 batch 的步骤如下：\n采样随机噪声 → 生成器生成假图 真实图片和假图分别送入判别器 计算生成器损失和判别器损失 反向传播更新两个网络的参数 @tf.function def train_step(images): noise = tf.random.normal([BATCH_SIZE, noise_dim]) with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape: generated_images = generator(noise, training=True) real_output = discriminator(images, training=True) fake_output = discriminator(generated_images, training=True) gen_loss = generator_loss(fake_output) disc_loss = discriminator_loss(real_output, fake_output) gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables) gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables) generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables)) discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables)) 8.3 图片保存函数 # 每个 epoch 结束后生成并保存当前生成器的输出图片：\n!test ! -d generated_images \u0026amp;\u0026amp; mkdir generated_images def generate_and_save_images(model, epoch, test_input): predictions = model(test_input, training=False) fig = plt.figure() generated_image = predictions * 255 generated_image = generated_image.numpy() plt.imshow(generated_image[0].astype(\u0026#34;uint8\u0026#34;)) plt.axis(\u0026#39;off\u0026#39;) plt.savefig(\u0026#39;./generated_images/image_at_epoch_{:04d}.png\u0026#39;.format(epoch)) plt.show() 8.4 完整训练流程 # def train(dataset, epochs): for epoch in range(epochs): print(\u0026#39;epoch: \u0026#39;, epoch) start = time.time() batch_idx = 0 for image_batch in dataset: train_step(image_batch) batch_idx += 1 display.clear_output(wait=True) generate_and_save_images(generator, epoch+1, seed) print(f\u0026#39;Time for epoch {epoch+1} is {time.time()-start} sec\u0026#39;) checkpoint.save(file_prefix=checkpoint_prefix) display.clear_output(wait=True) generate_and_save_images(generator, epochs, seed) 启动训练：\ntrain(train_ds, EPOCHS) 8.5 检查点文件 # 训练完成后，模型权重保存在检查点文件中：\ntotal 53M -rw-r--r-- 1 root root 69 Aug 27 01:41 checkpoint -rw-r--r-- 1 root root 53M Aug 27 01:41 ckpt-1.data-00000-of-00001 -rw-r--r-- 1 root root 5.6K Aug 27 01:41 ckpt-1.index 检查点文件约 53MB，包含生成器和判别器的全部权重。\n9. 最终生成 # 训练完成后，使用生成器批量生成 18 张人脸图片：\n!test ! -d final_generated \u0026amp;\u0026amp; mkdir final_generated num_img = 18 def Potrait_Generator(): Generated_Paintings = [] seed = tf.random.normal([num_img, noise_dim]) generated_image = generator(seed) generated_image *= 255 generated_image = generated_image.numpy() for i in range(num_img): img = tf.keras.preprocessing.image.array_to_img(generated_image[i]) Generated_Paintings.append(img) img.save(\u0026#34;./final_generated/image{:02d}.png\u0026#34;.format(i)) return def Show_Img(data): plt.figure(figsize=(15, 15)) for images in data.take(1): for i in range(18): ax = plt.subplot(6, 6, i + 1) ax.imshow(images[i].numpy().astype(\u0026#34;uint8\u0026#34;)) ax.axis(\u0026#34;off\u0026#34;) Images = Potrait_Generator() 加载并显示生成的 18 张人脸：\nGenerated_path = \u0026#34;./final_generated/\u0026#34; Potraits_generated = tf.keras.preprocessing.image_dataset_from_directory(Generated_path, label_mode=None) Show_Img(Potraits_generated) Found 18 files belonging to 1 classes. 10. 总结 # 项目 说明 模型类型 DCGAN（深度卷积生成对抗网络） 数据集 CelebA — 约 20 万张人脸图片 输入 100 维随机正态噪声 输出尺寸 56×56×3（RGB 人脸） 生成器结构 Dense + Conv2DTranspose + BatchNorm + LeakyReLU 判别器结构 Conv2D + LeakyReLU + Dropout 堆叠，最终输出单标量 损失函数 BinaryCrossentropy (from_logits=True) 优化器 Adam (lr=1e-4) 训练轮数 500 epochs Batch size 256 总参数量（检查点） 53MB 最终生成数量 18 张 本教程完整实现了从数据下载、预处理、模型构建到训练和生成的全流程。你可以通过调整网络结构、超参数或训练更长时间来进一步提升生成质量。\n","date":"22 March 2025","externalUrl":null,"permalink":"/ai/gan_%E9%9D%A2%E9%83%A8%E7%94%9F%E6%88%90/","section":"AI 相关内容","summary":"","title":"GAN 实践-人脸图像生成","type":"ai"},{"content":" Quack — 一款用 C++ 编写的轻量级 3D 游戏引擎，致敬 id Software 的传奇引擎 Quake 项目地址： https://github.com/Oh-noodles/Quack\n项目简介 # Quack 是一款基于 OpenGL 3.3 Core Profile 的 C++ 游戏引擎，其命名和精神内核致敬了 id Software 由约翰·卡马克（John Carmack）主导开发的革命性 3D 引擎 Quake。如同 Quake 在 1996 年将 3D 游戏带入全新时代，Quack 旨在以精简而现代的方式，探索实时渲染、物理碰撞、骨骼动画等游戏引擎核心技术。\n本引擎并非对 Quake 的复刻，而是受其启发的独立实现，展现了现代 OpenGL 图形管线与经典游戏引擎架构的融合。\n技术栈 # 类别 技术 说明 编程语言 C++14 核心开发语言，使用 g++（Linux）或 clang++（macOS）编译 图形 API OpenGL 3.3 Core Profile 跨平台 3D 图形渲染接口 窗口与输入 GLFW 3.3 跨平台窗口管理、键盘、鼠标输入处理 OpenGL 加载 GLAD 轻量级 OpenGL 函数加载器 数学库 GLM 0.9.9 OpenGL Mathematics，提供向量、矩阵、四元数等数学运算 模型加载 Assimp 5.x 支持多种 3D 模型格式（.obj, .gltf, .glb, .dae 等） 图片加载 STB Image 轻量级单头文件图片库，加载纹理资源 构建系统 GNU Make 跨平台 Makefile，支持 Linux 和 macOS 核心特性 # 渲染系统 # Blinn-Phong 着色器：实现了环境光（Ambient）、漫反射（Diffuse）和镜面反射（Specular）三阶段光照计算，支持法线贴图和纹理映射 多光源支持：单个着色器可同时处理 4 个点光源、1 个方向光和 1 个聚光灯 纹理系统：支持漫反射贴图、镜面反射贴图、法线贴图、粗糙度贴图等 PBR 材质属性 着色器管理：自研 Shader 类，支持从文件加载顶点/片段着色器，提供类型安全的 uniform 接口 场景管理 # 多场景架构：Engine 单例管理多个 Scene 实例，支持场景切换 游戏对象树：每个场景维护 GameObject 映射表，游戏对象通过唯一 ID 管理 生命周期回调：每个对象可注册 renderFrameCallback，在每一帧执行更新逻辑 动态对象创建/销毁：支持运行时添加和标记销毁游戏对象 光照系统 # Light (基类) ├── DirectionalLight — 方向光，模拟太阳光等无限远光源 ├── PointLight — 点光源，带距离衰减（constant / linear / quadratic） └── SpotLight — 聚光灯，带内外锥角 cutoff / outerCutOff 光照计算采用 Blinn-Phong 模型，结合 attenuation 实现物理正确的距离衰减。\n碰撞检测 # 引擎实现了自定义 包围盒层次树（Bounding Volume Hierarchy, BVH） 碰撞检测系统：\nNode：表示一个 AABB（轴对齐包围盒）节点，支持叶节点和内部节点 DTree：基于红黑树思想的 BVH 数据结构，支持高效的空间查询 动态维护：支持叶节点的插入、删除和位置更新，父节点自动重新适配（refit） 碰撞查询：getCollidedNodes() 方法返回所有与给定碰撞体相交的节点 可视化调试：支持将 BVH 树结构导出为 SVG 图像（front / top / left 三视图） 游戏对象与实体 # GameObject (基类) ├── Tank::Tank — 坦克基类 │ ├── Player — 玩家控制的坦克（绑定相机） │ └── Enemy — AI 敌方坦克（自动行驶） └── Cannon::Cannon — 发射的炮弹（带重力抛物线轨迹） 变换系统：每个游戏对象拥有 position、rotation、scaling 三元组，支持独立的位移、旋转、缩放 碰撞体组件：Collider 以偏移量方式挂载到 GameObject，动态计算 AABB 可扩展设计：通过继承 GameObject 和覆写 renderFrameCallback 创建自定义实体 骨骼动画 # 引擎支持完整的骨骼动画系统：\nAssimp 集成：从模型文件中提取骨骼层级、动画通道（位置/旋转/缩放关键帧） 骨骼矩阵：每帧递归计算骨骼的全局变换矩阵，传递到着色器 顶点蒙皮：每个顶点支持最多 4 骨骼加权（MX_NR_BONE_INFLUENCE） 支持格式：.gltf / .glb / .dae（带动画的模型） 相机系统 # Euler 角控制：支持 Yaw / Pitch 旋转，Pitch 限幅防止翻转 多种移动方式：键盘 WASD + Space（上）+ Shift（下）六自由度移动 鼠标视角：鼠标控制相机朝向，支持 FOV 缩放 坐标系：右手坐标系，front / right / up 向量自动更新 架构设计 # ┌─────────────────────────────────────────────────┐ │ Engine (单例) │ │ ┌───────────┐ ┌───────────────┐ ┌─────────┐ │ │ │ Window │ │ Shader 管理 │ │ 场景树 │ │ │ └───────────┘ └───────────────┘ └────┬────┘ │ └─────────────────────────────────────────┼───────┘ │ ┌─────────────────────▼─────────────────────┐ │ Scene │ │ ┌─────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Camera │ │ Lights │ │ GameObjects│ │ │ └─────────┘ └──────────┘ └─────┬────┘ │ └──────────────────────────────────┼─────────┘ │ ┌────────────────────────▼──────────────┐ │ GameObject │ │ ┌──────────┐ ┌────────────────────┐ │ │ │ Model │ │ Collider (BVH) │ │ │ └──────────┘ └────────────────────┘ │ └─────────────────────────────────────────┘ 核心数据流：\n每一帧，Engine::run() 驱动主循环 计算 deltaTime 并调用全局输入处理 清空帧缓冲，调用 renderObjects() 遍历活动场景 对每个 GameObject 调用 renderFrameCallback(deltaTime) 更新逻辑 按模型矩阵渲染，应用光照 uniform 触发全局 renderFrameCallback（用于碰撞检测等游戏逻辑） 交换缓冲区、轮询事件 目录结构 # Quack/ ├── include/ # 头文件 │ ├── engine/ # 引擎核心 │ │ ├── engine.hpp # 引擎单例，主循环驱动 │ │ ├── scene.hpp # 场景管理，光照定义 │ │ ├── gameObject.hpp # 游戏对象基类 │ │ ├── bvh.hpp # BVH 碰撞检测数据结构 │ │ ├── collider.hpp # 碰撞体组件 │ │ └── utils.hpp # 工具函数（UUID 生成） │ ├── game/ # 游戏层 │ │ ├── player.hpp # 玩家坦克 │ │ ├── enemy.hpp # AI 敌方坦克 │ │ ├── tank.hpp # 坦克基类 │ │ └── cannon.hpp # 炮弹 │ ├── learn/ # LearnOpenGL 教程框架 │ │ ├── camera.hpp # 相机类 │ │ ├── model.hpp # 模型加载器 │ │ ├── mesh.hpp # 网格封装 │ │ ├── shader_s.hpp # 着色器管理 │ │ ├── shader.hpp # 基础着色器 │ │ ├── animation.hpp # 动画系统 │ │ ├── animator.hpp # 动画控制器 │ │ ├── bone.hpp # 骨骼数据 │ │ ├── animdata.hpp # 动画数据结构 │ │ ├── MyAnimation.hpp # 自定义动画实现 │ │ ├── model_animation.hpp # 模型动画 │ │ ├── assimp_glm_helpers.hpp # Assimp-GLM 转换工具 │ │ └── stb_image.h # STB 图片加载 │ ├── glm/ # GLM 数学库 │ ├── assimp/ # Assimp 导入器 │ ├── GLFW/ # GLFW 声明 │ ├── KHR/ # OpenGL 平台头 │ └── glad/ # GLAD 加载器 ├── src/ # 源文件 │ ├── engine/ # 引擎实现 │ │ ├── engine.cpp │ │ ├── scene.cpp │ │ ├── gameObject.cpp │ │ ├── bvh.cpp │ │ ├── collider.cpp │ │ └── utils.cpp │ ├── game/ # 游戏实现 │ │ ├── main.cpp # 游戏入口 │ │ ├── player.cpp │ │ ├── enemy.cpp │ │ ├── tank.cpp │ │ └── cannon.cpp │ ├── phong_shading/ # Phong 着色演示 │ ├── model_loading/ # 模型加载演示 │ ├── skeletal_animation/ # 骨骼动画演示 │ ├── shader_showcase/ # 自定义着色器演示 │ ├── test/ # BVH 测试 │ ├── stb/ # STB 实现 │ └── glad/ # GLAD 实现 ├── resources/ # 资源资产 │ ├── objects/ # 3D 模型 │ │ ├── backpack/ # 背包模型 (.obj) │ │ ├── simple_tank/ # 坦克模型 (.gltf) │ │ ├── concept_tank/ # 概念坦克 (.glb) │ │ ├── pixel_tank/ # 像素风坦克 (.glb) │ │ ├── tank/ # 基础坦克 (.glb) │ │ ├── cannon/ # 炮弹模型 (.glb) │ │ ├── knight/ # 骑士模型 (.obj) │ │ ├── guardian/ # 守护者模型 (.obj) │ │ ├── Horse/ # 马模型 (.glb) │ │ ├── LittlestTokyo/ # 东京夜景 (.glb) │ │ ├── Television/ # 电视机 (.gltf) │ │ ├── shapes/ # 基础几何体 (.glb) │ │ ├── anim_zombie/ # 动画僵尸 (.gltf) │ │ ├── anim_male/ # 动画男性 (.gltf) │ │ ├── anim_fish/ # 动画鱼 (.gltf) │ │ ├── anim_car/ # 动画汽车 (.gltf) │ │ ├── vampire/ # 吸血鬼动画 (.dae) │ │ └── blender_cube/ # Blender 立方体 (.gltf) │ └── textures/ # 纹理贴图 ├── Makefile # 构建配置 ├── compile_flags.txt # 编译标志 └── README.md # 本文件 演示项目 # phong_shading # 基础着色演示，展示 Phong / Blinn-Phong 光照模型：\n10 个旋转立方体环绕点光源 环境光、漫反射、镜面反射三段着色 镜面高光指数 32 model_loading # 模型加载演示，完整展示 Assimp 模型加载管线：\n加载 backpack.obj 等复杂模型 多材质、多纹理支持 方向光 + 4 点光源 + 聚光灯复合光照 skeletal_animation # 骨骼动画演示：\n加载带动画的 .gltf 模型（僵尸、鱼、男性） 实时播放模型内置动画 骨骼矩阵传递到片段着色器进行顶点蒙皮 shader_showcase # 自定义着色器效果展示：\n实时水位/水波动画效果 基于时间的动态着色 支持高分辨率渲染（1920×1080） test # BVH 碰撞检测系统单元测试：\n创建 6 个顺序排列的 AABB 节点 测试插入、碰撞查询、删除、更新操作 导出三视图 SVG 可视化文件 game # 主游戏项目 — 坦克对战：\n玩家控制一辆坦克（WASD 移动，鼠标转向，Space 开炮） AI 敌方坦克自动行驶与碰撞避让 炮弹带重力抛物线轨迹 实时碰撞检测与坦克朝向判定 方向光 + 4 点光源 + 聚光灯完整光照 构建与运行 # 依赖项 # Linux:\nsudo apt-get install libglfw3-dev libassimp-dev macOS:\nbrew install glfw assimp 系统还需安装 OpenGL 3.3 兼容的显卡驱动。\n编译 # 项目使用 GNU Make 构建系统，Makefile 支持 Linux（g++）和 macOS（clang++）：\n# 编译主游戏 make # 编译后会生成 ./main 可执行文件 编译参数说明：\n编译器：g++（Linux）/ clang++（macOS） 标准：-std=c++14（Linux）/ -std=c++11（macOS，建议升级至 C++14 以获得最佳兼容性） 警告：-Wall 调试符号：-g 包含路径：-I include 链接库：-l glfw -l assimp -lm 编译其他演示项目：\n目前 Makefile 默认仅编译 src/game/main.cpp。如需编译其他演示，可修改 ENTRY 变量或手动编译：\n# 编译 Phong 着色演示 g++ -Wall -std=c++14 -g -I include src/glad/glad.c src/stb/stb_image.cpp src/phong_shading/main.cpp -l glfw -l assimp -o phong_demo # 编译模型加载演示 g++ -Wall -std=c++14 -g -I include src/glad/glad.c src/stb/stb_image.cpp src/model_loading/main.cpp -l glfw -l assimp -o model_demo # 编译骨骼动画演示 g++ -Wall -std=c++14 -g -I include src/glad/glad.c src/stb/stb_image.cpp src/skeletal_animation/main.cpp -l glfw -l assimp -o anim_demo # 编译着色器演示 g++ -Wall -std=c++14 -g -I include src/glad/glad.c src/stb/stb_image.cpp src/shader_showcase/main.cpp -l glfw -l assimp -o shader_demo # 编译 BVH 测试 g++ -Wall -std=c++14 -g -I include src/engine/bvh.cpp src/test/main.cpp -o bvh_test 运行 # # 运行主游戏 ./main # 运行其他演示 ./phong_demo ./model_demo ./anim_demo ./shader_demo ./bvh_test 注意：部分演示程序的工作目录需要定位到项目根目录下，以便正确加载资源文件。\n输入控制 # 操作 按键 前进 W 后退 S 左移 A 右移 D 向上 Space 向下 Left Shift 开火 Space（主游戏） 鼠标视角 鼠标移动 缩放 鼠标滚轮 退出 ESC 资源资产 # 项目 resources/objects/ 目录中包含多种格式的 3D 模型资产：\n资产 格式 说明 Backpack .obj 带漫反射/法线/高光贴图的背包 Simple Tank .gltf PBR 材质坦克模型 Concept Tank .glb 概念设计坦克 Pixel Tank .glb 像素风格坦克 Knight .obj 中世纪骑士模型 Guardian .obj 守护者模型 Horse .glb 低多边形马 LittlestTokyo .glb 东京夜景场景 Zombie / Male / Fish / Car .gltf 带动画的骨骼模型 Vampire .dae 带动画的吸血鬼 Cannon .glb 炮弹模型 部分资源采用 CC BY 等许可证发布，详细授权请参见各子目录下的 license.txt 文件。\n设计哲学 # 精简而不简陋：Quack 采用扁平化的 C++ 架构，没有过度设计的抽象层，每个组件职责清晰 教学友好：代码结构参考了 LearnOpenGL 教程的组织方式，适合学习 3D 图形编程 可扩展：通过 GameObject 继承体系和回调机制，可方便地添加新的游戏实体 跨平台：在 Linux 和 macOS 上均可编译运行 致敬经典：引擎命名 \u0026ldquo;Quack\u0026rdquo; 致敬 id Software 的 Quake 引擎，表达对传奇游戏引擎开发者约翰·卡马克的敬意 致谢 # id Software / John Carmack — Quake 引擎的开创者，无数游戏开发者的灵感源泉 LearnOpenGL — 本项目的图形编程教学基础框架源自 LearnOpenGL 教程 Assimp — 强大的跨平台 3D 模型导入库 GLFW — 简洁易用的跨平台窗口与输入库 GLM — 符合 OpenGL 标准的数学库 STB — 高效的单头文件图片加载库 ","date":"22 September 2024","externalUrl":null,"permalink":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6/3d%E6%B8%B8%E6%88%8F%E5%BC%95%E6%93%8Equack/","section":"计算机图形学","summary":"","title":"3D 游戏引擎 Quack","type":"计算机图形学"},{"content":"背包问题是一类动态规划问题，其发源自两个经典的题目：「0-1背包」和「完全背包」。题目大意是说有几样物品和一个背包，每一件物品都有其重量和价值，背包有最大容量。问背包最多能够装下多大价值的物品。0-1 背包与完全背包的区别在于，物品数量是一个还是无穷个。\n符合以上描述的问题，被称为纯背包问题。无论题干是把背包换成篮子，还是把物品换成钞票，若只是换了换场景，我们都可以套用固定的解法。\n无论谜面如何变换，背包问题特征依然显著。只要是从一个集合中挑选一些元素，达成某个要求，基本上就能断定这是一个背包问题。因此，leetcode 中不存在纯背包问题，除了谜面的一些障眼法，往往会在核心问题上进行变换。比如背包可能有两个，物品有不同种类，比如不问最大最小值，而问是否可行（True/False），又或是有多少种组合。\n要攻克这一大类型，还是从最基本的纯背包问题开始。\n0-1背包 # 💡 有 3 件物品，质量分别为 weight = [1, 3, 4]，价值分别为 value = [15, 20, 30]。有一个背包，承重上限为 capacity = 4。将物品装入背包，每件物品最多装入一个，求能装入的最大价值。 二维数组解法 # 建一个二维数组 dp，dp[i][j] 表示从下标为 [0 - i] 的物品中任意选取，装入容量为 j 的背包，所能获得的最大价值。\n对数组进行初始化。\ndp = [[0] * (capacity + 1) for _ in range(weight)] for i in range(weight[0], capacity + 1): dp[0][i] = weight[0] 当 j = 0 时，背包装不下任何东西，所能得到的价值为 0. 因此对于任意 i， dp[i][0] = 0. 当只有一件物品可选时，能装入的价值要么等于该物品价值，要么等于 0（该物品装不下）。据此，我们可以初始化 dp[0]。初始化后的 dp 数组如上图。\n接下来进行推演，以得到状态转移方程。\ndp[i] 相对于 dp[i - 1] 来说，区别在于多了物品 i 可供选择。那么根据物品 i 的重量，dp[i][j] 的值有两种情况：\nweight[i] \u0026gt; j\n即物品 i 无法装入背包，因此多了这个物品并没有什么分别。\ndp[i][j] = dp[i - 1][j]\nweight[i] ≤ j\n物品 i 可以装入背包，我们应该尝试这个新物品能否给我们带来更大的价值。将背包腾空一些，正好能够放下该物品就行，然后放入该物品计算一下总价值（dp[i - 1][j - weight[i]] + value[i]），看是不是比不放此物品的价值（dp[i - 1][j]）大。\ndp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i] + value[i])\n有了初始值，有了状态转移方程，就可以开始推演结果了。\nfor i in range(1, len(weight)): for j in range(1, capacity + 1): dp[i][j] = dp[i - 1][j] if weight[i] \u0026gt; j else max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]) 返回数组的最后一个元素，即是本题的结果。\n代码中的两个循环遍历，我们先遍历了物品，后遍历了背包容量，这很符合直觉。能否颠倒一下，先遍历背包容量，后遍历物品呢？\n作为一个纯0-1背包问题的二维数组解法，是可以颠倒的。（其他非「纯0-1背包问题」，非一维数组解法，后面再探讨。）\nfor j in range(1, capacity + 1): for i in range(1, len(weight)): dp[i][j] = dp[i - 1][j] if weight[i] \u0026gt; j else max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]) 可颠倒的原因在于，dp[i][j] 的计算，依赖于一维下标为 i-1 ，二维下标为 j 或 j - weight[i] 的元素。从几何位置上来看，其依赖的是其左上方的元素。而无论我们是先遍历一维还是先遍历二维，都能够保证左上方的元素先于当前元素访问。\n💡再来琢磨一个问题，我们的动态规划，背包容量是从 0 开始的，可选物品是从数量为 1 开始的。我们为什么没有从可选物品数量为 0 开始呢？能否这样做呢？答案是完全可以。\n并且，多了这一行后，我们发现，只有一个物品时的数据也可以根据第 0 行推演出来，推演公式是一模一样的。这样，我们的初始化就更简单了。\n那么一开始为什么没有这样做呢，主要还是为了便于理解。在我们这个新的数组里，dp[i][j] 表示的背包容量仍是 j，但是其所可选取的范围是 0 ~ i-1，琢磨的也是第 i-1 个物品要不要放入的问题，关心的是 weight[i - 1] 和 value[i -1]，为了让大家少转一个弯，所以还是采用了见 i 是 i 的方式。\n在这个新的 dp 数组下，代码是这样的：\ndp = [[0] * (capacity + 1) for _ in range(len(weight) + 1)] for i in range(1, len(weight) + 1): for j in range(1, capacity + 1): dp[i][j] = dp[i - 1][j] if weight[i - 1] \u0026gt; j else max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]) 代码上的区别就在于初始化更简单了，以及 weight[i - 1]、value[i - 1] 的下标变化。\n一维数组解法 # 观察二维解法我们会发现，第 i 行数据的计算，仅依赖于第 i-1 行。那么我们完全不用保存整张二维数组的所有数据，仅用一个数组即可完成计算。\ndp = [0] * (capacity + 1) for i in range(weight[0], capacity + 1): dp[i] = value[0] for i in range(1, len(weight)): for j in range(capacity, weight[i] - 1, -1): dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) 注意在内循环中，背包容量是从后往前遍历的。这很好理解，在二维数组中，第 i 行依赖于第 i-1 行，体现在一维数组中则是新数据依赖于旧数据。而数组右侧的数据又依赖于左侧的数据。要保证这两个依赖不被破坏，我们只能从后往前遍历。\n那么，一维数组解法，两个循环是否能颠倒呢？\n就此题此解来说（纯0-1背包的一维数组解法），答案显然是不能。我们之所以能够把二维降为一维，就是因为利用了数据更新上的时间差。也就是第 i 层和第 i-1 层之间，从右往左依赖的时间差。所以我们从右往左遍历的时候，右边元素的更新，依赖的是左边的元素，转换到二维数组里，其实是第 i-1 层左边的元素。即我们的这个一维数组，已经更新的（右边的元素）是第 i 层的元素，尚未更新的（左边的元素）是第 i-1 层的元素。因此，内循环从右往左，外循环从 i-1 到 i，这是无法改变的。\n💡\n同样引申之前所说的从可选物品数量为 0 为起始。我们发现在一维数组中实现起来甚至更顺。\ndp = [0] * (capacity + 1) for i in range(1, len(weight) + 1): for j in range(capacity, weight[i - 1] - 1, -1): dp[j] = max(dp[j], dp[j - weight[i - 1]] + value[i - 1]) 这个从可选物品数量为 0 开始的思考方式非常关键，从本质上来说，它才是咱们推理的源头。只不是为了便于理解，咱们多做一步，把 1 个物品的情况手动推理了，也可以。所以咱们下面还是按从 1 个物品开始推理来讲解。（也不排除存在无法从 0 状态推理到 1 状态的情况，这个我不打包票，但是我还没遇见过。）\n完全背包 # 💡 有 3 件物品，质量分别为 weight = [1, 3, 4]，价值分别为 value = [15, 20, 30]。有一个背包，承重上限为 capacity = 4。将物品装入背包，每件物品的取用都是无穷的，求能装入的最大价值。 完全背包与 0-1 背包的区别就在于，每件物品的数量是无穷的。只要你能装下，对方就能供应。\n琢磨一下，这意味着什么。\n当我们在 0-1 背包中对物品进行遍历时，我们对该物品的操作无非是二元的要与不要（这也是 0-1 这个名字的由来），这件物品只有一个，不能重复添加，我们不可能根据已经添加过这件物品的数据来推算，而只能根据这件物品出现之前的数据，dp[i-1][?]，来推算。\n但是，当这件物品可以取多次的话，情况就不一样了。在之前的计算数据中， 我们可能已经取过这件物品了，而现在还可以再取。\n打个比方，当我们要计算 dp[i][12]，此时可取的范围是 0-i 号物品，背包容量是 12，物品 i 的重量是 6. 老思路，假使我们想要尝试添加这件物品，那么就要保证有足够的空间，我们把背包容量减去 6，看看该容量下能装入的最大价值是多少。注意，容量减去 6 之后的那个背包，可能就已经装过该物品了！所以，我们不要去看 dp[i - 1][12 - 6]，而应去看 dp[i][12 - 6].\n仍然从更直观的二维数组解法入手：\n二维数组解法 # 状态转移方程：\nweight[i] \u0026gt; j\n即物品 i 无法装入背包，因此多了这个物品并没有什么分别。\ndp[i][j] = dp[i - 1][j]\nweight[i] ≤ j\n物品 i 可以装入背包，我们应该尝试装入该物品，与不装该物品比较，看是否得到更大价值。\ndp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i] + value[i])\n数据初始化也与 0-1 背包有所区别。\ndp = [[0] * (capacity + 1) for _ in range(len(weight))] for i in range(weight[0], capacity + 1): dp[0][i] = dp[0][i - weight[0]] + value[0] 通过遍历推演结果。\nfor i in range(1, len(weight)): for j in range(1, capacity + 1): dp[i][j] = dp[i - 1][j] if weight[i] \u0026gt; j else max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]) 接下来探讨循环是否可颠倒。（为什么我们老在乎能不能颠倒的问题呢，直接按照最符合直觉的方法来写，保证不会出错不就好了？不然，在背包问题的某些问题中，循环的嵌套顺序恰恰就是解题的关键，我们从一开始的纯背包问题就关注循环嵌套顺序、遍历方向，对我们后面的理解是十分有帮助的。）\n答案是可以（纯完全背包，二维数组解法）。究其原因，跟之前的分析是一样的。当前元素的计算，只依赖于其左边，其上边的元素。只要能够保证这个顺序，我们把循环颠倒一下也没关系。\nfor j in range(1, capacity + 1): for i in range(1, len(weight)): dp[i][j] = dp[i - 1][j] if weight[i] \u0026gt; j else max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]) 一维数组解法 # dp = [0] * (capacity + 1) for i in range(weight[0], capacity + 1): dp[i] = dp[i - weight[0]] + value[0] for i in range(1, len(weight)): for j in range(weight[i], capacity + 1): dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) 发现端倪！这里内循环又变成从左往右了！（0-1 背包一维数组解法内循环是从右往左的。）\n究其本质，原因都是一致的。我们展开到二维来看，dp[i][j] 依赖于 dp[i][j - weight[i]]，前面说过了，二维数组中的 dp[i] 和 dp[i - 1] 反映到一维数组中，就是一种时序上的区别，在第 i 轮循环中，dp[i - 1] 就是更新之前的数据，dp[i] 就是更新之后的数据。此时，我们依赖于更新后的数据，右边又依赖左边，所以当然就要从左往右更新了。\n再次，此场景（纯完全背包，一维数组解法）的循环能颠倒吗？不行，我们要保持从 i-1 到 i 的渐进关系不被打破，此处一维数组的循环仍然不能颠倒。\n多重背包 # 多重背包问题是指物品数量既不是一个，也不是无限个，而是有限个。其介于 0-1 背包与完全背包之间。其实我们只要经过小小的转化，将物品一件件摊开，就可以将其转化成 0-1 背包。\n摊开前：\n摊开后：\n当然，这是原始，不优雅的做法。\n要优雅，也不难。加一层循环来解决。\n二维数组解法 # # 初始化 dp = [[0] * (capacity + 1) for _ in range(len(weight))] for i in range(weight[0], capacity + 1): k = 1 while k \u0026lt;= nums[0] and i - k * weight[0] \u0026gt;= 0: dp[0][i] = max(dp[0][i], 0 + k * value[0]) k += 1 # 推演 for i in range(1, len(weight)): for j in range(1, capacity + 1): dp[i][j] = dp[i - 1][j] k = 1 while k \u0026lt;= nums[i] and j - k * weight[i] \u0026gt;= 0: dp[i][j] = max(dp[i][j], dp[i - 1][j - k * weight[i]] + k * value[i]) k += 1 逻辑也很简单：装 1 件试试、装 2 件试试、装 3 件试试。。\n但是呢，我们看代码中 dp 数组的初始化，不免就琢磨过味来了。这个初始化，略显复杂啊。\n再想想我们前面所说的以没有物品作为初始条件，是否会更简单呢？确实如此。\ndp = [[0] * (capacity + 1) for _ in range(len(weight) + 1)] for i in range(1, len(weight) + 1): for j in range(1, capacity + 1): dp[i][j] = dp[i - 1][j] k = 1 while k \u0026lt;= nums[i - 1] and j - k * weight[i - 1] \u0026gt;= 0: dp[i][j] = max(dp[i][j], dp[i - 1][j - k * weight[i - 1]] + k * value[i -1]) k += 1 循环的嵌套顺序（多重背包，二维数组解法）也是颠倒的，就不上代码了。\n一维数组解法 # 我们直接上以无物品作为起始条件的代码。\ndp = [0] * (capacity + 1) for i in range(1, len(weight) + 1): for j in range(capacity, 0, -1): k = 1 while k \u0026lt;= nums[i - 1] and j - k * weight[i - 1] \u0026gt;= 0: dp[j] = max(dp[j], dp[j - k * weight[i - 1]] + k * value[i - 1]) k += 1 组合问题 # 在纯背包问题中，题目要求的是最值。而在组合问题中，题目问的是组合的种类。\n前面 0-1、完全、多重的区别我们已经讨论的很清楚了，因此在这一环节，我们就专注在组合这一点上，就不专门地按照 0-1 变形、完全变形这样的情形来讲解了。\n在求最值的纯背包问题中 ，我们在状态转移方程中用到的是 max / min 这样的最值函数，而求组合数量，用到的当然就是加法了。\n根据对于组合结果的要求，组合问题又可以分为两类：无顺序组合、有顺序组合。\n无顺序组合 # 无顺序组合呢，就是说，不关注你取用物品的顺序，先选用 1 号物品，再选用 3 号物品，又或是先选 3 号后选 1 号，这都算是同一种选择。\nleetcode 上典型的无顺序组合背包问题是 518. Coin Change 2，我们就以这个问题来讲解。\n💡 给定不同面额的硬币 coins 和一个总金额 amout。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。 示例 1:\n输入: amount = 5, coins = [1, 2, 5] 输出: 4 解释: 有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1\n示例 2:\n输入: amount = 3, coins = [2] 输出: 0 解释: 只用面额2的硬币不能凑成总金额3。\n示例 3:\n输入: amount = 10, coins = [10] 输出: 1\n二维数组解法 # class Solution: def change(self, amount: int, coins: List[int]) -\u0026gt; int: if amount == 0: return 1 elif not coins: return 0 dp = [[0] * (amount + 1) for _ in range(len(coins))] for row in dp: row[0] = 1 for i in range(len(coins)): for j in range(1, amount + 1): dp[i][j] = dp[i - 1][j] if coins[i] \u0026gt; j else dp[i - 1][j] + dp[i][j - coins[i]] return dp[-1][-1] dp[i][j] 表示从下标为 0-i 的金币中任意挑选，使总金额为 j 的组合数。\n这其实跟纯 0-1 问题没有多大差别，只是 max() 换成了 + 罢了。\n此场景下，内外循环也是可以颠倒的，就不贴代码了。\n一维数组解法 # class Solution: def change(self, amount: int, coins: List[int]) -\u0026gt; int: if amount == 0: return 1 elif not coins: return 0 dp = [0] * (amount + 1) dp[0] = 1 for coin in coins: for x in range(coin, amount + 1): dp[x] += dp[x - coin] return dp[-1] 也就是纯 0-1 背包问题的一维解法递推过来。\n那么这个场景下，内外循环能否颠倒呢，答案显然也是不能，原因跟纯 0-1 背包的一维解法不能颠倒是一样的。\n有顺序组合 # leetcode 上典型的有顺序组合背包问题是 377. Combination Sum IV。\n💡 给定一个由正整数组成且不存在重复数字的数组，找出和为给定目标正整数的组合的个数。 示例:\nnums = [1, 2, 3] target = 4\n所有可能的组合为： (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)\n请注意，顺序不同的序列被视作不同的组合。\n因此输出为 7。\n二维数组解法 # class Solution: def combinationSum4(self, nums: List[int], target: int) -\u0026gt; int: if target == 0: return 1 if not nums: return 0 dp = [[0] * (target + 1) for _ in range(len(nums))] for row in dp: row[0] = 1 for j in range(1, target + 1): for i in range(len(nums)): dp[i][j] = dp[i - 1][j] if nums[i] \u0026gt; j else dp[i - 1][j] + dp[-1][j - nums[i]] return dp[-1][-1] 比较一下无顺序问题中的二维解法，关键区别在于：\n# 无顺序组合 dp[i][j] = dp[i - 1][j] + dp[i - 1][j - coins[i]] # 有顺序组合 dp[i][j] = dp[i - 1][j] + dp[-1][j - nums[i]] 体现在二维数组中的差异如下：\n图中，蓝色的 dp[i][j] 由黄色的两个元素相加所得。该如何来解读二者的含义呢？\n对于不区分顺序的组合来说，我们可以把数组 coins 的顺序给锁死，遍历的时候只需要管前面已经遍历过的元素，不用管尚未遍历到的元素，反正后面的元素也过不了前面来。前面的元素先被挑选，后面的元素后被挑选。\n但是当新的顺序就是一个新的组合的时候，我们不能将 nums 这个数组的元素顺序锁死。当我们要计算 dp[i][j] 时，如果只把眼光放在 dp[i - 1] 上，那岂不是把「后面的元素先被选用」这种情况完全忽略了吗？实际上，在后面的元素也有需要被先选的情况下，i 已经不能约束元素的先后顺序了，那么 dp[i][j] 的含义是什么呢，我们应该如何理解 i 在其中的意义呢？\n控制变量法，我们固定 j 不动，看在不同的 i 中，dp[i][j] 所表达的意义。大家知道，最终我们关心的只是最后一行，但是这最后一行的结果，又是由前面的行逐步累加过来的，琢磨出点味儿了，迭代，有点迭代的味道。i 是对 nums 数组进行遍历的一个迭代因子。再把它具象化一点，我们计算的是组合的种类，不妨把可能的组合分个类，就按尾元素来分好了（就像我们交作业的时候，按学号尾数来交一样），我们的 i 从 0 迭代到 len(nums - 1)，也就是把所有可能的组合，按尾元素从 nums[0] 到 nums[-1] 分了个类，绝不重复。\n那么，以 nums[i] 为尾的组合有多少种呢？尾元素是固定的，自然是前面的组合有多少种，那就有多少种。加上尾元素的和是 j，去掉尾元素，那不就是 j - nums[i] 嘛，组合总数，那不就是 dp[-1][j - nums[i]] 吗？\n现在再来看一看转移方程：dp[i][j] = dp[i - 1][j] + dp[-1][j - nums[i]]\n这不就是以 nums[i] 为尾元素的组合数（dp[-1][j - nums[i]]），再累加上前面已经计算的总数（dp[i - 1][j]）吗？\n怎么样，是不是有一种通透感。其实这个场景，我们用一维数组的解法，会「看起来」更顺畅一点，因为在一维数组解法中，这个 i 就真的是一个纯粹的迭代。但是，「看起来」的顺畅，会使你心中的疑惑被巧妙地粉饰和掩藏。真正把它摊开后，你发现自己心中还是有疙瘩没有捋顺。这也就是我们一直如此坚持，如此愚笨地，非得二维解法、一维解法一个不落，一遍遍讲的原因了。\n一维数组解法 # class Solution: def combinationSum4(self, nums: List[int], target: int) -\u0026gt; int: if not nums: return 0 dp = [0] * (target+1) dp[0] = 1 for i in range(1,target+1): for num in nums: if i \u0026gt;= num: dp[i] += dp[i-num] return dp[target] 对于此场景（有顺序组合，一维解法）的解法，我们就再钻进去唠叨了。就追问一个问题：循环能颠倒吗？\n不能。nums 的遍历就是一个迭代，我们在一轮迭代中计算完了和为 j - 1 的组合数，再在一轮新的迭代中计算和为 j 的组合数。因而，nums 的遍历，一定是内循环。\nTrue / False 问题 # True / False 问题也就是问你条件能不能达成。其在 leetcode 的典型问题是 416. Partition Equal Subset Sum.\n💡 给定一个只包含正整数的非空数组 nums。是否可以将这个数组分割成两个子集，使得两个子集的元素和相等。 示例 1:\n输入: [1, 5, 11, 5]\n输出: true\n解释: 数组可以分割成 [1, 5, 5] 和 [11]. 示例 2:\n输入: [1, 2, 3, 5]\n输出: false\n解释: 数组不能分割成两个元素和相等的子集.\n此题是问能否分割为两个等和子串，其实也就是问能否从数组中挑选一些数，使其和是数组总和的一半。即目标 target = sum(nums) // 2.\n二维数组解法 # class Solution: def canPartition(self, nums: List[int]) -\u0026gt; bool: if not nums or len(nums) \u0026lt;= 1: return False if sum(nums) % 2 == 1: return False target = sum(nums) // 2 dp = [[False] * (target + 1) for _ in range(len(nums))] for row in dp: row[0] = False if nums[0] \u0026lt;= target: dp[0][nums[0]] = True for i in range(1, len(nums)): for j in range(1, target + 1): dp[i][j] = (dp[i - 1][j] or dp[i - 1][j - nums[i]]) if j \u0026gt;= nums[i] else dp[i - 1][j] return dp[-1][-1] 内外循环能颠倒吗？可以的，与之前的分析一致。\n一维数组解法 # class Solution: def canPartition(self, nums: List[int]) -\u0026gt; bool: if not nums or len(nums) \u0026lt;= 1: return False if sum(nums) % 2 == 1: return False target = sum(nums) // 2 dp = [False] * (target + 1) if nums[0] \u0026lt;= target: dp[nums[0]] = True for i in range(1, len(nums)): for j in range(target, nums[i] - 1, -1): dp[j] = dp[j] or dp[j - nums[i]] return dp[-1] 内外循环能颠倒吗？不行的，与之前的分析一致。\n问题整理 # 最大最小问题\n322. Coin Change\n组合问题\n518. Coin Change 2 （无顺序组合）\n377. Combination Sum IV （有顺序组合）\nTrue / False 问题\n416. Partition Equal Subset Sum\n题外絮叨 # 分数背包问题\n分数背包问题的题干和 0-1 类似，区别在于物品可以部分装入，而不是只能做出二元（0-1）选择。但是这道题我认为不应该放到背包问题这个大类里面来讲，因为背包问题的特点和难点就在于物品重量和物品价值的取舍。而如果商品可以部分装入，这个制约关系也就不存在了，物品的最优选择变得非常容易。\n在《算法导论》中，分数背包问题是用来和 0-1 背包问题做对比，以比较贪心算法和动态规划的区别。在分数背包问题中，我们只需要计算物品的单位价值 v/w, 然后优先选择单位价值高的物品就可以了。这是因为物品可以部分装入，所以我们可以保证要么背包被装满，要么物品被装完。而 0-1 背包问题则无法用贪心算法解决。\n参考资料 # 力扣\n咱就把01背包问题讲个通透！\n力扣\n动态规划：关于完全背包，你该了解这些！\n力扣\n动态规划：关于多重背包，你该了解这些！\n力扣\n希望用一种规律搞定背包问题\n","date":"3 November 2022","externalUrl":null,"permalink":"/%E7%AE%97%E6%B3%95/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E5%A4%A7%E5%85%A8%E8%A7%A3/","section":"算法","summary":"","title":"背包问题大全解","type":"算法"},{"content":"本教程使用 TensorFlow 构建一个卷积神经网络（CNN）对 48×48 灰度人脸图片进行 7 种情绪分类，再结合 OpenCV 的 Haar Cascade 从电影海报中检测人脸并识别情绪。\nPart 1：情绪分类模型训练 # 1. 数据集下载 # 从 Kaggle 下载面部情绪数据集：\n# download dataset !wget https://www.kaggle.com/api/v1/datasets/download/dilkushsingh/facial-emotion-dataset?datasetVersionNumber=8 --2024-08-14 06:50:35-- https://www.kaggle.com/api/v1/datasets/download/dilkushsingh/facial-emotion-dataset?datasetVersionNumber=8 Resolving www.kaggle.com (www.kaggle.com)... 35.244.233.98 Connectin...(truncated) 解压数据：\n# unzip dataset !unzip \u0026#39;facial-emotion-dataset?datasetVersionNumber=8\u0026#39; inflating: train_dir/sad/img_161024360.jpg inflating: train_dir/sad/img_161024361.jpg inflating: train_dir/sad/img_161024362....(truncated) 2. 数据清洗 # 检查训练集（29,417 张）和测试集（7,340 张）中的损坏图片并移除：\n# check and remove bad files from pathlib import Path from tensorflow.io import read_file from tensorflow.image import decode_image def check(data_dir): cnt = 0 for image in sorted(data_dir.glob(\u0026#39;*/*\u0026#39;)): cnt += 1 try: img = read_file(str(image)) img = decode_image(img) if img.ndim != 3: raise Exception(\u0026#39;ndim != 3\u0026#39;) except Exception as e: print(\u0026#39;bad file: \u0026#39;, str(image)) image.unlink() print(\u0026#39;checked {} files\u0026#39;.format(cnt)) check(Path.cwd()/\u0026#39;train_dir\u0026#39;) check(Path.cwd()/\u0026#39;test_dir\u0026#39;) checked 29417 files checked 7340 files 训练集 29,417 张，测试集 7,340 张，均无损坏文件。\n3. 数据加载与预处理 # 使用 image_dataset_from_directory 加载 48×48 灰度图，转换为 float32 并添加 cache/prefetch 优化：\n# read dataset import tensorflow as tf from tensorflow.keras.preprocessing import image_dataset_from_directory ds_train_ = image_dataset_from_directory( \u0026#39;/content/train_dir\u0026#39;, labels = \u0026#39;inferred\u0026#39;, label_mode = \u0026#39;categorical\u0026#39;, color_mode = \u0026#39;grayscale\u0026#39;, image_size = [48, 48], batch_size = 32, shuffle = True, ) ds_valid_ = image_dataset_from_directory( \u0026#39;/content/train_dir\u0026#39;, labels = \u0026#39;inferred\u0026#39;, label_mode = \u0026#39;categorical\u0026#39;, color_mode = \u0026#39;grayscale\u0026#39;, image_size = [48, 48], batch_size = 32, shuffle = True, ) def convert_to_float(image, label): image = tf.image.convert_image_dtype(image, dtype=tf.float32) return image, label AUTOTUNE = tf.data.experimental.AUTOTUNE ds_train = ( ds_train_. map(convert_to_float) .cache(). prefetch(buffer_size = AUTOTUNE) ) ds_valid = ( ds_valid_ .map(convert_to_float) .cache() .prefetch(buffer_size = AUTOTUNE) ) Found 29417 files belonging to 7 classes. Found 29417 files belonging to 7 classes. 情绪标签 # 7 种情绪类别：\nemotions = [\u0026#39;angry\u0026#39;, \u0026#39;disgust\u0026#39;, \u0026#39;fear\u0026#39;, \u0026#39;happy\u0026#39;, \u0026#39;neutral\u0026#39;, \u0026#39;sad\u0026#39;, \u0026#39;surprise\u0026#39;] 4. 数据预览 # 查看部分训练样本及其对应标签：\n# preview dataset import matplotlib.pyplot as plt import numpy as np ds = ds_train.take(1) for item in ds: images, labels = item plt.figure(figsize = (12, 8)) for i in range(6): plt.subplot(2, 3, i+1) plt.axis(\u0026#39;off\u0026#39;) plt.imshow(images[i], cmap=\u0026#39;gray\u0026#39;) plt.title(emotions[np.argmax(labels[i].numpy())]) 5. 构建 CNN 模型 # 模型结构：多层 Conv2D + Dropout 防止过拟合，最后通过 Dense + softmax 输出 7 类概率。\n# define model from tensorflow import keras from tensorflow.keras import layers from tensorflow.keras.callbacks import EarlyStopping model = keras.Sequential([ layers.Input([48, 48, 1]), layers.Conv2D(filters=32, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.MaxPool2D(), layers.Conv2D(filters=64, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.Conv2D(filters=64, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.Conv2D(filters=64, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.Conv2D(filters=64, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.Conv2D(filters=128, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Conv2D(filters=128, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.Conv2D(filters=128, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Conv2D(filters=128, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.Conv2D(filters=256, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.Conv2D(filters=256, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.Flatten(), layers.Dense(64, input_dim=8, activation=\u0026#39;relu\u0026#39;), layers.Dropout(0.2), layers.Dense(7, activation=\u0026#39;softmax\u0026#39;), ]) model.compile( optimizer = tf.keras.optimizers.Adam(epsilon=0.01), loss = \u0026#39;categorical_crossentropy\u0026#39;, metrics = [\u0026#39;accuracy\u0026#39;] ) early_stopping = EarlyStopping( min_delta = 0.001, patience = 20, restore_best_weights = True, ) history = model.fit( ds_train, validation_data = ds_valid, callbacks = [early_stopping], epochs = 100 ) Epoch 1/100 920/920 ━━━━━━━━━━━━━━━━━━━━ 66s 55ms/step - accuracy: 0.2055 - loss: 1.9932 - val_accuracy: 0.2174 - val_loss: 1.9872 Epoch 2/100 920/920 ━━━━━━━━━━━━━...(truncated) 使用 EarlyStopping（patience=20）防止过拟合，最多训练 100 个 epoch。\n6. 模型预测测试 # 在验证集上测试模型的预测效果：\nimport numpy as np ds = ds_valid.take(1) for item in ds: prediction = model.predict(item) print(prediction[3]) print(np.argmax(prediction[3])) images, labels = item plt.figure(figsize = (10, 20)) for i in range(images.shape[0]): plt.subplot(8, 4, i+1) plt.axis(\u0026#39;off\u0026#39;) plt.imshow(images[i], cmap=\u0026#39;gray\u0026#39;) plt.title(emotions[np.argmax(labels[i])]) 1/1 ━━━━━━━━━━━━━━━━━━━━ 1s 551ms/step [2.8921450e-02 1.4205185e-04 1.8577275e-05 9.5940810e-01 1.7444472e-04 1.1266178e-02 6.9209687e-05] 3 预测结果中最高概率对应的索引为 3，即 neutral（中性）。\n7. 模型保存 # from datetime import datetime now = datetime.now().strftime(\u0026#34;%Y-%m-%d-%H-%M-%S\u0026#34;) model.save(\u0026#39;face-emotion-{}.keras\u0026#39;.format(now)) Part 2：人脸检测与情绪识别 # 8. 下载电影海报数据集 # 从 Kaggle 下载电影海报数据集，用于人脸检测测试：\n# download movie poster dataset !wget https://www.kaggle.com/api/v1/datasets/download/raman77768/movie-classifier?datasetVersionNumber=1 -O posters.zip --2024-08-14 06:09:10-- https://www.kaggle.com/api/v1/datasets/download/raman77768/movie-classifier?datasetVersionNumber=1 Resolving www.kaggle.com (www.kaggle.com)... 35.244.233.98 Connecting to www...(truncated) 解压海报数据：\n# unzip dataset !unzip posters.zip 整理目录结构：\n!mv Multi_Label_dataset/Images/ posters \u0026amp;\u0026amp; rm -rf Multi_Label_dataset/ 9. 下载 Haar Cascade 人脸检测模型 # OpenCV 预训练的 Haar Cascade 级联分类器，用于检测图片中的人脸：\n# download haarcascade_frontalface_default.xml !wget --no-check-certificate \\ https://raw.githubusercontent.com/computationalcore/introduction-to-opencv/master/assets/haarcascade_frontalface_default.xml \\ -O haarcascade_frontalface_default.xml --2024-08-14 06:30:10-- https://raw.githubusercontent.com/computationalcore/introduction-to-opencv/master/assets/haarcascade_frontalface_default.xml Resolving raw.githubusercontent.com (raw.githubuse...(truncated) 10. 批量人脸检测与裁剪 # 遍历所有电影海报，用 Haar Cascade 检测人脸并裁剪保存：\nimport os import cv2 import matplotlib.pyplot as plt from pathlib import Path face_cascade = cv2.CascadeClassifier(\u0026#39;haarcascade_frontalface_default.xml\u0026#39;) for fl in sorted((Path.cwd()/\u0026#39;posters\u0026#39;).glob(\u0026#39;*.jpg\u0026#39;)): img = cv2.imread(fl) img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) detects = face_cascade.detectMultiScale(img, 1.3, 5) faces = [img[y:y+h, x:x+w] for (x,y,w,h) in detects] for i in range(len(faces)): cv2.imwrite(\u0026#39;faces/{}_{}.jpg\u0026#39;.format(fl.name[:-4], i+1), faces[i]) 10.1 人脸检测效果预览 # 查看单张海报的人脸检测结果：\nimport matplotlib.pyplot as plt import cv2 img = cv2.imread(\u0026#39;/content/posters/tt0086593.jpg\u0026#39;) plt.figure() face_cascade = cv2.CascadeClassifier(\u0026#39;haarcascade_frontalface_default.xml\u0026#39;) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) detects = face_cascade.detectMultiScale(gray, 1.3, 5) faces = [gray[y:y+h, x:x+w] for (x,y,w,h) in detects] for face in faces: plt.figure() plt.axis(\u0026#39;off\u0026#39;) plt.imshow(face, cmap=\u0026#39;gray\u0026#39;) 11. 对检测到的人脸进行情绪预测 # 加载裁剪后的人脸图片，用训练好的情绪分类模型进行预测：\nds_test_ = image_dataset_from_directory( \u0026#39;/content/faces\u0026#39;, labels = None, color_mode = \u0026#39;grayscale\u0026#39;, image_size = [48, 48], batch_size = 32, shuffle = True, ) def convert_to_float(image): image = tf.image.convert_image_dtype(image, dtype=tf.float32) return image ds_test = ( ds_test_ .map(convert_to_float) .cache() .prefetch(buffer_size = AUTOTUNE) ) Found 4691 files. ds = ds_test.take(1) for item in ds: prediction = model.predict(item) print(prediction[0]) print(np.argmax(prediction[0])) images = item plt.figure(figsize = (10, 20)) for i in range(images.shape[0]): plt.subplot(8, 4, i+1) plt.axis(\u0026#39;off\u0026#39;) plt.imshow(images[i], cmap=\u0026#39;gray\u0026#39;) plt.title(emotions[np.argmax(prediction[i])]) 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 20ms/step [9.5885843e-02 1.2981243e-06 2.5813368e-05 4.4420755e-01 4.5889294e-01 3.1211486e-05 9.5540209e-04] 4 12. 总结 # 项目 说明 任务类型 图像分类（情绪识别）+ 目标检测（人脸检测） 数据集 Kaggle 面部情绪数据集 — 29,417 张训练 + 7,340 张测试 图片尺寸 48×48 灰度图 情绪类别 7 类：angry, disgust, fear, happy, neutral, sad, surprise CNN 模型 多层 Conv2D + Dropout + Dense，最终 softmax 输出 优化器 Adam (epsilon=0.01) 损失函数 categorical_crossentropy 早停机制 EarlyStopping(patience=20) 人脸检测 Haar Cascade 级联分类器 检测数据 电影海报 → 裁剪出 4,691 张人脸 本教程完整实现了从训练情绪分类模型，到用 OpenCV 从电影海报中检测人脸并进行实时情绪预测的完整流程。\n","date":"2 December 2024","externalUrl":null,"permalink":"/ai/%E6%83%85%E7%BB%AA%E6%A3%80%E6%B5%8B/","section":"AI 相关内容","summary":"","title":"CNN 分类器实践-面部情绪识别","type":"ai"},{"content":"回溯法 = 暴力遍历 + 剪枝。\n其中，遍历的方式是 DFS，我们结合 DFS 来理解回溯这个词。在回溯问题中，我们的每一次选择都对应树中的一个分支。之所以是 DFS 而非 BFS，关键就在于及时回头。\n我们看 DFS 在树中的路径，不就是走到底然后回头，换分支走到底再回头吗？这里的回头其实就是回溯法中的回溯。而剪枝就是提前回头。\n在回溯法的遍历过程中，我们会记下自己的沿途内容（track）和状态，回头的过程中，我们会进行内容和状态的回溯（track.pop()），回溯到上一层。回到岔路口，继续沿着尚未搜索的路径往下搜索。\n我们仍然是根据回溯法的几个题型大类来讲解。\n子集/组合问题 # 给你一个集合，让你计算满足条件的子集（组合），甚至就直接问全部的子集情况。\n注意这里的子集、组合，都是无关顺序的，有顺序的被称为排列。\n首先来看最最基本的一道题：78. Subsets\n💡 给你一个整数数组 nums ，数组中的元素互不相同 。返回该数组所有可能的子集。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。\n示例 1：\n输入：nums = [1,2,3] 输出：[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]\n示例 2：\n输入：nums = [0] 输出：[[],[0]]\n以上是递归树，可以看到，根据每一步的不同选择，可以得到很多分支。\n集合是不区分顺序的，因此 [1, 2] 和 [2, 1] 是同一种，我们不如就规定只考虑升序的，那么 [2, 1] 就不存在了，这样便不会重复计算。如图中所示，我们只根据后面的元素来创建分支。\n在 DFS 的过程中，我们把遍历的路径记录下来，当遍历到叶子时，说明我们走完了了一条新路径，将其加入全局变量 ans，回头继续找新的岔路走。\n由于是问所有的子集，因此这一题实际上没有进行剪枝。\nclass Solution: def subsets(self, nums: List[int]) -\u0026gt; List[List[int]]: n = len(nums) track = [] ans = [] def trackback(start): ans.append(track[:]) for i in range(start, n): track.append(nums[i]) trackback(i + 1) track.pop() trackback(0) return ans 接下来看看需要剪枝的问题：90. Subsets II\n💡 给定一个可能包含重复元素的整数数组 nums，返回该数组所有可能的子集。 说明：解集不能包含重复的子集。\n示例:\n输入: [1,2,2] 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]\n题干上的唯一区别是，数组 nums 可能包含了重复元素。我们在遍历中完成去重的方式，便是剪枝。\n还是刚刚那个递归树，只不过红框的部分需要剪掉。通过观察，我们发现，如果我们对原数组进行了排序，那么当我们在某一个回溯体内，正要通过 for 循环产生分支的时候，如果当前元素与上一个元素相同，即当前分支与上一个分支相同的时候，我们可以跳过当前这个重复的分支。\nclass Solution: def subsetsWithDup(self, nums: List[int]) -\u0026gt; List[List[int]]: nums.sort() n = len(nums) track = [] ans = [] def trackback(start): ans.append(track[:]) for i in range(start, n): if i \u0026gt; start and nums[i] == nums[i - 1]: continue track.append(nums[i]) trackback(i + 1) track.pop() trackback(0) return ans 以上是问所有子集，而一般情况下，还会要求你的子集满足某一条件，比如和为某一定值。处理方式还是一样的，只不过对结果多了一些条件，而这些条件也往往对剪枝有帮助。典型问题是如下两个：\n39. Combination Sum 40. Combination Sum II 这两个问题的区别就在于一个是元素可以重复使用，另一个则不能。其解法不再细讲。\n以上几题中代码的结构类似，而这基本也就是回溯法的固定模板。\n排列问题 # 排列问题与子集/组合的区别就在于，排列是讲究顺序的，不同元素顺序的组合，就是不同的排列。\n先来看最基本的排列问题：46. Permutations\n💡 给定一个 没有重复 数字的序列，返回其所有可能的全排列。 示例:\n输入: [1,2,3] 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]\n我们把这棵树和第一个子集问题中的递归树对比一下。\n全子集问题中，我们要保证元素顺序造成的差异不被重复计算，即 [1, 2] 和 [2, 1] 中只能计算一个，因此我们对原数组进行了排序，只向后进行分支。而在全排列中，我们要求 [1, 2] 和 [2, 1] 都不能被落下，因此我们不能只向后分支，而是未访问的都要进行分支。\nclass Solution: def __init__(self): self.ans = [] def dfs(self, nums, path): if not nums: self.ans.append(path[:]) for i in range(len(nums)): path.append(nums[i]) newNums = nums[:i] + nums[i + 1:] self.dfs(newNums, path) path.pop() def permute(self, nums: List[int]) -\u0026gt; List[List[int]]: self.dfs(nums, []) return self.ans 由于是全排列，以上问题同样未剪枝。针对要剪枝的排列问题，如 47. Permutations II，处理起来也是和之前一样的。\n搜索问题 # 其实无论是子集/组合还是排列，都是一种范式化的问题，当一个题目中暴露出了组合、排列这样的字眼了，这道题也就没有什么秘密可言了，因为回溯算法本身不是多么复杂和抽象的算法。很多题会把谜面多加粉饰，看你学得够不够活，能否意识到可以用回溯来解决本问题。我们把这样被粉饰的题就统称为搜索问题，毕竟回溯法的本质就是通过暴力遍历加剪枝来搜索最终答案嘛。\n此处我们讲解一个分割字符串的题：131. Palindrome Partitioning\n💡 给你一个字符串 s，请你将 s 分割成一些子串，使每个子串都是 回文串 。返回 s 所有可能的分割方案。 回文串 是正着读和反着读都一样的字符串。\n示例 1：\n输入：s = \u0026quot;aab\u0026quot; 输出：[[\u0026quot;a\u0026quot;,\u0026quot;a\u0026quot;,\u0026quot;b\u0026quot;],[\u0026quot;aa\u0026quot;,\u0026quot;b\u0026quot;]]\n示例 2：\n输入：s = \u0026quot;a\u0026quot; 输出：[[\u0026quot;a\u0026quot;]]\n对于字符串中的每一个字符来说，它都可以有两种选择，第一个选择是加入前面的子串，成为前一个子串的一部分，第二个选择是结束上一个子串，自己成为一个新子串的开头。\nclass Solution: def isPalindrome(self, s): return s == s[::-1] def partition(self, s: str) -\u0026gt; List[List[str]]: n = len(s) track = [] ans = [] def trackback(i): if i == n: if all(self.isPalindrome(subStr) for subStr in track): ans.append(track[:]) return if not track: track.append(s[i]) trackback(i + 1) track.pop() else: track.append(s[i]) trackback(i + 1) track.pop() track[-1] += s[i] trackback(i + 1) track[-1] = track[-1][:len(track[-1]) - 1] trackback(0) return ans 参考资料 # ","date":"7 August 2022","externalUrl":null,"permalink":"/%E7%AE%97%E6%B3%95/%E5%9B%9E%E6%BA%AF%E6%B3%95/","section":"算法","summary":"","title":"回溯法","type":"算法"},{"content":"本教程使用 TensorFlow 搭建一个 3D 卷积神经网络，模型采用 (2+1)D 卷积（空间卷积 + 时间卷积分离）和残差连接，在 Kaggle 人体动作识别视频数据集上进行训练。实现对视频中的人物行为进行分类。\n1. 环境准备 # 1.1 检查 GPU # !nvidia-smi 1.2 安装依赖 # !pip install remotezip einops !pip install -q git+https://github.com/tensorflow/docs 1.3 导入库 # import tensorflow as tf import cv2 import numpy as np import remotezip as rz import os import tqdm import random import pathlib import itertools import collections import imageio from IPython import display from urllib import request from tensorflow_docs.vis import embed import keras from keras import layers import matplotlib.pyplot as plt import einops import seaborn as sns 2. 数据集下载与探索 # 2.1 下载视频数据集 # 从 Kaggle 下载人体动作识别视频数据集，包含 7 类行为：\nURL = \u0026#39;https://www.kaggle.com/api/v1/datasets/download/sharjeelmazhar/human-activity-recognition-video-dataset?datasetVersionNumber=1\u0026#39; video_format = \u0026#39;mp4\u0026#39; 2.2 列出 ZIP 内的文件 # def list_files_from_zip_url(zip_url): files = [] with rz.RemoteZip(zip_url) as zip: for zip_info in zip.infolist(): files.append(zip_info.filename) return files files = list_files_from_zip_url(URL) files = [f for f in files if f.endswith(f\u0026#39;.{video_format}\u0026#39;)] files[:10] 2.3 提取类别名称 # def get_class(fname): return fname.split(\u0026#39;/\u0026#39;)[1] get_class(files[0]) \u0026#39;Clapping\u0026#39; 2.4 统计各类别样本量 # def get_files_for_class(files): files_for_class = collections.defaultdict(list) for fname in files: class_name = get_class(fname) files_for_class[class_name].append(fname) return files_for_class files_for_class = get_files_for_class(files) classes = list(files_for_class.keys()) for i in range(len(classes)): class_name = classes[i] files_num = len(files_for_class[class_name]) print(i, class_name, files_num) 类别 样本数 Clapping 146 Meet and Split 147 Sitting 156 Standing Still 174 Walking While Reading Book 176 Walking While Using Phone 143 Walking 171 3. 数据划分与下载 # 3.1 类别子集选择 # 选择前 7 类，每类取 50 个视频：\ndef select_subset_of_classes(files_for_class, classes, num_per_class): files_subset = dict() for class_name in classes: class_files = files_for_class[class_name] files_subset[class_name] = class_files[:num_per_class] return files_subset NUM_CLASS = 7 NUM_PER_CLASS = 50 files_subset = select_subset_of_classes(files_for_class, classes[:NUM_CLASS], NUM_PER_CLASS) list(files_subset.keys()) 3.2 从 ZIP 下载文件 # def download_from_zip(zip_url, to_dir, file_names): with rz.RemoteZip(zip_url) as zip: for fn in tqdm.tqdm(file_names): class_name = get_class(fn) zip.extract(fn, str(to_dir / class_name)) unzipped_file = to_dir / class_name / fn fn = pathlib.Path(fn).parts[-1] output_file = to_dir / class_name / fn unzipped_file.rename(output_file) 3.3 数据划分函数 # def split_class_lists(files_for_class, count): split_files = [] remain = {} for class_name in files_for_class: split_files.extend(files_for_class[class_name][:count]) remain[class_name] = files_for_class[class_name][count:] return split_files, remain def download_and_split_files(zip_url, num_classes, splits, to_dir): files = list_files_from_zip_url(zip_url) for f in files: path = os.path.normpath(f) tokens = path.split(os.sep) if len(tokens) \u0026lt;= 2: files.remove(f) files_for_class = get_files_for_class(files) classes = list(files_for_class.keys())[:num_classes] for class_name in classes: random.shuffle(files_for_class[class_name]) files_for_class = {x: files_for_class[x] for x in classes} dirs = {} for split_name, split_count in splits.items(): print(split_name, \u0026#34;:\u0026#34;) split_dir = to_dir / split_name split_files, files_for_class = split_class_lists(files_for_class, split_count) download_from_zip(zip_url, split_dir, split_files) dirs[split_name] = split_dir return dirs 3.4 执行划分与下载 # 每类取 30 个训练、10 个验证、10 个测试：\nto_dir = pathlib.Path(\u0026#39;./dataset\u0026#39;) subset_paths = download_and_split_files(URL, NUM_CLASS, {\u0026#34;train\u0026#34;: 30, \u0026#34;val\u0026#34;: 10, \u0026#34;test\u0026#34;: 10}, to_dir) train : 100%| 210/210 [16:41, 4.77s/it] val : 100%| 70/70 [04:59, 4.27s/it] test : 100%| 70/70 [05:09, 4.42s/it] 3.5 验证数据分布 # train_count = len(list(to_dir.glob(f\u0026#39;train/*/*.{video_format}\u0026#39;))) val_count = len(list(to_dir.glob(f\u0026#39;val/*/*.{video_format}\u0026#39;))) test_count = len(list(to_dir.glob(f\u0026#39;test/*/*.{video_format}\u0026#39;))) print(\u0026#39;train: \u0026#39;, train_count) print(\u0026#39;val: \u0026#39;, val_count) print(\u0026#39;test: \u0026#39;, test_count) print(\u0026#39;total: \u0026#39;, total) train: 210 val: 70 test: 70 total: 350 每类验证集分布：\nClapping: 10 Standing Still: 10 Meet and Split: 10 Sitting: 10 Walking: 10 Walking While Reading Book: 10 Walking While Using Phone: 10 共 350 个视频，每个类别 30 训练 + 10 验证 + 10 测试。\n四、视频帧提取与预处理 # 视频数据不能像图片那样直接输入神经网络，核心挑战在于：\n视频长度不固定 需要同时保留空间信息（画面内容）和时序信息（动作变化） 需对帧进行标准化处理 单帧格式化 # def format_frame(frame, output_size): frame = tf.image.convert_image_dtype(frame, tf.float32) frame = tf.image.resize_with_pad(frame, *output_size) return frame 将单帧转换为 float32，并缩放到统一尺寸（224×224），保持宽高比填充（resize_with_pad）。\n从视频文件中提取帧序列 # def frames_from_video_file(video_path, n_frames, output_size = (224, 224), frame_step = 15): result = [] src = cv2.VideoCapture(str(video_path)) video_length = src.get(cv2.CAP_PROP_FRAME_COUNT) need_length = 1 + (n_frames - 1) * frame_step if need_length \u0026gt; video_length: start = 0 else: max_start = video_length - need_length start = random.randint(0, max_start + 1) src.set(cv2.CAP_PROP_POS_FRAMES, start) ret, frame = src.read() result.append(format_frame(frame, output_size)) for _ in range(n_frames - 1): for _ in range(frame_step): ret, frame = src.read() if ret: frame = format_frame(frame, output_size) result.append(frame) else: result.append(np.zeros_like(result[0])) src.release() result = np.array(result)[..., [2, 1, 0]] return result 关键设计：\nframe_step=15：跳帧采样，每 15 帧取一帧，避免连续帧信息冗余 随机起始位置：如果视频足够长，从随机位置开始采样，增强数据多样性 不足补零：视频不够长时，用零填充（np.zeros_like） BGR→RGB：[..., [2, 1, 0]] 将 OpenCV 的 BGR 转为 RGB 帧生成器类 # class FrameGenerator: def __init__(self, path, n_frames, training = False): self.path = path self.n_frames = n_frames self.training = training self.class_names = sorted(set(p.name for p in self.path.iterdir() if p.is_dir())) self.class_ids_for_name = dict((name, idx) for idx, name in enumerate(self.class_names)) def get_files_and_class_names(self): video_paths = list(self.path.glob(f\u0026#39;*/*.{video_format}\u0026#39;)) classes = [p.parent.name for p in video_paths] return video_paths, classes def __call__(self): video_paths, classes = self.get_files_and_class_names() pairs = list(zip(video_paths, classes)) if self.training: random.shuffle(pairs) for path, name in pairs: video_frames = frames_from_video_file(path, self.n_frames) label = self.class_ids_for_name[name] yield video_frames, label FrameGenerator 封装了完整的帧提取流程：\n遍历数据集目录下的所有视频文件 对每个视频调用 frames_from_video_file 提取帧序列 训练时打乱顺序（shuffle），验证/测试时保持顺序 每帧形状：(10, 224, 224, 3)（10 帧 × 224×224 × RGB） 五、数据管道构建 # 使用 tf.data.Dataset 构建高效的数据流水线。\noutput_signature = ( tf.TensorSpec(shape = (None, None, None, 3), dtype = tf.float32), tf.TensorSpec(shape = (), dtype = tf.int16) ) train_ds_ = tf.data.Dataset.from_generator( FrameGenerator(subset_paths[\u0026#39;train\u0026#39;], 10, training=True), output_signature = output_signature ) val_ds_ = tf.data.Dataset.from_generator( FrameGenerator(subset_paths[\u0026#39;val\u0026#39;], 10), output_signature = output_signature ) test_ds_ = tf.data.Dataset.from_generator( FrameGenerator(subset_paths[\u0026#39;test\u0026#39;], 10), output_signature = output_signature ) AUTOTUNE = tf.data.AUTOTUNE batch_size = 8 train_ds = train_ds_.cache().shuffle(1000).prefetch(buffer_size = AUTOTUNE).batch(batch_size) val_ds = val_ds_.cache().shuffle(1000).prefetch(buffer_size = AUTOTUNE).batch(batch_size) test_ds = test_ds_.cache().shuffle(1000).prefetch(buffer_size = AUTOTUNE).batch(batch_size) output_signature：指定生成器输出的张量类型和形状，None 维度表示帧数、高、宽可变 cache()：将数据集缓存到内存，加速后续 epoch 的读取 shuffle(1000)：随机打乱样本顺序 prefetch(AUTOTUNE)：在 GPU 训练的同时预加载下一批数据，隐藏 I/O 延迟 batch(8)：每批 8 个视频（每个视频 10 帧） 批次形状：(8, 10, 224, 224, 3) — 8 个视频 × 10 帧 × 224 × 224 × RGB\n六、(2+1)D 卷积模型 # 本教程的核心创新是将 3D 卷积分解为空间卷积 + 时间卷积，称为 Conv2Plus1D。\nConv2Plus1D：分解 3D 卷积 # class Conv2Plus1D(keras.layers.Layer): def __init__(self, filters, kernel_size, padding): super().__init__() self.seq = keras.Sequential([ # Spatial decomposition layers.Conv3D( filters = filters, kernel_size = (1, kernel_size[1], kernel_size[2]), padding = padding ), # Temporal decomposition layers.Conv3D( filters = filters, kernel_size = (kernel_size[0], 1, 1), padding = padding ) ]) def call(self, x): return self.seq(x) 为什么分解？一个 (3, 3, 3) 的 3D 卷积核有 27 个参数/位置，而分解为 (1, 3, 3) 空间部分（9 个位置）+ (3, 1, 1) 时间部分（3 个位置），总共 12 个位置，参数量减少 55%。\n这种分解：\n降低过拟合风险 加速训练 在视频任务上通常表现不逊于完整 3D 卷积 残差模块 # class ResidualMain(keras.layers.Layer): def __init__(self, filters, kernel_size): super().__init__() self.seq = keras.Sequential([ Conv2Plus1D( filters = filters, kernel_size = kernel_size, padding = \u0026#39;same\u0026#39; ), layers.LayerNormalization(), layers.ReLU(), Conv2Plus1D( filters = filters, kernel_size = kernel_size, padding = \u0026#39;same\u0026#39; ), layers.LayerNormalization(), ]) def call(self, x): return self.seq(x) 残差结构：两个 Conv2Plus1D + LayerNorm + ReLU，通过跳跃连接让梯度直接流通。\nclass Project(keras.layers.Layer): def __init__(self, units): super().__init__() self.seq = keras.Sequential([ layers.Dense(units), layers.LayerNormalization(), ]) def call(self, x): return self.seq(x) def add_residual_block(input, filters, kernel_size): out = ResidualMain(filters, kernel_size)(input) res = input if out.shape[-1] != input.shape[-1]: res = Project(out.shape[-1])(res) return layers.add([res, out]) 当残差路径的通道数发生变化时，Project 层通过 Dense 将跳跃连接投影到相同维度。\n视频下采样 # class ResizeVideo(keras.layers.Layer): def __init__(self, height, width): super().__init__() self.height = height self.width = width self.resizing_layer = layers.Resizing(self.height, self.width) def call(self, video): old_shape = einops.parse_shape(video, \u0026#39;b t h w c\u0026#39;) images = einops.rearrange(video, \u0026#39;b t h w c -\u0026gt; (b t) h w c\u0026#39;) images = self.resizing_layer(images) videos = einops.rearrange( images, \u0026#39;(b t) h w c -\u0026gt; b t h w c\u0026#39;, t = old_shape[\u0026#39;t\u0026#39;]) return videos 使用 einops 在批次-时间维度间重排，将视频帧展开为独立图片进行下采样，再恢复为视频张量。\n完整模型结构 # input_shape = (None, 10, HEIGHT, WIDTH, 3) input = layers.Input(shape = (input_shape[1:])) x = input x = Conv2Plus1D(filters = 16, kernel_size = (3, 7, 7), padding = \u0026#39;same\u0026#39;)(x) x = layers.BatchNormalization()(x) x = layers.ReLU()(x) x = ResizeVideo(HEIGHT // 2, WIDTH // 2)(x) # Block 1 x = add_residual_block(x, 16, (3, 3, 3)) x = ResizeVideo(HEIGHT // 4, WIDTH // 4)(x) # Block 2 x = add_residual_block(x, 32, (3, 3, 3)) x = ResizeVideo(HEIGHT // 8, WIDTH // 8)(x) # Block 3 x = add_residual_block(x, 64, (3, 3, 3)) x = ResizeVideo(HEIGHT // 16, WIDTH // 16)(x) # Block 4 x = add_residual_block(x, 128, (3, 3, 3)) x = layers.GlobalAveragePooling3D()(x) x = layers.Flatten()(x) x = layers.Dense(10)(x) model = keras.Model(input, x) frames, label = next(iter(train_ds)) model.build(frames) keras.utils.plot_model(model, expand_nested=True, dpi=60, show_shapes=True) 架构总览：\n层级 组件 输出空间尺寸 通道数 输入 — 224×224 3 Stem Conv2Plus1D(3,7,7) + BN + ReLU 224×224 16 Down 1 ResizeVideo /2 112×112 16 Block 1 Residual(3,3,3) 112×112 16 Down 2 ResizeVideo /2 56×56 16 Block 2 Residual(3,3,3) 56×56 32 Down 3 ResizeVideo /2 28×28 32 Block 3 Residual(3,3,3) 28×28 64 Down 4 ResizeVideo /2 14×14 64 Block 4 Residual(3,3,3) 14×14 128 Head GAP + Flatten + Dense(10) — 10 空间维度从 224 逐级下采样到 14，通道数从 3 扩展到 128，最终通过全局平均池化和全连接层输出 10 类 logits。\n七、模型编译与训练 # model.compile( loss = keras.losses.SparseCategoricalCrossentropy(from_logits=True), optimizer = keras.optimizers.Adam(learning_rate=0.0001), metrics = [\u0026#39;accuracy\u0026#39;] ) 损失函数：SparseCategoricalCrossentropy（标签为整数，非 one-hot） 优化器：Adam，学习率 0.0001（较小，适合视频任务的稳定性） 评估指标：准确率 history = model.fit( x = train_ds, epochs = 50, validation_data = val_ds ) .... Epoch 45/50 27/27 [==============================] - 4s 161ms/step - loss: 0.6367 - accuracy: 0.7667 - val_loss: 1.1064 - val_accuracy: 0.6143 Epoch 46/50 27/27 [==============================] - 4s 160ms/step - loss: 0.6492 - accuracy: 0.7619 - val_loss: 1.1573 - val_accuracy: 0.6286 Epoch 47/50 27/27 [==============================] - 4s 159ms/step - loss: 0.6652 - accuracy: 0.7476 - val_loss: 1.2028 - val_accuracy: 0.5143 Epoch 48/50 27/27 [==============================] - 4s 160ms/step - loss: 0.7080 - accuracy: 0.7286 - val_loss: 1.0872 - val_accuracy: 0.5857 Epoch 49/50 27/27 [==============================] - 4s 161ms/step - loss: 0.5570 - accuracy: 0.8381 - val_loss: 1.1155 - val_accuracy: 0.6000 Epoch 50/50 27/27 [==============================] - 4s 160ms/step - loss: 0.6262 - accuracy: 0.7857 - val_loss: 0.9865 - val_accuracy: 0.6714 训练 50 个 epoch 每个 epoch 约 27 个 batch（210 训练样本 / batch_size 8） 同时计算验证集指标 首次 epoch 较慢（约 73s），后续 epoch 因 cache() 缓存加速，每个 epoch 只需几秒。\n训练曲线绘制：\ndef plot_history(history): fig, (ax1, ax2) = plt.subplots(2) fig.set_size_inches(18.5, 10.5) ax1.set_title(\u0026#39;Loss\u0026#39;) ax1.plot(history.history[\u0026#39;loss\u0026#39;], label = \u0026#39;train\u0026#39;) ax1.plot(history.history[\u0026#39;val_loss\u0026#39;], label = \u0026#39;val\u0026#39;) ax1.set_ylabel(\u0026#39;Loss\u0026#39;) max_loss = max(history.history[\u0026#39;loss\u0026#39;] + history.history[\u0026#39;val_loss\u0026#39;]) ax1.set_ylim([0, np.ceil(max_loss)]) ax1.set_xlabel(\u0026#39;Epoch\u0026#39;) ax1.legend([\u0026#39;Train\u0026#39;, \u0026#39;Validation\u0026#39;]) ax2.set_title(\u0026#39;Accuracy\u0026#39;) ax2.plot(history.history[\u0026#39;accuracy\u0026#39;], label=\u0026#39;train\u0026#39;) ax2.plot(history.history[\u0026#39;val_accuracy\u0026#39;], label=\u0026#39;val\u0026#39;) ax2.set_ylim([0, 1]) ax2.set_xlabel(\u0026#39;Epoch\u0026#39;) ax2.legend([\u0026#39;Train\u0026#39;, \u0026#39;Validation\u0026#39;]) plt.show() plot_history(history) 八、模型评估 # 混淆矩阵 # def get_actual_predicted_labels(dataset): actual = [ labels for _, labels in dataset.unbatch() ] predicted = model.predict(dataset) actual = tf.stack(actual, axis = 0) predicted = tf.concat(predicted, axis = 0) predicted = tf.argmax(predicted, axis = 1) return actual, predicted def plot_confusion_matrix(actual, predicted, labels, ds_type): cm = tf.math.confusion_matrix(actual, predicted) ax = sns.heatmap(cm, annot=True, fmt=\u0026#39;g\u0026#39;) sns.set(rc={\u0026#39;figure.figsize\u0026#39;: (12, 12)}) sns.set(font_scale = 1.4) ax.set_title(\u0026#39;Confusion matrix for \u0026#39; + ds_type) ax.set_xlabel(\u0026#39;Predicted\u0026#39;) ax.set_ylabel(\u0026#39;Actual\u0026#39;) plt.xticks(rotation = 90) plt.yticks(rotation = 0) ax.xaxis.set_ticklabels(labels) ax.yaxis.set_ticklabels(labels) labels = list(fg.class_ids_for_name.keys()) actual, predicted = get_actual_predicted_labels(test_ds) plot_confusion_matrix(actual, predicted, labels, \u0026#39;test\u0026#39;) 精确率与召回率 # def calculate_classification_metrics(actual, predicted, labels): cm = tf.math.confusion_matrix(actual, predicted) tp = np.diag(cm) precision = dict() recall = dict() for i in range(len(labels)): col = cm[:, i] fp = np.sum(col) - tp[i] row = cm[i, :] fn = np.sum(row) - tp[i] precision[labels[i]] = tp[i] / (tp[i] + fp) recall[labels[i]] = tp[i] / (tp[i] + fn) return precision, recall precision, recall = calculate_classification_metrics(actual, predicted, labels) 精确率：预测为该类中实际正确的比例 召回率：实际为该类中被正确预测的比例 九、预览测试 # 从测试集取一个 batch，对每个视频预测类别并可视化帧序列。\ndef preview_test(): ds = test_ds.take(1) for batch in ds: prediction = model.predict(batch[0]) videos, labels = batch for i in range(len(labels)): images = videos[i] print(classes[np.argmax(prediction[i])]) return to_gif(images) preview_test() 1/1 [==============================] - 0s 84ms/step Walking 十、总结 # 本教程实现了一个完整的视频行为识别系统，关键技术点回顾：\n方面 技术方案 帧采样 跳帧采样（step=15），随机起始位置 帧标准化 resize_with_pad 统一到 224×224，float32 数据管道 tf.data + cache + shuffle + prefetch + batch 卷积核 Conv2Plus1D 分解（空间 + 时间），参数减少 55% 残差连接 跳跃连接 + LayerNorm + Project 维度对齐 空间下采样 ResizeVideo（einops 重排后 Resizing） 优化器 Adam（lr=0.0001） 损失函数 SparseCategoricalCrossentropy 评估指标 准确率、混淆矩阵、精确率、召回率 这种 (2+1)D 卷积架构平衡了计算效率和表达能力，是视频理解任务中非常实用的基线方案。完整的代码兼容 UCF-101、HMDB-51 等标准视频行为数据集，只需调整数据集路径即可迁移。\n","date":"1 May 2025","externalUrl":null,"permalink":"/ai/3dcnn%E8%A7%86%E9%A2%91%E8%A1%8C%E4%B8%BA%E8%AF%86%E5%88%AB/","section":"AI 相关内容","summary":"","title":"3DCNN 实践-视频行为识别","type":"ai"},{"content":"在本教程中，我们将使用 TensorFlow 搭建一个卷积神经网络（CNN），对 Kaggle 的 Microsoft Cats vs Dogs 数据集进行二分类。你将学会完整的流程：数据准备 → 数据清洗 → 数据集划分 → 模型构建 → 训练 → 评估。\n1. 环境准备 # 首先检查 GPU 是否可用。本项目在 Tesla T4 上运行，CUDA 12.2。\nnvidia-smi Fri Aug 9 01:49:22 2024 +---------------------------------------------------------------------------------------+ | NVIDIA-SMI 535.104.05 Driver Version: 535.104.05 CUDA Version: 12.2 | |-----------------------------------------+----------------------+----------------------+ | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+======================+======================| | 0 Tesla T4 Off | 00000000:00:04.0 Off | 0 | | N/A 50C P8 10W / 70W | 0MiB / 15360MiB | 0% Default | +-----------------------------------------+----------------------+----------------------+ 2. 下载数据集 # 从 Kaggle 下载猫狗数据集（约 788 MB）。\n!wget https://www.kaggle.com/api/v1/datasets/download/shaunthesheep/microsoft-catsvsdogs-dataset?datasetVersionNumber=1 --2024-08-09 01:49:23-- https://www.kaggle.com/api/v1/datasets/download/... Resolving www.kaggle.com... 35.244.233.98 Connecting to www.kaggle.com|35.244.233.98|:443... connected. HTTP request sent, awaiting response... 302 Found Location: https://storage.googleapis.com/... [following] --2024-08-09 01:49:23-- https://storage.googleapis.com/... Resolving storage.googleapis.com... 74.125.137.207, ... Connecting to storage.googleapis.com|74.125.137.207|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 825979578 (788M) [application/zip] Saving to: \u0026#39;microsoft-catsvsdogs-dataset?datasetVersionNumber=1\u0026#39; microsoft-catsvsdog 100%[===================\u0026gt;] 787.71M 65.8MB/s in 12s 2024-08-09 01:49:36 (65.7 MB/s) - saved [825979578/825979578] 解压后得到 PetImages/ 目录，包含 Cat/ 和 Dog/ 两个子文件夹，各约 12500 张图片。\nunzip \u0026#39;microsoft-catsvsdogs-dataset?datasetVersionNumber=1\u0026#39; Extracting dataset archive... [Inflating ~25000 images to PetImages/Cat/ and PetImages/Dog/] 3. 数据清洗 # 原始数据集中有少量损坏图片（空文件、格式不对等），需要清理掉。我们用 TensorFlow 的 read_file + decode_image 逐一验证。\nfrom pathlib import Path from tensorflow.io import read_file from tensorflow.image import decode_image def verify(data_dir): for image in sorted(data_dir.glob(\u0026#39;*\u0026#39;)): try: img = read_file(str(image)) img = decode_image(img) if img.ndim != 3: print(f\u0026#34;[FILE_CORRUPT] {str(image).split(\u0026#39;/\u0026#39;)[-1]} DELETED\u0026#34;) image.unlink() except Exception as e: print(f\u0026#34;[ERR] {str(image).split(\u0026#39;/\u0026#39;)[-1]}: {e} DELETED\u0026#34;) image.unlink() data_dir = Path.cwd()/\u0026#39;PetImages\u0026#39; verify(data_dir/\u0026#39;Cat\u0026#39;) verify(data_dir/\u0026#39;Dog\u0026#39;) [FILE_CORRUPT] 10125.jpg DELETED [ERR] 10404.jpg: ... Unknown image file format ... DELETED [FILE_CORRUPT] 10501.jpg DELETED ... [ERR] Thumbs.db: ... Unknown image file format ... DELETED ... [ERR] 11233.jpg: ... Number of channels ... was 2 ... DELETED ... ... (truncated, 62 total lines) 共清理了约 62 个损坏文件，包括空文件、非图片文件（如 Thumbs.db）、灰度图等。\n4. 数据集划分 # 将数据按 90% : 5% : 5% 划分为训练集、验证集和测试集。\n4.1 创建目录结构 # import os try: os.mkdir(\u0026#39;/content/cats-v-dogs\u0026#39;) os.mkdir(\u0026#39;/content/cats-v-dogs/training\u0026#39;) os.mkdir(\u0026#39;/content/cats-v-dogs/validation\u0026#39;) os.mkdir(\u0026#39;/content/cats-v-dogs/test\u0026#39;) os.mkdir(\u0026#39;/content/cats-v-dogs/training/cats\u0026#39;) os.mkdir(\u0026#39;/content/cats-v-dogs/training/dogs\u0026#39;) os.mkdir(\u0026#39;/content/cats-v-dogs/validation/cats\u0026#39;) os.mkdir(\u0026#39;/content/cats-v-dogs/validation/dogs\u0026#39;) os.mkdir(\u0026#39;/content/cats-v-dogs/test/cats\u0026#39;) os.mkdir(\u0026#39;/content/cats-v-dogs/test/dogs\u0026#39;) except OSError: print(\u0026#39;Error failed to make directory\u0026#39;) 4.2 划分脚本 # import random from shutil import copyfile CAT_DIR = \u0026#39;/content/PetImages/Cat\u0026#39; DOG_DIR = \u0026#39;/content/PetImages/Dog\u0026#39; TRAINING_DIR = \u0026#34;/content/cats-v-dogs/training/\u0026#34; VALIDATION_DIR = \u0026#34;/content/cats-v-dogs/validation/\u0026#34; TEST_DIR = \u0026#34;/content/cats-v-dogs/test/\u0026#34; TRAINING_CATS = os.path.join(TRAINING_DIR, \u0026#34;cats/\u0026#34;) VALIDATION_CATS = os.path.join(VALIDATION_DIR, \u0026#34;cats/\u0026#34;) TEST_CATS = os.path.join(TEST_DIR, \u0026#34;cats/\u0026#34;) TRAINING_DOGS = os.path.join(TRAINING_DIR, \u0026#34;dogs/\u0026#34;) VALIDATION_DOGS = os.path.join(VALIDATION_DIR, \u0026#34;dogs/\u0026#34;) TEST_DOGS = os.path.join(TEST_DIR, \u0026#34;dogs/\u0026#34;) INCLUDE_TEST = True def split_data(main_dir, training_dir, validation_dir, test_dir=None, include_test_split=True, split_size=0.9): files = [] for file in os.listdir(main_dir): if os.path.getsize(os.path.join(main_dir, file)): files.append(file) shuffled_files = random.sample(files, len(files)) split = int(0.9 * len(shuffled_files)) train = shuffled_files[:split] split_valid_test = int(split + (len(shuffled_files)-split)/2) if include_test_split: validation = shuffled_files[split:split_valid_test] test = shuffled_files[split_valid_test:] else: validation = shuffled_files[split:] for element in train: copyfile(os.path.join(main_dir, element), os.path.join(training_dir, element)) for element in validation: copyfile(os.path.join(main_dir, element), os.path.join(validation_dir, element)) if include_test_split: for element in test: copyfile(os.path.join(main_dir, element), os.path.join(test_dir, element)) print(\u0026#34;Split sucessful!\u0026#34;) split_data(CAT_DIR, TRAINING_CATS, VALIDATION_CATS, TEST_CATS, INCLUDE_TEST, 0.9) split_data(DOG_DIR, TRAINING_DOGS, VALIDATION_DOGS, TEST_DOGS, INCLUDE_TEST, 0.9) Split sucessful! Split sucessful! 4.3 检查各集合样本数 # print(len(os.listdir(TRAINING_CATS))) print(len(os.listdir(TRAINING_DOGS))) print(len(os.listdir(VALIDATION_CATS))) print(len(os.listdir(VALIDATION_DOGS))) print(len(os.listdir(TEST_CATS))) print(len(os.listdir(TEST_DOGS))) 11227 11218 624 623 624 624 集合 猫 狗 合计 训练集 11,227 11,218 22,445 验证集 624 623 1,247 测试集 624 624 1,248 5. 数据加载与预处理 # 使用 image_dataset_from_directory 加载图片，统一缩放为 128×128，batch size 为 32，并使用 prefetch + cache 加速数据流水线。\nimport matplotlib.pyplot as plt import numpy as np import tensorflow as tf from tensorflow.keras.preprocessing import image_dataset_from_directory ds_train_ = image_dataset_from_directory( \u0026#39;/content/cats-v-dogs/training\u0026#39;, labels=\u0026#39;inferred\u0026#39;, label_mode=\u0026#39;binary\u0026#39;, color_mode=\u0026#39;rgb\u0026#39;, image_size=[128, 128], interpolation=\u0026#39;nearest\u0026#39;, batch_size=32, shuffle=True, ) ds_valid_ = image_dataset_from_directory( \u0026#39;/content/cats-v-dogs/validation\u0026#39;, labels=\u0026#39;inferred\u0026#39;, label_mode=\u0026#39;binary\u0026#39;, image_size=[128, 128], color_mode=\u0026#39;rgb\u0026#39;, interpolation=\u0026#39;nearest\u0026#39;, batch_size=32, shuffle=True, ) ds_test_ = image_dataset_from_directory( \u0026#39;/content/cats-v-dogs/test\u0026#39;, labels=\u0026#39;inferred\u0026#39;, label_mode=\u0026#39;binary\u0026#39;, image_size=[128, 128], color_mode=\u0026#39;rgb\u0026#39;, interpolation=\u0026#39;nearest\u0026#39;, batch_size=32, shuffle=True, ) def convert_to_float(image, label): image = tf.image.convert_image_dtype(image, dtype=tf.float32) return image, label AUTOTUNE = tf.data.experimental.AUTOTUNE ds_train = ( ds_train_ .map(convert_to_float) .cache() .prefetch(buffer_size=AUTOTUNE) ) ds_valid = ( ds_valid_ .map(convert_to_float) .cache() .prefetch(buffer_size=AUTOTUNE) ) ds_test = ( ds_test_ .map(convert_to_float) .cache() .prefetch(buffer_size=AUTOTUNE) ) Found 22445 files belonging to 2 classes. Found 1247 files belonging to 2 classes. Found 1248 files belonging to 2 classes. 检查一个 batch 的数据形状：\nds = ds_train.take(1) for item in ds: image, label = item print(image.shape) print(label.shape) plt.figure() plt.imshow(image[0]) (32, 128, 128, 3) (32, 1) 6. 构建 CNN 模型 # 模型采用多层卷积 + 池化 + Dropout 的结构，最后接全连接层和 sigmoid 输出。\n网络结构概览：\n5 个卷积块，每块包含 1-2 层 Conv2D，后接 MaxPool2D 卷积核数量逐块递增：32 → 64 → 64 → 128 → 128 → 256 Dropout 正则化（rate=0.2）防止过拟合 最终通过 Flatten + Dense(6) + Dense(1, sigmoid) 输出二分类概率 from tensorflow import keras from tensorflow.keras import layers from tensorflow.keras.callbacks import EarlyStopping model = keras.Sequential([ layers.Input([128, 128, 3]), layers.Conv2D(filters=32, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.MaxPool2D(), layers.Conv2D(filters=64, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.Conv2D(filters=64, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.MaxPool2D(), layers.Conv2D(filters=64, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.Conv2D(filters=64, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.MaxPool2D(), layers.Conv2D(filters=128, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Conv2D(filters=128, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.MaxPool2D(), layers.Conv2D(filters=128, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Conv2D(filters=128, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.MaxPool2D(), layers.Conv2D(filters=256, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.Conv2D(filters=256, kernel_size=3, activation=\u0026#39;relu\u0026#39;, padding=\u0026#39;same\u0026#39;), layers.Dropout(0.2), layers.MaxPool2D(), # Head layers.Flatten(), layers.Dense(6, activation=\u0026#39;relu\u0026#39;), layers.Dropout(0.2), layers.Dense(1, activation=\u0026#39;sigmoid\u0026#39;), ]) model.compile( optimizer=tf.keras.optimizers.Adam(epsilon=0.01), loss=\u0026#39;binary_crossentropy\u0026#39;, metrics=[\u0026#39;binary_accuracy\u0026#39;] ) early_stopping = EarlyStopping( min_delta=0.001, patience=20, restore_best_weights=True, ) history = model.fit( ds_train, validation_data=ds_valid, callbacks=[early_stopping], epochs=100, ) 7. 训练过程 # 使用 EarlyStopping 监控验证集 loss，连续 20 轮无改善则自动停止并恢复最优权重。以下是训练日志摘要（46 个 epoch 后触发 early stop）：\nEpoch 1/100 702/702 ━━━━━━━━━━━━━━━━━━━━ 38s 44ms/step - binary_accuracy: 0.5179 - loss: 0.6913 - val_binary_accuracy: 0.6231 - val_loss: 0.6795 Epoch 2/100 702/702 ━━━━━━━━━━━━━━━━━━━━ 30s 35ms/step - binary_accuracy: 0.5858 - loss: 0.6564 - val_binary_accuracy: 0.6047 - val_loss: 0.6465 Epoch 3/100 702/702 ━━━━━━━━━━━━━━━━━━━━ 25s 35ms/step - binary_accuracy: 0.6444 - loss: 0.6063 - val_binary_accuracy: 0.7073 - val_loss: 0.5710 Epoch 4/100 702/702 ━━━━━━━━━━━━━━━━━━━━ 25s 35ms/step - binary_accuracy: 0.6757 - loss: 0.5710 - val_binary_accuracy: 0.7634 - val_loss: 0.5307 Epoch 5/100 702/702 ━━━━━━━━━━━━━━━━━━━━ 25s 35ms/step - binary_accuracy: 0.7055 - loss: 0.5307 - val_binary_accuracy: 0.7883 - val_loss: 0.4909 ... (training output lines omitted) Epoch 41/100 702/702 ━━━━━━━━━━━━━━━━━━━━ 25s 35ms/step - binary_accuracy: 0.9761 - loss: 0.0669 - val_binary_accuracy: 0.9342 - val_loss: 0.1947 Epoch 42/100 702/702 ━━━━━━━━━━━━━━━━━━━━ 41s 35ms/step - binary_accuracy: 0.9761 - loss: 0.0596 - val_binary_accuracy: 0.9399 - val_loss: 0.1936 Epoch 43/100 702/702 ━━━━━━━━━━━━━━━━━━━━ 25s 36ms/step - binary_accuracy: 0.9830 - loss: 0.0464 - val_binary_accuracy: 0.9463 - val_loss: 0.2122 Epoch 44/100 702/702 ━━━━━━━━━━━━━━━━━━━━ 40s 35ms/step - binary_accuracy: 0.9812 - loss: 0.0544 - val_binary_accuracy: 0.9278 - val_loss: 0.2564 Epoch 45/100 702/702 ━━━━━━━━━━━━━━━━━━━━ 25s 36ms/step - binary_accuracy: 0.9838 - loss: 0.0450 - val_binary_accuracy: 0.9310 - val_loss: 0.2071 Epoch 46/100 702/702 ━━━━━━━━━━━━━━━━━━━━ 25s 35ms/step - binary_accuracy: 0.9830 - loss: 0.0492 - val_binary_accuracy: 0.9318 - val_loss: 0.2311 训练关键指标：\n训练集准确率从 51.8% 提升至 98.3% 验证集准确率峰值约 94.8%（Epoch 35） 总耗时约 25 分钟（Tesla T4） 绘制损失和准确率曲线：\nimport pandas as pd history = pd.DataFrame(history.history) history.loc[:, [\u0026#39;loss\u0026#39;, \u0026#39;val_loss\u0026#39;]].plot() history.loc[:, [\u0026#39;binary_accuracy\u0026#39;, \u0026#39;val_binary_accuracy\u0026#39;]].plot(); 8. 模型评估 # 8.1 可视化预测结果 # 展示一个 batch 的预测结果：\nds = ds_test.take(1) for item in ds: prediction = model.predict(item) images, labels = item plt.figure(figsize=(14, 24)) for i in range(len(labels)): plt.subplot(8, 4, i+1) plt.axis(\u0026#39;off\u0026#39;) plt.imshow(images[i]) plt.title(\u0026#39;cat\u0026#39; if prediction[i][0] \u0026lt; 0.5 else \u0026#39;dog\u0026#39;) 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 314ms/step ds = ds_test.take(19) total = 0 correct = 0 for item in ds: prediction = model.predict(item) images, labels = item total += len(labels) correct += sum([ (0. if prediction[i][0] \u0026lt; 0.5 else 1.) == labels[i][0].numpy() for i in range(len(labels)) ]) print(correct) print(total) print(correct / total) 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 64ms/step 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 81ms/step ... (19 batches) 558 608 0.9177631578947368 9. 总结 # 指标 数值 测试集准确率 91.8% 验证集峰值准确率 94.8% 训练轮数 46（EarlyStopping 触发） 模型参数量 ~12M（CNN 架构） 单 epoch 耗时 ~25-42s（Tesla T4） 项目用不到 30 分钟就训练出了一个能准确区分猫狗的 CNN 模型，后续可以通过数据增强、迁移学习（如使用预训练的 MobileNet/VGG16）或更复杂的架构进一步提升准确率。\n","date":"2 December 2024","externalUrl":null,"permalink":"/ai/catvsdog/","section":"AI 相关内容","summary":"","title":"Cats vs Dogs — 猫狗图像分类实战教程","type":"ai"},{"content":"本教程使用 TensorFlow 和预训练的 VGG19 网络，实现神经风格迁移（Neural Style Transfer, NST）。通过分离图片的内容特征和风格特征，将一张风格图的艺术风格迁移到一张内容图上，生成具有特定艺术风格的新图片。\n1. 环境准备 # 导入所需的库：\nimport tensorflow as tf import os import IPython.display as display import matplotlib.pyplot as plt import matplotlib as mpl import numpy as np import PIL.Image import time import functools # 确保从 TensorFlow Hub 加载压缩模型 os.environ[\u0026#39;TFHUB_MODEL_LOAD_FORMAT\u0026#39;] = \u0026#39;COMPRESSED\u0026#39; mpl.rcParams[\u0026#39;axes.grid\u0026#39;] = False 2. 数据集下载 # 设置工作目录：\n!test ! -d /content/NST \u0026amp;\u0026amp; mkdir /content/NST %cd /content/NST /content/NST 下载内容图（一只黄色拉布拉多犬）和风格图（康定斯基的抽象画）：\ncontent_path = tf.keras.utils.get_file( \u0026#39;YellowLabradorLooking_new.jpg\u0026#39;, \u0026#39;https://storage.googleapis.com/download.tensorflow.org/example_images/YellowLabradorLooking_new.jpg\u0026#39; ) style_path = tf.keras.utils.get_file( \u0026#39;kandinsky5.jpg\u0026#39;, \u0026#39;https://storage.googleapis.com/download.tensorflow.org/example_images/Vassily_Kandinsky%2C_1913_-_Composition_7.jpg\u0026#39; ) Downloading data from https://storage.googleapis.com/download.tensorflow.org/example_images/YellowLabradorLooking_new.jpg 83281/83281 ━━━━━━━━━━━━━━━━━━━━ 0s 3us/step Downloading data from https://storage.googleapis.com/download.tensorflow.org/example_images/Vassily_Kandinsky%2C_1913_-_Composition_7.jpg 195196/195196 ━━━━━━━━━━━━━━━━━━━━ 1s 3us/step 3. 图像预处理 # 3.1 Tensor 转图像 # def tensor_to_image(tensor): tensor = tensor * 255 tensor = np.array(tensor, dtype=np.uint8) if np.ndim(tensor) \u0026gt; 3: assert tensor.shape[0] == 1 tensor = tensor[0] return PIL.Image.fromarray(tensor) 3.2 图像加载与缩放 # 将图片最长边缩放到 512 像素，保持宽高比，并添加 batch 维度：\ndef load_img(path): max_dim = 512 img = tf.io.read_file(path) img = tf.image.decode_image(img, channels=3) img = tf.image.convert_image_dtype(img, tf.float32) shape = tf.cast(tf.shape(img)[:-1], tf.float32) long_dim = max(shape) scale = max_dim / long_dim new_shape = tf.cast(shape * scale, tf.int32) img = tf.image.resize(img, new_shape) img = img[tf.newaxis, :] return img 3.3 辅助显示函数 # def imshow(img, title=None): if len(img.shape) \u0026gt; 3: img = tf.squeeze(img, axis=0) plt.axis(\u0026#39;off\u0026#39;) plt.imshow(img) if title: plt.title(title) 加载内容图和风格图：\ncontent_img = load_img(content_path) style_img = load_img(style_path) plt.subplot(1, 2, 1) imshow(content_img, \u0026#39;Content Image\u0026#39;) plt.subplot(1, 2, 2) imshow(style_img, \u0026#39;Style Image\u0026#39;) \u0026lt;Figure size 640x480 with 2 Axes\u0026gt; 左图为内容图（拉布拉多犬），右图为风格图（康定斯基抽象画）。\n4. 构建 VGG19 特征提取器 # NST 的核心思想是利用预训练 VGG19 网络不同层的输出来分别表示内容和风格。\n4.1 提取中间层输出 # def vgg_layers(layer_names): vgg = tf.keras.applications.VGG19(include_top=False, weights=\u0026#39;imagenet\u0026#39;) vgg.trainable = False # 冻结 VGG，不参与训练 outputs = [vgg.get_layer(name).output for name in layer_names] model = tf.keras.Model([vgg.input], outputs) return model 4.2 Gram 矩阵 — 风格表示 # 风格由特征图之间的相关性定义。Gram 矩阵计算特征通道间的内积，捕获纹理、颜色分布等风格信息：\ndef gram_matrix(input_tensor): result = tf.linalg.einsum(\u0026#39;bijc,bijd-\u0026gt;bcd\u0026#39;, input_tensor, input_tensor) input_shape = tf.shape(input_tensor) num_locations = tf.cast(input_shape[1] * input_shape[2], tf.float32) return result / num_locations 4.3 内容-风格模型 # 将 VGG19 的中间层输出封装成一个 Keras Model，分别提取风格层和内容层的特征：\nclass StyleContentModel(tf.keras.models.Model): def __init__(self, style_layers, content_layers): super(StyleContentModel, self).__init__() self.vgg = vgg_layers(style_layers + content_layers) self.style_layers = style_layers self.content_layers = content_layers self.num_style_layers = len(style_layers) self.vgg.trainable = False def call(self, inputs): inputs = inputs * 255.0 preprocessed_input = tf.keras.applications.vgg19.preprocess_input(inputs) outputs = self.vgg(preprocessed_input) style_outputs, content_outputs = ( outputs[:self.num_style_layers], outputs[self.num_style_layers:], ) style_outputs = [gram_matrix(style_output) for style_output in style_outputs] content_dict = { content_name: value for content_name, value in zip(self.content_layers, content_outputs) } style_dict = { style_name: value for style_name, value in zip(self.style_layers, style_outputs) } return {\u0026#39;content\u0026#39;: content_dict, \u0026#39;style\u0026#39;: style_dict} 4.4 选择特征层 # style_layers = [ \u0026#39;block1_conv1\u0026#39;, \u0026#39;block2_conv1\u0026#39;, \u0026#39;block3_conv1\u0026#39;, \u0026#39;block4_conv1\u0026#39;, \u0026#39;block5_conv1\u0026#39;, ] content_layers = [\u0026#39;block5_conv2\u0026#39;] num_style_layers = len(style_layers) num_content_layers = len(content_layers) extractor = StyleContentModel(style_layers, content_layers) style_targets = extractor(style_img)[\u0026#39;style\u0026#39;] content_targets = extractor(content_img)[\u0026#39;content\u0026#39;] 风格层：使用 5 个浅层到深层的特征图，捕获全局风格信息 内容层：使用 block5_conv2，保留高层次内容结构 5. 损失函数 # 5.1 风格损失与内容损失 # 内容损失：生成图与内容图在内容层输出的 MSE 风格损失：生成图与风格图在风格层 Gram 矩阵的 MSE style_weight = 1e-2 content_weight = 1e4 def style_content_loss(outputs): style_outputs = outputs[\u0026#39;style\u0026#39;] content_outputs = outputs[\u0026#39;content\u0026#39;] style_loss = tf.add_n([ tf.reduce_mean((style_outputs[name] - style_targets[name]) ** 2) for name in style_outputs.keys() ]) style_loss *= style_weight / num_style_layers content_loss = tf.add_n([ tf.reduce_mean((content_outputs[name] - content_targets[name]) ** 2) for name in content_outputs.keys() ]) content_loss *= content_weight / num_content_layers return style_loss + content_loss 5.2 总变差损失（平滑正则化） # 加入总变差（Total Variation）损失抑制噪声，使输出图像更平滑：\ntotal_variation_weight = 30 6. 优化器与训练步骤 # 使用 Adam 优化器，直接优化输入图像的像素值：\noptimizer = tf.keras.optimizers.Adam(learning_rate=0.02, beta_1=0.99, epsilon=1e-1) @tf.function() def train_step(img): with tf.GradientTape() as tape: outputs = extractor(img) loss = style_content_loss(outputs) loss += total_variation_weight * tf.image.total_variation(img) grad = tape.gradient(loss, img) optimizer.apply_gradients([(grad, img)]) img.assign(tf.clip_by_value(img, clip_value_min=0.0, clip_value_max=1.0)) 每次梯度更新后，将像素值裁剪到 [0, 1] 范围内，确保生成图像合法。\n7. 训练验证 # 以内容图作为初始值开始优化，逐步迁移风格：\nstart_time = time.time() epochs = 10 steps_per_epoch = 100 img = tf.Variable(content_img) step = 0 for n in range(epochs): for m in range(steps_per_epoch): step += 1 train_step(img) print(\u0026#34;. \u0026#34;, end=\u0026#34;\u0026#34;, flush=True) display.clear_output(wait=True) display.display(tensor_to_image(img)) print(f\u0026#39;Train step: {step}\u0026#39;) end_time = time.time() print(\u0026#34;Total time: {:.1f}\u0026#34;.format(end_time - start_time)) Train step: 1000 Total time: 81.8 共训练 10 个 epoch（1000 步），耗时约 82 秒（GPU 加速）。每一步生成图的内容结构和风格纹理都会逐渐逼近目标。\n8. 总结 # 项目 说明 模型 VGG19（ImageNet 预训练，冻结权重） 内容图 黄色拉布拉多犬（512 像素最长边） 风格图 康定斯基抽象画 Composition 7 内容层 block5_conv2 风格层 block1~5_conv1（5 层） 损失函数 内容 MSE + 风格 Gram MSE + 总变差正则化 优化器 Adam（lr=0.02, beta_1=0.99, epsilon=1e-1） 训练步数 1000 步（10 epochs × 100 steps） 训练耗时 ~82 秒 本教程基于 Gatys et al. (2016) 的经典 NST 方法，通过预训练 VGG19 分离图像的内容与风格特征，利用梯度下降直接优化像素，实现任意风格图到内容图的艺术风格迁移。\n","date":"2 March 2025","externalUrl":null,"permalink":"/ai/nst_%E5%9B%BE%E7%89%87%E9%A3%8E%E6%A0%BC%E8%BF%81%E7%A7%BB/","section":"AI 相关内容","summary":"","title":"NST 实践-图片风格迁移","type":"ai"},{"content":" 前言 # MCP 基于大模型，相当于 HTTP 基于互联网。\nMCP 是目前大火的概念。不少人认为，MCP 基于大模型，相当于 HTTP 基于互联网。\n对此我比较认同。当我看到 MCP 时，第一感受是：做应用开发的程序员终于有活干了。\n大模型终究是巨头们竞争的舞台，就像操作系统一样，硝烟平息之后，可能只剩下那么几家。\n整个产业的繁荣，还是得依靠大量的应用开发者，就像 iOS 和 Android 平台上的千万开发者推动互联网的浪潮一样。\n当然，在 AI 编程工具的竞争下，程序员的需求量势必是会下降的，这个就是后话了。\n现在我们要做的就是，学好 AI 技术，用好 AI 编程工具，学好 MCP.\nMCP 是什么 # MCP (Model Context Protocol，模型上下文协议) 是由 Anthropic 在 2024 年底推出的一种开放协议，它通过提供一种标准化的接口，旨在通过标准化的接口实现大语言模型 (LLM) 与外部数据源及工具的无缝集成。\n最初推出时，仅有 Claude 的桌面应用支持，市场反响平平，且不乏质疑之声。但近期，随着诸多 AI 编辑器 (如 Cursor、Windsurf，以及 Cline 等插件) 纷纷加入对 MCP 的支持，其热度逐渐攀升，已然展现出成为事实标准的潜力。\nBefore / after MCP # 为何 MCP 会诞生，它要解决什么问题？\n理解 MCP，先要了解其为何诞生。\n即 MCP 出现之前，我们面临什么问题，而 MCP 又如何解决这一问题。\nBefore MCP\n最初，大语言模型确实掌握了语言，但也仅限语言。\n问黄鹤楼的历史，它能 balabala 告诉我们一大堆。\n问能否帮忙在 blender 创建某种场景，它能告诉我们操作步骤，但也仅此而已。\n「只能说，不能做。」\n问我有没有新邮件，它只能说，抱歉，没有能力读取你的邮箱信息。\n「只能获取公开信息，获取不了私域信息。」\nFunction Calling\n要让大模型突破语言的能力，扩展出更多能力。\n我们需要一些 function 来让大模型 call.\n要让大模型突破语言，扩展出更多能力。\n我们需要一些 function 来让大模型 call.\n2023 年 6 月，OpenAI 首次在 GPT-4 和 GPT-3.5-turbo 中集成了 Function Calling，让大模型具备了调用 function 的能力。这一举措意义重大，首次将大模型从「纯文本生成」升级为「可编程接口」。\n紧随其后，Anthropic（Claude）、Google（Gemini）、Meta（Llama）等厂商陆续推出类似功能。但由于没有形成统一的标准，大家的实现方式各异，在调用格式和调用逻辑上均有差别，导致生态碎片化。\n如上图所示，不同的大模型使用各自实现的 function call 去调用目标服务，未能形成统一生态。\nAfter MCP\nMCP 其实就是标准化的 Function Calling.\n2024 年底，Anthropic 发布 MCP 协议，旨在统一碎片化的 Function Calling 生态。以目前市场、社区热度，以及主要大模型及应用厂商的集成情况来看，MCP 显然已经成为统一标准。\n架构 # MCP 长啥样？\n下图是 MCP 官方提供的架构图，比较简单，总结下来就是：实现了 MCP Client 的 Host 应用，通过同一套 MCP 协议与不同的 MCP Server 对接，各 Server 提供了各自的不同能力。\n我通过下图将其更加具象化地呈现。\n我们的聊天应用实现了一个支持 MCP 协议的 Client，Gmail 服务和 Blender 应用则各自实现了 MCP Server. 就像是装上了标准插头，我们的聊天应用和 Gmail 及 Blender 对接了起来，通过 MCP 协议可以调用它们提供的能力。\nMCP (以及 Function Calling) 在本质上其实和 RPC (Remote Procedure Call) 远程过程调用是相通的。\n即一种专门面向大模型应用场景的 RPC.\n一个小 Demo # 为了更直观地理解 MCP，我们直接实现一个小而完整的 demo 来进行演示。\nMCP 的架构简单，实现起来也不难。\n官方已经提供了各种语言的 SDK 来帮助我们快速完成 Client 及 Server 的开发。\n我们使用 Python 实现一个聊天工具，通过自然语言的交互，可以帮你：\n获取本机工作空间中的文件列表 在本机工作空间中新建文件 🚧 此处插入 Demo 视频\n代码量很小，包括：\nserver: 10+ 行 client: 20+ 行 chat: 100+ 行 其它信息：\nLLM API 提供商：OpenRouter LLM： Google Gemini2.0 pro 代码会贴在最后，不在此处展开。\n实际上，应用本身很简单，关键的是理解协议的原理及流程。\n原理及流程 # 我们举一个日常生活中办事的例子，来类比 MCP。\n假设有一位用户去服务大厅办事，不过是简单地查询一些个人信息（调人才档案），但是却涉及到了跨部门协作（所查询的信息保存在文件管理处）。以往的话，服务大厅会开一个介绍信，告诉用户，凭介绍信去文件管理处，你的档案在那里。\n但是现在，服务大厅和文件管理处都增设了外联部。服务大厅通过外联部与文件管理处取得通讯后，对方直接将用户的档案发送了过来。因而用户不用再多跑一趟腿，直接在服务大厅就办完了事情。\n类比到 MCP，概念映射如下：\n用户：即用户 服务大厅：用户所使用的 agent 软件，如 Claude Desktop、Windsurf、Cline 咨询部：聊天窗口 决策部：大模型 外联部：MCP Client 文件管理处：具备文件管理能力的软件 外联部：MCP Server 执行部：文件管理接口 用户来到 Agent 服务大厅，想知道自己的工作空间中有哪些文件。咨询部将此问题传达给了大模型，大模型经过思考后，认为应该找文件管理处。于是让外联部与文件管理处联系。由文件管理处的执行部门完成文件的查询工作。\n文件管理处完成查询工作后，将结果通过外联部反馈回了服务大厅。决策部根据查询的结果，组织了清晰而友好的回复内容，最终由咨询部向用户进行回复。\n以下是一个更为详细和严谨的时序图。\n关键概念 # 以上我们一直是使用 tool 来举例分析。\n还有一些其它的关键概念我们在此简单提一下。\nResources # Resource 用于 server 向 client 提供可获取的数据。可以包括文件内容、数据库记录、API 返回、系统数据、图片、日志文件，等等。\nTools # Tool 用于 server 向 client 提供可执行的操作。\n它很像是 RPC 远程调用，让 client 具备了超出其运行环境的调用能力。\nPrompts # Server 可以定义好一些 prompt 提示词，这些提示词可以给到 client，由 client 提供给用户及 LLM，从而能够帮助用户及 LLM 更好地完成任务。\nSamplings # Sampling 允许 server 向 client 发送其希望 LLM 执行的操作。\nSampling 的请求和返回均由 client 来传递，client 可以对请求内容进行修改。\nRoots # Roots 用于定义 server 聚焦的资源边界。client 可以通过它来告诉 server 应该聚焦于哪些资源。\nRoots 以 URI (Uniform Resource Identifier) 的形式提供，包括 HTTP URL、文件路径等。\n如：\nfile:///home/user/projects/myapp \u0026lt;https://api.example.com/v1\u0026gt; 应用场景 # 实体 → 数字化 → agentic 化\nMCP 的应用场景非常有潜力，想想互联网浪潮，一切都数字化了，而在将来，一切都要 agentic 化。这些都是 MCP 施展的地方。\n在此不展开哪个具体场景。不妨看看目前市场上已有的 MCP Server，获取能够受到启发。\n以下是两个 MCP Server 商城页面的截图，从 categories 中可以看到非常广泛的类别。\n另外提一下，连支付宝都已经支持 MCP 了。以后 agent 就可以自己找我们收钱了，还挺赛博的。。。\n参考资料 # MCP 官网\nhttps://modelcontextprotocol.io/\nMCP 商城\nhttps://mcp.so/\nhttps://smithery.ai/\n","date":"2 August 2025","externalUrl":null,"permalink":"/ai/mcp%E5%8D%8F%E8%AE%AE%E8%AF%A6%E8%A7%A3/","section":"AI 相关内容","summary":"","title":"MCP 协议详解","type":"ai"},{"content":"对于很多对 AI 技术感兴趣的小伙伴来说，算力资源是挡在面前的第一道门槛。\n幸而，我们有了 AutoDL。\nAutoDL的目标是为用户提供稳定可靠、价格公道的GPU算力，让GPU不再是您成为数据科学家道路上的拦路石。\n数个月的使用当中，AutoDL 使我的的 AI 研究过程相当的愉悦。在提高效率和省钱方面，我也整理了一些心得。\n无卡模式 # ”就凭这个模式，AutoDL 已经配享太庙了。“\n众所周知，机器学习最耗时的并不是训练过程，而是环境和数据的准备。尤其是国内网络环境下，依赖包、数据集的下载，可谓时间黑洞。让我们昂贵的 GPU ，等着我们的网络龟速搬数据，那我们的钱包可等不起。\n后文我们再说下载加速，此处我们单表这个无卡模式。\n所谓无卡模式，即在不占用 GPU 资源的状态下开机，此时我们的花费降低至 ¥0.1/小时！！我们可以在该模式下完成环境搭建、依赖安装、数据集下载，而毫无金钱压力。等环境、数据、代码全部都准备好了，我们再以 GPU 模式开机，脚本一跑，训练工作便无缝地开始了。\n如图所示，在关机状态下，更多下拉框中，即可看到「无卡模式开机」选项。\n定时关机 # 垂死病中惊坐，你的实例没关机！！😱💭\n”别开玩笑，我设定了定时关机。“ 🤗\n通过控制台，我们可以很方便地设定定时关机。当然，这只是一个保底策略，确保我们不会因为忘了关机，而最终收到一张欠费账单。\n另一个常见情况是，我们希望在程序执行完毕后关机。这种情况下我们可以使用 shutdown 命令。\n# 假设您的程序原执行命令为 python train.py # 那么可以在您的程序后跟上shutdown命令 python train.py; /usr/bin/shutdown # 用;拼接意味着前边的指令不管执行成功与否，都会执行shutdown命令 python train.py \u0026amp;\u0026amp; /usr/bin/shutdown # 用\u0026amp;\u0026amp;拼接表示前边的命令执行成功后才会执行shutdown。请根据自己的需要选择 或者我们可以直接在 python 中调用 shutdown.\nimport os if __name__ == \u0026#34;__main__\u0026#34;: # xxxxxx os.system(\u0026#34;/usr/bin/shutdown\u0026#34;) 小程序控制台 # 垂死病中惊坐，你的实例没关机！！😱💭\n“而且你也没有设定时关机！” 😱💬\n掏出手机，搞定。 🤗\n使用 AutoDL 微信小程序，无需打开电脑，即可完成实例开关机操作。\n订阅GPU通知 # ”客官，你的 GPU 来啦！“\n如果你想要的 GPU 资源不足，无需盯着屏幕苦苦等待。订阅一下 GPU，可以先去水一下啦。资源到位了，自然会通过微信通知你。\n社区镜像 # ”不用装依赖啦，环境我们都已经给你搭好了，啥环境都有啊。“\nAutoDL 不仅是一个算力平台，还是一个镜像平台。\n在 AutoDL 的镜像社区(https://www.codewithgpu.com/image)里，有上千个深度学习的 docker 镜像，StableDiffusion、Flux、SoVITS、ComfyUI 应有尽有。创建实例时直接选用。甚至已有实例也可以直接切换镜像。真正做到了开箱即用。装依赖、搭环境的整套流程都不需要我们操心啦。\n同时，我们也可以创建自己的镜像，以更好地适配特定需求。\n保存的过程也很简单，甚至都不需要我们进行 docker 操作。AutoDL 控制台提供了一键保存功能，只需要鼠标一点就行了。\n实例克隆 # 有时，我们当前主机没有足够的空闲 GPU 资源，当我们在同地区租用其他主机时，无需在新机器上重新安装环境。我们只需要使用实例克隆功能，即可将原主机的系统盘和数据盘快速克隆到新的主机上。免去了重新安装环境的烦恼，高效又方便。克隆后，原主机上的数据不会被删除，在新主机上的操作也不会对原主机产生影响。有时我们想要在同样环境的多台主机上进行不同参数的对照实验，也可以使用此功能。\n跨实例拷贝数据 # 相比于实例克隆，跨实例拷贝数据提供了更灵活的数据拷贝功能。如图所示，我们可以将当前实例下的指定路径拷贝到目标实例当中。\n文件存储 # 如果我们的数据非常通用，在多个实例当中都需要用到，那么我们可以将其存放到文件存储当中。文件存储作为一种网络共享存储，可以挂载到同一地区的不同实例当中，实现同一份数据在实例间共享。\n在控制台的文件存储界面，选定目标地区后，点击初始化文件存储。初始化完成后，该文件存储将会挂载至该地区实例的 /root/autodl-fs 路径。\nhuggingface/github 加速 # 针对 huggingface 下载慢的问题，国内的 hf-mirror 镜像(https://hf-mirror.com/)提供的配套方案已经非常成熟。\n我一般是通过设置环境变量：\nexport HF_ENDPOINT=https://hf-mirror.com 当然，也可以使用其开发的下载工具 hfd.\ngithub 加速的话，我一般是用这个项目 https://github.com/ineo6/hosts 提供的 hosts, 更新及时，速率稳定。\nhuggingface 缓存到数据盘 # 默认情况下，huggingface 在下载的时候，会将数据缓存在 ~/.cache/huggingface 也就是主机的系统盘。AI model 一般都很大，这会导致我们的数据盘很快就被占满。\n我们可以通过设置 HF_HOME 环境变量将缓存路径设置到数据盘。\nexport HF_HOME=/root/autodl-tmp/.cache/huggingface 或者我们也可以通过 --local-dir 来指定本次下载路径。\nhuggingface-cli download adept/fuyu-8b --cache-dir ./path/to/cache 自定义服务 # 有时我们想要在云主机中启动 ComfyUI 等 web 服务，AutoDL 为我们准备好了 6006 端口作为开放端口。只需要在控制台中启用该实例的自定义服务功能即可。\n","date":"2 October 2025","externalUrl":null,"permalink":"/ai/autodl%E6%8F%90%E6%95%88%E4%B8%8E%E7%9C%81%E9%92%B1%E5%BF%83%E5%BE%97/","section":"AI 相关内容","summary":"","title":"AutoDL 提效与省钱心得","type":"ai"},{"content":" 欢迎来到我的小站 # 这里是我记录生活，留下数字足迹的地方。\n我会分享一些读书笔记、原创小说、绘画作品。\n以及计算机领域的技术文章。\n我研究的一些东西 # 深度学习 # GAN 模型实践-黑暗之魂游戏画面生成 使用 TensorFlow 搭建深度卷积生成对抗网络（DCGAN），在 FromSoftware 魂系游戏（Dark Souls）截图数据集上训练，实现从随机噪声生成黑暗之魂的游戏画面。 GAN 模型实践-人脸生成 使用 TensorFlow 搭建深度卷积生成对抗网络（DCGAN），在人脸数据集上训练，实现从随机噪声生成人脸。 CNN 模型实践-情绪检测 使用 TensorFlow 构建一个卷积神经网络（CNN）对 48×48 灰度人脸图片进行 7 种情绪分类，再结合 OpenCV 的 Haar Cascade 从电影海报中检测人脸并识别情绪。 3DCNN 实践-视频行为识别 使用 TensorFlow 搭建一个 3DCNN 模型，对视频中的人物行为进行分类。模型采用 (2+1)D 卷积（空间卷积 + 时间卷积分离）和残差连接，在 Kaggle 人体动作识别视频数据集上进行训练。实现对视频中的人物行为进行分类。 NST 实践-图片风格迁移 使用 TensorFlow 和预训练的 VGG19 网络，实现神经风格迁移（Neural Style Transfer, NST）。通过分离图片的内容特征和风格特征，将一张风格图的艺术风格迁移到一张内容图上，生成具有特定艺术风格的新图片。 计算机图形学 # 3D软渲染程序sRenderer sRenderer 是一个纯软件渲染器——每一个像素都完全在 CPU 上计算完成，不依赖任何 GPU。没有 OpenGL，没有 Vulkan，没有 DirectX。唯一的外部依赖是 SFML，它仅用于创建窗口和显示画面。从向量运算到透视投影再到纹理映射，其他所有功能都是从零开始构建的。 3D游戏引擎Quack Quack — 一款用 C++ 编写的轻量级 3D 游戏引擎，致敬 id Software 的传奇引擎 Quake。 光栅化渲染的朴素原理 光栅化渲染是一种将 3D 场景渲染为 2D 图像计算机图形学渲染技术。它通过将 3D 模型的每个像素转换为 2D 坐标，将 3D 模型渲染到 2D 显示屏幕上。 Bounding Volume Hierarchy Bounding Volume Hierarchy（BVH）是一种用于加速 3D 场景渲染的场景分层结构。它通过将场景中的所有对象组织成一个树状结构，BVH 能够快速地确定哪些对象与给定的查询点相交，从而加速渲染过程。 ","date":"2 October 2025","externalUrl":null,"permalink":"/","section":"","summary":"","title":"","type":"page"},{"content":"","date":"2 October 2025","externalUrl":null,"permalink":"/tags/ai/","section":"Tags","summary":"","title":"AI","type":"tags"},{"content":"","date":"2 October 2025","externalUrl":null,"permalink":"/ai/","section":"AI 相关内容","summary":"","title":"AI 相关内容","type":"ai"},{"content":"","date":"2 October 2025","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"2 August 2025","externalUrl":null,"permalink":"/tags/agent/","section":"Tags","summary":"","title":"Agent","type":"tags"},{"content":"","date":"2 August 2025","externalUrl":null,"permalink":"/tags/mcp/","section":"Tags","summary":"","title":"MCP","type":"tags"},{"content":"","date":"1 May 2025","externalUrl":null,"permalink":"/tags/3dcnn/","section":"Tags","summary":"","title":"3DCNN","type":"tags"},{"content":" What is Bounding Volume Hierarchy # Bounding Volume Hierarchy (BVH) is a self-balancing binary tree structure, each node stores the information about bounding volume wraps the node and all its children. It was widely used in collision detection by game engines.\nBVH is a typical application of self-balancing binary tree, it’s efficient, elegant, and actually simple.\nIn this article, we will explain how BVH work, and how to implement it. We will explain it in 2D, which is easier to understand, then smoothly extend to 3D. Also, for simplify, we will use Axis-Aligned Bounding Box (AABB) to represent the bounding volume, which is the simplest situation and we will introduce it soon.\nRequirements # We assume you meet following requirements:\nFamiliar with AVL tree and know how to implement it Can read C++ code (or you can just skip the implementation and implement it by yourself with your preferred language) Overview # In the classic FC game Battle City, players driving a tank to shoot with enemies. When player pressed the fire button, a cannon shot out from the tank, flying straight to enemies. Then it’s the computer’s (actually programmer’s) duty to checkout if the cannon collided with something.\nThere are many objects on screen, the player, enemies, walls, etc. And to provide a good experience, we have to output 30-60 frames every second. So we must finish our collision check in a short period with a efficient algorithm.\nLet’s simplify our scenario to basic geometries, stars are the objects may be collided, and the arrow is the cannon’s moving direction.\nOur goal is to find out the object been hit, which we have filled with red color.\nAxis-Aligned Bounding Box (AABB) # Here in our demonstration, we use a star representing collision targets, which is much simplified. In real games, objects are much more complicated. It’s very helpful if we use a container to wrap our object, and only check if we have collided with this container. AABB is the simplest container for us.\nAABB is the bounding box of a shape, which is axis aligned, as the name indicates. Here we have a 2D geometry in the diagram, it’s AABB is the box made by (minX, minY) and (minY, maxY). So, for a AABB, we can use only two points to represent it.\nThe node in our tree should looks like:\n// Node in the tree class Node { float x1, y1; // the lower point of AABB float x2, y2; // the higher point of AABB Node *parent; // for backtrack Node *left; Node *right; bool isLeaf; // is leaf or internal node Node(float x1, float y1, float x2, float y2, bool isLeaf); } 💡 it’s easy to extend the definition to 3D, which represent a 3D box with points (minX, minY, minZ) and (maxX, maxY, maxZ). For simplify, we will only discuss 2D geometries for now. When we have understood the concept and finished the implement. We will briefly introduce how to extend it to 3D, and source code will be provided. After applying AABB, we have simplified the complex of object’s shape, and our task now was shown as diagram above.\nBut then comes with the complex of object’s number. In real games, the number of objects are huge, it’s too inefficient to check collision with them one by one. Since we have said that BVH is a tree structure, you may have come out the idea of dividing them.\nHere we have divided our objects into two groups. After checking the collision container, we can quickly drop out the bottom group, which contains 4 objects. It saves a lot of time.\nBut there are vary ways to groups them, how do we choose the best group way to build the AVL tree most efficient to search hit target? There must be some evaluation.\nSurface Area Heuristic (SAH) # Think about the question: here we have 3 shapes (a rectangle, a triangle, and a line) in the 2D plane, which shape has the max probability to be hit?\nAware that the bullet may come from any direction.\nIntuitively, you may think the one with bigger area is more possibly being hit. But think a minute more, does the line, with zero area, has no possibly to be hit?\nThe answer is: the object with longer perimeter is more likely to be hit.\nThink about these two rectangles, and we only consider bullet come from 4 directions. From each direction, we can only see a side line of the rectangle, longer the line, higher probability to be hit. We sum up the side line in 4 directions, and we got the perimeter.\nIn real case, bullets come from any directions. So it’s more precise to get the integral of geometry’s projection in every direction. But it’s OK to just use perimeter.\nfloat getSA(Node *node) { if (!node) return 0; // since AABB is axis aligned, // it\u0026#39;s easy to compute perimeter return 2.0f * (node-\u0026gt;x2 - node-\u0026gt;x1 + node-\u0026gt;y2 - node-\u0026gt;y1); } In 3D, bullets come from 6 directions to hit the box, bigger the surface area, higher probability to be hit. Also, integral of box’s projection area in every direction is more precise.\nThis is Surface Area Heuristic (SAH), the probability of a object being hit is proportional to it’s surface area.\n💡 Though the concept of SAH was proposed in 3D scene, people still use the name in 2D, but calculate the perimeter instead of surface area in implementation. 💡 Conclusion In 2D, SAH is the object’s perimeter. Longer perimeter, higher probability to be hit. In 3D, SAH is the object’s surface area. Bigger surface area, higher probability to be hit. Cost metric of a tree # We define the cost metric of a tree as the sum of all it’s nodes’ SAH, including root, internal nodes and leaves.\n$$ (C(T) = \\sum_{i \\in Nodes} SA(i)) $$The tree with less cost means that it’s bounding boxes (including internal boxes) are more tight, less waste and more precise.\n💡 Remember that SA mean Surface Area in 3D, but actually perimeter in 2D. Here we have two trees representing two different divisions.\nCompare these two, we can find out that their root and leaves’ SA have no difference, only internal nodes varies. That is to say, when computing the cost of a tree, we can ignore root and leaves, only compute internal nodes.\n💡 Actually we have no need to compute the SAH of a whole tree, thanks to the greedy strategy, we get global optimal solution from locally optimal solution. Tree # Here is the definition of Tree class.\nclass Tree { // dummy node is a commonly-used trick to modify tree safely Node *dummyRoot; void insertLeaf(Node *newLeaf); void removeLeaf(Node *rmLeaf); void updateLeaf(Node *updLeaf, float x1, float y1, float x2, float y2); Tree(); } Insert # A tree begins with a insertion. Don’t be afraid, SAH will guide us to find out the best place to insert.\nSAH insertion cost # The cost of a insertion is the increased SAH after insertion.\nObserve the change after insert leaf node 12 as sibling of node 10. As we marked out, only the internal nodes in the path matters. node C and node G’s SAH changed, and node J is the new created internal node.\nSo the cost should be:\n$$ \\Delta SA(C) + \\Delta SA(G) + SA(J) $$There are two parts:\nDirect cost: SA(J)\nThe SA of new created internal node\nInherited cost: ΔSA(C) + ΔSA(G)\nThe increased SA caused by refitting the ancestor’s bounding boxes\nBranch and Bound Algorithm # Every node in the tree is a potential sibling of our new leaf. It’s expensive to evaluate all of them. Here is the Branch and Bound Algorithm to speed it up.\nThe main idea is:\nSearch though tree recursively Skip sub-trees that cannot possibly better Every node in the tree is a potential sibling of our new leaf. It’s expensive to evaluate all of them. Here is the Branch and Bound Algorithm to speed it up.\nThe main idea is:\nSearch though tree recursively Skip sub-trees that cannot possibly better Suppose that the new leaf node 12 has arrived the position of node E, we have to make a choice from these 3 options:\nstay here, which means we choose current node as new leaf’s sibling go left child and keep seeking go right child and keep seeking What if we choose to stay here?\nWe will create a new internal node K, and make it be the parent of node E and node 12. The cost of doing this is:\n$$ cost = \\Delta SA(A) + \\Delta SA(B) + SA(K) $$Since the inherited cost ( ΔSA(A) + ΔSA(B) ) will keep the same whatever which position the new leaf was inserted, so we could just ignore them.\nThen, what is SA(K)? Apparently, it’s the union of node E and node 12.\n$$ cost\\_stay = SA(K) = SA(E∪node12) $$ Union of two AABBs\n// simplified code, some edge cases ignored float getUnionSA(vector\u0026lt;Node*\u0026gt; nodes) { Node *node = nodes[0]; float x1 = node-\u0026gt;x1; float y1 = node-\u0026gt;y1; float x2 = node-\u0026gt;x2; float y2 = node-\u0026gt;y2; for (int i = 1; i \u0026lt; nodes.size(); i++) { node = nodes[i]; x1 = min(x1, node-\u0026gt;x1); y1 = min(y1, node-\u0026gt;y1); x2 = max(x2, node-\u0026gt;x2); y2 = max(y2, node-\u0026gt;y2); } return 2.0f * (x2 - x1 + y2 - y1); } And the cost of choosing left child (node H) and right child (node I) is:\n$$ cost\\_left = \\sout{\\Delta SA(A) + \\Delta SA(B)} + \\Delta SA(E) + SA(H∪node12) $$$$ cost\\_right = \\sout{\\Delta SA(A) + \\Delta SA(B)} + \\Delta SA(E) + SA(I∪node12) $$and, ΔSA(E) = SA(K) - SA(E), because the impact to current hierarchy is the same though the leaf was inserted in children.\nSince the costs of all these 3 options have revealed, we now have enough information to make decision.\nOnce we had found the best sibling, we create a new internal node, make it be the parent of sibling and new leaf. Then we trackback to root, refit all ancestors.\n// simplified code, some edge cases ignored void insertLeaf(Node *newLeaf) { if (dummyRoot-\u0026gt;left == NULL) { dummyRoot-\u0026gt;left = newLeaf; newLeaf-\u0026gt;parent = dummyRoot; return; } // ==== stage 1: find the best sibling ==== Node *sibling = findBestSibling(dummyRoot-\u0026gt;left, newLeaf); // ==== stage 2: create new parent add insert ==== Node *newParent = new Node( std::min(sibling-\u0026gt;x1, newLeaf-\u0026gt;x1), std::min(sibling-\u0026gt;y1, newLeaf-\u0026gt;y1), std::max(sibling-\u0026gt;x2, newLeaf-\u0026gt;x2), std::max(sibling-\u0026gt;y2, newLeaf-\u0026gt;y2), false ); Node *oldParent = sibling-\u0026gt;parent; if (oldParent-\u0026gt;left == sibling) oldParent-\u0026gt;left = newParent; else oldParent-\u0026gt;right = newParent; newParent-\u0026gt;parent = oldParent; newParent-\u0026gt;left = sibling; newParent-\u0026gt;right = newLeaf; sibling-\u0026gt;parent = newParent; newLeaf-\u0026gt;parent = newParent; // ==== stage 3: trackback ro root, refit and rotate ==== Node *parent = oldParent; while (parent) { refit(parent); rotate(parent); parent = parent-\u0026gt;parent; } } Node* findBestSibling(Node *node, Node *leaf) { while (node \u0026amp;\u0026amp; !node-\u0026gt;isLeaf) { float cost = getUnionSA({node, leaf}); float delta = cost - getSA(node); float costLeft = node-\u0026gt;left ? getUnionSA({node-\u0026gt;left, leaf}) : cost + 1.0f; float costRight = node-\u0026gt;right ? getUnionSA({node-\u0026gt;right, leaf}) : cost + 1.0f; if (cost \u0026lt; costLeft \u0026amp;\u0026amp; cost \u0026lt; costRight) break; if (costLeft \u0026lt; costRight) node = node-\u0026gt;left; else node = node-\u0026gt;right; } return node; } Remove # Removing a leaf is intuitional, we just delete it from parent, and backtrack to root, refit all ancestors in path.\n// simplified code, some edge cases ignored void removeLeaf(Node *rmLeaf) { // delete Node *parent = rmLeaf-\u0026gt;parent; if (parent-\u0026gt;left == rmLeaf) parent-\u0026gt;left = NULL; else parent-\u0026gt;right = NULL; // trackback to root, refit and rotate while (parent) { refit(parent); rotate(parent); parent = parent-\u0026gt;parent; } } Update # To update a leaf, we just remove it from the tree, and insert a new one with updated value.\n// simplified code, some edge cases ignored void updateLeaf(Node *updLeaf, float x1, float y1, float x2, float y2) { // remove removeLeaf(updLeaf); // insert with updated value insertLeaf(new Node(x1, y1, x2, y2, true)); } Rotate # We have not finish yet, we need another operation named rotation. Without it, the scenario named sorted input could easily wreck our tree.\nThere are 4 objects in a row, and was added to scene one by one, from 1 to 4. After 4 times of insertion, our tree becomes the right. I has no difference to a linked list.\nWe have to dynamically rotate our tree to avoid this.\nThere are 4 kinds of rotations:\ntype 1: exchange C and D\nonly SA(B) changed: SA(D∪E) → SA(F∪G∪E)\ntype 2: exchange C and E\nonly SA(B) changed: SA(D∪E) → SA(D∪F∪G)\ntype 3: exchange B and F\nonly SA(C) changed: SA(F∪G) → SA(D∪E∪G)\ntype 4: exchange B and G\nonly SA(C) changed: SA(F∪G) → SA(F∪D∪E)\nOnce we have inserted or removed a node, while backtracking to root, for every ancestor, try to rotate it with max SA drop (if exist), after refitting.\n// simplified code, some edge cases ignored void rotate(Node *node) { Node *left = node-\u0026gt;left; Node *right = node-\u0026gt;right; if (!(left \u0026amp;\u0026amp; right)) return; Node *ll = left-\u0026gt;left; Node *lr = left-\u0026gt;right; Node *rl = right-\u0026gt;left; Node *rr = right-\u0026gt;right; // ==== compute delta SA for 4 types of rotations ==== float SA_B = getSA(left); float SA_C = getSA(right); float SA_FGE = getUnionSA({rl, rr, lr}); float SA_DFG = getUnionSA({ll, rl, rr}); float SA_DEG = getUnionSA({ll, lr, rr}); float SA_FDE = getUnionSA({rl, ll, lr}); float delta1 = SA_FGE - SA_B; float delta2 = SA_DFG - SA_B; float delta3 = SA_DEG - SA_C; float delta4 = SA_FDE - SA_C; // ==== choose the one with max drop ==== std::array\u0026lt;float, 4\u0026gt; deltas = { delta1, delta2, delta3, delta4 }; float minDelta = 0; int pickIndex = -1; for (int i = 0; i \u0026lt; 4; i++) { if (deltas[i] \u0026lt; minDelta) { minDelta = deltas[i]; pickIndex = i; } } switch (pickIndex) { case -1: // do not rotate break; case 0: // rotate type 1 node-\u0026gt;right = ll; ll \u0026amp;\u0026amp; (ll-\u0026gt;parent = node); left-\u0026gt;left = right; right-\u0026gt;parent = left; refit(left); break; case 1: // rotate type 2 node-\u0026gt;right = lr; lr \u0026amp;\u0026amp; (lr-\u0026gt;parent = node); left-\u0026gt;right = right; right-\u0026gt;parent = left; refit(left); break; case 2: // rotate type 3 node-\u0026gt;left = rl; rl \u0026amp;\u0026amp; (rl-\u0026gt;parent = node); right-\u0026gt;left = left; left-\u0026gt;parent = right; refit(right); break; case 3: // rotate type 4 node-\u0026gt;left = rr; rr \u0026amp;\u0026amp; (rr-\u0026gt;parent = node); right-\u0026gt;right = left; left-\u0026gt;parent = right; refit(right); break; default: break; } } Code # We have covered all the key concepts of BVH. With the key codes provided above, it’s not hard to implement the whole thing by yourself.\nAnd here is the complete code for your reference.\nCodeSnippets/BVH at main · Oh-noodles/CodeSnippets\nTo 3D # References # box2d.org\n","date":"3 November 2024","externalUrl":null,"permalink":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6/bounding_volume_hierarchy/","section":"计算机图形学","summary":"","title":"Bounding Volume Hierarchy","type":"计算机图形学"},{"content":"","date":"3 November 2024","externalUrl":null,"permalink":"/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6/","section":"Tags","summary":"","title":"计算机图形学","type":"tags"},{"content":"计算机图形学相关内容。\n","date":"3 November 2024","externalUrl":null,"permalink":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6/","section":"计算机图形学","summary":"","title":"计算机图形学","type":"计算机图形学"},{"content":"平衡二叉排序树查找是对普通二叉排序树查找的改进。其名称——AVL——取自其发明者，G. M. Adelson-Velsky 和 E. M. Landis.\n在普通的二叉排序树查找中，查找的比较次数取决于目标点和根结点的距离，即目标点在树的第几层（从根结点算起）。层数越高，比较的次数越多。处于底层的点，查找效率最低，比较次数等于树的深度。\n我们希望树的深度越小越好。\n想象一棵树如果是左倾（左边深度大于右边深度）或者右倾的，在同样的结点数量下，倾斜幅度越大，树的深度就越大。极端情况，若所有的结点都只有左子树（或只有右子树），树就变成了一个没有分支的线性表。也就不具备了二叉排序树的查找优势。\n因而，我们希望树是平衡的，即每个结点的左子树深度和右子树深度近似。\nAVL 树的核心思想就是在每次二叉排序树的结构发生变化时，对其平衡性进行检查和调整。这一揽子检查和调整的方法颇为复杂，倒也成体系，为此，我们需要先引入一些概念。\n平衡因子\n结点的左子树深度减去右子树深度。\n平衡树\n若一棵树满足二叉平衡树的定义，且树中所有结点的平衡因子的绝对值都不大于 1，则这棵树是一棵平衡树。\n最小不平衡子树\n当我们向树中插入一个新结点后，距离新结点最近的平衡因子绝对值大于 1 的结点为根的子树为最小不平衡子树。\n基本操作 # 我们调整平衡性的操作都是对最小不平衡子树进行的。\n接下来我们来分析几种最小不平衡子树的场景及对应的操作。\n右旋 # 如图所示，根结点的平衡因子为 2，其绝对值大于 1，并且是正值，因而这是一棵左倾（左高右低）的最小不平衡子树。我们需要对其做右旋处理。\n右旋之后，树平衡了。\n从上图来看，似乎右旋操作只是简单地将3号结点变成了2号结点的右孩子，2号结点成了新的根结点。\n然而，上图只是一个极度简化的情况，实际情况可能比这复杂。\n如上图，真实情况下，各个结点都是有子树的。图中圆形代表结点，三角形代表子树。\n在 N 结点插入前，L 结点的平衡因子为 0，P 结点的平衡因子为 1，树是平衡的。（由于在 AVL 算法中，每次树结构发生变化，我们都会调整其平衡性，因而在新结点插入前，树必然是平衡的，我们无需考虑不平衡状态下的插入。）\nN 结点插入后，L 结点的平衡因子变为 1，P 结点平衡因子变为 2，树失衡了。（实际上，P 结点上面可能还有父结点，但是在 AVL 算法中，我们只需要对最小不平衡子树做处理，因而对 P 的父结点及其以上的结点就无需关注了。）\n可以看到，由于子树的存在，一个完整的右旋操作可以归纳为以下几个步骤：\nL 的右子树变为 P 的左子树。 P 变为 L 的右子树。 L 变为新的根。 调整平衡因子，P、L 的平衡因子均变为 0。 左旋 # 通过右旋的类比，我们可以容易地理解左旋操作。二者呈对称关系。\n","date":"3 March 2023","externalUrl":null,"permalink":"/%E7%AE%97%E6%B3%95/avl%E6%A0%91/","section":"算法","summary":"","title":"AVL 树","type":"算法"},{"content":"算法相关内容。\n","date":"3 March 2023","externalUrl":null,"permalink":"/%E7%AE%97%E6%B3%95/","section":"算法","summary":"","title":"算法","type":"算法"},{"content":"","date":"3 November 2022","externalUrl":null,"permalink":"/tags/%E7%AE%97%E6%B3%95/","section":"Tags","summary":"","title":"算法","type":"tags"},{"content":"动态规划（Dynamic Programming）与分治方法相似，都是通过组合子问题的解来求解原问题。分治方法将问题划分为互不相交的子问题，递归地求解子问题，再将它们的解组合起来，求出原问题的解。与之相反，动态规划应用于子问题重叠的情况，即不同的子问题具有公共的子子问题。在这种情况下，分治算法会做许多不必要的工作，它会反复地求解那些公共子子问题。而动态规划算法对每个子子问题只求解一次，将其解保存在一个表格中，从而无需每次求解一个子子问题时都重新计算，避免了不必要的重复工作。\n——《算法导论》\n从动态规划的名字（Dynamic Programming，此处 Programming 指的是表格记录，而非编程）就可看出，表格记录是动态规划的一个关键特征，这也是其与分治法的一个区别。\n在分治法中，当父问题被分解为几个子问题后，尽管子问题又被分解为子子问题，但父问题的解仅由子问题的解计算出来，不直接依赖与子子问题。因而我们在得到下一层（子问题）的解后，就不保存下下一层（子子问题）的解了，即用不到表格记录。而在动态规划问题中，比如背包问题，当我们计算容量为 j 的背包时，可能会用到容量为 j - weight[i] 的背包的解，相比于分治的层层分离，这便是动态规划问题的重叠所在。\n而且，动态规划是一种通过递归思想自上向下分析问题，而通过循环自下而上解决问题的方法。因为问题的依赖性是上向下依赖，只有自下而上，才有可能实现有效的记忆化。\n不同算法应对的场景：\n子问题最优则原始问题最优——贪心算法或者动态规划算法。 子问题最优则原始问题最优，且子问题互相独立——分治算法。 子问题最优不能推导出原始问题最优——暴力搜索等。 狭义动态规划 与 广义动态规划 # 狭义的动态规划是指使用 dp table，以递推的方式来求解。 广义的动态规划可以是备忘录递归，也可以是 dp table 递推。 记忆化搜索（递归） vs 狭义动态规划（递推） # 记忆化搜索是通过记录已经遍历过的状态的信息，从而避免对同一状态重复遍历的搜索实现方式。\n因为记忆化搜索确保了每个状态只访问一次，所以它也属于（广义）动态规划。\n记忆化搜索与递推的代码，在形式上是高度类似的。这是由于他们使用了相同的状态表示方式和状态转移方程。也正因如此，二者的时间复杂度一般是差不多的（由于需要维护递归栈，记忆化搜索的时间开销会大一些），但是由于使用了递归，其空间复杂度要高得多。\n记忆化搜索和递推都确保了同一状态至多只被求解一次，而他们实现这一点的方式略有不同。递推通过设置明确的访问顺序来避免重复访问。而记忆化搜索虽然没有明确的访问顺序，但是通过给已经访问过的子状态打标记的形式，也达到了相同的目的。\n相同点 都有重叠子问题。相对于暴力求解，使用这两种方法的目的，都是为了避免重复计算这些重叠子问题。 都有状态（case）、base case 和状态转移方程。 区别点 记忆化搜索的编程模式是递归，而狭义动态规划（递推 DP） 的编程模式是递推。一般来说，递归的时间和空间开销比递推大。并且递推不一定需要保存所有状态，如 dp[i] = dp[i-1] + dp[i-2] 只需要保存两个状态即可。 记忆化搜索是自顶向下求解，从目标状态到边界条件。递推 DP 是自底向上，从边界条件到目标状态。 记忆化搜索不需要严格设计好计算顺序，备忘录没有记录则进行计算即可。而递推 DP 则通过严格计算 dp table 的递推计算顺序，来避免重复计算。 多个状态下，递推 DP 有可能会产生大量无效状态，而记忆化搜索则不会。这是记忆化搜索在速度上有可能战胜递推 DP 的地方。 回溯 → 记忆化搜索 → 递推 DP 三部曲 # step1，回溯：遍历所有状态（可能剪枝） step2，记忆化搜索：存下已访问过的状态以避免重复计算 step3，递推 DP：设置明确的访问顺序来避免重复 形象比喻 # 分治\n分而治之。先解决子问题，再将子问题的解合并求出原问题。\n贪心\n一条路走到黑。选择当下局部最优路线，没有后悔药。\n回溯\n一条路走到黑，手握后悔药，可以不断折返重来。\n动规\n上帝视角，手握无数个平行宇宙的历史存档，同时发展出无数个未来。\n参考资料 # 《算法导论》 https://zhuanlan.zhihu.com/p/33048876（此文亦是摘录自《算法导论》） ","date":"11 July 2022","externalUrl":null,"permalink":"/%E7%AE%97%E6%B3%95/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92_%E5%88%86%E6%B2%BB_%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%E5%AF%B9%E6%AF%94/","section":"算法","summary":"","title":"动态规划、分治、贪心算法对比","type":"算法"},{"content":"求解最优化问题的算法通常需要经过一系列的步骤，在每一个步骤都面临多种选择。对于许多最优化问题，使用动态规划算法有些杀鸡用牛刀了，可以使用更简单、更高效的算法。贪心算法（greedy algorithm）就是这样的算法，它在每一步都做出当时看起来最佳的选择。也就是说，它总是做出局部最优的选择，寄希望这样的选择能够导致全局最优解。 ——《算法导论》\n贪心算法（又称贪婪算法）是指，在对问题求解时，总是做出在当前看来是最好的选择。也就是说，不从整体最优解上加以考虑，他所做出的是在某种意义上的局部最优解。\n贪心算法不是对所有问题都能得到整体最优解，关键是贪心策略的选择，选择的贪心策略必须具备无后效性，即某个状态以前的过程不会影响以后的状态，只与当前状态有关。\n很多问题可以用动态规划来解决，但如果我们能够证明一直做出贪心选择就能得到最优解的话，我们就能得到一个贪心算法。\n设计贪心算法的过程包括如下步骤：\n确定问题的最优子结构 设计一个递归算法 证明如果我们做出一个贪心选择，则只剩下一个子问题 证明贪心选择总是安全的 设计一个递归算法实现贪心策略 将递归算法转换为迭代算法 在以上过程中我们可以看到，贪心算法是以动态规划为基础的（在思考问题方面）。比如在动态规划中，我们会定义子问题 S(i,j)，然后我们发现，如果总是做出贪心选择，则可以将子问题限定为 S(k) 的形式。也就是通过贪心选择来改变最优子结构，使得只有一个子问题被留下。 每一个用贪心算法解决的问题中，几乎总有一个更繁琐的动态规划算法。\n更一般地，我们可以将贪心算法地设计提炼为如下更直接的步骤：\n将最优化问题转化为如下形式：对其做出一个选择后，只剩下一个子问题需要解决。 证明做出贪心选择后，原问题总是存在最优解，即贪心选择总是安全的。 证明做出贪心选择后，剩余的子问题满足如下性质：其最优解与贪心选择组合即可得到原问题的最优解。 🔶 贪心选择性质\n当进行选择时，我们直接做出在当前问题中看来是最优的选择，而不必考虑子问题的解。这也是贪心算法与动态规划的不同之处。\n在动态规划中，每个步骤的选择通常依赖于子问题的解。因此，我们通常以一种自底向上的方式（分析的时候自顶向下，实现的时候自底向上）求解动态规划问题。在贪心算法中，我们总是做出当时看来是最佳的选择，然后求解剩下的唯一的子问题。贪心算法选择时可能依赖之前做出的选择，但不依赖任何将来的选择活着子问题的解。因此贪心算法在求解第一次选择前不求解任何子问题。一个动态规划算法是自底向上进行计算的，而一个贪心算法通常是自顶向下的，进行一次又一次贪心选择，将问题实例变得更小。\n❇️ 贪心问题解决思路 (摘自leetcode) 那么对于原始的复杂问题，如何能够知道他是否能被贪心解决呢？\n首先，我们需要将原始问题拆解成子问题，明确子问题的局面以及局面中可进行的操作。实际上不止贪心问题，很多问题都需要这样的拆解过程。 然后我们需要考虑子问题的最优，是否能保证全局最优。如果能的话，我们就可以只考虑如何解子问题，否则就可能需要动态规划或者搜索解了。 最后，我们需要考虑如何使子问题最优，可能有些问题存在乍一看可行的策略，但是我们仍然需要仔细思考甚至证明。以保证没有漏掉什么细节情况。 对于第三点，很多同学这里会经常犯错。比如以为贪心做法是对的，但是实际上有问题。例如错误地用贪心解决 01 背包问题（子问题最优，原问题不一定最优）等。所以如果我们觉得某道题目存在贪心策略，最好自己证明一下正确性再实现。\n贪心算法的现实应用 # 数据压缩编码（Huffman 编码） 最小生成树（minimum-spanning-tree） Dijkstra 最短路径算法 ","date":"11 July 2022","externalUrl":null,"permalink":"/%E7%AE%97%E6%B3%95/%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95/","section":"算法","summary":"","title":"贪心算法","type":"算法"},{"content":"差分数组其实就是用来描述另一个数组（暂且称作原始数组）的元素变化情况的数组。\n差分数组的第一个元素（d[0]）与原数组相同（a[0]），往后，d[i] = a[i] - a[i - 1]。\n以原数组 arr = [0, 2, 5, 4, 9, 7, 10, 0] 为例，其差分数组为 d = [0, 2, 3, -1, 5, -2, 3, -10]。\n图片来自：https://cloud.tencent.com/developer/article/1629357\n这个差分数组有什么用呢？我们所知道的是，这个差分数组是对原数组的一种描述，我们是可以通过遍历差分数组将原数组构造出来的。那么这种描述，是不是在某种场景下可以提供某种优势呢？\n是的，这个场景就是频繁的大批量区间修改。\n比如说，我想把下标 [1, 4] 范围内的元素都 +3。按传统手段，我们需要访问这四个元素，逐一修改。四个修改起来是很快的，但如果区间范围很大呢？又或者这种区间修改的操作经常出现呢？\n如果我们把这个操作转换到差分数组起来，我们就会发现，同样是将 [1, 4] 范围内的元素都 +3，我们只需要 d[1] + 3，d[5] - 3 就可以了。因为差分数组描述的是原数组的相邻变化，而我们对原数组的一个区间内进行了同加或同减操作的话，其实只有区间边界产生了相对变化。因此无论我们对多大的区间进行同加/同减，在差分数组中只需要修改两个值。\n","date":"3 July 2022","externalUrl":null,"permalink":"/%E7%AE%97%E6%B3%95/%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/","section":"算法","summary":"","title":"差分数组","type":"算法"},{"content":"分治法将复杂问题分解为若干子问题（子问题再分解为子子问题，直到问题被分解得足够小，能够直接得出解），再将子问题的解合并，得到最终解。不同于动态规划，分治法中的父问题仅依赖其下一层的子问题，而不会有跨层级的直接依赖。\n🤔 分治思想有什么好处呢？（摘自 leetcode）\n简化思维逻辑\n原始问题往往非常复杂，例如排序问题。假设原始我们需要考虑对 1000 个数进行排序，那么利用分治思想我们可以分别对左右的 500 个数进行排序，然后考虑合并两个有序数组。当然，排序 500 个数看起来仍然不容易，但是我们可以继续分治下去，最终我们只需要考虑 1~2 个数的排序策略，这就是经典的归并排序的思想。\n分布式算法\n虽然在算法学习的过程中少有接触多进程和分布式等思想。但是随着 CPU core 越来越多，能够被分治法拆解的问题显然更方便进行并行计算，从而节省总体时间。因此分治思想在工程实现上具有重要的意义。\n效率优化（并行计算情况下）\n虽然我们不常用并行解决算法问题，但是在某些情况下仍然能够帮助我们节省计算代价，代表就是快速幂算法。\n根据《算法导论》中的阐述，分治策略在递归地求解的过程中，其每一层递归应用了如下三个步骤：\n分解（Divide）\n将问题划分为若干子问题，子问题的形式与愿问题一样，只是规模更小。\n解决（Conquer）\n如果子问题的规模足够小，则停止递归，直接求解。\n合并（Combine）\n将子问题的解组合成原问题的解。\n我们同样从典型的基础题入手，之后再讲一些拐弯的题。\n最大连续数列 # 面试题 16.17. 连续数列\n💡 给定一个整数数组，找出总和最大的连续数列，并返回总和。 示例：\n输入： [-2,1,-3,4,-1,2,1,-5,4] 输出： 6 解释： 连续子数组 [4,-1,2,1] 的和最大，为 6。\n对于任何一个数组，我们关心三个值：从数组最左端起连续数列的最大和 leftLarge、从数组右侧起连续数列的最大和 rightLarge、整个数组中（不要求从左端起或从右端起）连续数列的最大和 wholeLarge。我们将其构成三元组（leftLarge， rightLarge，wholeLarge）。对于只有一个元素的数组，leftLarge、rightLarge、wholeLarge 的值自然都等于该元素。而对于长度大于 1 的数组，我们从中间将其划分成两段，分别计算各段的三元组。本数组的三元组则是由左右两段数组的三元组计算得来。\nclass Solution: def maxSubArray(self, nums: List[int]) -\u0026gt; int: def cal(nums, start, end): if start \u0026gt;= end: return (nums[end], nums[end], nums[end]) mid = (start + end) // 2 leftRet = cal(nums, start, mid) rightRet = cal(nums, mid + 1, end) leftLarge = max(leftRet[0], sum(nums[start: mid + 1]) + rightRet[0]) rightLarge = max(rightRet[1], sum(nums[mid + 1: end + 1]) + leftRet[1]) wholeLarge = max(leftRet[2], rightRet[2], leftLarge, rightLarge, leftRet[1] + rightRet[0]) return(leftLarge, rightLarge, wholeLarge) ret = cal(nums, 0, len(nums) - 1) return ret[2] 💡 另外，此题也可以通过动态规划解决（且更优雅），不在此处展开，具体可查看题解。\n搜索二维矩阵 # 240. 搜索二维矩阵 II\n💡 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性： 每行的元素从左到右升序排列。 每列的元素从上到下升序排列。 示例 1：\n输入：matrix = 1,4,7,11,15, target = 5 输出：true\n示例 2：\n输入：matrix = 1,4,7,11,15, target = 20 输出：false\n此题与上一题的差别其实并不大，不过是从一维数组转变成了二维数组。自然地，分治规模也从原来的分为两段，变成了分成四块。\n由于其左右递增、上下递增的排列特性，对于其中任意一个矩阵来说，其左上角的元素为最小值，右下角的元素为最大值。如果 target 不在这个区间内，我们则可以迅速的判断该矩阵中没有我们要找的元素。\nclass Solution: def searchMatrix(self, matrix: List[List[int]], target: int) -\u0026gt; bool: def divide(i, j, k, l): if any(w \u0026lt; 0 for w in [i, j, k, l]) or any(w \u0026gt;= len(matrix) for w in [i, k]) or any(w \u0026gt;= len(matrix[0]) for w in [j, l]): return False if i \u0026gt; k or j \u0026gt; l: return False if target \u0026lt; matrix[i][j] or target \u0026gt; matrix[k][l]: return False if i == k and j == l: return target == matrix[i][j] midRow = (i + k) // 2 midCol = (j + l) // 2 return divide(i, j, midRow, midCol) or divide(i, midCol + 1, midRow, l) or divide(midRow + 1, j, k, midCol) or divide(midRow + 1, midCol + 1, k, l) return divide(0, 0, len(matrix) - 1, len(matrix[0]) - 1) 至少有 K 个重复字符的最长子串 # 395. 至少有 K 个重复字符的最长子串\n💡 给你一个字符串 s 和一个整数 k ，请你找出 s 中的最长子串， 要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度。 示例 1：\n输入：s = \u0026ldquo;aaabb\u0026rdquo;, k = 3 输出：3 解释：最长子串为 \u0026ldquo;aaa\u0026rdquo; ，其中 \u0026lsquo;a\u0026rsquo; 重复了 3 次。 示例 2：\n输入：s = \u0026ldquo;ababbc\u0026rdquo;, k = 2 输出：5 解释：最长子串为 \u0026ldquo;ababb\u0026rdquo; ，其中 \u0026lsquo;a\u0026rsquo; 重复了 2 次， \u0026lsquo;b\u0026rsquo; 重复了 3 次。\n此题的有意思之处在于逆向思维，瞄准那些无法满足条件的字符，将其作为分治的分割点。\n题目要求子串中的每一个字符出现的次数都不少与 k，我们在分治之前先计算当前串中各个字符出现的次数，对于那些出现次数少于 k 的字符来说，无论其怎么分，更不可能在子串中满足 k 次的出现。因此任何子串想要满足条件，都不能包含该字符。我们索性以该字符作为分治的分割条件，递归地计算分割后的各个子串。\n而当某个子串中我们找不出出现此处小于 k 的字符，则说明整个字串都满足条件，我们返回子串的长度。\nclass Solution(object): def longestSubstring(self, s, k): if not s: return 0 for c in set(s): if s.count(c) \u0026lt; k: return max(self.longestSubstring(t, k) for t in s.split(c)) return len(s) 为运算表达式设计优先级 # 241. 为运算表达式设计优先级\n给定一个含有数字和运算符的字符串，为表达式添加括号，改变其运算优先级以求出不同的结果。你需要给出所有可能的组合的结果。有效的运算符号包含 +, - 以及 * 。 示例 1: 输入: \u0026#34;2-1-1\u0026#34; 输出: [0, 2] 解释: ((2-1)-1) = 0 (2-(1-1)) = 2 示例 2: 输入: \u0026#34;2*3-4*5\u0026#34; 输出: [-34, -14, -10, -10, 10] 解释: (2*(3-(4*5))) = -34 ((2*3)-(4*5)) = -14 ((2*(3-4))*5) = -10 (2*((3-4)*5)) = -10 (((2*3)-4)*5) = 10 如果没有接触过分治思想，这一题乍一看可能会有点难，但是学习过分治后，就会很容易想到，括号这种东西，不就是为了划分领地嘛。加上括号了，就能保证我括号内计算的完整性。\n因此我们可以以运算表达式为分割点对字符串进行分治处理。具体来说，当我们瞄准一个运算符的时候，比如说是 * 号，那么就可以在这个 * 的左右画上括号，变为 () * ()，得到的结果就是左边括号里的值，乘以右边括号里的值。而左边括号又有多种可能的划分，右边括号又有多种可能的划分，因此左右两边的结果应该是两个数组，最终结果等于两个数组中元素的交叉相乘。\n","date":"3 June 2022","externalUrl":null,"permalink":"/%E7%AE%97%E6%B3%95/%E5%88%86%E6%B2%BB%E6%B3%95/","section":"算法","summary":"","title":"分治法","type":"算法"},{"content":" 人眼与相机 # 计算机图形渲染的最本质原理，是对人眼成像的模拟。\n若把人视作造物主之创造，那这个被称作人体的仪器，其精密度显然处于鬼斧神工之巅。其中主管视觉的眼睛，更是神秘之所在。\n人类视神经的纤维数约为 770,000 到 1,700,000 ，也正是与每个视网膜连接的神经节细胞的轴突数。在视觉敏感的视网膜中央凹，每个视网膜神经节细胞联系少至五个感光细胞。在视网膜的其他区域，这个数目上可至几千。\n——维基百科\n人类视觉形成的完整细节，在生物学上仍有许多尚未揭开的神秘面纱。但是我们都知道的是，这一切都是因为有光。有了光打在物体上，经过吸收、折射、反射进入我们的眼睛，我们才能看到东西。没有光，就没有视觉。\n在此我们先抛开复杂的神经元，仅探讨光学成像方面的一些基础原理，因而先从最简单的小孔成像开始，之后是与人眼较为相近的透镜相机，最后再类比到人眼。\n小孔相机 # 小孔成像的原理简单到可以用两个道理来概括：1. 光沿直线传播；2. 两个点确定一条直线。\n可以看到，小孔限定了光线传播方向，使透过它的光线投在了对称位置的光屏上，因而得到一个倒置的蜡烛像。\n❓多小算小，为什么小孔放大后，画面就模糊了？\n大家可否产生过这样的迷思：所谓小孔成像，大和小是相对的，多小算是小孔，多大算是大？\n仅从粒子性的角度考虑的话，恰好能通过一个光子的小孔似乎是最理想的。它为我们的第二个原理（两点确定一条直线）提供了最完美的条件。其缺陷在于，孔小，进光量也小，如果我们的光屏是一块感光元件（能够与光子发生化学反应），则需要经过很长时间，接收到足够多的光子才能产生影像。\n然而爱因斯坦告诉我们，光具有波粒二象性。大家都知道大名鼎鼎的光的衍射现象（最早由意大利物理学家朗西斯科·格里马第（1618-1663）发现）。由于波动性的存在，当孔径小于光的波长时，会发生非常明显的衍射现象，破坏了小孔成像的第一个原理（光沿直线传播），光屏上出现一圈圈的衍射波纹。\n因而，孔径过小也是不行的。经验告诉我们，对于一个鞋盒大小的小孔相机来说，2mm 的孔径可以使我们得到一个较为清晰的影像。\n孔径再大些，画面怎么就模糊了？前面说过，小孔是两点确定一条直线中的第二个点。如果这个点变大，变成一个圆了，那么透过它的就不是一条直光线，而是一束光线圆锥了。我们看下面的对比图：\n如上是一个理想的小孔，物体某一点处的光线经过小孔后，落在光屏某一确定的位置。\n如上孔径明显增大，小孔变为圆洞，可见，物体某处的光线可经过圆洞的不同位置进入黑箱内，呈一定的发散状，光屏上的影像则变得模糊。\n小孔相机的原理十分简单，它通过小孔限制了光线的传播路径，使物体与投影在对称的位置一一对应。而这种结构也限制了它的进光量，使其在感光元件上形成影像所需的时间很长，因此通过该结构来制作相机是不合适的。真正的相机使用了凸透镜结构，其在光线聚焦的同时，依然保证了足够的进光量。\n透镜相机 # 相比于小孔成像，透镜相机的关键在于凸透镜的应用。而凸透镜成像的基本原理，可见于中小学课本，大家对于下图都不陌生：\n图中 F 点是透镜的焦距点，P 点是透镜的两倍焦距（2F）点。\n情景一：蜡烛放置于距离透镜距离大于 2F 处，此时得到小于原物的倒像。\n情景二：物体靠近透镜，放置于 2F 处，得到与原物等大的倒像。\n情景三：物体继续靠近透镜，处于 2F 与 F 之间时，得到大于原物的倒像。\n情景四：继续靠近，处于 F 点时，光线平行无法聚焦，无法成像。\n情境四：继续靠近，小于 F 时，光线发散。在透镜右侧，光线无法聚焦，而在其逆向的延长线上，我们可以找到聚焦点。此时，我们得到了一个放大的正立虚像。（在此之前，我们得到的一直都是实像。）\n照相机的应用属于情景一：得到一个缩小的倒像。\n人眼 # 人眼结构的光学成像原理与上面所述的凸透镜成像是完全相通的，晶状体对应于透镜，视网膜对对应于光屏。光线经过晶状体的折射聚拢，在视网膜上形成缩小的倒像。倒像经过视神经传递到大脑后，被转换成我们我们所感知到的正立影像。\n人眼是可变焦的，晶状体周围的环形肌可对晶状体的曲率进行调节，使焦距在大约 18 - 22 mm 的范围内变动。\n人眼是可变光圈的，人的瞳孔可根据光线亮度自动调节，其直径在约 2 - 8 mm 的范围内变动。\n❓ 小小迷思：虚像是如何被人眼看到的？\n前面我们讲透镜时提到了实像和虚像，实像是可以在光屏上成像的，而虚像由于无法聚焦，不能成像。但是这个虚像是可以被我们的眼睛看见的，最典型的就是放大镜。\n如图，我们透过放大镜，看到的就是正立放大的虚像。\n那么，这是怎么回事，虚像的这束光不是发散而不聚焦的吗，它是怎么在我们的视网膜中成像的？\n这里其实有一个思维误区，在蜡烛通过透镜在光屏上得到实像的场景中，我们自然地将透镜代入为我们的晶状体，将光屏代入为我们的视网膜，这是相当形象的。而虚像场景中，我们出于惯性思维继续将透镜代入为晶状体，并仍想着将我们的视网膜放在透镜后面，期望得到影像。沿着这个思路，由于光线是发散的，因此无法在我们的视网膜上聚焦，自然也无法形成清晰的图像。\n然而，以上完全是惯性思维带来的错误。当我们用放大镜观察物体时，透镜并不是我们的晶状体，光屏也不是我们的视网膜。透镜在我们手中，而晶状体和视网膜依然健在于眼眶中。因而实际的光路是这样的：物体 → 透镜 → 晶状体 → 视网膜。\n从图中可以清晰地看出，发散后的光经过我们晶状体的再次聚拢，在视网膜上汇聚成像。实际上生活中大部分的反射光都是发散的（漫反射），而透镜在此处造成的发散，使得其逆向聚焦虚像变得更大，试想一下，我们把图中的蜜蜂和透镜都拿掉，假设在虚像处确实有一只如此大的蜜蜂，其最终进入到我们眼中的光线，和图中虚像进入我们眼中的光线是一样的。\n3D 渲染技术 # 计算机科学大部分时候是对现实世界的模拟和抽象，3D 图形技术更是如此，它本质上是模拟了人眼的工作原理，更进一步来说是模拟了相机的工作原理，再进一步来说，模拟的是小孔成像（对此，我们后面会进一步说明）。\n要记住的是，我们的世界是连续的（宏观层面来说是如此），而计算机的世界是离散的（无论是二进制的存储单元，还是屏幕上一个个的像素点）。\n另外，我们的世界是三维的，而眼睛看到的其实是二维，对于显示器平面来说更是如此。具体来说，我们眼睛看到的是3D世界的光线在眼中形成的二维投影，双眼接收到的不同角度的投影经过大脑合成为具有景深感的画面。更准确来说，我们的视网膜是一个凹面，因而接收到的实际上也是一个凹面投影。就这一点来说，凹面的显示器是更适合人类观感的（想一想迪士尼等乐园中的球形巨幕），只不过出于成本和空间占用等原因，我们常用的显示器都是平面图矩形（市场上也有一些小众的曲面显示器）。对于计算机 3D 渲染来说，我们最终呈现的是由像素点构成的点阵画面，其本质也是计算机中的3D数据经过计算后投射在显示器平面上的投影。\n两个关键问题 # 综合上面所述的内容，我们可以总结计算机 3D 渲染的工作内容为：将 3D 物体投影到 2D 平面上，哦对了，是有颜色的。那么就需要解决如下两个关键问题：\n可见性（Visibility）\n这个点能否被看见，能看见的话，这个点应该出现在 2D 画面的哪个位置。\n着色（Shading）\n这个点应该是什么颜色。\n解决可见性问题的常见方法有两个：光线追踪（Ray-tracing）和光栅化（Rasterization）。此外，光线追踪的技术原理使得其非常天然地同时解决了着色问题。而光栅化技术并不能完美地实现着色，尤其是被非直接光源所照射的物体。在现代的光栅化渲染技术中，往往通过引入其他算法来计算非直接光源照射的着色。\n光线追踪与光栅化 # 如何迅速地描述光线追踪与光栅化各自的特点呢？\n有人会说：二者的计算方向是相反的，光栅化是将物体上的点投影到画布上，而光线追踪是顺着画布上的点去找场景中的物体。\n有人会说：二者的遍历顺序不一样，光栅化是先遍历物体再遍历画布，而光线追踪是先遍历画布上的点再遍历物体。\n以上说法都对，但光线追踪与光栅化的本质区别，在于解决问题的思维方式不同。可以说，光线追踪是最符合人类认知（或者说最符合物理学家认知？）的一种 3D 渲染技术。前面我们说，人之所以能看见东西，是因为有光。而光线追踪技术的本质，就是尽可能地去模拟光线在传播过程中的情况。\n如上图所示，我们之所以能看见这个绿色的气球，是因为光线照射在球面上，发生了吸收、折射和反射。太阳光是白色的，当它照射在球面上的时候，由于球面材料的性质，其吸收掉了红色光，因而剩下的光构成了红色光的补色——绿色。剩下的绿光又分成两路，一部分透过气球发生了折射，另一部分发生反射进入了我们的眼睛。因而，之前我们说光线追踪在解决可见性问题的同时，也非常自然地解决了着色问题，因为其核心思路就是对上述的光线传播过程进行模拟。另外需要需要注意的是，照射在气球表面的不一定是来自太阳的白光，而有可能是其他物体所反射的带颜色的光。同样的，在球面上发生折射的光也可能在经过其他表面的反射后再次进入我们的眼睛。最后，对于非镜面反射的物体来说，光往往是朝各个方向反射的，其中只有一部分会进入我们的眼睛。\n可以体会，光线传播的基本原理是很简单的，但是要完全模拟所有光线的传播情况，其计算量非常巨大。有两个显而易见的优化方法是：1. 限定反射光的计算量，当一束光经过多次反射后，忽略其作用。2. 由于只有进入眼睛的光能被看见，而光路是可逆的，其所有传播特性都可以实现逆运算，因此我们从眼睛的方向出发，逆向计算进入眼睛的光线经过了怎样的传播路径。\n可见，顺着眼睛寻找场景中的物体，并不是光线追踪技术的根本属性，而只是一种计算优化方法。\n由于其巨大的计算开销，在计算机性能尚未如今般强大时，光线追踪技术的应用一直收到限制，仅在某些非实时渲染的领域得到应用，如动画制作、电影后期、图片渲染等。而与之对应的，另一项渲染技术——光栅化渲染，由于其高效且易于管线化的计算方式，则得到了更为广泛的应用，大部分的 GPU 都采用了基于光栅化技术的管线化计算设计。\n光栅渲染 # 相较于光线追踪的物理化思维，光栅化渲染更接近数学。\n光栅化英文名为 Rasterization，其词根 Raster 的中文翻译即光栅，基本可以理解为显示器中由横竖排列的像素点所构成的栅格。因而，光栅化渲染是一种表征其目的的命名：将 3D 物体转化成栅格化的图像。\n光栅化渲染的主要步骤包括：透视投影、栅格化、解决可见行问题。\n透视投影 # 投影可以分为两种类型，正交投影和透视投影。在我们的生活经验中，同样的物体，离我们近就越大，离我们远就越小，这是因为我们的眼睛接受到的是物体的透视投影。假使我们的眼睛并非聚光成像，而是平行光成像，则我们的眼睛接收到的是平行光投影，届时我们的视野只有瞳孔那么大，且眼中的物体不会因为远近而发生大小变化。\n我们在 3D 渲染当中，自然也有正交和透视两种渲染方式。下图是游戏模拟城市中的画面，其使用了正交投影的渲染方式，可以看到，其中的建筑并没有遵循近大远小的透视规律。\n当然，3D 渲染的一个重要目标是创造出令人难辨真伪的画面，而正交投影的渲染的渲染方式显然是不符合人类的视觉经验的，我们着重讲解的是模拟人类视觉的透视投影。\n如上图，eye 便是我们眼睛的位置，而 canvas，我们暂且就将其理解为显示器平面，假设在显示器平面之后，有一个立方体存在，要在显示器平面上画出怎样一副 Image，才会让我们的眼睛相信这个假设呢？\n仔细观察，我们所看到的视觉光是从物体开始朝着我们眼睛的方向直线传播的，直线传播过程中与显示器平面相交的点，就是其在显示器上的投影。因而透视投影过程就被转化成了一个非常简单的几何问题。\n我们从显示器的侧面来观察这个问题，A 点是我们的眼睛，B\u0026rsquo;C\u0026rsquo; 所在的是我们的显示器平面，为了方便计算，我们定义眼睛到显示器平面的距离为 1 个单位。同时，为了存储和计算，我们需要定义坐标系：我们定义眼睛为坐标原点，从屏幕向眼睛的方向定义为 Z 轴，垂直向上的为 Y 轴，X 轴与 YZ 构成的平面垂直，在当前视角中，我们暂时忽略对它的计算。\n现在，我们要将 BC 两点投影到显示器平面上来。B 点与视线平齐，其 Y 坐标为 0，投影到显示平面上后， Y 坐标仍然为 0。C 点的 Y 坐标我们定义为 C.y，投影到显示平面后，点 C\u0026rsquo; 的 Y 坐标 C\u0026rsquo;.y 应该是多少呢？显然，这是一个近似三角形的问题。\n三角形 ABC 与 AB\u0026rsquo;C\u0026rsquo; 是近似三角形，因此我们有\n$$\\frac{B'C'}{AB'} = \\frac{BC}{AB}$$又，AB\u0026rsquo; = 1，我们有\n$$B'C' = \\frac{BC}{AB}$$而 AB 的值就是 B 点 Z 坐标的负数，C 点与 B 点 Z 坐标相同，因此也是 C 点 Z 坐标的负数。因此我们有\n$C\u0026rsquo;.y = \\frac{C.y}{-C.z}$\n我们以同样的方法来计算其投影后的 x 坐标\n$C\u0026rsquo;.x = \\frac{C.x}{-C.z}$\n就是这么简单，只需要经过简单的除法运算，我们就将三维场景中的点透视投影到了二维平面。另外，二维平面中的点是没有 Z 坐标的，那么我们如何处理原来三维场景中点的 Z 坐标值呢，直接将其舍弃吗？不，我们将其原封不动的保存下来，这个坐标值将在后续处理多个物体之间的遮挡和可见性的时候发挥作用。\n栅格化 # 好，现在我们有一个三维空间中三角形的 3 个顶点都已经投影到显示器平面了。为什么拿三角形来说事呢，因为所有由离散点构成的几何体都可以分解为若干个平面，而三角形则是最小的平面单元。毕竟三角形由三个不在一条直线上的点构成，两个点可以确定一条直线，直线加上一个不在该线上的点则可以确定一个平面。\n我们根据投影到平面上的三个点的位置，可以确定三角形的三条边。假设我们的三角形是绿色的，现在我们需要给它涂上色。（我们怎么得知这个三角形投影到显示器平面后仍然是绿色的呢？实际上，仅从光栅渲染的技术层面是无法得知的，毕竟我们做的投影是一个纯位置计算的过程。因此，如我们之前所说，真正的光栅化渲染的应用中，实际需要引入其他的算法来计算非直接光照等因素对物体表面颜色的影响，从而得到逼真的画面效果。）\n我们先忽略光路对颜色的复杂影响，暂且将它涂成绿色。时刻记住，我们的显示器图像是由离散像素点构成的数字图像。我们的涂色过程，所要做的，是找出该三角形所包围的像素点，将其置为绿色。那么，我们如何确定一个像素点是否被某三角形包围呢？\n幸好，有一个叫 Juan Pineda 的科学家 1988 年在其发表的论文 A Parallel Algorithm for Polygon Rasterization 中提出了一个边缘函数（edge function） 可以非常方便的帮我们解决这个问题。其原理倒也并不复杂。\n图中有一个三角形 v0v1v2，现在我聚焦于其中一条边 v0v1（我们定义 v0 -\u0026gt; v1 为它的方向），可以看到，v0v1 所在的直线将平面分成了两半（灰色 - left side，白色 - right side）。现在我们有一个点 P(x,y)，将边和点输入之后，边缘函数将返回一个正值，表示点在边的右侧，或一个负值，表示点在边的左侧。\n那么，这跟三角形又有什么关系呢？\n通过简单的观察我们可以发现，如果我们顺时针来定义边方向的话，会发现，如果一个点同时处在三条边的右侧，那么这个点必然处在三边所构成的三角形内的。（试想我们围着一个花园顺时针散步一圈，花园中的花自然始终是在我们右侧的。）这里的正负是相对的，如果我们逆时针定义边的方向的话，则三角形内点的边缘计算的值应当是负值。\n这个边缘函数长什么样呢？也非常简单：\n$E_{01}(P) = (P.x - v0.x) * (V1.y - V0.y) - (P.y - V0.y) * (V1.x - V0.x)$\n根据所得的值\nE(P) \u0026gt; 0\n点在线的右侧\nE(P) \u0026lt; 0\n点在线的左侧\nE(P) = 0\n点在线上\n这个边缘函数可以用矩阵来表达\n$\\begin{vmatrix}(P.x - V0.x) \u0026amp; (P.y - V0.y) \\ (V1.x - V0.x) \u0026amp; (V1.y - V0.y)\\end{vmatrix}$\n我们把 P - V0 定义为向量 A，把 V1 - V0 定义为向量 B\n则矩阵变为\n$\\begin{vmatrix} A.x \u0026amp; A.y \\ B.x \u0026amp; B.y\\end{vmatrix}$\n可见，我们把整个运算转变成了 A B 两个向量的叉乘。\n空间中的两个三维向量叉乘意味着什么呢？——这也让我们从另一个角度来理解边缘函数。\n空间中的两个三维向量叉乘，会得到第三个三维向量，这个向量会与前两个向量构成的平面垂直。可以看到这个向量的方向和大小会根据前两个向量的夹角关系发生变化。（实际上它的大小就等于前两个向量所构成的平行四边形的面积。）\n而 A 与 B 的夹角关系，也就表征了 B 是在 A 的左侧还是右侧。\n以上，我们介绍了边缘函数如何使用，以及其背后原理的理解。截至目前为止，我们举例的场景中，都只出现一个三角形，那么如果是多个三角形场景下，互相之间存在遮挡时，我们如何确定谁是可见的呢？\n可见性 # 问题的答案非常简单，离我最近的不会被遮挡。\n还记得我们之前投影的时候，原封不动地保存了点的 Z 坐标吗？现在可以发挥作用了。\n我们创建一个与画布等大的二维数组名为 z-buffer，用来存储各个位置当前可见投影点的 Z 坐标。刚开始的时候，我们还没有投影任何一个点，z-buffer 中的元素初始化为无穷大。之后，我们在投影的过程中，比较当前投影点的 Z 坐标，如果比 z-buffer 中的更小，则将当前点投影至平面，并更新对应 z-buffer 中 Z 值为当前点的值。（注意，由于 Z 坐标的方向是从物体到眼睛的方向，因此物体的 Z 坐标实际都是负值。而我们为叙述方便，此段中所述的 Z 坐标值是指其绝对值。）\n如上所述的算法被称作 z-buffer 算法。用来解决可见性的算法当然还有，但大部分分为两类，一类在遍历物体的时候做文章（如 painter 算法），一类在点投影到画布上之后再做文章。我们的 z-buffer 算法就属于后者。关于更多的可见性算法，我们不在此赘述。\n归一化 # 现在我们来考虑一个问题，现在大家用的显示器有不同的规格，有 1920*1080 的，有 2560x1440 的，那么全屏显示同一张图片的时候，前者横向有 1920 个像素，后者则有 2560 个像素。那么，我们 3D 渲染的时候应该怎么办呢，针对不同的显示器都要做一遍投影和渲染计算吗？没有必要，我们引入归一化的方法，对投影结果进行归一化后，我们就可以非常方便地将其计算并应用到不同的分辨率图像上了。\n在我们进行投影的时候，我们使用了这样一个投影平面，平面的正中心与我们的水平视线对齐。设投影平面为宽度为 2 个单位的正方形，定义与视线正对的平面中心为坐标原点，则平面左上角的坐标为 (-1, 1) 右下角的坐标为 (1, -1)。\n在将点投影到这个平面上之后，我们接下来做一个归一化的操作。\n所谓归一化，就是将坐标体系由原来的范围映射到 [0, 1] 的范围，映射后的坐标系，我们称之为归一化坐标系，又称 NDC 坐标系（Normalized Device Coordinate）。\n映射过程也是一个简单的数学运算，我们设映射前投影平面的宽度为 canvasWidth，高度为 canvasHeight，则 p 点在映射后，其在 NDC 坐标系中的坐标为：\npNDC.x = (pScreen.x + canvasWidth / 2) / canvasWidth;pNDC.y = (pScreen.y + canvasHeight / 2) / canvasHeight; 归一化之后，我们要将图像绘制为不同的分辨率就很简单了。有一点要注意，绘制平面的坐标系与 NDC 有所区别，NDC 坐标系的原点在左下角，Y 轴朝上，而绘制平面坐标系的原点在左上角，Y 轴朝下。\n我们进行如下计算\n$\\begin{array}{l}P\u0026rsquo;{raster}.x = \\lfloor{ P\u0026rsquo;{normalized}.x * \\text{ Pixel Width} }\\rfloor\\P\u0026rsquo;{raster}.y = \\lfloor{ (1 - P\u0026rsquo;{normalized}.y) * \\text{Pixel Height} }\\rfloor\\end{array}$\n注意，我们对计算出来的坐标结果进行了取模。这是因为我们之前所说的，显示器上的数字图像是由离散的像素点构成，因此我们必须通过取模来定位到具体的像素点。\n这里还有一个小问题， 由于取模是向下的，会导致 x 坐标的计算整体偏左移，比如说最右边那个像素点很难被算到，因此我们将 x 坐标整体加 1 后再取模。而 y 坐标由于已经进行了取反和加 1 操作，因此并不存在这个问题。修改之后，我们的公式变成如下\n$\\begin{array}{l}P\u0026rsquo;{raster}.x = \\lfloor{ (P\u0026rsquo;{normalized}.x + 1) * \\text{ Pixel Width} }\\rfloor\\P\u0026rsquo;{raster}.y = \\lfloor{ (1 - P\u0026rsquo;{normalized}.y) * \\text{Pixel Height} }\\rfloor\\end{array}$\n重心坐标与顶点着色 # 待续。\n","date":"3 June 2022","externalUrl":null,"permalink":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6/%E5%85%89%E6%A0%85%E5%8C%96%E6%B8%B2%E6%9F%93%E7%9A%84%E6%9C%B4%E7%B4%A0%E5%8E%9F%E7%90%86/","section":"计算机图形学","summary":"","title":"光栅化渲染的朴素原理","type":"计算机图形学"},{"content":"类型：树\n前序遍历构造二叉搜索树 💛 ⭐ https://leetcode-cn.com/problems/construct-binary-search-tree-from-preorder-traversal/\n❓ 给定一个二叉搜索树的前序遍历结果，据此还原构建出该二叉搜索树。\n💡 前序遍历 + 中序遍历\n二叉搜索树的中序遍历结果是一个升序序列。因此对给定的前序遍历结果升序排列后可以得到中序遍历结果。之后此题与「105. 从前序与中序遍历序列构造二叉树」一致。\nclass Solution: def bstFromPreorder(self, preorder: List[int]) -\u0026gt; TreeNode: if not preorder: return None inorder = preorder[:] inorder.sort() def geneNode(preorder_left, preorder_right, inorder_left, inorder_right): if preorder_left \u0026gt; preorder_right: return None # 前序遍历中的第一个节点就是根节点 preorder_root = preorder_left # 在中序遍历中定位根节点 inorder_root = index[preorder[preorder_root]] # 得到左子树中的节点数目 left_size = inorder_root - inorder_left # 先把根节点建立出来 root = TreeNode(preorder[preorder_root]) # 递归地构造左子树，并连接到根节点 # 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素 root.left = geneNode(preorder_left + 1, preorder_left + left_size, inorder_left, inorder_root - 1) # 递归地构造右子树，并连接到根节点 # 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素 root.right = geneNode(preorder_left + left_size + 1, preorder_right, inorder_root + 1, inorder_right) return root # 构造哈希映射，帮助我们快速定位根节点 index = {element: i for i, element in enumerate(inorder)} length = len(preorder) return geneNode(0, length - 1, 0, length - 1) 时间复杂度：O(NlogN)，空间复杂度：O(n)\n💡 二分查找分界线\n根据前序遍历的定义，前序遍历的首元素是树的根结点。\n根据搜索树的性质，可以把剩下的元素分为小于根结点和大于根结点两类，分别用于递归构建左子树和右子树。\n这两个区间的分界线，可以通过二分法来查找。\npublic class Solution { public TreeNode bstFromPreorder(int[] preorder) { int len = preorder.length; if (len == 0) { return null; } return dfs(preorder, 0, len - 1); } /** * 根据 preorder 的子区间 [left..right] 构建二叉树 * * @param preorder * @param left * @param right * @return */ private TreeNode dfs(int[] preorder, int left, int right) { if (left \u0026gt; right) { return null; } TreeNode root = new TreeNode(preorder[left]); if (left == right) { return root; } // 在区间 [left..right] 里找最后一个小于 preorder[left] 的下标 // 注意这里设置区间的左边界为 left ，不能是 left + 1 // 这是因为考虑到区间只有 2 个元素 [left, right] 的情况，第 1 个部分为空区间，第 2 部分只有一个元素 right int l = left; int r = right; while (l \u0026lt; r) { int mid = l + (r - l + 1) / 2; if (preorder[mid] \u0026lt; preorder[left]) { // 下一轮搜索区间是 [mid, r] l = mid; } else { // 下一轮搜索区间是 [l, mid - 1] r = mid - 1; } } TreeNode leftTree = dfs(preorder, left + 1, l); TreeNode rightTree = dfs(preorder, l + 1, right); root.left = leftTree; root.right = rightTree; return root; } } 时间复杂度：O(NlogN)，空间复杂度：O(n)\n💡 数值上下界递归构建\n我们在递归时维护一个 (lower, upper) 二元组，表示当前位置可以插入的节点的值的上下界。如果此时先序遍历位置的值处于上下界中，就将这个值作为新的节点插入到当前位置，并递归地处理当前位置的左右孩子的两个位置。否则回溯到当前位置的父节点。\n一开始的时候，lower 和 upper 分别为负无穷和正无穷。\n我们从前向后访问先序遍历数组，按照如下规律构建搜索树：\n如果当前元素的值 val 在 [lower, upper] 的范围内，则新建一个节点 root，并对其左孩子递归处理 helper(lower, val)，对其右孩子递归处理 helper(val, upper)。 如果当前元素的值 val 不在 [lower, upper] 范围内，则进行回溯。 class Solution: def __init__(self): self.index = 0 def bstFromPreorder(self, preorder: List[int]) -\u0026gt; TreeNode: if not preorder: return None upper = 2147483647 lowwer = -2147483648 self.index = 1 def doBst(node, lowwer, upper, preorder): if self.index \u0026lt; len(preorder) and preorder[self.index] \u0026gt; lowwer and preorder[self.index] \u0026lt; node.val: node.left = TreeNode(preorder[self.index]) self.index += 1 doBst(node.left, lowwer, node.val, preorder) if self.index \u0026lt; len(preorder) and preorder[self.index] \u0026gt; node.val and preorder[self.index] \u0026lt; upper: node.right = TreeNode(preorder[self.index]) self.index += 1 doBst(node.right, node.val, upper, preorder) head = TreeNode(preorder[0]) doBst(head, lowwer, upper, preorder) return head 时间复杂度：O(n)，空间复杂度：O(n)\n💡 数值上下界迭代构建\n数值上下界方法也可以通过迭代来实现。\n将先序遍历中的第一个元素作为二叉树的根节点，即 root = new TreeNode(preorder[0])，并将其放入栈中。 使用 for 循环迭代先序遍历中剩下的所有元素： 将栈顶的元素作为父节点，当前先序遍历中的元素作为子节点。如果栈顶的元素值小于子节点的元素值，则将栈顶的元素弹出并作为新的父节点，直到栈空或栈顶的元素值大于子节点的元素值。注意，这里作为父节点的是最后一个被弹出栈的元素，而不是此时栈顶的元素； 如果父节点的元素值小于子节点的元素值，则子节点为右孩子，否则为左孩子； 将子节点放入栈中。 import java.util.ArrayDeque; import java.util.Deque; public class Solution { public TreeNode bstFromPreorder(int[] preorder) { int len = preorder.length; if (len == 0) { return null; } TreeNode root = new TreeNode(preorder[0]); Deque\u0026lt;TreeNode\u0026gt; stack = new ArrayDeque\u0026lt;\u0026gt;(); stack.push(root); for (int i = 1; i \u0026lt; len; i++) { // 将栈的最后一个元素作为父元素，并从下一个前序遍历的节点创建子节点 TreeNode node = stack.peekLast(); TreeNode currentNode = new TreeNode(preorder[i]); // 栈中小于当前节点值的元素全部出栈，当前节点连接到最后一个弹出栈的结点的右边 while (!stack.isEmpty() \u0026amp;\u0026amp; stack.peekLast().val \u0026lt; currentNode.val) { node = stack.removeLast(); } if (node.val \u0026lt; currentNode.val) { node.right = currentNode; } else { node.left = currentNode; } stack.addLast(currentNode); } return root; } } 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"28 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1008_%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","section":"leetcode 题解","summary":"","title":"1008_前序遍历构造二叉搜索树","type":"leetcode题解"},{"content":"","date":"28 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/","section":"leetcode 题解","summary":"","title":"leetcode 题解","type":"leetcode题解"},{"content":"","date":"28 April 2021","externalUrl":null,"permalink":"/tags/%E6%A0%91/","section":"Tags","summary":"","title":"树","type":"tags"},{"content":"","date":"28 April 2021","externalUrl":null,"permalink":"/tags/%E9%A2%98%E8%A7%A3/","section":"Tags","summary":"","title":"题解","type":"tags"},{"content":"类型：链表\n排序链表 💛 ⭐ https://leetcode-cn.com/problems/sort-list/\n❓ 给定一个链表，将其升序排列后返回。\n💡 插入排序\n从前往后找插入点。\n如果是数组的插入排序，则数组的前面部分是有序序列，后面的元素插入到前面后，插入位置后面的元素都往后移动一位。\n对于链表来说，结点插入不需要挨个地挪位置，因此插入的时间复杂度是 O(1)，但是每查找一个插入位置都需要 O(n)，所以总的时间复杂度仍然达到了 O(n^2)。\n由于单向链表只有指向后一结点的指针，因此我们从前往后地进行插入。同时我们在头部建立虚拟头结点，以方便在头结点之前插入结点。\nclass Solution: def insertionSortList(self, head: ListNode) -\u0026gt; ListNode: if not head: return head dummyHead = ListNode(0) dummyHead.next = head lastSorted = head curr = head.next while curr: if lastSorted.val \u0026lt;= curr.val: lastSorted = lastSorted.next else: prev = dummyHead while prev.next.val \u0026lt;= curr.val: prev = prev.next lastSorted.next = curr.next curr.next = prev.next prev.next = curr curr = lastSorted.next return dummyHead.next 时间复杂度：O(n^2)，空间复杂度：O(1)\n💡 归并排序（自顶向下）\n过程如下：\n找到链表的中点，以中点为分界，将链表拆分成两个子链表。可以使用快慢指针。 对两个子链表分别排序。 将两个排序后的子链表合并。 class Solution: def sortList(self, head: ListNode) -\u0026gt; ListNode: def sortFunc(head: ListNode, tail: ListNode) -\u0026gt; ListNode: if not head: return head if head.next == tail: head.next = None return head slow = fast = head while fast != tail: slow = slow.next fast = fast.next if fast != tail: fast = fast.next mid = slow return merge(sortFunc(head, mid), sortFunc(mid, tail)) def merge(head1: ListNode, head2: ListNode) -\u0026gt; ListNode: dummyHead = ListNode(0) temp, temp1, temp2 = dummyHead, head1, head2 while temp1 and temp2: if temp1.val \u0026lt;= temp2.val: temp.next = temp1 temp1 = temp1.next else: temp.next = temp2 temp2 = temp2.next temp = temp.next if temp1: temp.next = temp1 elif temp2: temp.next = temp2 return dummyHead.next return sortFunc(head, None) 时间复杂度：O(NlogN)，空间复杂度：O(logN)\n💡 归并排序（自底向上）\n先计算链表长度 length。\n然后\n用 subLength 表示每次需要排序的子链表的长度，初始时 subLength=1。 每次将链表拆分成若干个长度为 subLength 的子链表（最后一个子链表的长度可以小于 subLength），按照每两个子链表一组进行合并，合并后即可得到若干个长度为 subLength×2 的有序子链表（最后一个子链表的长度可以小于 subLength×2）。 将 subLength 的值加倍，重复第 2 步，对更长的有序子链表进行合并操作，直到有序子链表的长度大于或等于 length，整个链表排序完毕。 class Solution: def sortList(self, head: ListNode) -\u0026gt; ListNode: def merge(head1: ListNode, head2: ListNode) -\u0026gt; ListNode: dummyHead = ListNode(0) temp, temp1, temp2 = dummyHead, head1, head2 while temp1 and temp2: if temp1.val \u0026lt;= temp2.val: temp.next = temp1 temp1 = temp1.next else: temp.next = temp2 temp2 = temp2.next temp = temp.next if temp1: temp.next = temp1 elif temp2: temp.next = temp2 return dummyHead.next if not head: return head length = 0 node = head while node: length += 1 node = node.next dummyHead = ListNode(0, head) subLength = 1 while subLength \u0026lt; length: prev, curr = dummyHead, dummyHead.next while curr: head1 = curr for i in range(1, subLength): if curr.next: curr = curr.next else: break head2 = curr.next curr.next = None curr = head2 for i in range(1, subLength): if curr and curr.next: curr = curr.next else: break succ = None if curr: succ = curr.next curr.next = None merged = merge(head1, head2) prev.next = merged while prev.next: prev = prev.next curr = succ subLength \u0026lt;\u0026lt;= 1 return dummyHead.next 时间复杂度：O(NlogN)，空间复杂度：O(1)\n","date":"27 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/148_%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8/","section":"leetcode 题解","summary":"","title":"148_排序链表","type":"leetcode题解"},{"content":"","date":"27 April 2021","externalUrl":null,"permalink":"/tags/%E9%93%BE%E8%A1%A8/","section":"Tags","summary":"","title":"链表","type":"tags"},{"content":"类型：图\n判断二分图 💛 ⭐ https://leetcode-cn.com/problems/is-graph-bipartite/\n❓ 给你一个邻接数组表示的无向图（不一定是连通图，即可能由几个互不连通的子图构成），判断其是否是一个二分图。\n二分图的定义是，对于每一条边，其两端的结点不能划分在同一个集合，如果在此约束下，整个图中的所有结点可以划分在两个集合内。那么这个图就可以被称为二分图。\n💡 涂色法 - DFS\n这是一道典型的涂色题，所谓涂色，也就是一种分类的方法，涂上同一种颜色的结点，就被视为分类到同一个集合。\n比如我们以 1/-1 来代表两种颜色，那么我们可以从图中任一结点，以任意颜色开始涂色。由于我们最终关系的只是分好类，因此先涂 1 色或 -1 色都没关系，毕竟颜色只是一个区分。\n题目的要求是相邻（由一条边所连接的两端）结点不能同色。\n我们使用 DFS 和 BFS 都是可以的。\n在 DFS 中，我们要做的是在遍历过程中携带颜色信息，每遍历一个结点就要切换一次颜色。如果我们在涂色过程中发现冲突，则返回失败。\n# 我的实现 1 # 1/-1 分别表示两种颜色，0 表示未涂色 class Solution: def dfs(self, grid, colors, i, color, N): colors[i] = color for j in range(N): if grid[i][j] == 1: # 同一条边的两端同色了，失败 if colors[j] == color: return False # 切换颜色，继续涂 if colors[j] == 0 and not self.dfs(grid, colors, j, -1 * color, N): return False return True def isBipartite(self, graph: List[List[int]]) -\u0026gt; bool: N = len(graph) grid = [[0] * N for _ in range(N)] colors = [0] * N # 把邻接表转换成邻接矩阵，这样处理更直观 for i in range(N): for j in graph[i]: grid[i][j] = 1 for i in range(N): # 如果没涂色，则涂色。如果涂色失败，则直接返回失败 if colors[i] == 0 and not self.dfs(grid, colors, i, 1, N): return False return True # 我的实现 2 # 1/-1 分别表示两种颜色，0 表示未涂色 class Solution: def __init__(self): self.colors = [] def dfs(self, i, color, graph): if not 0 in self.colors: return True # 检查顶点 i 是否可涂为当前颜色，没法图的话，说明冲突了，返回失败 if any(self.colors[k] == color for k in graph[i]): return False # 涂色 self.colors[i] = color for k in graph[i]: # 切换颜色，继续往下涂 if self.colors[k] == 0: if not self.dfs(k, color * -1, graph): return False return True def isBipartite(self, graph: List[List[int]]) -\u0026gt; bool: self.colors = [0] * len(graph) for i in range(len(graph)): # 没涂的话开始涂 if self.colors[i] == 0: if not self.dfs(i, 1, graph): return False return True class Solution: def isBipartite(self, graph: List[List[int]]) -\u0026gt; bool: n = len(graph) UNCOLORED, RED, GREEN = 0, 1, 2 color = [UNCOLORED] * n valid = True def dfs(node: int, c: int): nonlocal valid color[node] = c # 涂色 cNei = (GREEN if c == RED else RED) for neighbor in graph[node]: # 旁边还没涂，切换颜色继续往下涂 if color[neighbor] == UNCOLORED: dfs(neighbor, cNei) if not valid: return # 旁边涂了，检查一下冲不冲突 elif color[neighbor] != cNei: valid = False return for i in range(n): # 没涂的开始涂 if color[i] == UNCOLORED: dfs(i, RED) if not valid: break return valid 时间复杂度：O(n + m)，n 指结点数，m 指边数。空间复杂度：O(n)，n 指结点数\n💡 涂色法 - BFS\n按层涂也是一样的，每一层翻转一个颜色，如果发现冲突了，则返回失败。\nclass Solution: def isBipartite(self, graph: List[List[int]]) -\u0026gt; bool: n = len(graph) UNCOLORED, RED, GREEN = 0, 1, 2 color = [UNCOLORED] * n for i in range(n): if color[i] == UNCOLORED: q = collections.deque([i]) color[i] = RED while q: node = q.popleft() cNei = (GREEN if color[node] == RED else RED) for neighbor in graph[node]: if color[neighbor] == UNCOLORED: q.append(neighbor) color[neighbor] = cNei elif color[neighbor] != cNei: return False return True 时间复杂度：O(n + m)，n 指结点数，m 指边数。空间复杂度：O(n)，n 指结点数\n","date":"26 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/785_%E5%88%A4%E6%96%AD%E4%BA%8C%E5%88%86%E5%9B%BE/","section":"leetcode 题解","summary":"","title":"785_判断二分图","type":"leetcode题解"},{"content":"","date":"26 April 2021","externalUrl":null,"permalink":"/tags/%E5%9B%BE/","section":"Tags","summary":"","title":"图","type":"tags"},{"content":"https://leetcode.cn/problems/throne-inheritance/description/\n题目 # 一个王国里住着国王、他的孩子们、他的孙子们等等。每一个时间点，这个家庭里有人出生也有人死亡。\n这个王国有一个明确规定的王位继承顺序，第一继承人总是国王自己。我们定义递归函数 Successor(x, curOrder) ，给定一个人 x 和当前的继承顺序，该函数返回 x 的下一继承人。\nSuccessor(x, curOrder): 如果 x 没有孩子或者所有 x 的孩子都在 curOrder 中： 如果 x 是国王，那么返回 null 否则，返回 Successor(x 的父亲, curOrder) 否则，返回 x 不在 curOrder 中最年长的孩子 比方说，假设王国由国王，他的孩子 Alice 和 Bob （Alice 比 Bob 年长）和 Alice 的孩子 Jack 组成。\n一开始， curOrder 为 [\u0026quot;king\u0026quot;]. 调用 Successor(king, curOrder) ，返回 Alice ，所以我们将 Alice 放入 curOrder 中，得到 [\u0026quot;king\u0026quot;, \u0026quot;Alice\u0026quot;] 。 调用 Successor(Alice, curOrder) ，返回 Jack ，所以我们将 Jack 放入 curOrder 中，得到 [\u0026quot;king\u0026quot;, \u0026quot;Alice\u0026quot;, \u0026quot;Jack\u0026quot;] 。 调用 Successor(Jack, curOrder) ，返回 Bob ，所以我们将 Bob 放入 curOrder 中，得到 [\u0026quot;king\u0026quot;, \u0026quot;Alice\u0026quot;, \u0026quot;Jack\u0026quot;, \u0026quot;Bob\u0026quot;] 。 调用 Successor(Bob, curOrder) ，返回 null 。最终得到继承顺序为 [\u0026quot;king\u0026quot;, \u0026quot;Alice\u0026quot;, \u0026quot;Jack\u0026quot;, \u0026quot;Bob\u0026quot;] 。 通过以上的函数，我们总是能得到一个唯一的继承顺序。\n请你实现 ThroneInheritance 类：\nThroneInheritance(string kingName) 初始化一个 ThroneInheritance 类的对象。国王的名字作为构造函数的参数传入。 void birth(string parentName, string childName) 表示 parentName 新拥有了一个名为 childName 的孩子。 void death(string name) 表示名为 name 的人死亡。一个人的死亡不会影响 Successor 函数，也不会影响当前的继承顺序。你可以只将这个人标记为死亡状态。 string[] getInheritanceOrder() 返回 除去 死亡人员的当前继承顺序列表。 示例：\n输入： [\u0026#34;ThroneInheritance\u0026#34;, \u0026#34;birth\u0026#34;, \u0026#34;birth\u0026#34;, \u0026#34;birth\u0026#34;, \u0026#34;birth\u0026#34;, \u0026#34;birth\u0026#34;, \u0026#34;birth\u0026#34;, \u0026#34;getInheritanceOrder\u0026#34;, \u0026#34;death\u0026#34;, \u0026#34;getInheritanceOrder\u0026#34;] [[\u0026#34;king\u0026#34;], [\u0026#34;king\u0026#34;, \u0026#34;andy\u0026#34;], [\u0026#34;king\u0026#34;, \u0026#34;bob\u0026#34;], [\u0026#34;king\u0026#34;, \u0026#34;catherine\u0026#34;], [\u0026#34;andy\u0026#34;, \u0026#34;matthew\u0026#34;], [\u0026#34;bob\u0026#34;, \u0026#34;alex\u0026#34;], [\u0026#34;bob\u0026#34;, \u0026#34;asha\u0026#34;], [null], [\u0026#34;bob\u0026#34;], [null]] 输出： [null, null, null, null, null, null, null, [\u0026#34;king\u0026#34;, \u0026#34;andy\u0026#34;, \u0026#34;matthew\u0026#34;, \u0026#34;bob\u0026#34;, \u0026#34;alex\u0026#34;, \u0026#34;asha\u0026#34;, \u0026#34;catherine\u0026#34;], null, [\u0026#34;king\u0026#34;, \u0026#34;andy\u0026#34;, \u0026#34;matthew\u0026#34;, \u0026#34;alex\u0026#34;, \u0026#34;asha\u0026#34;, \u0026#34;catherine\u0026#34;]] 解释： ThroneInheritance t= new ThroneInheritance(\u0026#34;king\u0026#34;); // 继承顺序：king t.birth(\u0026#34;king\u0026#34;, \u0026#34;andy\u0026#34;); // 继承顺序：king \u0026gt;andy t.birth(\u0026#34;king\u0026#34;, \u0026#34;bob\u0026#34;); // 继承顺序：king \u0026gt; andy \u0026gt;bob t.birth(\u0026#34;king\u0026#34;, \u0026#34;catherine\u0026#34;); // 继承顺序：king \u0026gt; andy \u0026gt; bob \u0026gt;catherine t.birth(\u0026#34;andy\u0026#34;, \u0026#34;matthew\u0026#34;); // 继承顺序：king \u0026gt; andy \u0026gt;matthew \u0026gt; bob \u0026gt; catherine t.birth(\u0026#34;bob\u0026#34;, \u0026#34;alex\u0026#34;); // 继承顺序：king \u0026gt; andy \u0026gt; matthew \u0026gt; bob \u0026gt;alex \u0026gt; catherine t.birth(\u0026#34;bob\u0026#34;, \u0026#34;asha\u0026#34;); // 继承顺序：king \u0026gt; andy \u0026gt; matthew \u0026gt; bob \u0026gt; alex \u0026gt;asha \u0026gt; catherine t.getInheritanceOrder(); // 返回 [\u0026#34;king\u0026#34;, \u0026#34;andy\u0026#34;, \u0026#34;matthew\u0026#34;, \u0026#34;bob\u0026#34;, \u0026#34;alex\u0026#34;, \u0026#34;asha\u0026#34;, \u0026#34;catherine\u0026#34;] t.death(\u0026#34;bob\u0026#34;); // 继承顺序：king \u0026gt; andy \u0026gt; matthew \u0026gt;bob（已经去世）\u0026gt; alex \u0026gt; asha \u0026gt; catherine t.getInheritanceOrder(); // 返回 [\u0026#34;king\u0026#34;, \u0026#34;andy\u0026#34;, \u0026#34;matthew\u0026#34;, \u0026#34;alex\u0026#34;, \u0026#34;asha\u0026#34;, \u0026#34;catherine\u0026#34;] 提示：\n1 \u0026lt;= kingName.length, parentName.length, childName.length, name.length \u0026lt;= 15 kingName，parentName， childName 和 name 仅包含小写英文字母。 所有的参数 childName 和 kingName 互不相同。 所有 death 函数中的死亡名字 name 要么是国王，要么是已经出生了的人员名字。 每次调用 birth(parentName, childName) 时，测试用例都保证 parentName 对应的人员是活着的。 最多调用 105 次birth 和 death 。 最多调用 10 次 getInheritanceOrder 。 题解 # 用一棵树来存储这个族谱，先序遍历顺序就是继承顺序。 通过将节点保存在哈希表里可以加快查找性能。\n代码 # class Node: def __init__(self, name): self.name = name self.dead = False self.children = [] class ThroneInheritance: def __init__(self, kingName: str): node = Node(kingName) self.root = node self.order = [] self.hash = {kingName: node} def find(self, name): return self.hash[name] def birth(self, parentName: str, childName: str) -\u0026gt; None: parent = self.find(parentName) child = Node(childName) parent.children.append(child) self.hash[childName] = child def death(self, name: str) -\u0026gt; None: # the death of the person does not affect the # Succesor function nor the curent inheritance order # you can treat it as just marking the person as dead node = self.find(name) node.dead = True def dfs(self, node): if not node.dead: self.order.append(node.name) for child in node.children: self.dfs(child) def getInheritanceOrder(self) -\u0026gt; List[str]: # return a list representing the current order of inheritance # excluding dead people self.order = [] self.dfs(self.root) return self.order ","date":"25 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1600_%E7%8E%8B%E4%BD%8D%E7%BB%A7%E6%89%BF%E9%A1%BA%E5%BA%8F/","section":"leetcode 题解","summary":"","title":"1600_王位继承顺序","type":"leetcode题解"},{"content":"","date":"25 April 2021","externalUrl":null,"permalink":"/tags/hash/","section":"Tags","summary":"","title":"Hash","type":"tags"},{"content":"类型：链表\n复制带随机指针的链表 💛 https://leetcode-cn.com/problems/copy-list-with-random-pointer/\n❓ 给你一个长度为 n 的链表，每个节点包含一个额外增加的随机指针 random ，该指针可以指向链表中的任何节点或空节点。构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成，其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点，并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。\n💡 哈希表\n第一次遍历，在创建新链表结点的同时，建立新旧结点的哈希映射。旧结点为键，新结点为值。\n第二次遍历，完善 random 指针，根据哈希表中新旧结点的对应关系。\nxclass Solution: def copyRandomList(self, head: \u0026#39;Node\u0026#39;) -\u0026gt; \u0026#39;Node\u0026#39;: if not head: return None newList = Node(head.val) hashMap = {head: newList} preNewNode = newList oldNode = head.next while oldNode: newNode = Node(oldNode.val) hashMap[oldNode] = newNode preNewNode.next = newNode preNewNode = newNode oldNode = oldNode.next oldNode = head newNode = newList while oldNode and newNode: if oldNode.random: newNode.random = hashMap[oldNode.random] oldNode = oldNode.next newNode = newNode.next return newList 时间复杂度：O(n)，空间复杂度：O(1)\n💡 孵化\n我们直接在原链表上创建新结点，每个新建的拷贝结点都放在原结点的旁边。\n遍历原来的链表并拷贝每一个节点，将拷贝节点放在原来节点的旁边，创造出一个旧节点和新节点交错的链表。 迭代这个新旧节点交错的链表，并用旧节点的 random 指针去更新对应新节点的 random 指针。比方说， B 的 random 指针指向 A ，意味着 B\u0026rsquo; 的 random 指针指向 A\u0026rsquo; 。 现在 random 指针已经被赋值给正确的节点， next 指针也需要被正确赋值，以便将新的节点正确链接同时将旧节点重新正确链接。 class Solution(object): def copyRandomList(self, head): \u0026#34;\u0026#34;\u0026#34; :type head: Node :rtype: Node \u0026#34;\u0026#34;\u0026#34; if not head: return head # Creating a new weaved list of original and copied nodes. ptr = head while ptr: # Cloned node new_node = Node(ptr.val, None, None) # Inserting the cloned node just next to the original node. # If A-\u0026gt;B-\u0026gt;C is the original linked list, # Linked list after weaving cloned nodes would be A-\u0026gt;A\u0026#39;-\u0026gt;B-\u0026gt;B\u0026#39;-\u0026gt;C-\u0026gt;C\u0026#39; new_node.next = ptr.next ptr.next = new_node ptr = new_node.next ptr = head # Now link the random pointers of the new nodes created. # Iterate the newly created list and use the original nodes random pointers, # to assign references to random pointers for cloned nodes. while ptr: ptr.next.random = ptr.random.next if ptr.random else None ptr = ptr.next.next # Unweave the linked list to get back the original linked list and the cloned list. # i.e. A-\u0026gt;A\u0026#39;-\u0026gt;B-\u0026gt;B\u0026#39;-\u0026gt;C-\u0026gt;C\u0026#39; would be broken to A-\u0026gt;B-\u0026gt;C and A\u0026#39;-\u0026gt;B\u0026#39;-\u0026gt;C\u0026#39; ptr_old_list = head # A-\u0026gt;B-\u0026gt;C ptr_new_list = head.next # A\u0026#39;-\u0026gt;B\u0026#39;-\u0026gt;C\u0026#39; head_old = head.next while ptr_old_list: ptr_old_list.next = ptr_old_list.next.next ptr_new_list.next = ptr_new_list.next.next if ptr_new_list.next else None ptr_old_list = ptr_old_list.next ptr_new_list = ptr_new_list.next return head_old 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"24 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/138_%E5%A4%8D%E5%88%B6%E5%B8%A6%E9%9A%8F%E6%9C%BA%E6%8C%87%E9%92%88%E7%9A%84%E9%93%BE%E8%A1%A8/","section":"leetcode 题解","summary":"","title":"138_复制带随机指针的链表","type":"leetcode题解"},{"content":"类型：链表\n将二叉搜索树转化为排序的双向链表 💛 https://leetcode-cn.com/problems/convert-binary-search-tree-to-sorted-doubly-linked-list/\n❓ 将一个 二叉搜索树就地转化为一个已排序的双向循环链表 。左孩子作为前驱指针，右孩子作为后继指针，第一个节点的前驱是最后一个节点，最后一个节点的后继是第一个节点。返回链表中最小元素的指针，作为头部。\n💡 递归\n递归是一种聚焦局部的思考方式。我们考虑搜索树中的一个结点，其左子树中的结点均比它小，其右子树中的结点均比它大。我们可以这么考虑，如果当前，左子树中的结点已经构建完成了一个双向链表，右子树中的结点也已经构建成为了一个双向链表。那么当前要做的就是，把当前结点，衔接在这两个双向链表中。\n# 官方实现 class Solution: def treeToDoublyList(self, root: \u0026#39;Node\u0026#39;) -\u0026gt; \u0026#39;Node\u0026#39;: def helper(node): \u0026#34;\u0026#34;\u0026#34; Performs standard inorder traversal: left -\u0026gt; node -\u0026gt; right and links all nodes into DLL \u0026#34;\u0026#34;\u0026#34; nonlocal last, first if node: # left helper(node.left) # node if last: # link the previous node (last) # with the current one (node) last.right = node node.left = last else: # keep the smallest node # to close DLL later on first = node last = node # right helper(node.right) if not root: return None # the smallest (first) and the largest (last) nodes first, last = None, None helper(root) # close DLL last.right = first first.left = last return first # 我的实现 class Solution: def treeToDoublyList(self, root: \u0026#39;Node\u0026#39;) -\u0026gt; \u0026#39;Node\u0026#39;: if not root: return None leftHead = self.treeToDoublyList(root.left) if root.left else None rightHead = self.treeToDoublyList(root.right) if root.right else None if leftHead and rightHead: leftTail = leftHead.left rightTail = rightHead.left leftTail.right = root root.left = leftTail leftHead.left = rightTail root.right = rightHead rightHead.left = root rightTail.right = leftHead return leftHead elif not leftHead and not rightHead: root.right = root root.left = root return root elif leftHead: leftTail = leftHead.left leftTail.right = root root.left = leftTail root.right = leftHead leftHead.left = root return leftHead else: rightTail = rightHead.left root.left = rightTail root.right = rightHead rightHead.left = root rightTail.right = root return root ","date":"23 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/426_%E5%B0%86%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E8%BD%AC%E5%8C%96%E4%B8%BA%E6%8E%92%E5%BA%8F%E7%9A%84%E5%8F%8C%E5%90%91%E9%93%BE%E8%A1%A8/","section":"leetcode 题解","summary":"","title":"426_将二叉搜索树转化为排序的双向链表","type":"leetcode题解"},{"content":" 题目 # 给定一个由唯一字符串构成的 0 索引 数组 words 。\n回文对 是一对整数 (i, j) ，满足以下条件：\n0 \u0026lt;= i, j \u0026lt; words.length，\ni != j ，并且\nwords[i] + words[j]（两个字符串的连接）是一个。\n回文串\n返回一个数组，它包含 words 中所有满足 回文对 条件的字符串。\n你必须设计一个时间复杂度为 O(sum of words[i].length) 的算法。\n示例 1：\n输入：words = [\u0026#34;abcd\u0026#34;,\u0026#34;dcba\u0026#34;,\u0026#34;lls\u0026#34;,\u0026#34;s\u0026#34;,\u0026#34;sssll\u0026#34;] 输出：[[0,1],[1,0],[3,2],[2,4]] 解释：可拼接成的回文串为[\u0026#34;dcbaabcd\u0026#34;,\u0026#34;abcddcba\u0026#34;,\u0026#34;slls\u0026#34;,\u0026#34;llssssll\u0026#34;] 示例 2：\n输入：words = [\u0026#34;bat\u0026#34;,\u0026#34;tab\u0026#34;,\u0026#34;cat\u0026#34;] 输出：[[0,1],[1,0]] 解释：可拼接成的回文串为[\u0026#34;battab\u0026#34;,\u0026#34;tabbat\u0026#34;] 示例 3：\n输入：words = [\u0026#34;a\u0026#34;,\u0026#34;\u0026#34;] 输出：[[0,1],[1,0]] 提示：\n1 \u0026lt;= words.length \u0026lt;= 5000 0 \u0026lt;= words[i].length \u0026lt;= 300 words[i] 由小写英文字母组成 解法 # 如果 word_i 和 word_j 能够拼成一个回文串，那么我们考虑 word_i 和 word_j_inverse, 二者必然其中一个是另一个的前缀，并且较长的那个剩下的部分，本身是一个回文串。\n比如 abc + deedcba 是一个回文串，那么 abc 是 abcdeed (deedcba 的逆串) 的前缀，且剩下的部分 deed 也是一个回文串。\n我们将所有的逆串存储到字典树中，由于最终答案要输出的是下标列表，因此我们在字典树的终止节点中存放的是愿数组中的下标位置。\n之后我们遍历 words, 同时试图在字典树中找到符合条件的 word_j_inverse, 存在如下两种情况：\nlen(word_i) \u0026gt; len(word_j_inverse)\n我们检测 word_i 的剩余部分是不是回文串，是的话，则 i,j 可构成一对\nlen(word_i) \u0026lt;= len(word_j_inverse)\n我们找到所有存储于此路径下的 word_j_inverse, 检查剩余部分是否为回文串，是的话, 则对应的 i,j 可构成一对\n代码 # class Trie: def __init__(self): self.root = {\u0026#39;idx\u0026#39;: -1} def add(self, word, idx): node = self.root for c in word: if c not in node: node[c] = {\u0026#39;idx\u0026#39;: -1} node = node[c] node[\u0026#39;idx\u0026#39;] = idx def wordTails(self, node, res = []): if node[\u0026#39;idx\u0026#39;] != -1: res.append(node[\u0026#39;idx\u0026#39;]) for x in node.keys(): if x != \u0026#39;idx\u0026#39;: self.wordTails(node[x], res) return res class Solution: def __init__(self): self.trie = Trie() self.drows = [] def isPalindrome(self, s): n = len(s) for i in range(n//2): if s[i] != s[n-1-i]: return False return True def doSearch(self, word, i, index, node, res = []): if i == len(word): idxes = self.trie.wordTails(node, []) for idx in idxes: if self.isPalindrome(self.drows[idx][i:]): if index != idx: res.append([index, idx]) else: if node[\u0026#39;idx\u0026#39;] != -1 and self.isPalindrome(word[i:]): if index != node[\u0026#39;idx\u0026#39;]: res.append([index, node[\u0026#39;idx\u0026#39;]]) c = word[i] if c in node: self.doSearch(word, i+1, index, node[c], res) return res def palindromePairs(self, words: List[str]) -\u0026gt; List[List[int]]: self.trie = Trie() self.drows = [] ans = [] for i in range(len(words)): word = words[i] drow = word[::-1] self.drows.append(drow) self.trie.add(drow, i) for i in range(len(words)): word = words[i] ans += self.doSearch(word, 0, i, self.trie.root, []) return ans ","date":"22 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/336_%E5%9B%9E%E6%96%87%E5%AF%B9/","section":"leetcode 题解","summary":"","title":"336 回文对","type":"leetcode题解"},{"content":"","date":"22 April 2021","externalUrl":null,"permalink":"/tags/%E5%AD%97%E5%85%B8%E6%A0%91/","section":"Tags","summary":"","title":"字典树","type":"tags"},{"content":"类型：图\n无向图中连通分量的数目 💛 https://leetcode-cn.com/problems/number-of-connected-components-in-an-undirected-graph/\n❓ 给你一个无向图，包含 n 个结点，其中结点编号从 0 ~ n-1，边则以边集数组的形式存储。\n请你计算图中连通分量的数目。\n输入: n = 5 和 edges = 0, 1\n0 3 | | 1 --- 2 4 输出: 2\n💡 并查集 - 普通\nclass UnionFind: def __init__(self, n): self.parent = list(range(n)) self.cnt = n def find(self, x): while x != self.parent[x]: x = self.parent[x] return x def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return self.parent[root_x] = root_y self.cnt -= 1 class Solution: def countComponents(self, n: int, edges: List[List[int]]) -\u0026gt; int: uf = UnionFind(n) for x, y in edges: uf.union(x, y) return uf.cnt 💡 并查集 + 路径压缩\nclass UnionFind: def __init__(self, n): self.parent = list(range(n)) self.cnt = n def find(self, x): if x != self.parent[x]: self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return self.parent[root_x] = root_y self.cnt -= 1 class Solution: def countComponents(self, n: int, edges: List[List[int]]) -\u0026gt; int: uf = UnionFind(n) for x, y in edges: uf.union(x, y) return uf.cnt 💡 并查集 + 路径压缩 + 按秩合并\nclass UnionFind: def __init__(self, n): self.parent = list(range(n)) self.rank = [0] * n self.cnt = n def find(self, x): if x != self.parent[x]: self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return if self.rank[root_x] \u0026gt; self.rank[root_y]: root_x, root_y = root_y, root_x self.parent[root_x] = root_y if self.rank[root_x] == self.rank[root_y]: self.rank[root_y] += 1 self.cnt -= 1 class Solution: def countComponents(self, n: int, edges: List[List[int]]) -\u0026gt; int: uf = UnionFind(n) for x, y in edges: uf.union(x, y) return uf.cnt ","date":"21 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/323_%E6%97%A0%E5%90%91%E5%9B%BE%E4%B8%AD%E8%BF%9E%E9%80%9A%E5%88%86%E9%87%8F%E7%9A%84%E6%95%B0%E7%9B%AE/","section":"leetcode 题解","summary":"","title":"323_无向图中连通分量的数目","type":"leetcode题解"},{"content":"类型：字符串\nK 次操作转变字符串 💛 https://leetcode-cn.com/problems/can-convert-string-in-k-moves/\n❓ 给你两个字符串 s 和 t ，你的目标是在 k 次操作以内把字符串 s 转变成 t 。\n在第 i 次操作时（1 \u0026lt;= i \u0026lt;= k），你可以选择进行如下操作：\n选择字符串 s 中满足 1 \u0026lt;= j \u0026lt;= s.length 且之前未被选过的任意下标 j （下标从 1 开始），并将此位置的字符切换 i 次。 不进行任何操作。 切换 1 次字符的意思是用字母表中该字母的下一个字母替换它（字母表环状接起来，所以 \u0026lsquo;z\u0026rsquo; 切换后会变成 \u0026lsquo;a\u0026rsquo;）。\n请记住任意一个下标 j 最多只能被操作 1 次。\n如果在不超过 k 次操作内可以把字符串 s 转变成 t ，那么请你返回 true ，否则请你返回 false 。\n示例 1：\n输入：s = \u0026#34;input\u0026#34;, t = \u0026#34;ouput\u0026#34;, k = 9 输出：true 解释：第 6 次操作时，我们将 \u0026#39;i\u0026#39; 切换 6 次得到 \u0026#39;o\u0026#39; 。第 7 次操作时，我们将 \u0026#39;n\u0026#39; 切换 7 次得到 \u0026#39;u\u0026#39; 。 示例 2：\n输入：s = \u0026#34;abc\u0026#34;, t = \u0026#34;bcd\u0026#34;, k = 10 输出：false 解释：我们需要将每个字符切换 1 次才能得到 t 。我们可以在第 1 次操作时将 \u0026#39;a\u0026#39; 切换成 \u0026#39;b\u0026#39; ，但另外 2 个字母在剩余操作中无法再转变为 t 中对应字母。 示例 3：\n输入：s = \u0026#34;aab\u0026#34;, t = \u0026#34;bbb\u0026#34;, k = 27 输出：true 解释：第 1 次操作时，我们将第一个 \u0026#39;a\u0026#39; 切换 1 次得到 \u0026#39;b\u0026#39; 。在第 27 次操作时，我们将第二个字母 \u0026#39;a\u0026#39; 切换 27 次得到 \u0026#39;b\u0026#39; 。 💡 统计操作次数\n对于每个下标，如果 s 和 t 在该下标位置的字符不相同，则需要进行切换，所需最小切换次数最多为 25。因此，遍历 s 和 t，计算每个下标的最小切换次数（如果不需要切换，则最小切换次数为 0），并统计每个最小切换次数的出现次数。\n由于每次操作只能对一个未选过的下标位置的字符进行切换，因此如果有两个下标的最小切换次数相同，则如果其中的一个下标在第 i 次操作时进行了切换，另一个下标必须等到第 i+26 次操作时才能进行切换。如果有多个下标的最小切换次数相同，则每个下标都必须在前一个下标进行切换操作之后的第 26 次操作才能进行切换。\n如果有 j 个下标的最小切换次数都是 i，其中 1≤i≤25，则需要 i+26×(j−1) 次操作才能将 j 个下标的字符都切换。如果 i+26×(j−1)\u0026gt;k，则无法在 k 次操作以内完成全部的切换操作，因此返回 false。\n如果对于所有的最小切换次数，所有的下标都可以在 k 次操作以内进行切换，则返回 true。\n# 官方解法 class Solution: def canConvertString(self, s: str, t: str, k: int) -\u0026gt; bool: if len(s) != len(t): return False counts = [0] * 26 for si, ti in zip(s, t): difference = ord(ti) - ord(si) if difference \u0026lt; 0: difference += 26 counts[difference] += 1 for i, count in enumerate(counts[1:], 1): maxConvert = i + 26 * (counts[i] - 1) if maxConvert \u0026gt; k: return False return True # 我的解法 class Solution: def canConvertString(self, s: str, t: str, k: int) -\u0026gt; bool: if s == None or t == None: return False if k \u0026lt; 0: return False if len(s) != len(t): return False loop = k // 26 remainder = k % 26 ops = [loop + 1] * remainder + [loop] * (26 - remainder) for i in range(len(s)): step = ord(t[i]) - ord(s[i]) if step \u0026lt; 0: step += 26 if step == 0: continue if step \u0026gt; k: return False if ops[step - 1] \u0026lt;= 0: return False else: ops[step - 1] -= 1 return True ","date":"20 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1540_k_%E6%AC%A1%E6%93%8D%E4%BD%9C%E8%BD%AC%E5%8F%98%E5%AD%97%E7%AC%A6%E4%B8%B2/","section":"leetcode 题解","summary":"","title":"1540_K_次操作转变字符串","type":"leetcode题解"},{"content":"","date":"20 April 2021","externalUrl":null,"permalink":"/tags/%E5%AD%97%E7%AC%A6%E4%B8%B2/","section":"Tags","summary":"","title":"字符串","type":"tags"},{"content":"类型：树\n具有所有最深节点的最小子树 💛 https://leetcode-cn.com/problems/smallest-subtree-with-all-the-deepest-nodes/\n❓ 每个结点的深度是指根结点到该结点的距离。求树中所有最深结点的最近公共祖先。\n💡 两次 DFS\n我们先进行一次 DFS 计算并保存所有结点的深度，以找到所有的最深结点。\n在进行一次 DFS 通过回溯来找到这些最深结点的最近公共祖先。在这次 DFS 中需要处理以下情况：\nnode 没有左右子树，则返回 node。 node 的左右子树中都有最深结点，返回 node。 node 的左子树（或右子树）中含有最深结点，则递归 node 的左子树（或右子树）。 以上都不符，说明当前子树中没有答案。 class Solution(object): def subtreeWithAllDeepest(self, root): # Tag each node with it\u0026#39;s depth. depth = {None: -1} def dfs(node, parent = None): if node: depth[node] = depth[parent] + 1 dfs(node.left, node) dfs(node.right, node) dfs(root) max_depth = max(depth.itervalues()) def answer(node): # Return the answer for the subtree at node. if not node or depth.get(node, None) == max_depth: return node L, R = answer(node.left), answer(node.right) return node if L and R else L or R return answer(root) 时间复杂度：O(n)，空间复杂度：O(n)\n💡 一次 DFS\n我们可以把上一方法中的两次 DFS 合成一次。\n在这个 DFS 中，我们使用后序遍历，即自底向上。我们自底向上地维护「当前」的最深深度，以及包含了这些最深结点的最近公共祖先，返回给上一层。\n在后序处理逻辑中，我们根据左右子树返回来的深度来处理当前结点的返回。\n如果左右子树的深度相同，那么我们将最深深度加一，并且将本结点作为「当前」所有最深结点的最近公共祖先返回给上一层。 如果左子树的深度更深，那么显然右子树中的结点都不算是最深深度，我们将最深深度加一，并且将左子树作为「当前」所有最深结点的最近公共结点返回给上一层。 如果右子树的深度更深，那么显然左子树中的结点都不算是最深深度，我们将最深深度加一，并且将右子树作为「当前」所有最深结点的最近公共结点返回给上一层。 class Solution(object): def subtreeWithAllDeepest(self, root): # The result of a subtree is: # Result.node: the largest depth node that is equal to or # an ancestor of all the deepest nodes of this subtree. # Result.dist: the number of nodes in the path from the root # of this subtree, to the deepest node in this subtree. Result = collections.namedtuple(\u0026#34;Result\u0026#34;, (\u0026#34;node\u0026#34;, \u0026#34;dist\u0026#34;)) def dfs(node): # Return the result of the subtree at this node. if not node: return Result(None, 0) L, R = dfs(node.left), dfs(node.right) if L.dist \u0026gt; R.dist: return Result(L.node, L.dist + 1) if L.dist \u0026lt; R.dist: return Result(R.node, R.dist + 1) return Result(node, L.dist + 1) return dfs(root).node 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"19 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/865_%E5%85%B7%E6%9C%89%E6%89%80%E6%9C%89%E6%9C%80%E6%B7%B1%E8%8A%82%E7%82%B9%E7%9A%84%E6%9C%80%E5%B0%8F%E5%AD%90%E6%A0%91/","section":"leetcode 题解","summary":"","title":"865_具有所有最深节点的最小子树","type":"leetcode题解"},{"content":"类型：数组\n四数之和 💛 https://leetcode-cn.com/problems/4sum/\n❓ 找出数组 nums 中所有满足 a + b + c + d = target 的四元组。\n💡 排序 + 双指针\n使用两重循环分别枚举前两个数，然后在两重循环枚举到的数之后使用双指针枚举剩下的两个数。\n同时可以进行如下剪枝操作：\n在确定第一个数之后，如果 nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] \u0026gt; target，说明此时剩下的三个数无论取什么值，四数之和一定大于 \\textit{target}target，因此退出第一重循环。 在确定第一个数之后，如果 nums[i] + nums[length - 3] + nums[length - 2] + nums[length - 1] \u0026lt; target，说明此时剩下的三个数无论取什么值，四数之和一定小于 target，因此第一重循环直接进入下一轮，枚举 nums[i+1] 在确定前两个数之后，如果 nums[i] + nums[j] + nums[j + 1] + nums[j + 2] \u0026gt; target，说明此时剩下的两个数无论取什么值，四数之和一定大于 target，因此退出第二重循环。 在确定前两个数之后，如果 nums[i] + nums[j] + nums[length - 2] + nums[length - 1] \u0026lt; target，说明此时剩下的两个数无论取什么值，四数之和一定小于 target，因此第二重循环直接进入下一轮，枚举 nums[j+1] class Solution: def fourSum(self, nums: List[int], target: int) -\u0026gt; List[List[int]]: quadruplets = list() if not nums or len(nums) \u0026lt; 4: return quadruplets nums.sort() length = len(nums) for i in range(length - 3): if i \u0026gt; 0 and nums[i] == nums[i - 1]: continue if nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] \u0026gt; target: break if nums[i] + nums[length - 3] + nums[length - 2] + nums[length - 1] \u0026lt; target: continue for j in range(i + 1, length - 2): if j \u0026gt; i + 1 and nums[j] == nums[j - 1]: continue if nums[i] + nums[j] + nums[j + 1] + nums[j + 2] \u0026gt; target: break if nums[i] + nums[j] + nums[length - 2] + nums[length - 1] \u0026lt; target: continue left, right = j + 1, length - 1 while left \u0026lt; right: total = nums[i] + nums[j] + nums[left] + nums[right] if total == target: quadruplets.append([nums[i], nums[j], nums[left], nums[right]]) while left \u0026lt; right and nums[left] == nums[left + 1]: left += 1 left += 1 while left \u0026lt; right and nums[right] == nums[right - 1]: right -= 1 right -= 1 elif total \u0026lt; target: left += 1 else: right -= 1 return quadruplets 时间复杂度：O(N^3)，空间复杂度：O(logN)\n","date":"18 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/18_%E5%9B%9B%E6%95%B0%E4%B9%8B%E5%92%8C/","section":"leetcode 题解","summary":"","title":"18_四数之和","type":"leetcode题解"},{"content":"","date":"18 April 2021","externalUrl":null,"permalink":"/tags/%E6%95%B0%E7%BB%84/","section":"Tags","summary":"","title":"数组","type":"tags"},{"content":"类型：数组\n最大子序和 💚 ⭐ https://leetcode-cn.com/problems/maximum-subarray/\n❓ 给定一个整数数组 nums ，找到一个具有最大和的连续子数组（子数组最少包含一个元素），返回其最大和。\n💡 动态规划\n我们考虑以下标 i 元素为结尾的子数组。那么 nums[i] 有两种选择：加入之前的数组，以及独立开始一个新数组。我们选择其中较大的。\n状态转移方程： dp[i] = max(dp[i - 1] + nums[i], nums[i])\n由于在 dp 数组中 dp[i] 只与 dp[i - 1] 有关，因此我们可以只存储前一个 dp 值，使空间复杂度降低为 O(1).\nclass Solution: def maxSubArray(self, nums: List[int]) -\u0026gt; int: pre = 0 maxVal = nums[0] for val in nums: pre = max(pre + val, val) maxVal = max(maxVal, pre) return maxVal 时间复杂度：O(n)，空间复杂度：O(1)\n💡 分治\n对于一个区间 [l, r]，我们取 m = (l + r) // 2，对 [l, m] 和 [m + 1, r] 分治求解。当递归逐层深入到区间长度缩小为 1 的时候，递归开始回升。\n对于一个区间 [l, r]，我们维护以下四个量：\nlSum 表示 [l, r] 内以 l 为左端点的最大子段和 rSum 表示 [l, r] 内以 r 为右端点的最大子段和 mSum 表示 [l, r] 内的最大子段和 iSum 表示 [l, r] 的区间和 class Status: def __init__(self, lLen = 0, rLen = 0, mLen = 0, isCI = False, leftVal = 0, rightVal = 0): self.lLen = lLen # 左部连续递增长度 self.rLen = rLen # 右部连续递增长度 self.mLen = mLen # 中部连续递增长度 self.isCI = isCI # 全区间是否连续递增 self.leftVal = leftVal # 左端值 self.rightVal = rightVal # 右端值 class Solution: def pushUp(self, lSub: Status, rSub: Status) -\u0026gt; Status: lLen = lSub.lLen + rSub.lLen if lSub.isCI and lSub.rightVal \u0026lt; rSub.leftVal else lSub.lLen rLen = rSub.rLen + lSub.rLen if rSub.isCI and lSub.rightVal \u0026lt; rSub.leftVal else rSub.rLen mLen = max(lLen, rLen, lSub.mLen, rSub.mLen, (lSub.rLen + rSub.lLen) if lSub.rightVal \u0026lt; rSub.leftVal else 0) isCI = True if lSub.isCI and rSub.isCI and lSub.rightVal \u0026lt; rSub.leftVal else False leftVal = lSub.leftVal rightVal = rSub.rightVal return Status(lLen, rLen, mLen, isCI, leftVal, rightVal) def get(self, nums: list, left: int, right: int) -\u0026gt; list: if left == right: return Status(1, 1, 1, True, nums[left], nums[right]) mid = (left + right) // 2 lSub = self.get(nums, left, mid) rSub = self.get(nums, mid + 1, right) return self.pushUp(lSub, rSub) def findLengthOfLCIS(self, nums: List[int]) -\u0026gt; int: if not nums: return 0 return self.get(nums, 0, len(nums) - 1).mLen ","date":"17 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/53_%E6%9C%80%E5%A4%A7%E5%AD%90%E5%BA%8F%E5%92%8C/","section":"leetcode 题解","summary":"","title":"53_最大子序和","type":"leetcode题解"},{"content":"https://leetcode.cn/problems/cherry-pickup-ii/description/\n题目 # 给你一个 rows x cols 的矩阵 grid 来表示一块樱桃地。 grid 中每个格子的数字表示你能获得的樱桃数目。\n你有两个机器人帮你收集樱桃，机器人 1 从左上角格子 (0,0) 出发，机器人 2 从右上角格子 (0, cols-1)出发。\n请你按照如下规则，返回两个机器人能收集的最多樱桃数目：\n从格子 (i,j) 出发，机器人可以移动到格子 (i+1, j-1)，(i+1, j) 或者 (i+1, j+1) 。 当一个机器人经过某个格子时，它会把该格子内所有的樱桃都摘走，然后这个位置会变成空格子，即没有樱桃的格子。 当两个机器人同时到达同一个格子时，它们中只有一个可以摘到樱桃。 两个机器人在任意时刻都不能移动到 grid 外面。 两个机器人最后都要到达 grid 最底下一行。 示例 1：\n输入：grid = [[3,1,1],[2,5,1],[1,5,5],[2,1,1]] 输出：24 解释：机器人 1 和机器人 2 的路径在上图中分别用绿色和蓝色表示。 机器人 1 摘的樱桃数目为 (3 + 2 + 5 + 2) = 12 。 机器人 2 摘的樱桃数目为 (1 + 5 + 5 + 1) = 12 。 樱桃总数为： 12 + 12 = 24 。 示例 2：\n输入：grid = [[1,0,0,0,0,0,1],[2,0,0,0,0,3,0],[2,0,9,0,0,0,0],[0,3,0,5,4,0,0],[1,0,2,3,0,0,6]] 输出：28 解释：机器人 1 和机器人 2 的路径在上图中分别用绿色和蓝色表示。 机器人 1 摘的樱桃数目为 (1 + 9 + 5 + 2) = 17 。 机器人 2 摘的樱桃数目为 (1 + 3 + 4 + 3) = 11 。 樱桃总数为： 17 + 11 = 28 。 示例 3：\n输入：grid = [[1,0,0,3],[0,0,0,3],[0,0,3,3],[9,0,3,3]] 输出：22 示例 4：\n输入：grid = [[1,1],[1,1]] 输出：4 提示：\nrows == grid.length cols == grid[i].length 2 \u0026lt;= rows, cols \u0026lt;= 70 0 \u0026lt;= grid[i][j] \u0026lt;= 100 题解 # 此题其实比 题.741 其实还简单些，因为从题面上我们就能明确知道要同时考虑两个机器人的状态。 我们从第一行迭代到最后一行，记录下两个机器人分别处于任意位置时的收益。 定义状态： dp[j][k] 表示「在当前行」, 机器人 1 处在第 j 列，机器人 2 处在第 k 列时，当前的摘取 的总草莓数。 状态转移方程: 由于机器人可以向正下方、左下方、右下方三个方向移动，因而当前位置的机器人可能可能来自当前 格子的正上方、左上方、或有上方。同时考虑两个机器人，则一共有 6 中情况。我们区 6 中情况 中收益最大的，再加上当前格子的收益，注意当前位置重叠时，不要重复计算。 max([getDp(_j, _k) for _j in range(j-1, j+2) for _k in range(k-1, k+2)]) + grid[i][j] + (grid[i][k] if j!=k else 0) 最后，我们取最后一行中收益最大的情况。\n复杂度 时间复杂度: O(mnn) 空间复杂度: O(mnn) 代码 # class Solution: def cherryPickup(self, grid: List[List[int]]) -\u0026gt; int: m, n = len(grid), len(grid[0]) dp = [[-inf] * n for _ in range(n)] dp[0][-1] = grid[0][0] + grid[0][-1] def getDp(j, k): return dp[j][k] if 0 \u0026lt;= j \u0026lt; n and 0 \u0026lt;= k \u0026lt; n else -inf for i in range(1, m): ndp = [row[:] for row in dp] for j in range(n): for k in range(n): ndp[j][k] = max([getDp(_j, _k) for _j in range(j-1, j+2) for _k in range(k-1, k+2)]) + grid[i][j] + (grid[i][k] if j!=k else 0) dp = ndp return max([max(row) for row in dp]) ","date":"16 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1463_%E6%91%98%E6%A8%B1%E6%A1%83_ii/","section":"leetcode 题解","summary":"","title":"1463_摘樱桃 II","type":"leetcode题解"},{"content":"","date":"16 April 2021","externalUrl":null,"permalink":"/tags/dp/","section":"Tags","summary":"","title":"DP","type":"tags"},{"content":"类型：链表\nK 个一组翻转链表 ❤️ https://leetcode-cn.com/problems/reverse-nodes-in-k-group/\n❓ 给你一个链表，每 k 个节点一组进行翻转，请你返回翻转后的链表。\n输入：head = [1,2,3,4,5], k = 2 输出：[2,1,4,3,5]\n💡 本题不涉及复杂的算法，只是需求实现的细节比较多。\n💡 迭代\nclass Solution: def reverseKGroup(self, head: ListNode, k: int) -\u0026gt; ListNode: if not head or not head.next or k == 1: return head dummyHead = ListNode() dummyHead.next = head start = dummyHead end = dummyHead while end: for _ in range(k): if end: end = end.next if end: endNext = end.next pre = start.next cursor = start.next.next while True: nextCursor = cursor.next cursor.next = pre pre = cursor if cursor == end: break else: cursor = nextCursor nextStart = start.next start.next.next = endNext start.next = end start = end = nextStart return dummyHead.next 💡 递归\nclass Solution: def reverseKGroup(self, head: ListNode, k: int) -\u0026gt; ListNode: if not head or not head.next or k == 1: return head def reverse(head, tail, terminal): pre = None cur = head while cur != terminal: theNext = cur.next cur.next = pre pre = cur cur = theNext return tail, head dummyHead = ListNode() dummyHead.next = head pre = dummyHead tail = dummyHead while tail: for _ in range(k): if tail: tail = tail.next if tail: terminal = tail.next head, tail = reverse(pre.next, tail, terminal) pre.next = head tail.next = terminal pre = tail return dummyHead.next ","date":"15 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/25_k_%E4%B8%AA%E4%B8%80%E7%BB%84%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8/","section":"leetcode 题解","summary":"","title":"25_K_个一组翻转链表","type":"leetcode题解"},{"content":"类型：数组\n种花问题 💚 ⭐ https://leetcode-cn.com/problems/can-place-flowers/\n❓ 数组 flowerbed 表示花坛，0 表示没种花，1 表示种了花。我们要再往里种入 n 朵花，花与花不能相邻，问能否种入。\n💡 模拟法\n遍历花坛，只要出现了可种花的位置，立即种下一棵。该方法会修改原数组。\nclass Solution: def canPlaceFlowers(self, flowerbed: List[int], n: int) -\u0026gt; bool: if n \u0026lt;= 0: return True if not flowerbed: return False if len(flowerbed) \u0026lt; 2 * n - 1: return False for i in range(len(flowerbed)): if flowerbed[i] == 0 and (i \u0026lt;= 0 or flowerbed[i - 1] == 0) and (i \u0026gt;= len(flowerbed) - 1 or flowerbed[i + 1] == 0): flowerbed[i] = 1 n -= 1 if n \u0026lt;= 0: return True return False 时间复杂度：O(n)，空间复杂度：O(1)\n💡 贪心法\n如果 [i, j] 是连续空位，那么这段空位中可以种下 (j−i−2)/2 朵花。\nclass Solution: def canPlaceFlowers(self, flowerbed: List[int], n: int) -\u0026gt; bool: count, m, prev = 0, len(flowerbed), -1 for i in range(m): if flowerbed[i] == 1: if prev \u0026lt; 0: count += i // 2 else: count += (i - prev - 2) // 2 if count \u0026gt;= n: return True prev = i if prev \u0026lt; 0: count += (m + 1) // 2 else: count += (m - prev - 1) // 2 return count \u0026gt;= n 时间复杂度：O(n)，空间复杂度：O(1)\n💡 动态规划\n如果我们有一个空的花坛可以随意种，那么对于任意位置都有种（dp[i][1]）与不种（dp[i][0]）两种可能。但是现在花坛中已经种了一些花了，所以某些位置 dp[i][0] 的可能就被抹除了。\nclass Solution { public: bool canPlaceFlowers(vector\u0026lt;int\u0026gt;\u0026amp; nums, int n) { int size = nums.size(); int dp[size][2]; dp[0][1] = 1; dp[0][0] = nums[0] == 1 ? -10000 : 0; int sum = nums[0]; for (int i = 1; i \u0026lt; size; i++) { dp[i][1] = dp[i-1][0] + 1; dp[i][0] = nums[i] == 1 ? -10000 : max(dp[i-1][0], dp[i-1][1]); sum += nums[i]; } int mx = max(dp[size-1][0], dp[size-1][1]); return mx - sum \u0026gt;= n; } }; 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"14 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/605_%E7%A7%8D%E8%8A%B1%E9%97%AE%E9%A2%98/","section":"leetcode 题解","summary":"","title":"605_种花问题","type":"leetcode题解"},{"content":"类型：链表\n重排链表 💛 https://leetcode-cn.com/problems/reorder-list/\n❓ 给定一个单链表 L：L0→L1→…→Ln-1→Ln ，将其重新排列后变为： L0→Ln→L1→Ln-1→L2→Ln-2→…你不能只是单纯的改变节点内部的值，而是需要实际的进行节点交换。\n💡 线性表\n因为链表不支持下标访问，所以我们无法随机访问链表中任意位置的元素。\n容易想到的方法是，我们利用线性表存储该链表，利用线性表可以下标直接访问的特点，按顺序访问指定元素，重建该链表即可。\nclass Solution: def reorderList(self, head: ListNode) -\u0026gt; None: if not head: return vec = list() node = head while node: vec.append(node) node = node.next i, j = 0, len(vec) - 1 while i \u0026lt; j: vec[i].next = vec[j] i += 1 if i == j: break vec[j].next = vec[i] j -= 1 vec[i].next = None 时间复杂度：O(n)，空间复杂度：O(n)\n💡 寻找链表中点 + 链表逆序 + 合并链表\n我们注意到目标链表即为将原链表的左半端和反转后的右半端合并后的结果。\n这样我们的任务即可划分为三步：\n找到原链表的中点（参考「876. 链表的中间结点」）。 我们可以使用快慢指针来O(N) 地找到链表的中间节点。 将原链表的右半端反转（参考「206. 反转链表」）。 我们可以使用迭代法实现链表的反转。 将原链表的两端合并。 因为两链表长度相差不超过 1，因此直接合并即可。 class Solution: def reorderList(self, head: ListNode) -\u0026gt; None: if not head: return mid = self.middleNode(head) l1 = head l2 = mid.next mid.next = None l2 = self.reverseList(l2) self.mergeList(l1, l2) def middleNode(self, head: ListNode) -\u0026gt; ListNode: slow = fast = head while fast.next and fast.next.next: slow = slow.next fast = fast.next.next return slow def reverseList(self, head: ListNode) -\u0026gt; ListNode: prev = None curr = head while curr: nextTemp = curr.next curr.next = prev prev = curr curr = nextTemp return prev def mergeList(self, l1: ListNode, l2: ListNode): while l1 and l2: l1_tmp = l1.next l2_tmp = l2.next l1.next = l2 l1 = l1_tmp l2.next = l1 l2 = l2_tmp 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"13 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/143_%E9%87%8D%E6%8E%92%E9%93%BE%E8%A1%A8/","section":"leetcode 题解","summary":"","title":"143_重排链表","type":"leetcode题解"},{"content":"类型：字符串\n字符串相乘 💛 https://leetcode-cn.com/problems/multiply-strings/\n❓ 给定两个以字符串形式表示的非负整数 num1 和 num2，计算二者的乘积，同样以字符串形式返回。\n💡 模拟竖式乘法\n模拟乘法列竖式运算的过程，按位相乘并累加。\nclass Solution: def multiply(self, num1: str, num2: str) -\u0026gt; str: if num1 == \u0026#34;0\u0026#34; or num2 == \u0026#34;0\u0026#34;: return \u0026#34;0\u0026#34; ans = \u0026#34;0\u0026#34; m, n = len(num1), len(num2) for i in range(n - 1, -1, -1): add = 0 y = int(num2[i]) curr = [\u0026#34;0\u0026#34;] * (n - i - 1) for j in range(m - 1, -1, -1): product = int(num1[j]) * y + add curr.append(str(product % 10)) add = product // 10 if add \u0026gt; 0: curr.append(str(add)) curr = \u0026#34;\u0026#34;.join(curr[::-1]) ans = self.addStrings(ans, curr) return ans def addStrings(self, num1: str, num2: str) -\u0026gt; str: i, j = len(num1) - 1, len(num2) - 1 add = 0 ans = list() while i \u0026gt;= 0 or j \u0026gt;= 0 or add != 0: x = int(num1[i]) if i \u0026gt;= 0 else 0 y = int(num2[j]) if j \u0026gt;= 0 else 0 result = x + y + add ans.append(str(result % 10)) add = result // 10 i -= 1 j -= 1 return \u0026#34;\u0026#34;.join(ans[::-1]) 时间复杂度：O(mn + n^2)，空间复杂度：O(m + n)\n💡 转换成数组，逐位相乘后原地累加\n上面的模拟乘法，我们是先乘完一行后，再累加。如果我们转换成数组的话，我们可以每两位相乘之后就直接累加。这样可以免去累加时的遍历，简化运算。\nclass Solution: def multiply(self, num1: str, num2: str) -\u0026gt; str: if num1 == \u0026#34;0\u0026#34; or num2 == \u0026#34;0\u0026#34;: return \u0026#34;0\u0026#34; m, n = len(num1), len(num2) ansArr = [0] * (m + n) for i in range(m - 1, -1, -1): x = int(num1[i]) for j in range(n - 1, -1, -1): ansArr[i + j + 1] += x * int(num2[j]) for i in range(m + n - 1, 0, -1): ansArr[i - 1] += ansArr[i] // 10 ansArr[i] %= 10 index = 1 if ansArr[0] == 0 else 0 ans = \u0026#34;\u0026#34;.join(str(x) for x in ansArr[index:]) return ans 时间复杂度：O(mn)，空间复杂度：O(m + n)\n","date":"12 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/43_%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9B%B8%E4%B9%98/","section":"leetcode 题解","summary":"","title":"43_字符串相乘","type":"leetcode题解"},{"content":"类型：树\n二叉树的最近公共祖先 💛 ⭐ https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/\n❓ 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。（一个节点也可以是它自己的祖先。）\n💡 递归\n一般情况下，两个结点 p 和 q 的最近公共祖先 r 满足如下情况：p 和 q 分别位于 r 的左子树 和右子树中。\n特殊情况下，p 是 q 的子树，则二者的最近公共祖先就是 q；q 是 p 的子树，则二者的最近公共祖先就是 p。\n我们用 f(x) 表示 x 为根的子树中是否包含 p 或 q 结点。则综合以上两种情况的表达式可以表示为：\n(f(lson) and f(rson)) or ((x == p or x == q) and (f(lson) or f(rson)))\n其中 f(lson) and f(rson) 代表一般情况，(x == p or x == q) and (f(lson) or f(rson)) 代表特殊情况。\nclass Solution: def __init__(self): self.ans = None def dfs(self, root, p, q): if not root: return False lson = self.dfs(root.left, p, q) rson = self.dfs(root.right, p, q) if (lson and rson) or ((root.val == p.val or root.val == q.val) and (lson or rson)): self.ans = root return (lson or rson or root.val == p.val or root.val == q.val) def lowestCommonAncestor(self, root: \u0026#39;TreeNode\u0026#39;, p: \u0026#39;TreeNode\u0026#39;, q: \u0026#39;TreeNode\u0026#39;) -\u0026gt; \u0026#39;TreeNode\u0026#39;: self.dfs(root, p, q) return self.ans 时间复杂度：O(n)，空间复杂度：O(n)\n💡 存储父结点\n我们利用哈希表存储所有结点的父结点，利用此父结点信息从 p 不断往上跳，并记录期间访问过的结点，再从 q 结点往上跳，如果碰到已经访问过的结点，那么这个结点就是我们要找的最近公共祖先。\nclass Solution { Map\u0026lt;Integer, TreeNode\u0026gt; parent = new HashMap\u0026lt;Integer, TreeNode\u0026gt;(); Set\u0026lt;Integer\u0026gt; visited = new HashSet\u0026lt;Integer\u0026gt;(); public void dfs(TreeNode root) { if (root.left != null) { parent.put(root.left.val, root); dfs(root.left); } if (root.right != null) { parent.put(root.right.val, root); dfs(root.right); } } public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { dfs(root); while (p != null) { visited.add(p.val); p = parent.get(p.val); } while (q != null) { if (visited.contains(q.val)) { return q; } q = parent.get(q.val); } return null; } } 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"11 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/236_%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88/","section":"leetcode 题解","summary":"","title":"236_二叉树的最近公共祖先","type":"leetcode题解"},{"content":"类型：链表\n反转链表 II 💛 https://leetcode-cn.com/problems/reverse-linked-list-ii/\n❓ 给你单链表的头指针 head 和两个整数 left 和 right ，其中 left \u0026lt;= right 。请你反转从位置 left 到位置 right 的链表节点，返回 反转后的链表 。\n💡 两次遍历\n第一次遍历，找到 left 和 right 的指针位置。\n第二次遍历，反转 left 和 right 之间的链表。\nclass Solution: def reverseBetween(self, head: ListNode, left: int, right: int) -\u0026gt; ListNode: def reverse_linked_list(head: ListNode): # 也可以使用递归反转一个链表 pre = None cur = head while cur: next = cur.next cur.next = pre pre = cur cur = next # 因为头节点有可能发生变化，使用虚拟头节点可以避免复杂的分类讨论 dummy_node = ListNode(-1) dummy_node.next = head pre = dummy_node # 第 1 步：从虚拟头节点走 left - 1 步，来到 left 节点的前一个节点 # 建议写在 for 循环里，语义清晰 for _ in range(left - 1): pre = pre.next # 第 2 步：从 pre 再走 right - left + 1 步，来到 right 节点 right_node = pre for _ in range(right - left + 1): right_node = right_node.next # 第 3 步：切断出一个子链表（截取链表） left_node = pre.next curr = right_node.next # 注意：切断链接 pre.next = None right_node.next = None # 第 4 步：同第 206 题，反转链表的子区间 reverse_linked_list(left_node) # 第 5 步：接回到原来的链表中 pre.next = right_node left_node.next = curr return dummy_node.next 时间复杂度：O(n)，空间复杂度：O(1)\n💡 一次遍历\n一次遍历，直接完成反转。\nclass Solution: def reverseBetween(self, head: ListNode, left: int, right: int) -\u0026gt; ListNode: # 设置 dummyNode 是这一类问题的一般做法 dummy_node = ListNode(-1) dummy_node.next = head pre = dummy_node for _ in range(left - 1): pre = pre.next cur = pre.next for _ in range(right - left): next = cur.next cur.next = next.next next.next = pre.next pre.next = next return dummy_node.next 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"10 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/92_%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8_ii/","section":"leetcode 题解","summary":"","title":"92_反转链表_II","type":"leetcode题解"},{"content":" 题目 # 给你一个长度为 n 的整数数组 nums 和一个 正 整数 k 。\n一个整数数组的 能量 定义为和 等于 k 的子序列的数目。\n请你返回 nums 中所有子序列的 能量和 。\n由于答案可能很大，请你将它对 109 + 7 取余 后返回。\n示例 1：\n输入： nums = [1,2,3], k = 3\n输出： 6\n解释：\n总共有 5 个能量不为 0 的子序列：\n子序列 [***1***,***2***,***3***] 有 2 个和为 3 的子序列：[1,2,***3***] 和 [***1***,***2***,3] 。 子序列 [***1***,2,***3***] 有 1 个和为 3 的子序列：[1,2,***3***] 。 子序列 [1,***2***,***3***] 有 1 个和为 3 的子序列：[1,2,***3***] 。 子序列 [***1***,***2***,3] 有 1 个和为 3 的子序列：[***1***,***2***,3] 。 子序列 [1,2,***3***] 有 1 个和为 3 的子序列：[1,2,***3***] 。 所以答案为 2 + 1 + 1 + 1 + 1 = 6 。\n示例 2：\n输入： nums = [2,3,3], k = 5\n输出： 4\n解释：\n总共有 3 个能量不为 0 的子序列：\n子序列 [***2***,***3***,***3***] 有 2 个子序列和为 5 ：[***2***,3,***3***] 和 [***2***,***3***,3] 。 子序列 [***2***,3,***3***] 有 1 个子序列和为 5 ：[***2***,3,***3***] 。 子序列 [***2***,***3***,3] 有 1 个子序列和为 5 ：[***2***,***3***,3] 。 所以答案为 2 + 1 + 1 = 4 。\n示例 3：\n输入： nums = [1,2,3], k = 7\n输出： 0\n**解释：**不存在和为 7 的子序列，所以 nums 的能量和为 0 。\n提示：\n1 \u0026lt;= n \u0026lt;= 100 1 \u0026lt;= nums[i] \u0026lt;= 104 1 \u0026lt;= k \u0026lt;= 100 解法 # 题意比较绕，题目要求的是所有「子序列」的「子序列（满足条件）」个数。\n具体来说, nums = [1, 2, 3, 4, 5, 6], k = 9, 当我们找到一个满足条件的子序列 [1, 3, 5], 那么它应该在所有包含它的序列中都被计算一次。\n那么有多少序列会包含它呢，除了 1,3,5 必须包含外，还剩下 n-3 个元素，根据元素是否被包含，我们一共可以有 2^(n-3) 个序列。也就是说，当我们找到一个长度为 x 的满足条件的子序列时，一共有 2^(n-x) 个序列可以包含它。\n问题转化为，我们需要找到各个长度的满足 sum == k 的子序列，具体其长度和数量来计算最终的答案。\n我们使用背包 dp 来完成这一个过程，dp[i][j][p] 表示从下标为 [0,i] 的元素中选出 j 个元素构成序列，其中满足 sum == k 的序列的个数。根据定义, j 必须 \u0026lt;= i+1, 否则的话我们无法完成选择，对应值应当为 0.\n转移方程： dp[i][j][p] = dp[i-1][j][p] if i+1 \u0026gt;= j else 0 dp[i][j][p] += dp[i-1][j-1][p-nums[i]] if p-nums[i]\u0026gt;=0 else 0\n代码 # class Solution: def sumOfPower(self, nums: List[int], k: int) -\u0026gt; int: MOD = 10 ** 9 + 7 n = len(nums) dp = [ [0] + [0] * k for j in range(n+1) ] dp[0][0] = 1 for i in range(n): for j in range(i+1, 0, -1): for p in range(k, -1, -1): if i+1 \u0026lt; j: dp[j][p] = 0 if p - nums[i] \u0026gt;= 0: dp[j][p] += dp[j-1][p-nums[i]] ans = 0 for j in range(n+1): ans = (ans + dp[j][-1] * 2 ** (n-j)) % MOD return ans ","date":"9 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/3082_%E6%B1%82%E5%87%BA%E6%89%80%E6%9C%89%E5%AD%90%E5%BA%8F%E5%88%97%E7%9A%84%E8%83%BD%E9%87%8F%E5%92%8C/","section":"leetcode 题解","summary":"","title":"3082_求出所有子序列的能量和","type":"leetcode题解"},{"content":"","date":"9 April 2021","externalUrl":null,"permalink":"/tags/%E8%83%8C%E5%8C%85/","section":"Tags","summary":"","title":"背包","type":"tags"},{"content":"类型：图\n可能的二分法 💛 https://leetcode-cn.com/problems/possible-bipartition/\n❓ 给定一组 N 人（编号为 1, 2, \u0026hellip;, N），将他们分成两个小组。互相讨厌的人不能放到同一组。数组 dislikes 中，dislikes[i] = [a, b] 表示 a、b 两人互相讨厌。问是否能够顺利完成分组。\n输入：N = 4, dislikes = 1,2 输出：true 解释：group1 [1,4], group2 [2,3]\n💡 涂色法\n这一题其实跟「785. 判断二分图」是一样的，区别在于 785 题中是用邻接数组表示边，而此题是用边集数组表示边。\n# 我的实现 class Solution: def __init__(self): self.graph = [] self.colors = [] def dfs(self, i, color): # 如果涂色出现冲突 if any(self.colors[k] == color for k in self.graph[i]): return False # 涂色 self.colors[i] = color # 继续给邻接点涂色 for k in self.graph[i]: if self.colors[k] == 0: if not self.dfs(k, color * -1): return False return True def possibleBipartition(self, N: int, dislikes: List[List[int]]) -\u0026gt; bool: self.colors = [0] * (N + 1) # 将边集数组转换成邻接表 self.graph = [[] for _ in range(N + 1)] for edge in dislikes: if not edge[1] in self.graph[edge[0]]: self.graph[edge[0]].append(edge[1]) if not edge[0] in self.graph[edge[1]]: self.graph[edge[1]].append(edge[0]) for i in range(1, N+1): if self.colors[i] == 0: if not self.dfs(i, 1): return False return True class Solution(object): def possibleBipartition(self, N, dislikes): graph = collections.defaultdict(list) for u, v in dislikes: graph[u].append(v) graph[v].append(u) color = {} def dfs(node, c = 0): if node in color: return color[node] == c color[node] = c return all(dfs(nei, c ^ 1) for nei in graph[node]) return all(dfs(node) for node in range(1, N+1) if node not in color) 时间复杂度：O(N + E)，N 为结点数，E 为dislikes 长度。空间复杂度：O(N + E)\n","date":"8 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/886_%E5%8F%AF%E8%83%BD%E7%9A%84%E4%BA%8C%E5%88%86%E6%B3%95/","section":"leetcode 题解","summary":"","title":"886_可能的二分法","type":"leetcode题解"},{"content":"类型：字符串\n最长回文子串 💛 https://leetcode-cn.com/problems/longest-palindromic-substring/\n❓ 找到字符串 s 中的最长回文子串。\n💡 动态规划\n单个字符属于回文串。双字符如果相等的话，也属于回文串。\n另外，对于一个长度大于 2 的回文串来说，将它的首尾去掉后，剩下的部分仍然是回文串。\ndp[i][j] 表示子串 [i, j] 是否为回文串（True / False）。\n转移方程：dp[i][j] = dp[i + 1][j - 1] and s[i] == s[j]\n通过转移方程可以看出，长串的 dp 是依赖于短串的。因此我们通过串长度从小到大，先更新短串的 dp，再更新长串的 dp。先枚举串长度，再枚举串起点，串终点可以计算得来。\nclass Solution: def longestPalindrome(self, s: str) -\u0026gt; str: if not s: return \u0026#34;\u0026#34; n = len(s) start = end = 0 dp = [[False] * n for _ in range(n)] for length in range(1, n + 1): for i in range(n): j = i + length - 1 if j \u0026gt;= n: break if length == 1: dp[i][j] = True elif length == 2: dp[i][j] = s[i] == s[j] else: dp[i][j] = s[i] == s[j] and dp[i + 1][j - 1] if dp[i][j] and j - i \u0026gt; end - start: start, end = i, j return s[start: end + 1] 时间复杂度：O(n^2)，空间复杂度：O(n^2)\n💡 中心扩展法\n枚举回文子串的中心，向两边扩展。注意，子串中心可能是一个字符（如 aba），也可能是两个字符（如 abba）。\nclass Solution: def expandAroundCenter(self, s, left, right): while left \u0026gt;= 0 and right \u0026lt; len(s) and s[left] == s[right]: left -= 1 right += 1 return left + 1, right - 1 def longestPalindrome(self, s: str) -\u0026gt; str: start, end = 0, 0 for i in range(len(s)): left1, right1 = self.expandAroundCenter(s, i, i) left2, right2 = self.expandAroundCenter(s, i, i + 1) if right1 - left1 \u0026gt; end - start: start, end = left1, right1 if right2 - left2 \u0026gt; end - start: start, end = left2, right2 return s[start: end + 1] 时间复杂度：O(n^2)，空间复杂度：O(1)\n💡 Manacher 算法\n比较复杂，面试中一般不要求。可以作为发挥亮点。参见官方题解。\n时间复杂度：O(n)，空间复杂度：O(n)\n","date":"7 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/5_%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2/","section":"leetcode 题解","summary":"","title":"5_最长回文子串","type":"leetcode题解"},{"content":"类型：字符串\n最长公共前缀 💚 https://leetcode-cn.com/problems/longest-common-prefix/\n❓ 查找字符串数组的公共前缀。\n输入：strs = [\u0026ldquo;flower\u0026rdquo;,\u0026ldquo;flow\u0026rdquo;,\u0026ldquo;flight\u0026rdquo;] 输出：\u0026ldquo;fl\u0026rdquo;\n💡 横向扫描\nclass Solution: def longestCommonPrefix(self, strs: List[str]) -\u0026gt; str: if not strs: return \u0026#34;\u0026#34; prefix, count = strs[0], len(strs) for i in range(1, count): prefix = self.lcp(prefix, strs[i]) if not prefix: break return prefix def lcp(self, str1, str2): length, index = min(len(str1), len(str2)), 0 while index \u0026lt; length and str1[index] == str2[index]: index += 1 return str1[:index] 时间复杂度：O(mn)，m 指字符串平均长度，n 指字符串数量。空间复杂度：O(1)\n💡 纵向扫描\nclass Solution: def longestCommonPrefix(self, strs: List[str]) -\u0026gt; str: if not strs: return \u0026#34;\u0026#34; length, count = len(strs[0]), len(strs) for i in range(length): c = strs[0][i] if any(i == len(strs[j]) or strs[j][i] != c for j in range(1, count)): return strs[0][:i] return strs[0] 时间复杂度：O(mn)，空间复杂度：O(1)\n💡 二分查找\n首先，公共前缀长度不会长于最短子串。我们在 [0, minLength] 内进行二分查找。判断长度为 mid 的前缀相不相同。不相同的话则往前找，相同的话则往后找。判断前 mid 位是否相同的方式就可以用纵向扫描。此外，如果是往后找的话，前 mid 位就不用再判断了。\nclass Solution: def longestCommonPrefix(self, strs: List[str]) -\u0026gt; str: def isCommonPrefix(length): str0, count = strs[0][:length], len(strs) return all(strs[i][:length] == str0 for i in range(1, count)) if not strs: return \u0026#34;\u0026#34; minLength = min(len(s) for s in strs) low, high = 0, minLength while low \u0026lt; high: mid = (high - low + 1) // 2 + low if isCommonPrefix(mid): low = mid else: high = mid - 1 return strs[0][:low] 时间复杂度：O(mn * log(m))，其中 m 为最短字符串的长度，n 为字符串数量。空间复杂度：O(1)\n","date":"6 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/14_%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%89%8D%E7%BC%80/","section":"leetcode 题解","summary":"","title":"14_最长公共前缀","type":"leetcode题解"},{"content":"类型：字符串\n字符串中的第一个唯一字符 💚 https://leetcode-cn.com/problems/first-unique-character-in-a-string/\n❓ 找到字符串中第一个不重复的字符，返回其下标，不存在则返回 -1\n💡 哈希存出现次数\n第一次遍历，生成哈希表。字符为键，出现次数为值。\n第二次遍历，返回第一个出现次数为 1 的字符。\nclass Solution: def firstUniqChar(self, s: str) -\u0026gt; int: frequency = collections.Counter(s) for i, ch in enumerate(s): if frequency[ch] == 1: return i return -1 时间复杂度：O(n)，空间复杂度：O(x)，x 指字符集大小，x ≤ 26\n💡 哈希存储索引\n我们可以在构建哈希表的时候，直接存储字符的下标。当重复出现的时候，我们直接在哈希表中将该字符的值改为 -1，这样我们在第二次遍历的时候就只需要遍历哈希表，找到下标不等于 -1 且最小的字符即可。\nclass Solution: def firstUniqChar(self, s: str) -\u0026gt; int: position = dict() n = len(s) for i, ch in enumerate(s): if ch in position: position[ch] = -1 else: position[ch] = i first = n for pos in position.values(): if pos != -1 and pos \u0026lt; first: first = pos if first == n: first = -1 return first 时间复杂度：O(n)，空间复杂度：O(x)，x 指字符集大小，x ≤ 26\n💡 队列 + 哈希\n队列具有先进先出的性质，因此适合用来求这种「第一个」什么什么的问题。\n其实跟方法二差不多，只不过通过队列，把对哈希表的遍历也免了。\n具体地，当我们遍历到一个字符，若当前字符不在哈希表中，我们将其记录到哈希表，并将字符和下标构成二元组入队列尾。否则我们检查队列头部元素是否满足只出现一次，不满足的话将其弹出，务必保证头部元素只出现一次，此时我们就可以将当前数组和下标构成的二元组入队列尾了。当然，我们要同步更新哈希表。\n这里，我们只维护队列头部元素符合只出现一次。这是一种「延时删除」技巧。因为，如果头部元素符合要求的话，我们最终取的也就是头部元素。后面的元素是否符合要求对我们没有影响。只有在头部元素发生了变化，我们才需要逐一检查，最终目的也只是保证新的头部元素符合要求即可。\nclass Solution: def firstUniqChar(self, s: str) -\u0026gt; int: position = dict() q = collections.deque() n = len(s) for i, ch in enumerate(s): if ch not in position: position[ch] = i q.append((s[i], i)) else: position[ch] = -1 while q and position[q[0][0]] == -1: q.popleft() return -1 if not q else q[0][1] 时间复杂度：O(n)，空间复杂度：O(x)，x 指字符集大小，x ≤ 26\n","date":"5 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/387_%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%AD%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E5%94%AF%E4%B8%80%E5%AD%97%E7%AC%A6/","section":"leetcode 题解","summary":"","title":"387_字符串中的第一个唯一字符","type":"leetcode题解"},{"content":"类型：树\n填充每个节点的下一个右侧节点指针 💛 https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node/\n❓ 给你一个满二叉树（所有叶子节点都在最底层，每个父节点都有两个子节点）。对于每一个结点，请你填充其 next 指针，指向其右侧的结点。右侧没有结点，则置为 null。\n💡 层序遍历\nclass Solution: def connect(self, root: \u0026#39;Node\u0026#39;) -\u0026gt; \u0026#39;Node\u0026#39;: if not root: return None queue = [root] while queue: newQueue = [] for i in range(len(queue)): queue[i].next = queue[i + 1] if i + 1 \u0026lt; len(queue) else None if queue[i].left: newQueue.append(queue[i].left) if queue[i].right: newQueue.append(queue[i].right) queue = newQueue return root 时间复杂度：O(n)，空间复杂度：O(n)\n💡 原地逐层连接\n我们在填写 next 指针时，其实有两种情况。\n连接同一个父结点的两个孩子\n这种情况，我们直接连接即可。\n连接一个父结点的右孩子与另一个父结点的左孩子\n这种情况下，由于跨了两个不同的父结点，我们要上一层的 next 连接已经建立好了才好连接。即如下所示：\nclass Solution: def connect(self, root: \u0026#39;Node\u0026#39;) -\u0026gt; \u0026#39;Node\u0026#39;: if not root: return root # 从根节点开始 leftmost = root while leftmost.left: # 遍历这一层节点组织成的链表，为下一层的节点更新 next 指针 head = leftmost while head: # CONNECTION 1 head.left.next = head.right # CONNECTION 2 if head.next: head.right.next = head.next.left # 指针向后移动 head = head.next # 去下一层的最左的节点 leftmost = leftmost.left return root 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"4 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/116_%E5%A1%AB%E5%85%85%E6%AF%8F%E4%B8%AA%E8%8A%82%E7%82%B9%E7%9A%84%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%8F%B3%E4%BE%A7%E8%8A%82%E7%82%B9%E6%8C%87%E9%92%88/","section":"leetcode 题解","summary":"","title":"116_填充每个节点的下一个右侧节点指针","type":"leetcode题解"},{"content":"类型：字符串\n比较版本号 💛 https://leetcode-cn.com/problems/compare-version-numbers/\n❓ 比较两个版本号。\n💡 分割后逐一比较\nclass Solution: def compareVersion(self, version1: str, version2: str) -\u0026gt; int: ver_arr1 = version1.split(\u0026#39;.\u0026#39;) ver_arr2 = version2.split(\u0026#39;.\u0026#39;) while ver_arr1 or ver_arr2: val1 = int(ver_arr1.pop(0)) if ver_arr1 else 0 val2 = int(ver_arr2.pop(0)) if ver_arr2 else 0 if val1 \u0026lt; val2: return -1 elif val1 \u0026gt; val2: return 1 return 0 时间复杂度：O(m + n + max(m, n))，其中 m、n 分别表示两个字符串的长度。总的复杂度包括两个字符串分割各一次，O(m) + O(n)，遍历比较一次，O(max(m, n))。\n空间复杂度：O(m + n)，存储分割后的字符串。\n💡 双指针一次遍历\n上一个方法是先提前把两个版本号给分割好了，然后再遍历比较。为了优化，我们直接在遍历的同时，分割出版本号。\n在此，我们定义一个 get_next_chunk(version, n, p) 函数来获取字符串中的下一个子版本号，以及下一个分割点的起始位置。\nclass Solution: def get_next_chunk(self, version: str, n: int, p: int) -\u0026gt; List[int]: # if pointer is set to the end of string # return 0 if p \u0026gt; n - 1: return 0, p # find the end of chunk p_end = p while p_end \u0026lt; n and version[p_end] != \u0026#39;.\u0026#39;: p_end += 1 # retrieve the chunk i = int(version[p:p_end]) if p_end != n - 1 else int(version[p:n]) # find the beginning of next chunk p = p_end + 1 return i, p def compareVersion(self, version1: str, version2: str) -\u0026gt; int: p1 = p2 = 0 n1, n2 = len(version1), len(version2) # compare versions while p1 \u0026lt; n1 or p2 \u0026lt; n2: i1, p1 = self.get_next_chunk(version1, n1, p1) i2, p2 = self.get_next_chunk(version2, n2, p2) if i1 != i2: return 1 if i1 \u0026gt; i2 else -1 # the versions are equal return 0 时间复杂度：O(m + n)，空间复杂度：O(1)\n","date":"3 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/165_%E6%AF%94%E8%BE%83%E7%89%88%E6%9C%AC%E5%8F%B7/","section":"leetcode 题解","summary":"","title":"165_比较版本号","type":"leetcode题解"},{"content":" 题目 # 设计一个使用单词列表进行初始化的数据结构，单词列表中的单词 互不相同 。 如果给出一个单词，请判定能否只将这个单词中一个字母换成另一个字母，使得所形成的新单词存在于你构建的字典中。\n实现 MagicDictionary 类：\nMagicDictionary() 初始化对象 void buildDict(String[] dictionary) 使用字符串数组 dictionary 设定该数据结构，dictionary 中的字符串互不相同 bool search(String searchWord) 给定一个字符串 searchWord ，判定能否只将字符串中 一个 字母换成另一个字母，使得所形成的新字符串能够与字典中的任一字符串匹配。如果可以，返回 true ；否则，返回 false 。 示例：\n输入 [\u0026#34;MagicDictionary\u0026#34;, \u0026#34;buildDict\u0026#34;, \u0026#34;search\u0026#34;, \u0026#34;search\u0026#34;, \u0026#34;search\u0026#34;, \u0026#34;search\u0026#34;] [[], [[\u0026#34;hello\u0026#34;, \u0026#34;leetcode\u0026#34;]], [\u0026#34;hello\u0026#34;], [\u0026#34;hhllo\u0026#34;], [\u0026#34;hell\u0026#34;], [\u0026#34;leetcoded\u0026#34;]] 输出 [null, null, false, true, false, false] 解释 MagicDictionary magicDictionary = new MagicDictionary(); magicDictionary.buildDict([\u0026#34;hello\u0026#34;, \u0026#34;leetcode\u0026#34;]); magicDictionary.search(\u0026#34;hello\u0026#34;); // 返回 False magicDictionary.search(\u0026#34;hhllo\u0026#34;); // 将第二个 \u0026#39;h\u0026#39; 替换为 \u0026#39;e\u0026#39; 可以匹配 \u0026#34;hello\u0026#34; ，所以返回 True magicDictionary.search(\u0026#34;hell\u0026#34;); // 返回 False magicDictionary.search(\u0026#34;leetcoded\u0026#34;); // 返回 False 提示：\n1 \u0026lt;= dictionary.length \u0026lt;= 100 1 \u0026lt;= dictionary[i].length \u0026lt;= 100 dictionary[i] 仅由小写英文字母组成 dictionary 中的所有字符串 互不相同 1 \u0026lt;= searchWord.length \u0026lt;= 100 searchWord 仅由小写英文字母组成 buildDict 仅在 search 之前调用一次 最多调用 100 次 search 解法 # 将 words 存储在字典树中。\n在搜索过程中，我们尝试改变当前位置的字符（之前没改过的情况下）之后继续进行搜索。\n代码 # class MagicDictionary: def __init__(self): self.root = {\u0026#39;end\u0026#39;: False} def insert(self, word): node = self.root for c in word: if c not in node: node[c] = {\u0026#39;end\u0026#39;: False} node = node[c] node[\u0026#39;end\u0026#39;] = True def buildDict(self, dictionary: List[str]) -\u0026gt; None: for word in dictionary: self.insert(word) def doSearch(self, word, i, node, changed): if i == len(word): return changed and node and node[\u0026#39;end\u0026#39;] c = word[i] if c not in node and changed: return False return (c in node and self.doSearch(word, i+1, node[c], changed) ) or (not changed and any([ x!= \u0026#39;end\u0026#39; and x != c and self.doSearch(word, i+1, node[x], True) for x in node.keys() ]) ) def search(self, searchWord: str) -\u0026gt; bool: return self.doSearch(searchWord, 0, self.root, False) ","date":"2 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/676_%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E9%AD%94%E6%B3%95%E5%AD%97%E5%85%B8/","section":"leetcode 题解","summary":"","title":"676_实现一个魔法字典","type":"leetcode题解"},{"content":"类型：堆\n最小化舍入误差以满足目标 💛 ⭐ https://leetcode-cn.com/problems/minimize-rounding-error-to-meet-target/\n❓ 给定一系列价格数组 prices 和一个目标 target。对于 prices[i] 我们可以对其进行向下取整（floor）或向上取整（ceil），我们对 prices 数组中的元素进行取整操作（不同元素可以采用不同取整方案），使得最终数组和等于 target。我们定义舍入误差为 $Σ |Roundi(pi) - (pi)|$（ i 从 1 到 n ）。求最小的舍入误差。若无论如何舍入，都不能等于 target，则返回 -1。\n输入：prices = [\u0026ldquo;0.700\u0026rdquo;,\u0026ldquo;2.800\u0026rdquo;,\u0026ldquo;4.900\u0026rdquo;], target = 8 输出：\u0026ldquo;1.000\u0026rdquo; 解释： 使用 Floor，Ceil 和 Ceil 操作得到 (0.7 - 0) + (3 - 2.8) + (5 - 4.9) = 0.7 + 0.2 + 0.1 = 1.0 。\n💡 堆\n对所有元素取 floor 可以得到最小和 minSum 对所有元素取 ceil 可以得到最大和 maxSum 如果 target 不在此区间，则返回 \u0026ldquo;-1\u0026rdquo;\n对于大部分元素而言，取 ceil 和取 floor 的差值为 1，但是，本身是整数的元素除外。 这是此题的一个非常容易忽略的陷阱。 计算 target 与 minSum 的差值，target - minSum = ceilNum，这说明我们需要在 minSum 的基础上，将 ceilNum 个元素改为 ceil 处理（注意，这几个元素不能是整数），以保证和等于 target。要使得绝对差之和最小，我们需要尽量选取 ceil(price) - price 小的元素。通过维护一个大小为 ceilNum 的大顶堆可以实现。\nclass Solution: def minimizeError(self, prices: List[str], target: int) -\u0026gt; str: if not prices and target != 0: return \u0026#34;-1\u0026#34; minSum = sum(math.floor(float(price)) for price in prices) maxSum = sum(math.ceil(float(price)) for price in prices) if minSum \u0026gt; target or maxSum \u0026lt; target: return \u0026#34;-1\u0026#34; ceilNum = target - minSum heap = [] for i, price in enumerate(prices): price = float(price) diff = math.ceil(price) - price if diff == 0: continue if len(heap) \u0026lt; ceilNum or (len(heap) \u0026gt; 0 and diff \u0026lt; -heap[0][0]): heapq.heappush(heap, [-diff, i]) if len(heap) \u0026gt; ceilNum: heapq.heappop(heap) for i in range(len(heap)): heap[i][0] *= -1 ans = sum(float(price) for price in prices) - minSum for (diff, i) in heap: price = float(prices[i]) ans = ans - (price - math.floor(price)) + diff return str(format(ans, \u0026#39;.3f\u0026#39;)) ","date":"1 April 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1058_%E6%9C%80%E5%B0%8F%E5%8C%96%E8%88%8D%E5%85%A5%E8%AF%AF%E5%B7%AE%E4%BB%A5%E6%BB%A1%E8%B6%B3%E7%9B%AE%E6%A0%87/","section":"leetcode 题解","summary":"","title":"1058_最小化舍入误差以满足目标","type":"leetcode题解"},{"content":"","date":"1 April 2021","externalUrl":null,"permalink":"/tags/%E5%A0%86/","section":"Tags","summary":"","title":"堆","type":"tags"},{"content":"类型：数组\n避免洪水泛滥 💛 ⭐ https://leetcode-cn.com/problems/avoid-flood-in-the-city/\n❓ 当一个湖泊所在地下雨的时候，若湖泊是空的，则湖泊会装满，若下雨前湖泊已经是满的，则湖泊会溢水。一共有 n 个湖泊。\n给定一个数组 rains，其中：\nrains[i] \u0026gt; 0 表示第 i 天时，第 rains[i] 个湖泊会下雨。 rains[i] == 0 表示第 i 天没有湖泊会下雨，你可以选择一个湖泊并抽干这个湖泊的水。 返回一个表示抽水方案的数组 ans，避免任一湖泊发生溢水。\n如果 rains[i] \u0026gt; 0 ，那么 ans[i] == -1 。 如果 rains[i] == 0 ，ans[i] 是你第 i 天选择抽干的湖泊。 💡 贪心 - 事后诸葛亮\n参见：力扣加加\n我们贪心地想着把抽水用在最需要的地方，因此开始的时候能不抽就先不抽，但是把能抽水的时机存放在数组里，等到湖真的溢水了，我们后悔之前应该抽水的，此时才返回来修改之前的操作。\n因此，我们遍历 rains 数组。\nrains[i] = 0，表示是晴天，我们不抽干任何湖泊，但是我们把 i 记录到 sunny 数组。 rains[i] \u0026gt; 0，表示这个湖泊下雨了，我们去看看这个湖泊会不会溢水（我们用数组 lakes 来记录湖泊的状态）。 如果湖泊要溢水，我们就从 sunny 中找一个晴天去抽干它。要抽水的时候 sunny 为空，则说明溢水无法避免。 class Solution: def avoidFlood(self, rains: List[int]) -\u0026gt; List[int]: sunny = [] lakes = {} ans = [] for i, rain in enumerate(rains): if rain == 0: # 如果这天不下雨，那么不抽干任何湖泊，而是将该天加入 sunny 数组 sunny.append(i) ans.append(1) else: # 这天下雨了，看看会不会泛滥 ans.append(-1) if rain in lakes and lakes[rain] \u0026gt; -1: # 要泛滥了，应该在之前把它抽干 fullDay = lakes[rain] dryDay = -1 for index, day in enumerate(sunny): if day \u0026gt; fullDay: dryDay = day del sunny[index] break if dryDay == -1: # 没机会了 return [] ans[dryDay] = rain lakes[rain] = i return ans 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"31 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1488_%E9%81%BF%E5%85%8D%E6%B4%AA%E6%B0%B4%E6%B3%9B%E6%BB%A5/","section":"leetcode 题解","summary":"","title":"1488_避免洪水泛滥","type":"leetcode题解"},{"content":" 题目 # 实现支持下列接口的「快照数组」- SnapshotArray：\nSnapshotArray(int length) - 初始化一个与指定长度相等的 类数组 的数据结构。初始时，每个元素都等于 0。 void set(index, val) - 会将指定索引 index 处的元素设置为 val。 int snap() - 获取该数组的快照，并返回快照的编号 snap_id（快照号是调用 snap() 的总次数减去 1）。 int get(index, snap_id) - 根据指定的 snap_id 选择快照，并返回该快照指定索引 index 的值。 示例：\n输入：[\u0026#34;SnapshotArray\u0026#34;,\u0026#34;set\u0026#34;,\u0026#34;snap\u0026#34;,\u0026#34;set\u0026#34;,\u0026#34;get\u0026#34;] [[3],[0,5],[],[0,6],[0,0]] 输出：[null,null,0,null,5] 解释： SnapshotArray snapshotArr = new SnapshotArray(3); // 初始化一个长度为 3 的快照数组 snapshotArr.set(0,5); // 令 array[0] = 5 snapshotArr.snap(); // 获取快照，返回 snap_id = 0 snapshotArr.set(0,6); snapshotArr.get(0,0); // 获取 snap_id = 0 的快照中 array[0] 的值，返回 5 提示：\n1 \u0026lt;= length \u0026lt;= 50000 题目最多进行50000 次set，snap，和 get的调用 。 0 \u0026lt;= index \u0026lt; length 0 \u0026lt;= snap_id \u0026lt; 我们调用 snap() 的总次数 0 \u0026lt;= val \u0026lt;= 10^9 解法 # Tag：Binary Search 针对每次快照，我们没有必要进行全量保存，而只进行差异保存。 也就是说，当我们进行快照拍摄时，我们仅针对相对于上次快照产生了变化的元素保存一次快照值。 即每一个元素分别保存自己的快照列表，且仅保存发生过变化的快照。 当我们获取某元素的在某时间的快照值时，我们通过二分法查找到该快照 id 的对应值，若该 id 不在列表中，则说明该次快照并没有值变化，我们取最后一个小于该 id 的快照值。\n代码 # def bs(arr, target, left, right): if right \u0026lt; left: return right mid = (left + right) // 2 if arr[mid] \u0026gt; target: return bs(arr, target, left, mid - 1) else: return bs(arr, target, mid + 1, right) class SnapshotArray: def __init__(self, length: int): self.snapId = -1 self.changed = set() self.arr = [{-1: 0} for _ in range(length)] def set(self, index: int, val: int) -\u0026gt; None: self.changed.add(index) self.arr[index][-1] = val def snap(self) -\u0026gt; int: self.snapId += 1 for index in self.changed: self.arr[index][self.snapId] = self.arr[index][-1] self.changed.clear() return self.snapId def get(self, index: int, snap_id: int) -\u0026gt; int: ids = list(self.arr[index].keys()) idx = bs(ids, snap_id, 0, len(ids) - 1) if idx \u0026gt;= 0 and ids[idx] \u0026gt;= 0 and ids[idx] in self.arr[index]: return self.arr[index][ids[idx]] else: return 0 ","date":"30 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1146_%E5%BF%AB%E7%85%A7%E6%95%B0%E7%BB%84/","section":"leetcode 题解","summary":"","title":"1146_快照数组","type":"leetcode题解"},{"content":"","date":"30 March 2021","externalUrl":null,"permalink":"/tags/%E4%BA%8C%E5%88%86/","section":"Tags","summary":"","title":"二分","type":"tags"},{"content":"类型：堆\n积压订单中的订单总数 💛 ⭐ https://leetcode-cn.com/problems/number-of-orders-in-the-backlog/\n❓ 给你一个二维整数数组 orders ，其中每个 orders[i] = [price, amount, orderType] 表示有 amount 笔类型为 orderType、价格为 price 的订单。\n订单类型 orderTypei 可以分为两种：\n0 表示这是一批采购订单 buy 1 表示这是一批销售订单 sell 存在由未执行订单组成的 积压订单 。积压订单最初是空的。提交订单时，会发生以下情况：\n如果该订单是一笔采购订单 buy ，则可以查看积压订单中价格 最低 的销售订单 sell 。如果该销售订单 sell 的价格 低于或等于 当前采购订单 buy 的价格，则匹配并执行这两笔订单，并将销售订单 sell 从积压订单中删除。否则，采购订单 buy 将会添加到积压订单中。 反之亦然，如果该订单是一笔销售订单 sell ，则可以查看积压订单中价格 最高 的采购订单 buy 。如果该采购订单 buy 的价格 高于或等于 当前销售订单 sell 的价格，则匹配并执行这两笔订单，并将采购订单 buy 从积压订单中删除。否则，销售订单 sell 将会添加到积压订单中。 输入所有订单后，返回积压订单中的 订单总数 。由于数字可能很大，所以需要返回对 109 + 7 取余的结果。\n💡 堆\n我们用一个小顶堆来存储 sell 订单，用一个大顶堆来存储 buy 订单。\nclass Solution: def getNumberOfBacklogOrders(self, orders: List[List[int]]) -\u0026gt; int: sellHeap = [] # 小顶堆 buyHeap = [] # 大顶堆 orderNum = 0 max_num = 10 ** 9 + 7 for [price, amount, orderType] in orders: # 先入堆 if orderType == 0: heapq.heappush(buyHeap, [-price, amount]) else: heapq.heappush(sellHeap, [price, amount]) orderNum += amount while buyHeap and sellHeap and -buyHeap[0][0] \u0026gt;= sellHeap[0][0]: sellAmount = sellHeap[0][1] buyAmount = buyHeap[0][1] minAmount = min(sellAmount, buyAmount) orderNum -= 2 * minAmount if sellAmount - minAmount \u0026gt; 0: sellHeap[0][1] -= minAmount else: heapq.heappop(sellHeap) if buyAmount - minAmount \u0026gt; 0: buyHeap[0][1] -= minAmount else: heapq.heappop(buyHeap) return orderNum % max_num ","date":"29 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1801_%E7%A7%AF%E5%8E%8B%E8%AE%A2%E5%8D%95%E4%B8%AD%E7%9A%84%E8%AE%A2%E5%8D%95%E6%80%BB%E6%95%B0/","section":"leetcode 题解","summary":"","title":"1801_积压订单中的订单总数","type":"leetcode题解"},{"content":"类型：链表\n分隔链表 💛 https://leetcode-cn.com/problems/partition-list/\n❓ 给你一个链表的头节点 head 和一个特定值 x ，请你对链表进行分隔，使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。你应当 保留 两个分区中每个节点的初始相对位置。\n💡 模拟\n我们需要维护两个链表，一个专门存储小结点，一个专门存储大结点，然后在将两者衔接。\n为了实现上述思路，我们设 smallHead 和 largeHead 分别为两个链表的哑节点，即它们的 next 指针指向链表的头节点，这样做的目的是为了更方便地处理头节点为空的边界条件。\n# 官方实现 class Solution { public ListNode partition(ListNode head, int x) { ListNode small = new ListNode(0); ListNode smallHead = small; ListNode large = new ListNode(0); ListNode largeHead = large; while (head != null) { if (head.val \u0026lt; x) { small.next = head; small = small.next; } else { large.next = head; large = large.next; } head = head.next; } large.next = null; small.next = largeHead.next; return smallHead.next; } } # 我的实现，没有用到虚拟头结点 class Solution: def partition(self, head: ListNode, x: int) -\u0026gt; ListNode: lList = None gList = None lp = lList gp = gList while head: if head.val \u0026lt; x: if not lp: lList = head lp = lList else: lp.next = head lp = lp.next else: if not gp: gList = head gp = gList else: gp.next = head gp = gp.next head = head.next if gp: gp.next = None if lp: lp.next = gList return lList else: return gList 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"28 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/86_%E5%88%86%E9%9A%94%E9%93%BE%E8%A1%A8/","section":"leetcode 题解","summary":"","title":"86_分隔链表","type":"leetcode题解"},{"content":"类型：树\n二叉树中所有距离为 K 的结点 💛 https://leetcode-cn.com/problems/all-nodes-distance-k-in-binary-tree/\n❓ 找到二叉树中距目标结点 target 距离为 K 的所有结点。返回结点值列表。\n💡 DFS\n如果 target 节点在 root 节点的左子树中，且 target 节点深度为 3，那所有 root 节点右子树中深度为 K - 3 的节点到 target 的距离就都是 K。\n那么我们定义深度优先搜索方法 dfs(node)，该方法会返回 node 到 target 的距离。在 dfs(node) 中处理以下四种情况：\nnode == target，则把子树中所有距 node 距离为 k 的结点加入答案。 target 在 node 左子树中，假设 target 与 node 距离为 L+1，则找出 node 右子树中所有距离为 K - L - 1 的结点加入答案。 target 在 node 右子树中，与在左子树中的处理方法一样。 target 不在 node 子树中，不用处理。 class Solution(object): def distanceK(self, root, target, K): ans = [] # Return distance from node to target if exists, else -1 # Vertex distance: the # of vertices on the path from node to target def dfs(node): if not node: return -1 elif node is target: subtree_add(node, 0) return 1 else: L, R = dfs(node.left), dfs(node.right) if L != -1: if L == K: ans.append(node.val) subtree_add(node.right, L + 1) return L + 1 elif R != -1: if R == K: ans.append(node.val) subtree_add(node.left, R + 1) return R + 1 else: return -1 # Add all nodes \u0026#39;K - dist\u0026#39; from the node to answer. def subtree_add(node, dist): if not node: return elif dist == K: ans.append(node.val) else: subtree_add(node.left, dist + 1) subtree_add(node.right, dist + 1) dfs(root) return ans 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"27 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/863_%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E6%89%80%E6%9C%89%E8%B7%9D%E7%A6%BB%E4%B8%BA_k_%E7%9A%84%E7%BB%93%E7%82%B9/","section":"leetcode 题解","summary":"","title":"863_二叉树中所有距离为_K_的结点","type":"leetcode题解"},{"content":"类型：树\n好叶子节点对的数量 💛 https://leetcode-cn.com/problems/number-of-good-leaf-nodes-pairs/\n这是字节面试的原题。\n❓ 给你二叉树的根节点 root 和一个整数 distance 。\n如果二叉树中两个 叶 节点之间的 最短路径长度 小于或者等于 distance ，那它们就可以构成一组 好叶子节点对 。\n返回树中 好叶子节点对的数量 。\n💡 递归\n其实任意两个叶子结点之间，有且仅有一条最短路径，就是经过最近公共祖先 P 的那条。\n于是，我们枚举所有非叶子结点 P 作为公共祖先，找到以 P 为最近公共祖先的所有叶子结点对，计算每一对之间的距离，统计距离不超过 distance 的结点对数量。\n由于题目约束 distance ≤ 10，因此对于每一个非叶子结点 P，我们都开辟一个初度为 distance + 1 的数组 depths，其中 depths[i] 表示与 P 之间距离为 i 的叶子结点数目。\nclass Solution: def countPairs(self, root: TreeNode, distance: int) -\u0026gt; int: # 对于 dfs(root,distance)，同时返回： # 每个叶子节点与 root 之间的距离 # 以 root 为根节点的子树中好叶子节点对的数量 def dfs(root: TreeNode, distance: int) -\u0026gt; (List[int], int): depths = [0] * (distance + 1) isLeaf = not root.left and not root.right if isLeaf: depths[0] = 1 return (depths, 0) leftDepths, rightDepths = [0] * (distance + 1), [0] * (distance + 1) leftCount = rightCount = 0 if root.left: leftDepths, leftCount = dfs(root.left, distance) if root.right: rightDepths, rightCount = dfs(root.right, distance) for i in range(distance): depths[i + 1] += leftDepths[i] depths[i + 1] += rightDepths[i] cnt = 0 for i in range(distance + 1): for j in range(distance - i - 1): cnt += leftDepths[i] * rightDepths[j] return (depths, cnt + leftCount + rightCount) _, ret = dfs(root, distance) return ret 时间复杂度：O(n * distance^2)，n 为结点数。空间复杂度：O(h * distance)，h 为树高度。\n","date":"26 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1530_%E5%A5%BD%E5%8F%B6%E5%AD%90%E8%8A%82%E7%82%B9%E5%AF%B9%E7%9A%84%E6%95%B0%E9%87%8F/","section":"leetcode 题解","summary":"","title":"1530_好叶子节点对的数量","type":"leetcode题解"},{"content":" 题目 # 给你一个正整数数组 nums 。\n同时给你一个长度为 m 的整数数组 queries 。第 i 个查询中，你需要将 nums 中所有元素变成 queries[i] 。你可以执行以下操作 任意 次：\n将数组里一个元素 增大 或者 减小 1 。 请你返回一个长度为 m 的数组 **answer ，其中 **answer[i]是将 nums 中所有元素变成 queries[i] 的 最少 操作次数。\n注意，每次查询后，数组变回最开始的值。\n示例 1：\n输入：nums = [3,1,6,8], queries = [1,5] 输出：[14,10] 解释：第一个查询，我们可以执行以下操作： - 将 nums[0] 减小 2 次，nums = [1,1,6,8] 。 - 将 nums[2] 减小 5 次，nums = [1,1,1,8] 。 - 将 nums[3] 减小 7 次，nums = [1,1,1,1] 。 第一个查询的总操作次数为 2 + 5 + 7 = 14 。 第二个查询，我们可以执行以下操作： - 将 nums[0] 增大 2 次，nums = [5,1,6,8] 。 - 将 nums[1] 增大 4 次，nums = [5,5,6,8] 。 - 将 nums[2] 减小 1 次，nums = [5,5,5,8] 。 - 将 nums[3] 减小 3 次，nums = [5,5,5,5] 。 第二个查询的总操作次数为 2 + 4 + 1 + 3 = 10 。 示例 2：\n输入：nums = [2,9,6,3], queries = [10] 输出：[20] 解释：我们可以将数组中所有元素都增大到 10 ，总操作次数为 8 + 1 + 4 + 7 = 20 。 提示：\nn == nums.length m == queries.length 1 \u0026lt;= n, m \u0026lt;= 105 1 \u0026lt;= nums[i], queries[i] \u0026lt;= 109 解法 # 我们对 nums 升序排列，然后计算前缀和。\n对于每次 query, value = queries[i] 我们通过二分法找到第一个 \u0026gt;= value 的数的下标，那么该位置左边的数均 \u0026lt; value, 该位置及右边的数均 \u0026gt;= value.\n通过前缀和，我们知道左边要补多少，右边要减多少。\n代码 # class Solution: def binSearch(self, nums, target, left, right): if right \u0026lt; left: return left mid = (left + right) // 2 if nums[mid] \u0026lt; target: return self.binSearch(nums, target, mid + 1, right) else: return self.binSearch(nums, target, left, mid - 1) def minOperations(self, nums: List[int], queries: List[int]) -\u0026gt; List[int]: nums.sort() n = len(nums) prefix = nums[:] for i in range(1, n): prefix[i] += prefix[i-1] ans = [] for query in queries: idx = self.binSearch(nums, query, 0, n-1) ops = 0 if idx == 0 or idx == n-1: ops = abs(prefix[-1] - query * n) else: ops = (query * idx - prefix[idx-1]) + (prefix[-1] - prefix[idx-1] - query * (n-idx)) ans.append(ops) return ans ","date":"25 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/2602_%E4%BD%BF%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0%E5%85%A8%E9%83%A8%E7%9B%B8%E7%AD%89%E7%9A%84%E6%9C%80%E5%B0%91%E6%93%8D%E4%BD%9C%E6%AC%A1%E6%95%B0/","section":"leetcode 题解","summary":"","title":"2602_使数组元素全部相等的最少操作次数","type":"leetcode题解"},{"content":"","date":"25 March 2021","externalUrl":null,"permalink":"/tags/%E5%89%8D%E7%BC%80%E5%92%8C/","section":"Tags","summary":"","title":"前缀和","type":"tags"},{"content":"类型：数组\n从前序与中序遍历序列构造二叉树 💛 ⭐ https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/\n❓ 已知一棵树的前序遍历和中序遍历数组，构造出这棵二叉树。\n💡 递归\n对于任意一颗树而言，前序遍历的形式总是：\n[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]\n而中序遍历的形式总是：\n[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]\n据此，我们可以递归地构造出二叉树。\n我们可以使用哈希来帮助我们快速定位出根结点在中序遍历数组中的位置。\nclass Solution: def buildTree(self, preorder: List[int], inorder: List[int]) -\u0026gt; TreeNode: def myBuildTree(preorder_left: int, preorder_right: int, inorder_left: int, inorder_right: int): if preorder_left \u0026gt; preorder_right: return None # 前序遍历中的第一个节点就是根节点 preorder_root = preorder_left # 在中序遍历中定位根节点 inorder_root = index[preorder[preorder_root]] # 先把根节点建立出来 root = TreeNode(preorder[preorder_root]) # 得到左子树中的节点数目 size_left_subtree = inorder_root - inorder_left # 递归地构造左子树，并连接到根节点 # 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素 root.left = myBuildTree(preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1) # 递归地构造右子树，并连接到根节点 # 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素 root.right = myBuildTree(preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right) return root n = len(preorder) # 构造哈希映射，帮助我们快速定位根节点 index = {element: i for i, element in enumerate(inorder)} return myBuildTree(0, n - 1, 0, n - 1) 时间复杂度：O(n)，空间复杂度：O(n)\n💡 迭代\n迭代的方法比较巧妙，且不易表述。具体可以参见官方题解。\n","date":"24 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/105_%E4%BB%8E%E5%89%8D%E5%BA%8F%E4%B8%8E%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E5%BA%8F%E5%88%97%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%A0%91/","section":"leetcode 题解","summary":"","title":"105_从前序与中序遍历序列构造二叉树","type":"leetcode题解"},{"content":"类型：数组\n组合总和 II 💛 https://leetcode-cn.com/problems/combination-sum-ii/\n❓ 找出数组 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。\n💡 回溯\nclass Solution: def combinationSum2(self, candidates: List[int], target: int) -\u0026gt; List[List[int]]: n = len(candidates) candidates.sort() track = [] ans = [] def trackback(start): if sum(track) == target: ans.append(track[:]) return if sum(track) \u0026gt; target: return if start \u0026gt;= n: return for i in range(start, n): if i \u0026gt; start and candidates[i] == candidates[i - 1]: continue track.append(candidates[i]) trackback(i + 1) track.pop() trackback(0) return ans class Solution: def combinationSum2(self, candidates: List[int], target: int) -\u0026gt; List[List[int]]: def dfs(pos: int, rest: int): nonlocal sequence if rest == 0: ans.append(sequence[:]) return if pos == len(freq) or rest \u0026lt; freq[pos][0]: return dfs(pos + 1, rest) most = min(rest // freq[pos][0], freq[pos][1]) for i in range(1, most + 1): sequence.append(freq[pos][0]) dfs(pos + 1, rest - i * freq[pos][0]) sequence = sequence[:-most] freq = sorted(collections.Counter(candidates).items()) ans = list() sequence = list() dfs(0, target) return ans 时间复杂度：O(2^n * n)，空间复杂度：O(n)\n","date":"23 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/40_%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8C_ii/","section":"leetcode 题解","summary":"","title":"40_组合总和_II","type":"leetcode题解"},{"content":" 题目 # 给你一个二进制字符串 binary ，它仅有 0 或者 1 组成。你可以使用下面的操作任意次对它进行修改：\n操作 1 ：如果二进制串包含子字符串 \u0026quot;00\u0026quot; ，你可以用 \u0026quot;10\u0026quot; 将其替换。 比方说， \u0026quot;**00**010\u0026quot; -\u0026gt; \u0026quot;**10**010\u0026quot; 操作 2 ：如果二进制串包含子字符串 \u0026quot;10\u0026quot; ，你可以用 \u0026quot;01\u0026quot; 将其替换。 比方说， \u0026quot;000**10**\u0026quot; -\u0026gt; \u0026quot;000**01**\u0026quot; 请你返回执行上述操作任意次以后能得到的 最大二进制字符串 。如果二进制字符串 x 对应的十进制数字大于二进制字符串 y 对应的十进制数字，那么我们称二进制字符串 **x **大于二进制字符串 **y **。\n示例 1：\n输入：binary = \u0026#34;000110\u0026#34; 输出：\u0026#34;111011\u0026#34; 解释：一个可行的转换为： \u0026#34;000110\u0026#34; -\u0026gt; \u0026#34;000101\u0026#34; \u0026#34;000101\u0026#34; -\u0026gt; \u0026#34;100101\u0026#34; \u0026#34;100101\u0026#34; -\u0026gt; \u0026#34;110101\u0026#34; \u0026#34;110101\u0026#34; -\u0026gt; \u0026#34;110011\u0026#34; \u0026#34;110011\u0026#34; -\u0026gt; \u0026#34;111011\u0026#34; 示例 2：\n输入：binary = \u0026#34;01\u0026#34; 输出：\u0026#34;01\u0026#34; 解释：\u0026#34;01\u0026#34; 没办法进行任何转换。 解法 # 此题的数据量很大(10^5), 必须找到线性复杂度的方法才能解决。\n我们从左往右地逐一处理，一开始我们只考虑前两个字符，然后逐步往尾部增加字符来考虑。分析情况之后，我们会发现，该问题的局部最优解即是全局最优解，即我们可以用贪心法来解决。\n我们只需考虑尾部两个字符的情况：\n00: 转换为 10 10: 如果前面还存在 0, 比如 xxx01110, 则我们可以通过连续转换(10-\u0026gt;01)得到 xxx10111 11: 保持不变 01: 保持不变 由于字符串在 python 中是不可直接修改的，使用字符串拼接的开销也很大，我们可以用列表来存储字符，最后在将其拼接成字符串，但是可惜这个操作仍然会超时。\n观察规律，我们会发现，由以上四种操作后，我们的结果字符串中最多只出现一个 0。因而我们唯一需要保存的就是这个 0 出现的位置。\n还有一种直观的思维方式：\n通过操作 10-\u0026gt;01, 我们可以把所有的 1 都放到低位，所有的 0 都放到高位。\n通过操作 00-\u0026gt;10, 我们可以把高位连续的 00000 变成 11110\n代码 # class Solution: def maximumBinaryString(self, binary: str) -\u0026gt; str: if len(binary) \u0026lt; 2: return binary ans = [binary[0], binary[1]] if ans == [\u0026#39;0\u0026#39;, \u0026#39;0\u0026#39;]: ans = [\u0026#39;1\u0026#39;, \u0026#39;0\u0026#39;] idx0 = 0 if ans[0] == \u0026#39;0\u0026#39; else -1 lastChr = ans[-1] for i in range(2, len(binary)): c = binary[i] if lastChr + c == \u0026#39;01\u0026#39;: idx0 = i - 1 lastChr = \u0026#39;1\u0026#39; elif lastChr + c == \u0026#39;00\u0026#39;: lastChr = \u0026#39;0\u0026#39; elif lastChr + c == \u0026#39;10\u0026#39;: if idx0 \u0026gt; -1: idx0 += 1 lastChr = \u0026#39;1\u0026#39; else: lastChr = \u0026#39;0\u0026#39; else: lastChr = \u0026#39;1\u0026#39; return \u0026#39;1\u0026#39; * (len(binary) - 1) + lastChr if idx0 \u0026lt; 0 else \u0026#39;1\u0026#39; * idx0 + \u0026#39;0\u0026#39; + \u0026#39;1\u0026#39; * (len(binary) - idx0 - 1) ","date":"22 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1702_%E4%BF%AE%E6%94%B9%E5%90%8E%E7%9A%84%E6%9C%80%E5%A4%A7%E4%BA%8C%E8%BF%9B%E5%88%B6%E5%AD%97%E7%AC%A6%E4%B8%B2/","section":"leetcode 题解","summary":"","title":"1702_修改后的最大二进制字符串","type":"leetcode题解"},{"content":"","date":"22 March 2021","externalUrl":null,"permalink":"/tags/%E8%B4%AA%E5%BF%83/","section":"Tags","summary":"","title":"贪心","type":"tags"},{"content":" 题目 # 给你一个长度为 n 的数组 words ，该数组由 非空 字符串组成。\n定义字符串 word 的 分数 等于以 word 作为 前缀 的 words[i] 的数目。\n例如，如果 words = [\u0026quot;a\u0026quot;, \u0026quot;ab\u0026quot;, \u0026quot;abc\u0026quot;, \u0026quot;cab\u0026quot;] ，那么 \u0026quot;ab\u0026quot; 的分数是 2 ，因为 \u0026quot;ab\u0026quot; 是 \u0026quot;ab\u0026quot; 和 \u0026quot;abc\u0026quot; 的一个前缀。 返回一个长度为 **n 的数组 **answer **，其中 **answer[i] **是 **words[i] 的每个非空前缀的分数 总和 。\n**注意：**字符串视作它自身的一个前缀。\n示例 1：\n输入：words = [\u0026#34;abc\u0026#34;,\u0026#34;ab\u0026#34;,\u0026#34;bc\u0026#34;,\u0026#34;b\u0026#34;] 输出：[5,4,3,2] 解释：对应每个字符串的答案如下： - \u0026#34;abc\u0026#34; 有 3 个前缀：\u0026#34;a\u0026#34;、\u0026#34;ab\u0026#34; 和 \u0026#34;abc\u0026#34; 。 - 2 个字符串的前缀为 \u0026#34;a\u0026#34; ，2 个字符串的前缀为 \u0026#34;ab\u0026#34; ，1 个字符串的前缀为 \u0026#34;abc\u0026#34; 。 总计 answer[0] = 2 + 2 + 1 = 5 。 - \u0026#34;ab\u0026#34; 有 2 个前缀：\u0026#34;a\u0026#34; 和 \u0026#34;ab\u0026#34; 。 - 2 个字符串的前缀为 \u0026#34;a\u0026#34; ，2 个字符串的前缀为 \u0026#34;ab\u0026#34; 。 总计 answer[1] = 2 + 2 = 4 。 - \u0026#34;bc\u0026#34; 有 2 个前缀：\u0026#34;b\u0026#34; 和 \u0026#34;bc\u0026#34; 。 - 2 个字符串的前缀为 \u0026#34;b\u0026#34; ，1 个字符串的前缀为 \u0026#34;bc\u0026#34; 。 总计 answer[2] = 2 + 1 = 3 。 - \u0026#34;b\u0026#34; 有 1 个前缀：\u0026#34;b\u0026#34;。 - 2 个字符串的前缀为 \u0026#34;b\u0026#34; 。 总计 answer[3] = 2 。 示例 2：\n输入：words = [\u0026#34;abcd\u0026#34;] 输出：[4] 解释： \u0026#34;abcd\u0026#34; 有 4 个前缀 \u0026#34;a\u0026#34;、\u0026#34;ab\u0026#34;、\u0026#34;abc\u0026#34; 和 \u0026#34;abcd\u0026#34;。 每个前缀的分数都是 1 ，总计 answer[0] = 1 + 1 + 1 + 1 = 4 。 提示：\n1 \u0026lt;= words.length \u0026lt;= 1000 1 \u0026lt;= words[i].length \u0026lt;= 1000 words[i] 由小写英文字母组成 解法 # 我们将 words 存储到字典树中。\n在计算分数时，我们计算的是该前缀在多少个 word 中出现，即该路径被复用的次数。\n因而我们在插入 word 时，将沿途所有的节点的值都 +1. 节点值即表示从根到当前位置的路径形成的前缀在 words 中出现的次数。\n查找时，由于我们计算的是 word 的所有合法前缀的分数之和，因而我们在遍历查找的过程中进行累加操作。\n代码 # class Trie: def __init__(self): self.root = {\u0026#39;val\u0026#39;: 0} def add(self, word): node = self.root for c in word: if c not in node: node[c] = {\u0026#39;val\u0026#39;: 0} node = node[c] node[\u0026#39;val\u0026#39;] += 1 def search(self, word, i, node): c = word[i] if c not in node: return 0 node = node[c] val = node[\u0026#39;val\u0026#39;] if i \u0026lt; len(word) - 1: val += self.search(word, i+1, node) return val class Solution: def sumPrefixScores(self, words: List[str]) -\u0026gt; List[int]: trie = Trie() for word in words: trie.add(word) ans = [ trie.search(word, 0, trie.root) for word in words ] return ans ","date":"21 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/2416_%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E5%89%8D%E7%BC%80%E5%88%86%E6%95%B0%E5%92%8C/","section":"leetcode 题解","summary":"","title":"2416_字符串的前缀分数和","type":"leetcode题解"},{"content":"类型：树\n重建二叉树 💛\nhttps://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof/\n❓ 已知一棵树的前序遍历和中序遍历数组，构造出这棵二叉树。\n💡 递归\n对于任意一颗树而言，前序遍历的形式总是：\n[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]\n而中序遍历的形式总是：\n[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]\n据此，我们可以递归地构造出二叉树。\n我们可以使用哈希来帮助我们快速定位出根结点在中序遍历数组中的位置。\nclass Solution: def buildTree(self, preorder: List[int], inorder: List[int]) -\u0026gt; TreeNode: def myBuildTree(preorder_left: int, preorder_right: int, inorder_left: int, inorder_right: int): if preorder_left \u0026gt; preorder_right: return None # 前序遍历中的第一个节点就是根节点 preorder_root = preorder_left # 在中序遍历中定位根节点 inorder_root = index[preorder[preorder_root]] # 先把根节点建立出来 root = TreeNode(preorder[preorder_root]) # 得到左子树中的节点数目 size_left_subtree = inorder_root - inorder_left # 递归地构造左子树，并连接到根节点 # 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素 root.left = myBuildTree(preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1) # 递归地构造右子树，并连接到根节点 # 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素 root.right = myBuildTree(preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right) return root n = len(preorder) # 构造哈希映射，帮助我们快速定位根节点 index = {element: i for i, element in enumerate(inorder)} return myBuildTree(0, n - 1, 0, n - 1) 时间复杂度：O(n)，空间复杂度：O(n)\n💡 迭代\n迭代的方法比较巧妙，且不易表述。具体可以参见官方题解。\n","date":"20 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/%E5%89%91%E6%8C%87_offer_07_%E9%87%8D%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91/","section":"leetcode 题解","summary":"","title":"07_重建二叉树","type":"leetcode题解"},{"content":"https://leetcode.cn/problems/find-longest-awesome-substring/description/\n题目 # 给你一个字符串 s 。请返回 s 中最长的 超赞子字符串 的长度。\n「超赞子字符串」需满足满足下述两个条件：\n该字符串是 s 的一个非空子字符串 进行任意次数的字符交换后，该字符串可以变成一个回文字符串 示例 1：\n输入：s = \u0026#34;3242415\u0026#34; 输出：5 解释：\u0026#34;24241\u0026#34; 是最长的超赞子字符串，交换其中的字符后，可以得到回文 \u0026#34;24142\u0026#34; 示例 2：\n输入：s = \u0026#34;12345678\u0026#34; 输出：1 示例 3：\n输入：s = \u0026#34;213123\u0026#34; 输出：6 解释：\u0026#34;213123\u0026#34; 是最长的超赞子字符串，交换其中的字符后，可以得到回文 \u0026#34;231132\u0026#34; 示例 4：\n输入：s = \u0026#34;00\u0026#34; 输出：2 提示：\n1 \u0026lt;= s.length \u0026lt;= 10^5 s 仅由数字组成 题解 # 一个子串要互文，由于元素位置可以随意互换，因而元素位置并不重要，元素个数才重要。 更确切来说，元素个数的奇偶性才重要，由于互文是左右对称的，只有最中间的那一个元 素（长度为奇数时）才可能落单。\n由于字符串中只有数字字符，即只有 0~9, 我们要表征各个字符出现次数的奇偶性(0/1), 完全可以使用状态压缩。即对应的 ith 比特位为 0 则为偶， 1 则为奇。我们将这样的 一个状态存放在 mask 中。\n之后我们从左向右遍历，对于当前的数字字符 c, 我们通过计算 mask ^= 1 \u0026lt;\u0026lt; c便得到了从下标 0 到当前位置的子串中各个字符出现次数的奇偶性。\n以上内容都还比较容易想到，关键在于发现以下性质：\n当我们知道了 0~i 的 mask 为 mask[i], 0~j 的 mask 为 mask[j], 若满足以下之一的条件，则 i+1~j 对应的子串可组成互文串。\nmas[i] == mask[j] i+1~j 中所有字符出现次数都为偶数次 mask[j] 仅有一个 bit 位不同 i+1~j 中仅有一个字符出现次数为奇数次 整体解决思路为，从左向右遍历，把当前下标作为子串的结尾，同时维护 mask,对于当前 mask, 我们查看相同的 mask, 以及只差一个比特位的 mask 在之前是否出现，根据下标区间计算互文长度。注意只有当一个 mask 首次出现时，我们才将其记录到 hash 中，以 mask 为 key, 下标为值，重复的 mask 再度出现时我们并不更新下标值，因为我们是基于尾坐标去找头坐标，我们希望头坐标越小， 子数组越长越好。\n代码 # class Solution: def longestAwesome(self, s: str) -\u0026gt; int: n = len(s) ans = 0 hash = {0: -1} mask = 0 for i in range(n): c = s[i] mask ^= (1 \u0026lt;\u0026lt; int(c)) if mask in hash: ans = max(ans, i - hash[mask]) else: hash[mask] = i for b in range(10): pairMask = mask ^ (1 \u0026lt;\u0026lt; b) if pairMask in hash: ans = max(ans, i - hash[pairMask]) return ans ","date":"19 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1542_%E6%89%BE%E5%87%BA%E6%9C%80%E9%95%BF%E7%9A%84%E8%B6%85%E8%B5%9E%E5%AD%90%E5%AD%97%E7%AC%A6%E4%B8%B2/","section":"leetcode 题解","summary":"","title":"1542_找出最长的超赞子字符串","type":"leetcode题解"},{"content":"","date":"19 March 2021","externalUrl":null,"permalink":"/tags/bit/","section":"Tags","summary":"","title":"Bit","type":"tags"},{"content":"","date":"19 March 2021","externalUrl":null,"permalink":"/tags/%E7%8A%B6%E6%80%81%E5%8E%8B%E7%BC%A9/","section":"Tags","summary":"","title":"状态压缩","type":"tags"},{"content":"类型：树\n删除二叉搜索树中的节点 💛 ⭐ https://leetcode-cn.com/problems/delete-node-in-a-bst/\n❓ 删除二叉搜索树中值为 key 的结点，要求保证二叉树的性质不变。\n💡 递归\n有三种可能情况：\n被删除结点拥有右子树，此时我们从右子树中找到最小结点顶替它。 被删除结点没有右子树，但有左子树，我们直接用左子树（的根结点）顶替它。 被删除结点为叶子结点，可以直接删除。从代码层面来说，可以和上一种情况的处理合并在一起。 class Solution: def deleteNode(self, root: TreeNode, key: int) -\u0026gt; TreeNode: if not root: return None # 首先要查找结点 dummyRoot = TreeNode() dummyRoot.left = root def doDelete(node, key): if not node: return None if key \u0026lt; node.val: node.left = doDelete(node.left, key) elif key \u0026gt; node.val: node.right = doDelete(node.right, key) if node.val == key: if not node.right: return node.left # 找到右子树中的最小结点（在右子树中一路往左） parentNode = node childNode = node.right while childNode.left: parentNode = childNode childNode = childNode.left if childNode == parentNode.left: parentNode.left = childNode.right else: parentNode.right = childNode.right # 用右子树的最小结点顶替被删结点 childNode.left = node.left childNode.right = node.right return childNode else: return node dummyRoot.left = doDelete(dummyRoot.left, key) return dummyRoot.left ","date":"18 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/450_%E5%88%A0%E9%99%A4%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9/","section":"leetcode 题解","summary":"","title":"450_删除二叉搜索树中的节点","type":"leetcode题解"},{"content":" 题目 # 给你一个任务数组 tasks ，其中 tasks[i] = [actuali, minimumi] ：\nactuali 是完成第 i 个任务 需要耗费 的实际能量。 minimumi 是开始第 i 个任务前需要达到的最低能量。 比方说，如果任务为 [10, 12] 且你当前的能量为 11 ，那么你不能开始这个任务。如果你当前的能量为 13 ，你可以完成这个任务，且完成它后剩余能量为 3 。\n你可以按照 任意顺序 完成任务。\n请你返回完成所有任务的 最少 初始能量。\n示例 1：\n输入：tasks = [[1,2],[2,4],[4,8]] 输出：8 解释： 一开始有 8 能量，我们按照如下顺序完成任务： - 完成第 3 个任务，剩余能量为 8 - 4 = 4 。 - 完成第 2 个任务，剩余能量为 4 - 2 = 2 。 - 完成第 1 个任务，剩余能量为 2 - 1 = 1 。 注意到尽管我们有能量剩余，但是如果一开始只有 7 能量是不能完成所有任务的，因为我们无法开始第 3 个任务。 示例 2：\n输入：tasks = [[1,3],[2,4],[10,11],[10,12],[8,9]] 输出：32 解释： 一开始有 32 能量，我们按照如下顺序完成任务： - 完成第 1 个任务，剩余能量为 32 - 1 = 31 。 - 完成第 2 个任务，剩余能量为 31 - 2 = 29 。 - 完成第 3 个任务，剩余能量为 29 - 10 = 19 。 - 完成第 4 个任务，剩余能量为 19 - 10 = 9 。 - 完成第 5 个任务，剩余能量为 9 - 8 = 1 。 示例 3：\n输入：tasks = [[1,7],[2,8],[3,9],[4,10],[5,11],[6,12]] 输出：27 解释： 一开始有 27 能量，我们按照如下顺序完成任务： - 完成第 5 个任务，剩余能量为 27 - 5 = 22 。 - 完成第 2 个任务，剩余能量为 22 - 2 = 20 。 - 完成第 3 个任务，剩余能量为 20 - 3 = 17 。 - 完成第 1 个任务，剩余能量为 17 - 1 = 16 。 - 完成第 4 个任务，剩余能量为 16 - 4 = 12 。 - 完成第 6 个任务，剩余能量为 12 - 6 = 6 。 提示：\n1 \u0026lt;= tasks.length \u0026lt;= 105 1 \u0026lt;= actuali \u0026lt;= minimumi \u0026lt;= 104 题解 # 最终所有任务都需要完成，因而总能量不可能低于 actual 之和，真正会影响结果的是 minimum 这个启动门槛。\n我们希望避免出现这一状况：当前能当能够完成该任务，确无法启动该任务。\n因而我们将 tasks 以 minimum - actual 为 key 进行升序排列，我们设初始的 curEnergy 为 0, 遍历任务，当前能量无法启动并完成该任务时，我们便进行充能。记录下我们总共补充的能量即为答案。\n代码 # class Solution: def minimumEffort(self, tasks: List[List[int]]) -\u0026gt; int: tasks.sort(key = lambda x: x[0] - x[1]) curEnergy = 0 totalEnergy = 0 for [actual, mini] in tasks: need = max(actual, mini) if curEnergy \u0026lt; need: totalEnergy += need - curEnergy curEnergy = need curEnergy -= actual return totalEnergy ","date":"17 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1665_%E5%AE%8C%E6%88%90%E6%89%80%E6%9C%89%E4%BB%BB%E5%8A%A1%E7%9A%84%E6%9C%80%E5%B0%91%E5%88%9D%E5%A7%8B%E8%83%BD%E9%87%8F/","section":"leetcode 题解","summary":"","title":"1665_完成所有任务的最少初始能量","type":"leetcode题解"},{"content":" 题目 # 给你一棵二叉树的根节点 root ，二叉树中节点的值 互不相同 。另给你一个整数 start 。在第 0 分钟，感染 将会从值为 start 的节点开始爆发。\n每分钟，如果节点满足以下全部条件，就会被感染：\n节点此前还没有感染。 节点与一个已感染节点相邻。 返回感染整棵树需要的分钟数*。*\n示例 1：\n输入：root = [1,5,3,null,4,10,6,9,2], start = 3 输出：4 解释：节点按以下过程被感染： - 第 0 分钟：节点 3 - 第 1 分钟：节点 1、10、6 - 第 2 分钟：节点5 - 第 3 分钟：节点 4 - 第 4 分钟：节点 9 和 2 感染整棵树需要 4 分钟，所以返回 4 。 示例 2：\n输入：root = [1], start = 1 输出：0 解释：第 0 分钟，树中唯一一个节点处于感染状态，返回 0 。 提示：\n树中节点的数目在范围 [1, 105] 内 1 \u0026lt;= Node.val \u0026lt;= 105 每个节点的值 互不相同 树中必定存在值为 start 的节点 解法 # 我们使用后序遍历，对于每个节点维护以下信息：\n该节点被感染的时间(尚未感染则为 -1) 以该节点为根的树的深度 对于当前遍历的节点，我们分以下情况考虑：\n感染源就是该节点\n感染以本节点为根的树所需时间 max(左子树深度, 右子树深度)\n感染源在左子树中\n感染以本节点为根的树所需时间\n感染源在右子树中\n感染源既不是该节点，也不在其左右子树中\n代码 # class Solution: def amountOfTime(self, root: Optional[TreeNode], start: int) -\u0026gt; int: ans = 0 def dfs(node): nonlocal ans if not node: return (-1, 0) (leftInfectTime, leftDepth) = dfs(node.left) (rightInfectTime, rightDepth) = dfs(node.right) if node.val == start: ans = max(ans, max(leftDepth, rightDepth)) return (0, 0) elif leftInfectTime \u0026gt;= 0: ans = max(ans, leftInfectTime + 1 + rightDepth) return (leftInfectTime + 1, 0) elif rightInfectTime \u0026gt;= 0: ans = max(ans, rightInfectTime + 1 + leftDepth) return (rightInfectTime + 1, 0) else: return (-1, max(leftDepth, rightDepth) + 1) dfs(root) return ans ","date":"16 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/2385_%E6%84%9F%E6%9F%93%E4%BA%8C%E5%8F%89%E6%A0%91%E9%9C%80%E8%A6%81%E7%9A%84%E6%80%BB%E6%97%B6%E9%97%B4/","section":"leetcode 题解","summary":"","title":"2385_感染二叉树需要的总时间","type":"leetcode题解"},{"content":"","date":"16 March 2021","externalUrl":null,"permalink":"/tags/dfs/","section":"Tags","summary":"","title":"DFS","type":"tags"},{"content":"类型：字符串\n括号生成 💛 https://leetcode-cn.com/problems/generate-parentheses/\n❓ 有 n 对括号，生成其所有可能的有效组合。\n输入：n = 3 输出：[\u0026quot;((()))\u0026quot;,\u0026quot;(()())\u0026quot;,\u0026quot;(())()\u0026quot;,\u0026quot;()(())\u0026quot;,\u0026quot;()()()\u0026quot;]\n💡 回溯法\n我们在枚举的过程中，记录我们目前已经放置的左括号和右括号的数量。在枚举过程中，我们要遵从如下约束：\n已放置的左括号的数量小于 n 的时候，我们才可以继续放置左括号。 已放置的右括号数量小于已放置左括号的数量的时候，我们才可以继续放置右括号。 class Solution: def generateParenthesis(self, n: int) -\u0026gt; List[str]: ans = [] def backtrack(S, left, right): if len(S) == 2 * n: ans.append(\u0026#39;\u0026#39;.join(S)) return if left \u0026lt; n: S.append(\u0026#39;(\u0026#39;) backtrack(S, left+1, right) S.pop() if right \u0026lt; left: S.append(\u0026#39;)\u0026#39;) backtrack(S, left, right+1) S.pop() backtrack([], 0, 0) return ans 💡 按括号序列的长度递归生成\n一个合法的括号表达必然由 ( 开始，并且必然有一个对应的 )，它们中间还可以有括号表达式，它们后面也还可以有括号表达式。因此其形式可以表示为 (a)b，其实 a 和 b 都是合法的括号表达式（可以为空）。\n我们的函数 generate(n) 的计算过程如下：\n枚举与第一个 ( 对应的 ) 的位置，设为 2 * i + 1 递归调用 generate(i) 即可计算 a 的所有可能性 递归调用 generate(n - i - 1) 即可计算 b 的所有可能性 遍历 a 与 b 的所有可能性并拼接，即可得到所有长度为 2 * n 的括号序列 class Solution: @lru_cache(None) def generateParenthesis(self, n: int) -\u0026gt; List[str]: if n == 0: return [\u0026#39;\u0026#39;] ans = [] for c in range(n): for left in self.generateParenthesis(c): for right in self.generateParenthesis(n-1-c): ans.append(\u0026#39;({}){}\u0026#39;.format(left, right)) return ans ","date":"15 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/22_%E6%8B%AC%E5%8F%B7%E7%94%9F%E6%88%90/","section":"leetcode 题解","summary":"","title":"22_括号生成","type":"leetcode题解"},{"content":"类型：链表\n回文链表 💚 https://leetcode-cn.com/problems/palindrome-linked-list/\n❓ 判断一个链表是否为回文链表（如 1-\u0026gt;2-\u0026gt;2-\u0026gt;1）。\n💡 递归\n使用递归反向迭代节点，同时使用递归函数外的变量向前迭代，就可以判断链表是否为回文。\nclass Solution: def isPalindrome(self, head: ListNode) -\u0026gt; bool: self.front_pointer = head def recursively_check(current_node=head): if current_node is not None: if not recursively_check(current_node.next): return False if self.front_pointer.val != current_node.val: return False self.front_pointer = self.front_pointer.next return True return recursively_check() 时间复杂度：O(n)，空间复杂度：O(n)\n💡 后半部分反转\n反转后半部分链表，然后再判断回文。\nclass Solution: def isPalindrome(self, head: ListNode) -\u0026gt; bool: if not head or not head.next: return True preNode = None slowNode = head fastNode = head while fastNode is not None and fastNode.next is not None: fastNode = fastNode.next.next tempNode = slowNode.next slowNode.next = preNode preNode = slowNode slowNode = tempNode if fastNode: slowNode = slowNode.next while preNode: if preNode.val != slowNode.val: return False else: preNode = preNode.next slowNode = slowNode.next return True 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"14 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/234_%E5%9B%9E%E6%96%87%E9%93%BE%E8%A1%A8/","section":"leetcode 题解","summary":"","title":"234_回文链表","type":"leetcode题解"},{"content":"类型：树\n二叉树最大宽度 💛 ⭐ https://leetcode-cn.com/problems/maximum-width-of-binary-tree/\n❓ 计算一棵树的最大宽度。\n输入: 1 / \\ 3 2 / \\ \\ 5 3 9 输出: 4 解释: 最大值出现在树的第 3 层，宽度为 4 (5,3,null,9)。 💡 此题的主要方法是给每一个结点记录一个 position 值，如果我们走向左子树，那么 pos → pos * 2，如果我们走向右子树，那么 pos → pos * 2 + 1。通过同一层最两端的结点的 pos，就可以计算该层的宽度：R - L + 1。\n💡 BFS\ndef widthOfBinaryTree(self, root): queue = [(root, 0, 0)] cur_depth = left = ans = 0 for node, depth, pos in queue: if node: queue.append((node.left, depth+1, pos*2)) queue.append((node.right, depth+1, pos*2 + 1)) if cur_depth != depth: # 说明我们到了新的一层 cur_depth = depth left = pos ans = max(pos - left + 1, ans) return ans 时间复杂度：O(n)，空间复杂度：O(n)\n💡 DFS\n对于每一个深度，第一个到达的位置会被记录在 left[depth] 中。\n然后对于每一个节点，它对应这一层的可能宽度是 pos - left[depth] + 1 。我们将每一层这些可能的宽度取一个最大值就是答案。\nclass Solution(object): def widthOfBinaryTree(self, root): self.ans = 0 left = {} def dfs(node, depth = 0, pos = 0): if node: left.setdefault(depth, pos) self.ans = max(self.ans, pos - left[depth] + 1) dfs(node.left, depth + 1, pos * 2) dfs(node.right, depth + 1, pos * 2 + 1) dfs(root) return self.ans 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"13 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/662_%E4%BA%8C%E5%8F%89%E6%A0%91%E6%9C%80%E5%A4%A7%E5%AE%BD%E5%BA%A6/","section":"leetcode 题解","summary":"","title":"662_二叉树最大宽度","type":"leetcode题解"},{"content":"类型：链表\n两数相加 💛 https://leetcode-cn.com/problems/add-two-numbers/\n❓ 给你两个 非空 的链表，表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的，并且每个节点只能存储 一位 数字。请你将两个数相加，并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外，这两个数都不会以 0 开头。\n示例：\n输入：l1 = [2,4,3], l2 = [5,6,4] 输出：[7,0,8] 解释：342 + 465 = 807.\n💡 模拟\n# Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution: def addTwoNumbers(self, l1: ListNode, l2: ListNode) -\u0026gt; ListNode: if not l1 and not l2: return None elif not l1: return l2 elif not l2: return l1 def unitAdd(node1, node2, carry, preSum): if not node1 and not node2 and not carry: return val1 = node1.val if node1 else 0 val2 = node2.val if node2 else 0 unitSum = val1 + val2 + carry if unitSum \u0026gt; 9: unitSum -= 10 carry = 1 else: carry = 0 preSum.next = ListNode(unitSum) node1 = node1.next if node1 else None node2 = node2.next if node2 else None preSum = preSum.next unitAdd(node1, node2, carry, preSum) dummyHead = ListNode() carry = 0 unitAdd(l1, l2, carry, dummyHead) return dummyHead.next 时间复杂度：O(max(m, n))，空间复杂度：O(1)\n","date":"12 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/2_%E4%B8%A4%E6%95%B0%E7%9B%B8%E5%8A%A0/","section":"leetcode 题解","summary":"","title":"2_两数相加","type":"leetcode题解"},{"content":"类型：树\n对称二叉树 💚 ⭐ https://leetcode-cn.com/problems/symmetric-tree/\n❓ 给定一个二叉树，检查它是否是镜像对称的。\n💡 递归\n如果一棵树的左右子树互为镜像对称，那么这棵树是对称的。\n两棵树在什么情况下满足互为镜像对称？\n它们的根结点值相同 A 树的左子树与 B 树的右子树镜像对称 A 树的右子树与 B 树的左子树镜像对称 class Solution: def check(self, p: TreeNode, q: TreeNode) -\u0026gt; bool: if not p and not q: return True elif not p or not q: return False else: return p.val == q.val and self.check(p.left, q.right) and self.check(p.right, q.left) def isSymmetric(self, root: TreeNode) -\u0026gt; bool: if not root: return True return self.check(root.left, root.right) 时间复杂度：O(n)，空间复杂度：O(n)\n💡 迭代\n我们引入队列来将递归改写成迭代。\n初始时我们把根节点入队两次。每次提取两个结点并比较它们的值（队列中每两个连续的结点应该是相等的，而且它们的子树互为镜像），然后将两个结点的左右子结点按相反的顺序插入队列中。当队列为空时，或者我们检测到树不对称（即从队列中取出两个不相等的连续结点）时，该算法结束。\nclass Solution: def isSymmetric(self, root: TreeNode) -\u0026gt; bool: if not root: return True queue = [root.left, root.right] while queue: p = queue.pop(0) q = queue.pop(0) if not p and not q: continue elif not p or not q or p.val != q.val: return False queue.append(p.left) queue.append(q.right) queue.append(p.right) queue.append(q.left) return True 时间复杂度：O(n)。空间复杂度：O(n)，队列长度不会超过 n\n","date":"11 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/101_%E5%AF%B9%E7%A7%B0%E4%BA%8C%E5%8F%89%E6%A0%91/","section":"leetcode 题解","summary":"","title":"101_对称二叉树","type":"leetcode题解"},{"content":"类型：数组\n最接近的三数之和 💛 https://leetcode-cn.com/problems/3sum-closest/\n❓ 从数组 nums 中找出三个数，使得它们的和与 target 最接近。返回这三个数的和。\n💡 排序 + 双指针\nclass Solution: def threeSumClosest(self, nums: List[int], target: int) -\u0026gt; int: nums.sort() n = len(nums) best = 10**7 # 根据差值的绝对值来更新答案 def update(cur): nonlocal best if abs(cur - target) \u0026lt; abs(best - target): best = cur # 枚举 a for i in range(n): # 保证和上一次枚举的元素不相等 if i \u0026gt; 0 and nums[i] == nums[i - 1]: continue # 使用双指针枚举 b 和 c j, k = i + 1, n - 1 while j \u0026lt; k: s = nums[i] + nums[j] + nums[k] # 如果和为 target 直接返回答案 if s == target: return target update(s) if s \u0026gt; target: # 如果和大于 target，移动 c 对应的指针 k0 = k - 1 # 移动到下一个不相等的元素 while j \u0026lt; k0 and nums[k0] == nums[k]: k0 -= 1 k = k0 else: # 如果和小于 target，移动 b 对应的指针 j0 = j + 1 # 移动到下一个不相等的元素 while j0 \u0026lt; k and nums[j0] == nums[j]: j0 += 1 j = j0 return best 时间复杂度：O(N^2)，空间复杂度：O(logN)\n","date":"10 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/16_%E6%9C%80%E6%8E%A5%E8%BF%91%E7%9A%84%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/","section":"leetcode 题解","summary":"","title":"16_最接近的三数之和","type":"leetcode题解"},{"content":"类型：树\n完全二叉树的节点个数 💛 ⭐ https://leetcode-cn.com/problems/count-complete-tree-nodes/\n❓ 给你一棵完全二叉树，求结点个数。\n💡 二分查找\n根据完全二叉树的性质，其最左边的结点一定位于最底层，因此我们从根结点出发一直向左，就可以得到树的高度 h。\n根据完全二叉树的性质，其高度为 h，则结点数量在 [2^h， 2^(h+1) - 1] 的范围内。关键还是在于最底层。\n此题的一个关键应用在于，如何通过位运算得出编号为 k 的节点在完全二叉树中的路径。\nclass Solution: def countNodes(self, root: TreeNode) -\u0026gt; int: if not root: return 0 depth = -1 node = root while node: depth += 1 node = node.left minNo = 2 ** depth maxNo = 2 ** (depth + 1) - 1 # check 编号为 num 的结点是否存在 def check(node, num): path = [] while num: path.insert(0, num \u0026amp; 1) num = num \u0026gt;\u0026gt; 1 for direction in path[1:]: if direction == 0: if not node.left: return False else: node = node.left else: if not node.right: return False else: node = node.right return True while minNo \u0026lt;= maxNo: midNo = (minNo + maxNo) // 2 if check(root, midNo): minNo = midNo + 1 else: maxNo = midNo - 1 return maxNo ","date":"9 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/222_%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E8%8A%82%E7%82%B9%E4%B8%AA%E6%95%B0/","section":"leetcode 题解","summary":"","title":"222_完全二叉树的节点个数","type":"leetcode题解"},{"content":"类型：树\n验证二叉搜索树 💛 https://leetcode-cn.com/problems/validate-binary-search-tree/\n❓ 验证一棵二叉树是不是搜索树。\n可以用前序、中序、后序三种方法来做，请看 leetcode 提交记录。\n💡 中序遍历\n一棵二叉搜索树的中序遍历结果应该是递增序列。\nclass Solution: def isValidBST(self, root: TreeNode) -\u0026gt; bool: # 对搜索树进行中序遍历，其结果必然是有序的 if not root: return False res = [] def traverse(node): if not node: return traverse(node.left) res.append(node.val) traverse(node.right) traverse(root) return all(res[i] \u0026lt; res[i + 1] for i in range(len(res) - 1)) 时间复杂度：O(n)，空间复杂度：O(n)\n💡 递归\n对于搜索树的任意结点来说，其左子树的结点值均小于该结点，其右子树的结点值均大于该结点。\n我们设计一个函数 check(root, lower, upper) 来检查以 root 为根的子树是否结点值均在 (l, r) 范围内（开区间）。一开始的时候，我们的范围应该是 (-inf, +inf)。\nclass Solution: def isValidBST(self, root: TreeNode) -\u0026gt; bool: def check(node, lower = float(\u0026#39;-inf\u0026#39;), upper = float(\u0026#39;inf\u0026#39;)) -\u0026gt; bool: if not node: return True val = node.val if val \u0026lt;= lower or val \u0026gt;= upper: return False if not check(node.right, val, upper): return False if not check(node.left, lower, val): return False return True return check(root) 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"8 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/98_%E9%AA%8C%E8%AF%81%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","section":"leetcode 题解","summary":"","title":"98_验证二叉搜索树","type":"leetcode题解"},{"content":"类型：树\n序列化二叉树 ❤️ ⭐\nhttps://leetcode-cn.com/problems/xu-lie-hua-er-cha-shu-lcof/\n❓ 请实现两个函数，分别用来序列化和反序列化二叉树。\n💡 按层记录\n序列化和反序列化，关键在于二者的可逆性。也就是信息的完整性。本质在于找到一种存储方式，以及对应的序列、反序列方法。\n像一般的前序、中序、后序遍历，其所记录的信息都是不完整的。要记录完整信息当然也不是难事，比如「前序+中序」其实就是一个完整信息，参考：105. 从前序与中序遍历序列构造二叉树 。\n此处我们使用更为简单直观的存储方式——按层存储。其中的关键在于，我们要将按照完全二叉树的规格的存储，对于不存在的结点，我们用 null 来表示，这样才能够消除歧义。\nclass Codec: def serialize(self, root): if not root: return \u0026#34;[]\u0026#34; queue = collections.deque() queue.append(root) res = [] while queue: node = queue.popleft() if node: res.append(str(node.val)) queue.append(node.left) queue.append(node.right) else: res.append(\u0026#34;null\u0026#34;) return \u0026#39;[\u0026#39; + \u0026#39;,\u0026#39;.join(res) + \u0026#39;]\u0026#39; def deserialize(self, data): if data == \u0026#34;[]\u0026#34;: return vals, i = data[1:-1].split(\u0026#39;,\u0026#39;), 1 root = TreeNode(int(vals[0])) queue = collections.deque() queue.append(root) while queue: node = queue.popleft() if vals[i] != \u0026#34;null\u0026#34;: node.left = TreeNode(int(vals[i])) queue.append(node.left) i += 1 if vals[i] != \u0026#34;null\u0026#34;: node.right = TreeNode(int(vals[i])) queue.append(node.right) i += 1 return root 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"7 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/%E5%89%91%E6%8C%87_offer_37_%E5%BA%8F%E5%88%97%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91/","section":"leetcode 题解","summary":"","title":"37_序列化二叉树","type":"leetcode题解"},{"content":"类型：堆\n数据流的中位数 ❤️ ⭐ https://leetcode-cn.com/problems/find-median-from-data-stream/\n❓ 实现一个支持中位数获取的数据流结构。包含如下方法：\nvoid addNum(int num)\n添加数据 num 到数据流中。\ndouble findMedian()\n返回当前的中位数。\n💡 堆\n我们维护两个堆，一个是大顶堆，一个是小顶堆，保持这两个堆的大小之差不超过 1.\nclass MedianFinder: def __init__(self): \u0026#34;\u0026#34;\u0026#34; initialize your data structure here. \u0026#34;\u0026#34;\u0026#34; self.leftHeap = [] # 左堆是大顶堆 self.rightHeap = [] # 右堆是小顶堆 def addNum(self, num: int) -\u0026gt; None: if not self.leftHeap or num \u0026lt; -1 * min(self.leftHeap): # 插入到左堆中 heapq.heappush(self.leftHeap, -1 * num) else: # 插入到右堆中 heapq.heappush(self.rightHeap, num) # 调整两个堆的大小 if len(self.leftHeap) - len(self.rightHeap) \u0026lt; 0: # 右堆移给左堆 heapq.heappush(self.leftHeap, -1 * heapq.heappop(self.rightHeap)) elif len(self.leftHeap) - len(self.rightHeap) \u0026gt; 1: # 左堆移给右堆 heapq.heappush(self.rightHeap, -1 * heapq.heappop(self.leftHeap)) def findMedian(self) -\u0026gt; float: if not self.leftHeap and not self.rightHeap: return 0 if len(self.leftHeap) == len(self.rightHeap): return (-1 * min(self.leftHeap) + min(self.rightHeap)) / 2 else: return -1 * min(self.leftHeap) 时间复杂度：O(logN)，空间复杂度：O(N)\n","date":"6 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/295_%E6%95%B0%E6%8D%AE%E6%B5%81%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0/","section":"leetcode 题解","summary":"","title":"295_数据流的中位数","type":"leetcode题解"},{"content":"类型：堆\n雇佣 K 名工人的最低成本 ❤️ ⭐ https://leetcode-cn.com/problems/minimum-cost-to-hire-k-workers/\n参见：力扣加加\n❓ 有 N 名工人。数组 quality 表明了每个人的工作质量，数组 wage 表明了每个人的期望薪资。我们现在要从中雇佣 K 个人，在工资支付上我们需要遵从如下要求：\n每个人的薪资都不能低于其期望值 工人们的薪资比要符合他们的工作质量之比 求我们雇佣 K 个人的最低成本。\n💡 堆\n花最少的钱，则必然至少有一位员工拿的是其最低期望工资。以此为基准根据工作质量比计算其他人的工资。\n员工价值 R = w / q 如果一个员工的价值为 R，当他恰好拿到最低工资时，所有价值高于 R 的员工都无法拿到预期工资，而所有价值低于 R 的员工都能拿得比预期工资多。我们可以将员工按照价值从小到大排列，当我们以第 i 位员工的预期工资作为薪资标准时，则包括 i 在内的前 i 位员工是可以被招聘的，i 之后的员工则无法被招聘。\n通过员工价值，我们判断出了在具体工资标准下，哪些员工可被招聘。即员工价值决定了该员工能不能被招来。\n当我们设定以第 i 为员工作为薪资标准后。我们可以从 1 ～ i 号员工中选出 k 位，达到最低工资开销。\n这 k 为该怎么选呢，也是根据员工价值吗？不是的。 当我们选定了 i 号员工作为基准后，就相当于选定了以 i 号员工的价值作为所有原有员工的价值。此时其他员工的实际价值就已经没有意义了。 由于此题只关注员工数量，不关注实际工作产出量，在价值确定的情况下，我们倾向于工作质量（效率）低员工，因为你工作质量越高，干得活越多，我们就要给你越多的钱。\nclass Solution: def mincostToHireWorkers(self, quality: List[int], wage: List[int], K: int) -\u0026gt; float: from fractions import Fraction workers = sorted([(Fraction(w, q), q, w) for q, w in zip(quality, wage)]) ans = float(\u0026#39;inf\u0026#39;) pool = [] for ratio, q, w in workers: if not pool or len(pool) \u0026lt; K or q \u0026lt;= -1 * min(pool): heapq.heappush(pool, -q) if len(pool) \u0026gt; K: heapq.heappop(pool) if len(pool) == K: ans = min(ans, -1 * sum(pool) * ratio) return ans 时间复杂度：O(NlogN)，空间复杂度：O(N)\n","date":"5 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/857_%E9%9B%87%E4%BD%A3_k_%E5%90%8D%E5%B7%A5%E4%BA%BA%E7%9A%84%E6%9C%80%E4%BD%8E%E6%88%90%E6%9C%AC/","section":"leetcode 题解","summary":"","title":"857_雇佣_K_名工人的最低成本","type":"leetcode题解"},{"content":"类型：数组\n找到所有数组中消失的数字 💚 https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array/\n❓ 给定一个范围在 1 ≤ a[i] ≤ n ( n = 数组大小 ) 的 整型数组，数组中的元素一些出现了两次，另一些只出现一次。找到所有在 [1, n] 范围之间没有出现在数组中的数字。\n💡 排序\n如果没有重复和空缺的话，排序后应该是一个公差为 1 的等差数列。正是因为重复，造成了某些数字之间差为 0，某些数字之间差大于 1。\n排序后，我们找到这些差大于 1 的数字，其中所间隔的数字就是缺少的数字。\nclass Solution: def findDisappearedNumbers(self, nums: List[int]) -\u0026gt; List[int]: nums.sort() ans = [] for i in range(len(nums)): if (i == 0 and nums[i] \u0026gt; 1) or nums[i] \u0026gt; nums[i - 1] + 1: for n in range(nums[i - 1] + 1 if i \u0026gt; 0 else 1, nums[i]): ans.append(n) for n in range(nums[-1] + 1, len(nums) + 1): ans.append(n) return ans 时间复杂度：O(n^2)，空间复杂度：O(1)\n💡 原地修改 - 代替哈希\n这里其实和「剑指 Offer 03. 数组中重复的数字」中用到的是同一种方法。\n由于数字范围在 [1, n] 中，我们可以用一个长度为 n 的数组来代替哈希表。而 nums 数组的长度恰好也是 n，我们能否让 nums 充当哈希表呢？\n我们遍历 nums，每遇到一个数 x，就让 nums[x - 1] 增加 n（由于会存在多次增加，因此实际上应该是让 nums[(x - 1) % n] 增加 n）。我们再次遍历时，若发现 nums[i] 未大于 n，则说明数字 i + 1 是缺失的。\nclass Solution: def findDisappearedNumbers(self, nums: List[int]) -\u0026gt; List[int]: n = len(nums) for num in nums: x = (num - 1) % n nums[x] += n ret = [i + 1 for i, num in enumerate(nums) if num \u0026lt;= n] return ret 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"4 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/448_%E6%89%BE%E5%88%B0%E6%89%80%E6%9C%89%E6%95%B0%E7%BB%84%E4%B8%AD%E6%B6%88%E5%A4%B1%E7%9A%84%E6%95%B0%E5%AD%97/","section":"leetcode 题解","summary":"","title":"448_找到所有数组中消失的数字","type":"leetcode题解"},{"content":"类型：数组\n多数元素 💚 ⭐ https://leetcode-cn.com/problems/majority-element/\n❓ 从长度为 n 的数组 nums 中找出多数（出现次数大于 n/2）元素。假定多数元素必然存在。\n💡 投票法\nclass Solution: def majorityElement(self, nums: List[int]) -\u0026gt; int: count = 0 candidate = None for num in nums: if count == 0: candidate = num count += (1 if num == candidate else -1) return candidate 时间复杂度：O(n)，空间复杂度：O(1)\n💡 哈希表\nclass Solution: def majorityElement(self, nums: List[int]) -\u0026gt; int: counts = collections.Counter(nums) return max(counts.keys(), key=counts.get) 时间复杂度：O(n)，空间复杂度：O(1)\n💡 排序\n如果将数组 nums 中的所有元素按照单调递增或单调递减的顺序排序，那么下标为 n//2 的元素（下标从 0 开始）一定是众数。\nclass Solution: def majorityElement(self, nums: List[int]) -\u0026gt; int: nums.sort() return nums[len(nums) // 2] 时间复杂度：O(NlogN)，空间复杂度：O(n)\n💡 随机试探\n既然众数的出现概率高，我们随机挑选一个下标对应的元素并对其进行验证，有很大的概率能找到众数。\nclass Solution: def majorityElement(self, nums: List[int]) -\u0026gt; int: majority_count = len(nums) // 2 while True: candidate = random.choice(nums) if sum(1 for elem in nums if elem == candidate) \u0026gt; majority_count: return candidate 时间复杂度：最坏的话是O(∞)，平均来说，我们试探到众数的概率是常数，验证的复杂度是 O(n)，最终平均下来是 O(n)\n空间复杂度：O(1)\n","date":"3 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/169_%E5%A4%9A%E6%95%B0%E5%85%83%E7%B4%A0/","section":"leetcode 题解","summary":"","title":"169_多数元素","type":"leetcode题解"},{"content":"类型：字符串\n将数组拆分成斐波那契序列 💛 https://leetcode-cn.com/problems/split-array-into-fibonacci-sequence/\n❓ 给定一个数字字符串 S，比如 S = \u0026ldquo;123456579\u0026rdquo;，我们可以将它分成斐波那契式的序列 [123, 456, 579]。返回拆分后的数组，若不能则返回空数组。\n💡 回溯 + 剪枝\n从第 3 个数开始，每个数都等于前 2 个数的和，因此从第 3 个数开始，需要判断拆分出的数是否等于前 2 个数的和，只有满足要求时才进行拆分，否则不进行拆分。\n回溯过程中，还有三处可以进行剪枝操作。\n拆分出的数如果不是 0，则不能以 0 开头，因此如果字符串剩下的部分以 0 开头，那么只能考虑 0. 拆分出的数必须符合 32 位有符号正整数类型，即每个数必须在[0,2^31-1] 的范围内。 如果列表中至少有 22 个数，并且拆分出的数已经大于最后 22 个数的和，就不需要继续尝试拆分了。 回溯需要带返回值，表示是否存在符合要求的斐波那契式序列。\nclass Solution: def splitIntoFibonacci(self, S: str) -\u0026gt; List[int]: ans = list() def backtrack(index: int): if index == len(S): return len(ans) \u0026gt;= 3 curr = 0 for i in range(index, len(S)): if i \u0026gt; index and S[index] == \u0026#34;0\u0026#34;: break curr = curr * 10 + ord(S[i]) - ord(\u0026#34;0\u0026#34;) if curr \u0026gt; 2**31 - 1: break if len(ans) \u0026lt; 2 or curr == ans[-2] + ans[-1]: ans.append(curr) if backtrack(i + 1): return True ans.pop() elif len(ans) \u0026gt; 2 and curr \u0026gt; ans[-2] + ans[-1]: break return False backtrack(0) return ans ","date":"2 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/842_%E5%B0%86%E6%95%B0%E7%BB%84%E6%8B%86%E5%88%86%E6%88%90%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E5%BA%8F%E5%88%97/","section":"leetcode 题解","summary":"","title":"842_将数组拆分成斐波那契序列","type":"leetcode题解"},{"content":"类型：数组\n下一个排列 💛 https://leetcode-cn.com/problems/next-permutation/\n❓ 实现获取「下一个排列」的函数，算法需要将给定数字序列重新排列成「字典序」中下一个更大的排列。如果不存在下一个更大的排列，则将数字重新排列成最小的排列（即升序排列）。必须 原地 修改，只允许使用额外常数空间。\n💡 两遍扫描\n我们希望找到一种方法，能够找到一个大于当前序列的新序列，且变大的幅度尽可能小。\n我们需要将一个左边的「较小数」与一个右边的「较大数」交换，以能够让当前排列变大，从而得到下一个排列。 同时我们要让这个「较小数」尽量靠右，而「较大数」尽可能小。当交换完成后，「较大数」右边的数需要按照升序重新排列。这样可以在保证新排列大于原来排列的情况下，使变大的幅度尽可能小。 以排列 [4,5,2,6,3,1] 为例：\n我们能找到的符合条件的一对「较小数」与「较大数」的组合为 2 与 3，满足「较小数」尽量靠右，而「较大数」尽可能小。 当我们完成交换后，排列变为 [4,5,3,6,2,1]，此时我们可以重排「较小数」右边的序列，序列变为 [4,5,3,1,2,6]. 算法描述如下：\n首先从后向前查找第一个满足 a[i] \u0026lt; a[i+1] 的数。a[i] 即为最靠右的「较小数」。 经过上面的查找，我们可以得知 [i + 1, n) 必然是一个下降序列。我们在 [i + 1, n) 中从后向前找到第一个满足 a[i] \u0026lt; a[j] 的元素。a[j] 即是尽可能小的「较大数」。 交换 a[i] 与 a[j]。此时区间 [i + 1, n) 必为降序，我们可以直接使用双指针法反转 [i + 1, n) 使其变为升序。 如果在步骤 1 中找不到符合要求的「较小数」，说明当前序列已经是最大序列，跳过步骤 2，直接执行步骤 3，即可得到最小的升序序列。\nclass Solution: def nextPermutation(self, nums: List[int]) -\u0026gt; None: i = len(nums) - 2 while i \u0026gt;= 0 and nums[i] \u0026gt;= nums[i + 1]: i -= 1 if i \u0026gt;= 0: j = len(nums) - 1 while j \u0026gt;= 0 and nums[i] \u0026gt;= nums[j]: j -= 1 nums[i], nums[j] = nums[j], nums[i] left, right = i + 1, len(nums) - 1 while left \u0026lt; right: nums[left], nums[right] = nums[right], nums[left] left += 1 right -= 1 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"1 March 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/31_%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%8E%92%E5%88%97/","section":"leetcode 题解","summary":"","title":"31_下一个排列","type":"leetcode题解"},{"content":" 题目 # 一个整数数组 original 可以转变成一个 双倍 数组 changed ，转变方式为将 original 中每个元素 值乘以 2 加入数组中，然后将所有元素 随机打乱 。\n给你一个数组 changed ，如果 change 是 双倍 数组，那么请你返回 original数组，否则请返回空数组。original 的元素可以以 任意 顺序返回。\n示例 1：\n输入：changed = [1,3,4,2,6,8] 输出：[1,3,4] 解释：一个可能的 original 数组为 [1,3,4] : - 将 1 乘以 2 ，得到 1 * 2 = 2 。 - 将 3 乘以 2 ，得到 3 * 2 = 6 。 - 将 4 乘以 2 ，得到 4 * 2 = 8 。 其他可能的原数组方案为 [4,3,1] 或者 [3,1,4] 。 示例 2：\n输入：changed = [6,3,0,1] 输出：[] 解释：changed 不是一个双倍数组。 示例 3：\n输入：changed = [1] 输出：[] 解释：changed 不是一个双倍数组。 解法 # 我们先将 changed 排序，然后从左往右遍历。合格的话应该满足如下情况。\n对于一个元素来说：\n如果它是 original 元素，那么应该至少有一个 double 后的元素在后面，我们将其标记。 如果它是 double 后的元素，那么它应该在我们之前访问到其 original 值时已经被标记过了 如果出现不在二者之列的元素，则该 changed 数组不合格，返回空。 代码 # class Solution: def findOriginalArray(self, changed: List[int]) -\u0026gt; List[int]: changed.sort() n = len(changed) if n % 2 != 0: return [] ans = [] hash = collections.defaultdict(list) for i in range(n): hash[changed[i]].append(i) used = [False] * n for i in range(n): if used[i]: continue num = changed[i] twice = num * 2 used[i] = True paired = False while hash[twice]: j = hash[twice].pop() if not used[j]: used[j] = True paired = True break if paired: ans.append(num) else: return [] return ans ","date":"28 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/2007_%E4%BB%8E%E5%8F%8C%E5%80%8D%E6%95%B0%E7%BB%84%E4%B8%AD%E8%BF%98%E5%8E%9F%E5%8E%9F%E6%95%B0%E7%BB%84/","section":"leetcode 题解","summary":"","title":"2007_从双倍数组中还原原数组","type":"leetcode题解"},{"content":"类型：数组\n买卖股票的最佳时机含手续费 💛 https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/\n❓ 数组 prices 存储了股票的每日价格，完成一次交易（买入+卖出）需要付手续费 fee，购买股票前先要卖出手里的股票。求最大收益。\n💡 动态规划\n每天交易结束后，有两种可能的状态：手中不持有股票，手中持有一支股票\ndp[i][0] 表示不持有股票的收益，dp[i][1] 表示持有股票的收益。\n转移方程：\ndp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee)\ndp[i][0] = max(dp[i - 1][0] - prices[i], dp[i - 1][1])\nclass Solution: def maxProfit(self, prices: List[int], fee: int) -\u0026gt; int: if not prices: return 0 dp = [[0, -prices[0]]] for i in range(1, len(prices)): dp.append([ max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee), max(dp[i - 1][0] - prices[i], dp[i - 1][1]) ]) return max(dp[-1]) 时间复杂度：O(n)，空间复杂度：O(1)\n💡 贪心\nclass Solution: def maxProfit(self, prices: List[int], fee: int) -\u0026gt; int: n = len(prices) buy = prices[0] + fee # 把手续费算在购买阶段 profit = 0 for i in range(1, n): if prices[i] + fee \u0026lt; buy: buy = prices[i] + fee elif prices[i] \u0026gt; buy: # 假设我们只拿收益，而没有把股票卖出 profit += prices[i] - buy buy = prices[i] return profit 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"27 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/714_%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA%E5%90%AB%E6%89%8B%E7%BB%AD%E8%B4%B9/","section":"leetcode 题解","summary":"","title":"714_买卖股票的最佳时机含手续费","type":"leetcode题解"},{"content":"类型：字符串\n最小区间 ❤️ ⭐ https://leetcode-cn.com/problems/smallest-range-covering-elements-from-k-lists/\n❓ 你有 k 个 非递减排列 的整数列表。找到一个 最小 区间，使得 k 个列表中的每个列表至少有一个数包含在其中。我们定义如果 b-a \u0026lt; d-c 或者在 b-a == d-c 时 a \u0026lt; c，则区间 [a,b] 比 [c,d] 小。\n💡 贪心 + 堆\n参见：力扣加加\n该问题可以转化为，从 k 个列表中各取一个数，使得这 k 个数中的最大值与最小值的差（diff）最小。\n思路是我们使用多指针进行遍历，将指向的元素存入小顶堆。通过堆顶获取最小值，通过一个变量来记录最大值，这样我们可以很快地计算出 diff。每次更新指针都会产生一个新的 diff，不断重复这个过程并维护全局最小 diff 即可。\n我们有这么多个指针，应该先移动哪一个呢？应该移动指向元素最小的指针（即堆顶），这样保证最小的元素被移出堆，才能保证 diff 的变化是渐进性的，才不会漏掉。\nclass Solution: def smallestRange(self, martrix: List[List[int]]) -\u0026gt; List[int]: l, r = -10**9, 10**9 # 将每一行最小的都放到堆中，同时记录其所在的行号和列号，一共 n 个齐头并进 h = [(row[0], i, 0) for i, row in enumerate(martrix)] heapq.heapify(h) # 维护最大值 max_v = max(row[0] for row in martrix) while True: min_v, row, col = heapq.heappop(h) # max_v - min_v 是当前的最大最小差值， r - l 为全局的最大最小差值。因为如果当前的更小，我们就更新全局结果 if max_v - min_v \u0026lt; r - l: l, r = min_v, max_v if col == len(martrix[row]) - 1: return [l, r] # 更新指针，继续往后移动一位 heapq.heappush(h, (martrix[row][col + 1], row, col + 1)) max_v = max(max_v, martrix[row][col + 1]) ","date":"26 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/632_%E6%9C%80%E5%B0%8F%E5%8C%BA%E9%97%B4/","section":"leetcode 题解","summary":"","title":"632_最小区间","type":"leetcode题解"},{"content":" 题目 # 给你 n 个项目，编号从 0 到 n - 1 。同时给你一个整数数组 milestones ，其中每个 milestones[i] 表示第 i 个项目中的阶段任务数量。\n你可以按下面两个规则参与项目中的工作：\n每周，你将会完成 某一个 项目中的 恰好一个 阶段任务。你每周都 必须 工作。 在 连续的 两周中，你 不能 参与并完成同一个项目中的两个阶段任务。 一旦所有项目中的全部阶段任务都完成，那么你将停止工作；如果选择任意剩余任务都会导致违反上述规则，那么你也会 停止工作。注意，由于这些条件的限制，你可能无法完成所有阶段任务。\n返回在不违反上面规则的情况下你 最多 能工作多少周。\n示例 1：\n输入：milestones = [1,2,3] 输出：6 解释：一种可能的情形是： - 第 1 周，你参与并完成项目 0 中的一个阶段任务。 - 第 2 周，你参与并完成项目 2 中的一个阶段任务。 - 第 3 周，你参与并完成项目 1 中的一个阶段任务。 - 第 4 周，你参与并完成项目 2 中的一个阶段任务。 - 第 5 周，你参与并完成项目 1 中的一个阶段任务。 - 第 6 周，你参与并完成项目 2 中的一个阶段任务。 总周数是 6 。 示例 2：\n输入：milestones = [5,2,1] 输出：7 解释：一种可能的情形是： - 第 1 周，你参与并完成项目 0 中的一个阶段任务。 - 第 2 周，你参与并完成项目 1 中的一个阶段任务。 - 第 3 周，你参与并完成项目 0 中的一个阶段任务。 - 第 4 周，你参与并完成项目 1 中的一个阶段任务。 - 第 5 周，你参与并完成项目 0 中的一个阶段任务。 - 第 6 周，你参与并完成项目 2 中的一个阶段任务。 - 第 7 周，你参与并完成项目 0 中的一个阶段任务。 总周数是 7 。 注意，你不能在第 8 周参与完成项目 0 中的最后一个阶段任务，因为这会违反规则。 因此，项目 0 中会有一个阶段任务维持未完成状态。 提示：\nn == milestones.length 1 \u0026lt;= n \u0026lt;= 105 1 \u0026lt;= milestones[i] \u0026lt;= 109 解法 # 问题的关键在于，只有里程碑数最多的项目才会影响最终结果。\n对于一个里程碑数为 n 的项目来说，只要其它项目的里程碑数总和 m \u0026gt; n-1, 那么这个项目就可以全部完成。因此只有里程碑最多的那个项目才有可能没法全部完成。\n我们先计算所有项目的总里程碑数，然后将最大的那个项目中无法完成的里程碑数减去，就是我们最终的答案。\n代码 # class Solution: def numberOfWeeks(self, milestones: List[int]) -\u0026gt; int: weeks = 0 maxMt = 0 for m in milestones: weeks += m maxMt = max(maxMt, m) limit = weeks - maxMt + 1 invalidWeeks = maxMt - limit if invalidWeeks \u0026gt; 0: weeks -= invalidWeeks return weeks ","date":"25 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1953_%E4%BD%A0%E5%8F%AF%E4%BB%A5%E5%B7%A5%E4%BD%9C%E7%9A%84%E6%9C%80%E5%A4%A7%E5%91%A8%E6%95%B0/","section":"leetcode 题解","summary":"","title":"1953_你可以工作的最大周数","type":"leetcode题解"},{"content":"类型：树\n所有可能的满二叉树 💛 ⭐ https://leetcode-cn.com/problems/all-possible-full-binary-trees/\n这里所说的满二叉树其实是指完全二叉树。\n❓ 请问 N 个结点，可以构成哪些完全二叉树。设定所有结点值都为 0.\n输入：7 输出：\n[ [0,0,0,null,null,0,0,null,null,0,0], [0,0,0,null,null,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,null,null,null,null,0,0], [0,0,0,0,0,null,null,0,0] ] 解释：\n💡 递归\n一个完全二叉树具有这样的性质，首先，其至少包含了三个结点，并且，它的左子树和右子树，也是完全二叉树。\n我们以 FBT(N) 来表示所有结点数为 N 的完全二叉树的集合，那么对于 N ≥ 3，我们可以设定如下的递归策略：\nFBT(N) = FBT(x) + FBT(N - 1 - x)，即 FBT(N) 依赖于 FBT(x) 和 FBT(N - 1 - x),自然的，x 和 N - 1 - x 都小于 N，因此我们可以由 FBT(1) 一直推算到 FBT(N)。就像是动态规划一样。\nclass Solution(object): memo = {0: [], 1: [TreeNode(0)]} def allPossibleFBT(self, N): if N not in Solution.memo: ans = [] for x in range(N): y = N - 1 - x for left in self.allPossibleFBT(x): for right in self.allPossibleFBT(y): bns = TreeNode(0) bns.left = left bns.right = right ans.append(bns) Solution.memo[N] = ans return Solution.memo[N] 时间复杂度：O(2^N)，空间复杂度：O(2^N)\n","date":"24 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/894_%E6%89%80%E6%9C%89%E5%8F%AF%E8%83%BD%E7%9A%84%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91/","section":"leetcode 题解","summary":"","title":"894_所有可能的满二叉树","type":"leetcode题解"},{"content":"类型：数组\n单词搜索 💛 https://leetcode-cn.com/problems/word-search/\n❓ 给定一个 m x n 的二维字符网格 board 和一个字符串单词 word，如果 word 存在于网格中，返回 true，否则返回 false。word 必须由 board 中的相邻格子（上下左右）按序构成。\n💡 DFS\n对每一个位置 (i, j) 都调用函数 check 进行检查，只有有一处返回 true，就说明网格中能够找到相应单词。为了防止重复遍历相同的位置，需要额外维护一个与 board 等大的 visited 数组，用于标识每个位置是否被访问过。\nclass Solution: def exist(self, board: List[List[str]], word: str) -\u0026gt; bool: directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] def check(i: int, j: int, k: int) -\u0026gt; bool: if board[i][j] != word[k]: return False if k == len(word) - 1: return True visited.add((i, j)) result = False for di, dj in directions: newi, newj = i + di, j + dj if 0 \u0026lt;= newi \u0026lt; len(board) and 0 \u0026lt;= newj \u0026lt; len(board[0]): if (newi, newj) not in visited: if check(newi, newj, k + 1): result = True break visited.remove((i, j)) return result h, w = len(board), len(board[0]) visited = set() for i in range(h): for j in range(w): if check(i, j, 0): return True return False ","date":"23 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/79_%E5%8D%95%E8%AF%8D%E6%90%9C%E7%B4%A2/","section":"leetcode 题解","summary":"","title":"79_单词搜索","type":"leetcode题解"},{"content":"类型：字符串\n复原 IP 地址 💛 https://leetcode-cn.com/problems/restore-ip-addresses/\n❓ 有一个表示 IP 地址的字符串，但是用于分割的小数点被抹掉了。我们向其中添加小数点，可以构成几种有效的 IP 地址？\n💡 回溯\nclass Solution: def restoreIpAddresses(self, s: str) -\u0026gt; List[str]: SEG_COUNT = 4 ans = list() segments = [0] * SEG_COUNT def dfs(segId: int, segStart: int): # 如果找到了 4 段 IP 地址并且遍历完了字符串，那么就是一种答案 if segId == SEG_COUNT: if segStart == len(s): ipAddr = \u0026#34;.\u0026#34;.join(str(seg) for seg in segments) ans.append(ipAddr) return # 如果还没有找到 4 段 IP 地址就已经遍历完了字符串，那么提前回溯 if segStart == len(s): return # 由于不能有前导零，如果当前数字为 0，那么这一段 IP 地址只能为 0 if s[segStart] == \u0026#34;0\u0026#34;: segments[segId] = 0 dfs(segId + 1, segStart + 1) # 一般情况，枚举每一种可能性并递归 addr = 0 for segEnd in range(segStart, len(s)): addr = addr * 10 + (ord(s[segEnd]) - ord(\u0026#34;0\u0026#34;)) if 0 \u0026lt; addr \u0026lt;= 0xFF: segments[segId] = addr dfs(segId + 1, segEnd + 1) else: break dfs(0, 0) return ans ","date":"22 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/93_%E5%A4%8D%E5%8E%9F_ip_%E5%9C%B0%E5%9D%80/","section":"leetcode 题解","summary":"","title":"93_复原_IP_地址","type":"leetcode题解"},{"content":"类型：链表\n环形链表 II 💛 https://leetcode-cn.com/problems/linked-list-cycle-ii/\n❓ 给定一个链表，返回链表开始入环的第一个节点。 如果链表无环，则返回 null。不允许修改给定的链表。\n💡 哈希\nclass Solution: def detectCycle(self, head: ListNode) -\u0026gt; ListNode: hashMap = {} while head: if head in hashMap: return head else: hashMap[head] = True head = head.next return None 时间复杂度：O(n)，空间复杂度：O(n)\n💡 快慢指针\n从相遇点到入环点的距离加上 n−1 圈的环长，恰好等于从链表头部到入环点的距离。因此，当发现 slow 与 fast 相遇时，我们再额外使用一个指针 ptr。起始，它指向链表头部；随后，它和 slow 每次向后移动一个位置。最终，它们会在入环点相遇。\nclass Solution { public: ListNode *detectCycle(ListNode *head) { ListNode *slow = head, *fast = head; while (fast != nullptr) { slow = slow-\u0026gt;next; if (fast-\u0026gt;next == nullptr) { return nullptr; } fast = fast-\u0026gt;next-\u0026gt;next; if (fast == slow) { ListNode *ptr = head; while (ptr != slow) { ptr = ptr-\u0026gt;next; slow = slow-\u0026gt;next; } return ptr; } } return nullptr; } }; 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"21 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/142_%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8_ii/","section":"leetcode 题解","summary":"","title":"142_环形链表_II","type":"leetcode题解"},{"content":"类型：树\n完全二叉树插入器 💛 https://leetcode-cn.com/problems/complete-binary-tree-inserter/\n❓ 实现一个完全二叉树类。支持以下操作：\n初始化\nCBTInserter(TreeNode root)\n使用头节点为 root 的树初始化该对象。\n结点插入\nCBTInserter.insert(int v)\n向完全二叉树对象中插入新结点，结点值为 v。返回新插入结点的父结点的值。\n返回头结点\nCBTInserter.get_root()\n返回头结点地址\n💡 队列\n我们可以用队列 queue 来存储完全二叉树，对于结点 n 来说，其左孩子是结点 2 * n + 1，其右孩子是结点 2 * n + 2。我们每次有新结点的时候就将其加入 queue 末尾。\nclass CBTInserter(object): def __init__(self, root): self.deque = collections.deque() self.root = root q = collections.deque([root]) while q: node = q.popleft() if not node.left or not node.right: self.deque.append(node) if node.left: q.append(node.left) if node.right: q.append(node.right) def insert(self, v): node = self.deque[0] self.deque.append(TreeNode(v)) if not node.left: node.left = self.deque[-1] else: node.right = self.deque[-1] self.deque.popleft() return node.val def get_root(self): return self.root ","date":"20 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/919_%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91%E6%8F%92%E5%85%A5%E5%99%A8/","section":"leetcode 题解","summary":"","title":"919_完全二叉树插入器","type":"leetcode题解"},{"content":" 题目 # 你有一台电脑，它可以 同时 运行无数个任务。给你一个二维整数数组 tasks ，其中 tasks[i] = [starti, endi, durationi] 表示第 i 个任务需要在 闭区间 时间段 [starti, endi] 内运行 durationi 个整数时间点（但不需要连续）。\n当电脑需要运行任务时，你可以打开电脑，如果空闲时，你可以将电脑关闭。\n请你返回完成所有任务的情况下，电脑最少需要运行多少秒。\n示例 1：\n输入：tasks = [[2,3,1],[4,5,1],[1,5,2]] 输出：2 解释： - 第一个任务在闭区间 [2, 2] 运行。 - 第二个任务在闭区间 [5, 5] 运行。 - 第三个任务在闭区间 [2, 2] 和 [5, 5] 运行。 电脑总共运行 2 个整数时间点。 示例 2：\n输入：tasks = [[1,3,2],[2,5,3],[5,6,2]] 输出：4 解释： - 第一个任务在闭区间 [2, 3] 运行 - 第二个任务在闭区间 [2, 3] 和 [5, 5] 运行。 - 第三个任务在闭区间 [5, 6] 运行。 电脑总共运行 4 个整数时间点。 提示：\n1 \u0026lt;= tasks.length \u0026lt;= 2000 tasks[i].length == 3 1 \u0026lt;= starti, endi \u0026lt;= 2000 1 \u0026lt;= durationi \u0026lt;= endi - starti + 1 解法 # 将 tasks 按照 end 时间升序排列之后，我们发现，对于 tasks[i] 来说，如果它在之前的时间已经运行完了，这是最好的，如果没运行完（还剩下 d 分钟需要运行），那么我们还需要在[start, end] 中选出 d 分钟的空闲时间来让电脑运行。\n我们希望这 d 分钟越靠后越好，因为这样可以尽可能地与后面的任务重叠，让更多后面的任务受益。\n代码 # class Solution: def findMinimumTime(self, tasks: List[List[int]]) -\u0026gt; int: tasks.sort(key = lambda x: x[1]) run = [False] * (tasks[-1][1] + 1) for [start, end, duration] in tasks: runTime = sum(run[start: end+1]) moreTime = duration - runTime for t in range(end, start-1, -1): if moreTime \u0026lt;= 0: break if not run[t]: moreTime -= 1 run[t] = True return sum(run) 复杂度 # 时间复杂度: O(n) 空间复杂度: O(n) ","date":"19 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/2589_%E5%AE%8C%E6%88%90%E6%89%80%E6%9C%89%E4%BB%BB%E5%8A%A1%E7%9A%84%E6%9C%80%E5%B0%91%E6%97%B6%E9%97%B4/","section":"leetcode 题解","summary":"","title":"2589_完成所有任务的最少时间","type":"leetcode题解"},{"content":" 题目 # 给你一个二维整数数组 ranges ，其中 ranges[i] = [starti, endi] 表示 starti 到 endi 之间（包括二者）的所有整数都包含在第 i 个区间中。\n你需要将 ranges 分成 两个 组（可以为空），满足：\n每个区间只属于一个组。 两个有 交集 的区间必须在 同一个 组内。 如果两个区间有至少 一个 公共整数，那么这两个区间是 有交集 的。\n比方说，区间 [1, 3] 和 [2, 5] 有交集，因为 2 和 3 在两个区间中都被包含。 请你返回将 ranges 划分成两个组的 总方案数 。由于答案可能很大，将它对 109 + 7 取余 后返回。\n示例 1：\n输入：ranges = [[6,10],[5,15]] 输出：2 解释： 两个区间有交集，所以它们必须在同一个组内。 所以有两种方案： - 将两个区间都放在第 1 个组中。 - 将两个区间都放在第 2 个组中。 示例 2：\n输入：ranges = [[1,3],[10,20],[2,5],[4,8]] 输出：4 解释： 区间 [1,3] 和 [2,5] 有交集，所以它们必须在同一个组中。 同理，区间 [2,5] 和 [4,8] 也有交集，所以它们也必须在同一个组中。 所以总共有 4 种分组方案： - 所有区间都在第 1 组。 - 所有区间都在第 2 组。 - 区间 [1,3] ，[2,5] 和 [4,8] 在第 1 个组中，[10,20] 在第 2 个组中。 - 区间 [1,3] ，[2,5] 和 [4,8] 在第 2 个组中，[10,20] 在第 1 个组中。 提示：\n1 \u0026lt;= ranges.length \u0026lt;= 105 ranges[i].length == 2 0 \u0026lt;= starti \u0026lt;= endi \u0026lt;= 109 解法 # 其实是一道简单的数理统计题。\n我们可以在排序之后遍历 ranges, 找到重叠的区间，由于重叠的区间只能分在同一个 group, 所以我们索性将它们合并为一个区间。完成所有的合并之后，我们就得到了一些互相独立的区间，它们可以互不影响的分配在任意一个 group 当中，设合并后的区间数为 rCount, 则最终答案为 2^rCount.\n代码 # class Solution: def countWays(self, ranges: List[List[int]]) -\u0026gt; int: ranges.sort() rCount = 0 preEnd = -1 for [start, end] in ranges: if start \u0026gt; preEnd: rCount += 1 preEnd = end else: preEnd = max(preEnd, end) return (2 ** rCount) % (10 ** 9 + 7) ","date":"17 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/2580_%E7%BB%9F%E8%AE%A1%E5%B0%86%E9%87%8D%E5%8F%A0%E5%8C%BA%E9%97%B4%E5%90%88%E5%B9%B6%E6%88%90%E7%BB%84%E7%9A%84%E6%96%B9%E6%A1%88%E6%95%B0/","section":"leetcode 题解","summary":"","title":"2580_统计将重叠区间合并成组的方案数","type":"leetcode题解"},{"content":"","date":"17 February 2021","externalUrl":null,"permalink":"/tags/%E6%95%B0%E5%AD%A6/","section":"Tags","summary":"","title":"数学","type":"tags"},{"content":"","date":"17 February 2021","externalUrl":null,"permalink":"/tags/%E7%BB%9F%E8%AE%A1/","section":"Tags","summary":"","title":"统计","type":"tags"},{"content":"类型：数组\n在排序数组中查找元素的第一个和最后一个位置 💛 https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/\n❓ 给定一个按照升序排列的整数数组 nums，和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target，返回 [-1, -1]。\n💡 二分查找\n使用二分查找：\n在数组中寻找第一个大于等于 target 的下标 在数组中寻找第一个大于 target 的下标，然后将下标减一 class Solution { public int[] searchRange(int[] nums, int target) { int leftIdx = binarySearch(nums, target, true); int rightIdx = binarySearch(nums, target, false) - 1; if (leftIdx \u0026lt;= rightIdx \u0026amp;\u0026amp; rightIdx \u0026lt; nums.length \u0026amp;\u0026amp; nums[leftIdx] == target \u0026amp;\u0026amp; nums[rightIdx] == target) { return new int[]{leftIdx, rightIdx}; } return new int[]{-1, -1}; } public int binarySearch(int[] nums, int target, boolean lower) { int left = 0, right = nums.length - 1, ans = nums.length; while (left \u0026lt;= right) { int mid = (left + right) / 2; if (nums[mid] \u0026gt; target || (lower \u0026amp;\u0026amp; nums[mid] \u0026gt;= target)) { right = mid - 1; ans = mid; } else { left = mid + 1; } } return ans; } } 时间复杂度：O(logN)，空间复杂度：O(1)\n","date":"16 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/34_%E5%9C%A8%E6%8E%92%E5%BA%8F%E6%95%B0%E7%BB%84%E4%B8%AD%E6%9F%A5%E6%89%BE%E5%85%83%E7%B4%A0%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E5%92%8C%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E4%BD%8D%E7%BD%AE/","section":"leetcode 题解","summary":"","title":"34_在排序数组中查找元素的第一个和最后一个位置","type":"leetcode题解"},{"content":"类型：堆\n可以到达的最远建筑 💛 ⭐ https://leetcode-cn.com/problems/furthest-building-you-can-reach/\n❓ 你有 bricks 块砖块和 ladders 根梯子。整数数组 heights 表示建筑物的高度，你从 0 号建筑开始往后爬。\n当下一个建筑不高于当前建筑时，你可以直接过去。 当下一个建筑比当前建筑高 x 个单位时，你可以通过消耗 x 个砖块，或一根梯子爬过去。 合理利用砖块和建筑，求你最远能达到的建筑的下标。\n💡 堆（事后诸葛亮）\n此题的关键在于如何运用好梯子，砖块是很灵活的，填在哪里都没什么区别，而梯子相当于无穷的方块，不过用一次少一根。我们当然是希望好梯用在刀刃上。\n此题其实和「871. 最低加油次数」是类似的，用梯子就好比是加油，掌握好了用梯子的时机，也就得到了最佳的结果。\n如同题 871 中尝试不加油一样，我们在此题中也尝试不使用梯子，当走到某个地方砖块不够了，我们再找出之前消耗砖块最多的地方，换成使用梯子。这样我们就能保证每次梯子的使用都是最关键的。\nclass Solution: def furthestBuilding(self, heights: List[int], bricks: int, ladders: int) -\u0026gt; int: h = [] for i in range(1, len(heights)): diff = heights[i] - heights[i - 1] if diff \u0026lt;= 0: continue if bricks \u0026lt; diff and ladders \u0026gt; 0: # 要用梯子了，可能是用在此处，也可能是用在之前的最高处 ladders -= 1 if h and -h[0] \u0026gt; diff: bricks -= heapq.heappop(h) else: continue bricks -= diff if bricks \u0026lt; 0: return i - 1 heapq.heappush(h, -1 * diff) return len(heights) - 1 ","date":"15 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1642_%E5%8F%AF%E4%BB%A5%E5%88%B0%E8%BE%BE%E7%9A%84%E6%9C%80%E8%BF%9C%E5%BB%BA%E7%AD%91/","section":"leetcode 题解","summary":"","title":"1642_可以到达的最远建筑","type":"leetcode题解"},{"content":"类型：堆\n单线程 CPU 💛 https://leetcode-cn.com/problems/single-threaded-cpu/\n❓ 数组 task 存储了编号从 0 到 n-1 的 n 项任务。tasks[i] = [enqueueTime, processingTime] 表明了该项任务的开始时间和任务耗时分别是 enqueueTime 和 processingTime。\n现有一个单线程 CPU，当 CPU 处于空闲，且有任务可执行时，CPU 会选择可执行任务中耗时最短的任务执行。\n💡 堆\n我们用一个小顶堆来维护当前可执行的任务，以任务耗时作为比较键。当 CPU 空闲时，我们从堆顶取出任务，执行该任务，并将该任务执行期间将会产生的新任务都加入堆。\nclass Solution: def getOrder(self, tasks: List[List[int]]) -\u0026gt; List[int]: n = len(tasks) for i in range(n): tasks[i].append(i) tasks[i][1], tasks[i][2] = tasks[i][2], tasks[i][1] tasks.sort() ans = [] heap = [] end = tasks[0][0] i = 0 while i \u0026lt; n and tasks[i][0] \u0026lt;= tasks[0][0]: heapq.heappush(heap, (tasks[i][2], tasks[i][1], tasks[i][0])) i += 1 while heap or i \u0026lt; n: if heap: processing, index, start = heapq.heappop(heap) ans.append(index) end += processing else: end = tasks[i][0] while i \u0026lt; n and tasks[i][0] \u0026lt;= end: heapq.heappush(heap, (tasks[i][2], tasks[i][1], tasks[i][0])) i += 1 return ans ","date":"14 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1834_%E5%8D%95%E7%BA%BF%E7%A8%8B_cpu/","section":"leetcode 题解","summary":"","title":"1834_单线程_CPU","type":"leetcode题解"},{"content":"类型：树\n翻转二叉树以匹配先序遍历 💛 ⭐ https://leetcode-cn.com/problems/flip-binary-tree-to-match-preorder-traversal/\n❓ 给你一棵含有 n 个结点的二叉树，每个结点的值不同，且处于 1 - n 的范围。在给你一个数组 voyage 表示想要得到的前序遍历结果。交换一个结点的左右子树称为对该结点进行的一次交换操作。请你用最少的交换次数使得二叉树的前序遍历结果与 voyage 一致。可以完成的话，返回执行交换操作的结点的列表，否则返回 [-1]。\n💡 DFS\n进行深度优先遍历，如果我们即将遍历的结点的值与下一个期望数字 voyage[i] 不相同时，我们就要翻转一下当前这个结点。然后继续往下走。如果往下走之后仍然发现当前结点的值不等于 voyage[i] 那么就只能返回 [-1] 了。\nclass Solution(object): def flipMatchVoyage(self, root, voyage): self.flipped = [] self.i = 0 def dfs(node): if node: if node.val != voyage[self.i]: self.flipped = [-1] return self.i += 1 if (self.i \u0026lt; len(voyage) and node.left and node.left.val != voyage[self.i]): self.flipped.append(node.val) dfs(node.right) dfs(node.left) else: dfs(node.left) dfs(node.right) dfs(root) if self.flipped and self.flipped[0] == -1: self.flipped = [-1] return self.flipped 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"13 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/971_%E7%BF%BB%E8%BD%AC%E4%BA%8C%E5%8F%89%E6%A0%91%E4%BB%A5%E5%8C%B9%E9%85%8D%E5%85%88%E5%BA%8F%E9%81%8D%E5%8E%86/","section":"leetcode 题解","summary":"","title":"971_翻转二叉树以匹配先序遍历","type":"leetcode题解"},{"content":"类型：数组\n丢失的数字 💚 https://leetcode-cn.com/problems/missing-number/\n❓ 给定一个包含 [0, n] 中 n 个数的数组 nums ，找出 [0, n] 这个范围内没有出现在数组中的那个数。\n💡 排序\n排序后，先判断 0 是不是出现在首位，n 是不是出现在末位。\n扫描这个数组，如果某一个数比它前面的那个数大了超过 1，那么这两个数之间的那个数即为缺失的数字。\nclass Solution: def missingNumber(self, nums): nums.sort() # 0 是否在首位 if nums[-1] != len(nums): return len(nums) # n 是否在末位 elif nums[0] != 0: return 0 # 找到相邻差值大于 1 的数 for i in range(1, len(nums)): expected_num = nums[i-1] + 1 if nums[i] != expected_num: return expected_num 时间复杂度：O(NlogN)\n💡 哈希集合\nclass Solution: def missingNumber(self, nums): num_set = set(nums) n = len(nums) + 1 for number in range(n): if number not in num_set: return number 时间复杂度：O(n)，空间复杂度：O(n)\n💡 位运算\na ⊕ a = 0，0 ⊕ a = a\nclass Solution: def missingNumber(self, nums): missing = len(nums) for i, num in enumerate(nums): missing ^= i ^ num return missing 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"12 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/268_%E4%B8%A2%E5%A4%B1%E7%9A%84%E6%95%B0%E5%AD%97/","section":"leetcode 题解","summary":"","title":"268_丢失的数字","type":"leetcode题解"},{"content":"类型：树\n二叉树的最小深度 💚 https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/\n❓ 求一棵树的最小深度。\n跟这一题是对着的：104. 二叉树的最大深度 💚\n💡 DFS\nclass Solution: def minDepth(self, root: TreeNode) -\u0026gt; int: if not root: return 0 if not root.left and not root.right: return 1 min_depth = 10**9 if root.left: min_depth = min(self.minDepth(root.left), min_depth) if root.right: min_depth = min(self.minDepth(root.right), min_depth) return min_depth + 1 时间复杂度：O(n)，空间复杂度：O(n)\n💡 BFS\n当我们按层序遍历到第一个叶子结点时，直接返回当前的深度。\nclass Solution: def minDepth(self, root: TreeNode) -\u0026gt; int: if not root: return 0 que = collections.deque([(root, 1)]) while que: node, depth = que.popleft() if not node.left and not node.right: return depth if node.left: que.append((node.left, depth + 1)) if node.right: que.append((node.right, depth + 1)) return 0 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"11 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/111_%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%B0%8F%E6%B7%B1%E5%BA%A6/","section":"leetcode 题解","summary":"","title":"111_二叉树的最小深度","type":"leetcode题解"},{"content":"类型：数组\n买卖股票的最佳时机 II 💚 ⭐ https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/\n❓ 数组 prices 存储了股票每天的价格。你可以进行多次交易，但是每次买入股票前必须先卖出手里的股票。求最大收益。\n💡 动态规划\n每天交易结束后，有两种可能的状态：手中不持有股票，手中持有一支股票\ndp[i][0] 表示不持有股票的收益，dp[i][1] 表示持有股票的收益。\ndp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])\ndp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1])\n由于在 dp 数组中，dp[i] 只与 dp[i - 1] 有关，因此只要保存前一天的数据即可。\nclass Solution { public int maxProfit(int[] prices) { int n = prices.length; int dp0 = 0, dp1 = -prices[0]; for (int i = 1; i \u0026lt; n; ++i) { int newDp0 = Math.max(dp0, dp1 + prices[i]); int newDp1 = Math.max(dp1, dp0 - prices[i]); dp0 = newDp0; dp1 = newDp1; } return dp0; } } 💡 贪心\n既然购买没有限制，也没有手续费，那我们把所有能盈利的区间全收益了。即把股票走势中所有的上坡段的收益全取了。\nclass Solution { public int maxProfit(int[] prices) { int ans = 0; int n = prices.length; for (int i = 1; i \u0026lt; n; ++i) { ans += Math.max(0, prices[i] - prices[i - 1]); } return ans; } } 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"10 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/122_%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA_ii/","section":"leetcode 题解","summary":"","title":"122_买卖股票的最佳时机_II","type":"leetcode题解"},{"content":"类型：树\n恢复二叉搜索树 ❤️ https://leetcode-cn.com/problems/recover-binary-search-tree/\n❓ 给你二叉搜索树的根节点 root ，该树中的两个节点被错误地交换。请在不改变其结构的情况下，恢复这棵树。\n💡 中序遍历\n正常搜索树中序遍历的结果是升序的。我们可以通过中序遍历，得到该树结点值的中序遍历数组 inorderArr，同时我们也在遍历过程中，直接把结点地址存入一个数组 inorderNodes，方便我们后续修改。\n我们找出 inorderArr 中不符合升序的元素的下标，直接通过 inorderNodes 访问对应结点，修改其值即可。\nclass Solution: def recoverTree(self, root: TreeNode) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; Do not return anything, modify root in-place instead. \u0026#34;\u0026#34;\u0026#34; inorderNodes = [] inorderArr = [] def inorderTraverse(node): if not node: return inorderTraverse(node.left) inorderNodes.append(node) inorderArr.append(node.val) inorderTraverse(node.right) inorderTraverse(root) sortedArr = inorderArr[:] sortedArr.sort() # 通过中序遍历的队列与有序队列比较，找出错位的结点 disNodes = [] for i in range(len(inorderArr)): if inorderArr[i] != sortedArr[i]: disNodes.append(inorderNodes[i]) disNodes[0].val, disNodes[1].val = disNodes[1].val, disNodes[0].val 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"9 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/99_%E6%81%A2%E5%A4%8D%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","section":"leetcode 题解","summary":"","title":"99_恢复二叉搜索树","type":"leetcode题解"},{"content":"类型：链表\n反转链表 💚 https://leetcode-cn.com/problems/reverse-linked-list/\n❓ 给你单链表的头节点 head ，请你反转链表，并返回反转后的链表。\n💡 迭代\n在遍历链表时，将当前节点的 next 指针改为指向前一个节点。由于节点没有引用其前一个节点，因此必须事先存储其前一个节点。在更改引用之前，还需要存储后一个节点。最后返回新的头引用。\nclass Solution: def reverseList(self, head: ListNode) -\u0026gt; ListNode: if not head: return None leftNode = head rightNode = head.next leftNode.next = None while rightNode: tempNode = rightNode.next rightNode.next = leftNode leftNode = rightNode rightNode = tempNode return leftNode 时间复杂度：O(n)，空间复杂度：O(1)\n💡 递归\nclass Solution: def rec(self, node): if node.next: tailNode = self.rec(node.next) node.next = None tailNode.next = node return node else: return node def reverseList(self, head: ListNode) -\u0026gt; ListNode: if not head: return None tail = head while tail.next: tail = tail.next self.rec(head) return tail 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"8 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/206_%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/","section":"leetcode 题解","summary":"","title":"206_反转链表","type":"leetcode题解"},{"content":"类型：字符串\n基本计算器 II 💛 ⭐ https://leetcode-cn.com/problems/basic-calculator-ii/\n❓ 给你一个字符串表达式 s ，请你实现一个基本计算器来计算并返回它的值。\n💡 栈\n乘除优先于加减，我们考虑先进行所有的乘除运算，并将这些数值放回原表达式，再进行加减。\n因此我们可以使用一个栈。加减号后面的数字，我们直接压入栈。乘除号后面的数字，我们取出栈顶元素与其运算后入栈。\n具体来说，遍历字符串 s，并用变量 preSign 记录每个数字之前的运算符（第一个数字之前的运算符视为加号）。每次遍历到数字末尾时，根据 preSign 来决定计算方式：\n加号：将数字压入栈； 减号：将数字的相反数压入栈； 乘除号：计算数字与栈顶元素，并将栈顶元素替换为计算结果。 最后将栈中的元素累加。\nclass Solution: def calculate(self, s: str) -\u0026gt; int: n = len(s) stack = [] preSign = \u0026#39;+\u0026#39; num = 0 for i in range(n): if s[i] != \u0026#39; \u0026#39; and s[i].isdigit(): num = num * 10 + ord(s[i]) - ord(\u0026#39;0\u0026#39;) if i == n - 1 or s[i] in \u0026#39;+-*/\u0026#39;: if preSign == \u0026#39;+\u0026#39;: stack.append(num) elif preSign == \u0026#39;-\u0026#39;: stack.append(-num) elif preSign == \u0026#39;*\u0026#39;: stack.append(stack.pop() * num) else: stack.append(int(stack.pop() / num)) preSign = s[i] num = 0 return sum(stack) 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"7 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/227_%E5%9F%BA%E6%9C%AC%E8%AE%A1%E7%AE%97%E5%99%A8_ii/","section":"leetcode 题解","summary":"","title":"227_基本计算器_II","type":"leetcode题解"},{"content":"类型：树\n二叉树中的最长交错路径 💛 https://leetcode-cn.com/problems/longest-zigzag-path-in-a-binary-tree/\n❓ 返回一棵树中最长的交错路径（Z形路径）长度。（不一定要从根结点出发。）\n💡 DFS\n我们在 DFS 的过程中，维护当前的方向 direction。我们可以用 -1 和 1 来表示接下来要向左或向右。\n如果我们接下来确实是要向左了，那么我们在递归遍历左子树的时候就可以使长度 + 1，否则以长度为 0 递归遍历左子树。 如果我们接下来确实是要向右了，那么我们在递归遍历右子树的时候就可以使长度 + 1，否则以长度为 0 递归遍历右子树。 刚开始的时候，我们使 direction 为 0，即 direction 即不等于左，也不等于右，这样就会分别发起向左和向右的遍历。\nclass Solution: def __init__(self): self.maxLength = 0 def dfs(self, node, direction, length): if length \u0026gt; self.maxLength: self.maxLength = length direction = direction * -1 if node.left: self.dfs(node.left, -1, length + 1 if direction == -1 else 1) if node.right: self.dfs(node.right, 1, length + 1 if direction == 1 else 1) def longestZigZag(self, root: TreeNode) -\u0026gt; int: if not root: return 0 self.dfs(root, 0, 0) return self.maxLength 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"6 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1372_%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E7%9A%84%E6%9C%80%E9%95%BF%E4%BA%A4%E9%94%99%E8%B7%AF%E5%BE%84/","section":"leetcode 题解","summary":"","title":"1372_二叉树中的最长交错路径","type":"leetcode题解"},{"content":"类型：链表\n移除链表元素 💚 https://leetcode-cn.com/problems/remove-linked-list-elements/\n❓ 给你一个链表的头节点 head 和一个整数 val ，请你删除链表中所有满足 Node.val == val 的节点。返回完成删除后的链表结点。\n💡 一次遍历\nclass Solution: def removeElements(self, head: ListNode, val: int) -\u0026gt; ListNode: while head and head.val == val: head = head.next node = head while node: if node.next and node.next.val == val: node.next = node.next.next else: node = node.next return head class Solution: def removeElements(self, head: ListNode, val: int) -\u0026gt; ListNode: while head and head.val == val: head = head.next slowNode = head while slowNode: if slowNode.next and slowNode.next.val == val: fastNode = slowNode.next while fastNode.next and fastNode.next.val == val: fastNode = fastNode.next slowNode.next = fastNode.next else: slowNode = slowNode.next return head 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"5 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/203_%E7%A7%BB%E9%99%A4%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0/","section":"leetcode 题解","summary":"","title":"203_移除链表元素","type":"leetcode题解"},{"content":"类型：字符串\n统计同构子字符串的数目 💛 https://leetcode-cn.com/problems/count-number-of-homogenous-substrings/\n❓ 给你一个字符串 s ，返回 s 中同构子字符串的数目。同构字符串的定义为：由一个字符（可重复）构成的字符串，如 a、aa、aaa 等。\n输入：s = \u0026ldquo;abbcccaa\u0026rdquo; 输出：13 解释：同构子字符串如下所列： \u0026ldquo;a\u0026rdquo; 出现 3 次。 \u0026ldquo;aa\u0026rdquo; 出现 1 次。 \u0026ldquo;b\u0026rdquo; 出现 2 次。 \u0026ldquo;bb\u0026rdquo; 出现 1 次。 \u0026ldquo;c\u0026rdquo; 出现 3 次。 \u0026ldquo;cc\u0026rdquo; 出现 2 次。 \u0026ldquo;ccc\u0026rdquo; 出现 1 次。 3 + 1 + 2 + 1 + 3 + 2 + 1 = 13\n💡 一次遍历计算三角数\nUntitled\n对于长度为 n 重复字符串来说，其同构数是 n 的三角数，即 1 + 2 + 3 + \u0026hellip; + n. 因此我们只需要对 s 进行一次遍历，计算重复字符字串（包括单个字符）的三角数，并累加就可以了。\nclass Solution: def countHomogenous(self, s: str) -\u0026gt; int: if not s: return 0 max_count = 10 ** 9 + 7 count = 0 repeat = 1 for i in range(1, len(s) + 1): if i \u0026lt; len(s) and s[i] == s[i - 1]: repeat += 1 else: count += ((1 + repeat) * repeat // 2) if count \u0026gt;= max_count: count %= max_count repeat = 1 return count 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"4 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1759_%E7%BB%9F%E8%AE%A1%E5%90%8C%E6%9E%84%E5%AD%90%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E6%95%B0%E7%9B%AE/","section":"leetcode 题解","summary":"","title":"1759_统计同构子字符串的数目","type":"leetcode题解"},{"content":"类型：堆\n有序矩阵中的第 k 个最小数组和 ❤️ https://leetcode-cn.com/problems/find-the-kth-smallest-sum-of-a-matrix-with-sorted-rows/\n参考：力扣加加\n❓ 给你一个 m * n 的矩阵 mat，以及一个整数 k ，矩阵中的每一行都以非递减的顺序排列。你可以从每一行中选出 1 个元素形成一个数组。返回所有可能数组中的第 k 个 最小 数组和。\n💡 堆\n这道题的含义其实就是让你从矩阵的每一行选取一个数，得到和第 k 小的。\n显然可以用堆来解决，我们要保证的是在弹出堆顶元素的时候，不存在比它小却尚未入堆的元素。\nclass Solution: def kthSmallest(self, mat: List[List[int]], k: int) -\u0026gt; int: cur = (sum([row[0] for row in mat]), tuple([0] * len(mat))) heap = [cur] seen = set(cur) for _ in range(k): # poses 是当前指针情况 theSum, poses = heapq.heappop(heap) # 尝试将每个指针向后移一位 for i, pos in enumerate(poses): if pos \u0026lt; len(mat[0]) - 1: posesArr = list(poses) posesArr[i] = pos + 1 posesTuple = tuple(posesArr) if posesTuple not in seen: seen.add(posesTuple) heapq.heappush(heap, (sum(mat[i][posesTuple[i]] for i in range(len(mat))), posesTuple)) return theSum ","date":"3 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1439_%E6%9C%89%E5%BA%8F%E7%9F%A9%E9%98%B5%E4%B8%AD%E7%9A%84%E7%AC%AC_k_%E4%B8%AA%E6%9C%80%E5%B0%8F%E6%95%B0%E7%BB%84%E5%92%8C/","section":"leetcode 题解","summary":"","title":"1439_有序矩阵中的第_k_个最小数组和","type":"leetcode题解"},{"content":" 题目 # 给你一个下标从 0 开始的整数数组 nums ，它包含 n 个 互不相同 的正整数。如果 nums 的一个排列满足以下条件，我们称它是一个特别的排列：\n对于 0 \u0026lt;= i \u0026lt; n - 1 的下标 i ，要么 nums[i] % nums[i+1] == 0 ，要么 nums[i+1] % nums[i] == 0 。 请你返回特别排列的总数目，由于答案可能很大，请将它对 ****109 + 7 取余 后返回。\n示例 1：\n输入：nums = [2,3,6] 输出：2 解释：[3,6,2] 和 [2,6,3] 是 nums 两个特别的排列。 示例 2：\n输入：nums = [1,4,3] 输出：2 解释：[3,1,4] 和 [4,1,3] 是 nums 两个特别的排列。 提示：\n2 \u0026lt;= nums.length \u0026lt;= 14 1 \u0026lt;= nums[i] \u0026lt;= 109 题解 # 我们用一个二进制的 mask 来表示尚未被选用的元素，若其下标对应的比特位为 1,则说明该元素尚未被选用。若 mask 为 0, 则表示所有元素均已使用。\n之后我们使用深度搜索, dfs(mask, lastIdx), 每次，我们找出尚未被使用且符合条件的元素，更新 mask 和 lastIdx 递归调用 dfs, 若 mask 为 0, 则我们已经通过使用所有元素得到了一个排列。\n代码 # class Solution: def specialPerm(self, nums: List[int]) -\u0026gt; int: @cache def dfs(unused, lastIdx): if unused == 0: return 1 count = 0 for idx, val in enumerate(nums): if (unused \u0026gt;\u0026gt; idx) \u0026amp; 1 and (val % nums[lastIdx] == 0 or nums[lastIdx] % val == 0): count += dfs(unused ^ (1 \u0026lt;\u0026lt; idx), idx) return count n = len(nums) MOD = 10 ** 9 + 7 unused = (1 \u0026lt;\u0026lt; n) - 1 return sum([ dfs(unused ^ (1 \u0026lt;\u0026lt; i), i) for i in range(n) ]) % MOD 复杂度 # 时间复杂度: O(n!) 空间复杂度: O(n!) ","date":"2 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/2741_%E7%89%B9%E5%88%AB%E7%9A%84%E6%8E%92%E5%88%97/","section":"leetcode 题解","summary":"","title":"2741_特别的排列","type":"leetcode题解"},{"content":"","date":"2 February 2021","externalUrl":null,"permalink":"/tags/bitmasking/","section":"Tags","summary":"","title":"Bitmasking","type":"tags"},{"content":" 题目 # 给你一个仅由小写英文字母组成的字符串 s 。在一步操作中，你可以完成以下行为：\n选择 s 的任一非空子字符串，可能是整个字符串，接着将字符串中的每一个字符替换为英文字母表中的前一个字符。例如，\u0026lsquo;b\u0026rsquo; 用 \u0026lsquo;a\u0026rsquo; 替换，\u0026lsquo;a\u0026rsquo; 用 \u0026lsquo;z\u0026rsquo; 替换。 返回执行上述操作 恰好一次 后可以获得的 字典序最小 的字符串。\n子字符串 是字符串中的一个连续字符序列。\n现有长度相同的两个字符串 x\n和字符串 y，在满足 x[i] != y[i] 的第一个位置 i 上，如果 x[i] 在字母表中先于 y[i] 出现，则认为字符串 x 比字符串 y 字典序更小。\n示例 1：\n输入：s = \u0026#34;cbabc\u0026#34; 输出：\u0026#34;baabc\u0026#34; 解释：我们选择从下标 0 开始、到下标 1 结束的子字符串执行操作。 可以证明最终得到的字符串是字典序最小的。 示例 2：\n输入：s = \u0026#34;acbbc\u0026#34; 输出：\u0026#34;abaab\u0026#34; 解释：我们选择从下标 1 开始、到下标 4 结束的子字符串执行操作。 可以证明最终得到的字符串是字典序最小的。 示例 3：\n输入：s = \u0026#34;leetcode\u0026#34; 输出：\u0026#34;kddsbncd\u0026#34; 解释：我们选择整个字符串执行操作。 可以证明最终得到的字符串是字典序最小的。 提示：\n1 \u0026lt;= s.length \u0026lt;= 3 * 105 s 仅由小写英文字母组成 解法 # 操作可以将字符变小，字母 a 除外。\n同时我们希望我们的操作越靠左越好。\n操作的起点是第一个非 a 的字符，再往后遇到的第一个 z 则是操作的终点。由于操作必须执行，若字符串为全 a, 则将最后一个 a 操作为 z.\n代码 # class Solution: def smallestString(self, s: str) -\u0026gt; str: n = len(s) replacement = \u0026#39;\u0026#39; startIdx = 0 while startIdx \u0026lt; n and s[startIdx] == \u0026#39;a\u0026#39;: startIdx += 1 for i in range(startIdx, n): if s[i] == \u0026#39;a\u0026#39;: break replacement += chr(ord(s[i]) - 1) if not replacement: return s[:-1] + \u0026#39;z\u0026#39; return s[:startIdx] + replacement + s[startIdx + len(replacement):] 复杂度 # 时间复杂度: O(n) 空间复杂度: O(n) ","date":"1 February 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/2734_%E6%89%A7%E8%A1%8C%E5%AD%90%E4%B8%B2%E6%93%8D%E4%BD%9C%E5%90%8E%E7%9A%84%E5%AD%97%E5%85%B8%E5%BA%8F%E6%9C%80%E5%B0%8F%E5%AD%97%E7%AC%A6%E4%B8%B2/","section":"leetcode 题解","summary":"","title":"2734_执行子串操作后的字典序最小字符串","type":"leetcode题解"},{"content":"类型：字符串\n解码方法 💛 ⭐ https://leetcode-cn.com/problems/decode-ways/\n❓ 一条包含字母 A-Z 的消息通过以下映射进行了 编码 ：\n\u0026lsquo;A\u0026rsquo; -\u0026gt; 1 \u0026lsquo;B\u0026rsquo; -\u0026gt; 2 \u0026hellip; \u0026lsquo;Z\u0026rsquo; -\u0026gt; 26 要 解码 已编码的消息，所有数字必须基于上述映射的方法，反向映射回字母（可能有多种方法）。例如，\u0026ldquo;11106\u0026rdquo; 可以映射为：\n\u0026ldquo;AAJF\u0026rdquo; ，将消息分组为 (1 1 10 6) \u0026ldquo;KJF\u0026rdquo; ，将消息分组为 (11 10 6) 注意，消息不能分组为 (1 11 06) ，因为 \u0026ldquo;06\u0026rdquo; 不能映射为 \u0026ldquo;F\u0026rdquo; ，这是由于 \u0026ldquo;6\u0026rdquo; 和 \u0026ldquo;06\u0026rdquo; 在映射中并不等价。\n给你一个只含数字的 非空 字符串 s ，请计算并返回 解码 方法的 总数 。\n题目数据保证答案肯定是一个 32 位 的整数。\n💡 动态规划\ndp[i] 表示字符串前 i 个字符的解码方法数。当以第 i 个字符为结尾的时候，存在一下两种情况：\ns[i] 单独编码，该情况的编码数为 dp[i - 1] s[i] 与前一个字符构成一个双字符编码，需要满足 10⋅s[i−1]+s[i]≤26，则该情况的编码数为 dp[i - 2] 将两种情况累加即是 dp[i]。初始状态，dp[0] = 1.\n由于 dp[i] 只与 dp[i - 1]、dp[i - 2] 有关，因此我们只需存储前两个 dp 值即可。\nclass Solution: def numDecodings(self, s: str) -\u0026gt; int: n = len(s) dp = [0, 1] # dp[i - 2], dp[i - 1] for i in range(1, n + 1): count = 0 if s[i - 1] != \u0026#39;0\u0026#39;: count += dp[1] if i \u0026gt; 1 and s[i - 2] != \u0026#39;0\u0026#39; and int(s[i-2:i]) \u0026lt;= 26: count += dp[0] dp = [dp[1], count] return dp[-1] 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"31 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/91_%E8%A7%A3%E7%A0%81%E6%96%B9%E6%B3%95/","section":"leetcode 题解","summary":"","title":"91_解码方法","type":"leetcode题解"},{"content":"类型：堆\n最低加油次数 ❤️ https://leetcode-cn.com/problems/minimum-number-of-refueling-stops/\n❓ 数组 station 存储了加油站信息，station[i] = [d, f] 表示在距离起点 d 公里处的地方有加油站，站内有 f 升油。汽车从起点出发，假设其油箱容量无限，最初装有 startFuel 升油，1 升油可走 1 公里。求问，为了到达目的地，汽车至少加多少次油？若无法到达目的地，则返回 - 1.\n💡 堆（事后诸葛亮）\n我们假设完全不加油，看能开到多远。如果能直接到达目的地，那直接就成功了，如果不能到达，我们把油耗光的这个点叫做失败点。要在失败点之前「至少」要加一次油才行，我们自然就选尽量大的加油站。经过加油后，我们又到达了下一个失败点，说明除了上个失败点之前加的几次油外，我们在新的失败点之前还得加油，同样我们还是尽量从大的加油站加。就这样一直省着加，挑最大的加，直到到达（或到不了）目的地。\n此处还有一个小窍门，我们把目的地也当作一个加油站，把它放在 stations 列表的末尾，这样可以简化逻辑。\nclass Solution: def minRefuelStops(self, target: int, startFuel: int, stations: List[List[int]]) -\u0026gt; int: stations += [(target, 0)] ans = 0 lastPos = 0 h = [] cur = startFuel for i, fuel in stations: # 如果到这里还有油，那就不想加油的事 cur -= i - lastPos while cur \u0026lt; 0 and h: cur -= heapq.heappop(h) ans += 1 if cur \u0026lt; 0: return -1 lastPos = i heapq.heappush(h, -1 * fuel) return ans ","date":"30 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/871_%E6%9C%80%E4%BD%8E%E5%8A%A0%E6%B2%B9%E6%AC%A1%E6%95%B0/","section":"leetcode 题解","summary":"","title":"871_最低加油次数","type":"leetcode题解"},{"content":"类型：数组\n搜索旋转排序数组 💛 ⭐ https://leetcode-cn.com/problems/search-in-rotated-sorted-array/\n❓ nums 本来是一个无重复元素的升序排列数组，其在某处进行了旋转，比如 [0,1,2,4,5,6,7] 在下标 3 处旋转后变为 [4,5,6,7,0,1,2]。给你旋转后的 nums，以及一个整数 target，如果 nums 中存在目标值 target，返回它的下标，否则返回 -1.\n💡 二分查找\n二分查找适用于有序数组。但是这个数组不是整体有序，而是局部有序。我们同样可以使用二分查找。\n将旋转后的数组分成两部分的话，其中一定有一部分是有序的。我们可以在分割出来的两个部分 [l, mid] 和 [mid + 1, r] 中查看哪个是有序的，并且可以直接判断 target 在不在那个部分，据此选择向哪个部分继续进行查找。\nclass Solution: def search(self, nums: List[int], target: int) -\u0026gt; int: if not nums: return -1 l, r = 0, len(nums) - 1 while l \u0026lt;= r: mid = (l + r) // 2 if nums[mid] == target: return mid if nums[0] \u0026lt;= nums[mid]: if nums[0] \u0026lt;= target \u0026lt; nums[mid]: r = mid - 1 else: l = mid + 1 else: if nums[mid] \u0026lt; target \u0026lt;= nums[len(nums) - 1]: l = mid + 1 else: r = mid - 1 return -1 时间复杂度：O(logN)，空间复杂度：O(1)\n","date":"29 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/33_%E6%90%9C%E7%B4%A2%E6%97%8B%E8%BD%AC%E6%8E%92%E5%BA%8F%E6%95%B0%E7%BB%84/","section":"leetcode 题解","summary":"","title":"33_搜索旋转排序数组","type":"leetcode题解"},{"content":"类型：数组\n最短单词距离 💚 https://leetcode-cn.com/problems/shortest-word-distance/\n❓ 给定一个单词列表和两个单词 word1 和 word2，返回列表中这两个单词之间的最短距离。\n输入：words = [\u0026ldquo;practice\u0026rdquo;, \u0026ldquo;makes\u0026rdquo;, \u0026ldquo;perfect\u0026rdquo;, \u0026ldquo;coding\u0026rdquo;, \u0026ldquo;makes\u0026rdquo;]\n输入：word1 = “coding”, word2 = “practice”\n输出：3\n💡 一次遍历\n如果使用暴力遍历的话，我们需要 O(n) 来遍历第一个单词，又需要 O(n) 来遍历第二个单词。时间复杂度为O(n^2).\n我们可以通过记录两个下标 i1 和 i2 来优化时间，二者分别保存 word1 和 word2 最近出现的位置。每次发现单词新的出现位置时，我们无需遍历数组去寻找另一个单词，因为我们已经记录了其之前出现的最近下标。只需要计算并更新记录即可。\nclass Solution: def shortestDistance(self, words: List[str], word1: str, word2: str) -\u0026gt; int: lastIndex1 = -1 lastIndex2 = -1 minDistance = len(words) for i in range(len(words)): if words[i] == word1: lastIndex1 = i if lastIndex2 \u0026gt;= 0 and i - lastIndex2 \u0026lt; minDistance: minDistance = i - lastIndex2 elif words[i] == word2: lastIndex2 = i if lastIndex1 \u0026gt;= 0 and i - lastIndex1 \u0026lt; minDistance: minDistance = i - lastIndex1 return minDistance 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"28 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/243_%E6%9C%80%E7%9F%AD%E5%8D%95%E8%AF%8D%E8%B7%9D%E7%A6%BB/","section":"leetcode 题解","summary":"","title":"243_最短单词距离","type":"leetcode题解"},{"content":"类型：链表\n合并两个有序链表 💚 https://leetcode-cn.com/problems/merge-two-sorted-lists/\n❓ 将两个升序链表合并为一个新的 升序 链表并返回。\n💡 递归\nclass Solution: def mergeTwoLists(self, l1, l2): if l1 is None: return l2 elif l2 is None: return l1 elif l1.val \u0026lt; l2.val: l1.next = self.mergeTwoLists(l1.next, l2) return l1 else: l2.next = self.mergeTwoLists(l1, l2.next) return l2 时间复杂度：O(m + n)，空间复杂度：O(m + n)\n💡 迭代\nclass Solution: def mergeTwoLists(self, l1, l2): prehead = ListNode(-1) prev = prehead while l1 and l2: if l1.val \u0026lt;= l2.val: prev.next = l1 l1 = l1.next else: prev.next = l2 l2 = l2.next prev = prev.next # 合并后 l1 和 l2 最多只有一个还未被合并完，我们直接将链表末尾指向未合并完的链表即可 prev.next = l1 if l1 is not None else l2 return prehead.next 时间复杂度：O(m + n)，空间复杂度：O(1)\n","date":"27 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/21_%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8/","section":"leetcode 题解","summary":"","title":"21_合并两个有序链表","type":"leetcode题解"},{"content":"类型：树\n二叉树的锯齿形层序遍历 💛 https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/\n❓ 给定一个二叉树，返回其节点值的锯齿形层序遍历。（第一层从左往右，第二层从右往左……）。\n💡 BFS\n我们用一个变量来记录当前是从左往右还是从右往左。我们将同层结点存入队列，由于方向是交替切换的，正序取反后得到反序，反序取反后又回到了正序。\nclass Solution: def zigzagLevelOrder(self, root: TreeNode) -\u0026gt; List[List[int]]: if not root: return [] direction = 1 ans = [] queue = [root] while queue: newQueue = [] row = [] for i in range(len(queue) - 1, -1, -1): node = queue[i] row.append(node.val) if direction == -1: if node.right: newQueue.append(node.right) if node.left: newQueue.append(node.left) else: if node.left: newQueue.append(node.left) if node.right: newQueue.append(node.right) direction *= -1 ans.append(row[:]) queue = newQueue return ans 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"26 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/103_%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E9%94%AF%E9%BD%BF%E5%BD%A2%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86/","section":"leetcode 题解","summary":"","title":"103_二叉树的锯齿形层序遍历","type":"leetcode题解"},{"content":"类型：数组\n和为K的子数组 💛 ⭐ https://leetcode-cn.com/problems/subarray-sum-equals-k/\n❓ 找到整数数组中和为 k 的连续子数组的个数。\n输入: nums = [1,1,1], k = 2 输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。\n💡 枚举\n考虑以 i 为结尾的连续子数组。我们需要统计符合条件的下标 j 的个数，其中 0 ≤ j ≤ i，且子数组 [j, i] 的和恰好为 k。子数组 [j, i] 的和可以通过子数组 [j + 1, i] 的和推算出。\n时间复杂度：O(n^2)，空间复杂度：O(1)\n💡 前缀和 + 哈希\n枚举方法的瓶颈在于，对于每一个 i，我们需要枚举所有的 j 来判断是否符合条件。我们可以对此进行优化。\n定义 pre[i] 表示子数组 [0, i] 之和。显然，pre[i] = pre[i - 1] + nums[i]。\n要求子数组 [j, i] 之和为 k，那么 pre[i] - pre[j - 1] = k，即 pre[j - 1] = pre[i] -k\n那么我们要求以 i 为结尾的和为 k 的连续子数组，只需要统计有多少个前缀和为 pre[i] - k 的 pre[j] 即可。对此我们建立哈希表，以和为键，以出现次数为值。从左往右边更新哈希表边计算答案。\nclass Solution: def subarraySum(self, nums: List[int], k: int) -\u0026gt; int: ans = 0 mp = {} pre_sum = 0 for j in range(len(nums)): if pre_sum in mp: mp[pre_sum] += 1 else: mp[pre_sum] = 1 pre_sum += nums[j] if pre_sum - k in mp: ans += mp[pre_sum - k] return ans 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"25 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/560_%E5%92%8C%E4%B8%BAk%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84/","section":"leetcode 题解","summary":"","title":"560_和为K的子数组","type":"leetcode题解"},{"content":"类型：字符串\n外观数列 💚 https://leetcode-cn.com/problems/count-and-say/\n❓ 「外观数列」是一个整数序列，从数字 1 开始，序列中的每一项都是对前一项的描述。\n比如，前五个外观\n1. 1 2. 11 3. 21 4. 1211 5. 111221 第一项是数字 1 描述前一项，这个数是 1 即 “ 一 个 1 ”，记作 \u0026#34;11\u0026#34; 描述前一项，这个数是 11 即 “ 二 个 1 ” ，记作 \u0026#34;21\u0026#34; 描述前一项，这个数是 21 即 “ 一 个 2 + 一 个 1 ” ，记作 \u0026#34;1211\u0026#34; 描述前一项，这个数是 1211 即 “ 一 个 1 + 一 个 2 + 二 个 1 ” ，记作 \u0026#34;111221\u0026#34; 💡 递归模拟\nclass Solution: def doCountAndSay(self, inStr: str) -\u0026gt; str: if not inStr: return \u0026#34;\u0026#34; outStr = \u0026#34;\u0026#34; lastChar = inStr[0] count = 1 for c in inStr[1:]: if c == lastChar: count += 1 else: outStr = outStr + str(count) + lastChar lastChar = c count = 1 outStr = outStr + str(count) + lastChar return outStr def countAndSay(self, n: int) -\u0026gt; str: if n == 1: return \u0026#34;1\u0026#34; else: return self.doCountAndSay(self.countAndSay(n-1)) ","date":"24 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/38_%E5%A4%96%E8%A7%82%E6%95%B0%E5%88%97/","section":"leetcode 题解","summary":"","title":"38_外观数列","type":"leetcode题解"},{"content":"类型：字符串\n罗马数字转整数 💚 https://leetcode-cn.com/problems/roman-to-integer/\n❓ 将罗马数字翻译为阿拉伯数字。\n💡 从右往左遍历\n我们从右往左遍历罗马字符串，如果发现大数左边跟了一个小数的话，其实就是大数减去哪个小数。比如 IV（4），IX（9）。\nclass Solution: def romanToInt(self, s: str) -\u0026gt; int: index = len(s) - 1 result = 0 lastValue = 0 while index \u0026gt;= 0: c = s[index] if c == \u0026#39;I\u0026#39;: value = 1 elif c == \u0026#39;V\u0026#39;: value = 5 elif c == \u0026#39;X\u0026#39;: value = 10 elif c == \u0026#39;L\u0026#39;: value = 50 elif c == \u0026#39;C\u0026#39;: value = 100 elif c == \u0026#39;D\u0026#39;: value = 500 elif c == \u0026#39;M\u0026#39;: value = 1000 if value \u0026lt; lastValue: lastValue = value value = -value else: lastValue = value result += value index -= 1 return result 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"23 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/13_%E7%BD%97%E9%A9%AC%E6%95%B0%E5%AD%97%E8%BD%AC%E6%95%B4%E6%95%B0/","section":"leetcode 题解","summary":"","title":"13_罗马数字转整数","type":"leetcode题解"},{"content":"类型：树\n二叉树的前序遍历 💛 https://leetcode-cn.com/problems/binary-tree-preorder-traversal/\n❓ 给你二叉树的根节点 root ，返回它节点值的 前序 遍历。\n💡 递归\nclass Solution: def preorderTraversal(self, root: TreeNode) -\u0026gt; List[int]: def preorder(root: TreeNode): if not root: return res.append(root.val) preorder(root.left) preorder(root.right) res = list() preorder(root) return res 时间复杂度：O(n)，空间复杂度：O(n)\n💡 迭代\nclass Solution: def preorderTraversal(self, root: TreeNode) -\u0026gt; List[int]: res = list() if not root: return res stack = [] node = root while stack or node: while node: res.append(node.val) stack.append(node) node = node.left node = stack.pop() node = node.right return res 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"22 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/144_%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86/","section":"leetcode 题解","summary":"","title":"144_二叉树的前序遍历","type":"leetcode题解"},{"content":"类型：树\n最接近的二叉搜索树值 💚 https://leetcode-cn.com/problems/closest-binary-search-tree-value/\n❓ 在二叉搜索树中找到最接近目标值 target 的数。\n💡 利用搜索树的性质直接在树中搜索\n根据搜索树的性质，如果目标小于当前结点，则向左搜索，如果目标大于当前结点，则向右搜索。\nclass Solution: def closestValue(self, root: TreeNode, target: float) -\u0026gt; int: closest = root.val while root: closest = min(root.val, closest, key = lambda x: abs(target - x)) root = root.left if target \u0026lt; root.val else root.right return closest 时间复杂度：O(h)，空间复杂度：O(1)\n💡 中序遍历\n我们对搜索树中序遍历，即可以得到升序序列，从中找出最接近 target 的元素。\nclass Solution: def closestValue(self, root: TreeNode, target: float) -\u0026gt; int: def inorder(r: TreeNode): return inorder(r.left) + [r.val] + inorder(r.right) if r else [] return min(inorder(root), key = lambda x: abs(target - x)) 时间复杂度：O(n)，空间复杂度：O(n)\n💡 中序遍历 + 空间优化\n我们不需要得到完整的中序遍历结果再去找元素，由于中序遍历是升序的，我们发现随着元素继续增大，离目标值越来越远（即 nums[i]\u0026lt;=target\u0026lt;nums[i+1]）的话，就不必再关注后面的元素了。\n","date":"21 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/270_%E6%9C%80%E6%8E%A5%E8%BF%91%E7%9A%84%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E5%80%BC/","section":"leetcode 题解","summary":"","title":"270_最接近的二叉搜索树值","type":"leetcode题解"},{"content":"类型：数组\n汇总区间 💚 https://leetcode-cn.com/problems/summary-ranges/\n❓ 对于无重复元素的有序整数 nums，描述 nums 中数据的范围。\n输入：nums = [0,1,2,4,5,7] 输出：[\u0026ldquo;0-\u0026gt;2\u0026rdquo;,\u0026ldquo;4-\u0026gt;5\u0026rdquo;,\u0026ldquo;7\u0026rdquo;]\n💡 一次遍历\n我们从数组头部出发，向右遍历。每次遇到相邻元素差值大于 1 时，我们就找到了一个新区间。维护下标 low 和 high 分别记录区间的起点终点。\n当 low \u0026lt; high 时，区间的字符串表示为 \u0026ldquo;low → high\u0026rdquo; 当 low = high 时，区间的字符串表示为 \u0026ldquo;low\u0026rdquo; class Solution { public List\u0026lt;String\u0026gt; summaryRanges(int[] nums) { List\u0026lt;String\u0026gt; ret = new ArrayList\u0026lt;String\u0026gt;(); int i = 0; int n = nums.length; while (i \u0026lt; n) { int low = i; i++; while (i \u0026lt; n \u0026amp;\u0026amp; nums[i] == nums[i - 1] + 1) { i++; } int high = i - 1; StringBuffer temp = new StringBuffer(Integer.toString(nums[low])); if (low \u0026lt; high) { temp.append(\u0026#34;-\u0026gt;\u0026#34;); temp.append(Integer.toString(nums[high])); } ret.add(temp.toString()); } return ret; } } 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"20 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/228_%E6%B1%87%E6%80%BB%E5%8C%BA%E9%97%B4/","section":"leetcode 题解","summary":"","title":"228_汇总区间","type":"leetcode题解"},{"content":"类型：数组\n插入区间 💛 ⭐ https://leetcode-cn.com/problems/insert-interval/\n❓ 给你一个 无重叠的 *，*按照区间起始端点排序的区间列表。在列表中插入一个新的区间，你需要确保列表中的区间仍然有序且不重叠（如果有必要的话，可以合并区间）。\n输入：intervals = 1,3, newInterval = [2,5] 输出：1,5\n💡 模拟\nclass Solution: def insert(self, intervals: List[List[int]], newInterval: List[int]) -\u0026gt; List[List[int]]: left, right = newInterval placed = False ans = list() for li, ri in intervals: if li \u0026gt; right: # 在插入区间的右侧且无交集 if not placed: ans.append([left, right]) placed = True ans.append([li, ri]) elif ri \u0026lt; left: # 在插入区间的左侧且无交集 ans.append([li, ri]) else: # 与插入区间有交集，计算它们的并集 left = min(left, li) right = max(right, ri) if not placed: ans.append([left, right]) return ans 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"19 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/57_%E6%8F%92%E5%85%A5%E5%8C%BA%E9%97%B4/","section":"leetcode 题解","summary":"","title":"57_插入区间","type":"leetcode题解"},{"content":"类型：树\n从根到叶的二进制数之和 💚 https://leetcode-cn.com/problems/sum-of-root-to-leaf-binary-numbers/\n❓ 给出一棵二叉树，其上每个结点的值都是 0 或 1 。每一条从根到叶的路径都代表一个从最高有效位开始的二进制数。例如，如果路径为 0 -\u0026gt; 1 -\u0026gt; 1 -\u0026gt; 0 -\u0026gt; 1，那么它表示二进制数 01101，也就是 13 。\n对树上的每一片叶子，我们都要找出从根到该叶子的路径所表示的数字。\n返回这些数字之和。\n输入：root = [1,0,1,0,1,0,1] 输出：22 解释：(100) + (101) + (110) + (111) = 4 + 5 + 6 + 7 = 22\n💡 递归\n我们从根结点遍历到叶子结点，这条路径的计算过程其实就是，将已走过的路径左移一位，然后加上当前的值。\n我们可以用先序遍历来完成这个过程。\nclass Solution: def sumRootToLeaf(self, root: TreeNode) -\u0026gt; int: def dfs(node, theSum): if not node: return 0 theSum = (theSum \u0026lt;\u0026lt; 1) + node.val if not node.left and not node.right: return theSum else: return dfs(node.left, theSum) + dfs(node.right, theSum) return dfs(root, 0) 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"18 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1022_%E4%BB%8E%E6%A0%B9%E5%88%B0%E5%8F%B6%E7%9A%84%E4%BA%8C%E8%BF%9B%E5%88%B6%E6%95%B0%E4%B9%8B%E5%92%8C/","section":"leetcode 题解","summary":"","title":"1022_从根到叶的二进制数之和","type":"leetcode题解"},{"content":"类型：树\n最大二叉树 💛 https://leetcode-cn.com/problems/maximum-binary-tree/\n❓ 给定一个不含重复元素的整数数组 nums 。一个以此数组构建的最大二叉树定义如下：\n二叉树的根是数组 nums 中的最大元素。 左子树是通过数组中 最大值左边部分 递归构造出的最大二叉树。 右子树是通过数组中 最大值右边部分 递归构造出的最大二叉树。 返回有给定数组 nums 构建的 最大二叉树 。\n💡 递归\nclass Solution: def geneNode(self, node, arr): val = max(arr) index = arr.index(val) node.val = val leftArr = arr[:index] rightArr = arr[index + 1:] if not leftArr: node.left = None else: node.left = TreeNode() self.geneNode(node.left, leftArr) if not rightArr: node.right = None else: node.right = TreeNode() self.geneNode(node.right, rightArr) def constructMaximumBinaryTree(self, nums: List[int]) -\u0026gt; TreeNode: if not nums: return None head = TreeNode() self.geneNode(head, nums) return head 时间复杂度：O(n^2)，空间复杂度：O(n)\n","date":"17 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/654_%E6%9C%80%E5%A4%A7%E4%BA%8C%E5%8F%89%E6%A0%91/","section":"leetcode 题解","summary":"","title":"654_最大二叉树","type":"leetcode题解"},{"content":"类型：数组\n数组中重复的数字 💚\nhttps://leetcode-cn.com/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/\n❓ 长度为 n 的数组，元素范围在 0~n-1，找出其中任意一个重复的数字。\n💡 集合 + 一次遍历\nclass Solution: def findRepeatNumber(self, nums: List[int]) -\u0026gt; int: se = set() for num in nums: if num in se: return num else: se.add(num) 时间复杂度：O(N)，空间复杂度：O(N)\n💡 原地修改 - 代替哈希\n此方法会修改数组。\n当我们遍历到第 i 个元素的时候，它的值为 k = nums[i]，我们以 k 为下标，将 nums[k] -= n，由于数组元素值在 0~n-1 的范围内，那么 k 必然是合法的下标，且 nums[k] - n 必然小于 0. 因此当我们在某次访问到 nums[k] 小于 0 的时候，则说明它已经被修改过了，即值为 k 的元素已经出现过了。但是我们将 nums[k] -= n 破环了原数组，会不会造成信息损失呢，不必担心，我们在访问的时候再加上 n 即可。\nclass Solution: def findRepeatNumber(self, nums: List[int]) -\u0026gt; int: n = len(nums) for i in range(n): k = nums[i] if k \u0026lt; 0: k += n if nums[k] \u0026lt; 0: return k nums[k] -= n return -1 ","date":"16 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/%E5%89%91%E6%8C%87_offer_03_%E6%95%B0%E7%BB%84%E4%B8%AD%E9%87%8D%E5%A4%8D%E7%9A%84%E6%95%B0%E5%AD%97/","section":"leetcode 题解","summary":"","title":"03_数组中重复的数字","type":"leetcode题解"},{"content":"类型：堆\n最大平均通过率 💛 https://leetcode-cn.com/problems/maximum-average-pass-ratio/\n❓ 有一个表示班级情况的数组 classes，classes[i] = [pass, total] 表示 i 号班级总共有 total 名学生，其中只有 pass 名能够通过期末考试。现在额外有 extraStudents 名好学生（一定能通过考试），请将这些学生安排到班级中，使所有班级的通过率之和最大。 💡 堆\n对于一个班级 [pass, total] 来说，插入一名好学生后，通过率的提升程度为 (pass+1)/(total+1) - pass/total，我们要使好学生插入后，所有班级的通过率之和最大，自然就要把好学生往提升最显著的班级插。\nclass Solution: def maxAverageRatio(self, classes: List[List[int]], extraStudents: int) -\u0026gt; float: improvingClasses = [(pa / total - (pa + 1)/(total + 1), pa, total) for [pa, total] in classes] heapq.heapify(improvingClasses) while extraStudents: (_, pa, total) = heapq.heappop(improvingClasses) pa += 1 total += 1 heapq.heappush(improvingClasses, (pa / total - (pa + 1)/(total + 1), pa, total)) extraStudents -= 1 percentSum = sum(pa / total for (_, pa, total) in improvingClasses) return float(format(percentSum / len(classes), \u0026#39;.5f\u0026#39;)) ","date":"15 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1792_%E6%9C%80%E5%A4%A7%E5%B9%B3%E5%9D%87%E9%80%9A%E8%BF%87%E7%8E%87/","section":"leetcode 题解","summary":"","title":"1792_最大平均通过率","type":"leetcode题解"},{"content":"类型：数组\n移除元素 💚 https://leetcode-cn.com/problems/remove-element/\n❓ 移除数组 nums 中所有值为 val 的元素，返回移除后的数组的长度。\n💡 同向双指针\nleft、right 指针均从数组首向尾部移动。\n如果 right 指针指向的元素不等于 val，则它应该在输出数组里，将 right 指向的元素赋值给 left，然后将 left 和 right 同时右移。 如果 right 指向的元素等于 val，则它不能在输出数组里，left 指针不动，right 右移一位。 整个过程中，[0, left) 中的元素都不等于 val.\nclass Solution { public int removeElement(int[] nums, int val) { int n = nums.length; int left = 0; for (int right = 0; right \u0026lt; n; right++) { if (nums[right] != val) { nums[left] = nums[right]; left++; } } return left; } } 时间复杂度：O(n)，数组至多遍历两次。空间复杂度：O(1)\n💡 逆向双指针\n（此方法会改变数组，并且会改变元素间顺序。）\nleft、right 指针从两端向中间移动。\n如果 left 指向的元素等于 val，则将 right 指向的元素赋值给 left 指向的元素。同时 right 左移一位。 如果赋值过来的元素恰好也等于 val，则可以重复上一个操作。直到 left 指向的元素不等于 val 了，则 left 右移一步。 left 与 right 重合时，遍历结束。[0, left) 中的元素都不等于 val. 时间复杂度：O(n)，数组只遍历一次。空间复杂度：O(1)\n","date":"14 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/27_%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0/","section":"leetcode 题解","summary":"","title":"27_移除元素","type":"leetcode题解"},{"content":"类型：字符串\n仅执行一次字符串交换能否使两个字符串相等 💚 https://leetcode-cn.com/problems/check-if-one-string-swap-can-make-strings-equal/\n❓ 给你两个长度相等的字符串，如果对 其中一个字符串执行最多一次字符交换（交换字符串中两个字符的位置）就可以使两个字符串相等，返回 true ；否则，返回 false 。\n💡 一次遍历\n使用两个指针，同步遍历这两个字符串。如果符合要求的话，应该会正好找到两处错位。\nclass Solution: def areAlmostEqual(self, s1: str, s2: str) -\u0026gt; bool: firstDiff = [] secondDiff = [] for i in range(len(s1)): diff = ord(s1[i]) - ord(s2[i]) if diff == 0: continue if not firstDiff: firstDiff = [s1[i], s2[i]] elif not secondDiff: secondDiff = [s1[i], s2[i]] else: return False if firstDiff and not secondDiff: return False if not firstDiff and not secondDiff: return True return firstDiff[0] == secondDiff[1] and firstDiff[1] == secondDiff[0] 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"13 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1790_%E4%BB%85%E6%89%A7%E8%A1%8C%E4%B8%80%E6%AC%A1%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%BA%A4%E6%8D%A2%E8%83%BD%E5%90%A6%E4%BD%BF%E4%B8%A4%E4%B8%AA%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9B%B8%E7%AD%89/","section":"leetcode 题解","summary":"","title":"1790_仅执行一次字符串交换能否使两个字符串相等","type":"leetcode题解"},{"content":"类型：树\n二叉树的所有路径 💚 https://leetcode-cn.com/problems/binary-tree-paths/\n❓ 给定一个二叉树，返回所有从根节点到叶子节点的路径。\n输入: 1 / \\ 2 3 5 输出: [\u0026quot;1-\u0026gt;2-\u0026gt;5\u0026quot;, \u0026quot;1-\u0026gt;3\u0026quot;] ``` 💡 DFS ```python # 官方实现 class Solution: def binaryTreePaths(self, root): \u0026quot;\u0026quot;\u0026quot; :type root: TreeNode :rtype: List[str] \u0026quot;\u0026quot;\u0026quot; def construct_paths(root, path): if root: path += str(root.val) if not root.left and not root.right: # 当前节点是叶子节点 paths.append(path) # 把路径加入到答案中 else: path += '-\u0026gt;' # 当前节点不是叶子节点，继续递归遍历 construct_paths(root.left, path) construct_paths(root.right, path) paths = [] construct_paths(root, '') return paths ``` 时间复杂度：O(n^2)，注意代码中 path 是一个字符串，而 python 中字符串变量的赋值不是引用传递，而是构造拷贝。拷贝的时间复杂度是 O(n)，每个结点遍历到一次，因此总时间复杂度为 O(n^2)。 空间复杂度：O(n^2) ","date":"12 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/257_%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%89%80%E6%9C%89%E8%B7%AF%E5%BE%84/","section":"leetcode 题解","summary":"","title":"257_二叉树的所有路径","type":"leetcode题解"},{"content":" 题目 # 给你一个整数数组 nums 。每一次操作中，你可以将 nums 中 任意 一个元素替换成 任意 整数。\n如果 nums 满足以下条件，那么它是 连续的 ：\nnums 中所有元素都是 互不相同 的。 nums 中 最大 元素与 最小 元素的差等于 nums.length - 1 。 比方说，nums = [4, 2, 5, 3] 是 连续的 ，但是 nums = [1, 2, 3, 5, 6] 不是连续的 。\n请你返回使 nums 连续 的 最少 操作次数。\n示例 1：\n输入：nums = [4,2,5,3] 输出：0 解释：nums 已经是连续的了。 示例 2：\n输入：nums = [1,2,3,5,6] 输出：1 解释：一个可能的解是将最后一个元素变为 4 。 结果数组为 [1,2,3,5,4] ，是连续数组。 示例 3：\n输入：nums = [1,10,100,1000] 输出：3 解释：一个可能的解是： - 将第二个元素变为 2 。 - 将第三个元素变为 3 。 - 将第四个元素变为 4 。 结果数组为 [1,2,3,4] ，是连续数组。 提示：\n1 \u0026lt;= nums.length \u0026lt;= 105 1 \u0026lt;= nums[i] \u0026lt;= 109 解法 # 先对 nums 进行排序并去重。\n使用滑动窗口, nums[i] 为窗口起点, nums[j] = nums[i] + n - 1 为窗口末尾，\n则窗口内一共有 k = j - i + i 个非重复元素，还需要对另外 n - k 个元素进行操作。\n代码 # class Solution: def minOperations(self, nums: List[int]) -\u0026gt; int: n = len(nums) nums = sorted(set(nums)) m = len(nums) ans = n - 1 j = 0 for i in range(m): while j \u0026lt; m and nums[j] \u0026lt;= nums[i] + n - 1: j += 1 ans = min(ans, n - (j - i)) return ans ","date":"11 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/2009_%E4%BD%BF%E6%95%B0%E7%BB%84%E8%BF%9E%E7%BB%AD%E7%9A%84%E6%9C%80%E5%B0%91%E6%93%8D%E4%BD%9C%E6%95%B0/","section":"leetcode 题解","summary":"","title":"2009_使数组连续的最少操作数","type":"leetcode题解"},{"content":"","date":"11 January 2021","externalUrl":null,"permalink":"/tags/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/","section":"Tags","summary":"","title":"滑动窗口","type":"tags"},{"content":"类型：树\n不同的二叉搜索树 💛 https://leetcode-cn.com/problems/unique-binary-search-trees/\n❓ 求恰由 n 个节点组成且节点值从 1 到 n 互不相同的二叉搜索树有多少种？返回满足题意的二叉搜索树的种数。\n💡 动态规划\n假设我们要计算 numTrees(5) 那么，\nnumTrees(5) = numTress(0) * numTress(4) + numTress(1) * numTress(3) + numTress(2) * numTress(2) + numTrees(3) * numTrees(1) + numTrees(4) + numTrees(0) 而边界条件，\nnumTress(0) = 1 numTress(1) = 1 numTress(2) = 2 numTress(3) = numTress(0) * numTress(2) + numTress(1) * numTress(1) + numTrees(2) * numTress(0) ... 代码如下：\nclass Solution: def numTrees(self, n: int) -\u0026gt; int: counts = [1, 1, 2] if n \u0026lt;= 2: return counts[n] for k in range(3, n + 1): count = 0 for i in range(k): count += counts[i] * counts[k - i - 1] counts.append(count) return counts[-1] 时间复杂度：O(n^2)，空间复杂度：O(n)\n","date":"10 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/96_%E4%B8%8D%E5%90%8C%E7%9A%84%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","section":"leetcode 题解","summary":"","title":"96_不同的二叉搜索树","type":"leetcode题解"},{"content":"类型：数组\n顺时针打印矩阵 💚\nhttps://leetcode-cn.com/problems/shun-shi-zhen-da-yin-ju-zhen-lcof/\n❓ 输入一个矩阵，按照从外向里以顺时针的顺序依次打印出每一个数字。\n💡 模拟\nclass Solution: def spiralOrder(self, matrix: List[List[int]]) -\u0026gt; List[int]: if not matrix or type(matrix[0]) == int: return matrix ans = [] def travel(x, y, u, v): if x \u0026gt; u or y \u0026gt; v: return for j in range(y, v + 1): ans.append(matrix[x][j]) for i in range(x + 1, u + 1): ans.append(matrix[i][v]) if u \u0026gt; x: for j in range(v - 1, y - 1, -1): ans.append(matrix[u][j]) if v \u0026gt; y: for i in range(u - 1, x, -1): ans.append(matrix[i][y]) travel(x + 1, y + 1, u - 1, v - 1) travel(0, 0, len(matrix) - 1, len(matrix[0]) - 1) return ans 时间复杂度：O(mn)，空间复杂度：O(1)\n","date":"9 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/%E5%89%91%E6%8C%87_offer_29_%E9%A1%BA%E6%97%B6%E9%92%88%E6%89%93%E5%8D%B0%E7%9F%A9%E9%98%B5/","section":"leetcode 题解","summary":"","title":"29_顺时针打印矩阵","type":"leetcode题解"},{"content":"类型：树\n二叉搜索树的第k大节点 💚\nhttps://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/\n❓ 给定一棵二叉搜索树，请找出其中第k大的节点。\n💡 中序遍历\n中序遍历后得到有序的数组，返回第倒数第 k 个元素即可。\n时间复杂度：O(n)，空间复杂度：O(n)\n💡 反向中序遍历\n正常中序遍历的顺序是 左子树-根结点-右子树，我们可以把它反过来，右子树-根结点-左子树，这样得到的就是降序排列。我们可以提前返回。\n时间复杂度：O(n)，当极端情况下树变成一棵只有左子树的链表时，我们需要访问所有元素。空间复杂度：O(n)\n💡 递归\n和反向中序遍历很像，只不过我们不保存遍历数组，只记录当前是反向中序遍历中的第几个元素。\nclass Solution: def __init__(self): self.k = 0 self.ans = None def kthLargest(self, root: TreeNode, k: int) -\u0026gt; int: if not root: return None if root.right: self.kthLargest(root.right, k) self.k += 1 if self.k == k: self.ans = root.val return self.ans if root.left: self.kthLargest(root.left, k) return self.ans 时间复杂度：O(n)，当极端情况下树变成一棵只有左子树的链表时，我们需要访问所有元素。空间复杂度：O(n)\n","date":"8 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/%E5%89%91%E6%8C%87_offer_54_%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E7%9A%84%E7%AC%ACk%E5%A4%A7%E8%8A%82%E7%82%B9/","section":"leetcode 题解","summary":"","title":"54_二叉搜索树的第k大节点","type":"leetcode题解"},{"content":"类型：数组\n合并两个有序数组 💚 https://leetcode-cn.com/problems/merge-sorted-array/\n❓ 将有序数组 nums2 合并到有序数组 nums1 中。\n输入：nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 输出：[1,2,2,3,5,6]\n💡 逆向双指针\n从后向前遍历，每次取两者之中的较大者放进 nums1 的最后面。\nclass Solution: def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; Do not return anything, modify nums1 in-place instead. \u0026#34;\u0026#34;\u0026#34; p1, p2 = m - 1, n - 1 tail = m + n - 1 while p1 \u0026gt;= 0 or p2 \u0026gt;= 0: if p1 == -1: nums1[tail] = nums2[p2] p2 -= 1 elif p2 == -1: nums1[tail] = nums1[p1] p1 -= 1 elif nums1[p1] \u0026gt; nums2[p2]: nums1[tail] = nums1[p1] p1 -= 1 else: nums1[tail] = nums2[p2] p2 -= 1 tail -= 1 时间复杂度：O(m + n)，空间复杂度：O(1)\n","date":"7 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/88_%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84/","section":"leetcode 题解","summary":"","title":"88_合并两个有序数组","type":"leetcode题解"},{"content":"类型：字符串\n去除重复字母 💛 ⭐ https://leetcode-cn.com/problems/remove-duplicate-letters/\n❓ 请去除字符串 s 中的重复字母，使每个字母只出现一次。需保证返回结果的字典序最小（要求不能打乱其他字符的相对位置）。同：1081. 不同字符的最小子序列\n输入：s = \u0026ldquo;bcabc\u0026quot;输出：\u0026ldquo;abc\u0026rdquo;\n💡 贪心 + 单调栈\n一个单调递增的字符串，当然就是最小的字符串排列。当然，题目要求我们不能有重复字符，并且所有字符都得出现。因此我们在维护单调栈时需要注意以下约束：\n如果当前字符已经存在与栈中，则当前字符不能再入栈。 在弹出栈顶字符时，如果该栈顶字符在以后都不出现了，则不能弹出，因此需要记录每个字符的剩余数量。 class Solution: def removeDuplicateLetters(self, s: str) -\u0026gt; str: if not s: return \u0026#34;\u0026#34; counts = Counter(s) stack = [] for c in s: if c in stack: pass elif not stack or c \u0026gt; stack[-1] or counts[stack[-1]] \u0026lt;= 0: stack.append(c) else: while stack and counts[stack[-1]] \u0026gt; 0 and stack[-1] \u0026gt; c: stack.pop() stack.append(c) counts[c] -= 1 return \u0026#39;\u0026#39;.join(stack) 时间复杂度：O(n)\n","date":"6 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/316_%E5%8E%BB%E9%99%A4%E9%87%8D%E5%A4%8D%E5%AD%97%E6%AF%8D/","section":"leetcode 题解","summary":"","title":"316_去除重复字母","type":"leetcode题解"},{"content":"类型：字符串\n无重复字符的最长子串 💛 https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/\n❓ 找出字符串 s 中不含有重复字符的最长子串，返回其长度。\n💡 滑动窗口 + 哈希表\nleft、right 指针都从左向右。\n当窗口中没有重复字符时，left 指针不动，right 指针向右移动。 当 right 指针移动到出现重复字符，left 指针向右移动，直到窗口内没有重复字符。 通过维护一个与窗口中元素对应的哈希表来快速确认是否有重复字符。 class Solution: def lengthOfLongestSubstring(self, s: str) -\u0026gt; int: # 哈希集合，记录每个字符是否出现过 occ = set() n = len(s) # 右指针，初始值为 -1，相当于我们在字符串的左边界的左侧，还没有开始移动 rk, ans = -1, 0 for i in range(n): if i != 0: # 左指针向右移动一格，移除一个字符 occ.remove(s[i - 1]) while rk + 1 \u0026lt; n and s[rk + 1] not in occ: # 不断地移动右指针 occ.add(s[rk + 1]) rk += 1 # 第 i 到 rk 个字符是一个极长的无重复字符子串 ans = max(ans, rk - i + 1) return ans 时间复杂度：O(n)\n","date":"5 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/3_%E6%97%A0%E9%87%8D%E5%A4%8D%E5%AD%97%E7%AC%A6%E7%9A%84%E6%9C%80%E9%95%BF%E5%AD%90%E4%B8%B2/","section":"leetcode 题解","summary":"","title":"3_无重复字符的最长子串","type":"leetcode题解"},{"content":"类型：数组\n最小路径和 💛 https://leetcode-cn.com/problems/minimum-path-sum/\n❓ 给定一个包含非负整数的 m x n 网格，从左上角出发，每次只能向下或者向右移动一步，请找出一条到右下角的路径，使得路径上的数字总和为最小。\n💡 动态规划\n位置 (i, j) 可以由位置 (i - 1, j) 或 (i, j - 1) 走过来。\n转移方程：dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]\nclass Solution: def minPathSum(self, grid: List[List[int]]) -\u0026gt; int: if not grid or not grid[0]: return 0 rows, columns = len(grid), len(grid[0]) dp = [[0] * columns for _ in range(rows)] dp[0][0] = grid[0][0] for i in range(1, rows): dp[i][0] = dp[i - 1][0] + grid[i][0] for j in range(1, columns): dp[0][j] = dp[0][j - 1] + grid[0][j] for i in range(1, rows): for j in range(1, columns): dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j] return dp[rows - 1][columns - 1] 时间复杂度：O(mn)，空间复杂度：O(mn)\n","date":"4 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/64_%E6%9C%80%E5%B0%8F%E8%B7%AF%E5%BE%84%E5%92%8C/","section":"leetcode 题解","summary":"","title":"64_最小路径和","type":"leetcode题解"},{"content":"类型：树\n二叉搜索树的最近公共祖先 💛 https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/\n❓ 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。（一个节点也可以是它自己的祖先。）\n💡 一次遍历\n此题与「236. 二叉树的最近公共祖先」的关键区别在于，这是一棵二叉搜索树。我们利用搜索树的性质，可以很快地找到目标。\n我们从根结点开始遍历：\n如果 p 和 q 的值小于当前结点，那么 p 和 q 应该都在当前结点的左子树中，我们继续向左子树遍历。 如果 p 和 q 的值大于当前结点，那么 p 和 q 应该都在当前结点的右子树中，我们继续向右边子树遍历。 如果不满足上述两种情况，说明当前结点就是分岔点，也就是咱们要找的最近公共祖先。此时 p 和 q 要么分别位于当前结点的左右子树当中，要么其中一个就是当前结点。 class Solution: def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -\u0026gt; TreeNode: ancestor = root while True: if p.val \u0026lt; ancestor.val and q.val \u0026lt; ancestor.val: ancestor = ancestor.left elif p.val \u0026gt; ancestor.val and q.val \u0026gt; ancestor.val: ancestor = ancestor.right else: break return ancestor 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"3 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/235_%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88/","section":"leetcode 题解","summary":"","title":"235_二叉搜索树的最近公共祖先","type":"leetcode题解"},{"content":"类型：字符串\n字符串相加 💚 https://leetcode-cn.com/problems/add-strings/\n❓ 给定两个字符串形式的非负整数 num1 和num2 ，计算它们的和。\n💡 模拟\nclass Solution: def addStrings(self, num1: str, num2: str) -\u0026gt; str: if not num1 and not num2: return \u0026#34;\u0026#34; elif not num1: return num2 elif not num2: return num1 sumStr = \u0026#34;\u0026#34; carry = 0 i, j = len(num1) - 1, len(num2) - 1 while i \u0026gt;= 0 or j \u0026gt;= 0 or carry \u0026gt; 0: val1 = int(num1[i]) if i \u0026gt;= 0 else 0 val2 = int(num2[j]) if j \u0026gt;= 0 else 0 i -= 1 j -= 1 unitSum = val1 + val2 + carry if unitSum \u0026gt;= 10: unitSum -= 10 carry = 1 else: carry = 0 sumStr = str(unitSum) + sumStr return sumStr 时间复杂度：O(max(len(num1), len(num2))，空间复杂度：O(1)\n","date":"2 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/415_%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9B%B8%E5%8A%A0/","section":"leetcode 题解","summary":"","title":"415_字符串相加","type":"leetcode题解"},{"content":"类型：数组\n检查数组对是否可以被 k 整除 💛 https://leetcode-cn.com/problems/check-if-array-pairs-are-divisible-by-k/\n❓ 把长度为 n（偶数） 的数组分成 n/2 对，使每对数字的和都能被 k 整除。如果存在这样的分法，请返回 True*，*否则返回 False 。\n💡 求余统计 + 哈希\n两个数 x 和 y 的和能被 k 整除，当且仅当 x % k 与 y % k 的和能被 k 整除。\n我们对数组中的每一个数对 k 取模并统计，以取模结果为键，出现次数为值，存入哈希表 mp。看是否满足如下配对要求：\n如果 x % k = 0，那么找到另一个 y % k = 0 与其配对。即模为 0 的个数要为偶数个，即 mp[0] 为偶数。 如果 x % k \u0026gt; 0，那么需要另一个 y % k = k - x % k 预期配对。即二者的数量要相等，即，哈希表中 mp[t] 与 mp[k - t] 的值要相等。特殊情况下，当 t = k / 2（k 为偶数）时，即 t = k - t，此时 mp[t] 应该为偶数。由于此题中数组大小为偶数，在其他情况的元素都配对成功的情况下，剩下的 mp[t] 自然也是偶数。因此不用特殊考虑。 class Solution: def canArrange(self, arr: List[int], k: int) -\u0026gt; bool: mod = collections.Counter(num % k for num in arr) for t, occ in mod.items(): if t \u0026gt; 0 and (k - t not in mod or mod[k - t] != occ): return False return mod[0] % 2 == 0 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"1 January 2021","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1497_%E6%A3%80%E6%9F%A5%E6%95%B0%E7%BB%84%E5%AF%B9%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E8%A2%AB_k_%E6%95%B4%E9%99%A4/","section":"leetcode 题解","summary":"","title":"1497_检查数组对是否可以被_k_整除","type":"leetcode题解"},{"content":"类型：堆\n超级丑数 💛 https://leetcode-cn.com/problems/super-ugly-number/\n❓ 给你一个长度为 k 的质数列表 primes，如果某个数因式分解后，其所有质因数都在 primes 内，那么这样的数被称为超级丑数。请你返回从小到达排序的第 n 个超级丑数。\n输入: n = 12, primes = [2,7,13,19] 输出: 32 解释: 前 12 个超级丑数序列为：[1,2,4,7,8,13,14,16,19,26,28,32] 。\n💡 堆\n一个丑数由 primes 中的质因子相乘得来。那么根据因式分解的性质，一个丑数与 primes 中的数字相乘，得到的也是丑数。这样我们就知道了如何构建出一个丑数来。题目要求我们返回升序排列的第 n 个丑数。因此我们需要用一个小顶堆来实现。\nclass Solution: def nthSuperUglyNumber(self, n: int, primes: List[int]) -\u0026gt; int: heap = [1] ans = None lastNum = None for _ in range(n): while True: lastNum = ans ans = heapq.heappop(heap) if lastNum != ans: break for prime in primes: newPrime = ans * prime heapq.heappush(heap, newPrime) lastNum = newPrime return ans ","date":"31 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/313_%E8%B6%85%E7%BA%A7%E4%B8%91%E6%95%B0/","section":"leetcode 题解","summary":"","title":"313_超级丑数","type":"leetcode题解"},{"content":"类型：树\n二叉树的最大深度 💚 https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/\n❓ 给定一个二叉树，找出其最大深度。\n💡 递归\nclass Solution: def maxDepth(self, root: TreeNode) -\u0026gt; int: if not root: return 0 return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right)) 时间复杂度：O(n)，空间复杂度：O(n)\n💡 BFS\nclass Solution: def maxDepth(self, root: TreeNode) -\u0026gt; int: if not root: return 0 queue = [root] depth = 0 while queue: depth += 1 tempQueue = [] for node in queue: if node.left: tempQueue.append(node.left) if node.right: tempQueue.append(node.right) queue = tempQueue return depth 时间复杂度：O(n)，空间复杂度：O(x)，x 指最大层的元素数。\n","date":"30 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/104_%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6/","section":"leetcode 题解","summary":"","title":"104_二叉树的最大深度","type":"leetcode题解"},{"content":"类型：链表\n环形链表 💚 https://leetcode-cn.com/problems/linked-list-cycle/\n❓ 判断链表中是否有环\n💡 快慢指针\nclass Solution: def hasCycle(self, head: ListNode) -\u0026gt; bool: if not head or not head.next: return False slow = head fast = head.next while slow != fast: if not fast or not fast.next: return False slow = slow.next fast = fast.next.next return True 时间复杂度：O(n)，空间复杂度：O(1)\n💡 哈希\n遍历的过程中将结点地址存入哈希，如果遍历过程中遇到已经存入哈希的结点，则说明成环。\nclass Solution: def hasCycle(self, head: ListNode) -\u0026gt; bool: seen = set() while head: if head in seen: return True seen.add(head) head = head.next return False 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"29 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/141_%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8/","section":"leetcode 题解","summary":"","title":"141_环形链表","type":"leetcode题解"},{"content":" 题目 # 给你一个整数数组 nums 和一个正整数 k ，返回长度为 k 且最具 竞争力 的 **nums 子序列。\n数组的子序列是从数组中删除一些元素（可能不删除元素）得到的序列。\n在子序列 a 和子序列 b 第一个不相同的位置上，如果 a 中的数字小于 b 中对应的数字，那么我们称子序列 a 比子序列 b（相同长度下）更具 竞争力 。 例如，[1,3,4] 比 [1,3,5] 更具竞争力，在第一个不相同的位置，也就是最后一个位置上， 4 小于 5 。\n示例 1：\n输入：nums = [3,5,2,6], k = 2 输出：[2,6] 解释：在所有可能的子序列集合 {[3,5], [3,2], [3,6], [5,2], [5,6], [2,6]} 中，[2,6] 最具竞争力。 示例 2：\n输入：nums = [2,4,3,3,5,4,9,6], k = 4 输出：[2,3,3,4] 提示：\n1 \u0026lt;= nums.length \u0026lt;= 105 0 \u0026lt;= nums[i] \u0026lt;= 109 1 \u0026lt;= k \u0026lt;= nums.length 解法 # 比较容易想到使用单调栈来存储目前已存储的元素，但这里有一个问题是，我们最终得到的单调栈不能小于 k, 因而我们在维护单调栈的同时，也要查看目前单调栈中的元素个数，与剩余元素个数，总共够不够 k 个。若不够，则我们应该将单调性维护到此为止，将剩下的元素全部拼接在后面。\n代码 # class Solution: def mostCompetitive(self, nums: List[int], k: int) -\u0026gt; List[int]: n = len(nums) stack = [] for i in range(n): num = nums[i] while stack and stack[-1] \u0026gt; num and len(stack) - 1 + n - i \u0026gt;= k: stack.pop() stack.append(num) return stack[:k] ","date":"28 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1673_%E6%89%BE%E5%87%BA%E6%9C%80%E5%85%B7%E7%AB%9E%E4%BA%89%E5%8A%9B%E7%9A%84%E5%AD%90%E5%BA%8F%E5%88%97/","section":"leetcode 题解","summary":"","title":"1673_找出最具竞争力的子序列","type":"leetcode题解"},{"content":"","date":"28 December 2020","externalUrl":null,"permalink":"/tags/%E5%8D%95%E8%B0%83%E6%A0%88/","section":"Tags","summary":"","title":"单调栈","type":"tags"},{"content":"类型：树\n二叉树的右视图 💛 https://leetcode-cn.com/problems/binary-tree-right-side-view/\n❓ 给定一棵二叉树，想象自己站在它的右侧，按照从顶部到底部的顺序，返回从右侧所能看到的节点值。\n示例：\n输入: [1,2,3,null,5,null,4,7] 输出: [1, 3, 4, 7] 解释: 1 \u0026lt;--- / \\ 2 3 \u0026lt;--- \\ \\ 5 4 \u0026lt;--- / 7 \u0026lt;--- 💡 BFS\n其实就是层序遍历，只不过我们只要最右边的值。\nclass Solution: def rightSideView(self, root: TreeNode) -\u0026gt; List[int]: if not root: return [] ans = [] queue = [root] while queue: ans.append(queue[-1].val) new_queue = [] for node in queue: if node.left: new_queue.append(node.left) if node.right: new_queue.append(node.right) queue = new_queue return ans 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"27 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/199_%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%8F%B3%E8%A7%86%E5%9B%BE/","section":"leetcode 题解","summary":"","title":"199_二叉树的右视图","type":"leetcode题解"},{"content":"类型：数组\n组合总和 💛 https://leetcode-cn.com/problems/combination-sum/\n❓ 从无重复元素的数组 candidates 中找出所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。\n💡 回溯\nclass Solution: def combinationSum(self, candidates: List[int], target: int) -\u0026gt; List[List[int]]: candidates.sort() n = len(candidates) track = [] ans = [] def trackback(start): if sum(track) == target: ans.append(track[:]) return if sum(track) \u0026gt; target: return if start \u0026gt;= n: return for i in range(start, n): track.append(candidates[i]) trackback(i) track.pop() trackback(0) return ans 时间复杂度：O(S)，其中 S 为所有可行解的长度之和。\n空间复杂度：O(target)。空间复杂度取决于递归的栈深度，在最差情况下需要递归 target 层。\n","date":"26 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/39_%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8C/","section":"leetcode 题解","summary":"","title":"39_组合总和","type":"leetcode题解"},{"content":"类型：链表\n两数相加 II 💛 https://leetcode-cn.com/problems/add-two-numbers-ii/\n❓ 给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加，同样以链表返回。除了数字 0 之外，这两个数字都不会以零开头。\n💡 栈\n把链表元素压入两个栈，再逐一弹出相加。\nclass Solution: def addTwoNumbers(self, l1: ListNode, l2: ListNode) -\u0026gt; ListNode: s1, s2 = [], [] while l1: s1.append(l1.val) l1 = l1.next while l2: s2.append(l2.val) l2 = l2.next ans = None carry = 0 while s1 or s2 or carry != 0: a = 0 if not s1 else s1.pop() b = 0 if not s2 else s2.pop() cur = a + b + carry carry = cur // 10 cur %= 10 curnode = ListNode(cur) curnode.next = ans ans = curnode return ans 时间复杂度：O(max(m, n))，空间复杂度：O(m + n)\n","date":"25 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/445_%E4%B8%A4%E6%95%B0%E7%9B%B8%E5%8A%A0_ii/","section":"leetcode 题解","summary":"","title":"445_两数相加_II","type":"leetcode题解"},{"content":"类型：数组\n最长重复子数组 💛 ⭐ https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/\n❓ 给两个整数数组 A 和 B ，返回两个数组中公共的、长度最长的子数组的长度。\n输入： A: [1,2,3,2,1] B: [3,2,1,4,7] 输出：3 解释：长度最长的公共子数组是 [3, 2, 1] 。\n💡 动态规划\n此题动态规划解法的妙处在于从后往前规划。\n令 dp[i][j] 表示 A[i:] 和 B[j:] 的最长公共前缀，如果 A[i] == B[j]，那么 dp[i][j] = dp[i + 1][j + 1] + 1，否则 dp[i][j] = 0。我们倒过来，从后往前，首先计算 dp[len(A) - 1][len(B) - 1]，最后计算 dp[0][0]。\nclass Solution: def findLength(self, A: List[int], B: List[int]) -\u0026gt; int: n, m = len(A), len(B) dp = [[0] * (m + 1) for _ in range(n + 1)] ans = 0 for i in range(n - 1, -1, -1): for j in range(m - 1, -1, -1): dp[i][j] = dp[i + 1][j + 1] + 1 if A[i] == B[j] else 0 ans = max(ans, dp[i][j]) return ans 时间复杂度：O(mn)，空间复杂度：O(mn)\n","date":"24 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/718_%E6%9C%80%E9%95%BF%E9%87%8D%E5%A4%8D%E5%AD%90%E6%95%B0%E7%BB%84/","section":"leetcode 题解","summary":"","title":"718_最长重复子数组","type":"leetcode题解"},{"content":"类型：数组\n乘积最大子数组 💛 https://leetcode-cn.com/problems/maximum-product-subarray/\n❓ 从整数数组 nums 中找出乘积最大的连续子数组，返回这个最大的乘积。\n💡 动态规划\n我们考虑以第 i 个数为结尾的子数组乘积。对于第 i 个数来说，它有可能加入前面的子数组，也有可能独立成段。由于第 i 个数可能是正数或负数，对于负数来说，我们希望前面的子数组乘积尽可能「负得更多」，即越小越好。对于正数来说，我们希望前面子数组的乘积越大越好。\n因此我们需要同时维护以第 i 个数为结尾的子数组最大乘积和最小乘积。\nclass Solution: def maxProduct(self, nums: List[int]) -\u0026gt; int: dp = [[0, 0] for _ in range(len(nums))] dp[0] = [nums[0], nums[0]] for i in range(1, len(nums)): dp[i] = [ min(nums[i], nums[i] * dp[i - 1][0], nums[i] * dp[i - 1][1]), max(nums[i], nums[i] * dp[i - 1][0], nums[i] * dp[i - 1][1]) ] max_prod = nums[0] for row in dp: max_prod = max(max_prod, max(row)) return max_prod 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"23 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/152_%E4%B9%98%E7%A7%AF%E6%9C%80%E5%A4%A7%E5%AD%90%E6%95%B0%E7%BB%84/","section":"leetcode 题解","summary":"","title":"152_乘积最大子数组","type":"leetcode题解"},{"content":"类型：字符串\nZ 字形变换 💛 https://leetcode-cn.com/problems/zigzag-conversion/\n❓ 对于字符串 s 根据给定的行数 numRows ，以从上往下、从左到右进行 Z 字形排列。\n比如输入字符串为 \u0026quot;PAYPALISHIRING\u0026quot; 行数为 3 时，排列如下：\nP A H N A P L S I I G Y I R 输出：”PAHNAPLSIIGYIR“\n💡 Z 形逐行添加\n初始化 n 行字符串，Z 形访问并添加字符到各行的字符串中，移动到最顶行或最底行时转换方向。最后将各行合并。\nclass Solution: def convert(self, s: str, numRows: int) -\u0026gt; str: if not s or numRows \u0026lt;= 1: return s rows = [\u0026#34;\u0026#34; for _ in range(numRows)] direction = -1 r = 0 for i in range(len(s)): rows[r] += s[i] if r == 0 or r == numRows - 1: direction *= -1 r += direction ans = \u0026#34;\u0026#34; for row in rows: ans += row return ans 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"22 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/6_z_%E5%AD%97%E5%BD%A2%E5%8F%98%E6%8D%A2/","section":"leetcode 题解","summary":"","title":"6_Z_字形变换","type":"leetcode题解"},{"content":"类型：字符串\n实现 strStr() 💚 https://leetcode-cn.com/problems/implement-strstr/\n❓ 实现函数strStr() 用于在字符串 haystack 中找到子串 needle 的出现的位置，未出现则返回 -1\n💡 暴力匹配\nclass Solution { public int strStr(String haystack, String needle) { int n = haystack.length(), m = needle.length(); for (int i = 0; i + m \u0026lt;= n; i++) { boolean flag = true; for (int j = 0; j \u0026lt; m; j++) { if (haystack.charAt(i + j) != needle.charAt(j)) { flag = false; break; } } if (flag) { return i; } } return -1; } } 时间复杂度树：O(mn)，m 和 n 分别是 haystack 和 needle 的长度。空间复杂度：O(1)\n💡 KMP 算法\n这个一般不作为面试要求，可以理解原理，作为面试亮点。\n时间复杂度：O(m + n)，空间复杂度：O(n)，n 指 needle 的长度。\n","date":"21 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/28_%E5%AE%9E%E7%8E%B0_strstr/","section":"leetcode 题解","summary":"","title":"28_实现_strStr()","type":"leetcode题解"},{"content":"类型：树\n二叉树的层序遍历 💛 https://leetcode-cn.com/problems/binary-tree-level-order-traversal/\n❓ 给你一个二叉树，返回其层序遍历结果（二维数组）。\n示例：\n二叉树：[3,9,20,null,null,15,7], 3 / \\ 9 20 / \\ 15 7 结果： [ [3], [9,20], [15,7] ] 💡 BFS\nclass Solution: def levelOrder(self, root: TreeNode) -\u0026gt; List[List[int]]: if not root: return [] ans = [] queue = [root] while queue: newQueue = [] row = [] for node in queue: row.append(node.val) if node.left: newQueue.append(node.left) if node.right: newQueue.append(node.right) ans.append(row) queue = newQueue return ans 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"20 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/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/","section":"leetcode 题解","summary":"","title":"102_二叉树的层序遍历","type":"leetcode题解"},{"content":"类型：堆\n最后一块石头的重量 💚 https://leetcode-cn.com/problems/last-stone-weight/\n❓ 有一堆石头，重量都是正整数。每次从石堆中选出「两块最重的」石头，一起粉碎。如果两块石头一样重（x == y），那么二者都会被粉碎。如果其中一块较重(x \u0026gt; y)，那么较轻的石头将被粉碎完，更重的那块则质量变为 x - y。\n求最后剩下石头的重量，若无石头剩下，则返回 0.\n💡 堆\n构造大顶堆，每次从堆顶取出两块进行粉碎，剩下的再入堆。\nclass Solution: def lastStoneWeight(self, stones: List[int]) -\u0026gt; int: if not stones: return 0 if len(stones) == 1: return stones[0] # 将数组构造成大顶堆，每次从中取出最大的两个，再将二者的差值重新入堆，直至堆中只剩一个元素 heap = [stone * -1 for stone in stones] heapq.heapify(heap) while len(heap) \u0026gt; 1: stone1 = - heapq.heappop(heap) stone2 = - heapq.heappop(heap) heapq.heappush(heap, -1 * abs(stone1 - stone2)) return -1 * heap[0] if heap else 0 时间复杂度：O(NlogN)，空间复杂度：O(N)\n","date":"19 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1046_%E6%9C%80%E5%90%8E%E4%B8%80%E5%9D%97%E7%9F%B3%E5%A4%B4%E7%9A%84%E9%87%8D%E9%87%8F/","section":"leetcode 题解","summary":"","title":"1046_最后一块石头的重量","type":"leetcode题解"},{"content":"类型：堆\n前 K 个高频元素 💛 https://leetcode-cn.com/problems/top-k-frequent-elements/\n❓ 返回数组中出现频率前 k 高的元素。\n💡 哈希 + 堆\n先用哈希记录每个元素的出现次数。再以出现次数为比较键构建堆。\nclass Solution: def topKFrequent(self, nums: List[int], k: int) -\u0026gt; List[int]: if not nums: return [] counts = {} for num in nums: if num in counts: counts[num] += 1 else: counts[num] = 1 heap = [] for num in counts: count = counts[num] if len(heap) \u0026lt; k or count \u0026gt; heap[0][0]: heapq.heappush(heap, (counts[num], num)) if len(heap) \u0026gt; k: heapq.heappop(heap) ans = [] for (_, num) in heap: ans.append(num) return ans ","date":"18 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/347_%E5%89%8D_k_%E4%B8%AA%E9%AB%98%E9%A2%91%E5%85%83%E7%B4%A0/","section":"leetcode 题解","summary":"","title":"347_前_K_个高频元素","type":"leetcode题解"},{"content":"类型：字符串\n反转字符串中的元音字母 ****💚 https://leetcode-cn.com/problems/reverse-vowels-of-a-string/\n❓ 编写一个函数，以字符串作为输入，反转该字符串中的元音字母。\n💡 双指针\nclass Solution: def isVowel(self, c): return c in [\u0026#39;a\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;i\u0026#39;, \u0026#39;o\u0026#39;, \u0026#39;u\u0026#39;, \u0026#39;A\u0026#39;, \u0026#39;E\u0026#39;, \u0026#39;I\u0026#39;, \u0026#39;O\u0026#39;, \u0026#39;U\u0026#39;] def reverseVowels(self, s: str) -\u0026gt; str: ls = list(s) left, right = 0, len(ls) - 1 while True: while left \u0026lt; len(ls) and not self.isVowel(ls[left]): left += 1 while right \u0026gt;= 0 and not self.isVowel(ls[right]): right -= 1 if left \u0026gt;= right: break else: ls[left], ls[right] = ls[right], ls[left] left += 1 right -= 1 return \u0026#39;\u0026#39;.join(ls) 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"17 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/345_%E5%8F%8D%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%AD%E7%9A%84%E5%85%83%E9%9F%B3%E5%AD%97%E6%AF%8D/","section":"leetcode 题解","summary":"","title":"345_反转字符串中的元音字母","type":"leetcode题解"},{"content":"类型：树\n修剪二叉搜索树 💛 https://leetcode-cn.com/problems/trim-a-binary-search-tree/\n❓ 给你二叉搜索树的根节点 root ，同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树，使得所有节点的值在[low, high]中。不得破坏留在树中的结点的相对结构。\n💡 递归\n我们要利用好其搜索树的性质。\n如果结点值大于 high，我们就完全抛弃右子树，直接递归左子树 如果结点值小于 low，我们就完全抛弃左子树，完全递归右子树 如果结点值在 [low, high] 之间，则两个子树都要递归，并且要完成递归修剪后的拼接 class Solution(object): def trimBST(self, root, L, R): def trim(node): if not node: return None elif node.val \u0026gt; R: return trim(node.left) elif node.val \u0026lt; L: return trim(node.right) else: node.left = trim(node.left) node.right = trim(node.right) return node return trim(root) 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"16 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/669_%E4%BF%AE%E5%89%AA%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","section":"leetcode 题解","summary":"","title":"669_修剪二叉搜索树","type":"leetcode题解"},{"content":"类型：树\n二叉树的层序遍历 II 💛 https://leetcode-cn.com/problems/binary-tree-level-order-traversal-ii/\n❓ 给定一个二叉树，返回其节点值自底向上的层序遍历。\n💡 BFS\n在遍历完一层节点之后，将存储该层节点值的列表添加到结果列表的头部。\nclass Solution: def levelOrderBottom(self, root: TreeNode) -\u0026gt; List[List[int]]: if not root: return [] queue = [root] output = [] while queue: tempQueue = [] levelOuput = [] for node in queue: levelOuput.append(node.val) if node.left: tempQueue.append(node.left) if node.right: tempQueue.append(node.right) queue = tempQueue output.insert(0, levelOuput) return output 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"15 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/107_%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86_ii/","section":"leetcode 题解","summary":"","title":"107_二叉树的层序遍历_II","type":"leetcode题解"},{"content":"类型：字符串\n二进制求和 💚 ⭐ https://leetcode-cn.com/problems/add-binary/\n❓ 给你两个字符串表示的二进制数，求和，同样以字符串返回。\n💡 模拟\n模拟竖式运算，从后往前逐位相加并进位。\nclass Solution: def addBinary(self, a: str, b: str) -\u0026gt; str: if not a: return b if not b: return a c = \u0026#39;\u0026#39; i = len(a) - 1 carry = 0 if len(a) \u0026lt; len(b): a = \u0026#39;0\u0026#39; * (len(b) - len(a)) + a elif len(b) \u0026lt; len(a): b = \u0026#39;0\u0026#39; * (len(a) - len(b)) + b for i in range(len(a) - 1, -1, -1): val = int(a[i]) + int(b[i]) + carry c = str(val % 2) + c carry = 0 if val \u0026lt; 2 else 1 if carry == 1: c = \u0026#39;1\u0026#39; + c return c 时间复杂度：O(max(m, n))，空间复杂度：O(max(m, n))\n","date":"14 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/67_%E4%BA%8C%E8%BF%9B%E5%88%B6%E6%B1%82%E5%92%8C/","section":"leetcode 题解","summary":"","title":"67_二进制求和","type":"leetcode题解"},{"content":"类型：数组\n子集 II 💛 https://leetcode-cn.com/problems/subsets-ii/\n❓ 整数数组 nums 中可能包含重复元素，返回该数组所有可能的子集。解集不能包含重复的子集。返回的解集中，子集可以按任意顺序排列。\n💡 回溯法\n红框里面的要剪掉，具体来说是排序后，遍历到的当前元素与上一个元素相等时，则跳过。即下划线的这几个数 [1, 2, 2, 2, 4, 5, 5, 7] 。\nclass Solution: def subsetsWithDup(self, nums: List[int]) -\u0026gt; List[List[int]]: nums.sort() n = len(nums) track = [] ans = [] def trackback(start): ans.append(track[:]) for i in range(start, n): if i \u0026gt; start and nums[i] == nums[i - 1]: continue track.append(nums[i]) trackback(i + 1) track.pop() trackback(0) return ans ","date":"13 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/90_%E5%AD%90%E9%9B%86_ii/","section":"leetcode 题解","summary":"","title":"90_子集_II","type":"leetcode题解"},{"content":"类型：数组\n盛最多水的容器 💛 https://leetcode-cn.com/problems/container-with-most-water/\n❓ 给你一个 height 数组，代表木板高度，相邻木板间隔为 1 个单位。问最大的容水量。\n💡 双指针\n开始时，左右指针分别指向数组的左右两端。\n此时我们需要移动一个指针，移动哪一个呢？我们应该移动对应数字较小的那个指针。\n容水量 = 两个指针指向数字中的较小值 * 指针距离\n如果我们移动较大的那个指针，那么「两个指针指向数字中的较小值」不会增加，「指针之间的距离」会减小，容水量必然减小。\nclass Solution: def maxArea(self, height: List[int]) -\u0026gt; int: l, r = 0, len(height) - 1 ans = 0 while l \u0026lt; r: area = min(height[l], height[r]) * (r - l) ans = max(ans, area) if height[l] \u0026lt;= height[r]: l += 1 else: r -= 1 return ans 时间复杂度：O(n)，双指针总计最多遍历数组一次\n空间复杂度：O(1)\n","date":"12 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/11_%E7%9B%9B%E6%9C%80%E5%A4%9A%E6%B0%B4%E7%9A%84%E5%AE%B9%E5%99%A8/","section":"leetcode 题解","summary":"","title":"11_盛最多水的容器","type":"leetcode题解"},{"content":"类型：数组\n二维数组中的查找 💛\nhttps://leetcode-cn.com/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/\n❓ 在一个 n * m 的二维数组中，每一行都按照从左到右递增的顺序排序，每一列都按照从上到下递增的顺序排序。请完成一个高效的函数，输入这样的一个二维数组和一个整数，判断数组中是否含有该整数。\n💡 线性查找\n从二维数组的右上角开始查找。如果当前元素等于目标值，则返回 true。如果当前元素大于目标值，则移到左边一列。如果当前元素小于目标值，则移到下边一行。\nclass Solution: def findNumberIn2DArray(self, matrix: List[List[int]], target: int) -\u0026gt; bool: if not matrix: return False m, n = len(matrix), len(matrix[0]) i, j = 0, n - 1 while i \u0026lt; m and j \u0026gt;= 0: if matrix[i][j] == target: return True elif target \u0026lt; matrix[i][j]: j -= 1 else: i += 1 return False 时间复杂度：O(m + n)，空间复杂度：O(1)\n","date":"11 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/%E5%89%91%E6%8C%87_offer_04_%E4%BA%8C%E7%BB%B4%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E6%9F%A5%E6%89%BE/","section":"leetcode 题解","summary":"","title":"04_二维数组中的查找","type":"leetcode题解"},{"content":"类型：链表\n删除排序链表中的重复元素 II 💛 https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/\n❓ 给定一个升序排列的链表，请删除链表中「所有」数值重复的节点。不破坏剩下结点的升序排列。\n💡 一次遍历\n我们进行一次遍历，即可完成删除操作。由于头结点也可能删除，因此我们可以增加一个虚拟结点指向头结点。\nclass Solution: def deleteDuplicates(self, head: ListNode) -\u0026gt; ListNode: if not head: return head dummy = ListNode(0, head) cur = dummy while cur.next and cur.next.next: if cur.next.val == cur.next.next.val: x = cur.next.val while cur.next and cur.next.val == x: cur.next = cur.next.next else: cur = cur.next return dummy.next 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"9 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/82_%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0_ii/","section":"leetcode 题解","summary":"","title":"82_删除排序链表中的重复元素_II","type":"leetcode题解"},{"content":"类型：树\n将有序数组转换为二叉搜索树 💚 https://leetcode-cn.com/problems/convert-sorted-array-to-binary-search-tree/\n❓ 将一个升序整数数组 nums 转换为一棵平衡二叉搜索树（每个节点的左右两个子树的高度差的绝对值不超过 1）。\n💡 取中间结点\n我们选择中间元素作为树的根节点，以使树保持平衡。如果数组长度是奇数，则根节点的选择是唯一的，如果数组长度是偶数，则选取中间位置左边、右边或任选其一。\nclass Solution: def sortedArrayToBST(self, nums: List[int]) -\u0026gt; TreeNode: def helper(left, right): if left \u0026gt; right: return None # 总是选择中间位置右边的数字作为根节点 mid = (left + right + 1) // 2 root = TreeNode(nums[mid]) root.left = helper(left, mid - 1) root.right = helper(mid + 1, right) return root return helper(0, len(nums) - 1) 时间复杂度：O(N)，空间复杂度：O(logN)\n","date":"8 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/108_%E5%B0%86%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E8%BD%AC%E6%8D%A2%E4%B8%BA%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","section":"leetcode 题解","summary":"","title":"108_将有序数组转换为二叉搜索树","type":"leetcode题解"},{"content":"类型：树\n平衡二叉树 💚 https://leetcode-cn.com/problems/balanced-binary-tree/\n❓ 判断一棵二叉树是否是高度平衡的二叉树（每个节点的左右两个子树的高度差的绝对值不超过 1）。\n💡 递归\n类似于后序遍历，对于当前节点，先递归地判断其左右子树是否平衡，再判断以当前节点为根的子树是否平衡。如果一棵子树是平衡的，则返回其高度（高度一定是非负整数），否则返回 −1。如果存在一棵子树不平衡，则整个二叉树一定不平衡。\nclass Solution: def isBalanced(self, root: TreeNode) -\u0026gt; bool: def height(root: TreeNode) -\u0026gt; int: if not root: return 0 leftHeight = height(root.left) rightHeight = height(root.right) if leftHeight == -1 or rightHeight == -1 or abs(leftHeight - rightHeight) \u0026gt; 1: return -1 else: return max(leftHeight, rightHeight) + 1 return height(root) \u0026gt;= 0 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"7 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/110_%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91/","section":"leetcode 题解","summary":"","title":"110_平衡二叉树","type":"leetcode题解"},{"content":"类型：链表\n删除链表的倒数第 N 个结点 💛 https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/\n❓ 给你一个链表，删除链表的倒数第 n 个结点。\n💡 快慢指针\n快指针比慢指针先走 n 步。同时由于可能删除头结点，因此要注意虚拟结点的使用。\nclass Solution: def removeNthFromEnd(self, head: ListNode, n: int) -\u0026gt; ListNode: if not head: return None dummyHead = ListNode(next = head) fast = dummyHead for i in range(n): if not fast: return head fast = fast.next slow = dummyHead while fast and fast.next: slow = slow.next fast = fast.next slow.next = slow.next.next return dummyHead.next 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"6 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/19_%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E5%80%92%E6%95%B0%E7%AC%AC_n_%E4%B8%AA%E7%BB%93%E7%82%B9/","section":"leetcode 题解","summary":"","title":"19_删除链表的倒数第_N_个结点","type":"leetcode题解"},{"content":"类型：堆\n最接近原点的 K 个点 💛 https://leetcode-cn.com/problems/k-closest-points-to-origin/\n❓ 我们有一个由平面上的点组成的列表 points。请从中找出 K 个距离原点 (0, 0) 最近的点。\n💡 堆\n这题跟「面试题 17.14. 最小K个数」是一样的。\nclass Solution: def kClosest(self, points: List[List[int]], k: int) -\u0026gt; List[List[int]]: if not points or k \u0026lt;= 0: return [] h = [] for i in range(len(points)): p = points[i] dis = p[0] ** 2 + p[1] ** 2 if len(h) \u0026lt; k or dis \u0026lt; -h[0][0]: heapq.heappush(h, (-dis, i)) if len(h) \u0026gt; k: heapq.heappop(h) ans = [] for item in h: ans.append(points[item[1]]) return ans ","date":"5 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/973_%E6%9C%80%E6%8E%A5%E8%BF%91%E5%8E%9F%E7%82%B9%E7%9A%84_k_%E4%B8%AA%E7%82%B9/","section":"leetcode 题解","summary":"","title":"973_最接近原点的_K_个点","type":"leetcode题解"},{"content":"类型：树\n二叉树中的最大路径和 ❤️ https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/\n❓ 求二叉树中最大的路径和。路径至少包含一个结点，且不一定经过根结点。\n💡 递归 + 贪心\nclass Solution: def __init__(self): self.maxSum = -2147483648 def maxPathSum(self, root: TreeNode) -\u0026gt; int: if not root: return 0 def dfs(node): if not node: return 0 leftSum = dfs(node.left) rightSum = dfs(node.right) self.maxSum = max(self.maxSum, max(leftSum, 0) + max(rightSum, 0) + node.val) return max(leftSum, rightSum, 0) + node.val dfs(root) return self.maxSum 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"4 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/124_%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E8%B7%AF%E5%BE%84%E5%92%8C/","section":"leetcode 题解","summary":"","title":"124_二叉树中的最大路径和","type":"leetcode题解"},{"content":"类型：树\n二叉搜索树节点最小距离 💚 https://leetcode-cn.com/problems/minimum-distance-between-bst-nodes/\n❓ 给你一个二叉搜索树，返回树中两个不同结点差值的最小值。\n💡 中序遍历\n搜索树中序遍历之后就得到了升序排列的数组，然后遍历数组，计算所有相邻元素差值，返回其中最小值。\nclass Solution: def minDiffInBST(self, root: TreeNode) -\u0026gt; int: if not root: return 0 inorderArr = [] def inorderTraverse(node): if not node: return inorderTraverse(node.left) inorderArr.append(node.val) inorderTraverse(node.right) inorderTraverse(root) return min(inorderArr[i + 1] - inorderArr[i] for i in range(len(inorderArr) - 1)) 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"3 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/783_%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E8%8A%82%E7%82%B9%E6%9C%80%E5%B0%8F%E8%B7%9D%E7%A6%BB/","section":"leetcode 题解","summary":"","title":"783_二叉搜索树节点最小距离","type":"leetcode题解"},{"content":"类型：字符串\n字母异位词分组 💛 https://leetcode-cn.com/problems/group-anagrams/\n❓ 给定一个字符串数组，将字母异位词组合在一起。字母异位词指字母相同，但排列不同的字符串。\n输入: [\u0026ldquo;eat\u0026rdquo;, \u0026ldquo;tea\u0026rdquo;, \u0026ldquo;tan\u0026rdquo;, \u0026ldquo;ate\u0026rdquo;, \u0026ldquo;nat\u0026rdquo;, \u0026ldquo;bat\u0026rdquo;] 输出:[ [\u0026ldquo;ate\u0026rdquo;,\u0026ldquo;eat\u0026rdquo;,\u0026ldquo;tea\u0026rdquo;], [\u0026ldquo;nat\u0026rdquo;,\u0026ldquo;tan\u0026rdquo;], [\u0026ldquo;bat\u0026rdquo;] ]\n💡 哈希表\n两个字符串互为字母异位词，当且仅当两个字符串包含的字母相同，我们可以根据这一点来将其构建成哈希表的键。使得所有互为异位词的单词都有相同的键。所有互为异位词的单词根据字典排序后，都会变成同一个单词。\nclass Solution: def groupAnagrams(self, strs: List[str]) -\u0026gt; List[List[str]]: mp = collections.defaultdict(list) for st in strs: key = \u0026#34;\u0026#34;.join(sorted(st)) mp[key].append(st) return list(mp.values()) 时间复杂度：O(n * klog(k))，n 是字符串数量，k 是最长串的长度。空间复杂度：O(nk)\n","date":"2 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/49_%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D%E5%88%86%E7%BB%84/","section":"leetcode 题解","summary":"","title":"49_字母异位词分组","type":"leetcode题解"},{"content":"类型：树\n二叉树的坡度 💚 https://leetcode-cn.com/problems/binary-tree-tilt/\n❓ 给定一个二叉树，计算 整个树 的坡度 。\n节点的坡度：左子树的节点之和与右子树节点之和的差的绝对值 。\n树的坡度：所有节点的坡度之和。\n💡 后序遍历\nclass Solution: def __init__(self): self.ans = 0 def findTilt(self, root: TreeNode) -\u0026gt; int: # 典型的后序遍历题 if not root: return 0 def dfs(node): if not node: return 0 leftSum = dfs(node.left) rightSum = dfs(node.right) tilt = abs(leftSum - rightSum) self.ans += tilt return leftSum + node.val + rightSum dfs(root) return self.ans ","date":"1 December 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/563_%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%9D%A1%E5%BA%A6/","section":"leetcode 题解","summary":"","title":"563_二叉树的坡度","type":"leetcode题解"},{"content":"类型：数组\n搜索插入位置 💚 https://leetcode-cn.com/problems/search-insert-position/\n❓ 在已排序的数组（无重复元素）中找到目标值，并返回其下标。如果目标值不存在于数组中，返回它将会被按顺序插入的位置。\n💡 二分查找\n我们需要找到这样一个位置 pos，使得：nums[pos−1] \u0026lt; target ≤ nums[pos]\nclass Solution: def searchInsert(self, nums: List[int], target: int) -\u0026gt; int: if not nums: return 0 low = 0 high = len(nums) - 1 while low \u0026lt;= high: mid = (low + high) // 2 if nums[mid] == target: return mid elif nums[mid] \u0026gt; target: high = mid - 1 else: low = mid + 1 return low 时间复杂度：O(logN)，时间复杂度：O(1)\n","date":"30 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/35_%E6%90%9C%E7%B4%A2%E6%8F%92%E5%85%A5%E4%BD%8D%E7%BD%AE/","section":"leetcode 题解","summary":"","title":"35_搜索插入位置","type":"leetcode题解"},{"content":"类型：字符串\n有效的括号 💚 https://leetcode-cn.com/problems/valid-parentheses/\n❓ 给定一个只包括 \u0026lsquo;(\u0026rsquo;，\u0026rsquo;)\u0026rsquo;，\u0026rsquo;{\u0026rsquo;，\u0026rsquo;}\u0026rsquo;，\u0026rsquo;[\u0026rsquo;，\u0026rsquo;]\u0026rsquo; 的字符串 s ，判断括号是否合法匹配。\n💡 栈\n左括号压栈，右括号弹栈并比对。\nclass Solution: def isValid(self, s: str) -\u0026gt; bool: if len(s) % 2 == 1: return False pairs = { \u0026#34;)\u0026#34;: \u0026#34;(\u0026#34;, \u0026#34;]\u0026#34;: \u0026#34;[\u0026#34;, \u0026#34;}\u0026#34;: \u0026#34;{\u0026#34;, } stack = list() for ch in s: if ch in pairs: if not stack or stack[-1] != pairs[ch]: return False stack.pop() else: stack.append(ch) return not stack 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"29 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/20_%E6%9C%89%E6%95%88%E7%9A%84%E6%8B%AC%E5%8F%B7/","section":"leetcode 题解","summary":"","title":"20_有效的括号","type":"leetcode题解"},{"content":"类型：队列\n数据流中的移动平均值 💚 https://leetcode-cn.com/problems/moving-average-from-data-stream/\n❓ 实现一个滑动窗口类 MovingAverage：\nMovingAverage(int size)\n初始化示例，窗口大小初始化为 size。\ndouble next(int val)\n向窗口中压入数据 val，并返回当前窗口内数据平均值。\n💡 双端队列\nclass MovingAverage: def __init__(self, size: int): \u0026#34;\u0026#34;\u0026#34; Initialize your data structure here. \u0026#34;\u0026#34;\u0026#34; self.size = size self.arr = [] def next(self, val: int) -\u0026gt; float: self.arr.append(val) if len(self.arr) \u0026gt; self.size: self.arr = self.arr[len(self.arr) - self.size:] return sum(self.arr) / len(self.arr) ","date":"28 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/346_%E6%95%B0%E6%8D%AE%E6%B5%81%E4%B8%AD%E7%9A%84%E7%A7%BB%E5%8A%A8%E5%B9%B3%E5%9D%87%E5%80%BC/","section":"leetcode 题解","summary":"","title":"346_数据流中的移动平均值","type":"leetcode题解"},{"content":"","date":"28 November 2020","externalUrl":null,"permalink":"/tags/%E9%98%9F%E5%88%97/","section":"Tags","summary":"","title":"队列","type":"tags"},{"content":"类型：树\n统计二叉树中好节点的数目 💛 https://leetcode-cn.com/problems/count-good-nodes-in-binary-tree/\n❓ 二叉树中，好结点 X 定义为，从根到该结点的路径中，没有任何结点的值大于 X 的值。\n求计算二叉树中好结点的数目。\n💡 DFS\n我们在 DFS 的过程中，维护当前路径中的最大值即可。\nclass Solution: def goodNodes(self, root: TreeNode) -\u0026gt; int: if not root: return 0 def dfs(node, max_val): if not node: return 0 count = 0 if node.val \u0026gt;= max_val: count = 1 max_val = node.val return count + dfs(node.left, max_val) + dfs(node.right, max_val) return dfs(root, root.val) 时间复杂度：O(n)，空间复杂度：O(h)\n","date":"27 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1448_%E7%BB%9F%E8%AE%A1%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E5%A5%BD%E8%8A%82%E7%82%B9%E7%9A%84%E6%95%B0%E7%9B%AE/","section":"leetcode 题解","summary":"","title":"1448_统计二叉树中好节点的数目","type":"leetcode题解"},{"content":"类型：字符串\n赎金信 💚 https://leetcode-cn.com/problems/ransom-note/\n❓ 判断杂志 magazine 中的字符能否构成赎金信 ransom。\n💡 哈希\n遍历 magazine，构建哈希表，字符为键，出现次数为值。\n遍历 ransom，检验各个字符的出现次数，\nclass Solution: def canConstruct(self, ransomNote: str, magazine: str) -\u0026gt; bool: hashMap = {} for c in magazine: if c in hashMap: hashMap[c] += 1 else: hashMap[c] = 1 for c in ransomNote: if not c in hashMap: return False elif hashMap[c] == 1: del hashMap[c] else: hashMap[c] -= 1 return True 时间复杂度：O(m + n)，空间复杂度：O(m)\n","date":"26 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/383_%E8%B5%8E%E9%87%91%E4%BF%A1/","section":"leetcode 题解","summary":"","title":"383_赎金信","type":"leetcode题解"},{"content":"类型：数组\n跳跃游戏 💛 ⭐ https://leetcode-cn.com/problems/jump-game/\n❓ 给定一个非负整数数组 nums ，你最初位于数组的 第一个下标 。数组中的每个元素代表在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。\n💡 贪心\n对于每一个可以到达的位置 x，它使得 x+1, x+2,\u0026hellip;, x + nums[x] 这些连续的位置都可以到达。这样，我们依次遍历数组中的每一个位置，并实时维护最远可以到达的位置。\n在遍历的过程中，如果 最远可以到达的位置 大于等于数组中的最后一个位置，那就说明最后一个位置可达，我们就可以直接返回 True 作为答案。反之，如果在遍历结束后，最后一个位置仍然不可达，我们就返回 False 作为答案。\nclass Solution: def canJump(self, nums: List[int]) -\u0026gt; bool: furth = 0 for i in range(len(nums)): if i \u0026gt; furth: return False furth = max(furth, i + nums[i]) if furth \u0026gt;= len(nums): return True return True 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"25 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/55_%E8%B7%B3%E8%B7%83%E6%B8%B8%E6%88%8F/","section":"leetcode 题解","summary":"","title":"55_跳跃游戏","type":"leetcode题解"},{"content":"类型：链表\n奇偶链表 💛 ⭐ https://leetcode-cn.com/problems/odd-even-linked-list/\n❓ 给定一个单链表，把所有的奇数节点和偶数节点分别排在一起。请注意，这里的奇数节点和偶数节点指的是节点编号的奇偶性，而不是节点的值的奇偶性。\n输入: 1-\u0026gt;2-\u0026gt;3-\u0026gt;4-\u0026gt;5-\u0026gt;NULL 输出: 1-\u0026gt;3-\u0026gt;5-\u0026gt;2-\u0026gt;4-\u0026gt;NULL\n💡 分离后合并\n将链表分离成奇偶两条链表。再首尾连接。\nclass Solution: def oddEvenList(self, head: ListNode) -\u0026gt; ListNode: if not head: return head evenHead = head.next odd, even = head, evenHead while even and even.next: odd.next = even.next odd = odd.next even.next = odd.next even = even.next odd.next = evenHead return head 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"24 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/328_%E5%A5%87%E5%81%B6%E9%93%BE%E8%A1%A8/","section":"leetcode 题解","summary":"","title":"328_奇偶链表","type":"leetcode题解"},{"content":"类型：数组\n存在重复元素 II 💚 https://leetcode-cn.com/problems/contains-duplicate-ii/\n❓ 判断整数数组 nums 中是否存在 nums[i] = nums[j]，且 i, j 之差的绝对值小于等于 k。\n💡 哈希表 + 滑动窗口\n我们通过哈希表来维护一个大小为 k 的滑动窗口。\n遍历数组，针对每个元素做以下操作：\n在散列表中搜索当前元素，如果找到了就返回 true。 在散列表中插入当前元素。 如果当前散列表的大小超过了 k， 删除散列表中最旧的元素。 class Solution: def containsNearbyDuplicate(self, nums: List[int], k: int) -\u0026gt; bool: hashMap = {} for i in range(len(nums)): num = nums[i] if num in hashMap and i - hashMap[num] \u0026lt;= k: return True else: hashMap[num] = i return False 时间复杂度：O(1)，空间复杂度：O(min(n, k))\n","date":"23 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/219_%E5%AD%98%E5%9C%A8%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0_ii/","section":"leetcode 题解","summary":"","title":"219_存在重复元素_II","type":"leetcode题解"},{"content":"类型：数组\n旋转数组 💛 ⭐ https://leetcode-cn.com/problems/rotate-array/\n❓ 给定一个数组，将数组中的元素向右移动 k 个位置，其中 k 是非负数。\n输入: nums = [1,2,3,4,5,6,7], k = 3 输出: [5,6,7,1,2,3,4]\n💡 整体翻转 + 局部翻转\n先将整个数组翻转，再分别翻转 [0,k%n − 1] 和 [k%n, n - 1]。\nclass Solution: def rotate(self, nums: List[int], k: int) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; Do not return anything, modify nums in-place instead. \u0026#34;\u0026#34;\u0026#34; n = len(nums) k %= n if k == 0: return # 翻转整个数组 nums[:] = nums[::-1] # 翻转 0 - k-1 nums[: k] = nums[k - 1: : -1] # 翻转 k - n-1 nums[k:] = nums[n - 1: k - 1: -1] 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"22 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/189_%E6%97%8B%E8%BD%AC%E6%95%B0%E7%BB%84/","section":"leetcode 题解","summary":"","title":"189_旋转数组","type":"leetcode题解"},{"content":"类型：字符串\n验证回文串 💚 https://leetcode-cn.com/problems/valid-palindrome/\n❓ 判断一个字符串是否为回文串。只关注字符串中的字母和数字字符，且不区分字母的大小写。\n💡 双指针\n双指针从两端向中间遍历，并且跳过非字母和数字的字符。\nclass Solution: def isPalindrome(self, s: str) -\u0026gt; bool: i = 0 j = len(s) - 1 while i \u0026lt; j: while i \u0026lt; j and not s[i].isalpha() and not s[i].isdigit(): i += 1 while i \u0026lt; j and not s[j].isalpha() and not s[j].isdigit(): j -= 1 if s[i].lower() != s[j].lower(): return False else: i += 1 j -= 1 return True 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"21 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/125_%E9%AA%8C%E8%AF%81%E5%9B%9E%E6%96%87%E4%B8%B2/","section":"leetcode 题解","summary":"","title":"125_验证回文串","type":"leetcode题解"},{"content":"类型：数组\n使用最小花费爬楼梯 💚 https://leetcode-cn.com/problems/min-cost-climbing-stairs/\n❓ 数组 cost 表示到达每个台阶需要花费的体力值。每次可以向上爬一个台阶或两个台阶。求到达顶部的最低体力花费。你可以选择第 0 或第 1 级作为起始台阶。\n💡 动态规划\n第 i 级台阶可以由第 i - 1 级到达，也可以由第 i - 2 级到达。\n转移方程：dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])\n由于 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关，因此可以只保存前两个记录，优化空间。\nclass Solution: def minCostClimbingStairs(self, cost: List[int]) -\u0026gt; int: pays = [0, 0] for i in range(2, len(cost) + 1): pay = min(pays[0] + cost[i - 2], pays[1] + cost[i - 1]) pays[0], pays[1] = pays[1], pay return pays[1] 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"20 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/746_%E4%BD%BF%E7%94%A8%E6%9C%80%E5%B0%8F%E8%8A%B1%E8%B4%B9%E7%88%AC%E6%A5%BC%E6%A2%AF/","section":"leetcode 题解","summary":"","title":"746_使用最小花费爬楼梯","type":"leetcode题解"},{"content":"类型：数组\n移动零 💚 https://leetcode-cn.com/problems/move-zeroes/\n❓ 给定一个数组 nums，编写一个函数将所有 0 移动到数组的末尾，同时保持非零元素的相对顺序。\n💡 双指针\n使用双指针，左指针指向当前已经处理好的序列的尾部，右指针指向待处理序列的头部。\n左指针左边均为非零数； 左右指针之间均为零。 右指针不断向右移动，每次右指针指向非零数，则将左右指针对应的数交换，同时左指针右移。\n就好像是有一串零组成的泡泡在往右冒，当遇到新的零泡的时候，则会将其吸纳融合。\n因此每次交换，都是将左指针的零与右指针的非零数交换，且非零数的相对顺序并未改变。\nclass Solution: def moveZeroes(self, nums: List[int]) -\u0026gt; None: n = len(nums) left = right = 0 while right \u0026lt; n: if nums[right] != 0: nums[left], nums[right] = nums[right], nums[left] left += 1 right += 1 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"19 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/283_%E7%A7%BB%E5%8A%A8%E9%9B%B6/","section":"leetcode 题解","summary":"","title":"283_移动零","type":"leetcode题解"},{"content":"类型：数组\n数组中重复的数据 💛 https://leetcode-cn.com/problems/find-all-duplicates-in-an-array/\n❓ 给定一个整数数组 a，其中1 ≤ a[i] ≤ n （n为数组长度）, 其中有些元素出现两次而其他元素出现一次。找到所有出现两次的元素。\n💡 原地哈希\n通过取相反数的形式，既保留了原来元素的信息(绝对值不变)，同时还可以标记变化（第nums[i]个元素小于0说明之前出现过nums[i]）。\ndef findDuplicates(nums): if not nums: return [] res=[] n = len(nums) # 1\u0026lt;=num\u0026lt;=n 遍历到 num 则令第 num 个元素变成-num for i in range(n): num=abs(nums[i]) # 如果第num个数字已经是负的 说明之前遇到过num 说明num出现两次 if nums[num-1]\u0026lt;0: res.append(num) else: nums[num-1]=-nums[num-1] return res 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"18 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/442_%E6%95%B0%E7%BB%84%E4%B8%AD%E9%87%8D%E5%A4%8D%E7%9A%84%E6%95%B0%E6%8D%AE/","section":"leetcode 题解","summary":"","title":"442_数组中重复的数据","type":"leetcode题解"},{"content":"类型：数组\n子集 💛 https://leetcode-cn.com/problems/subsets/\n❓ 给你一个整数数组 nums ，数组中的元素互不相同 。返回该数组所有可能的子集。\n💡 回溯法\n参见 回溯法\nclass Solution: def subsets(self, nums: List[int]) -\u0026gt; List[List[int]]: n = len(nums) track = [] ans = [] def trackback(start): ans.append(track[:]) for i in range(start, n): track.append(nums[i]) trackback(i + 1) track.pop() trackback(0) return ans 时间复杂度：O(n * 2^n)，一共 2^n 种状态，每种状态需要 O(n) 的时间来构造子集。\n空间复杂度：O(n)\n","date":"17 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/78_%E5%AD%90%E9%9B%86/","section":"leetcode 题解","summary":"","title":"78_子集","type":"leetcode题解"},{"content":"类型：数组\n不同路径 💛 https://leetcode-cn.com/problems/unique-paths/\n❓ 一个机器人位于一个 m x n 网格的左上角。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角。问总共有多少条不同的路径？\n💡 动态规划\n位置 (i, j) 要么是从 (i - 1, j) 过来的，要么是从 (i, j - 1) 过来的。\n转移方程：dp[i][j] = dp[i - 1][j] + dp[i][j - 1]\nclass Solution: def uniquePaths(self, m: int, n: int) -\u0026gt; int: dp = [[1] * n for _ in range(m)] dp[0][0] = 1 for i in range(m): for j in range(n): if i == 0 and j == 0: continue dp[i][j] = (dp[i - 1][j] if i \u0026gt;= 1 else 0) + (dp[i][j - 1] if j \u0026gt;= 1 else 0) return dp[-1][-1] 时间复杂度：O(mn)，空间复杂度：O(mn)\n","date":"16 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/62_%E4%B8%8D%E5%90%8C%E8%B7%AF%E5%BE%84/","section":"leetcode 题解","summary":"","title":"62_不同路径","type":"leetcode题解"},{"content":"类型：数组\n跳跃游戏 II 💛 ⭐ https://leetcode-cn.com/problems/jump-game-ii/\n❓ 数组中的元素表示你在该位置可以跳跃的最大长度，问从数组首最少跳跃几次可到达数组尾。\n💡 贪心\n我们从数组首出发，维护当前能够到达的最大下标位置，记为边界。遍历边界内的元素，找到最远的跳跃距离(下标+跳跃长度)，更新为新的边界。每次更新边界都是一次跳跃，跳跃次数加一。我们不需要遍历到最后一个元素，到倒数第二个就行了。因为我们计算的是「起跳」的次数，我们并不会在最后一个元素起跳。\nclass Solution: def jump(self, nums: List[int]) -\u0026gt; int: n = len(nums) maxPos, end, step = 0, 0, 0 for i in range(n - 1): if maxPos \u0026gt;= i: maxPos = max(maxPos, i + nums[i]) if i == end: end = maxPos step += 1 return step 时间复杂度：O(N)，空间复杂度：O(1)\n","date":"15 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/45_%E8%B7%B3%E8%B7%83%E6%B8%B8%E6%88%8F_ii/","section":"leetcode 题解","summary":"","title":"45_跳跃游戏_II","type":"leetcode题解"},{"content":"类型：链表\n两个链表的第一个公共节点 💚\nhttps://leetcode-cn.com/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/\n❓ 输入两个链表头结点，找出它们的第一个公共结点。\n如图，c1 是它们的第一个公共结点。\n💡 双指针\n两个指针。一个从 A 出发，到了尾部之后接续到 B。一个从 B 出发，到了尾部之后接续到 A。\nclass Solution: def getIntersectionNode(self, headA: ListNode, headB: ListNode) -\u0026gt; ListNode: nodeA, nodeB = headA, headB while nodeA != nodeB: nodeA = nodeA.next if nodeA else headB nodeB = nodeB.next if nodeB else headA return nodeA 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"14 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/%E5%89%91%E6%8C%87_offer_52_%E4%B8%A4%E4%B8%AA%E9%93%BE%E8%A1%A8%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E5%85%AC%E5%85%B1%E8%8A%82%E7%82%B9/","section":"leetcode 题解","summary":"","title":"52_两个链表的第一个公共节点","type":"leetcode题解"},{"content":"类型：字符串\n用 Read4 读取 N 个字符 💚 https://leetcode-cn.com/problems/read-n-characters-given-read4/\n❓ 给你一个文件，并且该文件只能通过给定的 read4 方法来读取，请实现一个方法使其能够读取 n 个字符。API read4 可以从文件中读取 4 个连续的字符，并且将它们写入缓存数组 buf 中。\n💡 迭代\nclass Solution: def read(self, buf, n): index = -1 while n \u0026gt; 0: buf4 = [\u0026#39; \u0026#39;] * 4 count = read4(buf4) for i in range(min(count, n)): index += 1 buf[index] = buf4[i] n -= 1 if count \u0026lt; 4: return index + 1 return index + 1 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"13 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/157_%E7%94%A8_read4_%E8%AF%BB%E5%8F%96_n_%E4%B8%AA%E5%AD%97%E7%AC%A6/","section":"leetcode 题解","summary":"","title":"157_用_Read4_读取_N_个字符","type":"leetcode题解"},{"content":"类型：数组\n删除有序数组中的重复项 💚 https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/\n❓ 给你一个有序数组 nums ，请你 原地 删除重复出现的元素，使每个元素 只出现一次 ，返回删除后数组的新长度。\n💡 快慢指针遍历交换\n有点像快排里面的双指针 partition。\nclass Solution: def removeDuplicates(self, nums: List[int]) -\u0026gt; int: if not nums: return 0 n = len(nums) fast = slow = 1 while fast \u0026lt; n: if nums[fast] != nums[fast - 1]: nums[slow] = nums[fast] slow += 1 fast += 1 return slow 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"12 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/26_%E5%88%A0%E9%99%A4%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E9%A1%B9/","section":"leetcode 题解","summary":"","title":"26_删除有序数组中的重复项","type":"leetcode题解"},{"content":"类型：数组\n旋转图像 💛 https://leetcode-cn.com/problems/rotate-image/\n❓ 把 n x n 的二维矩阵 matrix 顺时针旋转 90 度。必须在原地修改。\n💡 推演\n从最外延到最中心，逐渐进行四角替换。\nclass Solution: def rotate(self, matrix: List[List[int]]) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; Do not return anything, modify matrix in-place instead. \u0026#34;\u0026#34;\u0026#34; n = len(matrix) for i in range(n // 2): for j in range(i, n - i - 1): temp = matrix[i][j] for _ in range(4): matrix[j][n - 1 - i] , temp = temp, matrix[j][n - 1 - i] i, j = j, n - 1 -i 时间复杂度：O(n^2)，空间复杂度：O(1)\n","date":"11 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/48_%E6%97%8B%E8%BD%AC%E5%9B%BE%E5%83%8F/","section":"leetcode 题解","summary":"","title":"48_旋转图像","type":"leetcode题解"},{"content":"类型：数组\n两数之和 II - 输入有序数组 💚 https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/\n❓ 从升序排列的数组 nums 中找出两个数相加等于 target。\n💡 双指针\nclass Solution: def twoSum(self, numbers: List[int], target: int) -\u0026gt; List[int]: low, high = 0, len(numbers) - 1 while low \u0026lt; high: total = numbers[low] + numbers[high] if total == target: return [low + 1, high + 1] elif total \u0026lt; target: low += 1 else: high -= 1 return [-1, -1] 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"10 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/167_%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C_ii_-_%E8%BE%93%E5%85%A5%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84/","section":"leetcode 题解","summary":"","title":"167_两数之和_II_-_输入有序数组","type":"leetcode题解"},{"content":"类型：数组\n合并区间 💛 https://leetcode-cn.com/problems/merge-intervals/\n❓ 数组 intervals 表示若干个区间的合集，intervals[i] = [start, end]，请合并所有重叠区间。\n💡 排序后合并\nclass Solution: def merge(self, intervals: List[List[int]]) -\u0026gt; List[List[int]]: if not intervals: return [] intervals.sort() ans = [] for [start, end] in intervals: if not ans or start \u0026gt; ans[-1][1]: ans.append([start, end]) else: ans[-1][1] = max(ans[-1][1], end) return ans 时间复杂度：O(N*logN)，快排的时间开销\n空间复杂度：O(N)\n","date":"9 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/56_%E5%90%88%E5%B9%B6%E5%8C%BA%E9%97%B4/","section":"leetcode 题解","summary":"","title":"56_合并区间","type":"leetcode题解"},{"content":"类型：树\n从上到下打印二叉树 💛\nhttps://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-lcof/\n❓ 从上到下打印出二叉树的每个节点，同一层的节点按照从左到右的顺序打印。\n💡 BFS\n就是一个普通的 BFS 问题。\nclass Solution: def levelOrder(self, root: TreeNode) -\u0026gt; List[int]: if not root: return [] ans = [] queue = [root] while queue: node = queue.pop(0) ans.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) return ans 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"8 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/%E5%89%91%E6%8C%87_offer_32_i_%E4%BB%8E%E4%B8%8A%E5%88%B0%E4%B8%8B%E6%89%93%E5%8D%B0%E4%BA%8C%E5%8F%89%E6%A0%91/","section":"leetcode 题解","summary":"","title":"32_I_从上到下打印二叉树","type":"leetcode题解"},{"content":"类型：链表\n从尾到头打印链表 💚\nhttps://leetcode-cn.com/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/\n❓ 输入一个链表的头节点，从尾到头反过来返回每个节点的值（用数组返回）。\n💡 递归\nclass Solution: def reversePrint(self, head: ListNode) -\u0026gt; List[int]: if not head: return [] ans = [] def rec(node): if not node: return rec(node.next) ans.append(node.val) rec(head) return ans 时间复杂度：O(n)，空间复杂度：O(n)\n💡 栈\n利用先进后出的特性。\n时间复杂度：O(n)，空间复杂度：O(n)\n","date":"7 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/%E5%89%91%E6%8C%87_offer_06_%E4%BB%8E%E5%B0%BE%E5%88%B0%E5%A4%B4%E6%89%93%E5%8D%B0%E9%93%BE%E8%A1%A8/","section":"leetcode 题解","summary":"","title":"06_从尾到头打印链表","type":"leetcode题解"},{"content":"类型：树\n找树左下角的值 💛 https://leetcode-cn.com/problems/find-bottom-left-tree-value/\n❓ 给定一棵树，返回树最底层最左侧的值。\n💡 层序遍历\nclass Solution: def findBottomLeftValue(self, root: TreeNode) -\u0026gt; int: if not root: return None queue = [root] ret = root.val while queue: ret = queue[0].val arr = queue queue = [] for node in arr: if node.left: queue.append(node.left) if node.right: queue.append(node.right) return ret 时间复杂度：O(n)，空间复杂度：O(w)\n","date":"6 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/513_%E6%89%BE%E6%A0%91%E5%B7%A6%E4%B8%8B%E8%A7%92%E7%9A%84%E5%80%BC/","section":"leetcode 题解","summary":"","title":"513_找树左下角的值","type":"leetcode题解"},{"content":"类型：数组\n买卖股票的最佳时机 💚 https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/\n❓ 数组 prices 表示股票在每天的价格，只能在某一天买入，某一天卖出。求最大收益。\n💡 一次遍历\n我们遍历的数组，考虑在当天卖出股票可获得的最大收益，为 prices[i] - minPrice.\n因此我们只需要遍历一次数组，并实时维护当前历史最低价。\nclass Solution: def maxProfit(self, prices: List[int]) -\u0026gt; int: inf = int(1e9) minprice = inf maxprofit = 0 for price in prices: maxprofit = max(price - minprice, maxprofit) minprice = min(price, minprice) return maxprofit 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"5 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/121_%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA/","section":"leetcode 题解","summary":"","title":"121_买卖股票的最佳时机","type":"leetcode题解"},{"content":"类型：图\n找出星型图的中心节点 💛 https://leetcode-cn.com/problems/find-center-of-star-graph/\n❓ 给你一个无向星形图。中间一个结点，周边有 n-1 个结点跟它相连。用边集数组存储。\n请你找出最中心的那个结点。\n输入：edges = 1,2 输出：2\n💡 直接返回\n由于所有的结点都跟中心结点。因此我们只要从前两个边中找出共同结点就行了。\nclass Solution: def findCenter(self, edges: List[List[int]]) -\u0026gt; int: if edges[0][0] == edges[1][0] or edges[0][0] == edges[1][1]: return edges[0][0] else: return edges[0][1] 时间复杂度：O(1)，空间复杂度：O(1)\n","date":"4 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1791_%E6%89%BE%E5%87%BA%E6%98%9F%E5%9E%8B%E5%9B%BE%E7%9A%84%E4%B8%AD%E5%BF%83%E8%8A%82%E7%82%B9/","section":"leetcode 题解","summary":"","title":"1791_找出星型图的中心节点","type":"leetcode题解"},{"content":"类型：数组\n数组大小减半 💛 https://leetcode-cn.com/problems/reduce-array-size-to-the-half/\n❓ 给你一个整数数组 nums，选定一个最小集合，至少可以囊括数组中一半的元素。返回集合大小。\n💡 哈希表\n统计各个数字出现的次数，存入哈希表。降序排序后，从哈希表中优先取出出现次数多的元素，直到过半。\nclass Solution: def minSetSize(self, arr: List[int]) -\u0026gt; int: freq = collections.Counter(arr) cnt, ans = 0, 0 for num, occ in freq.most_common(): cnt += occ ans += 1 if cnt * 2 \u0026gt;= len(arr): break return ans 时间复杂度：O(NlogN)，空间复杂度：O(n)\n","date":"3 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1338_%E6%95%B0%E7%BB%84%E5%A4%A7%E5%B0%8F%E5%87%8F%E5%8D%8A/","section":"leetcode 题解","summary":"","title":"1338_数组大小减半","type":"leetcode题解"},{"content":"类型：链表\n删除排序链表中的重复元素 💚 https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/\n❓ 给定一个升序链表，删除重复元素，使得每项重复元素只保留其中一个。不得破坏升序结构。\n💡 一次遍历\nclass Solution: def deleteDuplicates(self, head: ListNode) -\u0026gt; ListNode: if not head: return head cur = head while cur.next: if cur.val == cur.next.val: cur.next = cur.next.next else: cur = cur.next return head 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"2 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/83_%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0/","section":"leetcode 题解","summary":"","title":"83_删除排序链表中的重复元素","type":"leetcode题解"},{"content":"类型：树\n路径总和 💚 https://leetcode-cn.com/problems/path-sum/\n❓ 判断树中是否存在 根节点到叶子节点 的路径，路径上节点值相加等于目标和 targetSum。\n💡 递归\n假定从根节点到当前节点的值之和为 val，我们可以将这个大问题转化为一个小问题：是否存在从当前节点的子节点到叶子的路径，满足其路径和为 sum - val。\nclass Solution: def hasPathSum(self, root: TreeNode, sum: int) -\u0026gt; bool: if not root: return False if not root.left and not root.right: return sum == root.val return self.hasPathSum(root.left, sum - root.val) or self.hasPathSum(root.right, sum - root.val) 时间复杂度：O(n)。空间复杂度：O(h)，h 指树的高度。\n","date":"1 November 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/112_%E8%B7%AF%E5%BE%84%E6%80%BB%E5%92%8C/","section":"leetcode 题解","summary":"","title":"112_路径总和","type":"leetcode题解"},{"content":"类型：数组\n杨辉三角 💚 https://leetcode-cn.com/problems/pascals-triangle/\n❓ 生成杨辉三角的前 n 行\n💡 模拟\nclass Solution: def generate(self, numRows: int) -\u0026gt; List[List[int]]: ret = list() for i in range(numRows): row = list() for j in range(0, i + 1): if j == 0 or j == i: row.append(1) else: row.append(ret[i - 1][j] + ret[i - 1][j - 1]) ret.append(row) return ret 时间复杂度：O(n^2)，空间复杂度：O(1)\n","date":"31 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/118_%E6%9D%A8%E8%BE%89%E4%B8%89%E8%A7%92/","section":"leetcode 题解","summary":"","title":"118_杨辉三角","type":"leetcode题解"},{"content":"类型：数组\n最长连续递增序列 💚 https://leetcode-cn.com/problems/longest-continuous-increasing-subsequence/\n❓ 找出未排序数组 nums 的最长连续递增子序列，返回序列长度。\n💡 贪心\nclass Solution: def findLengthOfLCIS(self, nums: List[int]) -\u0026gt; int: ans = 0 n = len(nums) start = 0 for i in range(n): if i \u0026gt; 0 and nums[i] \u0026lt;= nums[i - 1]: start = i ans = max(ans, i - start + 1) return ans 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"30 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/674_%E6%9C%80%E9%95%BF%E8%BF%9E%E7%BB%AD%E9%80%92%E5%A2%9E%E5%BA%8F%E5%88%97/","section":"leetcode 题解","summary":"","title":"674_最长连续递增序列","type":"leetcode题解"},{"content":"类型：数组\n加一 💚 https://leetcode-cn.com/problems/plus-one/\n❓ 一个由整数构成的非空数组用于表示非负整数。返回该数加一后的数组表示。\n💡 模拟\n从后往前加，如果有进位的话就继续往前进，没有进位的话就可以终止了。如果进到最高位还有进位的话，前面要还要加个 1.\nclass Solution: def plusOne(self, digits: List[int]) -\u0026gt; List[int]: for i in range(len(digits) - 1, -1, -1): if digits[i] + 1 \u0026gt;= 10: digits[i] = 0 else: digits[i] += 1 return digits digits = [1] + digits return digits 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"29 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/66_%E5%8A%A0%E4%B8%80/","section":"leetcode 题解","summary":"","title":"66_加一","type":"leetcode题解"},{"content":"类型：数组\n最少操作使数组递增 💚 https://leetcode-cn.com/problems/minimum-operations-to-make-the-array-increasing/\n❓ 对于数组 nums，每次操作可以使数组中的某个元素加 1，求使数组变为严格递增最少操作次数。\n💡 遍历模拟\nclass Solution: def minOperations(self, nums: List[int]) -\u0026gt; int: count = 0 for i in range(1, len(nums)): if nums[i] \u0026lt;= nums[i - 1]: count += nums[i - 1] + 1 - nums[i] nums[i] = nums[i - 1] + 1 return count 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"28 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1827_%E6%9C%80%E5%B0%91%E6%93%8D%E4%BD%9C%E4%BD%BF%E6%95%B0%E7%BB%84%E9%80%92%E5%A2%9E/","section":"leetcode 题解","summary":"","title":"1827_最少操作使数组递增","type":"leetcode题解"},{"content":"类型：数组\n转置矩阵 💚 https://leetcode-cn.com/problems/transpose-matrix/\n❓ 给你一个二维整数数组 matrix， 返回 matrix 的 转置矩阵 。矩阵的 转置 是指将矩阵的主对角线翻转，交换矩阵的行索引与列索引。\n💡 模拟\nclass Solution: def transpose(self, matrix: List[List[int]]) -\u0026gt; List[List[int]]: m, n = len(matrix), len(matrix[0]) transposed = [[0] * m for _ in range(n)] for i in range(m): for j in range(n): transposed[j][i] = matrix[i][j] return transposed 时间复杂度：O(mn)，空间复杂度：O(1)\n","date":"27 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/867_%E8%BD%AC%E7%BD%AE%E7%9F%A9%E9%98%B5/","section":"leetcode 题解","summary":"","title":"867_转置矩阵","type":"leetcode题解"},{"content":"类型：字符串\n反转字符串 💚 https://leetcode-cn.com/problems/reverse-string/\n❓ 原地翻转字符串。\n💡 双指针\nclass Solution: def reverseString(self, s: List[str]) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; Do not return anything, modify s in-place instead. \u0026#34;\u0026#34;\u0026#34; length = len(s) for i in range(length // 2): if s[i] == s[length - 1 - i]: continue s[i], s[length - 1 - i] = s[length - 1 - i], s[i] 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"26 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/344_%E5%8F%8D%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2/","section":"leetcode 题解","summary":"","title":"344_反转字符串","type":"leetcode题解"},{"content":"类型：树\n相同的树 💚 https://leetcode-cn.com/problems/same-tree/\n❓ 判断两棵树是否相同。\n💡 递归\nclass Solution: def isSameTree(self, p: TreeNode, q: TreeNode) -\u0026gt; bool: if not p and not q: return True elif not p or not q: return False elif p.val != q.val: return False else: return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right) 时间复杂度：O(min(m, n))，空间复杂度：O(min(m, n))\n","date":"25 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/100_%E7%9B%B8%E5%90%8C%E7%9A%84%E6%A0%91/","section":"leetcode 题解","summary":"","title":"100_相同的树","type":"leetcode题解"},{"content":"类型：字符串\n最后一个单词的长度 💚 https://leetcode-cn.com/problems/length-of-last-word/\n❓ 字符串 s 由若干个单词组成，单词间用空格分割，返回最后一个单词的长度。\n💡 逆向一次遍历\n由于我们要算最后一个单词的长度，直接从后往前遍历好了。\nclass Solution: def lengthOfLastWord(self, s: str) -\u0026gt; int: count = 0 for i in range(len(s) - 1, -1, -1): if s[i] != \u0026#39; \u0026#39;: count += 1 elif count \u0026gt; 0: return count return count 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"24 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/58_%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E5%8D%95%E8%AF%8D%E7%9A%84%E9%95%BF%E5%BA%A6/","section":"leetcode 题解","summary":"","title":"58_最后一个单词的长度","type":"leetcode题解"},{"content":"类型：字符串\n判断句子是否为全字母句 💚 https://leetcode-cn.com/problems/check-if-the-sentence-is-pangram/\n❓ 全字母句 指包含英语字母表中每个字母至少一次的句子。给你一个仅由小写英文字母组成的字符串 sentence ，请你判断 sentence 是否为 全字母句 。\n💡 一次遍历\nclass Solution: def checkIfPangram(self, sentence: str) -\u0026gt; bool: se = set() for c in sentence: se.add(c) if len(se) == 26: return True return len(se) == 26 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"23 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1832_%E5%88%A4%E6%96%AD%E5%8F%A5%E5%AD%90%E6%98%AF%E5%90%A6%E4%B8%BA%E5%85%A8%E5%AD%97%E6%AF%8D%E5%8F%A5/","section":"leetcode 题解","summary":"","title":"1832_判断句子是否为全字母句","type":"leetcode题解"},{"content":"类型：字符串\n所有元音按顺序排布的最长子字符串 💛 https://leetcode-cn.com/problems/longest-substring-of-all-vowels-in-order/\n❓ 当一个字符串满足如下条件时，我们称它是 美丽的 ：\n所有 5 个英文元音字母（\u0026lsquo;a\u0026rsquo; ，\u0026rsquo;e\u0026rsquo; ，\u0026lsquo;i\u0026rsquo; ，\u0026lsquo;o\u0026rsquo; ，\u0026lsquo;u\u0026rsquo;）都必须 至少 出现一次。 这些元音字母的顺序都必须按照 字典序 升序排布（也就是说所有的 \u0026lsquo;a\u0026rsquo; 都在 \u0026rsquo;e\u0026rsquo; 前面，所有的 \u0026rsquo;e\u0026rsquo; 都在 \u0026lsquo;i\u0026rsquo; 前面，以此类推） 比方说，字符串 \u0026ldquo;aeiou\u0026rdquo; 和 \u0026ldquo;aaaaaaeiiiioou\u0026rdquo; 都是 美丽的 ，但是 \u0026ldquo;uaeio\u0026rdquo; ，\u0026ldquo;aeoiu\u0026rdquo; 和 \u0026ldquo;aaaeeeooo\u0026rdquo; 不是美丽的 。\n给你一个只包含英文元音字母的字符串 word ，请你返回 word 中 最长美丽子字符串的长度 。如果不存在这样的子字符串，请返回 0 。\n💡 状态机\n状态机问题，兴趣不大，还是看题解吧。\n","date":"22 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1839_%E6%89%80%E6%9C%89%E5%85%83%E9%9F%B3%E6%8C%89%E9%A1%BA%E5%BA%8F%E6%8E%92%E5%B8%83%E7%9A%84%E6%9C%80%E9%95%BF%E5%AD%90%E5%AD%97%E7%AC%A6%E4%B8%B2/","section":"leetcode 题解","summary":"","title":"1839_所有元音按顺序排布的最长子字符串","type":"leetcode题解"},{"content":"类型：数组\n雪糕的最大数量 💛 https://leetcode-cn.com/problems/maximum-ice-cream-bars/\n❓ 数组 costs 表示每支雪糕的价格，一共有现金 coins，问能买几个雪糕。\n💡 排序\nclass Solution: def maxIceCream(self, costs: List[int], coins: int) -\u0026gt; int: costs.sort() count = 0 for c in costs: if coins \u0026lt; c: return count coins -= c count += 1 return count 时间复杂度：O(NlogN)，空间复杂度：O(1)\n","date":"21 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1833_%E9%9B%AA%E7%B3%95%E7%9A%84%E6%9C%80%E5%A4%A7%E6%95%B0%E9%87%8F/","section":"leetcode 题解","summary":"","title":"1833_雪糕的最大数量","type":"leetcode题解"},{"content":"类型：数组\n爱生气的书店老板 💛 ⭐ https://leetcode-cn.com/problems/grumpy-bookstore-owner/\n❓ 数组中每分钟的在场顾客数为 customers[i]，grumpy[i] 表示老板在第 i 分钟会不会生气。生气时顾客不满意，否则顾客满意。老板可以通过情绪控制让自己保持连续 X 分钟不生气，但只能控制一次。如何让最多的顾客感到满意？\n💡 滑动窗口\n假设老板不进行情绪控制，则顾客满意数为 init_customer_num。假设老板情绪控制后，增长的满意数为 increase，我们要求 increase 的最大值。\n我们从头遍历到 n-X，更新维护 increase 的值：\nincrease_i = increase[i - 1] - customers[i - 1] * grumpy[i - 1] + customers[i + X - 1] * grumpy[i + X - 1]\n时间复杂度：O(n)，空间复杂度：O(1)\n","date":"20 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1052_%E7%88%B1%E7%94%9F%E6%B0%94%E7%9A%84%E4%B9%A6%E5%BA%97%E8%80%81%E6%9D%BF/","section":"leetcode 题解","summary":"","title":"1052_爱生气的书店老板","type":"leetcode题解"},{"content":"类型：字符串\n翻转游戏 💚 https://leetcode-cn.com/problems/flip-game/\n❓ 对于一个只包含 \u0026lsquo;+\u0026rsquo;、\u0026rsquo;-\u0026rsquo; 符号的字符串，一次有效的翻转操作是指将两个连续的 \u0026lsquo;++\u0026rsquo; 翻转为 \u0026lsquo;\u0026ndash;\u0026rsquo;。现给定字符串，求对其进行一次翻转操作后所有可能的结果。\n💡 一次遍历\nclass Solution: def generatePossibleNextMoves(self, s: str) -\u0026gt; List[str]: output = [] for i in range(len(s) - 1): if s[i] == \u0026#39;+\u0026#39; and s[i + 1] == \u0026#39;+\u0026#39;: output.append(s[0: i] + \u0026#34;--\u0026#34; + s[i + 2:]) return output 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"19 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/293_%E7%BF%BB%E8%BD%AC%E6%B8%B8%E6%88%8F/","section":"leetcode 题解","summary":"","title":"293_翻转游戏","type":"leetcode题解"},{"content":"类型：堆\n数组中的第K个最大元素 💛 https://leetcode-cn.com/problems/kth-largest-element-in-an-array/\n❓ 在数组中找到第 k 大的元素。\n💡 堆\n这是一个典型的堆题。\nclass Solution: def findKthLargest(self, nums: List[int], k: int) -\u0026gt; int: h = [] for num in nums: if len(h) \u0026lt; k or num \u0026gt; h[0]: heapq.heappush(h, num) if len(h) \u0026gt; k: heapq.heappop(h) return h[0] ","date":"18 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/215_%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E7%AC%ACk%E4%B8%AA%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0/","section":"leetcode 题解","summary":"","title":"215_数组中的第K个最大元素","type":"leetcode题解"},{"content":"类型：树\n翻转二叉树 💚 https://leetcode-cn.com/problems/invert-binary-tree/\n❓ 翻转一个二叉树。\n💡 递归\n递归后序处理。\nclass Solution: def invertTree(self, root: TreeNode) -\u0026gt; TreeNode: if not root: return root left = self.invertTree(root.left) right = self.invertTree(root.right) root.left, root.right = right, left return root 时间复杂度：O(n)，空间复杂度：O(n)\n","date":"17 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/226_%E7%BF%BB%E8%BD%AC%E4%BA%8C%E5%8F%89%E6%A0%91/","section":"leetcode 题解","summary":"","title":"226_翻转二叉树","type":"leetcode题解"},{"content":"类型：链表\n删除链表中的节点 💚 https://leetcode-cn.com/problems/delete-node-in-a-linked-list/\n❓ 请编写一个函数，使其可以删除某个链表中给定的（非末尾）节点。传入函数的唯一参数为 要被删除的节点 。\n💡 与下一个结点交换\n此题的关键在于函数没有给你传入链表头结点，而是只传入要删除的结点。\n因此我们只能把下一个结点的值赋给本结点，转而把下一个结点删除。\nclass Solution: def deleteNode(self, node): node.val = node.next.val node.next = node.next.next 时间复杂度：O(1)，空间复杂度：O(1)\n","date":"16 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/237_%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9/","section":"leetcode 题解","summary":"","title":"237_删除链表中的节点","type":"leetcode题解"},{"content":"类型：数组\n斐波那契数 💚 https://leetcode-cn.com/problems/fibonacci-number/\n❓ 计算斐波那契数组的第 n 个元素。\n💡 滑动窗口\nclass Solution: def fib(self, n: int) -\u0026gt; int: if n \u0026lt; 2: return n arr = [0, 1] for i in range(2, n + 1): val = sum(arr) arr[0], arr[1] = arr[1], val return arr[-1] 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"15 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/509_%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0/","section":"leetcode 题解","summary":"","title":"509_斐波那契数","type":"leetcode题解"},{"content":"类型：数组\n一维数组的动态和 💚 https://leetcode-cn.com/problems/running-sum-of-1d-array/\n❓ 动态和是指前 i 项之和。返回一个数组的动态和数组。\n💡 累加\nclass Solution: def runningSum(self, nums: List[int]) -\u0026gt; List[int]: ans = [] pre_sum = 0 for n in nums: pre_sum += n ans.append(pre_sum) return ans 时间复杂度：O(n)，空间复杂度：O(1)\n","date":"14 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1480_%E4%B8%80%E7%BB%B4%E6%95%B0%E7%BB%84%E7%9A%84%E5%8A%A8%E6%80%81%E5%92%8C/","section":"leetcode 题解","summary":"","title":"1480_一维数组的动态和","type":"leetcode题解"},{"content":"类型：字符串\n字符串转换整数 (atoi) 💛 https://leetcode-cn.com/problems/string-to-integer-atoi/\n❓ 实现 myAtoi(string s) 函数，其能将字符串转换成一个 32 位有符号整数（类似 C/C++ 中的 atoi 函数）。\n💡 状态机\n时间复杂度：O(n)，空间复杂度：O(1)\n","date":"13 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/8_%E5%AD%97%E7%AC%A6%E4%B8%B2%E8%BD%AC%E6%8D%A2%E6%95%B4%E6%95%B0_atoi/","section":"leetcode 题解","summary":"","title":"8_字符串转换整数_(atoi)","type":"leetcode题解"},{"content":"类型：数组\n三数之和 💛 https://leetcode-cn.com/problems/3sum/\n❓ 判断数组 nums 是否存在三个数 a + b + c = 0，找出所有的不重复三元组。\n💡 排序 + 双指针\n由于不需要返回下标，我们可以对数组进行排序。\n双重循环枚举前两个元素，第三个元素通过双指针在第二重循环枚举。\n时间复杂度：O(N^2)，空间复杂度：O(logN)\n","date":"12 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/15_%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/","section":"leetcode 题解","summary":"","title":"15_三数之和","type":"leetcode题解"},{"content":"类型：数组\n两数之和 💚 https://leetcode-cn.com/problems/two-sum/\n❓ 题目\n在数组 nums 中找出两个数，使其和为 target，返回二者下标。\n💡 哈希\n一次遍历即可\n时间复杂度：O(N)，空间复杂度：O(N)\n","date":"11 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1_%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C/","section":"leetcode 题解","summary":"","title":"1_两数之和","type":"leetcode题解"},{"content":"类型：字符串\n不同字符的最小子序列 https://leetcode-cn.com/problems/smallest-subsequence-of-distinct-characters/\n同：316. 去除重复字母\n","date":"10 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1081_%E4%B8%8D%E5%90%8C%E5%AD%97%E7%AC%A6%E7%9A%84%E6%9C%80%E5%B0%8F%E5%AD%90%E5%BA%8F%E5%88%97/","section":"leetcode 题解","summary":"","title":"1081_不同字符的最小子序列","type":"leetcode题解"},{"content":"类型：树\n二叉树的最近公共祖先 💚\nhttps://leetcode-cn.com/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof/\n同：236. 二叉树的最近公共祖先\n","date":"9 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/%E5%89%91%E6%8C%87_offer_68_ii_%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88/","section":"leetcode 题解","summary":"","title":"68_II_二叉树的最近公共祖先","type":"leetcode题解"},{"content":"类型：链表\n相交链表 💛 ⭐ https://leetcode-cn.com/problems/intersection-of-two-linked-lists/\n同：剑指 Offer 52. 两个链表的第一个公共节点\n","date":"8 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/160_%E7%9B%B8%E4%BA%A4%E9%93%BE%E8%A1%A8/","section":"leetcode 题解","summary":"","title":"160_相交链表","type":"leetcode题解"},{"content":"类型：数组\n存在重复元素 💚 https://leetcode-cn.com/problems/contains-duplicate/\n❓ 判断数组 nums 是否存在重复元素。\n💡 排序\n💡 哈希\n","date":"7 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/217_%E5%AD%98%E5%9C%A8%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0/","section":"leetcode 题解","summary":"","title":"217_存在重复元素","type":"leetcode题解"},{"content":"类型：数组\n杨辉三角 II 💚 https://leetcode-cn.com/problems/pascals-triangle-ii/\n❓ 返回杨辉三角的第 k 行。\n同：118. 杨辉三角\n","date":"6 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/119_%E6%9D%A8%E8%BE%89%E4%B8%89%E8%A7%92_ii/","section":"leetcode 题解","summary":"","title":"119_杨辉三角_II","type":"leetcode题解"},{"content":"类型：链表\n对链表进行插入排序 💛 https://leetcode-cn.com/problems/insertion-sort-list/\n同：148. 排序链表\n","date":"5 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/147_%E5%AF%B9%E9%93%BE%E8%A1%A8%E8%BF%9B%E8%A1%8C%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F/","section":"leetcode 题解","summary":"","title":"147_对链表进行插入排序","type":"leetcode题解"},{"content":"类型：数组\n主要元素 💚\nhttps://leetcode-cn.com/problems/find-majority-element-lcci/\n同：169.多数元素\n","date":"4 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/%E9%9D%A2%E8%AF%95%E9%A2%98_1710_%E4%B8%BB%E8%A6%81%E5%85%83%E7%B4%A0/","section":"leetcode 题解","summary":"","title":"1710_主要元素","type":"leetcode题解"},{"content":"类型：链表\n反转链表 💚 ⭐\nhttps://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/\n同：206. 反转链表\n","date":"3 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/%E5%89%91%E6%8C%87_offer_24_%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/","section":"leetcode 题解","summary":"","title":"24_反转链表","type":"leetcode题解"},{"content":"类型：堆\n数组的最小偏移量 ❤️ https://leetcode-cn.com/problems/minimize-deviation-in-array/\n","date":"2 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/1675_%E6%95%B0%E7%BB%84%E7%9A%84%E6%9C%80%E5%B0%8F%E5%81%8F%E7%A7%BB%E9%87%8F/","section":"leetcode 题解","summary":"","title":"1675_数组的最小偏移量","type":"leetcode题解"},{"content":"类型：树\n路径总和 II 💛 https://leetcode-cn.com/problems/path-sum-ii/\n同：剑指 Offer 34. 二叉树中和为某一值的路径\n","date":"1 October 2020","externalUrl":null,"permalink":"/leetcode%E9%A2%98%E8%A7%A3/113_%E8%B7%AF%E5%BE%84%E6%80%BB%E5%92%8C_ii/","section":"leetcode 题解","summary":"","title":"113_路径总和_II","type":"leetcode题解"},{"content":" Deep Learning # Kaggle\nhttps://www.kaggle.com/\nAI 界的 leetcode, Google 推出。交互式学习。\nNeural networks and deep learning\nhttp://neuralnetworksanddeeplearning.com/\n大神 Michael Nielsen 提供的详尽教程，从零实现神经网络，不依赖 tensorflow / pytorch 等框架！\n3Blue1Brown\nhttps://www.youtube.com/watch?v=aircAruvnKk\n油管大神 3Blue1Brown 深入浅出的视频讲解。参考了 Michael Nielsen 的内容。\nTensorflow tutorials\nhttps://www.tensorflow.org/tutorials\nCV 相关的内容挺不错。\nKeras Examples\nhttps://keras.io/examples\nKeras Examples 中包含很多案例，涵盖 CV、NLP 等等\nNLP # huggingface LLM course\nhttps://huggingface.co/learn/llm-course/\nKerbs Examples - NLP\nhttps://keras.io/examples/nlp/\ntensorflow - guide - word embeddings\nhttps://www.tensorflow.org/text/guide/word_embeddings\nembedding 可视化\nhttps://projector.tensorflow.org/\nComputer Graphics # Scratchapixel\nhttps://www.scratchapixel.com/\n开源在线书籍。真正的最底层原理，从人眼视觉成像原理，到矩阵运算，再到光栅和光追渲染。\nTinyRenderer\nhttps://github.com/ssloy/tinyrenderer/wiki\n掌握了上面的底层原理之后，可以开始手动实现软渲染器。这个项目是一个很好的参考，内容简洁，注重实践\nLearnOpenGL\nhttps://learnopengl.com/\n可以开始学习用 OpenGL 库了。\nThe Book of Shaders\nhttps://thebookofshaders.com/\n专门讲 GLSL 的在线书籍。内容系统性，且提供了基于 WebGL 的在线交互示例。\nWebGL showcase and playground\n以下再提供一些在线可交互的 WebGL 案例和 playground.\nhttps://shaderfrog.com/2/\n","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/ai%E5%92%8Ccg%E7%9A%84%E4%B8%80%E4%BA%9B%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99%E6%95%B4%E7%90%86/","section":"技术杂项","summary":"","title":"AI 和 CG 的一些学习资料整理","type":"技术杂项"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"goto 楔子\ngoto posts/楔子/\ngoto posts/楔子/\nThis is chapter 1.\nthis is the content.\n![Pasted image 20260722074348.png](/images/Pasted image 20260722074348.png)\n![Pasted image 20260722092234.png](/images/Pasted image 20260722092234.png)\n","externalUrl":null,"permalink":"/posts/chapter_1/","section":"Posts","summary":"","title":"Chapter_1","type":"posts"},{"content":" Chapter2: Blender “DNA” and Serialization # The blend file also provides a version’s “DNA” struct definitions called SDNA and information on pointersize and big- vs. little-endian byte order on the host machine that originally saved the file.\n(page: 26)\nSDNA is short for Structure DNA.\nTo serialize a blend file, Blender writes to a file a variable called DNAStr. DNAStr is a char array, which is the “DNA1” file-block of a blend file. DNAStr is defined in a file named dna.c and located in the makesdna mirrored directory of the source repository. In other words, dna.c is deposited within the build directory of the same name and relative path. The source file dna.c is not part of the repository source. It is generated from the executable makesdna, compiled and ran during the Blender build process from the file ource/blender/makesdna/intern/makesdna.c.\n(page: 32)\nThe header dna_rename_defs.h is interesting, in that the redefinition macros written there are expanded for the macro definition from the DNA_alias_maps() implementation, defined in dna_utils.c (see Listing 2-3). Conversions are made in the macro expansions for older fields and “DNA” struct names. These are added to hash maps for lookup during blend file loading of prior versions of the Blender executable.\n(page: 32)\nThis is how blender is compatible to old blender file.\nThe file dna_genfile.c provides functions for creating and accessing a SDNA struct, along with writing the “DNA1” file-block to a blend file. The dna_utils.c file provides low-level API functions.\n(page: 34)\nBefore we can appreciate this process, there are a couple of core Blender concepts to understand. First, the top-level data structure for maintaining Blender’s state is a struct named struct bContext. It is defined, appropriately, in source/blender/blenkernel/intern/context.c.\n(page: 35)\nThis struct contains two additional struct variables (wm and data), each with their own pointers to data structures describing UI (userinterface) elements and scene contents, respectively.\n(page: 36)\nThe data struct in struct bContext holds two other sub-structs: one of the struct Main type and another of struct Scene. The struct Main is defined in source/blender/blenkernel/BKE_main.h, whereas the struct Scene is defined in source/blender/makesdna/DNA_scene_types.h. While there is a vast amount of state stored in both of these data types, it is perhaps more instructive to consider only parts of these records with the most obvious information first. Broadly, scene objects are contained in struct Main, but information related to the scene view is stored in struct Scene (which further uses struct Camera and struct World to encapsulate data, i.e., both of these structs contain physics, audio, and background color settings).\n(page: 36)\nIn source/creator/creator.c’s main(), there are many calls to initialize various parts of the application. The first call directly related to the Blender’s DNA types is DNA_sdna_current_init(), defined in source/blender/makesdna/intern/dna_genfile.c. This function assigns an SDNA struct object to a global static variable called g_sdna. The g_sdna variable is accessible via the extern accessor functions of dna_genfile.c.\n(page: 38)\nSDNA (see Listing 2-6) is a meta-record, providing information about all of the potential serializable data for the version of Blender that is running. This struct is defined in source/blender/makesdna/DNA_sdna_types.h. It is the same SDNA that is written into a blend file.\n(page: 39)\nAfter having created an instance of the singleton SDNA object, we return back to main() and subsequently call BKE_blender_globals_init(). Another global struct, of type Global, is allocated a block of memory, the size of struct Main, and assigned to a pointer called main in Global. The allocation is performed by Blender’s memory allocation module source/intern/memutil, wrapping C library functions for memory management. This will be the struct Main to be filled out by reading either the char array of bytes representing the in-memory startup.blend for a “factory startup” or from a serialized binary data file (arbitrary blend file).\n(page: 40)\nThere is another executable binary called datatoc, not to be confused with the module datatoc, that outputs the source file startup.blend.c. The startup.blend.c file is generated at build time and therefore deposited in the build directory. Within startup.blend.c is a char array with the “factory” startup.blend file contents. The char array is called datatoc_startup_blend.\n(page: 41)\nUpon entering WM_init(), there are initializations, many of which set callback functions for the windowmanager module.\n(page: 41)\nFunctions that are prefaced with capital letters that abbreviate their respective modules form part of the module’s API (i.e., functional interface) for other Blender modules. For example, we have seen this with WM_init(), from the windowmanager API, and BLO_read_from_file(), etc., from the blenderloader module. Functions that maintain the same abbreviations (e.g., wm_homefile_read() or blo_filedata_from_memory()), but in lowercase, are not meant to be called from outside of the module itself.\n(page: 44)\nCTX_* are struct bContext accessor functions defined in source/blender/blendkernel/intern/context.c. context.c is located in the blendkernel module, and its interface is defined by BKE_context.h. However, because of the importance of the struct bContext type, Blender uses the CTX_* prefix instead of the BKE_* prefix on the API to the struct bContext type.\n(page: 53)\nChapter 3: ghost: Soul of the windowmanager Module # Generic Handy Operating System Toolkit (GHOST)\nWe will see that ghost directly interfaces with Xlib (X11), Mac OS, or MicroSoft Windows in order to create an application window. Native OS windows are the receivers of events from an operating system. Thus, ghost captures these events, and an application built upon ghost must then implement an appropriate handler routine.\n(page: 57)\nBoth GLUT and GLFW2 take over an application’s event loop, known as “inversion of control.” We have already seen in Chapter 1 that Blender’s event loop is in the windowmanager module (source/blender/windowmanager/intern/wm.c). As a comparison, GLFW is considered a “framework,” as the library itself runs the event loop. A client application using GLFW must register callback functions with it. In contrast, the windowmanager module maintains the event loop for Blender and is written on top of ghost.\n(page: 57)\nLike GLUT and GLFW, ghost deals directly with the platform’s API. An application using ghost only interacts with ghost, not the platform API. Thus, ghost makes Blender portable and simplifies the duties of the “core” codebase.\n(page: 59)\nghost began as an internal library, whose only dependencies were operating system APIs external to Blender’s repository.6 ghost’s internal dependencies are shown in Figure 3-2. Only “drag-and-drop” references source/blender/imbuf. The dependency arises from ghost/intern/GHOST_EventDragDrop.h, where both IMB_imbuf.h and IMB_imbuf_types.h\nare included.\n(page: 61)\nGLEW (Graphics Library Extension Wrangler) is an external open source library, used for OpenGL extension functions. The library gathers extensions for an application, as its name suggests. Blender’s glew-mx extends GLEW, supporting extensions for multiple rendering contexts. GLEW itself has limited support for this.\n(page: 62)\nAs the name implies, GHOST_ISystem is an abstract base class for an object-oriented representation of the operating system. We see its class diagram in Figure 3-5. GHOST_ISystem is instantiated as a singleton. It has a static factory member function called GHOST_ISystem::createSystem(), and a static accessor member function called GHOST_ISystem::getSystem(). Additionally, its constructor is protected, enforcing that only GHOST_ISystem::createSystem() be allowed to create an instance, when called from one of its child class (see Figure 3-5).\n(page: 64)\nGHOST_System inherits from GHOST_ISystem, an abstract class. GHOST_System therefore implements members found in concrete platform classes. Two examples are GHOST_System::getMilliSeconds() and GHOST_System::installTimer(). Figure 3-6 shows diagrams for two of these “system” classes: GHOST_SystemWin32 and GHOST_SystemX11. These represent Windows and X11 systems, respectively.\n(page: 65)\nBlender is written in C. However, ghost is a C++ library. Thus, Blender needs to interface ghost using a function-only interface. This is where intern/ghost/intern/GHOST_C-api.cpp and its associated header intern/ghost/GHOST_C-api.h are involved. They work as an adapter for the twoway C to C++ communication.\n(page: 67)\nIn C programs using ghost, handles are C struct pointers. In the C++ API implementation, these are cast to C++ object pointers. Parameters passed to the C API functions are transferred to the member functions of the corresponding C++ class.\n(page: 68)\nChapter 4: The blenlib and blenkernel Modules # blendlib handles generic utilities in Blender, while blenkernel is more specific to the Blender application. Therefore, blenlib’s functionality, at least in principle, could be used by other similar programs. However, blenlib is tightly coupled to the Blender codebase.\nblenkernel, on the other hand, offers a number of API functions for making needed manipulations on Blender “DNA,” and other statekeeping data structures.\n(page: 109)\nChapter 5: Blender’s Embedded Python # CPython is a C-based implementation of Python. It even contains an API. This allows CPython to be used as an external library and linked with a separate application written in C. Functions defined by an “embedding” program may be called via Python script, running on the embedded\ninterpreter.\n(page: 110)\nBecause Python is object-oriented, Blender creates “built-in” Python modules and classes, some of whose methods map to Blender’s internal C structs (as in the case of struct bContext) and internal module APIs, for example, the bmesh module. mathutils’s mathematical objects, such as vectors and matrices, are assigned a Python class. Math utility functions from blenlib become methods for these same Python classes.\n(page: 111)\nPyModule_New() and PyModule_Create() both are used to add an extension module. For instance, Blender’s Python mathutils module is added by PyModule_Create(), while its bpy module is via PyModule_New(). PyModule_New() was introduced in version 3.3 of the CPython, after PyModule_Create(). You will see both functions used for creating new\nmodules in Blender.\n(page: 115)\nPyModule_AddObject() adds a class to a module. For example, the mathutils.Vector is added as a built-in Python class using PyModule_AddObject(). PyImport_ExtendInittab() registers modules to be included in Python’s initialization table (i.e., Inittab from the function name). This table contains extension modules to be registered when Python is initialized.\n(page: 116)\nIn C, there is no explicit language mechanism for inheritance. By adding common fields to the beginning of a user-defined struct, we can treat these structs as having inherited those fields.\n(page: 116)\nPyObject_HEAD is expanded by the preprocessor into two fields. One field points to an object of PyTypeObject, the other a count of the object’s references.\n(page: 117)\nChapter 6: Blender “RNA” and the Data API # An additional aspect to the makesrna module is that multiple source files are generated during the build process. They are then fed back into the source code compiled as the runtime Blender executable. This means that makesrna is distinct from other Blender modules. It is implemented in this way, so that objects of “RNA” properties, that is, structs definitions with property data values such as initial settings, value ranges, and accessor function pointers, are not hand-coded.\n(page: 137)\nThe makesrna module has both a runtime and non-runtime portion. The non-runtime portion is dedicated to source code generation. An individual file in the repository’s makesrna module may include both runtime and non-runtime code. Runtime code becomes part of the Blender executable. Non-runtime code is compiled as part of the tools that generate runtime code.\n(page: 138)\nAs such, many of the source files in the repository are compiled twice, first for the non-runtime and then for the runtime after RNA_RUNTIME is defined by the generated source.\n(page: 139)\nrna_*_api.c functions implement callbacks triggered when an object is updated by the Data API, usually in response to values changed in the user interface or Python API call via an operator.\n(page: 142)\nChapter 7: The editors Module, Operators, and Event System # We will cover the connections between editors and the windowmanager module—as it is responsible for initializing operators and editors—as well as being the hub for events.\n(page: 173)\nThe editors module, put succinctly, contains the code that defines the different views and operations on Blender data. Blender’s graphical user interface (GUI) has a number of editor categories, for example, “General,” “Animation,” “Scripting,” and “Data.” A few editors are “3D Viewport,” “Image Editor,” “Text Editor,” “Outliner,” etc. Each editor provides a separate view, where each visualizes different parts of the Blender data or “DNA.” Editors also usually allow for changing (e.g., editing, deleting, or adding to) “DNA,” where upon their view is updated accordingly.\n(page: 173)\nThe space_outliner.c file contains the registration function, called from the windowmanager module. The callbacks (not the creation and initialization functions) are defined in the other files. This is the usual pattern for the other editors, unless the editor’s implementation is relatively short and entirely within a single space_*.c file, for example, the contents of space_topbar.\n(page: 176)\nThe SpaceLink struct is the “base” type for “concrete” editor types. Each SpaceLink allows “linking” into the linkedlist of all other editors in Main, using its next and prev fields. This is a convention in the Blender codebase.\n(page: 177)\nLooking at the SpaceLink variables, you can see a field named spacetype. The spacetype field stores a numerical code from 0 to 255, representing the type of editor. These values are defined as an enum, called eSpace_Type, defined in ource/blender/makesdna/DNA_space_types.h.\n(page: 178)\nThere is a tight coupling between the windowmanager and editors modules. All editors must live in a “space” (sometimes called a “Screen Area,” or more simply “area”) within a Blender application window.\nAt the highest level, all Blender windows are managed by the windowmanager module, as its title implies. We saw that the windowmanager module is a wrapper for GHOST, which is itself a wrapper for the underlying operating system’s windowing API.\n(page: 182)\nwmWindow represents a Blender window object. It has a pointer to a GHOST window (its void *ghostwin field), the underlying application window created by GHOST from the host operating system.\n(page: 184)\nwmOperator is implemented in the makesdna module, thus making it Blender “DNA” and serializable.\n(page: 192)\nNote that the wmOperator struct type is defined in makesdna, making it persistent. This allows for the operator stack to be saved to the blend file as well.\n(page: 193)\nAn event (e.g., a specific set of pressed keys) may be context dependent. The context is determined by the location of the mouse pointer in the application window and the mode that the application is in when an event occurs.\n(page: 194)\nghost_event_proc() handles events that are solely at the GHOST window-level. What is an event at the GHOST window-level? These events are anything not semantically Blender-specific, and therefore more general to an application’s behavior. Examples are window resizing and application closing, among other application behaviors.\n(page: 198)\nLet us look at how ghost_event_proc() treats the GHOST_kEventOpenMainFile event. Refer to Listing 7-18 once again. WM_operatortype_find() is used to look up a registered operator (an wmOperatorType object). In this example, the operator-type struct is accessed via the hashed char array “WM_OT_open_mainfile,” from global_ops_hash—a GHash hash table.\nGHash is implemented in source/blender/blenlib/intern/BLI_ghash.c. You will recall we covered the blenlib module in Chapter Four. BLI_*API functions are used to access and manipulate a GHash object. This particular data structure is used, as it is O(1) in search time on average.\nThis is necessary, because of the timing requirements of handling events. Obviously, there is a trade-off for speed over storage.\nOnce the proper wmOperatorType object is found, Data API calls are made to prepare the relevant “RNA” properties data (with \u0026ldquo;display_file_selector\u0026rdquo;). Then these properties are passed to WM_operator_name_call_ptr().\n(page: 198)\nWe can see in ghost_event_proc() that events which eventually go on to be handled at lower levels (i.e., in “areas” or “regions”) need to go on the active window’s event queue. For the shown events “GHOST_KEventButtonDown” and “GHOST_KEventButtonUp,” wm_event_add_ghostevent() is called.\n(page: 200)\nWindow level events are handled by ghost_event_proc() directly. Events lower levels are sent to queue by function wm_event_add_ghostevent()\nThe purpose of wm_event_add_ghostevent() is to process GHOST events, by further sorting and then adding information (mouse position, Blender keypress code, etc.) to a wmEvent object.\n(page: 201)\nAfter wm_window_process_events() returns in WM_main(), wm_event_do_handlers() is called.See Listing 7-20. In essence, the wm_event_do_handlers() does the following:\n• Loops over all wmWindow objects (i.e., the Blender windows)\n• Retrieves the active Scene and ViewLayer objects\n• For each wmWindow object\n• Traverses its respective event queue\n• For each such event (a wmEvent object)\n• Finds the Editor region the event occured\n• And then, calls wm_handlers_do() passing the corresponding AreaRegion object\n(page: 202)\nwm_handlers_do(), with further processing for multi-click events (not shown), calls wm_handlers_do_internal(). In wm_handlers_do_internal() calls the appropriate wm_handler_*_call(), which is eventually responsible for calling the appropriate callback function in the wmOperatorType. Also, during editor registration, the ARegion struct is provided a list of wmEventHandler structs (implemented in source/blender/windowmanager/wm_event_system.h), defining which event types a region should process.\n(page: 204)\nChapter 8: Editor Creation # The last function call in this loop is wm_draw_update(). It is in this call, after Blender’s data has been updated in the given loop, that we draw the ARegion “data-blocks” for each of our ditors.\n(page: 207)\nIn Listing 8-2, we show wm_draw_window(). Only regions tagged for redraw are copied from an offscreen buffer (i.e., “blitted” to video memory) to an eventual front-buffer, to be shown on the computer monitor.\n(page: 209)\nIn practice, it is better to implement such UI in the Python scripts that run at startup. This is more flexible and nearly as efficient, once registered and run from memory. However, using C in this chapter was intended as an introduction to the UI_*.\n(page: 215)\nEach editor is registered from the windowmanager module during WM_init(). From WM_init(), ED_spacetypes_init() is called, which itself then calls each of the editors’ separate registration functions.\n(page: 216)\nThe UI_* API is rather extensive. Blender “RNA” wraps it, for the Blender Python API, as much of the UI is scripted in Python. However, for illustration purposes, we use the UI_* API directly in our editor.\n(page: 227)\nYou will recall that we used the source/blender/editors/interface/interface_intern.h in our tutorial editor. This was to gain access to the struct uiBut, allowing us to manually assign its optype field with the wmOperatorType. Normally, you will be adding buttons via the Blender Python API, so including interface_intern.h in your editor’s source file is neither necessary or advised—in order to maintain the preferred practice of encapsulation. The Blender codebase\nenforces this by not placing the implementation of the struct uiBut in the UI_*.h files. When code tries to access the struct uiBut outside of editors/interface/, while not using interface_intern.h, a “dereferencing pointer to incomplete type” compile error results.\n(page: 232)\nIf you are familiar with the Blender Python API, you will know that UI elements can be added via Python scripts. You will also be aware that operators can be registered via Python, and the python module will use the callbacks defined in Python scripts for the operator’s functionality. In this chapter, we did everything in C—the language of the “core” codebase.\n(page: 236)\n","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/core_blender_development/","section":"技术杂项","summary":"","title":"Core Blender Development","type":"技术杂项"},{"content":" Loading and Execution # JavaScript\u0026rsquo;s tendency to block browser process, both HTTP requests and UI updates, is the most notable performance issue. Since downloading and executing of scripts block the loading of resources and page, it\u0026rsquo;s recommended to place all \u0026lt;script\u0026gt; tags as close to the bottom of \u0026lt;body\u0026gt; tag as possible. We should limit the total number of \u0026lt;script\u0026gt; tags in the page, since every \u0026lt;script\u0026gt; tag would cause a delay. Instead of using two \u0026lt;script\u0026gt; tags to load two external script files, you can combine two file addresses to one. Deferred Scripts # A \u0026lt;script\u0026gt; tag with defer attribute indicates that the script contained within the element is not going to modify the DOM and therefor execution can be deferred safely.\n\u0026lt;script type=\u0026quot;text/javascript\u0026quot; src=\u0026quot;file1.js\u0026quot; defer\u0026gt;\u0026lt;/script\u0026gt;\nThe JavaScript file (file1.js) will begin downloading at the point that \u0026lt;script\u0026gt; tag is parsed, but the code will not be execute until the DOM has been completely loaded, but before the onload event handler is called.\nDynamic Script Elements # var script = document.createElement(\u0026#34;script\u0026#34;); script.type = \u0026#34;text/javascript\u0026#34;; script.src = \u0026#34;file1.js\u0026#34; document.getElementsByTagName(\u0026#34;head\u0026#34;)[0].appendChild(script); The file begins downloading and executing after the element is added to the page, which means without blocking other page processes.\nIf the code in file1.js contains interfaces to be used by other scripts on the page, you need to track when the code has been fully downloaded and is ready for use.\nWhen the src of a \u0026lt;script\u0026gt; element has been retrieved, it will fire a load event in Firefox, Opera, Chrome and Safari. In Internet Explorer, there is a alternate implementation named readystatechange event.\nThere are 5 possible values for readyState:\nuninitialized: The default state. loading: Download has begun. loaded: Download has completed. interactive: Data is completely downloaded but isn\u0026rsquo;t fully available. complete: All data is ready to be used. The two states of most interest are \u0026ldquo;loaded\u0026rdquo; and \u0026ldquo;complete\u0026rdquo;. The safest to use the readystatechange event is to check for both of these states and remove the event handler when either one occurs, to ensure the event isn\u0026rsquo;t handled twice.\nvar script = document.createElement(\u0026#34;script\u0026#34;) script.type = \u0026#34;text/javascript\u0026#34; // For Internet Explorer script.onreadystatechange = function() { if (script.readyState == \u0026#34;loaded\u0026#34; || script.readyState == \u0026#34;complete\u0026#34;) { script.onreadystatechange = null; alter(\u0026#34;Script loaded.\u0026#34;); } } script.src = \u0026#34;file1.js\u0026#34;; document.getElementsByTagName(\u0026#34;head\u0026#34;)[0].appendChild(script); The following function encapsulates both the standard and IE-specific functionality:\nfunction loadScript(url, callback) { var script = document.createElement(\u0026#34;script\u0026#34;) script.type = \u0026#34;text/javascript\u0026#34;; if (script.readyState) { // IE script.onreadystatechange = function() { if (script.readyState == \u0026#34;loaded\u0026#34; || script.readyState == \u0026#34;complete\u0026#34;) { script.onreadystatechange = null; callback(); } }; } else { script.onload = function() { callback(); }; } script.src = url; document.getElementsByTagName(\u0026#34;head\u0026#34;)[0].appendChild(script); } You can dynamically load as many JavaScript files as necessary on a page, but make sure you consider the order in which files must be loaded. Only Firefox and Opera guarantee that the order of script execution will remain the same as you specify. Other browsers will download and execute the various code files in the order in which they are returned from the server. You can guarantee the order by chaining the downloads together, such as:\nloadScript(\u0026#34;file1.js\u0026#34;, function () { loadScript(\u0026#34;file2.js\u0026#34;, function() { loadScript(\u0026#34;file3.js\u0026#34;, function() { alter(\u0026#34;All files are loaded\u0026#34;); }) }) }) This approach can get a little bit difficult to manage if there are multiple files to download and execute. For multiple files, the preferred approach is to concatenate the files into a single file where each part is in the correct order.\nDynamic script loading is the most frequency used pattern for nonblocking JavaScript downloads due to its cross-browser compatibly and ease of use.\nXMLHttpRequest Script Injection # Another approach to nonblocking scripts is to retrieve the JavaScript code using an XMLHttpRequest(XHR) object and then inject the script into the page.\nvar xhr = new XMLHttpRequest(); xhr.open(\u0026#34;get\u0026#34;, \u0026#34;file1.js\u0026#34;, true); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status \u0026gt;= 200 \u0026amp;\u0026amp; xhr.status \u0026lt; 300 || xhr.status == 304) { var script = document.createElement(\u0026#34;script\u0026#34;); script.type = \u0026#34;text/javascript\u0026#34;; script.text = xhr.responseText; document.body.appendChild(script); } } }; xhr.send(null); The primary advantage of this approach is that you can download the JavaScript code without executing it immediately. Since the code is being returned outside of a \u0026lt;script\u0026gt; tag, it won\u0026rsquo;t automatically be executed upon download, allowing you to defer its execution until you are ready. Another advantage is that the same code works in all modern browsers without exception cases.\nThe primary limitation of this approach is that the JavaScript file must be located on the same domain as the page requesting it, which makes downloading from CDNs impossible. So this approach isn\u0026rsquo;t used on large-scale web applications.\nRecommended Nonblocking Pattern # Two steps:\nFirst, include the code necessary to dynamically load Javascript.\nAs small as possible, potentially containing just the loadScript() function.\nThen, load the rest of the JavaScript code needed for page initialization.\nOnce the initial code is in place, use it to load the remaining JavaScript.\nFor example:\n\u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;loader.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; // loader.js contains loadScript() function \u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; // use loadScript() function to load other code dynamically loadScript(\u0026#34;the-rest.js\u0026#34;, function() { Application.init(); }); \u0026lt;/script\u0026gt; Dynamic Script Loading Tools # YUI 3 LazyLoad library LABjs library Data Access # Literal value and local variable access tend to be faster than array item and object member access.\nUse literal values and local variables whenever possible and limit use of array items and object members, when speed is a concern.\nScope Chains Performance # Every function inJavaScript is represented as an object—more specifically, as an instance of Function.\nThe deeper into the execution context\u0026rsquo;s scope chain an identifier exists, the slower it is to access for both reads and writes. Consequently, local variables are always the fastest to access inside of a function, whereas global variables will generally be the slowest.\nA good rule of thumb is to always store out-of-scope values in local variables if they are used more than once within a function.\nPrototype Chains # A prototype is an object that serves as the base of another object, defining and implementing members that a new object must have.\nObjects can have two types of members: instance members (also called own members) and prototype members. Instance members exist directly on the object instance itself, whereas prototype members are inherited from the object prototype.\nThe process of resolving an object member is very similar to resolving a variable. First search on the object instance, then search on the prototype object. The deeper into the prototype chain that a member exists, the slower it is to retrieve.\nYou can determine whether an object has an instance member with a given name by using the hasOwnProperty() method. To determine whether an object has access to a property (instance member and prototype member) with a given name, you can use the in operator.\nvar book = { title: \u0026#34;High Performance JavaScript\u0026#34;, publisher: \u0026#34;Yahoo! Press\u0026#34; }; alert(book.hasOwnProperty(\u0026#34;title\u0026#34;)); // true alert(book.hasOwnProperty(\u0026#34;toString\u0026#34;)) // false alert(\u0026#34;title\u0026#34; in book); // true alert(\u0026#34;toString\u0026#34; in book); // true Nested Members # Nested members cause the JavaScript engine to go through the object member resolution process each time a dot is encountered. The deeper the nested member, the slower the data is accessed.\nIf you are going to read an object property more than one time in a function, it best to store that property value in a local variable.\nDOM Scripting # DOM and JavaScript implementations are independent of each other. Think of DOM as a piece of land and JavaScript as another piece of land, both connected with a toll bridge. For performance, you should cross that bridge as few times as possible and strive to stay in JavaScript land.\n","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/high_performance_javascript/","section":"技术杂项","summary":"","title":"High Performance JavaScript","type":"技术杂项"},{"content":"按照如下文章进行 React Native 下 iOS 推送的开发，出现了一些文章中没谈论到的错误，特此记录解决方案。\n使用Leancloud实现React Native App的消息推送（Push Notification）- iOS篇 - 半路出家老菜鸟 - SegmentFault 思否\n\u0026lsquo;React/RCTPushNotificationManager.h\u0026rsquo; file not found # 参照 https://facebook.github.io/react-native/docs/linking-libraries-ios 中 Manual linking 的步骤进行手动库链接操作，其中尤其注意的是 Step 3，讲库文件依赖的头文件路径添加进头文件搜索路径。\n添加的路径为 ${SRCROOT}/../node_modules/react-native/Libraries/PushNotificationIOS\n\u0026lsquo;React/RCTEventEmitter.h\u0026rsquo; file not found # 这个错误实际是从 RCTPushNotification 库中报出来的，所以我们实际上应该对 RCTPushNotification 库的头文件搜索路径也要做添加工作。步骤与添加项目的头文件搜索路径基本一致，只不过这次选中的是 RCTPushNotification.scodeproj\nBundle Id / Topics 的一致性 # 以下各处均涉及 Bundle Id 和 Topics 的填写，需保持正确一致：\nApple Developer 注册时的 Bundle Id leancloud 中新增 iOS 推送 Token Authentication 时，填入的 Topic 需要与 Apple Developer 的 Bundle Id 一致 Xcode 打开 .wcworkspace 工程后，Genenal 标签页中的 Bundle Identifier 需要与 Apple Developer 的 Bundle Id 一致 ","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/ios%E6%B6%88%E6%81%AF%E6%8E%A8%E9%80%81%E5%BA%93%E9%93%BE%E6%8E%A5%E8%B8%A9%E5%9D%91%E8%AE%B0%E5%BD%95/","section":"技术杂项","summary":"","title":"iOS 消息推送/库链接踩坑记录","type":"技术杂项"},{"content":" FAQ # Is my display supported?\nLVGL needs just one simple driver function to copy an array of pixels into a given area of the display. If you can do this with your display then you can use it with LVGL.\nLVGL Basics # Overview of LVGL\u0026rsquo;s Data Flow # this is a graph showing LVGL’s workflow.\nThe Tick Interface tells LVGL what time is it. Timer Handler drives LVGL\u0026rsquo;s timers which, in turn, perform all of LVGL\u0026rsquo;s time-related tasks.\nUpdate was driven by tick.\nAdd LVGL to Your Project # Configuration # Connecting LVGL to Your Hardware # you will need to complete a few more steps to get your project up and running with LVGL.\nInitialize LVGL once early during system execution by calling lv_init(). This needs to be done before making any other LVGL calls. Initialize your drivers. Connect the Tick Interface. Connect the Display Interface. Connect the Input-Device Interface. Drive LVGL time-related tasks by calling lv_timer_handler() every few milliseconds to manage LVGL timers. See Timer Handler for different ways to do this. Optionally set a theme with lv_display_set_theme(). Thereafter #include \u0026ldquo;lvgl/lvgl.h\u0026rdquo; in source files wherever you need to use LVGL functions. LVGL needs awareness of what time it is (i.e. elapsed time in milliseconds) for all of its tasks for which time is a factor: refreshing displays, reading user input, firing events, animations, etc.\n","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/lvgl_doc/","section":"技术杂项","summary":"","title":"LVGL doc","type":"技术杂项"},{"content":" Preface # This is a lot of time but things changed a lot even for me in these years. A totally different job, full of too many responsibilities, and a daughter came in the middle, and now my free time ranges from the 5:00am to 7:00am, and you can figure out how hard is to work to a book with 900 pages in just two hours a day.\n(page: i)\nAppreciate your hardworking, so inspiring.\nHow is the Book Organized # Moreover, a custom and secure bootloader is shown, which can upgrade the on-board firmware through the USART peripheral.\n(page: vi)\nThis teaches us how to upgrade the on-board firmware.\nChapter 26 describes a solution to interface Nucleo boards to the Internet by using the W5500 network processor. The chapter shows how-to develop Internet- and web-based applications using STM32 microcontrollers even if they do not provide a native Ethernet peripheral.\n(page: vi)\nHow to develop Internet and web base applications using STM32.\nIntroduction # Introduction to STM32 MCU Portfolio # 1.1 Introduction to ARM Based Processors # When dealing with ARM based processors, a lot of confusion may arise since there are many different ARM architecture revisions (ARMv6, ATMv6-M, ARMv7-M, ARMv7-A, ARMv8-M and so on) and many core architectures, which are in turn based on an ARM architecture revision. For the sake of clarity, for example, a processor based on the Cortex-M4 core is designed on the ARMv7-M architecture.\nAn ARM architecture is a set of specifications regarding the instruction set, the execution model, the memory organization and layout, the instruction cycles and more, which precisely describes a machine that will implement said architecture. If your compiler is able to generate assembly instructions for that architecture, it is able to generate machine code for all those actual machines (aka, processors) implementing that given architecture.\nCortex-M is a family of physical cores designed to be further integrated with vendor-specific silicon devices to form a finished microcontroller. The way a core works is not only defined by its related ARM architecture (eg. ARMv7-M), but also by the integrated peripherals and hardware capabilities defined by the silicon manufacturer. For example, the Cortex-M4 core architecture is designed to support bit-data access operations in two specific memory regions using a feature called bit-banding, but it is up to the actual implementation to add such feature or not.\n(page: 2)\nThis clarified the relationship between ARM and Cortex-M.\n1.1.1 Cortex and Cortex-M Based Processors # Cortex microcontrollers are divided into three main subfamilies:\nThe difference between different ARM Cortex families (Cortex-A, Cortex-M, Cortex-R).\n1. 1.1.1.1 Core Registers # Like all RISC architectures, Cortex-M processors are load/store machines, which perform operations only on CPU registers except1 for two categories of instructions: load and store, used to transfer data between CPU registers and memory locations. A metaphor about the essence of CPU.\n1.1.1.2 Memory Map # ARM defines a standardized memory address space common to all Cortex-M cores, which ensures code portability among different silicon manufacturers. The address space is 4GB wide, and it is organized in several sub-regions with different logical functionalities. Figure 1.3 shows the memory layout of a Cortex-M processor 3.\nThe next 0.5GB of memory is dedicated to the mapping of peripherals. Every peripheral provided by the MCU (timers, I2C and SPI interfaces, USARTs, and so on) has an alias in this region. It is up to the specific MCU to organize this memory space.\nPeripherals are mapped to this section of Memory.\nSTM32CubeMX Tool # Introduction to CubeMX Tool # There are many basic but useful informations about how to configure microcontroller using CubeMX.\nproject-name.ioc. This file is the CubeMX main project file, containing all the\nconfigurations performed in CubeMX. Different meanings when pins was colored. How to find alternative pins. How to set different functions of pin. How to lock a pin. Define custom labels for MCU signal (which will generate a corresponding macro in .h file). Different status of peripheral in Mode panel. What is the Cube Low-Layer API?\nIn the recent years, ST answered to strong criticism of the library’s performances by introducing the Cube Low-Layer (shortened LL) set of drivers. As the name suggest, the LL library is born to be very optimized, leaving to the programmer the responsibility to deal with very specific characteristics of the given STM32 series and the given P/N.\nThis book will not cover topics related to the LL library\nIf you need to control every single aspect of a given peripheral to reach the most optimized\ncode, then the LL library is what you need. But, at the first instance, I suggest you start designing the firmware by using the CubeHAL and then moving to the next step, unless you are a very experienced firmware developer.\n(page: 103)\nUnderstanding project structure # The Figure 4.14 is a good reference in this quick walkthrough.\n(page: 105)\nA quick walkthrough of the project structure generated by CubeMX.\nIntroduction to Debugging # What is behind a Debug Session # Figure 5.1 tries to provide an overview of the debug setup behind the scenes.\n(page: 117)\nWe can know how GDB client, STLINK GDB Server, STLINK and MCU work together in debugging.\nI/O Retargeting # Often, the usage of breakpoints is not possible while debugging, because this would cause the loss of relevant events. At the same time, to print on a serial console a few messages could help a lot in understanding what’s going wrong with our firmware.\nThe simplest and effective solution is to redefine the needed system calls (_write(), _read(), _isatty(), _close(), _fstat()) to retarget the STDIN, STDOUT and STDERR standard streams to the Nucleo USART2. This can be easily done in the following way:\n(page: 125)\nAfter retargeting I/O like print() function to USART, we can easily print messages.\nIf you are going to use printf()/scanf() functions to print/read float datatypes on the serial console (but also if you are going to use sprintf() and similar routines), you need to explicitly enable float support in newlib-nano, which is the more compact version of the C runtime library for embedded systems.\nIf you encountered problems when using printf to print float numbers, just check above.\nDiving into HAL # GPIO Management # STM32 Peripherals Mapping and HAL Handlers # Every STM32 peripheral is interconnected to the MCU core by several orders of buses, as shown in Figure 6.1\n(page: 130)\nHow peripherals are connected to MCU by buses, and mapped to address space.\nA peripheral is controlled by modifying and reading each register of these mapped regions.\n(page: 133)\nOne of the HAL roles is to abstract from the specific peripheral mapping. This is done by defining several handlers for each peripheral. A handler is nothing more than a C struct, whose references are used to point to real peripheral address.\n(page: 134)\nGPIOS Configuration # Interrupts Management # without the help by the hardware it is impossible to have a true preemptive system, which allows switching between several execution contexts without irreparably losing the current execution flow.\n(page: 143)\nNVIC Controller # NVIC is a dedicated hardware unit inside the Cortex-M based microcontrollers that is responsible othe exceptions handling. Figure 7.1 shows the relation between the NVIC unit, the Processor Core and peripherals.\n(page: 143)\nHere we have to distinguish two types of peripherals: those external to the Cortex M core, but internal to the STM32 MCU (e.g., timers, UARTS, and so on), and those peripherals external to the MCU at all.\n(page: 143)\nA dedicated programmable controller, named External Interrupt/Event Controller (EXTI), is responsible of the interconnection between the external I/O signals and the NVIC controller.\n(page: 143)\n- Reset: this exception is raised just after the CPU resets. Its handler is the real entry point of the running firmware. In an STM32 application all starts from this exception.\n(page: 144)\nIt’s interesting to know that firmware was started from a execption.\nEnabling Interrupts # Figure 7.3: The relation between GPIO, EXTI lines and corresponding ISR in an STM32F4 MCU\nInterrupt Lifecycle # It’s important to understand interrupt lifecycle, just read this part.\nThe Figure 7.11 clearly shows the relation between the peripheral IRQ pending state and the ISR pending state. Signal I/O is the external peripheral driving the I/O (e.g., a tactile switch connected to a pin). When the signal level changes, the EXTI line connected to that I/O generates an IRQ and the corresponding pending bit is asserted. As consequence, the NVIC generates the interrupt. When the processor starts servicing the ISR, the ISR pending bit is cleared automatically, but the peripheral IRQ pending bit will be held high until it is cleared by the application code.\n(page: 157)\nThe Figure 7.12 shows another case. Here we force the execution of the ISR setting its pending bit. Since this time the external peripheral is not involved, there is no need to clear the corresponding IRQ pending bit.\n(page: 157)\nIt’s interesting to check how signals changes in interrupt lifecycle.\nHowever, take in mind that to avoid losing important interrupts, it is a good design practice to clear peripherals IRQ pending status bit as their ISR start to be serviced. The processor core does not keep track of multiple interrupts (it does not queue interrupts), so if we clear the peripheral pending bit at the end of an ISR, we may lose important IRQs that fire in the middle.\n(page: 158)\nInterrupt Priority Levels # The complication arises from the fact that the IPR register can be logically subdivided in two parts: a series of bits defining the preemption priority¹³ and a series of bits defining the sub-priority.\n(page: 164)\nIt’s important to know this Priority Group feature.\nMark All Interrupts at Once or a Priority Basis # Sometimes we want to be sure that our code is not preempted to allow the execution of interrupts or more privileged code. That is, we want to ensure that our code is thread-safe. Cortex-M based processors allow to temporarily mask the execution of all interrupts and exceptions, without disabling one by one. Two special registers, named PRIMASK and FAULTMASK allow to disable all interrupts and exceptions respectively.\n(page: 173)\nHowever, take in mind that, as general rule, interrupt must be masked only for really short time, otherwise you could lose important interrupts. Remember that interrupts are not queued.\n(page: 173)\nCortex-M3/4/7/33 cores allow to selectively mask interrupts on a priority basis. The BASEPRI register masks exceptions or interrupts on a priority level.\n(page: 174)\nUniversal Asynchronous Serial Communications # Introduction to UARTs and USARTs # Here we know the difference between USART(Universal Synchronous Receiver/Transmitter) and UART(Universal Asynchronous Receiver/Transmitter).\nUART Initialization # The function attribute __weak is a GCC way to declare a symbol (here, a function name)\nwith a weak scope visibility, which we will be overwritten if another symbol with the same\nname with a global scope (that is, without the __weak attribute) is defined elsewhere in the\napplication (that is, in another relocatable file). The linker will automatically substitute the\ncall to the function HAL_UART_MspInit() defined inside the HAL if we implement it in our\napplication code.\nThis __weak function attribute is important.\nUART Communication in Interrupt Mode # A more elegant and performing solution is to use a temporary memory area where to store the byte sequences and to let the ISR to execute the transfer. A queue is the best options to handle FIFO events. There are several ways to implement a queue, both using static and dynamic data structure. If we decide to implement a queue with a predefined area of memory, a circular buffer is the data structure suitable for this kind of applications.\n(page: 199)\nIt’s interesting to check this circular buffer queue transmission solution.\nDMA Management # Introduction to DMA # Why the DMA is a so important feature? Every peripheral in an STM32 microcontroller needs to exchange data with the internal Cortex-M core. Some of them translate this data in electrical I/O signals to exchange it to the outside world according to a given communication protocol (this is the case, for example, of UART or SPI interfaces). Others are just designed so that the access to their registers inside the peripheral memory mapped region (from 0x4000 0000 to 0x5FFF FFFF) causes a changing to their state (for example, the GPIOx-\u0026gt;ODR register drives the state of all I/Os connected to that port). However, keep in mind that from the CPU point of view this also implies a memory transfer between the MCU core and the peripheral.\nThe MCU - in theory - could be designed so that every peripheral would have its own storage area (dedicated memories), and it in turn could be tightly coupled with the MCU core to minimize the costs related to memory transfers. This, however, complicates the MCU architecture, requiring a lot of more silicon and more “active components” that consume power. So, the approach used in all embedded microcontrollers is to use some portions of the internal SRAM memory as temporary area storage for different peripherals. It is up to the user to decide how much room to dedicate to these areas.\n(page: 206)\nHere we get the concept how MCU core exchanges data with peripherals:\nCommunication protocol (URAT, SPI, etc) Memory map The HAL_UART_Receive() function will access twenty times to the huart2.Instance-\u0026gt;DR data register to transfer bytes from the peripheral to the internal memory, plus it will poll the UART RXNE flag to detect when the new data is ready to be transferred. The CPU will be involved during these operations (see Figure 9.1), even if its role is “limited” to move data from the peripheral to the SRAM.\n(page: 207)\nTo receive a data with 20 bytes, the CPU has to transfer data from data register of the peripheral to SRAM 20 times.\n","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/mastering_stm32_2nd/","section":"技术杂项","summary":"","title":"Mastering STM32 2nd","type":"技术杂项"},{"content":" Nvidia in container # 首先确保 NixOS 本身已经配置好 Nvidia 驱动: https://nixos.wiki/wiki/Nvidia\nNixOS docker 相关配置\nvirtualisation.docker.enable = true; hardware.nvidia-container-toolkit.enable = true; 创建 distrobox 容器时的参数\n注意 \u0026ndash;additional-flags\n要传 \u0026ndash;device nvidia.com/gpu=all 不要传 \u0026ndash;gpus all distrobox-create --root -i ubuntu:24.04 -n ubuntu-2404-n --hostname ubuntu-2404-n --additional-flags \u0026#34;--device nvidia.com/gpu=all\u0026#34; distrobox 使用 host 的输入法 # 参考1：https://github.com/89luca89/distrobox/issues/957\n1.change GTK_IM_MODULE and QT_IM_MODULE to xim,like\nexport GTK_IM_MODULE=xim # 有些应用需要指定 xim, 如 vscode, 有些需要 fcitx 如 wps export QT_IM_MODULE=xim # 同上 export XMODIFIERS=@im=fcitx # 固定为 fcitx 2.enable locale in /etc/locale.gen and run locale-gen command\n参考2：https://github.com/fcitx/fcitx5/discussions/990\nUbuntu 容器内 vulkan 无法正常使用 # 问题描述 # 容器外 nvidia-smi 和 vulkaninfo 输出都正常，能看到 N 卡信息。\n$ nvidia-smi Fri Jul 4 10:32:09 2025 +-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 565.77 Driver Version: 565.77 CUDA Version: 12.7 | |-----------------------------------------+------------------------+----------------------+ | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+========================+======================| | 0 NVIDIA GeForce RTX 2080 Off | 00000000:01:00.0 On | N/A | | N/A 65C P8 8W / 50W | 529MiB / 8192MiB | 7% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ +-----------------------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=========================================================================================| | 0 N/A N/A 151550 G ...0jn1l86i6-xorg-server-21.1.16/bin/X 272MiB | | 0 N/A N/A 152101 G /run/current-system/sw/bin/cinnamon 77MiB | | 0 N/A N/A 154788 G ...bda102dc07718372145c47159957d6ca33b 79MiB | | 0 N/A N/A 158835 G ...an,WebOTP --variations-seed-version 20MiB | | 0 N/A N/A 181293 G /usr/share/code/code 58MiB | +-----------------------------------------------------------------------------------------+ $ vulkaninfo --summary ========== VULKANINFO ========== Devices: ======== GPU0: apiVersion = 1.3.289 driverVersion = 565.77.0.0 vendorID = 0x10de deviceID = 0x1ed0 deviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU deviceName = NVIDIA GeForce RTX 2080 driverID = DRIVER_ID_NVIDIA_PROPRIETARY driverName = NVIDIA driverInfo = 565.77 conformanceVersion = 1.3.8.2 deviceUUID = 714dd5f1-e57d-7b68-85b5-9e3f8ebbff93 driverUUID = 5d948742-de2b-5e32-9692-c2a5621aed9a GPU1: apiVersion = 1.3.289 driverVersion = 24.2.8 vendorID = 0x8086 deviceID = 0x3e98 deviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU deviceName = Intel(R) UHD Graphics 630 (CFL GT2) driverID = DRIVER_ID_INTEL_OPEN_SOURCE_MESA driverName = Intel open-source Mesa driver driverInfo = Mesa 24.2.8 conformanceVersion = 1.3.6.0 deviceUUID = 8680983e-0200-0000-0002-000000000000 driverUUID = 235d7fbb-cda3-983c-2211-66b6596d8146 GPU2: apiVersion = 1.3.289 driverVersion = 0.0.1 vendorID = 0x10005 deviceID = 0x0000 deviceType = PHYSICAL_DEVICE_TYPE_CPU deviceName = llvmpipe (LLVM 18.1.8, 256 bits) driverID = DRIVER_ID_MESA_LLVMPIPE driverName = llvmpipe driverInfo = Mesa 24.2.8 (LLVM 18.1.8) conformanceVersion = 1.3.1.1 deviceUUID = 6d657361-3234-2e32-2e38-000000000000 driverUUID = 6c6c766d-7069-7065-5555-494400000000 容器内，nvidia-smi 输出正常，vulkaninfo 输出却看不到 N 卡。\n$ nvidia-smi Fri Jul 4 01:30:07 2025 +-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 565.77 Driver Version: 565.77 CUDA Version: 12.7 | |-----------------------------------------+------------------------+----------------------+ | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+========================+======================| | 0 NVIDIA GeForce RTX 2080 Off | 00000000:01:00.0 On | N/A | | N/A 39C P8 9W / 50W | 364MiB / 8192MiB | 10% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ +-----------------------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=========================================================================================| | 0 N/A N/A 151550 G ...0jn1l86i6-xorg-server-21.1.16/bin/X 217MiB | | 0 N/A N/A 152101 G /run/current-system/sw/bin/cinnamon 58MiB | | 0 N/A N/A 154788 G ...bda102dc07718372145c47159957d6ca33b 68MiB | +-----------------------------------------------------------------------------------------+ $ vulkaninfo --summary ========== VULKANINFO ========== Devices: ======== GPU0: apiVersion = 1.3.289 driverVersion = 24.2.8 vendorID = 0x8086 deviceID = 0x3e98 deviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU deviceName = Intel(R) UHD Graphics 630 (CFL GT2) driverID = DRIVER_ID_INTEL_OPEN_SOURCE_MESA driverName = Intel open-source Mesa driver driverInfo = Mesa 24.2.8-1ubuntu1~24.04.1 conformanceVersion = 1.3.6.0 deviceUUID = 8680983e-0200-0000-0002-000000000000 driverUUID = 01445b43-3542-5b20-af99-316b29c3521c GPU1: apiVersion = 1.3.289 driverVersion = 0.0.1 vendorID = 0x10005 deviceID = 0x0000 deviceType = PHYSICAL_DEVICE_TYPE_CPU deviceName = llvmpipe (LLVM 19.1.1, 256 bits) driverID = DRIVER_ID_MESA_LLVMPIPE driverName = llvmpipe driverInfo = Mesa 24.2.8-1ubuntu1~24.04.1 (LLVM 19.1.1) conformanceVersion = 1.3.1.1 deviceUUID = 6d657361-3234-2e32-2e38-2d3175627500 driverUUID = 6c6c766d-7069-7065-5555-494400000000 问题原因 # 容器内，vulkan 相关的一些驱动和依赖程序没有安装。\n最直接的表现就是 /usr/share/vulkan/icd.d/nvidia_icd.json 文件不存在。\n解决方法 # sudo apt install software-properties-common sudo add-apt-repository universe multiverse # 启用标准扩展仓库 sudo add-apt-repository ppa:graphics-drivers/ppa # 添加专有驱动PPA sudo apt update sudo apt install vulkan-tools libvulkan1 libvulkan-dev libnvidia-gl-565/noble # 注意版本要匹配 验证 # $ cat /usr/share/vulkan/icd.d/nvidia_icd.json { \u0026#34;file_format_version\u0026#34; : \u0026#34;1.0.1\u0026#34;, \u0026#34;ICD\u0026#34;: { \u0026#34;library_path\u0026#34;: \u0026#34;libGLX_nvidia.so.0\u0026#34;, \u0026#34;api_version\u0026#34; : \u0026#34;1.3.289\u0026#34; } } $ vulkaninfo --summary ========== VULKANINFO ========== Devices: ======== GPU0: apiVersion = 1.3.289 driverVersion = 565.77.0.0 vendorID = 0x10de deviceID = 0x1ed0 deviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU deviceName = NVIDIA GeForce RTX 2080 driverID = DRIVER_ID_NVIDIA_PROPRIETARY driverName = NVIDIA driverInfo = 565.77 conformanceVersion = 1.3.8.2 deviceUUID = 714dd5f1-e57d-7b68-85b5-9e3f8ebbff93 driverUUID = 5d948742-de2b-5e32-9692-c2a5621aed9a GPU1: apiVersion = 1.3.289 driverVersion = 24.2.8 vendorID = 0x8086 deviceID = 0x3e98 deviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU deviceName = Intel(R) UHD Graphics 630 (CFL GT2) driverID = DRIVER_ID_INTEL_OPEN_SOURCE_MESA driverName = Intel open-source Mesa driver driverInfo = Mesa 24.2.8-1ubuntu1~24.04.1 conformanceVersion = 1.3.6.0 deviceUUID = 8680983e-0200-0000-0002-000000000000 driverUUID = 01445b43-3542-5b20-af99-316b29c3521c GPU2: apiVersion = 1.3.289 driverVersion = 0.0.1 vendorID = 0x10005 deviceID = 0x0000 deviceType = PHYSICAL_DEVICE_TYPE_CPU deviceName = llvmpipe (LLVM 19.1.1, 256 bits) driverID = DRIVER_ID_MESA_LLVMPIPE driverName = llvmpipe driverInfo = Mesa 24.2.8-1ubuntu1~24.04.1 (LLVM 19.1.1) conformanceVersion = 1.3.1.1 deviceUUID = 6d657361-3234-2e32-2e38-2d3175627500 driverUUID = 6c6c766d-7069-7065-5555-494400000000 ","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/nixos_distrobox/","section":"技术杂项","summary":"","title":"NixOS 下使用 distrobox 的一些问题记录","type":"技术杂项"},{"content":"","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"闭包其实就是一个作用域的概念，即在内部函数中，允许访问外部变量。\n但是，Pyhton 中闭包的相关性质，和 JS 相比较有很多的区别。（Python 的作用域设计本就与其它语言有不小区别。）\n总的来说呢就是，在闭包函数内，可以使用外部变量的值，但不能修改外部变量。\n比如以下代码的运行是符合预期的：\ndef func(): a = 1 def innerFunc(): print(a) innerFunc() func() # 输出： 1 我们在 innerFunc 中只是打印了 a 的值，没有对 a 进行修改。\n再看如下代码：\ndef func(): a = 1 def innerFunc(): a = 2 print(a) innerFunc() print(a) func() # 输出： # 2 # 1 在此例中，我们发现闭包函数中打印出 a 为 2, 而外部函数中 a 为 1. 这就是前面所说的，闭包函数只能使用外部变量的值，却不能对外部变量进行修改。实际上，当我们在内部函数中声明 a = 2 时，已经创建了一个新的局部变量 a， 这个局部变量会在闭包函数中屏蔽外部的变量 a，因而在闭包函数中所做的任何操作都是针对这个局部变量的，对外部变量 a 不会产生任何影响。\n以上的代码都没有报错，我们来看下面这段代码：\ndef func(): a = 1 def innerFunc(): a += 1 print(a) innerFunc() print(a） func() # 输出 # 错误：UnboundLocalError: local variable \u0026#39;a\u0026#39; referenced before assignment 在此例中，我们将闭包函数中的 a = 2，修改为了 a += 1，代码报错了。延续前面所阐述的概念，我们要对一个名为 a 的变量进行自加操作，即修改和赋值，而当前命名空间内，并不存在一个已被初始化的可修改的 a 变量（闭包函数无法修改外部变量，执行此语句时，外部的 a 变量实际已经被屏蔽了）。对一个不存在变量进行自加操作，所报出来的错误自然就是：变量在赋值前被引用。\n我们直到，Python 中的变量可分为值变量和引用变量（实际从本质上来说，Python 中的所有变量都是引用变量，详细看此文章），由于引用变量的存在，某些场景下，我们会产生闭包函数可以修改外部变量的错觉。\n看如下代码：\ndef func(): a = [1] def innerFunc(): a[0] += 1 print(a[0]) innerFunc() print(a[0]) func() # 输出： # 2 # 2 这段代码的执行结果不免使人疑惑，从这个结果来看，我们不是成功地在闭包函数内修改了 a[0] 的值吗？是的，我们确实成功修改了，但是这和前面所述的闭包函数不能修改外部变量冲突吗？不冲突。我们知道，这段代码与上一段的区别是，a 从一个整形变量变成了列表，而列表是一个引用变量。也就是说 a 变量存储的是列表的首地址。我们在闭包函数中修改了 a[0] 的值后，a 变量变化了吗，并没有，a 变量中存储的仍然是列表的首地址。也就是说，我们并没有在闭包函数中修改所谓的外部变量 a，我们只是根据外部变量 a 找到了列表的首地址，然后对列表进行了修改（列表是存储在堆中的，通过地址访问，就不存在什么作用域的问题了）。所以这段代码，乍一看好像闭包函数修改了外部变量，其实没有这回事儿，只是在闭包函数内操作了一下堆中的数据罢了。\n搞明白了其背后的根本原因，下面的代码，自然也迷糊不了你：\ndef func(): a = [1] def innerFunc(): a = [2] print(a[0]) innerFunc() print(a[0]) func() # 输出： # 2 # 1 闭包函数中的 a = [2] 又是对 a 变量的一个赋值的操作，遵从前面所属的原则，此时闭包函数内创建了一个新的局部变量 a（这次是一个引用变量），存储了一个新的列表的首地址。无论是针对它，还是针对其所指向的列表的操作，自然也就不会影响外部变量 a 及其所指向的列表了。\n","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/python%E9%97%AD%E5%8C%85%E8%AF%AF%E5%8C%BA/","section":"技术杂项","summary":"","title":"Python 闭包误区","type":"技术杂项"},{"content":"Python 中的线程会在一个单独的系统级线程（关于系统级（或称内核级）线程与用户级线程，可以再进一步了解）中执行（比如说一个 POSIX 线程或者一个 Windows 线程），这些线程将由操作系统来全权管理。\nfrom threading import Thread t = Thread(target=a_function, args=(10,)) t.start() 你可以通过 t.is_alive() 来查看一个线程是否在执行，也可以通过 t.join() 将其加入到当前线程，并等待其执行完成。\n除此之外，你无法对线程多更多的控制。线程一旦启动，就将独立执行直到目标函数结束。\n如果你确实想向线程发送信号来控制线程，你需要自己实现它。比如说你修改某个值，然后让线程轮询这个值。\n普通线程与后台线程（又称守护线程） # 当 Python 主线程代码执行完后，主线程是否退出，取决于当前是否有非后台线程仍在运行，有的话，主线程会等待它们运行结束后再退出，否则（没有线程或只有后台线程仍在运行），主线程将立即退出。主线程退出，所有的后台线程也就结束。\n要明确的是，GIL 只会影响到那些 CPU 任务重的程序。如果你的程序大部分只是因为涉及到 I/O、网络交互而需要用到多线程，那么你大可放心，即使创建几千个 Python 线程，现代计算机也不会有什么压力。\n许多程序员在遭遇线程性能问题的时候，立马就怪罪 GIL，往往是不厚道或太天真。\nGIL 限制 # 由于全局解释锁（GIL）的限制，Python 中的线程并不是真正同时运行的线程，解释器实际上同一时刻只执行其中一个线程。这一限制是的 Python 的多线程并不能利用多核 CPU 的优势。所以 Python 的多线程更适合于处理 I/O 操作或其他一些需要并发执行的阻塞操作，而并不适合于计算密集型任务的并发处理。\nEvent 事件 # 由于 Python 中的线程是独立运行的，我们无法向其发送信号，也不知道其运行得怎么样，运行到了哪一步，而往往线程之间又是需要协作的，比如说我们可能在 A 线程执行到某处时，想要等待其他线程完成了某件事情之后，A 再往下执行。如果使用共享变量的话，事情会变得复杂，而 event 事件则能优雅地应对此类场景。\nfrom threading import Thread, Event evt = Event() # A 线程的目标方法 def thread_func_A(evt): # do something evt.wait() # do something # B 线程的目标方法 def thread_func_B(evt): # do something evt.set() # do something 在如上的例子中，线程 A 的目标方法会在 evt.wait() 等待，待线程 B 的目标方法执行了 evt.set() 之后，线程 A 才会继续往下执行。\n尽管 event 还提供了 clear() 方法对其进行重制，但是一个 event 最好作为一次性使用。因为 event 的 clear 操作容易带来逻辑冲突以及错过处理，甚至死锁等问题。\n如果你想要一个可以重复使用的 event，那么你可以使用 Condition 对象。另外，event 还有一个特点是，当其被 set 的时候，所有正在等待它的线程都会被唤醒。如果你指向唤醒单个线程，也应该用 Condition。\nCondition 提供了以下方法：\nwait()\n等待其他线程发送通知（notification）之后再往下执行。\nnotify(n=1)\n发送通知，允许 n 个（如果有的话）等待此通知的线程被唤醒。\nnotify_all()\n发送通知，允许所有等待此通知的线程被唤醒。\n使用 Queue 进行线程间通信 # 编写涉及到大量线程同步问题的代码会让你痛不欲生，比较建议的方式是使用队列来进行线程间通信，或者把每个线程当作一个 Actor，利用 Actor 模型来控制并发。此处我们讲解线程间通信。\n一个线程向其他线程发送数据，最安全的方式是使用 queue 库中的队列。创建一个被多个线程共享的 Queue 对象，这些线程通过使用 put() 和 get() 操作来向队列中添加或者删除元素。\nQueue 对象已经包含了必要的锁，所以可以通过它在多个线程间安全地共享数据。\n（Queue 不是一个普通的列表，应当对其详细了解。）\n在使用队列时，协调生产者和消费者的关闭问题是一个麻烦，通常的方案是在队列中放一个特殊值，当消费者读到这个值时，终止执行。另外，如果存在多个消费者的话，某个消费者通过 get() 取到特殊值后，应该再将其 put() 回去，这样，其他的消费者也能够取到这个值并结束。\n使用队列进行进程间通信是一个单向、不确定的过程。如果生产者想要知道消费者是否接收并处理好了数据，则生产者可以将一个 event 对象和数据一起发送，Queue 和 event 相结合。\nfrom queue import Queue from threading import Thread, Event # A thread that produces data def producer(out_q): while running: # Produce some data ... # Make an (data, event) pair and hand it to the consumer evt = Event() out_q.put((data, evt)) ... # Wait for the consumer to process the item evt.wait() # A thread that consumes data def consumer(in_q): while True: # Get some data data, evt = in_q.get() # Process the data ... # Indicate completion evt.set() 我们还可以将 Queue 和 event、Condition 进行更丰富的封装，以实现更复杂的通信机制。\n基于队列编写多线程在多数情况下都是比较明智的选择，由于其本身实现了安全的同步机制，为你免去了许多烦恼。此外，使用 Queue 这种基于消息队列的通信机制，也方便你将代码移植到基于消息队列通信的分布式系统。\n带锁的共享变量 # 为避免多线程的竞态冲突，我们要对共享变量加锁，以保证原子性操作。\nimport threading class SharedCounter: \u0026#39;\u0026#39;\u0026#39; A counter object that can be shared by multiple threads. \u0026#39;\u0026#39;\u0026#39; def __init__(self, initial_value = 0): self._value = initial_value self._value_lock = threading.Lock() def incr(self,delta=1): \u0026#39;\u0026#39;\u0026#39; Increment the counter with locking \u0026#39;\u0026#39;\u0026#39; with self._value_lock: self._value += delta def decr(self,delta=1): \u0026#39;\u0026#39;\u0026#39; Decrement the counter with locking \u0026#39;\u0026#39;\u0026#39; with self._value_lock: self._value -= delta Lock 对象应该和 with 语句一起使用，以保证互斥执行，即每次只有一个线程可以执行 with 语句包含的代码块。with 语句会在这个代码块执行前自动获取锁，在执行结束后自动释放锁。\n相较于显式地调用 lock.acquire() 和 lock.release()，with 语句更加优雅，也不易出错，尤其是可以避免程序员忘了进行 release() 。\n为了避免出现死锁，我们应到保证一个线程一次只允许获取一个锁，不许嵌套。不然是很容易发生 A 在等 B 释放，B 在等 A 释放的死锁情况的。\n如果你真的必须要在同一个线程中获取多个锁，有一个避免死锁的方案就是为每一个锁分配一个唯一的 id，然后只允许按照升序规则来使用多个锁，不是说得逐一获取，只要不存在逆序就行。\n作用域为线程的变量 # 如果你想要这样一个变量，它在当前线程内就像是一个全局变量，当前线程内的所有函数都可以使用这个变量，而对其它线程又是不可见的。那么 threading.loca() 就是你想要的。\nimport threading # 创建全局ThreadLocal对象: local_school = threading.local() def student_greet(): # 获取当前线程关联的student: student = local_school.student print(\u0026#39;Hello, %s (in %s)\u0026#39; % (student, threading.current_thread().name)) def student_thread(student): # 绑定ThreadLocal的student: local_school.student = student student_greet() t1 = threading.Thread(target= student_thread, args=(\u0026#39;Alice\u0026#39;,), name=\u0026#39;Thread-A\u0026#39;) t2 = threading.Thread(target= student_thread, args=(\u0026#39;Bob\u0026#39;,), name=\u0026#39;Thread-B\u0026#39;) t1.start() t2.start() t1.join() t2.join() 这个实现原理其实很简单，在如上代码中，local_school 本身是一个全局变量，它实际上是为每一个线程维护着一个单独的实例字典，所以当我们在某一个线程中访问 local_school.student 的时候，访问的其实是 local_school 中针对你当前这个线程实例所映射的字典中存储的那个 student。\n线程池 # 线程池是什么呢，它是我们预先创建出来的一揽子线程。它的价值在于，当我们有几个任务需要线程去做时，我们可以把这几个任务给线程池中的空闲线程，让它们去做。而线程池中的线程执行完任务后，它不会被系统回收，而是会回到空闲状态，等待你再次给它分配任务。\n这样有什么好处呢？我们知道，Cpython 的线程是内核线程，是由操作系统来创建和管理的。所以一个线程的创建、销毁其实是挺消耗资源和机能的。所以如果我们创建出线程池，并且可以反复地利用它们，免去反复的创建和销毁，这也是一种性能优势。\n此外，线程池也方便我们对线程并发数量进行控制。\nfrom concurrent.futures import ThreadPoolExecutor # 创建线程池 thread_pool = ThreadPoolExecutor(5) # 向线程池中提交任务 future_task = pool.sbumit(task_function, args) 我们通过调用 future_task.done() 可以得知任务是否执行完成，通过调用 future_task.result() 可以得到任务的返回值。\n","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/python%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/","section":"技术杂项","summary":"","title":"Python 并发编程","type":"技术杂项"},{"content":" 简单叙述 # GIL 是 CPython(C 语言实现的 Python 解释器) 中一种用来实现多线程的机制，英文全称为 Global Interpreter Lock.\n历史缘由 # CPython 是 Python 官方实现的解释器，也是目前使用最广泛的 Python\n解释器。GIL 作为 CPython 中一个非常关键的机制，它的作用和局限性我们会在后面的详细论述。而追究起其存在的历史，应该说 GIL 机制是在 CPython 实现 Python 程序多线程的设计之初就已经存在的。至于采用 GIL 机制的缘由，官方文档里并没有详细说，只是提到，GIL 存在的必要性与 CPyhton 的内存管理机制是非线程安全的有主要关系(T_his lock is necessary mainly because CPython\u0026rsquo;s memory management is not thread-safe_)。而从 Jython(Java 语言实现的 Python 解释器)的官方文档里我们还可以得到一些蛛丝马迹：\nJython does not have the straightjacket of the GIL. This is because all Python threads are mapped to Java threads and use standard Java garbage collection support (the main reason for the GIL in CPython is because of the reference counting GC system).\n看来，GIL 机制的使用，还与 CPython 的另一非常重要的机制——垃圾回收机制有关。CPython 在实现 Python 的垃圾回收机制的时候，使用了一种叫引用计数(Reference Counting)的方法。而 CPython 之所以采用了 GIL 这样一种效率低下的方法，就是由于引用计数这种垃圾回收机制的限制。\n引用计数\n我们在创建一个对象的时候，可以把它理解为内存中的一个地址，而有多少个变量指向了这个地址，就可以理解为该对象有多少个引用，当这个地址没有任何变量指向它，即该对象没有任何引用的时候，便可以认定它已经是存在与内存中但不可能被用到的垃圾，就像是C语言中的野指针，再也不可能被后面的程序正常访问到，因此解释器就把其当作垃圾，而将该块内存回收了。\n引用计数的优点：\n1、实现简单\n2、实时性强：\n一旦没有引用，内存立马就被释放了。\n引用计数的缺点：\n1、维护引用计数消耗资源\n2、循环引用缺陷：\n比如两个对象互相引用，则引用计数永远不为零，所占用的内存也永远无法被回收。\n参考链接：\nReference Counting - Python 2.7.15 documentation\n[转载]Python垃圾回收机制\u0026ndash;完美讲解!\n机理和作用 # GIL(Global Interpreter Lock) 按字面意思来理解，就是解释器的一把全局锁。解释器中的任何一个线程 如果想要操作 Python 对象，就必须要先获得这把锁，以防止多个线程的 Python 代码同时被执行。而 CPython 的内存管理机制的非线程安全性，决定了 GIL 机制存在的必然。如果没有这把锁的限制，即使是最简单的操作，并行线程也可能导致灾难性的错误。比如说，当两个线程同时增加某一对象的引用计数，并发操作的话，可能最终这个对象的引用计数只增加了一次，而非预期的两次。\n因而，GIL 机制的存在保证了只有获得了全局锁的线程能够操作 Python 对象或者调用 Python/C API. 而为了保证线程的并行，解释器定期地在各个线程间切换。\n解释器还保存了一些线程相关的信息在一个名为 PyThreadState 的数据结构中。同时还有一个指向 PyThreadState 的全局变量，可以通过PyThreadState_Get() 获得。\n缺点 # GIL 机制因其使得 Python 程序无法真正发挥出多线程的性能优势而被人所诟病。\n某些场景下，在多核硬件上，两个线程调用同一个函数，与单线程调用该函数两次相比，前者的耗时不是后者的一半，也并非与后者持平，而是后者的近两倍！而单核设备上，这个差距能缩小一点，但仍是前者与后者有着不小的差距。这看起来很令人费解，下面这个链接详细解释了这个问题。\n参考链接：\n没有 GIL 的 Python 解释器 # 如前所述，GIL 是 CPython 中的一个特殊机制，并非所有 Python 解释器中都存在。Jython 和 IronPython 解释器中就没有 GIL 机制，并且能够充分发挥多线程的性能优势。\n因为 GIL 是 Python 官方解释器中的机制，以及其历史的悠久性，解释器中其他的很多特性都是基于该机制的，大量的 Python/C API 依赖于该特性，因此 CPython 的去 GIL 化是一个非常困难繁杂的工作。目前，该工作只是静静地躺在 Python 开发者的邮件列表中，偶尔被人提及，而没有人去认领。\nGetting rid of the GIL is an occasional topic on the python-dev mailing list. No one has managed it yet.\n","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/python%E4%B8%AD%E7%9A%84gil/","section":"技术杂项","summary":"","title":"Python 中的 GIL","type":"技术杂项"},{"content":" 虚拟 DOM # 我们知道，React 给我们创造了一个虚拟 DOM 的概念，它允许我们非常方便地用 JavaScript 来编写 DOM，React 会负责将这棵 DOM 树转化成真实的 DOM，并且更新它们的状态。\n那么我们就至少得有一个真实的 DOM 结点，它就是一个空的 root 结点。\n\u0026lt;div id=\u0026#34;root\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 这个 root 结点里的一切内容，将由 React 来操控。\n我们调用 ReactDOM.render()，传入 root 结点，和要绘制的 DOM 树，就可以将虚拟 DOM 绘制出来。\nReactDOM 会进行差异对比，只更新有变化的那一部分。\nDOM 转化 # 如下的 JSX 代码：\nconst element = ( \u0026lt;h1 className=\u0026#34;greeting\u0026#34;\u0026gt; Hello, world! \u0026lt;/h1\u0026gt; ); 会被转化成如下对象：\n// Note: this structure is simplified const element = { type: \u0026#39;h1\u0026#39;, props: { className: \u0026#39;greeting\u0026#39;, children: \u0026#39;Hello, world!\u0026#39; } }; 这就是 React 元素，它描述了要在屏幕上呈现什么。React-DOM 会读取这个对象，用其来构建 DOM，并且保持更新。\nProps and States # Props 只读 # 永远不在 component 内部去改变 props 的值。所有的 component，在它们的 props 面前，必须表现出纯函数的行为。\nState # State 是每个 component 自己维护的状态。component 可以根据自己的逻辑，去更新它。并且只有 component 自己可以更新自己的 state。而 state 的更新，会触发 DOM 绘制的更新。\nState 的使用要注意如下三件事：\n不能直接修改 this.state，而要使用 this.setState()\n// Wrong this.state.comment = \u0026#39;Hello\u0026#39;; // Correct this.setState({comment: \u0026#39;Hello\u0026#39;}); 你唯一能给 state 赋值的地方是构造函数 constructor()\nsetState() 是异步的\nsetState() 不会立即触发 DOM 的更新。React 有一个队列机制，它会将队列中的更新动作糅合成一个最终结果去更新。\n由于 this.props 和 this.state 是异步更新的，所以你不能依赖于它们的值去计算下一个状态值。\n比如像下面的代码是可能出错的：\n// Wrong this.setState({ counter: this.state.counter + this.props.increment, }); 正确的方法应该是下面这样：\n// Correct this.setState((state, props) =\u0026gt; ({ counter: state.counter + props.increment })); setState() 方法接受传入一个 function 作为参数。这个 function 的两个参数分别为 state 和 props，二者的值是对应的同一时刻的状态。\nsetState() 会对 state 对象进行 merge\n我们的 state 是一个 object，有很多的 object 成员变量。我们在调用 this.setState 的时候，不需要写全所有的变量，只写我们想要更新的即可。setState() 会 merge 进去。\n生命周期 # componentDidMount\n当我们的组件被绘制到 DOM 中了，该方法将被调用。这是一个适合于进行初始化的地方。\ncomponentWillUnmount\n当我们的组件将从 DOM 中销毁的时候，该方法将被调用。\n自上而下的数据流 # 组件应该尽量成为一个纯函数。父组件可以控制子组件的状态，而子组件则不应该控制父组件的状态。简单来说，我们应该把状态保存在父组件，而将父组件的状态和钩子函数（对状态的操作）传递给子组件，保持子组件的纯洁。\n屏蔽 DOM 元素的事件默认行为 # 在纯 HTML 中，我们可以通过返回 false 来屏蔽标签的事件默认行为。\n\u0026lt;form onsubmit=\u0026#34;console.log(\u0026#39;You clicked submit.\u0026#39;); return false\u0026#34;\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;Submit\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; 而在 react 中，我们要调用 preventDefault 方法。\nfunction Form() { function handleSubmit(e) { e.preventDefault(); console.log(\u0026#39;You clicked submit.\u0026#39;); } return ( \u0026lt;form onSubmit={handleSubmit}\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;Submit\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; ); } DOM 事件 # 在 react 中，我们没有必要调用 addEventListener 方法来添加 listener。直接添加事件处理函数就可以了。\nthis 的绑定 # You have to be careful about the meaning of this in JSX callbacks. In JavaScript, class methods are not bound by default. If you forget to bind this.handleClick and pass it to onClick, this will be undefined when the function is actually called.\nclass Toggle extends React.Component { constructor(props) { super(props); this.state = {isToggleOn: true}; } handleClick() { this.setState(prevState =\u0026gt; ({ isToggleOn: !prevState.isToggleOn })); } render() { return ( // This binding is necessary to make `this` work in the callback \u0026lt;button onClick={this.handleClick.bind(this)}\u0026gt; {this.state.isToggleOn ? \u0026#39;ON\u0026#39; : \u0026#39;OFF\u0026#39;} \u0026lt;/button\u0026gt; ); } } ReactDOM.render( \u0026lt;Toggle /\u0026gt;, document.getElementById(\u0026#39;root\u0026#39;) ); 如果你觉得在 class 组件中老是要绑定 this 太麻烦了，可以采用如下两种方法。\nclass LoggingButton extends React.Component { // This syntax ensures `this` is bound within handleClick. // Warning: this is *experimental* syntax. handleClick = () =\u0026gt; { console.log(\u0026#39;this is:\u0026#39;, this); } render() { return ( \u0026lt;button onClick={this.handleClick}\u0026gt; Click me \u0026lt;/button\u0026gt; ); } } class LoggingButton extends React.Component { handleClick() { console.log(\u0026#39;this is:\u0026#39;, this); } render() { // This syntax ensures `this` is bound within handleClick return ( \u0026lt;button onClick={() =\u0026gt; this.handleClick()}\u0026gt; Click me \u0026lt;/button\u0026gt; ); } } 但是，如上的方法是会有性能问题的。因为本质上，箭头函数是每次 render 的时候都创建了一个副本。如果这个函数作为 props 传给子组件，那么就相当于每次 render，这个 props 就会变，然后下面的子组件也跟着 render。所以如果考虑性能的话，最好还是老老实实 bind。\n控制组件的可见性 # 组件的可见性除了可以用 visible 属性来控制之外，还可以通过子组件通过条件判断返回 null 来实现。\nfunction WarningBanner(props) { // 通过 props.warn 来控制返回内容 if (!props.warn) { return null; } return ( \u0026lt;div className=\u0026#34;warning\u0026#34;\u0026gt; Warning! \u0026lt;/div\u0026gt; ); } class Page extends React.Component { constructor(props) { super(props); this.state = {showWarning: true}; this.handleToggleClick = this.handleToggleClick.bind(this); } handleToggleClick() { this.setState(state =\u0026gt; ({ showWarning: !state.showWarning })); } render() { return ( \u0026lt;div\u0026gt; // 如果 this.state.showWarning 是 false 的话，则 \u0026lt;WarningBanner/\u0026gt; 为 null \u0026lt;WarningBanner warn={this.state.showWarning} /\u0026gt; \u0026lt;button onClick={this.handleToggleClick}\u0026gt; {this.state.showWarning ? \u0026#39;Hide\u0026#39; : \u0026#39;Show\u0026#39;} \u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; ); } } ReactDOM.render( \u0026lt;Page /\u0026gt;, document.getElementById(\u0026#39;root\u0026#39;) ); 列表的 key # 由于列表这种数据结构只表征前后关系，而没有唯一标识。（下标是会变的，如果我们在列表中进行插入操作的话。）\n而 react 在渲染的时候，要实现只进行差异渲染，要明白到底列表中那个元素发生了变化，显然没有一个唯一标识是不行的。所以我们要给每个列表元素一个唯一的标识 key。前面说了，下标 index 是会变的，所以请不要用 index 当key。\nref # ref 用来获取自组件（标签）的状态。\nref 是一种比较 dirty 的功能，除非你是要去操作一些非 react 的元素，或者其它迫不得已的原因，否则不要用 ref，还是尽量保持自上而下的数据流。\nclass NameForm extends React.Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); // 创建一个 ref 对象 this.input = React.createRef(); } handleSubmit(event) { // 获取子组件状态 alert(\u0026#39;A name was submitted: \u0026#39; + this.input.current.value); event.preventDefault(); } render() { return ( \u0026lt;form onSubmit={this.handleSubmit}\u0026gt; \u0026lt;label\u0026gt; Name: // 绑定 ref \u0026lt;input type=\u0026#34;text\u0026#34; ref={this.input} /\u0026gt; \u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;Submit\u0026#34; /\u0026gt; \u0026lt;/form\u0026gt; ); } } state 提升 # 有时候，不同的子组件需要对同一数据做出反应。最好的方法是把他们共享的数据 state 提升到他们最近的公共父组件来，以满足自上而下的数据流模型。\n优化 # 动态 import\n// 之前 import { add } from \u0026#39;./math\u0026#39;; console.log(add(16, 26)); // 之后 import(\u0026#34;./math\u0026#34;).then(math =\u0026gt; { console.log(math.add(16, 26)); }); 如果使用了 webpack 的话，以上转换其实是会自动进行的。\nReact.lazy\n// 之前 import OtherComponent from \u0026#39;./OtherComponent\u0026#39;; // 之后 const OtherComponent = React.lazy(() =\u0026gt; import(\u0026#39;./OtherComponent\u0026#39;)); 使用 Suspense 来显示加载中。。。\nimport React, { Suspense } from \u0026#39;react\u0026#39;; const OtherComponent = React.lazy(() =\u0026gt; import(\u0026#39;./OtherComponent\u0026#39;)); function MyComponent() { return ( \u0026lt;div\u0026gt; \u0026lt;Suspense fallback={\u0026lt;div\u0026gt;Loading...\u0026lt;/div\u0026gt;}\u0026gt; \u0026lt;OtherComponent /\u0026gt; \u0026lt;/Suspense\u0026gt; \u0026lt;/div\u0026gt; ); } 使用 ErrorBoundary 来处理动态加载错误。\nErrorBoundary 就是指实现了 static getDerivedStateFromError() 或 componentDidCatch() 的组件。其中 static getDerivedStateFromError() 用来更新加载失败后的 state，以更新 UI。componentDidCatch() 用来记录错误信息。\nclass ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. return { hasError: true }; } componentDidCatch(error, errorInfo) { // You can also log the error to an error reporting service logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // You can render any custom fallback UI return \u0026lt;h1\u0026gt;Something went wrong.\u0026lt;/h1\u0026gt;; } return this.props.children; } } import React, { Suspense } from \u0026#39;react\u0026#39;; import ErrorBoundary from \u0026#39;./ErrorBoundary\u0026#39;; const OtherComponent = React.lazy(() =\u0026gt; import(\u0026#39;./OtherComponent\u0026#39;)); const AnotherComponent = React.lazy(() =\u0026gt; import(\u0026#39;./AnotherComponent\u0026#39;)); const MyComponent = () =\u0026gt; ( \u0026lt;div\u0026gt; \u0026lt;ErrorBoundary\u0026gt; \u0026lt;Suspense fallback={\u0026lt;div\u0026gt;Loading...\u0026lt;/div\u0026gt;}\u0026gt; \u0026lt;section\u0026gt; \u0026lt;OtherComponent /\u0026gt; \u0026lt;AnotherComponent /\u0026gt; \u0026lt;/section\u0026gt; \u0026lt;/Suspense\u0026gt; \u0026lt;/ErrorBoundary\u0026gt; \u0026lt;/div\u0026gt; ); 路由 # Context 上下文 # Context 给我们提供了一种方法，让我可以把数据传递给子组件树，而不用一层层地往下传 props。在传统的 react 模式中，数据流是自上而下的，但是对于某些大的数据来说（比如说语言偏好、主题），这显然是非常累赘的。context 正是提供了这样一个可以共享，却又不用层层传递的数据。\n// Context lets us pass a value deep into the component tree // without explicitly threading it through every component. // Create a context for the current theme (with \u0026#34;light\u0026#34; as the default). const ThemeContext = React.createContext(\u0026#39;light\u0026#39;); class App extends React.Component { render() { // Use a Provider to pass the current theme to the tree below. // Any component can read it, no matter how deep it is. // In this example, we\u0026#39;re passing \u0026#34;dark\u0026#34; as the current value. return ( \u0026lt;ThemeContext.Provider value=\u0026#34;dark\u0026#34;\u0026gt; \u0026lt;Toolbar /\u0026gt; \u0026lt;/ThemeContext.Provider\u0026gt; ); } } // A component in the middle doesn\u0026#39;t have to // pass the theme down explicitly anymore. function Toolbar() { return ( \u0026lt;div\u0026gt; \u0026lt;ThemedButton /\u0026gt; \u0026lt;/div\u0026gt; ); } class ThemedButton extends React.Component { // Assign a contextType to read the current theme context. // React will find the closest theme Provider above and use its value. // In this example, the current theme is \u0026#34;dark\u0026#34;. static contextType = ThemeContext; render() { return \u0026lt;Button theme={this.context} /\u0026gt;; } } React.createContext\n创建一个 context，当 react 渲染一个订阅了该 context 的组件，react 将从最近的 Provider 标签中读取当前该 context 的值。\nContext.Provider\n每一个 context 都有一个 Provider，它允许其子组件订阅该 context 的值。当 context 的值变化时，所有订阅了该 context 的组件也会更新渲染。\nClass.contextType\nclass MyClass extends React.Component { componentDidMount() { let value = this.context; /* perform a side-effect at mount using the value of MyContext */ } componentDidUpdate() { let value = this.context; /* ... */ } componentWillUnmount() { let value = this.context; /* ... */ } render() { let value = this.context; /* render something based on the value of MyContext */ } } MyClass.contextType = MyContext; 类属性 contextType 可以被赋值为一个 context 对象，这样你就可以通过 this.context 来使用该 context 对象。但是 contextType 仅允许你订阅一个 context 对象。要订阅多个 context，我们先介绍 Context.Consumer。\nContext.Consumer\nContext.Consumer 使我们更方便的订阅一个 context。\n\u0026lt;MyContext.Consumer\u0026gt; {value =\u0026gt; /* render something based on the context value */} \u0026lt;/MyContext.Consumer\u0026gt; 订阅多个 context：\n// Theme context, default to light theme const ThemeContext = React.createContext(\u0026#39;light\u0026#39;); // Signed-in user context const UserContext = React.createContext({ name: \u0026#39;Guest\u0026#39;, }); class App extends React.Component { render() { const {signedInUser, theme} = this.props; // App component that provides initial context values return ( \u0026lt;ThemeContext.Provider value={theme}\u0026gt; \u0026lt;UserContext.Provider value={signedInUser}\u0026gt; \u0026lt;Layout /\u0026gt; \u0026lt;/UserContext.Provider\u0026gt; \u0026lt;/ThemeContext.Provider\u0026gt; ); } } function Layout() { return ( \u0026lt;div\u0026gt; \u0026lt;Sidebar /\u0026gt; \u0026lt;Content /\u0026gt; \u0026lt;/div\u0026gt; ); } // A component may consume multiple contexts function Content() { return ( \u0026lt;ThemeContext.Consumer\u0026gt; {theme =\u0026gt; ( \u0026lt;UserContext.Consumer\u0026gt; {user =\u0026gt; ( \u0026lt;ProfilePage user={user} theme={theme} /\u0026gt; )} \u0026lt;/UserContext.Consumer\u0026gt; )} \u0026lt;/ThemeContext.Consumer\u0026gt; ); } Context.displayName\nContext.displayName 可以方便我们在 DevTools 中调试的时候区分 context。\nconst MyContext = React.createContext(/* some value */); MyContext.displayName = \u0026#39;MyDisplayName\u0026#39;; \u0026lt;MyContext.Provider\u0026gt; // \u0026#34;MyDisplayName.Provider\u0026#34; in DevTools \u0026lt;MyContext.Consumer\u0026gt; // \u0026#34;MyDisplayName.Consumer\u0026#34; in DevTools ","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/react%E6%9D%82%E8%AE%B0/","section":"技术杂项","summary":"","title":"React 杂记","type":"技术杂项"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":" 一句话阐述 # Web Worker 是 HTML5 提供的一个可突破 JavaScript 单线程限制的功能。其允许开发者创建线程， 并提供了与主线程之间的通信方法。\n概述 # 我们都知道 JavaScript 是单线程的。但是呢，浏览器是多线程（且多进程）的，并且 JavaScript 又支持异步和回调，似乎多线程在 JavaScript 中并非十分必要的需求。然而当前端页面需要做一些计算量大的工作（这些工作往往是背景工作），将其和负责响应 UI 的 JavaScript 主线程放在一起是显然不明智。其势必会拖慢主线程的执行，导致页面响应迟钝，影响用户体验。\nWeb workers 便在这种情境下产生。它允许开发者创建背景线程，然而这个线程与主线程不在同一上下文环境，他们的变量是不共享的，要实现两个线程间的通信和协作，则需使用 JavaScript 提供的消息通信方法。\nWeb workers 可分为 dedicated workers 和 shared workers。Dedicated workers 只能与创建它的脚本通信，shared workers 则可和多个脚本通信。\nDedicated workers # 我们先讨论 dedicated workers，由之可阐述 web worker 的绝大多数概念，而 shared workers 只是与 dedicated workers 有稍稍的不同。\n如前所述，dedicated workers 只能与创建它的脚本通信。\n创建 # Web worker 的创建非常简单：\n// main.js var myWorker = new Worker(\u0026#39;worker.js\u0026#39;); 如上代码中，我们在 main.js 脚本中创建了一个 dedicated worker，传入了 worker.js 作为其构造参数。前面说过，dedicated worker 只能与创建它的脚本通信。即，在本例中，我们只能在 main.js 中编写与 myWorker 通信的代码。\n通信 # JavaScript 提供了 postMessage 和 onmessage 方法来实现主线程与 worker 线程的通信。双方均用 postMessage 来发送消息，均用 onmessage 来接收消息（没搞明白为啥这两者的命名规则没统一）。因而主线程的 postMessage 和 worker 线程的 onmessage 是相对的，worker 线程的 postMessage 和主线程的 onmessage 是相对的。（这里有点像单片机的双机通信，接对发，发对接。）\n// main.js myWorker.postMessage([firstNumber, secondNumber]); myWorker.onmessage = function(e) { console.log(e.data); } // worker.js onmessage = function(e) { const sumValue = e.data[0] + e.data[1]; postMessage(sumValue); } 在本例中，主线程给 worker 线程发送了一个数组，并且定义了 onmessage 方法用于打印 worker 线程的返回的消息。worker 线程在收到主线程的消息后，将取出数组中的两个值并相加，之后将两数之和发回给主线程。这样我们便通过 worker 线程完成了两数相加的运算。（当然此处是大才小用，但是密集计算需求中，woker 线程非常必要。）\n值得注意的是，主线程和 worker 线程中传递数据的形式是消息通信，那么双方进行的是值传递，即使咱们在 postMessage 中传入的是引用类型，比如数组、对象，其传递的也是值，而非引用。实际上，引用数据在消息通信的过程中经历了序列化和反序列化的操作，即，引用数据在被传递出去时，会被序列化成串行数据，接收方接收后，再将其反序列化，构建成为一个拥有相同值的新变量。因而我们在 worker 线程中对变量做的任何操作，是不会在主线程中产生影响的。这很好理解，本身 worker 线程和主线程就不在同一上下文环境中，不然也不至于需要消息通信了。\n上下文环境 # 前面说过，worker 线程和主线程不在同一上下文环境中。在主线程中我们经常获取的 window 对象，在 worker 线程中直接使用是会报错的，你也不能直接在 worker 线程中操作 DOM 元素。但是，window 下的很多元素仍然可以使用，比如 WebSocket、IndexedDB 等。更多关于 web worker 中可使用的方法和类，可以点此查看。\n在 worker 脚本中 import 其他脚本 # 在 worker 脚本中通过 importScripts 方法可以 import 其他脚本。\nimportScripts(); /* imports nothing */ importScripts(\u0026#39;foo.js\u0026#39;); /* imports just \u0026#34;foo.js\u0026#34; */ importScripts(\u0026#39;foo.js\u0026#39;, \u0026#39;bar.js\u0026#39;); /* imports two scripts */ importScripts(\u0026#39;//example.com/hello.js\u0026#39;); /* You can import scripts from other origins */ 值得一提的题外话，JavaScript 中 import 的脚本，其加载（下载）完成的顺序是不定的，但是他们会严格按照 import 的顺序被执行。\n错误处理 # worker 线程执行出错时，主线程中的 myWorker.onerror 方法将被触发。（如果不想将错误抛给主线程，也可以通过 preventDefault 方法来阻止。）\n在 worker 线程中继续创建子线程 # worker 线程中可以和主线程一样继续创建子线程。\n待确认：worker 线程中创建的 dedicated 子线程是否只能与父级 worker 线程通信，或是也可以与主线程通信？\n销毁 # worker 线程的工作结束后，应当立即将其关闭，以节约系统资源。\n*/*/ 主线程 worker.terminate(); // Worker 线程 self.close(); Shared Workers # 不同于 dedicated workers，shared workers 可以与多个脚本进行通信，即使这些脚本属于不同的 window、iframe，甚至是 worker。\n创建 # Share workers 的创建与 dedicated workers 没有什么区别，只是构造方法的不同。\nvar myWorker = new SharedWorker(\u0026#39;worker.js\u0026#39;); 通信 # 由于要与不同的脚本通信，shared workers 在此处比 dedicated workers 要稍显复杂。无论是发送还是接收，都需要通过 port 对象来引用对应方法。我们在每次使用 new 方法创建一个 SharedWorker 对象时，其实都会分配一个 port，通过 port 可以区分不同脚本之间的通信信息。\n假设咱们的 worker.js 中有一个实现两数相乘的方法。我们的主线程中有两个脚本，multiple.js 和 square.js 分别需要通过 worker 线程实现乘法和平方（传入两个相同的乘数）。\n// multiple.js var myWorker = new SharedWorker(\u0026#34;worker.js\u0026#34;); myWorker.port.postMessage([firstNumber, secondNumber]); myWorker.port.onmessage = function(e) { console.log(\u0026#39;multiple: \u0026#39;, e.data); } // square.js var myWorker = new SharedWorker(\u0026#34;worker.js\u0026#34;); myWorker.port.postMessage([squareNumber, squareNumber]); myWorker.port.onmessage = function(e) { console.log(\u0026#39;square: \u0026#39;, e.data); } // worker.js onconnect = function(e) { var port = e.ports[0]; port.onmessage = function(e) { const workerResult = (e.data[0] * e.data[1]); port.postMessage(workerResult); } } 线程安全 # Worker 接口最终产生的是操作系统层面的真实线程。那么，有没有可能会引发线程安全问题呢？\n值得庆幸，根据 mozilla.org 的描述：几乎很难引发并发问题。因为 web workers 从一开始就是基于线程安全设计的，其整个运行过程不会使用到非线程安全的组件或者 DOM 元素。而 web worker 线程在运行时也像是一个仅有序列化数据输入输出的孤岛。因而「你必须在代码中费尽心思，才有可能成功引发线程安全问题」。\n在 umi 中的使用 # 前面都是从 JavaScript 的角度讲如何使用 web workers。在实际项目中，我们可能会用到各种开发框架，相对与手撸一个项目，封装了各种功能和配置的框架会让开发的效率更高。然而，得其利必然受其困，很多框架的配置并非那么的自由。在此以我常用的 umi 框架为例，介绍一下在 web workers 在其中的使用。\n当然，在 umi 中使用 web workers 和在纯 JavaScript 中是没有本质区别的。关键就在于如何在 umi 中配置 webpeck。\nUmi 的配置文件为 .umirc.ts，其中提供了 webpack-chain 配置项对 webpack 进行配置。\nchainWebpack: function(memo, { webpack }) { memo.module .rule(\u0026#39;compile\u0026#39;) .test(\u0026#39;/.worker.js$/\u0026#39;) .use(\u0026#39;worker\u0026#39;) .loader(\u0026#39;worker-loader\u0026#39;); memo.toString(); }, 根据配置， .worker.js 后缀的文件将被识别为 web worker 脚本，并使用 worker-loader 对其进行编译。\n配置好后，我们就可以在 umi 中正常使用 web workers 了。\n结尾 # 至此，web workers 的基本概念和使用就介绍完了。个人认为 web workers 这个东西，是为特殊需求而生的，有点违背了 JavaScript 当初设计的思想，当然，web workers 中的各种机制保障了线程安全并且 UI 主线程不会受影响，但这个东西还是给我一种畸形产物的感觉。所以，个人觉得如果你不知道 web workers 是什么东西，那你没有必要刻意去用它，如果你的页面响应出现卡顿了，那么先从其他方面找找问题，看看有没有不规范的，能优化的地方。\n","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/web_workers_%E6%A6%82%E8%BF%B0/","section":"技术杂项","summary":"","title":"Web Workers 概述","type":"技术杂项"},{"content":" Non-JavaScript # Most JS is written to run in and interact with environments like browsers.A good chunk of the stuff that you write in your code is, strictly speaking, not directly controlled by JavaScript.\nThe most common non-JavaScript JavaScript you\u0026rsquo;ll encounter is the DOM API. For example:\nvar el = document.getElementById( \u0026#34;foo\u0026#34; ); The document variable exists as a global variable when your code is running in a browser. It\u0026rsquo;s not provided by the JS engine, nor is it particularly controlled by the JavaScript specification. It takes the form of something that looks an awful lot like a normal JS object, but it\u0026rsquo;s not really exactly that. It\u0026rsquo;s a special object, often called a \u0026ldquo;host object.\u0026rdquo;\nMoreover, the getElementById(..) method on document looks like a normal JS function, but it\u0026rsquo;s just a thinly exposed interface to a built-in method provided by the DOM from your browser. In some (newer-generation) browsers, this layer may also be in JS, but traditionally the DOM and its behavior is implemented in something more like C/C++.\nSo does alert(..) and console.log(..).\nCompiler Theory # Despite the fact that JavaScript falls under the general category of \u0026ldquo;dynamic\u0026rdquo; or \u0026ldquo;interpreted\u0026rdquo; languages, it is in fact a compiled language. But not as other languages, JavaScript compilation doesn\u0026rsquo;t happen in a build step ahead of time. For JavaScript, the compilation that occurs happens, in many cases, mere microseconds (or less!) before the code is executed.\nEngine: responsible for start-to-finish compilation and execution of our JavaScript program. Compiler: one of Engine\u0026rsquo;s friends; handles all the dirty work of parsing and code-generation (see previous section). Scope: another friend of Engine; collects and maintains a look-up list of all the declared identifiers (variables), and enforces a strict set of rules as to how these are accessible to currently executing code. Statement var a = 2; will be proceed as:\nEncountering var a, Compiler asks Scope to see if a variable a already exists for that particular scope collection. If so, Compiler ignores this declaration and moves on. Otherwise, Compiler asks Scope to declare a new variable called a for that scope collection. Compiler then produces code for Engine to later execute, to handle the a = 2 assignment. The code Engine runs will first ask Scope if there is a variable called a accessible in the current scope collection. If so, Engine uses that variable. If not, Engine looks elsewhere (see nested Scope section below). If Engine eventually finds a variable, it assigns the value 2 to it. If not, Engine will raise its hand and yell out an error!\nScope # RHS and LHS\nRight-Hand-Search, for retrieve. Left-Hand-Search, for assignment.\nRHS and LHS behave differently in the circumstance where the variable has not yet been declared (is not found in any consulted Scope).\nIf an RHS look-up fails to ever find a variable, anywhere in the nested Scopes, this results in a ReferenceError being thrown by the Engine. It\u0026rsquo;s important to note that the error is of the type ReferenceError.\nIf an RHS look-up fails to ever find a variable, and if the program is not running in \u0026ldquo;Strict Modek, then the global Scope will create a new variable of that name in the global scope, and hand it back to Engine.\nLexical scope # There are two predominant models for how scope works. The first of these is by far the most common, used by the vast majority of programming languages. It\u0026rsquo;s called Lexical Scope, and we will examine it in-depth. The other model, which is still used by some languages (such as Bash scripting, some modes in Perl, etc.) is called Dynamic Scope.\nLexical scope is scope that is defined at lexing time. In other words, lexical scope is based on where variables and blocks of scope are authored, by you, at write time, and thus is (mostly) set in stone by the time the lexer processes your code.\nNo matter where a function is invoked from, or even how it is invoked, its lexical scope is only defined by where the function was declared.\nFunction Scope # The traditional way of thinking about functions is that you declare a function, and then add code inside it. But the inverse thinking is equally powerful and useful: take any arbitrary section of code you\u0026rsquo;ve written, and wrap a function declaration around it, which in effect \u0026ldquo;hides\u0026rdquo; the code.\nIn other words, you can \u0026ldquo;hide\u0026rdquo; variables and functions by enclosing them in the scope of a function.\nAnonymous vs. Named Function # Anonymous function expressions are quick and easy to type, and many libraries and tools tend to encourage this idiomatic style of code. However, they have several draw-backs to consider:\nAnonymous functions have no useful name to display in stack traces, which can make debugging more difficult. Without a name, if the function needs to refer to itself, for recursion, etc., the deprecated arguments.callee reference is unfortunately required. Another example of needing to self-reference is when an event handler function wants to unbind itself after it fires. Anonymous functions omit a name that is often helpful in providing more readable/understandable code. A descriptive name helps self-document the code in question. The best practice is to always name your function expressions.\nHoisting # Both function declarations and variable declarations are hoisted. But a subtle detail is that functions are hoisted first, and then variables.\nClosure Scope # function foo() { var a = 2; function bar() { console.log( a ); } return bar; } var baz = foo(); baz(); // 2 -- Whoa, closure was just observed, man. bar() is executed outside of its declared lexical scope.\nAfter foo() executed, normally we would expect that the entirety of the inner scope of foo() would go away, because we know that the Engine employs a Garbage Collector that comes along and frees up memory once it\u0026rsquo;s no longer in use. Since it would appear that the contents of foo() are no longer in use, it would seem natural that they should be considered gone.\nBut the \u0026ldquo;magic\u0026rdquo; of closures does not let this happen. That inner scope is in fact still \u0026ldquo;in use\u0026rdquo;, and thus does not go away. Who\u0026rsquo;s using it? The function bar() itself.\nbar() still has a reference to that scope, and that reference is called closure.\nfunction foo() { var a = 2; function baz() { console.log( a ); // 2 } bar( baz ); } function bar(fn) { fn(); // look ma, I saw closure! } We pass the inner function baz over to bar, and call that inner function (labeled fn now), and when we do, its closure over the inner scope of foo() is observed, by accessing a.\nWhatever facility we use to transport an inner function outside of its lexical scope, it will maintain a scope reference to where it was originally declared, and wherever we execute it, that closure will be exercised.\nThis # The key contrast: lexical scope is write-time, whereas dynamic scope (and this!) are runtime. Lexical scope cares where a function was declared, but dynamic scope cares where a function was called from.\nTo be clear, JavaScript does not, in fact, have dynamic scope. It has lexical scope. Plain and simple. But the this mechanism is kind of like dynamic scope.\nWhen a function is invoked, an activation record, otherwise known as an execution context, is created. This record contains information about where the function was called from (the call-stack), how the function was invoked, what parameters were passed, etc. One of the properties of this record is the this reference which will be used for the duration of that function\u0026rsquo;s execution.\nTo understand this binding, we have to understand the call-site: the location in code where a function is called (not where it\u0026rsquo;s declared). We must inspect the call-site to answer the question: what\u0026rsquo;s this this a reference to?\nfunction baz() { // call-stack is: `baz` // so, our call-site is in the global scope console.log( \u0026#34;baz\u0026#34; ); bar(); // \u0026lt;-- call-site for `bar` } function bar() { // call-stack is: `baz` -\u0026gt; `bar` // so, our call-site is in `baz` console.log( \u0026#34;bar\u0026#34; ); foo(); // \u0026lt;-- call-site for `foo` } function foo() { // call-stack is: `baz` -\u0026gt; `bar` -\u0026gt; `foo` // so, our call-site is in `bar` console.log( \u0026#34;foo\u0026#34; ); } baz(); // \u0026lt;-- call-site for `baz` ","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/you_dont_know_js/","section":"技术杂项","summary":"","title":"You don't know JS","type":"技术杂项"},{"content":"我是谁？\n这对我来说，一度是个严峻的问题。\n我姓H名Q，两个眼睛一个鼻子，中等身材，除此之外，并不知道如何来继续定义。\n人们称呼一个人，往往会提到他是做什么的，比如刘木匠、赵医生、王大厨。可见，人的存在，做什么事情很重要。\n我以前想做过很多事情。小学的时候我爆发出画画的梦想，在书上画，在墙上画，到公园去写生。后来不知为何，画着画着就去公园附近的新华书店。\n少儿读物和漫画区是小孩聚集的地方。但彼时我已有特立独行和故作高深的气质，径直跨过这两个区，开始在世界名著区徘徊。\n本该交给小卖部的钱交给了新华书店，换回了《一生》和《呼啸山庄》，我发现文字是比绘画更厚重的表达。文学梦想爆发了，开始写散文，写小说，投稿，然后石沉大海，那时我小学五年级。\n博客很快就在初中时兴起了。我和同学们频繁出入于网吧。几位同学一屁股坐下，挂上QQ, 启动酷狗音乐，最后打开梦幻西游。我一屁股坐下，挂上QQ, 启动酷狗音乐，最后打开新浪博客，我们一坐就是一天。\n我从小爱看电影，小时候跟着家斜对面影碟厅的小哥看香港无厘头、鬼片、僵尸片，以及王晶的三级片。就在当博客写手的日子里，我大概是在网吧看了一些不太娱乐化的片子。印象中曾经连续两个晚上通宵抓耳挠腮看了两遍《太阳照常升起》，未能参透。被李安的父亲三部曲打动，从电影中感受到了比文字更强的震撼。\n电影导演似乎是个遥远的行当，我仍然希冀在博客圈中有所建树。为了方便随时随地写博客，我去二手市场买了一台手机，通过彼时刚起步的WAP网页发布文章。谁料文章产出没有增多，手机却越换越频，甚至通过频繁交易赚起了差价。在UC浏览器中点开新浪博客的次数愈发的少，点开中关村和太平洋论坛的次数愈发的多。从琢磨手机操作系统又反过来折腾计算机。\n画家、作家、导演，短暂地幻想过这些或浪漫而难以生存、或简单却又艰辛、或遥远且飘渺的人生目标后，程序员这个脚踏实地又不产实物，仰望星空却琐碎庸碌，甚至或许带有某些宿命诅咒的身份找上了我。\n关于本站 # 本站以分享计算机技术为主，集中于算法与计算机图形学，添以其他奇技淫巧。\n此外也聊聊喜欢的电影和书籍，记录生活经历和感受。\n本站不会出现文章搬运，除特殊说明外，所发布内容均为原创。文章中的参考和引用会在末尾列出。\n某些技术文章会用英文写，之后会发布中文版。后续会考虑将本站点做成中英双语切换。\n若文章标题中出现（施工中）或（WIP）等标记，说明该文仍在撰写中。\n碎嘴一下 # 曾险些堕入虚无主义的黑暗，将博客全删了，并质问意义何在。幸从《阳光普照》《我与地坛》《活着为了讲述》中得药获救。暂不展开，留待后续。总之，人贵有臭美之心，旺德福，泰瑞宝，whatever.\n以下，列一些我喜欢的东西。\n书籍 阿城《棋王》 汪曾祺《受戒》 理查德·费曼 / 拉夫•雷顿《别闹了，费曼先生》 电影 钟孟宏《阳光普照》 李安《喜宴》 北野武《菊次郎的夏天》 乔纳森·戴顿 / 维莱莉·法瑞斯《阳光小美女》 塚本连平《我们与驻在先生的700日战争》 西拉维·休曼《疯狂约会美丽都》 大卫·芬奇《社交网络》 《白头神探》 电视剧 沈好放《贫嘴长大民的幸福生活》 管虎《外乡人》 《神秘博士》 游戏 Dark Souls Ⅰ, Dark Souls Ⅲ What Remains of Edith Finch Firewatch Life is strange 音乐 交工乐队《菊花夜行军》 罗大佑 My Little Airport 《年轻的茶餐厅老板娘》 Chinese Football 《守门员》 ","externalUrl":null,"permalink":"/%E5%85%B3%E4%BA%8E%E6%88%91/","section":"关于我","summary":"","title":"关于我","type":"关于我"},{"content":"","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/","section":"技术杂项","summary":"","title":"技术杂项","type":"技术杂项"},{"content":" 为什么使用 Taro ？ # Taro 是一套遵循 React 语法规范的多端开发解决方案，这意味着通过使用 Taro 编译工具，咱们可以编写一套代码，实现多端运行。比如微信小程序、支付宝小程序、web 端，甚至是 App 等。我们以微信小程序的实践为例进行讲解。它采用与 React 一致的组件化思想，支持使用 JSX 语法，支持 TypeScript，这可以使咱们的前端代码更加的优雅简洁。\n为什么使用 LeanCloud ？ # LeanCloud 是国内做得不错的一家 BaaS (Backend as a Service) 提供商。涵盖的面也非常的广，包括数据存储、文件存储、云引擎、容器、即时通讯、消息推送、短信、游戏云等等。通过使用 LeanCloud 的服务，我们可以极大地减轻后端的代码量，甚至达到无后端的效果。在本实践中，我们通过使用 LeanCloud 的即时通讯和数据/文件存储功能，便实现了不编写一条后端代码，就能完成一个可用的即时通讯小程序的效果。\nLeanCloud 在文档的完善度和可读性上也是国内做得比较好的。此外，LeanCloud 是个人认为可以算是互联网浪潮中气质比较清丽的一家公司，其官网的开放资源页面开放了一些产品之外的资源，包括文化价值、期权激励、薪酬体系、工作评价和反馈机制等等。其写道「我们相信开放和透明会使一家公司更具长期竞争力，所以我们尽可能把信息公开。让所有人了解我们、监督我们的同时，也希望这些信息可以为他人参考和借鉴，发挥最大的价值。」尤其是作为一家技术公司，其还收录了余光中先生对于中文表达的论述文章《余光中：怎样改进英式中文？- 论中文的常态与变态》，以作为文档风格的参考。由此可见公司对于产品气质的追求。\n前期准备 # 在写代码之前，我们需要准备开发环境，包括微信开发者环境、React 环境、Taro 环境等。LeanCloud 的开发环境我们暂且不用，当开发到这一阶段时再引入。\n微信开发者环境\n微信开发者环境的搭建非常简答，咱们按照官方文档的步骤完成帐号申请及开发者工具安装即可。\nTaro 开发环境\nnode 安装\nTaro 是基于 node 的，我们需要安装 ≥ 8.0.0 版本的 node.\n我们推荐使用 nvm 来进行 node 的安装。nvm 是一款 node 版本管理工具，它允许你同时安装多个版本的 node，并随时进行切换。\n参照如下的链接，可以完成 nvm 及 node 的安装：\n使用 nvm 安装 Nodejs\ntaro 安装\n安装好 node 后，我们就可以使用 npm 包管理工具了。\n通过如下命令，我们可以完成 taro 的安装：\n$ npm install -g @tarojs/cli 但是呢，npm 并不是特别的好用，尤其是在国内，速度感人（国内有 cnpm，然而坑更多）。我们可以先使用 npm 来安装 yarn，yarn 是 facebook 推出并开源的一款包管理工具，和 npm 相比，yarn 有着众多又是，其中最明显的便是速度快。然后我们让 yarn 来接替 npm 的一切工作：\n# 安装 yarn $ npm install -g yarn # 使用 yarn 来安装 taro yarn global add @tarojs/cli 创建项目 # 我们使用如下命令行，来创建一个名为 chattingDemo 的 taro 项目：\ntaro init chattingDemo 创建过程中会询问一些配置项。在询问是否使用 TypeScript 时，我们输入 Yes。在询问使用何种 CSS 预处理器时，我们选择 Sass。\n创建完成后，我们使用如下命令进行项目的实时编译：\n$ yarn dev:weapp --watch 之后我们打开微信开发者工具，导入项目，选择我们刚刚所创建项目的根目录。便能看到亲切的 hello world 了。\n使用任意一款的代码编辑器（此处使用 VS Code）打开项目目录，我们来看一下文件结构。\n最外层一共有四个文件夹，分别是配置文件夹 config、依赖文件夹 node_modules、代码文件夹 src，以及编译生成的文件夹 dist。而 src 中有用于存放不用页面的 pages 文件夹，我们创建项目时已经自动生成了一个首页，因此我们可以看到 pages 中有一个 index 文件夹，其中有着我们的 typescript 代码文件 index.tsx 以及样式文件 index.scss。\n此外，根目录下还有我们的入口文件 app.tsx 以及对应的样式文件 app.tsx。\n现在我们就进入 src 文件夹，开始我们的编码。\n页面的生命周期 # 打开 index.tsx 文件夹，我们发现 Taro 已经帮我们写了不少东西。\nimport Taro, { Component, Config } from \u0026#39;@tarojs/taro\u0026#39; import { View, Text } from \u0026#39;@tarojs/components\u0026#39; import \u0026#39;./index.scss\u0026#39; export default class Index extends Component { /** * 指定config的类型声明为: Taro.Config * * 由于 typescript 对于 object 类型推导只能推出 Key 的基本类型 * 对于像 navigationBarTextStyle: \u0026#39;black\u0026#39; 这样的推导出的类型是 string * 提示和声明 navigationBarTextStyle: \u0026#39;black\u0026#39; | \u0026#39;white\u0026#39; 类型冲突, 需要显示声明类型 */ config: Config = { navigationBarTitleText: \u0026#39;首页\u0026#39; } componentWillMount () { } componentDidMount () { } componentWillUnmount () { } componentDidShow () { } componentDidHide () { } render () { return ( \u0026lt;View className=\u0026#39;index\u0026#39;\u0026gt; \u0026lt;Text\u0026gt;Hello world!\u0026lt;/Text\u0026gt; \u0026lt;/View\u0026gt; ) } } 文件中声明了一个 Index 类（继承自 Taro 框架中的 Component 类），实现了 render 方法。在这里我们发现，在 render 方法里，我们直接返回了 HTML 标签。这就是 JSX 语法的妙处。它允许我们使用面向对象的方式来写 HTML。\n除了 render 方法之外，我们可以看到还有另外五个方法被实现了，虽然内容是空的。\n这个五个方法，对应了小程序页面生命周期的不同阶段。\n// 页面初次渲染前调用 componentWillMount () { } // 页面初次渲染后调用 componentDidMount () { } // 页面销毁前调用 componentWillUnmount () { } // 页面由后台切到前台后调用 componentDidShow () { } // 页面由前台切到后台后调用 componentDidHide () { } 总的来说，这个文件中的代码描述了页面要显示的内容，以及各个生命阶段要执行的操作。那么，操作的结果，如何对页面内容产生影响呢？\n","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/%E4%BD%BF%E7%94%A8taro_leancloud_%E5%BC%80%E5%8F%91%E4%B8%80%E6%AC%BE%E5%8D%B3%E6%97%B6%E9%80%9A%E8%AE%AF%E5%B0%8F%E7%A8%8B%E5%BA%8F/","section":"技术杂项","summary":"","title":"使用 Taro + LeanCloud 开发一款即时通讯小程序","type":"技术杂项"},{"content":"假定现有一个人力资源系统，页面如此：顶部一个 RadioGroup，有 3 个部门可以选择；下面是一个列表，显示所选部门的员工。列表中的数据通过网络获取（每切换一次部门，就会重新获取一次）。考虑到响应速度和带宽利用率，我们必须确保请求之间是异步的。假设用户频繁地发起了 3 次请求（切换了 3 个部门），由于接口返回数据的耗时不尽相同，第 1 次发起的请求可能晚于第 3 次请求返回。此时，我们的 RaidoGroup 显示第 3 部门，列表显示的却是第 1 部门的员工。这便是发生了请求竞态问题。\n其实竞态不止发生在请求间，凡是异步操作同一数据的（包括 DOM 元素），都有可能出现竞态问题。而网络请求的频繁程度、响应时长的不确定性，使其成为最常见的竞态场景。\n解决竞态问题也有很多方法，比如 Cancel Token 、AbortController，甚至是在响应中加入请求编号（当然，这很不优雅）。\n在此，我们讲解如何通过 React Hooks 和闭包来解决这个问题。\nReact Hooks # Hooks 是 React 16.8 加入的特性。这是一个重大的特性，不夸张地说，Hooks 的出现，改变了你我在使用 React 时的思维方式。\nHooks 的诸多优点和特性，我们不在此讲解。仅谈我们要用到的部分—— useEffect。\nimport React, { useState, useEffect } from \u0026#39;react\u0026#39;; export default () =\u0026gt; { const [employees, setEmployees] = useState([]); useEffect(() =\u0026gt; { queryEmployees(currentDepartment) // 这场发起一个网络请求 .then(res =\u0026gt; { setEmployees(res); }) }, [currentDepartment]) ****return( \u0026lt;div\u0026gt;balabala\u0026lt;/div\u0026gt; ) } useEffect 接受两个参数，第一个是要执行的方法（暂且称之为目标方法），第二个是条件数组。\n不传入条件数组的话，useEffect 的目标方法默认会在每次页面重新渲染（包括初次渲染）的时候被执行。在此处我们传入了条件数组[currentDepartment]，则 React 会在每次渲染的时候检查 currentDepartment的值，若相较上一次渲染发生了变化，目标方法则会被执行。\n咱们再来看目标方法，很简单，发起了一个网络请求，传入了要查询的部门，接收到返回值后通过 setEmployees 方法（这是一个 React 的 State Hook 方法）更新员工数据。\n可以看到，当每一次 currentDepartment 更新时，都会发起一个网络请求，各个请求返回后，都会调用 setEmployees 方法。而请求间返回的顺序不定，便造成结果与预期不符。\n💡 尽管读者大抵已经对 Hooks 有较为清晰的了解，为了确保文章完整性，在此还是对 State Hook 稍作说明。我们把目光聚焦到 `setEmployees` 方法，前面说了，每一个请求返回，都会执行这个方法，在上面的代码中，我们无法看到 `setEmployees` 的内部实现，但是既然这个方法会造成竞争冲突，其必然操作了至少这么一个变量，这个变量的作用域是贯穿每一次 `setEmployees` 执行的。它就是 `employees` 变量。在代码的第 5 行，我们通过 `useSate` 定义了一个 state 变量 `employees`，以及一个用来操作它的方法 `setEmployees`。React 会负责保存 state 变量的值，因而我们在各个请求返回结果中调用 `setEmployees` 时，他们更新的是同一个 `employees` 的值。 闭包 # 接下来开始讨论闭包的话题了。闭包并不是一个多么复杂的概念，但它确实是一个有着强大用处，且在使用中很容易出错的功能。再加上闭包这个名字的确不太直观，因而易使人望而生畏。\n闭包本质上来说是一个作用域的问题。\n几个关键概念我们先明确以下：\n内部方法使用外部作用域中变量的行为，称之为闭包 只有方法才会产生闭包，没有方法就没有闭包（此处方法是指内部方法，而非指外部作用域。外部作用域可以是一个外部方法的作用域，也可以是一个普通的作用域——比如花括号可以构成一个作用域——甚至全局作用域。） 闭包不是对外部变量的值快照，而是真实访问，可进行值更新等操作 可以出现多个方法对同一外部作用域变量产生闭包，他们共享对同一变量的访问 概念或许过于抽象，我们来看代码。\nfunction makeCounter() { var count = 0; return function getCurrent() { count = count + 1; return count; }; } var hits = makeCounter(); hits(); // 1 hits(); // 2 hits(); // 3 如上，调用 makeCounter，其会返回一个 getCurrent 方法。执行完 var hits = makeCounter(); 后，makeCounter 方法便结束了，按理说，其 count 变量应当被回收，而 getCurrent 方法作为返回值，返回给了 hits，当我们调用 hits 时，也就调用了 getCurrent 方法。然而有趣的是，getCurrent中直接使用了 count 变量，正常逻辑，我们调用 getCurrent 的时候，count 应该已经被回收了。之所以我们还能顺利地使用它，并且更新它，完成计数功能，正是由于闭包的存在。\n💡 虽然闭包是基于作用域的，但是不同于作用域在编译的时候被处理，闭包是一种运行时行为。 当我们的内部方法(getCurrent)使用了外部作用域(makeCounter)中的变量(count)时，闭包便形成了。此后，当 makeCounter 执行完，count 并不会被回收，直到 getCurrent 也不被引用了，count 才和 getCurrent 一起被回收。\n解决竞态问题 # Hooks 和闭包都介绍了，怎么通过它们来解决竞态问题呢？我们对一开始的那个代码做一下改造。\nimport React, { useState, useEffect } from \u0026#39;react\u0026#39;; export default () =\u0026gt; { const [employees, setEmployees] = useState([]); useEffect(() =\u0026gt; { let didCancel = false; queryEmployees(currentDepartment) // 这场发起一个网络请求 .then(res =\u0026gt; { if (!didCancel) { setEmployees(res); } }) return () =\u0026gt; { didCancel = true; } }, [currentDepartment]) ****return( \u0026lt;div\u0026gt;balabala\u0026lt;/div\u0026gt; ) } 所有的改动都在 useEffect 的目标方法内，我们新加了一个 didCancel 变量，当其不为 true 时，才会进行 setEmployees 操作。然后我们又给目标方法增加了一个返回值，其返回一个方法（注意，这里其实也是一个闭包！），此方法将 didCancel 赋值成了 true 。\n这里有一个 Effect Hook 的关键内容我们前面没讲，那就是目标方法的返回值（也是一个方法）。这个被返回的方法，被称之为 cleanup 方法。在已经理解了闭包的基础上，我们只要理解了cleanup，也就理解了竞态问题的解决原理。\n我们可以看到，在目标方法中，queryEmployees 和 cleanup 方法形成了两个闭包，这两个闭包使用了同一个 didCancel 变量。也就是说，cleanup 中改变 didCancel 的值，会影响 queryEmployees 的执行结果。另外，由于每一次画面重新渲染，useEffect 都会产生一个新的目标方法（注意，useEffect 方法本身又是一个闭包！），每个目标方法又会产生一个新的属于自己的 let didCancel = false; 变量，因而 didCancel 是不会在目标方法之间产生影响的。\n那么问题就来了，连续发出三次请求，就会产生三个目标方法，每一个目标方法都有自己的 didCancel ，而每一个 didCancel 又只会影响本目标方法的执行。那，我们要处理的竞态，是目标方法和目标方法之间的问题，我们是如何做到后面的请求，能够影响前面的请求，使得前面的请求返回不被处理呢？\n一切的秘密就在 cleanup 里。\n我们对目标方法里的内容很明了，一个变量 didCancel，和两个使用这个变量的闭包(queryEmployees 和 cleanup)。queryEmployees 会根据 didCancel 来决定是否更新员工列表，而 cleanup 则会修改 didCancel 的值。由此可见，如果 cleanup 的执行先于 queryEmployees 的响应返回的话，则返回的员工数据不会更新到 employees 变量中。那么关键就在于，cleanup 何时被执行呢？\nCleanup 会在下面两种情况下被执行：\n组件销毁的时候 下一次将要调用此 effect 时 咱们把注意力集中在第二种情况。\n看明白了没，咱们此次触发的 effect，其 cleanup 什么时候执行，取决于下一次触发此 effect 的时间！也就是说，在咱们这个查询员工的例子里，当用户点击触发一次查询后，这个查询的 cleanup 的执行时间，取决于用户什么时候触发下一次查询。如果用户 5 秒后才触发下一次查询，那么这个 cleanup 就会在 5 秒后才执行，这个时候咱们此次的查询数据大抵早已返回了；如果用户在半秒内即触发了第二次查询，那么 cleanup 即在半秒内触发，此时咱们的响应可能还没返回，等它返回的时候，didCancel 已经被 cleanup 置为 true 了，这时我们不更新员工数据，因为这个响应在还未返回前已经被后面的请求给取消了；如果用户不触发下一次请求，那么它的逻辑和 5 秒是一样的，只是比 5 秒更长了，长到此组件被销毁的时候，才会被 cleanup。\n闭包是 JavaScript 里面不太起眼却又十分强大的功能，而 Hooks 则可以看作是 React 对于闭包的一个集大成的使用。闭包和 Hooks 都不是那么好掌握的，并不是因为它的原理有多么复杂，只是其不那么符合直觉，因而需要更多的实践经验。但是一旦能够掌握好闭包和 React Hooks，开发的效率将会倍增。\n","externalUrl":null,"permalink":"/%E6%8A%80%E6%9C%AF%E6%9D%82%E9%A1%B9/%E9%80%9A%E8%BF%87_react_hooks_%E5%92%8C%E9%97%AD%E5%8C%85%E6%9D%A5%E8%A7%A3%E5%86%B3%E8%AF%B7%E6%B1%82%E7%AB%9E%E6%80%81%E9%97%AE%E9%A2%98/","section":"技术杂项","summary":"","title":"通过 React Hooks 和闭包来解决请求竞态问题","type":"技术杂项"},{"content":"goto 回溯法\n楔子：写在前面的话\n","externalUrl":null,"permalink":"/posts/%E6%A5%94%E5%AD%90/","section":"Posts","summary":"","title":"楔子","type":"posts"}]