] > Linear List

第 7 回 線形リスト

本日の内容


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

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

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

線形リスト

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

  1. リストの初期化
  2. 先頭の要素を指すイテレータを得る
  3. 最後の要素を指すイテレータを得る
  4. 要素を最後に足す
  5. 要素を先頭に足す
  6. リストが空かどうか調べる
  7. リストのサイズを調べる
  8. リスト同士の結合
  9. イテレータの指す値
  10. イテレータを隣の要素に
  11. イテレータの指す場所へ要素を挿入
  12. イテレータの指す場所の要素を削除

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

C++ での実装

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

リストの初期化

#include <string>
#include <list>

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

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

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

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

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

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

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

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

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

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

l.erase(i);

例7-1


#include <string>
#include <iostream>
#include <list>
using std::cout;
using std::endl;
using std::string;
using std::list;
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);
  }
}

例7-2

上の例では数の並び替えでは小さい順でしかできません。 しかし、様々な順序で並び替えたい場合もあります。 ここでは、任意の順番を指定して並び替えを行う例を示します。

整数を二進数に直した時、 1 となる桁の少ない順に数を並べることを考えま しょう。 例えば、 0 から 20 までだと次のようになります。

十進数二進数1 の数
000
111
2101
3112
41001
51012
61102
71113
810001
910012
1010102
1110113
1211002
1311013
1411103
1511114
16100001
17100012
18100102
19100113
20101002

したがって、 0 から 20 を並べると次のようになります。

0 個1 個2 個3 個4 個
0, 1, 2, 4, 8, 16, 3, 5, 6, 9, 10, 12, 17, 18, 20, 7, 11, 13, 14, 15

これを C++ で作るには、まずビットの数を計算する関数を書きます。

#include <iostream>
using std::cout;
using std::endl;
int numOfBits(int n){
  int k=0;
  while(n>0){
    k+=n%2;
    n/=2;
  }
  return k;
}
main(){
  for(int i=0; i<=20; i++){
    cout << i << "  " << numOfBits(i) << endl;
  }
}

数を x, y とした時、 1 の数が x より y が大きい 時だけ true, そうでない時 false が返すような () オペレータをメソッドと してもつクラスを作ります。


#include <iostream>
using std::cout;
using std::endl;
int numOfBits(int n){
  int k=0;
  while(n>0){
    k+=n%2;
    n/=2;
  }
  return k;
}
class compBit {
public:
  bool operator()(const int& x, const int& y){
    return numOfBits(x)<numOfBits(y);
  }
};
main(){
  compBit c;
  for(int i=0; i<=20; i++){
    cout << i ;
    for(int j=0; j<=20; j++){
      cout << "  " << c(i,j);
    }
    cout << endl;
  }
}

この compBit クラスを利用して数を並べ替えます。 この時、functional をインクルードして compBit を std::binary_function<int,int,bool> のサブク ラスにしておきます。


#include <iostream>
#include <list>
#include <functional>
using std::cout;
using std::endl;
using std::list;
int numOfBits(int n){
  int k=0;
  while(n>0){
    k+=n%2;
    n/=2;
  }
  return k;
}
class compBit : public std::binary_function<int,int,bool> {
public:
  bool operator()(const int& x, const int& y){
    return numOfBits(x)<numOfBits(y);
  }
};
main(){
  compBit c;
  list<int> l;
  for(int i=0; i<=20; i++){
    l.insert(l.end(),i);
  }
  for(list<int>::iterator i= l.begin(); i!=l.end(); i++){
    cout << *i << " ";
  }
  cout << endl << "---------------------------------------" << endl;
  l.sort(c);
  for(list<int>::iterator i= l.begin(); i!=l.end(); i++){
    cout << *i << " ";
  }
  cout << endl;
}

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()
リスト同士の結合

l.addAll(anotherList);
イテレータの指す要素の値を得て、イテレータを隣の要素に

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

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

i.remove();

例7-3


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()){
	    String s = (String) i.next();
	    System.out.println(s);
	}
	System.out.println("size = "+l.size());
	java.util.Collections.sort(l);
	i = l.listIterator();
	while(!l.isEmpty()){
	    String s = (String) i.next();
	    System.out.println(s);
	    i.remove();
	}
    }
}

例7-4

ここでも例7-2同様に、 Java において、任意の順序を指定して並び替えを行 うことを考えましょう。 そのため、 Java でもまず、 1 の数を計算する関数を作ります。


class compBit {
    static int numOfBits(int n){
	int k=0;
	while(n>0){
	    k+=n%2;
	    n/=2;
	}
	return k;
    }
    public static void main(String[] args){
	for(int i=0; i<=20; i++){
	    System.out.println(i+"  "+numOfBits(i));
	}
    }
}

そして、java.util.Collections.sort() を使うため、Comparator インタフェー スをインプリメントします。 つまり compare メソッドと equals メソッドを実装します。


class compBit implements java.util.Comparator {
    static int numOfBits(int n){
	int k=0;
	while(n>0){
	    k+=n%2;
	    n/=2;
	}
	return k;
    }
    public int compare(Object o1, Object o2){
	int r1=compBit.numOfBits(((Integer)o1).intValue());
	int r2=compBit.numOfBits(((Integer)o2).intValue());
	return (r1<r2)?-1:(r1==r2)?0:1;
    }
    public boolean equals(Object obj){
	return false;
    }
    public static void main(String[] args){
	compBit c=new compBit();
	for(int i=0; i<=20; i++){
	    System.out.print(i);
	    Integer anInteger=new Integer(i);
	    for(int j=0; j<=20; j++){
		System.out.print(" "+c.compare(anInteger, new Integer(j)));
	    }
	    System.out.println();
	}
    }
}

