Try   HackMD

Python 及 C++ 集合 (set)

作者:王一哲
日期:2023年8月13日

前言

集合 (set) 的性質與陣列很像,但是集合中的資料不能重複。以下的程式碼測試版本為 Python 3.10.12 及 C++14。


Python Set

Python 預設的資料容器有串列 (list)、數組 (tuple)、字典 (dictionary)、集合 (set),使用 set 不需要引入函式庫。集合中的資料不能重複、沒有排序,無法使用索引值存取、改變資料,但是可以移除或新增資料。

建立集合

語法有以下

名稱 = set()  # 産生空的集合,如果用 名稱 = {} 會産生空的字典
名稱 = {資料1, 資料2, 資料3, ...}  # 産生集合同時指定資料
名稱 = set(可迭代的資料)  # 由可迭代的資料産生集合

例如以下的程式碼

a = set() # 産生空的集合 a b = {1, 3, 5, 2, 4, 6} # 産生集合 b,print 輸出為 {1, 2, 3, 4, 5, 6} c = {1, -3, 5, -2, 4, 6}# 産生集合 c,print 輸出為 {1, 4, 5, 6, -3, -2} # 由串列産生集合 d,print 輸出為 {1, 2, 3, 4, 5, 6} mylist = [1, 3, 5, 2, 4, 6] d = set(mylist) # 由數組産生集合 f,print 輸出為 {1, 2, 3, 4, 5, 6} mytuple = (1, 3, 5, 2, 4, 6) f = set(mytuple) # 由字典産生集合 g、h、k mydict = {1: 3, 5: 2, 4: 6} g = set(mydict) # 預設由 key 産生集合,print 輸出為 {1, 4, 5} h = set(mydict.keys()) # 由 key 産生集合,print 輸出為 {1, 4, 5} k = set(mydict.values()) # 由 value 産生集合,print 輸出為 {2, 4, 6} # 由字串産生集合 s hello = "Hello World!" s = set(hello) # 産生集合 s,print 輸出為 {'d', 'e', 'l', ' ', 'r', 'o', '!', 'W', 'H'} # 産生內容為字元的集合 t,print 輸出為 {'c', 'A', 'C', 'a', 'b', 'B'} t = {'a', 'A', 'c', 'C', 'b', 'B'}

加入資料

語法為

名稱.add(資料)

一次只能加入一筆資料,如果集合中已經有這筆資料,則集合內容不變。例如以下的程式碼

a = {1, 3, 5, 2, 4, 6}
a.add(1)   # a 的內容不變
a.add(-1)  # a 的內容變為 {1, 2, 3, 4, 5, 6, -1}

複製集合

語法為

集合2 = 集合1.copy()

不需要加上任何參數。如果不使用 copy 而是用等號,則改變集合2時也會改變集合1的內容,例如以下的程式碼

a = {1, 3, 5, 2, 4, 6}  # a 的內容是 {1, 2, 3, 4, 5, 6}
b = a.copy()  # 將 a 的內容複製給 b
c = a         # c 指向 a,兩者會同時改變
c.add(-1)     # a、c 的內容都變為 {1, 2, 3, 4, 5, 6, -1},但是 b 不變

清除資料

語法為

名稱.clear()

不需要加上任何參數,清空資料集合長度為 0。

隨機移除一筆資料並回傳被移除的資料

語法為

回傳值 = 集合.pop()

不需要加上任何參數。但在測試時發現,如果集合的資料皆為數字,都是移除第一筆資料;如果集合中的資料皆為字元,每次移除的資料不固定,例如以下的程式碼

a = {1, 3, 5, 2, 4, 6}
b = {'a', 'A', 'c', 'C', 'b', 'B'}
print(a.pop())  # 每次都移除 1
print(b.pop())  # 隨機移除字元

移除指定的一筆資料

第一種語法為

集合.remove(資料)

只能加上一個參數,沒有回傳值。如果集合中沒有指定的資料,會回傳錯誤訊息 KeyError 並中止程式。例如以下的程式碼

a = {1, 3, 5, 2, 4, 6}
a.remove(5)   # a 的內容都變為 {1, 2, 3, 4, 6}
a.remove(0)   # 回傳錯誤訊息 KeyError 並中止程式

第二種語法為

集合.discard(資料)

只能加上一個參數,沒有回傳值。如果集合中沒有指定的資料,不會回傳錯誤訊息,不會中止程式,集合的內容不變。例如以下的程式碼

