js数据结构与算法

267 阅读31分钟

了解真相,你才能获得真正的自由

1 定义

  • 数据结构可以理解为,数据以哪种形式进行展示和存储,常见的数据结构如下图所示。

图1 常见的数据结构
  • 算法就是解决问题的方法

2 数组结构

数组结构可以说是我们最熟悉,也是日常使用最多的一种数据结构了。

数组常见的操作有:增加元素,删除元素,修改元素和获取元素等。

2.1 各个操作的性能对比

数组的创建通常需要申请一段连续的内存空间,并且大小时固定的(大多数编程语言都是固定的)。所以如果当前数组不能满足容量要求时,需要进行扩容,申请一个更大的数组。

当我们进行增加和删除操作的时候,尤其是在数组的首位增加或删除时,数组后面的每一个元素都会进行后移或者前移,举一个最简单的例子,例如一个数组有100位,当我们进行添加在首位添加某一个元素时,后面的99个都需要向后迭代。这很影响效率。但是当我们通过下标获取元素或者修改元素时,使用数组结构效率是非常高的。 (尽管我们已经学过的js的array方法可以帮我们做很多事,但背后的原理依然是这样。)

3 栈结构

数组是一种线性结构,并且可以在数组的任意位置插入和删除数据。但是栈结构是一种受限的线性结构,其最主要的特点是LIFO(last in first out后进先出)。

3.1 特点

  • LIFO栈结构只允许在表的一端进行插入和删除元素,这端称为栈顶,另外一端叫栈底
  • 插入新元素叫入栈
  • 删除元素叫出栈

图3.1 栈结构示意图

3.2 基本操作

  • push(el): 添加一个元素,同时返回添加的元素
  • pop: 删除一个元素,同时返回删除的元素
  • peek: 返回栈顶的元素,不对栈进行任何修改
  • isEmpty: 判断栈里有无元素,有为true,无则false
  • clear: 移除栈里的所有元素。
  • size: 返回栈里面元素的个数。

3.3 封装

    class Stack {
    	constructor() {
    		this.data = []
    	}
    	// 添加元素
    	push(el) {
    		return this.data.push(el)
    	} 
    	// 删除元素
    	pop() {
    		return this.data.pop()
    	}
    	// 返回栈顶元素
    	peek() {
    		let last = this.data.length - 1
    		return this.data[last]
    	}
    	//  判断有无元素
    	isEmpty() {
    		return this.data.length === 0
    	}
    	// 清空元素
    	clear() {
    		this.data = []
    	}
    	// 元素个数
    	size() {
    		return this.data.length
    	}
    }

4 队列结构

队列结构也是一种受限结构。

4.1 特点

  • FIFO(fist in first out先进先出),它值允许在表的前端进行删除操作,在表的后端进行添加操作。

图3.2 队列结构示意图

4.2 常见操作

  • enquene(el): 向队列尾部添加一个元素,同时返回新添加的元素。
  • dequene: 移除队列的第一个元素,同时返回删除的元素。
  • front: 返回队列中的第一个元素。
  • isEmpty: 判断队列有无元素,有为true,无则false。
  • size:返回队列元素的个数。

4.3 封装

    class Quene {
    	constructor() {
    		this.data = []
    	}
    	// 添加元素
    	enquene(el) {
    		return this.data.push(el)
    	}
    	// 删除元素
    	dequene() {
    		return this.data.shift()
    	}
    	// 返回第一个元素
    	front() {
    		return this.data[0]
    	}
    	// 是否为空
    	isEmpty() {
    		return this.data.length === 0
    	}
    	// 返回队列的元素个数
    	size() {
    		return this.data.length
    	}
    }

4.4 优先级队列

普通队列插入一个元素,数据会放在后面,并且在前面的元素都处理完以后,才能处理后面的元素。但是优先级队列,在插入元素的时候会考虑该元素的优先级,优先级高的元素会插在前面。

  • 封装
    class QueneItem {
		constructor(el, priority) {
			this.el = el
			this.priority = priority
		}
	}
    class PriorityQuene {
    	constructor() {
    		this.data = []
    	}
    	// 添加元素
    	enquene(el, priority) {
    		if(!el || !Number(priority)) return false;
    		let queneItem = new QueneItem(el, priority)
    		if(this.isEmpty()) {
    			this.data.push(queneItem)
    		}else{
    			// 我们规定数字最小,优先级最高
    			let index = this.data.findIndex(item => queneItem.priority < item.priority)
    			index > -1 ? this.data.splice(index, 0, queneItem): this.data.push(queneItem)
    		}
    	}
    	// 删除元素
    	dequene() {
    		return this.data.shift()
    	}
    	// 返回第一个元素
    	front() {
    		return this.data[0]
    	}
    	// 是否为空
    	isEmpty() {
    		return this.data.length === 0
    	}
    	// 返回队列的元素个数
    	size() {
    		return this.data.length
    	}
    }

5. 链表

要存储多个元素,首先想到的就是数组,我们已经知道,数组的创建需要申请一段连续的内存空间,当数组长度改变时,则会进行相应的扩容和减容操作。但是现在存储多个元素时,我们可以使用另外一种数据结构-链表。

5.1 特点

  • 不同于数组的是,链表中的元素在内存中不必是连续的空间,可以充分利用计算机的内存,实现灵活的内存动态管理。
  • 链表的每一个元素有一个存储元素本身和指向下一个元素的指针组成。
  • 链表在插入和删除时,效率很高。
  • 链表访问任何一个元素,都需要从头开始进行遍历。

图5.1 链表的数据结构

5.2 常见操作

  • append(el): 向列表尾部添加一个元素。
  • insert(pos, el): 向特定位置添加一个元素。
  • remove(el): 从列表中移除某一项。
  • indexOf(el): 返回元素的索引,如果没有则返回-1。
  • removeAt(pos): 从列表的特定位置移除一项。
  • update(pos, el): 根据下表修改某项的元素。
  • isEmpty: 判断列表有无元素。
  • size: 判断列表中元素的个数。
  • toString: 重写默认的toString方法。
  • getFirst: 获取第一个元素

