資料結構之Set | 讓我們一塊來學習資料結構

guojikun發表於2021-05-12

陣列(列表)、棧、佇列和連結串列這些順序資料結構對你來說應該不陌生了。現在我們要學習集合,這是一種不允許值重複的順序資料結構。我們將要學到如何建立集合這種資料結構,如何新增和移除值,如何搜尋值是否存在。你也會學到如何進行並集、交集、差集等數學運算。

本章內容包括:

  • 從頭建立一個 Set 類
  • 用 Set 來進行數學運算

構建資料集合

集合是由一組無序且唯一(即不能重複)的項組成的。該資料結構使用了與有限集合相同的數學概念,但應用在電腦科學的資料結構中。

在深入學習集合的電腦科學實現之前,我們先看看它的數學概念。在數學中,集合是一組不同物件的集。

比如說,一個由大於或等於 0 的整陣列成的自然數集合:N = {0, 1, 2, 3, 4, 5, 6, …}。集合中的物件列表用花括號({})包圍。

還有一個概念叫空集。空集就是不包含任何元素的集合。比如 24 和 29 之間的素數集合,由於 24 和 29 之間沒有素數(除了 1 和自身,沒有其他正因數的、大於 1 的自然數),這個集合就是空集。空集用{ }表示。

你也可以把集合想象成一個既沒有重複元素,也沒有順序概念的陣列。在數學中,集合也有並集、交集、差集等基本運算。本文也會介紹這些運算。

建立集合類

建立基礎類

用下面的 Set 類以及它的建構函式宣告作為開始。

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

有一個非常重要的細節是,我們使用物件而不是陣列來表示集合(items)。不過,也可以用陣列實現。此處用物件來實現,和我們在第 4 章與第 5 章中學習到的物件實現方式很相似。同樣地,JavaScript 的物件不允許一個鍵指向兩個不同的屬性,也保證了集合裡的元素都是唯一的。

接下來,需要宣告一些集合可用的方法(我們會嘗試模擬與 ECMAScript 2015 實現相同的Set 類)。

  • add(element):向集合新增一個新元素。
  • delete(element):從集合移除一個元素。
  • has(element):如果元素在集合中,返回 true,否則返回 false。
  • clear():移除集合中的所有元素。
  • size():返回集合所包含元素的數量。它與陣列的 length 屬性類似。
  • values():返回一個包含集合中所有值(元素)的陣列。

has(item)方法

首先要實現的是 has(element)方法,因為它會被 adddelete 等其他方法呼叫。它用來檢驗某個元素是否存在於集合中,下面看看它的實現。

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

除了使用Object.prototype.hasOwnProperty方法實現之外,還可以使用item in this.itemsthis.items.hasOwnProperty(item)來實現has方法。

add(item)方法

接下來要實現 add(item) 方法。

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

對於給定的 item,可以檢查它是否存在於集合中。如果不存在,就把 item 新增到集合中,並返回 true,表示新增了該元素。如果集合中已經有了這個元素,就返回 false,表示沒有新增它。

delete(item) 和 clear() 方法

下面要實現 delete(item) 方法。

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

在 delete 方法中,我們會驗證給定的 item 是否存在於集合中。如果存在,就從集合中移除 item,返回 true,表示元素被移除;否則返回 false。

由於我們是使用物件來儲存集合的items物件,那麼就可以簡單的使用物件的delete運算子從items中刪除元素。

如果想移除集合中的所有值,可以用 clear 方法。

clear() {
    this.items = {}
}

size() 方法

實現size方法有幾種方式:

  1. 使用一個 length 變數,每當使用 add 或 delete 方法時就控制它
  2. Object.keys(this.items).length
  3. 使用for in (要記得使用hasOwnProperty判斷一下)
  4. ...

現在我們使用第2中方式來實現,程式碼如下

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

values() 方法

要實現 values() 方法,我們同樣可以使用 Object 類內建的 values 方法。

values() {
    return Object.values(values); 
}

Object.values()方法返回了一個包含給定物件所有屬性值的陣列。它是在ECMAScript 2017 中被新增進來的,目前只在現代瀏覽器中可用。

使用Set類

現在資料結構已經完成了,看看如何使用它吧。試著執行一些命令,測試我們的 Set 類。

const set = new Set(); 
set.add(1); 
console.log(set.values()); // 輸出[1] 
console.log(set.has(1)); // 輸出 true 
console.log(set.size()); // 輸出 1 
set.add(2); 
console.log(set.values()); // 輸出[1, 2] 
console.log(set.has(2)); // 輸出 true 
console.log(set.size()); // 輸出 2 
set.delete(1); 
console.log(set.values()); // 輸出[2] 
set.delete(2); 
console.log(set.values()); // 輸出[] 

