从 LeetCode 到 this:深入理解算法复杂度与 JavaScript 上下文机制

40 阅读6分钟

在前端开发中,我们常常需要同时处理两个维度的问题:数据结构与算法的效率,以及 JavaScript 的执行上下文(this)行为。本文将围绕三个核心部分展开:LeetCode 第 88 题《合并两个有序数组》的最优解法、时间与空间复杂度的本质分析,以及 this 在不同调用场景下的指向问题。通过这三个主题,我们将打通算法思维与语言特性的理解。


一、LeetCode 第 88 题:合并两个有序数组

题目要求

给你两个按非递减顺序排列的整数数组 nums1nums2,另有两个整数 mn,分别表示 nums1nums2 中的有效元素数目。
请你 原地 合并 nums2nums1 中,使合并后的数组同样按非递减顺序排列。
注意:nums1 的长度为 m + n,其中后 n 个位置是预留的空位(初始值为 0)。

常见解法 vs 最优解法

❌ 普通双指针(从前向后)

一种直观的想法是使用两个指针分别从两个数组开头开始比较,把较小的元素放入一个新数组。但这样会额外占用 O(m+n) 的空间,不符合“原地”要求。

✅ 三指针从后向前(最优解)

function merge(nums1, m, nums2, n) {
  let i = m - 1;        // 指向 nums1 最后一个有效元素
  let j = n - 1;        // 指向 nums2 最后一个元素
  let k = m + n - 1;    // 指向 nums1 整体末尾(合并后的位置)

  // 从后往前比较,避免覆盖未处理的数据
  while (i >= 0 && j >= 0) {
    if (nums1[i] > nums2[j]) {
      nums1[k] = nums1[i];
      i--;
    } else {
      nums1[k] = nums2[j];
      j--;
    }
    k--;
  }

  // 如果 nums2 还有剩余,全部复制到 nums1 前面
  while (j >= 0) {
    nums1[k] = nums2[j];
    j--;
    k--;
  }
}

为什么从后往前?
因为 nums1 后面有 n 个空位,不会覆盖尚未处理的有效数据。如果从前向后合并,一旦把 nums1[0] 移走,后续元素就可能被覆盖,导致数据丢失。

为什么只处理 nums2 的剩余?
如果 nums1 有剩余(即 i >= 0j < 0),说明这些元素已经在正确位置,无需移动。而如果 nums2 有剩余,说明 nums1 已被完全处理,剩下的 nums2 元素都比 nums1 中最小的还小,应放在最前面。

该解法时间复杂度为 O(m+n)空间复杂度为 O(1) ,完美满足题目要求。


二、时间与空间复杂度:如何评价一个算法?

时间复杂度:衡量执行时间的增长趋势

时间复杂度不是精确的运行时间,而是随着输入规模 n 增大,操作次数的增长速率。我们关注的是主导项,忽略常数和低阶项。

示例 1:单层循环

function traverse(arr) {
  var len = arr.length;         // T(1)
  for (var i = 0; i < len; i++) { // i=0: T(1); i<len: T(n+1); i++: T(n)
    console.log(arr[i]);        // T(n)
  }
}
// 总操作数 T(n) = 1 + 1 + (n+1) + n + n = 3n + 3 → O(n)

示例 2:嵌套循环

for (var i = 0; i < outlen; i++) {
  for (var j = 0; j < inlen; j++) {
    console.log(arr[i][j]);
  }
}
// 若为 n×n 矩阵,则 T(n) ≈ 3n² + 5n + 1 → O(n²)

示例 3:对数时间

for (var i = 1; i < len; i = i * 2) {
  console.log(arr[i]);
}
// i 每次翻倍,循环次数为 log₂n → O(log n)

空间复杂度:衡量额外内存占用

关键原则

  • 输入数据不计入空间复杂度(如函数参数 arr)。
  • 只计算算法运行过程中临时申请的额外空间

示例:O(1) vs O(n)

// O(1):仅使用几个变量
function traverse(arr) { /* ... */ }

// O(n):显式创建新数组
function init(n) {
  var arr = []; // 新开辟 O(n) 空间
  for (var i = 0; i < n; i++) arr[i] = i;
  return arr;
}

常见复杂度排序(从小到大)

O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ)

为什么复杂度重要?
在处理大规模数据时,O(n²) 的算法可能需要数小时,而 O(n log n) 只需几秒。理解复杂度能帮助我们在设计阶段就规避性能陷阱。


三、JavaScript 中的 this:作用域与调用上下文

this 是 JavaScript 中最容易混淆的概念之一。它的值不是由定义位置决定,而是由调用方式决定

经典问题:setTimeout 中的 this 指向谁?

<script>
var name = 'windowName';
var a = {
  name: 'Cherry',
  func1: function() {
    console.log(this.name);
  },
  func2: function() {
    console.log(this); // → a 对象
    setTimeout(function() {
      console.log(this); // → window!
      this.func1();      // 报错:window.func1 is not a function
    }, 1000);
  }
};
a.func2();
</script>

原因分析

  • a.func2() 被调用时,this 指向 a
  • setTimeout 的回调是一个普通函数,在全局作用域中执行,因此 this 指向 window(非严格模式)。

三种解决方案

方案 1:使用 bind(推荐)

func2: function() {
  setTimeout(function() {
    this.func1();
  }.bind(a), 1000); // 返回一个新函数,this 永久绑定为 a
}

bind 不会立即执行,而是返回一个绑定了 this 的函数,适合用于异步回调。

方案 2:缓存 this(经典技巧)

func2: function() {
  var that = this; // 保存外部 this
  setTimeout(function() {
    that.func1(); // 通过闭包访问
  }, 1000);
}

✅ 利用闭包和作用域链,安全可靠,兼容性好。

方案 3:箭头函数(ES6+)

func2: function() {
  setTimeout(() => {
    console.log(this); // → a 对象!
    this.func1();
  }, 1000);
}

✅ 箭头函数没有自己的 this,它会继承外层作用域的 this
⚠️ 注意:箭头函数也不能作为构造函数(无 arguments、不能用 new)。

为什么 call/apply 不行?

setTimeout(function() { this.func1(); }.call(a), 1000);

.call(a)立即执行该函数,并将返回值(这里是 undefined)传给 setTimeout
结果:1 秒后执行 undefined,毫无效果。

✅ 验证:

setTimeout(function() {
  this.func1();
  return function() { console.log('hahaha'); }
}.call(a), 1000);

此时 setTimeout 接收到的是返回的匿名函数,1 秒后会打印 'hahaha',但 this.func1() 已在 .call 时同步执行了。


总结

  1. 算法层面

    • 合并有序数组时,利用“从后向前”的三指针策略,可实现 O(1) 空间复杂度。
    • 复杂度分析要抓住“主导项”,区分输入空间与额外空间。
  2. 语言层面

    • this 的指向由调用方式决定,而非定义位置。
    • 异步回调中 this 易丢失,可通过 bind、缓存 this 或箭头函数解决。
    • call/apply 会立即执行,不适合用于 setTimeout 回调绑定。

掌握这些核心原理,不仅能写出高效的代码,还能在面试和实际项目中游刃有余。希望本文能帮你打通算法与语言特性的任督二脉!

延伸思考:在 React 中,为什么类组件的方法要 bind(this)?这正是 this 丢失问题的典型应用场景。理解本质,方能举一反三。