前端面试题系列之算法系列

151 阅读7分钟

时间/空间复杂度

时间复杂度和空间复杂度是用于衡量算法性能的重要指标。

时间复杂度描述了算法解决问题所需的时间,通常使用大O符号(O(n))表示。常见的时间复杂度包括:

  • O(1):常数时间复杂度,表示算法的执行时间不随输入规模增大而增加。
  • O(log n):对数时间复杂度,通常出现在使用了分治或二分思想的算法中。
  • O(n):线性时间复杂度,算法的执行时间与输入规模成正比。
  • O(n log n):线性对数时间复杂度,通常出现在快速排序、归并排序等排序算法中。
  • O(n^2):平方时间复杂度,通常出现在简单的嵌套循环算法中。
  • O(2^n):指数时间复杂度,通常出现在穷举搜索等算法中。

空间复杂度描述了算法在运行过程中所需的内存空间,也通常使用大O符号(O(n))表示。常见的空间复杂度包括:

  • O(1):常数空间复杂度,表示算法的空间占用是固定的,与输入规模无关。
  • O(n):线性空间复杂度,算法的空间占用与输入规模成正比。
  • O(n^2):平方空间复杂度,空间占用与输入规模的平方成正比。

在比较时间复杂度和空间复杂度之间的大小关系时,通常可以得出以下结论:

  • 如果一个算法的时间复杂度较低,但空间复杂度较高,那么它可能会更快地执行,但会占用更多的内存空间。
  • 如果一个算法的空间复杂度较低,但时间复杂度较高,那么它可能会占用较少的内存空间,但执行速度可能会较慢。

栈是一种先进后出的线性数据结构。

栈具有两个主要操作:压入(push)和弹出(pop)。压入操作将元素添加到栈顶,而弹出操作则移除栈顶的元素。js中没有栈结构,但是可以用数组来模拟实现栈的所有功能。

常见需要使用栈的场景包括:

  1. 括号匹配:在编程中,栈常用于检查表达式中的括号是否匹配。遍历表达式时,遇到左括号则压入栈,遇到右括号则弹出栈顶元素进行匹配。
  2. 浏览器前进后退:浏览器的前进和后退功能可以通过两个栈来实现,一个栈用于存储前进的页面,另一个栈用于存储后退的页面。
  3. 函数调用:在计算机内部,函数调用时使用栈来保存调用信息。每次函数调用时,相关信息会被压入栈中,函数返回时再从栈中弹出信息。
  4. 表达式求值:中缀表达式转换为后缀表达式时,可以使用栈来存储操作符,以便进行求值。

题目一 有效的括号

解题思路:

  1. 新建一个栈
  2. 扫描字符串,遇到左括号则入栈,遇到右括号则和栈顶括号类型进行匹配,匹配成功则出栈;不匹配则判断为不合法
  3. 最后栈空了则合法,否则也是不合法
/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function (s) {
  const stack = [];
  if (s.length % 2 === 1) return false;

  const len = s.length;
  console.log(len);
  for (let i = 0; i < len; i++) {
    const c = s[i]
    if ('({['.includes(c)) {
      stack.push(c)
      console.log(stack,'push');
    } else {
      const t = stack[stack.length - 1]
      console.log(t,'t------');
      if (
        (t === '{' && c === '}') ||
        (t === '[' && c === ']') ||
        (t === '(' && c === ')')
      ) {
        stack.pop()
        console.log(stack,'pop');
      } else {
        return false
      }
    }
  }

  return stack.length === 0
};

时间复杂度为O(N)
空间复杂度为O(N)

题目二 浏览器前进后退

浏览器的前进和后退功能可以通过两个栈来实现。一个栈用于存储前进的页面,另一个栈用于存储后退的页面。当用户进行页面导航时,将页面压入相应的栈中,实现前进和后退的功能。注意,当用户进入到一个新的页面时,此时要清空前进页面的栈。

class BrowserHistory {
  constructor() {
    this.backStack = []; // 存储后退的页面
    this.forwardStack = []; // 存储前进的页面
    this.currentURL = null; // 当前页面
  }

