Webページでは数式を表すためにLaTeX表記が使えるMathJaxを利用しています。 WebブラウザにはSafari/Chrome/Firefoxを使って下さい(IEでは表示できないようです。)

データクラス stack

スタック(stack)はデータの並びが1次元的で、しかもデータアクセスにLIFO(Last-In First-Out)の制約がついたデータ構造をしている。

Pythonでは、Python入門(6)リストで紹介したように、リストデータ list に対して list.pop() や list.append(item) のようなスタック的メソッドが用意されているが、list.insert(item) のようなスタック構造には許されていないメソッドも併せて定義されているので(これらは大変便利なメソッドではあるが)、ここでは「純粋に」スタックデータクラスを定義して、これを利用したアルゴリズムを考えよう。

stackクラスのインスタンス mystack には次のメソッドが定義される。

stackクラスのメソッド
method名使用例意味
コンストラクタstack()stackインスタンスを生成し、空スタックを返す。 そのとき( )内にはパラメータは必要ない。
pushmystack.push(item)スタックトップに要素 item をプッシュ。プッシュする item の指定が必要で、メソッドは何も返さない。
popmystack.pop()スタックトップにあるデータをスタックから取り除き、メソッドはデータを返す。
peekmystack.peek()スタックトップのデータを返す。データを見るだけでスタックから取り除かない。
is_emptymystack.is_empty()スタックが空なら True を、空でなければ Falseを返す。
sizemystack.size()スタックに積み上がったデータ数を整数値で返す。

Pythonでstackクラスを定義する

stackクラスの定義に従って、リストデータを使って以下のように書いてみよう。 スタックトップの位置は、リスト末尾 len(self.items)-1 であること、 空スタックのサイズは 0 であることに注意する。

class stack:
    def __init__(self):
       self.items = []
       
    def is_empty(self):
       return self.items == []
       
    def push(self, item):
       self.items.append(item)
       
    def pop(self):
       return self.items.pop()
       
    def peek(self):
       return self.items[len(self.items)-1]
       
    def size(self):
       return len(self.items)

このスタックトップの位置をリスト末尾としてstackクラスを定義する以外に、次のようにスタックトップをリストの先頭と考え、stackクラスのメソッドを定義することも可能である。

  def push(self, item):
    self.items.insert(0, item)

  def pop(self):
    return self.items.pop(0)

  def peek(self):
    return self.items[0]
しかし、Pythonにおけるリストの実装では、 メソッド append(item) を使ってリスト末尾に item を追加する計算量が $\mathcal{O}(1)$ であるのに対して、insert(item) メソッドを使ってリスト先頭へのデータの挿入を行った場合には、既にあった $n$ 個のリスト要素を1つずつ後ろに移動させるための計算量 $\mathcal{O}(n)$ が発生し、スタック操作に余計な計算量が必要になる。 したがって、Pythonでリストを使ってstackクラスを定義するとにきは、スタックトップをリスト末尾とするのが都合が良い。

括弧の対応の整合性を調べる

スタックを使った典型的な応用として、括弧の対応を調べる問題がある。 多くのテキストエディタにも組み込まれており、括弧 ( { [ がそれぞれ ]}) に「合理的に」対応しているかをチェックするものだ(たとえば ( { [ と ) } ] とは合理的対応にはない )。

括弧の対応がとれているかどうかは、記号列を左から右へと読んでいったときに、直前の開き括弧 ( { [ どれかがが今注目している同じ種類の閉じ括弧 ] } ) に対応しているかを調べればよい。

まず、簡単のために、括弧文字からだけからなる文字列、それも1種類の括弧文字列だけならなる場合の処理を考えてから、他の記号も混じった文字列での1種類括弧の整合性を検出してみる。 次に、複数の括弧文字列だけなる括弧対応の整合性を調べてから、他の記号が混じった場合に拡張する。 最後に、引数で指定したファイルの括弧対応の整合性の検出スクリプトを書いてみよう。

1種類の括弧対応

簡単のために括弧が ( と ) の1種類の場合、文字 ( と ) からだけからなる文字列 given_string において括弧が対応しているかを判定してboole値を返す関数 parenthese_balance(given_string) は次のようになる。

5,6行で、まず括弧の対応がとれているとして balanced を True に、文字列内の指定した位置にある文字にアクセスするための添字 index を先頭の 0 にセットする。 7,8行で、与えられた文字列範囲内で balanced が True であるときに、while文で文字列 given_string の index 位置にある文字 given_string[index] を調べていく。

