Глава 39 Командная игра (массивы)



В чём сила компьютеров? В умении стремительно перемалывать огромные объёмы данных: сотни, тысячи, миллионы элементов! Под элементами данных мы разумеем числа, строки и тому подобное. Обратимся и мы к этой способности компьютера. Нет, с миллионом элементов погодим, начнем всего с нескольких: рассмотрим, к примеру, турнирную таблицу чемпионата.

Снежная лавина

Вот задача для болельщика: отсортировать команды в турнирной таблице чемпионата по убыванию набранных ими очков. Команд немного, всего 16. После каждого тура количество очков меняется, и таблица сортируется заново. Корпеть над этим вручную? – это не для нас! Итак, будущая программа должна принимать с клавиатуры очки, набранные командами, и распечатывать команды в порядке убывания этих чисел. При этом набранные очки мы будем вводить всегда в одном и том же порядке.

Сделаем это сначала для двух команд, пусть ими будут «Динамо» и «Спартак». Сортировка двух команд – что может быть проще?


{ ввод и сортировка двух команд (в программе 14 строк) }

var T1, T2 : integer;

begin

    Readln (T1, T2);     { Ввод очков для «Динамо» и «Спартак» }

    if T1>T2

    then begin

    Writeln('1.Динамо');

    Writeln('2.Спартак');

    end

    else begin

    Writeln('1.Спартак');

    Writeln('2.Динамо');

    end;

    Readln;

end.


Здесь для каждой из команд отведена переменная, хранящая набранные очки: T1 – для «Динамо» и T2 – для «Спартака». Вариантов расстановки всего два, поэтому и программа очень проста – всего 14 строк, не считая комментария.

Теперь добавим в чемпионат команду «Зенит». Вариантов расстановки стало втрое больше – шесть, и программа заметно усложнилась, вот она.


{ сортировка трех команд (в этой программе 45 строк) }

var T1, T2, T3 : integer;

begin

Readln (T1, T2, T3);     { «Динамо», «Спартак», «Зенит» }

if (T1>T2) and (T1>T3)

then begin

    Writeln('1.Динамо');

    if T2>T3

    then begin

    Writeln('2.Спартак');

    Writeln('3.Зенит');

    end

    else begin

    Writeln('2.Зенит');

    Writeln('3.Спартак');

    end

    end

else begin

    if (T2>T1) and (T2>T3)

    then begin

    Writeln('1.Спартак');

    if T1>T3

    then begin

    Writeln('2.Динамо');

    Writeln('3.Зенит');

    end

    else begin

    Writeln('2.Зенит');

    Writeln('3.Динамо');

    end

    end

    else begin

    Writeln('1.Зенит');

    if T1>T2

    then begin

    Writeln('2.Динамо');

    Writeln('3.Спартак');

    end

    else begin

    Writeln('2.Спартак');

    Writeln('3.Динамо');

    end

    end

    end;

Readln;

end.


Здесь уже 45 строк, что втрое больше, чем для двух команд. С добавлением последующих команд программа продолжит разбухать, как снежный ком. Для четырех команд она станет длиннее ещё в 4 раза (180 строк), для пяти – ещё в 5 раз (900 строк) и так далее. Дойдя до шестнадцати команд, мы насчитаем в программе триллионы строк. А ведь триллион – это «всего лишь» миллион миллионов! Скорей свернем с этой гибельной тропы, пока снежная лавина не накрыла нас с головой!

А где же волшебная палочка?

Вы ощущаете причину трудностей? В моих решениях нет циклов, способных выполнять огромное количество однообразных действий. Так, например, одним оператором цикла печатается хоть тысяча, хоть миллион чисел. Увы! Применить цикл к переменным с именами T1, T2 и T3 не получится. Хотя цифры в этих именах означают для нас порядковые номера команд, для компилятора они – всего лишь часть имени переменной, и не более. Как же втолковать компилятору то, чего мы добиваемся нумерацией переменных?

Для этого есть особый тип данных – массив переменных или, проще – массив. Вот она, спасительная волшебная палочка!

Массивы вокруг нас

Массив объединяет несколько однотипных переменных под одним общим именем. Отдельные переменные в массиве называют его элементами, и доступ к ним возможен по их номерам. Массивы придумали отнюдь не программисты. Возьмите любую спортивную команду – футбольную или хоккейную. Здесь, кроме фамилии, игрок снабжен номером, который лучше виден на поле. И это не единственный пример массива. Если отдельную переменную уподобить ящику с хранящейся в нём информацией, то массив переменных будет комодом с пронумерованными ящиками (рис. 88).



Рис.88 – Примеры простых переменных (слева) и массивов (справа)

Итак, массив – это собранные в одну команду переменные. Они получают общее на всех имя – имя своей команды. А внутри команды каждая переменная – элемент массива – обладает своим номером. Ну, чем не игроки?

Объявление массивов