a = {1, 3, 5, 2, 4, 6}
a.discard(5)   # a 的內容都變為 {1, 2, 3, 4, 6}
a.discard(0)   # a 的內容不變

回傳兩個集合的相對差集

相對差集 (relative complement) 或稱為差集,如果有兩個集合 a、b,則 a 在 b 的相對差集,就是集合 b 扣掉集合 a、b 都有的資料。語法為

相對差集 = 集合b.difference(集合a)

例如以下的程式碼

a = {1, 2, 3, 4, 5, 6}
b = {1, 3, 5, 7, 9, 11}
c = a.difference(b)  # c 的內容為 {2, 4, 6}
d = b.difference(a)  # d 的內容為 {9, 11, 7}

更新資料為兩個集合的相對差集

如果有兩個集合 a、b,要將 b 更新為 a 在 b 的相對差集,語法為

集合b.difference_update(集合a)

沒有回傳值。例如以下的程式碼

a = {1, 2, 3, 4, 5, 6}
b = {1, 3, 5, 7, 9, 11}
b.difference_update(a)  # b 的內容變為 {7, 9, 11}

回傳兩個集合的交集

兩個集合 a、b 的交集 (intersection),就是同時存在於 a、b 中的資料。以下兩種語法效果相同

交集 = 集合a.intersection(集合b)
交集 = 集合b.intersection(集合a)

例如以下的程式碼

a = {1, 2, 3, 4, 5, 6}
b = {1, 3, 5, 7, 9, 11}
c = a.intersection(b)  # c 的內容為 {1, 3, 5}
d = b.intersection(a)  # d 的內容為 {1, 3, 5}

更新資料為兩個集合的交集

如果有兩個集合 a、b,要將 a 更新為 a、b 的交集,語法為

集合a.intersection_update(集合b)

沒有回傳值。例如以下的程式碼

a = {1, 2, 3, 4, 5, 6}
b = {1, 3, 5, 7, 9, 11}
a.intersection_update(b)  # a 的內容變為 {1, 3, 5}

回傳兩個集合的聯集

兩個集合 a、b 的聯集 (union),就是 a、b 當中所有的資料。以下兩種語法效果相同

聯集 = 集合a.union(集合b)
聯集 = 集合b.union(集合a)

例如以下的程式碼

a = {1, 2, 3, 4, 5, 6}
b = {1, 3, 5, 7, 9, 11}
c = a.union(b)  # c 的內容為 {1, 2, 3, 4, 5, 6, 7, 9, 11}
d = b.union(a)  # d 的內容為 {1, 2, 3, 4, 5, 6, 7, 9, 11}

更新集合的資料

可以從另一個可迭代的物件更新集合的資料,只會加入目前集合中沒有的資料。語法為

集合.update(可迭代的物件)

沒有回傳值。例如以下的程式碼

a = {1, 2, 3, 4, 5, 6}
b = [1, 3, 5, 7, 9, 11]
a.update(b)  # a 的內容變為 {1, 2, 3, 4, 5, 6, 7, 9, 11}

回傳兩個集合是否沒有交集

如果集合 a、b 沒有交集回傳 True,如果有交集回傳 False。以下兩種語法效果相同

狀態 = 集合a.isdisjoint(集合b)
狀態 = 集合b.isdisjoint(集合a)

例如以下的程式碼

a = {1, 2, 3, 4, 5, 6}
b = {1, 3, 5, 7, 9, 11}
c = {7, 8, 9, 10}
print(a.isdisjoint(b))  # 印出 False
print(c.isdisjoint(a))  # 印出 True

回傳集合是否為另一個集合的子集

如果集合 a 包含集合 b 所有的資料,則 b 為 a 的子集,如果是子集回傳 True,反之回傳 False。語法為

狀態 = 集合b.issubset(集合a)

例如以下的程式碼

a = {1, 2, 3, 4, 5, 6}
b = {1, 3, 5, 7, 9, 11}
c = {1, 2, 3, 4}
print(c.issubset(a))  # 印出 True
print(c.issubset(b))  # 印出 False

回傳集合是否包含另一個集合

如果集合 a 包含集合 b 所有的資料回傳 True,反之回傳 False。語法為

狀態 = 集合a.issuperset(集合b)

例如以下的程式碼

a = {1, 2, 3, 4, 5, 6}
b = {1, 3, 5, 7, 9, 11}
c = {1, 2, 3, 4}
print(a.issuperset(c))  # 印出 True
print(b.issuperset(c))  # 印出 False

回傳兩個集合的對稱差

