Как сортировать массив римских цифр?

у меня есть массив, содержащий римские цифры (как строки, конечно). Вот так:

 $a = array('XIX', 'LII', 'V', 'MCCXCIV', 'III', 'XIII');

Я хочу сортировать в соответствии с числовыми значениями этих цифр, поэтому результаты должны быть примерно такими:

 $sorted_a = array('III', 'V', 'XIII', 'XIX', 'LII', 'MCCXCIV');

Итак, мой вопрос: каков наилучший способ сортировки массива римских цифр? Я знаю, как использовать функции сортировки массива PHP, меня интересует логика, которая происходит внутри функция сравнения.

редактировать: для простоты я ищу только способ, который имеет дело со строками, построенными из основных цифр стандартным способом (нет CCCC например):

I, V, X, L, C, D, M

РЕЗУЛЬТАТЫ ИСПЫТАНИЙ

Я потратил время, чтобы тщательно протестировать все примеры кода, которые были опубликованы. Было проведено два теста: один со случайным массивом из 20 римских цифр и второй с массивом, содержащим 4000 из них. Тот же машина, много итераций, среднее время, и все это выполняется несколько раз. конечно, это ничего официального, просто мои собственные тесты.

ТЕСТ С 20 ЦИФРАМИ:

  1. hakre, bazmegakapa - около 0.0005 s
  2. anemgyenge, Андреа, Дирк McQuickly - около 0.0010 s
  3. Джо Нельсон - вокруг 0.0050 s
  4. Роб Хруска - около 0.0100 s

ТЕСТ С 4000 ЦИФРАМИ:

  1. hakre, bazmegakapa - около 0.13 s
  2. anemgyenge - вокруг 1.4 s
  3. Дирк McQuickly, Андреа - вокруг 1.8 s
  4. Роб Хруска - около 2.8 s
  5. Джо Нельсон - около 15 s (удивление, проверено еще несколько раз)

мне трудно присуждать награду. мы с хакром сделали самые быстрые версии, следуя по тому же маршруту, но он сделал вариацию моей, которая ранее была основана на идее borrible. Поэтому я приму решение хакре, потому что это самое быстрое и приятное, чем мое (ИМО). Но я присужду награду анемгиенге, потому что мне нравится его версия, и, кажется, в нее вложено много усилий.

10 ответов


комплектации ваш класс для преобразования римских чисел в целые, определяемый пользователем обратный вызов сортировки может обрабатывать это для сортировки массива:

$a = array('XIX', 'LII', 'V', 'MCCXCIV', 'III', 'XIII');

$bool = usort($a, function($a, $b) {
    return RomanNumber::Roman2Int($a) - RomanNumber::Roman2Int($b);
});    
var_dump($a);

Итак, здесь вы найдете логику внутри функции сравнения: если оба значения имеют одинаковый вес, верните 0. Если первый меньше второго, верните < 0 (например,-1), в противном случае второй больше, чем первый, так что вернуть > 0 (например,1).

Естественно, любой другой тип функция, возвращающая десятичное значение для римского числа, также будет работать.

Edit:

как вы прокомментировали, вы не хотите выполнить преобразование для каждой пары. Это нормально, с помощью дополнительного массива, который содержит все преобразованные значения, вы можете запустить сортировку по десятичным значениям и использовать эту сортировку на римских числах (демо):

$a = array('XIX', 'LII', 'V', 'MCCXCIV', 'III', 'XIII');
$b = array_map('RomanNumber::Roman2Int', $a);
array_multisort($b, $a);
var_dump($a);

array_multisort в PHP Руководство делает большую часть магии.