Прежде, чем пользоваться массивом, его надо объявить: либо в секции VAR, либо через объявление пользовательского типа в секции TYPE.

Рассмотрим сначала первый способ, – объявим массив в секции VAR.


VAR Имя_Массива : ARRAY [..] OF <Тип элемента>


Здесь использована пара ключевых слов ARRAY… OF…, что переводится как «массив… из…». Имя массива – это обычный идентификатор, его программист придумывает сам; будем считать это имя названием команды переменных.

Справа, после двоеточия, указывают две характеристики массива: 1) диапазон для индексов и 2) тип элементов массива. Рассмотрим эти атрибуты массива подробней.

Диапазон для индексов определяет допустимые номера элементов внутри массива. Диапазон указывают в квадратных скобках после слова ARRAY, – это два выражения порядкового типа, условно обозначенные мною как MIN и MAX, они разделяются двумя точками. Говоря спортивным языком, здесь назначается диапазон номеров для «игроков команды».

После ключевого слова OF следует второй атрибут массива – тип данных для всех его элементов. Прибегнув вновь к спортивному языку, скажем, что здесь объявляют «вид спорта» для команды.

Вот пример объявления трех массивов: Names (фамилии), Ratings (оценки) и ChampShip (чемпионат).


VAR { объявления переменных-массивов }


    { 30 строковых переменных с фамилиями учеников класса }

    Names : ARRAY [1..30] OF string;


    { 30 байтовых переменных с оценками учеников этого класса }

    Ratings : ARRAY [1..30] OF byte;


    { 16 чисел с очками, набранными командами в чемпионате }

    ChampShip : ARRAY [1..16] OF integer;


Как видите, массив можно составить из элементов любого типа. Так, массив Names содержит внутри себя 30 переменных строкового типа: Names[1], Names[2] и так далее (номера переменных указывают в квадратных скобках).

Объявление массивов в секции VAR не слишком удобно. Почему? Рассмотрим следующий пример.


var A : array [1..5] of integer;

    B : array [1..5] of integer;

begin

    A:= B; { здесь компилятор видит ошибку несовместимости типов}

end.


Мы объявили массивы A и B; на первый взгляд, это массивы одного типа, поскольку каждый из них содержит по пять целых чисел. Для однотипных переменных, включая массивы, Паскаль допускает операцию копирования. Например, оператором


    A:=B


все элементы массива B копируются в элементы массива A. Увы, компилятор увидит здесь ошибку несовместимости типов. В чем дело? А в том, что он считает разнотипными массивы, объявленные в разных операторах. Даже если массивы совершенно одинаковы! Скажете, компилятор недостаточно умен? Может быть, но нам придётся как-то выкручиваться, и для этого есть два пути.

Во-первых, переменные A и B можно объявить в одном операторе.


var A, B : array [1..5] of integer;


Это устраняет проблему несовместимости типов.

Но есть и лучший способ – сначала объявить для массива пользовательский тип данных. Это делается в секции TYPE так:


TYPE Имя_Типа = ARRAY [..] OF <Тип элемента>


В сравнении с объявлением переменной разница мизерная: вместо двоеточия видим знак равенства, а вместо имени переменной – имя типа. Но каковы последствия! Объявите лишь однажды нужный вам тип, и тогда применяйте его, где угодно. Вот объявления типов для указанных выше переменных.


TYPE { примеры объявления типов-массивов }

    { тип для 30 строковых переменных с фамилиями учеников класса }

    TNames = ARRAY [1..30] OF string;


    { тип для 30 байтовых переменных с оценками учеников }

    TRatings = ARRAY [1..30] OF byte;


    { тип для 16 целых переменных с очками, набранными в чемпионате }

    TChampionShip = ARRAY [1..16] OF integer;


Здесь буква «T» в имени типа напоминает о назначении этого идентификатора (помните наше добровольное соглашение об именах?). Теперь учрежденные типы данных можно употребить для объявления переменных и параметров в любом месте программы, вот пример.


TYPE { тип для 30 байтовых переменных с оценками учеников }

    TRatings = ARRAY [1..30] OF byte;


VAR { 30 байтовых переменных с оценками учеников }

    Ratings : TRatings;


procedure ABC (var arg: TRatings); { параметр процедуры }

var A, B, C : TRatings;     { локальные переменные }

begin

    ...

end;


Здесь тип TRatings служит для объявления переменных и параметров в трех местах программы. В будущем мы всегда будем объявлять типы – как для массивов, так и для других сложных наборов данных.

Доступ к элементам (индексация)

Переменной-массивом можно ворочать как единым целым, например, при копировании одного массива в другой. Но чаще приходится работать с отдельными его элементами, как «выдернуть» их из массива?

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

Рассмотрим примеры доступа к элементам объявленных выше массивов.

Пример 1. Трем элементам массива Names присваиваем фамилии хоккеистов.


    Names[1]:= ’Петров’;

    Names[2]:= ’Михайлов’;

    Names[3]:= ’Харламов’;


