] > Linear List

第 7 回 線形リスト

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

7-1. 線形リストの操作

線形リストとは図のような特殊な二分木でした。

線形リスト

値は葉にだけ入れられ、左向きの枝には必ず葉が付きます。そして、右の枝に 付く葉には nil という値の葉が付きます。 このような線形リストについて次のような操作を行うことを考えます。

  1. リストの初期化
  2. 要素を最後に足す
  3. 要素を n 番目に足す
  4. リストが空かどうか調べる
  5. リストのサイズを調べる
  6. n 番目の要素を得る
  7. n 番目の要素を変更する
  8. n 番目の要素を消す

また、要素を順番に取り出すためにIterator(反復子)を使います。 Iterator(反復子)とは要素の位置を取り出すものですが、ポインタや参照とは 同じではありません。 まず、変数の番地を格納する変数を参照と言います。 そして特に、その番地同士の演算を認めているのがポインタです。 但し、ポインタの演算は不正なメモリアクセスの原因となるため、多くのプロ グラミング言語では認められてません。 その一方で、連続した情報をアクセスするため「次の参照」を得ることは重要 です。 参照に対して、「次の参照」と「次の参照の存在の確認」という操作が可能で あるものをIterator(反復子)と言います。

C++ での実装

C++ ではすでに線形リストは STL で提供されています。 したがって、上記の処理は特別なプログラムを組まなくても次のようにすれば 可能になります。以下はリストの中に文字列を入れる例です。

リストの初期化

#include <string>
#include <list>

list<string> l;
先頭の要素を指すイテレータを得る

list<string>::iterator i;
i=l.begin();
最後の要素の(次の)イテレータを得る

list<string>::iterator i;
i=l.end();
要素を最後に足す

l.insert(最後を指すイテレータ,要素);
要素を先頭に足す

l.insert(先頭を指すイテレータ,要素);
リストが空かどうか調べる

l.empty()
リストのサイズを調べる

l.size()
リスト同士の結合
イテレータの指す要素の値

*i;
イテレータを隣の要素に

i++;
イテレータの指す場所へ要素を挿入

l.insert(i,要素);
イテレータの指す場所の要素を削除

l.erase(i);

プログラム例


#include <string>
#include <iostream>
#include <list>
using namespace std;
main(){
  list<string> l;
  list<string>::iterator i;
  l.insert(l.end(),"abc");
  l.insert(l.end(),"def");
  l.insert(l.begin(),"ghi");
  for(i=l.begin(); i!=l.end(); i++){
    cout << *i << endl;
  }
  cout << "size = " << l.size() << endl;
  l.sort();
  while(!l.empty()){
    i=l.begin();
    cout << *i << endl;
    l.erase(i);
  }
}

Java での実装

Java では線形リストは java.util.LinkedList で提供されています。 また、 java.util.ListIterator をインプリメントしています。 なお、 C++ では Iterator はポインタを改造して作られましたが、 Java で は Iterator はクラス(interface)として実装されます。

リストの初期化

import java.util.*;

LinkedList l = new LinkedList();
先頭の要素を指すイテレータを得る

ListIterator i = l.listIterator();
最後の要素の(次の)イテレータを得る
一命令では存在しない。
要素を最後に足す

l.addLast(要素);
要素を先頭に足す

l.addFirst(要素);
リストが空かどうか調べる

l.isEmpty()
リストのサイズを調べる

l.size()
リスト同士の結合
イテレータの指す要素の値を得て、イテレータを隣の要素に

Object o = i.next();
イテレータの指す場所へ要素を挿入

i.add(要素);
next() をする前のイテレータの指す場所の要素を削除

i.remove();

プログラム例


import java.util.*;
class TestList {
    public static void main(String arg[]){
	LinkedList l = new LinkedList();
	l.addLast("abc");
	l.addLast("def");
	l.addFirst("ghi");
	ListIterator i =l.listIterator();
	while(i.hasNext()){
	    Object o = i.next();
	    System.out.println(o);
	}
	System.out.println("size = "+l.size());
	i =l.listIterator();
	while(!l.isEmpty()){
	    Object o = i.next();
	    System.out.println(o);
	    i.remove();
	}
    }
}

C 言語での実装

C++ や Java では線形リストがあらかじめ用意されていましたが、 C 言語に はありません。そこで、ポインタや構造体を使って実現する必要があります。

線形リストでは左の枝には必ず値を持つ頂点が付くので、左の枝を作る代わり に値を直接入れることにします。 右の枝はそのまま別の頂点を指すことにしますので、頂点を作る構造体は、左 の枝を意味する場所には値を入れ、右の枝を表す場所には別の頂点へのポイン タを入れることにします。 線形リストの頂点の構造体を定義すると次のようになります。


