10. 優先度付きキュー
著者:梅谷 武
各言語において優先度付きキューを実装し、比較する。
作成:2024-06-28
更新:2024-09-21
更新:2024-09-21
優先度付きキュー(Priority Queue)は各データに優先度を持たせ、その優先度に基づいてデータを取り出すデータ構造である。通常のキューFIFO: First In, First Out)とは異なり、優先度付きキューでは最も高い優先度を持つデータが優先して取り出される。
Racketの可変優先度付きキュー:data/heapを使って簡単な例を示す。
- 空キュー生成: (make-heap 比較関数)
- enqueue: (heap-add! pq value)
- リスト挿入: (heap-add-all! pq リスト)
- dequeue: (heap-remove-min! pq)
- 先頭参照: (heap-min pq)
- 配列変換: (heap->vector pq)
- キュー長: (heap-count pq)
- ヒープソート: (heap-sort! 配列 比較関数)
> (require data/heap)
> (define pq (make-heap <=))
> (heap-add-all! pq '(21 1 7 3 9 1 11 21 15 31))
> (heap-count pq)
10
> (heap->vector pq)
'#(1 1 3 7 9 11 15 21 21 31)
> (heap-min pq)
1
> (heap->vector pq)
'#(1 1 3 7 9 11 15 21 21 31)
> (heap-remove-min! pq)
> (heap->vector pq)
'#(1 3 7 9 11 15 21 21 31)
std::priority_queueを用いる。
OCamlの標準ライブラリには優先度付きキューあるいは二分ヒープ(binary heap)が含まれていないので次節で可変配列を使って可変二分ヒープを実装する。
Racketには標準でBinary Heapsという優先度付きキューが付属している。
pqueue.ml:
module PriorityQueue = struct
type 'a t = {
mutable heap: 'a array;
mutable size: int;
cmp: 'a -> 'a -> int;
}
let create cmp init_size init_value = {
heap = Array.make init_size init_value;
size = 0;
cmp
}
let parent i = (i - 1) / 2
let left i = 2 * i + 1
let right i = 2 * i + 2
let swap heap i j =
let temp = heap.(i) in
heap.(i) <- heap.(j);
heap.(j) <- temp
let insert pq x =
if pq.size = Array.length pq.heap then
pq.heap <- Array.append pq.heap (Array.make pq.size pq.heap.(0));
pq.heap.(pq.size) <- x;
let rec heapify_up i =
let p = parent i in
if i > 0 && pq.cmp pq.heap.(i) pq.heap.(p) < 0 then begin
swap pq.heap i p;
heapify_up p
end
in
heapify_up pq.size;
pq.size <- pq.size + 1
let extract_top pq =
if pq.size = 0 then failwith "PriorityQueue is empty";
let top = pq.heap.(0) in
pq.size <- pq.size - 1;
pq.heap.(0) <- pq.heap.(pq.size);
let rec heapify_down i =
let l = left i and r = right i in
let smallest = ref i in
if l < pq.size && pq.cmp pq.heap.(l) pq.heap.(!smallest) < 0 then
smallest := l;
if r < pq.size && pq.cmp pq.heap.(r) pq.heap.(!smallest) < 0 then
smallest := r;
if !smallest <> i then begin
swap pq.heap i !smallest;
heapify_down !smallest
end
in
heapify_down 0;
top
let rec heap_to_list pq acc =
if pq.size = 0 then
List.rev acc
else
let top = extract_top pq in
heap_to_list pq (top :: acc)
end
入力ファイルを入力配列:arrに読み込み、それから二分ヒープ:pqueを生成し、優先度順に出力配列:arr2に書き込み、それを出力ファイルに書き出す。
main.cpp:
#include <chrono>
#include <fstream>
#include <iostream>
#include <print>
#include <queue>
#include <sstream>
#include <stdexcept>
#include <vector>
bool test(const std::string &infile, const std::string &outfile) {
std::vector<long> arr;
// read file
auto start = std::chrono::high_resolution_clock::now();
std::ifstream fin(infile);
if (!fin) {
throw std::runtime_error("Could not open file for reading.");
return false;
}
int num;
while (fin >> num)
arr.push_back(num);
fin.close();
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::println("length = {}", arr.size());
std::println("read time: {:9.6f} [sec]", duration.count());
// make pqueue
start = std::chrono::high_resolution_clock::now();
std::priority_queue<long, std::vector<long>, std::greater<long>> pque;
for (long i : arr)
pque.push(i);
end = std::chrono::high_resolution_clock::now();
duration = end - start;
std::println("priority queue size = {}", pque.size());
std::println("make pque: {:9.6f} [sec]", duration.count());
// to vector
start = std::chrono::high_resolution_clock::now();
std::vector<long> arr2;
while (!pque.empty()) {
arr2.push_back(pque.top());
pque.pop();
}
end = std::chrono::high_resolution_clock::now();
duration = end - start;
std::println("length = {}", arr2.size());
std::println("to vector: {:9.6f} [sec]", duration.count());
// write file
start = std::chrono::high_resolution_clock::now();
std::ofstream fout(outfile);
if (!fout) {
throw std::runtime_error("Could not open file for writing.");
return false;
}
std::ostringstream buf;
for (long num : arr2)
buf << num << "\n";
fout << buf.str();
fout.close();
end = std::chrono::high_resolution_clock::now();
duration = end - start;
std::println("write time: {:9.6f} [sec]", duration.count());
return true;
}
int main(int argc, char *argv[]) {
try {
if (argc != 3) {
throw std::invalid_argument(
"Usage: main <infile> <outfile>");
}
std::string infile(argv[1]);
std::string outfile(argv[2]);
if (!test(infile, outfile))
std::println("An error has occurred.");
} 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 = main.cpp
EXES = main_g main_l
all: $(EXES)
main_g: $(SRC)
$(CPPG) $(CPPFLAGS) -o $@ $(SRC)
main_l: $(SRC)
$(CPPL) $(CPPFLAGS) -o $@ $(SRC)
clean:
rm -f $(EXES)
基準データ:uniform1m.txtで測定する。
$ measure ./main_g uniform1m.txt sorted_g.txt
length = 1000000
read time: 0.094745 [sec]
priority queue size = 1000000
make pque: 0.026717 [sec]
length = 1000000
to vector: 0.221406 [sec]
write time: 0.092484 [sec]
======================================
Process exited with status: 0
total time: 0.438419 [sec]
mem size: 45516 [KB]
code size: 138 [KB]
$ measure ./main_l uniform1m.txt sorted_l.txt
length = 1000000
read time: 0.095377 [sec]
priority queue size = 1000000
make pque: 0.028486 [sec]
length = 1000000
to vector: 0.194962 [sec]
write time: 0.090033 [sec]
======================================
Process exited with status: 0
total time: 0.411832 [sec]
mem size: 46392 [KB]
code size: 107 [KB]
データ格納には配列の代わりにリストを使う。
main.ml:
open Printf
module PQ = Pqueue.PriorityQueue
let read_data filename =
let ichan = open_in filename in
let rec read_lines acc =
match input_line ichan with
| line ->
let num = int_of_string line in
read_lines (num :: acc)
| exception End_of_file ->
close_in ichan;
List.rev acc in
read_lines []
let write_data filename lst =
let ochan = open_out filename in
List.iter (fun x -> fprintf ochan "%d\n" x) lst;
close_out ochan
let test infile outfile =
(* read file *)
let start = Unix.gettimeofday () in
let lst = read_data infile in
let end_ = Unix.gettimeofday () in
let len = (List.length lst) in
printf "length = %d\n" len;
printf "read time: %9.6f [sec]\n" (end_ -. start);
(* make pqueue *)
let start = Unix.gettimeofday () in
let pque = PQ.create compare len 0 in
List.iter (fun x -> PQ.insert pque x) lst;
let end_ = Unix.gettimeofday () in
printf "priority queue size = %d\n" (pque.size);
printf "make pque: %9.6f [sec]\n" (end_ -. start);
(* to list *)
let start = Unix.gettimeofday () in
let lst2 = PQ.heap_to_list pque [] in
let end_ = Unix.gettimeofday () in
printf "length = %d\n" (List.length lst2);
printf "to list: %9.6f [sec]\n" (end_ -. start);
(* write file *)
let start = Unix.gettimeofday () in
write_data outfile lst2;
let end_ = Unix.gettimeofday () in
printf "write time: %9.6f [sec]\n" (end_ -. start)
let () =
try
if Array.length Sys.argv <> 3 then
raise (Invalid_argument
"Usage: main <infile> <outfile>")
else
let infile = Sys.argv.(1) in
let outfile = Sys.argv.(2) in
test infile outfile
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
SRCS = pqueue.ml main.ml
EXES = main
all: $(EXES)
main: $(SRCS)
$(OCAMLOPT) $(OCAMLFLAGS) $(LIBPATH) -o $@ $(LIBS) $(SRCS)
clean:
rm -f $(EXES) *.o *.cmx *.cmi
基準データ:uniform1m.txtで測定する。
$ measure ./main uniform1m.txt sorted.txt
length = 1000000
read time: 0.298860 [sec]
priority queue size = 1000000
make pque: 0.096129 [sec]
length = 1000000
to list: 0.827671 [sec]
write time: 0.226117 [sec]
======================================
Process exited with status: 0
total time: 1.463985 [sec]
mem size: 72612 [KB]
code size: 3301 [KB]
データ格納には配列の代わりにリストを使う。
pqueue.rkt:
#lang racket
(require iso-printf)
(require data/heap)
(define (read-data filename)
(call-with-input-file filename
(lambda (port)
(let loop ([lst '()])
(let ([line (read-line port)])
(if (eof-object? line)
(reverse lst)
(loop (cons (string->number line) lst))))))))
(define (write-data filename lst)
(call-with-output-file filename #:exists 'replace
(lambda (port)
(for-each (lambda (num) (displayln num port))
lst))))
(define (test infile outfile)
; read file
(define start (current-inexact-milliseconds))
(define lst (read-data infile))
(define end (current-inexact-milliseconds))
(printf "length = %d\n" (length lst))
(printf "read time: %9.6f [sec]\n" (/ (- end start) 1000.0))
; make pqueue
(set! start (current-inexact-milliseconds))
(define pque (make-heap <=))
(heap-add-all! pque lst)
(set! end (current-inexact-milliseconds))
(printf "priority queue size = %d\n" (heap-count pque))
(printf "make pque: %9.6f [sec]\n" (/ (- end start) 1000.0))
; to list
(set! start (current-inexact-milliseconds))
(define lst2 (vector->list (heap->vector pque)))
(set! end (current-inexact-milliseconds))
(printf "length = %d\n" (length lst2))
(printf "to list: %9.6f [sec]\n" (/ (- end start) 1000.0))
; write file
(set! start (current-inexact-milliseconds))
(write-data outfile lst2)
(set! end (current-inexact-milliseconds))
(printf "write 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) 2))
(error "Usage: main <infile> <outfile>")]
[else
(let ([infile (vector-ref args 0)]
[outfile (vector-ref args 1)])
(test infile outfile))])))
(main (current-command-line-arguments))
基準データ:uniform1m.txtで測定する。
$ raco exe pqueue.rkt
$ measure ./pqueue uniform1m.txt sorted.txt
length = 1000000
read time: 0.802043 [sec]
priority queue size = 1000000
make pque: 0.118830 [sec]
length = 1000000
to list: 0.585107 [sec]
write time: 0.738563 [sec]
======================================
Process exited with status: 0
total time: 2.730633 [sec]
mem size: 171040 [KB]
code size: 12514 [KB]
測定結果を表にまとめる。単位は秒[sec]。
測定項目 | GCC | Clang | OCaml | Racket |
---|---|---|---|---|
push | 0.027 | 0.028 | 0.096 | 0.119 |
pop | 0.221 | 0.195 | 0.828 | 0.585 |
This document is licensed under the MIT License.
Copyright (C) 2024 Takeshi Umetani