疑似コード#

in English or the language of your choice.

import random

# 警告メッセージを非表示
import warnings
warnings.filterwarnings("ignore")

はじめに#

モジュールとアルゴリズムでは簡単なアルゴリズムをPythonコードとして表し実行したが,その裏にある考え方は他のコンピューター言語でも使うことができる。更に言うと,アルゴリズムを図や言葉で表すことも可能である。フローチャートと呼ばれるのがその一つであり,図を使いアルゴリズムの考えを表現している。そしてもう1つが疑似コードと言われるものであり,自然言語(日本語や英語)で書かれたコードのことである。この2つの手法の利点は,コンピューター言語で発生するエラーを気にせずに,アルゴリズムの考えを理解・整理し,より良い(速い)ものを作り出すことができる土壌となり得ることだろう。以下では疑似コードに基づいてコードの書き方を検討する。

上でも説明したが,疑似コードとは,自然言語(日本語や英語)で書かれたコードであるため,Pythonは実行できない箇条書きのような文章となっている。レポートを書く際に,まずアウトラインを考えて書き出す人もいると思うが,それをイメージすれば良いだろう。次の点が重要となる。

  • 明確さ

  • 簡潔さ

  • 細かい詳細は省く

    • アルゴリズムの核となる部分を捉える

  • 高い可読性

    • コードを理解できる人ならで誰が読んでも手順が分かるもの

一方で,疑似コードの決まった書き方はない。通常は人それぞれ自分のスタイルや好みに合わせて書いているのが実状である(出版のための共通のスタイルがある場合もある)。ここでは次のスタイルとする。

  • Pythonコードと同じインデントを使う。

  • Pythonコード(例えば,forwhile)を使えればそのまま使う。

    • 疑似コードを英語で書く場合,英語での記述と区別するために大文字を多用するが,日本語とは区別し易いので小文字を使う。

これら以外は,箇条書きスタイルとしよう。

FizzBuzz問題#

まず例として有名なFizzBuzz問題を考えよう。

1から100までの数字を表示する。ただし,

  • 3の倍数は数字の代わりにFizzと表示し,

  • 5の倍数は数字の代わりにBuzzと表示し,

  • 3と5の倍数は数字の代わりにFizzBuzzと表示する。

次のコードではfizzbuzz関数として書いている。

def fizzbuzz():
    
    for i in range(1,100+1):
        
        if i % 3 == 0 and i % 5 == 0:
            print('FizzBuzz')
            
        elif i % 3 == 0:
            print('Fizz')
            
        elif i % 5 == 0:
            print('Buzz')
            
        else:
            print(i)

ここで%は除算の余りを返す演算子である。実行してみよう。

fizzbuzz()
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
Fizz
22
23
Fizz
Buzz
26
Fizz
28
29
FizzBuzz
31
32
Fizz
34
Buzz
Fizz
37
38
Fizz
Buzz
41
Fizz
43
44
FizzBuzz
46
47
Fizz
49
Buzz
Fizz
52
53
Fizz
Buzz
56
Fizz
58
59
FizzBuzz
61
62
Fizz
64
Buzz
Fizz
67
68
Fizz
Buzz
71
Fizz
73
74
FizzBuzz
76
77
Fizz
79
Buzz
Fizz
82
83
Fizz
Buzz
86
Fizz
88
89
FizzBuzz
91
92
Fizz
94
Buzz
Fizz
97
98
Fizz
Buzz

これを疑似コードで表してみよう。

def フィズバズ()

    forループ i <-- 1から100まで
    
        if i を3で割ると余り0 and i を5で割ると余り0
            "FizzBuzz"を表示
            
        elif i を3で割ると余り0
            "Fizz"を表示
            
        elif i を5で割ると余り0
            "Buzz"を表示           
            
        else
            i を表示

Pythonコードと疑似コードを比べて,疑似コードがどのようなものかイメージできたのではないだろうか。上にも書いたが,疑似コードの目的はアルゴリズムを表現することであり,FizzBuzzの例はその目的を達成している。もちろん,これこそが「正しい」という疑似コードはないので,自分なりに書き替えてみてはどうだろうか。ただ,ここでの目的は質の高い疑似コードを書くことではない

