【科学技術計算講座ミニ】双方向循環リスト

前回、リスト型のデータ構造である単連結リストについてお話しました。

単連結リストについて解説します。連結リストのメリット、デメリット、データの挿入、削除の方法についても説明しています。また、Pythonでプログラムを作ります。

今回は同じ連結リストの一種である双方向連結リストについて解説したいと思います。

双方向リスト

単連結リストはリストの先頭から一方向にしかたどることが出来ませんでした。これに対して逆側にも連結して前方のノードの情報を持っているリストを双方向リストと呼びます。

双方向リスト

双方向リストの各ノードは、データの値(value)と後方のノード(next)に加え前方のノード(prev)のノード情報を持っています。

双方向リストのノード

双方向リストは逆向きにもリストをたどることが出来ます。また、前方のノードの情報を持っているため、途中の指定したノードに対して、挿入や削除を楽に行うことができます。

双方向リストは、例えばソフトウェアやブラウザなどで、操作やページを前に戻したり、次に進めたりする仕組みに使われていたりします。

双方向循環リスト

双方向リストの場合、リストの末尾を参照するためには、先頭からたどるか、headと同様に末尾を表すノードを作っておく必要があります。このため、次の双方向循環リストがよく使われます。

双方向循環リストは、双方向リストの末尾を先頭のheadノードに連結させた構造をしています。

双方向循環リスト

こうすると、リストの末尾を参照したい場合、headの前方ノード(head.prev)を見ればよいことがわかります。これでheadを起点として前方にも後方にもリストを自由に移動したり、参照したりすることができます。

双方向循環リストのプログラム

では、双方向循環リストのプログラムをPythonで作ってみましょう。

プログラム作成にあたって具体的な問題を考えた方がイメージしやすいので、次のような問題を考えます。

問題:
A、B、C、D、E、F、Gの7人が次のように一列に並びます。

①まず、A、B、C、D、E、Fの6人がこの順番に並びました。
②先頭の一人が列から外れました。
③Gがやってきて、Dの前に割り込みました。
④Eが行列から外れました。

では現在、Cは後ろから数えて何番目にいるでしょうか?

行列の問題

文章で書いてあると一見ややこしそうですが、リストに必要な操作(メソッド)の形で書いてみましょう。それぞれの出来事は、次のリスト操作に対応します。

①リストの末尾にデータを追加する
②リストの先頭のデータを削除する
③指定したデータの前方にデータを挿入する
④指定したデータを削除する
⑤指定したデータの位置をリストの末尾からカウントする

⑤は最後の問題文に対応しています。

では、実際にプログラムを作っていきます。単連結リストのプログラムをベースにしていくとよいでしょう。

まず、下準備としてノードを定義するクラスListNodeを作ります。

class ListNode:
  def __init__(self, value = None):
    self.value = value
    self.next = None
    self.prev = None

双方向リストなので、value(値)、next(後方ノード)、prev(前方ノード)のプロパティを持たせます。

次に双方向循環リストのクラスDoublyLinkedListを定義します。

class DoublyLinkedList:
  def __init__(self):
    self.head = ListNode()
    self.head.next = self.head
    self.head.prev = self.head

コンストラクターでは、先頭のheadノードを作ります。最初は、データのノードがないので、headの後方(head.next)と前方(head.prev)はhead自身としておきます。

※循環リストではheadにデータを持たせる構造にすることも出来ますが、ここではheadは値を持たないダミーのノードとして扱います。

①リストの末尾にデータを追加する

ではメソッドを作っていきましょう。まずは、リストの末尾にデータを追加するメソッドadd_lastです。

  def add_last(self, value):
    new = ListNode(value)
    last = self.head.prev
    new.next = self.head
    new.prev = last
    last.next = new
    self.head.prev = new 

まず、newに新しくノードを作っています。lastheadの前方を参照しているので、リストの末尾のノードです。

これを元につなぎ替えを行います。

new.next = self.head →newの後方にheadをつなぐ
new.prev = last →newの前方にlast
last.next = new
  →lastの後方にnew
self.head.prev = new headの前方にnew

これでnewに連結している4本の連結器がつながりました。

双方向循環リストの末尾へ追加

②リストの先頭のデータを削除する

次にリストの先頭のデータを削除するメソッドdelete_firstを定義します。

  def delete_first(self):
    first = self.head.next
    self.head.next = first.next
    first.next.prev = self.head
    first = None

firstheadの次のノードとします。firstの接続をつなぎ替えて、firstを削除します。

self.head.next = first.next →headの後方にfirstの次のノードをつなぐ
first.next.prev = self.head →firstの次のノードの前方にhead
first = None →firstを削除

双方向循環リストで先頭を削除

③指定したデータの前方にデータを挿入する

