前端JavaScript基础与进阶和算法与数据结构笔试题

13 阅读12分钟

1. JavaScript基础与进阶

基础语法

  1. 数据类型判断

    console.log(typeof null);
    console.log(typeof undefined);
    console.log(typeof []);
    console.log(typeof {});
    console.log(typeof NaN);
    

    答案

    "object"
    "undefined"
    "object"
    "object"
    "number"
    

    判断数组最佳方法:Array.isArray([])Object.prototype.toString.call([]) === '[object Array]'

  2. 隐式类型转换

    console.log(1 + "2");
    console.log("2" - 1);
    console.log(1 == "1");
    console.log(1 === "1");
    console.log([] == false);
    console.log({} + []);
    

    答案

    "12" (+运算符有字符串则进行字符串拼接)
    1 (-运算符将字符串转为数字)
    true (==比较会进行类型转换)
    false (===比较不会进行类型转换)
    true ([]转为空字符串,然后转为0false也转为0"[object Object]" ({}被当作代码块,+[]触发字符串转换)
    
  3. 变量提升

    console.log(a);
    var a = 2;
    
    foo();
    function foo() {
      console.log("foo");
    }
    
    bar();
    var bar = function() {
      console.log("bar");
    }
    

    答案

    undefined (变量a声明提升但赋值不提升)
    "foo" (函数声明整体提升)
    TypeError: bar is not a function (函数表达式只提升变量名,此时bar是undefined

作用域与闭包

  1. 作用域链

    var name = "全局";
    function outer() {
      var name = "外层";
      function inner() {
        console.log(name);
      }
      inner();
    }
    outer();
    

    答案"外层" - inner函数在查找变量时,首先在自身作用域找,没找到则向上级作用域查找。

  2. 闭包陷阱

    var fns = [];
    for (var i = 0; i < 5; i++) {
      fns[i] = function() { console.log(i); };
    }
    fns[2]();
    

    答案5 - var声明没有块级作用域,循环结束后i为5。

    修改方案

    var fns = [];
    for (var i = 0; i < 5; i++) {
      fns[i] = (function(j) {
        return function() { console.log(j); };
      })(i);
    }
    fns[2](); // 输出2
    
    // 或使用let(更简洁)
    let fns = [];
    for (let i = 0; i < 5; i++) {
      fns[i] = function() { console.log(i); };
    }
    fns[2](); // 输出2
    
  3. this指向

    const user = {
      name: "张三",
      sayName: function() {
        console.log(this.name);
      },
      sayNameArrow: () => {
        console.log(this.name);
      },
      friends: {
        name: "李四",
        sayName: function() {
          console.log(this.name);
        }
      }
    };
    
    user.sayName();
    user.sayNameArrow();
    user.friends.sayName();
    const fn = user.sayName;
    fn();
    

    答案

    "张三"this指向调用者user)
    undefined (箭头函数this指向定义时的外部作用域,此处为全局)
    "李四"this指向调用者friends)
    undefined (非严格模式下全局对象没有name属性)
    

原型与继承

  1. 原型链判断

    function Person(name) {
      this.name = name;
    }
    Person.prototype.sayName = function() {
      console.log(this.name);
    };
    
    const p1 = new Person("张三");
    console.log(p1.__proto__ === Person.prototype);
    console.log(Person.prototype.__proto__ === Object.prototype);
    console.log(Person.__proto__ === Function.prototype);
    console.log(Object.__proto__ === Function.prototype);
    

    答案

    true (实例的__proto__指向构造函数的prototype)
    true (构造函数的prototype是对象,其__proto__指向Object.prototypetruePerson是函数,函数的__proto__指向Function.prototypetrueObject作为函数,其__proto__也指向Function.prototype
  2. 继承实现

    function inherit(Child, Parent) {
      //
      Child.prototype = Object.create(Parent.prototype);
      Child.prototype.constructor = Child;
    }
    
  3. Class实现

    //
    class Person {
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
    
      sayHello() {
        console.log(`你好,我是${this.name}`);
      }
    
      static create(name, age) {
        return new Person(name, age);
      }
    }
    

异步编程

  1. 事件循环

    console.log(1);
    
    setTimeout(() => {
      console.log(2);
      Promise.resolve().then(() => console.log(3));
    }, 0);
    
    Promise.resolve().then(() => {
      console.log(4);
      setTimeout(() => console.log(5), 0);
    });
    
    console.log(6);
    

    答案

    1 (同步代码)
    6 (同步代码)
    4 (微任务队列中的Promise2 (宏任务队列中的setTimeout32之后产生的微任务)
    54之后产生的宏任务)
    
  2. Promise链式调用

    Promise.resolve()
      .then(() => {
        console.log(1);
        throw new Error("error");
      })
      .then(() => {
        console.log(2);
      })
      .catch(() => {
        console.log(3);
      })
      .then(() => {
        console.log(4);
      });
    

    答案

    1
    3 (捕获到前面抛出的错误)
    4catch后的then仍会执行)
    
  3. async/await实现

    //
    function sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    // 使用示例
    async function test() {
      console.log("start");
      await sleep(1000);
      console.log("end after 1s");
    }
    

