ようへいの日々精進XP

よかろうもん

アルゴリズムとプログラミング 「第 7 章 配列の操作」の予習とまとめ

これは

放送大学教養学部の「アルゴリズムとプログラミング」という授業で使われる「アルゴリズムとプログラミング」という教材書籍を自分なりにまとめたものです.

1. 配列のデータ挿入

既にデータが挿入されている配列に要素を追加する場合を考える.

添字 0 1 2 3 4 5 6 7 8 9
データ 100 300 500 400 700 800 600

添字 6 にデータを追加する場合, データを追加するだけなので特に難しいことは無い. ところが, 以下のように添字 0 にデータを挿入する場合, 各添字のデータを右に 1 つずつ移動して添字 0 にデータを挿入する為の空きを作る必要がある.

添字 0 1 2 3 4 5 6 7 8 9
データ 600 100 300 500 400 700 800

このデータ移動について, 配列内のデータの数を n とすると, 運が良ければデータ移動は不要であるが, 最悪の場合には $n$ 個のデータを移動させて配列の空きを確保する必要がある. この時に必要な平均の計算量は O(n) となる.

2. 配列からのデータ削除

挿入と同様に, 既にデータが挿入されている配列から添字を指定してデータを削除する場合を考える. また, この削除ではデータを削除した後にデータが配列の先頭から連続的に並ぶような操作を行う.

添字 0 1 2 3 4 5 6 7 8 9
データ 100 300 500 400 700 800 600

データ 600 を削除する. この削除は簡単でデータを削除した後でも既にデータが配列の先頭から連続的に並んでいる為, 特別な操作は不要である.

添字 0 1 2 3 4 5 6 7 8 9
データ 600 100 300 500 400 700 800

ところが, 上記のように添字 0 のデータ 600 を削除した場合, 削除した後は添字 0 に空きが出来てしまう為, データを 1 個ずつ順番に移動して配列先頭からデータが連続的に並ぶようにする必要がある. この場合, 挿入と同様に配列内のデータの数を n とすると, 最良の場合はデータの移動は不要であるが, 最悪の場合では $n$ 個のデータを移動させる必要がある為, 平均の計算量は O(n) となる.

3. 探索

線形探索, 二分探索

配列のデータを探索する手法として, 線形探索 (linear search) と二分探索 (binary search) について検討する.

3.1 線形探索

添字の値を順番に増加させながら, 配列の要素となるデータと, 探索キー (search key) となるデータを順番に比較していくことで探索出来る. 以下のコードは線形探索を行う実装例となる. (chapter 07 (Algorithms and Programming 2016) より引用.)

/* code: ex7-1.c   (v1.16.00) */
#include <stdio.h>
#include <stdlib.h>
#define ARRAY_SIZE 13
 
/* ------------------------------------------- */
int linear_search (int array[], int n, int key)
{
  int i;
  for (i = 0; i < n; i++) {
    if (array[i] == key) {
      return i;
    }
  }
  return -1;
}
 
/* ------------------------------------------- */
void print_array (int array[], int n)
{
  int i;
  for (i = 0; i < n; i++) {
    printf ("%d ", array[i]);
  }
  printf ("\n");
}
 
/* ------------------------------------------- */
int main ()
{
  int index, key;
  int array[ARRAY_SIZE] = {
    900, 990, 210, 50, 80, 150, 330,
    470, 510, 530, 800, 250, 280
  };
  key = 800;
  print_array (array, ARRAY_SIZE);
  index = linear_search (array, ARRAY_SIZE, key);
  if (index != -1) {
    printf ("Found: %d (Index:%d)\n", key, index);
  }
  else {
    printf ("Not found: %d\n", key);
  }
  return 0;
}

これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc ex7-1.c -g3 -o ex7-1
root@0be431eebb77:/work# ./ex7-1
900 990 210 50 80 150 330 470 510 530 800 250 280
Found: 800 (Index:10)

このコードの linear_search 関数は, 配列内から線形探索によって探索キーと一致するデータを探し, データが見つかれば, 見つかったデータの配列の添字の値を返す.

これを少し改修. 検索対象のデータ (key) を scanf 関数で入力出来るようにして, 探索対象となる配列のデータは rand 関数を利用して乱数で生成するようにした.

/* code: ex7-1a.c   (v1.16.00) */
#include <stdio.h>
#include <stdlib.h>
#define ARRAY_SIZE 1000000

/* ------------------------------------------- */
int linear_search (int array[], int n, int key)
{
  int i;
  for (i = 0; i < n; i++) {
    if (array[i] == key) {
      return i;
    }
  }
  return -1;
}

/* ------------------------------------------- */
void print_array (int array[], int n)
{
  int i;
  for (i = 0; i < n; i++) {
    printf ("%d ", array[i]);
  }
  printf ("\n");
}

/* ------------------------------------------- */
int main ()
{

  int key;
  printf ("Enter an integer: ");
  scanf ("%d", &key);

  int index;
  int array[ARRAY_SIZE];
  int i;
  for (i = 0; i < ARRAY_SIZE; i ++) {
    array[i] = rand () % ARRAY_SIZE;
  }
  // print_array (array, ARRAY_SIZE);
  index = linear_search (array, ARRAY_SIZE, key);
  if (index != -1) {
    printf ("Found: %d (Index:%d)\n", key, index);
  }
  else {
    printf ("Not found: %d\n", key);
  }
  return 0;
}

これをコンパイルして実行する.

root@0be431eebb77:/work# gcc ex7-1a.c -g3 -o ex7-1a
root@0be431eebb77:/work# ./ex7-1a
Enter an integer: 456
Found: 456 (Index:757414)

3.2 二分探索

二分探索 (binary search) は, 整列済みの配列に対して探索を行う. 整列 (sorting) とは, データを値の大小関係に従って並べる操作で, 整列された配列は順序配列 (ordered array) と呼ぶ. 二分探索では, 配列の中央の値と比較し, 検索するキーの値との大小関係を基に探索を進める.