5.3 封装

    <!--数据每一项存储的结构-->
    class ListItem {
		constructor(el) {
			this.el = el
			this.next = null
		}
	}
	<!--单向链表类-->
    class LinkedList {
    	constructor() {
    		this.head = null
    		this.length = 0
    	}
    	<!--添加元素-->
    	append(el) {
    		let newItem = new ListItem(el)
    		if(!this.head) {
    			this.head = newItem
    		}else {
    			let cur = this.head
    			while(cur.next){
    				cur = cur.next
    			}
    			cur.next = newItem 
    		}
    		this.length ++
    	}
    	<!--插入元素-->
    	insert(pos, el) {
    		if(pos < 0 || pos > this.length) return false

    		let newItem = new ListItem(el)
    		let cur = this.head
    		let pre 
    		if(pos == 0) {
    			// if(!cur) {
    			// 	this.head = newItem
    			// }else{
    				newItem.next = cur
    				this.head = newItem
    			// }
    		}else {
    			let index = 0
    			while(index < pos){
    				pre = cur
    				cur = cur.next
    				index ++
    			}
    			pre.next = newItem
    			newItem.next = cur
    		}
    		this.length ++
    		return true
    	}
    	<!--根据下标移除某一项-->
    	removeAt(pos) {
    		if(pos < 0 || pos > this.length - 1) return null
    		let cur = this.head,
    		pre, index = 0
    		if(pos == 0){
    			this.head = cur.next
    		}else {
    			while(index < pos) {
    				pre = cur
    				cur = cur.next
    				index ++
    			}
    			pre.next = cur.next
    		}
    		this.length --
    		return cur
    	}
    	<!--查找某项的索引-->
    	indexOf(el) {
    		let cur = this.head,
    		index = 0
    		while(cur) {
    			if(cur.el == el) return index
    			cur = cur.next
    			index ++
    		}
    		return -1
    	}
    	<!--根据内容移除某项-->
    	remove(el) {
    		// let cur = this.head,
    		// pre
    		// if(cur.el == el) {
    		// 	this.head = cur.next;
    		// 	this.length --
    		// 	return cur
    		// }else {
    		// 	while(cur) {
    		// 		if(cur.el == el) {
    		// 			pre.next = cur.next
    		// 			this.length --
    		// 			return cur
    		// 		}
    		// 		pre = cur
    		// 		cur = cur.next
    		// 	}
    		// 	return null
    		// }
    		// 或者
    		return this.removeAt(this.indexOf(el))
    	}
    	<!--修改某一项的元素-->
    	update(pos, el) {
    		if(pos < 0 || pos > this.length) return false
    		let cur = this.head
    		let index = 0
    		while(index < pos) {
    			cur = cur.next
    			index ++
    		}
    		cur.el = el
    	}
    	<!--检查是否为空-->
    	isEmpty() {
    		return this.length == 0
    	}
    	<!--链表的长度-->
    	size () {
    		return this.length
    	}
    	<!--返回第一个元素-->
    	getFirst() {
    		return this.head
    	}
    	<!--重写toSring方法-->
    	toString() {
    		let cur = this.head,
    		res = ''
    		while(cur) {
    			res += ',' + cur.el
    			cur = cur.next
    		}
    		return res.substring(1)
    	}
    }
    const linkedList = new LinkedList()

5.4 双向链表

上面的结构是单向链表的结构,其特点是只能从头遍历到尾,或者从尾遍历到头。除了这种结构,我们还有双向链表,它既可以从头到尾,也可以从尾到头进行遍历,虽然占用内存空间大一些,但是比单向链表更加灵活。

  • 封装
    class ListItem {
		constructor(el) {
			this.el = el
			this.next = null
			this.prev = null
		}
	}
	<!--封装双向列表类-->
    class DoubleLinkedList {
    	constructor() {
    		this.head = null
    		this.tail = null
    		this.length = 0
    	}
    	<!--添加元素-->
    	append(el) {
    		let newItem = new ListItem(el)
    		if(!this.head) {
    			this.head = this.tail = newItem
    		}else{
    			this.tail.next = newItem
    			newItem.prev = this.tail
    			this.tail = newItem
    		}
    		this.length ++
    	}
    	<!--插入元素-->
    	insert(pos, el) {
    		if(pos < 0 || pos > this.length) return false
    		let newItem = new ListItem(el)
    		if(pos == 0) {
    			if(!this.head) {
    				this.head = this.tail = newItem
    			}else {
    				this.head.prev = newItem
    				newItem.next = this.head
    				this.head = newItem
    			}
    		}else if(pos == this.length) {
    			this.tail.next = newItem
    			newItem.prev = this.tail
    			this.tail = newItem
    		}else {
    			let cur,
    			index,
    			prev,
    			next
    			// 说明靠近左面
    			if(this.getDirection(pos) == 'toRight') {
    				cur = this.head
    				index = 0
    				while(index < pos) {
	    				prev = cur
	    				cur = cur.next;
	    				index ++
	    			}
	    			prev.next = newItem
	    			newItem.next = cur
	    			newItem.prev = prev
	    			cur.prev = newItem
    			}else {
    				cur = this.tail
    				index = this.length 
    				while(pos < index) {
    					next = cur
    					cur = cur.prev
    					index --
    				}
    				cur.next = newItem
    				newItem.prev = cur
    				newItem.next = next
    				next.prev = newItem
    			}
    			
    			
    		}
    		this.length ++
    		return true

    	}
    	<!--修改数据-->
    	update(pos, el) {
    		if(pos < 0 || pos > this.length) return false
    		let cur = this.head
    		let index = 0
    		if(this.getDirection(pos) == 'toRight') {
    			while(index < pos) {
	    			cur = cur.next
	    			index ++
	    		}
    		}else {
    			cur = this.tail
    			index = this.length - 1
    			while(index > pos) {
    				cur = cur.prev
    				index --
    			}
    		}
    		
    		cur.el = el
    		return true
    	}
    	<!--根据下标移除元素-->
    	removeAt(pos) {
    		if(pos < 0 || pos > this.length - 1) return null
    		let cur = this.head
    		if(pos == 0) {
    			if(this.length == 1) {
    				this.head = this.tail = null
    			}else {
    				this.head = this.head.next
    				this.head.prev = null
    			}
    		}else if(pos == this.length - 1) {
    			cur = this.tail
    			this.tail = this.tail.prev
    			this.tail.next = null
    		}else {
    			let prev,
    			next,
    			index = 0
    			if(this.getDirection(pos) == 'toRight') {
	    			while(index < pos) {
	    				prev = cur
	    				cur = cur.next
	    				index ++
	    			}
	    			cur.next.prev = prev
	    			prev.next = cur.next
    			}else {
    				index = this.length
    				cur = this.tail
    				while(pos < index) {
    					next = cur
    					cur = cur.prev
    					index --
    				}
    				cur.prev.next = cur.next
    				next.prev = cur.prev
    			}
    			
    		}	
    		this.length --
    		return cur
    	}
    	<!--查找元素的索引-->
    	indexOf(el) {
    		let cur = this.head,
    		index = 0
    		while(cur) {
    			if(cur.el == el) return index
    			cur = cur.next
    			index ++
    		}
    		return -1
    	}
    	<!--根据内容移除元素-->
    	remove(el) {
    		return this.removeAt(this.indexOf(el))
    	}
    	<!--判断是否为空-->
    	isEmpty() {
    		return this.length == 0
    	}
    	<!--返回链表长度-->
    	size () {
    		return this.length
    	}
    	<!--返回第一个元素-->
    	getHead() {
    		return this.head
    	}
    	<!--返回最后元素-->
    	getTail() {
    		return this.tail
    	}
    	<!--正向字符串-->
    	forwardString() {
    		return  this.toString()
    	}
    	<!--反向字符串-->
    	reverseString() {
    		let cur = this.tail,
    		res = ''
    		while(cur) {
    			res += ',' + cur.el
    			cur = cur.prev
    		}
    		return res.substring(1)
    	}
    	<!--根据pos判断应该是从左往右还是从右往左遍历-->
    	getDirection(pos) {
    		let flag = (pos / this.length - 0.5) < 0
    		return flag ? 'toRight' : 'toLeft'
    	}
    	toString() {
    		let cur = this.head,
    		res = ''
    		while(cur) {
    			res += ',' + cur.el
    			cur = cur.next
    		}
    		return res.substring(1)
    	}
    }

