115
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ федеральное государственное бюджетное образовательное учреждение высшего профессионального образования «УЛЬЯНОВСКИЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ» А. Б. Шамшев, К. В. Святов АЛГОРИТМИЧЕСКОЕ МЫШЛЕНИЕ ПРИ РЕШЕНИИ ЗАДАЧ (на примере языка С#) Учебное пособие Ульяновск УлГТУ 2012

на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

  • Upload
    others

  • View
    2

  • Download
    0

Embed Size (px)

Citation preview

Page 1: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ федеральное государственное бюджетное образовательное учреждение

высшего профессионального образования

«УЛЬЯНОВСКИЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ»

А. Б. Шамшев, К. В. Святов

АЛГОРИТМИЧЕСКОЕ МЫШЛЕНИЕ

ПРИ РЕШЕНИИ ЗАДАЧ (на примере языка С#)

Учебное пособие

Ульяновск УлГТУ

2012

Page 2: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

УДК 004.4 (075) ББК 32.973-018 я7 Ш 19

Рецензенты: кафедра «Информационные технологии» Ульяновского государственного университета (зав. кафедрой, к.ф.-м. н. М. А. Волков);

заместитель генерального директора по качеству, эффективности и инженерно-техническому обеспечению А. А. Емельянов

Утверждено редакционно-издательским советом университета

в качестве учебного пособия Шамшев, А. Б. Алгоритмическое мышление при решении задач (на примере языка

C#) : учебное пособие / А. Б. Шамшев, К. В. Святов. – Ульяновск : УлГТУ, 2012. – 114 с.

ISBN 978-5-9795-1050-7 Представлены примеры решения задач, их декомпозиции на составные части.

Реализация задач приведена с использованием языка C# (версия 3.0 и выше). Учебное пособие предназначено для бакалавров, изучающих

программирование на языках высокого уровня (специальность 23010062 «Информатика и вычислительная техника»), а также для студентов других специальностей, связанных с программированием.

Подготовлено на кафедре «Вычислительная техника» Ульяновского государственного технического университета.

УДК 004.4 (075)

ББК 32.973-018 я7

© Шамшев А. Б., Святов К. В., 2012.

ISBN 978-5-9795-1050-7 © Оформление. УлГТУ, 2012.

Ш 19

Page 3: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

3

Содержание Введение ............................................................................................................................................. 5 

Задача 1: Обнаружить утечку памяти .............................................................................................. 6 

Задача 2 : Составить тесты к программе ......................................................................................... 9 

Задача 3а: Напечатать все содержимое файла на экране. ............................................................ 18 

Задача 3б: Напечатать все содержимое файла (оптимизация задачи 3). .................................... 20 

Задача 4а. Удалить все нечетные элементы из массива ............................................................... 23 

Задача 4б: Удалить все нечетные элементы из массива (оптимизация) ..................................... 29 

Задача 4в. Удалить все нечетные элементы из массива (последующая оптимизация) ............ 32 

Задача 4г: Удалить все нечетные элементы из массива (основа тестов) .................................... 35 

Задача 5а: В файле удалить все слова, которые начинаются и заканчиваются одной буквой ...... 38 

Задача 5б: В файле удалить все слова, которые начинаются и заканчиваются одной буквой (без использования потоков и std::string) ...................................................................................... 41 

Задача 6а: Удалить строки, в которых есть два одинаковых элемента (без использования std::vector) ......................................................................................................................................... 48 

Задача 6б: Удалить строки, в которых есть два одинаковых элемента (c использованием std::vector) ......................................................................................................................................... 56 

Задача 7: Отсортировать содержимое словаря.............................................................................. 58 

Задача 8. Реализовать сохранение и загрузку пользовательских структур данных с использованием fstream ................................................................................................................... 62 

Задача 9: Обработка списков фамилий студентов ........................................................................ 67 

Задача 10: Исследовать внутреннее представление любых численных типов .......................... 73 

Представление беззнаковых целых чисел в памяти ЭВМ ........................................................... 74 

Пример 1 ........................................................................................................................................... 74 

Пример 2 ........................................................................................................................................... 74 

Пример 3 ........................................................................................................................................... 76 

Представление знаковых целых чисел в памяти ЭВМ ................................................................. 76 

Обратный код ................................................................................................................................... 76 

Пример 1. .......................................................................................................................................... 76 

Пример 2. .......................................................................................................................................... 77 

Пример 3. .......................................................................................................................................... 77 

Пример 4. .......................................................................................................................................... 78 

Пример 5. .......................................................................................................................................... 78 

Дополнительный код ....................................................................................................................... 80 

Пример 1. .......................................................................................................................................... 80 

Пример 2. .......................................................................................................................................... 80 

Пример 3. .......................................................................................................................................... 81 

Пример 4. .......................................................................................................................................... 81 

Page 4: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

4

Пример 5. .......................................................................................................................................... 82 

Преобразование из машинного представления в десятичную систему ...................................... 83 

Пример 1. .......................................................................................................................................... 83 

Пример 2. .......................................................................................................................................... 83 

Пример 3. .......................................................................................................................................... 83 

Важное следствие (пример 1). ........................................................................................................ 84 

Важное следствие (пример 2). ........................................................................................................ 85 

Представление вещественных типов в памяти ЭВМ ................................................................... 86 

Пример 1 ........................................................................................................................................... 86 

Пример 2 ........................................................................................................................................... 87 

Переход в двоичную систему ......................................................................................................... 88 

Пример 1 ........................................................................................................................................... 88 

Пример 2 ........................................................................................................................................... 88 

Пример 3 ........................................................................................................................................... 89 

Пример 4 ........................................................................................................................................... 89 

Представление числа «0» в памяти ЭВМ. ..................................................................................... 90 

Общий алгоритм сложения (или вычитания) чисел с плавающей точкой ................................. 91 

Пример 1 ........................................................................................................................................... 91 

Пример 2 ........................................................................................................................................... 92 

Алгоритмы работы с числами, которые представлены строкой ................................................. 92 

Сложение положительных чисел, представленных в виде строки ............................................. 93 

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

Умножение двух положительных целых чисел, представленных в виде строки ...................... 98 

Реализация алгоритма на Pascal ..................................................................................................... 99 

Сравнение двух целых чисел, представленных в виде строки .................................................. 101 

Реализация алгоритма на Pascal ................................................................................................... 101 

Вычитание целых чисел, представленных в виде строки .......................................................... 102 

Реализация алгоритма на Pascal ................................................................................................... 102 

Реализация алгоритмов работы с целыми числами, представленными в виде строки на C# ... 103 

Реализация алгоритмов работы с целыми числами, представленными в виде строки на C++ .................................................................................................................................................. 106 

Заключение ..................................................................................................................................... 112 

Глоссарий ........................................................................................................................................ 113 

Список рекомендуемой литературы ............................................................................................ 114 

Page 5: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

5

Введение Одними из основных языков программирования являются языки С и С++.

В них есть много элементов, которые позволяют быстро и эффективно решать поставленные задачи.

Выписка из ГОС ВПО

ОПД. Программирование на языках высокого уровня: Ф.05 Основные этапы решения задач на ЭВМ; критерии качества

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

Page 6: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

6

Задача 1: Обнаружить утечку памяти Рассуждения: 1. Следует отметить, что С++ обладает необходимостью контроля памяти,

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

2. В Visual Studio 2010 эта возможность предусмотрена. Не будем углубляться в устройство, воспользуемся предлагаемым решением. #include <crtdbg.h> #include <iostream>  using namespace std;  int main(int argc, char * argv[]) {   _CrtSetDbgFlag(33);     int size;   cin >> size;     int *a = new int[size];     return 0; } 

3. Суть программы проста – мы вводим корректно число с клавиатуры, потом выделяем память под массив нужного размера и «забываем» ее освободить. После запуска программы и ее завершения в окне «Output» мы увидем следующие надписи:

Page 7: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

7

4. Самая главная из них – Detected memory leaks. Это означает, что в программе есть утечка памяти. В данном случае ее размер равен 16 байтам. Конечно, в данной программе легко догадаться, где находится утечка памяти. Однако в больших программах это сделать не так просто. Поэтому можно еще улучшить программу:

#include <crtdbg.h> #include <iostream> using namespace std; #ifdef _DEBUG #define DEBUG_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__) #define new DEBUG_NEW #endif int main(int argc, char * argv[]) { _CrtSetDbgFlag(33); int size; cin >> size; int *a = new int[size]; return 0; }

5. После запуска этой программы мы уже увидем более полное сообщение об утечке памяти

Теперь в нем видны не только факт утечки памяти, но и имя файла, и номер строки в нем, где была выделена память, которая не была удалена. Именно этот способ самый удобный.

6. Приведем пример верной программы, в которой нет утечки памяти.

#include <crtdbg.h> #include <iostream> using namespace std;

Page 8: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

8

#ifdef _DEBUG #define DEBUG_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__) #define new DEBUG_NEW #endif int main(int argc, char * argv[]) { _CrtSetDbgFlag(33); int size; cin >> size; int *a = new int[size]; delete[] a; return 0; }

После запуска данной программы в окне output надписи «Detected memory leaks» не будет, потому что есть операция освобождения памяти, и утечки памяти нет. Следует отметить, что если память выделялась через операцию new … [] , то удаляться эта память должна через delete [], а если память выделялась через операцию new «без квадратных скобок», то и удаляться она должна через операцию delete «без квадратных скобок».

 

Page 9: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

9

Задача 2 : Составить тесты к программе Рассуждения: 1. В современном мире использование Unit тестирования во время

работаботки программного обеспечения является почти повсеместным. Поэтому в этой задаче мы рассмотрим составление тестов к программе на примере программы сложения двух чисел.

2. Известно, что к Visual Studio существует множество различных плагинов, которые могут быть платными или бесплатными, и добавляют различные функциональности к возможностям Visual Studio. Один из таких плагинов – Visual Assert (http://www.visualassert.com). Данный плагин очень просто установить в Visual Studio и работать с ним довольно легко. Поэтому мы остановимся на нем.

3. Составим программу (файл назовем main.cpp), которая содержит класс для сложения двух чисел.

#include <iostream> using namespace std; class ClassForTest { public: int getSum(int a, int b) { return a + b; } }; int main(int argc, char * argv[]) { ClassForTest testClass = ClassForTest(); cout << testClass.getSum(1,2); return 0; }

4. Следует отметить, что на самом деле функция getSum должна быть статической. Но это сейчас нас не интересует, потому что наша задача – составить тесты. Если программу запустить, то в консоли появится число 3, и она сразу закроется.

5. По хорошему тону программирования тесты необходимо создавать в отдельном файле. Однако это потребует выноса ClassForTest в отдельный h файл (header file в папке проекта Header Files) и реализации в отдельный cpp файл (C++ File в папке проекта Source Files). Сделаем это. После этого в Solution Explorer будет следующая структура:

Page 10: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

10

А содержимое файлов будет следующим: ClassForTest.h

#pragma once class ClassForTest { public: int getSum(int a, int b); };

 

ClassForTest.cpp

#include "ClassForTest.h" int ClassForTest::getSum(int a, int b) { return a + b; }

 

main.cpp

#include <iostream> #include "ClassForTest.h" using namespace std; int main(int argc, char * argv[]) { ClassForTest testClass = ClassForTest(); cout << testClass.getSum(1,2); return 0; }

Page 11: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

11

6. Теперь составим тесты. Для этого создадим С++ файл с названием

tests.cpp. Его содержимое представлено ниже:

#include "ClassForTest.h" #include <cfixcc.h> class ExampleTest : public cfixcc::TestFixture { public: void test1() { ClassForTest z; int c = z.getSum(1, 2); CFIXCC_ASSERT_EQUALS(c, 3); } void test2() { ClassForTest z; int c = z.getSum(2, 2); CFIXCC_ASSERT_EQUALS(c, 4); } }; CFIXCC_BEGIN_CLASS(ExampleTest) CFIXCC_METHOD(test1) CFIXCC_METHOD(test2) CFIXCC_END_CLASS()

7. Отметим некоторые важные моменты в этом коде: a. #include <cfixcc.h> – подключает класс, от которого необходимо

наследоваться для создания тестов, b. public cfixcc::TestFixture – это наследование от базового класса

для тестов, c. CFIXCC_BEGIN_CLASS(ExampleTest)… CFIXCC_END_CLASS()

– внутри этого указываются, какие методы необходимо запускать во время тестирования.

8. Теперь перейдем в главном меню в «Visual Assert», подпункт «Test Explorer Window». В появившемся окне выберем кнопку «Run Tests without debugging»

Page 12: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

12

9. После этого мы можем просмотреть результаты тестирования (после разворачивания всех «плюсиков» панель Test Run представлена на рисунке):

10. Как видно из него, оба теста прошли успешно. Если у нас будет неверный тест или ошибка в кодах, то соответствующий тест не пройдет, и картина будет следующей (по ней очень легко понять, что test2 не прошел):

11. Однако теперь при запуске приложения (F5) мы будем видеть следующее сообщение (и программа не будет запускаться):

Page 13: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

13

12. Эту проблему легко исправить. Логично предположить, что тесты являются дополнительным кодом, который облегчает поддержку кода. Поскольку во время использования программы в Debug и в Release версии он не нужен, мы создадим специальную Test конфигурацию, в которую вынесем тесты.

13. Перейдем в Configuration Manager, как показано на рисунке

14. После этого появится следующее окно:

15. Создадим новую конфигурацию для Solution

Page 14: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

14

16. После выбора «New» появится окно, в котором надо ввести название новой конфигурации и от какой конфигурации брать настройки. Вид окна после заполнения представлен на рисунке.

17. После заполнения нажмем кнопку «OK» и закроем Configuration Manager. Проект будет находится в Test конфигурации.

18. Теперь зайдем в свойства проекта

Page 15: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

15

19. Перейдем в Configuration Properties, в С/С++,

Page 16: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

16

20. В Preprocessor Defenitions выберем пункт Edit, и появится следующее окно:

21. Туда добавим новую символьную константу – TEST, как показано на рисунке:

Page 17: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

17

22. Нажмем кнопку «OK» и вернемся к файлу tests.cpp. В начале файла добавим строку «#ifdef TEST», а в конце – «#endif». Тогда файл будет выглядеть следующим образом (середина файла не представлена для уменьшения объема):

#ifdef TEST #include "ClassForTest.h" #include <cfixcc.h> //старое содержимое CFIXCC_BEGIN_CLASS(ExampleTest) CFIXCC_METHOD(test1) CFIXCC_METHOD(test2) CFIXCC_END_CLASS() #endif

23. Теперь для запуска тестов необходимо перейти в Test конфигурацию и

работать с тестами. А для запуска приложения – перейти в Debug или Release конфигурацию и работать с приложением.

 

Page 18: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

18

Задача 3а: Напечатать все содержимое файла на экране Рассуждения: 1. Для того чтобы напечать все содержимое на экране, необходимо знать

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

cout << "enter filename: " << flush; string dictionary_name; cin >> dictionary_name;

2. После получение имени файла его надо попробовать открыть. Отметим, что в языке С++ существует тип ifstream, который в расшифровке означает «input file stream», т. е. поток чтения. Успешность открытия потока можно проверить с помощью функции is_open. Поэтому следующий блок кода для проверки корректности открытия файла будет выглядеть так:

ifstream ifs(dictionary_name); if (!ifs.is_open()) { cout << "Could not open file: " << dictionary_name << endl; return 1; }

3. Содержимое файла – множество строк. Заметим, что в данном случае под множеством понимается не «классическое» множество из курса математических дисциплин, а то, что в нем содержится несколько строк.

4. Раз у нас есть множество строк в файле, то необходимо пройти по нему. Вознимает вопрос – сколько раз надо пройти по файлу? Ответ на вопрос весьма логичен – пока не дойдем до конца файла. Для определения того, дошли до конца файла или нет, можно воспользоваться функцией eof, которая есть у ifstream (в нашем случае это переменная ifs).

5. Естественно, после окончания работы необходимо закрыть поток из файла. Это можно делать с помощью функции close.

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

7. Тогда алгоритм чтения всех строк будет таков:

vector<string> lines; //здесь будем хранить строки //пока не дошли до конца файла while (ifs.eof() == false) { string curLine; getline(ifs, curLine); lines.push_back(curLine); } ifs.close();   

Page 19: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

19

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

for (int i = 0; i < lines.size(); i++) { cout << lines[i] << endl; }

9. Приведем полный код решения задачи:

#include <iostream> #include <fstream> #include <string> #include <vector> using namespace std; int main() { //ввод имени файла cout << "enter filename: " << flush; string dictionary_name; //dictionary_name = "D:\\sample.txt"; cin >> dictionary_name; //открытие файла ifstream ifs(dictionary_name); //проверка на корректность открытия файла if (!ifs.is_open()) { cout << "Could not open file: " << dictionary_name <<endl; return 1; } //здесь будем хранить строки vector<string> lines; //пока не дошли до конца файла while (ifs.eof() == false) { string curLine; getline(ifs, curLine); lines.push_back(curLine); } ifs.close(); cout << endl; for (int i = 0; i < lines.size(); i++) { cout << lines[i] << endl; } cout << "----- file end -------" << endl << "press enter"; char z; cin.get(z); return 0; }

Page 20: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

20

Задача 3б: Напечатать все содержимое файла (оптимизация задачи 3)

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

реально произвольный доступ. При чтении в пункте 7 задачи 3 мы добавляли строки в конец вектора, а при выводе в пункте 8 мы перемещались по индексам последовательно, «без скачков». Так же мы добавляли строки только в конец массива. Именно это подсказывает оптимизацию решения – вместо vector для хранения строк использовать ДЕК. Заметим, что операция добавления в конец ДЕК занимает константное время. Поэтому часть программы для чтения строки из файла можно оптимизировать следующим образом:

//здесь будем хранить строки deque<string> lines; //пока не дошли до конца файла while (ifs.eof() == false) { string curLine; getline(ifs, curLine); lines.push_back(curLine); } ifs.close();

 

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

//здесь будем хранить строки deque<string> lines; string curLine; //пока что-то получаем из файла while (getline(ifs, curLine)) { lines.push_back(curLine); } ifs.close();

3. Чтобы оптимизировать 8-й пункт решения задачи 3а, необходимо

отметить последовательность перемещения по элементам вектора. Оптимизацию можно произвести с использованием стандартной функции for_each. Она может принимать 3 параметра – начало последовательности, окончание последовательности, указатель на функцию действия с одним элементом последовательности.

Page 21: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

21

4. Начало последовательности можно найти с помощью функции ДЕКА begin(), а окончание последовательности – с помощью функции ДЕКА end().

5. Необходимо написать функцию для вывода одного элемента последовательности:

void printLine(string line) { cout << line << endl; }

6. Приведем полный код оптимизированного решения:

#include <iostream> #include <fstream> #include <string> #include <deque> #include <algorithm> using namespace std; void printLine(string line) { cout << line << endl; } int main() { //ввод имени файла cout << "enter filename: " << flush; string dictionary_name; dictionary_name = "D:\\sample.txt"; //cin >> dictionary_name; //открытие файла ifstream ifs(dictionary_name); //проверка на корректность открытия файла if (!ifs.is_open()) { cout << "Could not open file: " << dictionary_name <<endl; return 1; } //здесь будем хранить строки deque<string> lines; //пока не дошли до конца файла while (ifs.eof() == false) { string curLine; getline(ifs, curLine); lines.push_back(curLine); } ifs.close(); cout << endl; for_each(lines.begin(), lines.end(), printLine); cout << "----- file end -------" << endl << "press enter";

Page 22: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

22

char z; cin.get(z); return 0; }

Page 23: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

23

Задача 4а: Удалить все нечетные элементы из массива Размышления. 1. Чтобы удалить элементы из массива, необходимо создать массив.

А для создания массива необходимо получить размер массива. Пусть пользователь вводит размер массива с клавиатуры.

2. Пользователь вводит размер с клавиатуры, которая является стандартным устройством ввода. Поэтому можно использовать поток ввода cin. Следует отметить, что для использования cin необходимо подключть подуль #include <iostream>. И если быть более точным, то использовать надо не cin, а std::cin. Но для сокращения записи подключим пространство имен std с помощью using namespace std.

3. Для операции ввода из стандартного потока используется оператор >>. Поэтому первый шаг в реализации получения размера будет:

#include <iostream> using namespace std; int main(int argc, char* argv[]) { int size; cin >> size; cout << "user enter value: " << size;a return 0; }

4. Но в этом решении есть недостаток – если пользователь введет некорректное значение, то программа упадет. Для исправления этого недостатка воспользуемся тем, что оператор >> является функцией, которая возвращает успешность или неуспешность операции. Если операция не успешна, то надо восстановить корректное состояние различных флагов с помощью функции clear() и убрать все данные, которые находятся в буфере. Заметим, что последним символом в буфере будет символ ‘\n’. После очистки входного потока надо повторно перейти к получению числа. Поэтому реализация считывания корректного числа будет следующая:

#include <iostream> using namespace std; int getCorrectValue() { while (true) { int size; if (!(cin >> size)) { cin.clear(); while(std::cin.get()!= '\n'); continue; } return size;

Page 24: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

24

} } int main(int argc, char* argv[]) { int size = getCorrectValue(); cout << "user enter value: " << size; return 0; }

5. Теперь можно перейти к созданию массива. Следует отметить, что для создания массива используется оператор new[], поэтому для его удаления необходимо использовать delete[]. С учетом этого следующий шаг в реализации задачи будет:

#include <iostream> using namespace std; int getCorrectValue() { while (true) { int size; if (!(cin >> size)) { cin.clear(); while(std::cin.get()!= '\n'); continue; } return size; } } int main(int argc, char* argv[]) { int size = getCorrectValue(); if (size < 0) { cout << "array size can not be negative number"; return 1; } int *data = new int[size]; delete[] data; return 0; }

6. Теперь возникает вопрос о заполнении массива. Пусть массив заполняется случайными числами. В С++ для генерации случайных чисел используется функция rand(), которая возвращает числа в диапазоне от 0 до RAND_MAX, которая является константой. Но в общем случае нам необходимо получать числа в диапазоне от [min;max). Поэтому для генерации числа необходимо адаптировать эту

Page 25: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

25

функцию. Если диапазон [0;RAND_MAX) поделить на RAND_MAX, то получим диапазон [0;1). Если диапазон от [0;1) умножить на (max-min), то получим диапазон [0;max-min). Если к результату прибавить min, то получим диапазон от [min; max). Поэтому функция генерации случайного числа будет выглядить следующим образом:

int getRandValue(int max, int min) { return (((double)rand())/RAND_MAX)*(max - min) + min; }

В ней отдельно следует отметить преобразование результата функции rand() в тип double. Если этого преобразования не будет, то rand()/RAND_MAX будет являтся операцией целое/целое, и оператор «/» будет является целочисленным делением, и результатом всегда будет 0. Поэтому для того чтобы оператор «/» стал делением, необходимо использовать явное преобразование в тип double. Еще одним обязательным действием вяляется инициализация генератора случайных чисел с помощью функции srand(). Следует отметить, что время запуска программы каждый раз является разным, поэтому в качестве стартового значения можно воспользоваться результатом функции time(). В кодах инициализация генератора случайных чисел будет следующая:

srand(time(NULL));

7. Теперь можно написать функцию заполнения массива случайными числами.

void fillArray(int *data, int size) { for (int i = 0; i < size; i++) { data[i] = getRandValue(-5, 10); } }

8. Следующей задачей является вывод массива до обработки и после обработки. Для вывода массива нам нужно знать сам массив, его размер. Но и так же потребуется «заголовок» для вывода массива (чтобы пользователь мог отличать массивы до обработки и после обработки). Заголовок – это строка. Можно воспользоваться типом std::string (подключив модуль #include <string>), но можно воспользоваться char * (т. к. в данном случае строка заголовка при выводе не будет меняться). Реализация этой задачи в кодах будет следующая:

void printArray(char *caption, int *data, int size) { cout << caption << ": "; for (int i = 0; i < size; i++) {

Page 26: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

26

cout << data[i] << " "; } cout << "\n"; }

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

void delByIndex(int *&data, int &size, int index) { int *newData = new int[size - 1]; for (int i = 0; i < index; i++) { newData[i] = data[i]; } for (int i = index; i < size - 1; i++) { newData[i] = data[i + 1]; } delete[] data; data = newData; size--; } void mainAction(int *&data, int &size) { for (int i = size - 1; i >= 0; i--) { if (data[i] % 2 == 1) { delByIndex(data, size, i); } } }

10. Наконец можно реализовать всю задачу. Следует отметить, что пользователю желательно реализовать задержку после завершения работы, чтобы он мог увидеть результаты работы программы. Для этого воспользуемся функцией _getch(). Заметим, что она без проблем работает в Visual C++, но в других компиляторах может называться getch() или отсутствовать. Но она не носит принципиального характера для логики решения задачи, поэтому воспользуемся ею.

#include <time.h> #include <conio.h> #include <iostream> using namespace std; void delByIndex(int *&data, int &size, int index) {

Page 27: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

27

int *newData = new int[size - 1]; for (int i = 0; i < index; i++) { newData[i] = data[i]; } for (int i = index; i < size - 1; i++) { newData[i] = data[i + 1]; } delete[] data; data = newData; size--; } void mainAction(int *&data, int &size) { for (int i = size - 1; i >= 0; i--) { if (data[i] % 2 == 1) { delByIndex(data, size, i); } } } int getCorrectValue() { while (true) { int size; if (!(cin >> size)) { cin.clear(); while(std::cin.get()!= '\n'); continue; } return size; } } void printArray(char *caption, int *data, int size) { cout << caption << ": "; for (int i = 0; i < size; i++) { cout << data[i] << " "; } cout << "\n"; } int getRandValue(int max, int min) { return (((double)rand())/RAND_MAX)*(max - min) + min; } void fillArray(int *data, int size) { for (int i = 0; i < size; i++) { data[i] = getRandValue(-5, 10); } } int main(int argc, char* argv[]) {

Page 28: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

28

srand(time(NULL)); cout << "enter array size: "; int size = getCorrectValue(); if (size < 0) { cout << "array size can not be negative number"; _getch(); return 1; } int *data = new int[size]; fillArray(data, size); printArray("before process", data, size); mainAction(data, size); printArray("after process", data, size); delete[] data; _getch(); return 0; }

Page 29: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

29

Задача 4б: Удалить все нечетные элементы из массива (оптимизация) 

Следует отметить, что в С++ существует Standart Type Library (STL). В ней есть множество готовых классов и функций, которые можно использовать.

Название Реализуемая структура

bitset Битовый массив deque Очередь с двухсторонним доступом list Двухсвязанный список map Ассоциативный массив multimap Мультиотображение queue Односторонная очередь (контейнер-адаптор) set Множество stack Стек (контейнер-адаптор) vector Динамический массив

В этой таблице некоторые строки отмечены как контейнеры-адапторы. Это

означает, что они являются «оберткой» над другим классом, но этот тип структур реализован на чем-то ином (для примера, queue реализован на list).

Одним из готовых классов является класс vector, для работы с которым надо подключить #include <vector>. Заметим, что он является шаблонным. Для его использования (для работы с целыми числами) надо написать std::vector<int>. Он обладает множеством полезных методов. Поэтому предыдущий вариант решения можно оптимизировать.

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

void fillVector(vector<int> &data, int size) { data.reserve(size); for (int i = 0; i < size; i++) { data.push_back(getRandValue(-5, 10)); } }

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

void printVector(char *caption, vector<int> &data) { cout << caption << ": "; for (int i = 0; i < data.size(); i++) { cout << data[i] << " ";

Page 30: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

30

} cout << "\n"; }

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

3. Для следующей оптимизации необходимо учесть, что vector имеет функцию erase для удаления цепочки элементов. Цепочка характеризуется позицией начала и позицией конца. При этом удаляются все элементы в диапазоне [начало, конец). Так же отметим, что начало цепочки указывается от начала вектора, которое можно получить с помощью функции вектора begin(). C учетом этого главная функция обработки будет такова:

void mainAction(vector<int> &data) { for (int i = data.size() - 1; i >= 0; i--) { if (data[i] % 2 == 1) { data.erase(data.begin() + i, data.begin() + i + 1); } } }

 

4. Приведем полный код решения:

#include <vector> #include <time.h> #include <conio.h> #include <iostream> using namespace std; void mainAction(vector<int> &data) { for (int i = data.size() - 1; i >= 0; i--) { if (data[i] % 2 == 1) { data.erase(data.begin() + i, data.begin() + i + 1); } } } int getCorrectValue() { while (true) { int size; if (!(cin >> size)) { cin.clear();

Page 31: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

31

while(std::cin.get()!= '\n'); continue; } return size; } } void printVector(char *caption, vector<int> &data) { cout << caption << ": "; for (int i = 0; i < data.size(); i++) { cout << data[i] << " "; } cout << "\n"; } int getRandValue(int max, int min) { return (((double)rand())/RAND_MAX)*(max - min) + min; } void fillVector(vector<int> &data, int size) { data.reserve(size); for (int i = 0; i < size; i++) { data.push_back(getRandValue(-5, 10)); } } int main(int argc, char* argv[]) { srand(time(NULL)); cout << "enter array size: "; int size = getCorrectValue(); if (size < 0) { cout << "array size can not be negative number"; _getch(); return 1; } vector<int> data; fillVector(data, size); printVector("before process", data); mainAction(data); printVector("after process", data); _getch(); return 0; }

   

