Глава E. Массивы Урок E2. Последовательный просмотр вектора Трудно отыскать черную кошку в темной комнате, особенно – если ее там нет. Конфуций Механизм последовательного перебора E1-1 находит применение нескольких базовых алгоритмах обработки векторов, в том числе, алгоритме поиска наибольшего (аналогично: наименьшего) элемента. Что касается массивов бо́льших размерностей, то представление их в виде линейной развертки сохраняет, по сути, работоспособность того же механизма. В зависимости от приложения, стоит говорить о двух вариантах алгоритма поиска максимума (минимума). Различаются они тем, что на выходе первого будет собственно наибольшее (наименьшее) значение среди компонент, а второй – выдаст индекс элемента, на котором это значение достигается. Примеров применения – сколько угодно. Вот приложение из области «компьютерного спорта»: вектор результатов работы системы, тестирующей абитуриентов. Каждая его компонента есть запись, включающая поля «фамилия», «количество решенных задач», «штрафное время»: type index1 = 0..N-1; {N > 1} Programmer = record of Name: string; Number: byte; Time: word end; var Results = array [index1] of Programmer; Если представляет интерес лучший результат «по задачам», то к массиву Results следует применить первый вариант алгоритма поиска максимума и обрабатывать поле «задачи». Если же важна фамилия победителя, то достаточно установить индекс ячейки с лучшим результатом, а в конце – вывести содержимое соответствующего поля отобранной компоненты. В обоих случаях систематической обработке подвергается только одно из полей записей–компонент вектора, это поле Number и его можно рассматривать как ключевое. Такая обработка, связанная с перебором именно ключей, достаточно типична. Например, набор сначала упорядочивается по ключам записей в соответствии с лексикографическим порядком, а далее используется информация, размещенная в других полях компонент массива, – теперь уже в новом, «хронологическом» порядке. Учитывая важность алгоритма поиска максимума, о чем мы говорили еще в Главе D, запишем его первый вариант в общем виде, причем будем считать, что доступ к компоненте обеспечивает непосредственно получение ее ключевого значения item. Алгоритм E2-1 (1) type index1 = 0..N-1; {N > 1} massive1 = array [index1] of item; function Max (Mas: massive1; left, right: index1): item; var i: index1; temp: item; begin temp:= Mas[left]; for i:= left+1 to right do if Mas[i] > temp then temp := Mas[i]; Max := temp end; Обратите внимание, что описание функции Max с тремя указанными параметрами делает возможным возврат наибольшего элемента не обязательно для всего массива, но также и любого его подмассива. Для этого при вызове достаточно заменить границы диапазона обработки, входящего в интервал 0..N-1. Мы еще встретимся с таким применением механизма, например, при обсуждении одного из алгоритмов сортировки вектора. Для второго варианта алгоритма, когда требуется установить местоположение искомого элемента в массиве, нужно лишь незначительно изменить обработку. Алгоритм E2-1 (2) function IndexMax (Mas: massive1; left, right: index1): index1; var i, temp: index1; begin temp:= left; for i:= left+1 to right do if Mas[i] > Mas[temp] then temp := i; IndexMax := temp end; Очевидно, значение наибольшего элемента доступно в ячейке Mas[IndexMax]. Не исключена ситуация, когда в массиве присутствуют несколько равных по значению «наибольших» элементов. Если для алгоритма E2-1 (1) это обстоятельство несущественно, то, применяя E2-1 (2), нужно иметь в виду, что алгоритм возвращает местоположение первого слева из подходящих элементов. Упражнение E2-1 a) Напишите функцию Min, возвращающую значение наименьшего элемента вектора. b) Напишите функцию IndexMinRight, возвращающую индекс последнего (правого) из наименьших элементов вектора. c) Напишите программу, отыскивающую наибольшее и наименьшее значения в заданной матрице. d) Напишите функцию Min2, возвращающую значение второго по величине элемента вектора (второй максимум). e) Назовем расстоянием между двумя элементами двумерного массива – Mas2[i1,j1] и Mas2[i2,j2] – величину abs(i1-i2)+abs(j1-j2). Напишите программу, которая выводит расстояние между первым слева из его наименьших элементов и первым справа из наибольших элементов. Во входном потоке: 2 пары натуральных чисел – значений левых и правых границ каждой из размерностей; далее набор целочисленных элементов массива в порядке построчной развертки, причем каждый элемент занимает не более 2 байт. f) Напишите программу, которая выводит количество одинаковых наибольших элементов в заданном массиве MasL. Во входном потоке: размерность L массива; затем L пар натуральных чисел – значений левых и правых границ каждой из размерностей; далее набор целочисленных элементов массива в порядке линейной развертки, причем каждый элемент занимает не более 2 байт. Очевидно, эффективность любого варианта алгоритма последовательного перебора составляет O(n), поскольку шаг 2 алгоритма E1-1 выполняется ровно по одному разу для каждой компоненты массива. Такой перебор характерен, в основном, для задач, связанных с установлением некоторого инварианта для всего массива. Приведем еще несколько подобных примеров. Примеры E2-1 a) Отыскать центр масс набора точек на плоскости; компонентами вектора здесь являются записи, включающие три поля – декартовы координаты и массу точки. b) Определить стоимость «потребительской корзины». В наиболее простой форме задачи речь требуется обыкновенное суммирование значений всех элементов вектора. В чуть усложненной постановке нужно установить некоторые взвешенные значения для каждой из компонент, содержащих информацию по одной группе товаров. В этом случае, помимо «длинного» входного массива, может понадобиться еще один вектор, количество компонент которого задается как параметр корзины. Здесь уместно говорить о механизме синхронной обработки двух векторов, когда всякое обращение к одному массиву связано с обращением к другому. В данном случае будут поочередно прочитаны компоненты входного массива, причем их ключи станут выполнять роль индекса для обращения ко второму вектору. Затем надо будет обработать уже только второй вектор, в точном соответствии с алгоритмом перебора. Замечание. Ничего алгоритмически оригинального, на наш взгляд, в механизмах синхронной и асинхронной («расшифровать» термин читателю не составит труда) обработки массивов нет. Здесь они упоминаются только потому, что подобная классификация встретилась автору – в какой-то книге по программированию – и он счел своей обязанностью поделиться с читателем этой информацией. Альтернативу последовательному перебору составляет внешне похожий на него алгоритм просмотра, который не обязательно обрабатывает все компоненты, от начала до конца массива. Например, если требуется обратиться к некоторому элементу по значению, то его местоположение в массиве, в общем случае, еще предстоит установить. Чаще всего речь идет об алгоритме поиска в линейном массиве. Он применяется при следующей постановке задачи: в векторе Mas длиной N>1 найти элемент, имеющий заданное значение sample, и вернуть его индекс, либо установить факт отсутствия таких элементов. Очевидно, образец может встретиться неоднократно. В отличие от алгоритма E2-1 (1), в постановке задачи следует непременно оговорить, как поступать в подобных случаях. Приведем вариант алгоритма, в котором обработка завершается при первом же «успехе». Алгоритм E2-2 (1) type index1 = 0..N-1; {N > 1} massive1 = array [index1] of item; function Search (Mas: massive1; left, right: index1; sample: item): index; var i: index; found: boolean; begin i := left; for i := left to right do if Mas[i] = sample then begin Search := i; Break end; end; Текст трудно назвать удачным, так как отсутствие искомого образца среди элементов массива оставляет результат неопределенным. Конечно, исправить ситуацию несложно: можно использовать глобальную булевскую переменную, либо процедуру с двумя выходными параметрами. Но мы сознательно допустили указанную неточность, – чтобы получить повод для демонстрации типичного приема, который часто используется при обработке векторов. Речь идет о добавлении в массив «фиктивного» элемента, когда вместо вектора длиной N используется вектор длиной N+1. С этим приемом мы еще встретимся. Алгоритм E2-2 (2) type index = 0..N; {N > 1} massive1 = array [index] of item; function Search (Mas: massive1; left, right: index; sample: item): index; var i: index; begin i := left; while (i<right) and (Mas[i] <> sample) do i := i+1; Search := i end; Ясно, что возврат функцией значения N свидетельствует об отсутствии образца в массиве. Упражнение E2-2 Напишите функцию SearchLast, возвращающую индекс последнего (первого с конца) вхождения образца в вектор. Легко представить ситуацию, когда требуется обнаружить все вхождения образца в вектор, но тогда алгоритм уже ничем не будет отличаться от последовательного перебора E1-1. Ничего нового в отношении трудоемкости алгоритма E2-2 сказать нельзя: его асимптотическая эффективность та же, что и у последовательного перебора, а именно – O(n). Если нас интересует более точная оценка, то ее дает вероятностный подход, и результат таков: при поиске образца среди компонент случайно заполненного вектора, в среднем, понадобится n/2 шагов для достижения цели. Выходит, достоинства массива, с точки зрения обращения к нужной компоненте, проявляются только при непосредственной адресации, либо, что практически то же самое, когда значение ключа элемента связано функциональной зависимостью с его индексом? К счастью, это не так, и существует универсальный механизм, намного эффективней последовательного поиска, но о нем – несколько позже. А пока приведем еще один вариант алгоритма перебора, отличающийся от предыдущих тем, что его, строго говоря, нельзя назвать последовательным, и проиллюстрируем его на примере. Пример E2-2 Задан массив Mas[0..N-1]; его компоненты заполнены десятичными нулями и единицами. Цель состоит в реорганизации вектора таким образом, чтобы все имеющиеся нули оказались в левой части массива, а единицы – в конце. Вполне очевидно решение, когда работает как раз последовательный перебор и подсчитывается число нулей Num0 (и/или единиц Num1). Затем, при повторном проходе в том же линейном порядке, в соответствующее количество ячеек записываются нули, после чего – оставшиеся единицы. Ясно, что трудоемкость алгоритма при оценке O(N) составит в точности 2N. Однако, вместо двойного просмотра удается обойтись лишь одним проходом по массиву, то есть N шагами. Идея состоит в том, чтобы обрабатывать массив с двух сторон, двигаясь навстречу. Для этого инициализируются сразу две индексных переменных – IndexLeft и IndexRight, причем первая переменная увеличивается, начиная со значения 0, а вторая – уменьшается от N1. Начав просмотр вектора слева и пропуская все элементы, стоящие «на месте», то есть нули, добираемся до 1. Как только обнаруживается это «нарушение», переключаемся на обработку справа налево. Теперь, при попадании на «плохой» элемент справа, то есть на нуль, следует обменять содержимое компонент Mas[IndexLeft] и Mas[IndexRight], после чего, разумеется, оба индекса вновь «растут» в свою сторону. Процесс завершается, как только значения индексов совпадут, что неизбежно. Упражнение E2-3 Напишите программу, реализующую описанный механизм реорганизации 0–1 вектора.