疑似コードには次の利点がある。

プログラミング言語の構文について特に注意を払わずに(エラーを心配せずに),コードの内容を計画・検討することを可能にする。

この特性を活かして,コードの書き方を検討しようというのがここでの目的である。言い換えると,漠然とした曖昧な疑似コードを修正する過程を通して,アルゴリズムをPythonコードに落とし込むことを考える。5つの例を使うが,疑似コードの修正プロセスがコードを書く上でのヒントになるのではないかと期待している。

リストの合計(forループ)#

ここでの目的:

リスト(タプル)の要素の合計を返す関数を作成する。ただしforループを使うこと。


<Ver. 1>

def 合計(リスト)

    最初の要素から最後の要素を全て足し合わせる #1
    
    その合計を返す
  • 全体像はイメージできるが,#1で要素をどう足し合わせるか不明。要素ごとに何をするかを書き表そう。


<Ver. 2>

def 合計(リスト)

    # リストの要素数:N+1

    0番目の要素に1番目の要素を加える
    それに2番目の要素を加える。      #1
    それに3番目の要素を加える。      #1
    ・・・・・
    それにN番目の要素を加える。      #1
    
    return その合計
  • 具体性が芽生えてきたが,まだコードに落とせない。特に,#1にある「それに」をどう扱うかを考えよう。


<Ver. 3>

def 合計(リスト)

    # リストの要素数:N+1

    0番目の要素に1番目の要素を加えて,合計をxとする。 #1
    xに2番目の要素を加えて,それをxとする。        #1
    xに3番目の要素を加えて,それをxとする。        #1
    ・・・・・
    xにN番目の要素を加えて,それをxとする。        #1
    
    return x
  • より具体的になってきたが,#1xをどう扱うかを考える必要がある。


<Ver. 4>

def 合計(リスト)

    # リストの要素数:N+1

    x = 0
    
    xに0番目の要素の値を足す #1
    xに1番目の要素の値を足す #1
    xに2番目の要素の値を足す #1
    ・・・・・
    xにN番目の要素の値を足す #1
    
    return x
  • 骨格ができてきた。#1で似た作業がリピートされているのでループを使おう。


<Ver. 5>

def 合計(リスト)
    x = 0
    
    forループ i <-- 0からNまで  #1
        i番目の要素の値をxに足す  #1
        
    return x
  • #1iはリストのインデックス番号になっているが,要素自体で良いのでは?


<Ver. 6>

def 合計(リスト)
    x = 0
    
    for v <-- リスト
        要素vをxに足す
        
    return x
  • ここまで来るとゴールは目の前‼️


def sum_for_loop(lst):
    x = 0
    
    for i in lst:
        x += i
    
    return x

実行してみよう。

sum_for_loop([1,2,3]), sum_for_loop(range(0,100))
(6, 4950)

リストの合計(whileループ)#

ここでの目的:

リスト(タプル)の要素の合計を返す関数を作成する。ただしwhileループを使うこと。

リストの合計(forループ)の<Ver. 4>まで同じ。


<Ver. 4>

def 合計(リスト)

    # リストの要素数:N+1

    x = 0
    
    xに0番目の要素の値を足す #1
    xに1番目の要素の値を足す #1
    xに2番目の要素の値を足す #1
    ・・・・・
    xにN番目の要素の値を足す #1
    
    return x
  • 骨格ができてきた。#1で似た作業がリピートされているのでループを使おう。


<Ver. 5>

def 合計(リスト)

    x = 0                    #1
    
    while 条件                #2
        i番目の要素の値をxに足す   #3

    return x
  • #2の条件は何にするのか。

  • 無限ループに入らないように#1#3に付け加えることはないか。


<Ver. 6>

def 合計(リスト)

    x = 0
    count = 0                  #1
    
    while count < リストの要素数  #2
        i番目の要素の値をxに足す    #3
        countを1増やす           #4

    return x
  • #3i#1#2#4countはどう関係しているのか?同じ?


<Ver. 7>