Page 32: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

32

Задача 4в: Удалить все нечетные элементы из массива (последующая оптимизация) 

Необходимо отметить, что STL имеет множество готовых функций. Для работы с этими функциями следует подключить #include <algorithm>. Одна из этих функций – remove_if, которая возвращает итератор на элементы, которые подходят под условие. В нашем случае (когда вектор чисел) этот итератор будет иметь тип vector<int>::iterator

1. Для использования функции remove_if вводим функцию-предикат, которая будет указывать на необходимость удаления элемента

bool isOdd(int s) { return s % 2 == 1; }

2. При вызове функции указываем диапазон вектора, который надо проверять. В нашем случае это весь вектор – от начала (vector.begin()) до конца (vector.end()). Так же следует указать функцию, которая будет указывать на необходимость удаления элемента – в нашем случае это функция isOdd.

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

void mainAction(vector<int> &data) { vector<int>::iterator newEnd = remove_if(data.begin(),data.end(), isOdd); data.erase(newEnd, data.end()); }

4. Заметим, что данную функцию в Visual C++ можно сократить, используя ключевое слово auto (полный аналог var в C# - автоопределяемые типы) и анонимные функции (для замены функции isOdd). Тогда главная функция примет следующий вид:

void mainAction(vector<int> &data) { auto newEnd = remove_if(data.begin(), data.end(), [](int s) {return s % 2==1; }); data.erase(newEnd, data.end()); }

Обратим внимание, что эти преобразования будут работать только в Visual C++, поскольку auto в других компиляторах С++ имеет другой смысл. Поэтому такими оптимизациями мы пользоваться не будем.

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

Page 33: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

33

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

void printVector(char *caption, vector<int> &data) { cout << caption << ": "; for (vector<int>::iterator i = data.begin(); i != data.end(); ++i) { cout << *i << " "; } cout << "\n"; }

6. Приведем полный код решения задачи:

#include <vector> #include <time.h> #include <conio.h> #include <iostream> #include <algorithm> using namespace std; bool isOdd(int s) { return s % 2 == 1; } void mainAction(vector<int> &data) { vector<int>::iterator newEnd = remove_if(data.begin(), data.end(), isOdd); data.erase(newEnd, data.end()); } int getCorrectValue() { while (true) { int size; if (!(cin >> size)) { cin.clear(); while(std::cin.get()!= '\n'); continue; } return size; } } void printVector(char *caption, vector<int> &data) { cout << caption << ": "; for (int i = 0; i < data.size(); i++) { cout << data[i] << " "; } cout << "\n"; }

Page 34: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

34

int getRandValue(int max, int min) { return (((double)rand())/RAND_MAX)*(max - min) + min; } void fillVector(vector<int> &data, int size) { data.reserve(size); for (int i = 0; i < size; i++) { data.push_back(getRandValue(-5, 10)); } } int main(int argc, char* argv[]) { srand(time(NULL)); cout << "enter array size: "; int size = getCorrectValue(); if (size < 0) { cout << "array size can not be negative number"; _getch(); return 1; } vector<int> data; fillVector(data, size); printVector("before process", data); mainAction(data); printVector("after process", data); _getch(); return 0; }

Page 35: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

35

Задача 4г: Удалить все нечетные элементы из массива (основа тестов) 

Теперь составим тесты для данной задачи. 1. Чтобы легче было писать тесты, необходима функция ввода, которая

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

void handFillVector(vector<int> &data, int size) { data.reserve(size); for (int i = 0; i < size; i++) { data.push_back(getCorrectValue()); } }

2. Заменим вызов функции заполнения вектора случайными числами на «ручной» ввод и вынесем в отдельную функцию.

void makeLab() { cout << "enter array size: "; int size = getCorrectValue(); if (size < 0) { cout << "array size can not be negative number"; _getch(); return; } vector<int> data; handFillVector(data, size); printVector("before process", data); mainAction(data); printVector("after process", data); }

3. Следует отметить, что в С++ можно перенаправлять стандартный поток ввода на ввод из файла, а стандартный поток вывода – на вывод в файл. Поэтому добавим функцию, которая принимает имя входного и выходного файлов и перенаправляет потоки в них. Отметим, что необходимо подключить модуль #include <fstream>.

void redirectStreams(string infileName, string outFileName) { streambuf *oldInput = cin.rdbuf(); streambuf *oldOutput = cout.rdbuf(); ifstream ifs(infileName.c_str()); ofstream ofs(outFileName.c_str()); cin.rdbuf(ifs.rdbuf()); cout.rdbuf(ofs.rdbuf()); makeLab();

Page 36: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

36

ifs.close(); ofs.close(); cin.rdbuf(oldInput); cout.rdbuf(oldOutput); }

4. Приведем полный код программы

#include <vector> #include <time.h> #include <conio.h> #include <list> #include <iostream> #include <fstream> #include <algorithm> #include <strstream> using namespace std; bool isOdd(int s) { return s % 2 == 1; } void mainAction(vector<int> &data) { vector<int>::iterator newEnd = remove_if(data.begin(),data.end(), isOdd); data.erase(newEnd, data.end()); } int getCorrectValue() { while (true) { int size; if (!(cin >> size)) { cin.clear(); while(std::cin.get()!= '\n'); continue; } return size; } } void printVector(char *caption, vector<int> &data) { cout << caption << ": "; for (vector<int>::iterator i = data.begin(); i != data.end(); ++i) { cout << *i << " "; } cout << "\n"; } void handFillVector(vector<int> &data, int size) { data.reserve(size); for (int i = 0; i < size; i++) {

Page 37: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

37

data.push_back(getCorrectValue()); } } void makeLab() { cout << "enter array size: "; int size = getCorrectValue(); if (size < 0) { cout << "array size can not be negative number"; _getch(); return; } vector<int> data; handFillVector(data, size); printVector("before process", data); mainAction(data); printVector("after process", data); } void redirectStreams(string infileName, string outFileName) { streambuf *oldInput = cin.rdbuf(); streambuf *oldOutput = cout.rdbuf(); ifstream ifs(infileName.c_str()); ofstream ofs(outFileName.c_str()); cin.rdbuf(ifs.rdbuf()); cout.rdbuf(ofs.rdbuf()); makeLab(); ifs.close(); ofs.close(); cin.rdbuf(oldInput); cout.rdbuf(oldOutput); } int main(int argc, char* argv[]) { redirectStreams("labin.txt", "labout.txt"); return 0; }

