Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Кетков.doc
Скачиваний:
17
Добавлен:
27.09.2019
Размер:
2.22 Mб
Скачать

9.1. Объявление и инициализация массивов.

Объявление массива сводится к указанию типа его элементов и количества элементов по каждому измерению:

#define Nmax 50

char a1[20],a2[2][80];

int b1[25],b2[Nmax];

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

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

char a[7]="Привет";

char b[7]={'П','р','и','в','е','т',0x0};

char c[]="Привет";

float d[10]={1.,2.,3.,4.};

int q[2][3]={{1,2,3},

{4,5,6}};

Обратите внимание на инициализацию символьных массивов a,b и c. В первом случае значения элементов массива совпадают с символами указанной строковой константы. Хотя значащих символов там 6, не следует забывать и о невидимом признаке конца строки – байте с нулевым значением. В случае инициализации массива b каждый его элемент задан символьной константой, не забыть и признак конца строки. Самый удобный способ использован при инициализации массива c – вместо того, чтобы указывать количество элементов, здесь заданы пустые скобки. Компилятор по заданному значению текстовой константы сам определит нужное количество байтов. Это позволяет избежать ненужных ошибок при подсчете количества символов в достаточно длинных строках.

При инициализации массива d вместо десяти значений заданы только четыре. Это означает, что указанные величины будут присвоены только первым четырем элементам. Остальные элементы не инициализированы. В случае если d является глобальным массивом, значения этих элементов будут равны 0 (память, выделяемая глобальным данным, предварительно чистится). Если массив d локализован в какой-то функции, то значения его элементов, начиная с d[4], предсказать невозможно – там будет находиться "грязь", которая при повторных вызовах функции может оказаться разной.

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

Использование двумерных массивов для хранения строковых данных – не всегда лучшее решение:

char spisok[][20]={"Иванов","Петров-Водкин","Репин"};

Для хранения элементов такого массива компилятор выделит 3*20=60 байт. Вместо этого можно было бы завести 3 указателя на соответствующие строковые константы:

char *sp[]={"Иванов","Петров-Водкин","Репин"};

В этом случае понадобилось бы 3*4=12 байт для хранения элементов массива sp и 27 байт для хранения строковых констант, т.е. почти в полтора раза меньше памяти. Дополнительный выигрыш может быть получен при дальнейшей обработке. Например, при сортировке строковых значений вместо перестановки фамилий могли бы меняться местами только четырехбайтовые указатели.

9.2. Некоторые приемы обработки числовых массивов

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

Пример 9.1. Переворот одномерного целочисленного массива – перестановка его элементов в обратном порядке. Выделим в отдельную функцию процедуру инвертирования:

void invert(int *a,int n)

{ for(int j=0,tmp; j<n/2; j++)

{ tmp=a[j]; a[j]=a[n-j-1]; a[n-j-1]=tmp; }

}

Для проверки ее работоспособности можно воспользоваться следующей программой:

#include <stdio.h>

#include <conio.h>

#define N 20

void invert(int *a,int n);

void main()

{ int j,a[N];

printf("Before reverse:\n");

for(j=0; j<N; j++)

{ a[j]=j+1; printf("%3d",a[j]); }

invert(a,N);

printf("\nAfter reverse:\n");

for(j=0; j<N; j++) printf("%3d",a[j]);

getch();

}

//=== Результат работы ===

Before reverse:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

After reverse:

20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1

Пример 9.2. Перестановка головы и хвоста массива без использования промежуточного массива. Алгоритм этой процедуры был опубликован много лет тому назад в книге Дж. Бентли "Жемчужины для программистов". Заключается он в том, что надо последовательно выполнить 3 инвертирования – головы массива, хвоста массива и всего массива целиком. Для этой цели мы и воспользуемся модификацией ранее написанной процедурой invert:

void invert1(int *a, int k, int n)

{//k – индекс первого элемента инвертируемого фрагмента массива

//n – количество инвертируемых элементов

int j,tmp;

for(j=k; j<k+n/2; j++)

{ tmp=a[j];

a[j]=a[2*k+n-j-1];

a[2*k+n-j-1]=tmp; }

}

Для проверки описанного выше алгоритма можно воспользоваться следующей программой:

#include <stdio.h>

#include <conio.h>

#define N 20

#define M 15

void invert1(int *a, int k, int n);

void main()

