书籍《学习JS数据结构与算法 第三版》[x] 书籍 [ ] 讲座 [ ] 视频
作者 [巴西]洛伊安妮·格罗纳 出版社 人民邮电出版社
ISBN 9787115510174 出版时间 2019-5
豆瓣网址 豆瓣 是否有电子版 图灵 微信读书
阅读日期 2020-12-3 更新日期
相关链接 Github仓库 备注

image.png

全书结构大致如下:

  • 第一章 js简介 略过
  • 第二章 es和ts概要 略过

剩下的是精华,量大且深。
书籍《学习Javascript数据结构与算法》 - 图2

3 数组

本章比较简单,但选择回顾,这是基础中的基础,尤其是数组的api操作,虽然没几个

  1. var arr = [1,2,3];
  2. // 后端插入 push
  3. arr.push(5)
  4. arr.push(5,6)
  5. // 后端删除 pop
  6. arr.pop()
  7. // 开头插入 插入费劲 unshift
  8. arr.unshift(0)
  9. // 开头删除
  10. arr.shift()
  11. // 任意位置添加或删除元素
  12. arr.splice(5,3) // 索引,删除个数,后续添加元素

删除一些知识。

4 栈 Stack

克隆本文最上方提供的GitHub仓库了。进入 examples ,执行 http-server 可以观看页面和控制台。

书籍《学习Javascript数据结构与算法》 - 图3

4.1 数据结构

像子弹仓,后加入的在栈顶,先加入的再栈底部。

有啥用?比如:浏览器的历史记录。

4.2 技术实现

接下来实现一个 Stack类

实现下面的方法:

  • push(elements) 添加新元素到栈顶。
  • pop 移除栈顶元素,并返回
  • peek 找到栈顶元素,只查找不变动
  • isEmpty 是否有有元素
  • clear 移除所有元素
  • size 栈内的个数

不难想象,我们可以使用js的数组来进行操作,每次操作都是 push和 pop 的操作。

  1. class Stack {
  2. constructor() {
  3. this.items = []
  4. }
  5. // 添加
  6. push(element) {
  7. this.items.push(element)
  8. }
  9. // 移除最后一个并返回
  10. pop() {
  11. return this.items.pop()
  12. }
  13. // 查看最后一个
  14. peek() {
  15. return this.items[this.items.length-1]
  16. }
  17. // 是否为空
  18. isEmpty() {
  19. return this.items.length === 0
  20. }
  21. // 元素个数
  22. size() {
  23. return this.items.length
  24. }
  25. }

看着不难,走出了第一步,循序渐进

4.3 使用栈

  1. const stack = new Stack()
  2. stack.push(1)
  3. // xx

但这种实现方式,时间复杂度是 O(n) ,如何更快?我们使用js的对象来完成。

4.4 技术实现2

  1. class Stack {
  2. constructor() {
  3. this.items = {}
  4. this.count = 0
  5. }
  6. push(element) {
  7. this.items[this.count] = element;
  8. this.count++ // 通过这种方式完成了数组的存放
  9. }
  10. size(){return this.count}
  11. isEmpty(){return this.count===0}
  12. pop() {
  13. if(this.isEmpty){
  14. return undefined
  15. }
  16. // 找到元素,删掉并返回
  17. this.count--
  18. const r = this.items[this.count]
  19. delete this.items[this.count]
  20. return r
  21. }
  22. peek() {
  23. if(this.isEmpty){
  24. return undefined
  25. }
  26. return this.items[this.count-1]
  27. }
  28. clear(){
  29. this.count=0
  30. this.items={}
  31. }
  32. }

通过维护对象和数字的方式,我们成功把时间复杂度讲到了 O(1) ,实现了指哪打哪。

要保护内部变量不被修改