5. Теперь необходимо вызвать эту функцию с передачей входного файла (содержимое которого для этой задачи может быть «5 1 2 3 4 5»), и сравнить содержимое выходного файла с эталонным выходным файлом. Задача для чтения всего содержимого файла уже рассматривалась. Задача составления тестов так же рассматривалась, поэтому не будем приводить здесь рассуждения.

Page 38: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

38

Задача 5а: В файле удалить все слова, которые начинаются     и заканчиваются одной буквой 

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

2. Для обработки строки необходима строка. Для улучшения характеристик программы строка будет изменяться во время обработки. Поэтому в сигнатуре функции будет строка, передаваемая по ссылке.

3. Для того чтобы удалить слова, которые начинаются и заканчиваются одной и той же буквой, надо уметь находить слова. Для нахождения слова надо уметь находить начало и длину слова. Логика реализации этих функций почти полностью аналогичная реализации на языке C#. Единственное отличие состоит в проверке вхождения символа во множество разделителей. В С++ одним из вариантов реализации этой задачи является функция strchr. Она принимает два параметра. Первый параметр является строкой, которая оканчивается нулевым символом. Второй параметр – символ, который проверяется на вхождение в строку. Возвращаемый результат – NULL, если символ не входит в строку, и указатель на позицию вхождения символа в строку (т. е. не NULL). С учетом этого функции для работы со словом примут следующий вид:

bool isSeparator(char z) { return strchr(" .,\\&!~/", z) != NULL; } bool isBeginOfWord(string &str, int index) { if (isSeparator(str[index])) { return false; } if (index == 0) { return true; } return isSeparator(str[index - 1]); } int getWordLength(string &str, int startIndex) { int res = 0; for (int i = startIndex; i < str.size(); i++) { if (isSeparator(str[i])) { break; } res++; } return res; }

Page 39: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

39

4. Обработка строки также является почти полностью аналогичной обработке строки в C#, за исключением того, что для удаления подстроки из строки, которая представлена типом std::string, необходимо воспользоваться функцией erase.

5. Приведем полный код решения:

#include <string.h> #include <iostream> #include <fstream> #include <string> using namespace std; bool isSeparator(char z) { return strchr(" .,\\&!~/", z) != NULL; } bool isBeginOfWord(string &str, int index) { if (isSeparator(str[index])) { return false; } if (index == 0) { return true; } return isSeparator(str[index - 1]); } int getWordLength(string &str, int startIndex) { int res = 0; for (int i = startIndex; i < str.size(); i++) { if (isSeparator(str[i])) { break; } res++; } return res; } void processLine(string &str) { for (int i = 0; i < str.size(); i++) { if (isBeginOfWord(str, i) == false) { continue; } int len = getWordLength(str, i); if (str[i] != str[i + len - 1]) { i = i + len; continue; } str.erase(str.begin() + i, str.begin() + i + len); } }

Page 40: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

40

void processFile(string infileName, string outFileName) { ifstream ifs(infileName.c_str()); if (ifs.is_open() == false) { return; } ofstream ofs(outFileName.c_str()); if (ofs.is_open() == false) { ifs.close(); return; } string curLine; while (ifs.eof() == false) { getline(ifs, curLine); processLine(curLine); ofs << curLine << "\n"; } ifs.close(); ofs.close(); } int main(int argc, char* argv[]) { processFile("labin.txt", "labout.txt"); return 0; }

   

Page 41: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

41

Задача 5б: В файле удалить все слова, которые начинаются  и заканчиваются одной буквой (без использования потоков  

и std::string)  

Следует отметить, что данную задачу можно решить способом, более близким к программированию на чистом C. Для решения другим способом будем использовать строки с завершающим нулем. Для работы с файлами воспользуемся типом FILE *. Это очень сильно изменит реализацию решения.

1. При решении одна из главных задач – получить содержимое файла. Следует отметить, что функция feof может указывать на достижение конца файла (FILE *). Файл можно открыть на чтение (read) или запись (write) или дозапись (append). Файл можно открыть как бинарный (binary) или текстовый (text). Для открытия файла необходимо воспользоваться функцией fopen, которая имеет два аргумента. Первый – имя файла. Второй – тип открытия файла. Тип открытия состоит из двух букв (хотя может состоять и из одной, и из трех). Первая буква – первая буква типа открытия (read, write, append), вторая – тип файла (binary, text). Если файл не открылся, то fopen возвращает NULL. Для закрытия файла необходимо воспользоваться функцией fclose. Основа открытия файлов будет выглядеть следующим образом:

void processFile(char *infileName, char *outFileName) { FILE *fin = fopen(infileName, "rt"); if (fin == NULL) { return; } FILE *fout = fopen(outFileName, "wt"); if (fout == NULL) { fclose(fin); return; } //обработка файла fclose(fin); fclose(fout); }

 

2. Приведем таблицу часто используемых функций для работы со строками с завершающим нулем: Название Что делает

strlen Возвращает длину строки без завершающего нуля strcmp Сравнивает строки. Возвращает число, меньшее 0, если первая строка

меньше второй; 0, если строки равны; число, больше 0, если первая строка больше второй

strchr Возвращает указатель на первое вхождение символа в строку. NULL,

Page 42: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

42

если символ не входит Название Что делает

strcat Объединяет строки atof Преобразует строку в дробное число atoi Преобразует строку в целое число strstr Ищет подстроку в строке. Возвращает указатель на первое вхождение

подстроки. NULL, если подстрока не найдена strcpy Копирует одну строку в другую (включая завершающий ноль) itoa Выполняет преобразование целого числа в строку ftoa Выполяет преобразование дробного числа в строку

3. Теперь встает вопрос об обработке файла. Для этого необходимо получить строку из файла. Однако заранее не известен ее размер, поэтому будем считывать ее посимвольно, пока не встретим enter (‘\n’) или не дойдем до конца файла. Изначально строка пустая. Но с учетом того, что в ней находится завершающий 0, необходимо выделить один байт. Каждый новый считанный символ необходимо добавлять в конец строки. Полученную строку можно записать в выходной файл с помощью функции fputs. Поэтому алгоритм будет таков:

void addCharToEnd(char *&str, char newC) { //находим длину строки без завершающего нуля int oldLen = strlen(str); //+1 - на новый символ //+1 - на завершающий 0 char *newStr = new char[oldLen + 1 + 1]; //копируем данные из str в newStr strcpy(newStr, str); //добавляем новый символ newStr[oldLen] = newC; //ставим завершающий 0 newStr[oldLen + 1] = 0; delete[] str; str = newStr; } char *getStrFromFile(FILE *&fin) { char *res = new char[1]; res[0] = 0; while (true) { char curChar = fgetc(fin); if (feof(fin)) { break; } addCharToEnd(res, curChar); if (curChar == '\n') { break; } }

Page 43: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

43

return res; } void processLine(char *&str) { //реализация логики по обработке одной строки } void processFile(char *infileName, char *outFileName) { FILE *fin = fopen(infileName, "rt"); if (fin == NULL) { return; } FILE *fout = fopen(outFileName, "wt"); if (fout == NULL) { fclose(fin); return; } while (true) { char *loadedStr = getStrFromFile(fin); processLine(loadedStr); fputs(loadedStr, fout); delete[] loadedStr; if (feof(fin)) { break; } } fclose(fin); fclose(fout); }

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

5. Проверка на то, является символ разделителем или нет, остается почти такой же (только в разделители необходимо добавить символы ‘\n’ и ‘\r’). Но при нахождении длины слова необходимо использовать тот факт, что в конце строки находится завершающий ноль, т. е. символ с кодом ноль. С учетом всех этих рассуждений реализация данных функций будет следующая:

bool isSeparator(char z) { return strchr(" .,\\&!~/\n\r", z) != NULL; } bool isBeginOfWord(char *str, int index) { if (isSeparator(str[index])) { return false;

Page 44: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

44

} if (index == 0) { return true; } return isSeparator(str[index - 1]); } int getWordLength(char *str, int startIndex) { int res = 0; for (int i = startIndex; str[i] != 0; i++) { if (isSeparator(str[i])) { break; } res++; } return res; }

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

void deleteWord(char *str, int startWord, int lengthWord) { for (int i = startWord + lengthWord; true; i++) { str[i - lengthWord] = str[i]; if (str[i] == 0) { break; } } }

7. Возвращаемся к обработке строки. Надо пройти по всей строке, находить начала и длины слов и удалять их по условию. Для увеличения скорости работы вычислим длину строки только в самом начале функции, и при удалении слова будем ее уменьшать на необходимую величину. В кодах это будет реализовано следующим образом:

void processLine(char *str) { int strLen = strlen(str); for (int i = 0; i < strLen; i++) { if (isBeginOfWord(str, i) == false) { continue; } int len = getWordLength(str, i); if (str[i] != str[i + len - 1]) { i += len; continue; }

Page 45: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

45

deleteWord(str, i, len); strLen -= len; } }

8. Приведем весь код программы:

#include <string.h> #include <stdio.h> bool isSeparator(char z) { return strchr(" .,\\&!~/\n\r", z) != NULL; } bool isBeginOfWord(char *str, int index) { if (isSeparator(str[index])) { return false; } if (index == 0) { return true; } return isSeparator(str[index - 1]); } int getWordLength(char *str, int startIndex) { int res = 0; for (int i = startIndex; str[i] != 0; i++) { if (isSeparator(str[i])) { break; } res++; } return res; } void deleteWord(char *str, int startWord, int lengthWord) { for (int i = startWord + lengthWord; true; i++) { str[i - lengthWord] = str[i]; if (str[i] == 0) { break; } } } void addCharToEnd(char *&str, char newC) { //находим длину строки без завершающего нуля int oldLen = strlen(str); //+1 - на новый символ //+1 - на завершающий 0 char *newStr = new char[oldLen + 1 + 1]; //копируем данные из str в newStr

Page 46: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

46

strcpy(newStr, str); //добавляем новый символ newStr[oldLen] = newC; //ставим завершающий 0 newStr[oldLen + 1] = 0; delete[] str; str = newStr; } char *getStrFromFile(FILE *&fin) { char *res = new char[1]; res[0] = 0; while (true) { char curChar = fgetc(fin); if (feof(fin)) { break; } addCharToEnd(res, curChar); if (curChar == '\n') { break; } } return res; } void processLine(char *str) { int strLen = strlen(str); for (int i = 0; i < strLen; i++) { if (isBeginOfWord(str, i) == false) { continue; } int len = getWordLength(str, i); if (str[i] != str[i + len - 1]) { i += len; continue; } deleteWord(str, i, len); strLen -= len; } } void processFile(char *infileName, char *outFileName) { FILE *fin = fopen(infileName, "rt"); if (fin == NULL) { return; } FILE *fout = fopen(outFileName, "wt"); if (fout == NULL) { fclose(fin); return; }

Page 47: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

47

while (true) { char *loadedStr = getStrFromFile(fin); processLine(loadedStr); fputs(loadedStr, fout); delete[] loadedStr; if (feof(fin)) { break; } } fclose(fin); fclose(fout); } int main(int argc, char* argv[]) { processFile("labin.txt", "labout.txt"); return 0; }

   

Page 48: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

48

Задача 6а: Удалить строки, в которых есть два одинаковых элемента (без использования std::vector) 

Для начала решим эту задачу таким способом. 1. Как и в C#, матрицу можно рассматривать как массив строк. Поэтому

для начала рассмотрим одну строку матрицы и создадим класс для нее. 2. Строка матрицы – массив чисел. Следовательно, нам в формируемом

классе нужна переменная типа int * (для хранения указателя на массив) и переменная типа int (для хранения размера массива, т. к. он нам не известен заранее).

3. Для создания строки нам нужно знать ее размер. Поэтому в конструктор строки будем передавать ее размер. С учетом того, что по заданию не сказано, как заполняется матрица, будем заполнять ее случайными числами. Если в конструктор передается размер массива, то в нем мы можем создать массив, используя оператор new []. Если мы его используем, то мы должны обязательно использовать оператор delete []. Оператор delete[] мы должны использовать, когда нам строка уже не нужна, т. е. в деструкторе (имя которого начинается с «~» и далее идет имя класса). Реализуем эти рассуждения в коде:

#include <stdlib.h> #include <time.h> class MatrixString { private: int *data; int size; int getRandValue(int max, int min) { return (((double)rand())/RAND_MAX)*(max - min) + min; } void fillString() { for (int i = 0; i < size; i++) { data[i] = getRandValue(-10, 10); } } public: MatrixString(int size_) { size = size_; data = new int[size]; fillString(); } ~MatrixString() { delete[] data; } };

Page 49: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

49

4. Теперь необходимо написать функцию, которая определяет, есть ли в

строке хотя бы два одинаковых элемента. Они есть, если количество вхождений какого-либо элемента больше одного. Поэтому в начале надо написать функцию, которая определяет количество вхождений элемента в строку. Она не представляет из себя сложной функции и почти полностью аналогична функции на C#:

int getCount(int element) { int res = 0; for (int i = 0; i < size; i++) { if (data[i] == element) { res++; } } return res; }

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

6. Ответом на вопрос, есть ли в строке два одинаковых элемента, является да или нет, т. е. логический тип. Реализация этой функции так же почти аналогичная функции на C#. Приведем полный код класса MatrixString, который получился на текущий момент:

#include <stdlib.h> #include <time.h> class MatrixString { private: int *data; int size; int getRandValue(int max, int min) { return (((double)rand())/RAND_MAX)*(max - min) + min; } void fillString() { for (int i = 0; i < size; i++) { data[i] = getRandValue(-10, 10); } } int getCount(int element) { int res = 0; for (int i = 0; i < size; i++) { if (data[i] == element) { res++;

Page 50: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

50

} } return res; } public: MatrixString(int size_) { size = size_; data = new int[size]; fillString(); } void print() { for (int i = 0; i < size; i++) { printf("%d ", data[i]); } printf("\n"); } bool needDelete() { for (int i = 0; i < size; i++) { if (getCount(data[i]) > 1) { return true; } } return false; } ~MatrixString() { delete[] data; } };

7. Возвращаемся к матрице. Матрица – массив строк. Как и в C#, чтобы создать массив строк, надо создать сам массив и создать каждый его элемент. Однако, чтобы создать элемент массива, необходимо вызвать оператор new SimpleString(РазмерСтроки). Поэтому элемент массива будет иметь тип SimpleString *, а сам массив будет объявляться как SimpleString **strings. Так же нам нужно знать его размер. Для создания матрица надо знать ее размеры, т. е. два числа. С учетом этого начало реализации класса матрицы будет выглядеть так:

class Matrix { private: MatrixString **strings; int size; public: Matrix(int sizeX, int sizeY) { size = sizeY; strings = new MatrixString *[sizeY];

Page 51: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

51

for (int i = 0; i < sizeY; i++) { strings[i] = new MatrixString(sizeX); } } ~Matrix() { for (int i = 0; i < size; i++) { delete strings[i]; } delete []strings; } };