検索したい値が中央の値より大きいか, 小さいか調べ, 二分割した配列の片方には, 目的のキーの値が存在しないことを確かめながら検索を行う. 二分探索は整列済みの配列でなければならないという制約があるものの, 平均の計算量は線形探索よりも高速である.

以下, 11 個の要素を持った順序配列に対して二分探索が行われる様子を図示したもの.

まずは, 配列の要素はソートされて 100 から順番に格納されている.

添字 0 1 2 3 4 5 6 7 8 9 10 11
データ 100 200 300 400 500 600 700 800 900 990 999

この配列からデータ 800 を二分探索していくと, まずは添字 5 のデータ 600 と探索対象の 800 を比較し, 600 以下のデータは探索の対象外となる.

添字 0 1 2 3 4 5 6 7 8 9 10 11
データ 100 200 300 400 500 600 700 800 900 990 999

次に残った配列の要素の中央値である添字 8 の 900 と探索対象の 800 を比較し, 800 よりも大きい為, 添字 8 以上のデータは探索の対象外となる.

添字 0 1 2 3 4 5 6 7 8 9 10 11
データ 100 200 300 400 500 600 700 800 900 990 999

という流れで二分探索によって 800 が探索されるが, 処理の過程で配列の探索範囲が半分になっていく為, データが多くなると, 二分探索は線形探索と比べると圧倒的に高速に処理されることが解る.

以下のコードは二分探索を行うコード例である. (chapter 07 (Algorithms and Programming 2016) より引用. 若干, 上の図に合わせて改変している.)

/* code: ex7-2.c   (v1.16.00) */
#include <stdio.h>
#include <stdlib.h>
#define ARRAY_SIZE 11

/* ------------------------------------------- */
int binary_search (int array[], int num, int key)
{
  int middle, low, high;
  low = 0;
  high = num - 1;
  while (low <= high) {
    middle = (low + high) / 2;
    if (key == array[middle]) {
      return middle;
    }
    else if (key < array[middle]) {
      high = middle - 1;
    }
    else {
      low = middle + 1;
    }
  }
  return -1;
}

/* ------------------------------------------- */
void print_array (int array[], int n)
{
  int i;
  for (i = 0; i < n; i++) {
    printf ("%d ", array[i]);
  }
  printf ("\n");
}

/* ------------------------------------------- */
int main ()
{
  int index, key;
  int array[ARRAY_SIZE] = {
    50, 80, 150, 210, 250, 280, 330,
    470, 510, 530, 800
  };

  key = 210;
  print_array (array, ARRAY_SIZE);
  index = binary_search (array, ARRAY_SIZE, key);
  if (index != -1) {
    printf ("Found: %d (Index:%d)\n", key, index);
  }
  else {
    printf ("Not found: %d\n", key);
  }
  return 0;
}

これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc ex7-2.c -g3 -o ex7-2
root@0be431eebb77:/work# ./ex7-2
50 80 150 210 250 280 330 470 510 530 800
Found: 210 (Index:3)

このコードは, 前の線形探索と同様に配列の要素をランダムに生成するようにして, 且つ, 探索の対象は画面から入力出来るように改造する余地は十分にある.

尚, 線形探索 (ex7-1) と二分探索 (ex7-2) で実行速度を比較してみると, 配列の要素数が 13 と小さいものの以下のような若干の差異が見られた.

root@0be431eebb77:/work# time ./ex7-1
900 990 210 50 80 150 330 470 510 530 800 250 280
Found: 800 (Index:10)

real    0m0.004s
user    0m0.000s
sys     0m0.000s
root@0be431eebb77:/work# time ./ex7-2
50 80 150 210 250 280 330 470 510 530 800 900 990
Found: 210 (Index:3)

real    0m0.003s
user    0m0.000s
sys     0m0.000s
root@0be431eebb77:/work# time ./ex7-1
900 990 210 50 80 150 330 470 510 530 800 250 280
Found: 800 (Index:10)

real    0m0.005s
user    0m0.000s
sys     0m0.000s
root@0be431eebb77:/work# time ./ex7-2
50 80 150 210 250 280 330 470 510 530 800 900 990
Found: 210 (Index:3)

real    0m0.004s
user    0m0.000s
sys     0m0.000s

演習問題

問 7.1

順序配列について説明しなさい.

  • データが整列された配列を順序配列 (ordered array) と呼ばれている

問 7.2

線形探索と二分探索で探索に必要な平均の計算量について答えなさい.

探索手法 平均の計算量 メモ
線形探索 O(n) データ量に応じて計算量が増える
二分探索 O(log\ n) 処理の度に処理対象を減らすようなアルゴリズム

問 7.3

二分探索の特徴を簡単に述べなさい.

  • 順序配列に対して探索を行う
  • 配列の中央値との値を比較し, 検索キーの値との大小関係を基に探索を進める
  • 検索したい値が中央の値よりも大きいか, 小さいかを調べ, 二分割した配列の片方には, 目的のキーの値が存在しないことを確かめながら検索を行う
  • 平均の計算量は O(log\ n) で高速である

問 7.4

C 言語のライブラリに含まれる lsearch 関数を利用したプログラムを作成しなさい.

こちら より引用.

/* code: q7-1.c   (v1.16.00) */
#include <stdio.h>
#include <stdlib.h>
#include <search.h>
 
#define ARRAY_SIZE 10
 
/* ------------------------------------------- */
int compare (int *x, int *y)
{
  return (*x - *y);
}
 
/* ------------------------------------------- */
void print_array (int array[], int n)
{
  int i;
  for (i = 0; i < n; i++) {
    printf ("%d ", array[i]);
  }
  printf ("\n");
}
 
