1

Тема: Додавання двох чисел у форматі linked list

Відгалуження цієї теми

ur_naz написав:
(mapcar '+ '(1 2 3)'(3 4 5 6))

;)

Ну і?..

Armed Bear Common Lisp 0.24.0
Java 1.8.0_51 Oracle Corporation
Java HotSpot(TM) Client VM
Low-level initialization completed in 4.691 seconds.
Startup completed in 15.395 seconds.
Type ":help" for a list of available commands.
CL-USER(1): (mapcar '+ '(1 2 3)'(3 4 5 6))
(4 6 8)
koala написав:

Ба більше, в умові не просто аби-які списки, а зв'язні. Ліспівські списки, звісно, абстрактні і можуть бути реалізовані і на зв'язних, але мова досить чітко іде про явні структури, а не приховані.

Базовою абстракцією для ліспівських списків (принаймні, в CL та Scheme; про Clojure розмова окрема) є cons-комірки, з яких щось, крім зв'язного списку, побудувати проблематично. Те, що записується як (1 2 3 4 5), можна альтернативно записати як (1 . (2 . (3 . (4 . (5 . NIL)))))

2

Re: Додавання двох чисел у форматі linked list

steamwater написав:

Ну поперше, ви у роздiлi С++, шановний. Тому ваша зневага у виглядi недо сіплюсплюсного висловлювання, не доречна. Я колись трохи кодив на auto-lisp, що є дiалектом Лiспу для AutoCAD. Це дивовижна мова, так. Але якщо казати про списки, то я б вже визначив LUA, тим бiшлше, що вона, iнколи зустрiчається у промислових С++ проектах.

Lua — на рівні архітектури чудова мова, але їй бракує лаконічної ліспівської нотації. Людину з вулиці з якоюсь алгоритмічною мовою за плечима навчити балакати на Lua простіше (звідси й одна з причин популярності у промислових проектах), але, з іншого боку, простеньку реалізацію примітивної лісп-подібної мови може з нуля самостійно написати і студент-сішник.

У нас також є розділ, присвячений Lua, де останні дописи датуються тим же 2016 роком. Якщо маєте бажання, розгорніть тему в тому розділі.

3

Re: Додавання двох чисел у форматі linked list

P.Y. написав:

ur_naz дав ліспівський код замість сіплюсплюсного, а ЛІСП, як відомо, є мовою, спеціально зробленою для роботи зі списками, тому програма в нього максимально коротка. Які я бачу недоліки в цьому коді:
1) Нема переносу розрядів — а отже, для такої пари чисел:

(mapcar '+ '(1 2 3)'(8 9 5))

результат буде (9, 11, 8 ), а не (9, 1, 9), як нам би того хотілось.
2) В умові сказано, що довжина списків може бути різною — отже, коротший список слід попередньо доповнити нулями чи іншим способом урахувати цю різницю.
3) З урахуванням переносу розрядів, кінцевий список може вийти довшим, ніж початкові — цього теж нема в поточній реалізації.

Отже, майбутня програма повинна реалізовувати цей функціонал.

