集合-運算實現

致于数据科学家的小陈發表於2024-06-21

前面對集合的定義做了一個基本實現, 這篇針對集合的運算 (交, 並, 差) 進行一個補充. 集合這塊我在資料處理方面用的還是比較多的.

集合定義

  • has() , 判斷元素是否在集合中
  • add() , 新增元素
  • delete() , 刪除元素
  • size() , 集合大小
  • values() , 集合值組成的陣列
class Set {
  constructor() {
    // 這裡用物件來實現集合, key 唯一比較方便, 當然也可以用陣列
    this.obj = {} 
  }

  // 查詢元素是否在集合中
  has(element) {
    // return element in this.obj 
    return Object.prototype.hasOwnProperty.call(this.obj, element)
  }

  // 新增元素
  add(element) {
    if (! this.has(element)) {
      this.obj[element] = element
      return true
    }
    return false 
  }

  // 刪除元素
  delete(element) {
    if (this.has(element)) {
      // 能用 delete 是因為底層用的是物件
      delete this.obj[element]
      return true 
    }
    return false 
  }

  // 清空集合
  clear() {
    this.obj = {}
  }

  // 集合元素個數
  size() {
    // 方法1: 使用一個 count 變數, 每次新增/刪除時進行控制
    // 方法2: 用 ES6 語法, Object.keys().length 返回鍵組成的陣列
    // return Object.keys(this.obj).length

    // 方法3: 手動提取底層物件的屬性進行計數
    let count = 0
    for (let key in this.obj) {
      // in 會包含原型鏈上的, hasOwnProperty() 只查詢例項自身的
      if (this.obj.hasOwnProperty(key)) count++
    }
    return count 
  }

  // 返回集合元素值
  values() {
    // 方法1: 用 ES6, Object.values() 返回值組成的陣列
    // return Object.values(this.obj)

    // 方法2: 遍歷物件 key 都存起來
    let arr = []
    for (let key in this.obj) {
      if (this.obj.hasOwnProperty(key)) arr.push(key)
    }
    return arr 
  }
}

集合運算

  • union() , 並集運算
  • intersection() , 交集運算
  • difference() , 差集運算
  • isSubSet() , 是否為子集

並集運算

對於集合 A, B, 分別遍歷出兩個集合元素, 都新增到新的結果集合中即可.

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

交集運算

對於集合 A, B, 遍歷集合 A 的每個元素時, 用集合B的 has() 方法判斷是否存在集合 B中.

// 交集 intersection
intersection(otherSet) {
    const intersectionSet = new Set()
    
    const values = this.values()
    if (!values) return undefined 
    
    for (let i = 0; i < values.length; i++) {
        const curValue = values[i]
        if (otherSet.has(curValue)) intersectionSet.add(curValue)
    }
    return intersectionSet
}

這裡有個最佳化的點是, 如果我們事先知道了 A, B 那個元素更少, 則直接遍歷更少的, 效率就更高.

// 交集最佳化
intersection2(otherSet) {
    const intersectionSet = new Set()
    
    // 區分出長度, 只遍歷短的即可
    let bigSet = this.values()
    let smallSet = otherSet.values()
    
    if (bigSet.length < smallSet.length) {
        bigSet = otherSet.values()
        smallSet = bigSet.values
    }
    
    // 用更短的遍歷
    smallSet.forEach(value => {
        if (bigSet.includes(value)) intersectionSet.add(value)
    })
    return intersectionSet
}

差集運算

對於集合 A, B, 差集就是 A-B = {X|X∈A ^ X !∈ B}, 即 A 有, 但B 沒有的元素.

// 差集 A-B = {X|X∈A ^ X !∈ B}
difference(otherSet) {
    const differenceSet = new Set()
    
    // 遍歷集合A的元素值, 如果它在B中不存在, 就拿捏
    this.values().forEach(value => {
        if (! otherSet.has(value)) differenceSet.add(value)
    })
    return differenceSet
}

判斷子集

對於集合 A, B, 如果 A ∈ B , 即 A 的所有元素都在 B 中, 則稱 A 是 B 的子集.

// 子集: A∈B
isSubSet(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
}