6 集合

集合可以看作是一种特殊的数组, es6中的Set类就是集合.

6.1 特点

  • 通常是有一组无序的, 不能重复的元素构成.
  • 没有顺序则不能通过下标的方式进行获取.

6.2 常用操作

  • add(el): 添加元素.
  • remove(el): 删除元素.
  • has(el): 元素存在则返回true, 否则返回false.
  • clear: 清空集合.
  • size: 集合数量
  • values: 集合所有值的数组
  • union(otherSet): 两个集合的并集
  • intersection(otherSet): 两个集合的交集
  • differency(otherSet): 差集, 当前集合有,但是otherSet没有的元素集合
  • subset(otherSet): 子集, 判断当前集合是否是otherSet的子集.

6.3 封装

    class Set {
		constructor() {
			this.data = {}
		}
		add(el) {
			if(this.has(el)) return false
			this.data[el] = el
			return true
		}
		remove(el) {
			if(!this.has(el)) return false
			delete this.data[el]
			this.length
			return true
		}
		has(el) {
			return  this.data.hasOwnProperty(el)
		}
		clear() {
			return this.data = {}
		}
		size() {
			return Object.keys(this.data).length
			// let count = 0
			// for(let item in this.data) {
			// 	if(this.has(item)) count ++ 
			// }
			// return count
		}
		values() {
			return Object.keys(this.data)
		}
		union(otherSet) {
			let res = new Set(),
			my = this.values(),
			other = otherSet.values();
			my.forEach(item => {
				res.add(item)
			})
			other.forEach(item => {
				res.add(item)
			})
			return res
		}
		intersection(otherSet) {
			let res = new Set(),
			my = this.values()
			my.forEach(item =>{
				if(otherSet.has(item)) {
					res.add(item)
				}
			})
			return res
		}
		differency(otherSet) {
			let res = new Set(),
			my = this.values()
			my.forEach(item =>{
				if(!otherSet.has(item)) {
					res.add(item)
				}
			})
			return res
		}
		subset(otherSet) {
			let my = this.values()
			for(let i = 0, len = my.length; i < len; i ++) {
				const item = my[i]
				if(!otherSet.has(item)) {
					return false
				}
			}
		}
	}

7 哈希表

哈希表是一种重要的数据结构,几乎所有的编程语言都直接或间接的应用这种数据结构。它的结构就是数组,但是它神奇的地方在于对下表值的一种变换,这种变换叫哈希函数。通过哈希函数可以获得hashCode。

7.1 特点

  • 哈希表是基于数组实现的,它可以提供快速的插入,删除,查找操作。
  • 哈希表的速度比树还要快,基本可以瞬间找到想要的元素。
  • 哈希表的数据是没有顺序的,所以不能以固定的方式(例如从小到大)来进行遍历。
  • 哈希表中的key是不能重复的。

7.2 原理

哈希表实现的原理就是将字母与数字建立起一一对应的关系,这样的话,当我们不知道数据的下标而只知道字母时(key)时,便可以轻松找出来了。

举个栗子,我们需要设计一种数据结构,保存联系人和电话,我们既可以通过下标的方式,也可以通过查找用户名的形式来进行检索数据,但是有没有一种办法将用户名与下标建立起一一对应的关系呢。哈希表正式借此应运而生。

  • 字母转数字

字母转成数字有多个方案,前提都是某个字母都是用唯一的数组进行标识。

第一,相加,例如‘hobby’,为了简单起见,h为1,o为2,b为3,y为4,所以hobby对应的数字就是13,但是这种方式有个很大的弊端,数字就易于重复。

第二,幂的连乘,同样是hobby: 1 * 26^4 + 2 * 26^3 + 3 * 26^2 + 3 * 26 + 4(备注:总共有26个英文字母) 这种数据计算结果过于巨大,如果我们仅仅只存储几个数据,但是数组的长度却过于巨大,严重浪费内存。

第三,通过哈希函数实现

  • 哈希相关知识

哈希化:将大数字转化为一定范围数组的下标过程。

哈希函数:通过将单词转变为大数字,建立对应关系,然后在进行哈希化的函数。

哈希表:最终将数据插入到这个数组,就是哈希表。

  • 冲突

虽然通过哈希函数可以建立一一对应关系,并且转为的数组也在一定范围内,但是不能保证没有一个数字是重复的,为了解决这种冲突,有两种方法:链地址法,开放地址法

  • 链地址法

