阅读 48

学习JavaScript数据结构与算法之栈(1)

1. 栈

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

1.1 模拟一个栈

 push(element(s)):添加一个(或几个)新元素到栈顶。
 pop():移除栈顶的元素,同时返回被移除的元素。
 peek():返回栈顶的元素,不对栈做任何修改(不会移除栈顶的元素,仅仅返回它)。
 isEmpty():如果栈里没有任何元素就返回 true,否则返回 false。
 clear():移除栈里的所有元素。
 size():返回栈里的元素个数。该方法和数组的 length 属性很类似。

注意:数组实现栈时间复杂度为O(n),对象实现栈时间复杂度为O(1)

1.1.1 数组实现

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; 
  }

  clear() { 
    this._items = []; 
  }

  size() { 
    return this._items.length; 
  }
}
复制代码

1.1.2 对象实现

class Stack { 
  constructor() { 
    this._count = 0; 
    this._items = {}; 
  } 

  push (element) { 
    this._items[this._count] = element; 
    this._count++; 
  }

  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]; 
  }

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

  clear() { 
    this._items = {}; 
    this._count = 0; 
    // LIFO 原则
    // while (!this.isEmpty()) { 
	//	 this.pop(); 
    // }
  }

  size() { 
    return this._count; 
  }

  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; 
  }
}
复制代码

1.2 实现私有属性

console.log(Object.getOwnPropertyNames(stack)); // [ '_count', '_items' ]
console.log(Object.keys(stack)); // [ '_count', '_items' ]
console.log(stack.items);
复制代码

1.2.1 下划线约定

class Stack { 
  constructor() { 
    this._count = 0; 
    this._items = {}; 
  } 
}
复制代码

这种方式只是一种约定,并不能保护数据,而且只能依赖于使用我们代码的开发者所具备的常识

1.2.2 WeakMap

const _items = new WeakMap();
const _count = new WeakMap();

class Stack {
  constructor() {
    _count.set(this, 0);
    _items.set(this, {});
  }

  push(element) {
    const items = _items.get(this);
    const count = _count.get(this);
    items[count] = element;
    _count.set(this, count + 1);
  }

  pop() {
    if (this.isEmpty()) {
      return undefined;
    }
    const items = _items.get(this);
    let count = _count.get(this);
    count--;
    _count.set(this, count);
    const result = items[count];
    delete items[count];
    return result;
  }

  peek() {
    if (this.isEmpty()) {
      return undefined;
    }
    const items = _items.get(this);
    const count = _count.get(this);
    return items[count - 1];
  }

  isEmpty() {
    return _count.get(this) === 0;
  }

  size() {
    return _count.get(this);
  }

  clear() {
    /* while (!this.isEmpty()) {
        this.pop();
      } */
    _count.set(this, 0);
    _items.set(this, {});
  }

  toString() {
    if (this.isEmpty()) {
      return '';
    }
    const items = _items.get(this);
    const count = _count.get(this);
    let objString = `${items[0]}`;
    for (let i = 1; i < count; i++) {
      objString = `${objString},${items[i]}`;
    }
    return objString;
  }
}
复制代码

1.3 常见的栈问题

1.3.1 从十进制到二进制

 后出来的余数反而要排到前面。
 把余数依次入栈,然后再出栈,就可以实现余数倒序输出

const Stack = require('./objectStack.js')

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(233)); // 11101001 
console.log(decimalToBinary(10)); // 1010 
console.log(decimalToBinary(1000)); // 1111101000
console.log(decimalToBinary(1000.99)); // 1111101000
复制代码

1.3.2 十进制转任意进制

const Stack = require('./objectStack.js')

function baseConverter(decNumber, base) {
  const remStack = new Stack()
  const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  let number = decNumber
  let rem;
  let binaryString = '';
  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()) {
    binaryString += digits[remStack.pop()]
  }
  return binaryString;
}

console.log(baseConverter(100345, 2)); // 11000011111111001 
console.log(baseConverter(100345, 8)); // 303771 
console.log(baseConverter(100345, 16)); // 187F9 
console.log(baseConverter(100345, 35)); // 2BW0
复制代码

1.3.3 有效的括号

 越靠后的左括号,对应的右括号越靠前。
 左括号入栈,右括号出栈,最后栈空了就是合法的

解题步骤:

新建一个栈
扫描字符串,遇左括号入栈,遇到和栈顶括号类型匹配的右括号就出栈,类型不匹配直接判定为不合法。
最后栈空了就合法,否则不合法

时间复杂度O(n), 空间复杂度O(n)

var isValid = function(s) {
  if(s.length % 2 === 1) { return false; }
  const stack = []
  for(let i = 0; i < s.length; i += 1) {
    const c = s[i]
    if(c === '(' || c === '{' || c === '[') {
      stack.push(c)
    } else {
      const t = stack[stack.length - 1]
      if(
        (t === '(' && c === ')') || 
        (t === '{' && c === '}') || 
        (t === '[' && c === ']')
      ) {
         stack.pop()
      } else {
        return false
      }
    }
  }
  return stack.length === 0
};
复制代码

1.3.4 函数调用堆栈

 最后调用的函数,最先执行完。
 JS解释器使用栈来控制函数的调用顺序。

1.3.5 回文检查器

回文是正反都能读通的单词、词组、数或一系列字符的序列,例如 madam或 racecar

var isPalindrome = function(x) {
    let startString = x + ''
    if (startString === undefined || startString === null || (startString !== null && startString.length === 0)) {
        return false
    }

    const stack = []
    let i = 0
    let lowerString = ''
    while(i < startString.length) {
        stack.push(startString[i++])
    }

    while(stack.length) {
        lowerString += stack.pop()
    }

    return lowerString === startString
};
复制代码