/* ------------------------------------------- */
int main ()
{
  int key;
  int *r;
  size_t elements;
  int array[ARRAY_SIZE] = {
    12, 19, 70, 44, 16, 38, 10, 30, 28, 98
  };
  key = 16;
  elements = ARRAY_SIZE;
  print_array (array, ARRAY_SIZE);
 
  r = (int *) lsearch (&key, &array, &elements, sizeof (int),
               (int (*)(const void *, const void *)) compare);
  if (r != NULL) {
    printf ("Found: %d\n", key);
  }
  else {
    printf ("Not found: %d\n", key);
  }
  return 0;
}

これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q7-1.c -g3 -o q7-1
root@0be431eebb77:/work# ./q7-1
12 19 70 44 16 38 10 30 28 98
Found: 16

問 7.5

C 言語のライブラリに含まれる bsearch 関数を利用したプログラムを作成しなさい.

こちら より引用.

/* code: q7-2.c   (v1.16.00) */
#include <stdio.h>
#include <stdlib.h>
#include <search.h>
 
#define ARRAY_SIZE 10
 
/* ------------------------------------------- */
int compare (int *x, int *y)
{
  return (*x - *y);
}
 
/* ------------------------------------------- */
void print_array (int array[], int n)
{
  int i;
  for (i = 0; i < n; i++) {
    printf ("%d ", array[i]);
  }
  printf ("\n");
}
 
/* ------------------------------------------- */
int main ()
{
  int key;
  int *r;
  int array[ARRAY_SIZE] = {
    10, 12, 16, 19, 28, 30, 38, 44, 70, 98
  };                /* ordered array! */
 
  key = 16;
  print_array (array, ARRAY_SIZE);
 
  r = (int *) bsearch (&key, &array, ARRAY_SIZE, sizeof (int),
               (int (*)(const void *, const void *)) compare);
  if (r != NULL) {
    printf ("Found: %d\n", key);
  }
  else {
    printf ("Not found: %d\n", key);
  }
  return 0;
}

これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q7-2.c -g3 -o q7-2
root@0be431eebb77:/work# ./q7-2
10 12 16 19 28 30 38 44 70 98
Found: 16

問 7.6

コード (q7-3.c) は, 1 節と 2 節で述べた配列操作に関連するコードである. このコードは, データ挿入, データ削除, データ挿入可能な場所の探索を行う関数からなっている. gprof 等のプロファイラで関数を分析しなさい.

こちら より引用.

/* code: q7-3.c   (v1.16.00) */
#include <stdio.h>
#include <stdlib.h>
#define MAX 1000000
 
/* ------------------------------------------- */
void array_print (int a[], int max)
{
  int i;
  for (i = 0; i < max; i++) {
    printf ("%02d ", a[i]);
  }
  printf ("\n");
}
 
/* ------------------------------------------- */
int array_find_empty (int a[], int max)
{
  int i;
  for (i = 0; i < max; i++) {
    if (a[i] == -1) {
      return i;
    }
  }
  return -1;
}
 
/* ------------------------------------------- */
void array_insert (int a[], int max, int index, int empty, int data)
{
  int i;
  if (empty > index) {
    for (i = empty; i > index; i--) {
      a[i] = a[i - 1];
    }
  }
  else {
    for (i = empty; i < index; i++) {
      a[i] = a[i + 1];
    }
  }
  a[index] = data;
}
 
/* ------------------------------------------- */
int array_delete (int a[], int index)
{
  int data;
  data = a[index];
  a[index] = -1;
  return data;
}
 
/* ------------------------------------------- */
int main ()
{
  int i, j, index_ins, index_del, empty, data;
  int a[MAX];
 
  for (j = 0; j < MAX; j++) {
    a[j] = rand () % 100;
  }
  data = a[MAX / 2];
  a[MAX / 2] = -1;
 
  for (i = 0; i < 1000; i++) {
    index_ins = rand () % MAX;
    index_del = rand () % MAX;
    /* printf("ins:%d  del:%d\n", index_ins, index_del );  */
 
    empty = array_find_empty (a, MAX);
    /* printf("empty:%d\n", empty ); */
 
    array_insert (a, MAX, index_ins, empty, data);
 
    data = array_delete (a, index_del);
    /* array_print( a, MAX ); */
  }
  return 0;
}

とりあえず, これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q7-3.c -g3 -o q7-3
root@0be431eebb77:/work# ./q7-3
root@0be431eebb77:/work#

何も出力されない. gprof で解析してみたが...解答のような結果が出力されないので要調査.

以上

  • 今まで何気なく配列にデータの出し入れを実装していたけど, その裏でこんなことが行われていたのかー, 面白い
  • 線形探索, 二分探索の違いについて理解出来た
  • 実際にコードを書く際には lsearch 関数, bsearch 関数を利用すれば良い
  • gprof の解析がうまく動かない (解答のような結果が出力されない) ので調査しよう

2018 年 11 月 14 日 (水)

ジョギング

  • 引き続き, 足の痛み (と言っても筋肉痛) が引かないのでお休み
  • 自宅から博多駅の筑紫口まで散歩する, 往復で 30 分くらい, ヨドバシに行くときにはこのルートで歩こうと思った次第

日課

  • お休み

奥さん, 風邪のひきはじめ

  • 先日の応援等で疲れが出てしまったんだと思う
  • あまり無理はしてほしくないものの, 明日は仕事で鹿児島に行かないといけないとのこと

夕飯

  • 適当鍋
  • 引き続き, 茅乃舎のだしを使ったので優しい味で美味しかった

2018 年 11 月 13 日 (火)

ジョギング

  • お休み
  • 30 分程散歩する

日課

  • お休み

マッサージ

  • お昼休みを利用して近所のマッサージ屋さんにて
  • 時間があれば 2 時間くらいやって頂きたい気分...

夕飯

  • 根菜鍋
  • 茅乃舎のだしを使ったので優しい味で美味しかった

別府大分毎日マラソン

来年の 2/3 に行われる, 第 68 回別府大分毎日マラソンにエントリーした.

www.betsudai.com

2018 年 11 月 12 日 (月)

ジョギング

  • お休み
  • 流石に朝起きると激しい筋肉痛で歩くのもままならない状態で...