Пример 2. Сравниваем третий и четвертый элементы массива Ratings. Здесь индексы заданы через целочисленную переменную n.


    Ratings[3]:= 12;

    Ratings[4]:= 8;

    n:=3;

    if Ratings[n] > Ratings [n+1] then … else …;


Как видите, индекс в массиве можно вычислять, а это открывает дорогу к циклам. И мы двинемся ею немедленно!

Ввод и вывод массивов

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

Взять, к примеру, массив Names, ввести который можно так:


    for i:=1 to 30 do Readln(F, Names[i]);


Здесь F – это открытый для чтения текстовый файл, каждая строка которого содержит фамилию.

На первый взгляд все просто. Просто, да не гладко, – это будет работать лишь с файлом, в котором не менее 30 строк (по числу циклов). А иначе случится ошибка: противозаконное чтение за пределами файла. Как избежать её? Внимательней присматривайте за концом файла, вот так:


    i:=1;

    { пока не конец файла и не введены все элементы }

    while not Eof(F) and (i<=30) do begin

    Readln(F, Names[i]);

    i:= i+1;

    end;


А вот ещё один хороший вариант.


    for i:=1 to 30 do begin

    if Eof(F) then break; { если конец файла, прервать цикл }

    Readln(F, Names[i]);

    end;


Вывод массива в файл не представляет труда, вот пример.


    for i:=1 to 30 do Writeln(F, Names[i]);


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

Ошибки индексации

Объявление массива, как сказано, содержит границы для индексов: MIN – номер первого элемента, и MAX – номер последнего. А что случится при попытке обратиться к элементу с меньшим, чем MIN номером? Или наоборот – с большим, чем MAX? Иначе говоря, что случится при попытке доступа к несуществующему элементу массива? Такие ошибки преследуют даже опытных программистов, а последствия зависят от способа, которым вы совершите сей проступок.

Предположим, в программу вкрался такой оператор:


    Names[200]:= ’Синичкин’;


Поскольку в массиве Names нет элемента с индексом 200, здесь вас остановит компилятор, – ошибка слишком явна, чтобы он промолчал. Вам не останется ничего иного, как исправить индекс, иначе программа не откомпилируется.

Но, когда индекс вычисляется при исполнении программы, нарушение границ проявляется и обрабатывается иначе, например:


    Readln(N);

    Writeln(Names[N]);


Нам не угадать, что введет пользователь в переменную N, – здесь ошибка нарушения границ может возникнуть при выполнении программы. В главе 27 мы рассматривали ошибки времени исполнения, – это как раз такой случай. Если указать индекс, выходящий за границы массива, то реакция программы будет зависеть от настройки компилятора, точнее, от опции контроля диапазонов. Напомню, что эта опция управляется директивой $R, а также доступна через меню по цепочке:

Options –> Compiler… –> Runtime Errors –> Range checking

Рассмотрим вариант компиляции при включенном контроле границ ($R+). Тогда, при нарушении границ индекса, программа выдаст аварийное сообщение «Range check error». То есть, она заметила нарушение границ индекса, «крикнула» об этом и прервала работу.

Теперь отключим контроль диапазонов ($R-) и перекомпилируем программу. Она станет «легче» и быстрее, и по ходу выполнения проверять границы не станет. Но ошибки не пройдут бесследно. Наоборот, последствия будут тяжелыми и непредсказуемыми! Отключать проверку диапазонов позволительно только в тщательно проверенной программе.

Лучший способ избежать нарушения границ индексов – взять проверку на себя. В данном случае это можно сделать так:


    repeat

    Readln(N);

    if N in [1..30]

    then Writeln(Names[N])

    else Writeln(’Ошибка! Введите индекс от 1 до 30’);

    until N in [1..30]


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

Итоги

• Массив – это сложный тип данных, объединяющий в себе несколько однотипных переменных – элементов массива.

• Все элементы массива носят одно общее имя – это имя самого массива. Внутри массива элементы различаются своими порядковыми номерами – индексами.

• В объявлении массива указывают две его характеристики: диапазон индексов и тип элементов.

• Индекс элемента может быть задан числом или выражением порядкового типа.

• Указание неверного индекса порождает ошибки либо при компиляции, либо при выполнении программы.

• Ввод массива из текстового файла и вывод в него возможен только поэлементно, для чего организуют цикл.

А слабо?

А) Массив A и переменная C объявлены так:


var A : array [’a’..’z’] of integer;

    C: char;


Допустимо ли такое объявление массива и почему? Сколько элементов содержит массив? Какие из указанных ниже операторов будут (или могут) вызывать ошибки нарушения диапазонов?


    A[’s’]:= 10;

    A[’R’]:= 10;

    C:=’d’; A[C]:= 10;

    Readln(C); A[C]:= 10;


Проверьте свои решения на практике.

Загрузка...