《算法》第一章学习笔记js实现

《算法》第一章学习笔记js实现

更多内容node

目标:总结本书主要内容,相应算法使用js来模仿实现git

在计算机科学领域,咱们用算法这个词来描述一种有限、肯定、有效的并适合用计算机程序来实现的解决问题的方法。

咱们关注的大多数算法都须要适当地组织数据,而为了组织数据就产生了数据结构github

原书全部代码是基于JAVA语法的,这里,咱们使用js来实现全部算法逻辑算法

队列、栈的实现

队列是一种先进先出的集合类型,栈是一种先进后出的集合类型

首先定义要实现的队列、栈的API数组

Queue 说明
Queue() 建立空队列
enqueue(item) 添加一个元素
dequeue() 删除最近添加的元素
isEmpty() 队列是否为空
size() 队列中元素的数量
iterator() 返回一个可迭代对象

Stack 说明
Stack() 建立空栈
push(item) 添加一个元素
pop() 删除最近添加的元素
isEmpty() 栈是否为空
size() 栈中元素的数量
iterator() 返回一个可迭代对象

Iterator 说明
hasNext() 是否还有下一个元素
next() 返回下一个元素
  • 数组方式

因为JS语言的特殊性,采用数组的方式来实现队列、栈是很是容易的,js中数组原本就提供了从头部插入、删除元素,从尾部插入、删除元素的功能。这里只须要简单的封装一下(js的弱类型特色,不须要像JAVA那样采用泛型来声明能够储存任意类型的数据,同时,js中数组是不定长的,能够动态扩展)网络

实现数据结构

队列的数组方式实现,并模拟可迭代功能学习

function Queue() {
    this.container = []
}
Queue.prototype.enqueue = function (ele) {
    this.container.push(ele)
}
Queue.prototype.dequeue = function () {
    return this.container.shift()
}
Queue.prototype.isEmpty = function () {
    return !this.container.length
}
Queue.prototype.size = function () {
    return this.container.length
}

Queue.prototype.iterator = function () {
    var container = this.container
    var current = 0
    return {
        hasNext: function () {
            return current !== container.length
        },
        next: function () {
            return container[current++]
        }
    }
}

用例:
var Qu = new Queue()
Qu.enqueue('to')
Qu.enqueue('be')
Qu.enqueue('or')
Qu.enqueue('not')
Qu.dequeue()
var iterator = Qu.iterator()
while (iterator.hasNext()) {
    console.log(iterator.next())
}
输出:
be
or
not

栈的数组方式实现,并模拟可迭代功能优化

class Stack {

    constructor() {
        this.container = []
    }

    push(ele) {
        this.container.unshift(ele)
    }

    pop() {
        return this.container.shift()
    }

    isEmpty() {
        return !this.container.length
    }
    size() {
        return this.container.length
    }

    iterator() {
        const container = this.container
        let current = 0
        return {
            hasNext: function () {
                return current !== container.length
            },
            next: function () {
                return container[current++]
            }
        }
    }

}
用例:
var St = new Stack()
Stack.push('to')
Stack.push('be')
Stack.push('or')
Stack.push('not')
Stack.pop()
var iterator = Stack.iterator()
while (iterator.hasNext()) {
    console.log(iterator.next())
}
输出:
or
be
to
  • 链表方式实现

链表是一种递归的数据结构,它或者为空(null),或者是指向一个结点(node)的引用,该结点含有一个泛型的元素和一个指向另外一个链表的引用。

在这个定义中,结点是一个可能含有任意类型数据的抽象实体,它所包含的指向结点的应用显示了它在构造链表之中的做用。ui

结点表示:

function Node(){
        this.item=null
        this.next=null
    }

构造链表:

在表头插入结点

var oldFirst=first
    first=new Node()
    first.next=oldFirst

从表头删除结点

first=first.next

从表尾插入结点

var oldlast=last
    lst=new Node()
    oldlast.next=last
实现任意插入和删除操做的标准解决方案是双向链表,其中每一个结点都含有两个连接,分别指向不一样的方向
  • 栈的链表实现
function Node(item) {
    this.item = item
    this.next = null
}

function Stack() {
    this.count = 0 //元素数量
    this.first = null //指向栈顶
}