方案一,下划线口头约定
方案二,我们对 item的key定义为 Symbol ,但这样依然可以通过 Object.getOwnProperty-Symbols 来进行修改
方案三, weakMap

  1. const items = new WeakMap()
  2. class Stack {
  3. constructor() {
  4. items.set(this,[]) // 实例自身为key,值是数组
  5. }
  6. push(element) {
  7. const s = items.get(this) // 后续都是获取再使用
  8. s.push(element)
  9. }
  10. pop() {
  11. const s = items.get(this)
  12. const r = s.pop()
  13. return r
  14. }
  15. }

知晓即可,可读性不强,拓展时候无法继承。

方案四,未来有一个 # private 修饰符。但依然是编译时候有用。

4.5 解决问题

从十进制到二进制。

比如10,对应的二进制如何求?就可以使用这个stack,这里我手写一个试试

  1. const stack=new Stack()
  2. const n=10
  3. while(n>0){
  4. cosnt y = n%2 // 0,1,0,1
  5. stack.push(y)
  6. const n = Math.floor(n/2) // 5,2,1,0
  7. }
  8. // 此时 stack=0101 这个时候翻转即可
  9. let str=''
  10. while(!stack.isEmpty()) {
  11. str+= stack.pop().toString() // 1 0 1 0
  12. return str
  13. }

稍作修改,就可以改为其他进制。因为不同进制的表示不同,所以我们直接定义一个数字表

  1. const num = '0123456789ABCDEFGHIJIK'

需要把余数进行映射,比如八进制中余数7 映射为7,如果是二进制,11映射为B之类的。

此外还有汉诺塔 hanoiStack
image.png
这个汉诺塔,我们从一个大的问题,不断(这就意味着递归)拆分成小的问题。

5 队列和双端队列

5.1 队列数据结构

队列就先个排队,先入先出。

从简单的技术实现开始

  1. class Queue {
  2. constructor() {
  3. this.count = 0;
  4. this.lowestCount = 0;
  5. this.items = {};
  6. }
  7. }

我们依然需要通过 count 和 items 来实现内容的存储,因为每次都会操作队首,我们需要一个lowestCount来存储第一个。

我们来补充一些功能:

  • enqueue 添加到尾部
  • dequeue 移除第一个并返回
  • peek 获取第一个
  • isEmpty
  • size

技术实现

  1. class Queue {
  2. // 添加
  3. enqueue(element) {
  4. this.items[this.count] = element;
  5. this.count++
  6. }
  7. // 移除
  8. dequeue(){
  9. if(this.isEmpty()) {
  10. return undefined;
  11. }
  12. const r = this.items[this.lowestCount];
  13. delete this.items[this.lowestCount]
  14. this.lowestCount++
  15. return r
  16. }
  17. // 查看第一个
  18. peek() {
  19. if(this.isEmpty()) {
  20. return undefined;
  21. }
  22. return this.items[this.lowestCount]
  23. }
  24. // 获取长度
  25. isEmpty() {
  26. return this.count - this.lowestCount === 0
  27. // or
  28. // this.size() === 0
  29. }
  30. size() {
  31. return this.count - this.lowestCount
  32. }
  33. // 清空
  34. clear() {
  35. this.imtes={}
  36. this.count=0;
  37. this.lowestCount = 0;
  38. }
  39. }

api的操作这里略过。

5.2 双端队列数据结构

应用场景:用户的每个操作都会记录到一个栈里,我们如果撤销会弹出一个操作。
但最大只能记录比如20条,超出的会把栈底也移除,这又是 队列的操作。

因此栈和队列的结合是双端队列。

技术实现

这块是糅合,感觉不复杂。

  1. addFront(element) {
  2. // 判断为空,直接 addBack(element)
  3. // 如果当前 lowestCount > 0,数值-1直接赋值
  4. // 如果是0需要整体移动
  5. for(let i = this.count; i>0;i--){
  6. this.items[i] = this.items[i-1]
  7. }
  8. this.count++
  9. this.lowestCount=0
  10. this.imtes[0]=element
  11. }

操作稍多,但是很容易理解。

解决问题

1. 击鼓传花。

循环队列

比如一群人围在一起,花依次传递,每次停止会退出一个,直到结束。

