Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Lab04_2010

.pdf
Скачиваний:
27
Добавлен:
07.06.2015
Размер:
1.13 Mб
Скачать

Рис. 36. Входная последовательность для случая, когда максимум расположен левее минимума.

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

Рис. 37. Сообщение о выходе за границы массива

В сообщении в скобках указаны (в виде гиперссылок) номера строк, которые и «вызвали» ошибку. Отметим, что строки указываются «по стеку», т.е. если мы начнем с нижней – это будет «начальная» точка, где возникла ошибка. Прежде чем мы займемся изучением этих строк, обратим внимание на то, что файл output.txt все же был сформирован. Переключившись на него, мы увидим довольно странный результат (рис. 38) – число 7 и очень много нулей.

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

Действительно, ответ совершенно непохож на то, что можно было бы ожидать: максимум и минимум разделены пятью (а не девятью) элементами, и если выводить их в обратном порядке, должна была бы получиться последовательность 2, 4, 7, 6, 2, но уж никак не единственная семерка.

Чтобы понять, какое из двух чисел 7 выводится, можно заменить одно из этих двух чисел каким-то другим (из диапазона от 2 до 6). Вы можете легко удостовериться, что на печать выводится последняя семерка, а не та, что находится между минимумом и максимумом.

§ 10. Поиск ошибки

Попробуем понять, что же произошло. Сначала посмотрим, на какие строки как на источник ошибок, указал компилятор Java. Перейдя по самой нижней гиперссылке

(SampleTask.java:26), мы окажемся на строке вызова метода solution (рис. 39):

Рис. 39. Ошибка случилась при вызове метода solution()

Если же теперь мы перейдем по гиперссылке на строку 96 (конечно, в нашем небольшом файле можно воспользоваться и ручной прокруткой, но в больших проектах, состоящих из нескольких файлов, удобнее пользоваться средством, предлагаемым средой), то увидим, что указанная строка – тело цикла, обеспечивающего вывод подпоследовательности между минимумом и максимумом (рис. 40).

Рис. 40. А в методе solution источником ошибки явилась строка, составляющая тело цикла

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

Переменные startIndex и stopIndex, которые и «ограничивают» область вывода получаются из значений «левых» и «правых» максимумов и минимумов (в зависимости от поставленных условий). Поскольку во входных данных есть только один минимум (на 9- ой позиции, если нумеровать элементы последовательности с нуля) и только один максимум (на 3-ей позиции), то значения iLeftMin и iRightMin, а также iLeftMax и iRightMax попарно совпадают.

Примечание.

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

Конечно, алгоритм поиска минимума или максимума в последовательности очень прост, и опытный программист вряд ли сделает в нем ошибку. Однако «хорошим тоном» является после написания каждого фрагмента кода выполнить его тестирование и убедиться, что он действительно делает то, что Вы планировали, и делает это правильно.

Следовательно, значения (iRightMax – iLeftMin) и (iLeftMax – iRightMin) также равны между собой по абсолютной величине, что приводит к истолкованию единственной

пары «минимум» и «максимум» как пары «правый» максимум и «левый» минимум. Код, который выполняется для такой пары (рис. 41), явно предполагает, что минимум находится левее, чем максимум, и поэтому номер элемента в массиве, с которого вывод начнется, определяется как номер минимума, увеличенный на 1, а номер, на котором вывод закончится, – как номер максимума, уменьшенный на 1. Соответственно, направление вывода (outputDirection) определяется как «прямое», от минимума к максимуму (в порядке возрастания индексов).

Рис. 41. Для единственной пары минимума и максимума программа всегда предполагает расположение «минимум левее максимума».

Что же происходит дальше? Переменная startIndex получает значение 10 (9+1), а переменная stopIndex – значение 2 (3–1). Из этих соотношений вычисляется длина (несуществующей) последовательности (len; получается равной 9), после чего организуется цикл по выводу элементов, начиная с 10-го, увеличивая индекс на 1, до тех пор, пока индекс не станет равным 2. Конечно, принципиально когда-то это может произойти: если Вы помните, как представлены целые числа со знаком в памяти компьютера, то знаете, что при добавлении единицы к максимальному (положительному) значению типа получится минимальное (отрицательное) значение типа (в случае типа int это 2147483647 и –2147483648 соответственно). Однако массив из 10000 элементов закончится раньше. И при попытке обращения к несуществующему элементу с номером 10000 произойдет ошибка выхода за границы массива, сообщение о которой и было получено.

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

