LeetCode 283:移动零 TypeScript 笔记
记录本题的最终实现、函数参数与返回值的传递机制,以及解构赋值、闭包、严格相等和命名习惯。
1. 实现代码
双指针,并将交换操作拆成独立函数,通过数组和下标修改原数组。
function swap(nums: number[], i: number, j: number): void {
[nums[i], nums[j]] = [nums[j], nums[i]];
}
function moveZeroes(nums: number[]): void {
const n = nums.length;
let currentSlot = 0;
let processingIndex = 0;
while (processingIndex < n) {
if (nums[processingIndex] !== 0) {
swap(nums, currentSlot, processingIndex);
currentSlot++;
}
processingIndex++;
}
}时间复杂度为 O(n),额外空间复杂度为 O(1)。函数直接修改传入的数组,不需要返回新数组。
2. 笔记
2.1 参数传递:变量重新赋值与对象内容修改
TypeScript 沿用 JavaScript 的运行时语义,函数参数采用按值传递(pass by value)。形参也是变量,调用函数时,程序用实参的值初始化形参,这与用一个变量的值初始化另一个变量遵循相同的规则。
number、string、boolean 等属于原始值。以数字变量为例:
let a = 1;
let b = a;
b = 2;
console.log(a); // 1
console.log(b); // 2b = a 将 a 保存的数字值复制给 b。两个变量分别保存自己的值,随后执行 b = 2,只让 b 保存新的数字值,a 仍然保存 1。原始值不可变:数字 1 本身不会变成数字 2,赋值改变的是变量保存哪个值。数字传给函数后,给形参重新赋值也只会改变形参自己的值。
数组、普通对象等都属于对象。每个对象具有自己的身份,变量保存的是访问对象的引用。对象与保存引用的变量是分开的,多个变量可以通过各自的引用访问同一个对象。
传入对象时,复制的是对象引用,对象本身不会被复制,因此形参与调用者的变量最初访问的是同一个对象。此后,需要区分两种操作:
- 通过形参修改对象内容:沿着形参保存的引用找到对象,再修改对象的元素或属性。对象发生了变化,其他访问同一个对象的变量也能观察到变化。
- 给形参重新赋值:改变形参保存的引用,让它访问另一个对象。调用者的变量仍然引用原对象,原对象的内容不会因这次重新赋值而改变。
function update(items: number[]): void {
// 此时:items → 数组 A,nums → 数组 A
// 修改数组 A 的内容,数组 A 变为 [99, 2]
items[0] = 99;
// 创建数组 B,让 items 保存数组 B 的引用
// 执行后:items → 数组 B,nums → 数组 A
items = [7, 8];
// 修改数组 B 的内容,数组 A 不受影响
items[0] = 100;
console.log(items); // [100, 8],数组 B
}
// 创建数组 A,让 nums 保存它的引用
const nums = [1, 2];
update(nums);
console.log(nums); // [99, 2],数组 Aitems[0] = 99 修改的是引用所指向的对象内容;items = [7, 8] 修改的是变量保存的引用。
const 约束的是变量不能被重新赋值,不会自动冻结对象。因此,示例中的 nums 不能被重新赋值,但其数组元素仍然可以修改。
2.2 多值返回:使用元组描述返回结果
函数每次调用返回一个值,这个值可以是包含多个元素的数组。TS 可以用元组类型描述各个位置的类型:
function swapped(a: number, b: number): [number, number] {
return [b, a];
}
const [x, y] = swapped(1, 2);
console.log(x); // 2
console.log(y); // 1[number, number] 是元组类型,描述一个包含两个数字的位置结构。元组在运行时仍然是数组,类型标注用于静态检查。
返回值也遵循相同的值语义:返回原始值,调用者得到该原始值;返回对象,调用者得到访问该对象的引用,不会自动复制对象。
本题可以采用两种接口:swap(nums, i, j) 直接修改数组,返回类型为 void;swapped(a, b) 返回交换后的结果,由调用者负责赋值。最终实现采用前一种。
2.3 解构声明、解构赋值与交换
解构可以将数组中的元素按位置分配给多个变量。
const [x, y] = swapped(1, 2);给已有变量赋值时,不重复声明变量:
let a = 1;
let b = 2;
[a, b] = swapped(a, b);直接交换两个变量可以写成:
[a, b] = [b, a];右侧先读取交换前的 b 和 a,随后才向左侧赋值,因此不会丢失其中一个原值。
解构赋值的目标也可以是数组元素:
[nums[i], nums[j]] = [nums[j], nums[i]];对于简单变量,解构赋值仍然遵守 let、const 的约束:已有的 const 变量不能被重新赋值;通过 const 数组变量修改元素则是允许的。
2.4 箭头函数与闭包
箭头函数是 TS 中常用于表达 lambda 的函数写法:
const add = (a: number, b: number): number => a + b;函数体只有一个需要返回的表达式时,可以省略花括号和 return。使用花括号时,返回值需要显式写 return:
const add = (a: number, b: number): number => {
return a + b;
};闭包是函数与其所能访问的外层词法环境的组合。函数可以访问定义位置的外层变量,并在需要时保留这种访问能力。普通函数和箭头函数都可以形成闭包。
例如,将交换函数定义在 moveZeroes 内部,就可以访问外层的 nums:
function moveZeroes(nums: number[]): void {
const swap = (i: number, j: number): void => {
[nums[i], nums[j]] = [nums[j], nums[i]];
};
const n = nums.length;
let currentSlot = 0;
let processingIndex = 0;
while (processingIndex < n) {
if (nums[processingIndex] !== 0) {
swap(currentSlot, processingIndex);
currentSlot++;
}
processingIndex++;
}
}这里的 swap 只接收下标,数组通过外层作用域取得。它访问的是定义位置的外层变量,不是任意调用者的局部变量。
函数也能直接修改外层可写的原始值变量:
let count = 0;
const increment = (): void => {
count++;
};
increment();
console.log(count); // 1这里赋值的目标就是外层的 count,不涉及通过形参写回调用者变量。闭包访问的是变量绑定,不是定义函数时冻结的一份值快照。
2.5 ===、!==:严格相等与对象身份
=== 和 !== 分别是严格相等与严格不相等运算符,比较时不会为了统一类型而进行隐式类型转换。!== 的结果始终是 === 结果的取反。
对于原始值,比较依据是类型及该类型的值规则。类型不同,=== 返回 false;类型相同时,数字按数值比较,字符串按字符序列比较,布尔值按 true、false 比较。数字中的两个特殊规则是:NaN 与自身严格不相等,+0 与 -0 严格相等。
对于对象,比较依据是对象身份,不会递归比较内容。两个引用访问同一个对象时严格相等;分别创建的两个对象,即使内容相同,也严格不相等。
const a = [1, 2];
const b = a;
const c = [1, 2];
console.log(a === b); // true:访问同一个数组
console.log(a === c); // false:访问两个不同的数组
console.log(a !== c); // true== 和 != 使用允许隐式类型转换的比较规则。下面展示 JavaScript 的运行时结果:
0 == "0"; // true
0 === "0"; // false
0 !== "0"; // true2.6 命名规则与变量声明习惯
TS 通常采用以下命名习惯:
| 用途 | 常见风格 | 示例 |
|---|---|---|
| 变量、参数、函数、属性、方法 | camelCase | currentSlot、processingIndex、moveZeroes |
| 类型别名、接口、类 | PascalCase | ListNode、UserProfile |
| 部分固定常量 | UPPER_SNAKE_CASE | MAX_RETRIES |
没有重新赋值的变量优先使用 const,需要重新赋值的变量使用 let。本题中,n 使用 const,两个不断移动的下标使用 let。