{ int j,a[N];

printf("Before reverse:\n");

for(j=0; j<N; j++)

{ a[j]=j+1; printf("%3d",a[j]); }

invert1(a,0,M);

printf("\nAfter reverse head:\n");

for(j=0; j<N; j++) printf("%3d",a[j]);

invert1(a,M,N-M);

printf("\nAfter reverse tail:\n");

for(j=0; j<N; j++) printf("%3d",a[j]);

invert1(a,0,N);

printf("\nAfter reverse all:\n");

for(j=0; j<N; j++) printf("%3d",a[j]);

getch();

}

//=== Результат работы ===

Before reverse:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

After reverse head:

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 16 17 18 19 20

After reverse tail:

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 20 19 18 17 16

After reverse all:

16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

Пример 9.3. Вывод целочисленной матрицы в заданном окне. Договоримся о следующих обозначениях параметров нашей процедуры вывода:

*c – указатель на первый элемент матрицы;

n – количество строк матрицы;

m – количество столбцов матрицы;

w – ширина каждой колонки матрицы при выводе на экран (w ≤ 9);

row, col – экранные координаты, определяющие позицию верхнего левого знакоместа окна, в котором должна быть размещена матрица.

Тогда функция printa, которая выводит заданную целочисленную матрицу в указанном окне, может быть оформлена следующим образом:

void printa(int row,int col,int w,int *c,int n, int m)

{ int j,k;

char f[4]="%0d"; //заготовка для форматного указателя

f[1] += w; //формирование форматного указателя %wd

for(j=0; j<n; j++)

for(k=0; k<m; k++)

{ gotoxy(col+k*w,row+j); //переход в позицию элемента c[j][k]

printf(f,c[k+j*m]);

}

}

Отметим три особенности приведенной выше программы. Во-первых, для вывода числовых данных в поле, содержащем w позиций, удобно воспользоваться функцией printf с форматным указателем %wd. Но w – это числовой параметр, передаваемый программе printa. Поэтому строку с форматным указателем можно сформировать. Именно для этой цели заведена строковая константа f, второй байт которой (символ f[1]) формируется как сумма кода цифры 0 с числом w. Затем сформированная строка используется в функции printf, первым аргументом которой может быть не только литеральная константа, но и ссылка на строку. Во-вторых, вместо того, чтобы перебирать элементы матрицы c[j][k] как элементы двумерного массива, в программе printa используются приведенные индексы (k+j*m). Наконец, для перевода курсора в позицию, соответствующую началу поля элемента c[j][k] используется функция gotoxy.

Работа функции printa может быть проверена с помощью следующей программы:

#include <stdio.h>

#include <conio.h>

void main()

{ int a[3][4]={{1,2,3,4},

{10,20,30,40},

{100,200,300,400}};

int b[4][4]={{1,2,3,4},

{5,6,7,8},

{9,10,11,12},

{13,14,15,16}};

printa(5,5,4,(int *)a,3,4);

printa(5,40,5,(int *)b,4,4);

getch();

}

Результат ее работы приведен на рис. 9.1. Обратите внимание еще на одну деталь: поскольку имена матриц являются указателями не на первый элемент матрицы, а на ее первую строку, то при обращении к функции printa потребовалось преобразовать указатель к типу (int *). Прибавление 1 к преобразованному указателю позволит с помощью указателя c перебирать элементы матрицы, а не ее строки.

Рис. 9.1. Вывод числовых матриц в заданных окнах

Следует отметить, что можно обойтись и без формирования переменного формата в функции printf, если воспользоваться сравнительно редко применяемым форматным указателем *:

printf("%*d",w,c[k+j*m]);

Значение переменной w в данном случае не выводится на экран, а замещает символ * в форматном указателе "%*d".

Пример 9.3. Количество "счастливых" билетов. Билеты для общественного транспорта хранятся в рулонах и идентифицируются номерами от 000000 до 999999. В молодежной среде "счастливым" считался билет, у которого сумма трех первых цифр совпадала с суммой трех последних цифр. Попытаемся оценить количество "счастливых билетов в полном рулоне с целью определения вероятности получить такой билет.

Самый простой способ заключается в организации 6 вложенных друг в друга циклов, где каждый счетчик перебирает цифры от 0 до 9, а во внутреннем цикле сравниваются суммы первых трех и последних трех счетчиков:

#include <iostream.h>

#include <conio.h>

void main()

{ int n=0;

for(int j1=0; j1<10; j1++)

for(int j2=0; j2<10; j2++)

for(int j3=0; j3<10; j3++)

for(int j4=0; j4<10; j4++)

for(int j5=0; j5<10; j5++)

for(int j6=0; j6<10; j6++)

if(j1+j2+j3==j4+j5+j6) n++;

cout << n;

getch();

}

