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

在栈里,新的元素都靠近栈顶,旧的元素都接近栈底。

生活中栈的例子:一摞书,叠放的盘子。

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

创建一个基于数组的栈

  • 向栈添加元素
  • 从栈移除元素
  • 查看栈顶元素
  • 检查栈是否为空
  • 清空栈元素
  • 使用 Stack
  1. /**
  2. * 创建一个基于数组的栈
  3. */
  4. class Stack {
  5. constructor() {
  6. this.items = []; // 选择一种数据结构(数组)来保存栈里的元素
  7. }
  8. // 添加一个(或几个)新元素到栈顶
  9. push(element) {
  10. this.items.push(element);
  11. }
  12. // 移除栈顶的元素,同时返回被移除的元素
  13. pop() {
  14. return this.items.pop();
  15. }
  16. // 返回栈顶的元素,但不对栈做任何修改
  17. peek() {
  18. return this.items[this.items.length - 1];
  19. }
  20. // 如果栈里没有元素就返回 true,否则返回 false
  21. isEmpty() {
  22. return this.items.length === 0;
  23. }
  24. // 返回栈里的元素个数
  25. size() {
  26. return this.items.length;
  27. }
  28. // 移除栈里的所有元素
  29. clear() {
  30. this.items = [];
  31. }
  32. }
  1. /**
  2. * 使用 Stack 类
  3. */
  4. // 初始化 Stack 类
  5. const stack = new Stack();
  6. // 验证栈是否为空
  7. console.log(stack.isEmpty()); // true
  8. // 往栈里添加元素
  9. stack.push(2);
  10. stack.push(5);
  11. stack.push(8);
  12. // 查看栈顶的元素
  13. console.log(stack.peek()); // 8
  14. // 查看栈里的元素个数
  15. console.log(stack.size()); // 3
  16. // 验证栈是否为空
  17. console.log(stack.isEmpty()); // false
  18. // 移除栈里的元素
  19. console.log(stack.pop()); // 8
  20. console.log(stack.pop()); // 5
  21. // 移除栈里的所有元素
  22. stack.clear();
  23. // 验证栈是否为空
  24. console.log(stack.isEmpty()); // true
  25. console.log(stack.peek()); // undefined
  26. console.log(stack.size()); // 0

创建一个基于 JavaScript 对象的 Srack

比基于数组的栈迭代占用的时间更少。

  1. /**
  2. * 创建一个基于数组的栈
  3. */
  4. class Stack {
  5. constructor() {
  6. this.count = 0;
  7. this.items = {};
  8. }
  9. // 向栈中插入元素
  10. // 在 JavaScript 中,对象是一系列键值对的集合。使用 count 变量作为 items 对象的键名,插入的元素则是它的值
  11. push(element) {
  12. this.items[this.count] = element;
  13. this.count++;
  14. }
  15. // 移除栈顶的元素,同时返回被移除的元素
  16. pop() {
  17. if (this.isEmpty()) {
  18. return undefined;
  19. }
  20. this.count--;
  21. const result = this.items[this.count];
  22. delete this.items[this.count];
  23. return result;
  24. }
  25. // 返回栈顶的元素,但不对栈做任何修改
  26. peek() {
  27. if (this.isEmpty()) {
  28. return undefined;
  29. }
  30. return this.items[this.count - 1];
  31. }
  32. // 如果栈里没有元素就返回 true,否则返回 false
  33. isEmpty() {
  34. return this.count === 0;
  35. }
  36. // 返回栈里的元素个数
  37. size() {
  38. return this.count;
  39. }
  40. // 移除栈里的所有元素
  41. clear() {
  42. this.items = {};
  43. this.count = 0
  44. // while (!this.isEmpty()) {
  45. // this.pop();
  46. // }
  47. }
  48. // 创建一个 toString 方法来打印出栈的内容
  49. toString() {
  50. if (this.isEmpty()) {
  51. return '';
  52. }
  53. let objString = `${this.items[0]}`;
  54. for (let i = 1; i < this.count; i++) {
  55. objString = `${objString}, ${this.items[i]}`;
  56. }
  57. return objString;
  58. }
  59. }
  1. /**
  2. * 使用 Stack 类
  3. */
  4. // 初始化 Stack 类
  5. const stack = new Stack();
  6. // 验证栈是否为空
  7. console.log(stack.isEmpty()); // true
  8. // 往栈里添加元素
  9. stack.push(2);
  10. stack.push(5);
  11. stack.push(8);
  12. // 打印栈的内容
  13. console.log(stack.toString()); // 2, 5, 8
  14. // 查看栈顶的元素
  15. console.log(stack.peek()); // 8
  16. // 查看栈里的元素个数
  17. console.log(stack.size()); // 3
  18. // 验证栈是否为空
  19. console.log(stack.isEmpty()); // false
  20. // 移除栈里的元素
  21. console.log(stack.pop()); // 8
  22. console.log(stack.pop()); // 5
  23. // 移除栈里的所有元素
  24. stack.clear();
  25. // 验证栈是否为空
  26. console.log(stack.isEmpty()); // true
  27. console.log(stack.peek()); // undefined
  28. console.log(stack.size()); // 0
  29. // 从下面行 {1} 和 行 {2} 得出:count 和 items 属性是公开的,我们可以像行 {3} 那样直接访问它们
  30. // 根据这种行为,可以对这两个属性赋新的值
  31. console.log(Object.getOwnPropertyNames(stack)); // [ 'count', 'items' ]
  32. console.log(Object.keys(stack)); // [ 'count', 'items' ]
  33. console.log(stack.items); // {}

