算法题没做出来面试又挂了

444 阅读4分钟

先来个段子提提神~

算法好难?哪里难了!这么多年一直都是这个难度,有些时候得找找自己原因,有没有认真刷题,刷题数量有没有涨,好吧,我简直都快疯掉了!

所以!我反省一下,我面试前做了多少算法题。

截屏2023-09-15 10.42.30.png

截屏2023-09-15 10.43.22.png

根据上面的图可以看到,也就八九十个算法题吧,然后屁颠屁颠的就跑去面试了,果不其然,面试凉了,内心更是凉透了。考的不会,会的不考,哎~。

话不多说,上才艺!

整体的面试流程是这样的,参加了两轮面试,每轮面试出了两道算法题,共四道:

  1. 用proxy实现一个数据劫持
  2. 最大子数组和
  3. LRU 缓存
  4. 实现 undo 和 redo

题目1解法:用proxy实现一个数据劫持。

const obj = new Proxy({}, {
  get: function (target, propKey, receiver) {
    console.log(`getting ${propKey}!`);
    return Reflect.get(target, propKey, receiver);
  },
  set: function (target, propKey, value, receiver) {
    console.log(`setting ${propKey}!`);
    return Reflect.set(target, propKey, value, receiver);
  }
});

题目2解法:最大子数组和。给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。

// 动态规划
const maxSubArray = function(nums) {
  let pre = 0, maxAns = nums[0];
  nums.forEach((x) => {
      pre = Math.max(pre + x, x);
      maxAns = Math.max(maxAns, pre);
  });
  return maxAns;
};

// 暴力解法
const maxSubArray = function(nums) {
  let max = Math.min(...nums);
  for (let i = 0; i < nums.length; i++) {
    let total = 0;
    for (let j = i; j < nums.length; j++) {
      total += nums[j];
      max = Math.max(total, max)
    }
  }
  return max;
};

题目3解法:LRU 缓存

class LRUCache {
  constructor(length) {
    if (length < 1) throw new Error("长度不能小于1");
    this.length = length;
    this.data = new Map(); // 数据map
  }

  set(key, value) {
    const data = this.data;
    // 如果存在该对象,则直接删除
    if (data.has(key)) {
      data.delete(key);
    }
    // 将数据对象添加到map中
    data.set(key, value);
    if (data.size > this.length) {
      // 如果map长度超过最大值,则取出map中的第一个元素,然后删除
      const delKey = data.keys().next().value;
      data.delete(delKey);
    }
  }
  get(key) {
    const data = this.data;
    // 数据map中没有key对应的数据,则返回null
    if (!data.has(key)) return null;
    const value = data.get(key);
    // 返回数据前,先删除原数据,然后在添加,就可以保持在最新
    data.delete(key);
    data.set(key, value);
    return value;
  }
}

const lruCache = new LRUCache(2);
lruCache.set('1', 1); // Map(1) {1 => 1}
lruCache.set('2',2); // Map(2) {1 => 1, 2 => 2}
console.log(lruCache.get('1')); // Map(2) {2 => 2, 1 => 1}
lruCache.set('3',3); // Map(2) {1 => 1, 3 => 3}
console.log(lruCache.get('2')); // null
lruCache.set('4',4); // Map(2) {3 => 3, 4 => 4}
console.log(lruCache.get('1')); // null
console.log(lruCache.get('3')); // Map(2) {4 => 4, 3 => 3}
console.log(lruCache.get('4')); // Map(2) {3 => 3, 4 => 4}

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

那么该数据结构就是当存储队列到达上限时,清除的是最久未被访问的节点,该节点一般认为是最可能无用的节点,保留下来的是最近都有使用过的节点,因此可以实现对"有用"数据的最大程度保留。

图片

Vue中在 keep-alive 组件中使用了 LRU 算法:官方文档

题目4解法:实现撤销和重做(undo 和 redo)功能。

class Command {
  constructor(executor) {
    this.executor = executor;
  }

  execute() {
    this.executor();
  }
}

class AddCommand extends Command {
  constructor(undoCommand, value) {
    super(() => {
      console.log(`Add: ${value}`);
      undoCommand.savedValue = value;
    });
    this.undoCommand = undoCommand;
  }

  undo() {
    console.log(`Undo Add: ${this.undoCommand.savedValue}`);
    this.undoCommand.savedValue = null;
  }
}

class DeleteCommand extends Command {
  constructor(undoCommand, value) {
    super(() => {
      console.log(`Delete: ${value}`);
      undoCommand.savedValue = value;
    });
    this.undoCommand = undoCommand;
  }

  undo() {
    console.log(`Redo Delete: ${this.undoCommand.savedValue}`);
    this.undoCommand.savedValue = null;
  }
}

class History {
  constructor() {
    this.commands = [];
    this.savedValue = null;
  }

  addCommand(command) {
    this.commands.push(command);
  }

  undo() {
    if (this.commands.length > 0) {
      const lastCommand = this.commands[this.commands.length - 1];
      lastCommand.undo();
      this.commands.pop();
    } else {
      console.log("No commands to undo");
    }
  }

  redo() {
    if (this.commands.length > 0) {
      const lastCommand = this.commands[this.commands.length - 1];
      lastCommand.execute();
      this.commands.pop();
    } else {
      console.log("No commands to redo");
    }
  }
  execute() {
    for(let command of this.commands) {
      command.execute();
    }
  }
}

const history = new History();
const addCommand1 = new AddCommand(history, "Value 1");
const addCommand2 = new AddCommand(history, "Value 2");
const deleteCommand = new DeleteCommand(history, "Value 2");
history.addCommand(addCommand1);
history.addCommand(addCommand2);
history.addCommand(deleteCommand);
history.execute();
history.undo();
history.redo();

首先,这段代码主要实现了撤销和重做功能,采用的是命令模式(Command Pattern)。这是一种软件设计模式,它封装了一个或多个操作的对象,直到这些操作被调用。

Command 类:这是一个抽象类,定义了一个 execute() 方法,这个方法是在发出命令时要执行的逻辑。同时,这个类还定义了一个 undo() 方法,这个方法是在撤销命令时执行的逻辑。

AddCommand 和 DeleteCommand 类:这两个类是继承了 Command 类的具体子类,分别实现了添加和删除操作的命令。在构造函数中,它们都接受一个 undoCommand 和一个值,当执行命令时,它们会将这个值保存在 undoCommand 的 savedValue 中。

History 类:这个类负责保存所有的命令,并且提供了撤销和重做的功能。在执行命令时,它会在命令列表的末尾添加新的命令。在撤销命令时,它会调用最后一个命令的 undo() 方法,并且从命令列表中移除这个命令。在重做命令时,它会再次执行最后一个命令,并且将这个命令重新添加到命令列表的末尾。

所以,当创建一个 History 对象并添加 AddCommand 或 DeleteCommand 对象,然后依次执行这些命令时,可以通过 undo() 和 redo() 方法来回退和重做这些命令。