ES6+特性

  1. 解构赋值

    const obj = { a: 1, b: { c: 2, d: [3, 4] } };
    
    //
    const { a, b: { c = 5 } } = obj;
    console.log(a, c); // 1, 2
    
  2. Set与Map

    // 使用Set实现数组去重
    const arr = [1, 2, 2, 3, 3, 4];
    
    //
    const uniqueArr = [...new Set(arr)];
    console.log(uniqueArr); // [1, 2, 3, 4]
    
    // 使用Map实现缓存函数
    function cached(fn) {
      //
      const cache = new Map();
      return function(arg) {
        if (cache.has(arg)) {
          return cache.get(arg);
        }
        const result = fn(arg);
        cache.set(arg, result);
        return result;
      };
    }
    
  3. Proxy应用

    //
    const obj = new Proxy({}, {
      get(target, property) {
        return property in target ? target[property] : "属性不存在";
      },
      set(target, property, value) {
        if (typeof value === 'number' && value < 0) {
          throw new Error("不能设置负数");
        }
        target[property] = value;
        return true;
      }
    });
    

函数式编程

  1. 纯函数

    // 答案分析
    function add(a, b) {
      return a + b;
    }
    // 纯函数:输入相同则输出相同,无副作用
    
    function random(min, max) {
      return Math.floor(Math.random() * (max - min)) + min;
    }
    // 非纯函数:输入相同输出不同,依赖外部Math.random
    
    let count = 0;
    function increment() {
      return count++;
    }
    // 非纯函数:修改了外部变量,有副作用
    
  2. 柯里化实现

    function add(a, b) {
      return a + b;
    }
    
    //
    function curry(fn) {
      return function(a) {
        return function(b) {
          return fn(a, b);
        };
      };
    }
    
    // 通用版本
    function curriedAdd(a) {
      return function(b) {
        return a + b;
      };
    }
    
    console.log(curry(add)(1)(2)); // 3
    console.log(curriedAdd(1)(2)); // 3
    
  3. 函数组合

    //
    function compose(...fns) {
      return function(x) {
        return fns.reduceRight((value, fn) => fn(value), x);
      };
    }
    
    // 使用示例
    const add1 = x => x + 1;
    const mul2 = x => x * 2;
    const div3 = x => x / 3;
    
    const compute = compose(div3, mul2, add1);
    console.log(compute(3)); // ((3 + 1) * 2) / 3 = 2.67
    

设计模式

  1. 单例模式

    //
    class Storage {
      static instance = null;
    
      constructor() {
        if (Storage.instance) {
          return Storage.instance;
        }
        this.data = {};
        Storage.instance = this;
      }
    
      setItem(key, value) {
        this.data[key] = value;
      }
    
      getItem(key) {
        return this.data[key];
      }
    }
    
    // 测试
    const s1 = new Storage();
    const s2 = new Storage();
    console.log(s1 === s2); // true
    
  2. 观察者模式

    //
    class EventEmitter {
      constructor() {
        this.events = {};
      }
    
      on(event, callback) {
        if (!this.events[event]) {
          this.events[event] = [];
        }
        this.events[event].push(callback);
        return this;
      }
    
      emit(event, ...args) {
        if (this.events[event]) {
          this.events[event].forEach(cb => cb(...args));
        }
        return this;
      }
    
      off(event, callback) {
        if (this.events[event]) {
          if (callback) {
            this.events[event] = this.events[event].filter(cb => cb !== callback);
          } else {
            delete this.events[event];
          }
        }
        return this;
      }
    }
    
  3. 工厂模式

    //
    class ProductFactory {
      createProduct(type) {
        switch (type) {
          case 'A':
            return new ProductA();
          case 'B':
            return new ProductB();
          case 'C':
            return new ProductC();
          default:
            throw new Error('无效产品类型');
        }
      }
    }
    
    class ProductA {
      constructor() {
        this.name = '产品A';
      }
    }
    
    class ProductB {
      constructor() {
        this.name = '产品B';
      }
    }
    
    class ProductC {
      constructor() {
        this.name = '产品C';
      }
    }
    