日課

  • お休み

両親

  • 昨日の福岡マラソンを応援にきてくれた両親が鹿児島に帰っていった
  • 本当に有り難いし, いつまで自分が走る姿を見せられるかなと思いつつ見送った

終日

  • 体調がイマイチな状態だった
  • 好きで走っているので, こんなことくらいで体調崩すわけにはいかんざき

久しぶりの #福岡マラソン で #ムラムラ大作戦が功を奏して 2 回目のサブスリー達成できたのでメモ

久しぶりの #福岡マラソン

明日は青山学院大学に倣って #ムラムラ大作戦 で完走を目指します。皆さん、心のなかで応援をお願い致します。#福岡マラソン2018

2015 年に出場して以来の福岡マラソン. 今年の 3 月の鹿児島マラソンサブスリーを達成していたので, 今回の目標ももちろんサブスリー.

ところが...

先週の木曜日あたりに右の胸の下あたりに強い痛みがあり, 病院にいくと肋軟骨損傷という診断. 呼吸をすればするだけ肋骨がきしむように痛む状況になってしまいました. また, 金曜日の夜中にベッドから降りる際に左足の筋を激しく伸ばしてしまい, あるくと強い痛みが出る状況になり... 万事休すかと思いましたが, それぞれの患部をあっためたり, 痛み止めを飲んだりしながら, なんとかスタートラインに立つことができました.

スタートラインに立てたことに感謝して走ってきまーす。#ムラムラ大作戦 #福岡マラソン

ムラムラ大作戦

日々のトレーニン

週 5 〜 週 7 で 30 分ジョッグがメインです. トレーニングでこれ以上走ることはほとんどありません. しかし, フルマラソン後半の足の攣りなどを考えると月に 2 回くらいは長い距離を走ったほうがいいかもしれません...

5 キロ毎のスプリット

f:id:inokara:20181111173126p:plain

ペースランナーに引っ張ってもらって 4'15"/Km 平均で走り切ることが出来ました.

ペースランナー

今回もペースランナーにずっとついて走りました. ペースランナーについて走ることでペースだけでなく走りのリズムもうまく合ってくるのでとても走りやすかったです. また, ペースランナーについて走ることで, 呼吸はほとんど乱れず走り続けることが出来ました. ペースランナーが引っ張っているグループで走っていると, ランナー同士で会話したり出来るので不思議な一体感も生まれてくるのでサイコーです. ただ, 大きな集団になるので前のランナーとの接触には気をつけたいところです.

35 キロ以降, 足が棒

ペースランナーに引っ張ってもらっていても, やはり, マラソンの鬼門, 35 キロ以降になると足が攣り始めて足が完全に棒になってしまい, 足を前を出す度にピキピキと音がするくらいに足が攣ってとても辛かったです. 特に鬼門と思っていたのらん坂を意外に簡単に登ることが出来ましたが, 下図 (福岡マラソンサイトより引用) のようにのらん坂以降に続く細かいアップダウンがかなり足に堪えました.

f:id:inokara:20181112062455p:plain

後半から終盤にかけての足の痛みをどの程度抑えることが出来るかがサブスリーへの鍵になると思います. また, 足の攣りについては, 以下のサイトに詳しく書かれていました.

www.kobayashi.co.jp

レース中に芍薬甘草湯を摂ることで, 足の攣りをある程度は回避出来るようです. 次のマラソンでは試してみたいと思います.

ムラムラ大作戦

沿道の若い女の子を目で追ってムラムラ..という作戦ではありませんが, 今回はマラソンシーズンの初戦ということで楽しく走ることを目標にしていました. 終盤は足が棒になりつつも楽しくゴールすることができたと思います.

最後に

福岡マラソンに限ったことではありませんが, 大会運営に携わっているたくさんの方々, ボランティアスタッフ, 沿道の皆さんの声援があってこそ無事に完走することが出来るんだなと思いました. この場をかりてお礼を申し上げたいと思います. 本当に有難うございました.

皆さんの応援のおかげで無事にゴールしました。#ムラムラ大作戦 #福岡マラソン ありがとうございました~。北島サブスリーです。

そして, マネージャー兼応援団長をやってくれる奥さん, わざわざ鹿児島から応援に来てくれた両親にスーパー有難うございました.

2018 年 11 月 11 日 (日)

ジョギング

  • 福岡マラソン出走
  • 2 回目のサブスリー 2 時間 58 分 01 秒, ベストタイムには及ばなかった
  • 色々と体を痛めてしまい, 一時は出場を辞退しないといけないという状況だったので (言い訳) このタイムには満足している

日課

  • お休み

応援

  • マネージャー兼応援団長の奥さんが頑張ってくれた
  • 本当にありがとう

2018 年 11 月 10 日 (土)

ジョギング

  • 40 分くらい
  • 走って明日の福岡マラソンのゼッケンなどを取りに行く

日課

  • お休み
  • 完全に痛みが引いたら再開する

両親

  • 両親が福岡マラソンの応援に駆けつけてくれた
  • 奥さんが一生懸命にもてなしてくれるので本当に有り難い

明日は

2018 年 11 月 09 日 (金)

ジョギング

  • お休み
  • お昼休みに近所のマッサージ屋さんで軽く腰から足をほぐしてもらう

日課

  • お休み
  • 完全に痛みが引いたら再開する

明日は

  • 福岡マラソンの準備をする (ゼッケンを取りに行ったり...)

2018 年 11 月 08 日 (木)

ジョギング

  • 山王公園往復
  • 引き続き, 胸の痛みが治まってきた感じ...
  • やっと 11 日のスタートラインに立てる気がしてきた

日課

  • お休み
  • 完全に痛みが引いたら再開する

43 回目の誕生日

  • まずは元気にこの世に命を授けてくれた両親に感謝
  • 自分勝手な自分をいつも支えてくれる奥さんに感謝
  • リモートで支えてくれるチームの皆さんに感謝

