ホーム » スタッフ » 斉藤徹 » 講義録 » 情報構造論 » 双方向リスト

双方向リスト

単純リストから双方向リストへ

ここまで説明してきた単純リストは、次のデータへのポインタを持つ。ここで、1つ後ろのデータ(N番目からN+1番目)をアクセスするのは簡単だけど、1つ前のデータ(N-1番目)を参照しようと思ったら、先頭から(N-1)番目を辿るしかない。でも、これは O(N) の処理であり時間がかかる処理。
ではどうすればよいのか?

この場合、一つ前のデータの場所を覚えているポインタがあれば良い。

Javaの場合

import java.util.*;

class BDListNode {
    BDListNode prev ;  // ひとつ前のデータ
    int        data ;
    BDListNode next ;  // 次のデータ
    
    BDListNode( BDListNode p , int d , BDListNode n ) {
        this.prev = p ;
        this.data = d ;
        this.next = n ;
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        // 進行方向だけひとまず生成
        BDListNode top
            = new BDListNode( null , 1 ,
                 new BDListNode( null , 2 ,
                    new BDListNode( null , 3 , null ) ) ) ;
        // 前に移動するためのポインタをつなげる
        top.next.prev = top ;
        top.next.next.prev = top.next ;

        // ポインタを進行方向に進めながらループ
        for( BDListNode p = top ; p != null ; p = p.next )
            System.out.println( p.data ) ; // 1,2,3

        // ポインタを逆方向に進めながらループ
        for( BDListNode p = top.next.next ; p != null ; p = p.prev )
            System.out.println( p.data ) ; // 3,2,1
    }
}

C言語の場合

// 双方向リストの宣言
struct BD_List {
    struct BD_List* prev ; // 1つ前のデータへのポインタ
    int             data ;
    struct BD_List* next ; // 次のデータへのポインタ
} ;

このデータ構造は、双方向リスト(bi-directional list)と呼ばれる。では、簡単なプログラムを書いてみよう。双方向リストのデータを簡単に生成するための補助関数から書いてみる。

// リスト生成補助関数
struct BD_List* bd_cons( struct BD_List* p ,
                         int d ,
                         struct BD_List* n ) {
    struct BD_List* ans ;
    ans = (struct BD_List*)malloc(
                         sizeof( struct BD_List ) ) ;
    if ( ans != NULL ) {
        ans->prev = p ;
        ans->data = d ;
        ans->next = n ;
    }
    return ans ;
}
void main() {
    struct BD_List* top ;
    struct BD_List* p ;

    // 順方向のポインタでリストを生成
    top = bd_cons( NULL , 1 ,
          bd_cons( NULL , 2 ,
          bd_cons( NULL , 3 , NULL ) ) ) ;
    // 逆方向のポインタを埋める
    top->next->prev = top ;
    top->next->next->prev = top->next ;

    // リストを辿る処理
    for( p = top ; p->next != NULL ; p = p->next )
        printf( "%d\n" , p->data ) ;
    for(         ; p->prev != NULL ; p = p->prev )
        printf( "%d\n" , p->data ) ;
}

双方向リストの関数作成

以上の説明で、双方向の基礎的なプログラムの意味が分かった所で、練習問題。

先のプログラムでは、1,2,3 を要素とするリストを、ナマで記述していた。実際には、どんなデータがくるか分からないし、指定したポインタ p の後ろに、データを1件挿入する処理 bd_insert( p , 値 ) , また、p の後ろのデータを消す処理 bd_delete( p ) を書いてみよう。

Javaの場合

public class Main {
    // 指定したノードの後ろに1件追加
    static void bd_insert( BDListNode p , int d ) {
        p.next = new BDListNode( null , d , p.next ) ;
        p.next.next.prev = p.next ;
        p.next.prev = p ;
    }
    // 指定したノードの後ろを1件削除
    static void bd_delete( BDListNode p ) {
        p.next = p.next.next ;
        p.next.prev = p ;
    }
    public static void main(String[] args) throws Exception {
        // 双方向リストの追加,削除            
        BDListNode top2
            = new BDListNode( null , 1 ,
                 new BDListNode( null , 2 ,
                    new BDListNode( null , 4 , null ) ) ) ;
        top2.next.prev = top2 ;
        top2.next.next.prev = top2.next ;
        
        // 途中に挿入の実験
        bd_insert( top2.next , 3 ) ;
        for( BDListNode p = top2 ; p != null ; p = p.next )
            System.out.println( p.data ) ; // 1,2,3,4
        for( BDListNode p = top2.next.next.next ; p != null ; p = p.prev )
            System.out.println( p.data ) ; // 4,3,2,1
        
        // 途中を削除の実験
        bd_delete( top2.next ) ;
        for( BDListNode p = top2 ; p != null ; p = p.next )
            System.out.println( p.data ) ; // 1,2,4
        for( BDListNode p = top2.next.next ; p != null ; p = p.prev )
            System.out.println( p.data ) ; // 4,2,1
    }
}

C言語の場合

// 双方向リストの指定場所 p の後ろに、値 d を要素とするデータを挿入せよ。
void bd_insert( struct BD_List* p , int d ) {
   struct BD_List*n = bd_cons( p , d , p->next ) ;
   if ( n != NULL ) {
      p->next->prev = n ;
      p->next = n ;
   }
}

// 双方向リストの指定場所 p の後ろのデータを消す処理は?
void bd_delete( struct BD_List* p ) {
   struct BD_List* d = p->next ;
   d->next->prev = p ;
   p->next = d->next ;
   free( d ) ;
}

// この手のリスト処理のプログラムでは、命令の順序が重要となる。
// コツとしては、修正したい箇所の遠くの部分を操作する処理から
// 書いていくと間違いが少ない。

番兵と双方向循環リスト

前述の bd_insert() だが、データの先頭にデータを挿入したい場合は、どう呼び出せば良いだろうか?
bd_insert() で、末尾にデータを挿入する処理は、正しく動くだろうか?

同じく、bd_delete() だが、データの先頭のデータを消したい場合は、どう呼び出せば良いだろうか?
また、データを消す場合、最後の1件のデータが消えて、データが0件になる場合、bd_delete() は正しく動くだろうか?

こういった問題が発生した場合、データが先頭・末尾で思ったように動かない時、0件になる場合に動かない時、特別処理でプログラムを書くことは、プログラムを読みづらくしてしまう。そこで、一般的には 循環リストの時にも紹介したが、番兵(Sentinel) を置くことが多い。

しかし、先頭用の番兵、末尾用の番兵を2つ用意するぐらいなら、循環リストにした方が便利となる。このような双方向リストでの循環した構造は、双方向循環リスト(bi-directional ring list)と呼ばれる。

deque(両端キュー)

双方向循環リストを使うと、(1)先頭にデータを挿入(addFirst/unshift)、(2)先頭のデータを取り出す(removeFirst/shift)、(3)末尾にデータを追加(addLast/push)、(4)末尾のデータを取り出す(removeLast/pop)、といった処理が簡単に記述できる。この4つの処理を使うと、単純リスト構造で説明した、待ち行列(queue)やスタック(stack) が実現できる。この特徴を持つデータ構造は、先頭・末尾の両端を持つ待ち行列ということで、deque (double ended queue) とも呼ばれる。