模块化

  1. CommonJS与ESM

    // CommonJS
    // module.js
    const data = {
      name: '张三',
      age: 25
    };
    module.exports = data;
    
    // 导入
    const data = require('./module.js');
    
    
    // ESM
    // module.js
    const data = {
      name: '张三',
      age: 25
    };
    export default data;
    
    // 导入
    import data from './module.js';
    
  2. 循环依赖

    // 答案分析
    // CommonJS模块循环引用时,导出的是模块的不完整副本
    
    // a.js输出:b: {}(空对象,因为b.js的导出尚未完成)
    // b.js输出:a: {name: 'a', say: [Function: say]}(a模块已部分加载)
    
    // 解决方法:重构代码避免循环依赖,或将公共部分提取到第三个模块
    
  3. 动态导入

    //  - 使用ESM动态导入
    function loadModule(condition) {
      if (condition) {
        return import('./moduleA.js')
          .then(module => module.default);
      } else {
        return import('./moduleB.js')
          .then(module => module.default);
      }
    }
    
    // async/await写法
    async function loadModule(condition) {
      if (condition) {
        const module = await import('./moduleA.js');
        return module.default;
      } else {
        const module = await import('./moduleB.js');
        return module.default;
      }
    }
    

2. TypeScript

基础类型系统

  1. 接口与类型别名区别

    // 问:以下代码是否正确?为什么?
    interface User {
      name: string;
    }
    interface User {
      age: number;
    }
    
    type Animal = {
      species: string;
    }
    type Animal = {
      age: number;
    }
    

    答案

    第一部分正确,第二部分错误。
    接口(interface)可以合并声明,相同名称的接口会自动合并。
    类型别名(type)不可重复声明,会报错"重复标识符"
  2. 泛型应用

    // 问:编写一个泛型函数getProperty,从对象中安全地获取属性值
    function getProperty<T, K>(obj: T, key: K): _______ {
      return obj[key];
    }
    

    答案

    function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
      return obj[key];
    }
    
  3. 枚举特性

    // 问:以下代码输出什么?
    enum Direction {
      Up,
      Down,
      Left,
      Right
    }
    
    enum Status {
      Success = "SUCCESS",
      Fail = "FAIL"
    }
    
    console.log(Direction.Up, Direction[0], Status.Success);
    

    答案

    0 "Up" "SUCCESS"
    
    数字枚举是双向映射的,可通过值获取键。
    字符串枚举只能从键到值单向映射。
    

高级类型

  1. 联合类型与交叉类型

    // 问:以下代码中T1和T2的类型分别是什么?
    interface A { a: string; c: boolean; }
    interface B { b: number; c: string; }
    
    type T1 = A | B;
    type T2 = A & B;
    
    const x: T1 = { a: "hello", c: true };
    const y: T2 = { a: "hello", b: 42, c: _______ };
    

    答案

    T1是联合类型,表示要么是A类型要么是B类型,必须满足其中一个接口的所有要求
    T2是交叉类型,表示同时是A类型和B类型,必须同时满足两个接口的所有要求
    
    y中的c不能被赋值,因为在A中c是boolean,在B中c是string,交叉后产生never类型
    
  2. 映射类型应用

    // 问:实现一个类型Readonly,使对象所有属性变为只读
    type MyReadonly<T> = _______
    
    interface Person {
      name: string;
      age: number;
    }
    
    const p: MyReadonly<Person> = { name: "张三", age: 30 };
    p.name = "李四"; // 这行应该报错
    

    答案

    type MyReadonly<T> = {
      readonly [K in keyof T]: T[K];
    }
    
  3. 索引类型查询

    // 问:以下代码输出什么?
    interface Person {
      name: string;
      age: number;
      address: {
        city: string;
        code: number;
      }
    }
    
    type T1 = keyof Person;
    type T2 = Person["address"];
    type T3 = Person["address"]["city"];
    
    // T1: _______
    // T2: _______
    // T3: _______
    

    答案

    T1: "name" | "age" | "address"
    T2: { city: string; code: number; }
    T3: string
    

类型守卫

  1. 类型守卫应用

    // 问:完善函数,对不同类型做不同处理
    interface Fish { swim(): void; }
    interface Bird { fly(): void; }
    
    function move(animal: Fish | Bird) {
      if (_______ animal _______) {
        animal.swim();
      } else {
        animal.fly();
      }
    }
    

    答案

    if ("swim" in animal) {
      animal.swim();
    }
    
    // 或使用自定义类型守卫
    function isFish(animal: Fish | Bird): animal is Fish {
      return (animal as Fish).swim !== undefined;
    }
    
    if (isFish(animal)) {
      animal.swim();
    }
    
  2. 类型断言

    // 问:下面的代码有什么问题?如何修复?
    const value: unknown = "Hello World";
    const length: number = value.length;
    

    答案

    // 问题:unknown类型不能直接访问属性
    // 修复方式1:类型断言
    const length: number = (value as string).length;
    
    // 修复方式2:类型守卫
    if (typeof value === "string") {
      const length: number = value.length;
    }
    

