list
list<T>
容器模板定義在 list 標頭檔案中,是 T 型別物件的雙向連結串列。
list 容器具有一些 vector 和 deque 容器所不具備的優勢,它可以在常規時間內,在序列已知的任何位置插入或刪除元素。這是我們使用 list,而不使用 vector 或 deque 容器的主要原因。
list 的缺點是無法透過位置來直接訪問序列中的元素,也就是說,不能索引元素。為了訪問 list 內部的一個元素,必須一個一個地遍歷元素,通常從第一個元素或最後一個元素開始遍歷。
可以用和其他序列容器相同的方式,來獲取 list 容器的迭代器。因為不能隨機訪問 list 中的元素,獲取到的迭代器都是雙向迭代器。
以 list 為引數,呼叫 begin() 可以得到指向 list 中第一個元素的迭代器。透過呼叫 end(),可以得到一個指向最後一個元素下一個位置的迭代器,因此像其他序列容器一樣,可以用它們來指定整個範圍的元素。
建立
#include <string>
#include <list>
using namespace std;
int main() {
// list 容器的建構函式的用法類似於 vector 或 deque 容器
list<string> words;
// 帶有給定數量的預設元素的列表
list<string> sayings{10};
// 生成一個包含給定數量的相同元素的列表
list<double> values(10, 3.14);
// 生成一個現有 list 容器的副本
list<double> save_values{values};
// 用另一個序列的開始和結束迭代器所指定的一段元素,來構造 list 容器的初始化列表
// 因為 list 容器的 begin() 和 end() 函式返回的都是雙向迭代器,所以不能用它們加減整數。
// 修改雙向迭代器的唯一方式是使用自增或自減運算子。
list<double> samples{++cbegin(values), --cend(values)};
}
可以透過呼叫 list 容器的成員函式 size()
來獲取它的元素個數。也可以使用它的 resize()
函式來改變元素個數。如果 resize()
的引數小於當前元素個數,會從尾部開始刪除多餘的元素。如果引數比當前元素個數大,會使用所儲存元素型別的預設建構函式來新增元素。
增加和插入
#include <string>
#include <list>
#include <vector>
#include <iostream>
using namespace std;
int main() {
list<int> data(4, 0);
// 1.在迭代器指定的位置插入一個新的元素
data.insert(++begin(data), 3);
// list: 0 3 0 0 0
// 2.在給定位置插入幾個相同元素的副本
auto iter = begin(data);
// 使用定義在 iterator 標頭檔案中的全域性函式 advance(),將迭代器增加 3。只能增加或減小雙向迭代器。
// 因為迭代器不能直接加 3,所以 advance() 會在迴圈中自增迭代器。
advance(iter, 3);
// 第一個引數是用來指定插入位置的迭代器,第二個引數是被插入元素的個數,第三個引數是被重複插入的元素
data.insert(iter, 3, 88);
// list: 0 3 0 88 88 88 0 0
// 3.將一段元素插入到data列表
vector<int> numbers(2, 5);
data.insert(--(--end(data)), cbegin(numbers), cend(numbers));
// list: 0 3 0 88 88 88 5 5 0 0
}
#include <string>
#include <list>
#include <vector>
#include <iostream>
using namespace std;
int main() {
list<string> names{"a", "b", "c", "d"};
// 1.引數作為物件被新增
names.push_front("e");
names.push_back("f");
// 2.成員函式 emplace_front() 和 emplace_back() 可以做得更好
names.emplace_front("g");
names.emplace_back("h");
// names: g e a b c d f h
string name("x");
names.emplace_back(std::move(name));
// move() 函式將 name 的右值引用傳入 emplace_back() 函式。
// 這個操作執行後,names 變為空,因為它的內容已經被移到 list 中.
cout << "name = " << name << endl;
names.emplace(++begin(names), "z");
// names: g z e a b c d f h x
}
刪除
對於 list 的成員函式 clear() 和 erase(),它們的工作方式及效果,和前面的序列容器相同。
#include <string>
#include <list>
using namespace std;
int main() {
list<int> numbers{2, 5, 2, 3, 6, 7, 8, 2, 9};
// 移除和引數匹配的元素
numbers.remove(2);
// numbers: 5 3 6 7 8 9
// 刪除偶數,這裡的引數是一個 lambda 表示式,但也可以是一個函式物件
numbers.remove_if([](int n) { return n % 2 == 0; });
// numbers: 5 3 7 9
list<string> words{"one", "two", "two", "two", "three", "two", "four", "four"};
// 移除連續的重複元素,只留下其中的第一個
words.unique();
// words: one two three two four
// 可以在對元素進行排序後,再使用 unique(),這樣可以保證移除序列中全部的重複元素
}
排序
sort() 函式模板定義在標頭檔案 algorithm 中,要求使用隨機訪問迭代器。但 list 容器並不提供隨機訪問迭代器,只提供雙向迭代器,因此不能對 list 中的元素使用 sort() 演算法。但是,還是可以進行元素排序,因為 list 模板定義了自己的 sort() 函式。sort() 有兩個版本:無參 sort() 函式將所有元素升序排列。第二個版本的 sort() 接受一個函式物件或 lambda 表示式作為引數,這兩種引數都定義一個斷言用來比較兩個元素。
list<string> names{"a", "b", "c", "d"};
// names.sort(greater<string>());
// 簡潔版的函式物件可以接受任何型別的引數,使用完美轉發 (perfect forwarding) 可以避免不必要的引數複製。
// 因此,完美轉發總是會快很多,因為被比較的引數會被移動而不是複製到函式中。
names.sort(std::greater<>());
// names: d c b a
在必要時可以將自定義的函式物件傳給斷言來對 list 排序。儘管對一般物件來說,並不需要這樣。如果為自己的類定義了 operator(),然後就可以繼續使用 greater<>。 當我們需要比較非預設型別時,就需要一個函式物件。
#include <string>
#include <list>
using namespace std;
class my_greater {
public:
bool operator()(const string &s1, const string &s2) {
if (s1[0] == s2[0])
// 將相同初始字元的字串按長度排序
return s1.length() > s2.length();
else
return s1 > s2;
}
};
int main() {
list<string> names{"Hugo", "Hannah", "Jane", "Jim", "Jules", "Janet", "Ann", "Alan"};
// 1.函式物件
// names.sort(my_greater());
// 2.lambda 表示式
names.sort([](const string &s1, const string &s2) {
if (s1[0] == s2[0])
return s1.length() > s2.length();
else
return s1 > s2;
});
// names: Jules Janet Jane Jim Hannah Hugo Alan Ann
}
合併
list 的成員函式 merge() 以另一個具有相同型別元素的 list 容器作為引數。兩個容器中的元素都必須是升序。引數 list 容器中的元素會被合併到當前的 list 容器中。
#include <string>
#include <list>
#include <iostream>
using namespace std;
int main() {
// 1.兩個容器中的元素都必須是升序
list<int> to_values{2, 4, 6, 14};
list<int> from_values{-2, 1, 7, 10};
to_values.merge(from_values);
// to_values: -2 1 2 4 6 7 10 14
// 沒有元素了
cout << from_values.empty();
// 2.提供一個比較函式作為該函式的第二個引數,用來在合併過程中比較元素
list<string> my_words{"three", "six", "eight"};
list<string> your_words{"seven", "four", "nine"};
// 字串物件比較函式是由 lambda 表示式定義的,這個表示式只比較第一個字元。
auto comp_str = [](const string &s1, const string &s2) { return s1[0] < s2[0]; };
my_words.sort(comp_str); //"eight" "six" "three"
your_words.sort(comp_str); //"four" "nine" "seven"
my_words.merge(your_words, comp_str); // "eight" "four" "nine" "six" "seven" "three"
}
list 節點在記憶體中的位置不會改變;只有連結它們的指標變了。在合併的過程中,兩個容器中的元素使用 operator() 進行比較。
list 容器的成員函式 splice() 有幾個過載版本。這個函式將引數 list 容器中的元素移動到當前容器中指定位置的前面。可以移動單個元素、一段元素或源容器的全部元素。
#include <string>
#include <list>
using namespace std;
int main() {
list<string> my_words{"three", "six", "eight"};
list<string> your_words{"seven", "four", "nine"};
// 1.移動單個元素
// 第一個引數是指向目的容器的迭代器。
// 第二個引數是元素的來源。
// 第三個引數是一個指向源list容器中被粘接元素的迭代器,它會被插入到第一個引數所指向位置之前。
my_words.splice(++begin(my_words), your_words, ++begin(your_words));
// my_words: "three", "four", "six", "eight"
// your_words: "seven", "nine"
// 2.當要粘接源 list 容器中的一段元素時,第 3 和第 4 個引數可以定義這段元素的範圍
// 將 my_words 從第二個元素直到末尾的元素,粘接到 your_words 的第二個元素之前
your_words.splice(++begin(your_words), my_words,
++begin(my_words), end(my_words));
// my_words: "three"
// your_words:"seven", "four", "six", "eight","nine"
// 3.將 your_words 的全部元素粘接到 my_words 中
// your_words 的所有元素被移到了 my_words 的第一個元素 "three" 之前,your_words 會變為空
my_words.splice(begin(my_words), your_words);
// 即使 your_words 為空,也仍然可以向它粘接元素
// 第一個引數也可以是 begin (your_words),因為當容器為空時,它也會返回一個結束迭代器。
your_words.splice(end(your_words), my_words);
}
訪問
list 的成員函式 front() 和 back(),可以各自返回第一個和最後一個元素的引用。在空 list 中呼叫它們中的任意一個,結果是未知的,因此不要這樣使用。可以透過迭代器的自增或自減來訪問 list 的內部元素。
begin() 和 end() 分別返回的是指向第一個和最後一個元素下一個位置的雙向迭代器。rbegin() 和 rend() 函式返回的雙向迭代器,可以讓我們逆序遍歷元素。因為可以對 list 使用基於範圍的迴圈,所以當我們想要處理所有元素時,可以不使用迭代器。