兩個集合 a、b 的對稱差 (symmetric difference),就是沒有同時存在於 a、b 的資料,也就是 a、b 的聯集扣掉交集。以下兩種語法效果相同

對稱差 = 集合a.symmetric_difference(集合b)
對稱差 = 集合b.symmetric_difference(集合a)

例如以下的程式碼

a = {1, 2, 3, 4, 5, 6}
b = {1, 3, 5, 7, 9, 11}
c = a.symmetric_difference(b)  # c 的內容為 {2, 4, 6, 7, 9, 11}
d = b.symmetric_difference(a)  # d 的內容為 {2, 4, 6, 7, 9, 11}

更新資料為兩個集合的對稱差

如果有兩個集合 a、b,要將 a 更新為 a、b 的對稱差,語法為

集合a.symmetric_difference_update(集合b)

沒有回傳值。例如以下的程式碼

a = {1, 2, 3, 4, 5, 6}
b = {1, 3, 5, 7, 9, 11}
a.symmetric_difference_update(b)  # a 的內容變為 {2, 4, 6, 7, 9, 11}

C++ STL set 函式庫

要先引入函式庫,函式庫的官方說明書在此 cplusplus.com std::set

#include <set>

集合中的資料不能重複,由小到大排序,無法使用索引值存取、改變資料,但是可以移除或新增資料。

建立集合

集合的資料可以是所有內建的資料格式,例如 int、float、string,也可以使用自訂的資料格式。語法為

// 産生空的集合 set<資料格式> 集合名稱; // 産生集合並指定資料 set<資料格式> 集合名稱 {用逗號分隔的資料}; set<資料格式> 集合名稱 = {用逗號分隔的資料}; // 由集合2複製資料,也可以改變記憶體位置只複製部分內容 set<資料格式> 集合名稱 (集合2名稱.begin(), 集合2名稱.end()); // 由集合2複製資料 set<資料格式> 集合名稱 (集合2名稱); // 由已定義的陣列複製資料 set<資料格式> 集合名稱 (陣列名稱, 陣列名稱 + 陣列長度);

例如以下的程式碼

set<int> a; // a 是空的 set<int> b {1, 3, 5, 2, 4, 6}; // b 的內容為 {1, 2, 3, 4, 5, 6} set<int> c = {1, 3, 5, 2, 4, 6}; // c 的內容為 {1, 2, 3, 4, 5, 6} set<int> d (b.begin(), b.end()); // d 的內容為 {1, 2, 3, 4, 5, 6} set<int> f (b); // f 的內容為 {1, 2, 3, 4, 5, 6} int data[] = {1, 3, 5, 2, 4, 6}; set<int> g (data, data + sizeof(data)/sizeof(int)); // g 的內容為 {1, 2, 3, 4, 5, 6} set<char> h = {'a', 'A', 'c', 'C', 'b', 'B'}; // h 的內容為 {A, B, C, a, b, c}

迭代器 iterator

迭代器可以用來取得物件對應的記憶體位址,有以下幾種:

集合名稱.begin(); // 回傳集合起點位址 集合名稱.end(); // 回傳集合終點位址 集合名稱.rbegin(); // 回傳集合終點位址,由後向前向讀取資料 集合名稱.rend(); // 回傳集合起點位址,由後向前向讀取資料 集合名稱.cbegin(); // 回傳集合起點位址,回傳值為常數 集合名稱.cend(); // 回傳集合終點位址,回傳值為常數 集合名稱.crbegin();// 回傳集合終點位址,由後向前向讀取資料,回傳值為常數 集合名稱.crend(); // 回傳集合起點位址,由後向前向讀取資料,回傳值為常數

迭代器的變數名稱通常是 it,寫法有兩種,例如以下的程式碼

set<int> a = {1, 3, 5, 2, 4, 6};
set<int>::iterator it = q.begin();
auto it = q.begin();

但是 set<int>::iterator 只能用在 begin()、end()、cbegin()、cend(),還是用 auto 會比較方便。如果要印出集合中所有的資料,可以配合迭代器處理,例如以下的程式碼

set<int> a = {1, 3, 5, 2, 4, 6}; for(auto it = a.begin(); it != a.end(); it++) cout << *it << " "; // 印出的值為 1 2 3 4 5 6 cout << "\n"; for(auto it = a.rbegin(); it != a.rend(); it++) cout << *it << " "; // 印出的值為 6 5 4 3 2 1 cout << "\n"; for(auto it = a.cbegin(); it != a.cend(); it++) cout << *it << " "; // 印出的值為 1 2 3 4 5 6 cout << "\n"; for(auto it = a.crbegin(); it != a.crend(); it++) cout << *it << " "; // 印出的值為 6 5 4 3 2 1 cout << "\n";

