JavaScript数据结构——集合

105 阅读7分钟

前言

嘿,掘友们!今天我们来了解并实现数据结构 —— 集合。

本文内容包括

  • 实现集合类

  • 数学计算(并集、交集、差集、子集)

  • ES6 原生 Set 类

集合

集合是一组无序且唯一的项组成。该数据结构使用了与有限集合相同的数学概念。

在数学中,集合是一组不同对象的集。比如,一个由大于或等于0的整数组成的自然数集合:N = {0, 1, 2, 3, 4, 5}。集合中的对象列表用花括号 {} 包围。

还有一个概念叫空集。空集就是不包含任何元素的集合。空集用 {} 表示。

我们也可以把集合想象成一个没有重复元素且没有顺序的数组。

集合存在并集、交集、差集等基本运算。

实现集合类

我们可以使用对象或者数组来表示集合。这里使用对象来实现集合类,最后直接给出用数组实现集合类的代码,供掘友自己练习参考。

这里为什么使用对象来表示集合呢?因为 JavaScript 的对象不允许一个键指向两个不同的属性,保证了集合里的元素都是唯一的。

首先,创建一个Set类和它的构造函数声明。

class Set {
  constructor() {
    this.items = {}
  }
}

然后,声明一些集合可用的方法。

  • add(element) 添加一个新元素
  • delete(element) 移除一个元素
  • has(element) 元素是否在集合内,在返回 true,不在返回 false
  • clear() 清空集合
  • size() 集合内元素的数量
  • values() 返回一个包含集合中所有值的数组

has(element) 方法

我们先实现 has(element) 方法,因为他会被 add、delete 等其他方法调用。

has(element) {
  return element in this.items
}

因为我们是使用对象来存储集合的元素,可以使用 in 运算符来验证给定元素是否是对象的属性。

这个方法还有更好的实现方式:

has(element) {
  return Object.prototype.hasOwnProperty.call(this.items, element)
}

Object 原型有 hasOwnProperty 方法。该方法返回一个表明对象是否具有特定属性的布尔值。in 运算符返回表示对象在原型链上是否有特定属性的布尔值。

当然我们也可以使用 this.items.hasOwnProperty(element)。如果对象上的hasOwnProperty方法被覆盖,会导致代码不能正常工作。避免出现这种情况,使用 Object.prototype.hasOwnProperty.call 是更安全的。

add(element) 方法

add(element) {
  if(!this.has(element)) {
    this.items[element] = element
    return true
  }
  return false
}

为保证集合内无重复元素,要检查 element 是否存在与集合内。如果不存在,就把 element 添加到集合中,返回 true,表示成功添加了元素。如果集合内存在了这个元素,就返回 false,表示没有添加它。

delete 和 clear 方法

delete(element) {
  if(this.has(element)) {
    delete this.items[element]
    return true
  }
  return false
}

delete 方法中,我们要验证给定的 element 是否存在集合中,如果存在,就从集合中移除它,返回 true,表示元素被移除,否则返回 false

我们使用对象来存储集合的元素,可以使用 delete 运算符移除 items 对象的属性。

如果我们想移除集合中的所有值,使用 clear 方法,重置 items 对象,把一个空对象重新复制给它。我们也可以迭代集合,用 delete 方法依次移除所有的值,不过这样做太麻烦了。

clear() {
  this.items = {}
}

size 方法

实现 size 方法,有三种实现方式。

第一种方式是在构造函数中添加一个 count 变量,每当使用 adddelete 方法时控制 count 加减。

第二种方式是使用 Object 类的 keys 方法,它会返回一个包含给定对象所有属性的数组,使用数组的 length 属性来返回 items 对象的属性个数。

size() {
  return Object.keys(this.items).length
}

第三种方式是手动提取 items 对象的每个属性,记录属性的个数并返回这个数。这个方法也比较麻烦,我在这里实现一下。

sizeLegacy() {
  let count = 0
  for(let i in this.items) {
    if(this.items.hasOwnProperty(i)) {
      count++
    }
  }
  return count
}

values 方法

要实现这个方法,我们可以使用 Object 类内置的 values 方法

values() {
  return Object.values(this.items)
}

Object.values() 方法返回一个包含给定对象所有属性值的数组。这个方法是在 ECMAScript 2017 中被添加,只在现代浏览器中可用。

如果想让代码在任何浏览器中都能执行,可以用于之前代码等价的下面这段代码

valuesLegacy() {
  let values = []
  for(let key in this.items) {
    if(this.items.hasOwnProperty(key)) {
      values.push(key)
    }
  }
  return values
}

完整代码

对象实现 Set 类

class Set {
  constructor() {
    this.items = {}
  }
  add(element) {
    if(!this.has(element)) {
      this.items[element] = element
      return true
    }
    return false
  }
  delete(element) {
    if(this.has(element)) {
      delete this.items[element]
      return true
    }
    return false
  }
  has(element) {
    // return element in this.items
    return Object.prototype.hasOwnProperty.call(this.items, element)
  }
  clear() {
    this.items = {}
  }
  size() {
    return Object.keys(this.items).length
  }
  values() {
    return Object.values(this.items)
  }
}

数组实现 Set 类

