Developer

競技プログラミング風 C++問題集【第3回】
2020.12.10
Lv3

競技プログラミング風 C++問題集【第3回】

目次

双方向リンクリスト(難易度:★★★☆☆)

問題

下図のように、型Tの要素を保持する各ノードが前後にリンクされたデータ構造を「双方向リンクリスト」と呼ぶ。

競技プログラミング風C++問題集【第3回】_01

リストに要素が存在しない場合でも、ノードを統一的に取り扱うために、ダミーのノードを保持している(下図参照)。

競技プログラミング風C++問題集【第3回】_02

上記を理解した上で、テストコードの ToDo 部分にコードを記述し、以下を実装しなさい。

1.prev, next を相互にリンクする関数 void reLink(Node* prev, Node* nexe)
2.T型要素を持つ双方向リンクリストクラス DblLnkList<T>() のコンストラクタ
3.T型の要素を持つノードをリスト末尾に追加するメンバ関数 void push_back(const T&)
4.要素数を返すメンバ関数 size_t size()
[java] #include <iostream> // 標準入出力ライブラリ
#include <string>
using namespace std; // std 名前空間使用
#define DO_TEST(exp) do_test(exp, __LINE__)
void do_test(bool b, int line) {
if( b ) return; // OK
cout << "\nNG at line " << line << "\n";
exit(1);
}
template<typename T>
struct Node {
public:
Node(const T& body = T())
: m_body(body)
{
m_prev = m_next = nullptr;
}
public:
Node<T> *m_prev; // 直前ノードへのポインタ
Node<T> *m_next; // 直後ノードへのポインタ
T m_body; // ノード要素
};
template<typename T>
void reLink(Node<T>* prev, Node<T>* next) { // prev と next を双方向にリンクする
// DoDo: ここに prev と next を双方向にリンクするコードを記述
}

template<typename T>
class DblLnkList { // 双方向リンクリスト クラス
public:
DblLnkList() { // コンストラクタ
// ToDo: ここに双方向リンクリスト初期化コードを記述
}
void push_back(const T& body) {
// ToDo: ここにコードを追加し、要素bodyを持ったノードをリスト末尾に追加
}
size_t size() const {
// ToDo: ここに要素数を返すコードを記述
return 0;
}
private:
Node<T>* m_dummy; // ダミーノードへのポインタ
size_t m_size; // 要素数
friend int main();
};
int main() {
// relink のテスト
Node<string> n1, n2;
reLink<string>(&n1, &n2);
DO_TEST(n1.m_prev == nullptr);
DO_TEST(n1.m_next == &n2);
DO_TEST(n2.m_prev == &n1);
DO_TEST(n2.m_next == nullptr);
// コンストラクタのテスト
DblLnkList<string> lst;
DO_TEST( lst.m_dummy != nullptr );
auto* dummy = lst.m_dummy;
DO_TEST( dummy->m_next == dummy );
DO_TEST( dummy->m_prev == dummy );
DO_TEST( lst.m_size == 0 );
// push_back のテスト
lst.push_back("abc");
DO_TEST( lst.m_size == 1 );
lst.push_back("xyzzz");
DO_TEST( lst.m_size == 2 );
auto* first = dummy->m_next;
DO_TEST(first->m_body == "abc");
auto* last = dummy->m_prev;
DO_TEST(last->m_body == "xyzzz");
// size() のテスト
DO_TEST( lst.size() == 2 );
lst.push_back("314");
DO_TEST( lst.size() == 3 );
//
cout << "\nGood Job!\n";
return 0;
}
[/java]

ヒント

・prev から next に順方向リンクは張るには「prev->m_next = next」と記述するんだぞ。
・next から prev に逆方向リンクは張るには「next->m_prev = prev」と記述するんだぞ。
・コンストラクでは [1] ダミーノードを生成 [2] ダミーノードを自分自身と双方向にリンク の2つの処理が必要だぞ。
・双方向にリンクを張るには reLink() 関数を使うぞ。
・要素を末尾に追加するには、その要素を持ったノードを生成し、末尾ノードとダミーノードの間に挿入するんだぞ。
・末尾ノードは、ダミーノードのひとつ前にリンクされているぞ。
・要素追加時には、メンバ変数の m_size をインクリメントするのを忘れないように。
・要素数を返すには、メンバ変数の m_size を返すだけだぞ。

解答例
[java] template<typename T>
void reLink(Node<T>* prev, Node<T>* next) { // prev と next を双方向にリンクする
prev->m_next = next; // 前から次へのリンクを張る
next->m_prev = prev; // 次から前へのリンクを張る
}

DblLnkList() { // コンストラクタ
m_size = 0; // 要素数を0に
m_dummy = new Node<T>(T()); // ダミーノード作成
reLink(m_dummy, m_dummy); // ダミーノードの前後を自分自身にリンクする
}

void push_back(const T& body) {
auto* ptr = new Node<T>(body); // 新規ノード作成
reLink(m_dummy->m_prev, ptr); // 末尾ノードの次に新規ノードをリンク
reLink(ptr, m_dummy); // 新規ノードの次にダミーノードをリンク
++m_size; // 要素数をインクリメント
}

size_t size() const {
return m_size; // メンバ変数で保持している要素数を返すだけ
}
[/java]

解説

まずは prev と next を双方向にリンクする void reLink(Node<T>* prev, Node<T>* next) を実装する。 実装は簡単で「prev->m_next = next;」「next->m_prev = prev;」を記述して、順方向・逆方向にリンクを張るだけだ。

コンストラクタでは、ダミーノードを生成し、reLink() 関数を使って、ダミーノードを自分自身と双方向にリンクすればよい。

要素を末尾に追加するには、末尾ノードとダミーノードの間に新規要素ノードを挿入するとよい。
末尾ノードは、ダミーノードのひとつ前にリンクされているので m_dummy->prev で取り出すことができる。 あとはすでに作成した reLink() 関数を使って、前後にリンクを張るだけだ。
注意としては、先に「reLink(ptr, m_dummy);」を行ってしまうと、m_dummy->prev が変更されてしまい、 末尾ノードが取り出せなくなるので、先に末尾ノードと新規ノードをリンクしてから、新規ノードとダミーノードをリンクする、 という順番にしなくてはいけないという点だ。
あとは、要素数をインクリメントするのをわすれずに。

最後に要素数を返す size() がだ、この実装は簡単でメンバ変数として保持している m_size を返すだけだ。

競技プログラミング風 標準C++ライブラリ入門 連載目次リンク

競技プログラミング風 標準C++ライブラリ入門 連載目次

筆者:津田伸秀
プロフィール:テニス・オセロ・ボードゲーム・パズル類が趣味の年齢不詳のおじさん。 自宅研究員(主席)。vi と C++が好き。迷走中・・・ ボードゲーム・パズル系アプリ開発・リリースしてます。