阅读视图

发现新文章,点击刷新页面。

一次彻底搞懂 Four Sum(四数之和):排序 + 双指针的终极形态

LeetCode 18|中等
核心思想:排序 + 枚举 + 双指针 + 去重
本文目标:让你不仅写得出,还能一眼看穿这一类题


一、题目描述

给定一个整数数组 nums 和一个目标值 target,判断数组中是否存在 四个不同下标 的元素,使得它们的和等于 target,并返回所有 不重复的四元组

示例:

nums = [1,0,-1,0,-2,2], target = 0
输出:
[
  [-2,-1,1,2],
  [-2,0,0,2],
  [-1,0,0,1]
]

二、暴力解法为什么行不通?

最直观的思路是四重循环:

for a
  for b
    for c
      for d

时间复杂度:

O(n^4)

n 稍微一大,直接超时。这也是 Four Sum 这道题存在的意义:逼你系统性掌握降维思路


三、核心优化思路:降维 + 双指针

观察等式:

a + b + c + d = target

我们可以把它拆成:

(a + b) + (c + d) = target

思路就非常清晰了:

  1. 固定前两个数 ab
  2. 剩下的 c + d双指针(Two Sum) 解决
  3. 通过排序解决双指针和去重问题

这正是 Two Sum → Three Sum → Four Sum 的统一解题套路。


四、完整 Java 实现

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> res = new ArrayList<>();
        int n = nums.length;
        if (n < 4) return res;

        Arrays.sort(nums);

        // 第一层枚举 a
        for (int i = 0; i < n - 3; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) continue;

            // 第二层枚举 b
            for (int j = i + 1; j < n - 2; j++) {
                if (j > i + 1 && nums[j] == nums[j - 1]) continue;

                int left = j + 1;
                int right = n - 1;

                // 双指针找 c + d
                while (left < right) {
                    long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];

                    if (sum == target) {
                        res.add(Arrays.asList(
                                nums[i], nums[j], nums[left], nums[right]
                        ));

                        while (left < right && nums[left] == nums[left + 1]) left++;
                        while (left < right && nums[right] == nums[right - 1]) right--;

                        left++;
                        right--;
                    } else if (sum < target) {
                        left++;
                    } else {
                        right--;
                    }
                }
            }
        }
        return res;
    }
}

五、为什么一定要排序?

排序在这道题中起到了决定性作用

  1. 让双指针成为可能
  2. 为去重提供天然条件
  3. 让结果具备统一顺序,避免重复解

可以说:没有排序,Four Sum 根本写不干净。


六、去重逻辑是整道题的灵魂

Four Sum 的难点不在算法,而在 不重不漏

1. 第一层去重(a)

if (i > 0 && nums[i] == nums[i - 1]) continue;

避免相同的 a 被重复枚举。


2. 第二层去重(b)

if (j > i + 1 && nums[j] == nums[j - 1]) continue;

注意这里是 j > i + 1,而不是 j > 0,这是很多人第一次写时会踩的坑。


3. 双指针去重(c、d)

while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;

只在 找到一个合法解之后 去重,这是关键。


七、为什么 sum 要用 long?

long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];

原因只有一个:防止整数溢出

  • nums[i] 可能是 10^9
  • 四个 int 相加很容易溢出

这是 Four Sum 的经典细节坑,面试和刷题都很爱考。


八、时间与空间复杂度分析

时间复杂度:

O(n^3)
  • 两层枚举:O(n²)
  • 内层双指针:O(n)

空间复杂度:

O(1)(不计结果集)

九、Four Sum 的本质总结

这一类题的本质其实非常统一:

题目 固定元素个数 剩余解法
Two Sum 0 双指针 / 哈希
Three Sum 1 双指针
Four Sum 2 双指针

一句话总结:

固定 k − 2 个数,把 k Sum 问题降维为 Two Sum。

移动端 Web 开发学习笔记:从视口到流式布局,一篇带你真正入门

在开始移动端 Web 开发之前,我一直以为「移动端就是把 PC 页面缩小」。
真正系统学完一轮之后才发现,视口、像素比、二倍图、布局方案,每一个点都决定了页面体验的好坏。

这篇文章是我在学习 移动端 Web 开发(流式布局方向) 时的完整总结,适合刚从 PC 端转向移动端,或者对移动端概念比较模糊的同学。


一、移动端开发的现状与特点

