【大厂真题曝光】:腾讯/阿里/字节跳动JS算法面试题TOP10深度解析

第一章:JS算法面试题库概览

JavaScript 算法面试题是前端与全栈岗位考核中的核心内容,主要考察候选人对数据结构、逻辑思维和代码优化能力的掌握程度。掌握高频题型和解题模式,有助于在有限时间内快速准确地完成编码任务。

常见考察方向

  • 数组操作:如去重、扁平化、查找配对等
  • 字符串处理:回文判断、子串搜索、正则应用
  • 递归与动态规划:斐波那契、爬楼梯、背包问题
  • 树与图的遍历:二叉树DFS/BFS、路径总和
  • 排序与搜索:手写快排、二分查找实现

典型题目示例

以下是一个常见的两数之和问题及其解法:
/**
 * 给定一个整数数组 nums 和一个目标值 target
 * 返回两个数的索引,使它们相加之和等于 target
 */
function twoSum(nums, target) {
  const map = new Map(); // 存储值与索引的映射
  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];
    if (map.has(complement)) {
      return [map.get(complement), i]; // 找到匹配对,返回索引
    }
    map.set(nums[i], i); // 将当前值和索引存入哈希表
  }
  return []; // 未找到结果时返回空数组
}

// 示例调用
console.log(twoSum([2, 7, 11, 15], 9)); // 输出: [0, 1]

题库分类参考

难度等级推荐掌握题量典型知识点
简单30题数组遍历、字符串基础操作
中等50题哈希表、双指针、递归
困难20题动态规划、图论、状态机
graph TD A[开始] --> B{输入数据} B --> C[选择合适的数据结构] C --> D[设计算法逻辑] D --> E[边界条件处理] E --> F[返回结果]

第二章:高频基础算法题深度解析

2.1 数组去重与扁平化:理论分析与最优解实现

核心思想与应用场景
数组去重与扁平化是数据预处理中的关键操作,广泛应用于前端状态管理、API 数据清洗等场景。去重需保证元素唯一性,扁平化则将多维数组转为一维。
去重的高效实现
使用 Set 结构结合扩展运算符可实现去重:
const unique = arr => [...new Set(arr)];
该方法时间复杂度为 O(n),适用于基本类型数组。
递归扁平化与性能优化
深度扁平化可通过递归实现:

const flatten = arr => 
  arr.reduce((acc, val) => 
    Array.isArray(val) ? acc.concat(flatten(val)) : acc.concat(val), []
  );
此实现兼容任意嵌套层级,逻辑清晰,但可进一步通过栈模拟优化递归深度问题。

2.2 字符串反转与回文判定:多种解法对比实战

双指针法实现字符串反转

使用双指针从字符串两端向中心靠拢,交换字符,时间复杂度为 O(n),空间复杂度为 O(1)。

func reverseString(s []byte) {
    left, right := 0, len(s)-1
    for left < right {
        s[left], s[right] = s[right], s[left]
        left++
        right--
    }
}

该方法原地修改字符数组,避免额外内存分配,适用于大规模数据处理场景。

回文判定的扩展应用

基于反转逻辑,可高效判断回文串。常见解法包括:

  • 完全反转后比较原始字符串
  • 双指针逐位比对,提前终止非回文情况
  • 利用语言内置函数简化代码(如 Python 的切片)
性能对比分析
方法时间复杂度空间复杂度
双指针比对O(n)O(1)
字符串反转比较O(n)O(n)

2.3 斐波那契数列的递归与动态规划优化实践

朴素递归实现及其缺陷
斐波那契数列定义为:F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)。最直观的实现是递归:
def fib_recursive(n):
    if n <= 1:
        return n
    return fib_recursive(n-1) + fib_recursive(n-2)
该方法逻辑清晰,但存在大量重复计算,时间复杂度为O(2^n),当n较大时性能急剧下降。
动态规划优化策略
采用自底向上的动态规划思想,将中间结果存储避免重复计算:
def fib_dp(n):
    if n <= 1:
        return n
    dp = [0] * (n+1)
    dp[1] = 1
    for i in range(2, n+1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]
此方法时间复杂度降为O(n),空间复杂度为O(n)。进一步可优化空间至O(1),仅保留前两项值完成状态转移。

2.4 快速排序与归并排序的手写实现与性能剖析

快速排序的实现与原理

快速排序采用分治策略,通过选择基准元素将数组划分为左右两部分,递归排序。


