—栈—

概念

栈是一个线性结构,在计算机中是一个相当常见的数据结构。
栈的特点是只能在某一端添加或删除数据,遵循先进后出的原则

实现

每种数据结构都可以用很多种方式来实现,其实可以把栈看成是数组的一个子集,所以这里使用数组来实现

  1. class Stack {
  2. constructor() {
  3. this.stack = []
  4. }
  5. push(item) {
  6. this.stack.push(item)
  7. }
  8. pop() {
  9. this.stack.pop()
  10. }
  11. peek() {
  12. return this.stack[this.getCount() - 1]
  13. }
  14. getCount() {
  15. return this.stack.length
  16. }
  17. isEmpty() {
  18. return this.getCount() === 0
  19. }
  20. }

应用

选取了 LeetCode 上序号为 20 的题目
题意是匹配括号,可以通过栈的特性来完成这道题目

  1. "([)]"
  2. var isValid = function (s) {
  3. let map = {
  4. '(': -1,
  5. ')': 1,
  6. '[': -2,
  7. ']': 2,
  8. '{': -3,
  9. '}': 3
  10. }
  11. let stack = []
  12. for (let i = 0; i < s.length; i++) {
  13. if (map[s[i]] < 0) {
  14. stack.push(s[i])
  15. } else {
  16. let last = stack.pop()
  17. if (map[last] + map[s[i]] != 0) return false
  18. }
  19. }
  20. if (stack.length > 0) return false
  21. return true
  22. };

—队列—

概念

队列是一个线性结构,特点是在某一端添加数据,在另一端删除数据,遵循先进先出的原则。

实现

这里会讲解两种实现队列的方式,分别是单链队列和循环队列。

单链队列

  1. class Queue {
  2. constructor() {
  3. this.queue = []
  4. }
  5. enQueue(item) {
  6. this.queue.push(item)
  7. }
  8. deQueue() {
  9. return this.queue.shift()
  10. }
  11. getHeader() {
  12. return this.queue[0]
  13. }
  14. getLength() {
  15. return this.queue.length
  16. }
  17. isEmpty() {
  18. return this.getLength() === 0
  19. }
  20. }

因为单链队列在出队操作的时候需要 O(n) 的时间复杂度,所以引入了循环队列。循环队列的出队操作平均是 O(1) 的时间复杂度。

优先队列

  1. function PriorityQueue() {
  2. var items = [];
  3. function QueueElement(element, priority) { // {1}
  4. this.element = element;
  5. this.priority = priority;
  6. }
  7. this.enqueue = function (element, priority) {
  8. var queueElement = new QueueElement(element, priority);
  9. if (this.isEmpty()) {
  10. items.push(queueElement); // {2}
  11. } else {
  12. var added = false;
  13. for (var i = 0; i < items.length; i++) {
  14. if (queueElement.priority <
  15. items[i].priority) {
  16. items.splice(i, 0, queueElement); // {3}
  17. added = true;
  18. break; // {4}
  19. }
  20. }
  21. if (!added) { //{5}
  22. items.push(queueElement);
  23. }
  24. }
  25. };
  26. //其他方法和默认的Queue实现相同
  27. }

默认的Queue类和PriorityQueue类实现上的区别是,要向PriorityQueue添加元素,需
要创建一个特殊的元素(行{1})。这个元素包含了要添加到队列的元素(它可以是任意类型)
及其在队列中的优先级。
如果队列为空,可以直接将元素入列(行{2})。否则,就需要比较该元素与其他元素的优
先级。当找到一个比要添加的元素的priority值更大(优先级更低)的项时,就把新元素插入
到它之前(根据这个逻辑,对于其他优先级相同,但是先添加到队列的元素,我们同样遵循先进
先出的原则)。要做到这一点,我们可以用第2章学习过的JavaScript的array类的splice方法。
一旦找到priority值更大的元素,就插入新元素(行{3})并终止队列循环(行{4})。这样,
队列也就根据优先级排序了。
如果要添加元素的priority值大于任何已有的元素,把它添加到队列的末尾就行了(行
{5}):
var priorityQueue = new PriorityQueue();
priorityQueue.enqueue(“John”, 2);
priorityQueue.enqueue(“Jack”, 1);
priorityQueue.enqueue(“Camila”, 1);
priorityQueue.print();
以上代码是一个使用PriorityQueue类的示例。在下图中可以看到每条命令的结果(以上
代码的结果):
image.png

?循环队列(622. Design Circular Queue,自己题解中没有设置扩容)

  1. class SqQueue {
  2. constructor(length) {
  3. this.queue = new Array(length + 1)
  4. // 队头
  5. this.first = 0
  6. // 队尾
  7. this.last = 0
  8. // 当前队列大小
  9. this.size = 0
  10. }
  11. enQueue(item) {
  12. // 判断队尾 + 1 是否为队头
  13. // 如果是就代表需要扩容数组
  14. // % this.queue.length 是为了防止数组越界
  15. if (this.first === (this.last + 1) % this.queue.length) {
  16. this.resize(this.getLength() * 2 + 1)
  17. }
  18. this.queue[this.last] = item
  19. this.size++
  20. this.last = (this.last + 1) % this.queue.length
  21. }
  22. deQueue() {
  23. if (this.isEmpty()) {
  24. throw Error('Queue is empty')
  25. }
  26. let r = this.queue[this.first]
  27. this.queue[this.first] = null
  28. this.first = (this.first + 1) % this.queue.length
  29. this.size--
  30. // 判断当前队列大小是否过小
  31. // 为了保证不浪费空间,在队列空间等于总长度四分之一时
  32. // 且不为 2 时缩小总长度为当前的一半
  33. if (this.size === this.getLength() / 4 && this.getLength() / 2 !== 0) {
  34. this.resize(this.getLength() / 2)
  35. }
  36. return r
  37. }
  38. getHeader() {
  39. if (this.isEmpty()) {
  40. throw Error('Queue is empty')
  41. }
  42. return this.queue[this.first]
  43. }
  44. getLength() {
  45. return this.queue.length - 1
  46. }
  47. isEmpty() {
  48. return this.first === this.last
  49. }
  50. resize(length) {
  51. let q = new Array(length)
  52. for (let i = 0; i < length; i++) {
  53. q[i] = this.queue[(i + this.first) % this.queue.length]
  54. }
  55. this.queue = q
  56. this.first = 0
  57. this.last = this.size
  58. }
  59. }