图7.2-a 链地址法原理

从上面的图片中可以看到,数组的每一个单元里面,存储的不在是单个数据,而是一个数组或者链表,因此要是相同的则直接将元素插入到数组或链表中。

效率:探测的次数和装填因子的关系

成功: 1 + loadFactor/2
失败:1 + loadFactor

图7.2-a(2) 链地址法的性能
  • 开放地址法

开放地址法主要是寻找空白单元格来添加重复数据。探索的方式有线性探测,二次探测,再哈希化

  • 线性探测

插入:线性探测很好理解:就是每次探测的步长一样。例如我们经过哈希化得到了一个index为5,但是此时5的位置上有元素了,所以,我们就从index+1的位置开始一点点找,直到找到空白的位置。

查询: 经过哈希化得到index为5,判断当前的位置结果key与经过哈希化之前得到的key是否相同,如果相同,则返回整个数据。如果不同,就从index+1的位置开始一点点寻找。查找的时候需要注意的一点就是,如果当前查找的数据我们之前没有存储,我们最远只需要查到空位置,就停止,因为插入之前不可能跳过这个空位置。

删除:删除操作和查询类似,但是需要注意的是,删除数据时,不可以将这个位置下标的内容设置为null,因为设置为null可能会影响我们的之后的查询操作,所以通常会进行特殊处理,例如设置为-1,这样的话,查询时遇到-1则继续查询,但是插入时候遇到-1可以放入数据。

缺点:如果遇到聚集(例如10,11,12,13,14,15,16等连续的位置都有数据)就会影响哈希表的性能。

效率:探测序列(p)和装填因子(L)的关系

对成功的查找:P = (1+1/(1-L))/2
对失败的查找:P = P=(1+1/(1-L)^2)/2

公式来自于Knuth(算法领域的专家)下面是其实际效率的示意图。

图7.2-b 线性探索的性能
  • 二次探测

二次探测主要时优化步长,及每次探测的步长不一样。例如从下标值为i的位置,则会再i, i + 2^2, i + 3^2等依次探测。但是二次探测也会造成步长不一的聚集,并不能从根本上解决问题。

  • 再哈希化

原理:在哈希化,就是把关键字用另外一个哈希函数,再做一次哈希化,用这次的结果作为步长。

特点:再次哈希化必须和第一次的哈希函数不同,不然结果还是原来的位置。并且不能输出为0,否则没有步长,则进入了死循环。

实践:前人栽树,后人乘凉,已有专家设计了一种非常好的哈希函数。

step = c - (key % c)

其中c为质数且小于数组的容量,并且step结果始终不会为0,例如 step = 7 - (key % 5)

缺点:平均探测长度以及平均存取时间,取决于填装因子,随着填装因子变大,探测长度也越来越长。随着装填因子变大,效率下降的情况,在不同开放地址法方案中比链地址法更严重。

效率:二次探索和在哈希的性能差不多,比线性探测性能略好。

成功: -log2(1 - loadFactor) / loadFactor
失败:1 / (1-loadFactor)

图7.2-c 二次探索和再哈希法的性能

定义:装填因子 = 总数据项 / 哈希表长度

开放地址法的装填因子最大为1,因为必须寻找空白的单元格才能放入,链地址法的装填因子可以大于1,因为每个单元格可以无限延申下去。

  • 总结

经过比较,链地址法相对来说效率是好于开放地址法的,并不会由于添加了某些元素以后性能急剧下降。并且操作相对简单易懂。Java中的hashMap使用的就是链地址法。

7.3 哈希函数

我们现在已经知道哈希化就是将一个大数字变成一定范围的小数据,那么我们如何将幂的连乘结果变成具有一定范围的下标呢,我们首先来了解其他几个概念。

  • 均匀分布

在设计哈希表的时候,如果遇到相同的下标时,我们已经知道使用链地址法或者开放地址法进行处理。但是为了提高效率,最好是让数据在哈希表中均匀分布,如何让数据在哈希表中均匀分布呢,此时我们需要使用质数(哈希表的长度以及幂的底数需要使用质数,例如31/37/41等等. 一个比较常用的数是37.)。

我们只举一个简单的,如果向深入了解为什么使用质数,请搜索算法相关资料。例:如果一个哈希表中的长度为15(0-14),有个特定关键字映射到0,此时步长为5,那么探测序列则会在0 - 5 - 10 - 0 - 5 - 10进行死循环。但是如果表长为13,探测序列则会0 - 5 - 10 - 2 - 7 - 12 - 4 - 9 - 1 - 6 - 11 - 3, 一直这样下去,这不仅不会循环,还可以均匀分布。

  • 链地址法中的质数并不像开放地址法中没有那么重要。

  • 哈希函数的实现。

    function hashFunc(str, length) {
        // 1.初始化hashCode的值
        var hashCode = 0
    
        // 2.霍纳算法, 来计算hashCode的数值
        for (var i = 0; i < str.length; i++) {
            hashCode = 37 * hashCode + str.charCodeAt(i)
        }
    
        // 3.取模运算, 即哈希化
        hashCode = hashCode % length
        return hashCode
    }

7.4 常用操作

  • put(key, value): 插入和修改数据
  • get(key): 获取数据
  • remove(key): 删除数据
  • isEmpty: 哈希表的长度是否位空
  • size: 哈希表的数据个数。

