前端涨薪功法:为什么需要栈数据结构 ?

534 阅读3分钟

栈数据结构在JavaScript中的重要性

为什么需要栈数据结构?

栈数据结构最突出的特点,叠盘子特性,遵循后入先出(LIFO)原则,就只有两个主要的操作

  • Push, 叠盘子到顶部
  • Pop, 从顶部移出盘子

image.png

由于其简单易懂的特性,栈数据结构被广泛应用于计算机领域。在JavaScript中,栈数据结构可以帮助实现高效、简洁且可读性强的代码。

栈数据结构优点

  • 叠盘子特性使得栈结构的操作简洁,容易理解。
  • 运用栈数据结构可以解决一些特定问题,如括号匹配、函数调用、表达式求值等。

栈数据结构各种操作的复杂度分析

以下是栈数据结构的基本操作及其时间复杂度:

  • push:向栈顶添加元素,时间复杂度为O(1)。
  • pop:移除栈顶元素,时间复杂度为O(1)。
  • peek:查看栈顶元素,时间复杂度为O(1)。
  • isEmpty:检查栈是否为空,时间复杂度为O(1)。
  • size:获取栈的大小,时间复杂度为O(1)。

最简单的实现数组实现

const stack = [];
stack.push(1); // add 1 to the stack
stack.push(2); // add 2 to the stack
console.log(stack.pop()); // remove 2 from the stack
console.log(stack.pop()); // remove 1 from the stack

简单应用反转字符串

function reverseString(str) {
  const stack = [];
  for (let i = 0; i < str.length; i++) {
    stack.push(str[i]);
  }
  let reversedStr = '';
  while (stack.length > 0) {
    reversedStr += stack.pop();
  }
  return reversedStr;
}

console.log(reverseString('hello')); // 'olleh'

基于数组的堆栈是最简单的实现

但是由于数组不适合频繁的进行动态操作以及底层对于大数据量需要动态的计算固定的储存空间,大批量的频繁操作pop, push可能导致效率低下。

链表数据栈结构的实现

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

class LinkedListStack {
  constructor() {
    this.top = null;
    this.length = 0;
  }

  push(value) {
    const newNode = new Node(value);
    newNode.next = this.top;
    this.top = newNode;
    this.length++;
  }

  pop() {
    if (this.isEmpty()) {
      return null;
    }
    const poppedNode = this.top;
    this.top = this.top.next;
    this.length--;
    return poppedNode.value;
  }

  peek() {
    return this.top ? this.top.value : null;
  }

  isEmpty() {
    return this.length === 0;
  }

  size() {
    return this.length;
  }
}

image.png

利用链表数据结构实现的栈,不用线性数据结构,适用于频繁的pop,push操作,不适用于频繁的检索操作

例如实现编辑器的回退撤销操作:

// 记录单词文本的修改
class TextChange {
  constructor(text, position) {
    this.text = text;
    this.position = position;
  }
}



class TextEditor {
  constructor() {
    this.text = '';
    this.history = new LinkedListStack();
    this.future = new LinkedListStack();
  }

  addText(newText) {
    const position = this.text.length;
    const change = new TextChange(newText, position);
    this.history.push(change);
    this.text += newText;
  }
  // 撤销
  undo() {
    const change = this.history.pop();
    if (change === null) {
      return;
    }
    const { text, position } = change;
    this.future.push(
    new TextChange(this.text.slice(position - text.length, position), position - text.length));
    this.text = this.text.slice(0, position - text.length) + this.text.slice(position);
  }
  // 回退
  redo() {
    const change = this.future.pop();
    if (change === null) {
      return;
    }
    const { text, position } = change;
    this.history.push(new TextChange(text, position));
    this.text = this.text.slice(0, position) + text + this.text.slice(position);
  }
}

const editor = new TextEditor();
editor.addText('Hello, world!');
console.log(editor.text); // 'Hello, world!'
editor.addText(' How are you?');
console.log(editor.text); // 'Hello, world! How are you?'
editor.undo();
console.log(editor.text); // 'Hello, world!'
editor.redo();
console.log(editor.text); // 'Hello, world! How are you?'

在此示例中,我们使用基于链表的堆栈来存储用户所做的文本更改的历史记录。

我们创建一个 TextChange 类来存储每次变化的文本和位置

LinkedListStack 类提供了 push()、pop()、peek() 和 isEmpty() 方法来操作堆栈

TextEditor 类使用堆栈来实现 undo() 和 redo() 方法,这些方法允许用户撤销和回退文本更改。

在这种情况下,基于链表的堆栈使我们能够根据需要为新的文本更改高效地分配内存,而无需预先分配固定数量的内存。

这在堆栈大小事先未知或随时间变化很大的情况下可能很重要

栈数据结构实际应用场景

栈数据结构具有广泛的实际应用场景,包括但不限于:

  • 函数调用堆栈:实现函数调用时的上下文切换。
  • 括号匹配:检查代码中的括号是否合法。
  • 表达式求值:解析和计算数学表达式。
  • 深度优先搜索算法:在图和树结构中搜索数据。

栈数据结构在前端中的应用

在前端领域,栈数据结构也有非常重要的应用,如:

  • 浏览器历史记录:通过栈实现浏览器的前进后退功能。
  • 路由管理:在单页面应用(SPA)中,实现路由切换及页面状态管理。
  • JavaScript事件循环:浏览器使用任务队列和调用栈实现异步操作。
  • DOM元素遍历:通过深度优先搜索遍历DOM树。