function sortRomanNum($a, $b) {
    if($a == $b) return 0;

    $str = "0IVXLCDM";
    $len = 0;

    if(strlen($a) >= strlen($b)) {
        $len = strlen($a);
        $b .= str_repeat("0", $len - strlen($b));
    }
    else {
        $len = strlen($b);
        $a .= str_repeat("0", $len - strlen($a));
    }

    for($i = 0; $i < $len - 1; $i++) {
        $a1 = $a[$i]; $b1 = $b[$i]; $a2 = $a[$i+1]; $b2 = $b[$i+1];

        if( strpos($str, $a1.$b1.$a2) !== false ) return 1;
        if( strpos($str, $b1.$a1.$b2) !== false ) return -1;

        if($a1 != $b1) return strpos($str, $a1) > strpos($str, $b1) ? 1 : -1;
    }

    if($a[$i] != $b[$i]) return strpos($str, $a[$i]) > strpos($str, $b[$i]) ? 1 : -1;
}

учитывая два числа (римские строки), $a и $b. Если в числах нет подстрок (IV, IX, XC и т. д.), то решение будет тривиальным:

for all $i in $a and $b
    if $a[$i] > $b[$i] then return 1; //($a is greater then $b)
    if $a[$i] < $b[$i] then return 1; //($a is lower then $b)
return 0 //equality

поскольку могут быть эти специальные части, расчет является более сложным. Но решение найти модели:

a: IX | XC | CM
b: V  | L  | D

Это единственные шаблоны, которые могут испортить тривиальное решение. Если вы найдете любой из них, то $a будет больше $b.

обратите внимание, что Роман числа не включают нули, как арабские. Поэтому теперь мы будем использовать их (и в основном положить нули, где они отсутствуют).

Итак, вот функция:

if $a == $b then return 0; //equality
create a string for ordering the roman numerals (strpos will give the right index)
define the length of the loop (take the longer string), and add zeros to the end of the shorter number
run the loop, and check:
    1. if the patterns above are found, return the comparision accordingly (1 or -1)
    2. otherwise do the trivial check (compare each numeral)
check the last numerals too.

некоторые люди предложили преобразовать римские цифры в целые числа, сортировку и сопоставление. Есть более простой способ. Все, что нам действительно нужно сделать, это сравнить любые два произвольных римских числа и пусть usort делать все остальное. Вот код, и я объясню его дизайн ниже.

$base = array( 'I' => 0, 'V' => 1, 'X' => 2, 'L' => 3,
               'C' => 4, 'D' => 5, 'M' => 6 ); 
function single($a) { global $base; return $base[$a]; }

function compare($a, $b) {
    global $base;
    if(strlen($a) == 0) { return true; }
    if(strlen($b) == 0) { return false; }
    $maxa = max(array_map('single', str_split($a)));
    $maxb = max(array_map('single', str_split($b)));
    if($maxa != $maxb) {
        return $maxa < $maxb;
    }
    if($base[$a[0]] != $base[$b[0]]) {
        return $base[$a[0]] < $base[$b[0]];
    }
    return compare(substr($a, 1), substr($b, 1));
}

$a = array('XIX', 'LII', 'V', 'MCCXCIV', 'III', 'XIII');
usort($a, compare);
print_r($a);

Сначала мы создаем массив поиска, чтобы назначить "величину" однозначным римским цифрам. Обратите внимание, что это не их десятичное значение, а просто числа, назначенные таким образом, что большие цифры получите большие значения. Затем мы создаем вспомогательную функцию single используется некоторыми функциями PHP для извлечения значений.

хорошо, теперь к мясу алгоритма. Это compare функция, которая иногда должна вызывать себя рекурсивно, когда ей нужно разорвать связь. По этой причине мы начинаем с некоторых тестов, чтобы увидеть, достиг ли он терминальных состояний в рекурсии. Не обращайте на это внимания и посмотрите на первый интересный тест. Он проверяет, имеет ли сравниваемое число цифра в нем, которая затмевает любые другие цифры. Например, если один из них был X в нем, а другой только I и V, потом X выигрывает. Это основывается на том, что некоторые римские цифры недействительны, например VV или VIIIII или IIIIIIIII. По крайней мере, я никогда не видел, чтобы они так писались, поэтому считаю их недействительными.