7.5 封装

    // 哈希表类
    class HashTable {
    	constructor() {
    		this.data = []
    		this.limit = 6
    		this.count = 0
    	}
    	// 哈希函数
    	hashFunc (str, length) {
	        // 1.初始化hashCode的值
	        let hashCode = 0
	        // 2.霍纳算法, 来计算hashCode的数值
	        for (let i = 0; i < str.length; i++) {
	            hashCode = 37 * hashCode + str.charCodeAt(i)
	        }
	        // 3.取模运算, 哈希化
	        hashCode = hashCode % length
	        return hashCode
	    }
	    // 增加或删除操作
		put(key, value) {
			let index = this.hashFunc(key, this.limit)
			// 获取每一个单元格的数据
			let bucket = this.data[index]

			// 如果当前单元格没有数据
			if(!bucket) {
				bucket = this.data[index] = []
			}
			
			let curItem = bucket.find(item => item[0] === key)

			// 如果存在
			if(curItem) {
				curItem[1] = value
			}else { // 如果不存在
				bucket.push([key, value])
				this.count ++
			}
			return true
		}	
		// 获取数据
		get(key) {
			let index = this.hashFunc(key, this.limit)
			// 获取每一个单元格的数据
			let bucket = this.data[index]

			if(!bucket) {
				return null
			}
			let curItem = bucket.find(item => item[0] === key)
			return curItem ? curItem[1] : null
		}
		// 删除数据
		remove(key) {
			let index = this.hashFunc(key, this.limit)
			// 获取每一个单元格的数据
			let bucket = this.data[index]

			if(!bucket) {
				return null
			}
			let curIndex = bucket.findIndex(item => item[0] === key)
			if(curIndex > -1) {
				this.count --
				return bucket.splice(curIndex, 1)
			}
			return null
		}
		// 是否为空
		isEmpty() {
			return this.count == 0
		}
		// 数据个数
		size() {
			return this.count
		}
    }

7.6 哈希表扩容和缩减

原因:虽然链地址法可以无线填充,但是随着数据的增多,每一个index对应的数据救护越多,这也会造成效率的降低。同理,如果我们不断的删除数据,那么也应该哈希表的容量也需要进行相应的缩减。

方式:比如当loadFactor(装填因子)> 0.75的时候进行扩容,当laodFactor < 0.25的时候进行缩减。

    resize(newLimit) {
			let oldData = this.data
			this.data = []
			this.limit = newLimit
			this.count = 0
			oldData.forEach(item => {
				if(item && item.length > 0) {
					for(let i = 0, len = item.length; i < len; i++) {
						this.put(item[i][0], item[i][1])
					}
				}
			})
		}

7.7 容量质数

为了保持容量恒为质数,我们首先需要编写一个判断是否为质数的函数。

  • 普通判断
    prime(num) {
		for(let i = 2; i < num; i ++){
			if(num % i == 0) return false
		}
		return true
	}
  • 优化判断
    prime(num) {
		let newNum = Math.sqrt(num) | 0
		for(var i = 2; i <= newNum; i ++){
			if(num % i === 0) return false
		}
		return true
	}

7.8 最终封装

    class HashTable {
    	constructor() {
    		this.data = []
    		this.limit = 6
    		this.count = 0
    	}
    	hashFunc (str, length) {
	        // 1.初始化hashCode的值
	        let hashCode = 0
	        // 2.霍纳算法, 来计算hashCode的数值
	        for (let i = 0; i < str.length; i++) {
	            hashCode = 37 * hashCode + str.charCodeAt(i)
	        }
	        // 3.取模运算, 哈希化
	        hashCode = hashCode % length
	        return hashCode
	    }
	    // 增加或删除操作
		put(key, value) {
			let index = this.hashFunc(key, this.limit)
			// 获取每一个单元格的数据
			let bucket = this.data[index]

			// 如果当前单元格没有数据
			if(!bucket) {
				bucket = this.data[index] = []
			}
			
			let curItem = bucket.find(item => item[0] === key)

			// 如果存在
			if(curItem) {
				curItem[1] = value
			}else { // 如果不存在
				bucket.push([key, value])
				this.count ++
				if(this.count > this.limit * 0.75) {
					let limit = this.getPrime(this.limit * 2)
					this.resize(limit)
				}
			}
			return true
		}	
		// 获取数据
		get(key) {
			let index = this.hashFunc(key, this.limit)
			// 获取每一个单元格的数据
			let bucket = this.data[index]

			if(!bucket) {
				return null
			}
			let curItem = bucket.find(item => item[0] === key)
			return curItem ? curItem[1] : null
		}
		// 删除数据
		remove(key) {
			let index = this.hashFunc(key, this.limit)
			// 获取每一个单元格的数据
			let bucket = this.data[index]

			if(!bucket) {
				return null
			}
			let curIndex = bucket.findIndex(item => item[0] === key)
			if(curIndex > -1) {
				let curItem = bucket.splice(curIndex, 1) 
				this.count --
				// 设定一个最小范围,不能无限制的缩减
				if(this.limit > 6 && this.count < this.limit * 0.25) {
					let limit = this.getPrime(this.limit / 2 | 0)
					this.resize(limit)
				}
				return curItem
			}
			return null
		}
		// 是否为空
		isEmpty() {
			return this.count == 0
		}
		// 数据个数
		size() {
			return this.count
		}
		// 扩容
		resize(newLimit) {
			let oldData = this.data
			this.data = []
			this.limit = newLimit
			this.count = 0
			oldData.forEach(item => {
				if(item && item.length > 0) {
					for(let i = 0, len = item.length; i < len; i++) {
						this.put(item[i][0], item[i][1])
					}
				}
			})
		}
		prime(num) {
			let newNum = Math.sqrt(num) | 0
			for(var i = 2; i <= newNum; i ++){
				if(num % i === 0) return false
			}
			return true
		}
		getPrime(num) {
			while(!this.prime(num)) {
				num ++
			}
			return num
		}
    }

8 树结构

8.1 各种数据结构对比

  • 数组:优点是根据下标值进行访问效率非常高,如果根据元素来进行查找的话,就比较低了,另外它在删除和插入的时候需要进行大量位移操作,效率很低。
  • 链表:优点时插入和删除效率很高,但是如果插入和删除的数据在中间位置,需要从头查找,查找效率非常低。
  • 哈希表: 增删改查效率都很高,但是它的空间利用率并不高,底层用的是数组,某些单元会出现空位置,并且哈希表中的元素是无序的,同时也不能快速找出最大值和最小值。
  • 树结构:树结构综合了上面数据结构的优点,当然并不是说树结构比其他结构都好,每种结构都有自己的应用场景,效率情况一般也没有哈希表高。

图8.1 树结构基本示意图

8.2 二叉树

  1. 如果一个树有只能由2个子节点,这样的树就成为"二叉树"。几乎所有树结构都可以用二叉树进行表示,二叉树由节点,左子节点和右子节点组成。
  • 二叉树第i层的最多节点数为:2(i-1), 其中i>=1;
  • 深度为k的二叉树最多节点数:2k-1, 其中k>=1;

