1. JavaScript简介
2. ECMAScript和TypeScript概述
2.2.7 使用类进行面向对象编程
class Book {
constructor(title, pages, isbn) {
this.title = title
this.pages = pages
this.isbn = isbn
}
printTitle() {
console.log(this.title)
}
}
let book = new Book('title', 'pag')
book.title // title
book.pages // pag
1. 继承
关于super
关键字:
ES6要求,子类的构造函数必须执行一次super函数,否则会报错。
super
这个关键字,既可以当做函数使用,也可以当做对象使用,这两种情况下,它的用法完全不通;
-
当做函数使用
在 constructor 中必须调用 super 方法,因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工,而 super 就代表了父类的构造函数。super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部的 this 指的是 B,因此 super() 在这里相当于
A.prototype.constructor.call(this, props)
-
当做对象使用
在普通方法中,指向父类的原型对象;在静态方法中,指向父类;
class ITBook extends Book {
constructor(title, pages, isbn, technology) {
super(title, pages, isbn)
this.technology = technology
}
printTechnology () {
console.log(this.technology)
}
}
let jsBook = new ITBook('学习js算法', '210', '123456', 'JavaScript')
jsBook.title // '学习js算法'
jsBook.printTechnology // 'JavaScript'
2. 使用属性存取器
class Person {
constructor(name) {
this._name = name
}
get name () {
return this._name
}
set name(value) {
this._name = value
}
}
2.2.8 乘方运算符
const area = 3.14 * r * r
const area = 3.14 * Math.pow(r, 2)
const area = 3.14 * (r ** 2)
// 以上三种方式相同
3. 数组
shift pop
unshift push
3.5 在任意位置添加或删除元素
number.splice(5, 3) // 从索引5开始,长度为3的三个元素被删除
number.splice(5, 0, 2, 3, 4) // 从索引5开始,插入2,3,4
number.splice(5, 3, 2, 3, 4) // 从索引5开始,长度为3的三个元素删除,同时插入2,3,4三个元素
3.6 二维和多维数组
矩阵(二维数组,或数组的数组)
// 想看矩阵的输出,可以创建一个通用函数
function printMatrix(myMatrix) {
for (let i = 0; i < myMatrix.length; i++) {
for (let j = 0l j < myMatrix[i].length; j++) {
console.log(myMatrix[i][j])
}
}
}
3.7 JavaScript的数组方法参考
3.7.1 数组合并
const zero = 0;
const positiveNumbers = [1, 2, 3];
const negativeNumbers = [-3, -2, -1];
let numbers = negativeNumbers.concat(zero, positiveNumbers) // [-3, -2, -1, 0, 1, 2, 3]
3.7.2 迭代器函数
4. 栈
4.2 栈数据结构
后进先出的有序集合,新添加或者待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫做栈底;
4.2.1 创建一个基于数组的栈
// 用数组
class Stack {
constructor() {
this.items = []
}
push(item) {
this.item.push(item)
}
pop() {
return this.items.pop()
}
peek() {
return this.items[this.item.length - 1]
}
isEmpty() {
return this.items.length === 0
}
size() {
return this.items.length
}
clear() {
this.items = []
}
}
4.3 创建一个基于JavaScript对象的Stack 类
// 用对象
class ObjStack {
constructor() {
this.count = 0
this.items = {}
}
push(item) {
this.items[this.count] = item
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]
}
clear() {
this.items = {}
this.count = 0
}
toString() {
if (this.isEmpty()) {
return ''
}
let str = `${this.items[0]}`
for (let i = 1; i < this.count; i++) {
str = `${str},${this.items[i]}`
}
return str
}
}
4.4 保护数据结构内部元素
尽管基于原型的类能节省内存空间并在拓展方面优于基于函数的类,但是这种方式不能声明私有属性或方法,这会使得数据结构内部十分危险,因为可以通过修改属性或者方法来改变数据结构;
4.4.1 下划线命名约定
一些开发者喜欢在js中使用下划线命名约定来标记一个属性为私有属性;
但是这只是一种约定,并不能保护数据,只能依赖于开发者的常识;
4.4.2 用Symbol实现类
const _items = Symbol('stackItems')
class Stack {
constructor() {
this[_items] = []
}
}
这种方法创建了一个假的私有属性,因为Object.getOwnPropertySymbols方法可以获取到类中声明的所有Symbols属性;
const stack = new Stack()
stack.push(5)
stack.push(8)
let objectSymbols = Object.getOwnPropertySymbols(stack)
stack[objectSymbols[0]].push(1)
console.log(stack) // 5,8,1
从上面的代码可以看到,访问stack[objectSymbols[0]]是可以得到_items的,并且_items是一个数组,可以进行任意的数组操作;
4.4.3 用WeakMap实现类
const items = new WeakMap()
class Stack {
constructor() {
items.set(this, [])
}
push(item) {
const s = item.get(this)
s.push(item)
}
pop() {
const s = item.get(this)
const r = s.pop()
return r
}
}
items是Stack类里真正的私有属性;采用这种方法,代码的可读性不强,而且在扩展该类时无法继承私有属性;
4.4.4 ECMAScript类属性提案
class Stack {
#count = 0
#items = 0
}
TS提供了一个给类属性和方法使用的private修饰符,然后该修饰符只在编译时有用,在代码被转移完成后,属性同样是公开的;
一个有关于js类中增加私有属性的提案;使用#作为前缀来声明私有属性;
4.5 用栈解决问题
4.5.1 从十进制到二进制
function decimalToBinary(decNumber) {
const remStack = new ObjStack()
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
}
4.5.2 包含min的栈
class MinStack {
constructor () {
// stackA 用于存储数据
this.stackA = []
this.countA = 0
// stackB 用于将数据降序存储
this.stackB = []
this.countB = 0
}
push (item) {
this.stackA[this.countA++] = item
if (this.countB === 0 || item < this.min()) {
this.stackB[this.countB++] = item
}
}
min () {
return this.stackB[this.countB - 1]
}
top () {
return this.stackA[this.countA - 1]
}
pop () {
if (this.top() === this.min()) {
delete this.stackB[--this.countB]
}
delete this.stackA[--this.countA]
}
}
4.5.3 leetCode739. 每日温度
// 输入每天的温度,输出该天的温度,经过多少天会升温
// 输入[73,74,75,71,69,72,76,73]
// 输出[1, 1, 4, 2, 1, 1, 0, 0]
// 每日温度问题
var dailyTemperatures = function(T) {
const stack = new Stack()
const arr = new Array(T.length).fill(0)
for (let i = 0; i < T.length; i++) {
let temp = T[i]
while(stack.size() && temp > T[stack.peek()]) {
let index = stack.pop()
arr[index] = i - index
}
stack.push(i)
}
return arr
}
console.log(dailyTemperatures([73,74,75,71,69,72,76,73]))
5. 队列和双端队列
队列是遵循先进先出原则的一组有序的项;队列在尾部添加新元素,并从顶部移除元素,最新添加的元素必须排在队列的末尾;
5.1 队列数据结构
因为我们要从头部移除元素,所以我们需要一个变量来追踪头部元素
对于队列的长度而言,变成了lowestCount到count之间的距离;
// 队列
class Queue {
constructor() {
this.count = 0 // 队列总数
this.lowestCount = 0 // 追踪第一个元素的序列
this.items = {}
}
// 向队列尾巴添加元素
enqueue(element) {
this.item[this.count] = element
this.count++
}
// 从队列移除头部元素
dequeue() {
if (this.isEmpty()) {
return undefined
}
const result = this.items[this.lowestCount]
delete this.item[this.lowestCount]
this.lowestCount++
return result
}
// 返回头部元素
peek() {
if (this.isEmpty()) {
return undefined
}
return this.items[this.lowestCount]
}
isEmpty() {
return this.size() === 0
}
size() {
return this.count - this.lowestCount
}
// 清空队列
clear() {
this.items = {}
this.count = 0
this.lowestCount = 0
}
toString() {
if (this.isEmpty()) {
return ''
}
let str = this.items[this.lowestCount]
for (let i = this.lowestCount + 1; i < this.count; i++) {
str +=',' + this.items[i]
}
return str
// return Object.value(this.items).join(',')
}
}
5.2 双端队列数据结构
双端队列是一种允许我们同时从前端和后端添加和移除元素的特殊队列;
由于双端队列同时遵守了先进先出和后进先出的原则,可以说它是把队列和栈结合的一种数据结构;
对于双端队列的长度而言,变成了lowestCount到count之间的距离;
因为前端和后端都会被改变,所以此时双端队列的头和尾都需要指针;
这里只需要专注于addFront方法的逻辑
有三种情况:
-
双端队列是空的;
直接调用addBack方法
-
已经有元素从双端队列的前端移除,lowestCount > 0时;
lowestCount需要减1,并在lowestCount位置添加新元素
-
没有元素从前端移除过,lowestCount === 0时;
所有的元素都需要向后移动一个位置,腾出lowestCount(此时为0)位置来添加新元素;
// 双端队列
class Deque {
constructor() {
this.count = 0 // 队尾的序列号
this.lowestCount = 0 // 追踪第一个元素的序列
this.items = {}
}
/*
* 新增addFront addBack removeFront removeBack peekFront peekBack 方法
*/
// 向双端队列的前端添加元素
addFront(element) {
if (this.isEmpty()) {
this.addBack(element)
// 已经有元素从前端被移除了
} else if (this.lowestCount > 0) {
this.lowestCount--
this.items[this.lowestCount] = element
// 没有元素被移除
} else {
// 所有的元素都向后移动了一位,来空出第一个位置
for (let i = this.count; i > 0; i--) {
this.items[i] = this.items[i - 1]
}
this.count++
this.lowestCount = 0 // 这一步可以考虑不要 因为本来就是0
this.item[0] = element
}
}
// 与enqueue方法相同
addBack(element) {
this.item[this.count] = element
this.count++
}
// 与dequeue方法相同
removeFront() {
if (this.isEmpty()) {
return undefined
}
const result = this.items[this.lowestCount]
delete this.item[this.lowestCount]
this.lowestCount++
return result
}
// 与Stack类中的pop方法一样
removeFront() {
if (this.isEmpty()) {
return undefined
}
this.count--
const result = this.items[this.count]
delete this.items[this.count]
return result
}
// 与peek方法相同
peekFront() {
if (this.isEmpty()) {
return undefined
}
return this.items[this.lowestCount]
}
// 与Stack类中的peek方法一样
peekBack() {
if (this.isEmpty()) {
return undefined
}
return this.items[this.count - 1]
}
}
5.2.1 leetCode59. 队列的最大值
// 请定义一个队列并实现函数max_value得到队列里的最大值
var MaxQueue = function () {
// 存储队列数据
this.queue = {}
// 双端队列维护最大值
this.deque = {}
// 准备队列相关数据
this.countQ = this.countD = this.headQ = this.headD = 0
}
MaxQueue.prototype.push_back = function(value) {
this.queue[this.countQ++] = value
// 检测是否可以将数据添加到双端队列中
// 新增的这个值 要是大于队尾值 则队尾出队 新值入队
// ? 直接把最大的值留下会怎么样?
// 不行!!如果这样删除的时候队列就会变成空值了
// 此时要保证双端队列是一个单调递减的队列 队首就是最大的值
// 10 9 11
// 10
// if (!this.isEmptyDeque()) {
// this.deque[0] = value
// }
// if (value > this.deque[0]) {
// this.deque[0] = value
// }
while (!this.isEmptyDeque() && value > this.deque[this.countD - 1]) {
delete this.deque[--this.countD]
}
this.deque[this.countD++] = value
}
MaxQueue.prototype.pop_front = function() {
if (this.isEmptyQueue()) {
return -1
}
// 比较deque与queue的队首值,如果相同,deque出队,否则deque不操作
if (this.queue[this.headQ] === this.deque[this.headD]) {
delete this.deque[this.headD++]
}
const frontData = this.queue[this.headQ]
delete this.queue[this.headQ++]
return frontData
}
MaxQueue.prototype.max_value = function () {
return this.deque[this.headD]
}
MaxQueue.prototype.isEmptyDeque = function () {
return !(this.countD - this.headD)
}
MaxQueue.prototype.isEmptyQueue = function () {
return !(this.countQ - this.headQ)
}
总结:
双端队列可以维护成单调递增或者单调递减;这样队首就是最小值或者最大值;
对于特殊情况,如果删除的数字刚好是队列的队首,那么需要把队首出队;
5.2.2 leetCode59. 滑动窗口
// 滑动窗口
var maxSlidingWindow = function(nums, k) {
if (k <= 1) {
return nums
}
const result = []
const deque = []
deque.push(nums[0])
let i = 1
for (; i < k; i++) {
while(deque.length && nums[i] > deque[deque.length - 1]) {
deque.pop()
}
// 意味着其实每个数字都要入队,只是看队尾的数字出不出队
deque.push(nums[i])
}
result.push(deque[0])
// 遍历后续的数据
const len = nums.length
for (; i < len; i++) {
while(deque.length && nums[i] > deque[deque.length - 1]) {
deque.pop()
}
deque.push(nums[i])
// 最关键的一步
// 当最大值在窗口外的时候 要将队首的去掉
// 检测当前最大值是否位于窗口外
if (deque[0] === nums[i - k]) {
deque.shift()
}
result.push(deque[0])
}
return result
}
总结:
前k长度的队列与k—nums.length长度的队列需要分开遍历
前k端直接查最大值
后len端查询的同时注意特殊情况 如果队列的长度大于了k(当最大值在窗口外的时候,要保证队列的长度不能大于k),要将队首的值出队,并将后续值继续入队(这里注意,当队列的值大于k时,deque[0] === nums[i - k]必定成立)
5.3 实践 —循环队列和回文检查
5.4 小结
- 学了了通过enqueue方法和dequeue方法并遵循先进先出原则来添加和移除元素
- 学习了双端队列数据结构,如何将元素添加到双端队列的前端和后端,以及如何将元素从双端队列的前端和后端移除
- 循环队列和回文检查
6. 链表
链表存储有序的元素集合,但不同于数组,链表中的元素在内存并不是连续放置的,每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称为指针,链接)组成;
想要访问链表中间的一个元素,则需要从起点开始迭代链表直到直到所需的元素;
6.1 单向链表
push方法
要向链表的尾部添加一个元素,必须找到最后一个元素,记住,我们只有第一个元素的引用!因此需要循环访问列表,知道找到最后一项,为此,我们需要一个指向链表中current项的变量;
removeAt方法
current变量总是对所循环列表的当前元素的引用,我们还需要一个对当前元素的前一个元素的引用previous;
// 链表
class LinkedList {
constructor(equalsFn = defaultEquals) {
this.count = 0 // 链表中的元素数量
this.head = undefined // 第一个元素的引用
this.equalsFn = equalsFn // 比较元素是否相等
}
// 尾巴添加一个新元素
push(element) {
const node = new Node(element)
let current;
if (this.head == null) {
this.head = node
} else {
// 很好的诠释了 想要访问链表中间的一个元素,则需要从起点开始迭代链表直到直到所需的元素;
current = this.head
// 找到了最后一个元素 最后一个元素的next为null
while (current.next != null) {
current = current.next
}
current.next = node
}
this.count++
}
// 向指定位置插入一个新元素
insert(element, index) {
if(index >= 0 && index <= this.count) {
const node = new Node(element)
let current = this.head
// 在第一个位置添加
if (index === 0) {
node.next = current
this.head = node
} else {
// 在中间位置添加
let previous = this.getElementAt(index - 1)
current = previous.next
node.next = current
previous.next = node
}
}
return false
}
// 移除一个元素
remove(element) {
const index = this.indexOf(element)
return this.removeAt(index)
}
// 返回元素在链表中的索引
indexOf(element) {
let current = this.head
for (let i = 0; i < this.count && current != null; i++) {
if (this.equalsFn(element, current.element)) {
return i
}
current = current.next
}
return -1
}
// 移除指定位置的元素
removeAt(index) {
// 检查越界值
if (index >= 0 && index < this.count) {
let previous; // 先前一项
let current = this.head // 当前一项
// 可能移除的是第一项
if (index === 0) {
this.head = current.next
} else {
// 是其中的某一项
for (let i = 0; i < index; i++) {
// 循环一次的时候 previous为head,current为第一项,current.next为第二项
previous = current // head
current = current.next // 为第二项
}
// 将先前的一项与当前项的下一项连接起来,跳过当前项,从而移除它
previous.next = current.next
}
this.count--
// 返回被删除的那个元素
return current.element
}
return undefined
}
// 使用getElementAt重构removeAt方法
newRemoveAt(index) {
// 检查越界值
if (index >= 0 && index < this.count) {
let previous; // 先前一项
let current = this.head // 当前一项
// 可能移除的是第一项
if (index === 0) {
this.head = current.next
} else {
previous = this.getElementAt(index - 1)
current = previous.next
previous.next = current.next
}
this.count--
// 返回被删除的那个元素
return current.element
}
return undefined
}
// 抽离方法 循环迭代链表直到目标位置index 返回当前元素
// 返回特定位置的元素,如果不存在则返回undefined
getElementAt(index) {
if (index >= 0 && index < this.count) {
let current = this.head
for (let i = 0; i < index && node != null; i++) {
current = current.next
}
return current
}
return undefined
}
isEmpty() {
return this.size() === 0
}
size() {
return this.count
}
toString() {
if (this.isEmpty()) {
return ''
}
let current = this.head.next
let str = this.head.element
for (let i = 1; i < this.count && current != null; i++) {
str += ',' + current.element
current = current.next
}
return str
}
getHead() {
return this.head
}
}
// 助手类Node
class Node {
constructor(element) {
this.element = element
this.next = null
}
}
function defaultEquals (a, b) {
return a === b
}
const demo = new LinkedList()
demo.push(3)
console.log(demo) // LinkedList {count: 1, head: Node, equalsFn: ƒ}count: 1equalsFn: ƒ defaultEquals(a, b)head: Node {element: 3, next: null}[[Prototype]]: Object
6.2 双向链表
双向链表与单向链表的区别在于,双向链表中的链接是双向的,一个链向下一个元素,一个链向前一个元素;
双向链表提供了两种迭代的方法: 从头到尾,从尾到头;
在单向链表中,如果迭代时错过了要找的元素,就需要回到起点,重新开始迭代;这是双向链表的一个优势;
// 双向链表
class DoublyLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals) {
super(equalsFn)
this.tail = null
}
// 向任意位置插入一个新元素的算法
insert(element, index) {
// 合法范围内
if (index >= 0 && index <= this.count) {
const node = new DoublyNode(element)
let current = this.head
// 如果在头部新增
if (index === 0) {
// 空链表
if (current == null) {
this.head = node
this.tail = node
} else {
node.next = current
current.prev = node
this.head = node
}
// 在最后一项新增
} else if (index === this.count) {
current = this.tail
current.next = node
node.prev = current
this.tail = node
// 在中间某一项新增
} else {
let previous = this.getElementAt(index - 1)
current = this.getElementAt(index)
previous.next = node
node.prev = previous
node.next = current
current.prev = node
}
this.count++
return true
}
return false
}
// 删除指定位置元素
removeAt(index) {
// 安全范围
if (index >= 0 && index < this.count) {
let current = this.head
// 删除的是头部
if (index = 0) {
this.head = current.next
// 如果只有一项
if (this.count == 1) {
this.tail = undefined
// 如果有多项
} else {
this.head.prev = undefined
}
// 删除的是尾部
} else if (index === this.count - 1) {
current = this.tail
previous = current.prev
previous.next = undefined
// 删除的是中间的元素
} else {
current = this.getElementAt(index)
let previous = current.prev
previous.next = current.next
current.next.prev = previous // 下一个元素的prev为上一个元素
}
this.count--
return current.element
}
return undefined
}
}
// 助手类
class DoublyNode extends Node {
constructor(element, next, prev) {
super(element, next)
this.prev = prev
}
}
6.3 循环链表
循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用;
循环链表与链表的唯一区别在于,最后一个元素指向下一个元素的指针(tail.next)不是引用undefined,而是指向第一个元素(this.head)
// 循环链表--单向链表
class CircularLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals) {
super(equalsFn)
}
// 在任意位置插入新元素
insert(element, index) {
// 边界条件
if (index > 0 && index <= this.count) {
const node = new Node(element)
let current = this.head
// 在头部插入
if (index === 0) {
if (current == null) {
this.head = node
node.next = this.head
} else {
node.next = current
current = this.getElementAt(this.size())
this.head = node
current.next = node
}
} else {
const previous = this.getElementAt(index)
current = previous.next
node.next = current
previous.next = node
}
this.count++
return true
}
return false
}
// 在任意位置移除元素
removeAt(index) {
if (index > 0 && index < this.count) {
let current = this.head
if (index === 0) {
if (this.size() === 1) {
this.head = undefined
} else {
current = this.getElementAt(this.size())
current.next = this.head.next
current = this.head
}
} else {
const previous = this.getElementAt(index - 1)
current = previous.next
previous.next = current.next
}
this.count--
return current.element
}
return undefined
}
}
*6.4 有序列表
有序链表是值保持元素有序的链表结构;
除了使用排序算法之外,还可以将元素插入到正确的位置来保证链表的有序性;
// 有序链表
const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1
}
function defaultCompare(a, b) {
if (a === b) {
return 0
}
// 如果a更小返回-1 a更大返回1
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, index = 0) {
if (this.isEmpty()) {
return super.insert(element, 0)
}
// 获取合适的序列点
const pos = this.getIndexNextSortedElement(element)
return super.insert(element, pos)
}
getIndexNextSortedElement(element) {
let current = this.head
let i = 0
for (; i < this.size() && current; i++) {
const comp = this.compareFn(element, current.element)
if (comp === Compare.LESS_THAN) {
return i
}
current = current.next
}
return i
}
}
6.5 使用链表创建其他数据结构 —栈
// 实现栈
class StackLinkedList {
constructor() {
// 使用双向链表
this.items = new DoublyLinkedList()
}
push(element) {
this.items.push(element)
}
pop() {
if(this.isEmpty()) {
return undefined
} else {
return this.items.removeAt(this.size() - 1)
}
}
}
之所以使用双向链表,是因为对于栈而言,我们会向尾部添加元素,也会从链表尾部移除元素;
双向链表有列表最后一个元素的引用,不需要迭代整个链表的元素就能获取它;
双向链表可以直接获取头尾的元素,减少过程消耗,它的时间复杂度和原始的Stack实现相同,为O(1);
保存一个尾部元素:
// 返回尾部元素
peek() {
if(this.items.isEmpty()) {
return undefined
} else {
return this.items.getElementAt(this.size() - 1).element
}
}
isEmpty() {
return this.items.isEmpty()
}
size() {
return this.items.size()
}
clear() {
return this.items.clear()
}
toString() {
return this.items.toString()
}
6.6 链相关的算法题
6.6.1 leetCode206 反转链表
用递归的方式反转链表
第一个newHead赋值为head了,注意Js的引用数据类型;
后续
node.next.next = node
node.next = null
会修改newHead的值
// 反转链表
var reverseList = function(node) {
// let prev = null
// let current = head
// while(current) {
// const next = current.next
// current.next = prev
// // 每次往下走
// prev = current
// current = next
// }
if (node === null || node.next === null) {
return node
}
const newHead = reverseList(node.next)
// 能够第一次执行到这里的节点为倒数第二个节点
node.next.next = node
node.next = null
return newHead
}
6.6.2 leetCode02.08环路检测
6.7 小结
- 了解了链表这种数据结构相比数组最重要的优点,那就是无须移动链表中的元素,就能轻松地添加和移除元素;因此,当你需要添加和移除很多元素的时候,最好的选择是链表,而非数组;
- 了解了如果用内部链表存储元素来创建一个栈,而不是使用数组和对象;
- 了解了复用其他数据结构中可用的操作有什么好处,而不是重写所有的逻辑代码;
7. 集合
集合是由一组无序且唯一(即不能重复)的项组成的;该数据结构使用了与有限集合相同的数学概念;
7.1 创建集合类
使用Object.prototype.hasOwnProperty.call()的目的是为了防止对象身上的hasOwnproperty方法丢失;
// 集合
class Set {
constructor() {
this.items = {}
}
has(element) {
// return element in this.items
return Object.prototype.hasOwnProperty.call(this.items, element)
}
add(element) {
if (!this.has(element)) {
this.items[element] = element
return true
}
return false
}
delete(element) {
if (this.has(element)) {
delete this.items[element]
return true
}
return false
}
clear() {
this.items = {}
}
size() {
return Object.keys(this.items).length
}
// size的优化版
sizeLegacy() {
let count = 0;
for(let key in this.items) {
// 此时key必定存在于this.items中,可以考虑把has判断去掉
if (this.has(key)) {
count++
}
}
return count
}
values() {
return Object.values(this.items)
}
// 改良兼容性后的values方法
valuesLegacy() {
let values = []
for(let key in this.items) {
// 此时key必定存在于this.items中,可以考虑把has判断去掉
if (this.has(key)) {
values.push(key)
}
}
return values
}
}
7.2 集合运算
我们可以对集合进行如下计算:
并集
:包含两个集合中所有元素的新集合;
交集
:包含两个集合中共有元素的新集合;
差集
:包含所有存在于第一个集合且不存在与第二个集合的元素的新集合;(除了给的集合的元素,剩下的元素的集合)
子集
:验证一个给定集合是否是另一集合的子集;
7.2.1 并集
union求并集的方法成为纯函数,不会修改当前的实例或者参数;
将两个集合都塞进一个新的集合中;
// 并集
union(otherSet) {
// 定义一个新的集合
const unionSet = new Set()
this.values.forEach(item => unionSet.add(item))
otherSet.values().forEach(item => unionSet.add(item))
return unionSet
}
7.2.2 交集
迭代一个集合,判断另外一个集合中是否含有迭代集合的元素,如果有则放入一个新的集合中;
此时我们迭代的集合应该是元素更少的集合,所以我们应当先判断元素更少的集合;
// 交集
intersection(otherSet) {
const intersectionSet = new Set()
const values = this.values()
for (let i = 0; i < values.length; i++) {
if (otherSet.has(values[i])) {
intersectionSet.add(values[i])
}
}
return intersectionSet
}
// 改良后的交集
nextIntersection(otherSet) {
const intersectionSet = new Set()
const values = this.values()
const otherValues = otherSet.values()
// 假设新集合的元素更少
let biggerSet = values;
let smallSet = otherValues;
// 新集合的元素比元集合元素多,交换变量
if (values.length < otherValues.length) {
smallSet = values
biggerSet = otherValues
}
// 迭代元素更少的集合即可
for (let i = 0; i < smallSet.length; i++) {
if (biggerSet.includes(smallSet[i])) {
intersectionSet.add(smallSet[i])
}
}
return intersectionSet
}
7.2.3 差集
除了给的集合的元素,剩下的元素的集合;
// 差集
difference(otherSet) {
const differenceSet = new Set()
this.values.forEach(item => {
if (!otherSet.has(item)) {
differenceSet.add(item)
}
})
return differenceSet
}
7.2.4 子集
判断该集合是否是给定集合的子集;
使用every方法处于性能考虑,当有一个返回false的时候,迭代就会停止;
// 子集
isSubsetOf(otherSet) {
// 如果当前集合的元素多余给定集合的元素,那肯定不是子集
if (this.size() > otherSet.size) {
return false
}
// 记录是否是子集的变量, 默认它是给定集合的子集
let isSubset = true
// for (let i = 0; i < this.size(); i++) {
// if(!otherSet.has(this.values()[i])) {
// isSubset = false
// }
// }
this.values().every(item => {
if (!otherSet.has(item)) {
isSubset = false
return false
}
return true
})
return isSubset
}
7.3 ECMAScript2015—Set类
原生的Set类与我们的Set有一些不同;
其中set的values方法返回Iterator,而且size是Set的一个属性;
7.3.1 模拟集合运算
模拟并集运算
const union = (setA, setB) => {
const unionAb = new Set()
setA.forEach(value => unionAb.add(value))
setB.forEach(value => unionAb.add(value))
return unionAb
}
模拟交集运算
const intersection = (setA, setB) => {
const intersectionSet = new Set()
setA.forEach(item => {
if (setB.has(item)) {
intersectionSet.add(value)
}
})
return intersectionSet
}
模拟差集运算
const difference = (setA, setB) => {
const differentSet = new Set()
setA.forEach(item => {
if (!setB.has(item)) {
differentSet.add(item)
}
})
return differentSet
}
7.3.2 使用拓展运算符
// 并集
new Set([...setA, ...setB])
// 交集
new Set([...setA].filter(item => setB.has(item)))
// 差集
new Set([...setA].filter(item => !setB.has(item)))
7.4 小结
我们事先了一个与ECMAScript2015中所定义的set类类似的set类,还介绍了集合数据结构的一些不常见的方法,比如并集,交集,差集,子集;
8. 字典和散列表
本章中我们继续学习使用字典和散列表来存储唯一值(不重复的值)的数据结构;
在集合中,我们感兴趣的是每个值的本身,并把它当作主要元素;
在字典中,我们用【键,值】对的形式来存储数据;
在散列表中也是一样,但字典的每个键只能有一个值;
8.1 字典
字典和集合很相似,集合以[值, 值]的形式存储元素,字典则以[键, 值]的形式存储元素;
字典也称作 映射,符号表或关联数组;
// 字典和散列表
class Dictionary {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn
this.table = {}
}
// 检查一个键是否存在于字典中
hasKey(key) {
return this.table[this.toStrFn(key)] != null
}
// 像字典中添加新元素
set(key, value) {
// key和value都是合法的
if(key != null && value != null) {
this.table[this.toStrFn(key)] = new ValuePair(key, value)
return true
}
return false
}
// 从字典中移除一个值
remove(key) {
if (this.hasKey(key)) {
delete this.table[this.toStrFn(key)]
return true
}
return false
}
// 从字典中检索一个值
get(key) {
// if (this.hasKey(key)) {
// return this.table[this.toStrFn(key)]
// }
// return undefined
const valuePair = this.table[this.toStrFn(key)]
return valuePair == null ? undefined : valuePair.value
}
// 以数组的形式返回字典中的所有valuePair对象
keyValues() {
// return Object.values(this.table)
const valuePairs = []
// 此时的k已经是toStrFn处理过的字符串了
for (const k in this.table) {
if (this.hasKey(k)) {
valuePairs.push(this.table[k])
}
}
return valuePairs
}
// 返回类中用于识别值得所有原始键名
keys() {
// return this.keyValues.map(valuePair => valuePair.key)
const keys = []
const valuePairs = this.keyValues()
for (let i = 0; i < valuePairs.length; i++) {
keys.push(valuePairs[i].key)
}
return key
}
// 返回所有值
values() {
return this.keyValues.map(valuePair => valuePair.value)
}
// 使用forEach迭代字典中的每个键值对
forEach(callback) {
const valuePairs = this.keyValues()
for (let i = 0; i < valuePairs.length; i++) {
const valuePair = valuePairs[i]
const result = callback(valuePair.key, valuePair.value)
// return false 就是跳出循环
if (result === false) {
break
}
}
}
size() {
return Object.keys(this.table).length
}
isEmpty() {
return this.size() === 0
}
clear() {
this.table = {}
}
toString() {
if(this.isEmpty()) {
return ''
}
const valuePairs = this.keyValues()
let str = `${valuePairs[0].toString()}`
for (let i = 1; i < valuePairs.length; i++) {
str = `${str},${valuePairs[i].toString()}`
}
return str
}
}
// 存储在字典的值中,包含key和value属性
class ValuePair {
constructor(key, value) {
this.key = key
this.value = value
}
toString() {
return `[#${this.key}: ${this.value}]`
}
}
// 默认的将key转化为字符串的函数
function defaultToString(item) {
if (item === null) {
return 'NULL'
} else if (item === undefined) {
return 'UNDEFINED'
} else if (typeof item === 'string' || item instanceof String) {
// 如果是字符串则返回本身
return `${item}`
}
return item.toString()
}
const demo = new Dictionary()
demo.set(111, 123)
demo.set(222, 123)
8.2 散列表(哈希表)
8.2.1 创建散列表
class HashTable {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn
this.table = {}
}
// 散列函数
loseloseHashCode(key) {
if (typeof key === 'number') {
return key
}
const tableKey = this.toStrFn(key)
let hash = 0
for (let i = 0; i < tableKey.length; i++) {
hash += tableKey.charCodeAt(i)
}
return hash % 37
}
// 获取hashCode值
hashCode (key) {
return this.loseloseHashCode(key)
}
//将键和值加入散列表中
put(key, value) {
if (key != null && value != null) {
const position = this.hashCode(key)
this.table[position] = new ValuePair(key, value)
return true
}
}
// 从散列表中获取一个值
get(key) {
const valuePair = this.table[this.hashCode(key)]
return valuePair == null ? undefined : valuePairs.value
}
// 从散列表中移除一个值
remove(key) {
const hashCode = this.hashCode(key)
if (this.table[hashCode]) {
delete this.table[hash]
return true
}
return false
}
}
8.2.3 散列表和散列集合
散列集合和散列表不同之处在于,不再添加键值对,而是只插入值而没有键;
8.2.4 处理散列表中的冲突
有时候,一些键会有相同的散列值;
不同的值在散列表中对应相同位置的时候,我们称其为冲突;
有相同的hashCode,有多个不同的元素;
处理冲突的方法有: 分离链路,线性探查和双散列法;
8.2.4.1 分离链路
分离链路法包括为散列表的每一个位置创建一个链表并将元素存储在里面;它是解决冲突最简单的方法,但是在HashTable实例之外还需要额外的存储空间;
// 用分离链路的方法来解决冲突
class HashTableSeparateChaining {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn
this.table = {}
}
put (key, value) {
// 确保key和value都存在
if (key != null && value != null) {
const position = this.hashCode(key)
if (this.table[position] == null) {
this.table[position] = new LinkedList()
}
this.table[position].push(new ValuePair(key, value))
return true
}
return false
}
get (key) {
const position = this.hashCode(key)
const LinkedList = this.table[position]
if (LinkedList != null && !LinkedList.isEmpty()) {
let current = LinkedList.getHead()
while(current != null) {
if (current.element.key === key) {
return current.element.value
}
current = current.next
}
}
return undefined
}
remove(key) {
const position = this.hashCode(key)
const LinkedList = this.table[position]
if (LinkedList != null && !LinkedList.isEmpty()) {
let current = LinkedList.getHead()
while(current != null) {
if (current.element.key === key) {
LinkedList.remove(current.element)
if (LinkedList.isEmpty()) {
delete this.table[position]
}
return true
}
current = current.next
}
}
return false
}
}
8.2.4.1 线性探查
将元素直接存储到表中,而不是在单独的数据结构中;
我们在table中搜索hashCode位置,如果没有,我们就将该值添加到正确的位置,如果被占据了,我们就迭代散列表,直到找到一个空闲的位置;
- 当我们从散列表中移除一个键值对的时候,仅将本章之前的数据结构所实现位置的元素移除是不够的;如果我们只是移除了元素,就可能在查找有相同hashCode的其他元素时找到一个空的位置,这会导致算法出现问题;
线性探查的技术分为两种:
第一种是软删除方法;
第二种是检验是否有必要将一个或多个元素移动到之前的位置;
// 重写put方法
put (key, value) {
// 确保key和value都存在
if (key != null && value != null) {
const position = this.hashCode(key)
if (this.table[position] == null) {
this.table[position] = new LinkedList()
} else {
// 重点,当前hashCode有元素时,继续迭代查找,直到下一个为null的位置
let index = position + 1
while(this.table[index] != null) {
index++
}
// 找到为Null的位置了
this.table[index] = new ValuePair(key, value)
}
this.table[position].push(new ValuePair(key, value))
return true
}
return false
}
// 重写get方法
get(key) {
const position = this.hasCode(key)
let current = this.table[position]
// 当前hashCode不为空
if (current != null) {
// 当前hashCode位置对应的key正好就是需要查找的key
if (current.key == key) {
return current.value
} else {
let index = position + 1
// 从当前hasCode位置开始迭代,如果当前位置为null,则返回undefined
while(this.table[index].key != key && this.table[index] != null) {
index++
}
// 如果当前位置不为null,且key值相等
if (this.table[index] != null && this.table[index].key === key) {
return this.table[index].value
}
}
}
return undefined
}
// 重写删除方法
remove(key) {
const position = this.hasCode(key)
let current = this.table[position]
// 当前hashCode不为空
if (current != null) {
// 当前hashCode位置对应的key正好就是需要查找的key
if (current.key == key) {
delete this.table[position]
this.verifyRemoveSideEffect(key, position)
return true
} else {
let index = position + 1
// 从当前hasCode位置开始迭代,如果当前位置为null,则返回undefined
while(this.table[index].key != key && this.table[index] != null) {
index++
}
// 如果当前位置不为null,且key值相等
if (this.table[index] != null && this.table[index].key === key) {
delete this.table[index]
this.verifyRemoveSideEffect(key, index)
return true
}
}
}
return false
}
// 传入key, 和删除的key的hasCode位置
verifyRemoveSideEffect(key, removePosition) {
// 当前key对应的初始hashCode位置
const hash = this.hashCode(key)
// 当前key对应的hashCode位置的下一个位置
const index = removePosition + 1
// 直到迭代的位置为空,才到安全位置
while(this.table[index] != null) {
// 迭代到的位置的key值所对应的初始hasCode值
const posHash = this.hashCode(this.table[index].key)
if (posHash <= hash || posHash <= removePosition) {
this.table[removePosition] = this.table[index]
delete this.table[index]
removePosition = index
}
index++
}
}
8.3 ES2015Map类
Map本质上就是键值对的集合;
Map和Dictionary类不同,values方法和keys方法都返回Iterator,而不是值或键构成的数组;
另外一个区别是,我们事先的size方法返回字典中存储的值的个数,而ES2015的Map类则有一个size属性;
8.4 ES2015 WeakMap类和WeakSet类
属于Set和Map的弱化方法;
-
WeakSet或WeakMap类没有entries、keys和values方法;
-
只能用对象作为键;
-
创建这两个类主要为了性能,WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用;
8.5 小结
学习了字典的相关知识,了解了如何添加、移除和获取元素以及其他一些方法;
还了解了字典和集合的不同之处;
学习了散列计算,怎么样创建一个散列表数据结构,如何添加、移除和获取元素,以及如何创建散列函数;
学习了使用不同的方法解决散列表中的冲突问题;
9. 递归
ECMAScript2015有 尾调用优化:
如果函数内的最后一个操作是调用函数,这意味着这个函数基本上已经完成了,那么它在调用最后的函数时就不需要创建一个新的帧栈,而是可以重用已有的函数帧栈,这样不仅速度快,而且节省内存;
10. 树
我们学习了第一个非顺序数据结构是散列表,接下来我们将要学习另一种非顺序数据结构——树,它对于存储需要快速查找的数据非常有用;
10.2 相关术语
根节点,内部节点,外部节点,子树;
二叉树 完全二叉树 满二叉树
10.3 二叉树和二叉搜索树
二叉树中的节点最多只能有两个子节点:一个是左侧子节点,一个是右侧子节点;
二叉搜索树是二叉树的一种,但是只循序你在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大的值;
// 二叉搜索树
// 节点
class TreeNode {
constructor(key) {
this.key = key
this.left = null
this.right = null
}
}
class BinarySearchTree {
// 这里的比较函数和有序链表的相似
constructor(compareFn = defaultCompare) {
this.compareFn = compareFn
this.root = null
}
// 向树中插入一个新键
insert(key) {
if (this.root == null) {
this.root = new TreeNode(key)
} else {
this.insertNode(this.root, key)
}
}
insertNode(node, key) {
// 新key比node.key更小的时候
if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
// 当更小的树为空的时候
if (node.left == null) {
node.left = new TreeNode(key)
} else {
// 当更小的树不为空的时候,重新再去检查一遍
this.insertNode(node.left, key)
}
} else {
if (node.right == null) {
node.right = new TreeNode(key)
} else {
this.insertNode(node.right, key)
}
}
}
}
10.4 树的遍历
访问树的每个节点并对它们进行某种操作的过程;
10.4.1 中序遍历
中序遍历是一种以上行顺序访问BST所有节点的遍历方式,也就是以从最小到最大的顺序访问所有节点;
左子树 =》根节点 =》右子树
// 中序遍历
inOrderTraverse(callback) {
this.inOrderTraverseNode(this.root, callback)
}
inOrderTraverseNode(node, callback) {
if (node != null) {
this.inOrderTraverseNode(node.left, callback)
callback(node.key)
this.inOrderTraverseNode(node.right, callback)
}
}
10.4.2 先序遍历
先序遍历是以优先于后代节点的殊勋访问每个节点的,先序遍历的一种应用是打印一个结构化的文档;
根节点 =》左子树 =》右子树
// 先序遍历
preOrderTraverse(callback) {
this.preOrderTraverseNode(this.root, callback)
}
preOrderTraverseNode(node, callback) {
if (node != null) {
callback(node.key)
this.preOrderTraverseNode(node.left, callback)
this.preOrderTraverseNode(node.right, callback)
}
}
10.4.2 后序遍历
后序遍历则是先访问节点的后代节点,再访问节点本身,后序遍历的一种应用是计算一个目录及其字母里中所有文件所占空间的大小;
左子树 =》右字数 =》根节点
// 后序遍历
postOrderTraverse(callback) {
this.postOrderTraverseNode(this.root, callback)
}
postOrderTraverseNode(node, callback) {
if (node != null) {
callback(node.key)
this.postOrderTraverseNode(node.left, callback)
this.postOrderTraverseNode(node.right, callback)
}
}
10.5 搜索树中的值
10.5.1 搜索最大值和最小值
最小值在最左侧,最大值在最右侧;
对于寻找最小值,总是沿着树的左边;而对于寻找最大值,总是沿着树的右边;
// 搜索最大值和最小值
min() {
return this.minNode(this.root)
}
minNode(node) {
let current = node
while(current != null && current.left != null) {
current = node.left
}
return current
}
max() {
return this.maxNode(this.root)
}
maxNode(node) {
let current = node
while(current != null && current.right != null) {
current = node.right
}
return current
}
10.5.2 搜索一个特定的值
找到返回true,没找到返回false
// 搜索一个特定的值
search(key) {
this.searchNode(this.root, key)
}
searchNode(node, key) {
if (node == null) {
return false
}
// 小了小了
if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
return this.searchNode(node.left, key)
} else if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
// 大了大了
return this.searchNode(node.right, ket)
} else {
// 就是他
return true
}
}
10.5.3 移除一个节点
移除一个节点首先要找到这个节点;
root被赋值为removeNode方法的返回值;
removeNode方法返回删除了指定节点后的树结构;
// 移除
remove(key) {
this.root = this.removeNode(this.root, key)
}
removeNode(node, key) {
if(node === null) {
return null
}
if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
node.left = this.removeNode(node.left, key)
return node
} else if (this.compareFn(key, node.key === Compare.BIGGER_THAN)) {
node.right = this.removeNode(node.right, key)
return node
} else {
// 第一种情况 无子节点
if (node.left == null && node.right == null) {
node = null
return node
}
// 第二种情况 只有单个的子节点
if (node.left == null) {
node = node.right
return node
} else if (node.right == null) {
node = node.left
return node
}
// 第三种情况 有两个子节点
// 需要找到右边子树中最小的节点
// 然后用最小的节点去更新这个节点的值,此时该节点已经被移除
// 把右侧最小节点移除
// 向它的父节点返回更新后的节点引用
const aux = this.minNode(node.right)
node.key = aux.key
node.right = this.removeNode(node.right, aux.key)
return node
}
}
10.6 自平衡树
BST存在一个问题:取决于你添加的节点数,树的一条边可能会非常深,也就是说,树的一条分支会有很多层,而其他的分支却只有几层;
这会引发一些性能问题,因此有一种树叫做AVL树;AVL树是一种自平衡二叉搜索树,意思是任何一个节点左右两侧子树的高度之差最多为1;