чтобы сделать эту проверку, мы сопоставляем цифры с величинами и сравниваем максимумы. Ну, этот тест может не решить вопрос. В этом случае безопасно сравнивать первые цифры каждого числа, так как нам не придется иметь дело с запутанными проблемами, такими как V < IX где первые цифры не указывают на истину. Эти запутанные ситуации были устранены путем сравнения самых больших цифр.

наконец, если первые цифры равны, снимите их и повторите. В какой-то момент одна из цифр будет сведена к пустой строке, и те начальные тесты, которые мы временно игнорировали, позаботятся о что.

этот метод прошел все тесты, которые я бросил на него, но дайте мне знать, если вы найдете ошибку или оптимизации.


существует три подхода, а именно:

  • преобразование чисел, сортировка с помощью стандартной целочисленной сортировки и преобразование обратно. (Или сохранить преобразованные версии с римскими цифрами и отсортировать структуры, чтобы избежать двойного преобразования.)
  • напишите функцию сортировки, которая принимает строки, в этот момент вызывает функцию преобразования и выполняет соответствующее сравнение.
  • напишите функцию сортировки, которая может напрямую сравнивать римские цифры, без необходимости полного обращения. Поскольку римские цифры сначала имеют более высокие компоненты (Ms, затем D / Cs. затем L/Xs, затем I / Vs) такая функция может быть способна к короткому замыканию раньше.

первый, очевидно, будет включать дополнительные накладные расходы для хранения. Второй будет включать дополнительные затраты на преобразование (так как одно и то же число может быть преобразовано много раз). Третий может включать некоторые ненужные накладные расходы на преобразование (опять же, тот же номер может быть преобразован несколько раз), но сохраните некоторые работы по короткому замыканию. Если накладные расходы на хранение не являются проблемой, первый, вероятно, будет лучшим.


Я очень заинтересовался @borrible в 1-й подход, поэтому я решил попробовать:

function sortRomanArray($array) {
     $combined=array_combine($array, array_map('roman2int', $array));
     asort($combined);
     return array_keys($combined);
}

это в основном преобразует все римские цифры в массиве в целые числа, используя array_map() и функция под названием roman2int() (которая может быть любой реализации). Затем он создает массив, где ключи являются римскими цифрами, а значения-целыми числами. Затем этот массив сортируется с помощью asort() это сохраняет ассоциации ключей,и ключи возвращаются в виде массива. Этот массив будет содержать отсортированные римские цифры.

мне нравится этот метод, потому что он запускает функцию преобразования только столько раз, сколько размер массива (6 с моим примером массива), и нет необходимости конвертировать обратно.

преобразование будет работать, конечно, гораздо больше, если мы ставим его в функцию сравнения (2 раза для каждого сравнения).


Я думаю, вам придется либо:

  1. оберните строки в RomanNumeral класс, который имеет метод сортировки или
  2. напишите метод для вычисления значения каждого элемента в массиве и отсортируйте по этому
  3. посмотрите, если кто - то уже написал RomanNumeral class/library, который делает это-что-то такой

в любом случае вам понадобится пользовательский код сортировки, который где-то вычисляет значение. Так начинаются символы в римских цифрах иногда могут означать "вычесть это значение", а не"добавить это значение". Это нормально, потому что, как вы указали, на самом деле вы сортируете по числовому значению, поэтому вам придется сказать компьютеру, как интерпретировать значение.


  1. преобразуйте цифру в десятичную с помощью этой
  2. сравните десятичные дроби

    function roman2dec($roman) {
        // see link above
    }
    
    function compare($a, $b) {
        return roman2dec($a) < $roman2dec($b) ? -1 : 1;
    }
    

самое простое решение, вероятно, сначала преобразовать каждую цифру в регулярное целое число (в новом массиве), а затем отсортировать оба массива на основе массива integer. Не уверен, что PHP содержит функцию для этого. Кроме того, можно определить функцию сравнения, которая преобразует два римских числа в целые и сравнивает их. Написание функции, которая напрямую сравнивает два римских числа без преобразования их в целые числа, вероятно, будет громоздким.


