什麼是集合?
- 集合是由一組無序且唯一的項組成。你可以把集合想象成一個既沒有重複元素,也沒有順序概念的陣列。還有一個概念叫空集。空集就是不包括任何元素的集合。空集用{}表示。
建立集合類
- ECMAScript2015介紹了Set類是Javascript API的一部分,我們將基於ES2015的Set類來實現我們自己的Set類。我們也會實現一些原生ES2015沒有提供的集合運算,例如並集、交集和差集。
class Set {
constructor() {
this.items = {};
}
}
複製程式碼
- 我們使用物件而不是陣列來表示集合(items)的原因有兩個:
- 第一個是因為在使用陣列時,大部分方法的時間複雜度是O(n)。即我們需要迭代整個陣列直到找到要找的那個元素,在最壞的情況下需要迭代陣列的所有位置。如果陣列有更多元素的話,所需的時間會更長。另外,陣列是元素的一個有序集合。為了保證元素排列有序,它會佔用更多的記憶體空間。
- 另一個就是因為Javascript的物件不允許一個鍵指向兩個不同屬性,也保證了集合裡的元素都是唯一的。
老規矩-來搞幾個Set類的方法
1. has() 檢驗某個元素是否存在於集合中
has(element) {
return Object.prototype.hasOwnProperty.call(this.items, element); // 返回一個表明物件是否具有特定屬性的布林值
}
複製程式碼
- Object原型有hasOwnProperty方法。該方法返回一個表明物件是否具有特定屬性的布林值。
- 也可以使用this.items.hasOwnProperty(element),但這樣的話程式碼檢查工具如(ESLint)會丟擲一個錯誤。錯誤的原因為不是所有的物件都繼承了Object.prototype,甚至繼承的Object,prototype的物件上的hasOwnProperty方法也有可能會被覆蓋,導致程式碼不能正常工作。
- element in items也可以。in運算子返回表示物件在原型鏈上是否有特定屬性的布林值。
2. add() 向集合新增一個新元素
add(element) {
if (!this.has(element)) { // 檢查
this.items[element] = element; // 如果不存在,就把element新增到集合中,返回true {element:element}
return true;
}
return false; // 如果集合中有相同的元素,返回false即可
}
複製程式碼
- 新增一個元素的時候,把它同時作為鍵和值儲存,因為這樣有利於查詢該元素
3. delete()從集合中刪除一個元素
delete(element) {
if (this.has(element)) { // 檢查,只能刪除存在與集合中的,(集合中都沒有,你刪誰去~)
delete this.items[element]; // 從集合中移除element
return true; // 返回true
}
return false; // 如果集合中沒有,返回false
}
複製程式碼
- 既然我們用物件來儲存集合的items物件,就可以簡單的使用delete運算子從items物件中移除屬性。
4. size()返回集合中有多少元素
size() { // 返回集合中有多少元素
return Object.keys(this.items).length; // 使用原生的內建方法,把物件的key轉化為陣列,再返回其length
}
複製程式碼
5. values()返回一個包含集合中所有所有值的陣列
values() { // 返回一個包含集合中所有所有值的陣列
return Object.values(this.items); // 同樣使用原生方法(它是在ECMAscript2017中被新增進來的,目前只在現在瀏覽器中可用)
}
複製程式碼
- 現在就實現了一個和ECMASscript2015中非常類似的Set類實現。
集合運算
- 我們可以對集合進行如下運算:
- 並集:對於給定的兩個集合,返回一個包含兩個集合中所有元素的新集合。
- 交集:對於給定的兩個集合,返回一個包含兩個集合中共有元素的新集合。
- 差集:對於給定的兩個集合,返回一個包含所有存在於第一個集合且不存在於第二個集合的元素的新集合。
- 子集:驗證一個給定集合是否是另一個集合的子集。
1.並集
- 該集合的定義是:x(元素)存在於A中,或x存在於B中。實現程式碼如下:
union(otherSet) {
const unionSet = new Set(); // 建立一個新的集合
this.values().forEach(value => unionSet.add(value)); // 獲取第一個集合(當前的set類例項)所有的值並新增到新集合中
otherSet.values().forEach(value => unionSet.add(value)); // 獲取第二個集合(傳入的set類例項)所有的值並新增到新集合中
return unionSet; // 最後返回建立的新集合
}
複製程式碼
2.交集
- 由所有屬於集合A且屬於集合B的元素所組成的集合,叫做集合A與集合B的交集。實現程式碼如下:
intersection(otherSet) {
const intersectionSet = new Set(); // 建立一個新的集合
const values = this.values(); // 獲取第一個集合(當前的set類例項)
const otherValues = otherSet.values(); // 獲取第二個集合(傳入的set類例項)
let biggerSet = values; // 假設當前集合的元素較多
let smallerSet = otherValues; // 傳入集合的元素較少
if (otherValues.length - values.length > 0) { // 比較兩個集合的元素個數
biggerSet = otherValues; // 如果傳入集合的元素個數比當前集合的元素個數多的話,就交換較多的等於傳入的
smallerSet = values; // 較少的等於當前集合
}
smallerSet.forEach(value => { // 最後迭代較少集合
if (biggerSet.includes(value)) { // 如果較大的集合中也有這個元素
intersectionSet.add(value); // 新增到新集合當中
}
});
return intersectionSet; // 返回新集合
}
複製程式碼
- 首先建立一個新的集合來存放intersection方法返回的結果。
- 然後要獲取當前集合例項中的值和傳入集合中的值。
- 先假設當前集合的元素較多, 傳入集合的元素較少。
- 再比較兩個集合的元素個數,如果另一個集合的元素個數比當前集合的元素個數多的話,就交換較多的等於傳入的,較少的等於當前集合。
- 迭代較少集合,如果兩個集合中都有當前元素,把它插入到新集合當中
- 最後返回新集合
- 對兩個集合的元素個數做比較的目的是為了儘量最少迴圈迭代,更少的迭代次數意味這更少的過程消耗。
3. 差集
(沒找到更好的圖片,自己也懶的畫了,意思大家表達清除就好 哈~~)- 元素存在A中,且x不存在於B中。實現程式碼如下:
difference(otherSet) {
const differenceSet = new Set(); // 建立一個新的集合
this.values().forEach(value => { // 當前集合的值轉換為陣列,並迴圈
if (!otherSet.has(value)) { // 如果傳入的集合中沒有這個元素
differenceSet.add(value); // 把它新增到新集合中
}
});
return differenceSet; // 返回新的集合
}
複製程式碼
4. 子集
- 集合A的每一個元素,也需要存在於集合B中。實現程式碼如下:
isSubsetOf(otherSet) {
if (this.size() > otherSet.size()) { // 如果當前集合的元素比傳入集合多,那它肯定不是傳入集合的子集,返回false
return false;
}
let isSubset = true; // 先假設當前集合是傳入集合的子集
// 迭代當前集合,當發現一個返回false,便不再執行。
this.values().every(value => {
if (!otherSet.has(value)) { // 驗證迭代的元素是否也存在傳入集合中
isSubset = false; // 只要有一個不是就改變變數
return false; // 返回false 不再往下執行
}
return true; // 如果都是,返回true
});
return isSubset; // 最後返回變數isSubset
}
複製程式碼
- 首先要驗證當前集的元素比傳入集的小,如果當前集合的元素比傳入集合還多,那它肯定不是傳入集合的子集,返回false
- 然後先假設當前集合是傳入集合的子集。
- 迭代當前集合的所有元素,驗證這些元素也存在於傳入集合中。
- 如果有任何元素不存在於傳入集合中,就意味著它不是一個子集,返回false。
- 如果所有元素都存在於傳入集合中,就返回true,isSubset變數不會改變。
- 在子集方法中我們用的是every方法。當我們發現一個值不存在於傳入集合時,可以停止迭代值,表示這不是一個子集。
ECMAScript2015——Set類
- 先來看看原生的set類怎麼用:
const set = new Set();
set.add(1);
console.log(set.values()); // 輸出@Iterator
console.log(set.has(1)); // 輸出true
console.log(set.values()); // 輸出1
複製程式碼
- ES2015的set的values方法返回Iterator(一個包含鍵值對的迭代器物件),而不是值構成的陣列。
- 另一個區別是,我們實現的size方法返回set中儲存的值的個數,而ES2015的 Set則有一個size屬性。
- 我們實現的set類實現了並集、交集、差集、子集這些數學運算,然而ES6原生的Set並沒有這些功能。
ES6Set運算模擬
- 首先建立兩個集合,等會兒會用到
const setA = new Set();
setA.add(1);
setA.add(2);
setA.add(3);
const setB = new Set();
setB.add(2);
setB.add(3);
setB.add(4);
複製程式碼
- 模擬並集運算
const union = (set1, set2) => { // 普通模擬
const unionAb = new Set();
set1.forEach(value => unionAb.add(value));
set2.forEach(value => unionAb.add(value));
return unionAb;
};
console.log(union(setA, setB)); // [1,2,3,4]
console.log(new Set([...setA, ...setB])); // 使用 擴充套件運算子模擬 [1,2,3,4]
複製程式碼
- 模擬交集運算
const intersection = (set1, set2) => { // 普通模擬, 未經優化的
const intersectionSet = new Set();
set1.forEach(value => {
if (set2.has(value)) {
intersectionSet.add(value);
}
});
return intersectionSet;
};
console.log(intersection(setA, setB)); // [2,3]
console.log(new Set([...setA].filter(x => setB.has(x)))); // 使用 擴充套件運算子模擬 [2,3]
複製程式碼
- 模擬差集運算
const difference = (set1, set2) => { // 普通模擬
const differenceSet = new Set();
set1.forEach(value => {
if (!set2.has(value)) {
differenceSet.add(value);
}
});
return differenceSet;
};
console.log(difference(setA, setB));// [1]
console.log(new Set([...setA].filter(x => !setB.has(x)))); // 使用 擴充套件運算子模擬 [1]
複製程式碼
最後
好了,今天的隨手筆記完事兒了。本文內容全來自本人閱讀過《學習Javascript資料結構與演算法》第七章後稍加整理而成。