JavaScript資料結構——集合的實現與應用

Jaxu發表於2019-08-02

  與數學中的集合概念類似,集合由一組無序的元素組成,且集合中的每個元素都是唯一存在的。可以回顧一下中學數學中集合的概念,我們這裡所要定義的集合也具有空集(即集合的內容為空)、交集、並集、差集、子集的特性。

  在ES6中,原生的Set類已經實現了集合的全部特性,稍後我們會介紹它的用法。

  我們使用JavaSctipt的物件來表示集合,下面是集合類的主要實現方法:

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

    add (value) { // 向集合中新增元素
        if (!this.has(value)) {
            this.items[value] = value;
            return true;
        }
        return false;
    }

    delete (value) { // 從集合中刪除對應的元素
        if (this.has(value)) {
            delete this.items[value];
            return true;
        }
        return false;
    }

    has (value) { // 判斷給定的元素在集合中是否存在
        return this.items.hasOwnProperty(value);
    }

    clear() { // 清空集合內容
        this.items = {};
    }

    size () { // 獲取集合的長度
        return Object.keys(this.items).length;
    }

    values () { // 返回集合中所有元素的內容
        return Object.values(this.items);
    }
}

  在使用JavaScript物件{ }來表示集合時,我們可以像運算元組一樣通過[ ]來設定和獲取集合內元素的值。通過這種方式,在設定集合元素的值時,如果元素不存在,則建立一個新元素,如果元素存在,則修改對應的值;在獲取集合元素的值時,如果元素存在,則返回對應的值,如果元素不存在,則返回undefined。此外,JavaScript物件還提供了一些基礎方法以方便我們對集合進行一些操作,例如hasOwenProperty()方法返回一個表明物件是否具有特定屬性的布林值,Object.keys()方法返回指定物件的所有屬性名稱的陣列,Object.values()方法方法指定物件的所有屬性值的陣列。

  上述程式碼很簡單,這裡就不再詳細解釋了。下面是一些測試用例和測試結果:

let 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()); // []

  下面我們來看看集合的數學運算:並集、交集、差集、子集。

並集

  對於給定的兩個集合,並集返回一個包含兩個集合中所有元素的新集合。示意圖如下:

  並集的實現程式碼:

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

  首先遍歷第一個集合,將所有的元素新增到新集合中,然後再遍歷第二個集合,將所有的元素新增到新集合中。然後返回新集合。不用擔心會新增重複的元素,因為集合的add()方法會自動排除掉已新增的元素。

  測試用例及結果:

let setA = new Set();
setA.add("first");
setA.add("second");
setA.add("third");

let setB = new Set();
setB.add("third");
setB.add("fourth");
setB.add("fifth");
setB.add("sixth");

console.log(setA.union(setB).values()); // [ 'first', 'second', 'third', 'fourth', 'fifth', 'sixth' ]

交集

  對於給定的兩個集合,交集返回一個包含兩個集合中共有元素的新集合。示意圖如下:

  交集的實現程式碼:

intersection (otherSet) { // 交集
    let intersectionSet = new Set();
    this.values().forEach(value => {
       if (otherSet.has(value)) intersectionSet.add(value);
    });
    return intersectionSet;
}

  遍歷第一個集合,如果元素出現在第二個集合中,則將它新增到新集合。然後返回新集合。

  測試用例及結果:

let setA = new Set();
setA.add("first");
setA.add("second");
setA.add("third");

let setB = new Set();
setB.add("second");
setB.add("third");
setB.add("fourth");

console.log(setA.intersection(setB).values()); // [ 'second', 'third' ]

差集

  對於給定的兩個集合,差集返回一個包含所有存在於第一個集合且不存在於第二個集合的元素的新集合。示意圖如下:

  差集的實現程式碼:

difference (otherSet) { // 差集
    let differenceSet = new Set();
    this.values().forEach(value => {
       if (!otherSet.has(value)) differenceSet.add(value);
    });
    return differenceSet;
}

  遍歷第一個集合,如果元素沒有出現在第二個集合中,則將它新增到新集合。然後返回新集合。

  測試用例及結果:

let setA = new Set();
setA.add("first");
setA.add("second");
setA.add("third");

let setB = new Set();
setB.add("second");
setB.add("third");
setB.add("fourth");

console.log(setA.difference(setB).values()); // [ 'first' ]

子集

  驗證一個給定集合是否是另一個集合的子集,即判斷給定的集合中的所有元素是否都存在於另一個集合中,如果是,則這個集合就是另一個集合的子集,反之則不是。示意圖如下:

  子集的實現程式碼:

subset (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;
}

  如果集合A比集合B的長度大,則直接返回false,因為這種情況A不可能是B的子集。然後使用every()函式遍歷集合A的所有元素,一旦碰到其中的元素沒有在集合B中出現,則直接返回false,並終止遍歷。這裡我們沒有使用forEach來遍歷集合A,是因為你無法根據某個條件來終止forEach迴圈。考慮下面這種情況:

var arr = ["first", "second", "third", "fourth"];
arr.forEach(item => {
    if(item === "third") return true;
    console.log(item);
});

  輸出結果是:

first
second
fourth

  很顯然,這裡的return true語句並不能退出forEach迴圈,它只能保證本次迴圈中餘下的語句不被執行,而接下來其它的元素還是會被遍歷到。

  在我們的subset()方法中,如果使用forEach語句,每一次都會遍歷集合中的所有元素,如果遇到其中的元素沒有在集合B中出現,就將isSubset變數的值設定為false,但並不能退出forEach,isSubset變數的值可能會被多次覆蓋。為了提高執行效率,推薦使用every()函式,它會遍歷集合中的元素,直到其中一個返回結果為false,就終止遍歷,並返回false,否則就遍歷所有的元素並返回true。有關every()函式的詳細介紹可以看這裡。與every()函式功能相似還有一個some()函式,它在遍歷集合的過程中,遇到返回結果為true時就終止遍歷,具體內容可以看這裡

  差集的測試用例及結果:

let setA = new Set();
setA.add("first");
setA.add("second");

let setB = new Set();
setB.add("first");
setB.add("second");
setB.add("third");

let setC = new Set();
setC.add("second");
setC.add("third");
setC.add("fourth");

console.log(setA.subset(setB)); // true
console.log(setA.subset(setC)); // false

   文章的開頭說過,ES6提供了原生的Set類,讓我們來看看它的一些使用方法:

let set = new Set();
set.add(1);
set.add(2);
set.add(3);
console.log(set.values()); // [Set Iterator] { 1, 2, 3 }
console.log(set.has(1)); // true
console.log(set.size); // 2

set.delete(1);
console.log(set.values()); // [Set Iterator] { 2, 3 }

set.clear();
console.log(set.values()); // [Set Iterator] {  }

  和前面我們自定義的Set類稍微有一點不同,values()方法返回的不是一個陣列,而是Iterator迭代器。另一個就是這裡的size是一個屬性而不是方法,其它部分都和我們前面定義的Set類相同。由於ES6的Set類不包含對集合的數學運算,我們可以按照前面我們提供的方法來對其進行擴充。

  下一章我們將介紹如何用JavaScript來實現字典和雜湊表。

相關文章