предположим, вы делаете этот "алфавит": I, IV, V, IX, X, XL, L, XC, C, CD, D, CM, M. Затем вы можете отсортировать римские цифры в соответствии с этим "алфавитом".

возможно, это даст кому-то новые вдохновения.

EDIT: получил рабочий пример. Не очень быстро, сортирует 1000 римских чисел за 1,3 секунды

EDIT 2: добавлена проверка, чтобы избежать "уведомлений", также немного оптимизирован код, работает немного быстрее и примерно в два раза быстрее, чем при преобразовании в целое число и чем сортировать это (используется пакет PEAR Number_Roman)

function sortromans($a, $b){
    $alphabet = array('M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I');
    $pos = 0;
    if ($a == $b) {
        return 0;
    }

    //compare the strings, position by position, as long as they are equal
    while(isset($a[$pos]) && isset($b[$pos]) && $a[$pos] === $b[$pos]){
        $pos++;
    }

    //if string is shorter than $pos, return value
    if(!isset($a[$pos])){
        return -1;
    } else if(!isset($b[$pos])){
        return 1;
    } else {

      //check the ´character´ at position $pos, and pass the array index to a variable
      foreach($alphabet as $i=>$ch){
            if(isset($a_index) && isset($b_index)){
         break;
        }
        $length = strlen($ch);
        if(!isset($a_index) && substr($a, $pos, $length) === $ch){
            $a_index = $i;
        }
        if(!isset($b_index) && substr($b, $pos, $length) === $ch){
            $b_index = $i;
        }
      }

    }

    return ($a_index > $b_index) ? -1 : 1;
}

$romans = array('III', 'IX', 'I', 'CM', 'LXII','IV');

usort($romans, "sortromans");

echo "<pre>";
print_r($romans);
echo "</pre>";

Я думаю лучшие (см. Мой комментарий) первое решение-использовать стандартную функцию USORT PHP с помощью специальной Римской функции сравнения.

следующее roman_compare функция очень интуитивно понятна и не использует никакого преобразования. Чтобы сохранить его простым, он использует хвостовую рекурсию.

function roman_start( $a )
{
    static $romans = array(
        'I'  => 1,    'V'  => 5,
        'X'  => 10,   'L'  => 50,
        'C'  => 100,  'D'  => 500,
        'M'  => 1000,
    );
    return $a[0] . ($romans[$a[0]] < $romans[$a[1]] ? $a[1] : '');
}

function roman_compare( $a, $b )
{
    static $romans = array(
        'I'  => 1,    'IV' => 4,   'V'  => 5,   'IX' => 9,
        'X'  => 10,   'XL' => 40,  'L'  => 50,  'XC' => 90,
        'C'  => 100,  'CD' => 400, 'D'  => 500, 'CM' => 900,
        'M'  => 1000,
    );
    $blockA = roman_start($a);
    $blockB = roman_start($b);
    if ($blockA != $blockB)
    {
        return $romans[$blockA] - $romans[$blockB];    
    }
    $compared = strlen($blockA);
    if (strlen($a) == $compared) //string ended
    {
        return 0;
    }
    return roman_compare(substr($a, $compared), substr($b, $compared));
}

используя вышеуказанные функции, мы можем написать

function array_equal( $a, $b )
{
    return count(array_diff_assoc($a, $b)) == 0 && count(array_diff_assoc($b, $a)) == 0;
}

$a        = array('XIX', 'LII', 'V', 'MCCXCIV', 'III', 'XIII');
$sorted_a = array('III', 'V', 'XIII', 'XIX', 'LII', 'MCCXCIV');

var_dump(array_equal($sorted_a, $a));
usort($a, 'roman_compare');
var_dump(array_equal($sorted_a, $a));

запуск всего вышеуказанного кода мы получаем

bool(false)
bool(true)