【アルゴリズム 2次元配列編】ざっくりわかるシェルスクリプト8

はじめに

さて。
前回の章では、アルゴリズムを勉強していくためのもととなるテンプレートとして、配列に値を入れる仕組みと、配列の中身を表示する仕組みを実装した01Array.shを作成しました。

とても淡白な配列実装
01Array.sh

今回の章では、前章で作成したテンプレートを2次元配列で実装を修正し、今後の高度なアルゴズムの実装に備えたいと思います。

Bashでは不可能とされてきた2次元配列を擬似的に振る舞う実装
01Eval_Array.sh

メインからの比較

2次元配列自体がよくわからない人もいると思います。
まず、配列の添字は0から数えます。(そういうものだと覚えるしかありません)

2次元配列のarray配列の1番目の人で、0に名前、1に住所を格納します。
array[0][0]=名前
array[0][1]=住所

2次元配列のarray配列の2番目の人で、0に名前、1に住所を格納します。
array[1][0]=名前
array[1][1]=住所

2次元配列のarray配列の3番目の人で、0に名前、1に住所を格納します。
array[2][0]=名前
array[2][1]=住所

:
:
<以下同文>

こうした一つの配列の要素に、複数の値を格納できることを2次元配列といいます。
当然、以下の様な3次元配列や

array[0][0]=名前
array[0][1]=住所
array[0][2]=電話番号

以下のような4次元配列もあります。

array[0][0]=名前
array[0][1]=住所
array[0][2]=電話番号
array[0][3]=メールアドレス

C言語やJavaでは当たり前のように使われる多次元配列が、Bashではサポートされていません。

array[0]

は、可能なのですが、

array[0][0]

ができないのです。
不便ですね。他の言語では、

array[0].getName() 

と、すると名前を取り出せたり、

array[0].getAddress()

とすると、住所が取り出せたり、さらには

array[0].setName() 

とすると、名前をセットしたりすることもできたりします。
そんな直感的なコーディングがBashではできなかったりします。

事実、アルゴリズムの勉強を進めていくと、ひとつの配列に、IDと値をそれぞれセットしたり、参照したり、置き換えたりしたくなるものです。

この章では、そうした「ふるまい」をBashで擬似的に実現してみようというチャレンジになります。

シェバン、そしてメインへ

前章で作成した配列版は以下のとおりです。

#!/usr/bin/bash

##
# <>execArray()
#
function execArray(){
  setArray $1;
  display;
}
##
#
time execArray 10;
exit;

今回の2次元配列版は以下のとおりです。

#!/usr/bin/bash


##
# <>execArray()
# メインルーチン
function execArray(){
  local N=$1;           #要素数
  setArray $N           #配列にセット
  display;              #表示
}
##
# 実行
#
time execArray 10;
exit;

ほぼ同じですね。
今回の2次元配列版では、前章の配列版よりもきちんと記述されています。(しています)

localは、関数内のスコープ内でのみ有効な変数という定義です。 function execArray()に渡された $1パラメータは、関数冒頭で、Nという変数に代入されています。これにより、ソースの中で「$1」はなんだっけ?ということにならなくなります。

ポイント
関数内でのみ使う変数はlocalをつける
関数パラメータ $1,$2などといった変数は、関数冒頭できちんと命名規則に則った変数に代入して見渡しの良いソースを書くことを心がける

配列をセットする

では、早速配列をセットしてみます。

前章で作成した配列版は以下のとおりです。

##
# <> set Array
#
function setArray(){
  nElems=0;
  for((i=0;i<$1;i++));do
      insert `echo "$RANDOM"`;
  done
}

今回の2次元配列版は以下のとおりです。

##
# <> set Array
# 配列を作成
function setArray(){
  local N=$1;           #すべての要素数
  local ID=100;         #100からの連番
  local value;          #配列に代入される要素の値
  for((i=0;i<N;i++)){
    value=$( echo $RANDOM );
    insert $((ID++)) $value;
  }
}

function setArray()もほぼ変化はありません。
関数パラメータで受け取った $1 を local変数 Nに代入しています。さらに、関数内で使われる2つの変数「ID」と「value」を同じくlocal変数で定義しています。