開き括弧 ( が見つかったら、スタックにプッシュし、そうでない、つまり対応する文字 ) が見つかったら、空でないスタック(にある直前の文字 ( )をポップする。 そうして、次の文字列位置を調べていく。 こうして全ての文字を調べ終わったときに、balanced が True で、スタックが空であるときにだけ、文字列の括弧対応がとれていたとするのである。

# declaration of stack class is assumed

def parenthese_balance(given_string):
    st = stack()
    balanced = True
    index = 0
    while index < len(given_string) and balanced:
        symbol = given_string[index]
        if symbol == "(":
            st.push(symbol)
        else:
            if not st.is_empty():
                st.pop()
            else:
                balanced = False
        index = index + 1
    if balanced and st.is_empty():
        return True
    else:
        return False
演習
print parenthese_balance('()(()())')
などを実行して正しい判定をするスクリプト parenthese_balance0.py を書いて実行しなさい。
演習: 関数 parenthese_balance(given_string) を修正して、注目する対応する括弧文字は同じく ( と ) として、与えた文字列が他の記号列を含んだときでも正しい判定をするようにしたスクリプト parenthese_balance1.py を書いて、適当な文字列を与えて実行してみなさい。

3種類の括弧対応

given_string の括弧文字が ( と ) だけでなく、{ と } および [ と ] だけからなる場合に、それらが「対応している」かを判定してboole値を返すように関数 parenthese_balance(given_string) を修正してみよう。

文字列の先頭から読み込んだ記号 symbol が開き括弧 (, {, [ のどれかである判定するために、Python的に9行目のように書くことができる。 その開き括弧記号 symbol をスタックにプッシュする。

そうでないとき((,{,[ のどれかではない、つまり閉じ括弧 ), }, ] のどれかの記号 symbol を読みこんだとき)、12行目でスタックが空でないことを確認した上で、14行目で関数 is_correspond を使ってスタックトップからポップした記号 top と読み込んだ記号 symbol とが対応していばければその時点で balanced 値 を False とする。 17行目にあるように、記号 symbol を読みこんだときうにスタックが空であるときもbalanced 値 を False とするのである。

# declaration of stack class is assumed

def parenthese_balance(given_string):
    st = stack()
    balanced = True
    index = 0
    while index < len(given_string) and balanced:
        symbol = given_string[index]
        if symbol in "({[":
            st.push(symbol)
        else:
            if not st.is_empty():
                top = st.pop()
                if not is_correspond(top, symbol):
                    balanced = False
            else:
                balanced = False
        index = index + 1
    if balanced and st.is_empty():
        return True
    else:
        return False

def is_correspond(open, close):
    openp = "([{"
    closedp = ")]}"
    return openp.find(open) == closedp.find(close)

括弧記号が対応しているかを判定する関数 is_correspond(open, close) は、与えておいた開き括弧文字列 openp および閉じ括弧文字列 closedp から記号 open と close のある文字位置を比較するというようにして判定させている。

演習: 上の修正した関数 parenthese_balance と is_correspond を使って、括弧文字 (,),{,},[,] だけからなる文字列を与えて、その括弧対応がとれているかを判定するスクリプト parenthese_balance2.py を書いて実行してみなさい。
演習: さらに、括弧文字 (,),{,},[,] だけでなく、これら以外の記号を含む文字列を与えたときにも、その括弧対応を正しく判定するように関数 parenthese_balance を拡張したスクリプト parenthese_balance3.py を書いて実行してみなさい。

(ヒント) 上の関数 parenthese_balance の定義では、if-else構文で、9行目の条件 「symbol in "({["」 を満たさないときは elseブロックの 12行目から17行目までを実行している。

新しく定義し直す関数 parenthese_balance を次のように書き換える。 if-else構文はif-elif構文に改め、7行目の条件 「symbol in "({["」 を満たさないときに、elifブロック(9行目から15行目)を使って、行目の条件「symbol in ")}]"」 を満たす場合には10行目から15行目のブロックを実行するようにしている。 これによって、括弧文字 (,),{,},[,] だけでなく、これら以外の記号を含む文字列が入り交じった文字列の括弧対応 を調べることができることに注意する。


def parenthese_balance(given_string):
    st = stack()
    balanced = True
    index = 0
    while index < len(given_string) and balanced:
        symbol = given_string[index]
        if symbol in "({[":
            st.push(symbol)
        elif symbol in ")}]":
            if not st.is_empty():
                top = st.pop()
                if not is_correspond(top, symbol):
                    balanced = False
            else:
                balanced = False
        index = index + 1
    if balanced and st.is_empty():
        return True
    else:
        return False

指定したファイルの括弧対応の整合性

次の演習では、上の最後の演習で再定義した関数 parenthese_balance(括弧文字 (,),{,},[,] だけでなく、これら以外の記号を含む文字列を与えたときにも、その括弧対応を正しく判定できる)を使う。

演習: Pythonスクリプトの1行目にshebang( スクリプトはShebangとマジックコメントで始める) を書いて保存し、そのファイルに実行属性を与えるようにする。 コマンドラインでファイル名を指定する方法を使って、 次のように、スクリプトを実行させる際に(パイルパスを含めて)コマンド引数で指定したファイルの括弧対応の結果を返すスクリプト parenthese_balance.py を書いて実行してみなさい。
$ ./parenthese_balance.py my_program.cc
またはWindowsでは次のようにスクリプトを実行
Z:\python\python parenthese_balance.py my_program.cc
True

ただし、上で改良した関数 parenthese_balance そのままでは、プログラムソースコードのコメントやprint文などの文字列に潜む括弧の不整合を正しく検出することはできない(たとえば、コメント記号の開始から行末まで読み飛ばすような処理を加えることなどが考えられる)。

(ヒント)pythonスクリプトファイルには、stackクラスの定義を記述し、上記の改良した関数 parenthese_balance および is_correspond を定義した後で、 実行部分を次のように書けばよい。

...
...
...
fh = open(sys.argv[1], 'r')# 引数で指定したファイルをreadモードでopen
file_string = fh.read()# ファイル内容を一括して読み込んで1つの文字列 file_string とする
print parenthese_balance(file_string)# ファイル内容の括弧対応を検査
fh.close()# 最後にファイルは必ず閉じる

このスクリプト parenthese_balance.py を指定した(日本語が混じらない)テキストファイル(たとえば your_python_script.py) の括弧対応を検査するには、ターミナルウィンドウ(DOSコマンドウィンドウ)で次のように実行する(プロンプト記号 > は入力しない)。

> python parenthese_balance.py your_python_script.py

スクリプト parenthese_balance.py をコマンド python に渡して実行するわけだが、ここでは引数として指定したファイル your_python_script.py はスクリプト parenthese_balance.py と同じディレクトリ(フォルダ)にあるとしている。