8. Чтобы распечатать матрицу, надо распечатать каждую строку в ней. С учетом того, что в массиве хранится указатель на строку, необходимо использовать оператор -> для вызова функции печати у строки.

void print() { for (int i = 0; i < size; i++) { strings[i]->print(); } printf("\n"); }

9. Следующим необходимым атомарным действием является удаление строки по индексу. В целом оно почти идентично удалению на C#. Но следует учитывать, что удаляемым элементом массива является указатель на память, выделенную с помощью оператора new. Поэтому перед удалением элемента из массива надо освободить память по удаляемому указателю с помощью оператора delete. И, естественно, надо удалить старый массив строк (а вернее, массив указателей на строки). С учетом этого реализация алгоритма примет следующий вид:

void delByIndex(int index) { MatrixString **newStrings = new MatrixString *[size - 1]; for (int i = 0; i < index; i++) { newStrings[i] = strings[i]; } delete strings[index]; for (int i = index; i < size; i++) { newStrings[i] = strings[i + 1]; } delete[] strings; strings = newStrings; size--; }

Page 52: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

52

10. Теперь надо удалить строки по условию. И чтобы избежать проблем, связанных со сдвигом индексации после удаления, будем идти с конца массива к его началу.

void process() { for (int i = size - 1; i >= 0; i--) { if (strings[i]->needDelete()) { delByIndex(i); } } }

11. Теперь разобьем получившееся решение на два h файла (для объявления строки матрицы и самой матрицы) и три cpp файла (главная функция, реализация строки матрицы и реализация матрицы). Структура файлов конечного решения представлены на рисунке:

Содержимое файла MatrixString.h:

#ifndef _MATRIXSTRING_H_ #define _MATRIXSTRING_H_ class MatrixString { private: int *data; int size; int getRandValue(int max, int min); void fillString(); int getCount(int element); public: MatrixString(int size_); ~MatrixString(); void print(); bool needDelete(); }; #endif

Page 53: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

53

Содержимое файла MatrixString.cpp:

#include <stdlib.h> #include "MatrixString.h" #include <stdio.h> int MatrixString::getRandValue(int max, int min) { return (((double)rand())/RAND_MAX)*(max - min) + min; } void MatrixString::fillString() { for (int i = 0; i < size; i++) { data[i] = getRandValue(-10, 10); } } int MatrixString::getCount(int element) { int res = 0; for (int i = 0; i < size; i++) { if (data[i] == element) { res++; } } return res; } MatrixString::MatrixString(int size_) { size = size_; data = new int[size]; fillString(); } void MatrixString::print() { for (int i = 0; i < size; i++) { printf("%d ", data[i]); } printf("\n"); } bool MatrixString::needDelete() { for (int i = 0; i < size; i++) { if (getCount(data[i]) > 1) { return true; } } return false; } MatrixString::~MatrixString() { delete[] data; }

 

Page 54: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

54

Содержимое файла Matrix.h:

#ifndef _MATRIX_H_ #define _MATRIX_H_ #include "MatrixString.h" class Matrix { private: MatrixString **strings; int size; void delByIndex(int index); public: Matrix(int sizeX, int sizeY); ~Matrix(); void print(); void process(); }; #endif

Содержимое файла Matrix.cpp:

#include "MatrixString.h" #include "Matrix.h" #include <stdio.h> void Matrix::delByIndex(int index) { MatrixString **newStrings = new MatrixString *[size - 1]; for (int i = 0; i < index; i++) { newStrings[i] = strings[i]; } delete strings[index]; for (int i = index; i < size; i++) { newStrings[i] = strings[i + 1]; } delete[] strings; strings = newStrings; size--; } Matrix::Matrix(int sizeX, int sizeY) { size = sizeY; strings = new MatrixString *[sizeY]; for (int i = 0; i < sizeY; i++) { strings[i] = new MatrixString(sizeX); } }

Page 55: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

55

void Matrix::print() { for (int i = 0; i < size; i++) { strings[i]->print(); } printf("\n"); } void Matrix::process() { for (int i = size - 1; i >= 0; i--) { if (strings[i]->needDelete()) { delByIndex(i); } } } Matrix::~Matrix() { for (int i = 0; i < size; i++) { delete strings[i]; } delete []strings; }

Содержимое файла lab2_1.cpp:

#include <stdlib.h> #include <time.h> #include <conio.h> #include "Matrix.h" #include <stdio.h> int main(int argc, char* argv[]) { srand(time(NULL)); int sizeX; int sizeY; printf("input size x: "); scanf("%d", &sizeX); printf("input size y: "); scanf("%d", &sizeY); Matrix z(sizeX, sizeY); z.print(); z.process(); z.print(); printf("press any key"); _getch(); return 0; }

Page 56: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

56

Задача 6б: Удалить строки, в которых есть два одинаковых элемента (c использованием std::vector) 

Теперь решим эту задачу с использованием STL. 1. Логика программы «глобально» при оптимизации не изменится. В

отличие от массива, у std::vector есть функция size(), которая возвращает размер вектора. Поэтому переменная size в классе MatrixString больше не нужна. С учетом этого содержимое файла MatrixString.h станет следующим:

#include <vector> class MatrixString { private: std::vector<int> data; int getRandValue(int max, int min); int getCount(int element); public: MatrixString(int size_); ~MatrixString(); void print(); bool needDelete(); };

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

MatrixString::MatrixString(int size) { data.reserve(size); for (int i = 0; i < size; i++) { data.push_back(getRandValue(-10, 10)); } }

3. Все остальные функции в MatrixString так же почти не изменятся, за исключением того, что везде size заменится на data.size().

4. Рассмотрим изменение класса Matrix. По полной аналогии заменим MatrixString ** на std::vector<MatrixString *> и избавимся от переменной size. Теперь обратим внимание на то, что в MatrixString не осталось операторов new и new []. Поэтому у нас не будет проблем с динамической памятью. Теперь не надо контролировать вызов создания строки матрицы и можно заменить std::vector<MatrixString *> на

Page 57: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

57

std::vector<MatrixString> и, таким образом, избавится от операторов new, new[], delete, delete[]. С учетом этого содержимое файлов Matrix.h и Matrix.cpp станет следующим: Содержимое файла Matrix.h:

#include <vector> #include "MatrixString.h" class Matrix { private: std::vector<MatrixString>strings; void delByIndex(int index); public: Matrix(int sizeX, int sizeY); void print(); void process(); };

Содержимое файла Matrix.cpp

#include "MatrixString.h" #include "Matrix.h" #include <stdio.h> void Matrix::delByIndex(int index) { strings.erase(strings.begin() + index, strings.begin() + index + 1); } Matrix::Matrix(int sizeX, int sizeY) { strings.reserve(sizeY); for (int i = 0; i < sizeY; i++) { strings.push_back(MatrixString(sizeX)); } } void Matrix::print() { for (int i = 0; i < strings.size(); i++) { strings[i].print(); } printf("\n"); } void Matrix::process() { for (int i = strings.size() - 1; i >= 0; i--) { if (strings[i].needDelete()) { delByIndex(i); } } }

5. Содержимое lab2_1.cpp не изменится

Page 58: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

58

Задача 7: Отсортировать содержимое словаря Задача: в файле «in.txt» находится словарь. Каждая строка словаря

содержит только одно слово. Необходимо в файл «out.txt» записать слова, отсортированные по алфавиту.

1. Логично предположить, что данная задача представима в виде последовательности трех действий:

a. Считывание строк из файла in.txt b. Сортировка строк c. Сохранение результатов в файл out.txt.

2. Первая подзадача уже разбиралась в задаче 3а, поэтому подробно на ней останавливаться не будем. Приведем код функции, которая отвечает за чтение всех строк из файла (обратите внимание на подключение fstream для работы с ifstream – input file stream):

#include <conio.h> #include <iostream> #include <fstream> #include <vector> #include <algorithm> #include <string> using namespace std; int readFromFile(std::string dictionary_name, std::vector<string> &words) { ifstream ifs(dictionary_name); if (!ifs.is_open()) { cout << "Could not open file: " << dictionary_name << endl; return 1; } //пока не дошли до конца файла while (ifs.eof() == false) { string curLine; getline(ifs, curLine); words.push_back(curLine); } ifs.close(); return 0; }

3. Теперь необходимо отсортировать список. Заметим, что данную задачу можно реализовать с помощью стандартных алгоритмов STL.

sort(words.begin(), words.end());

Естественно, возникает вопрос, какие еще стандартные алгоритмы доступны в модуле alogorithm. Их можно разделить на четыре категории:

1. Неизменяющие алгоритмы над последовательностями

Page 59: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

59

a. for_each – применяет фунцию к каждому элементу b. find – выполняет линейный поиск c. find_first_of – выполняет линейный поиск любого из

множества значений d. adjacent_find – выполняет линейный поиск смежных

равных элементов e. count – подсчитывает количество элементов с данным

значением f. mismatch – сканирует две последовательности в поисках

первой позиции, где они различны g. equal – сканирует две последовательности для проверки их

поэлементной эквивалентности h. search – сканирует последовательность в поисках другой

подпоследовательности i. search_n – сканирует последовательность в поисках ряда

идентичных элементов с данным значением j. find_end – сканирует последовательность в поисках

последнего совпадения с другой последовательностью 2. Изменяющие алгоритмы над последовательностями

a. copy – копирует элементы в другую последовательность b. swap – обменивает элементы одной последовательности с

элементами другой c. transform – замещает каждый элемент значением,

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

d. replace – замещает каждый эдемент, равный некоторому значению, копией другого заданного значения

e. fill – замещает каждый элемент копией другого заданного значения

f. generate – замещает каждый элемент значением, возвращаемым вызовом функции

g. remove – удаляет элементы, равные заданному значению h. unique – удаляет последовательные равные элементы i. reverse – обращает относительный порядок элементов j. rotate – выполняет циклический сдвиг элементов k. random_shuffle – псевдослучайно переупорядочивает

элементы l. partition – переупорядочивает элементы таким образом, что

элементы, удовлетворяющие указанному предикату, предшествуют элементам, не удовлетворяющим ему.

3. Алгоритмы, связанные с сортировкой a. sort, stable_sort, partial_sort – переставляют элементы

последовательности в порядке возрастания

Page 60: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

60

b. nth_element – находит N-й наименьший элемент последовательности

c. binary_search, lower_bound, upper_bound, equal_range – выполняют поиск в отсортированной последовательности методом деления пополам

d. megre – сливает две отсортированные последовательности в одну

e. includes, set_union, set_intersection, set_difference, set_symmetric_difference – выполняют теоретико-множественные операции над отсортированными структурами

f. push_heap, pop_heap, make_heap, sort_heap – выполняют операции по упорядочиванию последовательности, организованной в виде пирамиды

g. min, max, min_element, max_element – находят минимальный или максимальный элемент пары или последовательности

h. lexicographical_compare – лексикографически сравнивает две последовательности

i. next_permutation, prev_permutation – генерируют перестановки последовательности, основываясь на лексикографическом упорядочении множества всех перестановок.

4. Обобщенные числовые алгоритмы a. accumulate – вычисляет сумму элементов

последовательности b. inner_product – вычисляет сумму произведения

соответствующих элементов последовательностей c. partial_sum – вычисляет частичные суммы элементов

последовательности и сохраняет их в другой (или той же) последовательности

d. adjacent_difference – вычисляет разности смежных элементов и сохраняет их в другой (или той же) последовательности.

4. Запись в выходной файл не представляет сложностей:

int writeToFile(std::string fileName, std::vector<string> &words){ ofstream ofs(fileName); if (!ofs.is_open()) { cout << "Could not open file: " << fileName << endl; return 1; } for (vector<string>::iterator i = words.begin(); i != words.end(); ++i) { ofs << *i << "\n"; }

Page 61: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

61

ofs.close(); return 0; }

5. Приведем полный код программы:

#include <conio.h> #include <iostream> #include <fstream> #include <vector> #include <algorithm> #include <string> using namespace std; int readFromFile(string dictionaryName, vector<string> &words) { ifstream ifs(dictionaryName); if (!ifs.is_open()) { cout << "Could not open file: " << dictionaryName << endl; return 1; } while (ifs.eof() == false) { string curLine; getline(ifs, curLine); words.push_back(curLine); } ifs.close(); return 0; } int writeToFile(string fileName, vector<string> &words) { ofstream ofs(fileName); if (!ofs.is_open()) { cout << "Could not open file: " << fileName << endl; return 1; } for (vector<string>::iterator i = words.begin(); i != words.end(); ++i) { ofs << *i << "\n"; } ofs.close(); return 0; } int main(int argc, char* argv[]) { vector<string> words; readFromFile("in.txt", words); sort(words.begin(), words.end(), less<string>()); writeToFile("out.txt", words); //_getch(); return 0; }

Page 62: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

62

  Задача 8: Реализовать сохранение и загрузку пользовательских структур данных с использованием fstream 

Часто в решаемых задачах возникает проблема сохранения сложных пользовательских типов данных. Обычно данные сохраняют с помощью ofstream, а загружают с использованием ifstream. Проблема сохранения может быть решена с помощью перегрузки операторов.

Задача: реализовать сохранение и загрузку множества символов, реализованных в виде пользовательского класса.

Данная задача состоит из двух частей: 1. Реализация множества в пользовательском классе 2. Добавление возможности сохранения и загрузки множества в потоки. Решим первую задачу. Для реализации множества необходимы операции: 1. Проверка на существование элемента во множестве 2. Добавление элемента во множество 3. Удаление элемента из множества

1. Для реализации воспользуемся строками с стиле С (т. е. с завершающим

нулем). Для проверки существования элемента (т. е. символа) во множестве можно воспользоваться функцией strchr, которая возвращает NULL, если символа в строке нет и не NULL, если он есть (а если быть более точным, то указатель на первое вхождение символа).

2. Естественно, во множестве должна быть строка, которая будет инициализироваться в конструкторе и в которой будем искать символ. Для примера используем динамическую строку. Поэтому в конструкторе под нее еще будет выделяться память, а в деструкторе – удаляться. В коде эти рассуждения будут реализованы так:

#include <string.h> class CharSet { private: char *data; public: CharSet() { data = new char[1]; data[0] = 0; } bool contains(char c) { return (strchr(data, c) != NULL); } ~CharSet() { delete[] data; } };

Page 63: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

63

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

bool addChar(char c) { if (contains(c)) { return false; } int oldLen = strlen(data); char *newMem = new char[oldLen + 1 + 1]; strcpy(newMem, data); newMem[oldLen] = c; newMem[oldLen + 1] = 0; delete[] data; data = newMem; return true; }

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

bool delChar(char c) { if (contains(c) == false) { return false; } int oldLen = strlen(data); char *newData = new char[oldLen]; int curPos = 0; for (int i = 0; i < oldLen; i++) { if (data[i] == c) { curPos++; } newData[i] = data[curPos++]; } delete[] data; data = newData; }

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

Page 64: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

64

что в С++ существуют дружественные функции, которым доступны все области класса (private, public, protected). Дружественную функцию необходимо объявить в классе, а реализовать вне класса.

class CharSet { private: char *data; public: friend ofstream& operator<<(ofstream &stream, const CharSet &set); … }; ofstream& operator<<(ofstream &stream, const CharSet &set) { stream << set.data << "\n"; return stream; }

6. Следует обратить внимание на то, что функция возвращает ofstream &. Это необходимо для того, чтобы можно было писать «temp << value1 << value2». Сама реализация функции довольно простая. Теперь перейдем к вопросу реализации функции загрузки. Так как это пример демонстрационный, воспользуемся методом получения строки из потока и затем преобразуем ее в строку в стиле С.

ifstream& operator>>(ifstream &stream, CharSet &set) { string loadedStr; getline(stream, loadedStr); set.clear(); for (int i = 0; i < loadedStr.size(); i++) { set.addChar(loadedStr[i]); } return stream; }

7. Приведем полный код программы:

#include <string.h> #include <fstream> #include <string> using namespace std; class CharSet { private: char *data; public: friend ofstream& operator<<(ofstream &stream, const CharSet &set); friend ifstream& operator>>(ifstream &stream, CharSet &set);

Page 65: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

65