typedef struct lst {
  char *item;
  struct lst  *next;
} LIST;

このようにして作った構造体をメモリ上に作り、next フィールドに次の頂点 の番地を入れて連結します。 ところで、リストの最後の nil を含んだ葉も同じ頂点である必要があります。 そこで、 next フィールドに NULL が入っていることとして表現することにし ます。 すると、空のリストは NULL が入った頂点だけで表現することになります。 一方、プログラム内でリストを扱うには、先頭の頂点へのポインタを持ちます。 したがって、プログラムで空のリストを作るには、次のようにします。


LIST *l=(LIST *)malloc(sizeof(LIST));
l->next=NULL;

先頭への要素の追加

先頭に要素を付け足す関数 addfirst(LIST *l, char *i) は次のよう処理を行 います。

まず、C 言語は値呼出しなので、関数の引数 l に含まれている先頭の 頂点へのポインタ l は変更できません。 これは先頭の頂点のアドレスは変更できないことを意味します。 ですから、新たな要素 *i は既存の頂点へ代入されることになります。 したがって、付け足すべき新たな頂点には、現在の先頭の要素が入ることにな ります。つまり、新しい頂点の領域を確保したらその頂点は二番目の頂点にな るようにします。

新しい頂点の next フィールドは三番目、つまり付け足す前では二番目の頂点 を指すようにします。これは付け足す前では先頭の next フィールドに入って いたアドレスになります。

したがって処理をまとめると次のようになります。

  1. 新しい頂点の領域を作る
  2. 先頭の頂点の内容(要素、次の頂点へのポインタ)を新しい頂点にコピーす る。
  3. 先頭の頂点の要素には付け足すべき要素を代入し、次の頂点へのポインタ には新しく確保した頂点の領域のアドレスを入れる。

void addfirst(LIST *l, char *i){
   LIST *newnode=(LIST *)malloc(sizeof(LIST));
   newnode->item = l->item;
   newnode->next = l->next;
   l->item = i;
   l->next = newnode;
}

最後への要素の追加

一方、リストの最後に要素を付け足す関数 addend(LIST *l, char *i) は、 nil 頂点を探して、その頂点に新しい要素を入れ、新たに作った nil 頂点を 指すようにします。


void addend(LIST *l, char *i){
  LIST *newnode=(LIST *)malloc(sizeof(LIST));
  LIST *p=l;
  while(p->next != NULL){
    p = p->next;
  }
  p->item = i;
  p->next = newnode;
  newnode->next = NULL;
}

指している頂点の削除

単純に指している頂点を削除すると、その頂点を指す手前の頂点が次の頂点を 指すようにできません。 したがって、領域としてはその次の頂点の領域を消すことにし、 現在、指している頂点は、消さずに次の領域の値を消すことにします。

  1. 消すべき領域である、次の頂点のアドレスを記憶する
  2. 現在の頂点の領域に、次の頂点の要素とポインタをコピーする
  3. 消すべき領域を消す

void removenode(LIST *l){
  LIST *d=l->next;
  l->item = d->item;
  l->next = d->next;
  free(d);
}

サンプルコード


#include <stdio.h>
#include <stdlib.h>
typedef struct lst {
  char *item;
  struct lst  *next;
} LIST;
void addend(LIST *pointer, char *i){
  LIST *newnode=(LIST *)malloc(sizeof(LIST));
  newnode->next=NULL;
  while(pointer->next != NULL){
    pointer = pointer->next;
  }
  pointer->next=newnode;
  pointer->item=i;
}
void addfirst(LIST *pointer, char *i){
  LIST *newnode=(LIST *)malloc(sizeof(LIST));
  newnode->next=pointer->next;
  newnode->item=pointer->item;
  pointer->next=newnode;
  pointer->item=i;
}
int empty(LIST *pointer){
  return pointer->next == NULL;
}
int size(LIST *pointer){
  LIST *p=pointer;
  int i=0;
  while(p->next != NULL){
    p=p->next;
    i++;
  }
  return i;
}
void removeitem(LIST *pointer){
  LIST *d=pointer->next;
  pointer->item=d->item;
  pointer->next=d->next;
  /* free((pointer->next)->item) */
  free(d);
}

main(){
  LIST *l=(LIST *)malloc(sizeof(LIST));
  LIST *p;
  l->next=NULL;
  addend(l,"abc");
  addend(l,"def");
  addfirst(l,"ghi");
  p=l;
  while(p->next!=NULL){
    printf("%s\n",p->item);
    p=p->next;
  }
  printf("size = %d\n",size(l));
  while(!empty(l)){
    p=l;
    while(p->next!=NULL){
      printf("%s ",p->item);
      p=p->next;
    }
    printf("\n");
    removeitem(l);
  }
  
}

7-2. 線形リストと配列