循环队列——击鼓传花

还有另一个修改版的队列实现,就是循环队列。循环队列的一个例子就是击鼓传花游戏( Hot
Potato)。在这个游戏中,孩子们围成一个圆圈,把花尽快地传递给旁边的人。某一时刻传花停止,
这个时候花在谁手里,谁就退出圆圈结束游戏。重复这个过程,直到只剩一个孩子(胜者)。
在下面这个示例中,我们要实现一个模拟的击鼓传花游戏:

  1. function hotPotato(nameList, num) {
  2. var queue = new Queue(); // {1}
  3. for (var i = 0; i < nameList.length; i++) {
  4. queue.enqueue(nameList[i]); // {2}
  5. }
  6. var eliminated = '';
  7. while (queue.size() > 1) {
  8. for (var i = 0; i < num; i++) {
  9. queue.enqueue(queue.dequeue()); // {3}
  10. }
  11. eliminated = queue.dequeue();// {4}
  12. console.log(eliminated + '在击鼓传花游戏中被淘汰。 ');
  13. }
  14. return queue.dequeue();// {5}
  15. }
  16. var names = ['John', 'Jack', 'Camila', 'Ingrid', 'Carl'];
  17. var winner = hotPotato(names, 7);
  18. console.log('胜利者: ' + winner);


—链表—

概念

链表是一个线性结构,同时也是一个天然的递归结构。链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

实现

单向链表

  1. class ListNode {
  2. constructor(key) {
  3. this.next = null;
  4. this.key = key;
  5. }
  6. }
  7. class List {
  8. constructor() {
  9. this.head = null;
  10. this.length = 0;
  11. }
  12. static createNode(key) {
  13. return new ListNode(key);
  14. }
  15. // 往头部插入数据
  16. insert(node) {
  17. // 如果head后面有指向的节点
  18. if (this.head) {
  19. node.next = this.head;
  20. } else {
  21. node.next = null;
  22. }
  23. this.head = node;
  24. this.length++;
  25. }
  26. find(key) {
  27. let node = this.head;
  28. while (node !== null && node.key !== key) {
  29. node = node.next;
  30. }
  31. return node;
  32. }
  33. delete(node) {
  34. if (this.length === 0) {
  35. throw 'node is undefined';
  36. }
  37. if (node === this.head) {
  38. this.head = node.next;
  39. this.length--;
  40. return;
  41. }
  42. let prevNode = this.head;
  43. while (prevNode.next !== node) {
  44. prevNode = prevNode.next;
  45. }
  46. if (node.next === null) {
  47. prevNode.next = null;
  48. }
  49. if (node.next) {
  50. prevNode.next = node.next;
  51. }
  52. this.length--;
  53. }
  54. }

