Лицензия Creative Commons
Содержимое блога доступно по лицензии Creative Commons Атрибуция — С сохранением условий
(Attribution-ShareAlike) 3.0 Unported
, если не указано иное.
Показаны сообщения с ярлыком массивы. Показать все сообщения
Показаны сообщения с ярлыком массивы. Показать все сообщения

понедельник, 17 января 2011 г.

Замечания по задаче удаления из строки лишних пробелов

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

Я получил от Антона Александровича (преподавателя из НИИТа) несколько замечаний по решению этой задачи:


1. Бесконечный цикл while(1) можно заменить на цикл while(charray [i]).

Известно, что строка (в отличии от массива символов) всегда заканчивается нулевым символом, он же '\0'. Так же известно, что 0 (ноль) в языке Си (да и во многих других ЯП) означает "ложь" (false). Исходя из этих двух фактов становится ясно, что я могу использовать проверку кода i-того символа из строки в условии завершения цикла while().


while('\0' != charray[i]) {
  ...
  i++;
}

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

а) while('\0' != charray[i]) - цикл выполняется, пока i-й элемент строки не равен символу '\0'
б) while(0 != charray[i]) - цикл выполняется, пока i-й элемент строки не равен нулю
в) while(charray[i]) - цикл выполняется, пока проверка i-го элемента не возвращает значение "false".

Очевидно, что вышеперечисленные варианты равнозначны. Однако, вариант "в" работает не хуже варианта "а", а если нет разницы - то зачем писать больше? Поэтому, в конечном счёте я использовал вариант "в".

Использование вместо бесконечного цикла while(1) цикл с проверкой i-того элемента строки на соответствие нулевому символу позволило избавиться от двух строк в теле цикла:


if(i == len)
  break;

Понятно, что теперь эти строки не нужны, т.к. цикл завершится, когда проверка charray[i] вернёт "false".


2. Из функций можно выкинуть переменную len2.

Возьму для примера функцию удаления лишних пробелов.


int remove_n(char *arr, int len, int pos) {
  int i;
  int len2 = len-1;

  for(i = pos; i <= len2; i++)
    arr[i] = arr[i+1];
  
  return 0;
}

Я использовал дополнительную переменную len2 для сохранения результата вычисления len-1, чтобы не вычислять это значение при каждой итерации в цикле for(). Как я понимаю, в данном случае использование len2 излишне, и цикл можно переписать следующим образом:


for(i = pos; i <= len-1; i++)
  arr[i] = arr[i-1];

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


3. Проверка на наличие лишнего пробела, после работы конечного автомата, не является "костылём"

После размышлений, я понял, почему конечный автомат не может удалить все пробелы в конце строки. Рассмотрим два случая:


    _______________________
1. | * | * | * | * |   | 0 |
    -----------------------
                     ^
    _______________________
2. | * | * | * |   |   | 0 |
    -----------------------
                 ^

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


while(charray[i])

  switch(state) {

  case STATE_1:
    if(' ' == charray[i]) {
      state = STATE_2;
      if((0 == i) || !charray[i+1])) // <-- здесь
        continue;
    }
    break;

  case STATE_2:
    ...
    break;

  }

  i++;
}
...

То есть, если символ, находящийся в строке на позиции i+1 является нулём, я перехожу к следующей итерации цикла while(charray[i]) без инкремента i. Таким образом, я могу удалить последний пробел тем же способом, что я удаляю все пробелы в начале строки.

Но если я попробую использовать приведённый выше код для второго варианта (когда пробелов в конце строки больше 1), то опять же получу один лишний пробел в конце строки после работы программы. Почему? А потому, что условие !charray[i+1] никогда не выполнится уже в том случае, если количество пробелов в конце строки будет хотя бы равно двум: элемент строки, находящийся на позиции i+1, так же будет являться пробелом.

Поэтому я согласен с Антоном Александровичем, что проверка на наличие лишнего пробела после работы конечного автомата не является "костылём", а всего лишь дополняет решение задачи.

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

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


...
while(charray[i]) {
  ...
  i++;
}

if(' ' == charray[i-1])
  charray[i-1] = 0;
...

понедельник, 10 января 2011 г.

Заполнение массива равным количеством случайных положительных и отрицательных чисел

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

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

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

Генерирую случайное число


Для генерации случайных чисел, мне потребуются библиотеки