装饰器

  1. 类装饰器

    // 问:完成一个日志类装饰器,记录类的创建
    function _______(constructor: Function) {
      console.log(`${constructor.name}类已创建`);
    }
    
    @_______
    class Person {
      constructor(public name: string) {}
    }
    
    new Person("张三");
    

    答案

    function Logger(constructor: Function) {
      console.log(`${constructor.name}类已创建`);
    }
    
  2. 方法装饰器

    // 问:实现一个计时装饰器,记录方法执行时间
    function measure(target: any, key: string, descriptor: PropertyDescriptor) {
      const original = descriptor.value;
      descriptor.value = function(...args: any[]) {
        _______
        const result = original.apply(this, args);
        _______
        return result;
      }
      return descriptor;
    }
    

    答案

    function measure(target: any, key: string, descriptor: PropertyDescriptor) {
      const original = descriptor.value;
      descriptor.value = function(...args: any[]) {
        const start = performance.now();
        const result = original.apply(this, args);
        console.log(`${key} 执行时间: ${performance.now() - start}ms`);
        return result;
      }
      return descriptor;
    }
    

类型推断与兼容性

  1. 结构类型系统

    // 问:以下代码能否正常运行?为什么?
    interface Named {
      name: string;
    }
    
    class Person {
      name: string;
      constructor(name: string) {
        this.name = name;
      }
    }
    
    let p: Named;
    p = new Person("张三");
    

    答案

    可以正常运行。
    TypeScript使用结构类型系统,只要对象包含接口要求的所有属性且类型兼容,
    不管实际类型是什么,都认为该对象实现了该接口。
    
  2. 协变与逆变

    // 问:以下代码是否有类型错误?
    // 假设Cat是Animal的子类型
    class Animal { eat(): void {} }
    class Cat extends Animal { meow(): void {} }
    
    // 例1
    let animals: Animal[] = [];
    let cats: Cat[] = [];
    animals = cats;
    
    // 例2
    type AnimalFn = (a: Animal) => void;
    type CatFn = (c: Cat) => void;
    let animalFunc: AnimalFn = (a) => {};
    let catFunc: CatFn = (c) => {};
    catFunc = animalFunc;
    

    答案

    例1: 正确,数组类型是协变的,Cat[]可赋值给Animal[]
    例2: 错误,函数参数是逆变的,AnimalFn不能赋值给CatFn
    
    协变: 子类型关系保持一致 (如果Cat是Animal的子类,那么Cat[]是Animal[]的子类)
    逆变: 子类型关系反转 (如果Cat是Animal的子类,那么(Animal)=>void是(Cat)=>void的子类)
    

tsconfig配置

  1. 编译选项

    // 问:如何配置tsconfig.json使项目支持以下特性:
    // 1. 严格空值检查
    // 2. 支持装饰器
    // 3. 生成sourcemap
    

    答案

    {
      "compilerOptions": {
        "strictNullChecks": true,
        "experimentalDecorators": true,
        "sourceMap": true
      }
    }
    
  2. 模块解析策略

    // 问:什么是模块解析?如何在tsconfig.json中设置Node模块解析策略?
    

    答案

    模块解析是TypeScript确定import语句实际引用什么模块的过程。
    
    在tsconfig.json中设置Node模块解析策略:
    {
      "compilerOptions": {
        "moduleResolution": "node"
      }
    }
    
    这允许TypeScript使用Node.js方式解析模块(检查node_modules等)。
    

声明文件

  1. 声明文件编写

    // 问:为以下JavaScript库编写一个声明文件
    // calculator.js
    // const calculator = {
    //   add(a, b) { return a + b; },
    //   subtract(a, b) { return a - b; }
    // };
    // export default calculator;
    

    答案

    // calculator.d.ts
    declare namespace calculator {
      function add(a: number, b: number): number;
      function subtract(a: number, b: number): number;
    }
    
    export default calculator;
    
  2. 第三方库声明

    // 问:如何安装和使用第三方库jQuery的类型声明?
    

    答案

    安装jQuery类型声明:
    npm install --save-dev @types/jquery
    
    使用方式:
    import * as $ from 'jquery';
    // 现在可以使用带有类型提示的jQuery API
    $(document).ready(() => {
      $('#element').text('Hello');
    });
    

3. 算法与数据结构

一、JavaScript实现常见数据结构

1. 链表

题目: 实现单链表及其基本操作(增删查)

