Показаны сообщения с ярлыком src. Показать все сообщения
Показаны сообщения с ярлыком src. Показать все сообщения

суббота, 24 сентября 2011 г.

Как работает std::unique

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

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

[1 1 1 1 1 1 2 2 3 4 4 4 4 4 2 3 5 6 7 7 7 7 8 1] -> [1 2 3 4 2 3 5 6 7 8 1]

Самый главный плюс его в том, что на эту спорную деятельность он не отнимет много вашего времени - один проход по массиву, или O(n).

Казалось бы, проверить соседние элементы и, если они равны, удалить правый и пойти дальше, закончив работу ровно за один проход. Однако, хоть проход и действительно будет один, компьютер по завершении работы будет смотреть вам в глаза виновато: ему потребуется гораздо больше времени, чем O(n), потому что исходная последовательность может располагаться в непрерывной области памяти, из которой нельзя удалять, и единственный выход - сдвигать элементы... И вообще, хватит оправдываться - удалять нельзя! Вот такие новые условия задачи: элементы удалять нельзя, только менять их значения.

А как насчёт того, чтобы рассматривать отрезки с одинаковыми значениями один за другим? Вот так:
[1 1 1 1 1 1 | 2 2 | 3 | 4 4 4 4 4 | 2 | 3 | 5 | 6 | 7 7 7 7 | 8 | 1]

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

Вот, скажем, определились границы первого отрезка 1 1 1 1 1 1 | 2. По условию задачи ото всех элементов начиная со второй единицы нужно избавиться, поэтому сразу же копируем двойку на место второй единицы: 1 2 1 1 1 1 | 2. Но что делать с оставшимися единицами, которые всё путают?
А сделаем вот так: 1 | 2 2 2 2 2 2 ... - теперь всё выглядит намного лучше: есть канонический (из одного элемента) отрезок с единицей, есть отрезок из двоек, конец которого ещё предстоит найти, и больше ничего лишнего. Только откуда все эти двойки? Там же вверху было чё-то про одно копирование максимум... А двоек и нет: мы их видим, потому что [нас только что отключили от Матрицы] мы так хотим: ведь правда, что происходит с теми оставшимися единицами, нам всё равно настолько, что мы спокойно можем назвать их двойками и с облегчением идти дальше. Неприятный осадок, который остаётся в таком случае, - это что такой подход работает, пока мы не настигнем конца последовательности: за последним исправленным отрезком будет твориться хаос из дублей и мешанины, которой от нашего алгоритма пользователь совсем не ожидал. Хорошая новость: unique возвращает позицию нового конца последовательности, начиная с которой дальше будет твориться хаос, и это нормально.

Итак, алгоритм ещё раз:
1. заводим два маркера: на начало отрезка равных элементов - equal_begin - и на позицию за концом - equal_end (изначально это первые два элемента последовательности), и отдельно переменную длины позиции - equal_distance;
2. пока не коснёмся equal_end конца последовательности, повторяем шаг 3
3. проверяем позицию за концом: если её элемент равен начальному, то это ещё не за концом - сдвигаем equal_end, инкрементируем equal_distance; если же не равен, смотрим, канонический ли это отрезок (равен ли equal_distance единице), и в зависимости от этого либо просто сдвигаем оба маркера, либо сдвигаем только equal_begin, копируем туда значение по equal_end и двигаем его. В обоих случаях длина текущего отрезка не меняется.
4. возвращаем позицию за equal_begin

Важное замечание: входная последовательность подаётся по правилам STL: {начальный элемент, элемент за концом}.

И код на C++ для лабораторок:


template<typename It>
It uniq(It begin, It end)
{
    It range_end(begin);
    ++range_end;
    size_t equal_distance(1);
    while (range_end != end)
    {
        // current segment still going
        if (*range_end == *begin)
        {
            ++range_end;
            ++equal_distance;
        }
        // the next segment begins
        else
        {
            if (equal_distance > 1)
            {
                *(++begin) = *(range_end++);              
            }
            else
            {
                ++begin;
                ++range_end;
            }
        }
    }
    return ++begin;
}