CharSet() { data = new char[1]; data[0] = 0; } virtual ~CharSet() { delete[] data; } void clear() { data[0] = 0; } bool addChar(char c) { if (contains(c)) { return false; } int oldLen = strlen(data); char *newMem = new char[oldLen + 1 + 1]; strcpy(newMem, data); newMem[oldLen] = c; newMem[oldLen + 1] = 0; delete[] data; data = newMem; return true; } bool delChar(char c) { if (contains(c) == false) { return false; } int oldLen = strlen(data); char *newData = new char[oldLen]; int curPos = 0; for (int i = 0; i < oldLen; i++) { if (data[i] == c) { curPos++; } newData[i] = data[curPos++]; } delete[] data; data = newData; } bool contains(char c) { return (strchr(data, c) != NULL); } }; ofstream& operator<<(ofstream &stream, const CharSet &set) { stream << set.data << "\n";

Page 66: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

66

return stream; } ifstream& operator>>(ifstream &stream, CharSet &set) { string loadedStr; getline(stream, loadedStr); set.clear(); for (int i = 0; i < loadedStr.size(); i++) { set.addChar(loadedStr[i]); } return stream; } int main(int argc, char* argv[]) { CharSet z; for (char i = 'a'; i < 'h'; i++) { z.addChar(i); } ofstream outFile("data.txt"); outFile << z; outFile.close(); z.clear(); ifstream inFile("data.txt"); inFile >> z; inFile.close(); return 0; }

 

Page 67: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

67

Задача 9: Обработка списков фамилий студентов Задача: пусть существуют файлы, в которых перечислены фамилии

студентов и возможно предлоги. Необходимо прочитать содержимое файлов, объединить их в одну структуру. Получившийся список студентов отсортировать по фамилии, после чего удалить дубликаты. Затем необходимо подсчитать количество студентов, фамилия которых начинается на символ ‘a’. Напечатать получившийся список.

На данной задаче мы продемонстрируем возможности использования алгоритмов STL.

1. Пусть имена файлов, в которых записаны фамилии студентов будут «group1» и «group2». Первой задачей является чтения списка фамилий студентов (т. е. вектора строк) из каждого файла.

2. Прочитать файл можно несколькими способами. Следует отметить, что существует возможность использования istream_iterator для чтения данных из файла. Так же существует back_insert для добавления данного в конец коллекции. Алгоритм чтения будет таков:

void getNames(string fileName, vector<string> &students) { ifstream infile1(fileName); istream_iterator<string> input_set1( infile1 ), eos; copy(input_set1, eos, back_inserter(students)); }

Отметим, что переменная eos на первый взгляд не инициализирована. На самом деле она инициализирована значением конца потока по умолчанию. Поэтому функция copy вторым аргументом принимает eos. Так же следует упомянуть о том, что в С++ существуют 5 типов итераторов:

1. Итератор чтения можно использовать для получения элементов из контейнера, но поддержка записи в контейнер не гарантируется. Такой итератор должен обеспечивать следующие операции (итераторы, поддерживающие также дополнительные операции, можно употреблять в качестве итераторов чтения при условии, что они удовлетворяют минимальным требованиям): сравнение двух итераторов на равенство и неравенство, префиксная и постфиксная форма инкремента итератора для адресации следующего элемента (оператор ++), чтение элемента с помощью оператора разыменования (*). Такого уровня поддержки требуют, в частности, алгоритмы find(), accumulate() и equal(). Любому алгоритму, которому необходим итератор чтения, можно передавать также и итераторы категорий, описанных в пунктах 3, 4 и 5;

Page 68: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

68

2. Итератор записи можно представлять себе как противоположный по функциональности итератору чтения. Иными словами, его можно использовать для записи элементов контейнера, но поддержка чтения из контейнера не гарантируется. Такие итераторы обычно применяются в качестве третьего аргумента алгоритма (например, copy()) и указывают на позицию, с которой надо начинать копировать. Любому алгоритму, которому необходим итератор записи, можно передавать также и итераторы других категорий, перечисленных в пунктах 3, 4 и 5;

3. Однонаправленный итератор можно использовать для чтения и записи в контейнер, но только в одном направлении обхода (обход в обоих направлениях поддерживается итераторами следующей категории). К числу обобщенных алгоритмов, требующих как минимум однонаправленного итератора, относятся adjacent_find(), swap_range() и replace(). Конечно, любому алгоритму, которому необходим подобный итератор, можно передавать также и итераторы описанных ниже категорий;

4. Двунаправленный итератор может читать и записывать в контейнер, а также перемещаться по нему в обоих направлениях. Среди обобщенных алгоритмов, требующих как минимум двунаправленного итератора, выделяются place_merge(), next_permutation() и reverse();

5. Итератор с произвольным доступом помимо всей функциональности, поддерживаемой двунаправленным итератором, обеспечивает доступ к любой позиции внутри контейнера за постоянное время. Подобные итераторы требуются таким обобщенным алгоритмам, как binary_search(), sort_heap() и nth-element().

3. Следующим шагом будет получение векторов с именами студентов и

объединение их в вектор векторов для универсальности алгоритма

vector< vector<string> > sample; vector<string> g1, g2; getNames("D:\\123.txt", g1); getNames("D:\\234.txt", g2); sample.push_back(g1); sample.push_back(g2);

4. Пусть функция, которая выполняет обработку списков студентов из файлов, будет называться processGroups.

void processGroups(vector< vector<string> > &groups)

Page 69: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

69

Логично, что первым действием в ней будет объединение вектора списков студентов в один вектор, который будет содержать только фамилии. Для этого мы должны пройтись по каждому списку студентов группы.

vector<string> students; for (vector<vector<string>>::iterator iter = pvec.begin(); iter!= pvec.end(); ++iter) { …}

И добавить содержимое текущего вектора (т. е. от iter->begin() до iter->end() в конец вектора students. Поэтому следующая детализация алгоритма выглядит так:

vector<string> students; for (vector<vector<string>>::iterator iter = groups.begin(); iter != groups.end(); ++iter) { copy( iter->begin(), iter->end(), back_inserter( students )); }

Первыми двумя аргументами алгоритма copy() являются итераторы, ограничивающие диапазон подлежащих копированию элементов. Третий аргумент – это итератор, указывающий на место, куда надо копировать элементы. back_inserter называется адаптером итератора; он позволяет вставлять элементы в конец вектора, переданного ему в качестве аргумента.  

5. Следующей задачей является сортировка студентов. Есть функция sort, которая это выполняет. Поэтому реализация в кодах данного пункта решения будет выглядеть следующим образом:

stable_sort(students.begin(), students.end());

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

// удалить дубликаты vector<string>::iterator it = unique(students.begin(), students.end()); students.erase(it, students.end());

7. Теперь надо подсчитать количество студентов, фамилии которых начинаются на букву ‘a’. Для этого подойдет функция count_if. Однако ей нужно условие того, что элемент надо учитывать. Создадим отдельный класс, который за это отвечает.

class StudentCounter { public: StudentCounter(char startChar) : _startChar(startChar){

Page 70: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

70

} bool operator()(const string & s1) { return s1[0] == _startChar; } private: char _startChar; };

Следует отметить реализацию bool operator()(…). Это перегрузка оператора (), т. е. к экземпляру класса можно обращаться как к функции (поэтому такие классы часто называют функторами).

8. Теперь можно подсчитать количество студентов, фамилии которых начинаются на букву ‘a’.

int cnt = count_if(students.begin(), students.end(), StudentCounter('a')); cout << "Number of students with 'a' " << cnt << endl;

9. Теперь надо удалить различные предлоги и т. д. Для этого объявим массив значений, которые надо удалить.

string rw[] = { "and", "if", "or", "but", "the" };

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

string rw[] = { "and", "if", "or", "but", "the" }; for (int i = 0; i < 5; i++) { int cnt = count(students.begin(), students.end(), rw[i]); cout << cnt << " instances removed: " << rw[i] << endl; students.erase(remove(students.begin(), students.end(), rw[i]), students.end()); }

10. И теперь осталось только напечатать фамилии студентов

for (vector<string>::iterator i = students.begin(); i != students.end(); ++i) { cout << *i << " "; }

11. Приведем полный код программы:

#include <vector> #include <string> #include <algorithm> #include <iterator> #include <iostream>

Page 71: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

71

#include <fstream> using namespace std; class StudentCounter { private: char _startChar; public: StudentCounter(char startChar) : _startChar(startChar){ } bool operator()(const string & s1) { return s1[0] == _startChar; } }; void processGroups(vector< vector<string> > &groups) { vector<string> students; for (vector<vector<string>>::iterator iter = groups.begin(); iter != groups.end(); ++iter) { copy( iter->begin(), iter->end(), back_inserter( students )); } // отсортировать вектор, сохранив относительный порядок студентов stable_sort(students.begin(), students.end()); // удалить дубликаты vector<string>::iterator it = unique(students.begin(), students.end()); students.erase(it, students.end()); int cnt = count_if(students.begin(), students.end(), StudentCounter('a')); cout << "Number of students with 'a' " << cnt << endl; string rw[] = { "and", "if", "or", "but", "the" }; for (int i = 0; i < 5; i++) { int cnt = count(students.begin(), students.end(), rw[i]); cout << cnt << " instances removed: " << rw[i] << endl; students.erase(remove(students.begin(), students.end(), rw[i]), students.end()); } for (vector<string>::iterator i = students.begin(); i != students.end(); ++i) { cout << *i << " "; } } void getNames(string fileName, vector<string> &students) { ifstream infile1(fileName); istream_iterator<string> input_set1( infile1 ), eos; copy(input_set1, eos, back_inserter(students));

Page 72: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

72

} int main(int argc, char* argv[]) { vector< vector<string> > sample; vector<string> g1, g2; getNames("group1.txt", g1); getNames("group2.txt", g2); sample.push_back(g1); sample.push_back(g2); processGroups(sample); return 0; }

Page 73: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

73

Задача 10: Исследовать внутреннее представление любых численных типов 

Для начала напишем программу, которая выводит машинное представление любой переменной (например, на C#).

Любое значение в памяти ЭВМ представляется в виде массива байт. Так же любое значение имеет адрес, с которого оно начинается, и размер в байтах. Размер любой переменной в байтах можно определить с помощью стандартной функции sizeof.

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

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

Полный код программы приведен ниже:

public static string getBinView(byte value) { string res = ""; while (value > 0) { if (value % 2 == 1) { res = "1" + res; } else { res = "0" + res; } value = (byte)(value / 2); } res = res.PadLeft(8, '0'); return res; } static void Main(string[] args) { unsafe { string res = ""; Single temp = 4; byte* pointer = (byte *)&temp; for (int i = 0; i < sizeof(Single); i++) { res = getBinView(*pointer) + " " + res; pointer++; } Console.WriteLine(res); } Console.ReadKey(); } 

Page 74: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

74

Представление беззнаковых целых чисел в памяти ЭВМ Как известно, в К-разрядном двоичном числе может храниться 2k

различных значений. Следует отметить, что 0 – это тоже значение, поэтому все максимальные значения беззнаковых типов нечетные. Для знаковых типов количество положительных и отрицательных чисел одинаково, поэтому нижняя граница знаковых типов равна половине количества различных комбинаций, т.е. для k разрядного числа это будет 2k-1. Очевидно, что верхняя граница у знаковых чисел будет (2k-1 – 1).

Общеизвестно, что в 1 байте хранится 8 бит. Таким образом, для типа byte отводится 8 разрядов в двоичном представлении, для типа Word – 16 разрядов в двоичном представлении.

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

Чтобы получить внутреннее представление целого положительного числа N, хранящегося в К-разрядной ячейке, необходимо:

1. Перевести число N в двоичную систему счисления; 2. Полученный результат дополнить слева незначащими нулями до К

разрядов. 3. Возможно инвертировать порядок байт (см. пример 2). Часто этот формат хранения называют прямым кодом.

Пример 1 Найти представление числа 14 в памяти ЭВМ, если это тип byte. Решение. Переведем 1410 в двоичную. Это будет 11102. Размер byte равен 1 байту, т. е. 8 разрядам, и это беззнаковый тип. Поэтому 1410=11102=000011102.

Замечание. для удобства в записи числа вставляют пробелы после каждой четверки разрядов. 1410=11102=000011102=0000 11102 Это и является представлением числа в памяти ЭВМ. Замечание. Часто для удобства число записывают в 16-ричной системе: 0000 11102 = 0E16

Пример 2 Найти представление числа 1400 в памяти ЭВМ, если это тип word. Решение. Переведем 1400 10 в двоичную систему. Это будет 101011110002. Размер word равен 2 байтам, т. е. 16 разрядам, и это беззнаковый тип. Поэтому 140010=1010 1111 0002=0000 1010 1111 0002. Замечание. Для удобства в записи числа вставляют пробелы после каждой

четверки разрядов. 0000 1010 1111 0002=057816

Page 75: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

75

Старший байт в этом числе – 05, а младший – 78. Казалось бы, это и является внутренним представлением числа. Но есть одно «но» – это уже не однобайтовый тип. Все объекты, большие

байта, представляются на разных машинах по-разному, поэтому полагаться на какие-то определенные свойства было бы ошибкой. Короткие целые числа (обычно 16 битов, или 2 байта) могут иметь младший байт, расположенный как по меньшему адресу (иногда называют little-endian, младшеконечное расположение, порядок от младшего к старшему1), чем старший, так и по большему (big-endian, старшеконечное2). Выбор варианта произволен, а некоторые машины вообще поддерживают обе модели.

Таким образом, если машина является младшеконечной, то внутреннее представление числа будет 780516. А если старшеконечная машина, то внутреннее представление числа будет 057816. Поэтому ответ на вопрос о представлении числа в общем случае имеет два верных ответа (в данном случае 780516, 057816). Конкретный ответ можно дать, только зная тип машины.

Вопрос – как определить, какая у пользователя машина? Первый вариант – узнать, какой процессор у пользователя. Как

показывает статистика, в России распространены процессоры x86. Второй вариант очень прост – программирование – экспериментальная

наука. Надо написать небольшую программу и посмотреть на результирующий файл в 16-ричном коде.

Var Temp : word; F : file of word; Begin Temp := 1400; Assign(f, ‘test.dat’); Rewrite(f); Write(f, temp); Close(f); End.

На машине, на которой писалось это руководство, содержимое файла было «7805» в 16-ричной системе, т. е. это младшеконечная машина.

1 Порядок от младшего к старшему принят в памяти персональных компьютеров с x86-процессорами; иногда его называют интеловский порядок байтов (по названию фирмы-создателя архитектуры x86). 2 Этот порядок является стандартным для протоколов TCP/IP, он используется в заголовках пакетов данных и во многих протоколах более высокого уровня, разработанных для использования поверх TCP/IP. Поэтому порядок байтов от старшего к младшему часто называют сетевым порядком байтов (англ. network byte order). Этот порядок байтов используется процессорами IBM 360/370/390, Motorola 68000, SPARC (отсюда третье название — порядок байтов Motorola, Motorola byte order).

Page 76: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

76

Пример 3 Найти внутреннее представление числа 700, если это двухбайтовый

беззнаковый тип. Переведем 700 из десятичной системы в двоичную. 70010=10 1011 11002. Так как это двухбайтовый тип, то у него 16 разрядов, т. е. 70010=10 1011 11002=0000 0010 1011 11002=02BC16

В этом двухбайтовом числе 02 – старший байт, BC – младший байт. Тогда для старшеконечной машины внутреннее представление числа

будет BC0216, а для младшеконечной машины – 02BC16.

Представление знаковых целых чисел в памяти ЭВМ Целые числа со знаком обычно занимают в памяти компьютера один, два

или четыре байта. Для хранения целых чисел со знаком старший (левый) разряд в машинном слове отводится под знак числа (если число положительное, то в знаковый разряд записывается ноль, если число отрицательное – единица). Ровно половина из всех 2n

чисел будут отрицательными; учитывая необходимость нулевого значения, положительных будет на единицу меньше.

Таким образом, по первому биту в двоичном коде можно определить, положительное число или отрицательно.

Знаковые типы хранятся в памяти машины в дополнительном коде. Однако при работе с дополнительным кодом используется обратный код. Поэтому изучение формата хранения знаковых целых чисел начнем с него.

Обратный код В обратном коде могут храниться положительные или отрицательные

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

Обратный код положительного числа совпадает с его прямым кодом. Чтобы получить внутреннее представление положительного целого числа А, хранящегося в n-разрядном машинном слове, необходимо:

1) записать в старший бит 0; 2) перевести число А в двоичную систему счисления; 3) полученный результат дополнить слева незначащими нулями до (n–1)

разрядов; 4) «склеить» знаковый бит и результат пункта 3. Обратный код отрицательного числа получается инвертированием всех

бит, кроме знакового.

Пример 1 Перевести число 16 в обратный код, если это двухбайтовое знаковое

число Так как это двухбайтовая величина, то для него отводится 16 разрядов.

Page 77: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

77

Поэтому ответ будет вида ХХХХ ХХХХ ХХХХ ХХХХ. Так как 16 – положительное число, то старший бит будет равен 0.

Поэтому ответ будет вида 0ХХХ ХХХХ ХХХХ ХХХХ, т. к. у нас «осталось» 15 бит для числа.

Переведем 16 в двоичную систему. 1610=100002. Так как у нас «остались» 15 бит для числа, дополним

полученное число нулями слева до 15 разрядов. 1610=100002=000 0000 0001 00002. Теперь «склеим» знаковый бит (0) и полученное число (000 0000 0001

00002). Получим 0000 0000 0001 00002. Для удобства записи запишем в 16-ричной системе. 001016. У этого числа старший байт равен «00», а младший байт – «10».