答案:

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class LinkedList {
  constructor() {
    this.head = null;
    this.size = 0;
  }

  // 添加节点到末尾
  append(value) {
    const newNode = new Node(value);
    if (!this.head) {
      this.head = newNode;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      current.next = newNode;
    }
    this.size++;
  }

  // 在指定位置插入
  insert(value, index) {
    if (index < 0 || index > this.size) return false;

    const newNode = new Node(value);

    if (index === 0) {
      newNode.next = this.head;
      this.head = newNode;
    } else {
      let current = this.head;
      let previous = null;
      let count = 0;

      while (count < index) {
        previous = current;
        current = current.next;
        count++;
      }

      newNode.next = current;
      previous.next = newNode;
    }

    this.size++;
    return true;
  }

  // 删除指定位置节点
  removeAt(index) {
    if (index < 0 || index >= this.size) return null;

    let current = this.head;

    if (index === 0) {
      this.head = current.next;
    } else {
      let previous = null;
      let count = 0;

      while (count < index) {
        previous = current;
        current = current.next;
        count++;
      }

      previous.next = current.next;
    }

    this.size--;
    return current.value;
  }

  // 获取指定位置节点的值
  getAt(index) {
    if (index < 0 || index >= this.size) return null;

    let current = this.head;
    let count = 0;

    while (count < index) {
      current = current.next;
      count++;
    }

    return current.value;
  }
}

思路:

  • 链表由节点组成,每个节点包含值和指向下一节点的指针
  • 基本操作围绕维护节点间的引用关系
  • 时间复杂度:查找O(n),头部插入/删除O(1),尾部操作O(n)

2. 栈

题目: 用数组实现栈结构及其操作

答案:

class Stack {
  constructor() {
    this.items = [];
  }

  // 入栈
  push(element) {
    this.items.push(element);
  }

  // 出栈
  pop() {
    if (this.isEmpty()) return null;
    return this.items.pop();
  }

  // 查看栈顶元素
  peek() {
    if (this.isEmpty()) return null;
    return this.items[this.items.length - 1];
  }

  // 判断栈是否为空
  isEmpty() {
    return this.items.length === 0;
  }

  // 获取栈的大小
  size() {
    return this.items.length;
  }

  // 清空栈
  clear() {
    this.items = [];
  }
}

思路:

  • 栈遵循后进先出(LIFO)原则
  • 使用数组实现最为简单,利用push和pop方法
  • 所有操作时间复杂度均为O(1)

3. 队列

题目: 实现队列及其操作

答案:

class Queue {
  constructor() {
    this.items = {};
    this.frontIndex = 0;
    this.backIndex = 0;
  }

  // 入队
  enqueue(element) {
    this.items[this.backIndex] = element;
    this.backIndex++;
  }

  // 出队
  dequeue() {
    if (this.isEmpty()) return null;

    const item = this.items[this.frontIndex];
    delete this.items[this.frontIndex];
    this.frontIndex++;
    return item;
  }

  // 查看队首元素
  front() {
    if (this.isEmpty()) return null;
    return this.items[this.frontIndex];
  }

  // 判断队列是否为空
  isEmpty() {
    return this.backIndex - this.frontIndex === 0;
  }

  // 获取队列长度
  size() {
    return this.backIndex - this.frontIndex;
  }
}

思路:

  • 队列遵循先进先出(FIFO)原则
  • 使用对象而非数组实现,避免出队时的数组位移开销
  • 维护前后指针,确保O(1)时间复杂度

4. 二叉树

题目: 实现二叉树的创建和遍历

答案:

class TreeNode {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

class BinaryTree {
  constructor() {
    this.root = null;
  }

  // 前序遍历
  preOrderTraversal(node = this.root) {
    if (!node) return [];

    const result = [];
    result.push(node.value);
    result.push(...this.preOrderTraversal(node.left));
    result.push(...this.preOrderTraversal(node.right));

    return result;
  }

  // 中序遍历
  inOrderTraversal(node = this.root) {
    if (!node) return [];

    const result = [];
    result.push(...this.inOrderTraversal(node.left));
    result.push(node.value);
    result.push(...this.inOrderTraversal(node.right));

    return result;
  }

  // 后序遍历
  postOrderTraversal(node = this.root) {
    if (!node) return [];

    const result = [];
    result.push(...this.postOrderTraversal(node.left));
    result.push(...this.postOrderTraversal(node.right));
    result.push(node.value);

    return result;
  }

  // 层序遍历
  levelOrderTraversal() {
    if (!this.root) return [];

    const result = [];
    const queue = [this.root];

    while (queue.length) {
      const node = queue.shift();
      result.push(node.value);

      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }

    return result;
  }
}

思路:

  • 二叉树由节点组成,每个节点最多有两个子节点
  • 遍历方式:前序(根-左-右),中序(左-根-右),后序(左-右-根),层序(按层)
  • 递归实现简洁优雅,层序遍历使用队列辅助实现

5. 图

题目: 实现图结构及其遍历

答案:

class Graph {
  constructor() {
    this.vertices = {};
  }

  // 添加顶点
  addVertex(vertex) {
    if (!this.vertices[vertex]) {
      this.vertices[vertex] = [];
    }
  }

  // 添加边
  addEdge(v1, v2) {
    if (!this.vertices[v1]) this.addVertex(v1);
    if (!this.vertices[v2]) this.addVertex(v2);

    this.vertices[v1].push(v2);
    this.vertices[v2].push(v1); // 无向图
  }