線形リストも配列も複数のものを格納することができますが、それぞれにおい て処理の効率が変わります。 従って、用途に応じて選択する必要があります。

配列はメモリの連続した領域を確保し、 n 番目の要素へのアクセスを許しま す。 n 番目の要素へのアクセスは既に紹介したように、 (0 番目の要素のアドレス ) + n (要素のサイズ) でアドレスを計算します。 従って、時間計算量は n を計算する手間になります。 これは n の桁数に比例した時間かかります。 要素数が最大 N だとすると、全体の時間計算量は O( log N ) になります。 一方、特定の位置(先頭など)に要素を挿入したり、削除したりするには全部の 要素をずらす必要があります。 一つの要素をずらすのに定数時間で済んでも、最大で全要素数 N に比例する 時間だけかかります。 従って、全体の時間計算量は O( N ) になります。

線形リストでは n 番目の要素にアクセスするには、基本的には先頭から順に 見ていくしかありません。 従って、時間計算量は O( N ) になります。 しかし、一方、特定の位置に要素を挿入したり、削除したりするには、メモリ の適当な位置に頂点を確保し、リンクをつなげるだけなので、 O( 1 ) の時間計算量で済みます。

以上のように要素へのランダムアクセスが必要な時は配列、要素の追加、削除 が頻繁な時は線形リストが有利なことが分かります。

7-3. 双方向リスト

双方向リストとは、次の頂点へのポインタと前の頂点へのポインタの両方を保 持するものです。 特定の頂点に注目して処理する場合に便利です。 エディタなど、特定のに注目した処理をするのに用いられます。 実は C++ の list, Java の LinkedList のどちらとも双方向リストです。 C++ では Iterator に対して -- 演算が可能です。 一方、Java では ListIterator には previous() メソッドが用意されていま す。

演習7-1

双方向リストを使って less を作りなさい。 less とは more を拡張したコマンドで、次の動作をします。

  1. 標準入力からファイルを読み込みます。
  2. 先頭の 24 行を表示します。
  3. 空白か d の入力で次の 24 行を表示します。
  4. u の入力で前の 24 行を表示します。
  5. q の入力で終了します。
  6. < で先頭へ、 > で最後(から 24 行前)へ注目行を移動します。

付録 C 言語での双方向リストの実装


#include <stdio.h>
#include <stdlib.h>
typedef struct blst {
  char *item;
  struct blst  *next;
  struct blst  *previous;
} BLIST;
void add(BLIST *base, BLIST *pointer, char *i){
  BLIST *newnode=(BLIST *)malloc(sizeof(BLIST));
  newnode->item = i;
  newnode->next = pointer->next;
  if(pointer->next == NULL){
    newnode->previous = base->previous;
    base->previous = newnode;
  }else{
    newnode->previous = (pointer->next)->previous;
    (pointer->next)->previous = newnode;
  }
  pointer->next = newnode;
}

void printlist(BLIST *base){
  BLIST *p=base;
  while((p=p->next)!=NULL){
    printf("%s ",p->item);
  }
  printf("\n");
}
void printreverse(BLIST *base){
  BLIST *p=base;
  while((p=p->previous)!=NULL){
    printf("%s ",p->item);
  }
  printf("\n");
}
int empty(BLIST *base){
  return base->next == NULL;
}
int size(BLIST *base){
  BLIST *p=base;
  int i=0;
  while((p=p->next) != NULL){
    i++;
  }
  return i;
}
int removenode(BLIST *base, BLIST *pointer){
  BLIST *p,*n;
  if(base==pointer){
    return -1;
  }else{
    p = pointer->previous;
    n = pointer->next;
    if(p==NULL){
      base->next = n;
    }else{
      p->next = n;
    }
    if(n==NULL){
      base->previous = p;
    }else{
      n->previous = p;
    }
    /* free(pointer->item) */
    free(pointer);
 }
}
    
main(){
  BLIST *l=(BLIST *)malloc(sizeof(BLIST));
  BLIST *p;
  l->next=l->previous=NULL;
  add(l,l,"abc");
  printlist(l); printreverse(l);  printf("size = %d\n",size(l));
  add(l,l,"def");
  printlist(l);  printreverse(l);  printf("size = %d\n",size(l));
  add(l,l->previous,"ghi");
  printlist(l);  printreverse(l);  printf("size = %d\n",size(l));
  add(l,l->previous,"jkl");
  printlist(l);  printreverse(l);  printf("size = %d\n",size(l));
  add(l,l->next->next->next,"mno");
  printlist(l);  printreverse(l);  printf("size = %d\n",size(l));
  while(!empty(l)){
    printlist(l);  printreverse(l);  printf("size = %d\n",size(l));
    removenode(l, l->next);
  }
}

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科