集合運算

集合是數學中基礎的概念,在計算機領域也非常重要。它在電腦科學中的主要應用之一是資料庫,而資料庫是大多數應用程式的根基。集合被用於查詢的設計和處理。當我們建立一條從關係型資料庫(Oracle、Microsoft SQL Server、MySQL 等)中獲取一個資料集合的查詢語句時,使用的就是集合運算,並且資料庫也會返回一個資料集合。當我們建立一條 SQL 查詢命令時,可以指定是從表中獲取全部資料還是獲取其中的子集;也可以獲取兩張表共有的資料、只存在於一張表中的資料(不存在於另一張表中),或是存在於兩張表內的資料(通過其他運算)。這些 SQL 領域的運算叫作聯接,而 SQL 聯接的基礎就是集合運算。

我們可以對集合進行如下運算。

  • 並集:對於給定的兩個集合,返回一個包含兩個集合中所有元素的新集合。
  • 交集:對於給定的兩個集合,返回一個包含兩個集合中共有元素的新集合。
  • 差集:對於給定的兩個集合,返回一個包含所有存在於第一個集合且不存在於第二個集合的元素的新集合。
  • 子集:驗證一個給定集合是否是另一集合的子集。

重要的是要注意,本文實現的 unionintersectiondifference 方法不會修改當前的 Set 類例項或是作為引數傳入的 otherSet。沒有副作用的方法和函式被稱為純函式。純函式不會修改當前的例項或引數,只會生成一個新的結果。這在函數語言程式設計中是非常重要的概念。

並集

並集的數學概念。集合 A 和集合 B 的並集表示為 \(A ∪ B\),定義如下。

\[A ∪ B = { x ∣ x ∈ A ∨ x ∈ B } \]

意思是 x(元素)存在於 A 中,或 x 存在於 B 中。下圖展示了並集運算。

union.png

程式碼實現

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

交集

交集的數學概念。集合 A 和集合 B 的交集表示為 \(A ∩ B\),定義如下。

\[A ∩ B = { x ∣ x ∈ A ∧ x ∈ B } \]

意思是 x(元素)存在於 A 中,且 x 存在於 B 中。下圖展示了交集運算。

intersection.png

程式碼實現

intersection(otherSet) {
    let intersectionSet = new Set();
    const values = this.values();
    const otherValues = otherSet.values();
    let smallerValues = values;
    let biggerValues = otherValues;
    if (otherValues.length < values.length) {
        smallerValues = otherValues;
        biggerValues = values;
    }
    smallerValues.forEach((value) => {
        if (biggerValues.includes(value)) {
            intersectionSet.add(value);
        }
    });
    return intersectionSet;
}

> 為了減少迴圈次數,在程式碼中判斷了哪個集合的長度最小,然後迴圈長度較小的集合,以達到減少迴圈次數的目的。

差集

差集的數學概念。集合 A 和集合 B 的差集表示為 \(A - B\),定義如下。

\[A ∪ B = { x ∣ x ∈ A ∧ x ∉ B } \]

意思是 x(元素)存在於 A 中,且 x 不存在於 B 中。下圖展示了集合 A 和集合 B 的差集運算。

difference.png

程式碼實現

difference(otherSet) {
    let differenceSet = new Set();

    this.values().forEach((value) => {
        if (!otherSet.has(value)) {
            differenceSet.add(value);
        }
    });
    return differenceSet;
}

子集

要介紹的最後一個集合運算是子集。其數學概念的一個例子是集合 A 是集合 B 的子集(或集合 B 包含集合 A),表示為 \(A ∈ B\),定義如下。

\[A ∪ B = { x ∣ ∀x ∈ A => x ∈ B } \]

意思是集合 A 中的每一個 x(元素),也需要存在於集合 B 中。下圖展示了集合 A 是集合 B 的子集。

isSubsetOf.png

程式碼實現

isSubsetOf(otherSet) {
    if (this.size() > otherSet.size()) {
        return false;
    }
    let isSubset = true;
    const values = this.values();
    const size = this.size();
    for (let i = 0; i < size; i++) {
        if (!otherSet.has(values[i])) {
            isSubset = false;
            break;
        }
    }
    return isSubset;
}

參考資料

  • 學習JavaScript資料結構與演算法第三版

其它資料結構的文章

相關文章