Нужен код-ревью решения задачи представления десятичных чисел в римской системе счисления
Задача:
Семь различных символов представляют римские цифры со следующими значениями:
Римские числа | Десятичные числа |
---|---|
I | 1 |
V | 5 |
X | 10 |
L | 50 |
C | 100 |
D | 500 |
M | 1000 |
Римские цифры образуются путем добавления преобразований значений десятичных знаков от самого высокого к самому низкому. Преобразование значения десятичного знака в римское число имеет следующие правила:
Если значение не начинается с 4 или 9, выберите символ максимального значения, которое можно вычесть из входных данных, добавьте этот символ к результату, вычтите его значение и преобразуйте остаток в римскую цифру.
Если значение начинается с 4 или 9, используйте субтрактивную форму , представляющую один символ, вычитаемый из следующего символа, например, 4 на 1 ( I) меньше 5 ( V): IV и 9 на 1 ( I) меньше 10 ( X): IX. Используются только следующие субтрактивные формы: 4 ( IV), 9 ( IX), 40 ( XL), 90 ( XC), 400 ( CD) и 900 ( CM).
Только степени 10 ( I, X, C, M) могут быть добавлены последовательно не более 3 раз для представления кратных 10. Вы не можете добавить 5 ( V), 50 ( L) или 500 ( D) несколько раз. Если вам нужно добавить символ 4 раза, используйте вычитательную форму.
Дано целое число, преобразуйте его в римскую цифру.
Пример 1:
Ввод: число = 3749
Вывод: "MMMDCCXLIX"
Объяснение:
3000 = МММ как 1000 (М) + 1000 (М) + 1000 (М)
700 = DCC как 500 (D) + 100 (C) + 100 (C)
40 = XL как 10 (X) меньше 50 (L)
9 = IX как 1 (I) меньше 10 (X)
Примечание: 49 не на 1 (I) меньше 50 (L), поскольку преобразование основано на десятичных знаках.
Пример 2:
Ввод: число = 58
Вывод: "LVIII"
Объяснение:
50 = L, 8 = VIII
Пример 3:
Ввод: число = 1994
Вывод: "MCMXCIV"
Объяснение:
1000 = М
900 = СМ
90 = ХС
4 = IV
Ограничения:
1 <= num <= 3999
Ссылка:
https://leetcode.com/problems/integer-to-roman/description/
Недавно писал код для этой задачи. Ключевая особенность в том, что я решил не добавлять в кортежи тех значений, которых нет в таблице, (за исключением None). Вот что вышло:
def int_to_roman(num):
result = []
nums = (1000, 500, 100, 50, 10, 5, 1, None)
roman_nums = ('M', 'D', 'C', 'L', 'X', 'V', 'I')
while not num == 0:
for max_deductible in nums:
# Находим такое наибольшее вычитаемое, которое будет меньше либо равно num
if max_deductible <= num:
# Сохраняем индекс вычитаемого
max_deductible_index = nums.index(max_deductible)
break
# Сохраняем предыдущее число (до max_deductible)
previous_num = nums[max_deductible_index - 1]
flag = len(str(max_deductible)) == len(str(previous_num))
# Выбор вычитаемого из next_num, которое подходит для данного случая
possibly_deductible = (nums[max_deductible_index + 1], max_deductible)[flag]
# Если существует число менее num и более max_deductible (в nums)
if all((possibly_deductible, previous_num)) and previous_num - possibly_deductible <= num:
num -= (previous_num - possibly_deductible)
possibly_deductible_index = nums.index(possibly_deductible)
# В римском вычитании сначала ставится вычитаемое, а потом уменьшаемое
result.append(roman_nums[possibly_deductible_index] +
roman_nums[max_deductible_index - 1])
else: # Если такого числа не существует, вычитаем max_deductible
num -= max_deductible
result.append(roman_nums[max_deductible_index])
return ''.join(result) # Возращаем результат
Буду рад услышать любую критику. Но важно понимать, что я относительно недавно пришёл в эту сферу, и поэтому предлагать какие-нибудь функции, методы из библиотек/фреймворков не следует.
Ответы (1 шт):
Общее замечание - комментарии малосодержательны и отсутствуют в спорных местах, где они нужны.
def int_to_roman(num):
Было бы неплохо указать тип аргументов и возвращаемого значения. И почему бы не написать документирующую строку после декларации?
result = []
nums = (1000, 500, 100, 50, 10, 5, 1, None)
roman_nums = ('M', 'D', 'C', 'L', 'X', 'V', 'I')
Было бы правильным прокомментировать наличие None
в конце nums
, необходимость которого не очевидна.
while not num == 0:
В условии стоит ограничение 1 <= num <= 3999
. У вас проверки нет. Почему?
for max_deductible in nums:
# Находим такое наибольшее вычитаемое, которое будет меньше либо равно num
if max_deductible <= num:
# Сохраняем индекс вычитаемого
max_deductible_index = nums.index(max_deductible)
break
Во-первых, слово deductible в имени переменной выбрано неудачно, поскольку предполагает работу с финансами или страховкой, а это не ваш случай. Рассмотрите нейтральные альтернативы, например subtractable.
Во-вторых, от этого цикла можно (и нужно) избавиться, постепенно опускаясь вниз по значениям nums
. Но если уж использовать, то наверное с enumerate
чтобы не искать индекс. Также будет уместен комментарий, почему не нужно беспокоиться о сравнении None < num
.
# Сохраняем предыдущее число (до max_deductible)
previous_num = nums[max_deductible_index - 1]
Это то место, где комментарий может быть более содержательным, указывая на случай, когда индекс проваливается в -1
.
flag = len(str(max_deductible)) == len(str(previous_num))
# Выбор вычитаемого из next_num, которое подходит для данного случая
possibly_deductible = (nums[max_deductible_index + 1], max_deductible)[flag]
Очень странное место, вводящее в заблуждение выбором имени flag
. Считаю само наличие этой переменной необоснованным, было бы лучше использовать if ... else ...
. Также не лишним было бы прокомментировать случай len('1000') == len('None')
, поскольку это совпадение слишком притянуто за уши и по алгоритму не считывается.
# Если существует число менее num и более max_deductible (в nums)
if all((possibly_deductible, previous_num)) and previous_num - possibly_deductible <= num:
Если я правильно понимаю алгоритм, то if all((possibly_deductible, previous_num)) and ...
сводится к if previous_num is not None and ...
. Комментарий к строке довольно странный; он сбивает с толку, а не объясняет.
num -= (previous_num - possibly_deductible)
possibly_deductible_index = nums.index(possibly_deductible)
# В римском вычитании сначала ставится вычитаемое, а потом уменьшаемое
result.append(roman_nums[possibly_deductible_index] +
roman_nums[max_deductible_index - 1])
Спорное место. Если мы считаем выражения вида 'IV', 'IX', 'XL', ...
одной цифрой, представленной в виде комбинации двух графем, тогда претензий нет. А если эти выражения считаем комбинацией двух римских цифр, тогда вы неявно дублируете финальное соединение набора цифр. Полагаю, что вместо одного append
нужно делать два идущих друг за другом, чтобы исключить скрытое соединение цифр.
else: # Если такого числа не существует, вычитаем max_deductible
num -= max_deductible
result.append(roman_nums[max_deductible_index])
Если возвращаемое значение - строка, в которой римские цифры соединяются встык, то вся эта проверка убирается добавлением пар (900, 'CM'), (400, 'CD'), (90, 'XC'), ...
.
return ''.join(result) # Возращаем результат
Последний комментарий не информативен.
В целом, алгоритм понятен. С учетом указанных замечаний, его можно оптимизировать, добавив обозначения вида 'IV', 'XL', 'CD', ...
. Нет смысла вычислять их алгоритмически в силу малого количества:
def int_to_roman(num: int) -> str:
"""Return the Roman representation of the given number between 1 and 3999 including.
Examples:
>>> int_to_roman(3749)
'MMMDCCXLIX'
>>> int_to_roman(1994)
'MCMXCIV'
>>> int_to_roman(58)
'LVIII'
>>> int_to_roman(0) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
AssertionError
"""
assert 1 <= num <= 3999 # unalterable alternative: if not (1 <= num <= 3999): raise ValueError()
result = []
base_numbers = (1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1)
roman_codes = ('M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I')
base_code = zip(base_numbers, roman_codes)
base, roman = next(base_code)
while num > 0:
while base > num:
base, roman = next(base_code)
num -= base
result.append(roman)
return ''.join(result)
if __name__ == '__main__':
import doctest
doctest.run_docstring_examples(int_to_roman, None)
Если же в силу каких-либо соображений таблицы кодировки нужно ограничить простыми цифрами, то исходный алгоритм можно отредактировать так:
def int_to_roman(num: int) -> str:
"""Return the Roman representations for num in the interval [1, 3999]
>>> int_to_roman(3749)
'MMMDCCXLIX'
>>> int_to_roman(1994)
'MCMXCIV'
>>> int_to_roman(58)
'LVIII'
"""
assert 1 <= num <= 3999
result = []
base = (1000, 500, 100, 50, 10, 5, 1)
code = ('M', 'D', 'C', 'L', 'X', 'V', 'I')
step = 0
while num > 0:
# Находим такое наибольшее вычитаемое, которое будет меньше либо равно num
while base[step] > num:
# Сохраняем индекс вычитаемого
step += 1
# Выбор вычитаемого из next_num, которое подходит для данного случая
shift = step%2
# Если существует число менее num и более max_deductible (в nums)
if step > 0 and num >= (diff:=base[step-1] - base[step+shift]):
num -= diff
# В римском вычитании сначала ставится вычитаемое, а потом уменьшаемое
result.append(code[step+shift])
result.append(code[step-1])
else: # Если такого числа не существует, вычитаем max_deductible
num -= base[step]
result.append(code[step])
return ''.join(result)
Оригинальные комментарии сохранены для сравнения с исходным кодом. Значимые изменения:
- Из опорных чисел удален
None
, вместо него проверяемstep > 0
. - Смещение к наименьшему числу одного порядка (пр.: 1 для 500->100, 0 для 100->100) рассчитывается по четности текущей позиции.