Stack.prototype.isEmpty = function () {
    return this.first == null
}
Stack.prototype.size = function () {
    return this.count
}
Stack.prototype.push = function (ele) {
    var oldfirst = this.first
    var newnode = new Node(ele)
    newnode.next = oldfirst
    this.first = newnode
    this.count++
}
Stack.prototype.pop = function () {
    var ele = this.first.item
    this.first = this.first.next
    this.count--
    return ele
}
Stack.prototype.iterator = function () {
    var firstnode = this.first
    var count = this.count
    return {
        hasNext: function () {
            return  count
        },
        next: function () {
            var ele=firstnode.item
            firstnode=firstnode.next
            count--
            return ele
        }
    }
}
用例:
var stack=new Stack()
stack.push('to')
stack.push('be')
stack.push('or')
stack.push('not')
stack.push('to')
stack.push('be')
console.log(stack.size())
var iterator=stack.iterator()
while(iterator.hasNext()){
    console.log(iterator.next())
}
输出:
6
be
to
not
or
be
to
  • 队列的链表实现
将链表表示为一条从最先插入的元素到最近插入的元素的链表,实例变量first指向队列的开头,last指向队列的结尾。这样,要讲一个元素入列,就将它添加到表尾,要将一个元素出列,就删除表头的结点.
function Node(item) {
    this.item = item
    this.next = null
}

class Queue {

    constructor() {
        this.first = null
        this.last = null
        this.count = 0
    }

    isEmpty() {
        return this.first == null
    }
    size() {
        return this.count
    }
    enqueue(item) {
        const oldlast = this.last
        const last = new Node(item)
        this.last = last
        if (this.isEmpty()) {
            this.first = last
        } else {
            oldlast.next = last
        }
        this.count++
    }
    dequeue() {
        const ele = this.first.item
        this.first = this.first.next
        if (this.isEmpty()) {
            this.last = null
        }
        this.count--
        return ele
    }
    iterator() {
        let firstnode = this.first
        let count = this.count
        return {
            hasNext: function () {
                return count
            },
            next: function () {
                var ele = firstnode.item
                firstnode = firstnode.next
                count--
                return ele
            }
        }
    }
}
用例:
const queue=new Queue()
queue.enqueue('to')
queue.enqueue('be')
queue.enqueue('or')
queue.enqueue('not')
queue.enqueue('to')
queue.enqueue('be')
queue.dequeue()
console.log(queue.size())
const iterator=queue.iterator()
while(iterator.hasNext()){
    console.log(iterator.next())
}

输出:
5
be
or
not 
to
be
在结构化存储数据集时,链表是数组的一种重要的替代方式,二者都很是基础,经常被称为顺序存储和链式存储。

常见的时间复杂度的级别

  • threeSum问题分析

问题描述:

假设全部整数都不相同,统计一个数组中全部和为0的三整数元组的数量
  • 最基本的实现,暴力算法
function threesum(arr){
    var N=arr.length
    var count=0
    for(var i=0;i<N;i++){
        for(var j=i+1;j<N;j++){
            for(var k=j+1;k<N;k++){
                if(arr[i]+arr[j]+arr[k]==0){
                    count++
                }
            }
        }
    }
    return count
}

分析:

执行最频繁的指令决定了程序执行的总时间,对上面的threesum算法,最频繁的部分就是if语句判断,它套在三个for循环内,对于给定的N,if语句执行次数为N*(N-1)*(N-2)/6=N^3/6-N^2/2+N/3,当N很大时,首项后的其余项都相对较小能够忽略,因此if语句的执行次数约等于N^3/6,表示为(~N^3/6)

因此暴力算法的threesum执行用时的增加数量级为N^3

  • 优化
学习程序的增加数量级的一个重要动力是为了帮助咱们为同一个问题设计更快的算法

改进后的算法的思路是:当且仅当-( a[i]+a[j] )在数组中( 不是a[i]也不是a[j] )时,整数对( a[i]和a[j] )为某个和为0的三元组的一部分。要解决这个问题,首先对数组进行排序(为二分查找作准备),而后对数组中的每一个a[i]+a[j],使用二分查找算法对-(a[i]+a[j])进行二分查找,若是结果为k,且k>j,则count加一。

下面中的代码会将数组排序并进行N*(N-1)/2次二分查找,每次查找所需的时间都和logN成正比,所以总的运行时间和N^2logN成正比。