1. 移动端浏览器现状

移动端常见浏览器包括:

  • UC 浏览器
  • QQ 浏览器
  • 百度手机浏览器
  • Safari(iOS)
  • Chrome(Android)

一个非常重要的事实是:

国内主流移动端浏览器,基本都基于 WebKit 内核。

这意味着:

  • H5 + CSS3 可以放心使用
  • 私有前缀重点关注 -webkit-
  • 不需要像 PC 时代那样被 IE 折磨

2. 屏幕碎片化问题

移动设备的分辨率极其碎片化,例如:

  • Android:720×1280、1080×1920、2K、4K
  • iPhone:750×1334、1242×2208 等

但作为前端开发者:

不需要纠结 dp、dpi、ppi 等概念
我们关心的是:
CSS 像素如何映射到设备屏幕。


二、视口(Viewport)是移动端的第一道门槛

很多移动端布局问题,根源都在 视口没理解清楚

1. 三种视口的区别

1)布局视口(Layout Viewport)

  • 浏览器默认的布局宽度
  • 大多数移动端默认是 980px
  • 用来兼容早期 PC 页面

这也是为什么 不加 viewport 标签时,页面会整体缩小


2)视觉视口(Visual Viewport)

  • 用户当前看到的区域
  • 可以通过手指缩放改变
  • 不影响布局视口

3)理想视口(Ideal Viewport)

  • 设备最理想的展示宽度
  • 设备有多宽,布局就有多宽
  • 移动端开发真正想要的视口

2. 标准 viewport 写法(必须掌握)

<meta 
  name="viewport" 
  content="
    width=device-width,
    initial-scale=1.0,
    maximum-scale=1.0,
    minimum-scale=1.0,
    user-scalable=no
  "
>

这段代码的作用是:

  • 布局视口等于设备宽度
  • 默认不缩放
  • 禁止用户手动缩放

这是移动端页面的标配


三、二倍图与像素比:为什么图片会模糊

1. 物理像素 vs CSS 像素

在 Retina 屏幕下:

  • 1 个 CSS 像素 ≠ 1 个物理像素
  • iPhone 常见的是 2 倍、3 倍像素比

例如:

  • 页面上显示 50×50
  • 实际需要 100×100 或 150×150 的图片资源

2. 二倍图的解决方案

图片本身使用更大的尺寸,但在 CSS 中压缩显示。

img {
  width: 50px;
  height: 50px;
}

如果原图是 100×100,就能在 Retina 屏上保持清晰。


3. 背景图的处理方式

.box {
  background-image: url(bg@2x.png);
  background-size: 50px 50px;
}

关键点在于:

  • 图片是二倍图
  • background-size 写成设计稿尺寸

四、移动端开发方案选择

1. 两种主流方案

方案一:单独制作移动端页面(主流)

  • m.jd.com
  • m.taobao.com

特点:

  • 专门为手机设计
  • 性能和体验最好
  • 维护成本较高

方案二:响应式页面

  • 同一套 HTML
  • 通过媒体查询适配不同设备

缺点很明显:

  • 样式复杂
  • 维护成本高
  • 移动端体验一般

实际项目中,大厂基本都选择第一种方案。


五、移动端技术解决方案汇总

1. CSS 初始化推荐 normalize.css

相比 reset.css,normalize.css 的优点是:

  • 保留有价值的默认样式
  • 修复浏览器差异
  • 模块化、文档完善

2. CSS3 盒子模型(移动端强烈推荐)

* {
  box-sizing: border-box;
}

优点:

  • padding、border 不会撑大盒子
  • 布局更直观
  • 非常适合移动端

3. 移动端常见特殊样式

* {
  -webkit-tap-highlight-color: transparent;
}

input,
button {
  -webkit-appearance: none;
}

img,
a {
  -webkit-touch-callout: none;
}

这些样式可以:

  • 去掉点击高亮
  • 统一表单样式
  • 禁止长按弹出菜单

六、移动端常见布局方式

1. 流式布局(百分比布局)

流式布局的核心思想:

  • 宽度使用百分比
  • 随屏幕变化自适应
.container {
  width: 100%;
}

.item {
  width: 50%;
}

2. 限制最大最小宽度(常用技巧)

body {
  min-width: 320px;
  max-width: 640px;
  margin: 0 auto;
}

好处是:

  • 防止页面过窄或过宽
  • 常用于电商移动端页面