public static void quickSort(int[] arr, int low, int high) {
    if (low < high) {
        int pivot = partition(arr, low, high); // 分区操作
        quickSort(arr, low, pivot - 1);
        quickSort(arr, pivot + 1, high);
    }
}
private static int partition(int[] arr, int low, int high) {
    int pivot = arr[high]; // 选最后一个元素为基准
    int i = low - 1;
    for (int j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            i++;
            swap(arr, i, j);
        }
    }
    swap(arr, i + 1, high);
    return i + 1;
}

partition 函数将小于等于基准的元素移到左侧,返回基准最终位置。平均时间复杂度为 O(n log n),最坏为 O(n²)。

归并排序的稳定优势

归并排序同样使用分治法,但具有稳定的 O(n log n) 时间复杂度。

  • 将数组从中间分割,递归排序左右子数组
  • 合并两个有序数组为一个有序数组
  • 需要额外 O(n) 空间存储临时数组

2.5 二分查找的边界处理与典型应用场景

边界条件的精准控制
二分查找在实现时,边界的开闭处理直接影响结果正确性。常见模式为左闭右开 [left, right),循环条件为 left < right,更新时 mid = left + (right - left) / 2 避免溢出。
func binarySearch(nums []int, target int) int {
    left, right := 0, len(nums)
    for left < right {
        mid := left + (right - left)/2
        if nums[mid] == target {
            return mid
        } else if nums[mid] < target {
            left = mid + 1
        } else {
            right = mid
        }
    }
    return -1
}
该实现确保搜索区间始终合法, right 不包含,避免死循环。
典型应用场景
  • 在有序数组中查找目标值的插入位置
  • 寻找旋转排序数组中的最小值
  • 求解满足条件的最左或最右边界

第三章:数据结构类真题精讲

3.1 手动实现链表及常见操作(增删查)

链表是一种动态数据结构,通过节点引用串联数据。每个节点包含数据域和指向下一个节点的指针。
节点定义与基础结构
在Go中,链表节点可定义为:
type ListNode struct {
    Val  int
    Next *ListNode
}
该结构体包含当前值 Val 和指向下一节点的指针 Next,构成链式存储。
常见操作实现
  • 插入:在头节点前插入新节点,时间复杂度 O(1);
  • 删除:找到目标前驱节点,修改其 Next 指针;
  • 查找:从头遍历,直到匹配或到达末尾。
示例插入操作:
func (l *LinkedList) Insert(val int) {
    newNode := &ListNode{Val: val, Next: l.Head}
    l.Head = newNode
}
此方法将新节点置为头节点,原链表整体后移,实现快速插入。

3.2 栈与队列的相互模拟与面试变种题

用栈实现队列
通过两个栈可以模拟队列的先进先出特性。一个栈用于入队操作,另一个用于出队。

class MyQueue {
    private Stack<Integer> in = new Stack<>();
    private Stack<Integer> out = new Stack<>();

    public void push(int x) {
        in.push(x);
    }

    public int pop() {
        if (out.isEmpty()) {
            while (!in.isEmpty()) {
                out.push(in.pop());
            }
        }
        return out.pop();
    }
}
当出栈为空时,将入栈元素依次弹出并压入出栈,从而反转顺序,实现队列行为。
常见变种题型
  • 用队列实现栈:利用两个队列,始终保持一个为空,插入到非空队列,弹出时将前n-1个元素移到另一队列
  • 最小栈设计:在栈中维护当前最小值,可通过辅助栈或元组方式实现O(1)查询

3.3 二叉树遍历(递归与非递归)代码实现

递归实现三种遍历方式

二叉树的前序、中序和后序遍历可通过递归简洁实现。以下为Python示例:

def inorder(root):
    if root:
        inorder(root.left)
        print(root.val)
        inorder(root.right)

该函数先递归左子树,访问根节点,再递归右子树,时间复杂度为O(n),空间复杂度取决于树高,最坏为O(n)。

非递归使用栈模拟

利用栈可将递归转换为迭代。以前序遍历为例:

def preorder_iterative(root):
    stack = [root]
    while stack and root:
        node = stack.pop()
        print(node.val)
        if node.right: stack.append(node.right)
        if node.left: stack.append(node.left)

栈结构模拟调用栈行为,先入右子树确保左子树先处理,实现根-左-右的访问顺序。

第四章:复杂逻辑与场景设计题突破

4.1 实现深拷贝函数:解决循环引用与类型判断

在JavaScript中,实现一个健壮的深拷贝函数需处理对象嵌套、循环引用及特殊类型。基础思路是递归复制每个属性,但遇到`Date`、`RegExp`或`Array`时需做类型判断。
类型识别与分支处理
使用 `Object.prototype.toString.call()` 精确判断数据类型,避免 `typeof` 的局限性。