//二分查找
function binarySearch(key, arr) {
    var start = 0
    var end = arr.length - 1
    while (start <= end) {
        var mid = start + Math.floor((end - start) / 2)
        if (key < arr[mid]) {
            end = mid - 1
        } else if (key > arr[mid]) {
            start = mid + 1
        } else {
            return mid
        }
    }
    return -1
}

function threesum(arr) {
    var N = arr.length
    var count = 0
    arr = arr.sort(function (a, b) {
        return a > b ? 1 : -1
    })
    for (var i = 0; i < N; i++) {
        for (var j = i + 1; j < N; j++) {
            if (binarySearch(-arr[i] - arr[j], arr) > j) {
                count++
            }
        }
    }
    return count
}
  • 增加数量级的分类

案例研究:union-find算法

动态连通性问题

首先咱们详细说明一下问题

问题的输入是一列整数对,对于一对整数p,q,若是p,q不相连,则将p,q链接

所谓的相连:

  • [x] 自反性: p与p是相连的
  • [x] 对称性: 若p与q是相连的,则q与p是相连的
  • [x] 传递性: 若p与q是相连的,且q和r相连,则p与r是相连的

咱们假设相连的整数构成了一个“集合”,对于新的链接,就是在将新的元素加入“集合”来构成更大的“集合”,若判断p,q是否相连,只要判断p,q是否在同一个“集合”中便可。

这里咱们应用动态连通性来处理计算机网络中的主机之间的连通关系

输入中的整数表示的多是一个大型计算机网络中的计算机,而整数对则表示网络中的链接,这个程序可以断定咱们是否须要在p和q之间架设一条新的链接来通讯,或是咱们能够经过已有的链接在二者之间创建通讯线路。

这里咱们使用网络方面的术语,将输入的整数称为触点,将造成的集合称为连通份量

分析

为了说明问题,咱们设计一份API来封装所需的基本操做:初始化、链接两个触点、判断包含某个触点的份量、判断两个触点是否存在于同一个份量之中以及返回全部份量的数量

UF 说明
UF(N) 以整数标识(0到N-1)初始化N个触点
union(p,q) 链接触点p、q
find(p) 返回p所在份量的标识符
connected(p,q) 判断p,q是否存在于同一个连通份量中
count() 连通份量的数量

咱们看到,为解决动态连通性问题设计算法的任务转化成了实现这份API,全部的实现都应该

[x] 定义一种数据结构表示已知的链接

[x] 基于此数据结构实现高效的union()、find()、connected()、count()

咱们用一个以触点为索引的数组id[]做为基本数据结构来表示全部份量,咱们将使用份量中的某个触点的名称做为份量的标识符

一开始,咱们有N个份量,每一个触点都构成了一个只含有本身的份量,所以咱们将id[i]的值设为i。

class UF {

    /**
     * 
     * @param {number} N 
     */
    constructor(N) {
        this.id = new Array(N).fill(0).map((x, index) => index)
        this.count = 0
    }

    count(){
        return this.count
    }

    /**
     * 
     * @param {number} p 
     * @param {number} q 
     */
    connected(p,q){
        return this.find(p)===this.find(q)
    }

    /** 
     * @param {number} p 
     */
    find(p){

    }
    /**
     * 
     * @param {number} p 
     * @param {number} q 
     */
    union(p,q){

    }

}

find()和union()是实现的重点,咱们将讨论三种不一样的实现,它们均根据以触点为索引的id[]数组来肯定两个触点是否存在于相同的连通份量中

实现

  • quick-find算法

思想是:保证当且仅当id[p]==id[q]时,p和q是连通的。换句话说,在同一个连通份量中的全部触点在id[]数组中的值都同样。

/** 
     * @param {number} p 
     */
    find(p){
        return this.id[p]
    }

    /**
     * 
     * @param {number} p 
     * @param {number} q 
     */
    union(p,q){
        var pId=this.find(p)
        var qId=this.find(q)
        if(pId==qId) return
        this.id.forEach(x=>{
            if(id[x]==pId){
                id[x]==qId
            }
        })
        this.count--
    }

复杂度分析:

find()操做很快,它只访问id[]数组一次,但union()会整个扫描id[]数组

在union()中,find p、q会访问2次数组,for循环及赋值操做会访问数组 N+1 ~ N+(N-1)次。

因此union()方法访问数组的次数在(2+N+1) ~(2+N+(N-1)) 即 N+3 ~ 2N+1 次之间

