1. 实现代码

ts
function maxArea(height: number[]): number {
    const n = height.length;
    let left = 0;
    let right = n - 1;
    let bestArea = 0;

    while (left < right) {
        const leftHeight = height[left];
        const rightHeight = height[right];

        bestArea = Math.max(
            bestArea,
            Math.min(leftHeight, rightHeight) * (right - left)
        );

        if (leftHeight <= rightHeight) {
            left++;
        } else {
            right--;
        }
    }

    return bestArea;
}

时间复杂度为 O(n),额外空间复杂度为 O(1)。高度相等时,移动任意一侧都可以。

2. 笔记

2.1 多变量声明

一个 let 声明可以包含多个变量,但每个变量都需要分别写自己的初始化表达式,逗号分隔的是完整的变量声明项。

ts
const n = 10;
let left = 0, right = n - 1;

以下写法只初始化了 right:

ts
let left, right = 0;

left 没有初始化表达式,执行声明后其值为 undefined;right 被初始化为 0。

工程中通常更倾向于每行声明一个变量,并在声明时初始化:

ts
const n = 10;
let left = 0;
let right = n - 1;

这种写法更便于阅读、修改,也方便为各个变量添加注释。

2.2 JavaScript 标准库 API 与内置对象

Math、数组和字符串的方法等,通常称为 JavaScript 标准库 API。更具体地说,Math 是标准内置对象,max、min 是它的方法。

TypeScript 沿用 JavaScript 的这些运行时能力,并提供相应的类型检查。调用方式取决于 API 所属的位置:

ts
Math.max(3, 8);       // 内置对象的方法
nums.push(5);         // 数组实例的方法
parseInt("42", 10);   // 全局函数

max、min 没有作为全局函数提供,所以需要写 Math.max(...)、Math.min(...)。Math 可以直接使用,无需导入,也无需通过 new 创建。

2.3 常见的 Math 方法

写法作用结果
Math.max(3, 8, 5)最大值8
Math.min(3, 8, 5)最小值3
Math.abs(-3)绝对值3
Math.floor(2.8)向下取整2
Math.ceil(2.2)向上取整3
Math.trunc(-2.8)截去小数部分,向零取整-2
Math.round(2.6)取最近的整数,恰好居中时取较大的整数3

Math.floor 与 Math.trunc 对负数的处理不同:

ts
Math.floor(-2.8); // -3
Math.trunc(-2.8); // -2

Math.max、Math.min 可以接收多个数字。已有数组时,可以使用展开语法将数组元素作为各个参数传入:

ts
const nums = [3, 8, 5];

Math.max(...nums); // 8,相当于 Math.max(3, 8, 5)
Math.min(...nums); // 3,相当于 Math.min(3, 8, 5)

2.4 写题时常见的其他标准库 API