def 合計(リスト)

    x = 0
    count = 0
    
    while count < リストの要素数
        count番目の要素の値をxに足す
        countを1増やす

    return x
  • ここまで来るとゴールは目の前‼️


def sum_while_loop(lst):
    
    x = 0
    count = 0
    
    while count < len(lst):
        x += lst[count]
        count += 1
    
    return x

関数を実行しよう。

sum_while_loop([1,2,3]), sum_while_loop(range(0,100))
(6, 4950)

コイントス(forループ)#

ここでの目的:

n回コイントスし,表と裏の回数を表示する関数を作成する。ただしrandom.randint()forループを使うこと。


<Ver. 1>

def コイントス(n)
   
    0回目のコイントス → 表・裏  #1
    1回目のコイントス → 表・裏  #1
    2回目のコイントス → 表・裏  #1
    ・・・
    n-1回目のコイントス → 表・裏   #1
   
    表の合計を計算  #2
    裏の合計を計算  #2
    
    表の合計と裏の合計を返す
  • #1で表と裏をどう表す?

  • #2の合計の計算はどうする?


<Ver. 2>

def コイントス(n)
    
    # 表:1
    # 裏:0
   
    0回目のコイントス → random.randint(0,1)   #1
    1回目のコイントス → random.randint(0,1)   #1
    2回目のコイントス → random.randint(0,1)   #1
    ・・・
    n-1回目のコイントス → random.randint(0,1) #1
   
    表の合計を計算  #2
    裏の合計を計算  #2
    
    表の合計と裏の合計を返す
  • #1は似たコードになるのでループを使おう。

  • #2の合計の計算はどうする?


<Ver. 3>

def コイントス(n)
    
    # 表:1
    # 裏:0
   
    for ループ
        random.randint(0,1)  #1
   
    表の合計を計算  #2
    裏の合計を計算  #2
    
    表の合計と裏の合計を返す
  • #1が実行されるたびに0又は1が出るが,記録されていない。記録する変数を導入する必要がある。

  • その記録を使って#2の合計の計算できる。


<Ver. 4>

def コイントス(n)
    
    # 表:1
    # 裏:0
   
    heads = 0
    tails = 0
    
    for ループ                 #1
        random.randint(0,1)
        結果が1であれば,headsを1増やす
        結果が0であれば,tailsを1増やす
       
    return heads, tails
  • #1で使うイタラブルは何か?

  • #1で使うイタラブル用の変数は何か?


<Ver. 5>

def コイントス(n)
    
    # 表:1
    # 裏:0
   
    heads = 0
    tails = 0
    
    for _ <-- 0からn-1まで            #1
        random.randint(0,1)
        結果が1であれば,headsを1増やす  #2
        結果が0であれば,tailsを1増やす  #2
       
    return heads, tails
  • #1_を使っている理由:

    • ijなどでも良いが,forループの「本体」で使われていない変数には_が使われることがよくある。

  • #2if文に書き換える必要がある。


<Ver. 6>

def コイントス(n)
    
    # 表:1
    # 裏:0
   
    heads = 0
    tails = 0
    
    for _ <-- 0からn-1まで
    
        random.randint(0,1)の結果をresultに割り当てる
        
        if resultが1
            headsを1増やす
        
        else
            tailsを1増やす
       
    return heads, tails
  • ここまで来ればゴールは目の前‼️


def coin_toss_for_loop(n):
    
    heads = 0
    tails = 0
    
    for _ in range(n):
        
        result = random.randint(0,1)
        
        if result == 1:
            heads += 1
            
        else:
            tails += 1
    
    return heads, tails

実行しよう

coin_toss_for_loop(100)
(54, 46)

コイントス(whileループ)#

ここでの目的:

n回コイントスし,表と裏の回数を表示する関数を作成する。ただしrandom.randint()whileループを使うこと。

コイントス(forループ)の<Ver. 2>まで同じ。


<Ver. 2>

