前言
本文为图灵程序设计丛书《学习JavaScript数据结构与算法.第3版》的读书笔记,记录常用的数据结构与算法。
书籍代码示例
数组
数组方法
| 方法 | 描述 |
|---|---|
| push | 在数组末尾插入元素 |
| pop | 在数组末尾删除元素 |
| unshift | 在数组开头插入元素 |
| shift | 在数组开头删除元素 |
| splice | 在数组任意位置添加或删除元素 |
| concat | 连接 2 个或更多数组,并返回结果 |
| every | 对数组中的每个元素运行给定函数,如果该函数对每个元素都返回 true,则返回 true |
| filter | 对数组中的每个元素运行给定函数,返回该函数会返回 true 的元素组成的数组 |
| forEach | 对数组中的每个元素运行给定函数。这个方法没有返回值 |
| join | 将所有的数组元素连接成一个字符串 |
| indexOf | 返回第一个与给定参数相等的数组元素的索引,没有找到则返回-1 |
| lastIndexOf | 返回在数组中搜索到的与给定参数相等的元素的索引里最大的值 |
| map | 对数组中的每个元素运行给定函数,返回每次函数调用的结果组成的数组 |
| reverse | 颠倒数组中元素的顺序,原先第一个元素现在变成最后一个,同样原先的最后一个元素变成了现在的第一个 |
| slice | 传入索引值,将数组里对应索引范围内的元素作为新数组返回 |
| some | 对数组中的每个元素运行给定函数,如果任一元素返回 true,则返回 true |
| sort | 按照字母顺序对数组排序,支持传入指定排序方法的函数作为参数 |
| toString | 将数组作为字符串返回 |
| valueOf | 和 toString 类似,将数组作为字符串返回 |
数组方法示例
数组操作
let numbers = [1, 2, 3, 4, 5];
// 末尾插入元素
numbers.push(6);
// 末尾删除元素
numbers.pop();
// 开头插入元素
numbers.unshift(6);
// 开头删除元素
numbers.shift();
// 任意位置添加或删除元素
numbers.splice(2, 1, 2, 3, 4);
数组合并
const zero = 0;
const positiveNumbers = [1, 2, 3];
const negativeNumbers = [-3, -2, -1];
let numbers = negativeNumbers.concat(zero, positiveNumbers);
数组遍历
const numbers = [1, 2, 3, 4, 5];
const isEven = x => x % 2 === 0;
// 用 every 方法迭代
// 我们要尝试的第一个方法是 every。every 方法会迭代数组中的每个元素,直到返回 false。
numbers.every(isEven);
// 用 some 方法迭代
// 和 every 的行为相反,会迭代数组的每个元素,直到函数返回 true。
numbers.some(isEven);
// 用 forEach 方法迭代
// 要迭代整个数组,可以用 forEach 方法。它和使用 for 循环的结果相同。
numbers.forEach(x => console.log(x % 2 === 0));
// 用 map 方法迭代
// 返回新数组的迭代方法。
numbers.map(isEven);
// 用 filter 方法迭代
// 返回新数组的迭代方法。返回的新数组由使函数返回 true 的元素组成。相当于过滤。
numbers.filter(isEven);
// 使用 reduce 方法
// reduce 方法接收一个有如下四个参数的函数:previousValue、currentValue、index 和 array。因为 index 和 array 是可选的参数,所以如果用不到它们的话,可以不传。这个函数会返回一个将被叠加到累加器的值,reduce 方法停止执行后会返回这个累加器。如果要对一个数组中的所有元素求和,这就很有用。
numbers.reduce((previous, current) => previous + current);
数组排序
// 反序
numbers.reverse();
// 排序
numbers.sort();
numbers.sort((a, b) => a - b);
数组搜索
console.log(numbers.indexOf(3));
console.log(numbers.lastIndexOf(4));
ECMAScript 6 和数组的新功能
| 方法 | 描述 |
|---|---|
| @@iterator | 返回一个包含数组键值对的迭代器对象,可以通过同步调用得到数组元素的键值对 |
| copyWithin | 复制数组中一系列元素到同一数组指定的起始位置 |
| entries | 返回包含数组所有键值对的@@iterator |
| includes | 如果数组中存在某个元素则返回 true,否则返回 false。E2016 新增 |
| find | 根据回调函数给定的条件从数组中查找元素,如果找到则返回该元素 |
| findIndex | 根据回调函数给定的条件从数组中查找元素,如果找到则返回该元素在数组中的索引 |
| fill | 用静态值填充数组 |
| from | 根据已有数组创建一个新数组 |
| keys | 返回包含数组所有索引的@@iterator |
| of | 根据传入的参数创建一个新数组 |
| values | 返回包含数组中所有值的@@iterator |
新功能示例
const numbers = [1, 2, 3, 4, 5];
// 使用 for...of 循环迭代
for (const n of numbers) {
console.log(n % 2 === 0 ? 'even' : 'odd');
}
// 使用@@iterator 对象
// ES2015 还为 Array 类增加了一个@@iterator 属性,需要通过 Symbol.iterator 来访问。
let iterator = numbers[Symbol.iterator]();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
// 数组的 entries、keys 和 values 方法
let aEntries = numbers.entries(); // 得到键值对的迭代器
console.log(aEntries.next().value); // [0, 1] - 位置 0 的值为 1
console.log(aEntries.next().value); // [1, 2] - 位置 1 的值为 2
console.log(aEntries.next().value); // [2, 3] - 位置 2 的值为 3
const aKeys = numbers.keys(); // 得到数组索引的迭代器
console.log(aKeys.next()); // {value: 0, done: false }
console.log(aKeys.next()); // {value: 1, done: false }
console.log(aKeys.next()); // {value: 2, done: false }
const aValues = numbers.values(); // 得到数组值的迭代器
console.log(aValues.next()); // {value: 1, done: false }
console.log(aValues.next()); // {value: 2, done: false }
console.log(aValues.next()); // {value: 3, done: false }
// 使用 from 方法
// Array.from 方法根据已有的数组创建一个新数组。比如,要复制 numbers 数组。
let numbers2 = Array.from(numbers);
// 使用 Array.of 方法
// Array.of 方法根据传入的参数创建一个新数组。
let numbers3 = Array.of(1);
let numbers4 = Array.of(1, 2, 3, 4, 5, 6);
let numbersCopy = Array.of(...numbers4);
// 使用 fill 方法
// fill 方法用静态值填充数组。
let numbersCopy = Array.of(1, 2, 3, 4, 5, 6);
numbersCopy.fill(0); // [0, 0, 0, 0, 0, 0]
numbersCopy.fill(2, 1); // [0, 2, 2, 2, 2, 2]
numbersCopy.fill(2, 1, 5);
// 使用 copyWithin 方法
// copyWithin 方法复制数组中的一系列元素到同一数组指定的起始位置。看看下面这个例子。
let copyArray = [1, 2, 3, 4, 5, 6];
// 假如我们想把 4、5、6 三个值复制到数组前三个位置,得到[4, 5, 6, 4, 5, 6]这个数组,可以用下面的代码达到目的。
copyArray.copyWithin(0, 3);
// 假如我们想把 4、5 两个值(在位置 3 和 4 上)复制到位置 1 和 2,可以这样做:
copyArray = [1, 2, 3, 4, 5, 6];
copyArray.copyWithin(1, 3, 5);
// 这种情况下,会把从位置 3 开始到位置 5 结束(不包括 3 和 5)的元素复制到位置 1,结果是得到数组[1, 4, 5, 4, 5, 6]。
// find、findIndex
let numbers = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];
function multipleOf13(element, index, array) {
return (element % 13 == 0);
}
console.log(numbers.find(multipleOf13));
console.log(numbers.findIndex(multipleOf13));
// 使用 includes 方法
// 如果数组里存在某个元素,includes 方法会返回 true,否则返回 false。
console.log(numbers.includes(15));
console.log(numbers.includes(20));
栈
栈是一种遵从后进先出(LIFO)原则的有序集合。新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。
创建一个基于数组的栈
class Stack {
constructor() {
this.items = [];
}
// 向栈添加元素
push(element) {
this.items.push(element);
}
// 从栈移除元素
pop() {
return this.items.pop();
}
// 查看栈顶元素
peek() {
return this.items[this.items.length - 1];
}
// 检查栈是否为空
isEmpty() {
return this.items.length === 0;
}
// 获取栈的长度
size() {
return this.items.length;
}
// 清空栈元素
clear() {
this.items = [];
}
toArray() {
return this.items;
}
toString() {
return this.items.toString();
}
}
创建一个基于JavaScript对象的Stack类
class Stack {
constructor() {
this.count = 0;
this.items = {};
}
push(element) {
this.items[this.count] = element;
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];
}
isEmpty() {
return this.count === 0;
}
size() {
return this.count;
}
clear() {
this.items = {};
this.count = 0;
}
toString() {
if (this.isEmpty()) {
return '';
}
let objString = `${this.items[0]}`;
for (let i = 1; i < this.count; i++) {
objString = `${objString},${this.items[i]}`;
}
return objString;
}
}
队列
队列是遵循先进先出(FIFO,也称为先来先服务)原则的一组有序的项。队列在尾部添加新元素,并从顶部移除元素。最新添加的元素必须排在队列的末尾。
创建一个基于对象的队列
class Queue {
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
enqueue(element) {
this.items[this.count] = element;
this.count++;
}
dequeue() {
if (this.isEmpty()) {
return undefined;
}
const result = this.items[this.lowestCount];
delete this.items[this.lowestCount];
this.lowestCount++;
return result;
}
peek() {
if (this.isEmpty()) {
return undefined;
}
return this.items[this.lowestCount];
}
isEmpty() {
return this.size() === 0;
}
clear() {
this.items = {};
this.count = 0;
this.lowestCount = 0;
}
size() {
return this.count - this.lowestCount;
}
toString() {
if (this.isEmpty()) {
return '';
}
let objString = `${this.items[this.lowestCount]}`;
for (let i = this.lowestCount + 1; i < this.count; i++) {
objString = `${objString},${this.items[i]}`;
}
return objString;
}
}
双端队列
双端队列(deque,或称 double-ended queue)是一种允许我们同时从前端和后端添加和移除元素的特殊队列。
class Deque {
constructor() {
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
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.items[0] = element;
}
}
addBack(element) {
this.items[this.count] = element;
this.count++;
}
removeFront() {
if (this.isEmpty()) {
return undefined;
}
const result = this.items[this.lowestCount];
delete this.items[this.lowestCount];
this.lowestCount++;
return result;
}
removeBack() {
if (this.isEmpty()) {
return undefined;
}
this.count--;
const result = this.items[this.count];
delete this.items[this.count];
return result;
}
peekFront() {
if (this.isEmpty()) {
return undefined;
}
return this.items[this.lowestCount];
}
peekBack() {
if (this.isEmpty()) {
return undefined;
}
return this.items[this.count - 1];
}
isEmpty() {
return this.size() === 0;
}
clear() {
this.items = {};
this.count = 0;
this.lowestCount = 0;
}
size() {
return this.count - this.lowestCount;
}
toString() {
if (this.isEmpty()) {
return '';
}
let objString = `${this.items[this.lowestCount]}`;
for (let i = this.lowestCount + 1; i < this.count; i++) {
objString = `${objString},${this.items[i]}`;
}
return objString;
}
}
链表
链表
function defaultEquals(a, b) {
return a === b;
}
class Node {
constructor(element, next) {
this.element = element;
this.next = next;
}
}
class LinkedList {
constructor(equalsFn = defaultEquals) {
this.equalsFn = equalsFn;
this.count = 0;
this.head = undefined;
}
push(element) {
const node = new Node(element);
let current;
if (this.head == null) {
// catches null && undefined
this.head = node;
} else {
current = this.head;
while (current.next != null) {
current = current.next;
}
current.next = node;
}
this.count++;
}
getElementAt(index) {
if (index >= 0 && index <= this.count) {
let node = this.head;
for (let i = 0; i < index && node != null; i++) {
node = node.next;
}
return node;
}
return undefined;
}
insert(element, index) {
if (index >= 0 && index <= this.count) {
const node = new Node(element);
if (index === 0) {
const current = this.head;
node.next = current;
this.head = node;
} else {
const previous = this.getElementAt(index - 1);
node.next = previous.next;
previous.next = 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;
} else {
const previous = this.getElementAt(index - 1);
current = previous.next;
previous.next = current.next;
}
this.count--;
return current.element;
}
return undefined;
}
remove(element) {
const index = this.indexOf(element);
return this.removeAt(index);
}
indexOf(element) {
let current = this.head;
for (let i = 0; i < this.size() && current != null; i++) {
if (this.equalsFn(element, current.element)) {
return i;
}
current = current.next;
}
return -1;
}
isEmpty() {
return this.size() === 0;
}
size() {
return this.count;
}
getHead() {
return this.head;
}
clear() {
this.head = undefined;
this.count = 0;
}
toString() {
if (this.head == null) {
return '';
}
let objString = `${this.head.element}`;
let current = this.head.next;
for (let i = 1; i < this.size() && current != null; i++) {
objString = `${objString},${current.element}`;
current = current.next;
}
return objString;
}
}
双向链表
双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个节点的链接;而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素。
import { defaultEquals } from '../util';
import LinkedList from './linked-list';
import { DoublyNode } from './models/linked-list-models';
export default class DoublyLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals) {
super(equalsFn);
this.tail = undefined;
}
push(element) {
const node = new DoublyNode(element);
if (this.head == null) {
this.head = node;
this.tail = node; // NEW
} else {
// attach to the tail node // NEW
this.tail.next = node;
node.prev = this.tail;
this.tail = node;
}
this.count++;
}
insert(element, index) {
if (index >= 0 && index <= this.count) {
const node = new DoublyNode(element);
let current = this.head;
if (index === 0) {
if (this.head == null) { // NEW
this.head = node;
this.tail = node; // NEW
} else {
node.next = this.head;
this.head.prev = node; // NEW
this.head = node;
}
} else if (index === this.count) { // last item NEW
current = this.tail;
current.next = node;
node.prev = current;
this.tail = node;
} else {
const previous = this.getElementAt(index - 1);
current = previous.next;
node.next = current;
previous.next = node;
current.prev = node; // NEW
node.prev = previous; // NEW
}
this.count++;
return true;
}
return false;
}
removeAt(index) {
if (index >= 0 && index < this.count) {
let current = this.head;
if (index === 0) {
this.head = this.head.next;
// if there is only one item, then we update tail as well //NEW
if (this.count === 1) {
// {2}
this.tail = undefined;
} else {
this.head.prev = undefined;
}
} else if (index === this.count - 1) {
// last item //NEW
current = this.tail;
this.tail = current.prev;
this.tail.next = undefined;
} else {
current = this.getElementAt(index);
const previous = current.prev;
// link previous with current's next - skip it to remove
previous.next = current.next;
current.next.prev = previous; // NEW
}
this.count--;
return current.element;
}
return undefined;
}
indexOf(element) {
let current = this.head;
let index = 0;
while (current != null) {
if (this.equalsFn(element, current.element)) {
return index;
}
index++;
current = current.next;
}
return -1;
}
getHead() {
return this.head;
}
getTail() {
return this.tail;
}
clear() {
super.clear();
this.tail = undefined;
}
toString() {
if (this.head == null) {
return '';
}
let objString = `${this.head.element}`;
let current = this.head.next;
while (current != null) {
objString = `${objString},${current.element}`;
current = current.next;
}
return objString;
}
inverseToString() {
if (this.tail == null) {
return '';
}
let objString = `${this.tail.element}`;
let previous = this.tail.prev;
while (previous != null) {
objString = `${objString},${previous.element}`;
previous = previous.prev;
}
return objString;
}
}
循环链表
循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用。循环链表和链表之间唯一的区别在于,最后一个元素指向下一个元素的指针(tail.next)不是引用undefined,而是指向第一个元素(head)。 双向循环链表有指向 head 元素的 tail.next 和指向 tail 元素的 head.prev。
import { defaultEquals } from '../util';
import LinkedList from './linked-list';
import { Node } from './models/linked-list-models';
export default class CircularLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals) {
super(equalsFn);
}
push(element) {
const node = new Node(element);
let current;
if (this.head == null) {
this.head = node;
} else {
current = this.getElementAt(this.size() - 1);
current.next = node;
}
// set node.next to head - to have circular list
node.next = this.head;
this.count++;
}
insert(element, index) {
if (index >= 0 && index <= this.count) {
const node = new Node(element);
let current = this.head;
if (index === 0) {
if (this.head == null) {
// if no node in list
this.head = node;
node.next = this.head;
} else {
node.next = current;
current = this.getElementAt(this.size());
// update last element
this.head = node;
current.next = this.head;
}
} else {
const previous = this.getElementAt(index - 1);
node.next = previous.next;
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 {
const removed = this.head;
current = this.getElementAt(this.size() - 1);
this.head = this.head.next;
current.next = this.head;
current = removed;
}
} else {
// no need to update last element for circular list
const previous = this.getElementAt(index - 1);
current = previous.next;
previous.next = current.next;
}
this.count--;
return current.element;
}
return undefined;
}
}
有序链表
有序链表是指保持元素有序的链表结构。除了使用排序算法之外,我们还可以将元素插入到正确的位置来保证链表的有序性。
import { Compare, defaultCompare, defaultEquals } from '../util';
import LinkedList from './linked-list';
export default class SortedLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals, compareFn = defaultCompare) {
super(equalsFn);
this.equalsFn = equalsFn;
this.compareFn = compareFn;
}
push(element) {
if (this.isEmpty()) {
super.push(element);
} else {
const index = this.getIndexNextSortedElement(element);
super.insert(element, index);
}
}
insert(element, index = 0) {
if (this.isEmpty()) {
return super.insert(element, index === 0 ? index : 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;
}
}
用链表创建栈
class StackLinkedList {
constructor() {
this.items = new DoublyLinkedList();
}
push(element) {
this.items.push(element);
}
pop() {
if (this.isEmpty()) {
return undefined;
}
return this.items.removeAt(this.size() - 1);
}
peek() {
if (this.isEmpty()) {
return undefined;
}
return this.items.getElementAt(this.size() - 1).element;
}
isEmpty() {
return this.items.isEmpty();
}
size() {
return this.items.size();
}
clear() {
this.items.clear();
}
toString() {
return this.items.toString();
}
}
集合 Set
集合是由一组无序且唯一(即不能重复)的项组成的。该数据结构使用了与有限集合相同的数学概念,但应用在计算机科学的数据结构中。
export default class Set {
constructor() {
this.items = {};
}
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;
}
has(element) {
return Object.prototype.hasOwnProperty.call(this.items, element);
}
values() {
return Object.values(this.items);
}
union(otherSet) {
const unionSet = new Set();
this.values().forEach(value => unionSet.add(value));
otherSet.values().forEach(value => unionSet.add(value));
return unionSet;
}
intersection(otherSet) {
const intersectionSet = new Set();
const values = this.values();
const otherValues = otherSet.values();
let biggerSet = values;
let smallerSet = otherValues;
if (otherValues.length - values.length > 0) {
biggerSet = otherValues;
smallerSet = values;
}
smallerSet.forEach(value => {
if (biggerSet.includes(value)) {
intersectionSet.add(value);
}
});
return intersectionSet;
}
difference(otherSet) {
const differenceSet = new Set();
this.values().forEach(value => {
if (!otherSet.has(value)) {
differenceSet.add(value);
}
});
return differenceSet;
}
isSubsetOf(otherSet) {
if (this.size() > otherSet.size()) {
return false;
}
let isSubset = true;
this.values().every(value => {
if (!otherSet.has(value)) {
isSubset = false;
return false;
}
return true;
});
return isSubset;
}
isEmpty() {
return this.size() === 0;
}
size() {
return Object.keys(this.items).length;
}
clear() {
this.items = {};
}
toString() {
if (this.isEmpty()) {
return '';
}
const values = this.values();
let objString = `${values[0]}`;
for (let i = 1; i < values.length; i++) {
objString = `${objString},${values[i].toString()}`;
}
return objString;
}
}
字典和散列表
集合表示一组互不相同的元素(不重复的元素)。在字典中,存储的是[键,值]对,其中键名是用来查询特定元素的。字典和集合很相似,集合以[值,值]的形式存储元素,字典则是以[键,值]的形式来存储元素。字典也称作映射、符号表或关联数组。
字典
import { defaultToString } from '../util';
import { ValuePair } from './models/value-pair';
export default class Dictionary {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;
this.table = {};
}
set(key, value) {
if (key != null && value != null) {
const tableKey = this.toStrFn(key);
this.table[tableKey] = new ValuePair(key, value);
return true;
}
return false;
}
get(key) {
const valuePair = this.table[this.toStrFn(key)];
return valuePair == null ? undefined : valuePair.value;
}
hasKey(key) {
return this.table[this.toStrFn(key)] != null;
}
remove(key) {
if (this.hasKey(key)) {
delete this.table[this.toStrFn(key)];
return true;
}
return false;
}
values() {
return this.keyValues().map(valuePair => valuePair.value);
}
keys() {
return this.keyValues().map(valuePair => valuePair.key);
}
keyValues() {
return Object.values(this.table);
}
forEach(callbackFn) {
const valuePairs = this.keyValues();
for (let i = 0; i < valuePairs.length; i++) {
const result = callbackFn(valuePairs[i].key, valuePairs[i].value);
if (result === false) {
break;
}
}
}
isEmpty() {
return this.size() === 0;
}
size() {
return Object.keys(this.table).length;
}
clear() {
this.table = {};
}
toString() {
if (this.isEmpty()) {
return '';
}
const valuePairs = this.keyValues();
let objString = `${valuePairs[0].toString()}`;
for (let i = 1; i < valuePairs.length; i++) {
objString = `${objString},${valuePairs[i].toString()}`;
}
return objString;
}
}
散列表
import { defaultToString } from '../util';
import { ValuePair } from './models/value-pair';
export default 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;
}
/* djb2HashCode(key) {
const tableKey = this.toStrFn(key);
let hash = 5381;
for (let i = 0; i < tableKey.length; i++) {
hash = (hash * 33) + tableKey.charCodeAt(i);
}
return hash % 1013;
} */
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;
}
return false;
}
get(key) {
const valuePair = this.table[this.hashCode(key)];
return valuePair == null ? undefined : valuePair.value;
}
remove(key) {
const hash = this.hashCode(key);
const valuePair = this.table[hash];
if (valuePair != null) {
delete this.table[hash];
return true;
}
return false;
}
getTable() {
return this.table;
}
isEmpty() {
return this.size() === 0;
}
size() {
return Object.keys(this.table).length;
}
clear() {
this.table = {};
}
toString() {
if (this.isEmpty()) {
return '';
}
const keys = Object.keys(this.table);
let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
for (let i = 1; i < keys.length; i++) {
objString = `${objString},{${keys[i]} => ${this.table[keys[i]].toString()}}`;
}
return objString;
}
}
递归
递归是一种解决问题的方法,它从解决问题的各个小部分开始,直到解决最初的大问题。递归通常涉及函数调用自身。
每个递归函数都必须有基线条件,即一个不再递归调用的条件(停止点),以防止无限递归。
function understandRecursion(doIunderstandRecursion) {
const recursionAnswer = confirm('Do you understand recursion?');
if (recursionAnswer === true) { // 基线条件或停止点
return true;
}
understandRecursion(recursionAnswer); // 递归调用
}
understandRecursion 函数会不断地调用自身,直到 recursionAnswer 为真(true)。
recursionAnswer 为真就是上述代码的基线条件。
递归阶乘
function factorial(n) {
if (n === 1 || n === 0) { // 基线条件
return 1;
}
return n * factorial(n - 1); // 递归调用
}
斐波那契数列
function fibonacci(n){
if (n < 1) return 0;
if (n <= 2) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// 尾调用优化
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
树
树是一种分层数据的抽象模型。一个树结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了顶部的第一个节点)以及零个或多个子节点。
二叉树和二叉搜索树
二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。 二叉搜索树(BST)是二叉树的一种,但是只允许你在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大的值。
树的遍历
中序遍历是一种以上行顺序访问 BST 所有节点的遍历方式,也就是以从最小到最大的顺序访问所有节点。中序遍历的一种应用就是对树进行排序操作。(inOrderTraverse)
先序遍历是以优先于后代节点的顺序访问每个节点的。先序遍历的一种应用是打印一个结构化的文档。(preOrderTraverse)
后序遍历则是先访问节点的后代节点,再访问节点本身。后序遍历的一种应用是计算一个目录及其子目录中所有文件所占空间的大小。(postOrderTraverse)
import { Compare, defaultCompare } from '../util';
class Node {
constructor(key) {
this.key = key;
this.left = undefined;
this.right = undefined;
}
toString() {
return `${this.key}`;
}
}
export default class BinarySearchTree {
constructor(compareFn = defaultCompare) {
this.compareFn = compareFn;
this.root = undefined;
}
insert(key) {
// special case: first key
if (this.root == null) {
this.root = new Node(key);
} else {
this.insertNode(this.root, key);
}
}
insertNode(node, key) {
if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
if (node.left == null) {
node.left = new Node(key);
} else {
this.insertNode(node.left, key);
}
} else if (node.right == null) {
node.right = new Node(key);
} else {
this.insertNode(node.right, key);
}
}
getRoot() {
return this.root;
}
search(key) {
return 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);
} if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
return this.searchNode(node.right, key);
}
return true;
}
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);
}
}
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);
}
}
postOrderTraverse(callback) {
this.postOrderTraverseNode(this.root, callback);
}
postOrderTraverseNode(node, callback) {
if (node != null) {
this.postOrderTraverseNode(node.left, callback);
this.postOrderTraverseNode(node.right, callback);
callback(node.key);
}
}
min() {
return this.minNode(this.root);
}
minNode(node) {
let current = node;
while (current != null && current.left != null) {
current = current.left;
}
return current;
}
max() {
return this.maxNode(this.root);
}
maxNode(node) {
let current = node;
while (current != null && current.right != null) {
current = current.right;
}
return current;
}
remove(key) {
this.root = this.removeNode(this.root, key);
}
removeNode(node, key) {
if (node == null) {
return undefined;
}
if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
node.left = this.removeNode(node.left, key);
return node;
} if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
node.right = this.removeNode(node.right, key);
return node;
}
// key is equal to node.item
// handle 3 special conditions
// 1 - a leaf node
// 2 - a node with only 1 child
// 3 - a node with 2 children
// case 1
if (node.left == null && node.right == null) {
node = undefined;
return node;
}
// case 2
if (node.left == null) {
node = node.right;
return node;
} if (node.right == null) {
node = node.left;
return node;
}
// case 3
const aux = this.minNode(node.right);
node.key = aux.key;
node.right = this.removeNode(node.right, aux.key);
return node;
}
}
自平衡树
有一种树叫作 Adelson-Velskii-Landi 树(AVL 树)。AVL 树是一种自平衡二叉搜索树,意思是任何一个节点左右两侧子树的高度之差最多为 1。
红黑树
和 AVL 树一样,红黑树也是一个自平衡二叉搜索树。我们学习了对 AVL 书插入和移除节点可能会造成旋转,所以我们需要一个包含多次插入和删除的自平衡树,红黑树是比较好的。如果插入和删除频率较低(我们更需要多次进行搜索操作),那么 AVL 树比红黑树更好。
二叉堆和堆排序
二叉堆
二叉堆是一种特殊的二叉树,有以下两个特性。
- 它是一棵完全二叉树,表示树的每一层都有左侧和右侧子节点(除了最后一层的叶节点),并且最后一层的叶节点尽可能都是左侧子节点,这叫作结构特性。
- 二叉堆不是最小堆就是最大堆。最小堆允许你快速导出树的最小值,最大堆允许你快速导出树的最大值。所有的节点都大于等于(最大堆)或小于等于(最小堆)每个它的子节点。这叫作堆特性。
import {
Compare, defaultCompare, reverseCompare, swap
} from '../util';
export class MinHeap {
constructor(compareFn = defaultCompare) {
this.compareFn = compareFn;
this.heap = [];
}
getLeftIndex(index) {
return (2 * index) + 1;
}
getRightIndex(index) {
return (2 * index) + 2;
}
getParentIndex(index) {
if (index === 0) {
return undefined;
}
return Math.floor((index - 1) / 2);
}
size() {
return this.heap.length;
}
isEmpty() {
return this.size() <= 0;
}
clear() {
this.heap = [];
}
findMinimum() {
return this.isEmpty() ? undefined : this.heap[0];
}
insert(value) {
if (value != null) {
const index = this.heap.length;
this.heap.push(value);
this.siftUp(index);
return true;
}
return false;
}
siftDown(index) {
let element = index;
const left = this.getLeftIndex(index);
const right = this.getRightIndex(index);
const size = this.size();
if (
left < size
&& this.compareFn(this.heap[element], this.heap[left]) === Compare.BIGGER_THAN
) {
element = left;
}
if (
right < size
&& this.compareFn(this.heap[element], this.heap[right]) === Compare.BIGGER_THAN
) {
element = right;
}
if (index !== element) {
swap(this.heap, index, element);
this.siftDown(element);
}
}
siftUp(index) {
let parent = this.getParentIndex(index);
while (
index > 0
&& this.compareFn(this.heap[parent], this.heap[index]) === Compare.BIGGER_THAN
) {
swap(this.heap, parent, index);
index = parent;
parent = this.getParentIndex(index);
}
}
extract() {
if (this.isEmpty()) {
return undefined;
}
if (this.size() === 1) {
return this.heap.shift();
}
const removedValue = this.heap[0];
this.heap[0] = this.heap.pop();
this.siftDown(0);
return removedValue;
}
heapify(array) {
if (array) {
this.heap = array;
}
const maxIndex = Math.floor(this.size() / 2) - 1;
for (let i = 0; i <= maxIndex; i++) {
this.siftDown(i);
}
return this.heap;
}
getAsArray() {
return this.heap;
}
}
export class MaxHeap extends MinHeap {
constructor(compareFn = defaultCompare) {
super(compareFn);
this.compareFn = compareFn;
this.compareFn = reverseCompare(compareFn);
}
}
图
排序和搜索算法
排序
冒泡排序
冒泡排序比较所有相邻的两个项,如果第一个比第二个大,则交换它们。元素项向上移动至正确的顺序,就好像气泡升至表面一样,冒泡排序因此得名。
function swap(array, a, b) {
/* const temp = array[a];
array[a] = array[b];
array[b] = temp; */ // 经典方式
[array[a], array[b]] = [array[b], array[a]]; // ES2015 的方式
}
function bubbleSort(array, compareFn = defaultCompare) {
const { length } = array;
for (let i = 0; i < length; i++) {
for (let j = 0; j < length - 1; j++) {
if (compareFn(array[j], array[j + 1]) === Compare.BIGGER_THAN) {
swap(array, j, j + 1);
}
}
}
return array;
}
// 改进后的冒泡排序
// 如果从内循环减去外循环中已跑过的轮数,就可以避免内循环中所有不必要的比较
function modifiedBubbleSort(array, compareFn = defaultCompare) {
const { length } = array;
for (let i = 0; i < length; i++) {
for (let j = 0; j < length - 1 - i; j++) {
if (compareFn(array[j], array[j + 1]) === Compare.BIGGER_THAN) {
swap(array, j, j + 1);
}
}
}
return array;
}
选择排序
选择排序算法是一种原址比较排序算法。选择排序大致的思路是找到数据结构中的最小值并将其放置在第一位,接着找到第二小的值并将其放在第二位,以此类推。
import { Compare, defaultCompare, swap } from '../../util';
export const selectionSort = (array, compareFn = defaultCompare) => {
const { length } = array;
let indexMin;
for (let i = 0; i < length - 1; i++) {
indexMin = i;
// console.log('index ' + array[i]);
for (let j = i; j < length; j++) {
if (compareFn(array[indexMin], array[j]) === Compare.BIGGER_THAN) {
// console.log('new index min ' + array[j]);
indexMin = j;
}
}
if (i !== indexMin) {
// console.log('swap ' + array[i] + ' with ' + array[indexMin]);
swap(array, i, indexMin);
}
}
return array;
};
插入排序
插入排序每次排一个数组项,以此方式构建最后的排序数组。假定第一项已经排序了。接着,它和第二项进行比较——第二项是应该待在原位还是插到第一项之前呢?这样,头两项就已正确排序,接着和第三项比较(它是该插入到第一、第二还是第三的位置呢),以此类推。
import { Compare, defaultCompare } from '../../util';
export const insertionSort = (array, compareFn = defaultCompare) => {
const { length } = array;
let temp;
for (let i = 1; i < length; i++) {
let j = i;
temp = array[i];
// console.log('to be inserted ' + temp);
while (j > 0 && compareFn(array[j - 1], temp) === Compare.BIGGER_THAN) {
// console.log('shift ' + array[j - 1]);
array[j] = array[j - 1];
j--;
}
// console.log('insert ' + temp);
array[j] = temp;
}
return array;
};
归并排序
归并排序是第一个可以实际使用的排序算法。你在本书中学到的前三个排序算法性能不好,但归并排序性能不错,其复杂度为 O(nlog(n))。
import { Compare, defaultCompare } from '../../util';
function merge(left, right, compareFn) {
let i = 0;
let j = 0;
const result = [];
while (i < left.length && j < right.length) {
result.push(compareFn(left[i], right[j]) === Compare.LESS_THAN ? left[i++] : right[j++]);
}
return result.concat(i < left.length ? left.slice(i) : right.slice(j));
}
export function mergeSort(array, compareFn = defaultCompare) {
if (array.length > 1) {
const { length } = array;
const middle = Math.floor(length / 2);
const left = mergeSort(array.slice(0, middle), compareFn);
const right = mergeSort(array.slice(middle, length), compareFn);
array = merge(left, right, compareFn);
}
return array;
}
快速排序
快速排序也许是最常用的排序算法了。它的复杂度为 O(nlog(n)),且性能通常比其他复杂度为 O(nlog(n))的排序算法要好。和归并排序一样,快速排序也使用分而治之的方法,将原始数组分为较小的数组(但它没有像归并排序那样将它们分割开)。
import { Compare, defaultCompare, swap } from '../../util';
function partition(array, left, right, compareFn) {
const pivot = array[Math.floor((right + left) / 2)];
let i = left;
let j = right;
while (i <= j) {
while (compareFn(array[i], pivot) === Compare.LESS_THAN) {
i++;
}
while (compareFn(array[j], pivot) === Compare.BIGGER_THAN) {
j--;
}
if (i <= j) {
swap(array, i, j);
i++;
j--;
}
}
return i;
}
function quick(array, left, right, compareFn) {
let index;
if (array.length > 1) {
index = partition(array, left, right, compareFn);
if (left < index - 1) {
quick(array, left, index - 1, compareFn);
}
if (index < right) {
quick(array, index, right, compareFn);
}
}
return array;
}
export function quickSort(array, compareFn = defaultCompare) {
return quick(array, 0, array.length - 1, compareFn);
}
计数排序
计数排序是我们在本书中学习的第一个分布式排序。分布式排序使用已组织好的辅助数据结构(称为桶),然后进行合并,得到排好序的数组。计数排序使用一个用来存储每个元素在原始数组中出现次数的临时数组。在所有元素都计数完成后,临时数组已排好序并可迭代以构建排序后的结果数组。
它是用来排序整数的优秀算法(它是一个整数排序算法),时间复杂度为 O(n+k),其中 k 是临时计数数组的大小;但是,它确实需要更多的内存来存放临时数组。
function findMaxValue(array, compareFn = defaultCompare) {
if (array && array.length > 0) {
let max = array[0];
for (let i = 1; i < array.length; i++) {
if (compareFn(max, array[i]) === Compare.LESS_THAN) {
max = array[i];
}
}
return max;
}
return undefined;
}
export function countingSort(array) {
if (array.length < 2) {
return array;
}
const maxValue = findMaxValue(array);
let sortedIndex = 0;
const counts = new Array(maxValue + 1);
array.forEach(element => {
if (!counts[element]) {
counts[element] = 0;
}
counts[element]++;
});
// console.log('Frequencies: ' + counts.join());
counts.forEach((element, i) => {
while (element > 0) {
array[sortedIndex++] = i;
element--;
}
});
return array;
}
桶排序
桶排序(也被称为箱排序)也是分布式排序算法,它将元素分为不同的桶(较小的数组),再使用一个简单的排序算法,例如插入排序(用来排序小数组的不错的算法),来对每个桶进行排序。然后,它将所有的桶合并为结果数组。
基数排序
基数排序也是一个分布式排序算法,它根据数字的有效位或基数(这也是它为什么叫基数排序)将整数分布到桶中。基数是基于数组中值的记数制的。
搜索算法
顺序搜索
顺序或线性搜索是最基本的搜索算法。它的机制是,将每一个数据结构中的元素和我们要找的元素做比较。顺序搜索是最低效的一种搜索算法。
二分搜索
二分搜索算法的原理和猜数字游戏类似,就是那个有人说“我正想着一个 1~100 的数”的游戏。我们每回应一个数,那个人就会说这个数是高了、低了还是对了。
这个算法要求被搜索的数据结构已排序。以下是该算法遵循的步骤。
(1) 选择数组的中间值。
(2) 如果选中值是待搜索值,那么算法执行完毕(值找到了)。
(3) 如果待搜索值比选中值要小,则返回步骤 1 并在选中值左边的子数组中寻找(较小)。
(4) 如果待搜索值比选中值要大,则返回步骤 1 并在选种值右边的子数组中寻找(较大)。
import { Compare, defaultCompare, DOES_NOT_EXIST } from '../../util';
import { quickSort } from '../sorting/quicksort';
export function binarySearch(array, value, compareFn = defaultCompare) {
const sortedArray = quickSort(array);
let low = 0;
let high = sortedArray.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const element = sortedArray[mid];
// console.log('mid element is ' + element);
if (compareFn(element, value) === Compare.LESS_THAN) {
low = mid + 1;
// console.log('low is ' + low);
} else if (compareFn(element, value) === Compare.BIGGER_THAN) {
high = mid - 1;
// console.log('high is ' + high);
} else {
// console.log('found it');
return mid;
}
}
return DOES_NOT_EXIST;
}
内插搜索
内插搜索是改良版的二分搜索。二分搜索总是检查 mid 位置上的值,而内插搜索可能会根据要搜索的值检查数组中的不同地方。
这个算法要求被搜索的数据结构已排序。以下是该算法遵循的步骤:
(1) 使用 position 公式选中一个值;
(2) 如果这个值是待搜索值,那么算法执行完毕(值找到了);
(3) 如果待搜索值比选中值要小,则返回步骤 1 并在选中值左边的子数组中寻找(较小);
(4) 如果待搜索值比选中值要大,则返回步骤 1 并在选种值右边的子数组中寻找(较大)。
随机算法
Fisher-Yates 随机
它的含义是迭代数组,从最后一位开始并将当前位置和一个随机位置进行交换。这个随机位置比当前位置小。这样,这个算法可以保证随机过的位置不会再被随机一次(洗扑克牌的次数越多,随机效果越差)。
import { swap } from '../../util';
export function shuffle(array) {
let currentIndex = array.length;
while (currentIndex !== 0) {
const randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
swap(array, currentIndex, randomIndex);
}
return array;
}
算法设计与技巧
分而治之
前面我们学习了归并和排序算法。两者的共同点在于它们都是分而治之算法。分而治之是算法设计中的一种方法。它将一个问题分成多个和原问题相似的小问题,递归解决小问题,再将解决方式合并以解决原来的问题。
分而治之算法可以分成三个部分。
(1) 分解原问题为多个子问题(原问题的多个小实例)。
(2) 解决子问题,用返回解决子问题的方式的递归算法。递归算法的基本情形可以用来解决子问题。
(3) 组合这些子问题的解决方式,得到原问题的解。
动态规划
动态规划(dynamic programming,DP)是一种将复杂问题分解成更小的子问题来解决的优化技术。
注意,动态规划和分而治之是不同的方法。分而治之方法是把问题分解成相互独立的子问题,然后组合它们的答案,而动态规划则是将问题分解成相互依赖的子问题。
用动态规划解决问题时,要遵循三个重要步骤:
(1) 定义子问题;
(2) 实现要反复执行来解决子问题的部分(这一步要参考前一节讨论的递归的步骤);
(3) 识别并求解出基线条件。
能用动态规划解决的一些著名问题如下。
- 背包问题:给出一组项,各自有值和容量,目标是找出总值最大的项的集合。这个问题的限制是,总容量必须小于等于“背包”的容量。
- 最长公共子序列:找出一组序列的最长公共子序列(可由另一序列删除元素但不改变余下元素的顺序而得到)。
- 矩阵链相乘:给出一系列矩阵,目标是找到这些矩阵相乘的最高效办法(计算次数尽可能少)。相乘运算不会进行,解决方案是找到这些矩阵各自相乘的顺序。
- 硬币找零:给出面额为 d1, …, dn的一定数量的硬币和要找零的钱数,找出有多少种找零的方法。
- 图的全源最短路径:对所有顶点对(u, v),找出从顶点 u 到顶点 v 的最短路径。我们在第 9 章已经学习过这个问题的 Floyd-Warshall 算法。
贪心算法
贪心算法遵循一种近似解决问题的技术,期盼通过每个阶段的局部最优选择(当前最好的解),从而达到全局的最优(全局最优解)。它不像动态规划算法那样计算更大的格局。
回溯算法
回溯是一种渐进式寻找并构建问题解决方式的策略。我们从一个可能的动作开始并试着用这个动作解决问题。如果不能解决,就回溯并选择另一个动作直到将问题解决。根据这种行为,回溯算法会尝试所有可能的动作(如果更快找到了解决办法就尝试较少的次数)来解决问题。
有一些可用回溯解决的著名问题:
- 骑士巡逻问题
- N 皇后问题
- 迷宫老鼠问题
- 数独解题器
函数式编程
算法复杂度
大O表示法
它用于描述算法的性能和复杂程度。大 O 表示法将算法按照消耗的时间进行分类,依据随输入增大所需要的空间/内存。
分析算法时,时常遇到以下几类函数。
| 符号 | 名称 |
|---|---|
| O(1) | 常数的 |
| O(log(n)) | 对数的 |
| O((log(n))c) | 对数多项式的 |
| O(n) | 线性的 |
| O(n2) | 二次的 |
| O(nc) | 多项式的 |
| O(cn) | 指数的 |