图8.2-1 二叉树的示意图
  1. 特殊二叉树
  • 完美二叉树(又称满二叉树):除了叶子节点外,每个节点都有两个子节点。
  • 完全二叉树:除了二叉树的最后一层外,其他各层节点数都达到最大个数,并且最后一层的节点,从左向右必须连续存在,只能允许缺右侧若干节点。

图8.2-2 完美二叉树和完全二叉树结构示意图
  1. 链表存储

二叉树最常见的还是使用链表存储,每个节点都包含当前存储的数据,以及左,右节点的引用。

图8.2-3 链表存储示意图

8.3 二叉搜索树(Binary Search Tree)

  1. 二叉搜索树是一颗二叉树,可以为空,如果不为空,满足一下性质:
  • 非空左子树的所有键值都小于根节点的键值
  • 非空右子树的所有键值都大于根节点的键值
  • 左右子树本身也是二叉搜索树

图8.3 二叉搜索树判断
  1. 特点
  • 相对较小的值在左节点上,较大的值在右节点上。
  • 搜索效率很高。
  1. 常见操作
  • insert(key): 新增数据。
  • search(key): 查找有无key值,有返回true,否则返回false。
  • inOrderTraverse: 通过中序遍历方式遍历所有节点。
  • perOrderTraverse: 通过先序遍历所有节点。
  • postOrderTraverse: 通过后序遍历所有节点。
  • levelOrderTraverse: 层序遍历。
  • min: 返回树中最小值。
  • max: 返回树中最大值。
  • remove(key): 从树中移除某个值
  1. 名词解释
  • 先序遍历:遍历顺序为根节点-->左子节点-->右子节点

图8.3-1 先序遍历过程图
  • 中序遍历:左子节点-->根节点-->右子节点

图8.3-2 中序遍历过程图
  • 后序遍历:左子节点-->右子节点-->根节点

图8.3-3 后序遍历过程图
  • 层序遍历:从上到下每层按照顺序遍历。

图8.3-4 后序遍历过程图
  • 前驱:比current节点小一丢丢的节点,这个节点称之为current的前驱。
  • 后继:比current节点大一丢丢的节点,这个节点称之为current的后继。

前驱和后继是数值最接近current的两个节点,当我们删除一个节点时,就需要找前驱或者后继的节点来进行补位。

  1. 封装
    class Node {
    	constructor(key) {
    		this.key = key
    		this.left = null
    		this.right = null
    	}
    }
    class BinarySearchTree {
    	constructor() {
    		this.root = null
    	}
    	insert(key) {
    		let newNode = new Node(key)
    		this.root === null
    			? 	this.root = newNode
    			: 	this.insertNode(this.root, newNode)
        	}
    	insertNode(node, newNode) {
    		newNode.key < node.key // 新节点比旧节点小,则应该插入左子节点
    			? 	node.left == null
    					? 	node.left = newNode
    				 	:   this.insertNode(node.left, newNode)
    		 	:   node.right == null // 新节点比旧节点小,则应该插入右子节点
    		 			?   node.right = newNode
    		 			:   this.insertNode(node.right, newNode)
    	}
    	// 先序遍历:先访问根节点,再遍历左子树,然后遍历右子树。
    	preOrderTreverse() {
    		this.preOrderTreverseNode(this.root)
    	}
    	preOrderTreverseNode(node) {
    		if(node) {
    			console.log(node.key) 
        		this.preOrderTreverseNode(node.left)
        		this.preOrderTreverseNode(node.right)
    		}
    	}
    	// 中序遍历:先遍历左子树,根节点,中序遍历右子树
    	inOrderTreverse() {
    		this.inOrderTreverseNode(this.root)
    	}
    	inOrderTreverseNode(node) {
    		if(node) {
    			this.inOrderTreverseNode(node.left)
    			console.log(node.key)
    			this.inOrderTreverseNode(node.right)
    		}
    	}
    	// 后序遍历:先遍历左子树,再遍历右子树,最后遍历根节点
    	postOrderTreverse() {
    		this.postOrderTreverseNode(this.root)
    	}
    	postOrderTreverseNode(node) {
    		if(node) {
        		this.postOrderTreverseNode(node.left)
        		this.postOrderTreverseNode(node.right)
        		console.log(node.key) 
    		}
    	}	
    	// 层序遍历:一层一层遍历
    	levelOrderTreverse() {
    		this.levelOrderTreverseNode(this.root)
    	}
    	levelOrderTreverseNode(node) {
    		let temp = []
    		temp.push(node)
    		while(temp.length > 0) {
    			let cur = temp.shift()
    			console.log(cur.key)
    			if(cur.left) {
    				temp.push(cur.left)
    			}
    			if(cur.right) {
    				temp.push(cur.right)
    			}
    		}
    	}   
    	// 获取最小值
    	min() {
    		let node = this.root
    		while(node.left) {
    			node = node.left
    		}
    		return node.key
    	} 
    	// 获取最大值
    	max() {
    		let node = this.root
    		while(node.right) {
    			node = node.right
    		}
    		return node.key
    	} 
    	// 搜索值
    	search(key) {
    		if(!key) return false
    		return this.searchNode(this.root, key)
    	}
    	// 搜索值
    	searchNode(node, key) {
    		if(!node) return false
    		if(key < node.key) {
    			return this.searchNode(node.left, key)
    		}else if(key > node.key) {
    			return this.searchNode(node.right, key)
    		}else {
    			return true
    		}	
    	}
    	// 移除值
    	remove(key) {
    		if(!key) return false
    		// 先找到要删除的节点
    		let parrent = null, current = this.root, isLeft = true;
    		while(current.key != key) {
    			parent = current
    			if(key < current.key) {
    				isLeft = true
    				current = current.left
    			}else {
    				isLeft = false
    				current = current.right
    			}
    			// 如果没有current,则说明没有找到。
    			if(!current) return false
    		}
    		
    		// 如果当前节点是叶子节点
    		if(!current.left && !current.right) {
    			current == this.root 
    				?	this.root = null
    				: 	isLeft 
    					? 	(parent.left = null) 
    					:   (parent.right = null)
    		}
    		// 如果当前只有左子节点
    		else if(!current.right) {
    			current == this.root 
    				?	this.root = current.left
    				:   isLeft 
    					? 	parent.left = current.left
    					: 	parent.right = current.left
    		}
    		// 如果当前只有右子节点
    		else if(!current.left) {
    			current == this.root
    				? 	this.root = current.right
    				: 	isLeft
    					?	parent.left = current.right
    					: 	parent.right = current.right
    		}
    		// 当前有2个子节点
    		else {
    			let successor = this.getSuccessor(current)
    			current == this.root
    				? 	this.root = successor
    				: 	isLeft
    					? 	parent.left = successor
    					: 	parent.right = successor
    			;successor.left = current.left
    		}
    	}
    	// 寻找后继
    	getSuccessor(node) {
    		let successor = node
    		let current = node.right
    		let parent = null
    		while(current) {
    			parent = successor
    			successor = current 
    			current = current.left
    		}
    		if(successor != node.right) {
    			parent.left = successor.right
    			successor.right = node.right
    		}
    		return successor
    	}
    }

