Реализация односвязного списка
Односвязный список предполагает организацию хранения данных в памяти, при которой перемещение может быть выполнено только в одном направлении (от начала списка в конец). Расположение в памяти элементов односвязного списка можно изобразить следующим образом (рис. 9).
В настоящем пособии реализация односвязного списка не приводится (предлагается выполнить самостоятельно, основываясь на информации о реализации двусвязного списка, приводимого ниже).
Реализация двусвязного списка
Двусвязный список - это организация хранения данных в памяти, позволяющая выполнять перемещение в обоих направлениях (от начала списка в конец и наоборот). Расположение в памяти двусвязного списка можно изобразить следующим образом (рис. 10).
В приведенном ниже примере для доступа к элементам списка используется разработанный класс, реализующий простые итераторы.
#include <iostream>
#include <cassert>
using namespace std;
template <class T>
class D_List
{private:
class D_Node
{ public:
D_Node *next; // указатель на следующий узел
D_Node *prev; // указатель на предыдущий узел
T val; // поле данного
D_Node(T node_val) : val(node_val) { } // конструктор
D_Node() {} // конструктор
~D_Node(){} // деструктор
// для вывода элементов, тип которых определен пользователем,
// неодходимо перегрузить операцию << operator<<( ).
void print_val( ) { cout << val << " "; }
};
public:
Class iterator
{private:
friend class D_List<T>;
D_Node * the_node;
public:
iterator( ) : the_node(0) { }
iterator(D_Node * dn) : the_node(dn) { }
// Copy constructor
iterator(const iterator & it) : the_node(it.the_node) { }
iterator& operator=(const iterator& it)
{ the_node = it.the_node;
return *this;
}
bool operator==(const iterator& it) const
{ return (the_node == it.the_node); }
bool operator!=(const iterator& it) const
{ return !(it == *this); }
iterator& operator++( )
{ if ( the_node == 0 )
throw "incremented an empty iterator";
if ( the_node->next == 0 )
throw "tried to increment too far past the end";
the_node = the_node->next;
return *this;
}
iterator& operator--( )
{ if ( the_node == 0 )
throw "decremented an empty iterator";
if ( the_node->prev == 0 )
throw "tried to decrement past the beginning";
the_node = the_node->prev;
return *this;
}
T& operator*( ) const
{ if ( the_node == 0 )
throw "tried to dereference an empty iterator";
return the_node->val;
}
};
private:
D_Node *head; // указатель на начало списка
D_Node *tail; // указатель на элемент вне списка
D_List & operator=(const D_List &);
D_List(const D_List &);
iterator head_iterator;
iterator tail_iterator;
public:
D_List( )
{ head = tail = new D_Node;
tail->next = 0;
tail->prev = 0;
head_iterator = iterator(head);
tail_iterator = iterator(tail);
}
// конструктор (создание списка, содержащего один элемент)
D_List(T node_val)
{ head = tail = new D_Node;
tail->next = 0;
tail->prev = 0;
head_iterator = iterator(head);
tail_iterator = iterator(tail);
add_front(node_val);
}
// деструктор
~D_List( )
{ D_Node *node_to_delete = head;
for (D_Node *sn = head; sn != tail;)
{ sn = sn->next;
delete node_to_delete;
node_to_delete = sn;
}
delete node_to_delete;
}
bool is_empty( ) {return head == tail;}
iterator front( ) { return head_iterator; }
iterator rear( ) { return tail_iterator; }
void add_front(T node_val)
{ D_Node *node_to_add = new D_Node(node_val);
node_to_add->next = head;
node_to_add->prev = 0;
head->prev = node_to_add;
head = node_to_add;
head_iterator = iterator(head);
}
// добавление нового элемента в начало списка
void add_rear(T node_val)
{ if ( is_empty( ) ) // список не пустой
add_front(node_val);
else
// не выполняется для пустого списка, т.к. tail->prev =NULL
// и, следовательно, tail->prev->next бессмысленно
{ D_Node *node_to_add = new D_Node(node_val);
node_to_add->next = tail;
node_to_add->prev = tail->prev;
tail->prev->next = node_to_add;
tail->prev = node_to_add;
tail_iterator = iterator(tail);
}
}
bool remove_it(iterator & key_i)
{ for ( D_Node *dn = head; dn != tail; dn = dn->next )
if ( dn == key_i.the_node ) // найден узел для удаления
{ dn->prev->next = dn->next;
dn->next->prev = dn->prev;
delete dn; // удаление узла
key_i.the_node = 0;
return true;
}
return false;
}
// поиск итератора по значению узла
iterator find(T node_val) const
{ for ( D_Node *dn = head; dn != tail; dn = dn->next )
if ( dn->val == node_val ) return iterator(dn);
return tail_iterator;
}
int size( ) const
{ int count = 0;
for ( D_Node *dn = head; dn != tail; dn = dn->next ) ++count;
return count;
}
// Вывод содержимого списка
void print( ) const
{ for ( D_Node *dn = head; dn != tail; dn = dn->next )
dn->print_val( );
cout << endl;
}
};
В файле d_list.cpp содержится main-функция, демонстрирующая некоторые примеры работы со списком.
#include "dlist_2.h"
typedef int tip;
D_List<tip> the_list; // создается пустой список
int main()
{ int ret = 0;
D_List<tip>::iterator list_iter;
// занесение значений 0 1 2 3 4 в список
for (int j = 0; j < 5; ++j)
the_list.add_front(j);
// вывод содержимого списка используя компоненту-функцию
// класса D_List
the_list.print( );
// повторный вывод значения содержимого списка
// используя итератор
for ( list_iter = the_list.front( ) ;
list_iter != the_list.rear( ) ;
++list_iter )
cout << *list_iter << " ";
cout << endl;
// вывод содержимого списка в обратном порядке
for ( list_iter = the_list.rear( ) ; list_iter != the_list.front( ) ; )
{ --list_iter; // декремент итератора
cout << *list_iter << " ";
}
cout << endl;
the_list.remove_it(the_list.find(3));
the_list.print( );
cout<<the_list.size( )<<endl;
return 0;
}
Результат работы программы:
3 2 1 0
3 2 1 0
0 1 2 3 4
2 1 0
Итератор реализован как открытый вложенный класс D_List::iterator. Так как класс открытый, может быть в main создан его объект. Класс iterator объявляет дружественным классу D_List, чтобы функция remuv_it класа D_List имела возможность обращаться к private-члену the_node класса iterator.
В дополнение к стандартным указателям на голову и хвост списка (адрес за пределами списка) в программе (в d_list.h) объявлены итераторы head_iterator и tail_iterator, также ссылающиеся на голову и хвост списка.
Использование итератора позволяет скрыть единственный элемент данных класса D_List:
D_Node * the_node.
В классе iterator выполнена перегрузка некоторых операций, позволяющих манипулировать узлом в строго определенных правилах (табл. 5).
Таблица 5
Функция | Описание |
operator=() | Присваивает the_node значение the_node правой части |
operator==() | Возвращает true, если оба итератора ссылаются на один узел |
operator!=() | Отрицание операции == |
operator++() | Перемещает итератор на следующий узел |
operator--() | Перемещает итератор на предыдущий узел |
operator*() | Возвращает значение node_val узла D_node |
Для поддержки использования итераторов в качестве аргументов или возвращаемых значений определен конструктор копирования.
В программе функция find(T) возвращает не bool, а iterator, который может быть передан другим функциям, принимающим параметры-итераторы, для доступа к данным или прохода по списку.
Использование итераторов позволяет пользователю манипулировать списком, при этом детали его реализации остаются надежно скрытыми.
Реализация двоичного дерева
В отличие от списков двоичные деревья представляют собой более сложные структуры данных.
Дерево - непустое конечное множество элементов, один из которых называется корнем, а остальные делятся на несколько непересекающихся подмножеств, каждое из которых также является деревом. Одним из разновидностей деревьев являются бинарные деревья. Бинарное дерево имеет один корень и два непересекающихся подмножества, каждое из которых само является бинарным деревом. Эти подмножества называются левым и правым поддеревьями исходного дерева. На рис. 11 приведены графические изображения бинарных деревьев. Если один или более узлов дерева имеют более двух ссылок, то такое дерево не является бинарным.
Бинарные деревья с успехом могут быть использованы, например, при сравнении и организации хранения очередной порции входной информации с информацией, введенной ранее, и при этом в каждой точке сравнения может быть принято одно из двух возможных решений. Так как информация вводится в произвольном порядке, то нет возможности предварительно упорядочить ее и применить бинарный поиск. При использовании линейного поиска время пропорционально квадрату количества анализируемых слов. Каким же образом, не затрачивая большое количество времени, организовать эффективное хранение и обработку входной информации. Один из способов постоянно поддерживать упорядоченность имеющейся информации - это перестановка ее при каждом новом вводе информации, что требует существенных временных затрат. Построение бинарного дерева осуществляется на основе лексикографического упорядочивания входной информации.
Лексикографическое упорядочивание информации в бинарном дереве заключается в следующем. Считывается первая информация и помещается в узел, который становится корнем бинарного дерева с пустыми левым и правым поддеревьями. Затем каждая вводимая порция информации сравнивается с информацией, содержащейся в корне. Если значения совпадают, то, например, наращиваем число повторов и переходим к новому вводу информации. Если же введенная информация меньше значения в корне, то процесс повторяется для левого поддерева, если больше - для правого. Так продолжается до тех пор, пока не встретится дубликат или пока не будет достигнуто пустое поддерево. В этом случае число помещается в новый узел данного места дерева.
Приводимый далее пример шаблонного класса B_tree демонстрирует простое дерево поиска. Как и для программы очереди, код программы организован в виде двух файлов, где файл Bt.h является непосредственно шаблонным классом дерева, а Bt.cpp демонстрирует работу функций шаблонного класса. Вначале приведен текст файла Bt.h.
#include <iostream>
#include <cassert>
using namespace std;
///////////////////////////////////////////////////////////////
// реализация шаблона класса B_tree
// Тип Т должен поддерживать следующие операции:
// operator=( );
// operator<<( );
// operator==( );
// operator!=( );
// operator<( );
//
///////////////////////////////////////////////////////////////
template <class T>
class B_tree
{
private:
struct T_node
{ friend class B_tree;
T val; // данные узла
T_node *left; // указатель на левый узел
T_node *right; // указатель на правый узел
int count; // число повторов val при вводе
T_node(); // конструктор
T_node( const T node_val ) : val(node_val) { } // конструктор
~T_node(){} // деструктор
// печать данных в виде дерева "на боку" с корнем слева
// "Обратный" рекурсивный обход (т.е. справа налево)
// Листья показаны как "@".
void print ( const int level = 0 ) const
{ // Инициализация указателем this (а не корнем)
// это позволяет пройти по любому поддереву
const T_node *tn = this;
if(tn) tn->right->print(level+1); // сдвиг вправо до листа
for (int n=0; n<level;++n)
cout << " ";
if(tn)
cout << tn->val << '(' << tn->count << ')' << endl;
else
cout << "@" << endl;
if(tn) tn->left->print(level+1); // сдвиг на левый узел
}
};
private:
T_node *root;
T_node *zero_node;
// Запретить копирование и присваивание
B_tree(const B_tree &);
B_tree & operator=( const B_tree & );
// Создать корневой узел и проинициализировать его
void new_root( const T root_val )
{ root = new T_node(root_val);
root->left = 0;
root->right = 0;
root->count = 1;
}
// Find_node(T find_value) возвращает ссылку на
// указатель для упрощения реализации remove(T).
T_node * & find_node( T find_value )
{ T_node *tn = root;
while ((tn != 0) && (tn->val != find_value))
{ if(find_value < tn->val)
tn = tn->left; // движение налево
else
tn = tn->right; // движение направо
}
if (!tn) return zero_node;
else return tn;
}
// Присоединяет новое значение ins_val к соответствующему листу,
// если значения нет в дереве, и увеличивает count для каждого
// пройденного узла
T_node * insert_node( const T ins_val, T_node * tn = 0 )
{ if(!root)
{ new_root(ins_val);
return root;
}
if(!tn) tn = root;
if((tn ) && (tn->val != ins_val))
{ if(ins_val < tn->val)
{ if(tn->left) // просмотр левого поддерева
insert_node(ins_val,tn->left);
else
{ attach_node(tn,tn->left,ins_val); // вставка узла
return tn->left;
}
}
else
{ if(tn->right) // просмотр правого поддерева
insert_node(ins_val,tn->right);
else
{ attach_node(tn,tn->right,ins_val); // вставка узла
return tn->right;
}
}
}
else
if(tn->val==ins_val) add_count(tn,1);
assert(tn); // Оценивает выражение и, когда результат ЛОЖЕН, печатает
// диагностическое сообщение и прерывает программу
return 0;
}
// Создание нового листа и его инициализация
void attach_node( T_node * new_parent,
T_node * & new_node, T insert_value )
{ new_node = new T_node( insert_value );
new_node->left = 0;
new_node->right = 0;
new_node->count = 1;
}
// Увеличение числа повторов содержимого узла
void add_count( T_node * tn, int incr )
{ tn->count += incr; }
// Удаление всех узлов дерева в обходе с отложенной
// выборкой. Используется в ~B_tree().
void cleanup (T_node *tn)
{ if(!tn) return;
if(tn->left)
{ cleanup(tn->left);
tn->left = 0;
}
if(tn->right != 0 )
{ cleanup(tn->right);
tn->right = 0;
}
delete tn;
}
// рекурсивно печатает значения в поддереве с корнем tn
// (обход дерева с предварительной выборкой)
void print_pre(const T_node * tn) const
{ if(!tn) return;
cout << tn->val << " ";
if(tn->left)
print_pre(tn->left);
if(tn->right)
print_pre( tn->right );
}
// рекурсивно печатает значения в поддереве с корнем tn
// (обход дерева )
void print_in(const T_node * tn) const
{ if(!tn) return;
if(tn->left)
print_in(tn->left);
cout << tn->val << " ";
if(tn->right)
print_in(tn->right);
}
// рекурсивно печатает значения в поддереве с корнем tn
// (обход дерева с отложенной выборкой)
void print_post(const T_node * tn) const
{ if(!tn) return;
if(tn->left)
print_post(tn->left);
if(tn->right)
print_post(tn->right);
cout << tn->val << " ";
}
public:
B_tree() : zero_node(0) {root = 0;}
B_tree(const T root_val) : zero_node(0)
{ new_root(root_val); }
~B_tree()
{ cleanup(root); }
// Добавляет к дереву через функцию insert_node() значение, если его
// там еще нет. Возвращает true, если элемент добавлен, иначе false
bool add(const T insert_value)
{ T_node *ret = insert_node(insert_value);
if(ret) return true;
else return false;
}
// Find(T) возвращает true, если find_value найдено
// в дереве, иначе false
bool find(T find_value)
{ Tree_node *tn = find_node(find_value);
if(tn) return true;
else return false;
}
// Print() производит обратную порядковую выборку
// и печатает дерево "на боку"
void print() const
{ cout << "\n=---------------------------\n"<< endl;
// Это вызов Binary_tree::Tree_node::print( ),
// а не рекурсивный вызов Binary_tree::print( ).
root->print( );
}
// последовательная печать дерева при обходе
// с предварительной, порядковой и отложенной выборкой.
// Вызываются рекурсивные private-функции, принимающие
// параметр Tree_node *
void print_pre_order( ) const
{ print_pre(root);
cout << endl;
}
void print_in_order( ) const
{ print_in(root);
cout << endl;
}
void print_post_order( ) const
{ print_post(root);
cout << endl;
}
};
Далее приведено содержимое файла Bt.cpp.
#include "bin_tree.h"
B_tree<int> my_bt( 7 ); // создание корня бинарного дерева
// Заполнение дерева целыми значениями
void populate( )
{ my_bt.add( 5 );
my_bt.add( 5 );
my_bt.add( 9 );
my_bt.add( 6 );
my_bt.add( 5 );
my_bt.add( 9 );
my_bt.add( 4 );
my_bt.add( 11 );
my_bt.add( 8 );
my_bt.add( 19 );
my_bt.add( 2 );
my_bt.add( 10 );
my_bt.add( 19 );
}
int main( )
{ populate( );
my_bt.print ( );
cout << endl;
cout << "Pre-order: " ;
my_bt.print_pre_order ( );
cout << "Post-order: " ;
my_bt.print_post_order ( );
cout << "In-order: " ;
my_bt.print_in_order ( );
return 0;
}
Результат работы программы:
B_tree содержит вложенный закрытый класс T_node (структуру) для представления внутреннего описания элемента (узла) бинарного дерева. Структура его скрыта от пользователя. Поле val содержит значение, хранимое в узле, left и right – указатели на левый и правый потомки данного узла.
В классе B_tree два открытых конструктора. Конструктор по умолчанию создает пустое бинарное дерево B_tree, а конструктор В_tree(const T) дерево с одним узлом. Деструктор ~B_tree() удаляет каждый T_node, производя обход бинарного дерева с отложенной выборкой. Отложенная выборка гарантирует, что никакой узел не будет удален, пока не удалены его потомки.
Для объектов класса B_tree с целью упрощения не определены (а только объявлены закрытыми) конструктор копирования и операция присваивания. Это позволяет компилятору сообщить об ошибке при попытке выполнить эти операции. Если эти операции потребуется использовать, то их необходимо сделать открытыми и определить.
Функция find_node() возвращает ссылку на указатель T_node. Для того чтобы функция могла сослаться на нулевой указатель, вводится поле zero_node.
Рекурсивные функции print_pre_order(), print_in_order() и print_post_order() печатают элементы дерева в линейной последовательности в соответствии со стандартными схемами обхода двоичного дерева. Функция print_pre_order() печатает значение узла до того, как будет напечатан любой из его потомков. Функция print_post_order(), наоборот, печатает значение узла только после того, как будут напечатаны все его потомки. И, наконец, функция print_in_order() выводит на печать элементы бинарного дерева в порядке возрастания.
Литература
1. Дейтел Х., Дейтел П.. Как прграммировать на С++: Пер. с англ. – М.: ЗАО «Издательство БИНОМ», 2001.
2. Шилд Г. Программирование на Borland C++ для профессионалов. –Мн.: ООО «Попурри» , 1998.
3. Скляров В.А. Язык С++ и объектно-ориентированное программирование. Мн.: Выш. шк., 1997.
4. Ирэ П. Объектно-ориентированное программирование с использованием С++. Пер. с англ. – Киев: НИПФ «ДиаСофт Лтд», 1995.
5. Мейерс С. Наиболее эффективное использование С++. 35 новых рекомендаций по улучшению ваших программ и проектов. Пер. с англ. – М: ДМК Пресс, 2000.
Вопросы по курсу ООП
1. Как обратиться к глобальной переменной , если в функции определена локальная переменная с таким же именем?
2. Чему будут равны значения y и s после выполнения следующего кода
int y = 8;
int &s = y;
s++;
ответ s=9 y=9
3. Что происходит если new не смог выделить требуемую память?
ответ bad_alloc
4.Определить, не компилируя, что напечатает программа
class Lock{
public:
Lock(){ cout << "A"; }
Lock(Lock& B){ cout << "B"; }
~Lock(){ cout << "C"; }
};
int main(int argc, char* argv[])
{
try{
Lock guard;
throw 0; // вариант посложнее throw guard (ответ АВСЕС)
cout << "D";
}
catch( Lock& ){
cout << "E";
}
catch(...){
cout << "X";
}
cout << endl;
return 0;
}
ответ ACX
Вопросы без ответов
- как удалить массив объектов
- что такое полиморфизм
- что такое абстрактный базовый класс
- как определить чисто виртуальную функцию
- зачем нужен виртуальный деструктор
- как реализован виртуальный деструктор в C++
- чем отличаются const char* ptr и char const *ptr
- в каких случаях вызывается конструктор копирования
- в чем потенциальная опасность конструктора копирования по умолчанию
Содержание
Введение. 3
Объектно-ориентированное программирование. 3
Абстрактные типы данных. 3
Базовые принципы объектно-ориентированного программирования. 4
Основные достоинства языка С++. 6
Особенности языка С++. 6
Ключевые слова. 7
Константы и переменные. 7
Операции. 7
Типы данных. 7
Передача аргументов функции по умолчанию.. 7
Простейший ввод и вывод. 8
Объект cout 8
Манипуляторы hex и oct 8
Другие манипуляторы.. 9
Объект cin. 11
Операторы для динамического выделения и освобождения памяти (new и delete) 11
Базовые конструкции объектно-ориентированных программ. 14
Объекты.. 14
Понятие класса. 16
Конструктор копирования. 28
Конструктор explicit 29
Указатели на компоненты класса. 32
Встроенные функции (спецификатор inline) 32
Организация внешнего доступа к локальным компонентам класса (спецификатор friend) 33
Вложенные классы.. 38
Static-члены (данные) класса. 41
Указатель this. 43
Компоненты-функции static и const 46
Proxi-классы.. 49
Ссылки. 50
Параметры ссылки. 53
Независимые ссылки. 54
Практические приемы ограничения числа объектов класса. 54
Наследование (производные классы) 56
Конструкторы и деструкторы при наследовании. 60
Виртуальные функции. 64
Абстрактные классы.. 75
Виртуальные деструкторы.. 81
Множественное наследование. 83
Виртуальное наследование. 86
Перегрузка функций. 90
Перегрузка операторов. 91
Перегрузка бинарного оператора. 92
Перегрузка унарного оператора. 96
Дружественная функция operator 98
Особенности перегрузки операции присваивания. 99
Перегрузка оператора [] 101
Перегрузка оператора () 104
Перегрузка оператора ->. 105
Перегрузка операторов new и delete. 106
Преобразование типа. 111
Явные преобразования типов. 111
dynamic_cast Operator. 112
Преобразования типов, определенных в программе. 115
Шаблоны.. 117
Параметризированные классы.. 117
Передача в шаблон класса дополнительных параметров. 120
Шаблоны функций. 120
Совместное использование шаблонов и наследования. 122
Шаблоны класса и friend. 124
Некоторые примеры использования шаблона класса. 124
Реализация smart-указателя. 124
Классы поддерживающие транзакции. 126
Задание значений параметров класса по умолчанию.. 129
Свойства в С++. 131
Пространства имен. 135
Ключевое слово using как директива. 136
Ключевое слово using как объявление. 137
Псевдоним пространства имен. 138
Организация ввода-вывода. 139
Состояние потока. 142
Строковые потоки. 144
Организация работы с файлами. 145
Организация файла последовательного доступа. 148
Создание файла произвольного доступа. 151
Основные функции классов ios, istream, ostream.. 154
Исключения в С++. 156
Основы обработки исключительных ситуаций. 157
Перенаправление исключительных ситуаций. 166
Исключительная ситуация, генерируемая оператором new.. 167
Генерация исключений в конструкторах. 169
Задание собственной функции завершения. 171
Спецификации исключительных ситуаций. 171
Задание собственного неожиданного обработчика. 172
Иерархия исключений стандартной библиотеки. 173
Стандартная библиотека шаблонов (STL) 174
Общее понятие о контейнере. 174
Общее понятие об итераторе. 178
Категории итераторов. 183
Основные итераторы.. 183
Вспомогательные итераторы.. 193
Операции с итераторами. 196
Контейнерные классы.. 197
Контейнеры последовательностей. 197
Контейнер последовательностей vector 197
Контейнер последовательностей list 199
Контейнер последовательностей deque. 203
Ассоциативные контейнеры.. 205
Ассоциативный контейнер multiset 205
Ассоциативный контейнер set 203
Ассоциативный контейнер multimap. 204
Ассоциативный контейнер map. 206
Адаптеры контейнеров. 206
Адаптеры stack. 206
Адаптеры queue. 207
Адаптеры priority_queue. 208
Пассивные и активные итераторы.. 209
Алгоритмы.. 212
Алгоритмы сортировки sort, partial_sort, sort_heap. 212
Алгоритмы поиска find, find_if, find_end, binary_search. 213
Алгоритмы fill, fill_n, generate и generate_n. 214
Алгоритмы equal, mismatch и lexicographical_compare. 215
Математические алгоритмы.. 217
Алгоритмы работы с множествами. 218
Алгоритмы swap, iter_swap и swap_ranges. 219
Алгоритмы copy, copy_backward, merge, unique и reverse. 220
Примеры реализации контейнерных классов. 220
Связанные списки. 220
Реализация односвязного списка. 220
Реализация двусвязного списка. 221
Реализация двоичного дерева. 226
Литература. 235
Св. план 2003, резерв
Учебное издание
Луцик Юрий Александрович,
ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ.
ЯЗЫК С++
Учебное пособие
по курсу «Объектно-ориентированное программирование»
для студентов специальности «Вычислительные машины,
системы и сети» всех форм обучения
Редактор Т.А. Лейко
Корректор Е.Н. Батурчик
Компьютерная верстка
Подписано в печать . .2003. Формат 60х84 1/16. Бумага офсетная.
Гарнитура Times New Roman. Печать ризографическая. Усл. печ. л. .
Уч.- изд. л. 12,0. Тираж 200 экз. Заказ .
Издатель и полиграфическое исполнение:
Учреждение образования
«Белорусский государственный университет информатики и радиоэлектроники»
Лицензия ЛП № 156 от 05.02. 2001.
Лицензия ЛВ № 509 от 03.08. 2001.
220013, Минск, П.Бровки, 6.