加入資料

語法為

集合名稱.insert(資料);

一次只能加入一筆資料,如果集合中已經有這筆資料,則集合內容不變。例如以下的程式碼

set<int> a = {1, 3, 5, 2, 4, 6};
a.insert(3);   // a 的內容不變
a.insert(-1);  // a 的內容變為 {-1, 1, 2, 3, 4, 5, 6}

另一個工具是 emplace,語法為

集合名稱.emplace(資料);

看起來效果和 insert 差不多,但是據說少了複製構造的步驟,所以效率更高。例如以下的程式碼

set<int> a = {1, 3, 5, 2, 4, 6};
a.emplace(3);   // a 的內容不變
a.emplace(-1);  // a 的內容變為 {-1, 1, 2, 3, 4, 5, 6}

刪除指定的資料

語法為

集合名稱.erase(資料);

一次只能刪除一筆資料,如果集合中沒有指定的資料則集合內容不變。例如以下的程式碼

set<int> a = {1, 3, 5, 2, 4, 6};
a.erase(3);   // a 的內容變為 {1, 2, 4, 5, 6}
a.erase(-1);  // a 的內容不變,仍然為 {1, 2, 4, 5, 6}

erase 也可以刪除指定範圍的資料,請參考下文中 lower_bound 及 upper_bound 的說明。

清除所有資料

語法為

集合名稱.clear();

不需要加上任何參數,清空資料後集合長度為 0。

交換兩個集合的資料

語法為

集合名稱1.swap(集合名稱2);

取得指定資料的位址

如果集合中有指定的資料,會回傳該資料位址的迭代器;如果集合中沒有指定的資料,會回 .end()。語法為

auto it = 集合名稱.find(資料);

例如以下的程式碼

set<int> a = {1, 3, 5, 2, 4, 6};
auto it = a.find(3);
a.erase(it);  // a 的內容變為 {1, 2, 4, 5, 6}
if (a.find(-1) == a.end()) cout << "Not found\n";  // 集合中沒有 -1,印出 Not found
else cout << "Found\n";

另外有兩個工具,lower_bound 及 upper_bound,語法為

auto lowerit = 集合名稱.lower_bound(資料);
auto upperit = 集合名稱.upper_bound(資料);  // 回傳的位置包含這筆資料 

可以搭配 erase 刪除某個範圍內的資料,例如以下的程式碼

set<int> a = {1, 2, 3, 4, 5, 6};
auto lowit = a.lower_bound(2);
auto upit = a.upper_bound(5);
a.erase(lowit, upit);  // a 的內容變為 {1, 6}

還有一個工具可以同時回傳指定資料對應的 lower_bound 及 upper_bound,語法為

auto ret = 集合名稱.equal_range(資料);

回傳值分為兩個部分,ret.first 為 lower_bound,ret.second 為 upper_bound。例如以下的程式碼

set<int> a = {1, 2, 3, 4, 5, 6};
auto ret = a.equal_range(3);
a.erase(ret.first, ret.second);  // a 的內容變為 {1, 2, 4, 5, 6}

計算指定資料的數量

由於集合的資料不能重覆,因此指定資料的數量只有0或1,回傳值格式為 size_t,語法為

集合名稱.count(資料);

例如以下的程式碼

set<int> a = {1, 3, 5, 2, 4, 6};
cout << a.count(3) << endl;   // 印出 1
cout << a.count(-1) << endl;  // 印出 0

取得集合長度

語法為

集合名稱.size();

不需要任何參數,回傳值格式為 size_t,沒有正負號的整數。

回傳集合最大長度

語法為

集合名稱.max_size();

不需要任何參數,回傳值格式為 size_t,沒有正負號的整數。在我使用的系統中回傳值為 230584300921369395。

檢查集合是否為空

語法為

集合名稱.empty();

不需要任何參數,如果集合為空回傳 1,如果有資料回傳 0。

結語

Python 的集合除了可以儲存資料,還可以處理兩個集合的交集、聯集、差集、對稱差,功能十分強大。C++ 的集合基本上是儲存資料的容器,如果要處理兩個集合的交集、聯集、差集、對稱差,需要自己手刻一些工具。以上是我目前常用到的集合功能,如果之後有遇到其它需求會再補充內容。

參考資料

  1. Built-in Types: Set
  2. Python Set Methods
  3. cplusplus.com std::set

tags:C++Python