JS数据结构与算法读书笔记

##数组
1.几乎所有的编程语言都原生支持数组类型,数组是最简单的内存数据结构
2.创建和初始化数组
let daysOfWeek = [];
3.访问元素和迭代元素
for (let i = 0;i < daysOfWeek;i++){
console.log(daysOfWeek[i]);
}
4.添加元素
numbers[numbers.length] = 10;
push:数组末尾添加
unshift:数组开头插入
js里,数组是一个可以修改的对象,会动态增长,在其他语言中,改变数组要创建一个全新的数组
5.删除元素
pop:删除数组最靠后的元素
shift:删除数组第一个元素
6.任意位置添加和删除
使用splice指定位置/索引
splice第一个参数表示要删除/插入的元素的索引值,第二个参数是删除的个数
第三个参数往后是需要添加进去的值
7.二维和多维数组
1.二维数组
数组嵌套数组
let averageTemp = [];
averageTemp[0] = [72,75,76];
averageTemp[1] = [81,54,56];
#迭代二维数组
function printMatrix(myMatrix){
for (let i = 0;i < mymatrix.length;i++){
for (let j = 0;j < myMatrix[i].length;j++){
console.log(myMatrix[i][j])
}
}
}
2.多维数组
同理创建更多的维度
8.JS数组方法
1.concat:连接2个或者更多数组,并返回结果
every:对每一个元素都运行指定函数,如果每一个元素都返回true,则返回true
filter:对每一个元素都运行指定函数,返回该函数会返回true的元素组成的数组
forEach:对每一个元素都运行指定函数,无返回值
join:将所有数组元素连接成一个字符串
indexOf:返回第一个与给定参数相等的元素的索引,木有则返回-1
lastIndexOf:返回在数组中搜索到的与给定参数相等的元素的最后一个元素的索引
map:对数组中的元素运行给定函数,返回每次函数调用的结果组成的数组
reverse:倒置
slice:传入索引值,将对应索引范围内的元素作为新数组返回
some:对每一个元素都运行指定函数,如果有一个元素返回true,则返回true
sort:按照字母顺序对数组进行排序,支持传入指定排序方法的函数作为参数
toString:作为字符串返回
valueOf:与toString类似
reduce:累加器
2.ES6新增数组方法
@@iterator:返回一个包含数组键值对的迭代对象,可以通过同步调用得到数组元素的键值对
copyWithin:赋值数组中一系列元素到同一数组指定的起始位置
entries:返回包含数组所有键值对的@@iterator
includes:如果数组中存在某个元素则返回true否则返回false
find:根据回调函数给定的条件从数组中查找元素,找到了就返回该元素
findIndex:根据回调函数给定的条件从数组中查找元素,找到了就返回索引
fill:用静态值填充数组
from:根据已有数组创建一个新数组
keys:返回包含数组所有索引的@@iterator
of:根据传入的参数创建一个新数组
values:返回包含数组中所有值的@@iterator
1.使用for…of循环迭代
for (const n of numbers){
console.log(n % 2 === 0 ? ‘even’ : ‘odd’);
}
2.使用@@iterator对象
ES5新增,需要Symbol.iterator访问
let iterator = numbersSymbol.iterator;
console.log(iterator.next().value);
当数组中所有值都迭代完后,返回undefined
3.数组的entries、keys、values
1.entries:返回包含键值对的@@iterator
let aEntries = numbers.entries();
console.log(aEntries.next().value);
2.keys:返回包含数组索引的@@iterator
一旦没有可以迭代的值,返回一个value为undefined,done为true的对象
3.values:返回包含数组的值
4.from
根据已有数组创建一个新数组,复制、过滤值
5.Array.of
根据传入的参数创建一个新数组
let numberCopy = Array.of(…numbers4)
(…)将数组的值全部展开成参数
6.fill
填充,第一个参数为填充值,第二个为开始填充索引,第三个参数为结束填充索引
7.copyWithin
复制一系列元素到指定位置
3.排序元素
function compare(a,b){
if (a<b){
return -1;
}
if (a>b){
return 1;
}
return 0;
}
numbers.sort(compare)
可以对任何对象类型的数组排序
在对字符串进行比较时,根据字符对应的ASCII来比较
4.搜索
5.类型数组
JS数组不是强类型,可以存储任何类型的数据
类型数组用于存储单一类型的数组,声明数组类型
##栈(stack)
有时需要能在删除或添加进行更多控制的数据结构
1.栈数据结构
栈遵从后进先出(LIFO)原则的有序集合,新添加或待删除的都保存在栈的同一端,称为栈顶,另一端叫栈底
新元素都靠近栈顶,就元素都靠近栈底
2.创建一个基于数组的栈
class Stack{
constructor(){
this.items = [];
}
}
栈方法
push:添加一个或几个新元素到栈顶
pop:移除栈顶的元素,返回被移除的元素
peek:返回栈顶的元素,不对栈做任何修改
isEmpty:栈里没有任何元素就返回true,否则返回false
clear:移除栈里的所有元素
size:返回栈的元素个数
1.push(element){//因为使用数组来保存栈里的元素,所有可以使用push方法
this.items.push(element);
}
2.pop(){
return this.items.pop();
}
3.peek(){
return this.items[this.items.length - 1];
}
4.isEmpty(){
return this.items.length === 0;
}
5.clear(){
this.items = [];
}
6.使用stack类
const stack = new Stack();
console.log(stack.isEmpty());
3.创建一个基于JS对象的stack类
1.声明一个stack类
class Stack{
constructor(){
this.count = 0;
this.items = {};
}
}
2.向栈插入元素
将使用count变量作为items对象的键名,插入的元素则是它的值
3.检验一个栈是否为空
通过检验count的值
4.从栈中弹出元素
pop(){
if (this.isEmpty()){
return undefined;
}
this.count–;
const result = this.items[this.count];
delete this.items[this.count];
return result;
}
5.查看栈顶的值并将栈清空
6.创建toString方法
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;
}
7.保护数据结构内部元素
希望保护内部的元素,只有我们暴露的方法才能修改内部结构
对于stack类来说,确保元素只会被添加到栈顶,而不是其他地方
8.使用JS实现私有属性的方法
一部分开发者喜欢使用下划线约定一格私有属性(并不是实现了私有属性)
1.用ES5的WeakMap实现类
有一种数据类型可以确保属性是私有的,WeakMap,可以存储键值对,键是对象,值可以使任何数据类型
声明一个WeakMap类型的变量items
const items = new WeakMap();
class Stack{
constructor(){
items.set(this, []);
}
}
9.用栈解决问题
##队列和双端队列
1.队列遵循先进先出(FIFO),队列在尾部添加新元素,并从顶部移除元素,新元素必须添加到队列末尾
2.创建队列
class Queue{
constructor(){
this.count = 0;
this.lowestCount = 0;
this.items = {};
}
}
首先需要一个存储队列中的元素的数据结构,声明一些队列可用的方法
enqueue():向队列尾部添加一个或多个新的项
dequeue:移除队列第一个元素,返回被移除的元素
peek:返回队列第一个元素,队列不做变化
isEmpty:检测队列是否为空
size:大小
1.向队列添加元素
enqueue(element){
this.items[this.count] = element;
this.count++;
}
2.从队列移除元素
dequeue(){
if (this.isEmpty()){
return undefined;
}
const result = this.items[this.lowestCount];
delete this.items[this.lowestCount];
this.lowestCount++;
return result;
}
3.查看队列头元素peek
4.检测队列是否为空并获取它的长度
5.清空队列:clear
6.创建toString方法
3.双端队列数据结构
双端队列:允许我们同时从前端和后端添加和移除元素的特殊队列
1.创建Deque类
class Deque{
constructor(){
this.count = 0;
this.lowestCount = 0;
this.items = 0;
}
}
1.方法
addFront:前端添加新元素
addBack:后端添加新元素
removeFront:前端移除新元素
removeBack:后端移除新元素
peekFront:返回前端第一个新元素
peekBack:返回后端第一个新元素
2.向双端对列的前端添加元素
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.lowestCount = 0;
this.items[0] = element;
}
}
3.击鼓传花 检测回文
##链表
数组大小固定,从数组的起点或中间插入或移除的成本很高,因为需要移动元素
链表动态数据结构,可以随意添加和移除项,按需进行扩容
1.链表数据结构
链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置
每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称为指针或链接)组成
数组:可以直接访问任何位置的任何元素
链表:访问链表中的元素,需要从起点(表头)开始迭代链表直到找到所需的元素
2.创建链表
import {defaultEquals} from ‘…/util’;
import {Node} from ‘./models/linked-list-models’;

        export default class LinkedList{
            constructor(equalsFn = defaultEquals){
                this.count = 0;
                this.head = undefined;
                this.equalsFn = equalsFn;
            }
        }
        注释:indexOf方法可以帮我们在链表找到一个特定的元素
             要比较链表中的元素是否相等,需要一个内部调用的函数,名为equalsFn
             head用来保存第一个元素的引用
             要保存链表中第一个以及其他元素,需要一个助手类,叫做Node
             Node表示我们想要添加到链表的项,包含一个element属性,该属性表示要加入链表元素的值
             以及一个next属性,还属性就是指向下一个元素的指针
        方法:
            push:链表尾部添加
            insert:链表插入
            getElementAt:返回特定位置的元素,不存在则返回undefined
            remove:链表移除
            indexOf:返回元素在链表的索引,不存在返回-1
            removeAt:从特定位置移除
            isEmpty:
            size:
            toString:
        1.push方法
            push(element){
                const node = new Node(element);传入element创建Node项
                let current;
                if (this.head == null){
                    this.head = node;
                }else{
                    current = this.head;
                    while(current.next != null){
                        current = current.next;
                    }
                    current.next = node;
                }
                this.count++;
            }
        2.removeAt/remove
            removeAt(index){
                if (index >= 0 && index < this.count){
                    let current = this.node;
                    if (index === 0){
                        this.head = current.next;
                    }else{
                        let previous;
                        for (let i = 0;i < index;i++){
                            previous = current;
                            current = current.next;
                        }
                        previous.next = current.next;
                    }
                    this.count--;
                    return current.element;
                }
                return undefined;
            }
        3.循环迭代链表直到目标位置
            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;
            }
        4.insert
            insert(element,index){
                if (index >= 0 && index <= this.count){
                    const node = new Node(elelment);
                    if (index === 0){
                        const current = this.head;
                        node.next = current;
                        this.head = node;
                    }else{
                        const previous = this.getElementAt(index - 1);
                        const current = previous.next;
                        node.next = current;
                        previous.next = node;
                    }
                    this.count++;
                    return true;
                }
                return false;
            }
        5.indexOf
            indexOf(element){
                let current = this.head;
                for (let i = 0;i < this.count && current != null;i++){
                    if (this.equalsFn(element,current.element)){
                        return i;
                    }
                    current = current.next;
                }
                return -1;
            }
        6.移除元素
            创建完indexOf方法,可以实现其他方法,比如remove
                remove(element){
                    const index = this.indexOf(element);
                    return this.removeAt(index);
                }
    3.双向链表
        链接是双向的,一个链向下一个元素,一个链向上一个元素,同时控制next和prev两个指针
    4.循环链表
        最后一个元素指向下一个元素的指针(tail.next)不是引用undefined,而是指向第一个元素(head)
    5.有序链表
        保持元素有序的链表,将元素插入到正确位置实现链表的有序性