在 Stack 类声明的 itemscount 属性并没有得到保护。ES6 类是基于原型的,基于原型的类能节省内存空间并在扩展方面优于函数的类,但这种方式不能声明私有属性(变量)或方法。我们希望 Stack 类的用户只能访问我们在类中暴露的方法。

保护数据结构内部元素

1. 下划线命名约定

  1. class Stack1 {
  2. constructor() {
  3. this._count = 0;
  4. this._items = {};
  5. }
  6. // 栈的方法
  7. }

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

2. 用 ES6 的限定作用域 Symbol 实现 Stack 类

  1. const _items = Symbol('stackItems'); // 声明 Symbol 类型的变量 _items
  2. class Stack2 {
  3. constructor() {
  4. this[_items] = []; // 在类的构造函数中初始化它的值
  5. }
  6. // 栈的方法
  7. // 向栈中插入元素
  8. // 在 JavaScript 中,对象是一系列键值对的集合。使用 count 变量作为 items 对象的键名,插入的元素则是它的值
  9. push(element) {
  10. this[_items][this.count] = element;
  11. this.count++;
  12. }
  13. // 移除栈顶的元素,同时返回被移除的元素
  14. pop() {
  15. if (this.isEmpty()) {
  16. return undefined;
  17. }
  18. this.count--;
  19. const result = this[_items][this.count];
  20. delete this[_items][this.count];
  21. return result;
  22. }
  23. // 返回栈顶的元素,但不对栈做任何修改
  24. peek() {
  25. if (this.isEmpty()) {
  26. return undefined;
  27. }
  28. return this[_items][this.count - 1];
  29. }
  30. // 如果栈里没有元素就返回 true,否则返回 false
  31. isEmpty() {
  32. return this.count === 0;
  33. }
  34. // 返回栈里的元素个数
  35. size() {
  36. return this.count;
  37. }
  38. // 移除栈里的所有元素
  39. clear() {
  40. this[_items] = {};
  41. this.count = 0
  42. // while (!this.isEmpty()) {
  43. // this.pop();
  44. // }
  45. }
  46. // 创建一个 toString 方法来打印出栈的内容
  47. toString() {
  48. if (this.isEmpty()) {
  49. return '';
  50. }
  51. let objString = `${this[_items][0]}`;
  52. for (let i = 1; i < this.count; i++) {
  53. objString = `${objString}, ${this[_items][i]}`;
  54. }
  55. return objString;
  56. }
  57. }
  58. const stack = new Stack2();
  59. stack.push(5);
  60. stack.push(8);
  61. let objectSymbols = Object.getOwnPropertySymbols(stack);
  62. console.log(objectSymbols.length); // 1
  63. console.log(objectSymbols); // [ Symbol(stackItems) ]
  64. console.log(objectSymbols[0]); // Symbol(stackItems)
  65. stack[objectSymbols[0]].push(2);
  66. console.log(stack.toString()); // 2

