基本情報技術者試験より 〜スタックを操作する再帰処理をPythonで再現〜

突然ですが、再帰処理って難しくないですか?

今回は基本情報技術者試験の勉強をしていて、問題の解説を読んでもいまいち腹落ちできず、本当にそう動くのかをPythonで記載して検証してみましたのでご報告します。

1. 問題

以下の問題についてです。みなさん解けますでしょうか。

三つのスタックA,B,Cのいずれの初期状態も[1,2,3]であるとき,再帰的に定義された関数f()を呼び出して終了した後のBの状態はどれか。ここで,スタックが,[a1 a2,…,an-1]の状態のときにanをpushした後のスタックの状態は[a1 a2,…,an-1,an]で表す。

f(){
 Aが空ならば{
  何もしない。
 }
 そうでない場合{
  Aからpopした値をCにpushする。
  f()を呼び出す。
  Cからpopした値をBにpushする。
 }
}

基本情報技術者 平成31年春 午前試験 問6

2.答えと解説

A・B・C3つのスタックがあり、問題に記載された関数fを実行すると、Bのスタックは [1, 2, 3, 1, 2, 3] となります。
(Aは[](空)、Cは[1, 2, 3])

スタックは「後入後出し(Last In First Out)」なので、AからCに「3 → 2 → 1」の順に渡されて、CからBには「1 → 2 → 3」の順で渡されるので、答えが[1, 2, 3, 1, 2, 3] となること自体はOKです。

文句はありません。

ところが解説を読んでみると・・・

<プログラム開始>
A[1,2,3] B[1,2,3] C[1,2,3]

〔f() 呼び出し1回目〕
Aからpopした値をCにpushする。Aから一番右の"3"が取り出され、Cの一番右に追加される(以下、取出位置と追加位置は同様)。
A[1,2] B[1,2,3] C[1,2,3,3]

〔f() 呼び出し2回目〕
Aからpopした"2"をCにpushする。
A[1] B[1,2,3] C[1,2,3,3,2]

〔f() 呼び出し3回目〕
Aからpopした"1"をCにpushする。
A[] B[1,2,3] C[1,2,3,3,2,1]

〔f() 呼び出し4回目〕
Aが空なので何もしない

〔f() 呼び出し3回目の続き〕
Cからpopした"1"をBにpushする。
A[] B[1,2,3,1] C[1,2,3,3,2]
//f() 3回目終了

〔f() 呼び出し2回目の続き〕
Cからpopした"2"をBにpushする。
A[] B[1,2,3,1,2] C[1,2,3,3]
//f() 2回目終了

〔f() 呼び出し1回目の続き〕
Cからpopした"3"をBにpushする。
A[] B[1,2,3,1,2,3] C[1,2,3]
//f() 1回目終了

<プログラム終了>

関数f()が終了した後のBの状態は[1,2,3,1,2,3]になっています。したがって「ア」が正解です。

基本情報技術者試験ドットコム より

問題の関数ですが、Aのスタックが空でないとき、Aの値をCに格納(Push)する。その後、関数内で再度同じ関数を呼び出し、Cの値をBに格納(Push)します。

再度呼び出された関数もまず、Aのスタックが空でなければ、Aの値をCにPushして、また関数を呼び出し、Cの値をBに格納します・・・
なんかこんがらがってきませんか?

並び順に異論はないものの、処理の流れが頭の中だとイメージできない。
解説を読んで、一瞬分かった気になるものの、やっぱり腑に落ちない・・・

そこでPythonでコードを実際に書いて動かしてみます。

3.Pythonで問題の関数と配列A・B・Cを用意して実行してみる

以下のように作成します。

#pop_push関数を定義する(a,b,cに配列を、nは何回目の処理かをカウントする変数)

def pop_push(a, b, c, n):
        #配列aが空なら、Aは空と出力する
    if len(a) == 0:
        print('Aは空')
        pass
    #配列Aがからでない場合の処理を記載
    else:
                #カウント変数nをカウントアップする
        n += 1
        
        print("---------------------------------")

                #処理結果を出力する(何回目の呼び出しか)
        print('{}回目の呼び出し'.format(n))

                #配列aの最後尾の値を、配列cに追加し、aから削除
        c.append(a[-1])
        a.pop(-1)

                #配列aとcの内容を出力する
        print('リストaの状態:{}'.format(a))
        print('リストcの状態:{}'.format(c))
        print("---------------------------------")

        #再度関数pop_pushを呼び出す
        pop_push(a, b, c, n)
        print("---------------------------------")
                
                #呼び出し回数と各配列の内容を出力する
        print('{}回目の呼び出し'.format(n))
        print('リストaの状態:{}'.format(a))
        print('リストbの状態:{}'.format(b))
        print('リストcの状態:{}'.format(c))
        print("---------------------------------")
        
                #配列bに配列cの最後尾の値を追加し、配列cからは削除する
                b.append(b[-1])
        c.pop(-1)

         #処理結果として、配列a, b, cを返す
    return a, b, c

#配列a, b, cを定義する
a = [1,2,3]
b = [1,2,3]
c = [1,2,3]

#result変数に処理結果を返すよう、関数pop_pushに配列a, b, cとカウント変数0を渡す
result = pop_push(a, b, c, 0)

#resultに返ってきた処理結果を出力する
print("---------------------------------")
print("結果:{}".format(result))
print("---------------------------------")

4.処理経過と結果を確認する

プログラムを実行すると・・・

---------------------------------
1回目の呼び出し
リストaの状態:[1, 2]
リストbの状態:[1, 2, 3, 3]
---------------------------------
---------------------------------
2回目の呼び出し
リストaの状態:[1]
リストbの状態:[1, 2, 3, 3, 2]
---------------------------------
---------------------------------
3回目の呼び出し
リストaの状態:[]
リストbの状態:[1, 2, 3, 3, 2, 1]
---------------------------------
Aは空
---------------------------------
3回目の呼び出し
リストaの状態:[]
リストbの状態:[1, 2, 3]
リストcの状態:[1, 2, 3, 3, 2, 1]
---------------------------------
---------------------------------
2回目の呼び出し
リストaの状態:[]
リストbの状態:[1, 2, 3, 1]
リストcの状態:[1, 2, 3, 3, 2]
---------------------------------
---------------------------------
1回目の呼び出し
リストaの状態:[]
リストbの状態:[1, 2, 3, 1, 2]
リストcの状態:[1, 2, 3, 3]
---------------------------------
---------------------------------
結果:([], [1, 2, 3, 1, 2, 3], [1, 2, 3])
---------------------------------

まさに解説の通り、Aが空になるまで関数を再度呼び出しながら配列aの値をcにPushし続け、Aが空になると今度は3回目・2回目・1回目の処理の続きとして配列cの値をbにPushし続けてますね。

ただ、僕だけかもしれないですが、やっぱり完全に理解しきれないです。

なので、もし同様の処理をするとしたら、自分なら以下のように書くと思います。

def pop_push(a, b, c):
    while len(a) > 0:
        c.append(a[-1])
        a.pop(-1)

    while len(c) > 3:
        b.append(c[-1])
        c.pop(-1)

    return a, b, c

a = [1, 2, 3]
b = [1, 2, 3]
c = [1, 2, 3]

result = pop_push(a, b, c)

print(result)

出力結果は、以下のように同一となります。

([], [1, 2, 3, 1, 2, 3], [1, 2, 3])

頭がいい人ならできるのかもしれないですが、再帰関数を利用するのは難しいのではないかと思います。(なんか予想外の動きをして思い通りになれなそう・・・)

以上

この記事が気に入ったらサポートをしてみませんか?