请实现这个函数

  1. const names = [1,2,3,4,5]
  2. const r = hotPotato(names,7)
  3. // r.elininated [] 淘汰人
  4. // r.winner 最后剩下的人

思路,每次迭代names,把第一个移动到最后,如果是7就停止。

2. 回文

比如 madam 上海自来水来自海上

方法很多,但反向排列字符串,然后做对比是最简单的。

如果和反向排列字符串?只能使用 双端队列的话,每次取首尾做对比即可。

3. 事件循环

提到事件循环自然会 想到这篇文章 https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

6 链表 LinkList

6.1 数据结构

image.png
观察上图,链表的特点是,拥有一个 value 和 next 指针,指针指向下一个。

好处是移动或者增删元素,不需要移动其他元素,但是只能从头开始查找。

和数组显然是对立的。比如火车,一列一列连起来的。

6.2 技术实现

  1. class LinkList {
  2. constructor(equalsFn = defalutEquals){
  3. this.count = 0; // 存储数量
  4. this.head = undefined; // 第一个元素引用
  5. this.equalsFn = equalsFn; // 对比是否相等 a===b
  6. }
  7. }
  8. // 结构
  9. class Node {
  10. constructor(element){
  11. this.element = element;
  12. this.next = undefined;
  13. }
  14. }

我们要准备下面的方法:

  • push(ele) 尾部添加一个新元素
  • insert(ele, position) 特定位置插入元素
  • getElementAt(index) 获取特定位置的元素
  • remove(ele) 移除元素
  • indexOf(ele) 返回索引
  • removeAt(position) 移除特定位置的一个元素
  • isEmpty() 如果链表不包含任何元素,返回true
  • size()
  • toString() 值输出value

实现 push

  1. class LinkList {
  2. // xx
  3. // 插入元素
  4. push(ele) {
  5. const node = new Node(ele)
  6. let current;
  7. if(this.head == null) { // 如果是空的
  8. this.head = node // 设置第一个
  9. } else {
  10. current = this.head // 如果不是空的,先填充current指针
  11. while(current.next != null) {
  12. current = current.next // 一直找到最后一个
  13. }
  14. current.next = node // 给最后一个的next指向当前元素
  15. }
  16. this.count++ // 数值++
  17. }
  18. }

思路比较明确。

实现 removeAt

  1. class LinkList {
  2. removeAt(index) {
  3. // 判断index合法
  4. if(index >=0 && index < this.count) {
  5. let current = this.head;
  6. if(index === 0){ // 如果移除第一个,直接修改指针
  7. this.head = current.next
  8. } else {
  9. let prev;
  10. // index之前的都捋一遍
  11. for(let i=0;i<index;i++) {
  12. prev = current;
  13. current = current.next;
  14. }
  15. // 停止时候 能拿到 prev 和 next
  16. prev.next = current.next
  17. }
  18. this.count--;
  19. return current.element
  20. }
  21. return undefined
  22. }
  23. }

总之就是找到关键节点,保留上一个节点,直接跳过current

实现 getElementAt

通过索引找到元素

  1. class LinkList {
  2. getElementAt(index) {
  3. if(边界){
  4. let node = this.head
  5. for(let i=0;i<index&&node!=null;i++) {
  6. node = node.next
  7. }
  8. return node
  9. }
  10. return undefined
  11. }
  12. }

我们封装这个方法,可以在上一个 removeAt 中使用。因此我们可以重构

  1. class LinkList {
  2. removeAt(index) {
  3. // 判断index合法
  4. if(index >=0 && index < this.count) {
  5. let current = this.head;
  6. if(index === 0){ // 如果移除第一个,直接修改指针
  7. this.head = current.next
  8. } else {
  9. const prev = this.getElementAt(index-1);
  10. current = prev.next;
  11. prev.next = current.next
  12. }
  13. this.count--;
  14. return current.element
  15. }
  16. return undefined
  17. }
  18. }

少了一些代码,看起来更容易阅读。