##集合
一种不允许值重复的顺序数据结构
1.构建数据集合
集合由一组无序且唯一的项组成{}
2.构建集合类
class Set{
constructor(){
this.items = {};
}
}
方法:
add:添加新元素
delete:移除元素
has:元素是否在集合中
clear:清除所有元素
size:返回集合包含的数目
values:返回一个包含集合中所有值的数组
##字典和散列表
##递归
特殊的方法操作树和图数据结构,那就是递归
1.要理解递归,首先要理解递归
2.递归是解决问题的一种方法,解决问题的各个小部分开始,直到解决最后的大问题,通常涉及函数调用自身
3.每个递归函数都必须有基线条件,即一个不再递归调用的条件
4.迭代阶乘
function factorialIterative(number){
if (number < 0) return undefined;
let tatal = 1;
for (let n = number;n > 1;n–){
tatal = tatal * n;
}
return total;
}
console.log(factorialIterative(5));
5.递归阶乘
function factorial(n){
if (n === 1 || n === 0){
return 1;
}
return n * factorial(n - 1);
}
console.log(factorial(5));
6.调用栈
每当一个函数被一个算法调用时,该函数会进入调用栈的顶部,当使用递归的时候,每个函数调用都会堆叠在调用栈的顶部
每个调用都可能依赖前一个调用的结果
7.斐波那契数列
function fibonacci(n){
if (n < 1) return 0;
if (n <= 2) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
8.迭代比递归版本快得多,但是递归更容易理解,需要的代码也更少,解决问题更简单
##树
非顺序数据结构–树,对存储需要快速查找的数据非常有用
1.树数据结构
树结构包含一系列存在父子关系的节点,每个节点都有一个父节点(除了顶部的第一个节点),以及零个或多个子节点
1.根节点:位于树顶部的节点叫做根节点,没有父节点
2.树中每一个元素都叫做节点,节点分为内部节点和外部节点
1.内部节点:至少有一个子节点的节点
2.外部节点:没有子节点的称为外部节点或叶节点
3.一个节点可以有祖先和后代
4.子树:子树由节点和它的后代构成
5.深度:节点的深度取决于他的祖先节点的数量
6.高度:取决于所有节点深度的最大值,层级从0开始,根节点在第0层
2.二叉树和二叉搜索树
1.二叉树中的节点最多只能有两个子节点,一个左侧子节点,一个右侧子节点,有利于我们进行高效的进行查找、插入、删除节点
2.二叉搜索树(BST):是二叉树的一种,只允许在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大的值
3.创建BinarySearchTree类
export class Node{
constructor(key){
this.key = key;//节点值
this.left = null;//左侧子节点引用
this.right = null;//右侧子节点引用
}
}
通过指针来表示节点之间的关系(边),树中也使用两个指针,一个指向左侧子节点,一个指向右侧子节点
声明一个Node类来表示树中的每个节点,键是树中对节点的称呼
声明一个root来控制树的第一个节点
方法:
1.insert:向树中插入键
2.search:在树中查找键,有责返回true,无则返回false
3.inOrderTraverse:中序遍历所有节点
4.preOrderTraverse:先序遍历所有节点
5.postOrderTraverse:后序遍历所有节点
6.min:返回最小的值/键
7.max:返回最大的值/键
8.remove:移除键
4.向二叉搜索树插入一个键
import { Compare, defaultCompare } from ‘…/util’;
import {Node} from ‘./models/node’;
export default class BinarySearchTree{
constructor(compareFn = defaultCompare){
this.compareFn = compareFn;
this.root = null;
}
}
insert(key){
if (this.root == null){
this.root = new Node(key);
}else{
this.insertNode(this.root, key);
}
}
1.如果插入的是第一个节点,创建一个Node实例,并将它赋值给root,指向这个新节点
2.添加到根节点以外的其他位置,需要一个辅助方法
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.indertNode(node.right, key);
}
}
}
3.indertNode会帮助我们找到新节点应该插入的正确位置
1.传入树的根节点和要插入的节点
2.新节点的键小于根节点的键,那么需要检测左侧子节点,使用compareFn来比较值
如果没有左侧子节点,就在那插入新节点,如果有子节点,就递归调用insertNode
寻找树的下一层
3.如果新节点的键大于根节点的键,同理,检测右侧子节点,插入新节点
5.树的遍历
遍历一棵树是访问每个节点并进行某种操作
1.中序遍历(左 自己 右)
中序遍历是一种以上行顺序访问BST所有节点的遍历方式,从最小到最大的顺序访问所有节点
中序遍历的一种应用就是对树进行排序操作
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);
}
}
首先判断以参数传入的节点是否为null,即停止递归继续执行的判断条件
递归调用相同函数来访问左侧子节点,然后进一些操作,然后访问右侧子节点
2.先序遍历(自己 左 右)
先序遍历是以优先于后代节点的顺序访问每个节点,打印一个结构化的文档
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);
}
}
3.后序遍历(左 右 自己)
后序遍历则是先访问节点的后代节点,再访问节点本身,文件所占空间的大小
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);
}
}
4.搜索树中的值
1.搜索最大值和最小值
最小值:沿着树的左边遍历
最大值:沿着树的右边遍历
min(){
return this.minNode(this.root);
}
minNode(node){
let current = node;
while (current != null && current.left != null){
current = current.left;
}
return current;
}
maxNode(node){
let current = node;
while (current != null && current.right != null){
current = current.right;
}
return current;
}
2.搜索特定的值
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);
}else if(
this.compareFn(key, node.key) === Compare.BIGGER_THAN
){
return this.searchNode(node.right, key);
}else{
return true;
}
}
首先检测传入的值是否为空,比当前节点小就在左侧子树找,大就在右侧子树找
5.移除节点
remove(key){
this.root = this.removeNode(this.root, key);
}
root被赋值为removeNode方法的返回值
removeNode(node, key){
if (node == null){
return null;
}
}
if (this.compareFn(key,node.key) === Compare.LESS_THAN){
node.left = this.removeNode(node.left, key);
return node;
}else if (
this.compareFn(key, node.key) === Compare.BIGGER_THAN
){
node.right = this.removeNode(node.right, key);
return node;
}else{
if (node.left == null && node.right == null){
node = null;2
return node;
}
if (node.left == null){
node = node.left;
return node;
} else if (node.right == null){
node = node.right;
return node;
}
const aux = this.minNode(node.right);
node.key = aux.key;
node.right = this.removeNode(node.right, aux.key);
return node;
}
1.检测键为null,返回null
2.存在则需要找到要移除的键,找到后要处理三种不同的情况
1.移除一个叶节点
该节点没有任何的左侧或右侧子节点的叶节点,给他赋值null
但是赋值null是不够的,还需要处理指针
可是他没有子节点,于是返回null将对应的父节点指针赋予null值
2.移除有一个左侧或右侧子节点的节点
需要跳过这个节点,直接将父节点指向它的指针指向子节点
3.移除有两个子节点的节点
1.找到了要移除的节点,需要找到它右边子树中的最小节点
2.然后用这个最小节点的键去更新这个节点的值,也就是说它被移除了
3.但是拥有两个相同键的节点不合理,继续把右侧子树中的最小节点移除
4.向它的父节点返回更新后的节点引用
6.自平衡树
BST存在一个问题,一边会非常深,一边却只有几层
1.AVL树:自平衡二叉搜索树,任何一个节点左右两侧子树的高度之差最多为1
2.添加或移除节点时,它会尝试保持平衡
3.扩展我们写的BST类,覆盖用来维持AVL树平衡的方法,检验它的平衡因子
4.节点高度
getNodeHeight(node){
if (node == null){
return -1;
}
return Math.max(
this.getNodeHeight(node.left), this.getNodeHeight(node.right)
) + 1;
}
右子树高度hr和左子树高度hl之间的差值,应为1、-1、0,如果不是则需要平衡该AVL树
7.红黑树
需要一个包含多次插入和删除的操作,那么红黑树是比较好的
1.节点不为红就为黑
2.树的根节点是黑的
3.所有叶节点都是黑的
4.如果一个节点为红,那么它的两个子节点都为黑
5.不能有两个相邻的红节点
6.从给定节点到它的后代节点所有路径包含相同数量的黑色节点
##二叉堆和堆排序
特殊的二叉树,也就是堆数据结构,也叫二叉堆
可以高效、快速的找出最大值和最小值,常被用于优先队列,堆排序算法
1.一棵完全二叉树,树每一层都有左侧和右侧子节点(除最后一层的叶节点),
并且最后一层的叶节点尽可能使左侧子节点,叫做结构特性
2.二叉堆不是最小堆就是最大堆,允许你快速找出最小/大值,
所有的节点都大于等于(最大堆)或小于等于(最小堆)每个它的子节点,叫做堆特性
3.不一定是二叉搜索树,每个子节点都要大于等于父节点或小于等于父节点
4.创建最小堆类
import {defaultCompare} from ‘…/util’;
export class MinHeap{
constructor(compareFn = defaultCompare){
this.compareFn = compareFn;
this.heap = [];
}
}
1.二叉树两种表示方法:指针/数组,对于数组使用以下方法
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);
}
1.insert:插入一个新值
将值插入堆的底部叶节点,再执行siftUp方法,表示要与父节点进行交换,直到父节点小于这个插入值
insert(value){
if (value != null){
this.heap.push(value);
this.siftUp(this.heap.length - 1);
return true;
}
return false;
}
上移操作
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);
}
}
交换函数
const swap = (array, a, b) => [array[a], array[b]] = [array[b], array[a]];
2.extract:移除最大值/最小值,并返回这个值
将堆的最后一个元素移动到根部执行siftDown函数
extract(){
if (this.isEmpty()){
return undefined;
}
if (this.size() === 1){
return this.heap.shift();
}
const removedValue = this.heap.shift();
this.siftDown(0);
return removedValue;
}
如果堆为空,返回undefined
下移操作
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[right]) >
Compare.BIGGER_THAN
) {
element = right;
}
if (index !== element){
swap(this.heap, index, element);
this.siftDown(elelment);
}
}
3.findMinimum:返回最大值/最小值,不会移除这个值
size(){
return this.heap.length;
}
isEmpty(){
return this.size() === 0;
}
findMinimum(){
return this.isEmpty() ? undefined : this.heap[0];
}
5.创建最大堆类
类似于创建最小堆类
6.堆排序算法
1.用数组创建一个最大堆用作源数据
2.创建最大堆后,最大的值会被存储在堆的第一个位置,要将它替换为堆的最后一个值,堆大小减一
3.将堆的根节点下移并重复步骤2直到堆的大小为1
##图
非线性数据结构:图
图是网络结构的抽象模型,一组由边连接的节点,任何二元关系都可以用图来表示
1.一个图G=(V,E)
V:一组顶点
E:一组边,连接V中的顶点
2.由一条边连接在一起的顶点称为相邻顶点
一个顶点的度是其相邻顶点的数量
路径是顶点的一个连续序列
简单路径要求不包含重复的顶点
环也是一个简单路径
无环:无环的 连通的:每两个顶点间都存在路径
3.有向图/无向图
图可以是无向的或是有向的,双向上都存在路径则是强连通的
4.图的表示
1.邻接矩阵,每个节点都和一个整数相关联,用一个二维数组来表示顶点之间的连接
array[i][j] === 1,array[i][j] === 0
不是强连通的图如果用邻接矩阵来表示,则矩阵中将会有很多0
意味着要浪费计算机存储空间来表示根本不存在的边
2.邻接表
由图中每个顶点的相邻顶点列表所组成
3.关联矩阵
矩阵的行表示顶点,列表示边,顶点v是边e的入射点
array[v][e] === 1,array[v][e] === 0
5.创建Graph类
class Graph{
constructor(isDirected = false){
this.isDirected = isDirected;
this.vertices = [];
this.adjList = new Dictionnary();
}
}
接受一个参数来判断图是否有向,默认情况下图是无向的
使用一个数组来存储图中所有顶点的名字,以及一个字典来存储邻接表,顶点为键,邻接点列表为值
添加一个新的顶点
addVertex(v){
if (!this.vertices.includes(v)){
this.vertices.push(v);
this.adjList.set(v, []);
}
}
添加顶点之间的边
addEdge(v, w){
if (!this.adjList.get(v)){
this.addVertex(v);
}
if (!this.adjList.get(w)){
this.addVertex(w);
}
this.adjList.get(v).push(w);
if (!this.isDirected){
this.adjList.get(w).push(v);
}
}
6.图的遍历
两种算法:广度优先搜索/深度优先搜索
可以寻找特定的点或寻找两个顶点之间的路径,检查图是否连通,检查图是否含有环
思想:必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索,需要指定第一个被访问的节点
完全搜索一个顶点要求我们查看该顶点的每一条边,对于连接的未被访问的标注被发现,加入待访问顶点列表
为了保证算法的效率,务必访问每个顶点至多两次,连通图中每条边和顶点都会被访问到
1.广度优先搜索:队列 存入队列,最先入队的顶点先被探索
先宽后深的访问顶点
1.创建一个队列Q
2.标注v为被发现的,并将v入队列Q
3.如果Q非空,则运行以下的步骤
1.将u从Q出队列
2.标注u被发现
3.将u所有未被访问过的邻点入队列
4.标注u为已被探索的
2.深度优先搜索:栈 存入栈,沿路径被探索,存在新的相邻顶点就去访问
先深后广的访问顶点
1.标注v为被发现的
2.对于v的所有未访问的邻点w,访问顶点w
3.标注v为已被探索的
3.白:还没被访问 灰:被访问过,并未被探索过 黑:被访问且被完全探索过
7.最短路径算法
1.Dijkstra算法
计算一个从单点源到所有其他源的最短路径的贪心算法
2.Floyd-Warshall算法
计算图中所有最短路径的动态规划法
8.最小生成树
最低成本实现桥梁互通-使用MST算法来解决
1.Prim算法
2.Kruskal算法
##排序和搜索算法
1.冒泡排序
比较所有相邻的两个项
function bubbleSort(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;
}
function swap(array, a, b){
[array[a], array[b]] = [array[b], array[a]];
}
算法复杂度:O(n^2)
2.选择排序
找到最小值并将其放在第一位,依次类推
function selectionSort(array, compareFn = defaultCompare){
const {length} = array;
let indexMin;
for (let i = 0;i < length - 1;i++){
indexMin = i;
for (let j = i;j < length;j++){
if (compareFn(array[indexMin], array[j]) === Compare.BIGGER_THAN){
indexMin = j
}
}
if (i != indexMin){
swap(array, i, indexMin);
}
}
return array;
}
算法复杂度:O(n^2)
3.插入排序
每次拍一个数组项,假定第一项已经排好了,接着,与第二项进行比较
function insertionSort(array, compareFn = defaultCompare){
const {length} = array;
let temp;
for (let i = 1;i < length;i++){
let j = i;
temp = array[i];
while (j > 0 && compareFn(array[j-1], temp) === Compare.BIGGER_THAN){
array[j] = array[j - 1];
j–;
}
array[j] = temp;
}
return array;
}
4.归并排序
将原始数组分成较小的数组,直到每个小数组只有一个位置,接着将小数组归并成大数组
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;
}
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));
}
算法复杂度:O(n*log(n))
5.快排
1.首先,在数组中选择一个值作为主元,也就是数组中间的那个值
2.创建两个指针引用,左边一个指向数组的第一个值,右边一个指向数组的最后一个值
移动左指针找到一个比主元大的值,移动右指针找到一个比主元小的值,然后交换
重复这个过程,直到左指针超过右指针,这一步叫做划分操作
3.接着对划分后的小数组重复之前的步骤,最后数组完全排序
function quickSort(array, compareFn = defaultCompare){
return quick(array, 0, arraty.length - 1, compareFn);
}
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;
}
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[i], pivot) === Compare.GIGGER_THAN){
j–;
}
if (i <= j){
swap(array, i, j);
i++;
j–;
}
}
return i;
}
算法复杂度:O(nlog(n))
##算法设计与技巧
1.分而治之

##算法复杂度
衡量算法的效率:资源:CPU占用、内存占用、硬盘占用、网络占用
1.O表示法:一般考虑的是CPU占用,也就是时间占用
1.O(1):和参数无关,也就是说函数复杂度为常数
2.O(n):和参数有关,存在最好和最坏,n是输入的数组的大小
3.O(n^2):双层嵌套循环
2.NP完全理论概述
一般来说一个算法的复杂度为O(n^k),k为常数,则认为这个算法是高效得
对于给定的问题,如果存在多项式算法,则计为P多项式
NP非确定性多项式算法,一个问题可以在多项式时间内验证是否正确,则计为NP
P都是NP,P=NP?
NP完全问题,满足以下条件,则称决策问题L是NP完全的
1.L是NP问题,可以在多项式时间内验证,但还没找到多项式算法
2.所有NP问题都能在多项式时间内归纳为L
不可解问题与启发式算法
有些问题是无解的,但是可以在符合要求的时间内找到一个近似解,这就是启发式算法
未必是最优解,足够解决问题
通过对算法的学习,可以提高我们解决问题的能力

发布了34 篇原创文章 · 获赞 34 · 访问量 1107

猜你喜欢

转载自blog.csdn.net/qq_45517916/article/details/103183335