Facebook でもたくさんのおめでとうごコメントを頂いて, 本当に有難うございました. コメントを返しながら, 誕生日で懐かしい人達との再開を与えてくれたり, それらの人たちに対して感謝をする日なんだろうなあと.

夕飯は奥さんが気を利かせて新型 MacBook Air をサプライズで買ってきてくれると思っていたが, それは流石に無かったけど, 尾ノ上の特上寿司をテイクアウトしてくれて細やかな誕生祭を催してくれた. ありがとう . 尾ノ上のお寿司, 本当に美味しかった.

www.apple.com

ということで, 今年一年, 家族のみんなが元気に過ごせますように.

アルゴリズムとプログラミング 「第 6 章 配列の仕組み」の予習とまとめ

これは

放送大学教養学部の「アルゴリズムとプログラミング」という授業で使われる「アルゴリズムとプログラミング」という教材書籍を自分なりにまとめたものです.

1. 配列の仕組み

  • 同じ型の変数データを多数利用する場合, 変数の宣言や取扱が煩雑になる
  • このような問題を解決するする方法の一つとして配列 (array) がある
  • 配列の要素を指定する為の通し番号は添字 (index) と呼ばれる

以下は配列と添字の関係を示したもの.

添字 0 1 2 3 4 5 6 7 8 9
30 20 10 25 15
  • 添字には整数値が使われ, その値は 0 や 1 からスタートするが, -1 や -2 のような負の値の添字を使うことが出来るプログラミング言語もある
  • 連想配列と呼ばれる, 整数以外の文字列等の添字を使うことが出来るプログラミング言語もある
  • 配列の要素には同じデータ型しか使えないが, 異なるデータ型を混ぜて使える配列も存在する

以下は C 言語での配列の宣言例.

int a[10];

この例では 10 個の整数型の様子を持つ a という名前の配列を宣言している.

以下, 配列を使わずに 5 つの整数の平均値を求めるコード.

/* code: ex6-1.c */
#include <stdio.h>

int main ()
{
  int a, b, c, d, e;
  int sum, avg;
  
  a = 30;
  b = 20;
  c = 10;
  d = 25;
  e = 15;
  sum = a + b + c + d + e;
  avg = sum / 5;
  printf ("%d\n", avg);
  
  return 0;
}

上記のコードについて, 配列を使うと以下のようになる.

/* code: ex6-2.c */
#include <stdio.h>

int main ()
{
  int a[10];
  int i, sum, avg;
  
  a[0] = 30;
  a[1] = 20;
  a[2] = 10;
  a[3] = 25;
  a[4] = 15;
  sum = 0;
  for (i = 0; i < 5; i++) {
    sum += a[i];
  }
  avg = sum / 5;
  printf ("%d\n", avg);
  
  return 0;
}

このコードでは, for ループにより変数 i が増加する. この変数は配列の添字となっており, 配列の各要素が加算されて 5 つの数の合計が求められる.

root@0be431eebb77:/work# gcc ex6-2.c -o ex6-2 -g3
root@0be431eebb77:/work# ./ex6-2
20

以下は ex6-2.c と同じ動作をするコードである.

/* code: ex6-3.c */
#include <stdio.h>

int main ()
{
  int a[10] = { 30, 20, 10, 25, 15 };
  int i, sum, avg;
  
  sum = 0;
  for (i = 0; i < 5; i++) {
    sum += a[i];
  }
  avg = sum / 5;
  printf ("%d\n", avg);
  
  return 0;
}

これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc ex6-3.c -o ex6-3 -g3
root@0be431eebb77:/work# ./ex6-3
20

このコードでは配列の初期化を配列の宣言文内で行っている. 配列の要素は波括弧 {} 内に列挙する. 要素数が宣言した配列の数より少ない場合は, 列挙した要素以外は初期化されない. 列挙した要素が多い場合は, 多くのコンパイラでは警告が出る. 例えば, 以下のように宣言した数よりも要素の数が多い場合.

/* code: ex6-3a.c */
#include <stdio.h>

int main ()
{
  int a[3] = { 30, 20, 10, 25, 15 };
  int i, sum, avg;
  
  sum = 0;
  for (i = 0; i < 5; i++) {
    sum += a[i];
  }
  avg = sum / 5;
  printf ("%d\n", avg);
  
  return 0;
}

以下のようにコンパイラ時エラーが発生することを確認した.

# gcc ex6-3a.c -o ex6-3a -g3
ex6-3a.c: In function 'main':
ex6-3a.c:6:28: warning: excess elements in array initializer
   int a[3] = { 30, 20, 10, 25, 15 };
                            ^~
ex6-3a.c:6:28: note: (near initialization for 'a')
ex6-3a.c:6:32: warning: excess elements in array initializer
   int a[3] = { 30, 20, 10, 25, 15 };
                                ^~
ex6-3a.c:6:32: note: (near initialization for 'a')

以下のコードは, 要素数 10 の配列の中に 0 〜 99 まで範囲の乱数を入れていくコードである.

/* code: ex6-4.c */
#include <stdio.h>
#include <stdlib.h>

#define ARRAY_SIZE 10

int main ()
{
  int a[ARRAY_SIZE];
  int i, sum, avg;

  for (i = 0; i < ARRAY_SIZE; i++) {
    a[i] = rand () % 100;
  }

  for (i = 0; i < ARRAY_SIZE; i++) {
    printf ("%03d ", a[i]);
  }
  printf ("\n");

  return 0;
}

これをコンパイルして実行する. 以下のように 0 パディングされた 100 以下のランダムな整数が 10 個出力される.

root@0be431eebb77:/work# gcc ex6-4.c -o ex6-4 -g3
root@0be431eebb77:/work# ./ex6-4
083 086 077 015 093 035 086 092 049 021

上記の例では #define を利用して, 配列の要素数 ARRAY_SIZE を定義している. 配列の要素数を変更したい場合には, ARRAY_SIZE を変更するだけで良い. rand 関数は, 0 以上, RAND_MAX で定義された値以下の疑似乱数整数を返す. % 記号はモジュロ演算で, 剰余の計算をする.

