10«В» — Комментарии к решениям самостоятельной работы №1.

Ошибки

Не используйте неинициализированную память!
Такие ошибки очень тяжело найти — программа иногда работает, иногда нет.

В следующем тексте первый цикл ничего не записывает в последний элемент массива.
А во втором цикле все элементы массива (в том числе и последний) проверяются на некоторое условие!


int i,j,boo=0;
int t[m];

for(i=0;i<m-1;i++){
    t[i]=i+2;
}
for(i=0;i<m;i++){
    for(j=0;j<i;j++){
        if(t[i]%t[j]==0)    boo=1;
        ...
    }
    ...
}
 
 

Пишите правильную букву (тип в спецификаторе формата) в scanf и printf!
Например, для unsigned требуется %u.

Если пользователь следующей программы введёт -1, он получит очень интересный результат…


unsigned int m;

scanf ("%d", &m);

...
 
 

Задавайте правильное число параметров правильного типа для функций scanf и printf!

Не надо делать так, как в следующем тексте:


int n, i, j;

printf("\n", i, j);
 
 

Задача B. Двумерные массивы

Ввести число n. Заполнить массив n × n числами 1, 2,… ,n2 по спирали.
 

Варианты ваших (правильных!) решений

Легко ли понять, что эти программы действительно правильные?

Интересно, что в примере 1 есть только 4 записи в массив! С другой стороны, это наименее понятный текст, да и вопросов к нему хватает (зачем обнуляется массив? почему не вычислить s так: s=(m+1)/2 ? ну и — массивы C++).


void spirale(int m){
    int t[m][m];
    int i,j,s=((m%2)+m)/2,r,f=1;
    for(i=0;i<m;i++){
        for(j=0;j<m;j++){
            t[i][j]=0;
        }
    }
    for(r=0;r<s;r++){
        for(i=r;i<m-1-r;i++){
            t[i][r]=f;
            f++;
        }
        for(i=r;i<(m-1)-r;i++){
            t[(m-1)-r][i]=f;
            f++;
        }
        for(i=m-r-1;i>r-1;i--){
            t[i][m-1-r]=f;
            f++;
        }
        for(i=(m-2)-r;i>r;i--){
            t[r][i]=f;
            f++;
        }
    }
    ...
}
 

int n, i, j, l=1;
unsigned int k[100][100];

/* ... ввод и проверка n ... */

for (i=0; i<(n/2); i++)
    {

    for (j=i; j<n-i-1; j++)
        {
        k[i][j]=l;
        l+=1;
        }

    for (j=i; j<n-i-1; j++)
        {
        k[j][n-i-1]=l;
        l+=1;
        }

    for (j=n-i-1; j>i; j--)
        {
        k[n-i-1][j]=l;
        l+=1;
        }

    for (j=n-i-1; j>i; j--)
        {
        k[j][i]=l;
        l+=1;
        }
    }
if (n%2==1)
   {
   k[n/2][n/2]=n*n;
   }
 

#define N 100 // задаём максимальную длину массива

int n, i, j, k = 1;
int arr[N][N];

/* ... ввод и проверка n ... */

for (i = 0; i <= (n - 1) / 2; ++i)
{  
    arr[i][i] = k;
    ++k;
    for (j = i + 1; j <= n - i - 1; ++j)
    {  
        arr[i][j] = k;
        ++k;
    }
    for (j = i + 1; j <= n - i - 1; ++j)
    {  
        arr[j][n - i - 1] = k;
        ++k;
    }
    for (j = n - i - 2; j >= i; --j)
    {  
        arr[n - i - 1][j] = k;
        ++k;
    }
    for (j = n - i - 2; j >= i + 1; --j)
    {  
        arr[j][i] = k;
        ++k;
    }
}
 

А если сделать так?

Больше всего это похоже на пример 2 из левой колонки. Основное отличие в том, что здесь сразу видно, сколько раз выполняются внутренние циклы. В примерах слева — приходится соображать.
 
Заполнение 2-мерного массива по спирали (послойно, снаружи внутрь)
#define NMAX 100

int m[NMAX][NMAX];
int n, value = 1;
int i, len, j;

/* ... ввод и проверка n ... */

/*
 *  (i,i) -- координаты угловой клетки слоя (от 0 до n/2)
 *  len*4 -- количество клеток в слое
 *            (кроме центральной, но в неё мы сейчас не попадём)
 */
for (i = 0, len = n - 1;  len > 0;  len -= 2, i++)
{
    int r = i;      // row
    int c = i;      // column

    for (j = 0; j < len; j++)   m[r  ][c++] = value++;
    for (j = 0; j < len; j++)   m[r++][c  ] = value++;
    for (j = 0; j < len; j++)   m[r  ][c--] = value++;
    for (j = 0; j < len; j++)   m[r--][c  ] = value++;
}
if (len == 0)           // при чётном n в конце будет len < 0
    m[i][i] = value;    // иначе -- запись в центральную клетку
 