双向链表

  1. //封装双向链表
  2. function DoublyLinkedList() {
  3. //内部类
  4. function Node(data) {
  5. this.data = data
  6. this.prev = null
  7. this.next = null
  8. }
  9. //属性
  10. this.head = null
  11. this.tail = null
  12. this.length = 0
  13. //1.append方法
  14. DoublyLinkedList.prototype.append = function (data) {
  15. //1.创建新的节点
  16. var newNode = new Node(data)
  17. //2.判断是否为第一个节点
  18. if (this.length == 0) {
  19. this.head = newNode
  20. this.tail = newNode
  21. } else {
  22. newNode.prev = this.tail
  23. this.tail.next = newNode
  24. this.tail = newNode
  25. }
  26. //3.length+1
  27. this.length += 1
  28. }
  29. //2.将链表转变为字符串形式
  30. //2.1 toString方法
  31. DoublyLinkedList.prototype.toString = function () {
  32. return this.backwardString()
  33. }
  34. //2.2 forwardString方法
  35. DoublyLinkedList.prototype.forwardString = function () {
  36. //1.定义变量
  37. var current = this.tail
  38. var resultString = ''
  39. //2.依次向后遍历,获取每一个节点
  40. while (current) {
  41. resultString += current.data + ''
  42. current = current.prev
  43. }
  44. return resultString
  45. }
  46. //2.3 backwardString方法
  47. DoublyLinkedList.prototype.backwardString = function () {
  48. //1.定义变量
  49. var current = this.head
  50. var resultString = ''
  51. //2.依次向后遍历,获取每一个节点
  52. while (current) {
  53. resultString += current.data + ' '
  54. current = current.next
  55. }
  56. return resultString
  57. }
  58. //3.insert方法
  59. DoublyLinkedList.prototype.insert = function (position, data) {
  60. //1.越界判断
  61. if (position < 0 || position > this.length) return false
  62. //2.创建新的节点
  63. var newNode = new Node(data)
  64. //3.判断原来列表是否为空
  65. if (this.length == 0) {
  66. this.head = newNode
  67. this.tail = newNode
  68. } else {
  69. if (position == 0) {//3.1 判断插入位置是否为0
  70. this.head.prev = newNode //原来的第一个节点.prev = newNode
  71. newNode.next = this.head// newNode.next= 原来的第一个节点
  72. this.head = newNode
  73. } else if (position == this.length) {//3.2判断插入位置是否为最后一位
  74. newNode.prev = this.tail
  75. this.tail.next = newNode
  76. this.tail = newNode
  77. } else {
  78. var current = this.head
  79. var index = 0
  80. while (index++ < position) {
  81. current = current.next//current为要插入点的后一个节点
  82. }
  83. newNode.next = current
  84. newNode.prev = current.prev
  85. current.prev.next = newNode
  86. current.prev = newNode
  87. }
  88. }
  89. //4.length+1
  90. this.length += 1
  91. console.log(this.length)
  92. return true
  93. }
  94. //4.get方法
  95. DoublyLinkedList.prototype.get = function (position) {
  96. //1.越界判断
  97. if (position < 0 || position >= this.length) return false
  98. //this.length/2 > position :从头向后遍历
  99. //this.length/2 < position :从后往前遍历 index-->position
  100. //2.获取元素
  101. var current = this.head
  102. var index = 0
  103. while (index++ < position) {
  104. current = current.next
  105. }
  106. console.log(current.data)
  107. return current.data
  108. }
  109. //5.indexOf方法
  110. DoublyLinkedList.prototype.indexOf = function (data) {
  111. //1.定义变量
  112. var current = this.head
  113. var index = 0
  114. //2.找到与data相同的节点
  115. while (current) {
  116. if (current.data == data) {
  117. return index
  118. }
  119. current = current.next
  120. index += 1
  121. }
  122. console.log(index)
  123. return -1
  124. }
  125. //6.update方法
  126. DoublyLinkedList.prototype.update = function (position, newData) {
  127. //1.越界判断
  128. if (position < 0 || position >= this.length) return false
  129. //2.查找节点
  130. var current = this.head
  131. var index = 0
  132. while (index++ < position) {
  133. current = current.next
  134. }
  135. current.data = newData
  136. return true
  137. }
  138. //7.removeAt方法
  139. DoublyLinkedList.prototype.removeAt = function (position) {
  140. //1.越界判断
  141. if (position < 0 || position >= this.length) return null
  142. //2.判断是否只有一个节点
  143. var current = this.head
  144. if (this.length == 1) {
  145. this.head = null
  146. this.tail = null
  147. } else {
  148. if (position == 0) {//判断是否是删除第一个节点
  149. this.head.next.prev = null
  150. this.head = this.head.next
  151. } else if (position == this.length - 1) {//是最后一个节点
  152. current = this.tail
  153. this.tail.prev.next = null
  154. this.tail = this.tail.prev
  155. } else {
  156. var index = 0
  157. while (index++ < position) {
  158. current = current.next
  159. }
  160. current.prev.next = current.next
  161. current.next.prev = current.prev
  162. }
  163. }
  164. //3.length-1
  165. this.length -= 1
  166. return current.data
  167. }
  168. //8.remove方法
  169. DoublyLinkedList.prototype.remove = function (data) {
  170. var index = this.indexOf(data)
  171. return this.removeAt(index)
  172. }
  173. DoublyLinkedList.prototype.isEmpty = function (data) {
  174. return this.length == 0
  175. }
  176. DoublyLinkedList.prototype.size = function (data) {
  177. return this.length
  178. }
  179. DoublyLinkedList.prototype.getHead = function (data) {
  180. return this.head.data
  181. }
  182. DoublyLinkedList.prototype.getTail = function (data) {
  183. return this.tail.data
  184. }
  185. }
  186. var doubly = new DoublyLinkedList()
  187. doubly.append('abc')
  188. doubly.append('def')
  189. doubly.append('hjk')
  190. console.log(doubly)
  191. doubly.insert(0, '123')
  192. doubly.insert(2, '456')
  193. doubly.insert(5, '789')
  194. console.log(doubly)
  195. console.log(doubly.get(5))
  196. console.log(doubly.indexOf('789'))
  197. doubly.update(5, '012')
  198. console.log(doubly)
  199. console.log(doubly.removeAt(0))
  200. console.log(doubly.removeAt(1))
  201. console.log(doubly)
  202. console.log(doubly.removeAt(3))
  203. console.log(doubly)

集合

我们要实现的类就是以ECMAScript 6中Set类的实现为基础的

  1. function Set() {
  2. var items = {};
  3. this.has = function (value) {
  4. return value in items;
  5. };
  6. this.add = function (value) {
  7. if (!this.has(value)) {
  8. items[value] = value; //{1}
  9. return true;
  10. }
  11. return false;
  12. };
  13. this.remove = function (value) {
  14. if (this.has(value)) {
  15. delete items[value]; //{2}
  16. return true;
  17. }
  18. return false;
  19. };
  20. this.clear = function () {
  21. items = {}; // {3}
  22. };
  23. this.size = function () {
  24. return Object.keys(items).length; //{4}
  25. };
  26. this.sizeLegacy = function () {
  27. var count = 0;
  28. for (var prop in items) { //{5}
  29. if (items.hasOwnProperty(prop)) //{6}
  30. ++count; //{7}
  31. }
  32. return count;
  33. };
  34. this.values = function () {
  35. return Object.keys(items);
  36. };
  37. //并集
  38. this.union = function (otherSet) {
  39. var unionSet = new Set(); //{1}
  40. var values = this.values(); //{2}
  41. for (var i = 0; i < values.length; i++) {
  42. unionSet.add(values[i]);
  43. }
  44. values = otherSet.values(); //{3}
  45. for (var i = 0; i < values.length; i++) {
  46. unionSet.add(values[i]);
  47. }
  48. return unionSet;
  49. };
  50. //交集
  51. this.intersection = function (otherSet) {
  52. var intersectionSet = new Set(); //{1}
  53. var values = this.values();
  54. for (var i = 0; i < values.length; i++) { //{2}
  55. if (otherSet.has(values[i])) { //{3}
  56. intersectionSet.add(values[i]); //{4}
  57. }
  58. }
  59. return intersectionSet;
  60. }
  61. //差集
  62. this.difference = function (otherSet) {
  63. var differenceSet = new Set(); //{1}
  64. var values = this.values();
  65. for (var i = 0; i < values.length; i++) { //{2}
  66. if (!otherSet.has(values[i])) { //{3}
  67. differenceSet.add(values[i]); //{4}
  68. }
  69. }
  70. return differenceSet;
  71. };
  72. //子集
  73. this.subset = function (otherSet) {
  74. if (this.size() > otherSet.size()) { //{1}
  75. return false;
  76. } else {
  77. var values = this.values();
  78. for (var i = 0; i < values.length; i++) { //{2}
  79. if (!otherSet.has(values[i])) { //{3}
  80. return false; //{4}
  81. }
  82. }
  83. return true; //{5}
  84. }
  85. };
  86. }