假设咱们使用quick-union算法来解决动态连通性问题并最后只获得一个连通份量,则至少须要调用(N-1)次 union(),
即(N+3)(N-1) ~(2N+1)(N-1)次数组访问

因此此算法的时间复杂度是平方级别的

  • quick-union算法

此算法的重点是提升union()方法的速度,它也是基于相同的数据结构--以触点做为索引的id[]数组,但咱们赋予这些值的意义不一样,咱们须要用他们来定义更加复杂的数据结构:

每一个触点所对应的id[]元素都是同一个份量中的另外一个触点的名称(也能够说是它本身,即根触点)--咱们将这种联系称为连接。
/** 
     * 找到根触点,即份量的标识符
     * @param {number} p 
     */
    find(p) {
        while (p !== this.id[p]) p = this.id[p]
        return p
    }

    /**
     * 
     * @param {number} p 
     * @param {number} q 
     */
    union(p, q) {
        let pRoot = this.find(p)
        let qRoot = this.find(q)
        if (pRoot == qRoot) return
        id[pRoot] = qRoot
        this.count--
    }

如图所示:id[]数组用父连接的形式表示了一片森林

复杂度分析:

一棵树的大小是它的节点的数量,树中一个节点的深度是它到根节点路径上的连接数

quick-union算法的分析依赖于输入的特色,find()访问数组的次数为1加上给定的触点所对应的节点的深度的2倍。

在最好的状况下,find()只须要访问数组1次就可以获得当前触点所在份量的标识符

在最坏的状况下,find()须要1 + 2*(N-1) 即 2N-1 次数组访问

以下图所示

对最坏的状况,处理N对整数所需的全部find()操做访问数组的总次数为:

等差数列 (1+ 2N-1) *N /2 = N^2,即在最差的状况下,quick-union算的复杂度为平方级的

union()访问数组的次数是两次find()操做,(若是union中给定的两个触点在不一样的份量还要加1)

由此,咱们构造了一个最佳状况的输入使得算法的运行时间是线性的,最差状况的输入使得算法的运行时间是平方级的。

  • 加权 quick-union算法 (控制树的深度)
与其在union()中随意将一颗树链接到另外一棵树,咱们如今会记录每一颗树的大小并老是将较小的树链接到较大的树上。

class UF {

    /**
     * 
     * @param {number} N 
     */
    constructor(N) {
        this.id = new Array(N).fill(0).map((x, index) => index)
        //各个根节点所对应的份量的大小
        this.sz = new Array(N).fill(1)
        this.count = 0
    }

    count() {
        return this.count
    }

    /**
     * 
     * @param {number} p 
     * @param {number} q 
     */
    connected(p, q) {
        return this.find(p) === this.find(q)
    }

    /** 
     * 找到根触点,即份量的标识符
     * @param {number} p 
     */
    find(p) {
        while (p !== this.id[p]) p = this.id[p]
        return p
    }
    /**
     * 
     * @param {number} p 
     * @param {number} q 
     */
    union(p, q) {
        let pRoot = this.find(p)
        let qRoot = this.find(q)
        if (pRoot == qRoot) return
        //将小树链接到大树上
        if (sz[pRoot] < sz[qRoot]) {
            id[p] = qRoot
            sz[qRoot] += sz[pRoot]
        } else {
            id[q] = pRoot
            sz[pRoot] += sz[qRoot]
        }
        this.count--
    }

}

复杂度分析:

如图所示,在最坏的状况下,其中将要被归并的树的大小老是相等的,它们均含有2^n个节点(树的高度为n),当咱们归并两个2^n个节点的树时,获得的树的高度增长到n+1。

对于加权quick-union算法和N个触点,在最坏的状况下,find() union()的运行时间的增加数量级为logN

加权quick-union算法处理N个触点和M条链接时最多访问数组cMlgN次,这与quick-find须要MN造成了鲜明对比

总结

经过《算法》第一章我学习了

  • [x] 基本的数据类型栈、队列
  • [x] 经过数组、链表来构造队列和栈
  • [x] 数组和链表是两种基本的数据结构
  • [x] 时间复杂度的分析和常见的复杂度增加数量级
  • [x] 二分查找算法
  • [x] 对一个问题寻求解决方案时,要肯定好基本的数据结构,好的数据结构是构造高效算法的前提
  • [x] 动态连通性问题
  • [x] 动态连通性问题的解决方案,并不断优化算法
相关文章
相关标签/搜索