1. 栈
特点
栈是遵循 后进先出(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
}
// 获取栈的长度
size() {
return this.items.length
}
// 清空栈元素
clear() {
this.items = []
}
}
基于对象实现
基于数组实现栈是最简单的方式,但在使用时,我们需要迭代整个数组直到找到需要的元素,时间复杂度为O(n);另外数组是元素的一个有序集合,为了保证元素排列有序,会占用更多的内存空间。因此我们可以增加一个count属性,用对象来实现栈结构。
class Stack {
constructor() {
this.count = 0
this.items = {}
}
push(element) {
this.items[this.count++] = element
}
pop() {
if (this.isEmpty()) {
return undefined
}
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
}
size() {
return this.count
}
clear() {
this.count = 0
this.items = {}
}
// 自定义toString方法
toString() {
if (this.isEmpty()) {
return ''
}
let result = ''
for (let i = 0; i < this.count; i++) {
result += `${this.items[i]}${i === this.count - 1 ? '' : ','}`
}
return result
}
}
保护数据结构内部元素
对于栈数据结构来说,要确保元素只会被添加到栈顶,而不是栈底或其他任意位置。但是,在Stack类中声明的items和count属性并没有得到保护,可以被任意修改。
const stack = new Stack()
Object.getOwnPropertyNames(stack) // [ 'count', 'items' ]
Object.keys(stack) // [ 'count', 'items' ]
console.log(stack.items, stack.count) // {}, 0
我们希望Stack类的用户只能访问我们在类中暴露的方法
用ES6的限定作用域Symbol实现类
ES6新增了Symbol原始数据类型,表示独一无二的值,我们这里用作对象的属性。
const _ items = Symbol('stackItem')
class Stack {
constructor() {
this.count = 0
this[_items] = {}
}
// self-defining function
// 新增一个打印方法
print() {
return this[_items]
}
}
这种方式也不能避免属性被访问,虽然用了Symbol类型来避免被Object.keys()和Object.getOwnPropertyNames()方法访问,但ES6同样提供了Object.getOwnPropertySymbols()方法来获取所有Symbol属性
const stack = new Stack()
Object.getOwnPropertyNames(stack) // [ 'count' ]
Object.getOwnPropertySymbols(stack) // [ Symbol(stackItem) ]
let objectSymbol = stack[Object.getOwnPropertySymbols(stack)[0]] // 获取_items对象
objectSymbol[1] = 2
stack.print() // { '1': 2 }
使用ES6的WeakMap实现类
思路就是通过WeakMap保存一个数组,之后的步骤都先用get函数将数组取出来进行操作;缺点是代码可读性不强,而且扩展该类时无法继承私有属性。
const items = new WeakMap()
class Stack {
constructor() {
items.set(this, [])
}
push(element) {
const s = items.get(this)
s.push(element)
}
pop() {
const s = items.get(this)
const result = s.pop()
return result
}
peek() {
const s = items.get(this)
return s[s.length - 1]
}
isEmpty() {
const s = items.get(this)
return s.length === 0
}
size() {
const s = items.get(this)
return s.length
}
clear() {
items.set(this, [])
}
}
实际使用场景
转换进制
function baseConverter(decNumber, base = 2) {
const remStack = new Stack()
const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
let number = decNumber
let baseString = ''
// 2-36进制
if (base < 2 || base > 36) {
return ''
}
// 取decNumber的模运算
while (number > 0) {
let rem = Math.floor(number % base)
remStack.push(rem)
number = Math.floor(number / base)
}
// 取反操作
while (!remStack.isEmpty()) {
baseString += digits[remStack.pop()]
}
return baseString
}
2. 队列和双端队列
特点
- 队列是遵循 先进先出(
FIFO) 原则的一组有序的项。 - 双端队列是一种允许同时在前端和后端添加/移除元素的特殊队列,可以看作是队列和栈相结合的一种数据结构。
实现
队列
class Queue {
constructor() {
this.countIndex = 0 // 最后一个元素(队尾)的索引
this.lowestIndex = 0 // 第一个元素(队首)的索引
this.items = {}
}
// 向队列尾部添加元素
enqueue(element) {
this.items[this.countIndex++] = element
}
// 移除队列第一项
dequeue() {
if (this.isEmpty()) {
return undefined
}
const result = this.items[this.lowestIndex]
delete this.items[this.lowestIndex++] // 将第一个元素移除,并将下标往后移动
return result
}
// 返回队列第一个元素
peek() {
if (this.isEmpty()) {
return undefined
}
return this.items[this.lowestIndex]
}
// 通过两个下标差值判断isEmpty和size
isEmpty() {
return this.size() === 0
}
size() {
return this.countIndex - this.lowestIndex
}
// 清空对列,直接初始化操作
clear() {
this.countIndex = 0
this.lowestIndex = 0
this.items = {}
}
toString() {
if (this.isEmpty()) {
return ''
}
let result = ''
for (let i = this.lowestIndex; i < this.countIndex; i++) {
result += `${this.items[i]}${i === this.countIndex - 1 ? '' : ','}`
}
return result
}
}
双端队列
class Deque {
constructor() {
this.countIndex = 0
this.lowestIndex = 0
this.items = {}
}
// 在队列前端添加元素
addFront(element) {
if (this.isEmpty()) {
this.addBack() // 实现了countIndex++的逻辑,直接调用
} else {
this.items[--this.lowestIndex] = element
}
}
addBack(element) {
this.items[this.countIndex++] = element
}
// 移除队列第一项,和队列移除方法一致
removeFront() {
if (this.isEmpty()) {
return undefined
}
const result = this.items[this.lowestIndex]
delete this.items[this.lowestIndex++] // 将第一个元素移除,并将下标往后移动
return result
}
removeBack() {
if (this.isEmpty()) {
return undefined
}
const result = this.items[this.countIndex - 1]
delete this.items[this.countIndex--] // 将最后一个元素移除,并将下标往前移动
return result
}
peekFront() {
if (this.isEmpty()) {
return undefined
}
return this.items[this.lowestIndex]
}
peekEnd() {
if (this.isEmpty()) {
return undefined
}
return this.items[this.countIndex - 1]
}
// 通过两个下标差值判断isEmpty和size,和queue方法一致
isEmpty() {
return this.size() === 0
}
size() {
return this.countIndex - this.lowestIndex
}
clear() {
this.countIndex = 0
this.lowestIndex = 0
this.items = {}
}
toString() {
if (this.isEmpty()) {
return ''
}
let result = ''
for (let i = this.lowestIndex; i < this.countIndex; i++) {
result += `${this.items[i]}${i === this.countIndex - 1 ? '' : ','}`
}
return result
}
}
实际使用场景
使用队列模拟击鼓传花
function hotPotato(elementsList, num) {
const queue = new Queue()
const elimitatedList = [] // 淘汰者列表
for (let i = 0; i < elementsList.length; i++) {
queue.enqueue(elementsList[i]) // 为队列添加元素
}
// size为1时,决出优胜者
while (queue.size() > 1) {
for (let i = 0; i < num; i++) {
queue.enqueue(queue.dequeue()) // 将队首元素添加到队尾
}
elimitatedList.push(queue.dequeue()) // 将队首元素设置为淘汰者
}
return {
eliminated: elimitatedList,
winner: queue.dequeue(), // 返回队列中的最后一个元素为优胜者
}
}
const names = ['John', 'Jack', 'Camila', 'Ingrid', 'Carl']
const result = hotPotato(names, 7)
result.eliminated.forEach(name => {
console.log(`${name}在击鼓传花游戏中被淘汰`)
})
console.log(`胜利者: ${result.winner}`)
利用双端队列检查回文
function palindromeChecker(str) {
if (str === undefined || str === null || !str.length) {
return false
}
const deque = new Deque()
const lowerStr = str.toLowerCase().split(' ').join('')
let isEqual = true
let firstChar, lastChar
for (let i = 0; i < str.length; i++) {
deque.addBack(lowerStr.charAt(i)) // 为双端队列添加元素
}
while(deque.size() > 1 && isEqual) { // 如果只剩最后一个字母,且之前判断isEqual都为true,那么肯定是回文
firstChar = deque.removeFront()
lastChar = deque.removeBack()
isEqual = firstChar === lastChar // 判断队首和队尾是否相同
}
return isEqual
}
3. 链表
特点
- 链表存储有序的元素集合,但不用于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用组成。相较于数组,这种数据结构在添加或移除元素的时候不需要移动其他元素。
- 双向链表在普通链表的基础上,在
Node节点增加了prev属性,并增加tail指向链表尾,使链表变成双向。 - 循环链表既可以像普通链表一样单向引用,也可以像双向链表一样有双向引用,它的特点是最后一个元素指向下一个元素的指针不是
undefined,而是指向第一个元素。 - 有序列表是确保元素按一定顺序排列的链表结构。其中排序算法可以自定义。
实现
普通链表
function defaultEquals(a, b) {
return a === b
}
class Node {
constructor(element) {
this.element = element
this.next = undefined
}
}
class LinkedList {
constructor(equalsFn = defaultEquals) {
this.count = 0
this.head = undefined // 第一个元素的引用
this.equalsFn = equalsFn
}
// 向链表尾部添加一个元素,调用insert函数,用count作为索引,count = max_length + 1
push(element) {
this.insert(element, this.count)
}
insert(element, index) {
if (index < 0 || index > this.count) {
return false // 超出索引,返回false
}
const node = new Node(element)
if (index === 0) {
const current = this.head // 用current保存head指针
node.next = current // node.next指向current
this.head = node // 更新指针
} else {
const previous = this.getElementAt(index - 1) // 获取添加位置的上一个node
const current = previous.next // this.getElementAt(index)
// 在previous和current中间插入node
previous.next = node
node.next = current
}
this.count += 1
return true
}
// 获取指定位置的元素
getElementAt(index) {
if (index < 0 || index >= this.count) {
return undefined // 超出索引,返回undefined
}
let current = this.head
for (let i = 0; i < index && current != null; i++) {
current = current.next
}
return current
}
// 返回元素在链表中的索引
indexOf(element) {
let current = this.head
for (let i = 0; i < this.count && current != null; i++) {
if (this.equalsFn(current.element, element)) {
return i
}
current = current.next
}
return -1
}
// 根据element值移除元素
remove(element) {
const index = this.indexOf(element)
return this.removeAt(index)
}
// 移除某个位置的元素
removeAt(index) {
if (index < 0 || index >= this.count) {
return undefined // 超出索引,返回undefined
}
let current = this.head
if (index === 0) {
// 删除第一个元素,直接改变head指向
this.head = current.next
} else {
// 将previous.next指向current.next,跳过current即代表删除
let previous = this.getElementAt(index - 1)
current = previous.next
previous.next = current.next
}
this.count -= 1
return current
}
isEmpty() {
return this.size() === 0
}
size() {
return this.count
}
toString() {
if (this.isEmpty()) {
return ''
}
let result = ''
let current = this.head
for (let i = 0; i < this.count; i++) {
result += `${current.element}${i === this.count - 1 ? '' : ','}`
current = current.next
}
return result
}
}
双向链表
class DoublyNode extends Node {
constructor(element, next, prev) {
super(element, next)
this.prev = prev
}
}
class DoublyLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals) {
super(equalsFn)
this.tail = undefined // 链表尾部的引用
}
insert(element, index) {
if (index < 0 || index > this.count) {
return false // 超出索引,返回false
}
const node = new DoublyNode(element)
let current = this.head
if (index === 0) {
if (this.count === 0) {
// 如果没有元素,则head,tail指向同一元素
this.head = node
this.tail = node
} else {
node.next = current
current.prev = node // 不同于普通链表,还需要设置prev属性
this.head = node
}
} else if (index === this.count) {
// 添加最后一位,记住同时设置next和prev即可
current = this.tail
current.next = node
node.prev = current
this.tail = node
} else {
const previous = this.getElementAt(index - 1)
current = previous.next
// 三个节点的next,prev互相引用
previous.next = node
current.prev = node
node.next = current
node.prev = previous
}
this.count += 1
return true
}
removeAt(index) {
if (index < 0 || index >= this.count) {
return undefined // 超出索引,返回undefined
}
let current = this.head
if (index === 0) {
this.head = current.next
this.head.prev = undefined // 将prev设置为undefined
if (this.count === 1) {
// 如果只有一个元素,删除后就清空head和tail
this.tail = undefined
}
} else if (index === this.count - 1) {
// 删除链表尾
current = this.tail
this.tail = current.prev
this.tail.next = undefined
} else {
current = this.getElementAt(index)
const previous = current.prev
previous.next = current.next
current.next.prev = previous // current的下一个元素的prev指向previous
}
this.count -= 1
return current
}
// 从后向前查找元素
lastIndexOf(element) {
let current = this.tail
for (let i = this.count - 1; i >= 0 && current != null; i--) {
if (this.equalsFn(current.element, element)) {
return i
}
current = current.prev
}
return -1
}
}
循环链表
class CircularLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals) {
super(equalsFn)
}
insert(element, index) {
if (index < 0 || index > this.count) {
return false // 超出索引,返回false
}
const node = new Node(element)
if (index === 0) {
if (this.count == 0) {
this.head = node // 如果是添加的第一个元素,将head指向node
node.next = this.head // node.next指向第一个元素,即head本身
} else {
let current = this.head // 找到首尾两个元素
let tail = this.getElementAt(this.size() - 1)
node.next = current
this.head = node
tail.next = this.head
}
} else {
// 在中间或尾部插入不会有任何变化
const previous = this.getElementAt(index - 1)
const current = previous.next
previous.next = node
node.next = current
}
this.count += 1
return true
}
removeAt(index) {
if (index < 0 || index >= this.count) {
return undefined // 超出索引,返回undefined
}
let current = this.head
if (this.count === 1) {
// 本身只有一个元素,直接将head赋值undefined
this.head = undefined
} else {
if (index === 0) {
// 删除第一个元素
const tail = this.getElementAt(this.count - 1) // 获取最后一个元素
this.head = current.next
tail.next = this.head
} else {
const previous = this.getElementAt(index - 1)
current = previous.next
previous.next = current.next
}
}
this.count -= 1
return current
}
}
有序列表
const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1,
}
function defaultCompare(a, b) {
if (a === b) {
return 0
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN
}
class SortedLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals, compareFn = defaultCompare) {
super(equalsFn)
this.compareFn = compareFn
}
insert(element) {
if (this.isEmpty()) {
return super.insert(element, 0)
}
const pos = this.getIndexNextSortedElement(element)
return super.insert(element, pos)
}
// 获取element应该插入的位置
getIndexNextSortedElement(element) {
let current = this.head
let i = 0
for (; i < this.count && current != null; i++) {
const result = this.compareFn(element, current.element)
if (result === Compare.LESS_THAN) {
return i // 找到比element大的位置,插入该位置
}
current = current.next
}
return i // 找不到,即element最大,插入最后一位
}
}
实际使用场景
使用链表实现栈结构
class StackLinkedList {
constructor() {
this.items = new DoublyLinkedList() // 使用双向链表实例给items赋值
}
push(element) {
this.items.push(element)
}
pop() {
if (this.isEmpty()) {
return undefined
}
return this.items.removeAt(this.size() - 1)
}
peek() {
if (this.isEmpty()) {
return undefined
}
return this.items.getElementAt(this.size() - 1).element
}
isEmpty() {
return this.items.isEmpty()
}
size() {
return this.items.size()
}
toString() {
return this.items.toString()
}
}