Мысль про алгоритмы (как читать, как писать)

Jul 23, 2009 10:24

Вчера скачал себе первый номер журнала "Практика функционального программирования" (. Кроме всего прочего, там есть глава, которую интересно читать с точки зрения императивных языков - "Изменяемое состояние: опасности и борьба с ними". Среди прочего, там приводится пример достаточно сложного алгоритма (добавление э-та в двусвязный список), который натолкнул меня на мысли - как подобное надо читать, чтобы облегчить себе задачу понимания.

Цитата:
"Вот (слегка сокращенный) фрагмент кода, осуществляющего встав-
ку в двусвязный список в GNU Classpath. Он выглядит невинно, но можете ли вы в
уме убедиться в его корректности, не рисуя на бумаге диаграмм для трех возможных
случаев и не отслеживая последовательно влияние каждой строки кода на диаграмму?"

public void add(int index, Object o) {
    Entry e = new Entry(o);
    if (index < size) {
        Entry after = getEntry(index);
        e.next = after;
        e.previous = after.previous;
        if (after.previous == null)
            first = e;
        else
            after.previous.next = e;
        after.previous = e;
    } else if (size == 0) {
        first = last = e;
    } else {
        e.previous = last;
        last.next = e;
        last = e;
    }
    size++;
}

Что заметил: если читать этот алгоритм сверху-вниз, то он действительно сложен для понимания в уме. ИМХО это происходит из-за того, что приходится держать в голове не только текущие условия, но и то, что что-то будет потом (следующие группы else). Т.е. мы имеем дело с обычной ограниченностью "оперативной памяти" человеческого мозга.
Однако, если алгоритм рассматривать сниузу-вверх, то получим:
1) Счётчик у нас будет увеличен в любом случае (это правильно)
2) Если мы добавляем элемент в конец списка, то имеем дело с последним блоком else. Правильность проверяется на счёт раз.
3) Если длина списка нулевая, то имеем дело вообще с фактически одним присваиванием ( first = last = e;). Очевидно, что это верно.
4) Соответственно, нам надо абстрагироваться от всего, и смотреть на код добавления в середину списка. Фактически, речь идёт о декомпозиции кода на функции в уме:

Entry after = getEntry(index);
        e.next = after;
        e.previous = after.previous;
        if (after.previous == null)
            first = e;
        else
            after.previous.next = e;
        after.previous = e;

Разбираем:
а) Создали элемент для промежуточного хранения элемента по индексу вставки.
б) Для нового элемента устанавливаем следующим элемент к промежуточный.
в) Предыдущим для нового элемента устанавливаем предыдущий элемент временно.
г) Если новый элемент является первым - делаем его таковым, иначе приводим в соответствие индекс следующего элемента, для предыдущего элемента нашей новой записи (звучит стрёмно, но вполне очевидно)
д) Для промежуточного элемента устанавливаем предком нашу новую запись.

В общем - привет классикам, которые говорят про пользу декомпозиции. Пример явно доказывает, что вынос этого кода в private - функцию нашего двусвязного списка заметно облегчил бы понимание (хотя скорость работы несколько бы упала, что плохо).

Ну или есть ещё вариант писать код в обратном порядке:

public void add(int index, Object o) {
    Entry e = new Entry(o);
    if (index == size){
        e.previous = last;
        last.next = e;
        last = e;
    } else if (size == 0) {
        first = last = e;
    } else
        Entry after = getEntry(index);
        e.next = after;
        e.previous = after.previous;
        if (after.previous == null)
            first = e;
        else
            after.previous.next = e;
        after.previous = e;
    }
    size++;
}

ИМХО это также несколько облегчает задачу по пониманию.

программерское, мысли

Previous post Next post
Up