JavaScript数据结构与算法

JavaScript数据结构与算法(上)

前言:本篇文章主要对编程语言中常见的数据结构进行解释和讨论,并使用JavaScript进行一个封装,因为大家知道,JavaScript是一门弱类型的语言,不仅没有类型检测,一些比如,栈,队列等比较实用且效率较高的数据结构也没有进行封装,因此我们接下来就来讨论一下关于数据结构相关的内容。

一、栈
  1. 栈是一种先进后出的数据结构,也就是说一个数据如果先进入这个容器,往往后面才能真正的取出来。

栈结构

  1. 大家可以看到入栈和进栈都是同一个通道,因此才会导致先进后出的现象。因此我们可以基于数组封装一个只能使用 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进行操作。
  • 一起使用起来吧!
二、队列
  1. 如果理解了栈,那么队列也就特别好理解了,这是一种先进先出的数据结构,javascript的事件任务队列就是基于队列这样的数据结构的。
  2. 好,认识了队列我们用一张图来描述一下!
    队列结构
  3. 上面则是队列的图形描述,可以观察到,入队列和出队列是相反的,而且遵循以下原则,入队列口不能够取元素,出队列口不能够加入元素,无论栈还是队列不允许从中间插入元素。了解了这几个原则,我们继续基于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]
  }
}
三、哈希表
  1. 在了解哈希表之前,我们先来了解一个需求,如果我们需要快速查找一个数据比如 , 一下就帮我找到张三的电话,那么你可以这样存 let arr = [ '15242536253' , '15874585692' ]
  2. 如果你提前知道张三的电话是存在下标为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中也可以使用链表了,可以有的小伙伴有疑问,我们不是已经有数组了么,为什么还要搞什么链表呢,弄这么多花里胡哨的干嘛!是的,之前我也会有这样的疑问,其实从功能上来看,其实确实,只要链表能做的,其实数组也都能做,不过,有一点我们需要注意的是,他们在处理同一个业务需求场景,可能消耗的性能是不一样的,这便是我们要去研究原因。

  • 目前我们可以得出,链表更适合于插入和删除操作,尤其在中间插入元素。因为链表只需要改变一下指针就可以了。

  • 数组更适合根据下标查找操作,这样可以不用遍历。但是链表无论查找任意元素都需要从第一个开始一个一个遍历找。

结语:本来想一口气写完的,不过已经有1万字了,考虑的读者的阅读体验,我剩下的部分写在下一篇吧,涉及到 树 、图 、遍历算法 、排序算法。希望我们能在下一章一起交流!加油!
  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值