воскресенье, 31 января 2010 г.

Ссылки и указатели C++ как входные параметры

Есть ещё одна маленькая деталь работы со ссылками и указателями, о которой стоит рассказать. Она касается передачи их как параметров в функцию. Вспомним сначала, зачем ссылки и указатели вообще передавать в функцию:

void do_some_calc(int arg)
{
    int count = arg;
    int prev = 1, prev_prev =0;
    for(int i = 0; i < count; i++)
    {
        if(i < 1)
        {
            prev_prev = prev;
            prev = arg;
        }
        arg = prev + prev_prev;
    }
}

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

Допустим, мы создали переменную int number = 3 и отправили её в процедуру do_some_calc(number). Но по выходу из процедуры переменная number не сможет подтвердить, что мы действительно что-то вычисляли, потому что значение её не изменится.

А всё потому, что передавалась не сама переменная, а её копия. И вычисленное значение, сохранённое в копии переменной number с именем arg, по выходу из процедуры просто удалилось вместе со всем, что использовалось в её теле.

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

Так вот, теперь к главному. Допустим, мы приняли такой слегка странный стиль программирования и предпочли процедуру функции в следующей задаче: написать процедуру выделения памяти для указателя

void allocate_memory_for_ptr(int* ptr)
{
    ptr = new int[1000];
}


Да, передаётся указатель int* tricky_pointer. Да, выделяется память для него allocate_memory_for_ptr(tricky_pointer). Но по выходу из процедуры доступ к tricky_pointer[0]..tricky_pointer[whatever] получить не удастся. Более того, в процедуре только что нашими руками была создана утечка памяти.
Как ни странно, а в allocate_memory_for_ptr(...) была передана копия указателя tricky_pointer, которой повезло так же, как и переменной arg из прошлого примера. Можно заключить, что всё (абсолютно всё, включая указатели) передаваемое параметром в функцию, просто копируется. И этого эффекта будет незаметно, если в процедуре меняется только содержимое указателя или не меняется вообще ничего, как в примере с суммой элементов массива (для этого, в общем-то, придумана константность, но о ней как-нибудь попозже). Когда же меняется сам адрес, лучше поступить вот так:

void allocate_memory_for_ptr(int*& ptr)
{
    ptr = new int[1000];
}

А если говорить непосредственно о ссылках, то вот самая типичная процедура:

void change_ref(int& ref)
{
    int p = 5;
    ref = p;
}

При таком её использовании:

int i = 0;
int& r = i;
change_ref(r);

с самой ссылкой ничего не станет, изменится лишь значение i. Ссылки предоставляют меньше свободы в работе с адресом, и вся процедура change_ref(...) по сути может быть  расписана как получение указателя на i в качестве параметра (по ссылке, по значению - без разницы, мы ведь меняем лишь значение) и изменения содержимого указателя. А вот поиграться непосредственно с адресом ссылки, как мы это проделывали в примере allocate_memory_for_ptr(...) не получится из-за ограничений на операции со ссылками.

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

четверг, 14 января 2010 г.

Ссылки и указатели C++. Арифметика

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

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

int sum(int* inLeft, int* inRight)
{
    if(inLeft == inRight)
        return *inLeft;
    else
        return *inRight + sum(inLeft, --inRight);
}

Для девятиэлементного массива mas вызов функции будет иметь такой вид:
int massum = sum(&mas[0], &mas[8]);


