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

четверг, 25 августа 2011 г.

scroller.el - простое дополнение для GNU Emacs

Решил добавить в Emacs функцию, которую не нашёл во всём многообразии keybinding'ов (может, я просто плохо искал?)

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


;;; These simple functions allows scrolling with keeping
;;; position of cursor

(defun scroll-down-keeping-cursor (&optional arg)
  "Scrolling down one line while keep cursor position."
  (interactive "p")
  (if (interactive-p)
      (progn
        (scroll-down arg)
        (scroll-down 1))))

(defun scroll-up-keeping-cursor (&optional arg)
  "Scrolling up one line while keep cursor position."
  (interactive "p")
  (if (interactive-p)
      (progn
        (scroll-up arg)
        (scroll-up 1))))

Теперь неплохо бы привязать эти функции к клавишам:


(global-set-key (kbd "M-p") 'scroll-down-keeping-cursor)
(global-set-key (kbd "M-n") 'scroll-up-keeping-cursor)

Как и при использовании обычных комбинаций C-n и C-p, эти функции можно вызывать в интерактивном режиме, передавая количество строк, на которое следует прокрутить буфер:

C-u 10 M-n

Конечно, если прописать все функции, настройки и keybinding'и в custom.el, они загрузятся. Но IMHO пользовательские файлы для Emacs выглядят гораздо опрятнее, если их разложить по отдельным файлам/каталогам.

Например, пусть это дополнение будет в отдельном файле scroller.el и положим мы его в ~/.emacs.d/. Тогда в ~/.emacs.d/custom.el останется только загрузить этот файл:


(load-file "~/.emacs.d/scroller.el")


Или так:


(setq emacs-dotdir
      (expand-file-name ".emacs.d" "~"))
(load-file (expand-file-name "scroller.el" emacs-dotdir))

пятница, 22 апреля 2011 г.

Проект "Амёба"

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


Содержание

  1. Постановка задачи
  2. «Царство Протозоа, класс Лобозные амёбы, отряд...»
  3. Реализация методов

  4. Разделять и властвовать


Постановка задачи


В один прекрасный день на курсах мы начали изучать классы в C++.


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


Эта задача легко решается с помощью статических (static) полей класса.


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



«Царство Протозоа, класс Лобозные амёбы, отряд...»


Поскольку я хочу понять работу с классами, с конструирования класса и начнём. Из-за сходства поведения (доступных методов) этого класса с амёбой, я решил назвать мой класс в честь этих микроскопических организмов - clsAmoeba (по-русски, я думаю, это будет что-то вроде "КАмёба", т.е. "Класс амёба"). Название класса само собой стало названием этого проекта.


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


Тем не менее, стандартная КАмёба должна уметь как минимум три вещи:


  1. Делиться;
  2. Сообщать количество уже существующих КАмёб;
  3. Знать всех своих потомков.

Каждая КАмёба имеет свой IDентификационный номер, который тоже умеет сообщать (иначе как мы узнаем, с какой КАмёбой мы имеем дело?).


Я объявил класс в заголовочном файле main.h и включил этот файл с помощью директивы include в другие файлы проекта.


Листинг 1: Класс clsAmoeba.


class clsAmoeba
{
 public:
  clsAmoeba();
  clsAmoeba(const clsAmoeba& pt);
  ~clsAmoeba();

  /* This methods works with the counter */
  void incr()     { counter++;      };
  void decr()     { counter--;      };
  int  getCount() { return counter; };

  /* This method returns ID of an amoeba */
  int getID() { return id; };

  /* This method returns pointer to parent of an amoeba */
  clsAmoeba& getParent() { return *parent; };

  /* Get count of the descendants of an amoeba */
  int getDCount() { return dCount; }

  /* Get descendant of an amoeba */
  clsAmoeba& getDescendant(int i) { return *descendants[i]; }

  /* This method divide an amoeba */
  void divide();

 private:
  /* ID of the amoeba */
  int         id;
  
  /* Pointer to parent of an amoeba */
  clsAmoeba*  parent;
  
  /* Descendants of an amoeba */
  clsAmoeba** descendants;
  int         dCount;

  /* Counter of amoebas */
  static int counter;
};

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


Полями класса называются переменные этого класса.


Ключевые слова "public" и "private" являются спецификаторами области видимости всего того, что объявлено после них (до конца объявления класса или же до следующего спецификатора). Методы, как правило, представляют собой интерфейс доступа к классу. Прямого доступа к полям "извне" желательно избегать. Поля предназначены для "внутреннего использования" в классе, и доступ к ним должны иметь только особо посвящённые - а именно, методы этого класса. Поэтому обычно методы объявляются в секции "public" и доступны извне, а поля объявляются в секции "private" и скрыты от внешнего мира.


Доступ к потомкам КАмёбы обеспечен через метод getDescendant(). Этот метод принимает номер потомка (не ID, а именно номер потомка в массиве descendants), и возвращает указатель на класс-потомок. Метод getDCount() возвращает количество потомков.


Самый интересный метод в классе clsAmoeba - метод divide(). При вызове этого метода КАмёба (т.е. класс) делится две части: класс создаёт свою копию в массиве потомков.


Часть методов - например, простые методы вроде getID(), которые просто возвращают значение какого-то поля класса - объявлены прямо в теле класса. Такие классы называются "инлайновыми" (англ. Inline), т.е. в переводе на русский язык, встраиваемыми. И это действительно так - компилятор заменяет в коде вызовы этих методов на код внутри самих методов. Такие методы работают быстрее, так как при работе программы не тратится время на вызов метода. Встраиваемые методы не обязательно объявлять в теле класса - вместо этого, можно использовать ключевое слово inline перед объявлением метода.



Реализация методов


Все не-инлайновые методы КАмёбы я вынес в отдельный модуль amoeba.cpp.


Первым логично создать конструкторы и деструкторы класса - которые вызываются при создании экземпляра класса и при его уничтожении, соответственно.



Конструкторы и деструкторы


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


Первый конструктор производит инициализацию полей класса, увеличивает счётчик КАмёб на 1 и присваивает текущее значение счётчика в качестве ID созданной КАмёбе. Этот конструктор вызывается при создании первой КАмёбы.


Листинг 2: Конструктор 1.


clsAmoeba::clsAmoeba() :
  parent(NULL),
  descendants(NULL),
  dCount(0)
{
  incr();
  id = counter;
}

Второй конструктор называется конструктором копирования и вызывается в случае деления КАмёбы.


Листинг 3: Конструктор 2.


clsAmoeba::clsAmoeba(const clsAmoeba& pt) :
  descendants(NULL),
  dCount(0)
{
  if(&pt != this)
    {
      incr();
      id = counter;
      parent = (clsAmoeba*) &pt;
    }
}

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


Деструктор вызывается при уничтожении экземпляра класса, и участвует в его уничтожении. В данном случае, он уменьшает на 1 количество "живых" КАмёб и уничтожает своих потомков (что, кстати, не совсем логично).


Листинг 4: Деструктор.


clsAmoeba::~clsAmoeba()
{
  decr();
  parent = NULL;
  /* I think here may be a memory leak
   *   because I don't call delete for the
   *   descendents of the every descendent...
   */
  delete [] descendants;
}


Метод divide()


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


Листинг 5: Создаю первый экземпляр КАмёбы (файл main.cpp).


#include <iostream>
#include "main.h"

using namespace std;

int clsAmoeba::counter = 0; // Инициализирую счётчик КАмёб

int main()
{
  clsAmoeba amoeba1;
  
  //...

  return 0;
}

Вокруг - крайне недружелюбная атмосфера. Однако, других живых созданий крупнее нашей КАмёбы мы здесь вряд ли найдём. Поэтому хваталки и кусалки для нашего экземпляра не особенно и нужны (возможно, его потомки обзаведутся этими "фичами"). Что действительно важно, так это возможность создавать себе подобных. В случае с КАмёбой этот процесс достаточно прост - он сводится к делению оригинала надвое. Для этого предназначен метод divide().


Листинг 6: Метод divide() (файл amoeba.cpp)


void clsAmoeba::divide()
{
  if(!dCount)
    {
      descendants = new clsAmoeba*;
      descendants[dCount++] = new clsAmoeba(*this);
    }
  else
    {
      clsAmoeba** tmp = new clsAmoeba* [dCount];
      for(int i = 0; i < dCount; i++)
        tmp[i] = descendants[i];
      
      delete [] descendants;

      dCount++;
      descendants = new clsAmoeba* [dCount];
      for(int i = 0; i < dCount; i++)
        descendants[i] = tmp[i];      
      descendants[dCount-1] = new clsAmoeba(*this);

      delete [] tmp;
    }
}

Если КАмёба делится первый раз, то потомков 0. Следовательно, выделяю память под первого потомка и создаю его. Если потомков на момент вызова метода больше 0, то создаю временное хранилище tmp под существующих потомков, освобождаю память, выделенную под массив descendants. Увеличиваю количество потомков на 1, снова выделяю память под массив descendants, но уже для нового количества потомков. Далее копирую существующих потомков из tmp в descendants. В завершении, создаю нового потомка в конце массива и освобождаю память, выделенную под временный массив tmp.



Разделять и властвовать


Для тестирования программы я создал 24 КАмёбы в main.cpp путём деления amoeba1 в цикле, а потом - её потомков, и потомков потомков...

Листинг 7: Делим, делим, делим...


// ...
  /* I'm making heap of amoebas */
  for(int i = 0; i < 10; i++)
    amoeba1.divide();

  clsAmoeba& amoebaN = amoeba1.getDescendant(2);  
  for(int i = 0; i < 5; i++)
    amoebaN.divide();

  clsAmoeba& amoebaN2 = amoebaN.getDescendant(4);
  for(int i = 0; i < 4; i++)
    amoebaN2.divide();

  clsAmoeba& amoebaN3 = amoebaN2.getDescendant(2);
  for(int i = 0; i < 4; i++)
    amoebaN3.divide();
//...

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

вторник, 15 марта 2011 г.

Рисуем новогоднюю ёлку с помощью Си: алгоритм номер 2.

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

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

Постановка задачи


Последний (и самый, на мой взгляд, интересный) способ отрисовки ели заключается в следующем. Я рисую в строке количество звёздочек, равное номеру строки. Для того, чтобы ель сохраняла некоторую симметричность, я сдвигаю каждую чётную строку вправо на 1 символ.

В итоге, должно получиться следующее:


01      X
02      **
03     ***
04     ****
05    *****
06    ******
07   *******
08   ********
09  *********
10  **********
12      ##
13      ##

Реализация


Для начала, зададим цвет для звезды на верхушке ели, цвет для самой ели и цвет для ствола дерева. Несмотря на то, что программа будет работать в консоли, это не проблема - можно использовать управляющие символы. О них можно прочитать, например, здесь:
http://www.linuxjournal.com/article/8603

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

#include <stdio.h>

#define N 15

int main() {
  char c_red[N]    = "\033[49;31;5m"; // Красный
  char c_green[N]  = "\033[49;32;5m"; // Зелёный
  char c_yellow[N] = "\033[49;33;5m"; // Жёлтый
  char c_reset[N]  = "\033[0;0;0m";   // Возврат к первоначальным
                                      //   значениям
  // ...
}

Теперь я могу использовать более осмысленные названия цветов в вызове функции printf(), например:


printf("%sHello %sWorld%s\n", c_yellow, c_green, c_reset); 

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


  int base_width, half_base_width;
  int i, j;

  printf("Please enter height of the fir-tree:\n> ");
  scanf("%d", &base_width);

  half_base_width = base_width / 2;

  for(i = 1; i <= base_width; i++) {
    /* Печатаю номер строки */
    printf("%s%02d\t", c_red, i);

    /* Печатаю одну линию */
    for(j = 1; j < (half_base_width + (i / 2) + 1); j++) {
      if(((i % 2) >  0) && (j <  (half_base_width - (i / 2))) ||
         ((i % 2) == 0) && (j <= (half_base_width - (i / 2))))
        putchar(' ');
      else
        if(i == 1)
          printf("%sX", c_red);  // Большая звезда на верхушке
        else
          printf("%s*", c_green); // Ель
    }
    
    putchar('\n');
  }

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


  for(i = base_width + 1; 
      i < (base_width + base_width / 10 * 3); i++) {
    printf("%s%02d\t", c_red, i);
    for(j = 0; j <= half_base_width + (base_width / 10) / 2; j++)
      if(j < half_base_width - (base_width / 10))
        putchar(' ');
      else
        printf("%s#", c_yellow);
    putchar('\n');
  }

Значение (base_width + base_width / 10 * 3) используется для задания высоты ствола в зависимости от высоты ели. Значение half_base_width + (base_width / 10) / 2 и half_base_width - (base_width / 10) используется для задания ширины ствола.

В итоге, ель у меня выглядит вот так:

суббота, 12 февраля 2011 г.

Рисуем новогоднюю ёлку с помощью Си: алгоритм номер 1.

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

Теперь пришло время нарисовать первое подобие ёлки.

Постановка задачи


Должно получиться следующее:


   _
  |      X <-------- звезда
  |      *
  |     ***
  |     ***
 10    *****
  |    ***** <------ ёлка
  |   ******* 
  |   *******
  |  *********
  |_ *********
    |----9----|

       рис.1

Всё очень просто: нужно взять за основу алгоритм отрисовки треугольника и удвоить каждую строку: 1, 1, 3, 3, 5, 5...

Высота дерева задаётся пользователем.

Дабы повысить соответствие оригиналу, я хочу поместить на верхушку ёлки "звезду". Ну, вроде тех массивных стеклянных/пластмассовых штуковин, которые часто надевают на верхушку ёлки для усиления праздничного эффекта. За звезду сойдёт английская буква "X" (см. рис.1)

Реализация


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


for(i = 0; i < height; i++) {
  // ...
}

С ним не будет особых проблем. А вот над вложенным циклом нужно подумать. Каким образом реализовать удвоение каждой строки? Посмотрим, как будет расти количество звёздочек в строке, в зависимости от номера строки. Как видно из куска кода выше, переменная i изменяется в цикле от 0 до height - 1 (о чём говорит знак "меньше").


  i | кол. звёздочек в строке
  --|------------------------
  0 | 1        X
  1 | 1        *
  2 | 3       ***
  3 | 3       ***
  4 | 5      *****
  5 | 5      *****
  6 | 7     *******
  7 | 7     *******
  8 | 9    *********
  9 | 9    *********
  ...
        рис.2

Из рисунка выше видно, что количество звёздочек увеличивается на 2 на каждой чётной строке. Это даёт подсказку, как можно реализовать алгоритм.

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


for(i = 1; i <= h; i++)
  for(j = 1; j <= (h + i); j++) {
    if(j <= (h - i + 1))
      putchar(' ');
    else
      putchar('*');
    putchar('\n');
  }

Начальное значение счётчиков i, j равно 1, чтобы обеспечить отступ в один дополнительный символ перед каждой строкой.

Нарисуем 3 строку треугольника (i = 3). Пусть высота h равна 5.


h + i = 8;
h - i + 1 = 3;

 |<------------- 8 ------------->|
  _______________________________
 |   |   |   | * | * | * | * | * |
  -------------------------------
             |<----- j > 3 ----->|

             рис.3

Я думаю, из рисунка 3 можно понять работу этого алгоритма.

В алгоритме отрисовки ёлки половину её ширины нужно вычислять отдельно - ведь теперь эта величина не равна высоте, как в случае с отрисовкой треугольника. Однако, не всё так уж и плохо - есть простая формула, о которой я уже говорил в предыдущем посте.

hw = h / 2 - 1          (1)


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

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

Я просто разделю i на 2 во всех проверках условий во вложенном цикле. Вот так:

// ...
int hw;
// ...
hw = height / 2 - 1;

for(i = 0; i < height; i++)
  /* Печатаю номер строки в обратном порядке (снизу вверх) */
  printf("%02d\t", height-i);

  for(j = 0; j <= (hw + i / 2); j++) { // <-- здесь
    if(j < (hw - i / 2)) // <-- и здесь
      putchar(' ');
    else
      if(i == 0) // или можно написать: if(!i)
        /* Рисую большую звезду на верхушке ёлки */
        putchar('X');
      else
        putchar('*');

    putchar('\n');
  }
// ...

Если строка нечётная, то hw будет увеличиваться в проверке условия завершения цикла for() и уменьшаться в условии if() - на остаток от деления, т.е. на 1. В результате, количество звёздочек в нечётной строке увеличиться на 2. Если же номер строки - чётный, то никаких изменений не происходит, просто копируется предыдущая строка.

Мне кажется, это потрясающе просто и понятно.

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

Рисуем новогоднюю ёлку с помощью Си: алгоритм номер 0.

Некоторые даже в предпраздничные дни забивают себе голову алгоритмами, программированием и прочими непраздничными вещами.

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

С чего всё началось


А началось всё со следующей задачи.

По условию задачи пользователь вводит число строк, а программа должна нарисовать равнобедренный треугольник с помощью звёздочек - "*". Треугольник должен выглядеть так:


    *
   ***
  *****

Для примера, положим, что я ввёл высоту, равную 5 строкам.


 _
|      *
|     ***
5    *****
|   *******
|_ *********
  |----9----|

Тогда, как видно на примере выше, ширина основания треугольника равна 9 звёздочкам. Если я возьму другую высоту - например, 10 строк - то получу треугольник шириной 19 звёздочек. Отсюда выводим формулу:

w = h / 2 - 1          (1)


где w - ширина, h - высота.

Рисую равнобедренный треугольник


Чтобы нарисовать ровный треугольник, мне нужно помещать определённое количество пробелов перед каждой строкой, в зависимости от номера строки.


 -Что есть-  -Что должно получиться-
1 *              1     *
2 ***            2    ***
3 *****      =>  3   *****
4 *******        4  *******
5 *********      5 *********
 \             /
  номер строки 


Чтобы это осуществить, надо знать номер центрального "столбца".


строки
  1     *
  2    ***
  3   *****
  4  *******
  5 *********
    123456789  <-- столбцы
        ^
   центральный
     столбец

Очевидно, в треугольнике высотой 5 строк, центральным будет 5-й столбец. И тут оказывается, что для нахождения центрального столбца нам нужно знать только высоту h треугольника, введённую пользователем - ведь, если верить формуле 1, то номер центрального столбца всегда будет совпадать с высотой. Это позволяет упростить программу.

Вот, кстати, код на Си:


#include <stdio.h>

int main() {
  int height;
  int i, j;

  printf("Please enter height:\n> ");
  scanf("%d", &height);

  for(i = 1; i <= height; i++) {
    for(j = 1; j <= (height + i); j++) {
      if(j <= (height - i + 1))
        putchar(' '); // Печатаем пробел
      else
        putchar('*'); // Печатаем звёздочку
    }
    putchar('\n'); // Переходим на новую строку
  }

  return 0;    
}

Этот код, несмотря на его простоту, нуждается в дополнительных комментариях для понимания.

Первый цикл работает, пока i меньше или равно половины ширины треугольника. А поскольку, как уже было сказано выше, высота треугольника совпадает с номером центрального столбца, т.е. его центра - то я могу использовать значение введённой пользователем высоты height. Что я и делаю.

Второй цикл работает, пока j меньше или равно height плюс значение i (т.е. номер текущей строки).

Далее я проверяю, если j меньше или равно height минус i плюс 1. Единица здесь задаёт смещение всего треугольника от левой границы экрана. Если условие выполняется, то я печатаю пробел, иначе - звёздочку.

Эта программа решает поставленную задачу - рисует треугольник из звёздочек высотой N строк. Но...

Что не так с этим алгоритмом?..


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

Как бы нарисовать что-то, похожее на ёлку - да ещё так, чтобы эта ёлка высотой N, будучи вписанной в квадрат размером NxN, в основании была так же равной N?

Я подошёл к решению этой проблемы со всей серьёзностью, и нарисовал следующее:


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

Алгоритм отрисовки треугольника я не случайно назвал "алгоритмом номер 0" - следующие два алгоритма отрисовки ёлки реализуются на основе идей, реализованных в программе, решающей задачу с треугольником.

fig. 2 показывает, как можно нарисовать нечто похожее на ёлку - путём удвоения каждой строки. Но ширина основания по-прежнему меньше высоты.

fig. 3 показывает алгоритм отрисовки ёлки. Это как раз то, что нужно. Как видно из рисунка, ширина ёлки при таком способе заполнения строк равна высоте. Причём, здесь можно заметить интересную особенность данного алгоритма - она показана на рисунке справа от fig. 3.

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

Реализацию второго алгоритма я рассмотрю в следующем посте.

четверг, 20 января 2011 г.

Нахождение последовательностей одинаковых символов в строке с помощью указателей

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


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


Отлично, теперь пора перейти к реализации.


Считываю строку в массив


Мне понадобится символьный массив размером N, массив указателей на тип char (тоже размером N), переменная для хранения количества последовательностей, счётчик для цикла вывода последовательностей и... всё.



#include <stdio.h>

#define N 256

/* Прототип будущей функции */
int findSequences(const char *arr, char **arrPtr);

int main() {
  char charray[N];
  char *sequence[N];
  int count = 0;
  int i;

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

  count = findSequences(charray, sequence);

  // ...

  return 0;
}

/* Полное описание функции */
int findSequences(const char *arr, char **arrPtr) {
  // невидимый текст ;)
}

Кое-что о функциях и их прототипах


Прототип функции не является обязательным элементом программы. Он необходим, если нужно обратиться к функции до её полного объявления, и для проверки передаваемых функции параметров. Например, я мог бы написать полное объявление функции findSequences() до функции main() - в таком случае, мне не потребовался бы её прототип. Но у данного способа объявления функций есть два недостатка:

  • Во-первых, если функций много, то главная функция main(), с которой начинается выполнеие любой программы на Си, оказывается где-то далеко в конце файла исходника. Читать такой код стороннему человеку, да и самому тоже, неудобно.
  • Во-вторых,иногда нужно сделать так, чтобы функция f1 вызывала функцию f2, а дать полное описание функции f2 до её использования в функции f1 не представляется возможным. А если из функции f1 нужно вызвать f2, а из f2 - f1 (т.е. необходима косвенная рекурсия)? Тогда без прототипов никак не обойтись.


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


Разумеется, никто не требует давать полное описание всех функций до main(), или же после - и использовать прототипы. Можно комбинировать эти два метода, в зависимости от удобства и задачи.


Всё это, конечно, хорошо, но... что насчёт функции findSequences()?


В поисках последовательностей...


Посмотрим ещё раз на прототип функции findSequences():



int findSequences(const char *arr, char **arrPtr);


Ясно, что она возвращает значение типа integer. А в качестве параметров принимает ссылку на начало строки (т.е. на начало символьного массива charray, в котором хранится строка), и указатель на массив указателей. Причём, ключевое слово const перед типом параметра говорит, что этот параметр ни за что, ни при каких условиях не должен (и не будет) изменяться. Что мне и нужно - я хочу, чтобы функция только искала и считала последовательности в строке, а не портила её.


Пришло время заглянуть внутрь функции.



int findSequences(const char *arr, char **arrPtr) {
  char *p = arr;
  int count = 0;

  arrPtr[0] = p;
  while(*p && ('\n' != *p))
    if(*p != *(p+1)) 
      arrPtr[++count] = ++p;
    else
      p++;

  return count;
}

В начале, я создаю указатель на тип char и присваиваю ему адрес первого элемента массива arr. Соответственно, первый элемент массива указателей arrPtr должен содержать ссылку на первый элемент arr (т.к. начало массива - это начало последовательности).


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


Цикл while() работает, пока символ по адресу p не равен 0 (т.е. не достигнут конец строки) и не равен '\n' (т.е. символу перевода строки). Звёздочка '*' перед указателем p говорит о том, что я работаю не самим указателем, а с данными, на которые он указывает. В цикле я делаю проверку на определённое событие - если символ *p не равен символу *(p+1), т.е. символу в следующей ячейке массива, то я увеличиваю счётчик найденных последовательностей на 1, передвигаю указатель вправо, чтобы он указывал на следующую ячейку массива, и сохраняю его адрес в массиве указателей. Поскольку знак инкремента '++' стоит до переменной, которую он увеличивает - то действие инкремента, т.е. увеличения значения переменной на единицу, происходит до того, как её значение будет использовано в выражении.


По завершению цикла, я возвращаю значение count, т.е. количество найденных последовательностей в строке.


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

понедельник, 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;
...