Опубликован: 06.10.2011 | Доступ: свободный | Студентов: 1681 / 107 | Оценка: 4.67 / 3.67 | Длительность: 18:18:00
Лекция 7:

Списки

< Лекция 6 || Лекция 7: 123 || Лекция 8 >
Аннотация: В лекции рассматриваются такие структуры данных как кортежи и списки разного рода.

6.1. Списки

Список, известный также как последовательность, является контейнером, хранящим элементы в некотором порядке, обычно в порядке их вставки. Математически он представляет всюду определенную функцию, отображающую целочисленный интервал 1.. count в множество G. Это напоминает массивы, но разница в том, что count может свободно изменяться при вставке новых элементов.

Список

Рис. 6.1. Список

Рисунок показывает список, составленный из пяти элементов. Стрелка просто показывает, что порядок имеет значение. Те же элементы, но организованные в другом порядке, составляют другой список.

Подобно массивам и другим структурам, где элементы упорядочены, мы систематически начинаем нумерацию с 1.

Возможны различные реализации списков. В EiffelBase они поддерживаются такими классами, как LINKED_LIST, TWO_WAY_LIST, ARRAYED_LIST, MULTI_ARRAYED_LIST. В данном разделе описываются свойства, общие для всех этих вариантов, заданные в классе LIST. Более подробно будет рассмотрен важный вариант связного списка, а затем будет дан обзор других вариантов. Реализация не будет обсуждаться во всех деталях, но примеры реализации и базисные идеи будут рассмотрены. Для получения полной картины следует обратиться к библиотеке классов EiffelBase и проанализировать соответствующие тексты.

Класс LIST описывает общие абстрактные понятия, представляя пример отложенного класса, который прямо или косвенно наследуется другими классами. Это значит, что потомки не повторяют общие элементы, а получают их от класса более высокого уровня. Эти концепции будут подробно обсуждаться в лекции, посвященной наследованию.

Классы, задающие списки, рассматривают список не просто как коллекцию элементов, но как машину, которая в любой точке своего существования имеет состояние, характеризуемое курсором:

Список с курсором

Рис. 6.2. Список с курсором

Это не новое понятие. Когда мы рассматривали линии метро как список станций, мы уже встречались с курсором.

Наличие курсора облегчает выполнение базисных операций – доступ, вставку, удаление элементов, позволяя соответствующим методам иметь более простой интерфейс – действия будут выполняться над элементом, заданным позицией курсора, так что им не требуется дополнительное указание позиции элемента. Конечно, это требует, чтобы существовала возможность перемещения курсора к нужному элементу.

В этой схеме курсор является внутренним понятием – у каждого списка свой собственный курсор. Возможно введение объектов, задающих внешний курсор; таких курсоров может быть несколько, позволяя различным клиентам списка рассматривать собственное представление списка. В частности, это полезно в параллельном программировании. Для деталей следует обратиться к библиотеке EiffelBase, рассмотрев класс CURSOR и его потомков. Типичное применение внешнего курсора иллюстрируется в последующих лекциях, где внешний курсор используется для запоминания начального состояния внутреннего курсора и его восстановления после завершения операции, требующей проход по списку.

Запросы, связанные с курсором

Мы должны позволять курсору списка, показанному на последнем рисунке, занимать позиции из расширенного интервала 0.. count + 1, а не из интервала 1.. count, где находятся элементы списка. Курсор может находиться левее первого элемента и правее последнего элемента списка. В полезности такого представления легко убедиться. Позицию курсора дает запрос:

index: INTEGER
          — Текущая позиция курсора.
        

Предложения, задающие инвариант, выглядят так:

non_negative_index: index >= 0
index_small_enough: index <= count + 1
        

Для характеристики двух экстремальных случаев позиции курсора введены два запроса:

before: BOOLEAN
            — Правда, что слева от курсора нет правильной позиции?
after: BOOLEAN
            — Правда, что справа от курсора нет правильной позиции?
        

Обратите внимание на корректность формулировок комментариев, которые должны соответствовать всем возможным случаям, в частности, учитывать возможность пустого списка.

В соответствии со стандартом стиля, принятым для булевских запросов, наши запросы должны именоваться как is_before, is_after, но они уже так давно используются, что переименовывать их "рука не поднялась". Другие запросы, примененные ниже, такие как is_empty, следуют нормальному соглашению.

Если в текущем состоянии курсор списка находится в позиции before или after, то мы говорим, что он "off":

Позиция курсора before и after

Рис. 6.3. Позиция курсора before и after
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 будут ложными; курсор может быть на первом (последнем) элементе, если в списке есть хотя бы один элемент. Следует всегда помнить о принципе экстремальных случаев, требующем обращать особое внимание на граничные ситуации. В отсутствии элементов рисунок, иллюстрирующий список, выглядит так:

Пустой список с его двумя возможными позициями курсора

Рис. 6.4. Пустой список с его двумя возможными позициями курсора

В этом случае count равно нулю и максимальная позиция index, удовлетворяющая инварианту, count + 1, равна 1. В таком пустом списке курсор может быть либо в позиции 0, либо в позиции 1. В любом случае off будет иметь место, что следует из предложения инварианта:

empty_constraint: is_empty implies off
        

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

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

Для получения доступа к элементу в позиции курсора

Текущий элемент

Рис. 6.5. Текущий элемент

используйте запрос

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
        

Рис. 6.6.

Предусловия гарантируют, что индекс остается в границах, заданных предыдущими предложениями инварианта: 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 в конец, не перемещая курсор.
        

Как показывают комментарии, эти процедуры спроектированы так, чтобы не оказывать воздействия на курсор, поскольку нет причин, чтобы вставка изменяла текущую активную позицию курсора.

Во многих случаях реализация должна временно изменять позицию курсора, например, можно следующим образом реализовать расширение списка extend(v):
original_index:= index
finish
put_right (v)
go_i_th (original_index)
        
Здесь, как и в has, записывается начальная позиция, которая восстанавливается в конце.

Для удаления элементов можно использовать:

remove
          — Удалить элемент в позиции курсора; передвинуть курсор к правому
          — соседу (или к after, если правого соседа нет).
      require
          item_exists: not off
      ensure
          removed: count = old count – 1
          after_when_empty: is_empty implies after
        

В этом случае курсор должен быть передвинут, поскольку исчез элемент, на который указывал курсор.

Разрешается удалять элементы слева или справа от курсора, не изменяя при этом позицию курсора. В качестве упражнения напишите реализацию методов remove_left, remove_right, задав их спецификацию (сигнатуру, заголовочный комментарий, контракт).

Удаление текущего элемента

Рис. 6.7. Удаление текущего элемента
< Лекция 6 || Лекция 7: 123 || Лекция 8 >