記事ディレクトリ
1 はじめに
以前に文字列とベクトルを学習しましたが、今日は C++ の別のコンテナであるリストを学習します。リストの最下層は主要な双方向循環リンク リストです。
2. リストの紹介
1.listは一定範囲内の任意の位置に挿入・削除が可能な連続コンテナであり、双方向に反復可能です。
2. リストの最下層は二重連結リスト構造になっており、二重連結リストの各要素は互いに関連性のない独立したノードに格納され、ノード内では前の要素と次の要素を指すポインタが配置されます。 。
3. list は forward_list に非常に似ています。主な違いは、forward_list が単一のリンクされたリストであり、前方にのみ反復できるため、より単純かつ効率的であることです。
4. 他のシリアル コンテナ (配列、ベクトル、デキュー) と比較して、リストは通常、任意の位置での要素の挿入および削除の実行効率が優れています。
5. 他の順次コンテナと比較すると、list と forward_list の最大の欠点は、任意の位置でのランダム アクセスをサポートしていないことです。たとえば、リストの 6 番目の要素にアクセスするには、既知の位置 (たとえば、先頭または末尾) からこの位置まで、この位置を反復処理するには線形時間オーバーヘッドが必要です。リストには、各ノードの関連情報を保持するための追加スペースも必要です (これは、小さい型の要素を格納する大きなリストでは重要な問題になる可能性があります)。の要素)
3. リストの共通インターフェース
3.1 リストのコンストラクタ
関数名 | 機能の説明 |
---|---|
list (size_type n, const value_type& val = value_type()) | 構築されたリストには、値が val である n 個の要素が含まれます |
リスト() | 空のリストを作成する |
リスト (const list& x) | コピーコンストラクター |
list (InputIterator が最初、InputIterator が最後) | [first, last) の範囲内の要素を含むリストを構築します |
在这里插入代码片void Test1()
{
list<int> lt1;
list<int> lt2(10, 1);
list<int> lt3(lt2);
}
3.2 イテレータの使用
ここで、反復子は一時的に、リスト内の特定のノードを指すポインターとして理解できます。
関数名 | 機能の説明 |
---|---|
開始+終了 | 最初の要素への反復子を返します + 最後の要素の次の位置への反復子を返します |
r開始+終了 | 最初の要素の reverse_iterator (終了位置) を返し、最後の要素の次の位置 (開始位置) の reverse_iterator を返します。 |
void Test2()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
lt1.push_back(5);
list<int>::iterator it = lt1.begin();
while (it != lt1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
it = lt1.begin();
while (it != lt1.end())
{
(*it)++;
cout << *it << " ";
it++;
}
cout << endl;
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
}
3.3 リストスペース管理
関数名 | 機能の説明 |
---|---|
空 | リストが空かどうかを確認し、true を返し、それ以外の場合は false を返します。 |
サイズ | リスト内の有効なノードの数を返します。 |
void Test3()
{
list<int> lt1;
list<int> lt2;
lt2.push_back(1);
lt2.push_back(2);
lt2.push_back(3);
lt2.push_back(4);
lt2.push_back(5);
cout << lt1.empty() << endl;
cout << lt2.size() << endl;
}
3.4 リストノードへのアクセス
関数名 | 機能の説明 |
---|---|
正面 | リストの最初のノードの値への参照を返します。 |
戻る | リストの最後のノードの値への参照を返します。 |
void Test4()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
lt1.push_back(5);
cout << lt1.front() << endl;
cout << lt1.back() << endl;
int& a = lt1.front();
int& b = lt1.back();
a++;
b++;
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
}
3.5 リストの追加、削除、確認、変更
関数名 | 機能の説明 |
---|---|
プッシュフロント | 値 val を持つ要素をリストの最初の要素の前に挿入します。 |
ポップフロント | リストの最初の要素を削除します |
プッシュバック | 値 val を持つ要素をリストの最後に挿入します。 |
ポップバック | リストの最後の要素を削除します |
入れる | 値 val を持つ要素をリストの位置に挿入します |
消す | リスト位置の要素を削除します |
スワップ | 2 つのリストの要素を交換する |
クリア | リスト内の有効な要素をクリアします |
void Test5()
{
list<int> lt1;
lt1.push_front(1);
lt1.push_front(2);
lt1.push_front(3);
lt1.push_front(4);
lt1.push_front(5);
for (auto e : lt1)
{
cout << e << ' ';
}
cout << endl;
lt1.pop_front();
lt1.pop_front();
lt1.pop_front();
for (auto e : lt1)
{
cout << e << ' ';
}
cout << endl;
list<int> lt2;
lt2.push_back(1);
lt2.push_back(2);
lt2.push_back(3);
lt2.push_back(4);
lt2.push_back(5);
for (auto e : lt2)
{
cout << e << ' ';
}
cout << endl;
lt2.pop_back();
lt2.pop_back();
lt2.pop_back();
for (auto e : lt2)
{
cout << e << ' ';
}
cout << endl;
list<int> lt3;
lt3.push_back(1);
lt3.push_back(2);
lt3.push_back(3);
lt3.push_back(4);
lt3.push_back(5);
list<int>::iterator pos = find(lt3.begin(), lt3.end(), 3);
lt3.insert(pos, 10);//迭代器不会失效
lt3.insert(pos, 20);
for (auto e : lt3)
{
cout << e << ' ';
}
cout << endl;
//lt3.erase(pos);//迭代器会失效
pos = lt3.erase(pos);
pos = lt3.erase(pos);
for (auto e : lt3)
{
cout << e << ' ';
}
cout << endl;
lt1.swap(lt3);
for (auto e : lt1)
{
cout << e << ' ';
}
cout << endl;
for (auto e : lt3)
{
cout << e << ' ';
}
cout << endl;
lt2.clear();
for (auto e : lt2)
{
cout << e << ' ';
}
cout << endl;
}
4. リスト反復子の無効化の問題
前述したように、イテレータはポインタに似たものとして一時的に理解できますが、イテレータの無効化とは、イテレータが指すノードが無効になる、つまりノードが削除されることを意味します。リストの基礎となる構造は、先頭ノードを持つ双方向の循環リンク リストであるため、リストの反復子は、リストに挿入されたときに無効化されません。無効になるのは削除されたときだけです。削除されたノードを指す反復は無効化されますが、他の反復子は影響を受けません。
イテレータの無効化:
void Test6()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
lt1.push_back(5);
list<int>::iterator it = lt1.begin();
while (it != lt1.end())
{
//错误——迭代器失效
//erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值
lt1.erase(it);
++it;
}
}
変更後:
void Test7()
{
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
lt1.push_back(5);
list<int>::iterator it = lt1.begin();
while (it != lt1.end())
{
it = lt1.erase(it);//正确
}
}
5. リストシミュレーションの実装
#pragma once
#include <assert.h>
namespace fiora
{
template<class T>
struct list_node
{
T _data;
list_node<T>* _next;
list_node<T>* _prev;
list_node(const T& x = T())
:_data(x)
, _next(nullptr)
, _prev(nullptr)
{
}
};
// typedef __list_iterator<T, T&, T*> iterator;
// typedef __list_iterator<T, const T&, const T*> const_iterator;
// 像指针一样的对象
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T, Ref, Ptr> iterator;
typedef bidirectional_iterator_tag iterator_category;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef ptrdiff_t difference_type;
Node* _node;
__list_iterator(Node* node)
:_node(node)
{
}
bool operator!=(const iterator& it) const
{
return _node != it._node;
}
bool operator==(const iterator& it) const
{
return _node == it._node;
}
Ref operator*()
{
return _node->_data;
}
//T* operator->()
Ptr operator->()
{
return &(operator*());
}
// ++it
iterator& operator++()
{
_node = _node->_next;
return *this;
}
// it++
iterator operator++(int)
{
iterator tmp(*this);
_node = _node->_next;
return tmp;
}
// --it
iterator& operator--()
{
_node = _node->_prev;
return *this;
}
// it--
iterator operator--(int)
{
iterator tmp(*this);
_node = _node->_prev;
return tmp;
}
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
list()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
void push_back(const T& x)
{
//Node* tail = _head->_prev;
//Node* newnode = new Node(x);
_head tail newnode
//tail->_next = newnode;
//newnode->_prev = tail;
//newnode->_next = _head;
//_head->_prev = newnode;
insert(end(), x);
}
void push_front(const T& x)
{
insert(begin(), x);
}
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next);
}
private:
Node* _head;
};
void test_list1()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
it = lt.begin();
while (it != lt.end())
{
*it *= 2;
++it;
}
cout << endl;
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
struct Pos
{
int _a1;
int _a2;
Pos(int a1 = 0, int a2 = 0)
:_a1(a1)
, _a2(a2)
{
}
};
void test_list2()
{
int x = 10;
int* p1 = &x;
cout << *p1 << endl;
Pos aa;
Pos* p2 = &aa;
p2->_a1;
p2->_a2;
list<Pos> lt;
lt.push_back(Pos(10, 20));
lt.push_back(Pos(10, 21));
list<Pos>::iterator it = lt.begin();
while (it != lt.end())
{
//cout << (*it)._a1 << ":" << (*it)._a2 << endl;
cout << it->_a1 << ":" << it->_a2 << endl;
++it;
}
cout << endl;
}
void Func(const list<int>& l)
{
list<int>::const_iterator it = l.begin();
while (it != l.end())
{
//*it = 10;
cout << *it << " ";
++it;
}
cout << endl;
}
void test_list3()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
Func(lt);
}
void test_list4()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
it = lt.begin();
while (it != lt.end())
{
*it *= 2;
++it;
}
cout << endl;
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
lt.push_front(10);
lt.push_front(20);
lt.push_front(30);
lt.push_front(40);
lt.pop_back();
lt.pop_back();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
auto pos = find(lt.begin(), lt.end(), 4);
if (pos != lt.end())
{
lt.insert(pos, 40);
//lt.insert(pos, 30);
*pos *= 100;
}
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
}
6. リストとベクトルの比較
ベクターとリストはどちらも STL における非常に重要なシーケンシャル コンテナです。2 つのコンテナの基礎となる構造が異なるため、その特性とアプリケーション シナリオは異なります。主な違いは次のとおりです。
ベクター | リスト | |
---|---|---|
基礎構造 | ダイナミックシーケンステーブル、連続空間 | ヘッドノードを持つ二重リンク循環リスト |
ランダムアクセス | ランダム アクセスをサポートし、要素へのアクセス効率は O(1) です。 | ランダム アクセスはサポートされておらず、要素へのアクセス効率は O(N) です。 |
挿入と削除 | 任意の位置での挿入と削除は非効率であり、要素を移動する必要があります。時間計算量は O(N) です。挿入する場合、容量を増やす必要がある場合があります。増加: 新しい領域を開き、要素をコピーし、古い領域を解放します。効率が低下する | 任意の位置での挿入と削除は非常に効率的で、要素を移動する必要がなく、時間計算量は O(1) です。 |
スペース利用 | 最下層は連続した空間であり、メモリの断片化、高い空間使用率、高いキャッシュ使用率を引き起こしにくいです。 | 基礎となるノードは動的に開かれ、小さなノードはメモリの断片化、スペース使用率の低下、キャッシュ使用率の低下を引き起こす可能性があります。 |
イテレータ | オリジナルのエコロジー指針 | オリジナルのエコロジーポインタ(ノードポインタ)をカプセル化 |
イテレータの無効化 | 要素を挿入するときは、すべての反復子を再割り当てする必要があります。要素を挿入すると再拡張が発生し、元の反復子が無効になる可能性があるためです。削除する場合は、現在の反復子を再割り当てする必要があります。そうしないと無効になります。 | 要素を挿入してもイテレータは無効になりません。要素を削除すると、現在のイテレータのみが無効になり、他のイテレータは影響を受けません。 |
使用するシーン | 効率的なストレージを必要とし、ランダムアクセスをサポートし、挿入と削除の効率を気にしません | 大量の挿入および削除操作、ランダム アクセスは気にしない |
7. エンディング
ここでは C++ のリスト コンテナーについて学びました。C++ のいくつかのコンテナーは非常に重要な知識ポイントです。それらは非常にシンプルで使いやすいですが、最も重要なことは、基礎となる原理を理解し、それぞれの利点を習得する必要があることです。デメリットと違い. ポイントを押さえて、今後の勉強や仕事に柔軟に活用できるようにしましょう。
最後に、辛抱強く読んでサポートしていただいたことに感謝します。この記事がよく書かれていると思う友人は、3 回フォローしてサポートすることができます。この記事に質問がある場合、または間違いがある場合は、私にプライベート メッセージを送信するか、コメント欄にメッセージを残してください。ディスカッション、皆さんに改めて感謝します。