字典

集合以[值,值]的形式存储元素,字 典则是以[键,值]的形式来存储元素。字典也称作映射。

  1. //字典数据结构
  2. function Dictionary(){
  3. this.items = {};
  4. //检查是否有某一个键
  5. this.has = function(key){
  6. return this.items.hasOwnProperty(key);
  7. }
  8. //为字典添加某一个值
  9. this.set = function(key,val){
  10. this.items[key] = val;
  11. }
  12. //删除某一个键
  13. this.delete = function(key){
  14. if(this.has(key)){
  15. delete this.items[key];
  16. return true;
  17. }
  18. return false;
  19. }
  20. //查找某一特定项
  21. this.get = function(key){
  22. return this.has(key) ? this.items[key] : undefined;
  23. }
  24. //返回字典中的所有值
  25. this.values = function(){
  26. var res = [];
  27. for(var prop in this.items){
  28. if(this.has(prop)){
  29. res.push(this.items[prop]);
  30. }
  31. }
  32. return res;
  33. }
  34. //清空字典
  35. this.clear = function(){
  36. this.items = {};
  37. }
  38. //获取字典的长度
  39. this.size = function(){
  40. return Object.keys(this.items).length;
  41. }
  42. //获取字典所有的键
  43. this.keys = function(){
  44. return Object.keys(this.items);
  45. }
  46. //返回字典本身
  47. this.getItems = function(){
  48. return this.items;
  49. }
  50. }

ES6中的Set和Map

Set

上面代码通过add()方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。
Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
image.png
向 Set 加入值的时候,不会发生类型转换,所以5"5"是两个不同的值。

WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
首先,WeakSet 的成员只能是对象,而不能是其他类型的值。
其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

Map

它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
image.png image.png
—两种添加元素的方式
事实上,不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作Map构造函数的参数。这就是说,SetMap都可以用来生成新的 Map。

只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。

  1. const map = new Map();
  2. map.set(['a'], 555);
  3. map.get(['a']) // undefined

如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等(===),Map 将其视为一个键,比如0-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefinednull也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

WeakMap

WeakMapMap的区别有两点。
首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。

其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

  1. const e1 = document.getElementById('foo');
  2. const e2 = document.getElementById('bar');
  3. const arr = [
  4. [e1, 'foo 元素'],
  5. [e2, 'bar 元素'],
  6. ];

上面代码中,e1e2是两个对象,我们通过arr数组对这两个对象添加一些文字说明。这就形成了arre1e2的引用。
一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放e1e2占用的内存。

  1. // 不需要 e1 和 e2 的时候
  2. // 必须手动删除引用
  3. arr [0] = null;
  4. arr [1] = null;

上面这样的写法显然很不方便。一旦忘了写,就会造成内存泄露。
WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

散列表HashTable