def コイントス(n)
    
    # 表:1
    # 裏:0
   
    0回目のコイントス → random.randint(0,1)   #1
    1回目のコイントス → random.randint(0,1)   #1
    2回目のコイントス → random.randint(0,1)   #1
    ・・・
    n-1回目のコイントス → random.randint(0,1) #1
   
    表の合計を計算  #2
    裏の合計を計算  #2
    
    表の合計と裏の合計を返す
  • #1は似たコードになるのでループを使おう。

  • #2の合計の計算はどうする?


<Ver. 3>

def コイントス(n)
    
    # 表:1
    # 裏:0
   
    while ループ
        random.randint(0,1)  #1
   
    表の合計を計算  #2
    裏の合計を計算  #2
    
    表の合計と裏の合計を返す
  • #1で結果を記録する必要がある。

  • その記録を使って#2の合計の計算できる。


<Ver. 4>

def コイントス(n)
    
    # 表:1
    # 裏:0
    
    heads = 0
    tails = 0        #1
   
    while 条件       #2
    
        random.randint(0,1)
        結果が1であれば,headsを1増やす
        結果が0であれば,tailsを1増やす  #3
       
    return heads, tails
  • #2の条件は何にするのか。

  • 無限ループに入らないように#1#3に何を付け加える必要があるか。


<Ver. 5>

def コイントス(n)
    
    heads = 0
    tails = 0
    count = 0

    while count < n
    
        random.randint(0,1)
        結果が1であれば,headsを1増やす  #1
        結果が0であれば,tailsを1増やす  #1
        countを1増やす
       
    return heads, tails
  • #1if文として書く。


<Ver. 6>

def コイントス(n)
    
    heads = 0
    tails = 0
    count = 0

    while count < n
    
        random.randint(0,1)の結果をresultに割り当てる
        
        if resultが1
            headsを1増やす
        
        else
            tailsを1増やす
            
        countを1増やす
       
    return heads, tails
  • ここまで来ればゴールは目の前‼️


def coin_toss_while_loop(n):
    
    heads = 0
    tails = 0
    count = 0
    
    while count < n:
        
        result = random.randint(0,1)
        
        if result == 1:
            heads += 1
            
        else:
            tails += 1
        
        count += 1
    
    return heads, tails

コードを実行しよう。

coin_toss_while_loop(100)
(53, 47)

リストの最大値#

ここでの目的:

リスト(又はタプル)を引数とし,含まれる要素の中から最大値を返す関数を作成する。


<Ver. 1>

def 最大値を求める(リスト):

    0番目からN番目の要素を比較して最大値を探す  #1
    
    最大値を返す
  • ぼやっとした全体像となっているのは否めない。

  • #1の「比較」をどうするかが重要となる。。:


<Ver. 2>

def 最大値を求める(リスト=l):
    
    # N = リストの要素数
    
    <0> l[0]をl[1]〜l[N]と比較し,
        l[0]が全てに対して大きければ,l[0]が最大値。
        そうでなければ<1>に進む。
    <1> l[1]をl[2]〜l[N]と比較し,
        l[2]が全てに対して大きければ,l[2]が最大値。
        そうでなければ<2>に進む。
    <2> l[2]をl[3]〜l[N]と比較し,
        l[3]が全てに対して大きければ,l[3]が最大値。
        そうでなければ<3>に進む。
    ・・・・・
    <N-1> l[N-1]をl[N]と比較し,
          l[N-1]大きければ,l[N-1]が最大値。
          そうでなければl[N]が最大値。
  
    最大値を返す

良さそうにも見えるが,問題がある。

  • <0>ではN回の比較をおこなう。

  • <1>ではN-1回の比較をおこなう。

  • <2>ではN-2回の比較をおこなう。

  • ・・・・

  • <N>では1回の比較をおこなう。

  • 比較する回数の合計は1+2+3+・・・+N=N(N+1)/2。比較回数が非常に多い。もっと良い方法があるのでは?


<Ver. 3>

def 最大値を求める(リスト=l):

    # N = リストの要素数
    
    l[0]とl[1]を比較し,大きい方を残す    #1
    l[1]とl[2]を比較し,大きい方を残す    #1
    l[2]とl[3]を比較し,大きい方を残す    #1
    ・・・
    l[N-1]とl[N]を比較し,大きい方を残す  #1
    
    最大値を返す
  • 比較する回数はN回で<Ver. 2>より少ない。

  • #1の「大きい方を残す」はどうする?


