Table of Contents generated with DocToc
栈 概念 栈是一个线性结构,在计算机中是一个相当常见的数据结构。
栈的特点是只能在某一端添加或删除数据,遵循先进后出的原则
实现 每种数据结构都可以用很多种方式来实现,其实可以把栈看成是数组的一个子集,所以这里使用数组来实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Stack { constructor ( ) { this .stack = []; } push (item ) { this .stack.push(item); } pop ( ) { this .stack.pop(); } peek ( ) { return this .stack[this .getCount() - 1 ]; } getCount ( ) { return this .stack.length; } isEmpty ( ) { return this .getCount() === 0 ; } }
应用 选取了 LeetCode 上序号为 20 的题目
题意是匹配括号,可以通过栈的特性来完成这道题目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var isValid = function (s ) { let map = { '(' : -1 , ')' : 1 , '[' : -2 , ']' : 2 , '{' : -3 , '}' : 3 , }; let stack = []; for (let i = 0 ; i < s.length; i++) { if (map[s[i]] < 0 ) { stack.push(s[i]); } else { let last = stack.pop(); if (map[last] + map[s[i]] != 0 ) return false ; } } if (stack.length > 0 ) return false ; return true ; };
队列 概念 队列一个线性结构,特点是在某一端添加数据,在另一端删除数据,遵循先进先出的原则。
实现 这里会讲解两种实现队列的方式,分别是单链队列和循环队列。
单链队列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Queue { constructor ( ) { this .queue = []; } enQueue (item ) { this .queue.push(item); } deQueue ( ) { return this .queue.shift(); } getHeader ( ) { return this .queue[0 ]; } getLength ( ) { return this .queue.length; } isEmpty ( ) { return this .getLength() === 0 ; } }
因为单链队列在出队操作的时候需要 O(n) 的时间复杂度,所以引入了循环队列。循环队列的出队操作平均是 O(1) 的时间复杂度。
循环队列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 class SqQueue { constructor (length ) { this .queue = new Array (length + 1 ); this .first = 0 ; this .last = 0 ; this .size = 0 ; } enQueue (item ) { if (this .first === (this .last + 1 ) % this .queue.length) { this .resize(this .getLength() * 2 + 1 ); } this .queue[this .last] = item; this .size++; this .last = (this .last + 1 ) % this .queue.length; } deQueue ( ) { if (this .isEmpty()) { throw Error ('Queue is empty' ); } let r = this .queue[this .first]; this .queue[this .first] = null ; this .first = (this .first + 1 ) % this .queue.length; this .size--; if (this .size === this .getLength() / 4 && this .getLength() / 2 !== 0 ) { this .resize(this .getLength() / 2 ); } return r; } getHeader ( ) { if (this .isEmpty()) { throw Error ('Queue is empty' ); } return this .queue[this .first]; } getLength ( ) { return this .queue.length - 1 ; } isEmpty ( ) { return this .first === this .last; } resize (length ) { let q = new Array (length); for (let i = 0 ; i < length; i++) { q[i] = this .queue[(i + this .first) % this .queue.length]; } this .queue = q; this .first = 0 ; this .last = this .size; } }
链表 概念 链表是一个线性结构,同时也是一个天然的递归结构。链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
实现 单向链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 class Node { constructor (v, next ) { this .value = v; this .next = next; } } class LinkList { constructor ( ) { this .size = 0 ; this .dummyNode = new Node(null , null ); } find (header, index, currentIndex ) { if (index === currentIndex) return header; return this .find(header.next, index, currentIndex + 1 ); } addNode (v, index ) { this .checkIndex(index); let prev = this .find(this .dummyNode, index, 0 ); prev.next = new Node(v, prev.next); this .size++; return prev.next; } insertNode (v, index ) { return this .addNode(v, index); } addToFirst (v ) { return this .addNode(v, 0 ); } addToLast (v ) { return this .addNode(v, this .size); } removeNode (index, isLast ) { this .checkIndex(index); index = isLast ? index - 1 : index; let prev = this .find(this .dummyNode, index, 0 ); let node = prev.next; prev.next = node.next; node.next = null ; this .size--; return node; } removeFirstNode ( ) { return this .removeNode(0 ); } removeLastNode ( ) { return this .removeNode(this .size, true ); } checkIndex (index ) { if (index < 0 || index > this .size) throw Error ('Index error' ); } getNode (index ) { this .checkIndex(index); if (this .isEmpty()) return ; return this .find(this .dummyNode, index, 0 ).next; } isEmpty ( ) { return this .size === 0 ; } getSize ( ) { return this .size; } }
树 二叉树 树拥有很多种结构,二叉树是树中最常用的结构,同时也是一个天然的递归结构。
二叉树拥有一个根节点,每个节点至多拥有两个子节点,分别为:左节点和右节点。树的最底部节点称之为叶节点,当一颗树的叶数量数量为满时,该树可以称之为满二叉树。
二分搜索树 二分搜索树也是二叉树,拥有二叉树的特性。但是区别在于二分搜索树每个节点的值都比他的左子树的值大,比右子树的值小。
这种存储方式很适合于数据搜索。如下图所示,当需要查找 6 的时候,因为需要查找的值比根节点的值大,所以只需要在根节点的右子树上寻找,大大提高了搜索效率。
实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Node { constructor (value ) { this .value = value; this .left = null ; this .right = null ; } } class BST { constructor ( ) { this .root = null ; this .size = 0 ; } getSize ( ) { return this .size; } isEmpty ( ) { return this .size === 0 ; } addNode (v ) { this .root = this ._addChild(this .root, v); } _addChild (node, v ) { if (!node) { this .size++; return new Node(v); } if (node.value > v) { node.left = this ._addChild(node.left, v); } else if (node.value < v) { node.right = this ._addChild(node.right, v); } return node; } }
以上是最基本的二分搜索树实现,接下来实现树的遍历。
对于树的遍历来说,有三种遍历方法,分别是先序遍历、中序遍历、后序遍历。三种遍历的区别在于何时访问节点。在遍历树的过程中,每个节点都会遍历三次,分别是遍历到自己,遍历左子树和遍历右子树。如果需要实现先序遍历,那么只需要第一次遍历到节点时进行操作即可。
以下都是递归实现,如果你想学习非递归实现,可以 点击这里阅读
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 preTraversal ( ) { this ._pre(this .root) } _pre (node ) { if (node) { console .log(node.value) this ._pre(node.left) this ._pre(node.right) } } midTraversal ( ) { this ._mid(this .root) } _mid (node ) { if (node) { this ._mid(node.left) console .log(node.value) this ._mid(node.right) } } backTraversal ( ) { this ._back(this .root) } _back (node ) { if (node) { this ._back(node.left) this ._back(node.right) console .log(node.value) } }
以上的这几种遍历都可以称之为深度遍历,对应的还有种遍历叫做广度遍历,也就是一层层地遍历树。对于广度遍历来说,我们需要利用之前讲过的队列结构来完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 breadthTraversal ( ) { if (!this .root) return null let q = new Queue() q.enQueue(this .root) while (!q.isEmpty()) { let n = q.deQueue() console .log(n.value) if (n.left) q.enQueue(n.left) if (n.right) q.enQueue(n.right) } }
接下来先介绍如何在树中寻找最小值或最大数。因为二分搜索树的特性,所以最小值一定在根节点的最左边,最大值相反
1 2 3 4 5 6 7 8 9 10 11 12 13 14 getMin ( ) { return this ._getMin(this .root).value } _getMin (node ) { if (!node.left) return node return this ._getMin(node.left) } getMax ( ) { return this ._getMax(this .root).value } _getMax (node ) { if (!node.right) return node return this ._getMin(node.right) }
向上取整和向下取整 ,这两个操作是相反的,所以代码也是类似的,这里只介绍如何向下取整。既然是向下取整,那么根据二分搜索树的特性,值一定在根节点的左侧。只需要一直遍历左子树直到当前节点的值不再大于等于需要的值,然后判断节点是否还拥有右子树。如果有的话,继续上面的递归判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 floor (v ) { let node = this ._floor(this .root, v) return node ? node.value : null } _floor (node, v ) { if (!node) return null if (node.value === v) return v if (node.value > v) { return this ._floor(node.left, v) } let right = this ._floor(node.right, v) if (right) return right return node }
排名 ,这是用于获取给定值的排名或者排名第几的节点的值,这两个操作也是相反的,所以这个只介绍如何获取排名第几的节点的值。对于这个操作而言,我们需要略微的改造点代码,让每个节点拥有一个 size
属性。该属性表示该节点下有多少子节点(包含自身)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class Node { constructor (value ) { this .value = value this .left = null this .right = null this .size = 1 } } _getSize (node ) { return node ? node.size : 0 } _addChild (node, v ) { if (!node) { return new Node(v) } if (node.value > v) { node.size++ node.left = this ._addChild(node.left, v) } else if (node.value < v) { node.size++ node.right = this ._addChild(node.right, v) } return node } select (k ) { let node = this ._select(this .root, k) return node ? node.value : null } _select (node, k ) { if (!node) return null let size = node.left ? node.left.size : 0 if (size > k) return this ._select(node.left, k) if (size < k) return this ._select(node.right, k - size - 1 ) return node }
接下来讲解的是二分搜索树中最难实现的部分:删除节点。因为对于删除节点来说,会存在以下几种情况
需要删除的节点没有子树 需要删除的节点只有一条子树 需要删除的节点有左右两条树 对于前两种情况很好解决,但是第三种情况就有难度了,所以先来实现相对简单的操作:删除最小节点,对于删除最小节点来说,是不存在第三种情况的,删除最大节点操作是和删除最小节点相反的,所以这里也就不再赘述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 delectMin ( ) { this .root = this ._delectMin(this .root) console .log(this .root) } _delectMin (node ) { if ((node != null ) & !node.left) return node.right node.left = this ._delectMin(node.left) node.size = this ._getSize(node.left) + this ._getSize(node.right) + 1 return node }
最后讲解的就是如何删除任意节点了。对于这个操作,T.Hibbard 在 1962 年提出了解决这个难题的办法,也就是如何解决第三种情况。
当遇到这种情况时,需要取出当前节点的后继节点(也就是当前节点右子树的最小节点)来替换需要删除的节点。然后将需要删除节点的左子树赋值给后继结点,右子树删除后继结点后赋值给他。
你如果对于这个解决办法有疑问的话,可以这样考虑。因为二分搜索树的特性,父节点一定比所有左子节点大,比所有右子节点小。那么当需要删除父节点时,势必需要拿出一个比父节点大的节点来替换父节点。这个节点肯定不存在于左子树,必然存在于右子树。然后又需要保持父节点都是比右子节点小的,那么就可以取出右子树中最小的那个节点来替换父节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 delect (v ) { this .root = this ._delect(this .root, v) } _delect (node, v ) { if (!node) return null if (node.value < v) { node.right = this ._delect(node.right, v) } else if (node.value > v) { node.left = this ._delect(node.left, v) } else { if (!node.left) return node.right if (!node.right) return node.left let min = this ._getMin(node.right) min.right = this ._delectMin(node.right) min.left = node.left node = min } node.size = this ._getSize(node.left) + this ._getSize(node.right) + 1 return node }
AVL 树 概念 二分搜索树实际在业务中是受到限制的,因为并不是严格的 O(logN),在极端情况下会退化成链表,比如加入一组升序的数字就会造成这种情况。
AVL 树改进了二分搜索树,在 AVL 树中任意节点的左右子树的高度差都不大于 1,这样保证了时间复杂度是严格的 O(logN)。基于此,对 AVL 树增加或删除节点时可能需要旋转树来达到高度的平衡。
实现 因为 AVL 树是改进了二分搜索树,所以部分代码是于二分搜索树重复的,对于重复内容不作再次解析。
对于 AVL 树来说,添加节点会有四种情况
对于左左情况来说,新增加的节点位于节点 2 的左侧,这时树已经不平衡,需要旋转。因为搜索树的特性,节点比左节点大,比右节点小,所以旋转以后也要实现这个特性。
旋转之前:new < 2 < C < 3 < B < 5 < A,右旋之后节点 3 为根节点,这时候需要将节点 3 的右节点加到节点 5 的左边,最后还需要更新节点的高度。
对于右右情况来说,相反于左左情况,所以不再赘述。
对于左右情况来说,新增加的节点位于节点 4 的右侧。对于这种情况,需要通过两次旋转来达到目的。
首先对节点的左节点左旋,这时树满足左左的情况,再对节点进行一次右旋就可以达到目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 class Node { constructor (value ) { this .value = value; this .left = null ; this .right = null ; this .height = 1 ; } } class AVL { constructor ( ) { this .root = null ; } addNode (v ) { this .root = this ._addChild(this .root, v); } _addChild (node, v ) { if (!node) { return new Node(v); } if (node.value > v) { node.left = this ._addChild(node.left, v); } else if (node.value < v) { node.right = this ._addChild(node.right, v); } else { node.value = v; } node.height = 1 + Math .max(this ._getHeight(node.left), this ._getHeight(node.right)); let factor = this ._getBalanceFactor(node); if (factor > 1 && this ._getBalanceFactor(node.left) >= 0 ) { return this ._rightRotate(node); } if (factor < -1 && this ._getBalanceFactor(node.right) <= 0 ) { return this ._leftRotate(node); } if (factor > 1 && this ._getBalanceFactor(node.left) < 0 ) { node.left = this ._leftRotate(node.left); return this ._rightRotate(node); } if (factor < -1 && this ._getBalanceFactor(node.right) > 0 ) { node.right = this ._rightRotate(node.right); return this ._leftRotate(node); } return node; } _getHeight (node ) { if (!node) return 0 ; return node.height; } _getBalanceFactor (node ) { return this ._getHeight(node.left) - this ._getHeight(node.right); } _rightRotate (node ) { let newRoot = node.left; let moveNode = newRoot.right; newRoot.right = node; node.left = moveNode; node.height = 1 + Math .max(this ._getHeight(node.left), this ._getHeight(node.right)); newRoot.height = 1 + Math .max(this ._getHeight(newRoot.left), this ._getHeight(newRoot.right)); return newRoot; } _leftRotate (node ) { let newRoot = node.right; let moveNode = newRoot.left; newRoot.left = node; node.right = moveNode; node.height = 1 + Math .max(this ._getHeight(node.left), this ._getHeight(node.right)); newRoot.height = 1 + Math .max(this ._getHeight(newRoot.left), this ._getHeight(newRoot.right)); return newRoot; } }
Trie 概念 在计算机科学,trie ,又称前缀树 或字典树 ,是一种有序树,用于保存关联数组,其中的键通常是字符串。
简单点来说,这个结构的作用大多是为了方便搜索字符串,该树有以下几个特点
根节点代表空字符串,每个节点都有 N(假如搜索英文字符,就有 26 条) 条链接,每条链接代表一个字符 节点不存储字符,只有路径才存储,这点和其他的树结构不同 从根节点开始到任意一个节点,将沿途经过的字符连接起来就是该节点对应的字符串 、
实现 总得来说 Trie 的实现相比别的树结构来说简单的很多,实现就以搜索英文字符为例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 class TrieNode { constructor ( ) { this .path = 0 ; this .end = 0 ; this .next = new Array (26 ).fill(null ); } } class Trie { constructor ( ) { this .root = new TrieNode(); } insert (str ) { if (!str) return ; let node = this .root; for (let i = 0 ; i < str.length; i++) { let index = str[i].charCodeAt() - 'a' .charCodeAt(); if (!node.next[index]) { node.next[index] = new TrieNode(); } node.path += 1 ; node = node.next[index]; } node.end += 1 ; } search (str ) { if (!str) return ; let node = this .root; for (let i = 0 ; i < str.length; i++) { let index = str[i].charCodeAt() - 'a' .charCodeAt(); if (!node.next[index]) { return 0 ; } node = node.next[index]; } return node.end; } delete (str ) { if (!this .search(str)) return ; let node = this .root; for (let i = 0 ; i < str.length; i++) { let index = str[i].charCodeAt() - 'a' .charCodeAt(); if (--node.next[index].path == 0 ) { node.next[index] = null ; return ; } node = node.next[index]; } node.end -= 1 ; } }
并查集 概念 并查集是一种特殊的树结构,用于处理一些不交集的合并及查询问题。该结构中每个节点都有一个父节点,如果只有当前一个节点,那么该节点的父节点指向自己。
这个结构中有两个重要的操作,分别是:
Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。 Union:将两个子集合并成同一个集合。
实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class DisjointSet { constructor (count ) { this .parent = new Array (count); this .rank = new Array (count); for (let i = 0 ; i < count; i++) { this .parent[i] = i; this .rank[i] = 1 ; } } find (p ) { while (p != this .parent[p]) { this .parent[p] = this .parent[this .parent[p]]; p = this .parent[p]; } return p; } isConnected (p, q ) { return this .find(p) === this .find(q); } union (p, q ) { let i = this .find(p); let j = this .find(q); if (i === j) return ; if (this .rank[i] < this .rank[j]) { this .parent[i] = j; } else if (this .rank[i] > this .rank[j]) { this .parent[j] = i; } else { this .parent[i] = j; this .rank[j] += 1 ; } } }
堆 概念 堆通常是一个可以被看做一棵树的数组对象。
堆的实现通过构造二叉堆 ,实为二叉树的一种。这种数据结构具有以下性质。
任意节点小于(或大于)它的所有子节点 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层从左到右填入。 将根节点最大的堆叫做最大堆 或大根堆 ,根节点最小的堆叫做最小堆 或小根堆 。
优先队列也完全可以用堆来实现,操作是一模一样的。
实现大根堆 堆的每个节点的左边子节点索引是 i * 2 + 1
,右边是 i * 2 + 2
,父节点是 (i - 1) /2
。
堆有两个核心的操作,分别是 shiftUp
和 shiftDown
。前者用于添加元素,后者用于删除根节点。
shiftUp
的核心思路是一路将节点与父节点对比大小,如果比父节点大,就和父节点交换位置。
shiftDown
的核心思路是先将根节点和末尾交换位置,然后移除末尾元素。接下来循环判断父节点和两个子节点的大小,如果子节点大,就把最大的子节点和父节点交换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 class MaxHeap { constructor ( ) { this .heap = []; } size ( ) { return this .heap.length; } empty ( ) { return this .size() == 0 ; } add (item ) { this .heap.push(item); this ._shiftUp(this .size() - 1 ); } removeMax ( ) { this ._shiftDown(0 ); } getParentIndex (k ) { return parseInt ((k - 1 ) / 2 ); } getLeftIndex (k ) { return k * 2 + 1 ; } _shiftUp (k ) { while (this .heap[k] > this .heap[this .getParentIndex(k)]) { this ._swap(k, this .getParentIndex(k)); k = this .getParentIndex(k); } } _shiftDown (k ) { this ._swap(k, this .size() - 1 ); this .heap.splice(this .size() - 1 , 1 ); while (this .getLeftIndex(k) < this .size()) { let j = this .getLeftIndex(k); if (j + 1 < this .size() && this .heap[j + 1 ] > this .heap[j]) j++; if (this .heap[k] >= this .heap[j]) break ; this ._swap(k, j); k = j; } } _swap (left, right ) { let rightValue = this .heap[right]; this .heap[right] = this .heap[left]; this .heap[left] = rightValue; } }