给定一个key参数,我们就能根据组成key的每个字符的ASCII码值的和得到一个数字。所以, 首先需要一个变量来存储这个总和(行{1})。然后,遍历key(行{2})并将从ASCII表中查到 的每个字符对应的ASCII值加到hash变量中(可以使用JavaScript的String类中的charCodeAt 方法——行{3})。后,返回hash值。为了得到比较小的数值,我们会使用hash值和一个任意 数做除法的余数(mod)。

  1. function HashTable() {
  2. var table = [];
  3. }
  4. var loseloseHashCode = function (key) { var hash = 0; //{1} for (var i = 0; i < key.length; i++) { //{2} hash += key.charCodeAt(i); //{3} } return hash % 37; //{4} };

Jonathan、Jamie和Sue有相同的散列值,也就是5。由于Sue是后一个被添加的,Sue 将是在HashTable实例中占据位置5的元素。首先,Jonathan会占据这个位置,然后Jamie会覆 盖它,然后Sue会再次覆盖。这对于其他发生冲突的元素来说也是一样的。
使用一个数据结构来保存数据的目的显然不是去丢失这些数据,而是通过某种方法将它们全部保存起来。因此,当这种情况发生的时候就要去解决它。处理冲突有几种方法:分离链接、线 性探查和双散列法。在本书中,我们会介绍前两种方法

分离链接

为了实现一个使用了分离链接的HashTable实例,我们需要一个新的辅助类来表示将要加入 LinkedList实例的元素。我们管它叫ValuePair类(在HashTable类内部定义):
var ValuePair = function(key, value){ this.key = key; this.value = value;

this.toString = function() { return ‘[‘ + this.key + ‘ - ‘ + this.value + ‘]’; } };
这个类只会将key和value存储在一个Object实例中。我们也重写了toString方法,以便 之后在浏览器控制台中输出结果。

(1) put方法
我们来实现第一个方法,put方法,代码如下:
this.put = function(key, value){ var position = loseloseHashCode(key);

if (table[position] == undefined) { //{1} table[position] = new LinkedList(); } table[position].append(new ValuePair(key, value)); //{2} }; 在这个方法中,将验证要加入新元素的位置是否已经被占据(行{1})。如果这个位置是第 一次被加入元素,我们会在这个位置上初始化一个LinkedList类的实例(你已经在第5章中学 习过)。然后,使用第5章中实现的append方法向LinkedList实例中添加一个ValuePair实例 (键和值)(行{2})。
(2) get方法
然后,我们实现用来获取特定值的get方法:
this.get = function(key) { var position = loseloseHashCode(key);

if (table[position] !== undefined){ //{3}

//遍历链表来寻找键/值 var current = table[position].getHead(); //{4}

while(current.next){ //{5} if (current.element.key === key){ //{6} return current.element.value; //{7} } current = current.next; //{8} }

//检查元素在链表第一个或最后一个节点的情况 if (current.element.key === key){ //{9} return current.element.value; } } return undefined; //{10} }; 我们要做的第一个验证,是确定在特定的位置上是否有元素存在(行{3})。如果没有,则 返回一个undefined表示在HashTable实例中没有找到这个值(行{10})。如果在这个位置上有 值存在,我们知道这是一个LinkedList实例。现在要做的是遍历这个链表来寻找我们需要的元 素。在遍历之前先要获取链表表头的引用(行{4}),然后就可以从链表的头部遍历到尾部(行 {5},current.next将会是null)

(3) remove方法
使用分离链接法从HashTable实例中移除一个元素和之前在本章实现的remove方法有一些 不同。现在使用的是链表,我们需要从链表中移除一个元素。来看看remove方法的实现:
this.remove = function(key){ var position = loseloseHashCode(key);

if (table[position] !== undefined){

var current = table[position].getHead(); while(current.next){ if (current.element.key === key){ //{11} table[position].remove(current.element); //{12} if (table[position].isEmpty()){ //{13} table[position] = undefined; //{14} } return true; //{15} } current = current.next; }

// 检查是否为第一个或最后一个元素 if (current.element.key === key){ //{16} table[position].remove(current.element); if (table[position].isEmpty()){ table[position] = undefined; } return true; } }

return false; //{17} }; 在remove方法中,我们使用和get方法一样的步骤找到要找的元素。遍历LinkedList实例 时,如果链表中的current元素就是要找的元素(行{11}),使用remove方法将其从链表中移 除。然后进行一步额外的验证:如果链表为空了(行{13}——链表中不再有任何元素了),就将 散列表这个位置的值设为undefined(行{14}),

线性探查

另一种解决冲突的方法是线性探查。当想向表中某个位置加入一个新元素的时候,如果索引 为index的位置已经被占据了,就尝试index+1的位置。如果index+1的位置也被占据了,就尝试 index+2的位置,以此类推。
(1) put方法
让我们继续实现需要重写的三个方法。第一个是put方法:
this.put = function(key, value){ var position = loseloseHashCode(key); // {1}

if (table[position] == undefined) { // {2} table[position] = new ValuePair(key, value); // {3} } else { var index = ++position; // {4} while (table[index] != undefined){ // {5} index++; // {6} } table[index] = new ValuePair(key, value); // {7} } };
和之前一样,先获得由散列函数生成的位置(行{1}),然后验证这个位置是否有元素存在 (如果这个位置被占据了,将会通过行{2}的验证)。如果没有元素存在,就在这个位置加入新元 素(行{3}——一个ValuePair的实例)。

如果这个位置已经被占据了,需要找到下一个没有被占据的位置(position的值是 undefined),因此我们声明一个index变量并赋值为position+1(行{4}——在变量名前使用 自增运算符++会先递增变量值然后再将其赋值给index)。然后验证这个位置是否被占据(行 {5}),如果被占据了,继续将index递增(行{6}),直到找到一个没有被占据的位置。然后要 做的,就是将值分配到这个位置(行{7})。
(2) get方法 现在插入了所有的元素,让我们实现get方法来获取它们的值吧:
this.get = function(key) { var position = loseloseHashCode(key);

if (table[position] !== undefined){ //{8} if (table[position].key === key) { //{9} return table[position].value; //{10} } else { var index = ++position; while (table[index] === undefined || table[index].key !== key){ //{11} index++; } if (table[index].key === key) { //{12} return table[index].value; //{13} } } } return undefined; //{14} }; 要获得一个键对应的值,先要确定这个键存在(行{8})。如果这个键不存在,说明要查找 的值不在散列表中,因此可以返回undefined(行{14})。如果这个键存在,需要检查我们要找 的值是否就是这个位置上的值(行{9})。如果是,就返回这个值(行{10})。
如果不是,就在散列表中的下一个位置继续查找,直到找到一个键值与我们要找的键值相同 的元素(行{11})。然后,验证一下当前项就是我们要找的项(行{12}——只是为了确认一下) 并且将它的值返回(行{13})。
我们无法确定要找的元素实际上在哪个位置,这就是使用ValuePair来表示HashTable元 素的原因。 (3) remove方法
remove方法和get方法基本相同,不同之处在于行{10}和{13},它们将会由下面的代码代替:

table[index] = undefined; 要移除一个元素,只需要给其赋值为undefined,来表示这个位置不再被占据并且可以在必 要时接受一个新元素。

—树—

二叉树

树拥有很多种结构,二叉树是树中最常用的结构,同时也是一个天然的递归结构。
二叉树拥有一个根节点,每个节点至多拥有两个子节点,分别为:左节点和右节点。树的最底部节点称之为叶节点,当一颗树的叶数量数量为满时,该树可以称之为满二叉树。

二分搜索树(BST)

二分搜索树也是二叉树,拥有二叉树的特性。但是区别在于二分搜索树每个节点的值都比他的左子树的值大,比右子树的值小。
这种存储方式很适合于数据搜索。如下图所示,当需要查找 6 的时候,因为需要查找的值比根节点的值大,所以只需要在根节点的右子树上寻找,大大提高了搜索效率。

实现

  1. class Node {
  2. constructor(value) {
  3. this.value = value
  4. this.left = null
  5. this.right = null
  6. }
  7. }
  8. class BST {
  9. constructor() {
  10. this.root = null
  11. this.size = 0
  12. }
  13. getSize() {
  14. return this.size
  15. }
  16. isEmpty() {
  17. return this.size === 0
  18. }
  19. addNode(v) {
  20. this.root = this._addChild(this.root, v)
  21. }
  22. // 添加节点时,需要比较添加的节点值和当前
  23. // 节点值的大小
  24. _addChild(node, v) {
  25. if (!node) {
  26. this.size++
  27. return new Node(v)
  28. }
  29. if (node.value > v) {
  30. node.left = this._addChild(node.left, v)
  31. } else if (node.value < v) {
  32. node.right = this._addChild(node.right, v)
  33. }
  34. return node
  35. }
  36. }

以上是最基本的二分搜索树实现。

树的遍历

对于树的遍历来说,有三种遍历方法,分别是先序遍历、中序遍历、后序遍历。三种遍历的区别在于何时访问节点。在遍历树的过程中,每个节点都会遍历三次,分别是遍历到自己,遍历左子树和遍历右子树。如果需要实现先序遍历,那么只需要第一次遍历到节点时进行操作即可。

  1. // 先序遍历可用于打印树的结构
  2. // 先序遍历先访问根节点,然后访问左节点,最后访问右节点。
  3. preTraversal() {
  4. this._pre(this.root)
  5. }
  6. _pre(node) {
  7. if (node) {
  8. console.log(node.value)
  9. this._pre(node.left)
  10. this._pre(node.right)
  11. }
  12. }
  13. // 中序遍历可用于排序
  14. // 对于 BST 来说,中序遍历可以实现一次遍历就
  15. // 得到有序的值
  16. // 中序遍历表示先访问左节点,然后访问根节点,最后访问右节点。
  17. midTraversal() {
  18. this._mid(this.root)
  19. }
  20. _mid(node) {
  21. if (node) {
  22. this._mid(node.left)
  23. console.log(node.value)
  24. this._mid(node.right)
  25. }
  26. }
  27. // 后序遍历可用于先操作子节点
  28. // 再操作父节点的场景
  29. // 后序遍历表示先访问左节点,然后访问右节点,最后访问根节点。
  30. backTraversal() {
  31. this._back(this.root)
  32. }
  33. _back(node) {
  34. if (node) {
  35. this._back(node.left)
  36. this._back(node.right)
  37. console.log(node.value)
  38. }
  39. }

以上的这几种遍历都可以称之为深度遍历,对应的还有种遍历叫做广度遍历,也就是一层层地遍历树。

对于广度遍历来说,我们需要利用之前讲过的队列结构来完成。

  1. breadthTraversal() {
  2. if (!this.root) return null
  3. let q = new Queue()
  4. // 将根节点入队
  5. q.enQueue(this.root)
  6. // 循环判断队列是否为空,为空
  7. // 代表树遍历完毕
  8. while (!q.isEmpty()) {
  9. // 将队首出队,判断是否有左右子树
  10. // 有的话,就先左后右入队
  11. let n = q.deQueue()
  12. console.log(n.value)
  13. if (n.left) q.enQueue(n.left)
  14. if (n.right) q.enQueue(n.right)
  15. }
  16. }

接下来先介绍如何在树中寻找最小值或最大数。因为二分搜索树的特性,所以最小值一定在根节点的最左边,最大值相反

  1. getMin() {
  2. return this._getMin(this.root).value
  3. }
  4. _getMin(node) {
  5. if (!node.left) return node
  6. return this._getMin(node.left)
  7. }
  8. getMax() {
  9. return this._getMax(this.root).value
  10. }
  11. _getMax(node) {
  12. if (!node.right) return node
  13. return this._getMin(node.right)
  14. }

向上取整和向下取整
**排名
**删除节点

—AVL 树—

概念

二分搜索树实际在业务中是受到限制的,因为并不是严格的 O(logN),在极端情况下会退化成链表,比如加入一组升序的数字就会造成这种情况。
AVL 树改进了二分搜索树,在 AVL 树中任意节点的左右子树的高度差都不大于 1,这样保证了时间复杂度是严格的 O(logN)。基于此,对 AVL 树增加或删除节点时可能需要旋转树来达到高度的平衡。

实现

因为 AVL 树是改进了二分搜索树,所以部分代码是于二分搜索树重复的,对于重复内容不作再次解析。
对于 AVL 树来说,添加节点会有四种情况
数据结构 - 图5
对于左左情况来说,新增加的节点位于节点 2 的左侧,这时树已经不平衡,需要旋转。因为搜索树的特性,节点比左节点大,比右节点小,所以旋转以后也要实现这个特性。
旋转之前:new < 2 < C < 3 < B < 5 < A,右旋之后节点 3 为根节点,这时候需要将节点 3 的右节点加到节点 5 的左边,最后还需要更新节点的高度。
对于右右情况来说,相反于左左情况,所以不再赘述。
对于左右情况来说,新增加的节点位于节点 4 的右侧。对于这种情况,需要通过两次旋转来达到目的。
首先对节点的左节点左旋,这时树满足左左的情况,再对节点进行一次右旋就可以达到目的。

  1. class Node {
  2. constructor(value) {
  3. this.value = value
  4. this.left = null
  5. this.right = null
  6. this.height = 1
  7. }
  8. }
  9. class AVL {
  10. constructor() {
  11. this.root = null
  12. }
  13. addNode(v) {
  14. this.root = this._addChild(this.root, v)
  15. }
  16. _addChild(node, v) {
  17. if (!node) {
  18. return new Node(v)
  19. }
  20. if (node.value > v) {
  21. node.left = this._addChild(node.left, v)
  22. } else if (node.value < v) {
  23. node.right = this._addChild(node.right, v)
  24. } else {
  25. node.value = v
  26. }
  27. node.height =
  28. 1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
  29. let factor = this._getBalanceFactor(node)
  30. // 当需要右旋时,根节点的左树一定比右树高度高
  31. if (factor > 1 && this._getBalanceFactor(node.left) >= 0) {
  32. return this._rightRotate(node)
  33. }
  34. // 当需要左旋时,根节点的左树一定比右树高度矮
  35. if (factor < -1 && this._getBalanceFactor(node.right) <= 0) {
  36. return this._leftRotate(node)
  37. }
  38. // 左右情况
  39. // 节点的左树比右树高,且节点的左树的右树比节点的左树的左树高
  40. if (factor > 1 && this._getBalanceFactor(node.left) < 0) {
  41. node.left = this._leftRotate(node.left)
  42. return this._rightRotate(node)
  43. }
  44. // 右左情况
  45. // 节点的左树比右树矮,且节点的右树的右树比节点的右树的左树矮
  46. if (factor < -1 && this._getBalanceFactor(node.right) > 0) {
  47. node.right = this._rightRotate(node.right)
  48. return this._leftRotate(node)
  49. }
  50. return node
  51. }
  52. _getHeight(node) {
  53. if (!node) return 0
  54. return node.height
  55. }
  56. _getBalanceFactor(node) {
  57. return this._getHeight(node.left) - this._getHeight(node.right)
  58. }
  59. // 节点右旋
  60. // 5 2
  61. // / \ / \
  62. // 2 6 ==> 1 5
  63. // / \ / / \
  64. // 1 3 new 3 6
  65. // /
  66. // new
  67. _rightRotate(node) {
  68. // 旋转后新根节点
  69. let newRoot = node.left
  70. // 需要移动的节点
  71. let moveNode = newRoot.right
  72. // 节点 2 的右节点改为节点 5
  73. newRoot.right = node
  74. // 节点 5 左节点改为节点 3
  75. node.left = moveNode
  76. // 更新树的高度
  77. node.height =
  78. 1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
  79. newRoot.height =
  80. 1 +
  81. Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))
  82. return newRoot
  83. }
  84. // 节点左旋
  85. // 4 6
  86. // / \ / \
  87. // 2 6 ==> 4 7
  88. // / \ / \ \
  89. // 5 7 2 5 new
  90. // \
  91. // new
  92. _leftRotate(node) {
  93. // 旋转后新根节点
  94. let newRoot = node.right
  95. // 需要移动的节点
  96. let moveNode = newRoot.left
  97. // 节点 6 的左节点改为节点 4
  98. newRoot.left = node
  99. // 节点 4 右节点改为节点 5
  100. node.right = moveNode
  101. // 更新树的高度
  102. node.height =
  103. 1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
  104. newRoot.height =
  105. 1 +
  106. Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))
  107. return newRoot
  108. }
  109. }

—Trie—

概念

在计算机科学,trie,又称前缀树字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。
简单点来说,这个结构的作用大多是为了方便搜索字符串,该树有以下几个特点

  • 根节点代表空字符串,每个节点都有 N(假如搜索英文字符,就有 26 条) 条链接,每条链接代表一个字符
  • 节点不存储字符,只有路径才存储,这点和其他的树结构不同
  • 从根节点开始到任意一个节点,将沿途经过的字符连接起来就是该节点对应的字符串

数据结构 - 图6

实现

总得来说 Trie 的实现相比别的树结构来说简单的很多,实现就以搜索英文字符为例。

  1. class TrieNode {
  2. constructor() {
  3. // 代表每个字符经过节点的次数
  4. this.path = 0
  5. // 代表到该节点的字符串有几个
  6. this.end = 0
  7. // 链接
  8. this.next = new Array(26).fill(null)
  9. }
  10. }
  11. class Trie {
  12. constructor() {
  13. // 根节点,代表空字符
  14. this.root = new TrieNode()
  15. }
  16. // 插入字符串
  17. insert(str) {
  18. if (!str) return
  19. let node = this.root
  20. for (let i = 0; i < str.length; i++) {
  21. // 获得字符先对应的索引
  22. let index = str[i].charCodeAt() - 'a'.charCodeAt()
  23. // 如果索引对应没有值,就创建
  24. if (!node.next[index]) {
  25. node.next[index] = new TrieNode()
  26. }
  27. node.path += 1
  28. node = node.next[index]
  29. }
  30. node.end += 1
  31. }
  32. // 搜索字符串出现的次数
  33. search(str) {
  34. if (!str) return
  35. let node = this.root
  36. for (let i = 0; i < str.length; i++) {
  37. let index = str[i].charCodeAt() - 'a'.charCodeAt()
  38. // 如果索引对应没有值,代表没有需要搜素的字符串
  39. if (!node.next[index]) {
  40. return 0
  41. }
  42. node = node.next[index]
  43. }
  44. return node.end
  45. }
  46. // 删除字符串
  47. delete(str) {
  48. if (!this.search(str)) return
  49. let node = this.root
  50. for (let i = 0; i < str.length; i++) {
  51. let index = str[i].charCodeAt() - 'a'.charCodeAt()
  52. // 如果索引对应的节点的 Path 为 0,代表经过该节点的字符串
  53. // 已经一个,直接删除即可
  54. if (--node.next[index].path == 0) {
  55. node.next[index] = null
  56. return
  57. }
  58. node = node.next[index]
  59. }
  60. node.end -= 1
  61. }
  62. }

—并查集—

概念

并查集是一种特殊的树结构,用于处理一些不交集的合并及查询问题。该结构中每个节点都有一个父节点,如果只有当前一个节点,那么该节点的父节点指向自己。
这个结构中有两个重要的操作,分别是:

  • Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
  • Union:将两个子集合并成同一个集合。

数据结构 - 图7

实现

  1. class DisjointSet {
  2. // 初始化样本
  3. constructor(count) {
  4. // 初始化时,每个节点的父节点都是自己
  5. this.parent = new Array(count)
  6. // 用于记录树的深度,优化搜索复杂度
  7. this.rank = new Array(count)
  8. for (let i = 0; i < count; i++) {
  9. this.parent[i] = i
  10. this.rank[i] = 1
  11. }
  12. }
  13. find(p) {
  14. // 寻找当前节点的父节点是否为自己,不是的话表示还没找到
  15. // 开始进行路径压缩优化
  16. // 假设当前节点父节点为 A
  17. // 将当前节点挂载到 A 节点的父节点上,达到压缩深度的目的
  18. while (p != this.parent[p]) {
  19. this.parent[p] = this.parent[this.parent[p]]
  20. p = this.parent[p]
  21. }
  22. return p
  23. }
  24. isConnected(p, q) {
  25. return this.find(p) === this.find(q)
  26. }
  27. // 合并
  28. union(p, q) {
  29. // 找到两个数字的父节点
  30. let i = this.find(p)
  31. let j = this.find(q)
  32. if (i === j) return
  33. // 判断两棵树的深度,深度小的加到深度大的树下面
  34. // 如果两棵树深度相等,那就无所谓怎么加
  35. if (this.rank[i] < this.rank[j]) {
  36. this.parent[i] = j
  37. } else if (this.rank[i] > this.rank[j]) {
  38. this.parent[j] = i
  39. } else {
  40. this.parent[i] = j
  41. this.rank[j] += 1
  42. }
  43. }
  44. }

—堆—

概念

堆通常是一个可以被看做一棵树的数组对象。
堆的实现通过构造二叉堆,实为二叉树的一种。这种数据结构具有以下性质。

  • 任意节点小于(或大于)它的所有子节点
  • 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层从左到右填入。

将根节点最大的堆叫做最大堆大根堆,根节点最小的堆叫做最小堆小根堆
优先队列也完全可以用堆来实现,操作是一模一样的。

实现大根堆

堆的每个节点的左边子节点索引是 i * 2 + 1,右边是 i * 2 + 2,父节点是 (i - 1) /2
堆有两个核心的操作,分别是 shiftUpshiftDown 。前者用于添加元素,后者用于删除根节点。
shiftUp 的核心思路是一路将节点与父节点对比大小,如果比父节点大,就和父节点交换位置。
shiftDown 的核心思路是先将根节点和末尾交换位置,然后移除末尾元素。接下来循环判断父节点和两个子节点的大小,如果子节点大,就把最大的子节点和父节点交换。
数据结构 - 图8

  1. class MaxHeap {
  2. constructor() {
  3. this.heap = []
  4. }
  5. size() {
  6. return this.heap.length
  7. }
  8. empty() {
  9. return this.size() == 0
  10. }
  11. add(item) {
  12. this.heap.push(item)
  13. this._shiftUp(this.size() - 1)
  14. }
  15. removeMax() {
  16. this._shiftDown(0)
  17. }
  18. getParentIndex(k) {
  19. return parseInt((k - 1) / 2)
  20. }
  21. getLeftIndex(k) {
  22. return k * 2 + 1
  23. }
  24. _shiftUp(k) {
  25. // 如果当前节点比父节点大,就交换
  26. while (this.heap[k] > this.heap[this.getParentIndex(k)]) {
  27. this._swap(k, this.getParentIndex(k))
  28. // 将索引变成父节点
  29. k = this.getParentIndex(k)
  30. }
  31. }
  32. _shiftDown(k) {
  33. // 交换首位并删除末尾
  34. this._swap(k, this.size() - 1)
  35. this.heap.splice(this.size() - 1, 1)
  36. // 判断节点是否有左孩子,因为二叉堆的特性,有右必有左
  37. while (this.getLeftIndex(k) < this.size()) {
  38. let j = this.getLeftIndex(k)
  39. // 判断是否有右孩子,并且右孩子是否大于左孩子
  40. if (j + 1 < this.size() && this.heap[j + 1] > this.heap[j]) j++
  41. // 判断父节点是否已经比子节点都大
  42. if (this.heap[k] >= this.heap[j]) break
  43. this._swap(k, j)
  44. k = j
  45. }
  46. }
  47. _swap(left, right) {
  48. let rightValue = this.heap[right]
  49. this.heap[right] = this.heap[left]
  50. this.heap[left] = rightValue
  51. }
  52. }