$RANDOMで取得したランダムな数字をinsert()関数に渡すわけですが、きちんと変数「value」に代入した上で、insert()関数に渡しています。

$((ID++))は新しく出現した変数ですね。
今回の章では、値だけでは一般的な配列で実装できてしまうので、あえて「ID」といった要素を追加しています。「ID」は100からの連番として`insert()を呼び出すたびにインクリメントされます。

let "ID=ID+1";

という書き方よりも、他のプログラム言語でも使われる書式

(( ID++ ));

のほうが、慣れ親しんでいる人も多いかと思います。

(( ID++ ));

は、インクリメントするだけの場合の書き方となります。
インクリメントした値を取り出して使う場合は、「$」をつけます。

$(( ID++ ));
ポイント
関数内でのみ使う変数はlocalをつける
関数パラメータ $1,$2などといった変数は、関数冒頭できちんと命名規則に則った変数に代入して見渡しの良いソースを書くことを心がける
echo $RANDOMといったよく使われる動的変数は、valueといった値にきちんと格納して使う
インクリメントは (( ID++ )); 値を取り出して使う場合は、 $(( ID++ ));

配列に値をセット

ここでは、配列に値をセットしてみたいと思います。
まずは、前章と、今回の章をみくらべてみましょう。

前章で作成した配列版は以下のとおりです。

##
# <> insert
#
function insert(){
  array[((nElems++))]=$1;
}

今回の2次元配列版は以下のとおりです。

##
# <> insert
# 配列の要素に値を代入
function insert(){
  local ID=$1;          #100からの連番
  local value=$2;       #配列に代入される要素の値
  setID     "$nElems"    "$ID";      #IDをセット
  setValue  "$nElems"    "$value";   #Valueをセット
  ((nElems++));
}

function insert()に渡されたパラメータの処理はきちんと、localで定義し、$1,$2をソース内で乱用せずに、関数冒頭でわかりやすい変数に代入しています。

((nElems++));

こちらは、値をインクリメントしているという事がわかります。
インクリメントするだけで値を取り出す必要がないので $(( nElems++));とはなりません。

新規で追加されているのは以下の2行ですね。

  setID     "$nElems"    "$ID";      #IDをセット
  setValue  "$nElems"    "$value";   #Valueをセット

どうやら、function setID()function setValue()といった関数が新たに追加されているようですね。
それぞれに、 $nElems と、$IDといった2つのパラメータを渡していることも見て取れます。
では次の項でfunction setID()function setValue() といった「セッター」メソッドを見てみましょう。

2つのセッターメソッド 「謎のevalコマンド」

セッターメソッドは、前章にはなかった新しい関数です。
変数に値を入れるための関数で、IDに値を入れたい場合はfunction setID()、valueに値を入れたい場合はfunction setValue()を呼び出します。

2つの関数冒頭のlocalと$1,$2を変数に置き換える話は、これまで何度化してきたので、もう説明は不要かと思います。

##
# <>setValue() 
# セッター
function setValue() {
  #今後挿入や置き換えに備えてnElemsとは別の変数を用意しておく
  local Elem="$1";      
  local value="$2";
	eval "aRray[$Elem].getValue()      { echo "$value"; }"
}
##
# <>setID()
# セッター
function setID(){
  #今後挿入や置き換えに備えてnElemsとは別の変数を用意しておく
  local Elem="$1";      
  local ID="$2";
	eval "aRray[$Elem].getID()         { echo "$ID"; }"
}

evalが新しく登場するコマンドです。
setID()の中の以下の一行に注目してみます。

	eval "aRray[$Elem].getID()         { echo "$ID"; }"

evalを使うことによっては文字列をコマンドとして扱うことができます。
上記の例では、

"aRray[$Elem].getID"

という文字列に

echo "$ID";

をセットしています。

"aRray[$Elem].getID"

の、変数 $Elemは展開されますので、例えば

“aRray[0].getID”

と、なりますね。“aRray[0].getID"に"aRray[0].getID"に対応するIDを代入しておいて、必要なときに「aRray[0].getID」と書けば、値を取り出せます。valueに関しても同様のロジックです。

ポイント
関数内でのみ使う変数はlocalをつける
関数パラメータ $1,$2などといった変数は、関数冒頭できちんと命名規則に則った変数に代入して見渡しの良いソースを書くことを心がける
echo $RANDOMといったよく使われる動的変数は、valueといった値にきちんと格納して使う
インクリメントは (( ID++ )); 値を取り出して使う場合は、 $(( ID++ ));
evalコマンドを使って2次元配列を擬似的に実装することは可能

残念なことが一つ。

実はこれ、配列に見せているだけで、配列として動いてはいないのです。あくまで、getID/getValueという呼び出しによって、適宜取り出しやすくなるというメリットです。

とはいえ、配列(っぽい)変数にsetID/setValueできたりgetID/getValueできたり、はてには、2次元、3次元で要素を格納できたりできることは悲願です。

display()出力関数の実装

ソースの冒頭に declare -i で数値を扱う変数として定義しています。bashでは変数定義は省略できますが、きちんと書いておくことにマイナス要素はありません。きちんと書きましょう。

#
# グローバル変数
declare -i nElems=0;
##
# <>display()  
# 配列を表示
function display(){
  for((i=0;i<nElems;i++)){
    echo -n "aRray[$i]  \
    ID: " $( aRray[$i].getID ) " \
    Value:" $( aRray[$i].getValue ) ; 
    echo "";
  }
}

さて、function desplay()ですが、forの書き方がC/Java風ですね。

中括弧{}で関数内容を囲むことができます。
小括弧()で条件式を囲むことができます。
小括弧の2重化で(())、条件式の変数前の$が不要となります。
当然、do/doneは不要です。

echo -nは、行末の改行がないことを示します。よって次の行が続けて出力されます。
echo ""は、空行の挿入です。

    ID: " $( aRray[$i].getID ) " \
    Value:" $( aRray[$i].getValue ) ; 

ここは、文字列のなかで値をはめ込みたいから $(….)で囲んでいます。値だけを出力したい場合は、以下を実行すれば値だけが出力されます。

  aRray[$i].getID;
  aRray[$i].getValue;
ポイント
関数内でのみ使う変数はlocalをつける
関数パラメータ $1,$2などといった変数は、関数冒頭できちんと命名規則に則った変数に代入して見渡しの良いソースを書くことを心がける
echo $RANDOMといったよく使われる動的変数は、valueといった値にきちんと格納して使う
インクリメントは (( ID++ )); 値を取り出して使う場合は、 $(( ID++ ));
evalコマンドを使って2次元配列を擬似的に実装することは可能
グローバルスコープでの変数定義は declare -i で数値、declare -a で配列

実行結果

実行結果は以下のとおりです。

bash-5.1$ bash 01Eval_Array.sh
aRray[0]      ID:  100      Value: 31091
aRray[1]      ID:  101      Value: 8361
aRray[2]      ID:  102      Value: 21900
aRray[3]      ID:  103      Value: 7788
aRray[4]      ID:  104      Value: 26435
aRray[5]      ID:  105      Value: 18735
aRray[6]      ID:  106      Value: 19322
aRray[7]      ID:  107      Value: 7051
aRray[8]      ID:  108      Value: 2967
aRray[9]      ID:  109      Value: 30591

real	0m0.037s
user	0m0.016s
sys	0m0.020s
bash-5.1$

ソース全文

この章のソース全文は以下のとおりです。

#######################################
# 01Array.shを、少しだけオブジェクティブに
# aRray[0].getValue() で値を取得できるように改変した
# 配列にIDと値を入れるだけのbashスクリプト
#######################################
#
# グローバル変数
declare -i nElems=0;
##
# <>display()  
# 配列を表示
function display(){
  for((i=0;i<nElems;i++)){
    echo -n "aRray[$i]  \
    ID: " $( aRray[$i].getID ) " \
    Value:" $( aRray[$i].getValue ) ; 
    echo "";
  }
}
##
# <>setValue() 
# セッター
function setValue() {
  #今後挿入や置き換えに備えてnElemsとは別の変数を用意しておく
  local Elem="$1";      
  local value="$2";
	eval "aRray[$Elem].getValue()      { echo "$value"; }"
}
##
# <>setID()
# セッター
function setID(){
  #今後挿入や置き換えに備えてnElemsとは別の変数を用意しておく
  local Elem="$1";      
  local ID="$2";
	eval "aRray[$Elem].getID()         { echo "$ID"; }"
}
##
# <> insert
# 配列の要素に値を代入
function insert(){
  local ID=$1;          #100からの連番
  local value=$2;       #配列に代入される要素の値
  setID     "$nElems"    "$ID";      #IDをセット
  setValue  "$nElems"    "$value";   #Valueをセット
  ((nElems++));
}
##
# <> set Array
# 配列を作成
function setArray(){
  local N=$1;           #すべての要素数
  local ID=100;         #100からの連番
  local value;          #配列に代入される要素の値
  for((i=0;i<N;i++)){
    value=$( echo $RANDOM );
    insert $((ID++)) $value;
  }
}
##
# <>execArray()
# メインルーチン
function execArray(){
  local N=$1;           #要素数
  setArray $N           #配列にセット
  display;              #表示
}
##
# 実行
#
time execArray 10;
exit;
ヒント
Bash/シェルスクリプトによる、擬似的な2次元配列の実現が叶いました。つぎからはバブルソートの実装に入りたいと思います。お楽しみに!

「ざっくり」シリーズのご紹介

【アルゴリズム 再帰】ざっくりわかるシェルスクリプト15
https://suzukiiichiro.github.io/posts/2022-10-07-01-algorithm-recursion-suzuki/
【アルゴリズム キュー】ざっくりわかるシェルスクリプト14
https://suzukiiichiro.github.io/posts/2022-10-06-01-algorithm-queue-suzuki/
【アルゴリズム スタック】ざっくりわかるシェルスクリプト13
https://suzukiiichiro.github.io/posts/2022-10-06-01-algorithm-stack-suzuki/
【アルゴリズム 挿入ソート】ざっくりわかるシェルスクリプト12
https://suzukiiichiro.github.io/posts/2022-10-05-01-algorithm-insertionsort-suzuki/
【アルゴリズム 選択ソート】ざっくりわかるシェルスクリプト11
https://suzukiiichiro.github.io/posts/2022-10-05-01-algorithm-selectionsort-suzuki/
【アルゴリズム バブルソート】ざっくりわかるシェルスクリプト10
https://suzukiiichiro.github.io/posts/2022-10-05-01-algorithm-bubblesort-suzuki/
【アルゴリズム ビッグオー】ざっくりわかるシェルスクリプト9
https://suzukiiichiro.github.io/posts/2022-10-04-01-algorithm-bigo-suzuki/
【アルゴリズム 2次元配列編】ざっくりわかるシェルスクリプト8
https://suzukiiichiro.github.io/posts/2022-10-03-01-algorithm-eval-array-suzuki/
【アルゴリズム 配列準備編】ざっくりわかるシェルスクリプト7
https://suzukiiichiro.github.io/posts/2022-10-03-01-algorithm-array-suzuki/
【アルゴリズム 配列編】ざっくりわかるシェルスクリプト6
https://suzukiiichiro.github.io/posts/2022-09-27-01-array-suzuki/
【grep/sed/awkも】ざっくりわかるシェルスクリプト5
https://suzukiiichiro.github.io/posts/2022-02-02-01-suzuki/
【grep特集】ざっくりわかるシェルスクリプト4
https://suzukiiichiro.github.io/posts/2022-01-24-01-suzuki/
【はじめから】ざっくりわかるシェルスクリプト3
https://suzukiiichiro.github.io/posts/2022-01-13-01-suzuki/
【はじめから】ざっくりわかるシェルスクリプト2
https://suzukiiichiro.github.io/posts/2022-01-12-01-suzuki/
【はじめから】ざっくりわかるシェルスクリプト1
https://suzukiiichiro.github.io/posts/2022-01-07-01-suzuki/

【TIPS】ざっくりわかるシェルスクリプト
https://suzukiiichiro.github.io/posts/2022-09-26-01-tips-suzuki/

書籍の紹介

【アルゴリズム ビッグオー】ざっくりわかるシェルスクリプト9

【アルゴリズム ビッグオー】ざっくりわかるシェルスクリプト9

【アルゴリズム 配列準備編】ざっくりわかるシェルスクリプト7

【アルゴリズム 配列準備編】ざっくりわかるシェルスクリプト7