А теперь с конца: если у имеется некая переменная, получить её адрес можно применением к ней оператора взятия адреса &.
Именно результатом взятия адреса и может быть инициализирован указатель (инициализировать его значением переменной не получится, потому что это означало бы, что мы хотим получить доступ к произвольной области памяти, что есть unsafe. Формально компилятором это выражается в невозможности привести тип A к типу "указатель на А"). А дальше всё довольно прямолинейно: если есть желание работать с адресом, предоставляемым указателем, можно использовать его (указатель) безо всяких дополнительных символов (причём, разумеется, применение оператора взятия адреса к указателю будет расцениваться как адрес его самого и интерпретироваться компилятором как "указатель на указатель на А"); если же необходимо получить доступ к значению, на которое он всеми силами указывает, нужно применить оператор доступа по значению ("звёздочка"; если говорить правильно, разыменовывание). При этом к "звёздочке" можно применять арифметические операторы и совершать изменение значения самой переменной. Для  оператора взятия адреса такой возможности нет. Сущность, которая подвергается взятию адреса, должна быть rvalue, то есть стоять справа от знака "равно" (такой способ, как
int** new_pointer = ++&old_pointer дело тоже не спасает).

Что ж, в теме поста с легкой руки были затронуты и ссылки. Поэтому теперь можно придумать что-нибудь и про них.
Ссылка - тип данных, который появился уже в языке C++ и который выполняет те же функции, что и указатель, но в этаком "локальном" контексте.
Ссылка также содержит адрес объекта и занимает те же самые 4 байта (только убедиться в этом чуть сложнее:) ). только разыменовывание у неё выведено на передний план, и взятие значения теперь осуществляется без дополнительных звёздочек и амперсандов, а адрес получается только соответствующим оператором &.
Ещё одна немаловажная особенность ссылки в том, что она не может быть неициализирована. Если оставить в коде строку int* i; , то компилятор максимум, что из себя выжмет, так это предупреждение. Если же подобным образом поступить со ссылкой: int& i;, то тут у него появится шанс схватить программиста за рукав. Такая особенность языка C++: ссылки и соответствующие им переменные - это одно целое.

Теперь настала пора переписать уже набивший оскомину код вычисления суммы массива, только уже в более модном стиле, на ссылках:

int sum(int& inLeft, int& inRight)
{
    if(&inLeft == &inRight)
        return inLeft;
    else
    {
        int* right_temp = &inRight;
        right_temp--;
        int& new_right = *right_temp;
        return inRight + sum(inLeft, new_right);
    }
}
Сразу стало заметно, насколько больше "мусора" в этой реализации. А всё потому, что ссылки не поддерживают арифметические операции. Ссылка прикрепляется к переменной "намертво", без возможности "перепрыгивания" на соседний или совсем отдалённый объект. И именно поэтому для процедуры обновления адресов пришлось прибегать к указателям.

Однако, наверное, основное назначение ссылки - передача параметров в функцию. В C++ дело вот в чём: существует 2 типа передачи, по значению и по адресу. В передаче параметра по значению (выглядит примерно так: int func(int par)) сама функция получает не что иное, как копии передаваемых параметров. В большинстве случаев именно это и требуется, однако для объектов, хранящих своё состояние (тут прямо почувствовалось, как разговор перешёл в тему объектно-ориентированного программирования), типа матриц, копия должна быть обязательно сконструирована, а это бывает (абсолютно всегда) затруднительно. При передаче же по адресу (выглядит так: int func(int& par) либо так: int func(int* par)) передаётся не сама сущность, а её адрес. И ссылка, безусловно, облегчает работу с такими параметрами: в отличие от указателя, разыменовывается она автоматически.

Подводя итог, можно скопировать всё написанное в окошко редактирования ещё раз (ведь так он всегда подводится) и описать всё то, что очень сподручно знать про саму суть указателей и ссылкок:

указатель /* */ ссылка
Объявление
int* pointer /* */ int& ref
без инициализации /* */ только с инициализацией
Взятие адреса
pointer /* */ &ref

Разыменовывание
*pointer /* */ ref

Арифметические операции с адресом
(--pointer++) += 3; // сложение и вычитание /* */ не предусмотрено

Инициализация
pointer1 = pointer2; /* */ ref1 = ref2;
pointer = &ref; /* */ ref = *pointer;
pointer = &val;/* */ ref1 = val;

P.S. Как всегда, нашлись ещё ценные источники. К примеру, тонкости оператора & как взятия адреса и части объявления ссылки описаны здесь: http://novikovmaxim.livejournal.com/120178.html