2.4.1 数组 Array<T>
操作常见写法返回值与注意事项
创建空数组const nums: number[] = []元素类型为 number
创建并填充new Array<number>(n).fill(0)返回长度为 n、全部填为 0 的数组;单独 new Array(n) 不会填零
按下标生成Array.from({ length: n }, (_, i) => i)返回 [0, 1, ..., n - 1]
创建二维数组Array.from({ length: rows }, () => new Array<number>(cols).fill(0))每行是独立数组;用 .fill(同一个数组) 会让各行共享引用
长度nums.length元素个数;是属性,不加 ()
读取、修改元素nums[i]、nums[i] = value下标从 0 开始;越界读取为 undefined;修改会影响原数组
尾部追加nums.push(value)修改原数组,返回新长度;可一次追加多个元素
尾部删除nums.pop()修改原数组,返回末尾元素;空数组返回 undefined
头部插入nums.unshift(value)修改原数组,返回新长度;通常需要移动已有元素,耗时 O(n)
头部删除nums.shift()修改原数组,返回首元素;空数组返回 undefined;通常耗时 O(n)
截取nums.slice(start, end)返回新数组,包含 start、不包含 end;原数组不变,复制是浅复制
浅复制[...nums]、nums.slice()返回新数组;如果元素是对象,仍然共享那些对象
删除、插入、替换nums.splice(start, deleteCount, ...items)修改原数组,返回被删除元素组成的数组;中间增删通常耗时 O(n)
查找值nums.indexOf(value)返回首次出现的下标,找不到返回 -1;通常耗时 O(n)
判断包含nums.includes(value)返回 boolean;通常耗时 O(n)
按条件查找nums.find(x => x > 0)、nums.findIndex(x => x > 0)分别返回首个匹配元素或其下标;找不到时分别为 undefined、-1
数字排序nums.sort((a, b) => a - b)原地升序,返回原数组;降序使用 b - a;默认排序按字符串顺序比较
反转nums.reverse()原地反转,返回原数组
映射nums.map(x => x * 2)返回映射后的新数组
筛选nums.filter(x => x > 0)返回满足条件的元素组成的新数组
累积nums.reduce((sum, x) => sum + x, 0)示例返回总和;0 是累积初值,空数组时也能返回结果
条件判断nums.some(x => x > 0)、nums.every(x => x > 0)返回是否存在匹配元素、是否全部匹配;空数组分别返回 false、true
连接为字符串nums.join(",")返回字符串,原数组不变
遍历for (const x of nums) { ... }依次取得元素,可用 break、continue
同时遍历下标与元素for (const [i, x] of nums.entries()) { ... }每轮取得 [下标, 元素]
清空nums.length = 0修改原数组,其他共享该数组的引用也能看到清空结果
2.4.2 栈:使用数组实现
操作常见写法返回值与注意事项
创建const stack: number[] = []JS/TS 标准库没有独立的 Stack 类;用数组尾部作为栈顶
入栈stack.push(value)修改数组,返回新长度;通常均摊 O(1)
出栈stack.pop()返回并删除栈顶;空栈返回 undefined;通常 O(1)
查看栈顶stack[stack.length - 1]不删除元素;空栈得到 undefined
元素个数stack.length当前栈大小
判空stack.length === 0返回 boolean
2.4.3 队列:使用数组与队头下标实现
操作常见写法返回值与注意事项
创建const queue: number[] = []; let head = 0JS/TS 标准库没有独立的 Queue 类;用 head 表示当前队头
入队queue.push(value)追加到队尾;通常均摊 O(1)
出队const value = queue[head++]先确认 head < queue.length;读取队头并推进下标,避免移动剩余元素
查看队头queue[head]不出队;空队列得到 undefined
有效元素个数queue.length - head已出队元素仍保留在底层数组中,不能只用 .length 表示队列大小
判空head === queue.length在每次出队前判空、避免越界推进下标的前提下使用
处理所有入队元素while (head < queue.length) { const value = queue[head++]; ... }循环内部可以继续 push(),适合 BFS 等场景
简单出队方式queue.shift()另一种实现方式:不用 head,直接删除首元素;通常耗时 O(n),大量出队时不优先采用;不要与队头下标方案混用
重置queue.length = 0; head = 0清空底层数组并重置队头;仅推进 head 不会释放数组保存的已出队元素引用
2.4.4 哈希映射 Map<K, V>
操作常见写法返回值与注意事项
创建const counts = new Map<string, number>()保存键与值的对应关系
从键值对创建new Map<string, number>([["a", 1], ["b", 2]])返回包含初始条目的 Map
元素个数counts.size键值对数量;是属性,不加 ()
插入、更新counts.set(key, value)修改 Map,返回 Map 自身;已有键会覆盖原值
读取counts.get(key)返回对应值,找不到返回 undefined;不是 counts[key]
判断键存在counts.has(key)返回 boolean;即使允许保存 undefined,也能区分键是否存在
计数加一counts.set(key, (counts.get(key) ?? 0) + 1)不存在时按 0 处理,再将新次数写回
删除counts.delete(key)修改 Map;删除成功返回 true,键不存在返回 false
清空counts.clear()删除全部条目,返回 undefined
遍历键值对for (const [key, value] of counts) { ... }按键的插入顺序遍历;也可使用 counts.entries()
遍历键、值counts.keys()、counts.values()返回可用于 for...of 的迭代器
转成数组Array.from(counts.keys())、Array.from(counts.values())返回键数组、值数组;Array.from(counts) 返回键值对数组
键的比较counts.set(objectKey, value)对象键按身份区分,不按内容比较;另一个内容相同的对象不是同一个键
查找性能get()、has()、set()、delete()标准要求平均访问耗时低于线性;具体复杂度取决于引擎实现
2.4.5 哈希集合 Set<T>
操作常见写法返回值与注意事项
创建const seen = new Set<number>()保存不重复的值
从数组创建、去重new Set(nums)返回集合,重复值只保留一份
元素个数seen.size不重复的元素数量;是属性,不加 ()
添加seen.add(value)修改 Set,返回 Set 自身;重复添加不会增加大小
判断存在seen.has(value)返回 boolean
删除seen.delete(value)修改 Set;删除成功返回 true,值不存在返回 false
清空seen.clear()删除全部元素,返回 undefined
遍历for (const value of seen) { ... }按插入顺序遍历;没有按位置读取的 seen[i]
转成数组Array.from(seen)、[...seen]返回包含集合元素的新数组;[...new Set(nums)] 可用于数组去重
元素的比较seen.add(objectValue)对象按身份区分,内容相同的不同对象仍可同时存在;Map、Set 都将 NaN 视为与自身相等,+0、-0 视为相等
查找性能has()、add()、delete()标准要求平均访问耗时低于线性;具体复杂度取决于引擎实现
2.4.6 字符串 string
操作常见写法返回值与注意事项
长度text.lengthUTF-16 编码单元数量;对英文字母、数字可直接按常见字符计数理解
按下标读取text[i]返回该位置的字符串,越界为 undefined;不能用下标修改字符串
读取编码text.charCodeAt(i)返回 UTF-16 编码单元值;例如 c.charCodeAt(0) - "a".charCodeAt(0) 计算小写字母下标
截取text.slice(start, end)包含 start、不包含 end,返回字符串
判断包含text.includes(part)返回 boolean
查找位置text.indexOf(part)首次出现的位置,找不到返回 -1
判断前缀、后缀text.startsWith(prefix)、text.endsWith(suffix)返回 boolean
拆分text.split(",")按分隔符返回字符串数组;英文字母场景可用 text.split("") 拆成字符数组
拼接a + b返回拼接后的字符串;字符串不可变,text += part 会给变量重新赋值
去除两端空白text.trim()返回处理后的字符串
大小写转换text.toLowerCase()、text.toUpperCase()返回转换后的字符串
替换text.replace("a", "b")、text.replaceAll("a", "b")字符串模式下分别替换首个、全部匹配,返回处理后的字符串
遍历for (const c of text) { ... }按 Unicode 码点遍历;并不总与 .length、下标访问采用相同单位
2.4.7 数字检查与类型转换
操作常见写法返回值与注意事项
转成数字Number("42")返回数字 42;转换失败通常为 NaN,空字符串转为 0
转成字符串String(42)返回字符串 "42"
解析整数parseInt("101", 2)按指定进制解析,示例为 5;可以只解析有效前缀,如 parseInt("12px", 10) 为 12
判断整数Number.isInteger(value)返回 boolean,不先把字符串转成数字
判断有限数值Number.isFinite(value)排除 NaN、正负无穷,也不做类型转换
判断 NaNNumber.isNaN(value)判断值是否为 NaN;不能用 value === NaN 判断
2.4.8 其他常见算法数据结构
数据结构标准库支持与常见表示使用要点
双端队列 Deque无独立内置类;数组可组合 push()、pop()、unshift()、shift()数组头部增删通常为 O(n);单调队列等大量操作场景可自行实现环形缓冲区等结构
堆、优先队列无标准内置类;通常用数组自行实现二叉堆,或使用题目平台明确提供的类没有通用标准的 PriorityQueue API,需按实现或平台文档使用
链表无通用内置链表类;通常由题目提供 ListNode,或自行定义节点通过 node.val、node.next 访问,具体字段和类型以定义为准
二叉树无通用内置二叉树类;通常由题目提供 TreeNode通过 node.val、node.left、node.right 访问;注意空节点
图的邻接表编号连续时可用 number[][],任意键时可用 Map<number, number[]>每个节点对应一个邻居数组,使用数组或 Map 的 API 构建、遍历
并查集无内置类;常用 parent: number[]、size: number[] 等数组表示find、union 需要自行实现,不是标准库函数

学习 API 时,重点确认参数是什么、返回什么、是否修改原对象。例如,slice() 返回新数组,sort() 修改原数组,这会直接影响程序行为。