Вот совершенно отличный от предыдущих вариант (сделан на основе программы Георгия Соколова)

Здесь только один цикл. И начальное обнуление массива необходимо. Так как именно данные массива (и его границы) управляют обходом клеток.

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

 
Заполнение 2-мерного массива по спирали (управляется данными в массиве)
#define NMAX 100

int r = 0;                      // начальная клетка спирали:    row
int c = 0;                      //                              column
const int dc[4] = {1, 0, -1, 0};        // приращения координат
const int dr[4] = {0, 1, 0, -1};        //      в спирали (4 направления)
int dir = 0;                    // начальное направление
                                //  (меняется так: 0 1 2 3 0 1 2 3 ...)
int m[NMAX][NMAX];
int n, value, maxval;
int i, j;

/* ... ввод и проверка n ... */

for (i = 0;  i < n;  i++)
    for (j = 0;  j < n;  j++)
        m[i][j] = 0;          // сначала в массиве ничего нет (= 0)

maxval = n*n;   // столько клеток надо заполнить
/*
 *  идём в текущем направлении, пока можно,
 *  затем меняем направление на следующее
 *      (после смены направления всегда можно сделать хотя бы один шаг)
 */
for (value = 1;  value <= maxval;  value++)
{
    int next_r, next_c;

    m[r][c] = value;          // записали очередное значение

    next_r = r + dr[dir];     // такие будут координаты следующей клетки,
    next_c = c + dc[dir];     //      если направление не изменится

    if ( next_r >= 0  &&  next_r < n    // можно ли не менять направление? --
     &&  next_c >= 0  &&  next_c < n    //      row и column попадут в массив?
     &&  m[next_r][next_c] == 0 )       //      следующая клетка ещё свободна?
    {                            // да, всё хорошо,
        r = next_r;              // записываем новые координаты
        c = next_c;              //      в r, c
    }
    else                         // нет, надо менять направление
    {
        dir = (dir + 1) & 3;     // новое направление (0,1,2,3 -> 1,2,3,0)
        r += dr[dir];            // новые
        c += dc[dir];            //      координаты
    }
}
 

Как теперь вывести двумерный массив?

Варианты ваших решений и их выдача

Не user friendly здесь только вариант 1.
Вариант 2 можно оформить короче и красивее.
Вариант 3 можно сделать оптимальнее.
 

Можно было сделать так

Если ограничиться массивом 100 × 100, то, конечно, можно отвести по 5 символов на число. Как в примере 1 или 2 (обратите внимание на правильный последний printf в 1 и на %u для unsigned в 2!).
Или же посчитать (заранее!) нужное количество, как в примере 3.
 

#define N 100 // задаём максимальную длину массива

int n, i, j;
int arr[N][N];

for (i = 0; i < n; ++i)
{  
    for (j = 0; j < n; ++j)
    {  
        printf("%d ", arr[i][j]);
    }
    printf("\n", i, j);
}
 

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

#define N 100 // задаём максимальную длину массива

int n, i, j;
int arr[N][N];

for (i = 0; i < n; ++i)
{  
    for (j = 0; j < n; ++j)
    {  
        printf(" %5d", arr[i][j]);
    }
    printf("\n");
}
 

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

int n, i, j;
unsigned int k[100][100];

for (i=0; i<n; i++)
    {
    for (j=0; j<n; j++)
        {
        if (k[i][j]<10)
           {
           printf ("%d     ", k[i][j]);
           }
           else
           {
           if (k[i][j]<100)
              {
              printf ("%d    ", k[i][j]);
              }
              else
              {
              if (k[i][j]<1000)
                 {
                 printf ("%d   ", k[i][j]);
                 }
                 else
                 {
                 if (k[i][j]<10000)
                    {
                    printf ("%d  ", k[i][j]);
                    }
                    else
                    {
                    printf ("%d ", k[i][j]);
                    }
                 }
              }
           }
        }
    printf ("\n");
    }
 

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

int n, i, j;
unsigned int k[100][100];

for (i = 0;  i < n;  i++)
{
    for (j = 0;  j < n;  j++)
    {
        unsigned int m = k[i][j];

        if (m < 10)     printf("%u     ", m);   else
        if (m < 100)    printf("%u    ", m);    else
        if (m < 1000)   printf("%u   ", m);     else
        if (m < 10000)  printf("%u  ", m);      else
                        printf("%u ", m);
    }
    printf("\n");
}
 