Если в последовательности один минимум и один максимум, то возможны два варианта их взаимного расположения (рис. 42).

Рис. 42. Синим помечены минимумы, красным – максимумы

Если единственный минимум расположен слева, как на верхней части рисунка, то условный оператор (рис. 41) срабатывает правильно, если же справа – то нет (фактически выше мы только что рассмотрели эту ситуацию).

Если в последовательности один минимум и два максимума, то возможны три варианта их размещения (рис. 43, первому из изображенных (отчасти) соответствует входной файл input.04).

Рис. 43. Возможные взаимные расположения двух максимумов и одного минимума

Когда минимум располагается между двумя максимумами, сравнение будет выполнено корректно. Действительно, позиции iLeftMin и iRightMin совпадают, но iLeftMax и iRightMax различаются. Если расстояние между «левым» максимумом и минимумом окажется больше, нежели между «правым» максимумом и минимумом, то минимум в этом случае распознается как «правый» и находящийся правее максимума. Это в действительности так, и границы и направление вывода фрагмента последовательности определяются верно. Верно они определяются и в случае, когда большим оказывается расстояние между правым максимумом и минимумом.

Аналогичные рассуждения можно провести и для двух других взаимных расположений одного минимума и двух максимумов, а также одного максимума и двух минимумов (убедитесь в этом самостоятельно!). Ситуация, когда есть пара максимумов и пара минимумов, обсуждалась в первом параграфе.

Итак, рассуждения показывают, что имеется лишь один особый случай, ускользнувший от нашего внимания при первоначальном рассмотрении: случай, когда и минимум, и максимум в последовательности единственны. Установить факт единственности можно, сравнив попарно позиции iLeftMin, iRightMin и iLeftMax, iRightMax. Затем потребуется определить, что расположено левее – единственный минимум или единственный максимум, и, в зависимости от этого, вычислить значения startIndex, stopIndex и outputDirection. Фрагмент дополненного метода приведен на рис. 44.

После внесения исправлений сохраните и запустите программу на выполнение. Теперь по входной последовательности, приведенной на рис. 36, получается ожидаемая выходная последовательность (рис. 45). Сохраните входной и выходной файлы как input.07 и output.07 в папке test. Полезно проверить, как отразились изменения на тех тестах, которые программа уже проходила: во всяком случае, проверка теста № 6 должна быть выполнена обязательно, другие же тесты можно проверить выборочно.

Рис. 44. Фрагмент метода solution() с внесенными исправлениями

Рис. 45. Теперь программа выдает верный результат

С помощью окна Files откройте файл input.06 и скопируйте его содержимое в input.txt. Запустите программу на выполнение и убедитесь, что она выводит верный результат (совпадающий с output.06). Проверьте аналогичным образом выполнение еще, по крайней мере, двух тестов из сформированного набора.

Несмотря на то, что «программа-минимум» по тестированию еще не выполнена полностью (минимальный и максимальный набор данных еще не рассмотрены), у Вас может возникнуть желание отправить исправленную программу для проверки автоматизированной системой. Конечно, Вы можете это сделать. Однако Вы увидите сообщение об ошибке Runtime Error все на том же тесте № 5. Это значит, что логическая ошибка, обнаруженная нами, была не единственной, и поиски следует продолжить.

§ 11. Тестирование на входной последовательности минимальной длины

Следующим пунктом «программы-минимум» по тестированию будет пункт г) – изучение поведения программы на последовательности минимальной длины – т.е. состоящей из двух чисел (рис. 46).

Рис. 46. Входной файл, содержащий последовательность из двух элементов

Сохраните входной файл и запустите приложение на выполнение. В результате будет получено сообщение об ошибке (рис. 47, вновь ошибка выхода за границы массива), а файл output.txt будет заполнен длинной последовательностью нулей (рис. 48).

Рис. 47. Ошибка для входного файла, содержащего последовательность из двух элементов

Рис. 48. Выходной файл формируется неверно

Что же происходит в этом случае? Минимум и максимум здесь единственные. Переменные iLeftMin и iRightMin получают значение 0, а переменные iLeftMax и iRightMax – значение 1. Согласно фрагменту кода, который был добавлен в предыдущем параграфе, переменная startIndex получит значение 1, переменная stopIndex – значение 0, переменная outputDirection – значение 1. Формально все верно – но в логике, реализованной в методе solution, предполагалось, что минимум и максимум разделяет хотя бы один элемент. А когда длина выводимой последовательности – ноль, то эта логика «дает сбой». В этом случае предпринимается попытка вывести все элементы, начиная с первого и до нулевого, увеличивая индекс на каждом шаге. Дальнейшее было описано в параграфе 10.