function getType(obj) {
  return Object.prototype.toString.call(obj).slice(8, -1);
}
// 返回如 "Object", "Array", "Date" 等
该函数用于区分普通对象、数组、正则等类型,指导后续差异化拷贝策略。
解决循环引用
采用 `WeakMap` 记录已访问对象,防止无限递归。

function deepClone(obj, seen = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (seen.has(obj)) return seen.get(obj);

  const clone = Array.isArray(obj) ? [] : {};
  seen.set(obj, clone);

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], seen);
    }
  }
  return clone;
}
`seen` 参数缓存原始对象与克隆对象的映射关系,当再次遇到同一引用时直接返回对应副本,确保环状结构安全复制。

4.2 函数柯里化原理剖析与通用实现方案

函数柯里化(Currying)是将接收多个参数的函数转换为一系列使用单个参数的函数链的技术。其核心思想在于延迟执行,通过闭包保存已传入的参数,直到收集完所有参数后触发最终运算。
柯里化的基础实现
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function (...nextArgs) {
        return curried.apply(this, args.concat(nextArgs));
      };
    }
  };
}
上述代码中,`fn.length` 表示原函数期望的参数数量。当累积参数不足时,返回新函数继续收集;否则立即执行。该实现利用了闭包与函数的 `length` 属性完成自动参数判断。
应用场景对比
场景普通函数柯里化函数
日志记录log(level, msg)curry(log)('ERROR')('file not found')
数据过滤filter(arr, pred)const filterByType = curry(filter)(type)

4.3 Promise封装异步控制:并发限制与错误重试

在处理大量异步任务时,直接并发执行可能导致资源耗尽。通过Promise封装,可实现并发数控制与失败自动重试。
并发限制实现
function asyncPool(poolLimit, array, iteratorFn) {
  const ret = [];
  const executing = [];
  for (const item of array) {
    const p = Promise.resolve().then(() => iteratorFn(item));
    ret.push(p);
    if (poolLimit <= array.length) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      if (executing.length >= poolLimit) {
        await Promise.race(executing);
      }
    }
  }
  return Promise.all(ret);
}
该函数通过维护正在执行的Promise队列,利用 Promise.race 控制最大并发数,避免过多请求同时触发。
错误重试机制
结合Promise封装重试逻辑,可在网络波动时提升稳定性。

4.4 虚拟DOM diff算法核心逻辑模拟实现

diff算法基本思想
虚拟DOM的diff算法通过比较新旧节点树,最小化真实DOM操作。其核心在于分层对比、类型判断与key优化。
核心逻辑代码实现
function diff(oldNode, newNode, parent) {
  // 若节点类型不同,直接替换
  if (oldNode.tagName !== newNode.tagName) {
    parent.replaceChild(newNode.render(), oldNode.render());
    return;
  }

  // 比对属性
  const oldProps = oldNode.props || {};
  const newProps = newNode.props || {};
  for (const key in { ...oldProps, ...newProps }) {
    if (oldProps[key] !== newProps[key]) {
      parent.setAttribute(key, newProps[key]);
    }
  }

  // 递归比对子节点(简化版)
  const children = newNode.children || [];
  children.forEach((child, i) => {
    if (oldNode.children[i]) {
      diff(oldNode.children[i], child, parent.childNodes[i]);
    } else {
      parent.appendChild(child.render());
    }
  });
}
上述函数首先判断标签是否变化,若变化则整块替换;随后同步属性差异;最后递归处理子节点。该实现体现了diff的逐层比对策略,结合key可进一步优化列表更新性能。

第五章:总结与进阶学习路径

构建可扩展的微服务架构
在实际项目中,采用 Go 语言构建高并发微服务时,应优先考虑使用 gRPC 替代传统 REST API。以下是一个基于 protobuf 定义的服务接口示例:
service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
}

message UserRequest {
  string user_id = 1;
}
结合 etcd 实现服务注册与发现,可显著提升系统的容错能力。部署时建议使用 Kubernetes 进行容器编排,并通过 Istio 实现流量控制与链路追踪。
性能调优实战策略
真实案例中,某电商平台在大促期间遭遇 QPS 下降问题。通过 pprof 工具分析,定位到数据库连接池配置不当导致阻塞。优化方案如下:
  • 将最大连接数从 50 调整至 200
  • 启用连接复用与空闲超时回收
  • 引入 Redis 缓存热点用户数据
调整后系统吞吐量提升 3.8 倍,P99 延迟从 420ms 降至 110ms。
推荐学习资源与路径
阶段学习内容推荐资源
初级Go 基础语法、并发模型The Go Programming Language(书籍)
中级分布式系统设计Martin Kleppmann《数据密集型应用系统设计》
高级云原生架构实践CNCF 官方认证课程(CKA/CKAD)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值