实现 insert

  1. insert(element,index){
  2. if(边界) {
  3. const node = new Node(element)
  4. if(index === 0){ // 在最前面添加,直接挂上去
  5. const current = this.head;
  6. node.next = current;
  7. this.head = node;
  8. } else {
  9. const prev = this.getElement(index - 1)
  10. const current = prev.next;
  11. node.next = current
  12. prev.next = node
  13. }
  14. this.count++
  15. return true
  16. }
  17. return false
  18. }

思路也相对清晰。

实现 indexOf 也相对简单

有了indexOf ,删除某个元素也简单

isEmpty size getHead 都比较简单。

6.3 双向链表

下面是双向链表:

image.png

和单项链表相比,多了 prev 是一个完整体。

  1. class DoublyNode extends Node {
  2. constructor(element, next, prev){
  3. super(element,next)
  4. this.prev = prev
  5. }
  6. }
  7. class DoublyLinkedList extends LinkedList {
  8. constructor(equalsFn){
  9. super(equalsFn)
  10. this.tail = undefined; // 最后一个引用
  11. }
  12. }

实现insert

  1. insert(element, index) {
  2. if(边界){
  3. const node = new DoublyNode(element);
  4. let currrent = this.head
  5. }
  6. }

6.2.1

6.4 循环链表

在单向链表的基础上,最后一个指向第一个,形成循环单向链表
image.png

此外也有双向循环链表。

6.5 有序链表

SortedLinkedList 元素有序,可以使用排序算法,还可以插入元素

6.6 链表实现栈

实现 StackLinkedList

7 集合

7.1 数据结构

集合是不允许值重复的顺序数据结构,并且存在 交、差、并集

7.2 技术实现

这里我们使用对象来表示集合,也因为js对象的key是唯一的,保证集合的元素唯一。

  1. class Set {
  2. constructor() {
  3. this.items = {}
  4. }
  5. }

依然要实现:

  • add(ele) 添加元素
  • delete(ele) 移除元素
  • has(ele) 元素是否在集合中
  • clear 移除集合的所有元素
  • size 返回数量
  • values 返回所有值的数组

我看了内容相对简单,后续的 js原生Set类也完美解决此类问题。

特点是

  1. var set = {}

为啥原生Set有这些原生方法,是带着使命来的。

7.3 集合运算:交并差

8 字典和散列表

内容:

  • 字典数据结构
  • 散列表数据结构
  • Map WeakMap WeakSet

在集合Set中,我们key和value相同。在字典Dict 中使用 key-value 存储结构

在 散列表中也是 key-value,实现略有不同。

8.1 字典

Dictionary

字典也叫 映射、符号表、关联数组。

我们可以使用 Map 来实现字典。

要实现下面的方法:

  • set(key,value) 添加或者覆盖
  • remove(key) 删除
  • hasKey(key) 查看是否存在某个key
  • get(key) 查找key
  • clear() 删除所有值
  • size() 字典个数
  • isEmpty() 是否为空
  • key() 获取所有的key,返回数组
  • values() 获取所有的数值,返回数组
  • keyValues() 返回所有的 key-value
  • forEarch(fn) 迭代所有,回调返回false时候终止

8.2 散列表

HashTable 也叫 HashMap 类。这是 Dict的一种是散列表实现方式。

散列算法的作用是尽可能快找到一个值,在字典中如果要找一个值,得迭代器去查找。

如果使用 散列函数,就能知道值的具体位置,通过值找到地址。

在脚手架中的应用,对数据库进行索引。

在js内部就是使用散列表来表示每个对象。

lose lose 散列函数

要实现的话:

  • put(key, value) 散列表增加一个项
  • remove(key) 移除
  • get(key) 搜索特定的值

这里跳的太快,如果数据冲突会丢失数据。要解决冲突有几种方法:

  • 分离链接
  • 线性探查
  • 双散列发

djb2 散列函数。比较推崇

8.3 Map 类

9 递归

理解递归、基本应用、js调用栈

这部分抽离到 JS函数的递归部分

10 树

散列表之后的另一个非顺序数据结构,树。

概念、二叉搜索树、遍历、AVL树

10.2