#include <stdlib.h>
#include <time.h>


Разумным будет задание размера массива N и максимальное значение MAX для генератора случайных чисел с помощью define:


#define N 10
#define MAX 100


Далее нужно запустить генератор случайных чисел с помощью srand() на основе текущего времени, возвращаемого функцией time(). Внутрь функции я поместил цикл while(), и с каждой новой итерацией будет генерироваться случайное число с помощью функции rand() и сохраняться в переменную buf.

Чтобы генерировать числа от 0 до 99, я должен получить остаток от деления результата работы rand() на MAX.


srand(time(0)); // Завожу генератор случайных чисел
buf = rand() % MAX; // Генерирую случайное число


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


buf = rand % MAX - (MAX / 2);


Теперь самое интересное. Мне предстоит...

Заполнение массива


Как заполнить массив одинаковым количеством положительных и отрицательных чисел в случайном порядке? Судя по условию поставленной задачи, количество положительных и отрицательных чисел в массиве размером N должно быть равно N / 2.

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

Отлично. Завожу два счётчика, один из которых будет считать количество "положенных" в массив отрицательных чисел, другой - положительных. Цикл while() будет выполняться, пока rand() не сгенерирует достаточное количество случайных чисел для заполнения массива.


int array_fill(int *arr, int len, int max) {
  int max_h = max / 2;
  int len_h = len / 2;
  int buf;
  int p = 0; // Счётчик положительных чисел
  int n = 0; // Счётчик отрицательных чисел
  int i = 0;

  srand(time(0));
  
  while(i < len) {
    buf = rand() % max - max_h;
 
    if((buf < 0) && (n < len_h)) {
      arr[i] = buf;
      n++;
      i++;
    }

    if((buf >= 0) && (p < len_h)) {
      arr[i] = buf;
      p++;
      i++;
    }
    
  }

  return 0;
}

Итак, предположим, что "лимит" на отрицательные числа исчерпан (n == len_h). Цикл будет повторяться, пока в buf не окажется положительное число. То же произойдёт, если p == len_h, только в этом случае цикл будет повторяться до появления в buf отрицательного числа. Таким образом, мы получаем массив, заполненный в равном количестве как положительными, так и отрицательными числами. Задача решена.

Пример вызова функции:


array_fill(arr, N, MAX);



P.S. На самом деле, у этой функции есть один недостаток - при нечётном размере массива она зацикливается. Этот недостаток можно преодолеть, добавив в функцию проверку на чётность размера массива. Если проверка даёт положительный результат, то остаётся сгенерировать ещё одно число и записать его в начало (или конец) массива.

суббота, 8 января 2011 г.

Удаление из строки "лишних" пробелов

У меня есть программа, которая должна запросить у пользователя строку, и произвести с полученной строкой определённые действия. Но вот ведь незадача - некоторые очень неаккуратно работают с клавиатурой, и при вводе данных могут случайно нажать пробел несколько раз вместо одного, добавить совершенно ненужных пробелов в начале строки, или даже в конце. А представьте, что будет, если по клавиатуре пройдётся ваш любимый кот? Это будет катастрофа! Но спокойно, можно предусмотреть и это. Далее я хочу рассмотреть метод (несомненно, один из многих) удаления лишних пробелов из строки.

Для начала определим, какие пробелы считаются лишними. Лишними считаются
  1. все пробелы, которые стоят в начале строки (т. е. перед первым символом строки, не являющегося пробелом);
  2. пробелы между символами, если количество идущих подряд пробелов равно двум или более;
  3. все пробелы в конце строки.

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

Копать или не копать?


Для решения этой задачи удобно использовать так называемый конечный автомат. То есть, некий абстрактный автомат, который может пребывать в конечном количестве состояний. Например, в двух: "копать" или "не копать". Этот автомат будет переключаться из одного состояния в другое, в зависимости от выполнения каких-то условий.

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

Но прежде, чем что-то удалять из строки, мне нужна сама строка.

Получаю строку c помощью функции fgets()


Напишем работающую программу, которая считывает строку.


#include <stdio.h>
#include <string.h>
#define N 256

int main() {
  int charray[N];

  printf("Please enter string:\n> ");
  fgets(charray, N, stdin);
  
  return 0;
}

Эта программа пока только считывает строку со стандартного ввода (stdin) с помощью функции fgets() в массив символов размером N.