<Ver. 4>

def 最大値を求める(リスト=l):

    # N = リストの要素数

    max_ = -1_000_000        #1
    
    l[0]とl[1]を比較し,大きい方をmax_に割り当てる。   #2
    l[1]とl[2]を比較し,大きい方をmax_に割り当てる。   #2
    l[2]とl[3]を比較し,大きい方をmax_に割り当てる。   #2
    ・・・
    l[N-1]とl[N]を比較し,大きい方をxに割り当てる。 #2
    
    return max_
  • #2だけを見ると最大値は求められそう。

  • #1では最大値が-1_000_000未満の場合をカバーできない。


<Ver. 5>

def 最大値を求める(リスト=l):

    # N = リストの要素数
    
    max_ = l[0]
    
    max_とl[1]を比較し,大きい方をmax_に割り当てる。  #1
    max_とl[2]を比較し,大きい方をmax_に割り当てる。  #1
    max_とl[3]を比較し,大きい方をmax_に割り当てる。  #1
    ・・・
    max_とl[N]を比較し,大きい方をmax_に割り当てる。  #1
    
    return max_
  • #1は似たようなコードになるのでループを使おう。


<Ver. 5>

def 最大値を求める(リスト=l):

    # N = リストの要素数
    
    max_ = l[0]
    
    for ループ
        max_とl[i]を比較し,大きい方をmax_に割り当てる。 #1
    
    return max_
  • #1は条件分岐させる必要がある。


<Ver. 6>

def 最大値を求める(リスト=l):

    # N = リストの要素数
    
    max_ = l[0]
    
    for ループ     #1
        max_ < l[i]の場合は,l[i]をxに割り当てる。
        max_ >= l[i]の場合は,pass
    
    return max_
  • #1のイタラブルは何にする?

  • #1のイタラブル用の変数は何にする?


<Ver. 7>

def 最大値を求める(リスト=l):

    # N = リストの要素数
    
    max_ = l[0]
    
    for i <-- 0から(lの要素数-1)まで                #1
        max_ < l[i]の場合は,l[i]をmax_に割り当てる。 #1
        max_ >= l[i]の場合は,pass                #1
    
    return max_
  • #1iはリストのインデックス番号だが,直接要素自体を使う方がPython的である。


<Ver. 8>

def 最大値を求める(リスト=l):

    # N = リストの要素数
    
    max_ = l[0]
    
    for v <-- リスト
        max_ < vの場合は,vをxに割り当てる。 #1
        max_ >= vの場合は,pass          #1
    
    return max_
  • #1if文に書き換える。


<Ver. 9>

def 最大値を求める(リスト=l):

    max_ = l[0]
    
    for v <-- リスト
    
        if max_ < v
            max_にvを割り当てる。
        else
            pass
    
    return max_
  • 後はPythonコードを書くだけ‼️


def search_max(l):
    
    max_ = l[0]
    
    for v in l:
        if v > max_:
            max_ = v
        else:         # 省略可
            pass      # 省略可
    
    return max_

実行してみよう。

lst = [random.gauss(0,1) for _ in range(1000)]

search_max(lst)
3.3246121055655

Note

<Ver. 2>をコードで書くと次の様になる。上のコードと比べると実行時間が1000倍近く長くなる。

def search_max(l):
    
    max_ = l[0]
    
    # 最後の要素はカバーしていない
    for idx, val in enumerate(l[:-1]):
        
        temp = max_
        

        for k in l[idx+1:]:
            if val <= k:
                continue
            else:
                temp = val
        
        if temp > max_:
            max_ = temp
    
    # 最後の要素について
    if max_ < l[-1]:
        max_ = l[-1]

    return max_

最後に#

コードを書くことに慣れてくると,簡単なコードに疑似コードの修正プロセスは必要なくなるだろう。しかし,その段階になればより複雑なコードを書くことになるはずだ。その時には疑似コードが役に立つのではないかと思われる。少し頭をひねるような問題であれば,疑似コードを試してみるのも一案である。