З лісп-подібних мов я краще орієнтуюсь у Scheme та Clojure, але списки в Clojure побудовано з незрозумілих абстракцій (тому це буде незовсім такий зв'язний список, як нам треба), а Scheme вже й так знаю (чи, принаймні, знав колись раніше), тому спробую Common LISP, у якому плаваю і в якому мені є що вчити.

Треба написати функцію, що приймає два списки чисел і повертає список-суму.

Вирівнювання кількості розрядів. Можна знайти довжини списків, додати в обидва потрібну кількість нулів (так, щоб довжина кожного з них була на 1 довшою, ніж довший з початкових списків), а після обчислень відкинути кінцевий нуль (якщо він буде) в отриманому результаті.

Саме́ обчислення можна зробити тим же mapcar'ом, але замість простого додавання буде функція, що додає два числа та перенесений розряд (який зберігається у змінній за межами функції), результат додавання ділить з остачею на 10, частку записує як перенесений розряд, остачу повертає як результат. Якщо mapcar викликає функцію послідовно при проходжені кожного елемента, то ніби має спрацювати.

4

Re: Додавання двох чисел у форматі linked list

Додавання з переносом розрядів.

(defvar *carry* 0)
(defun c+ (a b)
    (let ((sum (+ a b *carry*)))
        (setf *carry* (floor sum 10))
        (mod sum 10)))

Розряд зберігаємо в динамічній змінній *carry*. Такий підхід відрізняється від чистого функціонального і більш властивий процедурним мовам, але CL і не позиціонується як чисте ФП. Принаймні, для даної задачі це працює:

CL-USER(44): *carry*
0
CL-USER(45): (c+ 5 3)
8
CL-USER(46): *carry*
0
CL-USER(47): (c+ 5 9)
4
CL-USER(48): *carry*
1
CL-USER(49): (c+ 5 9)
5
CL-USER(50): *carry*
1
CL-USER(51): (c+ 5 3)
9
CL-USER(52): *carry*
0

Тепер зробімо додавання списків з використанням c+ замість +:

CL-USER(55): (let ((*carry* 0)) (mapcar 'c+ '(1 2 3)'(8 9 5)))
(9 1 9)

Для списків однакової довжини це працює, за умови, що сума двох чисел-списків не виходить за межі початкової кількості розрядів.
Конструкція (let ((*carry* 0)) потрібна для випадку, коли в *carry* випадково залишилось ненульове значення. Перед обчисленнями в *carry* записується нуль (а старе значення запам'ятовується), після обчислень відновлюється старе значення. Оскільки *carry* попередньо було створено як динамічну змінну, let робить саме тимчасовий запис у неї, а не створює локальну змінну з таким же ім'ям, тому це впливає і на роботу функції c+, яка цю змінну використовує. Погляньмо, як це працює без let і з let:

CL-USER(57): (setf *carry* 127)
127
CL-USER(58): (mapcar 'c+ '(1 2 3)'(8 9 5))
(6 4 0)
CL-USER(59): *carry*
1
CL-USER(60): (setf *carry* 127)
127
CL-USER(61): (let ((*carry* 0)) (mapcar 'c+ '(1 2 3)'(8 9 5)))
(9 1 9)
CL-USER(62): *carry*
127

Далі на черзі — вирівнювання довжини списків...

Подякували: leofun011

5

Re: Додавання двох чисел у форматі linked list

Решта коду:

(defun longate (lst lng)
    (append lst
        (make-list (- lng (length lst)) :initial-element 0)))
(defun killfinalzero (lst)
    (reverse (let ((lst (reverse lst)))
        (if (= 0 (car lst))
            (cdr lst)
            lst))))
(defun addlists (L1 L2)
    (let ((reslen (+ 1 (max (length L1) (length L2)))))
    (let    ((L1 (longate L1 reslen))
         (L2 (longate L2 reslen))
         (*carry* 0))
    (killfinalzero (mapcar 'c+ L1 L2)))))

Функція longate збільшує список до заданої довжини, дописуючи нулі в його кінець. Побудована на стандартних функціях для роботи зі списками. Незовсім оптимально (напр., повторне обчислення довжини списку може бути тривалим для довгих списків, ще одне проходження по списку відбувається при конкатенації), видовження списків можна було б зробити циклом з мінімальною кількістю проходів, але я з цими засобами мови поки що не розібрався, тому так.
Функція killfinalzero перевертає список, викидає нуль (якщо список починається з нуля), перевертає назад. Теж можна було б зробити оптимальніше, відійшовши від функціонального стилю в процедурний, але поки що так.

Результат роботи:

CL-USER(103): (addlists '(5 6 7)'(8 9))
(3 6 8)
CL-USER(105): (addlists '(5 6 7)'(8 9 9))
(3 6 7 1)
CL-USER(106): (addlists '(1 2 3)'(6 7 8 9 9 9))
(7 9 1 0 0 0 1)

Ніби все як треба. Чекаю нищівної критики.

Подякували: leofun011

6

Re: Додавання двох чисел у форматі linked list

Версія з обчисленням за один прохід:

(defvar *carry* 0)
(defun c+ (a b)
    (let ((sum (+ a b *carry*)))
        (setf *carry* (floor sum 10))
        (mod sum 10)))
(defun lst2num (lst)
    (if lst (car lst) 0))
(defun addlists (L1 L2)
    (let ((*carry* 0))
        (loop
            for p1 = l1 then (cdr p1)
            for p2 = l2 then (cdr p2)
            while (or p1 p2 (> *carry* 0))
            collect (c+ (lst2num p1) (lst2num p2)))))

Від mapcat відмовився — всі повторювані дії тепер організовано за допомогою більш універсального loop.
У ньому є конструкція for змінна = початкове-значення then наступне-значення , за допомогою якої я здійснюю проходження по списках. Існує і більш спеціалізована конструкція for змінна in список, але вона завершує роботу циклу при досягненні кінця списку (коротшого з них), а мені треба продовжити цикл після кінця списків, поки в *carry* лишається ненульове значення переносу розряду, тому рух по списках організовано за допомогою cdr (який повертає всі елементи, крім першого, якщо список не порожній, або nil, якщо список порожній; nil і порожній список () — у термінах CL, одне й те ж, тому (cdr p1) нескінченно повертатиме nil після кінця списку), а перевірку умови продовження зроблено окремо конструкцією while умова.

Умова продовження циклу: залишок першого чи другого списку має бути непорожнім, або в *carry* має лежати ненульове значення. Оскільки будь-яке значення, крім nil, вважається істиною, то замість (not (eq p1 nil)) можна написати просто p1.

Конструкція collect вираз збирає значення, обчислювані в кожній ітерації, і повертає їх у вигляді списку. Для обчислення значень я використав ту ж функцію c+ та допоміжну функцію lst2num, яка повертає перший елемент непорожнього списку або 0, якщо список порожній.

Нова версія addlists видає такі ж результати, як попередня:

CL-USER(143): (addlists '(1 2 3)'(4 5 6))
(5 7 9)
CL-USER(144): (addlists '(1 2 3)'(7 8 9))
(8 0 3 1)
CL-USER(145): (addlists '(1 0 1 1)'(7 8 9))
(8 8 0 2)

але зроблена більш оптимально, займає менше рядків коду, на довгих списках має показувати вищу швидкодію.

Подякували: leofun011