そして、線形リストに対して java.util.Collections.sort() で整列します。


class compBit implements java.util.Comparator {
    static int numOfBits(int n){
	int k=0;
	while(n>0){
	    k+=n%2;
	    n/=2;
	}
	return k;
    }
    public int compare(Object o1, Object o2){
	int r1=compBit.numOfBits(((Integer)o1).intValue());
	int r2=compBit.numOfBits(((Integer)o2).intValue());
	return (r1<r2)?-1:(r1==r2)?0:1;
    }
    public boolean equals(Object obj){
	return false;
    }
    public static void main(String[] args){
	compBit c=new compBit();
	java.util.LinkedList l = new java.util.LinkedList();
	for(int i=0; i<=20; i++){
	    l.addLast(new Integer(i));
	}
	for(java.util.Iterator i = l.iterator(); i.hasNext();){
	    System.out.print(i.next()+" ");
	}
	System.out.println();
	System.out.println("---------------------------------------");
	java.util.Collections.sort(l,c);
	for(java.util.Iterator i = l.iterator(); i.hasNext();){
	    System.out.print(i.next()+" ");
	}
	System.out.println();
    }
}

Smalltalk での実装

Smalltalk では線形リスト的なものは Collections-Sequenceable カテゴリに ある OrderedCollection で提供されています。 このクラス自体、 do: メソッドなど要素毎の繰返しメソッドを持っているた め、イテレータを実装することはプログラミングスタイルに合いません。

リストの初期化

aList ← OrederedCollection new.
先頭の要素を指すイテレータを得る
最後の要素の(次の)イテレータを得る
イテレータという概念自体がプログラミングスタイルに合わない。 リストに対して要素毎に処理させるメッセージを使う。 (以下 each は単に各要素を指す変数名であり別の名前でも良い)
単純に各要素毎に処理する

aList do: [:each | each に対する処理].
各要素毎に処理したもので新しいリストを作る

newList ← aList collect: [:each | each に対する処理].
各要素毎に処理し、結果がtrueなものだけでリストを作る

newList ← aList select: [:each | each に対する処理].
各要素毎に処理し、結果がfalseなものだけでリストを作る

newList ← aList reject: [:each | each に対する処理].
そのほか、集計などに使う inject: などさまざまなメソッドが定義され ています
要素を最後に足す

aList addLast: 要素.
要素を先頭に足す

aList addFirst: 要素.
リストが空かどうか調べる

aList isEmpty
リストのサイズを調べる

aList size
リスト同士の結合
,(カンマ)演算子で二つのリストをつなぐと、連結した一つのリストを返 します。
ランダムアクセス
インデックスを指定するメソッドには at:put: at: removeAt: などがあ ります。
並べ換え
OrderedCollection のサブクラスとして SortedCollection があります。 これは各 Collection のインスタンスに asSortedCollection メソッドを送る と得られるもので、値が整列されています。 このクラスは MappedCollection とは違い、要素の挿入に対して順序を保証す るものではありません。 しかし、得られたインスタンスに対しては do: により順番に値にアクセスで きます。

プログラム例


testList: aStream
  | aCollection aSorted |
  aCollection←OrderedCollection new.
  aCollection addLast: 'abc';
              addLast: 'def';
              addFirst: 'ghi'.
  aStream cr.
  aCollection do: [:each | each printOn: aStream.].
  aStream cr; nextPutAll: aCollection size asString.
  aSorted ← aCollection asSortedCollection.
  aStream cr.
  [aSorted notEmpty] whileTrue: [ aSorted removeFirst printOn: aStream].
  aStream endEntry.

例7-5

ここでも同様に二進数における 1 の数で並び替えます。 Smalltalk では数はオブジェクトですから、二進数における 1 の数を求める には、既存の Integer クラスのメソッドに numberOfBits を書きます。


numberOfBit
| num returnValue |
	num ← self.
	returnValue ← 0.
	[ num > 0 ] whileTrue: [ returnValue ← returnValue + (num \\\ 2 ).
							 num ← num // 2.].
	 ↑returnValue.
テスト例

( 0 to: 20) collect: [ :each | each numberOfBit ].


 #(0 1 1 2 1 2 2 3 1 2 2 3 2 3 3 4 1 2 2 3 2)


これに対して 0 から 20 を並べるには次のようにする。


(0 to: 20) sortBy: [:a :b | (a numberOfBit > b numberOfBit) not].

このように Smalltalk はまとまった手続きがブロックオブジェクトとして記 述できたり、 0 から 20 までのリストが 0 to: 20と書けたり するので簡潔に記述できる。

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 ) の時間計算量で済みます。

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

ランダムアクセス要素の追加、削除
配列 O( log N ) O( N )
線形リスト O( N ) O( 1 )

7-3. 双方向リスト

双方向リストとは、次の頂点へのポインタと前の頂点へのポインタの両方を保 持するものです。 特定の頂点に注目して処理する場合に便利です。 エディタなど、特定のに注目した処理をするのに用いられます。 実は C++ の list, Java の LinkedList, Smalltalk の OrderedList のどれも双方向リストです。 C++ では Iterator に対して -- 演算が可能です。 一方、Java では ListIterator には previous() メソッドが用意されていま す。 Smalltalk ではイテレータを使いませんが、ランダムアクセスとして、整数値 をインデックスとして値を取り出す at: や、逆に指定したインデックスに値 を入れる at:put: などのメソッドが定義されています。

演習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>
東京電機大学工学部情報通信工学科