На самом деле, всё проще:

int n, i, j;
unsigned int k[100][100];

for (i = 0;  i < n;  i++)
{
    for (j = 0;  j < n;  j++)
    {
        printf("%-5u ", k[i][j]);
    }
    printf("\n");
}
 

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

#define NMAX 500

int arr[NMAX][NMAX];
int n, i, j;

for (i = 0; i < n; i++) {
    for (j = 0; j < n; j++) {
        printf("%d ", arr[i][j]);
        for (k = 10; k <= n * n; k *= 10) {
            if (arr[i][j] < k)    printf(" ");
        }
    }
    printf("\n");
}
 

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

#define NMAX 500

int arr[NMAX][NMAX];
int n, i, j;
int width, k;

width = 0;
for (k = n * n;  k > 0;  k /= 10)
    width++;

for (i = 0;  i < n;  i++)
{
    for (j = 0;  j < n;  j++)
        printf("%-*d ", width, arr[i][j]);
    printf("\n");
}
 

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

Проверка правильности ввода и зацикливание программы

По-отдельности каждое из этих требований не слишком сложно.
В самом деле: проверили входные данные "на вшивость" и вышли из программы, если плохо.
Или: поместили всю работу программы в цикл (спрашивая в конце, надо ли продолжать).

Проблема возникает, когда нам надо сделать и то, и другое.

Предположим, пользователь ввёл настолько неправильные данные, что после их обработки какими-нибудь функциями ввода (scanf и т.п.) в буфере ввода осталось ещё что-то.
Например, если вместо целого числа программе было выдано такое: 123 Yyye-e-e-essss!!!, то весь "хвост" после 123 останется в буфере и будет использован при вводе следующих данных (ответа на вопрос, продолжать ли, целого числа для следующего цикла работы и т.д.). Последствия могут быть самые разные…

Ниже показан один из возможных вариантов решения этой проблемы.

Слева приведена общая структура программы. Весь ввод вынесен в функции, которые, помимо сообщения о правильности/неправильности данных, могут вернуть ещё и признак того, что программе надо завершиться (в наших консольных программах такое бывает, если поток ввода закончился: например, встретился конец входного файла — EOF — или пользователь ввёл этот признак с клавиатуры — Ctrl-Z в Windows, Ctrl-D в Linux).

Справа — возможная реализация функций ввода, использующая только функцию scanf. Идея состоит в том, чтобы после каждого ввода смотреть, что ещё осталось в буфере ввода, но не во всём буфере, а только до конца текущей строки. И если там остались "непустые" символы (проверяется функцией isspace из ctype.h), то считать, что ввод был неправильный. Конечно, этот "хвост" строки надо в любом случае удалить из буфера, чтобы больше не мешался.
Такое решение — неоднозначное, мы тем самым несколько ограничиваем возможности ввода для пользователя. Но оно вполне приемлемо, особенно если учесть, какими простыми средствами мы воспользовались.

 

Общая структура программы (параметры в глобальных переменных)


int n, k;               // входные параметры


/*
 *  return > 0 -- параметры введены и правильные (EOF мог быть после них)
 *  return ==0 -- параметры не введены или ошибочны, был встречен EOF
 *  return < 0 -- параметры не введены или ошибочны, EOF не было
 */
int input_and_check(void)
{
        ... ввод параметров (n,k) и проверка ...
}


void do_work(void)
{
        ... использование параметров (n,k) ...
}


/*
 *  return > 0 -- repeat (Y or y)
 *  return ==0 -- do not repeat (N or n or EOF)
 *  return < 0 -- ошибка
 */
int is_cycling(void)
{
        printf("Repeat? (Y/N)  ");
        ... ввод и проверка ответа ...
}


int main(void)
{
        int cycle;      // 0 - надо завершать программу
                        // >0 - OK, работаем и зацикливаемся
                        // <0 - плохие параметры - зацикливаемся
        do {
                cycle = input_and_check();
                if (cycle <= 0)         // если ошибка при вводе
                        continue;       //      или если надо завершать

                do_work();

                do {                            // повторяем вопрос,
                        cycle = is_cycling();
                } while (cycle < 0);            // пока не получим Y or N

        } while (cycle != 0);           // while (cycle); -- то же самое

        return 0;
}
 

Общая структура программы (параметры в локальных переменных)


/*
 *  return > 0 -- параметры введены и правильные (EOF мог быть после них)
 *  return ==0 -- параметры не введены или ошибочны, был встречен EOF
 *  return < 0 -- параметры не введены или ошибочны, EOF не было
 */
int input_and_check(int *pn, int *pk)
{
        ... ввод параметров (*pn,*pk) и проверка ...
}