  visit(url) {
    if (this.currentURL !== null) {
      this.backStack.push(this.currentURL); // 将当前页面压入后退栈
      this.forwardStack = []; // 清空前进栈
    }
    this.currentURL = url; // 设置当前页面为新访问的页面
  }

  back() {
    if (this.backStack.length > 0) {
      this.forwardStack.push(this.currentURL); // 将当前页面压入前进栈
      this.currentURL = this.backStack.pop(); // 弹出后退栈的页面作为当前页面
    }
  }

  forward() {
    if (this.forwardStack.length > 0) {
      this.backStack.push(this.currentURL); // 将当前页面压入后退栈
      this.currentURL = this.forwardStack.pop(); // 弹出前进栈的页面作为当前页面
    }
  }
}

visit、back和forward函数的时间复杂度都为 O(1),而空间复杂度为 O(n)

题目三 二叉树的前序遍历

解题思路:

  1. 使用栈的方式来遍历,第一步将根节点放入栈中
  2. 将栈中的节点取出,将当前节点的值放入res中,并且先将取出节点的右节点再放入栈中,再将节点的左节点放入栈中(这样做是为了能先访问节点的左节点,即完成前序遍历)
  3. 重复第二个步骤,直到栈中没有节点了,即表示遍历完了二叉树
/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
const preorderTraversal = function(root) {
    const res = [];
    const stack = [];
    if (root) stack.push(root)
    while(stack.length) {
        const n = stack.pop()
        res.push(n.val)
        if (n.right) stack.push(n.right)
        if (n.left) stack.push(n.left)
    }
    return res;
};

时间复杂度:

  • 遍历整棵树的时间复杂度为 O(n),其中 n 为二叉树中节点的数量。因为需要访问每个节点一次,所以时间复杂度为 O(n)。

空间复杂度:

  • 使用了一个栈来辅助遍历,栈中最多同时存储树的高度个节点,因此空间复杂度为 O(h),其中 h 为二叉树的高度。在最坏的情况下,当二叉树退化为链表时,树的高度为 n,此时空间复杂度为 O(n)。

队列

队列是一种先进先出的数据结构。

在队列中,新元素被添加到队尾,而从队列中移除元素时则从队头开始。

队列通常用于需要按顺序处理数据的场景,以下是一些常见的队列应用场景:

  1. 任务调度:操作系统中的任务调度通常使用队列来管理待执行的任务,确保任务按照先后顺序得到执行。
  2. 消息队列:在分布式系统中,消息队列被用于异步通信和解耦,允许不同组件之间通过发送和接收消息来进行通信。
  3. 广度优先搜索(BFS):在图论和树结构中,广度优先搜索算法通常使用队列来管理待访问的节点,以确保按层级顺序进行搜索。
  4. 缓冲区管理:在计算机网络中,队列被用于管理数据包的传输和处理,以平衡发送和接收数据的速率。
  5. 线程池任务队列:在多线程编程中,线程池通常使用队列来管理待执行的任务,以实现任务的异步执行和资源的复用。

题目一 最近的请求次数

思路:

  1. 有新请求就入队,3000ms前发出的请求出队
  2. 队列的长度就是最近请求次数
var RecentCounter = function() {
    this.q = []
};

/** 
 * @param {number} t
 * @return {number}
 */
RecentCounter.prototype.ping = function(t) {
    this.q.push(t);
    while(this.q[0] < t -3000) {
        this.q.shift()
    }
    return this.q.length
};

这个函数的时间复杂度为O(n),其中n是ping方法被调用的次数。在每次调用ping方法时,都会执行一个while循环,该循环的迭代次数取决于队列中满足条件的元素个数,最坏情况下需要遍历整个队列,因此时间复杂度为O(n)。

空间复杂度为O(n),因为队列q会随着每次调用ping方法而增长,最坏情况下需要存储n个元素,因此空间复杂度为O(n)。

每周末更新一两个知识点 💪