学习JavaScript数据结构与算法 第四章

发布时间 2023-05-08 10:12:17作者: 三槐111

第四章,栈

有时候还需要一种能在添加或删除元素时进行更多控制的数据结构。有两种类似于数组的数据结构在添加和删除元素时更为可控,它们就是栈和队列。

4.2 栈数据结构

栈是一种遵从后进先出(LIFO)原则的有序集合。新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。

栈也被用在编程语言的编译器和内存中保存变量、方法调用等,也被用于浏览器历史记录(浏览器的返回按钮)。

class Stack {
  constructor() {
    this.items = []
  }
  // 向栈添加元素
  push(element) {
    this.items.push(element)
  }
  // 从栈移出元素
  pop() {
    return this.items.pop()
  }
  // 查看栈顶元素
  peek() {
    return this.items[this.items.length - 1]
  }
  // 检查栈是否为空
  isEmpty() {
    return this.items.length === 0
  }
  // 栈的length
  size() {
    return this.items.length
  }
  // 清空栈元素
  clear() {
    this.items = []
  }
}

const stack = new Stack()
console.log(stack)

stack.push(5)
stack.push(8)

console.log(stack.peek())

stack.push(11);
console.log(stack.size())
console.log(stack.isEmpty())

stack.push(15)

stack.pop()
stack.pop()
console.log(stack.size())


console.log(stack)

4.3 创建一个基于 JavaScript 对象的 Stack 类

在使用数组时,大部分方法的时间复杂度是 O(n)。

O(n) 的意思是,我们需要迭代整个数组直到找到要找的那个元素,在最坏的情况下需要迭代数组的所有位置,其中的 n 代表数组的长度。如果数组有更多元素的话,所需的时间会更长。另外,数组是元素的一个有序集合,为了保证元素排列有序,它会占用更多的内存空间。

class Stack {
  constructor() {
    this.count = 0
    this.items = {}
  }
  // 向栈中添加元素
  push(element) {
    this.items[this.count] = element
    // this.count = this.count + 1
    // this.count += 1
    this.count++
  }
  // 栈的大小
  size() {
    return this.count
  }
  // 栈是否为空
  isEmpty() {
    return this.count === 0
  }
  // 从栈中弹出元素
  pop() {
    if (this.isEmpty()) return undefined

    this.count--
    const result = this.items[this.count]
    delete this.items[this.count]
    return result
  }
  // 查看栈顶
  peek() {
    if (this.isEmpty()) return undefined

    return this.items[this.count - 1]
  }
  // 清空栈
  clear() {
    this.items = {}
    this.count = 0
    // 遵循 LIFO 原则
    // while (!this.isEmpty()) {
    //   this.pop()
    // }
  }
  // 创建toString方法
  toString() {
    if (this.isEmpty()) return ''

    let objString = `${this.items[0]}`
    for (let i = 1; i < this.count; i++) {
      objString = `${objString},${this.items[i]}`
    }
    return objString
  }
}

const stack = new Stack()
stack.push(5)
stack.push(8)
stack.push(10)
console.log(stack)
console.log(stack.toString())

除了 toString 方法,我们创建的其他方法的复杂度均为 O(1),代表我们可以直接找到目标元素并对其进行操作(push、pop 或 peek)。

4.4 保护数据结构内部元素

在创建别的开发者也可以使用的数据结构或对象时,我们希望保护内部的元素,只有我们暴露出的方法才能修改内部结构。

本章使用 ES2015(ES6)语法创建了 Stack 类。ES2015 类是基于原型的。尽管基于原型的类能节省内存空间并在扩展方面优于基于函数的类,但这种方式不能声明私有属性(变量)或方法。另外,在本例中,我们希望 Stack 类的用户只能访问我们在类中暴露的方法。

4.4.1 下划线命名约定

4.4.2 用 ES2015 的限定作用域 Symbol 实现类

4.4.3 用 ES2015 的 WeakMap 实现类

4.4.4 ECMAScript 类属性提案

ES2022 正式为 class 添加了私有属性,方法是在属性名之前使用#表示。

4.5 用栈解决问题

栈的实际应用非常广泛。在回溯问题中,它可以存储访问过的任务或路径、撤销的操作。

本节,我们将介绍如何解决十进制转二进制问题,以及任意进制转换的算法。

要把十进制转化成二进制,我们可以将该十进制数除以 2(二进制是满二进一)并对商取整,直到结果是 0 为止。


function decimalToBinary(decNumber) {
  const remStack = new Stack()
  let number = decNumber
  let rem
  let binaryString = ''


  while (number > 0) {
    rem = Math.floor(number % 2)
    remStack.push(rem)
    number = Math.floor(number / 2)
  }

  while (!remStack.isEmpty()) {
    binaryString += remStack.pop().toString()
  }

  return binaryString
}

console.log(decimalToBinary(10)) // 1010

进制转换算法,我们可以修改之前的算法,使之能把十进制转换成基数为 2~36 的任意进制。除了把十进制数除以 2 转成二进制数,还可以传入其他任意进制的基数为参数。


function baseConverter(decNumber, base) {
  const remStack = new Stack()

  const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'

  let number = decNumber
  let rem
  let baseString = ''

  if (!(base >= 2 && base <= 36)) return ''

  while (number > 0) {
    rem = Math.floor(number % base)
    remStack.push(rem)
    number = Math.floor(number / base)
  }

  while (!remStack.isEmpty()) {
    baseString += digits[remStack.pop()]
  }
  return baseString
}

console.log(baseConverter(100345, 16))

汉诺塔

平衡圆括号