指定したデータの前方に新しいノードを挿入するメソッドinsertを作ります。

  def insert(self, value, value2):
    node = self.head.next
    while node != self.head:
      if node.value == value2:
        new = ListNode(value)
        new.next = node
        new.prev = node.prev
        node.prev.next = new
        node.prev = new
        return
      else:  
        node = node.next

nodeを現在アクセスしているノードとして先頭からリストをたどっていきます。nodeの値が2番めの引数value2と同じであれば、その前に新しくノードnewを挿入します。つなぎ替えはこれまでと同様に行います。

new.next = node → newの後方にnode
new.prev = node.prev →newの前方にnode.prev
node.prev.next = new →node.prevの後方にnew
node.prev = new →nodeの前方にnew

双方向リストの挿入

※ただし、2番目と4番目のつなぎ替えの順番が逆だと、new.prevnewになってしまうので順番に注意してください。

④指定したデータを削除する

続いて、指定したデータを削除するメソッドdelete_valueを定義します。

  def delete_value(self, value):
    node = self.head.next
    while node != self.head:
      if node.value == value:
        node.prev.next = node.next
        node.next.prev = node.prev
        node = None
        return
      else:
        node = node.next

これもリストの先頭からたどっていき、nodeの値と引数valueが同じになったら、そのノードを削除します。ここも同様に前後の連結をつなぎ替えます。

双方向リストの削除

⑤指定したデータの位置をリストの末尾からカウントする

最後に、指定したデータの位置をリストの末尾からカウントしていくメソッドcount_backを作成します。

  def count_back(self, value):
    node = self.head.prev
    count = 0
    while node != self.head:
      count += 1
      if node.value == value:
        return count
      node = node.prev
    return -1  

末尾からカウントするため、head.prevheadノードの前方(すなわちリストの末尾)からたどっていきます。nodeの値が引数の値と一致したら、カウントを返します。もし見つからなければ、-1を返しています。

問題を解く

プログラムが出来たので、実際に問題を解きましょう。

list = DoublyLinkedList() #リストの作成
list.add_last("A") #①Aが並ぶ
list.add_last("B") #①Bが並ぶ
list.add_last("C") #①Cが並ぶ    
list.add_last("D") #①Dが並ぶ       
list.add_last("E") #①Eが並ぶ    
list.add_last("F") #①Fが並ぶ 
list.delete_first() #②列の先頭が外れる    
list.insert("G", "D") #③GがDの前に割り込む   
list.delete_value("E") #④Eが列から外れる  
list.print() #リストを出力

count = list.count_back("C") #⑤Cの後ろからの順番を数える
print(count) #答えを出力

list変数にリストを作成して、それぞれのメソッドを使い問題文のとおり順番に実行していきます。

さて答えはいくつになりましたか?

答え:4番目(B C G D F)

全体のプログラム

今回説明したプログラム全体をあげておきます。

class ListNode:
  def __init__(self, value = None):
    self.value = value
    self.next = None
    self.prev = None

class DoublyLinkedList:
  def __init__(self):
    self.head = ListNode()
    self.head.next = self.head
    self.head.prev = self.head

  def add_last(self, value):
    new = ListNode(value)
    last = self.head.prev
    new.next = self.head
    new.prev = last
    last.next = new
    self.head.prev = new  

  def insert(self, value, value2):
    node = self.head.next
    while node != self.head:
      if node.value == value2:
        new = ListNode(value)
        new.next = node
        new.prev = node.prev
        node.prev.next = new
        node.prev = new
        return
      else:  
        node = node.next

  def delete_first(self):
    first = self.head.next
    self.head.next = first.next
    first.next.prev = self.head
    first = None

  def delete_value(self, value):
    node = self.head.next
    while node != self.head:
      if node.value == value:
        node.prev.next = node.next
        node.next.prev = node.prev
        node = None
        return
      else:
        node = node.next

  def print(self):
    node = self.head.next
    while node != self.head:
      print(node.value, end=" ")
      node = node.next
    print()  

  def count_back(self, value):
    node = self.head.prev
    count = 0
    while node != self.head:
      count += 1
      if node.value == value:
        return count
      node = node.prev
    return -1  


list = DoublyLinkedList()
list.add_last("A")
list.add_last("B")    
list.add_last("C")    
list.add_last("D")       
list.add_last("E")    
list.add_last("F") 
list.delete_first()     
list.insert("G", "D")   
list.delete_value("E")  
list.print()

count = list.count_back("C")
print(count)

まとめ

数値計算では、まとまった数値データを扱うときには配列で済ませるということが多いかもしれません。ただ、問題によってはこのようなリスト構造を考えたほうがデータが扱いやすく、スマートに処理できることもよくあるので、ぜひリストも使ってみてください。


全体の目次

スポンサーリンク
科学技術計算のご相談は「キャットテックラボ」へ

科学技術計算やCAEに関するご相談、計算用プログラムの開発などお困りのことは「株式会社キャットテックラボ」へお問い合わせください。

お問い合わせはこちら

フォローする