У функции fgets() есть замечательное свойство - используя её, я никогда не выйду за границу массива, если попытаюсь ввести строку длиннее, чем размер массива. Однако, у неё есть одна особенность: стоит мне ввести строку и нажать [Enter], как в конец введённой строки добавиться знак "\n", т.е. символ перехода на новую строку. Эта проблема решается гениально просто. Нужно лишь узнать длину введённой строки с помощью strlen():


len = strlen(charray); // Определяем длину строки
if('\n' == charray[len-1]) // Если предпоследний символ в строке='\n'
  charray[len-1] = 0;      // то заменяем его на нулевой символ, 
                           // т.е. символ конца строки, он же '\0'

Теперь у меня есть строка, готовая к дальнейшей обработке.

Реализация конечного автомата


Для удаления одного лишнего пробела, нужно сдвинуть элементы массива влево на 1. То есть, если пробел находится на позиции S, то на его место запишется символ из ячейки S+1, вместо S+1 запишется S+2 и т.д.

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


int remove_spc(char *arr, int len, int pos) {
  int i;
  int len2 = len-1;

  for(i = pos; i <= len2; i++)
    arr[i] = arr[i + 1];

  return 0;
}

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

Самое забавное, что функции remove_spc() всё равно, какой элемент стоит после позиции pos - она так же будет сдвигать все пробелы, которые следуют за этой позицией. Более того, этой функции безразлично, какой элемент стоит на текущей позиции pos

Поэтому мне требуется что-то большее, чем эта функция. Лучше всего подходит для этой цели бесконечный цикл. В него я и помещу свой автомат. К слову, сейчас самое время подумать над его реализацией. Конечному автомату для решения этой задачи потребуется только два состояния, назову их STATE_1 и STATE_2. Находясь в состоянии STATE_1 автомат будет просто проверять, является ли текущий символ в просматриваемом массиве пробелом. Если пробел обнаруживается, автомат переключается в состояние STATE_2. В этом состоянии он будет копать удалять лишние пробелы.

Работу автомата удобно представить ввиде ленты и считывающей/записывающей головки над ней. Пустые ячейки - это пробелы, звёздочками отмечены занятые ячейки, т.е. в которых присутствуют символы, не являющиеся пробелами.

   _______________________________
1 | * | * |   |   |   | * | * | * |
   -------------------------------
        ^
   _______________________________
2 | * | * |   |   |   | * | * | * |
   -------------------------------
            ^
   _______________________________
3 | * | * |   |   |   | * | * | * |
   -------------------------------
                ^
   _______________________________
4 | * | * |   |   | * | * | * |   |
   -------------------------------
                ^
   _______________________________
5 | * | * |   | * | * | * |   |   |
   -------------------------------
                ^

На шаге 1 автомат находится в состоянии STATE_1. Ячейка занята, так что считывающая головка передвигается вправо на одну клетку.

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

Теперь посмотрим, что же происходит, когда автомат находится в состоянии STATE_2. А происходит вот что: автомат начинает выкидывать пробелы из строки, вызывая функцию remove_spc (шаг 4). При этом, сама "считывающая головка" остаётся неподвижной, и после каждого вызова функции проверяет содержимое ячейки, над которой она находится.
Как только в ней оказывается какой-либо символ (шаг 5), не являющийся пробелом, автомат переключается вновь в состояние STATE_1.

Вот, как это выглядит в программном коде:


#define STATE_1 1
#define STATE_2 2

...

while(1) {
  switch(state) {
  
  case STATE_1:
    if(' ' == charray[i]) {
      state = STATE_2;
      if(0 == i)
        continue;
    }
    break;

  case STATE_2:
    if(' ' != charray[i])
      state = STATE_1;
    else {
      remove_spc(charray, len, i);
      continue;
    }
    break;

  } // end switch

  i++;

  if(i == len)
    break;

} // end while

А теперь плохая новость. После работы автомата, при количестве лишних пробелов в конце строки больше одного, после последнего символа всё равно оставался один пробел. Я не смог пока разобраться, почему так происходит. Поэтому, после завершения бесконечного цикла while(1), пришлось использовать костыль в виде следующих строчек:


len = strlen(charray);
if(' ' == charray[len-1])
  charray[len-1] = 0;

Этот кусок кода удаляет самый последний пробел, если он остаётся после работы автомата.

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

Исходник:
remove-spaces.c