1. リスト
著者:梅谷 武
各言語においてリストの基本動作を確認し、比較する。
作成:2024-03-29
更新:2024-10-24
更新:2024-10-24
広い意味ではリストとは要素列の意味であり、例えば配列も一種のリストであると言えなくもない。しかし、通常は要素を一つ格納できるノードをポインターでつないだ線形リストと呼ばれるデータ構造の意味で用いられる。詳しくはデータ構造とアルゴリズムの教科書を参照していただきたい。
リスト処理(list processing)の元祖であるLISP系言語ではconsと呼ばれる操作を繰り返すことによりリストが構築される。Racketを使って簡単な例を示す。
> (define x (cons 1 (cons 2 (cons 3 '()))))
> x
'(1 2 3)
> (define y '(4 5 6))
> y
'(4 5 6)
> (define y (list 4 5 6))
> y
'(4 5 6)
> (define z (append x y))
> z
'(1 2 3 4 5 6)
> (length z)
6
> (reverse z)
'(6 5 4 3 2 1)
> (list-ref z 0)
1
> (list-ref z 5)
6
> (car z)
1
> (cdr z)
'(2 3 4 5 6)
> (first z)
1
> (rest z)
'(2 3 4 5 6)
同じことをOCamlでやってみる。
# let x = 1 :: 2 :: 3 :: [];;
val x : int list = [1; 2; 3]
# let y = [4; 5; 6];;
val y : int list = [4; 5; 6]
# let z = x @ y;;
val z : int list = [1; 2; 3; 4; 5; 6]
# let z = List.append x y;;
val z : int list = [1; 2; 3; 4; 5; 6]
# List.length z;;
- : int = 6
# List.rev z;;
- : int list = [6; 5; 4; 3; 2; 1]
# List.nth z 0;;
- : int = 1
# List.nth z 5;;
- : int = 6
# List.hd z;;
- : int = 1
# List.tl z;;
- : int list = [2; 3; 4; 5; 6]
OCamlでは整数型リストがint listと静的型付けされる。
C++では関数型言語のリストに対応する様々なコンテナがあり、目的に応じて選べるようになっている。ここではstd::dequeを用いてOCaml、Racketと比較する。
OCamlのリストはそのすべての要素が同じ型でなければならない。これはどんな型の要素も許容するLISP系のリストとは異なっている。ここではOCamlに合わせることにする。オンラインマニュアル:Listsを参照のこと。
リストについての概説はThe Racket Guide>3 Built-in Datatypes>3.8 Pairs and Lists、詳細仕様はThe Racket Reference>4 Datatypes>4.10 Pairs and Listsにある。
- consをn回繰り返し、長さnのリストlstを作る。
- リストlstを反転し、リストlst2を作る。
consはpush_front()に相当する。標準ライブラリにはリストを反転し、それを元のリストに書き戻すstd::reverseがあるが、ここでは関数型言語に合わせて出力用リストを生成し、std::reverse_copyを用いて反転複写する。
cons.cpp
#include <algorithm>
#include <chrono>
#include <deque>
#include <iterator>
#include <print>
#include <stdexcept>
void test(long n) {
std::deque<long> lst, lst2;
// cons
auto start = std::chrono::high_resolution_clock::now();
for (long i = n; i >= 1l; --i)
lst.push_front(i);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::println("length = {}", n);
std::println("{} ---> {}", lst[0], lst[n - 1]);
std::println("cons time: {:9.6f} [sec]", duration.count());
// reverse
start = std::chrono::high_resolution_clock::now();
std::reverse_copy(lst.begin(), lst.end(), std::back_inserter(lst2));
end = std::chrono::high_resolution_clock::now();
duration = end - start;
std::println("{} ---> {}", lst2[0], lst2[n - 1]);
std::println("rev time: {:9.6f} [sec]", duration.count());
}
int main(int argc, char *argv[]) {
try {
if (argc != 2) {
throw std::invalid_argument(
"Usage: cons <positive integer>");
}
long n = std::stol(argv[1]);
if (n <= 0l) {
throw std::invalid_argument(
"The argument must be a positive integer.");
}
test(n);
} catch (const std::logic_error &e) {
std::println(stderr, "Logic error: {}", e.what());
} catch (const std::runtime_error &e) {
std::println(stderr, "Runtime error: {}", e.what());
} catch (const std::exception &e) {
std::println(stderr, "Another std::exception: {}", e.what());
} catch (...) {
std::println(stderr, "Unknown exception.");
}
return 0;
}
makefile:
CPPG = g++
CPPL = clang++
CPPFLAGS = -std=c++23 -O2
SRC = cons.cpp
EXES = cons_g cons_l
all: $(EXES)
cons_g: $(SRC)
$(CPPG) $(CPPFLAGS) -o $@ $(SRC)
cons_l: $(SRC)
$(CPPL) $(CPPFLAGS) -o $@ $(SRC)
clean:
rm -f $(EXES)
n = 10000000で測定する。
$ measure ./cons_g 10000000
length = 10000000
1 ---> 10000000
cons time: 0.068595 [sec]
10000000 ---> 1
rev time: 0.091221 [sec]
======================================
Process exited with status: 0
total time: 0.194512 [sec]
mem size: 167960 [KB]
code size: 128 [KB]
$ measure ./cons_l 10000000
length = 10000000
1 ---> 10000000
cons time: 0.077080 [sec]
10000000 ---> 1
rev time: 0.107523 [sec]
======================================
Process exited with status: 0
total time: 0.216572 [sec]
mem size: 167136 [KB]
code size: 102 [KB]
OCamlにおいてconsは中置演算子::を使う。
cons.ml:
open Printf
let rec push_front n lst =
if n = 0 then
lst
else
push_front (n - 1) (n :: lst)
let test n =
(* cons *)
let start = Unix.gettimeofday () in
let lst = push_front n [] in
let end_ = Unix.gettimeofday () in
printf "length = %d\n" n;
printf "%d ---> %d\n" (List.nth lst 0) (List.nth lst (n-1));
printf "cons time: %9.6f [sec]\n" (end_ -. start);
(* reverse *)
let start = Unix.gettimeofday () in
let lst2 = List.rev lst in
let end_ = Unix.gettimeofday () in
printf "%d ---> %d\n" (List.nth lst2 0) (List.nth lst2 (n-1));
printf "rev time: %9.6f [sec]\n" (end_ -. start)
let () =
try
if Array.length Sys.argv <> 2 then
raise (Invalid_argument
"Usage: cons <positive integer>")
else
let n = int_of_string Sys.argv.(1) in
if n <= 0 then
raise (Invalid_argument
"The argument must be a positive integer.")
else
test n
with
| Invalid_argument msg ->
eprintf "Logic error: %s\n" msg
| Failure msg ->
eprintf "Runtime error: %s\n" msg
| exn ->
eprintf "Another exception: %s\n" (Printexc.to_string exn)
makefile:
OCAMLOPT = ocamlopt
OCAMLFLAGS = -O2
LIBPATH = -I +unix
LIBS = unix.cmxa
SRC = cons.ml
EXES = cons
all: $(EXES)
cons: $(SRC)
$(OCAMLOPT) $(OCAMLFLAGS) $(LIBPATH) -o $@ $(LIBS) $(SRC)
clean:
rm -f $(EXES) *.o *.cmx *.cmi
n = 10000000で測定する。
$ measure ./cons 10000000
length = 10000000
1 ---> 10000000
cons time: 0.623374 [sec]
10000000 ---> 1
rev time: 0.665878 [sec]
======================================
Process exited with status: 0
total time: 1.383176 [sec]
mem size: 473452 [KB]
code size: 3289 [KB]
cons.rkt:
#lang racket
(require iso-printf)
(define (push_front i lst)
(if (zero? i)
lst
(push_front (- i 1) (cons i lst))))
(define (test n)
; cons
(define start (current-inexact-milliseconds))
(define lst (push_front n '()))
(define end (current-inexact-milliseconds))
(printf "length = %d\n" n)
(printf "%d ---> %d\n" (list-ref lst 0) (list-ref lst (- n 1)))
(printf "cons time: %9.6f [sec]\n" (/ (- end start) 1000.0))
; reverse
(set! start (current-inexact-milliseconds))
(define lst2 (reverse lst))
(set! end (current-inexact-milliseconds))
(printf "%d ---> %d\n" (list-ref lst2 0) (list-ref lst2 (- n 1)))
(printf "rev time: %9.6f [sec]\n" (/ (- end start) 1000.0)))
(define (main args)
(with-handlers
([exn:fail? (λ (e) (eprintf "%s\n" (exn-message e)))]
[exn? (λ (e) (eprintf "Unexpected: %s\n" (exn-message e)))])
(cond
[(not (= (vector-length args) 1))
(error "Usage: cons <positive integer>")]
[else
(let ([n (string->number (vector-ref args 0))])
(cond
[(not (integer? n))
(error "The argument must be an integer.")]
[(<= n 0)
(error "The argument must be a positive integer.")]
[else
(test n)]))])))
(main (current-command-line-arguments))
n = 10000000で測定する。
$ raco exe cons.rkt
$ measure ./cons 10000000
length = 10000000
1 ---> 10000000
cons time: 0.553803 [sec]
10000000 ---> 1
rev time: 0.748057 [sec]
======================================
Process exited with status: 0
total time: 1.961181 [sec]
mem size: 444932 [KB]
code size: 12444 [KB]
要素数k個(k=10000固定)のリストを二つ連結する操作をn回繰り返す。
- consをk回繰り返し、長さkのリストlstを作る。
- リストlstを二つ連結する操作をn回繰り返す。
append.cpp:
#include <algorithm>
#include <chrono>
#include <deque>
#include <iterator>
#include <print>
#include <stdexcept>
void test(long n) {
std::deque<long> lst, lst2;
// cons
long k = 10000;
for (long i = k; i >= 1l; --i)
lst.push_front(i);
std::println("length = {}", lst.size());
std::println("{} ---> {}", lst[0], lst[k - 1]);
// append
std::deque<long> tmp1(lst), tmp2(lst);
auto start = std::chrono::high_resolution_clock::now();
for (long i = 1l; i <= n; ++i) {
tmp1.insert(tmp1.end(), tmp2.begin(), tmp2.end());
tmp2 = tmp1;
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::println("{} times append: length = {}", n, tmp1.size());
std::println("app time: {:9.6f} [sec]", duration.count());
}
int main(int argc, char *argv[]) {
try {
if (argc != 2) {
throw std::invalid_argument(
"Usage: append <positive integer>");
}
long n = std::stol(argv[1]);
if (n <= 0l) {
throw std::invalid_argument(
"The argument must be a positive integer.");
}
test(n);
} catch (const std::logic_error &e) {
std::println(stderr, "Logic error: {}", e.what());
} catch (const std::runtime_error &e) {
std::println(stderr, "Runtime error: {}", e.what());
} catch (const std::exception &e) {
std::println(stderr, "Another std::exception: {}", e.what());
} catch (...) {
std::println(stderr, "Unknown exception.");
}
return 0;
}
makefile:
CPPG = g++
CPPL = clang++
CPPFLAGS = -std=c++23 -O2
SRC = append.cpp
EXES = append_g append_l
all: $(EXES)
append_g: $(SRC)
$(CPPG) $(CPPFLAGS) -o $@ $(SRC)
append_l: $(SRC)
$(CPPL) $(CPPFLAGS) -o $@ $(SRC)
clean:
rm -f $(EXES)
n = 10で測定する。
$ measure ./append_g 10
length = 10000
1 ---> 10000
10 times append: length = 10240000
app time: 0.171895 [sec]
======================================
Process exited with status: 0
total time: 0.208210 [sec]
mem size: 170824 [KB]
code size: 142 [KB]
$ measure ./append_l 10
length = 10000
1 ---> 10000
10 times append: length = 10240000
app time: 0.169502 [sec]
======================================
Process exited with status: 0
total time: 0.206438 [sec]
mem size: 170884 [KB]
code size: 120 [KB]
OCamlにおいては二つのリストlst1,lst2の連結lst1@lst2あるいはList.append lst1 lst2は末尾再帰になっていないためリストが大きくなるとメモリを大量に消費する。本プログラムにおいても再帰の中に@演算やList.appendを入れるとスタックオーバーフローが発生する。そこでメモリ消費量を抑制するためにlst1@lst2の代わりにList.rev_append (List.rev lst1) lst2を使う。List.rev_appendは末尾再帰になっている。
append.ml:
open Printf
let rec push_front n lst =
if n = 0 then
lst
else
push_front (n - 1) (n :: lst)
let rep_app n lst =
let rec aux n acc =
if n = 0 then
acc
else
aux (n - 1) (List.rev_append (List.rev acc) acc)
in aux n lst
let test n =
(* cons *)
let k = 10000 in
let lst = push_front k [] in
printf "length = %d\n" n;
printf "%d ---> %d\n" (List.nth lst 0) (List.nth lst (k-1));
(* append *)
let start = Unix.gettimeofday () in
let result = rep_app n lst in
let end_ = Unix.gettimeofday () in
printf "%d times append: length = %d\n" n (List.length result);
printf "app time: %9.6f [sec]\n" (end_ -. start)
let () =
try
if Array.length Sys.argv <> 2 then
raise (Invalid_argument
"Usage: append <positive integer>")
else
let n = int_of_string Sys.argv.(1) in
if n <= 0 then
raise (Invalid_argument
"The argument must be a positive integer.")
else
test n
with
| Invalid_argument msg ->
eprintf "Logic error: %s\n" msg
| Failure msg ->
eprintf "Runtime error: %s\n" msg
| exn ->
eprintf "Another exception: %s\n" (Printexc.to_string exn)
makefile:
OCAMLOPT = ocamlopt
OCAMLFLAGS = -O2
LIBPATH = -I +unix
LIBS = unix.cmxa
SRC = append.ml
EXES = append
all: $(EXES)
append: $(SRC)
$(OCAMLOPT) $(OCAMLFLAGS) $(LIBPATH) -o $@ $(LIBS) $(SRC)
clean:
rm -f $(EXES) *.o *.cmx *.cmi
n = 10で測定する。
$ measure ./append 10
length = 10
1 ---> 10000
10 times append: length = 10240000
app time: 1.246856 [sec]
======================================
Process exited with status: 0
total time: 1.300116 [sec]
mem size: 363976 [KB]
code size: 3289 [KB]
RacketではOCamlのような問題はないので再帰の中にappendを入れている。
append.rkt:
#lang racket
(require iso-printf)
(define (push_front i lst)
(if (zero? i)
lst
(push_front (- i 1) (cons i lst))))
(define (rep_app n lst)
(if (zero? n)
lst
(rep_app (- n 1) (append lst lst))))
(define (test n)
; cons
(define k 10000)
(define lst (push_front k '()))
(printf "length = %d\n" k)
(printf "%d ---> %d\n" (list-ref lst 0) (list-ref lst (- k 1)))
; append
(define start (current-inexact-milliseconds))
(define result (rep_app n lst))
(define end (current-inexact-milliseconds))
(printf "%d times append: length = %d\n" n (length result))
(printf "app time: %9.6f [sec]\n" (/ (- end start) 1000.0)))
(define (main args)
(with-handlers
([exn:fail? (λ (e) (eprintf "%s\n" (exn-message e)))]
[exn? (λ (e) (eprintf "Unexpected: %s\n" (exn-message e)))])
(cond
[(not (= (vector-length args) 1))
(error "Usage: append <positive integer>")]
[else
(let ([n (string->number (vector-ref args 0))])
(cond
[(not (integer? n))
(error "The argument must be an integer.")]
[(<= n 0)
(error "The argument must be a positive integer.")]
[else
(test n)]))])))
(main (current-command-line-arguments))
n = 10で測定する。
$ raco exe append.rkt
$ measure ./append 10
length = 10000
1 ---> 10000
10 times append: length = 10240000
app time: 1.323059 [sec]
======================================
Process exited with status: 0
total time: 2.131618 [sec]
mem size: 585648 [KB]
code size: 12443 [KB]
- consをn回繰り返し、長さnのリストlstを作る。
- リストlstの全要素についてmemberでlstへの帰属を調べる。
std::findで線形探索する。
member.cpp:
#include <algorithm>
#include <chrono>
#include <deque>
#include <iterator>
#include <print>
#include <stdexcept>
void test(long n) {
std::deque<long> lst, lst2;
// cons
auto start = std::chrono::high_resolution_clock::now();
for (long i = n; i >= 1l; --i)
lst.push_front(i);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::println("length = {}", n);
std::println("{} ---> {}", lst[0], lst[n - 1]);
std::println("cons time: {:9.6f} [sec]", duration.count());
// member
start = std::chrono::high_resolution_clock::now();
for (long i = 1l; i <= n; ++i)
if (std::find(lst.begin(), lst.end(), i) != lst.end())
;
else
std::println("{} is not found", i);
end = std::chrono::high_resolution_clock::now();
duration = end - start;
std::println("Searched {} times.", n);
std::println("mem time: {:9.6f} [sec]", duration.count());
}
int main(int argc, char *argv[]) {
try {
if (argc != 2) {
throw std::invalid_argument(
"Usage: member <positive integer>");
}
long n = std::stol(argv[1]);
if (n <= 0l) {
throw std::invalid_argument(
"The argument must be a positive integer.");
}
test(n);
} catch (const std::logic_error &e) {
std::println(stderr, "Logic error: {}", e.what());
} catch (const std::runtime_error &e) {
std::println(stderr, "Runtime error: {}", e.what());
} catch (const std::exception &e) {
std::println(stderr, "Another std::exception: {}", e.what());
} catch (...) {
std::println(stderr, "Unknown exception.");
}
return 0;
}
makefile:
CPPG = g++
CPPL = clang++
CPPFLAGS = -std=c++23 -O2
SRC = member.cpp
EXES = member_g member_l
all: $(EXES)
member_g: $(SRC)
$(CPPG) $(CPPFLAGS) -o $@ $(SRC)
member_l: $(SRC)
$(CPPL) $(CPPFLAGS) -o $@ $(SRC)
clean:
rm -f $(EXES)
n = 10000, 20000で測定する。
$ measure ./member_g 10000
length = 10000
1 ---> 10000
cons time: 0.000063 [sec]
Searched 10000 times.
mem time: 0.024780 [sec]
======================================
Process exited with status: 0
total time: 0.025962 [sec]
mem size: 4136 [KB]
code size: 128 [KB]
$ measure ./member_g 20000
length = 20000
1 ---> 20000
cons time: 0.000129 [sec]
Searched 20000 times.
mem time: 0.102776 [sec]
======================================
Process exited with status: 0
total time: 0.101685 [sec]
mem size: 4156 [KB]
code size: 128 [KB]
$ measure ./member_l 10000
length = 10000
1 ---> 10000
cons time: 0.000072 [sec]
Searched 10000 times.
mem time: 0.035165 [sec]
======================================
Process exited with status: 0
total time: 0.037181 [sec]
mem size: 3896 [KB]
code size: 102 [KB]
$ measure ./member_l 20000
length = 20000
1 ---> 20000
cons time: 0.000146 [sec]
Searched 20000 times.
mem time: 0.142752 [sec]
======================================
Process exited with status: 0
total time: 0.142037 [sec]
mem size: 4032 [KB]
code size: 102 [KB]
List.memで探索する。
member.ml:
open Printf
let rec push_front n lst =
if n = 0 then
lst
else
push_front (n - 1) (n :: lst)
let test n =
(* cons *)
let start = Unix.gettimeofday () in
let lst = push_front n [] in
let end_ = Unix.gettimeofday () in
printf "length = %d\n" n;
printf "%d ---> %d\n" (List.nth lst 0) (List.nth lst (n-1));
printf "cons time: %9.6f [sec]\n" (end_ -. start);
(* member *)
let start = Unix.gettimeofday () in
for i = 1 to n do
if (List.mem i lst) then
()
else
printf "%d is not found.\n" i
done;
let end_ = Unix.gettimeofday () in
printf "Searched %d times.\n" n;
printf "mem time: %9.6f [sec]\n" (end_ -. start)
let () =
try
if Array.length Sys.argv <> 2 then
raise (Invalid_argument
"Usage: member <positive integer>")
else
let n = int_of_string Sys.argv.(1) in
if n <= 0 then
raise (Invalid_argument
"The argument must be a positive integer.")
else
test n
with
| Invalid_argument msg ->
eprintf "Logic error: %s\n" msg
| Failure msg ->
eprintf "Runtime error: %s\n" msg
| exn ->
eprintf "Another exception: %s\n" (Printexc.to_string exn)
makefile:
OCAMLOPT = ocamlopt
OCAMLFLAGS = -O2
LIBPATH = -I +unix
LIBS = unix.cmxa
SRC = member.ml
EXES = member
all: $(EXES)
member: $(SRC)
$(OCAMLOPT) $(OCAMLFLAGS) $(LIBPATH) -o $@ $(LIBS) $(SRC)
clean:
rm -f $(EXES) *.o *.cmx *.cmi
n = 10000, 20000で測定する。
$ measure ./member 10000
length = 10000
1 ---> 10000
cons time: 0.000137 [sec]
Searched 10000 times.
mem time: 0.556304 [sec]
======================================
Process exited with status: 0
total time: 0.552474 [sec]
mem size: 3852 [KB]
code size: 3290 [KB]
$ measure ./member 20000
length = 20000
1 ---> 20000
cons time: 0.000279 [sec]
Searched 20000 times.
mem time: 2.207118 [sec]
======================================
Process exited with status: 0
total time: 2.204695 [sec]
mem size: 4064 [KB]
code size: 3290 [KB]
memberで探索する。
member.rkt:
#lang racket
(require iso-printf)
(define (push_front i lst)
(if (zero? i)
lst
(push_front (- i 1) (cons i lst))))
(define (test n)
; cons
(define start (current-inexact-milliseconds))
(define lst (push_front n '()))
(define end (current-inexact-milliseconds))
(printf "length = %d\n" n)
(printf "%d ---> %d\n" (list-ref lst 0) (list-ref lst (- n 1)))
(printf "cons time: %9.6f [sec]\n" (/ (- end start) 1000.0))
; member
(set! start (current-inexact-milliseconds))
(for ([i (range 1 (+ n 1))])
(if (member i lst)
(void)
(printf "%d is not found.\n" i)))
(set! end (current-inexact-milliseconds))
(printf "Searched %d times\n" n)
(printf "mem time: %9.6f [sec]\n" (/ (- end start) 1000.0)))
(define (main args)
(with-handlers
([exn:fail? (λ (e) (eprintf "%s\n" (exn-message e)))]
[exn? (λ (e) (eprintf "Unexpected: %s\n" (exn-message e)))])
(cond
[(not (= (vector-length args) 1))
(error "Usage: member <positive integer>")]
[else
(let ([n (string->number (vector-ref args 0))])
(cond
[(not (integer? n))
(error "The argument must be an integer.")]
[(<= n 0)
(error "The argument must be a positive integer.")]
[else
(test n)]))])))
(main (current-command-line-arguments))
n = 10000, 20000で測定する。
$ raco exe member.rkt
$ measure ./member 10000
length = 10000
1 ---> 10000
cons time: 0.000029 [sec]
Searched 10000 times
mem time: 1.786239 [sec]
======================================
Process exited with status: 0
total time: 2.268130 [sec]
mem size: 133908 [KB]
code size: 12444 [KB]
$ measure ./member 20000
length = 20000
1 ---> 20000
cons time: 0.000052 [sec]
Searched 20000 times
mem time: 7.158655 [sec]
======================================
Process exited with status: 0
total time: 7.804120 [sec]
mem size: 134048 [KB]
code size: 12444 [KB]
測定結果を表にまとめる。単位は秒[sec]。
測定項目 | GCC | Clang | OCaml | Racket |
---|---|---|---|---|
cons | 0.069 | 0.077 | 0.623 | 0.554 |
reverse | 0.091 | 0.108 | 0.666 | 0.748 |
append | 0.172 | 0.170 | 1.300 | 1.323 |
member(1) | 0.025 | 0.035 | 0.556 | 1.786 |
member(2) | 0.103 | 0.143 | 2.207 | 7.159 |
This document is licensed under the MIT License.
Copyright (C) 2024 Takeshi Umetani