  // 深度优先遍历
  dfs(startVertex) {
    const result = [];
    const visited = {};

    const dfsHelper = (vertex) => {
      if (!vertex) return null;

      visited[vertex] = true;
      result.push(vertex);

      this.vertices[vertex].forEach(neighbor => {
        if (!visited[neighbor]) {
          dfsHelper(neighbor);
        }
      });
    };

    dfsHelper(startVertex);
    return result;
  }

  // 广度优先遍历
  bfs(startVertex) {
    const result = [];
    const visited = {};
    const queue = [startVertex];

    visited[startVertex] = true;

    while (queue.length) {
      const currentVertex = queue.shift();
      result.push(currentVertex);

      this.vertices[currentVertex].forEach(neighbor => {
        if (!visited[neighbor]) {
          visited[neighbor] = true;
          queue.push(neighbor);
        }
      });
    }

    return result;
  }
}

思路:

  • 图由顶点和边组成,可用邻接表(对象+数组)实现
  • DFS使用递归或栈实现,探索尽可能深的路径
  • BFS使用队列实现,按层次探索,适合寻找最短路径

二、排序算法

1. 快速排序

题目: 实现快速排序算法

答案:

function quickSort(arr) {
  if (arr.length <= 1) return arr;

  const pivot = arr[Math.floor(arr.length / 2)];
  const left = [];
  const middle = [];
  const right = [];

  for (let i = 0; i < arr.length; i++) {
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else if (arr[i] > pivot) {
      right.push(arr[i]);
    } else {
      middle.push(arr[i]);
    }
  }

  return [...quickSort(left), ...middle, ...quickSort(right)];
}

思路:

  • 选择基准值(pivot),将数组分为三部分:小于、等于和大于基准值
  • 递归地对左右两部分进行快速排序
  • 平均时间复杂度O(nlogn),最坏情况O(n²)

2. 归并排序

题目: 实现归并排序算法

答案:

function mergeSort(arr) {
  if (arr.length <= 1) return arr;

  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));

  return merge(left, right);
}

function merge(left, right) {
  const result = [];
  let leftIndex = 0;
  let rightIndex = 0;

  while (leftIndex < left.length && rightIndex < right.length) {
    if (left[leftIndex] < right[rightIndex]) {
      result.push(left[leftIndex]);
      leftIndex++;
    } else {
      result.push(right[rightIndex]);
      rightIndex++;
    }
  }

  return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}

思路:

  • 分治思想:将数组分为两半,分别排序,再合并
  • 合并过程比较两个有序数组的元素,按序合并
  • 时间复杂度稳定在O(nlogn),空间复杂度O(n)

3. 堆排序

题目: 实现堆排序算法

答案:

function heapSort(arr) {
  const n = arr.length;

  // 构建最大堆
  for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
    heapify(arr, n, i);
  }

  // 从堆顶取出最大元素,放到数组末尾
  for (let i = n - 1; i > 0; i--) {
    [arr[0], arr[i]] = [arr[i], arr[0]]; // 交换
    heapify(arr, i, 0); // 重新调整堆
  }

  return arr;
}

function heapify(arr, n, i) {
  let largest = i;
  const left = 2 * i + 1;
  const right = 2 * i + 2;

  // 如果左子节点大于根节点
  if (left < n && arr[left] > arr[largest]) {
    largest = left;
  }

  // 如果右子节点大于当前最大值
  if (right < n && arr[right] > arr[largest]) {
    largest = right;
  }

  // 如果最大值不是根节点,交换并继续堆化
  if (largest !== i) {
    [arr[i], arr[largest]] = [arr[largest], arr[i]];
    heapify(arr, n, largest);
  }
}

思路:

  • 将数组视为完全二叉树,构建最大堆
  • 不断取出堆顶(最大值),调整剩余元素为新堆
  • 时间复杂度O(nlogn),空间复杂度O(1)

三、搜索算法

1. 二分查找

题目: 实现二分查找算法

答案:

function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (arr[mid] === target) {
      return mid;
    } else if (arr[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }

  return -1; // 未找到
}

思路:

  • 要求数组已排序
  • 每次比较中间元素,缩小查找范围一半
  • 时间复杂度O(logn),空间复杂度O(1)

2. 深度优先搜索(DFS)

题目: 使用DFS遍历二叉树

答案:

function dfs(root) {
  const result = [];

  function traverse(node) {
    if (!node) return;

    // 前序位置
    result.push(node.value);

    traverse(node.left);
    // 中序位置
    traverse(node.right);
    // 后序位置
  }

  traverse(root);
  return result;
}