这种方式创建了一个假的私有属性,因为 ES6 新增的 Object.getOwnPropertySymbols 方法能够取到类里面声明的所有 Symbol 属性。

3. 用 ES6 的 WeakMap 实现 Stack 类

  1. const items = new WeakMap(); // 声明一个 WeakMap 类型的变量 items
  2. class Stack3 {
  3. constructor() {
  4. items.set(this, []); // 以 this(Stack3 类自己的引用)为键,把代表栈的数组存入 items
  5. }
  6. push(element) {
  7. const s = items.get(this); // 从 WeakMap 中取出值,以 this 为键从 items 中取值
  8. s.push(element);
  9. }
  10. pop() {
  11. const s = items.get(this);
  12. const r = s.pop();
  13. return r;
  14. }
  15. // 栈的其他方法
  16. }

这种方式,items 在 Stack 类里是真正的私有属性。但代码可读性不强,而且在扩展该类时无法继承私有属性。鱼与熊掌不可兼得!

4. 通过在属性前添加 # 作为前缀来声明私有属性

  1. class Stack4 {
  2. #count = 0;
  3. #items = 0;
  4. // 栈的方法
  5. }

用栈解决进制转换问题

  1. /**
  2. * 创建一个基于数组的栈
  3. */
  4. class Stack {
  5. constructor() {
  6. this.items = []; // 选择一种数据结构(数组)来保存栈里的元素
  7. }
  8. // 添加一个(或几个)新元素到栈顶
  9. push(element) {
  10. this.items.push(element);
  11. }
  12. // 移除栈顶的元素,同时返回被移除的元素
  13. pop() {
  14. return this.items.pop();
  15. }
  16. // 返回栈顶的元素,但不对栈做任何修改
  17. peek() {
  18. return this.items[this.items.length - 1];
  19. }
  20. // 如果栈里没有元素就返回 true,否则返回 false
  21. isEmpty() {
  22. return this.items.length === 0;
  23. }
  24. // 返回栈里的元素个数
  25. size() {
  26. return this.items.length;
  27. }
  28. // 移除栈里的所有元素
  29. clear() {
  30. this.items = [];
  31. }
  32. }
  • 十进制转二进制
  1. // 十进制转二进制
  2. function decimalToBinary(decNumber) {
  3. const remStack = new Stack();
  4. let number = decNumber;
  5. let rem;
  6. let binaryString = '';
  7. while (number > 0) {
  8. rem = Math.floor(number % 2);
  9. remStack.push(rem);
  10. number = Math.floor(number / 2);
  11. }
  12. while (!remStack.isEmpty()) {
  13. binaryString += remStack.pop().toString();
  14. }
  15. return binaryString;
  16. }
  17. console.log(decimalToBinary(233)); // 11101001
  18. console.log(decimalToBinary(10)); // 1010
  19. console.log(decimalToBinary(1000)); // 1111101000
  • 进制转换算法
  1. // 进制转换算法
  2. function baseCnverter(decNumber, base) {
  3. const remStack = new Stack();
  4. const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  5. let number = decNumber;
  6. let rem;
  7. let baseString = '';
  8. if (!(base >= 2 && base <= 36)) {
  9. return '';
  10. }
  11. while (number > 0) {
  12. rem = Math.floor(number % base);
  13. remStack.push(rem);
  14. number = Math.floor(number / base);
  15. }
  16. while (!remStack.isEmpty()) {
  17. baseString += digits[remStack.pop()];
  18. }
  19. return baseString;
  20. }
  21. console.log(baseCnverter(100345, 2)); // 11000011111111001
  22. console.log(baseCnverter(100345, 8)); // 303771
  23. console.log(baseCnverter(100345, 16)); // 187F9
  24. console.log(baseCnverter(100345, 35)); // 2BW0