Считает эта программа правильно и довольно быстро (на ПК с частотой 2 ГГц время работы не превышает 1 сек). Но работу этой программы можно ускорить почти в 1000 раз за счет использования массивов. Сумма трех цифр принадлежит диапазону [0,27]. Допустим, что нам удалось подсчитать, сколько раз встретилась каждая сумма – s0 (количество сочетаний, давших сумму 0), s1 (количество сочетаний, давших сумму 1), … . Тогда количество "счастливых" билетов будет равно (s0)2+(s1)2+ ... . Поэтому гораздо более быстрой программой будет следующая:

#include <iostream.h>

#include <conio.h>

void main()

{ int n=0,k,s[28];

for(k=0; k<28; k++) s[k]=0;

for(int j1=0; j1<10; j1++)

for(int j2=0; j2<10; j2++)

for(int j3=0; j3<10; j3++)

s[j1+j2+j3]++;

for(k=0; k<28; k++)

n += s[k]*s[k];

cout << n;

getch();

}

Количество циклов, которое "крутится" в этой программе равно 28+1000+28, тогда как в предыдущей программе тело самого внутреннего цикла повторялось 1000000 раз.

Пример 9.4. Ход конем. Одна из задач, связанных с шахматами заключается в определении минимального количества ходов коня, за которое он может перебраться из стартовой клетки шахматного поля в заданную клетку. Напомним, что конь ходит "буквой Г", совершая за один ход перемещение на 2 клетки по одной координате (вертикали или горизонтали) и на одну клетку по другой координате. Шахматисты пользуются алфавитно-цифровой кодировкой полей шахматной доски, обозначая их по горизонтали буквами (A,B,C,D,E,F,G,H), а по вертикали – цифрами (1,2,3,4,5,6,7,8). Однако для программы удобнее иметь дело только с цифровыми индексами, изменяющимися (с учетом требований языка C) от 0 до 7. Идея определения минимального количества ходов заключается в заполнении клеток шахматного поля числами досягаемости. Сначала мы находимся в стартовой позиции и пытаемся сделать из нее один из 8 возможных ходов (не выходя за пределы шахматной доски). Каждую из достижимых таким образом клеток отметим числом 1. Затем из каждой из них попытаемся сделать тоже один из возможных ходов, не возвращаясь в те клетки, где мы уже побывали. Вновь достижимые клетки пометим числом 2. Таким способом можно заполнить все клетки шахматного поля, включая и ту, в которую мы должны были придти из стартовой позиции. Для того чтобы выделить свободные клетки, еще не помеченные уровнем досягаемости, распишем в начале все клетки числом -1.

Пусть массив a размерности 8×8 моделирует шахматную доску. Распишем элементы этого массива числами -1 (признак того, что все клетки свободны). Затем запросим у пользователя индексы стартовой позиции и занесем в этот элемент массива 0 (из стартовой позиции в нее же можно добраться за 0 ходов). Пусть функция, которая пытается сделать один из 8 возможных ходов, называется newlevel и использует в качестве исходной информации данные, вынесенные в глобальные переменные – матрицу a, уровень досягаемости k и управляющую переменную xod. Ее задачей является поиск всех незанятых клеток, находящихся на расстоянии одного хода от клеток с уровнем досягаемости k и записью в них кода k+1. Значение управляющей переменной xod=1, если хотя бы один такой ход удалось сделать. Если все клетки шахматной доски помечены неотрицательными уровнями досягаемости, т.е. ни одного хода больше сделать нельзя, то значение переменной xod равно 0. В функции newlevel удобно выделить процедуру try_ (подчерк добавлен к имени потому, что слово try является служебным), которая пробует, достижима и свободна ли позиция с индексами (p,q), в которой надо разместить очередной уровень досягаемости (k+1).

Описанные выше функции newlevel и try_ могут быть организованы следующим образом:

void try_(int p, int q)

{ if(p>=0 && p<8 && q>=0 && q<8 && a[p][q]<0)

{ a[p][q]=k+1; xod=1; }

}

//----------------------------------

void newlevel()

{ char di[8]={-2,-2,-1,-1, 1,1, 2,2};

char dj[8]={-1, 1,-2, 2,-2,2,-1,1};

xod=0;

for(int i=0; i<8; i++)

for(int j=0; j<8; j++)

if(a[i][j]==k)

for(int r=0; r<8; r++)

try_(i+di[r],j+dj[r]);

k++;

}

//----------------------------------

Обратите внимание на массивы di и dj. Их одноименные элементы образованы из всевозможных сочетаний ±1 и ±2, которые определяют 8 всевозможных направлений для хода конем. Цикл по r перебирает все эти комбинации относительно позиции (i,j).