Если машина младшеконечная, то число будет храниться в виде «0010» в памяти машины.

Если машина старшеконечная, то число будет храниться в виде «1000» в памяти машины.

Пример 2. Перевести число 100737027 в обратный код, если это четырехбайтовое

знаковое число. Так как это четырехбайтовая величина, то для него отводится 32 разряда.

Поэтому ответ будет вида ХХХХ ХХХХ ХХХХ ХХХХ ХХХХ ХХХХ ХХХХ ХХХХ.

Так как 100737027 – положительное число, то старший бит будет равен 0. Поэтому ответ будет вида 0ХХХ ХХХХ ХХХХ ХХХХ ХХХХ ХХХХ ХХХХ ХХХХ, так как у нас «остался» 31 бит для числа.

Переведем 100737027 в двоичную систему, дополнив нулями до 31 бита. 100737027 10=000 0110 0000 0010 0000 0000 00112=0601200316 Теперь «склеим» знаковый бит (0) и полученное число (000 0110 0000

0010 0000 0000 00112). Получим 0000 0110 0000 0010 0000 0000 00112. Для удобства записи запишем в 16-ричной системе. 0601200316. В этом числе содержатся следующие байты:0616, 0116, 2016, 0316.

Если машина младшеконечная, то число будет храниться в виде «0601200316» в памяти машины, т. к. порядок байтов не меняется.

Если машина старшеконечная, то число будет храниться в виде «0320010616» в памяти машины, т. к. порядок байтов инвертируется.

Пример 3 Перевести –128 в обратный код, если это однобайтовое знаковое число Так как это однобайтовая величина, то для него отводится 8 разрядов.

Поэтому ответ будет вида ХХХХ ХХХХ. Так как –128 – отрицательное число, то старший бит будет равен 1.

Поэтому ответ будет вида 1ХХХ ХХХХ, т. е. у нас «остались» 7 бит для числа.

Page 78: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

78

Переведем 128 в двоичную систему. 12810=1000 00002 Так как это число отрицательное, то все биты в числе 128 необходимо

инвертировать 1000 00002 0111 11112

Т. к. у нас оставались 7 бит для числа, то уберем старший бит, поскольку он незначащий. Получим 111 11112

Теперь «склеим» знаковый бит (1) и полученное число (111 11112). Получим 1111 11112. Для удобства записи запишем в 16-ричной системе: FF16.

Пример 4 Перевести -1 в обратный код, если это знаковое однобайтовое число. Заданное число – однобайтовая величина, то для него отводится 8

разрядов. Ответ будет вида ХХХХ ХХХХ. Так как –1 – отрицательное число, то старший бит будет равен 1. Поэтому

ответ будет вида 1ХХХ ХХХХ, т. е. у нас «остались» 7 бит для числа. Переведем 1 в двоичную систему. 110=000 00012 Так как это число отрицательное, то все биты в числе 1 необходимо

инвертировать 000 00012 111 11102

Теперь «склеим» знаковый бит (1) и полученное число (111 11102). Получим 1111 11102. Для удобства записи запишем в 16-ричной системе: FE16.

Следствие (одна из причин, по которой обратный код редко

употребляется). Зададимся простым вопросом – сколько будет 1 + (–1). Очевидно, что 0. Проверим это. +1 в обратном коде будет 0000 0001 –1 в обратном коде будет 1111 1110 Сложим эти два числа. 0000 0001 1111 1110 1111 1111. Это число из примера, т. е. ответ оказался не 0, а –128. В дополнительном коде, который будет рассмотрен ниже, этот недостаток

сумели убрать.

Пример 5 Перевести –1000 в обратный код, если это двухбайтовое знаковое число. Так как это двухбайтовая величина, то для него отводится 16 разрядов.

Поэтому ответ будет вида ХХХХ ХХХХ XXXX XXXX.

Page 79: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

79

Так как – 1000 – отрицательное число, то старший бит будет равен 1. Поэтому ответ будет вида 1ХХХ ХХХХ XXXX XXXX, т. е. у нас «остались» 15 бит для числа.

Переведем 1000 в двоичную систему. 100010=11 1110 10002 Дополним спереди нулями до 15 разрядов 100010=11 1110 10002 = 000 0011 1110 10002

Так как это число отрицательное, то все биты в числе 1000 необходимо инвертировать

000 0011 1110 10002

111 1100 0001 01112

Теперь «склеим» знаковый бит (1) и полученное число (111 1100 0001 01112). Получим 1111 1100 0001 01112. Для удобства записи запишем в 16-ричной системе. FС1716.

В старшеконечной машине число будет 17FС16

В младшеконечной машине число будет FC1716.

   

Page 80: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

80

Дополнительный код В дополнительном коде могут храниться целые знаковые числа. Так же

как и в обратном коде, старший бит является знаковым (1, если число отрицательное, и 0 – если число положительное).

Для положительных чисел дополнительный код полностью совпадает с обратным кодом (который совпадает с прямым кодом).

Дополнительный код отрицательного числа m равен 2k–|m|, где k – количество разрядов в ячейке. Отсюда вытекает алгоритм получения дополнительного кода:

1. Получить прямой код числа. 2. Получить обратный код этого числа (инвертировать все биты). 3. К полученному числу прибавить 1.

Пример 1 Перевести число 16 в дополнительный код, если это двухбайтовое

знаковое число. Так как это двухбайтовая величина, то для него отводится 16 разрядов.

Поэтому ответ будет вида ХХХХ ХХХХ ХХХХ ХХХХ. Так как 16 – положительное число, то старший бит будет равен 0.

Поэтому ответ будет вида 0ХХХ ХХХХ ХХХХ ХХХХ, так как у нас «остались» 15 бит для числа.

Переведем 16 в двоичную систему. 1610=100002. Так как у нас «остались» 15 бит для числа, дополним

полученное число нулями слева до 15 разрядов. 1610=100002=000 0000 0001 00002. Теперь «склеим» знаковый бит (0) и полученное число (000 0000 0001

00002). Получим 0000 0000 0001 00002. Для удобства записи запишем в 16-ричной системе. 001016. У этого числа старший бит равен «00», а младший бит – «10».

Если машина младшеконечная, то число будет храниться в виде «0010» в памяти машины.

Если машина старшеконечная, то число будет храниться в виде «1000» в памяти машины.

Пример 2 Перевести число 100737027 в дополнительный код, если это

четырехбайтовое знаковое число. Так как это четырехбайтовая величина, то для него отводится 32 разряда.

Поэтому ответ будет вида ХХХХ ХХХХ ХХХХ ХХХХ ХХХХ ХХХХ ХХХХ ХХХХ.

Так как 100737027 – положительное число, то старший бит будет равен 0. Поэтому ответ будет вида 0ХХХ ХХХХ ХХХХ ХХХХ ХХХХ ХХХХ ХХХХ ХХХХ, т. к. у нас «остался» 31 бит для числа.

Page 81: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

81

Переведем 100737027 в двоичную систему, дополнив нулями до 31 бита. 100737027 10=000 0110 0000 0010 0000 0000 00112=0601200316 Теперь «склеим» знаковый бит (0) и полученное число (000 0110 0000

0010 0000 0000 00112). Получим 0000 0110 0000 0010 0000 0000 00112. Для удобства записи запишем в 16-ричной системе. 0601200316. В этом числе содержатся следующий байты:0616, 0116, 2016, 0316

Если машина младшеконечная, то число будет храниться в виде «0601200316» в памяти машины, т. к. порядок байтов не меняется

Если машина старшеконечная, то число будет храниться в виде «0320010616» в памяти машины, т. к. порядок байтов инвертируется.

Пример 3 Перевести –128 в дополнительный код, если это однобайтовое знаковое

число. Поскольку это однобайтовая величина, то для него отводится 8 разрядов.

Поэтому ответ будет вида ХХХХ ХХХХ. Так как –128 – отрицательное число, то старший бит будет равен 1.

Поэтому ответ будет вида 1ХХХ ХХХХ, т. е. у нас «остались» 7 бит для числа. Переведем 128 в двоичную систему. 12810=1000 00002 Так как это число отрицательное, то все биты в числе 128 необходимо

инвертировать 1000 00002 0111 11112

Теперь прибавим к полученному числу 1. Получим 1000 00002. Так как у нас оставались 7 бит для числа, то уберем старший бит. Получим 000 00002.

Теперь «склеим» знаковый бит (1) и полученное число (000 00002). Получим 1000 00002. Для удобства записи запишем в 16-ричной системе: 8016.

Пример 4 Перевести –1 в дополнительный код, если это знаковое однобайтовое

число. Решение. Так как это однобайтовая величина, то для него отводится 8 разрядов.

Поэтому ответ будет вида ХХХХ ХХХХ. Так как –1 – отрицательное число, то старший бит будет равен 1. Поэтому

ответ будет вида 1ХХХ ХХХХ, т. е. у нас «остались» 7 бит для числа. Переведем 1 в двоичную систему. 110=000 00012 Так как это число отрицательное, то все биты в числе 1 необходимо

инвертировать 000 00012 111 11102

Page 82: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

82

Теперь прибавим к этому числу 1. Получим 111 11112. Теперь «склеим» знаковый бит (1) и полученное число (111 11112). Получим 1111 11112. Для удобства записи запишем в 16-ричной системе: FE16.

Следствие (одна из причин, по которой дополнительный код часто

употребляется). Зададимся простым вопросом – сколько будет 1 + (–1)? Очевидно, что 0. Проверим это. +1 в дополнительном коде будет 0000 0001 –1 в дополнительном коде будет 1111 1111 Сложим эти два числа. 0000 0001 1111 1111 0000 0000. Это число равно +0, т. е. 0. Поэтому в дополнительном коде

можно суммировать любые числа (положительные и отрицательные). Замечание. Если быть более точным, то в данном сложении у нас

«вышла» единица за 8-разрядную сетку. Но при сложении чисел на языках высокого уровня это можно не учитывать.

Пример 5 Перевести –1000 в дополнительный код, если это двухбайтовое знаковое

число. Решение. Так как это двухбайтовая величина, то для него отводится 16 разрядов.

Поэтому ответ будет вида ХХХХ ХХХХ XXXX XXXX. Так как –1000 – отрицательное число, то старший бит будет равен 1.

Поэтому ответ будет вида 1ХХХ ХХХХ XXXX XXXX, т. е. у нас «остались» 15 бит для числа.

Переведем 1000 в двоичную систему. 100010=11 1110 10002 Дополним спереди нулями до 15 разрядов 100010=11 1110 10002 = 000 0011 1110 10002

Так как это число отрицательное, то все биты в числе 1000 необходимо инвертировать

000 0011 1110 10002

111 1100 0001 01112

После этого прибавим к полученному числу 1. Получиться 111 1100 0001 10002.

Теперь «склеим» знаковый бит (1) и полученное число (111 1100 0001 10002). Получим 1111 1100 0001 10002. Для удобства записи запишем в 16-ричной системе: FС1816.

В старшеконечной машине число будет 18FС16. В младшеконечной машине число будет FC1816.

Page 83: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

83

Преобразование из машинного представления в десятичную систему

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

Пример 1 Найти десятичное значение числа AF0416, если это двухбайтовый

знаковый тип и старшеконечная машина. Так как машина старшеконечная, то это число 04AF16. Переведем это

число в двоичную систему 04AF16=0000 0100 1010 11112. Как видно, старший бит равен 0. Поэтому это положительное число. Так

как это число положительное, то не надо инвертировать биты. Достаточно просто перевести 0000 0100 1010 11112 в десятичную систему. Это будет число 1199.

Ответ: +1199.

Пример 2 Найти десятичное значение числа AFA416, если это двухбайтовый

знаковый тип и старшеконечная машина. Поскольку машина старшеконечная, то это число A4AF16. Переведем это

число в двоичную систему A4AF16=1010 0100 1010 11112. Как видно, старший бит равен 1. Поэтому это число отрицательное. Чтобы получить модуль значения, необходимо вначале отнять 1. 010 0100 1010 11112-12=010 0100 1010 11102

Потом необходимо инвертировать все биты 010 0100 1010 11102

101 1011 0101 00012

Затем необходимо перевести 101 1011 0101 00012 в десятичную систему. Это число 23377.

Ответ: -23377.

Пример 3 Найти десятичное значение числа AF16, если это однобайтовый знаковый

тип. Переведем это число в двоичную систему. AF16=1010 11112. Как видно, старший бит равен 1. Поэтому это число отрицательное.

Чтобы получить модуль значения, необходимо в начале отнять 1.

Page 84: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

84

010 11112–12=010 11102

Потом необходимо инвертировать все биты 010 11102

101 00012

Затем надо 101 00012 перевести в десятичную систему. Это число 81. Ответ: –81.

Важное следствие (пример 1) Очень часто в программировании возникает необходимость в

преобразовании чисел одного типа в другой. Если мы преобразуем из менее точного (или менее большого) в более точный (или более большой), то сложностей не возникает.

Например,

var a : byte; begin clrscr; a := 21; write(integer(a)); readkey; end.

Очевидно, что в данном случае пользователь увидит на экране «21», потому что мы преобразуем из меньшего типа (byte, который занимает 1 байт) в больший (integer, который занимает 2 байта).

Но что увидит пользователь, если немного изменим программу?

var a : integer; begin clrscr; a := 12198; write(byte(a)); readkey; end.

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

Это число положительное, двухбайтовое, поэтому оно будет занимать 16 бит, старший бит равен 0. 1219810=10 1111 1010 01102. Дополним до 15 бит и «склеим» со знаковым битом. Получим 0010 1111 1010 01102. Для удобства запишем в 16-ричной системе. Получим 2FA616.

Если машина старшеконечная, то в памяти эти 2 байта будут

Page 85: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

85

располагаться в обратном порядке, т. е. A62F16. При преобразовании в однобайтовый тип (byte) будет взят только первый

байт, т. е. A616. Так как byte – беззнаковый тип, то для получения ответа необходимо просто перевести A616 в десятичную систему. Получим 166.

Ответ: 166.

Важное следствие (пример 2) Но что увидит пользователь, если мы немного изменим программу?

var a : shortint; begin clrscr; a := 12198; write(shortint(a)); readkey; end.

Очевидно, что число 12198 не укладывается в диапазон типа shortint. Для того чтобы ответить на вопрос, необходимо получить внутреннее представление числа 12198. Возьмем его из предыдущего примера – 2FA616.

Если машина старшеконечная, то в памяти эти 2 байта будут располагаться в обратном порядке, т. е. A62F16.

При преобразовании в однобайтовый тип (shortint) будет взят только первый байт, т. е. A616.

Переведем это число в двоичную систему. A616=1010 01102. Очевидно, что shortint – знаковый тип, и старший бит равен 1. Поэтому

это число отрицательное. Чтобы получить модуль этого числа, необходимо вначале вычесть

единицу (естественно знаковый бит отбрасывается при получении модуля). 010 01102–12=010 01012. Теперь необходимо инвертировать все биты. Получим 010 01012

101 10102 Теперь переведем 101 10102 в десятичную систему. Получим 90. Ответ: –90.

Page 86: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

86

Представление вещественных типов в памяти ЭВМ Для начала вспомним, что такое нормальная форма десятичного числа. Нормальная форма в общем виде выражается следующим образом: R=m×pn, где m – мантисса, p – основание системы счисления, n – порядок. Приведем пару примеров нормировки чисел

число нормированное число 456 4,56⨉102 0,02 2⨉10-2

123,02 1,2302⨉102 100 1⨉102 1 1⨉100

Для объяснения внутреннего представления чисел приведем аналогичные рассуждения, наложив ограничение: первая значащая цифра в нормированном числе всегда «1».

Пример 1 Пусть у нас есть нормированное число: 1,56⨉10-2, которое надо

представить в 16 ячейках, одна из которых отведена под знак, 10 ячеек отведены под мантиссу, 5 ячеек отведены под порядок.

В общем виде результат представления равен X X X X X X X X X X X X X X X X

Одинарным подчеркиванием выделены разряды для мантиссы, двойным подчеркиванием выделены разряды для порядка.

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

Теперь нам надо записать мантиссу в 10 разрядов. Поэтому дополним число 1,56 незначащими нулями до 10 разрядов: 1,560000000.

Так как у нас есть ограничение 1, то мы можем «отбросить» первую единицу, потому что она у нас всегда там есть (замечание: единственное исключение – число 0. Оно в памяти ЭВМ представляется исключительно). Так как мы отбросили «1» в начале, то допишем еще один незначащий ноль в конце и запишем в соответствующие разряды. 0 X X X X X 5 6 0 0 0 0 0 0 0 0