2. 多次元配列

多くのプログラミング言語では, 多次元配列を利用することが出来る. 以下は 2 次元配列を図示したものである.

C0 C1 C2 C3
R0 a[0][0] a[0][1] a[0][2] a[0][3]
R1 a[1][0] a[1][1] a[1][2] a[1][3]
R2 a[2][0] a[2][1] a[2][2] a[2][3]

以下のコードは, 上記の 2 次元配列表が示すような整数型の 2 次元の配列を作成し, 値を代入した後に二重の for ループによって配列内の値を出力する.

/* code: ex6-5.c */
#include <stdio.h>

int main ()
{
  int i, j;
  int a[3][4] = {
    {0, 10, 20, 30},
    {40, 50, 60, 70},
    {80, 90, 100, 110}
  };

  for (i = 0; i < 3; i++) {
    for (j = 0; j < 4; j++) {
      printf ("array[%d][%d] = %3d\n", i, j, a[i][j]);
    }
  }

  return 0;
}

これをコンパイラして実行してみる.

root@0be431eebb77:/work# gcc ex6-5.c -o ex6-5 -g3
root@0be431eebb77:/work# ./ex6-5
array[0][0] =   0
array[0][1] =  10
array[0][2] =  20
array[0][3] =  30
array[1][0] =  40
array[1][1] =  50
array[1][2] =  60
array[1][3] =  70
array[2][0] =  80
array[2][1] =  90
array[2][2] = 100
array[2][3] = 110

以下は, 3 次元配列のコード例である.

/* code: ex6-6.c */
#include <stdio.h>

int main ()
{
  int i, j, k;
  int a[2][3][4] = {
    {{0, 1, 2, 3},
     {4, 5, 6, 7},
     {8, 9, 10, 11}},
    {{0, 10, 20, 30},
     {40, 50, 60, 70},
     {80, 90, 100, 110}}
  };

  for (i = 0; i < 2; i++) {
    for (j = 0; j < 3; j++) {
      for (k = 0; k < 4; k++) {
        printf ("array[%d][%d][%d] = %3d\n", i, j, k, a[i][j][k]);
      }
    }
  }

  return 0;
}

これをコンパイラして実行してみる.

root@0be431eebb77:/work# gcc ex6-6.c -o ex6-6 -g3
root@0be431eebb77:/work# ./ex6-6
array[0][0][0] =   0
array[0][0][1] =   1
array[0][0][2] =   2
array[0][0][3] =   3
array[0][1][0] =   4
array[0][1][1] =   5
array[0][1][2] =   6
array[0][1][3] =   7
array[0][2][0] =   8
array[0][2][1] =   9
array[0][2][2] =  10
array[0][2][3] =  11
array[1][0][0] =   0
array[1][0][1] =  10
array[1][0][2] =  20
array[1][0][3] =  30
array[1][1][0] =  40
array[1][1][1] =  50
array[1][1][2] =  60
array[1][1][3] =  70
array[1][2][0] =  80
array[1][2][1] =  90
array[1][2][2] = 100
array[1][2][3] = 110

C 言語の場合, 要素数を宣言する [] を増やすことによって, 配列の次元を増やすことが出来る. 多次元配列は 1 次元配列に変換することが可能である. 例えば, 3 次元配列 a の各次元が X, Y, Z であり, 0 以上の値となる添字を i, j, k とすると, 1 次元配列 b は以下の式で変換することが出来る.

b[X × Y × i + Y × j + k] = a[i][j][k]

一般的に多次元の配列は要素数が大きくなりやすい為, 配列に使用出来るメモリの上限に注意する必要がある. データを格納する為の配列のメモリは, スタック領域と呼ばれる場所に確保される. スタック領域で利用出来るメモリサイズは OS やコンパイラの制限を受ける.

3. 文字列

  • 文字列 (string) は一連の文字 (character) から出来ている
  • C 言語では標準で文字列型を持っておらず, 文字列は配列を利用して作ることが出来る

以下のコードは OUJ という文字列を出力するコードである.

/* code: ex6-7.c */
#include <stdio.h>

int main ()
{
  char s[4];
  s[0] = 'O';
  s[1] = 'U';
  s[2] = 'J';
  s[3] = '\0';
  printf ("%s\n", s);
  
  return 0;
}

これをコンパイルして実行する.

root@0be431eebb77:/work# gcc ex6-7.c -o ex6-7 -g3
root@0be431eebb77:/work# ./ex6-7
OUJ

このコードは文字型の要素数が 4 の配列 s を宣言し, その配列の先頭から順番に O, U, J の文字を配列に代入している. 最後には \0 という文字列の終端を表す為の特別な文字 (ヌル文字) を配列に代入している. 長さ n の文字列を格納する為には, ヌル文字も必要になる為, n + 1 個の要素数を持つ配列を用意する必要がある.

C 言語では, 文字列のような配列に対しては代入演算子を利用することが出来ない為, ある配列から別の配列への要素を代入する場合, 以下のコードのように, ループ文等を用いて配列の要素を 1 つずつコピーしていく必要がある.

/* code: ex6-8.c */
#include <stdio.h>

void string_copy (char *target, char *source) // void string_copy (char target[], char source[]) と記述出来る
{
  int i;
  i = 0;
  while (source[i] != '\0') {
    target[i] = source[i];
    i++;
  }
  target[i] = '\0';
}

int main ()
{
  char s[20] = "University";
  char t[20];
  
  string_copy (t, s);
  printf ("%s\n", t);
  
  return 0;
}

これをコンパイルして実行する.

root@0be431eebb77:/work# gcc ex6-8.c -o ex6-8 -g3
root@0be431eebb77:/work# ./ex6-8
University

このコードの string_copy 関数は, for ループを用いて, 配列 source の各要素からヌル文字以外を配列 target の各要素にコピーしていく. 最後にヌル文字を追加して文字列をコピーする. この関数を呼び出す時には, コピー先に十分なメモリが確保されていなければならない.

