栈数据结构
栈是一种遵从后进先出(LIFO)原则的有序集合。新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都靠近栈底。
在现实生活中也有很多栈的例子,比如课桌上的一摞书或者餐厅里叠放的盘子。栈也被用在编程语言的编译器和内存中保存变量、方法调用等,也被用于浏览器历史记录(浏览器的返回按钮)。
封装一个基于数组的栈
我们需要一种数据结构来保存栈里的元素。可以选择数组。数组允许我们在任何位置添加或删除元素。由于栈遵循 LIFO 原则 ,需要对元素的插入和删除功能进行限制。栈常用的方法如下:
- push(element(s)): 添加一个(或几个)新元素到栈顶。
- pop():移除栈顶的元素,同时返回被移除的元素。
- peek():返回栈顶的元素,不对栈做任何修改(该方法不会移除栈顶的元素,仅仅返回它)。
- isEmpty():如果栈里没用任何元素就返回 true,否则返回 false。
- clear():移除栈里的所有元素。
- size():返回栈里的元素个数。该方法和数组的 length 属性很类似。
封装一个基于 JavaScript 对象的栈
在上面使用数组时,大部分方法的时间复杂度是 O(n)。我们需要迭代整个数组直到找到要找的那个元素,在最坏的情况下需要迭代数组的所有位置,n 代表数组的长度。如果数组有更多的元素,所需的时间更长。另外,数组是元素的一个有序集合,为了保证元素排列有序,它会占用更多的内存空间。
如果我们能直接获取元素,占用较少的内存空间,并且仍然保证所有元素按照我们的需要排列,那不是更好吗?对于使用 JavaScript 语言实现栈数据结构的场景,我们也可以使用一个 JavaScript 对象来存储所有的栈元素,保证它们的顺序并且遵循 LIFO 原则。
保护数据结构内部元素
对于 Stack 类来说,要确保元素只会被添加到栈顶,而不是栈底或其它任意位置(比如栈的中间)。但是我们创建的 Stack 类中的 items 和 count 属性并没有受到保护,我们希望使用 Stack 类的用户只能访问在类中暴露的方法。 ES2015 类是基于原型的,尽管基于原型的类能节省空间并在扩展方面优于基于函数的类,但这种方式不能声明私有属性(变量)或方法。下面看看有几种方式实现私有属性:
- 下划线命名约定
下划线命名约定就是在属性名称之前加上一个下划线( _ ),不过这种方式只是一种约定,并不能保护数据,而且只能依赖于开发者也使用这种约定。
- 用 ES2015 的限定作用域 Symbol 实现类
ES2015 新增了一种叫作 Symbol 的基本类型,它是不可变的,可以用作对象的属性。看看怎么用它在 Stack 类中声明 items 属性(我们使用数组来存储元素以简化代码)。
当然这种方法也是创建来一个假的私有属性,因为 ES2015 新增的 Object.getOwnPropertySymbols 方法能够取到类里面声明的所有 Symbols 属性。
- 用 ES2015 的 WeakMap 实现类
ES2015 提供的 WeakMap 数据类型可以确保属性是私有的, WeakMap 可以存储键值对,其中键是对象,值可以是任意数据类型。但是采用这种方法,代码的可读性不强,而且在扩展时无法继承私有属性。
- ECMAScript 类属性提案
TypeScript 提供了一个给类属性和方法使用的 private 修饰符。然而,该修饰符只在编译时有用,在代码被转译完成后,属性同样是公开的。事实上,我们不能像在其它编程语言中一样声明私有属性和方法。虽然有很多种方法都可以达到相同的效果,但无论是在语法还是性能层面,这些方法都有各自的优点和缺点。有一个关于在 JavaScript 类中增加私有属性的提案,可以通过在属性前添加 # 作为前缀来声明私有属性。
用栈解决问题
- 十进制转二进制
现实生活中,我们主要使用十进制。但在计算科学中,二进制非常重要,因为计算机里的所有内容都是用二进制数字表示的(0和1)。没有十进制和二进制相互转化的能力,与计算机交流就很困难。要把十进制转化成二进制,我们可以将该十进制数除以2(二进制是满二进一)并对商取整,直到结果是0为止。最后将余数倒序排列。
- 进制转换算法
我们可以修改之前的算法,使之能把十进制转换成基数为2~36的任意进制。除了把十进制数除以2转成二进制数,还可以传入其他任意进制的基数为参数,就像下面的算法这样。
我们只需要改变一个地方。在将十进制转成二进制时,余数是0或1;在将十进制转成八进制时,余数是 0~7;但是将十进制转成十六进制时,余数是 0~9 加上A、B、C、D、E和F(对应10、11、12、13、14和15)。因此,我们需要对栈中的数字做个转化才可以。因此,从十一进制开始,字母表中的每个字母将表示相应的基数。字母A代表基数11, B代表基数12,以此类推。