void do_work(int n, int k)
{
        ... использование параметров (n,k) ...
}


/*
 *  return > 0 -- repeat (Y or y)
 *  return ==0 -- do not repeat (N or n or EOF)
 *  return < 0 -- ошибка
 */
int is_cycling(void)
{
        printf("Repeat? (Y/N)  ");
        ... ввод и проверка ответа ...
}


int main(void)
{
        int n, k;       // входные параметры

        int cycle;      // 0 - надо завершать программу
                        // >0 - OK, работаем и зацикливаемся
                        // <0 - плохие параметры - зацикливаемся
        do {
                cycle = input_and_check(&n, &k);
                if (cycle <= 0)         // если ошибка при вводе
                        continue;       //      или если надо завершать

                do_work(n, k);

                do {                            // повторяем вопрос,
                        cycle = is_cycling();
                } while (cycle < 0);            // пока не получим Y or N

        } while (cycle != 0);           // while (cycle); -- то же самое

        return 0;
}
 

Реализация функций ввода (параметры в локальных переменных)

Удобно использовать вспомогательную функцию, которая будет проверять конец строки в буфере ввода на пустоту и удалять его из буфера.

#include <ctype.h>


/*
 *  input and discard string of chars (up to, including, \n)
 *
 *  return > 0 -- OK -- пустая строка (with \n)
 *  return ==0 -- пустая строка, ошибка scanf (EOF - конец потока ввода)
 *  return < 0 -- непустая строка (with or without \n)
 */
int inp_blank_string(void)
{
        int i, spc = 1;
        char ch;

        // обязательно вводим всё вместе с концом строки

        while ((i = scanf("%c", &ch)) > 0  &&  ch != '\n')
                if (!isspace(ch))       // или:
                        spc = 0;        //      spc &= isspace(ch);

        if (!spc) return -1;    // непустая строка
        if (i <= 0) return 0;   // пустая, ошибка scanf
        return 1;               // пустая, вместе с концом строки
}


/*
 *  return > 0 -- параметры введены и правильные (EOF мог быть после них)
 *  return ==0 -- параметры не введены или ошибочны, был встречен EOF
 *  return < 0 -- параметры не введены или ошибочны, EOF не было
 */
int input_and_check(int *pn, int *pk)
{
        int num, i, eof = 0;

        printf("1st parameter (from 1 to 20) = ");
        num = scanf("%d", pn);          // num > 0 - OK
        i = inp_blank_string();
        eof ||= !i;
        if (i < 0)              // непустая строка
                num = -1;
        if (num > 0)            // есть параметр
        {
                if (*pn < 1 || *pn > 20)        // проверка параметра
                        num = -1;               // если -- плохой
        }
        if (num <= 0)
        {
                printf("? - 1st parameter is invalid\n");
                if (eof) return 0;
                return -1;
        }

        printf("2nd parameter (from 1 to %d) = ", *pn);
        num = scanf("%d", pk);          // num > 0 - OK
        i = inp_blank_string();
        eof ||= !i;
        if (i < 0)              // непустая строка
                num = -1;
        if (num > 0)            // есть параметр
        {
                if (*pk < 1 || *pk > *pn)       // проверка параметра
                        num = -1;               // если -- плохой
        }
        if (num <= 0)
        {
                printf("? - 2nd parameter is invalid\n");
                if (eof) return 0;
                return -2;
        }

        return 1;
}


/*
 *  return > 0 -- repeat (Y or y)
 *  return ==0 -- do not repeat (N or n or EOF)
 *  return < 0 -- ошибка
 */
int is_cycling(void)
{
        int i = 1;      // хороший ввод (i <= 0 -- плохой)
        char ch;

        printf("Repeat? (Y/N)  ");
        if (scanf(" %c", &ch) <= 0)     // с пропуском начальных "пустых"
                return 0;               // если EOF -- do not repeat

        if (inp_blank_string() < 0)
                i = -1;                 // непустая строка
        else
        {
                ch = toupper(ch);       // upper case
                if (ch != 'Y' && ch != 'N')     // проверка ввода
                        i = -1;                 // если -- плохой ввод
        }
        if (i <= 0) return -1;  // ни Y, ни N
        if (ch == 'Y') return 1;        // Y
        return 0;                       // N
}
Обратите внимание на вызов scanf(" %c", &ch) в функции is_cycling, пропускающий все пробельные символы (в том числе \n).
 

Конечно же, такого рода функции прямо напрашиваются, чтобы их вынесли в заголовочный файл. Затем этот файл можно просто подключать в свои программы директивой #include "my_inp_funcs.h".
(Конкретно здесь для этого не очень пригодна функция input_and_check — хотелось бы вводить и проверять любой набор параметров любых типов!)