list

N1ce2cu發表於2024-08-20

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 使用基於範圍的迴圈,所以當我們想要處理所有元素時,可以不使用迭代器。