8.4 红黑树

二叉搜索树可以快速的查找某个值,也可以快速的插入和删除。 但是二叉搜索树有个很严重的问题,那就是如果插入的数据是有序数据,例如 1,2,3,4,5,6,则这样的二叉搜索树就是相当于链表了,效率也会变得非常低。

因此比较好的二叉搜索树数据应该是左右均匀分布的,左右均匀分布的二叉树,称之为平衡树,反之则称为非平衡树。

  1. 介绍

红黑树是属于平衡树的一种,它依赖一些特性来保持树的平衡,现在的平衡树的应用基本都是红黑树。

  1. 特点

这些特点决定了红黑树的关键特性:从根到叶子节点的最长路径,不会超过最短路径的2倍长。

  1. 变换
  • 变色: 将红色变为黑色,或者将黑色变为红色。
  • 左旋转:逆时针旋转红黑树的两个节点,使得父节点被自己的右子节点取代,父节点则称为右子节点的左孩子。
  • 右旋转:顺时针旋转红黑树的两个节点,使得父节点被自己的左子节点取代,父节点则成为自己左子节点的右孩子。
  1. 插入操作分析

图8.4-1 节点基本关系图 我们通常默认插入的都是红节点
  • 没有根节点:此时只需要将插入的红色节点变为黑色即可(特点2)。
  • 父节点是黑色:新插入的红色节点不需要任何变化。
  • 父(P),叔(U)节点是红色,爷爷(G)节点是黑色:需要将P,U变为黑色,G变为红色即可。 但是如果G节点的父节点是红色的,可以采用递归,即将爷爷节点看作是新插入的节点。

  • 父(P)是红色,爷爷(G),叔(U)是黑色,当前C是左儿子: 将P变为黑节点,G变为红色节点,进行右旋转。

  • 父(P)是红色,爷爷(G), 叔(U)是黑色,当前C是右儿子: 将P/C/B进行左旋转,此时如果将P作为新插入的红色节点,就会变成和上一个相同的情况,将N变为黑色,G变为红色,将G/N/U进行右旋转。

9 图结构

9.1 介绍

我们通常可以使用图结构描述点与点之间的关系,例如地铁线路图,人与人,地方与地方之间的关系等等。

图9.1 图结构基本图例

9.2 名词

  • 度: 一个顶点的相邻节点的数量。
  • 无向图/有向图:表示边有无方向。
  • 无权图/有权图:表示边有没有携带权重(路径/时间的长短)

9.3 表示方式

  • 邻接矩阵:就是使用二维数组来表示点与点之间的关系,如下图所示,0表示没有连线,1表示有连线,通常A-A,即自己-自己可以看作是0

图9.3-1 邻接矩阵基本结构
  • 邻接表:由每个点和以及和点相连的点组成,可以使用数组,链表,哈希表都可以。邻接表计算出度(指向别人的数量)比较简单,但是计算入度(指向自己的数量)就很困难,此时需要设计一个逆邻接表。

图9.3-2 邻接表基本结构

9.4 遍历算法

  • 广度优先搜索(Breadth First Search, BFS):会从指定的第一个顶点开始遍历,先访问其所有的相邻点,然后再访问相邻点的相邻点,这是一种先宽后深的访问顶点,基于队列实现。

  • 深度优先搜索(Depth First Search, DFS):从第一个顶点开始遍历图,沿着路径直到这条路径最后被访问,接着原路返回,继续探索下一条路路径,可以使用栈完成,也可以使用递归。

为了记录节点有没有被访问过,我们使用3中颜色来进行记录:白色该节点还没有被访问,灰色表示该节点被访问,但未被探索,黑色表示该节点被访问过并且被探索过。

9.5 封装

class Graph {
	constructor() {
		this.vertexes = []
		this.edges = {}
	}
	// 添加顶点
	addVertex(v) {
		// 如果已经包含v,说明v已经存在则直接返回false
		if(this.vertexes.includes(v)) return false;
		this.vertexes.push(v)
		this.edges[v] = []
		return true
	}
	// 添加边
	addEdge(v, e){
		// 如果没有包含v,即v不存在,所以添加v的边失败,直接返回false
		if(!this.vertexes.includes(v)) return false;
		let edges = this.edges[v]
		if(edges.includes(e)) return true;
		edges.push(e)
		this.addEdge(e,v) // 因为这是无向的,所以都需要添加
	}
	toString() {
		let res = ''
		for(let i = 0, len = this.vertexes.length; i < len; i ++) {
			let vertex = this.vertexes[i],
			edges = this.edges[vertex]
			res = res + vertex + '-->';
			for(let j = 0; j < edges.length; j ++) {
				res += edges[j] + ','
			}
			res += '\n'
		}
		return res
	}
	// 初始化颜色
	initColor(){
		let colors = {}
		for(let i = 0, len = this.vertexes.length; i <len ; i ++) {
			let item = this.vertexes[i]
			colors[item] = 'white'
		}
		return colors;
	}
	// 广度优先搜索
	bfs(v) {
		let quene = []
		let colors = this.initColor()
		quene.push(v)
		while(quene.length > 0) {
			let curV = quene.shift() // 取出并删除第一个元素
			colors[curV] = 'gray'

			let vList = this.edges[curV]
			for(let i = 0,len = vList.length; i < len; i ++){
				let item = vList[i]
				if(colors[item] == 'white') {
					quene.push(item)
					colors[item] = 'gray'
				}
			}
			console.log(curV)
			colors[curV] = 'black'
		}
	}
	// 深度优先搜索
	dfs(v) {
		let colors = this.initColor()
		this.dfsList(v, colors)
	}
	dfsList(v,colors) {	
		colors[v] = 'gray'
		console.log(v)
		let vList = this.edges[v]
		for(let i = 0, len = vList.length; i < len; i ++){
			let item = vList[i]
			if(colors[item] == 'white') {
				this.dfsList(item, colors)
			}
		}
			colors[v] = 'black'
		}
	}

