要約
このレッスンは、コースの第 6 単元、複合データを扱う単元の始まりである。単元が何のためにあるかを説明する。データのコレクション(固定サイズと可変サイズの並び、表、レコード)を表し、それを基本的なアルゴリズムで処理すること、そのアルゴリズムを手で書くこともライブラリから借りることもでき、そのコストを見積もることを教えるのである。単元の六つのレッスンと、それらを結ぶ糸を紹介する。単元が前提とすること、とりわけ不変条件をもつループと const 参照渡しを振り返る。単元が解決する問題を述べる。個数のわからないデータを保存しなければならないのに、ばらばらの変数で書くと、余ったデータを黙って見過ごしてしまうプログラムの問題である。アルゴリズムを測る第二の尺度として、正しさと並ぶコストを前もって紹介する。そして、この先の単元と「C++ プログラミング II」のコースで、単元がどこへつながるかを示す。
学習目標
- 単元の内容の順序を説明し、各レッスンが次のレッスンに何をもたらすかを説明する。
- ループ、その不変条件、引数の渡し方(値渡し、参照渡し、const 参照渡し)を思い出し、必要なら復習する。
- 例を通して、アルゴリズムのコストを、正しさと並ぶその質の第二の尺度として前もってつかむ。
- 単元を終えたときに何ができるようになっているべきかを知り、それをこの先の単元で学ぶことと区別する。
導入テスト
単元に取りかかる前に、出発点を測っておくとよい。このテストは自己チェックのためのもので、採点はしない。目的はただ学習の方向づけにある。最初の四問は、単元が前提とすることを確かめる。最大値を探すループの不変条件、大きな引数の渡し方、二重の入れ子のループの周回数、そして符号なしの算術である。続く六問は、レッスンの順に、単元の中心となる考え方を扱う。並びの有効な添字、ベクターの作り方、あるサイズを何回半分にできるか、ソート済みのデータでの探索、表の二重の添字、そして同じ実体のデータを別々のコレクションに保存する危険である。まだ正解できなくてもかまわないし、そのうちのいくつかでは「まだわからない」が誠実な答えである。正解であれ不正解であれ、どの答えもその要点を説明し、それを扱うレッスンを示すからである。同じ技能は、単元の最後のまとめのレッスンで再び測る。
単元を始める前に、自分の出発点を測りましょう。短い自己評価です。採点はされず、結果はあなたのブラウザーにだけ保存されます。
問 1前の単元からコード片
int largest{0}; int t{}; while (std::cin >> t) { if (t > largest) { largest = t; } }は、largestに、読み込んだ気温のうち最大のものを残すはずである。どんな入力で失敗するか。最初の周回はそのデータを 0 と比べ、正なら保存する。正のデータが一つだけなら結果は正しい。答える前にトレースしてみるとよい。>では、最大値と等しいデータは最大値を置き換えないが、最大値は同じ数である。繰り返しは結果を変えない。初期化を不変条件で確かめていないことを示している。正しいループでは、最初の周回の前に不変条件が成り立っていなければならない。問 2前の単元からある関数が、
std::stringに保存された百万文字のテキストに母音がいくつあるかを数え、テキストは変更しない。その仮引数はどう宣言するのがよいか。値渡しは呼び出しのたびに百万文字をコピーする。正しくはあるが、何の役にも立たないコストを払っている。コピーはしないが、必要もないのにテキストの変更を許し、リテラルも一時オブジェクトも受け取れない。「コピーしない」と「変更を許す」を混同している。渡し方はインタフェースの一部である。値渡しは関数に専用のコピーを約束し、プログラムはそれを作らなければならない。const 参照は、テキストをコピーせず、変更する権限ももたずに読み、そのうえ一時オブジェクトも受け取れる。第 6 単元では、この選択が規則になる。百万個のデータのベクターを読む関数はそれをconst std::vector<int>&として受け取り、不要なコピーはコストに関する最も多い誤りになる。復習: 引数とスコープ
問 3前の単元から
++countはfor (int i{0}; i < 6; ++i) { for (int j{0}; j < i; ++j) { ++count; } }の中で何回実行されるか。正しい答え:
内側のループは のとき 0 周回、 のとき 1 周回、と続いて のとき 5 周回する。合計は で、これは である。レッスン アルゴリズムのコストはこの数え方を一般化する。 個のデータのすべての組をたどる二つのループは 周回し、この数が、アルゴリズムが百万個のデータに使えるかどうかを決める。復習: ループ
問 4前の単元から
std::println("{}", 0u - 1);は、unsigned intが 32 ビットのシステムで何を出力するか。符号なしの型が負の数を受け入れると思っている。引き算が型の最小値で止まると思っている。符号なしの算術は一周するのであって、飽和はしない。起こりうる警告とエラーを混同している。この式は正しく、その値は定まっている。問 5C++ と同じように番号をつけた 個の要素の並びで、要素を指す添字 はどれか。
日常のことばのように 1 から番号をつけている。C++ ではa[n]はもう並びの外にある。最も多い 1 ずれのエラーである。添字を 個数えているが、要素は 個しかない。誠実な答えである。レッスン 6.1 で、ゼロから数える理由とともに説明する。問 6
vは、std::vector<int> v(3, 7);のあと何を含むか。丸かっこを波かっこであるかのように読んでいる。二つの数の役割を逆にしている。誠実な答えである。レッスン 6.2 で、ベクターとその作り方を紹介する。問 7
for (int m{1024}; m > 1; m /= 2) { ... }は何周回するか。正しい答え:
各周回の始めのmの値は 1024、512、256、128、64、32、16、8、4、2 である。10 周回であり、その理由は であることで、10 は である。各ステップで問題を半分にするアルゴリズムは、およそ ステップを要し、レッスン アルゴリズムのコストはこれを対数と呼ぶ。十億個のデータでも三十ほどで足りる。復習: アルゴリズムのコスト
問 8ある電話帳には百万の名前が五十音順に並んでいる。一つを見つけるには、電話帳を真ん中で開き、名前がありえない半分を捨て、残りの半分で同じことを繰り返す。最大で何回開く必要があるか。
名前を一つずつ調べる探索の回数である。順序を利用しなければ、平均して電話帳の半分をたどることになる。百万の平方根である。部分を半分に分けることと、ブロックに分けることを混同している。常用対数を使っている。開くたびに捨てるのは半分であって、十分の九ではない。問 9
std::vector<std::vector<int>> t{{1, 2, 3}, {4, 5, 6}};のとき、t[1][0]とt.size()の値はいくつか。最初の添字を列として読み、行の代わりに列を数えている。size()が表のすべてのマスを数えると思っている。誠実な答えである。レッスン 6.5 で、表をベクターのベクターとして紹介する。問 10あるプログラムが、グループの名前を一つのベクターに、成績を別のベクターに保存し、成績
grades[i]がnames[i]の成績になるようにしている。成績のベクターだけをソートすると、何が起こるか。コンパイラにとって二つのベクターは独立している。関係は、プログラムを書いた人の頭の中にしかない。このアルゴリズムが受け取るのは一つのベクターだけで、もう一つのベクターの存在を知らない。どのアクセスも有効で、どの結果も定まっている。欠陥は論理の誤りであり、だからコースの二つのサニタイザ(メモリエラーのサニタイザ AddressSanitizer と未定義動作のサニタイザ UndefinedBehaviorSanitizer)のどちらもこれを報告しない。
コンテンツを読み込んでいます…