У меня есть ряд списков событий. События всегда происходят в заданном порядке, но не каждое событие всегда происходит. Вот вход в качестве примера:
[[ do, re, fa, ti ],
[ do, re, mi ],
[ do, la, ti, za ],
[ mi, fa ],
[ re, so, za ]]
Входные значения не имеют никакого свойственного порядка. Они - на самом деле сообщения как "создание символьных ссылок" и "переиндексация поиска". Они отсортированы в отдельном списке, но нет никакого способа посмотреть на только 'fa' в первом списке и 'ми' во втором и определить, который прибывает перед другим.
Я хотел бы смочь взять тот вход и генерировать отсортированный список всех событий:
[ do, re, mi, fa, so, la, ti, za ]
или еще лучше, некоторая информация о каждом событии, как количество:
[ [do, 3], [re, 3], [mi, 2],
[fa, 2], [so, 1], [la, 1],
[ti, 1], [za, 2] ]
Существует ли название того, что я делаю? Существуют ли принятые алгоритмы? Я пишу это в Perl, если это имеет значение, но псевдокод сделает.
Я знаю, что, учитывая мой пример вводил, мне, вероятно, нельзя гарантировать "правильного" порядка. Но мой реальный вход имеет тонны больше точек данных, и я уверен, что с некоторым умом это будет 95%-е право (который является действительно всем, в чем я нуждаюсь). Я просто не хочу изобретать велосипед, если я не имею к.
Вы можете использовать tsort
для вывода разумного - хотя и не обязательно уникального - порядка сортировки (известного как топологический порядок) из наблюдаемого вами порядка. Возможно, вам будет интересно прочитать оригинальное использование tsort
, которое по структуре похоже на вашу проблему.
Обратите внимание, что tsort
требует ациклического графа. Применительно к вашему примеру это означает, что вы не могли видеть do, за которым следует re в одной последовательности, и re, за которым следует do в другой.
#! /usr/bin/perl
use warnings;
use strict;
use IPC::Open2;
sub tsort {
my($events) = @_;
my $pid = open2 my $out, my $in, "tsort";
foreach my $group (@$events) {
foreach my $i (0 .. $#$group - 1) {
print $in map "@$group[$i,$_]\n", $i+1 .. $#$group;
}
}
close $in or warn "$0: close: $!";
chomp(my @order = <$out>);
my %order = map +(shift @order => $_), 0 .. $#order;
wantarray ? %order : \%order;
}
Поскольку вы описали данные как разреженные, приведенный выше код предоставляет tsort
как можно больше информации о матрице смежности событий.
Имея эту информацию, вычислить гистограмму и отсортировать ее компоненты очень просто:
my $events = [ ... ];
my %order = tsort $events;
my %seen;
do { ++$seen{$_} for @$_ } for @$events;
my @counts;
foreach my $event (sort { $order{$a} <=> $order{$b} } keys %seen) {
push @counts => [ $event, $seen{$event} ];
print "[ $counts[-1][0], $counts[-1][1] ]\n";
}
Для входных данных, приведенных в вашем вопросе, выходной результат будет следующим
[ do, 3 ] [ la, 1 ] [ re, 3 ] [ so, 1 ] [ mi, 2 ] [ fa, 2 ] [ ti, 2 ] [ za, 2 ]
Это выглядит забавно, потому что мы знаем порядок сольфеджирования, но re и la несравнимы в частичном порядке, определенном $events
: мы знаем только, что они оба должны идти после do.
Если вам не нравится писать много кода, вы можете использовать утилиту командной строки unix tsort
:
$ tsort -
do re
re fa
fa ti
do re
re mi
do la
la ti
ti za
mi fa
re so
so za
Которая представляет собой список всех пар в вашем входном примере. В результате на выходе получается:
do
la
re
so
mi
fa
ti
za
что, в общем-то, то, что вам нужно.
Это идеальный кандидат для Merge Sort. Перейдите на страницу Википедии здесь для довольно хорошего представления алгоритма http://en.wikipedia.org/wiki/Merge_sort
То, что вы описали, на самом деле является подмножеством/малой модификацией сортировки слиянием. Вместо того, чтобы начинать с несортированного массива, у вас есть набор отсортированных массивов, которые вы хотите объединить вместе. Просто вызовите функцию "merge", как описано на странице википедии, для пар ваших массивов и результатов функции merge, пока не получите один массив (который будет отсортирован).
Чтобы настроить вывод так, как вам нужно, вам потребуется определить функцию сравнения, которая может возвращать, если одно событие меньше, равно или больше другого события. Затем, когда ваша функция слияния найдет два одинаковых события, вы можете объединить их в одно событие и вести подсчет для этого события.
Только что понял, что ваш вопрос сказал, что их порядок не предопределен, поэтому это может не относиться к делу.
Perl-код:
$list = [
['do', 're', 'fa', 'ti' ],
['do', 're', 'mi' ],
['do', 'la', 'ti', 'za' ],
['mi', 'fa' ],
['re', 'so', 'za' ]
];
%sid = map{($_,$n++)}qw/do re mi fa so la ti za/;
map{map{$k{$_}++}@$_}@$list;
push @$result,[$_,$k{$_}] for sort{$sid{$a}<=>$sid{$b}}keys%k;
print "[@$_]\n" for(@$result);
вывод:
[do 3]
[re 3]
[mi 2]
[fa 2]
[so 1]
[la 1]
[ti 2]
[za 2]
Я тоже не совсем уверен, как это будет называться, но я придумал способ найти порядок, задав массив массивов в качестве входных данных. По сути, псевдокод выглядит так:
10 Найти самый ранний элемент во всех массивах
20 Поместить его в список
30 Удалить этот элемент из всех массивов
40 Goto 10 если остались элементы
Вот рабочий прототип:
#!/usr/bin/perl
use strict;
sub InList {
my ($x, @list) = @_;
for (@list) {
return 1 if $x eq $_;
}
return 0;
}
sub Earliest {
my @lists = @_;
my $earliest;
for (@lists) {
if (@$_) {
if (!$earliest
|| ($_->[0] ne $earliest && InList($earliest, @$_))) {
$earliest = $_->[0];
}
}
}
return $earliest;
}
sub Remove {
my ($x, @lists) = @_;
for (@lists) {
my $n = 0;
while ($n < @$_) {
if ($_->[$n] eq $x) {
splice(@$_,$n,1);
}
else {
$n++
}
}
}
}
my $list = [
[ 'do', 're', 'fa', 'ti' ],
[ 'do', 're', 'mi' ],
[ 'do', 'la', 'ti', 'za' ],
[ 'mi', 'fa' ],
[ 're', 'so', 'za' ]
];
my @items;
while (my $earliest = Earliest(@$list)) {
push @items, $earliest;
Remove($earliest, @$list);
}
print join(',', @items);
Выходные данные:
do,re,mi,fa,la,ti,so,za
Это решает исходный вопрос до того, как он был изменен задавшим его.
#!/usr/local/bin/perl -w
use strict;
main();
sub main{
# Changed your 3-dimensional array to a 2-dimensional array
my @old = (
[ 'do', 're', 'fa', 'ti' ],
[ 'do', 're', 'mi' ],
[ 'do', 'la', 'ti', 'za' ],
[ 'mi', 'fa' ],
[ 're', 'so', 'za' ]
);
my %new;
foreach my $row (0.. $#old ){ # loop through each record (row)
foreach my $col (0..$#{$old[$row]} ){ # loop through each element (col)
$new{ ${$old[$row]}[$col] }{count}++;
push @{ $new{${$old[$row]}[$col]}{position} } , [$row,$col];
}
}
foreach my $key (sort keys %new){
print "$key : $new{$key} " , "\n"; # notice each value is a hash that we use for properties
}
}
local $" = ', '; # pretty print ($") of array in quotes
print $new{za}{count} , "\n"; # 2 - how many there were
print "@{$new{za}{position}[1]} \n"; # 4,2 - position of the second occurrence
# remember it starts at 0
По сути, мы создаем уникальный список элементов в хэше. Для каждого из этих элементов у нас есть хэш "property", который содержит скаляр count
и массив для position
. Количество элементов в массиве должно меняться в зависимости от того, сколько вхождений элемента было в оригинале.
Свойство scalar на самом деле не нужно, поскольку вы всегда можете взять скаляр массива position
для получения того же числа. Примечание: если вы будете добавлять/удалять элементы из массива count
и position
не будут коррелировать по значению.
print scalar @{$new{za}{position}};
даст вам то же самое, что и print $new{za}{count};
Теоретически позвольте мне предложить следующий алгоритм:
PS
Это только при условии, что все события происходят в определенном порядке (всегда!). Если это не так, проблема становится NP-Complete.
PPS
И просто для того, чтобы у вас было что-то полезное: Sort :: Topological (не знаю, действительно ли это работает, но кажется правильным)
perl -de 0
DB<1> @a = ( ['a','b','c'], ['c','f'], ['h'] )
DB<2> map { @m{@{$_}} = @$_ } @a
DB<3> p keys %m
chabf
Самый быстрый способ, который я могу придумать. В любом случае, вы должны пройтись по всем элементам хотя бы один раз...
Грубо говоря, я бы назвал это "хэшированием". Вы помещаете вещи в пары "имя-значение". Если вы хотите сохранить некое подобие порядка, вам нужно дополнить хэш массивом, который сохраняет порядок. Для меня этот порядок - "порядок встреч".
use strict;
use warnings;
my $all
= [[ 'do', 're', 'fa', 'ti' ],
[ 'do', 're', 'mi' ],
[ 'do', 'la', 'ti', 'za' ],
[ 'mi', 'fa' ],
[ 're', 'so', 'za' ]
];
my ( @order, %counts );
foreach my $list ( @$all ) {
foreach my $item ( @$list ) {
my $ref = \$counts{$item}; # autovivs to an *assignable* scalar.
push @order, $item unless $$ref;
$$ref++;
}
}
foreach my $key ( @order ) {
print "$key: $counts{$key}\n";
}
# do: 3
# re: 3
# fa: 2
# ti: 2
# mi: 2
# la: 1
# za: 2
# so: 1
Есть и другие ответы, подобные этому, но мой содержит этот аккуратный трюк автовивификации.