// 非递归实现
function dfsIterative(root) {
  if (!root) return [];

  const result = [];
  const stack = [root];

  while (stack.length) {
    const node = stack.pop();
    result.push(node.value);

    // 先放右节点,后放左节点,确保左节点先被访问
    if (node.right) stack.push(node.right);
    if (node.left) stack.push(node.left);
  }

  return result;
}

思路:

  • 沿着一个方向探索到底,然后回溯
  • 可递归实现(简洁)或使用栈实现(节省调用栈)
  • 适合搜索所有可能解,寻找连通性

3. 广度优先搜索(BFS)

题目: 使用BFS遍历二叉树

答案:

function bfs(root) {
  if (!root) return [];

  const result = [];
  const queue = [root];

  while (queue.length) {
    const node = queue.shift();
    result.push(node.value);

    if (node.left) queue.push(node.left);
    if (node.right) queue.push(node.right);
  }

  return result;
}

思路:

  • 按层次访问节点,使用队列实现
  • 先访问邻近节点,再访问较远节点
  • 适合查找最短路径,层次遍历

四、动态规划与贪心

1. 斐波那契数列(动态规划)

题目: 求斐波那契数列的第n项

答案:

// 递归(效率低)
function fibRecursive(n) {
  if (n <= 1) return n;
  return fibRecursive(n - 1) + fibRecursive(n - 2);
}

// 动态规划
function fibDP(n) {
  if (n <= 1) return n;

  let dp = [0, 1];
  for (let i = 2; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2];
  }

  return dp[n];
}

// 空间优化
function fibOptimized(n) {
  if (n <= 1) return n;

  let prev = 0;
  let curr = 1;

  for (let i = 2; i <= n; i++) {
    const next = prev + curr;
    prev = curr;
    curr = next;
  }

  return curr;
}

思路:

  • 递归解法有大量重复计算,时间复杂度O(2^n)
  • 动态规划自下而上计算,避免重复计算,时间复杂度O(n)
  • 空间优化版本只存储必要状态,空间复杂度O(1)

2. 背包问题(动态规划)

题目: 01背包问题:有n个物品,每个物品有重量w和价值v,背包容量为W,求最大价值

答案:

function knapsack(weights, values, capacity) {
  const n = weights.length;
  // dp[i][j]表示前i个物品,容量为j的最大价值
  const dp = Array(n + 1).fill().map(() => Array(capacity + 1).fill(0));

  for (let i = 1; i <= n; i++) {
    for (let j = 1; j <= capacity; j++) {
      if (weights[i - 1] <= j) {
        // 可以放入第i个物品
        dp[i][j] = Math.max(
          dp[i - 1][j],  // 不放入
          dp[i - 1][j - weights[i - 1]] + values[i - 1]  // 放入
        );
      } else {
        // 不能放入第i个物品
        dp[i][j] = dp[i - 1][j];
      }
    }
  }

  return dp[n][capacity];
}

思路:

  • 定义状态:dp[i][j]表示前i个物品,容量为j时的最大价值
  • 状态转移:每个物品有放与不放两种选择
  • 时间复杂度O(nW),空间复杂度O(nW)

3. 找零钱问题(贪心)

题目: 给定不同面额的硬币和一个总金额,求所需最少的硬币个数

答案:

// 贪心算法(仅适用于特定币值系统,如美元)
function coinChangeGreedy(coins, amount) {
  // 确保coins按降序排列
  coins.sort((a, b) => b - a);

  let count = 0;
  let remaining = amount;

  for (const coin of coins) {
    const numCoins = Math.floor(remaining / coin);
    count += numCoins;
    remaining -= numCoins * coin;
  }

  return remaining === 0 ? count : -1;
}

// 动态规划(通用解)
function coinChangeDP(coins, amount) {
  // dp[i]表示金额i所需的最少硬币数
  const dp = Array(amount + 1).fill(Infinity);
  dp[0] = 0;

  for (const coin of coins) {
    for (let i = coin; i <= amount; i++) {
      dp[i] = Math.min(dp[i], dp[i - coin] + 1);
    }
  }

  return dp[amount] === Infinity ? -1 : dp[amount];
}

思路:

  • 贪心:每次选择最大面额的硬币(只适用于某些特定币值系统)
  • 动态规划:dp[i]表示金额i所需的最少硬币数
  • 贪心时间复杂度O(n),动态规划时间复杂度O(n*amount)

五、字符串处理

1. 正则表达式匹配

题目: 验证邮箱格式是否合法

答案:

function validateEmail(email) {
  const regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  return regex.test(email);
}

思路:

  • 正则表达式描述字符串的模式
  • ^$ 表示开始和结束
  • [] 表示字符集,+ 表示一个或多个
  • {2,} 表示至少2个字符

2. KMP字符串匹配算法

题目: 实现KMP算法查找模式串在文本串中的位置

答案:

function kmpSearch(text, pattern) {
  if (pattern.length === 0) return 0;

  // 计算前缀表(部分匹配表)
  const lps = computeLPSArray(pattern);

  let i = 0; // 文本指针
  let j = 0; // 模式指针
  const results = []; // 存储所有匹配位置

  while (i < text.length) {
    if (pattern[j] === text[i]) {
      i++;
      j++;
    }

    if (j === pattern.length) {
      // 找到一个匹配
      results.push(i - j);
      j = lps[j - 1]; // 寻找下一个匹配
    } else if (i < text.length && pattern[j] !== text[i]) {
      if (j !== 0) {
        j = lps[j - 1];
      } else {
        i++;
      }
    }
  }

  return results;
}

function computeLPSArray(pattern) {
  const lps = [0]; // 首字符的最长公共前后缀长度为0
  let len = 0; // 当前已匹配的长度
  let i = 1;

  while (i < pattern.length) {
    if (pattern[i] === pattern[len]) {
      len++;
      lps[i] = len;
      i++;
    } else {
      if (len !== 0) {
        len = lps[len - 1];
      } else {
        lps[i] = 0;
        i++;
      }
    }
  }

  return lps;
}

思路:

  • KMP算法避免不必要的字符比较,利用已知信息
  • 构建部分匹配表(lps数组),记录最长公共前后缀长度
  • 失配时,根据lps数组回退到合适位置继续匹配
  • 时间复杂度O(n+m),其中n和m分别是文本串和模式串长度

六、前端算法应用

1. 虚拟DOM的Diff算法

题目: 简化版虚拟DOM的Diff算法实现

答案:

function diff(oldVNode, newVNode) {
  // 如果节点类型不同,直接替换
  if (oldVNode.type !== newVNode.type) {
    return {
      type: 'REPLACE',
      newVNode
    };
  }

  // 如果是文本节点且内容不同
  if (!oldVNode.children && !newVNode.children) {
    if (oldVNode.text !== newVNode.text) {
      return {
        type: 'TEXT',
        text: newVNode.text
      };
    }
    return null; // 无变化
  }

  // 如果老节点无子节点,新节点有子节点
  if (!oldVNode.children) {
    return {
      type: 'REPLACE',
      newVNode
    };
  }

  // 如果新节点无子节点,老节点有子节点
  if (!newVNode.children) {
    return {
      type: 'REPLACE',
      newVNode
    };
  }

  // 子节点对比
  const patches = [];
  const len = Math.max(oldVNode.children.length, newVNode.children.length);

  for (let i = 0; i < len; i++) {
    // 新增节点
    if (i >= oldVNode.children.length) {
      patches.push({
        type: 'ADD',
        node: newVNode.children[i],
        index: i
      });
      continue;
    }

    // 删除节点
    if (i >= newVNode.children.length) {
      patches.push({
        type: 'REMOVE',
        index: i
      });
      continue;
    }

    // 递归对比
    const childPatch = diff(oldVNode.children[i], newVNode.children[i]);
    if (childPatch) {
      patches.push({
        type: 'PATCH',
        patch: childPatch,
        index: i
      });
    }
  }

  if (patches.length === 0) return null;

  return {
    type: 'CHILDREN',
    patches
  };
}

思路:

  • 比较两棵树的差异,生成最小操作集(补丁)
  • 节点类型不同时整体替换,相同时递归比较子节点
  • 采用深度优先遍历,对比过程自顶向下
  • 优化策略:列表节点使用key进行优化(简化版未实现)

2. 状态管理设计

题目: 实现简易版发布-订阅模式(类似Redux)

答案:

class Store {
  constructor(reducer, initialState = {}) {
    this.reducer = reducer;
    this.state = initialState;
    this.listeners = [];
  }

  // 获取状态
  getState() {
    return this.state;
  }

  // 订阅状态变化
  subscribe(listener) {
    this.listeners.push(listener);
    // 返回取消订阅函数
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }

  // 发送action更新状态
  dispatch(action) {
    this.state = this.reducer(this.state, action);
    // 通知所有监听器
    this.listeners.forEach(listener => listener());
    return action;
  }
}

// 使用示例
function counterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
}

const store = new Store(counterReducer, { count: 0 });

// 订阅状态变化
const unsubscribe = store.subscribe(() => {
  console.log('状态更新:', store.getState());
});

// 发送action
store.dispatch({ type: 'INCREMENT' }); // 状态更新: { count: 1 }
store.dispatch({ type: 'INCREMENT' }); // 状态更新: { count: 2 }
store.dispatch({ type: 'DECREMENT' }); // 状态更新: { count: 1 }

// 取消订阅
unsubscribe();

思路:

  • 发布-订阅模式:组件订阅状态变化,状态变化时通知组件更新
  • 单一数据源:整个应用的状态存储在单个对象中
  • 状态只读:唯一改变状态的方式是发送action
  • 使用纯函数reducer来执行状态更新