class arraySet {
  constructor() {
    this.items = []
  }
  has(element) {
    return this.items.includes(element)
    return this.items.indexOf(element) !== -1
  }
  add(element) {
    if(!this.has(element)) {
      this.items.push(element)
      return true
    }
    return false
  }
  delete(element) {
    if(this.has(element)) {
      for(let i in this.items) {
        if(this.items[i] === element) {
          this.items.splice(i, 1)
          return
        }  
      }
      return true
    }
    return false
  }
  clear() {
    this.items = []
  }
  size() {
    return this.items.length
  }
  values() {
    return this.items
  }
}

集合运算

并集

给定两个集合,返回一个包含两个集合中所有元素的新集合

union(otherSet) {
  const unionSet = new Set()
  this.values().forEach(value => unionSet.add(value))
  otherSet.values().forEach(value => unionSet.add(value))
  return unionSet
}

首先需要创建一个新集合,代表两个集合的并集。然后获取第一个集合所有的值,迭代并全部添加到代表并集的结合中,然后对第二个集合做相同的事,最后返回并集。

交集

给定两个集合,返回一个包含两个集合中共有元素的新集合

intersection(otherSet) {
  const intersectionSet = new Set()
  const values = this.values()
  const otherValues = otherSet.values()
  let biggerSet = values
  let smallSet = otherValues
  if(otherValues.length - values.length > 0) {
    biggerSet = otherValues
    smallSet = values
  }
  smallSet.forEach(value => {
    if(biggerSet.has(value)) {
      intersectionSet.add(value)
    }
  })
  return intersectionSet
}

首先创建一个新的集合,代表两个集合的并集。然后我们要获取两个集合实例中的值,先假定给定集合的元素多与另一个集合,比较两个集合的元素个数,如果另一个集合的元素多与给定集合,我们就交换 biggerSetsmallSet 的值。最后迭代较小的集合来计算出两个集合的共有元素并返回。

差集

返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合

difference(otherSet) {
  const differenceSet = new Set()
  this.values.forEach(value => {
    if(!otherSet.has(value)) {
      differenceSet.add(value)
    }
  })
  return differenceSet
}

我们不想改变原来的集合,就创建一个新的集合,代表两个集合的差集。这个函数实现的功能是给定集合 - 另一个集合。迭代给定集合,如果另一个集合不存在,则向差集添加元素,最后返回差集。

子集

验证一个给定集合是否是另一个集合的子集

inSubsetOf(otherSet) {
  if(this.size() > otherSet.size()) return false
  let isSubset = true
  this.values().every(value => {
    if(!otherSet.has(value)) {
      isSubset = false
      return false
    }
    return true
  })
  return isSubset
}

首先验证当前 Set 的大小,如果当前实例中的元素比 otherSet 实例更多,它就不是一个子集。子集的元素个数应小于或等于要比较的集合。

然后验证当前 Set 实例中的元素是否都存在于 otherSet 中,如果有一个不再就返回 false。如果都在,就返回 true

这里没有像并集、交集和差集一样,使用 forEach 方法,而是使用 every 方法。在子集逻辑汇总,当我们发现一个值不存在于 otherSet 时,可以停止迭代,表示这不是一个子集。

ES6 Set

我们先来看看原生的 Set 类的属性和方法

属性

  • size 返回 Set 对象中元素的个数

方法

  • add(value) 向 Set 对象的末尾添加一个特定的值
  • has(value) 返回一个布尔值指示value 是否存在 Set 对象中
  • clear() 清空一个 Set 对象中的所有元素
  • delete(value) 删除指定的元素
  • values() 按照元素插入顺序返回一个具有 Set 对象每个元素值的全新 Iterator 迭代器对象
  • entries() 返回一个 [value, value] 形式的数组迭代器对象
  • forEach(callback) 根据集合中元素的插入顺序,依次执行提供的回调函数

模拟并集运算

const union = (setA, setB) => {
  const unionSet = new Set()
  setA.foreach(value => unionSet.add(value))
  setB.foreach(value => unionSet.add(value))
  return unionSet
}

模拟交集运算

const intersection = (setA, setB) => {
  const intersectionSet = new Set()
  setA.foreach(value => {
    if(setB.has(value)) {
      intersection.add(value)
    }
  })
  return intersectionSet
}

模拟差集运算

const difference = (setA, setB) => {
  const differenceSet = new Set()
  setA.foreach(value => {
    if(!setB.has(value)) {
      differenceSet.add(value)
    }
  })
  return differenceSet
}

模拟子集运算

const inSubsetOf = (setA, setB) => {
  if(setA.size > setB.size) return false
  let isSubset = true
  setA.forEach(value => {
    if(!setB.has(value)) {
      isSubset = false 
      return
    }
  })
  return isSubset
}

使用扩展运算符

我们可以使用扩展运算符,更快的实现并集、交集和差集。

const setA = new Set()
const setB = new Set()

// 并集
console.log(new Set([...setA, ...setB]))
// 交集
console.log(new Set([...setA].filter(x => setB.has(x))))
// 差集
console.log(new Set([...setA].filter(x => !setB.has(x))))

结语

集合的逻辑相对比较简单,集合最大的特点就是元素不重复。也掌握了如何进行并集、交集、差集和子集的数学运算。