第 2 章 配列¶
はじめに¶
配列はプログラミングの基本的なデータ構造です。C 言語では配列はメモリ上に連続して確保された領域であり、インデックスでアクセスします。この章では配列の基本操作と素数列挙アルゴリズムを TDD で実装します。
1. 配列の最大値¶
Red — 失敗するテストを書く¶
void test_max_of(void) {
int a[] = {1, 7, 3, 5, 2};
ASSERT_EQ_INT(7, max_of(a, 5));
int b[] = {-3, -1, -5};
ASSERT_EQ_INT(-1, max_of(b, 3));
}
Green — テストを通す実装¶
int max_of(const int *a, int n) {
int max = a[0];
for (int i = 1; i < n; i++)
if (a[i] > max) max = a[i];
return max;
}
C では配列をポインタ(const int *a)として受け取り、長さ(n)を別に渡します。Python と違い len() が存在しないため、必ず長さを渡す必要があります。
2. 配列の逆順コピー¶
Red — 失敗するテストを書く¶
void test_reverse_array(void) {
int src[] = {1, 2, 3, 4, 5};
int dst[5];
reverse_array(src, dst, 5);
ASSERT_EQ_INT(5, dst[0]);
ASSERT_EQ_INT(1, dst[4]);
}
Green — テストを通す実装¶
void reverse_array(const int *src, int *dst, int n) {
for (int i = 0; i < n; i++)
dst[i] = src[n - 1 - i];
}
3. 基数変換¶
10 進数を任意の基数(2〜36 進数)に変換します。
Red — 失敗するテストを書く¶
void test_card_conv(void) {
char buf[64];
card_conv(59, 2, buf, sizeof(buf));
ASSERT_EQ_STR("111011", buf);
card_conv(59, 16, buf, sizeof(buf));
ASSERT_EQ_STR("3B", buf);
}
Green — テストを通す実装¶
void card_conv(int x, int r, char *buf, size_t bufsz) {
static const char digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
char tmp[66];
int i = 0;
do {
tmp[i++] = digits[x % r];
x /= r;
} while (x > 0 && i < 65);
tmp[i] = '\0';
/* 文字列を逆順にしてコピー */
int len = i;
for (int j = 0; j < len && (size_t)j < bufsz - 1; j++)
buf[j] = tmp[len - 1 - j];
buf[len < (int)bufsz ? len : (int)bufsz - 1] = '\0';
}
剰余を使って下の桁から求め、最後に逆順にすることで正しい基数表現を得ます。
4. 素数列挙¶
素数を 3 通りの方法で列挙し、パフォーマンスを比較します。
Red — 失敗するテストを書く¶
void test_prime(void) {
int primes[200];
int n1 = prime1(primes, 200);
int n2 = prime2(primes, 200);
int n3 = prime3(primes, 200);
ASSERT_EQ_INT(25, n1); /* 1000以下の素数は25個 */
ASSERT_EQ_INT(25, n2);
ASSERT_EQ_INT(25, n3);
}
Green — 方法 1: 全数割り算¶
int prime1(int *out, int max_count) {
int n = 0;
for (int i = 2; n < max_count; i++) {
int is_prime = 1;
for (int j = 2; j < i; j++) {
if (i % j == 0) { is_prime = 0; break; }
}
if (is_prime) {
out[n++] = i;
if (i > 999) break;
}
}
return n;
}
Green — 方法 2: 既知の素数のみで割り算¶
int prime2(int *out, int max_count) {
out[0] = 2;
int n = 1;
for (int i = 3; i <= 1000 && n < max_count; i += 2) {
int is_prime = 1;
for (int j = 1; j < n; j++) {
if (i % out[j] == 0) { is_prime = 0; break; }
}
if (is_prime) out[n++] = i;
}
return n;
}
Green — 方法 3: 平方根まで割り算(最効率)¶
int prime3(int *out, int max_count) {
(void)max_count; /* 固定範囲(1-1000)のため未使用 */
out[0] = 2; out[1] = 3;
int n = 2;
for (int i = 5; i <= 1000; i += 2) {
int is_prime = 1;
for (int j = 1; (long long)out[j] * out[j] <= i; j++) {
if (i % out[j] == 0) { is_prime = 0; break; }
}
if (is_prime) out[n++] = i;
}
return n;
}
方法 3 は √i までしか割り算しないため、方法 1 と比べて大幅に高速です。
| 方法 | 割り算の回数 | 特徴 |
|---|---|---|
| prime1 | 多い | シンプル |
| prime2 | 中程度 | 既知の素数で割る |
| prime3 | 少ない | √i で打ち切り |
テスト実行結果¶
$ make test-chapter02/arrays
=== Testing chapter02/arrays ===
13 tests run: 13 passed, 0 failed
まとめ¶
| 関数 | C の特徴 |
|---|---|
max_of |
const int *a, int n の組で配列を渡す |
reverse_array |
入力・出力バッファを別々に受け取る |
card_conv |
char *buf, size_t bufsz でバッファを受け取る |
prime1/2/3 |
int *out, int max_count で出力先を受け取る |
参考文献¶
- 『新・明解 C で学ぶアルゴリズムとデータ構造』 — 柴田望洋
- 『テスト駆動開発』 — Kent Beck