以下の表のように, C 言語のライブラリには文字列を扱う関数が多数含まれている. これらの関数は string.h に含まれる為, 利用する場合には #include <string.h> を宣言する必要がある.

関数 英語での意味 関数の説明
strcpy copy a string 文字列をコピー
strcat concatenate two strings 2 つの文字列を連結
strcmp compare two strings 2 つの文字列を比較
strncmp compare part of two strings 2 つの文字列を文字数を指定して比較
strchr locate character in strings 文字列の先頭から文字を探索
strstr locate a substrings 文字列から文字列を探索
strlen calculate the length of string 文字列の長さを求める

以下のコードは, 文字列をコピーする関数 strcpy 関数を利用した例で ex6-8.c と同様の結果となる.

/* code: ex6-9.c */
#include <stdio.h>
#include <string.h>

int main ()
{
  char s[20] = "University";
  char t[20];
  
  strcpy (t, s);
  printf ("%s\n", t);
  
  return 0;
}

これをコンパイルして実行する.

root@0be431eebb77:/work# gcc ex6-9.c -o ex6-9 -g3
root@0be431eebb77:/work# ./ex6-9
University

関数を使うことで, ex6-8.c よりもだいぶんコードの記述量が減っている. 特に理由が無い限りは関数を使うべきなのかな.

以下のコードは strcmp 関数を利用したコードである.

/* code: ex6-10.c */
#include <stdio.h>
#include <string.h>

int main ()
{
  char s0[] = "aaaaa";
  char s1[] = "bbbbb";
  char s2[] = "aaaaaaa";
  
  int i;
  printf ("strcmp(str1, str2)\n");
  i = strcmp (s0, s0);
  printf ("[%s] [%s] (%d)\n", s0, s0, i);

  i = strcmp (s0, s1);
  printf ("[%s] [%s] (%d)\n", s0, s1, i);

  i = strcmp (s1, s0);
  printf ("[%s] [%s] (%d)\n", s1, s0, i);

  i = strcmp (s0, s2);
  printf ("[%s] [%s] (%d)\n", s0, s2, i);
  
  return 0;
}

これをコンパイルして実行する.

root@0be431eebb77:/work# gcc ex6-10.c -o ex6-10 -g3
root@0be431eebb77:/work# ./ex6-10
strcmp(str1, str2)
[aaaaa] [aaaaa] (0)
[aaaaa] [bbbbb] (-1)
[bbbbb] [aaaaa] (1)
[aaaaa] [aaaaaaa] (-97)

strcmp 関数は 2 つの文字列 s1 と s2 を比較する. strcmp 関数は以下の書式で定義されている. const は定数である為, 書き換え不可能な文字列であり, 関数は文字列を関数内では変更しないことを意味している.

int strcmp(const char *s1, const char *s2)

この関数は s1 が s2 に比べて小さい場合, 等しい場合, 大きい場合にそれぞれ, 以下のような整数値を返す.

  • 小さい場合... ゼロよりも小さい整数
  • 等しい場合... ゼロと等しい整数
  • 大きい場合... ゼロより大きい整数

演習問題

問 6.1

コード 6.2 では, 整数型の配列が宣言されている. これを浮動小数点型の配列に変更したプログラムを作成しなさい.

/* code: q6-1.c */
#include <stdio.h>

int main ()
{
  float a[10];
  int i;
  float sum, avg;
  
  a[0] = 30;
  a[1] = 20;
  a[2] = 10;
  a[3] = 25;
  a[4] = 15;
  sum = 0.0;
  for (i = 0; i < 5; i++) {
    sum += a[i];
  }
  avg = sum / 5.00;
  printf ("%f\n", avg);
  
  return 0;
}

このコードをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q6-1.c -o q6-1 -g3
root@0be431eebb77:/work# ./q6-1
20.000000
root@0be431eebb77:/work#

ポイントは変数の宣言に float を利用している点. また, 平均値を出力する printf 関数において, %d\n ではなく %f\n としている点.

問 6.2

コード 6.5 を参考にして, 九九表 (掛け算表) を表示するプログラムを作成しなさい. コードでは, 九九表の値を一度, 2 次元配列に代入してから, 配列内の値を出力しなさい.

/* code: q6-2.c */
#include <stdio.h>
#define TABLE 9

int main ()
{
  int i, j;
  int a[TABLE][TABLE]; 

  for (i = 0; i < TABLE; i++) {
    for (j = 0; j < TABLE; j++) {
      a[i][j] = (i + 1) * (j + 1);
    }
  }

  for (i = 0; i < TABLE; i++) {
    for (j = 0; j < TABLE; j++) {
      printf ("%02d ", a[i][j]);
    }
    printf ("\n");
  }

  return 0;
}

このコードをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q6-2.c -o q6-2 -g3
root@0be431eebb77:/work# ./q6-2
01 02 03 04 05 06 07 08 09
02 04 06 08 10 12 14 16 18
03 06 09 12 15 18 21 24 27
04 08 12 16 20 24 28 32 36
05 10 15 20 25 30 35 40 45
06 12 18 24 30 36 42 48 54
07 14 21 28 35 42 49 56 63
08 16 24 32 40 48 56 64 72
09 18 27 36 45 54 63 72 81

問 6.3

コード 6.6 を変更にして, 3 次元配列に 1 以上 100 以下の乱数を代入するプログラムを作成しなさい. コードでは, 乱数の値を一度, 3 次元配列に代入してから, 配列内の値を出力しなさい.

/* code: q6-3.c */
#include <stdio.h>
#include <stdlib.h>

int main ()
{
  int i, j, k;
  int a[2][3][4]; 

  for (i = 0; i < 2; i++) {
    for (j = 0; j < 3; j++) {
      for (k = 0; k < 4; k++) {
        a[i][j][k] = (rand () % 100) + 1;
      }
    }
  }

  for (i = 0; i < 2; i++) {
    for (j = 0; j < 3; j++) {
      for (k = 0; k < 4; k++) {
        printf ("%03d ", a[i][j][k]);
      }
      printf ("\n");
    }
    printf ("\n");
  }
  return 0;
}

これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q6-3.c -o q6-3 -g3
root@0be431eebb77:/work# ./q6-3
084 087 078 016
094 036 087 093
050 022 063 028

091 060 064 027
041 027 073 037
012 069 068 030

問 6.4

コード 6.10 を変更にして, 文字列の最初の 3 文字のみを比較するようにしなさい. 文字列の比較には, srtncmp 関数を用いること.

/* code: q6-4.c */
#include <stdio.h>
#include <string.h>

int main ()
{
  char s0[] = "aaaaa";
  char s1[] = "bbbbb";
  char s2[] = "aaaaaaa";
  
  int i;
  printf ("strncmp(str1, str2)\n");
  i = strncmp (s0, s0, 3);
  printf ("[%s] [%s] (%d)\n", s0, s0, i);

  i = strncmp (s0, s1, 3);
  printf ("[%s] [%s] (%d)\n", s0, s1, i);

  i = strncmp (s1, s0, 3);
  printf ("[%s] [%s] (%d)\n", s1, s0, i);

  i = strncmp (s0, s2, 3);
  printf ("[%s] [%s] (%d)\n", s0, s2, i);
  
  return 0;
}

これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q6-4.c -o q6-4 -g3
root@0be431eebb77:/work# ./q6-4
strncmp(str1, str2)
[aaaaa] [aaaaa] (0)
[aaaaa] [bbbbb] (-1)
[bbbbb] [aaaaa] (1)
[aaaaa] [aaaaaaa] (0)

strncmp を man してみる.

$ man strncmp
...
     #include <string.h>

     int
     strcmp(const char *s1, const char *s2);

     int
     strncmp(const char *s1, const char *s2, size_t n);
...

strncmp は第三引数に比較する文字数を指定する.

問 6.5

strlen 関数を用いて文字列 "abcdefg" の長さを表示するプログラムを作成しなさい.

/* code: q6-5.c */
#include <stdio.h>
#include <string.h>

int main ()
{
  char s[] = "abcdefg";
  
  int i;
  i = strlen (s);
  printf ("[%s] (%d)\n", s, i);
  
  return 0;
}

これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q6-5.c -o q6-5 -g3
root@0be431eebb77:/work# ./q6-5
[abcdefg] (7)

入力した文字列の長さを返すように改変してみる.

/* code: q6-5a.c */
#include <stdio.h>
#include <string.h>

int main ()
{
  char s[10];

  printf ("Enter Words: ");
  scanf ("%s", &s);

  int i;
  i = strlen (s);
  printf ("[%s] (%d)\n", s, i);

  return 0;
}

実行してみる.

root@0be431eebb77:/work# ./q6-5a
Enter Words: foobar
[foobar] (6)

問 6.6

配列はデータの集まりを処理する為に重要である. 配列と一緒に使うと便利なものとして構造体 (structure) がある. 構造体を用いると複数のデータ型を 1 つにまとめて扱うことが出来る. (1) 構造体の配列, (2) 構造体のポインタ配列を使ったプログラムを作成しなさい.

chapter 06 (Algorithms and Programming 2016) より引用.

/* code: q6-6.c   (v1.16.00) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#define MAX 10
 
struct student
{
  int id;
  char grade;
  char name[128];
};
typedef struct student STUDENT_TYPE;
 
/* ------------------------------------------- */
int main ()
{
  STUDENT_TYPE db1[MAX];
  STUDENT_TYPE *db2[MAX];
  int i;
 
  printf ("database1\n");
  for (i = 0; i < MAX; i++) {
    db1[i].id = 100 + i;
    db1[i].grade = 'a' + rand () % 5;
    strcpy (db1[i].name, "John Doe");
    printf ("%d %c %s\n", db1[i].id, db1[i].grade, db1[i].name);
  }
 
  printf ("\n");
  printf ("database2\n");
  for (i = 0; i < MAX; i++) {
    db2[i] = malloc (sizeof (STUDENT_TYPE));
    db2[i]->id = 200 + i;
    db2[i]->grade = 'a' + rand () % 5;
    strcpy (db2[i]->name, "John Doe");
    printf ("%d %c %s\t\t", db2[i]->id, db2[i]->grade, db2[i]->name);
    printf ("%d %c %s\n", (*db2[i]).id, (*db2[i]).grade, (*db2[i]).name);
  }
  for (i = 0; i < MAX; i++) {
    free (db2[i]);
  }
 
 
  return 0;
}

これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q6-6.c -o q6-6 -g3
root@0be431eebb77:/work# ./q6-6
database1
100 d John Doe
101 b John Doe
102 c John Doe
103 a John Doe
104 d John Doe
105 a John Doe
106 b John Doe
107 c John Doe
108 e John Doe
109 b John Doe

database2
200 c John Doe          200 c John Doe
201 c John Doe          201 c John Doe
202 a John Doe          202 a John Doe
203 e John Doe          203 e John Doe
204 d John Doe          204 d John Doe
205 b John Doe          205 b John Doe
206 a John Doe          206 a John Doe
207 b John Doe          207 b John Doe
208 c John Doe          208 c John Doe
209 b John Doe          209 b John Doe

ふむ. 現時点で, このコードを理解するのは難しい. ごめんなさい. 以下, 印刷教材より引用.

  • 構造体のメンバ参照に使われているドット演算子とアロー演算子に注意すること
  • db2[i]->id(*db2[i]).id は同義である
  • 構造体のポインタ配列では, malloc と free が利用されている

以上

  • 配列の理解が深まった (これまではふわっとしか理解していなかった)
  • 文字列の生成に配列を利用する必要があるというのは意外だった
  • 関数のドキュメントは man 関数名 でいける
  • ポインタとか構造体とかがサラッと出てくるので焦る...