Головная программа, обращающаяся к функции newlevel, может иметь следующий вид:

#include <stdio.h>

#include <conio.h>

char a[8][8],i,j,k=0,xod;

void main()

{ for(i=0;i<8;i++)

for(j=0; j<8;j++) a[i][j]=-1;

printf("begin position=");

scanf("%d %d",&i,&j);

a[i][j]=0;

do newlevel()

while (xod==1);

for(i=0;i<8;i++)

{ for(j=0; j<8; j++) printf("%3d",a[i][j]);

printf("\n");

}

getch();

}

Результат работы для начальной позиции (1,1) приведен на рис 9.2.

Рис. 9.2. Ход конем

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

#include <stdio.h>

#include <conio.h>

void sort(int *a,int n)

{ int tmp;

for(int i=0;i<n-1;i++)

for(int j=i+1;j<n; j++)

if(a[j]<a[i])

{tmp=a[i]; a[i]=a[j]; a[j]=tmp; }

}

int difference(int *a,int n)

{ int i,m=1;

sort(a,n); //сортировка исходного массива

for(i=0; i<n-1; i++)

if(a[i]!=a[i+1]) m++;

return m;

}

void main()

{ int a0[5]={0,0,0,0,0};

int a1[5]={1,1,1,1,1};

int a2[5]={0,1,1,1,1};

int a3[5]={0,0,1,1,2};

int a4[5]={0,1,2,3,4};

int a5[5]={1,2,3,4,5};

printf("\na0:%d",difference(a0,5));

printf("\na1:%d",difference(a1,5));

printf("\na2:%d",difference(a2,5));

printf("\na3:%d",difference(a3,5));

printf("\na4:%d",difference(a4,5));

printf("\na5:%d",difference(a5,5));

getch();

}

//=== Результат работы ===

a0:1

a1:1

a2:2

a3:3

a4:5

a5:5

Вариант 2. При первом просмотре определяем, содержится ли в исходном массиве хотя бы один нулевой элемент. Если содержится, то в переменную k0 заносим 1, в противном случае в k0 заносим 0. При втором проходе нулевые элементы исключаем из рассмотрения и сравниваем a[i] c a[j]. В случае равенства в элемент a[i] заносим 0. При третьем проходе подсчитываем ненулевые элементы и добавляем к сумме k0. Мы ограничимся только видоизмененной функцией difference:

int difference(int *a,int n)

{ int i,j,k0=0,m=0;

for(i=0; i<n; i++) //первый проход

if(a[i]==0) { k0=1; break; } //поиск нулевого элемента

for(i=0; i<n-1; i++) //второй проход

{ if(a[i]==0) continue; //обход нулей

for(j=i+1; j<n; j++) //поиск дубликатов

if(a[i]==a[j])

{ a[i]=0; break; } //забой дубликата

}

for(i=0;i<n;i++) //подсчет ненулевых элементов

if(a[i]!=0) m++;

return m+k0;

}

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

Обратите внимание на следующие приемы работы со шкалой. Во-первых, под нее надо запросить память (желательно, чистую). Это можно сделать следующим образом. Если длина нашего массива не превышает 32767 элементов, то под логическую шкалу потребуется не более 4096 байт. Запрос памяти под шкалу лучше всего организовать через библиотечную функцию calloc. У нее задается два аргумента – количество элементов данных и число байт, необходимых для хранения каждого элемента:

b=calloc(4096,1); //запрос чистой памяти

Во вторых, для перевода целого числа N в соответствующий разряд шкалы b удобнее воспользоваться номером байта шкала (переменная byte) и номером бита (переменная bit) в этом байте. Кроме этого нам потребуется массив из 8 однобайтовых констант, каждая из которых представляет один бит шкалы:

char mask[8]={128,64,32,16,8,4,2,1};

Для определения номеров байта и бита, соответствующих числу N, достаточно проделать два деления:

byte=N / 8;

bit =N % 8;

В итоге окончательный вид функции difference таков:

int difference(int *a,int n)

{ int bit,byte,i,m=0;

char mask[8]={128,64,32,16,8,4,2,1};

char *b=(char *)calloc(4096,1); //запрос и очистка памяти

for(i=0; i<n; i++)

{ byte = a[i] / 8;

bit = a[i] % 8;

if((b[byte]& mask[bit])==0) //проверка бита шкалы

{ m++; b[byte] |= mask[bit]; } //вписывание бита в шкалу

}

free(b); //освобождение памяти

return m;

}

При окончательной сборке программы надо не забыть подключить заголовочный файл alloc.h, в котором находится прототип функции calloc.