七、总结

通过这一阶段的学习,我对移动端 Web 开发有了几个明确的认知:

  • 视口是移动端布局的基础
  • 二倍图是清晰显示的关键
  • 流式布局是最常用的移动端布局方式
  • CSS3 盒模型极大降低了布局复杂度
  • 移动端开发更注重体验和性能

如果你正准备从 PC 端转向移动端,这些内容几乎是必经之路

一次吃透「移除元素」:双指针的最朴素、也最重要的形态

LeetCode 27|简单
核心思想:快慢指针(双指针)
关键词:原地修改、覆盖、不关心顺序


一、题目回顾

给你一个数组 nums 和一个值 val,你需要 原地移除所有等于 val 的元素,并返回移除后数组的新长度。

要求:

  • 不使用额外数组
  • 不关心新数组后面的内容
  • 返回的是“有效元素的长度”

示例:

nums = [3,2,2,3], val = 3
返回 2
nums 前 2 个元素为 [2,2]

二、这道题最容易踩的坑

很多人一开始会纠结:

  • “删元素后数组怎么变?”
  • “是不是要真的把数组缩短?”
  • “后面的值要不要清空?”

但题目其实已经偷偷告诉你答案了:

你只需要保证前 k 个元素是正确的,后面是什么不重要。

这一点非常关键。


三、核心思路:双指针在“做什么”?

我们用两个指针:

  • right:扫描整个数组(读)
  • left:指向下一个“应该被保留下来的位置”(写)

它们的分工非常明确:

right 负责看每一个元素,left 只在“遇到合法元素”时前进。


四、代码实现(Java)

class Solution {
    public int removeElement(int[] nums, int val) {
        int n = nums.length;
        int left = 0;

        for (int right = 0; right < n; right++) {
            if (nums[right] != val) {
                nums[left] = nums[right];
                left++;
            }
        }

        return left;
    }
}

五、逐行拆解这段代码的“真实含义”

1. left 指针的意义

int left = 0;

left 永远指向:

下一个可以放“有效元素”的位置

它不是在找 val,也不是在删除元素,而是在“维护结果数组的长度”。


2. right 指针在干嘛?

for (int right = 0; right < n; right++) {

right 是一个纯扫描指针

  • 从头到尾看一遍数组
  • 不回头、不跳跃
  • 保证每个元素只看一次

3. 为什么只在 != val 时才赋值?

if (nums[right] != val) {
    nums[left] = nums[right];
    left++;
}

这一步非常精髓:

  • 如果当前元素是 val

    • 直接跳过,相当于“删掉”
  • 如果不是 val

    • 把它覆盖写到 left 的位置
    • left 前进,表示有效长度 +1

注意:这是“覆盖”,不是交换。


六、用一个具体例子走一遍

输入:

nums = [0,1,2,2,3,0,4,2]
val = 2

执行过程:

right nums[right] 是否保留 left nums 前部
0 0 1 [0]
1 1 2 [0,1]
2 2 2 [0,1]
3 2 2 [0,1]
4 3 3 [0,1,3]
5 0 4 [0,1,3,0]
6 4 5 [0,1,3,0,4]
7 2 5 [0,1,3,0,4]

最终返回 left = 5


七、为什么这道题不需要交换?

有些双指针题会用“左右交换”,但这里不需要,原因是:

  • 题目不要求保留原有顺序之外的任何信息
  • 我们只关心“哪些值留下”
  • 覆盖写入比交换更简单、更稳定

这类写法也被称为:

慢指针构造结果,快指针遍历输入


八、时间与空间复杂度

  • 时间复杂度:O(n)

    • 每个元素只看一次
  • 空间复杂度:O(1)

    • 原地修改,没有额外数组

九、这道题在整个双指针体系里的位置

这是最基础、最干净的双指针模型:

  • 没有排序
  • 没有边界博弈
  • 没有复杂条件

但它是下面这些题的“地基”:

  • 移除重复元素
  • 移动零
  • 有效数组长度类问题
  • 原地过滤问题

一句话总结它的思想:

用一个指针遍历世界,用另一个指针构造答案。


十、最后的小总结

这道题的难点不在代码,而在观念转变:

  • 不要执着于“删除”
  • 把问题转化为“保留什么”
  • 用指针去描述“状态变化”

当你真正理解了这道题,
后面很多数组题都会突然变得顺眼。

❌