完整實現

class Set {
  constructor() {
    // 這裡用物件來實現集合, key 唯一比較方便, 當然也可以用陣列
    this.obj = {} 
  }

  // 查詢元素是否在集合中
  has(element) {
    // return element in this.obj 
    return Object.prototype.hasOwnProperty.call(this.obj, element)
  }

  // 新增元素
  add(element) {
    if (! this.has(element)) {
      this.obj[element] = element
      return true
    }
    return false 
  }

  // 刪除元素
  delete(element) {
    if (this.has(element)) {
      // 能用 delete 是因為底層用的是物件
      delete this.obj[element]
      return true 
    }
    return false 
  }

  // 清空集合
  clear() {
    this.obj = {}
  }

  // 集合元素個數
  size() {
    // 方法1: 使用一個 count 變數, 每次新增/刪除時進行控制
    // 方法2: 用 ES6 語法, Object.keys().length 返回鍵組成的陣列
    // return Object.keys(this.obj).length

    // 方法3: 手動提取底層物件的屬性進行計數
    let count = 0
    for (let key in this.obj) {
      // in 會包含原型鏈上的, hasOwnProperty() 只查詢例項自身的
      if (this.obj.hasOwnProperty(key)) count++
    }
    return count 
  }

  // 返回集合元素值
  values() {
    // 方法1: 用 ES6, Object.values() 返回值組成的陣列
    // return Object.values(this.obj)

    // 方法2: 遍歷物件 key 都存起來
    let arr = []
    for (let key in this.obj) {
      if (this.obj.hasOwnProperty(key)) arr.push(key)
    }
    return arr 
  }

  // 集合運算
  // 並集 union
  union(otherSet) {
    const unionSet = new Set()
    // 分別遍歷出兩個集合元素, 都新增到新集合裡面即可
    this.values().forEach(value => unionSet.add(value))
    otherSet.values().forEach(value => unionSet.add(value))
    return unionSet
  }

  // 交集 intersection
  intersection(otherSet) {
    const intersectionSet = new Set()
    // 遍歷集合A的每個元素時, 用集合B的 has() 方法判斷是否存在集合B中
    const values = this.values()
    if (!values) return undefined
    
    for (let i = 0; i < values.length; i++) {
      const curValue = values[i]
      if (otherSet.has(curValue)) {
        intersectionSet.add(curValue)
      }
    }
    return intersectionSet
  }

  // 交集最佳化
  intersection2(otherSet) {
    const intersectionSet = new Set()

    // 區分出長度, 然後用短的遍歷
    let biggerSet = this.values()
    let smallSet = otherSet.values()

    if (biggerSet.length < smallSet.length) {
      biggerSet = otherSet.values()
      smallSet = this.values()
    }
    // 用短的進行遍歷
    smallSet.forEach(value => {
      if (biggerSet.includes(value)) {
        intersectionSet.add(value)
      }
    })
    return intersectionSet
  }

  // 差集 A-B = {X|X∈A ^ X !∈ B}
  difference(otherSet) {
    const differenceSet = new Set()
    // 遍歷就行
    this.values().forEach(value => {
      if (! otherSet.has(value)) differenceSet.add(value)
    })
    return differenceSet
  }

  // 子集: A∈B
  isSubSet(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
  }

}


// test 
const setA = new Set()
setA.add(1)
setA.add(2)
setA.add(3)

const setB = new Set()
setB.add(3)
setB.add(4)
setB.add(5)

const unionAB = setA.union(setB)
console.log('A + B: ', unionAB.values());

// const intersectionAB = setA.intersection(setB)
const intersectionAB = setA.intersection2(setB)
console.log('A && B:', intersectionAB.values());

const differenceAB = setA.difference(setB)
console.log('A-B: ', differenceAB);

const isSubSetAB = setA.isSubSet(setB)
console.log('A∈B:', isSubSetAB);

測試輸出:

A + B:  [ '1', '2', '3', '4', '5' ]

A && B: [ '3' ]

A-B:  Set { obj: { '1': '1', '2': '2' } }

A∈B: false

至次, 集合的基本應用就到這裡啦.

相關文章