Теперь нам надо записать порядок в 5 разрядов. Но проблема заключается в том, что он равен P = –2, т. е. знаковому числу. Поэтому принято смещать порядок относительно 0, т. е. записывают не P, а P + 10000 (09999 взято для примера. Необходимы 5 цифр, потому что отведены 4 разряда, и первый незначащий 0). Поэтому в разряды порядка попадет число (–2 + 09999 = 09997) 0 0 9 9 9 7 5 6 0 0 0 0 0 0 0 0

Page 87: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

87

Мы получили число, которое храниться по принципам, аналогичным принципам хранения чисел с плавающей запятой.

Пример 2 Пусть у нас есть нормированное число: –1,56⨉102 , которое надо

представить в 16 ячейках, одна из которых отведена под знак, 10 ячеек отведены под мантиссу, 5 ячеек отведены под порядок.

В общем виде результат представления равен X X X X X X X X X X X X X X X X

В первом разряде храниться «1», потому что число отрицательное. Поэтому следующий шаг к ответу будет 1 X X X X X X X X X X X X X X X

Теперь нам надо записать мантиссу в 10 разрядов. Поэтому дополним число 1,56 незначащими нулями до 10 разрядов: 1,560000000.

Так как у нас есть ограничение 1, то мы можем «отбросить» первую единицу. Так как мы отбросили «1» в начале, то допишем еще один незначащий ноль в конце и запишем в соответствующие разряды. 1 X X X X X 5 6 0 0 0 0 0 0 0 0

Теперь нам надо записать порядок (P=2) в 5 разрядов. Прибавим к порядку смещение, получим 09999 + 2 = 10001. 1 1 0 0 0 1 5 6 0 0 0 0 0 0 0 0

Мы получили число, которое храниться по принципам, аналогичным принципам хранения чисел с плавающей запятой.

   

Page 88: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

88

Переход в двоичную систему Что измениться при переходе в двоичную систему?

1. Ограничение 1 будет выполняться автоматически. 2. Основание системы счисления будет равно 2.

А все остальные принципы остаются такими же.

Пример 1 Представить число 3.25 в 16 битах в форме с плавающей запятой, один из

которых отведен под знак, 10 отведены под мантиссу, 5 отведены под порядок. Переведем 3.25 в двоичную систему. Получим 11.01. Пронормируем это число. Получим 1,101⨉21. В общем виде результат представления равен

X X X X X X X X X X X X X X X X Так как число положительное, то в первом бите будет храниться 0.

Поэтому следующий шаг к ответу будет 0 X X X X X X X X X X X X X X X

Теперь нам надо записать мантиссу в 10 разрядов. Поэтому дополним число 1,101 незначащими нулями до 10 разрядов: 1,101000000.

Мы можем «отбросить» первую единицу, потому что она у нас всегда там есть. Так как мы отбросили «1» в начале, то допишем еще один незначащий ноль в конце и запишем в соответствующие разряды. 0 X X X X X 1 0 1 0 0 0 0 0 0 0

Теперь нам надо записать порядок в 5 разрядов. В качестве смещения выберем 011112. Поэтому в 5 разрядов надо записать (011112+12=100002). 0 1 0 0 0 0 1 0 1 0 0 0 0 0 0 0

Для удобства запишем в 16-ричной системе:501116.

Пример 2 Найти представление числа 3.25 в форме с плавающей запятой, если это

4-байтовое число и под порядок отводится 9 бит. Это тип Single. Так как он занимает 4 байта, то это 32 бита. Поэтому в

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

Переведем число 3.25 в двоичную систему. 3.2510=11.012 Пронормируем это число. Получим 11.012=1.101⨉21. Порядок равен 1. Так как под него отведено 9 бит, то смещение будет

равно 011111111. Поэтому в порядок запишется (0111111112+12=100000000). Поэтому следующий шаг к ответу будет.

Page 89: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

89

0 1 0 0 0 0 0 0 0 0

Мантисса у нашего числа получилась 1.101. Откинем первую единицу, получим .101. Это число дополним нулями с конца, чтобы оно занимало 22 разряда (32 – 1 – 9 = 22). Поэтому ответ будет 0 1 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Для удобства запишем в 16-ричной системе: 4028000016. Если машина старшеконечная, то в памяти ЭВМ байты будут

распологаться в таком порядке: 0000284016

Пример 3 Найти представление числа 0.375 в форме с плавающей запятой, если это

4-байтовое число и под порядок отводится 9 бит. Это тип Single. Так как он занимает 4 байта, то это 32 бита. Поэтому в

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

Переведем число 0.375 в двоичную систему. 0.375= 0.0112 Пронормируем это число 0.0112=1.1⨉2-2. Порядок равен –210= –102. Под него отведено 9 бит, то смещение будет

равно 011111111. Поэтому в порядок запишется (0111111112–102=011111101). Поэтому следующий шаг к ответу будет. 0 0 1 1 1 1 1 1 0 1

Мантисса у нашего числа получилась 1.1. Откинем первую единицу, получим .1. Это число дополним нулями с конца, чтобы оно занимало 22 разряда (32 – 1 – 9 = 22). Поэтому ответ будет 0 0 1 1 1 1 1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Для удобства запишем в 16-ричной системе: 3F60000016. Если машина старшеконечная, то в памяти ЭВМ байты будут

распологаться в таком порядке: 0000603F16.

Пример 4 Найти представление числа 139,76 в форме с плавающей запятой, если

это 4-байтовое число и под порядок отводится 9 бит. Это тип Single. Так как он занимает 4 байта, то это 32 бита. Поэтому в

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

Переведем число 139,76 в двоичную систему.

Page 90: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

90

139,7610 = 10001011,11000010100011112

Пронормируем полученное число 10001011,11000010100011112=1,000101111000010100011112⨉27

Порядок равен 710=1112. Под него отведены 9 бит, то смещение будет равно 011111111. Поэтому в порядок запишется (0111111112+1112=100000010). Поэтому следующий шаг к ответу будет 0 1 0 0 0 0 0 0 1 0

Мантисса у нашего числа получилась 1.000101111000010100011112. Откинем первую единицу, получим .000101111000010100011112. Однако это число получилось больше, чем 22 разряда. Поэтому необходимо откинуть последнюю единицу (из-за этого появляется погрешность при представлении числа в памяти ЭВМ). Ответ будет 0 1 0 0 0 0 0 0 1 0 0 0 0 1 0 1 1 1 1 0 0 0 0 1 0 1 0 0 0 1 1 1

Для удобства запишем в 16-ричной системе: 4085E14716. Если машина старшеконечная, то в памяти ЭВМ это число будет

располагаться 47E1854016.

Представление числа «0» в памяти ЭВМ Число 0 – исключение из правил, так как у него нет значащей единицы. Поэтому его представление в памяти ЭВМ – все 0. То есть в 16-ричной системе для 4-байтового числа с плавающей запятой это будет 0000000016.

   

Page 91: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

91

Общий алгоритм сложения (или вычитания) чисел с плавающей точкой

Алгоритм сложения (или вычитания) чисел с плавающей точкой следующий:

1. Если у чисел разные порядки, то сдвигать вправо число с меньшим порядком до тех пор, пока порядки не станут равными.

2. Сложить (или вычесть) мантиссы. 3. Если надо, то пронормировать результат. Учитывать, что при

нормировке изменится порядок числа.

Пример 1 Сложить 2 числа с плавающей точкой. Числа равны 3.25 и 0.375. Для

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

ЭВМ. 3.25

0 1 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

0.375 0 0 1 1 1 1 1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

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

Сдвинем число последовательно по 1 разряду 3 раза. Получим 0 0 1 1 1 1 1 1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

0 0 1 1 1 1 1 1 1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

0 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Таким образом, мы получили 3.25 (с «отброшенной единицей»)

0 1 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

0.375 (без «отброшенной единицы») 0 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Теперь можно сложить мантиссы. Получим 0 1 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Важно отметить, что у нас не было переполнения мантиссы, т. е. не надо нормировать результат сложения мантисс. Для удобства запишем число в 16-ричной системе: 4014000016.

Page 92: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

92

Важное следствие: Если порядки сильно отличаются, то второе число может быть просто

«отброшено».

Пример 2 Вычесть из 3.75 число 3.25, если это числа с плавающей точкой. Для

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

ЭВМ. 0.375

0 1 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

3.25 0 1 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Как видим, эти 2 числа имеют одинаковый порядок, следовательно, ни

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

И произведем вычитание. Получим 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

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

Теперь можно откинуть первую значащую единицу 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

И записать порядок, предварительно уменьшив его на 2, т. е. на 102. 1000000002–102=0111111012.

Очевидно, что при вычитании знак числа не поменялся, поэтому в старшем бите будет 0. 0 0 1 1 1 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Для удобства запишем в 16-ричной системе:3F40000016

Алгоритмы работы с числами, которые представлены строкой Исследование формата чисел полезно, если программист хочет написать

эффективную, быструю программу. Однако часто стандартных численных типов не хватает. Максимальная точность, которую можно получить, – это 20 знаков. Поэтому возникает вопрос об альтернативных методах работы с числами.

Первый из них – представление больших чисел в виде строки. В этом

Page 93: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

93

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

Сложение положительных чисел, представленных в виде строки Пусть у нас стоит задача сложения двух положительных чисел, которые

представлены в виде строки. Например, в результате выполнения операции «17» + «11» мы получим 28.

Как это происходит? В школе нас научили сложению «столбиком». 1 7 1 1 2 8

Как нас научили, мы начинаем складывать с конца числа, поразрядно. 7 + 1 = 8 1 + 1 = 2. Казалось бы, что алгоритм сложения будет выглядеть так: начиная с

конца чисел складывать поразрядно и формировать результат. Но это самый простой случай. Чуть усложним его. Пусть у нас стоит задача сложить числа «17» и «2». Как это происходит?

1 7 0 2 1 9

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

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

Еще усложним наш пример. Что будет, если складывать «17» и «15»? 1 7 1 5 3 2

Результат сложения 7 и 5 – это 12. Но это больше 9, поэтому остается только 2, и появляется перенос в старший разряд.

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

Page 94: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

94

вычесть из него 10 и установить флаг переноса. Если результат поразрядного сложения меньше 9, то сбросить флаг переноса. Записать результат текущего сложения разрядов в результат.

Это уже лучше. Однако у нас в этом алгоритме не отобразилось, на что влияет флаг переноса. Поэтому алгоритм примет следующий вид:

Если числа имеют разное количество знаков, то дополнить меньшее из них нулями спереди до тех пор, пока они не станут равной длины. Потом, начиная с конца чисел, складывать поразрядно. Если флаг переноса установлен, то прибавить к сумме 1. Если результат сложения больше 9, то вычесть из него 10 и установить флаг переноса. Если результат сложения меньше 9, то сбросить флаг переноса. Записать результат текущего сложения разрядов в результат.

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

Сбросить флаг переноса. Если числа имеют разное количество знаков, то дополнить меньшее из них нулями спереди до тех пор, пока они не станут равной длины. Потом, начиная с конца чисел, складывать поразрядно. Если флаг переноса установлен, то прибавить к сумме 1. Если результат сложения больше 9, то вычесть из него 10 и установить флаг переноса. Если результат сложения меньше 9, то сбросить флаг переноса. Записать результат текущего сложения разрядов в результат.

Теперь, казалось бы, все нормально. Но этот алгоритм подразумевает, что результат суммы имеет такое же количество разрядов, как и слагаемые (после дополнения нулями). Однако это не так. Примером может служить сумма 50 и 60 – результат будет 110, т. е. 3 разряда, а не 2.

Поэтому алгоритм сложения будет выглядеть следующим образом: Сбросить флаг переноса. Если числа имеют разное количество знаков, то дополнить меньшее из них нулями спереди до тех пор, пока они не станут равной длины. Потом, начиная с конца чисел, складывать поразрядно. Если флаг переноса установлен, то прибавить к сумме 1. Если результат сложения больше 9, то вычесть из него 10 и установить флаг переноса. Если результат сложения меньше 9, то сбросить флаг переноса. Записать результат текущего сложения разрядов в результат. Если после цикла флаг переноса установлен, то добавить к результату спереди 1.

Однако в данном алгоритме никак не прозвучало то, что числа хранятся в виде строки. Поэтому возникает вопрос о преобразовании символа, который хранится в элементе строки, в соответствующий код (и наоборот, при формировании результата). Известно, что в ANSII кодировке цифры располагаются последовательно от 0 до 9 включительно. Это свойство мы и

Page 95: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

95

будем использовать в решении. Еще не прозвучала инициализация результата сложения пустой строкой. Ниже приведено полное решение данной задачи. Логика переменных:

s1, s2 – строки, в которых хранятся числа, которые ввел пользователь res – строка, в которой будет записан результат сложения flag – флаг переноса i – счетчик цикла для того, чтобы проходить по элементам строки num1, num2 – десятичное значение текущих разрядов в первом и втором числе соответственно sum – результат сложения текущих разрядов с учетом флага переноса sumChar – символ, в который записывается результат сложения текущих разрядов в виде символа.

function getSum(s1, s2 : string) : string; var res : string; flag : boolean; i : integer; num1, num2 : byte; sum : byte; sumChar : char; begin res := ''; flag := false; while (length(s1) < length(s2)) do s1 := '0' + s1; while (length(s2) < length(s1)) do s2 := '0' + s2; for i := length(s1) downto 1 do begin num1 := ord(s1[i]) - ord('0'); num2 := ord(s2[i]) - ord('0'); sum := num1 + num2; if (flag) then sum := sum + 1; flag := (sum > 9); if (sum > 9) then sum := sum - 10; sumChar := chr(sum + ord('0')); res := sumChar + res; end; if (flag) then res := '1' + res; getSum := res; end;

Page 96: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

96

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

Пусть у нас задача умножения положительного числа на одноразрядный коэффициент. Например, в результате выполнения операции «17» * «2» мы получим 34.

Как это происходит? Мы умножаем каждый разряд, начиная с конца числа, на коэффициент. В разряд результата мы заносим остаток от деления на 10, а в следующий разряд мы переносим целую часть от деления на 10.

Отличием от сложения чисел является то, то переносом может быть не только 1 или 0, а числа от 0 до 8. Поэтому флаг переноса нам уже не подойдет, необходима переменная типа byte. Остальной алгоритм почти не изменится.

Получившийся алгоритм: 1. В перенос записать 0 2. Инициализировать результат пустой строкой 3. От конца строки до начала

a. Преобразовать текущий символ в цифру b. Умножить цифру на коэффициент c. Добавить к полученному результату перенос d. В перенос записать целую часть от деления результата на

10 e. В результат записать остаток от деления результата на 10 f. Преобразовать результат в символ g. Добавить полученный символ в начало результирующей

строки. 4. Если остался перенос, то

a. Преобразовать его в символ b. Добавить полученный символ в начало строки.

Ниже приведено полное решение данной задачи.

function getMul(s : string; k : byte) : string; var res : string; i : integer; carry : byte; num : byte; numChar : char; begin res := ''; carry := 0; for i := length(s) downto 1 do begin num := ord(s[i]) - ord('0');

Page 97: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

97

num := num * k; num := num + carry; carry := num div 10; num := num mod 10; numChar := chr(num + ord('0')); res := numChar + res; end; if (carry > 0) then begin numChar := chr(carry + ord('0')); res := numChar + res; end; {$B-} while ((length(res) > 1) and (res[1] = '0')) do delete(res, 1, 1); getMul := res; end;

Page 98: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

98

Умножение двух положительных целых чисел, представленных

в виде строки Пусть у нас стоит задача умножения двух целых чисел, представленных в

виде строки, например «123» ⨉ «12». Как это происходит?

1 2 3 1 2 2 4 6 1 2 3 0

Вначале мы умножаем 123 на младший разряд (в этом случае 2). Потом умножаем на более старший разряд (в данном случае на 1). При этом результат умножения смещается на 1 разряд. Смещение выражается в том, что справа к результату дописывается ноль.

Таким образом, умножение можно записать 123*12=123*2+(123*1) *10. Как видим, в данной записи у нас используются только операции

суммирования, умножения числа на одноразрядный коэффициент, умножение числа на 10.

Однако в этой записи никак не фигурирует результат (как переменная, которая будет «накапливать» операции сложения). Поэтому умножение надо записать в следующем виде: 123*12= 0 + 123*2+(123*1)*10

Используя ранее написанные операции, легко реализовать алгоритм умножения.

Получился алгоритм: 1. Инициализировать res (результат) «0». 2. Инициализировать смещение 0. 3. Для каждого символа второго числа, начиная с конца,

a. Преобразовать символ в цифру. b. Первое число умножить на получившуюся цифру и

результат умножения записать во временную строковую переменную.

c. От одного до смещения приписывать к временной переменной справа по одному нулю.

d. Увеличить смещение на 1. e. Просуммировать res и временную переменную и записать

результат суммирования в res. 4. Res – результат умножения двух чисел, представленных в виде

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

Page 99: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

99

Реализация алгоритма на Pascal

