JavaScript数据结构与算法(上)
前言:本篇文章主要对编程语言中常见的数据结构进行解释和讨论,并使用JavaScript进行一个封装,因为大家知道,JavaScript是一门弱类型的语言,不仅没有类型检测,一些比如,栈,队列等比较实用且效率较高的数据结构也没有进行封装,因此我们接下来就来讨论一下关于数据结构相关的内容。
一、栈
- 栈是一种先进后出的数据结构,也就是说一个数据如果先进入这个容器,往往后面才能真正的取出来。
- 大家可以看到入栈和进栈都是同一个通道,因此才会导致先进后出的现象。因此我们可以基于数组封装一个只能使用 push和 pop方法的数据结构 ,我们取一个较为适合的名字Stack.
class Stack<T>{
private stack: Array<T> = []
constructor() {
this.stack = []
}
// 压栈
public push(element: T): Array<T> {
this.stack.push(element)
return this.stack
}
// 弹出栈
public pop(): T | null {
if (!this.stack.length) return null
return this.stack.pop()
}
// 获取栈顶元素
public getTop(): T {
return this.stack[this.stack.length - 1]
}
// 获取栈的长度
public getSize(): number {
return this.stack.length
}
// 是否为空
public isEmpty(): boolean {
return this.stack.length === 0
}
}
- 我们使用TS对栈进行一个简单的封装,这个栈有getSize , getTop , isEmpty , pop , push 等方法 , 可以而且仅可以用这些方法对stack进行操作。
- 一起使用起来吧!
二、队列
- 如果理解了栈,那么队列也就特别好理解了,这是一种先进先出的数据结构,javascript的事件任务队列就是基于队列这样的数据结构的。
- 好,认识了队列我们用一张图来描述一下!
- 上面则是队列的图形描述,可以观察到,入队列和出队列是相反的,而且遵循以下原则,入队列口不能够取元素,出队列口不能够加入元素,无论栈还是队列不允许从中间插入元素。了解了这几个原则,我们继续基于JavaScript的数组对队列进行一个封装,我们再取一个好听的名字 Queue.
/ 对队列来进行一个封装
class Queue<T>{
private queue: Array<T> = []
constructor() {
this.queue = []
}
// 进入队列
public in(ele: T): Array<T> {
this.queue.push(ele)
return this.queue
}
// 出队列
public out(): T {
return this.queue.shift()
}
// 获取队列长度
public getSize(): number {
return this.queue.length
}
// 是否为空
public isEmpty(): boolean {
return this.isEmpty.length === 0
}
public getFirst(): T {
return this.queue[0]
}
}
三、哈希表
- 在了解哈希表之前,我们先来了解一个需求,如果我们需要快速查找一个数据比如 , 一下就帮我找到张三的电话,那么你可以这样存
let arr = [ '15242536253' , '15874585692' ]
- 如果你提前知道张三的电话是存在下标为0的位置的,那么你直接通过
arr[0]
就可以拿到相应的电话号码。如果是这样那就太好啦,不过现实是残酷的,在现实中,往往不会是这样的,我们往往不知道下标,我们只知道我们要找张三的电话,所以我们就得这样去存以及查找
let arr = [
{ name:'张三' , tel: '15242536253' },
{ name:'李四' ,tel :'15874585692' }
]
function findTel(name){
return (arr.find(item=>item.name === name)).tel
}
let target = findTel('张三')
- 各位发现了么,其实这样查找好像也可以,不过它似乎不够完美,因为它依然需要遍历,需要遍历每一个,然后找到符合的那一个,我们能不能找到一种结构能够输入一个张三,然后一下帮我们找到电话号码,不需要任何的遍历呢!可能有的小伙伴立马就反应过来了,那我可以用对象呀,但是现在我要告诉你,不能使用对象,至于原因我们后面会解释,但现在请记住不能使用对象,好么! , 这个时候就要好好想想了!
- 提供一种思路,既然数组能够通过下标立马查找到相应的元素 ,那我们可能还是要基于数组咯,那也就意味着我们需要把类似于
张三
这样的字符串转换为下标,我之前天真的以为这TM怎么可能啊
,但是还真可以! - 怎么做呢,我们需要一个函数,我们称之为散列函数,这个函数可以将任意一个字符串转换为一个数字(也就是下标),我们可以要求这个数字必须在某个范围,假如这个函数真能写出来,是不是我们便能够把
张三
变成一个下标,当在创建这个数组的时候我们就将电话号码
放在有散列函数生成的那个下标的位置,所以流程应该是这样的。 - 创建时:张三 -散列函数 -> 2 -> [ null , null , ‘15245236253’ ]
- 查找时 : 输入张三 --> 获得下标2 -->
- 虽然这似乎是个不错的解决方案,但是,革命还未成功呀,我们散列函数还没写出来呢,怎么写呢!
- 有一个思路,我们可以利用每一个字符其实都拥有一个唯一的编码,比如每一个字母都拥有属于他唯一的编码
因此我们就可以最起码将任意一个字符表示为一个的数值了,但是由于数组位置不能重复,所以我们我们必须尽量保持每一个生成的数值的唯一性,如果简单相加肯定是不行的,因为那太容易让多个不同的字符对应的是同一个的数值的情况了,为了避免这种情况,我们可以使用霍纳法则
。
let hashCode = 0
// 霍纳法则
for (var i = 0; i < string.length; i++) {
hashCode = 37 * hashCode + string.charCodeAt(i)
}
console.log(hashCode)
- 有兴趣的同学可以去查看一下这个资料,这里就不细说啦,总之,这个法则可以让任意的字符生成的哈希Code保持与字符尽可能的一一对应,
- 但是
问题来了
,这个哈希Code 也太大了吧,这存到数组里面好多空间都是浪费的,那怎么办呢,可以将其缩小一下。那就是取余
// 问题:如何书写这个函数
// 将所有的文字信息,编码成为数字,再通过取余的方式缩小大数字,最终得到较小的数字。
// 设计属于自己的哈希函数
function hashFunc(string, size) {
let hashCode = 0
// 霍纳法则
for (var i = 0; i < string.length; i++) {
hashCode = 37 * hashCode + string.charCodeAt(i)
}
console.log(hashCode)
return hashCode % size
}
-
但是即便是这样,也依然会存在问题,无论我们如何努力,其实肯定会存在冲突的情况,那就是某些不同的字符映射到了数组某个相同的位置,这个时候就迎来一个比较大的课题了,那就是解决冲突!
-
如何解决冲突,其实方法还是比较多的,但是我这里介绍一种比较常用和易于理解的方式,那就是链表法,本质上我们可以这样理解,
-
虽然有冲突,但是冲突的个数应该是比较少的,所以我们可暂时用一个二维数组来进行表示,也就是主数组的某个位置 也用数组保存多个冲突的元素,他们拥有同一个位置。
-
这样的结构我们就叫做哈希表 (也叫做散列表)
-
据说,facebook的数据库就是用这样的数据结构存储的,各位发现了么,通过这样的方式,我们直接可以通过将某个字符映射成下标位置,这样查找的速度就会非常的快,即便遇到冲突了,我们也只需要遍历冲突的那几个元素就好了,因为好的散列表会尽可能让整个散列表分布均匀,且冲突尽可能的少呢!
-
在来看一下数组,如果真的将海量的数组存到数组当中,那不是要一个一个遍历好久,及其浪费性能。
-
是时候回答一下我们为什么不使用对象了,熟悉对象的伙伴可能知道,使用对像很轻松就是实现这个需求,那我们为什么不用对象呢!那我就要告诉各位,javascript其实也是基于哈希表去实现对象的,之所有对象有这样的查找速度不用根据下标进行查找,其实也是基于哈希表的。所以各位接下来请记住,对象不是理所应当就出现了哦!
将了一大堆是时候也来实现一下哈希表啦!打起精神看代码啦!答应我,一定要看完,有问题也可以私信,一起交流!
class HashTable {
constructor(size = 7) {
this.storage = []
this.size = size
this.count = 0
}
hashFunc(string, size) {
let hashCode = 0
// 霍纳法则
for (var i = 0; i < string.length; i++) {
hashCode = 37 * hashCode + string.charCodeAt(i)
}
return hashCode % size
}
put(key, value) {
let index = this.hashFunc(key, this.size)
if (!this.storage[index]) {
// 说明为空
let butket = [{ key, value }]
this.storage[index] = butket
this.count++
} else {
let obj = this.storage[index].find(ele => {
return ele.key == key
});
if (obj) {
obj.value = value
} else {
this.storage[index].push({ key, value })
this.count++
}
}
}
get(key) {
let index = this.hashFunc(key, this.size)
if (!this.storage[index]) {
return null
} else {
let obj = this.storage[index].find(ele => {
return ele.key == key
})
return obj ? obj : null
}
}
remove(key) {
if (!get(key)) {
return false
} else {
let index = this.storage[index].findIndex(ele => {
return ele.key == key
})
this.storage[index].splice(index, 1)
this.count--
return true
}
}
isEmpty() {
return this.count === 0
}
count() {
return this.count
}
// 扩容
resize(newSize) {
let oldStorage = this.storage
this.storage = []
oldStorage.forEach(item => {
if (item && item.length > 0) {
item.forEach(e => {
this.put(e, key, e.value)
})
}
})
}
isPrime(num) {
for (var i = 2; i < num; i++) {
if (num % i === 0) {
return false
}
}
return true
}
getPrime(num) {
while (!this.isPrime(num)) {
num++
}
return num
}
}
恭喜你,看完了哈希表的封装,可以去大吃一顿,犒劳一下自己!
四、字典
- 如果我说其实在javascript中其实也有字典这个数据类型,你会不会怀疑呢!其实却是是有的。那就是Map类型,其实在很多语言中都有字典这个类型的,标准的键值对的映射关系,因为还是比较简单,的我们不进行封装了,大家有兴趣一定要去研究一下Map和Object的区别!这个还是比较重要的。
let map = new Map()
map.set('a' , 1)
map.get('a') // 1
五、集合
- 另外还有一种数据类型,很重要,表示一种不会重复的数据类型,那就是集合,集合是一个特殊的存在,他特别想数组,但是他有个很好的特点那就是内部元素绝对不会重复。在很多语言当中都有这样的一种类型,在Javascript中其实也有,那就是Set,为了了解其本质,我们还是来封装一下吧!
class Set {
constructor() {
this.box = {}
this.length = 0
}
add(element) {
if (this.has(element)) {
return false
}
if (typeof element === 'object') {
let key = JSON.stringify(element)
this.box[key] = element
this.length++
} else {
this.box[element] = element
this.length++
}
}
has(element) {
if (typeof element === 'object') {
let key = JSON.stringify(element)
return this.box.hasOwnProperty(key)
} else {
return this.box.hasOwnProperty(element + '')
}
}
values() {
let values = []
for (var key in this.box) {
values.push(this.box[key])
}
return values
}
clone() {
let copy = new Set()
for (var key in this.box) {
copy.add(this.box[key])
}
return copy
}
clear() {
this.box = {}
}
size() {
return this.length
}
isEmpty() {
return this.length === 0
}
remove(element) {
if (!this.has(element)) {
return null
} else {
element = typeof element === 'object' ? JSON.stringify(element) : element
delete this.box[element]
}
}
union(set) {
// 了解即可,求并集
if (!(set instanceof Set)) {
return console.warn('不是Set类型')
} else {
let copy = this.clone()
for (var key in set.box) {
copy.add(set.box[key])
}
return copy
}
}
intersection(set) {
// 了解即可,求交集
if (!(set instanceof Set)) {
return console.warn('不是Set类型')
} else {
let newset = new Set()
let resorse = set.box
for (var key in resorse) {
if (this.has(resorse[key])) {
newset.add(resorse[key])
}
}
return newset
}
}
diff(set) {
// 了解即可,求差集
let copy = this.clone()
let intersection = this.intersection(set)
for (var key in intersection.box) {
copy.remove(intersection.box[key])
}
return copy
}
childOf(set) {
if (set.length < this.length) {
return false
} else {
return this.values().every(item => {
return set.has(item)
})
}
}
}
六、链表
- 接下来是一个很不错的话题,建议你吃饱喝足之后,状态满满之后再来看哦!
- 链表是什么,它就像是一辆火车,每个元素都有至少两个属性,一个属性保存着当前的值,另一个则保存着指向下一个元素的引用。这样由元素构成的一串一串的数据结构就是链表!上一张图,感受一下吧!
怎么样,是不是秒懂啦,没错这就是链表,但是上面的其实是单向链表,也就是上一个节点可以指向下一个节点,但是下一个节点没有指向上一个节点,如果下一个节点可以指向上一个节点的话,就形成了双向链表!
- 了解了链表的分类了之后,我们就要开始封装了,值得注意的是,这时候我们不是基于数组,我们要创建一个新的数据结构,我们给这个类取个好听的名字List.
class Node {
constructor(value, next) {
this.value = value
this.next = next
}
next() {
console.log('asdf')
return this.next.value
}
}
class List {
constructor() {
this.length = 0
this.head = null
}
// a b c d e f
append(element) {
const newNode = new Node(element, null)
this.length++
if (!this.head) {
this.head = newNode
} else {
//
let current = this.head
while (current.next) {
current = current.next
}
current.next = newNode
}
return this
}
toString() {
let str = ''
let current = this.head
while (current.next) {
str += current.value + "-"
current = current.next
}
str += current.value
return str
}
insert(position, element) {
if (this.length === 0) {
this.head = element
} else {
if (position > this.length || position < 0) {
return false
} else {
const newNode = new Node(element, null)
let current = null
let tmp = ''
for (var i = 0; i < position; i++) {
current = this.head.next
}
tmp = current.next
current.next = newNode
newNode.next = tmp
}
}
this.length++
}
get(position = 0) {
if (position > this.length || position < 0) {
return null
}
let current = this.head
for (var i = 0; i < position; i++) {
current = current.next
}
return current
}
indexOf(element) {
let index = 0
let current = this.head
for (var i = 0; i < this.length; i++) {
if (element === current.value) {
return index
}
current = current.next
index++
}
return -1
}
update(position, element) {
if (position > this.length || position < 0) {
return false
} else {
let current = this.head
let prev = null
for (var i = 0; i < position; i++) {
prev = current
current = current.next
}
const newNode = new Node(element, current.next)
prev.next = newNode
}
}
removeAt(position) {
if (position > this.length || position < 0) {
return null
} else if (position === 0) {
this.head = this.head.next
} else {
let current = this.head
let prev = null
for (var i = 0; i < position; i++) {
prev = current
current = current.next
}
prev.next = current.next
this.length--
return current
}
}
remove(element) {
this.length--
let index = this.indexOf(element)
return this.removeAt(index)
}
isEmpty() {
return this.length === 0
}
size() {
return this.length
}
}
-
这样今后我们在javascript中也可以使用链表了,可以有的小伙伴有疑问,我们不是已经有数组了么,为什么还要搞什么链表呢,弄这么多花里胡哨的干嘛!是的,之前我也会有这样的疑问,其实从功能上来看,其实确实,只要链表能做的,其实数组也都能做,不过,有一点我们需要注意的是,他们在处理同一个业务需求场景,可能消耗的性能是不一样的,这便是我们要去研究原因。
-
目前我们可以得出,链表更适合于插入和删除操作,尤其在中间插入元素。因为链表只需要改变一下指针就可以了。
-
数组更适合根据下标查找操作,这样可以不用遍历。但是链表无论查找任意元素都需要从第一个开始一个一个遍历找。