我有一个包含Roman numerals 的数组(当然是字符串)。像这样:
$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个数字:
使用4000个数字进行测试:
我很难获得赏金。 hakre和我制作了最快的版本,遵循相同的路线,但他做了我的变种,这是以前基于可怕的想法。所以我会接受hakre的解决方案,因为这比我的(IMO)最快,更好。但我会将赏金奖励给他,因为我喜欢他的版本,并且似乎付出了很多努力。
答案 0 :(得分:26)
Picking your class to convert roman numbers to integers,用户定义的排序回调可以处理这个以对数组进行排序:
$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
)。
当然,任何其他类型的函数都可以返回罗马数字的十进制值。
修改强>
正如您所评论的那样,您不希望为每对运行转换。没关系,在包含所有转换值的附加数组的帮助下,您可以对小数值运行排序,并对罗马数字进行排序(Demo):
$a = array('XIX', 'LII', 'V', 'MCCXCIV', 'III', 'XIII');
$b = array_map('RomanNumber::Roman2Int', $a);
array_multisort($b, $a);
var_dump($a);
array_multisort
PHP Manual在这里完成了大部分的魔法。
答案 1 :(得分:10)
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.
答案 2 :(得分:4)
有些人建议将罗马数字转换为整数,排序和映射。有一种更简单的方法。我们真正需要做的就是比较任意两个任意罗马数字,让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
之类的混淆问题,其中第一个数字不表示真相。通过比较最大数字来处理这些令人困惑的情况。
最后,如果第一个数字相等,则将其剥离并重复。在某些时候,其中一个数字将被缩减为空字符串,而我们暂时无视的初始测试将会处理这些问题。
此方法已通过我投入的所有测试,但如果您发现错误或优化,请告诉我。
答案 3 :(得分:2)
似乎有三种方法,即:
第一个显然会涉及额外的存储开销。第二个将涉及额外的转换开销(因为相同的数字可能被转换多次)。第三个可能涉及一些不必要的转换开销(同样,相同的数字可能会被转换几次),但节省了一些短路工作。如果存储开销不是问题,那么第一个可能是最好的。
答案 4 :(得分:2)
我对@borrible's 1st approach很感兴趣,所以我决定尝试一下:
function sortRomanArray($array) {
$combined=array_combine($array, array_map('roman2int', $array));
asort($combined);
return array_keys($combined);
}
这基本上使用array_map()
和一个名为roman2int()
的函数(可以是任何实现)将数组中的所有罗马数字转换为整数。然后它创建一个数组,其中键是罗马数字,值是整数。然后,此数组使用asort()
进行排序,以保留键关联,并将键作为数组返回。该数组将包含已排序的罗马数字。
我喜欢这种方法,因为它只运行转换函数的次数与数组的大小一样多(我的示例数组为6),并且无需转换回来。
如果我们将它放在比较函数中(每次比较2次),转换肯定会更多。
答案 5 :(得分:1)
我认为你必须要么:
无论哪种方式,您都需要自定义排序代码来计算某处的值。因为罗马数字中的字符前缀有时可能意味着“减去此值”而不是“添加此值”。这很好,因为正如你所指出的,你真正在做的是按数值排序,所以你必须告诉计算机如何解释这个值。
答案 6 :(得分:1)
比较小数
function roman2dec($roman) {
// see link above
}
function compare($a, $b) {
return roman2dec($a) < $roman2dec($b) ? -1 : 1;
}
答案 7 :(得分:0)
最简单的解决方案可能是首先将每个数字转换为常规整数(在新数组中),然后根据整数数组对两个数组进行排序。不过,不确定PHP是否包含一个函数。或者,您可以定义一个比较函数,将两个罗马数字转换为整数并进行比较。编写一个直接比较两个罗马数字而不将它们首先转换为整数的函数可能会很麻烦。
答案 8 :(得分:0)
假设您使用“字母”:I,IV,V,IX,X,XL,L,XC,C,CD,D,CM,M。 然后你可以根据这个'字母'对罗马数字进行排序。
也许这会给别人带来新的灵感。
编辑:得到了一个有效的例子。不是很快,在1.3秒内对1000罗马数字进行排序编辑2:添加了一个检查以避免'通知',还优化了一点代码,运行速度更快,速度比转换为整数快两倍,而不是排序(使用PEAR Number_Roman包)< / p>
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>";
答案 9 :(得分:0)
我认为 best (请参阅我的评论)第一个解决方案是在特殊的罗马比较函数的帮助下使用标准的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)