Списки
6.1. Списки
Список, известный также как последовательность, является контейнером, хранящим элементы в некотором порядке, обычно в порядке их вставки. Математически он представляет всюду определенную функцию, отображающую целочисленный интервал 1.. count в множество G. Это напоминает массивы, но разница в том, что count может свободно изменяться при вставке новых элементов.
Рисунок показывает список, составленный из пяти элементов. Стрелка просто показывает, что порядок имеет значение. Те же элементы, но организованные в другом порядке, составляют другой список.
Возможны различные реализации списков. В EiffelBase они поддерживаются такими классами, как LINKED_LIST, TWO_WAY_LIST, ARRAYED_LIST, MULTI_ARRAYED_LIST. В данном разделе описываются свойства, общие для всех этих вариантов, заданные в классе LIST. Более подробно будет рассмотрен важный вариант связного списка, а затем будет дан обзор других вариантов. Реализация не будет обсуждаться во всех деталях, но примеры реализации и базисные идеи будут рассмотрены. Для получения полной картины следует обратиться к библиотеке классов EiffelBase и проанализировать соответствующие тексты.
Классы, задающие списки, рассматривают список не просто как коллекцию элементов, но как машину, которая в любой точке своего существования имеет состояние, характеризуемое курсором:
Это не новое понятие. Когда мы рассматривали линии метро как список станций, мы уже встречались с курсором.
Наличие курсора облегчает выполнение базисных операций – доступ, вставку, удаление элементов, позволяя соответствующим методам иметь более простой интерфейс – действия будут выполняться над элементом, заданным позицией курсора, так что им не требуется дополнительное указание позиции элемента. Конечно, это требует, чтобы существовала возможность перемещения курсора к нужному элементу.
Запросы, связанные с курсором
Мы должны позволять курсору списка, показанному на последнем рисунке, занимать позиции из расширенного интервала 0.. count + 1, а не из интервала 1.. count, где находятся элементы списка. Курсор может находиться левее первого элемента и правее последнего элемента списка. В полезности такого представления легко убедиться. Позицию курсора дает запрос:
index: INTEGER — Текущая позиция курсора.
Предложения, задающие инвариант, выглядят так:
non_negative_index: index >= 0 index_small_enough: index <= count + 1
Для характеристики двух экстремальных случаев позиции курсора введены два запроса:
before: BOOLEAN — Правда, что слева от курсора нет правильной позиции? after: BOOLEAN — Правда, что справа от курсора нет правильной позиции?
Обратите внимание на корректность формулировок комментариев, которые должны соответствовать всем возможным случаям, в частности, учитывать возможность пустого списка.
Если в текущем состоянии курсор списка находится в позиции before или after, то мы говорим, что он "off":
off: BOOLEAN — Верно ли, что курсор вне списка? ensure definition: Result = (after or before)
Следующие предложения инварианта выражают свойства этих запросов (возможны также и соответствующие постусловия):
before_definition: before = (index = 0) after_definition: after = (index = count + 1) off_definition: off = (index = 0 or index = count + 1)
Другие запросы о курсоре:
is_first: BOOLEAN — Задает ли курсор первый элемент? ensure valid_position: Result implies (not is_empty) is_last: BOOLEAN — Задает ли курсор последний элемент? ensure valid_position: Result implies (not is_empty)
Еще один важный запрос:
is_empty — Пуст ли список?)
Список может пустым, и тогда запросы is_first и is_last будут ложными; курсор может быть на первом (последнем) элементе, если в списке есть хотя бы один элемент. Следует всегда помнить о принципе экстремальных случаев, требующем обращать особое внимание на граничные ситуации. В отсутствии элементов рисунок, иллюстрирующий список, выглядит так:
В этом случае count равно нулю и максимальная позиция index, удовлетворяющая инварианту, count + 1, равна 1. В таком пустом списке курсор может быть либо в позиции 0, либо в позиции 1. В любом случае off будет иметь место, что следует из предложения инварианта:
empty_constraint: is_empty implies off
Заметьте повторяющееся накопление предложений инварианта, позволяющее выразить шаг за шагом наше понимание используемых структур объектов.
Почувствуй методологию: использование инвариантов
Для получения доступа к элементу в позиции курсора
используйте запрос
item: G — Элемент в позиции курсора. require not_off: not off
Этот запрос возвращает результат типа G, родовой параметр классов, задающих списки (LIST [G], LINKED_LIST [G] и т.д.). Обратите внимание на предусловие: в состоянии off (пустой список) нет текущего элемента.
Перемещение курсора
В нашем распоряжении есть несколько команд, позволяющих перемещать курсор. Вот команды, позволяющие устанавливать курсор в начало или в конец списка:
start — Переместить курсор в первую позицию — (не выполняется для пустого списка) ensure at_first: (not is_empty) implies is_first finish — Переместить курсор в последнюю позицию — (не выполняется для пустого списка) ensure at_last: (not is_empty) implies is_last
Вызов start означает истинность is-first, а вызов finish означает истинность is-last. Для пустых списков это не справедливо, что отражено в постусловии. Курсор можно также перемещать на одну позицию вправо и влево:
forth — Переместить курсор в следующую позицию. require not_after: not after ensure moved_forth: index = old index + 1 back — Переместить курсор в предыдущую позицию. require not_before: not before ensure moved_back: index = old index – 1
Предусловия гарантируют, что индекс остается в границах, заданных предыдущими предложениями инварианта: non_negative_index и index_small_enough. Курсор можно установить в заданную позицию:
go_i_th (i: INTEGER) — Переместить курсор в i-ю позицию. require valid_cursor_position: i >= 0 and i <= count + 1 ensure position_expected: index = i
Итерирование списка
Часто приходится применять одну и ту же операцию ко всем элементам списка. Предположим, что операция задается методом:
your_operation (x: G)
С этой ситуацией мы уже встречались при рассмотрении линий метро – общую форму задает цикл:
from your_list.start until your_list.after loop your_operation (your_list.item) your_list.forth variant your_list.count – your_list.index + 1 end
Здесь операция применяется к некоторому существующему в вашей программе списку your_list. Эта же схема будет использована и в классах, задающих списки, для прохода по текущему списку, но в этом случае вызовы будут неквалифицированными – просто start, after, forth без your_list. Примеры скоро появятся в методах search и has, где разыскиваются нужные элементы списка.
Существуют вариации этой схемы, например, операция применятся к элементам списка, пока не встретится элемент, удовлетворяющий некоторому условию:
from your_list.start until your_list.after or else your_condition (your_list.item) loop your_operation (your_list.item) your_list.forth variant your_list.count – your_list.index + 1 end
Такие схемы являются примером итерирования структуры данных.
Определение: итерирование
Другой термин для "итерирования" – это "обход". Итерация – это применение механизма итерирования к структуре, хотя часто этот термин используется, когда речь идет об одном шаге процесса ("на каждой итерации цикла курсор передвигается на одну позицию"). Итератор – это механизм, преобразующий операцию над отдельным элементом в операцию над всеми элементами структуры.
Примером реализации, использующей механизм итераций, который разделяется всеми классами, задающими списки, является процедура search, осуществляющая поиск элемента в списке. Ее текст выглядит так:
search (v: G) —Если курсор установлен в позиции before и список не пуст, — то курсор передвигается в начало списка. — При поиске курсор устанавливается на первом элементе, совпадающем с v. — Если такового нет, то курсор переходит в позицию after. do from if before and not is_empty then forth end until after or else item = v loop forth end end
Этот метод является командой, перемещающей курсор:
- если заданное значение v встречается в текущей позиции или в позиции справа от курсора – то к первой такой позиции;
- в противном случае – к граничной позиции справа (after).
Это соглашение позволяет использовать метод search повторно для поиска последовательных вхождений значения. Процедура также применяется в реализации запроса has, отвечающего на вопрос о существовании в списке элемента с заданным значением:
has (v: G) — Содержит ли структура вхождение v? local original_index: INTEGER do original_index:= index start search (v) Result:= not after go_i_th (original_index) end
Будучи запросом, has должна оставлять структуру в состоянии, предшествующем запросу. Поэтому здесь вводится локальная переменная original_index, запоминающая начальную позицию курсора и восстанавливающую ее в конце работы, благодаря методу go_i_th.
Как search, так и has требуют О(count) времени как в среднем, так и в максимуме.
Итерационная схема, иллюстрируемая search, многократно встречается при работе со списками и другими последовательными структурами; мы уже встречались с ней в нескольких примерах, начиная с цикла, подсчитывающего общее время проезда по линии метро.
В конце этой лекции мы вернемся к концепциям итерации и получим первое представление об общих механизмах, позволяющих применять стандартный механизм итерирования вместо того, чтобы каждый раз задавать явный цикл с start, forth, item и after.
Добавление и удаление элементов
Для добавления элемента в список – в его начало, конец или в позицию курсора – можно использовать одну из операций со следующими спецификациями:
put_front (v: G) — Добавить v в начало, не перемещая курсор. put_left (v: G) — Добавить v слева от позиции курсора, не перемещая курсор. require not_before: not before put_right (v: G) — Добавить v справа от позиции курсора, не перемещая курсор. require not_after: not after extend (v: G) — Добавить v в конец, не перемещая курсор.
Как показывают комментарии, эти процедуры спроектированы так, чтобы не оказывать воздействия на курсор, поскольку нет причин, чтобы вставка изменяла текущую активную позицию курсора.
original_index:= index finish put_right (v) go_i_th (original_index)
Для удаления элементов можно использовать:
remove — Удалить элемент в позиции курсора; передвинуть курсор к правому — соседу (или к after, если правого соседа нет). require item_exists: not off ensure removed: count = old count – 1 after_when_empty: is_empty implies after
В этом случае курсор должен быть передвинут, поскольку исчез элемент, на который указывал курсор.
Разрешается удалять элементы слева или справа от курсора, не изменяя при этом позицию курсора. В качестве упражнения напишите реализацию методов remove_left, remove_right, задав их спецификацию (сигнатуру, заголовочный комментарий, контракт).