更多内容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
在结构化存储数据集时,链表是数组的一种重要的替代方式,二者都很是基础,经常被称为顺序存储和链式存储。
问题描述:
假设全部整数都不相同,统计一个数组中全部和为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 }
首先咱们详细说明一下问题
问题的输入是一列整数对,对于一对整数p,q,若是p,q不相连,则将p,q链接
所谓的相连:
咱们假设相连的整数构成了一个“集合”,对于新的链接,就是在将新的元素加入“集合”来构成更大的“集合”,若判断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[]数组来肯定两个触点是否存在于相同的连通份量中
思想是:保证当且仅当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)次数组访问
因此此算法的时间复杂度是平方级别的
此算法的重点是提升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)
由此,咱们构造了一个最佳状况的输入使得算法的运行时间是线性的,最差状况的输入使得算法的运行时间是平方级的。
与其在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造成了鲜明对比
经过《算法》第一章我学习了