一、数组
1.1、常用方法
方法 | 作用 |
---|---|
concat | 连接两个或更多数组,并返回结果,不影响原数组 |
slice | 传入索引值,将数组里对应索引范围内的元素作为新数组返回,不影响原数组 |
splice | 指定位置/索引,就可以删除相应位置和数量的元素,并以新元素代替,影响原数组 |
reverse | 数组元素反转,影响原数组 |
join | 将所有的数组元素以指定字符连接成字符串,不影响原数组 |
toString | 将数组整体作为字符串返回,不影响原数组 |
indexOf | 返回第一个与给定参数相等的数组元素的索引,没有找到则返回-1 |
lastIndexOf | 返回与给定参数相等的数组元素索引的最大值 |
sort | 按照字符对应的ASCII值对数组排序,支持传入指定排序方法的函数作为参数 |
map | 对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组 |
forEach | 对数组中的每一项运行给定函数,不影响原数组中的项 |
filter | 对数组中的每一项运行给定函数,返回该函数会返回true的项组成的数组 |
every | 对数组中的每一项运行给定函数,如果该函数中每一项都返回true,则返回true |
some | 对数组中的每一项运行给定函数,如果该函数中任一项返回true,则返回true |
reduce | 接收一个函数作为参数,做数组的累加器,或者拼接数组项作为字符串,不改变原数组 |
注
- 字符串也有
indexOf
和lastIndexOf
方法,用法和数组类似 slice
如果只有一个参数且为负数,那么会加上数组长度取值,如果加上数组长度后仍为负数,则取整个数组。如果有两个参数,会给负数加上数组长度取值,如果第一个参数大于等于第二个参数,取空
1.2、增删改查
pop
:删除并返回最后一个元素push
:向最后增加一个元素,返回数组长度shift
:删除第一个元素并返回unshift
:向头部添加一个元素,返回数组长度
1.3、length
- 数组的
length
是可读写的,并且会影响到原数组
let arr = [1, 2, 3];
console.log(arr.length); //3
arr.length = 2;
console.log(arr); // [1, 2]
二、栈
- 栈是一种遵从后进先出原则的有序集合。新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端就叫栈底。
2.1、方法
push()
:添加一个新元素到栈顶;pop()
:移除栈顶的元素,同时返回被移除的元素;peek()
:返回栈顶的元素,不对栈做任何修改;isEmpty()
:如果栈里没有任何元素就返回true,否则返回false;clear()
:移除栈里的所有元素;size()
:返回栈里的元素个数。
2.2、实现
class Stack {
constructor() {
this.items = [];
}
pop() {
return this.isEmpty()
? new Error("no item in Stack")
: this.items.pop();
}
push(item) {
this.items.push(item);
}
peek() {
return this.isEmpty()
? new Error("no item in Stack")
: this.items[this.size() - 1];
}
size() {
return this.items.length;
}
clear() {
this.items = [];
}
isEmpty() {
return this.items.length === 0;
}
}
三、队列
- 队列是遵循先进先出原则的一组有序的项,队列在尾部添加新元素,并从顶部移除元素。
3.1、方法
enquene(item)
:向队列尾部添加一个(或多个)新的项;dequene()
:移除队列的第一(即排在队列最前面的)项,并返回被移除的元素;front()
:返回队列中的第一个元素,队列没变动;isEmpty()
:如果队列中不包含任何元素,返回true,否则返回false;size()
:返回队列包含的元素个数。
3.2、实现
- 普通队列
class Quene {
constructor() {
this.items = [];
}
enquene(item) {
this.items.push(item);
}
dequene() {
return this.isEmpty()
? new Error("no item in Quene")
: this.items.shift();
}
front() {
return this.isEmpty()
? new Error("no item in Quene")
: this.items[0];
}
isEmpty() {
return this.items.length === 0;
}
size() {
return this.items.length;
}
clear() {
this.items = [];
}
}
- 优先队列
class QueneItem {
constructor(item, priority) {
this.item = item;
this.priority = priority;
}
}
class Quene {
constructor() {
this.items = [];
}
enquene(item, priority) {
let el = new QueneItem(item, priority);
if (this.isEmpty()) {
this.items.push(el);
return;
} else {
let i = 0;
while (this.items[i]) {
if (this.items[i].priority < el.priority) {
this.items.splice(i, 0, el);
return;
}
i++;
}
this.items.push(el);
}
}
//...其他方法
}
应用
- 击鼓传花
function game(arr, n) {
let quene = new Quene();
for (let i = 0, len = arr.length; i < len; i++) {
quene.enquene(arr[i]);
}
while (quene.size > 1) {
for (let i = 0; i < n; i++) {
quene.enquene(quene.dequene());
}
}
return quene.dequene();
}
四、链表
- 要存储多个元素,数组可能是最常用的数据结构。数组查询数据方便,但是在数组中插入或移除项的成本很高,因为需要移动元素。
4.1、单向链表
class Node {
constructor(el) {
this.el = el;
this.next = null;
}
}
class LinkedList {
constructor() {
this.length = 0;
this.head = null;
}
append(el) {
let node = new Node(el);
let current = this.head;
if (this.isEmpty()) {
this.head = node;
} else {
while (current.next) {
current = current.next;
}
current.next = node;
}
this.length++;
}
insert(el, pos) {
if (pos < 0 || pos > this.length) {
return new Error("Invalid pos");
}
let node = new Node(el);
let index = 0,
previos;
let current = this.head;
if (pos === 0) {
node.next = current;
this.head = node;
} else {
while (index++ < pos) {
previos = current;
current = current.next;
}
previos.next = node;
node.next = current;
}
this.length++;
return true;
}
removeAt(pos) {
let current = this.head;
let index = 0,
previos;
if (pos < 0 || pos > this.length) {
return new Error("Invalid pos");
}
if (pos === 0) {
this.head = current.next;
} else {
while (index++ < pos) {
previos = current;
current = current.next;
}
previos.next = current.next;
}
this.length--;
return current.el;
}
toString(){
let res = ""
let current = this.head
while(current){
res += current.el
current = current.next
}
return res
}
indexOf(el){
let current = this.head
let index = 0
while(current){
if(current.el === el){
return index
}
current = current.next
index++
}
return -1
}
remove(el){
let index = this.indexOf(el)
this.removeAt(index)
}
size() {
return this.length;
}
isEmpty() {
return this.length === 0;
}
getHead(){
return this.head
}
}
3.2、双向链表
class Node {
constructor(el) {
this.prev = null;
this.el = el;
this.next = null;
}
}
class doubleLinkedList {
constructor() {
this.length = 0;
this.head = null;
this.tail = null;
}
append(el) {
let node = new Node(el);
if (this.isEmpty()) {
this.head = node;
this.tail = node;
} else {
node.prev = this.tail;
this.tail.next = node;
this.tail = node;
}
this.length++;
}
insert(el, pos) {
let node = new Node(el);
if (pos < 0 || pos > this.length) {
return new Error("Invalid pos");
}
if (pos === 0) {
if (this.isEmpty()) {
this.head = node;
this.tail = node;
} else {
node.next = this.head;
this.head.prev = node;
this.head = node;
}
} else if (pos === this.length) {
this.tail.next = node;
node.prev = this.tail;
this.tail = node;
} else {
let current = this.head;
let previos,
index = 0;
while (index++ < pos) {
previos = current;
current = current.next;
}
previos.next = node;
node.prev = previos;
current.prev = node;
node.next = current;
}
this.length++;
return true;
}
remove(el) {
let index = this.indexOf(el);
this.removeAt(index);
}
removeAt(pos) {
let current = this.head;
let previos,
index = 0;
if (pos < 0 || pos >= this.length) {
return new Error("Invalid pos");
}
if (pos === 0) {
this.head = current.next;
if (this.length === 1) {
this.tail = null;
} else {
this.head.prev = null;
}
} else if (pos === this.length - 1 ) {
current = this.tail;
previos = current.prev;
previos.next = null;
this.tail = previos;
} else {
while (index++ < pos) {
previos = current;
current = current.next;
}
previos.next = current.next;
current.next.prev = previos;
}
this.length--;
return current.el;
}
indexOf(el) {
let index = 0;
let current = this.head;
while (current) {
if (current.el === el) {
return index;
}
current = current.next;
index++;
}
return -1;
}
getHead() {
return this.head;
}
toString() {
let current = this.head;
let str = "";
while (current) {
str += current.el;
current = current.next;
}
return str;
}
size() {
return this.length;
}
isEmpty() {
return this.length === 0;
}
}
五、集合
- 集合是由一组无序且唯一(即不能重复)的项组成。
5.1、实现
class Set {
constructor() {
this.set = {};
}
has(item) {
return this.set.hasOwnProperty(item);
}
clear() {
this.set = {};
}
add(...arg) {
arg.forEach((item) => {
if (this.has(item)) {
return false;
} else {
this.set[item] = item;
return true;
}
});
}
remove(item) {
if (this.has(item)) {
delete this.set[item];
return true;
} else {
return false;
}
}
size() {
return Object.keys(this.set).length;
}
values() {
return Array.from(Object.keys(this.set));
}
}
5.2、集合操作的实现
//并集
union(s) {
let res = new Set();
let n1 = this.values();
let n2 = s.values();
res.add(...n1, ...n2);
return res;
}
//交集
intersection(s) {
let res = new Set();
let n1 = this.values();
let n2 = s.values();
let n3 = n1.filter((val) => {
return n2.indexOf(val) > -1;
});
res.add(...n3);
return res;
}
//差集
difference(s) {
let res = new Set();
let n1 = this.values();
let n2 = s.values();
let n3 = n1.filter((val) => {
return n2.indexOf(val) === -1;
});
res.add(...n3);
return res;
}
//当前集合是否为另一个集合的子集
subset(s) {
let res = new Set();
let n1 = this.values();
let n2 = s.values();
return n1.every((val) => {
return n2.indexOf(val) > -1;
});
}
六、字典和散列表
6.1、实现字典
class Dictionary {
constructor() {
this.items = {};
}
has(item) {
return this.items.hasOwnProperty(item);
}
clear() {
this.item = {};
}
set(item, value) {
this.items[item] = value;
}
remove(item) {
if (this.has(item)) {
delete this.items[item];
return true;
}
return false;
}
get(item) {
return this.has(item) ? this.items[item] : undefined;
}
values() {
return Array.from(Object.values(this.items));
}
keys() {
return Array.from(Object.keys(this.items));
}
size() {
return this.keys().length;
}
getItems() {
return this.items;
}
}
6.2、散列表
实现简单HashTable类
class HashTable {
constructor() {
this.table = [];
}
loseloseHashCode(str) {
let n = 0;
for (let i = 0, len = str.length; i < len; i++) {
n += str.charCodeAt(i);
}
return n % 100; // 根据需要table的长度决定100位置的数
}
put(key, val) {
let hash = this.loseloseHashCode(key);
console.log(hash + " - " + key);
this.table[hash] = val;
}
get(key) {
let hash = this.loseloseHashCode(key);
return this.table[hash];
}
remove(key) {
let hash = this.loseloseHashCode(key);
this.table[hash] = undefined;
}
}
6.3、散列碰撞
- 不同的值在散列表中对应相同位置的时候,我们称这种情况为散列碰撞。若是不做处理,则后面的值会覆盖前面的值。解决散列碰撞的常用方法有:线性探测法(寻址法)、再哈希法、拉链法、建立一个公共溢出区。
线性探测法
- 当发生碰撞时,检测下一个位置是否为空。如果为空,就将此数据存入该位置;如果不为空,则会继续检查下一个位置,直到找到下一个空的位置为止。
class XXHashTable {
constructor() {
this.keys = [];
this.values = [];
}
loseloseHashCode(str) {
let hash = 0;
for (let i = 0, len = str.length; i < len; i++) {
hash += str.charCodeAt(i);
}
return hash % 37;
}
put(key, value) {
let pos = this.loseloseHashCode(key);
while (this.keys[pos] !== undefined) {
pos++;
}
console.log(pos + " - " + key);
this.keys[pos] = key;
this.values[pos] = value;
}
get(key) {
let pos = this.loseloseHashCode(key);
while (this.keys[pos] !== key && this.keys[pos] !== undefined) {
pos++;
}
return this.values[pos];
}
remove(key) {
let pos = this.loseloseHashCode(key);
while (this.keys[pos] !== key && this.keys[pos] !== undefined) {
pos++;
}
this.values[pos] = undefined;
}
}
再哈希法
- 当发生冲突时,使用第二个、第三个哈希函数计算地址,直到无冲突为止。缺点:计算时间增加。
建立一个公共溢出区
- 假设哈希函数的值域为
[0, m-1]
,则设向量HashTable[0, m-1]
为基本表,另外设立存储空间向量OverTable[0…v]
用以存储发生冲突的记录。
拉链法
- 将所有关键字的哈希值相等的记录存储在同一线性链表中
class LLItem {
constructor(key, value) {
this.key = key;
this.value = value;
}
}
class LLHashTable {
constructor() {
this.table = [];
}
loseloseHashCode(key) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash += key.charCodeAt(i);
}
return hash % 37; //37为一般定义为table的长度,余数结果就是0-36
}
put(key, value) {
let pos = this.loseloseHashCode(key);
if (this.table[pos] === undefined) {
this.table[pos] = new LinkedList();
}
this.table[pos].append(new LLItem(key, value));
}
get(key) {
let pos = this.loseloseHashCode(key);
if (this.table[pos] !== undefined) {
let current = this.table[pos].getHead();
while (current) {
if (current.el.key === key) {
return current.el.value;
}
current = current.next;
}
}
return undefined;
}
remove(key) {
let pos = this.loseloseHashCode(key);
if (this.table[pos] !== undefined) {
let current = this.table[pos].getHead();
while (current) {
if (current.el.key === key) {
this.table[pos].remove(current.el);
return true;
}
current = current.next;
}
}
return false;
}
}
6.4、 性能
散列表性能受以下因素影响
1、装填因子: 已装填的数据/总容量
,也就是空余位置越多,发生冲突可能性越小,性能越好;一般设置为0.75,是性能和空间的一个折中,当达到0.75时,哈希表容量自动扩容。
2、良好的散列函数: 让数据中的值成均匀分布,尽量不要扎堆,使数据查找和删除时间复杂度为O(1)和O(n)。
6.5、 HashTable类与HashMap的区别
七、树
7.1、术语
- 根节点、叶子结点
- 父节点、祖父节点、曾祖父节点、子节点、孙子节点、曾孙节点
- 内部节点、外部节点
- 深度:节点的深度取决于它的祖先节点的数量
- 树的高度:取决于所有节点深度的最大值
7.2、二叉树和二叉搜索树:
- 二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。
- 二叉搜索树(BST)是二叉树的一种,但是它只允许你在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大(或者等于)的值。
7.2.1 二叉搜索树的创建
class Node {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
function minNode(node) {
while (node.left) {
node = node.left;
}
return node;
}
class BinarySearchTree {
constructor() {
this.root = null;
}
insert(value) {
let node = new Node(value);
if (!this.root) {
this.root = node;
} else {
this.insertNode(this.root, node);
}
}
insertNode(node, newNode) {
if (newNode.value < node.value) {
if (!node.left) {
node.left = newNode;
} else {
this.insertNode(node.left, newNode);
}
} else {
if (!node.right) {
node.right = newNode;
} else {
this.insertNode(node.right, newNode);
}
}
}
search(value) {
return this.searchNode(this.root, value);
}
searchNode(node, value) {
if (!node) {
return false;
}
if (value > node.value) {
return this.searchNode(node.right, value);
} else if (value < node.value) {
return this.searchNode(node.left, value);
} else {
return true;
}
}
min() {
let node = this.root;
if (!node) {
return null;
}
while (node.left) {
node = node.left;
}
return node.value;
}
max() {
let node = this.root;
if (!node) {
return null;
}
while (node.right) {
node = node.right;
}
return node.value;
}
inOrderTraverse() {
let res = [];
this.inOrder(this.root, res);
return res;
}
inOrder(node, res) {
if (!node) {
return;
}
this.inOrder(node.left, res);
res.push(node.value);
this.inOrder(node.right, res);
}
preOrderTraverse() {
let res = [];
this.preOrder(this.root, res);
return res;
}
preOrder(node, res) {
if (!node) {
return;
}
res.push(node.value);
this.preOrder(node.left, res);
this.preOrder(node.right, res);
}
postOrderTraverse() {
let res = [];
this.postOrder(this.root, res);
return res;
}
postOrder(node, res) {
if (!node) {
return;
}
this.postOrder(node.left, res);
this.postOrder(node.right, res);
res.push(node.value);
}
remove(value) {
return this.removeNode(this.root, value);
}
removeNode(node, value) {
if (!node) {
return null;
}
if (value > node.value) {
node.right = this.removeNode(node.right, value);
} else if (value < node.value) {
node.left = this.removeNode(node.left, value);
} else {
if (node.left === null && node.right === null) {
node = null;
return node;
} else if (node.left === null) {
node = node.right;
return node;
} else if (node.right === null) {
node = node.left;
return node;
} else {
let min = minNode(node.right);
node.value = min.value;
this.removeNode(node.right, min.value);
return node;
}
}
return node;
}
}
7.3、平衡二叉树
- 平衡二叉搜索树又被称为AVL树(有别于AVL算法),且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。二叉树的常用实现方法有:AVL、红黑树等。
- 平衡因子: 左子树减去右子树的高度,所以平衡二叉树的平衡因子取值只有:1、0、-1。
- 作用: 一般的二叉搜索树,其期望高度为(log2(n)),其各种操作的时间复杂度O(log2(n))也由此决定。但是,在某些极端情况下(如插入的序列是有序的),二叉搜索树将退化成类似的单链表,此时,其操作的时间复杂度将退化成线性的,即O(n)。在平衡二叉搜索树中,其高度一般都良好的维持在了O(log2(n)),大大降低了操作的时间复杂度。一棵好的平衡二叉树应该容易维护,也就是说,在做数据项的插入或删除操作时,为平衡树所做的一些辅助操作时间开销为O(1)。
- 调整方法
1、 插入点位置必须满足二叉查找树的性质,即任意一棵子树的左节点都小于根节点,右节点大于根节点。
2、 找出插入节点后,对不平衡的最小二叉树进行调整,如果是整个树不平衡,才进行整个树的调整。 - 调整方式
(1) LL型:新节点插入位置为左子树的左孩子,需要进行右旋(向右旋转)
(2) RR型:新节点插入位置为右子树的右孩子,需要进行左旋(向左旋转)
(3) LR型: 新节点插入位置为左子树的右孩子,要进行两次旋转,先左旋转,再右旋转;第一次最小不平衡子树的根节点先不动,调整插入节点所在子树,使之形成LL型树;第二次再调整LL型树,形成平衡树。
4) RL型: 插入位置为右子树的左孩子,进行两次调整,先右旋转再左旋转;第一次最小不平衡子树的根节点先不动,调整插入节点所在子树,使之形成RR型树;第二次再调整RR型树,形成平衡树。
7.4、红黑树
7.4.1、概念
- 红黑树是一棵二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是RED或BLACK。通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出两倍,因而是近似于平衡的。
- 树的每个结点包含5个属性:color、key、left、right、p(父结点)。如果一个结点没有子节点或父节点,则该结点相应指针属性的值为NIL,我们可以把这些NIL视为指向二叉搜索树的叶结点(外部结点)的指针,而把带关键字的节点视为树的内部结点。
- 一棵红黑树是满足以下性质的二叉搜索树:
1、每个节点或是红色的,或是黑色的;
2、根节点是黑色的;
3、每个叶结点(NIL)是黑色的;
4、如果一个结点是红色的,则它的两个子结点是黑色的;
5、对每个结点,从该结点到其所有后代叶结点的所有路径上,均包含相同数目的黑色结点。
7.5、表示法
- 双亲表示法
- 孩子表示法
- 孩子兄弟表示法
7.6、其他树
- B-tree
- B+tree
- B*tree
- R-tree
八、图
8.1、概念
- 有向图
- 无向图
- 简单图
简单图满足以下两条内容:
1)不存在重复边
2)不存在顶点到自身的边 - 完全图
无向图中任意两点之间都存在边,称为无向完全图。
有向图中任意两点之间都存在方向向反的两条弧,称为有向完全图; - 子图
- 连通、连通图、连通分量
- 强连通图、强连通分量
- 生成树和生成森林
- 顶点的度、入度和出度
对于无向图,顶点的边数为度,度数之和是顶点边数的两倍。
对于有向图,入度是以顶点为终点,出度相反。有向图的全部顶点入度之和等于出度之和且等于边数。顶点的度等于入度与出度之和。
注意: 入度与出度是针对有向图来说的。 - 边的权和网
- 路径、路径长度和回路
两顶点之间的路径指顶点之间经过的顶点序列,经过路径上边的数目称为路径长度。若有n
个顶点,且边数大于n-1
,此图一定有环。 - 简单路径、简单回路
顶点不重复出现的路径称为简单路径。
除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。 - 距离
若两顶点存在路径,其中最短路径长度为距离。 - 有向树
有一个顶点的入度为0,其余顶点的入度均为1的有向图称作有向树。
8.2、实现
class Graph {
constructor() {
this.verticies = [];
this.eages = new Dictionary();
}
addVertex(dot) {
this.verticies.push(dot);
this.eages.set(dot, []);
}
addEdge(a, b) {
if (
this.verticies.indexOf(a) === -1 ||
this.verticies.indexOf(b) === -1
) {
return new Error("the dots is not all in graph");
}
this.eages.get(a).push(b); //根据有向图无向图更改
this.eages.get(b).push(a);
}
toString() {
let res = "";
this.verticies.forEach((value) => {
res += value + "->";
let eages = this.eages.get(value);
eages.forEach((value) => {
res += value + ",";
});
res += "\n";
});
return res;
}
}
8.3、遍历
8.3.1、深度优先
- 类似于树的前序遍历
function Traver(matrix) {
let dotNum = matrix.length;
let dots = new Array(dotNum);
dots.fill(false); //初始化每个点,均为访问过
for (let i = 0; i < dotNum; i++) {
if (dots[i] === false) {
dots[i] = true;
console.log("--", i);
DFS(i);
}
}
function DFS(i) {
console.log(i);
for (let j = 0; j < dotNum; j++) {
if (
matrix[i][j] !== 0 &&
matrix[i][j] !== 65535 &&
dots[j] === false
) {
dots[j] = true;
DFS(j);
}
}
}
}
8.3.2、广度优先
- 其从图中某顶点v出发,访问了v之后一次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,且先被访问的顶点的邻接点先于后被访问的顶点的邻接点,直至图中所有顶点都被访问。
function Traver(matrix) {
let dotNum = matrix.length;
let dots = new Array(dotNum);
dots.fill(false);
let currentDot;
let quequ = [];
for (let i = 0; i < dotNum; i++) {
if (dots[i] === false) {
dots[i] = true;
quequ.push(i);
while (quequ.length > 0) {
currentDot = quequ.shift();
console.log("V" + currentDot);
for (let j = 0; j < dotNum; j++) {
if (
matrix[currentDot][j] !== 0 &&
matrix[currentDot][j] !== 65535 &&
dots[j] === false
) {
quequ.push(j);
dots[j] = true;
}
}
}
}
}
}
8.4、最小生成树
8.4.1、原理
- MST性质:假设
N=(V,{E})
是一个连通网,U是顶点集V的一个非空子集,如果(u,v)
是一条具有最小权值的边,其中u属于U,v属于V-U
,则必定存在一颗包含边(u,v)
的最小生成树
8.4.2、普里姆算法—Prim算法
- 算法思路
首先从图中的一个起点a开始,把a加入U集合,然后,寻找从与a有关联的边中,权重最小的那条边并且该边的终点b在顶点集合:(V-U)
中,将b加入到集合U中;然后,寻找与a关联和b关联的边中,权重最小的那条边c并且该边的终点在集合:(V-U)
中,把c加入到集合U中。一次类推,直到所有顶点都加入到了集合U。 - 实现
let matrix = [
//邻接矩阵
[0, 10, 65535, 65535, 65535, 11, 65535, 65535, 65535],
[10, 0, 18, 65535, 65535, 65535, 16, 65535, 12],
[65535, 65535, 0, 22, 65535, 65535, 65535, 65535, 8],
[65535, 65535, 22, 0, 20, 65535, 65535, 16, 21],
[65535, 65535, 65535, 20, 0, 26, 65535, 7, 65535],
[11, 65535, 65535, 65535, 26, 0, 17, 65535, 65535],
[65535, 16, 65535, 65535, 65535, 17, 0, 19, 65535],
[65535, 65535, 65535, 16, 7, 65535, 19, 0, 65535],
[65535, 12, 8, 21, 65535, 65535, 65535, 65535, 0],
];
class Graph {
constructor() {
this.dots = []; //存储点的表
this.edges = []; //存储边的表,即邻接矩阵
this.dotNum = 0; //点数
this.edgeNum = 0; //边数
}
createGraph(dotNum, edgeNum, matrix) { //初始化表的点数,边数和邻接矩阵
this.dotNum = dotNum;
this.edgeNum = edgeNum;
for (let i = 0; i < dotNum; i++) {
this.dots[i] = "V" + i;
this.edges[i] = [];
for (let j = 0; j < dotNum; j++) {
this.edges[i][j] = matrix[i][j];
}
}
}
miniTree() { //生成最小树
let res = "";
let lowDot = []; //存储最小权边的另一个点
let lowEdge = []; //存储当前下标点所连接的最小权的边
//从V0开始生成树,进行初始化
for (let i = 0; i < this.dotNum; i++) {
lowDot[i] = 0;
lowEdge[i] = this.edges[0][i];
}
console.log(this.dotNum);
for (let i = 1; i < this.dotNum; i++) {
//最小树需要n-1条边,所以进行n-1次循环,每次找一条边
let min = 65535;
let k = 0;
for (let j = 0; j < this.dotNum; j++) {
//找出当前最小权的边
if (lowEdge[j] !== 0 && lowEdge[j] < min) {
min = lowEdge[j];
k = j;
}
}
//此时k表示当前最小权边的点
res += `${this.dots[lowDot[k]]}-->${this.dots[k]} in ${
lowEdge[k]
} \n`;
lowEdge[k] = 0;
for (let j = 0; j < this.dotNum; j++) { //更新每个点的最小权边和最小权边的另一个点
if (lowEdge[j] > this.edges[k][j]) {
lowEdge[j] = this.edges[k][j];
lowDot[j] = k;
}
}
}
return res;
}
}
8.4.3、克鲁斯卡算法
- 算法思路
(1)将图中的所有边都去掉。
(2)将边按权值从小到大的顺序添加到图中,保证添加的过程中不会形成环
(3)重复上一步直到连接所有顶点,此时就生成了最小生成树。这是一种贪心策略。 - 实现
let matrix = [
//邻接矩阵
[0, 10, 65535, 65535, 65535, 11, 65535, 65535, 65535],
[10, 0, 18, 65535, 65535, 65535, 16, 65535, 12],
[65535, 65535, 0, 22, 65535, 65535, 65535, 65535, 8],
[65535, 65535, 22, 0, 20, 65535, 65535, 16, 21],
[65535, 65535, 65535, 20, 0, 26, 65535, 7, 65535],
[11, 65535, 65535, 65535, 26, 0, 17, 65535, 65535],
[65535, 16, 65535, 65535, 65535, 17, 0, 19, 65535],
[65535, 65535, 65535, 16, 7, 65535, 19, 0, 65535],
[65535, 12, 8, 21, 65535, 65535, 65535, 65535, 0],
];
class Edge {
constructor(i, j, cost) {
this.begin = i;
this.end = j;
this.cost = cost;
}
getCost() {
return this.cost;
}
getBegin() {
return this.begin;
}
getEnd() {
return this.end;
}
}
function changeMatrixToEdgeArray(matrix) {
let res = [];
const rows = matrix.length;
const cols = rows;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (matrix[i][j] !== 0 && matrix[i][j] !== 65535) {
res.push(new Edge(i, j, matrix[i][j]));
matrix[i][j] = 65535;
matrix[j][i] = 65535;
}
}
}
return res.sort((a, b) => a.getCost() - b.getCost());
}
function kruskal(matrix) {
let edges = changeMatrixToEdgeArray(matrix);
let way = new Array(matrix.length).fill(0); //存储路径
let res = [];
for (let i = 0; i < edges.length; i++) {
let edge = edges[i];
let n = findEnd(way, edge.getBegin());
let m = findEnd(way, edge.getEnd());
console.log(way, n, m);
if (n !== m) {
//此时无环
res.push(edge);
way[n] = m;
}
}
return res;
}
function findEnd(arr, start) {
//寻找当前点所在路径的终点
while (arr[start]) {
start = arr[start];
}
return start;
}
console.log("result=", kruskal(matrix));
九、算法
9.1、排序算法
9.1.1、冒泡排序
function bubbleSort(arr) {
let len = arr.length;
for (let i = 0; i < len - 1; i++) {
for (let j = 0; j < len - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
let temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
- 未借用辅助数组,所以空间复杂度为:
O(1)
- 在待排序数组已经是有序数组的情况下,时间复杂度有最好情况,只需数组整体的一次冒泡,最好时间复杂度为:
O(n)
- 平均时间复杂度为:
O(n^2)
- 冒泡排序是稳定的排序
- 待排序数组均需比较的情况下,时间复杂度为
(n+(n-1)+(n-2)+…+2+1)=n*(n+1)/2
,所以最差情况下时间复杂度为:O(n^2)
9.1.2、快速排序
- 需要辅助数组
function quickSort(arr) {
if (arr.length <= 1) {
return arr;
}
let right = [];
let left = [];
let middle = Math.floor(arr.length / 2);
let middleValue = arr[middle];
arr.splice(middle, 1);
for (let i = 0, len = arr.length; i < len; i++) {
if (arr[i] > middleValue) {
right.push(arr[i]);
} else {
left.push(arr[i]);
}
}
return quickSort(left).concat(middleValue, quickSort(right));
}
- 不需要辅助数组
function quickSort(arr, start, end) {
if (start < end) {
let pivot = arr[start];
let low = start;
let high = end;
while (low < high) {
while (low < high && arr[high] >= pivot) {
high--;
}
arr[low] = arr[high];
while (low < high && arr[low] < pivot) {
low++;
}
arr[high] = arr[low];
}
arr[low] = pivot;
quickSort(arr, low + 1, end);
quickSort(arr, start, low - 1);
}
}
- 借用辅助数组时,空间复杂度为:O(nlog(n));
- 最好时间复杂度为:O(nlog(n));
- 在所有数都比哨兵大或小时,最差情况下时间复杂度为:O(n^2);
- 平均时间复杂度为:O(nlog(n))。
- 快速排序是非稳定排序。
9.1.3、插入排序
function insertSort(arr) {
for (let i = 1, len = arr.length; i < len; i++) {
if (arr[i] < arr[i - 1]) {
let j = i - 1;
let temp = arr[i];
while (j >= 0 && arr[j] > temp) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = temp;
}
}
}
- 未借用辅助数组,空间复杂度为:
O(1)
- 数组有序的情况下,最好时间复杂度为:
O(n)
- 最差情况下,时间复杂度为
(n+(n-1)+(n-2)+…+2+1)=n*(n+1)/2
,所以最差情况下时间复杂度为:O(n^2)
- 平均时间复杂度为:
O(n^2)
- 稳定排序。
9.1.4、Shell排序
function shellSort(arr) {
let len = arr.length;
let gap = Math.floor(len / 2);
let temp;
while (gap >= 1) {
for (let i = gap; i < len; i++) {
temp = arr[i];
let j = i - gap;
for (; j >= 0, arr[j] > temp; j = j - gap) {
arr[j + gap] = arr[j];
}
arr[j + gap] = temp;
}
gap = Math.floor(gap / 2);
}
return arr;
}
- 未借用辅助数组,空间复杂度为:
O(1)
- 数组有序的情况下,最好时间复杂度为:
O(n)
- 最差情况下时间复杂度为:
O(n^2)
- 平均时间复杂度为:
O(n^1.3)
- 不稳定排序(分组后打乱排序)
9.1.5、直接选择排序
function selectSort(arr) {
let len = arr.length;
for (let i = 0; i < len; i++) {
let minIndex = i;
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) minIndex = j;
}
if (minIndex !== i) {
let temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
- 未借用辅助数组,空间复杂度为:
O(1)
- 时间复杂度统一为为:
O(n^2)
- 由于在直接选择排序中存在着不相邻元素之间的互换,因此,直接选择排序是一种不稳定的排序方法
9.1.6、堆排序
//交换辅助函数
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//核心,递归排序堆
function heapSort(arr, i, len) {
for (let j = i * 2 + 1; j < len; j = j * 2 + 1) {
if (j + 1 < len && arr[j + 1] > arr[j]) {
j++;
}
if (arr[i] < arr[j]) {
swap(arr, i, j);
i = j;
}
}
}
function sort(arr) {
//初始化最大堆
for (let i = Math.floor(arr.length / 2 - 1); i >= 0; i--) {
heapSort(arr, i, arr.length);
}
//将每次排序好的最大数放到数组最后
for (let i = arr.length - 1; i > 0; i--) {
swap(arr, 0, i);
heapSort(arr, 0, i);
}
return arr;
}
- 未借用辅助数组,空间复杂度为:
O(1)
- 时间复杂度统一为为:
O(nlogn)
- 不稳定排序
9.1.7、归并排序
function merge(left, right) {
let res = [];
while (left.length > 0 && right.length > 0) {
if (left[0] < right[0]) {
res.push(left.shift());
} else {
res.push(right.shift());
}
}
return res.concat(left).concat(right);
}
function mergeSort(arr) {
if (arr.length < 2) {
return arr;
}
let middle = Math.floor(arr.length / 2);
let left = arr.slice(0, middle);
let right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
- 借用辅助数组,空间复杂度为:
O(n)
- 时间复杂度统一为为:
O(nlogn)
- 稳定排序
9.1.8、桶排序
- 计算并设置固定数量的空桶
- 将数据放入对应的桶中
- 对桶中的数据进行排序
- 把每个桶的数据进行合并
9.1.9、基数排序
function radixSort(arr) {
let d = Math.max.apply(null, arr).toString().length;
let n = 1;
let base = [];
let baseNum = [];
for (let i = 0; i < 9; i++) {
base[i] = [];
baseNum[i] = 0;
}
while (d--) {
for (let i = 0; i < arr.length; i++) {
let index = Math.floor(arr[i] / n) % 10;
base[index][baseNum[index]] = arr[i];
baseNum[index]++;
}
let k = 0;
for (let i = 0; i < 9; i++) {
if (baseNum[i] !== 0) {
for (let j = 0; j < baseNum[i]; j++) {
arr[k++] = base[i][j];
}
baseNum[i] = 0;
}
}
n *= 10;
}
}
9.1.10、计数排序
function blukeSort(arr) {
let bluke = [];
let res = [];
let len = arr.length;
for (let i = 0; i < len; i++) {
//装好桶
if (bluke[arr[i]] === undefined) {
bluke[arr[i]] = 1;
} else {
bluke[arr[i]]++;
}
}
for (let i = 0; i < bluke.length; i++) {
while (bluke[i] >= 1) {
res.push(i);
bluke[i]--;
}
}
return res;
}
9.2、复杂度计算
9.2.1、常见的算法时间复杂度由小到大
Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)<…<Ο(2n)<Ο(n!)
9.2.2、计算算法时间复杂度时几个简单的程序分析法则
(1) 对于一些简单的输入输出语句或赋值语句,近似认为需要O(1)
时间
(2) 对于顺序结构,需要依次执行一系列语句所用的时间可采用大O下"求和法则"
求和法则:是指若算法的2个部分时间复杂度分别为 T1(n)=O(f(n))
和 T2(n)=O(g(n)),则 T1(n)+T2(n)=O(max(f(n), g(n)))
特别地:若T1(m)=O(f(m)), T2(n)=O(g(n)),
则 T1(m)+T2(n)=O(f(m) + g(n))
(3) 对于选择结构,如if语句,它的主要时间耗费是在执行then
字句或else
字句所用的时间,需注意的是检验条件也需要O(1)
时间
(4) 对于循环结构,循环语句的运行时间主要体现在多次迭代中执行循环体以及检验循环条件的时间耗费,一般可用大O下"乘法法则"
乘法法则: 是指若算法的2个部分时间复杂度分别为 T1(n)=O(f(n))
和 T2(n)=O(g(n))
,则 T1*T2=O(f(n)*g(n))
(5) 对于复杂的算法,可以将它分成几个容易估算的部分,然后利用求和法则和乘法法则技术整个算法的时间复杂度
- 另外还有以下2个运算法则
(1) 若g(n)=O(f(n))
,则O(f(n))+ O(g(n))= O(f(n))
(2)O(Cf(n)) = O(f(n))
,其中C是一个正常数
常见算法时间和空间复杂度
- PS:归并排序空间复杂度为
O(n)
9.3、搜索算法
9.3.1、顺序查找
- 该算法的时间复杂度为
O(n)
9.3.2、二分查找
- 最优时间复杂度:
O(1)
- 最坏时间复杂度:
O(logn)
function binarySearch(arr, star, end, target) {
if (star > end) {
return false;
}
let middle = Math.floor((star + end) / 2);
if (target > arr[middle]) {
return binarySearch(arr, middle + 1, end, target);
} else if (target < arr[middle]) {
return binarySearch(arr, 0, middle - 1, target);
} else {
return middle;
}
}
9.3.4、分块查找
- 要求
1、将n个数据元素"按块有序"划分为m块(m ≤ n)
2、每一块中的结点不必有序,但块与块之间必须"按块有序"
3、先选取各块中的最大关键字构成一个索引表 - 查找分两个部分
1、先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;
2、在已确定的块中用顺序法进行查找。 - 时间复杂度:
O(log(m)+N/m)
9.3.5、哈希查找
- 哈希查找的操作步骤
1、用给定的哈希函数构造哈希表;
2、根据选择的冲突处理方法解决地址冲突;
3、在哈希表的基础上执行哈希查找。 - 建立哈希表操作步骤
1、step1 取数据元素的关键字key,计算其哈希函数值(地址)。若该地址对应的存储空间还没有被占用,则将该元素存入;否则执行step2解决冲突。
2、step2 根据选择的冲突处理方法,计算关键字key的下一个存储地址。若下一个存储地址仍被占用,则继续执行step2,直到找到能用的存储地址为止。 - 哈希查找步骤为
1、 Step1 对给定k值,计算哈希地址Di=H(k)
;若HST
为空,则查找失败;若HST=k
,则查找成功;否则,执行step2(处理冲突)。
2、 Step2 重复计算处理冲突的下一个存储地址Dk=R(Dk-1)
,直到HST[Dk]
为空,或HST[Dk]=k
为止。若HST[Dk]=K
,则查找成功,否则查找失败。
9.3.6、斐波那契查找
9.3.7、二叉树查找(BST)
9.3.8、插值查找
- 原理
mid=low+(key-a[low])/(a[high]-a[low])*(high-low)
,也就是将比例参数1/2改进为自适应的,根据关键字在整个有序表中所处的位置,让mid
值的变化更靠近关键字key
,这样也就间接地减少了比较次数。 - 作用
对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。
function binarySearch(arr, star, end, target) {
if (star > end || arr[star] > target || arr[end] < target) {
//防止循环爆栈
return false;
}
let middle = Math.floor(
star + ((target - arr[star]) / (arr[end] - arr[star])) *
(end - star)
);
if (target > arr[middle]) {
return binarySearch(arr, middle + 1, end, target);
} else if (target < arr[middle]) {
return binarySearch(arr, 0, middle - 1, target);
} else {
return middle;
}
}
9.3.9、其他查找
- B树查找
- 红黑树查找(平衡查找树)
- B+树查找(平衡查找树)
- 2-3树(平衡查找树)
9.4、动态规划
9.4.1、三角形路径
- 题目: 在上面的数字三角形中寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或 右下走。只需要求出这个最大和即可,不必给出具体路径。 三角形的行数大于1小于等于100,数字为 0 - 99
- 第一版:简单递归
let arr = [[7], [3, 8], [8, 1, 0], [2, 7, 4, 4], [4, 5, 2, 6, 5]];
function find(arr) {
let len = arr.length;
function max(i, j) {
if (i === len - 1) {
return arr[i][j];
}
return Math.max(max(i + 1, j), max(i + 1, j + 1)) + arr[i][j];
}
return max(0, 0);
}
console.log(find(arr));
- 第二版:添加辅助数组,减少重复运算
let arr = [[7], [3, 8], [8, 1, 0], [2, 7, 4, 4], [4, 5, 2, 6, 5]];
function find(arr) {
let len = arr.length;
let temp = [];
for (let i = 0; i < len; i++) {
temp[i] = [];
for (let j = 0, l = arr[i].length; j < l; j++) {
temp[i][j] = -1;
}
}
function max(i, j) {
if (temp[i][j] !== -1) {
return temp[i][j];
}
if (i === len - 1) {
return arr[i][j];
}
temp[i][j] = Math.max(max(i + 1, j), max(i + 1, j + 1)) + arr[i][j];
return temp[i][j];
}
return max(0, 0);
}
console.log(find(arr));
- 第三版:递归改成递推(可以再优化temp数组为一维或直接用arr)
let arr = [[7], [3, 8], [8, 1, 0], [2, 7, 4, 4], [4, 5, 2, 6, 5]];
function find(arr) {
let len = arr.length;
let temp = [];
for (let i = 0; i < len; i++) {
temp[i] = [];
}
for (let i = 0, j = arr[len - 1].length; i < j; i++) {
temp[len - 1][i] = arr[len - 1][i];
}
for (let i = len - 2; i >= 0; i--) {
for (let j = 0, l = arr[i].length; j < l; j++) {
temp[i][j] =
Math.max(temp[i + 1][j], temp[i + 1][j + 1]) + arr[i][j];
}
}
console.log(temp);
return temp[0][0];
}
console.log(find(arr));
9.4.2、背包问题
function bag(arr, total) {
let res = [];
let len = arr.length;
for (let i = 0; i < len; i++) {
res[i] = [];
for (let j = 0; j < total; j++) {
if (i === 0) {
if (arr[i].weight <= j + 1) {
res[i][j] = arr[i].value;
} else {
res[i][j] = 0;
}
} else {
if (arr[i].weight > j + 1) {
res[i][j] = res[i - 1][j];
} else if (arr[i].weight === j + 1) {
res[i][j] = Math.max(res[i - 1][j], arr[i].value);
} else {
res[i][j] = Math.max(
res[i - 1][j],
arr[i].value + res[i - 1][j - arr[i].weight]
);
}
}
}
}
return res[len - 1][total - 1];
}
9.5、贪心算法
9.5.1、加油站问题
- 思路:找到汽车满油量时可以行驶的最大路程范围内的最后一个加油站,加油后则继续用此方法前进。需要检查每一小段路程是否超过汽车满油量时的最大支撑路程。
const CAN_NOT_REACH = new Error("can not reach end");
function greepy(arr, max) {
//arr为每两个加油站之间距离组成的数组,第一数为第一个加油站到起点距离,
最后一个数为最后一个加油站到终点距离
let flag = arr.every((val) => {
return max >= val;
});
if (!flag) {
return CAN_NOT_REACH;
}
let temp = 0;
let count = 0;
for (let i = arr.length - 1; i >= 0; i--) {
temp += arr[i];
if (temp > max) {
count++;
temp = arr[i];
}
}
return count;
}
9.5.2、找零钱问题
- 每一步尽可能用面值大的纸币
function change(arr, want) {
arr.sort((n1, n2) => {
return n2.value - n1.value;
});
let temp = 0;
let res = [];
for (let i = 0, len = arr.length; i < len; i++) {
while (arr[i].count > 0) {
if (temp + arr[i].value <= want) {
res.push(arr[i].value);
temp += arr[i].value;
arr[i].count--;
} else {
break;
}
}
}
if (temp !== want) {
return new Error("can figure it out");
}
return res;
}
9.5.3、过河问题
- 先将所有人过河所需的时间按照升序排序。将整个过程拆分为每次送两个最耗时的人过去。有两种方式:
1.最快的和次快的过河,然后最快的将船划回来;次慢的和最慢的过河,然后次快的将船划回来,所需时间为:t[0]+2*t[1]+t[n-1]
2.最快的和最慢的过河,然后最快的将船划回来,最快的和次慢的过河,然后最快的将船划回来,所需时间为:2*t[0]+t[n-2]+t[n-1]
- 当人数为三人时,时间总是三人时间之和,两人时,为较耗时那个人的时间
function river(arr) {
let time = 0;
arr.sort((n1, n2) => n1 - n2);
while (arr.length > 3) {
if (
arr[arr.length - 1] + arr[arr.length - 2] + 2 * arr[0] >
arr[arr.length - 1] + arr[0] + 2 * arr[1]
) {
time += arr[arr.length - 1] + arr[0] + 2 * arr[1];
} else {
time += arr[arr.length - 1] + arr[arr.length - 2] +
2 * arr[0];
}
arr.pop();
arr.pop();
}
if(arr.length===3){
time += arr[0] + arr[1] + arr[2];
}else{
time += arr[1]
}
return time;
}
9.5.4、活动选择
- 每次选取结束时间最早的活动
function activity(arr) {
arr.sort((n1, n2) => n1.end - n2.end);
let res = [];
let temp = 0; //记录当前结束时间
let len = arr.length;
for (let i = 0; i < len; i++) {
if (arr[i].begin >= temp) {
res.push(arr[i].name);
temp = arr[i].end;
}
}
return res;
}