function getSum(s1, s2 : string) : string; var res : string; flag : boolean; i : integer; num1, num2 : byte; sum : byte; sumChar : char; begin res := ''; flag := false; while (length(s1) < length(s2)) do s1 := '0' + s1; while (length(s2) < length(s1)) do s2 := '0' + s2; for i := length(s1) downto 1 do begin num1 := ord(s1[i]) - ord('0'); num2 := ord(s2[i]) - ord('0'); sum := num1 + num2; if (flag) then sum := sum + 1; flag := (sum > 9); if (sum > 9) then sum := sum - 10; sumChar := chr(sum + ord('0')); res := sumChar + res; end; if (flag) then res := '1' + res; getSum := res; end; function getMul(s : string; k : byte) : string; var res : string; i : integer; carry : byte; num : byte; numChar : char; begin res := ''; carry := 0; for i := length(s) downto 1 do begin

Page 100: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

100

num := ord(s[i]) - ord('0'); num := num * k; num := num + carry; carry := num div 10; num := num mod 10; numChar := chr(num + ord('0')); res := numChar + res; end; if (carry > 0) then begin numChar := chr(carry + ord('0')); res := numChar + res; end; {$B-} while ((length(res) > 1) and (res[1] = '0')) do delete(res, 1, 1); getMul := res; end; function getStrMul(const s1, s2 : string) : string; var res : string; tempMul : string; curVal : integer; offset : integer; i, j : integer; begin res := '0'; offset := 0; for i := length(s2) downto 1 do begin tempMul := s1; curVal := ord(s2[i]) - ord('0'); tempMul := getMul(tempMul, curVal); for j := 1 to offset do begin tempMul := tempMul + '0'; end; inc(offset); res := getSum(res, tempMul); end; getStrMul := res; end;

Page 101: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

101

Сравнение двух целых чисел, представленных в виде строки Пусть у нас стоит задача сравнения двух целых чисел, представленных в

виде строки. Например, надо сравнить «123» и «12». Но с точки зрения математики записи «12» и «012» абсолютно

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

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

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

В программировании часто встречаются функции сравнения a и b ,

которые возвращают

1,

0,

1,

a b

compare a b

a b

.

Будем придерживаться аналогичных правил. Полная реализация алгоритма приведена ниже.

Реализация алгоритма на Pascal

function compare(s1, s2 : string) : shortint; function prepare(s : string) : string; var res : string; begin res := s; {$B-} while (length(res) > 0) and (res[1] = '0') do delete(res, 1, 1); prepare := res; end; var i : integer; begin s1 := prepare(s1); s2 := prepare(s2); if (length(s1) > length(s2)) then begin compare := 1; exit; end; if (length(s1) < length(s2)) then begin

Page 102: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

102

compare := -1; exit; end; for i := 1 to length(s2) do begin if (s1[i] = s2[i]) then continue; if (s1[i] > s2[i]) then compare := 1 else compare := -1; exit; end; compare := 0; end;

Вычитание целых чисел, представленных в виде строки Пусть у нас стоит задача вычитания двух целых чисел, представленных в

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

меньше по длине число нулями спереди. То есть в данном случае мы будем искать разность не «123» и «12», а «123» и «012».

Из-за того, что вычитание – обратная операция сложению, работа с флагом переноса превратиться в работу с флагом заема. То есть если он установлен, то из разряда вычитается, а не прибавляется единица.

Однако возникает ситуация, когда разность получается отрицательная. Но эта ситуация очень просто решается, если воспользоваться аналогией

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

Реализация алгоритма на Pascal

function strSub(s1, s2 : string) : string; var res : string; i : integer; flag : boolean; n1, n2 : shortint; r : shortint; tempStr : string; begin while (length(s1) < length(s2)) do s1 := '0' + s1; while (length(s2) < length(s1)) do s2 := '0' + s2;

Page 103: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

103

res := ''; flag := false; for i := length(s1) downto 1 do begin n1 := ord(s1[i]) - ord('0'); n2 := ord(s2[i]) - ord('0'); if (flag) then n1 := n1 - 1; r := n1 - n2; flag := (r < 0); if (flag) then r := r + 10; res := chr(r + ord('0')) + res; end; {$B-} while (length(res) > 1) and (res[1] = '0') do delete(res, 1, 1); if (flag) then begin tempStr := ''; while (length(tempStr) < length(res)) do tempStr := tempStr + '0'; tempStr := '1' + tempStr; res := strSub(tempStr, res); res := '-' + res; end; strSub := res; end;

Реализация алгоритмов работы с целыми числами, представленными в виде строки на C#

Для сравнения приведем реализацию тех же самых алгоритмов на языке C# (версии 3.5)

class LongNumber { private string num; public LongNumber(string _num) { Debug.Assert(_num != null); num = _num; } public override string ToString() { return num; } public static LongNumber operator +(LongNumber a, LongNumber b) { var res = new LongNumber(""); var str1 = a.num; var str2 = b.num; str1 = str1.PadLeft(str2.Length, '0');

Page 104: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

104

str2 = str2.PadLeft(str1.Length, '0'); var f = false; for (var i = str1.Length - 1; i > -1; i--) { var sum = Byte.Parse("" + str1[i]) + Byte.Parse("" + str2[i]); if (f) { sum++; } f = (sum > 9); sum = sum % 10; res.num = "" + sum + res.num; } if (f) { res.num = "1" + res.num; } return res; } public static LongNumber operator *(LongNumber a, LongNumber b) { var res = new LongNumber("0"); int offset = 0; for (int i = b.num.Length - 1; i > -1; i--) { var tempMul = new LongNumber(a.num); var curVal = Int32.Parse("" + b.num[i]); tempMul = tempMul*curVal; for (int k = 0; k < offset; k++) { tempMul.num = tempMul.num + "0"; } offset++; res = res + tempMul; } res.num = res.num.TrimStart('0'); if (res.num == "") { res.num = "0"; } return res; } public static LongNumber operator -(LongNumber a, LongNumber b) { var s1 = a.num; var s2 = b.num; s1 = s1.PadLeft(s2.Length, '0'); s2 = s2.PadLeft(s1.Length, '0'); var res = ""; var f = false; for (int i=s1.Length - 1;i>-1;i--) { var n1 = Int32.Parse("" + s1[i]); var n2 = Int32.Parse("" + s2[i]); if (f) { n1--; } var r = n1 - n2; f = (r < 0); if (f) {

Page 105: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

105

r += 10; } res = "" + r + res; } res = res.TrimStart('0'); if (res == "") { res = "0"; } if (f) { var tempStr = new String('0', res.Length); tempStr = "1" + tempStr; var l1 = new LongNumber(tempStr); var l2 = new LongNumber(res); res = (l1 - l2).num; res = "-" + res; } return new LongNumber(res); } public int compare(LongNumber a) { if (this.num.Length > a.num.Length) { return 1; } if (this.num.Length < a.num.Length) { return -1; } for (int i = 0; i < this.num.Length; i++) { if (this.num[i] == a.num[i]) { continue; } if (this.num[i] > a.num[i]) { return 1; } return -1; } return 0; } public static LongNumber operator *(LongNumber a, int k) { Debug.Assert(k > -1); Debug.Assert(k < 10); var res = new LongNumber(""); int f = 0; for (int i = a.num.Length - 1; i > -1; i--) { var sum = Int32.Parse("" + a.num[i]); sum *= k; sum += f; f = sum / 10; sum = sum % 10; res.num = "" + sum + res.num; } if (f > 0) { res.num = "" + f + res.num;

Page 106: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

106

} res.num = res.num.TrimStart('0'); if (res.num == "") { res.num = "0"; } return res; } }

Реализация алгоритмов работы с целыми числами, представленными в виде строки на C++

Для увеличения читабельности кода напишем небольшой вспомогательный класс для работы со строками.

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

Полный код реализации приведен ниже:

#pragma once #include <assert.h> class CppString { public: ~CppString() { if (data != NULL) { delete data; } } CppString() { data = new char[1]; data[0] = 0; } int length() { return strlen(data); } CppString(char *_data) { int size = strlen(_data); data = new char[size + 1]; memmove(data, _data, sizeof(char) * size + 1); } CppString(const CppString &_data) { int size = strlen(_data.data); data = new char[size + 1]; memmove(data, _data.data, sizeof(char) * size + 1); }

Page 107: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

107

void addCharToEnd(char newC) { int oldSize = strlen(data); char *newData = new char[oldSize + 2]; memmove(newData, data, oldSize * sizeof(char)); newData[oldSize] = newC; newData[oldSize + 1] = 0; if (data != NULL) { delete data; } data = newData; } char getChar(int index) { assert(data != NULL); assert(index > -1); assert(index < strlen(data)); return data[index]; } void print() { if (data == NULL) { printf("%s\n", "String is empty"); } else { printf("%s\n", data); } } void delCharFromStart() { int oldSize = strlen(data); char *newData = new char[oldSize ]; memmove(newData, &data[1], oldSize * sizeof(char)); if (data != NULL) { delete data; } data = newData; } void addCharToStart(char newC) { int oldSize = strlen(data); char *newData = new char[oldSize + 2]; memmove(&newData[1], data, oldSize * sizeof(char)); newData[0] = newC; newData[oldSize + 1] = 0; if (data != NULL) { delete data; } data = newData; }

Page 108: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

108

CppString operator + (const char &newChar) { CppString res = CppString(*this); res.addCharToEnd(newChar); return CppString(res); } friend CppString operator + (const char &newCgar, const CppString &right); CppString operator + (const CppString &left) { CppString res; int sizeRight = strlen(data); int sizeLeft = strlen(left.data); int sumSize = sizeLeft + sizeRight + 1; res.data = new char[sumSize]; memmove(res.data, data, sizeof(char) * sizeRight); memmove(&res.data[sizeRight], left.data, sizeof(char) * sizeLeft); res.data[sizeLeft + sizeRight] = 0; return CppString(res.data); } CppString operator = (const CppString &left) { if (this == &left) { return *this; } if (data != NULL) { delete data; } int size = strlen(left.data) + 1; data = new char[size]; memmove(data, left.data, size * sizeof(char)); return *this; } private: char *data; }; class NumberWorker { public: static CppString getSum(const CppString &right, const CppString &left) { CppString a = right; CppString b = left; while (a.length() < b.length()) { a = '0' + a; } while (b.length() < a.length()) { b = '0' + b; }

Page 109: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

109

CppString res; bool f = false; for (int i = a.length() - 1; i > -1; i--) { char ca = a.getChar(i) - '0'; char cb = b.getChar(i) - '0'; char c = ca + cb; if (f) { c++; } f = (c > 9); if (c > 9) { c -= 10; } res = (c + '0') + res; } if (f) { res = '1' + res; } return CppString(res); } static int compare(CppString &right, CppString &left) { if (right.length() > left.length()) { return 1; } if (right.length() < left.length()) { return -1; } for (int i = 0; i < right.length(); i++) { if (right.getChar(i) == left.getChar(i)) { continue; } return (right.getChar(i) - left.getChar(i)); } return 0; } static CppString getMul(CppString &right, int k) { char f = 0; CppString res; for (int i = right.length() - 1; i > -1; i--) { char curVal = right.getChar(i) - '0'; curVal *= k; curVal += f; f = (curVal / 10); curVal = curVal % 10; res = (curVal + '0') + res; } if (f != 0) {

Page 110: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

110

res = (f + '0') + res; } return CppString(res); } static CppString getMul(CppString &right, CppString &left) { CppString res; int offset = 0; for (int i = left.length() - 1; i > -1; i--) { char curChar = left.getChar(i) - '0'; CppString curMul = getMul(right, curChar); for (int i = 0; i < offset; i++) { curMul = curMul + '0'; } offset++; res = getSum(res, curMul); } return CppString(res); } static CppString getSub(const CppString &right, const CppString &left) { CppString s1 = right; CppString s2 = left; while (s1.length() < s2.length()) { s1 = '0' + s1; } while (s2.length() < s1.length()) { s2 = '0' + s2; } CppString res; bool flag = false; for (int i = s1.length() - 1; i > -1; i--) { char n1 = s1.getChar(i) - '0'; char n2 = s2.getChar(i) - '0'; if (flag) { n1--; } char r = n1 - n2; flag = (r < 0); if (flag) { r = r + 10; } res = (r + '0') + res; } while ((res.length() > 1) && (res.getChar(0) == '0')) { res.delCharFromStart(); } if (flag) { CppString tempStr("1"); while (tempStr.length() <= res.length()) {

Page 111: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

111

tempStr = tempStr + '0'; } res = getSub(tempStr, res); res = '-' + res; } return CppString(res); } }; CppString operator + (const char &newChar, const CppString &right) { CppString res(right); res.addCharToStart(newChar); return CppString(res); }

  

Page 112: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

112

Заключение Представленные выше задачи раскрывали вопросы декомпозиции

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

При программировании на языках высокого уровня в промышленном масштабе следует учитывать предметную область, для которой составляется программа, и технологии, на которых написана программа. Именно уровень технологии определяет степень детализации размышлений программиста. Так же важной характеристикой программы является простота чтения кода программы. Это особенно важно с учетом того, что реальные проекты имеют сложность в несколько человеко-лет.

Плюсом С-подобных языков является большая распространенность и универсальность.

 

Page 113: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

113

Глоссарий Аргумент функции – значение, передаваемое функции, а также

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

Итерация – единичное выполнение тела цикла.

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

Консоль – окно для вывода системных сообщений и приема команд.

Массив – именованный набор однотипных переменных, расположенных в памяти непосредственно друг за другом, доступ к которым осуществляется по индексу.

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

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

Тело цикла – последовательность инструкций, предназначенная для многократного исполнения.

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

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

Цикл с постусловием – цикл, в котором условие проверяется после выполнения тела цикла. Отсюда следует, что тело всегда выполняется хотя бы один раз.

Цикл с предусловием – цикл, который выполняется, пока истинно некоторое условие, указанное перед его началом.

Цикл со счетчиком – цикл, в котором некоторая переменная изменяет свое значение от заданного начального значения до конечного значения с некоторым шагом, и для каждого значения этой переменной тело цикла выполняется один раз.

 

Page 114: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

114

Список рекомендуемой литературы 

1. Баженова, И. Ю. С++ && Visual Studio NET. Самоучитель программиста / И. Ю. Баженова. – М. : КУДИЦ-ОБРАЗ, 2003. – 448 с.

2. Гилберт, С. Самоучитель Visual C++ 6 в примерах / С. Гилберт, Б. Макартни. – К. : ООО "ТИД ДС", 2003. – 496 с.

3. Касаткин, А. И. Профессиональное программирование на языке Си: От Турбо Си к С++ / А. И. Касаткин, А. Н. Вальвачев. – Минск : Вышэйшая школа, 1992. – 240 с.

4. Николенко, Д. В. Самоучитель по Visual C++ / Д. В. Николаенко. – СПб. : Наука и техника, 2001. – 368 с.

5. Павловская, Т. А. С/С++. Программирование на языке высокого уровня / Т.А. Павловская. – СПб. : Питер, 2003. – 461 с.

6. Паппас, К. Программирование на С и С++ / К. Паппарс, У. Мюррей. – К. : BHV, 2000. – 320 c.

7. Савич, У. С++ во всей полноте / У. Савич. – К. : BHV; СПб: Питер, 2005. – 784 с.

8. Холзнер, С. Visual C++: Учебный курс / С. Холзнер. – СПб. : Питер, 2000. – 576 с.

9. Уэйт, М. Язык Си. Руководство для начинающих / М. Уэйт, С. Прата, Д. Мартин. – М. : Мир, 1988. – 512 с.

10. Шиманович, Е. Л. C/C++ в примерах и задачах / Е. Л. Шиманович. – Минск: Новое знание, 2004. – 528 с.

11. Шмидский, Я. К. Программирование на языке С/С++. Самоучитель / Я. К. Шмидский. – М. : Вильямс, 2004. – 352 с.

Page 115: на примере языка С#)venec.ulstu.ru/lib/disk/2013/Shamshev.pdf · 2013-02-19 · Реализация алгоритмов работы с целыми числами,

Учебное электронное издание

ШАМШЕВ Алексей Борисович СВЯТОВ Кирилл Валерьевич

АЛГОРИТМИЧЕСКОЕ МЫШЛЕНИЕ ПРИ РЕШЕНИИ ЗАДАЧ

(на примере языка C#)

Учебное пособие

Редактор Н. А. Евдокимова

Усл. печ. л. 6,74. Объем данных 0,98 Мб. ЭИ № 46.

Печатное издание

ЛР №020640 от 22.10.97 Подписано в печать 24.12.2012. Формат 60x84/16.

Усл. п. л. 6,74. Тираж 75 экз. Заказ 99. Типография УлГТУ. 432027, Ульяновск, Сев. Венец, 32.

Ульяновский государственный технический университет 432027, г. Ульяновск, ул. Сев. Венец, 32.

Тел.: (8422) 778-113. E-mail: [email protected]

http://www.venec.ulstu.ru