Заметим, что эта ситуация воспроизведется для входной последовательности любой длины, если минимум и максимум в ней окажутся соседними элементами – причем не важно, какой из них будет расположен левее, а какой правее. Чтобы убедиться в этом, создадим такую входную последовательность. Но прежде сохраним текущий файл input.txt в папке test как input.08. Файл output.08 нужно создать в папке test вручную: он будет содержать 0 в первой строке и пустую вторую строку (рис. 49). В условии задачи

ничего не было сказано о том, как следует оформить ответ в случае пустой подпоследовательности, но логично предположить, что в этом случае вместо последовательности в файле должна быть пустая строка (по описанию он содержит две строки; случаи, которые не соответствуют «общему» описанию, как правило, оговариваются отдельно).

Рис. 49. Созданный вручную ouput.08 будет использоваться для дальнейшего контроля правильности работы программы.

Теперь изменим входные данные так (файл input.txt), как показано на рис. 50. Максимум (12) и минимум (1) в нем также являются соседями, однако, во-первых, последовательность состоит из большего числа элементов, и, во-вторых, минимум здесь расположен правее максимума (разумеется, можно протестировать и другой случай).

Рис. 50. Еще одна входная последовательность, приводящая к ошибке

Сохраните файл и запустите программу на выполнение. Вы увидите сообщение об ошибке (рис. 51), которое будет отличаться от предыдущих указанием другого индекса элемента массива (а именно, индекса –1), обращение к которому и привело к этой ошибке.

Рис. 51. Ошибка возникает при перемещении по массиву влево

Выходной файл оказывается при этом несформированным: переключение на вкладку output.txt позволяет нам увидеть лишь пустую строку (рис. 52).

Рис. 52. Выходной файл – просто пустая строка.

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

Как можно «обойти» эту ошибку? Например, можно проверить соответствие между знаком разности (stopIndex – startIndex) и знаком outputDirection. Когда мы полагаем, что индекс должен увеличиваться в «прямом» направлении, то тем самым полагаем и что начальный индекс не превосходит завершающего. Для «обратного» направляения аналогично: если индекс убывает, значит, мы должны двигаться от большего индекса к меньшему.

Если знаки оказываются разными, значит, фрагмент последовательности, который нужно вывести, пуст. В этом случае переменной len можно присвоить значение 0 и не выполнять цикл по выводу элементов. Измененный фрагмент кода метода solution() представлен на рис. 53.

Рис. 53. Исправленный с учетом нулевой длины последовательности код

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

После внесения изменений в код сохраните и запустите приложение на выполнение. Теперь сообщения об ошибке нет, и выходной файл правильно формируется

(рис. 54).

Рис. 54. Правильный выходной файл

Сохраните файлы input.txt и output.txt в папке test под именами input.09 и output.09.

Проверьте, верно ли работает программа на входных данных из файла input.08.

Нами найдена еще одна ошибка. Является ли она последней – мы еще не знаем. От проверки в системе пока воздержимся – до выполнения последнего пункта «программыминимум» тестирования.

§ 12. Тестирование на максимальном наборе входных данных

Небольшие по длине входные последовательности несложно составить вручную. Но сейчас мы хотим проверить работоспособность программы на последовательности максимально разрешенной в задаче длины – 10000 элементов.

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

Примечание.

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

Генератор случайных чисел реализован в классе Random из пакета java.util. В частности, метод nextInt(int n), которому передается положительный аргумент, генерирует (псевдо)случайное число в диапазоне от 0 до n–1 (включительно).

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

Рис. 55. Метод класса Task39 для генерации входного файла

Не забудьте, что класс Random должен быть импортирован из пакета java.util (рис. 56). Среда предложит Вам сделать это, как только Вы опишете переменную random – нужно просто согласиться с этим предложением.

Рис. 56. Необходимо импортировать класс Random

Теперь остается лишь дополнить метод main() вызовом метода createRandomInputFile() (рис. 57), и перед вызовом метода решения мы получим случайно сгенерированный входной файл из 10000 элементов.

Рис. 57. Сначала создадим файл с исходными данными

Соседние файлы в предмете Программирование на Java