10 排序算法

10.1 冒泡排序

  1. 冒泡排序是一种简单的排序算法。
  • 每次都是比较相邻的两个元素,如果第一个元素比第二个元素大,则交换它们两个位置。
  • 对每一对的相邻元素都做上面同样的工作,从第一对到最后最后一对,这样在最后的元素会是最大的数,至此,一轮比较结束。
  • 重复上面两个步骤,直至排序结束。

图10.1 冒泡排序原理图
  1. 代码实现
    function bubbleSort(arr) {
    	for(let len = arr.length, i = len - 1; i >= 0; i --) {
    		for(let j = 0; j < i; j ++){
    			if(arr[j] > arr[j+1]) {
    				let temp = arr[j]
    				arr[j] = arr[j+1]
    				arr[j+1] = temp 
    			}
    		}
    	}
    } 

10.2 选择排序

  1. 选择排序也是属于简单排序的一种。
  • 首先在未排序的序列中找到最小的元素,存放到队列的起始位置。
  • 然后再剩余队列中继续寻找最小元素,放到已排序的序列末尾。
  • 重复上面步骤,直至所有元素排序完成。

图10.2 选择排序原理图
  1. 代码实现
    function selectionSort(arr) {
    	for(let i = 0, len = arr.length; i < len - 1; i ++) {
    		let index = i;
    		for(let j = i + 1; j < len; j ++){
    			if(arr[index] > arr[j]) {
    				index = j;
    			}
    		}
    		let temp = arr[i]
    		arr[i] = arr[index]
    		arr[index] = temp
    	}
    }

10.3 插入排序

  1. 插入排序也属于简单排序的一种,它的工作原理就是将未排序的元素,放入已经排好顺序的序列中找到相应位置并插入。
  • 默认第一个元素已排序。
  • 取出下一个元素,在已排序的序列中,由后向前扫描。
  • 如果新元素小于已排序中的某一元素,则将该元素移到下一个位置。
  • 重复以上步骤,直至排序结束。

图10.3 插入排序的原理图
  1. 代码实现
    function insertionSort(arr) {
    	for(let i = 1, len = arr.length; i < len; i ++){
    		let cur = arr[i] // 当前需要插入的项
    		let j = i; // 初始化索引
    		while(j >=0 && cur < arr[j-1]) {
    			arr[j] = arr[j-1]
    			j--
    		}
    		arr[j] = cur
    	}
    }

10.4 希尔排序

  1. 第一个突破O(N2)的排序算法,是插入排序的改良版。它和插入排序的不同是,它会优先比较最元的元素。
  • 选定一个增量k,将整个未排序的序列分成若干个子序列。
  • 对每个子序列分别进行插入排序。
  • 然后再设置一个合适的增量,比如k/2。重复上面的步骤。
  • 当增量为1时,整个序列作为一个表来处理。

图10.4 希尔排序原理图
  1. 代码实现
function shellSort(arr) {
    	let len = arr.length;
    	for(let gap = Math.floor(len/2); gap >= 1; gap = Math.floor(gap/2) ){
    		for(let i = gap; i < len; i ++){
    			let j = i;
    			let cur = arr[i];
    			while(j > gap-1 && arr[j-gap] > cur){
    				arr[j] = arr[j-gap]
    				j = j - gap;
    			}
    			arr[j] = cur
    		}
    	}
    }

10.5 快速排序

  1. 快速排序像希尔排序一样,也是属于高级排序的一种。快速排序可以说是目前所有排序中最快的算法,主要是挖坑填法和分治法。
  • 从数组中取出一个数作为基准值(pivot)
  • 在原数组中进行移动,将大于基准值的数放在右边,小于基准值的数放到左边。
  • 以基准值划分为左右两个区域,左右两区域的数组依次递归,直至,剩下一个子数组。
  1. 详细步骤如下:
  • 初始数组如下图所示,取第一个数为pivot, 此时:left = 0, right = 9, pivot = arr[0] = 36; 此时将left = 0的位置就相当于一个坑,left先不变,right-- 从后往前小于pivot的数。

图10.5(a) 快速排序步骤1
  • 当 left=0,right=8时,将arr[8]放入arr[0]的坑中,此时a[0]为24,arr[8]就是坑,然后调换顺序,从前往后找比pivot大的数(left ++,right不变)。

图10.5(b) 快速排序步骤2
  • 当left = 3, right = 8时,将arr[3]放入arr[8]中,此时arr[8]为43,arr[3]就变为坑。 然后调换顺序从后往前找比pivot小的数(left不变,right--)

图10.5(c) 快速排序步骤3
  • 当 left = 3, right = 5时,将arr[5]放入arr[3]中,arr[5]变为了坑。然后调换顺序,从前往后找比pivot大的数(left ++,right不变)。

图10.5(d) 快速排序步骤4
  • left == right == 5时,第一次排序结束。此时以arr[5]为界分成左右两个数组。

图10.5(e) 快速排序步骤5
  • 左右2数组分别重复上面的过程,知道子数组长度为1.
  1. 代码实现
function quickSort(arr, left, right) {
    let initLeft = left = left!==undefined ? left : 0
    let initRight = right = right!== undefined ? right: arr.length - 1
    if(initLeft >= initRight) return;
    let pivot = arr[initLeft]
    while(left < right) {
    	while(arr[right] >= pivot && left < right) {
    		right --;
    	}
    	if(left < right) {
    		arr[left] = arr[right]
    		left ++;
    	}
    	while(arr[left] <= pivot && left < right){
    		left ++;
    	}
    	if(left < right) {
    		arr[right] = arr[left]
    		right --;
    	}
    }
    arr[left] = pivot
    quickSort(arr, initLeft, left - 1)
    quickSort(arr, left + 1, initRight)
}