Какова лучшая структура данных C++, которую можно использовать для хранения и управления коллекцией целых чисел?

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

Я получил свой первый вопрос на собеседовании, и меня отвергли из-за моей реализации.

Вопрос в том:

Разработайте и реализуйте класс C++, который хранит коллекцию целых чисел. На стройке коллекция должна быть пустой. Один и тот же номер может храниться более одного раза.

Реализуйте следующие методы:

  1. Вставьте (int x). Вставьте запись для значения "х".

  2. Стереть (int x). Удалите одну запись со значением "x" (если она существует) из коллекции.

  3. Стереть (int от, int до). Удалите все записи со значением в диапазоне [от, до).

  4. Считать (int от, int до). Посчитайте, сколько записей имеют значение в диапазоне [от, до).

Я подумал, что хорошей реализацией будет использование связанных списков, так как он использует несмежную память, и удаление записей не потребует перетасовки большого количества данных (как в векторах или массивах). Тем не менее, я получил отзыв от компании, в котором говорилось, что моя реализация была O (n ^ 2) временной сложности и была очень неэффективной, поэтому я был отклонен. Я не хочу повторять ту же ошибку, если подобный вопрос появляется в другом интервью, поэтому я хотел бы знать, как лучше всего подойти к этому вопросу (друг предложил использовать карты, но он также не уверен).

Мой код:

void IntegerCollector::insert(int x)
{
    entries.push_back(x);
}

void IntegerCollector::erase(int x)
{
    list<int>::iterator position = find(entries.begin(), entries.end(), x);
    if (position != entries.end())
        entries.erase(position);
}

void IntegerCollector::erase(int from, int to)
{
    list<int>::iterator position = entries.begin();

    while (position != entries.end())
    {
        if (*position >= from && *position <= to)
            position = entries.erase(position);
        else
            position++;
    }
}

int IntegerCollector::count(int from, int to)
{
    list<int>::iterator position = entries.begin();
    int count = 0;

    while (position != entries.end())
    {
        if (*position >= from && *position <= to)
            count++;

        position++;
    }

    return count;
}

В отзывах говорилось, что они будут нанимать только кандидатов, способных реализовать решения со сложностью O (nlogn).

Ответы

Ответ 1

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

Затем вы можете просто использовать std::map<int, size_t> качестве вспомогательной структуры, которая отображает каждое целое число (ключ) на количество раз, которое оно существует в вашей структуре данных (value = количество).

Вставка и стирание отдельных элементов - это просто увеличение и уменьшение (возможно, удаление в последнем случае) значений для данного ключа (оба O(log(distinct_values_in_container)) для поиска ключа).

Поскольку std::map упорядочен, вы можете использовать lower_bound и upper_bound для бинарного поиска, поэтому поиск ключей в [from, to) очень эффективен (поиск диапазона также O(log(distinct_values_in_container))). Стирать их или суммировать их счетчики легко (здесь время выполнения более сложное).


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

Что эти асимптотические среды выполнения означают на практике, во многом зависит от модели использования. Если дубликаты никогда не вставляются, мы находимся на O(n), но вы также можете получить произвольно хорошие времена (с точки зрения n= количество вставок), если есть много идентичных элементов (например, если у каждого ключа есть O(exp(n)) затем O(distinct_values_in_container) = O(log(n))). В крайнем случае, когда все задействованные целые числа одинаковы, все операции являются O(1).

Как собеседник, я бы также поговорил о том, значат ли эти асимптотические среды выполнения на практике. Может случиться так, что древовидная структура карты (которая является токсичной для кеша и предиктора ветвлений) проигрывает простому std::vector<std::pair<int, size_t>> (если стирание всегда массово) или даже std::vector<size_t> (если ключи "плотные") для данного приложения.


Я думаю, что ваша основная ошибка (и почему вы были отклонены) заключается в том, что вы не понимаете, что нет необходимости хранить каждое вставленное целое число отдельно. Вы, к сожалению, также, кажется, упустили возможность сохранить список отсортированным, но я не вижу, откуда O(n^2).

Ответ 2

Если бы вас нанимали на роль, которая не требовала какого-либо предыдущего опыта программирования, я бы не отказался от вас только на этом примере кода.

Использование std::list было интересным выбором и показало, что вы думали об этом. Тот факт, что вы использовали стандартный библиотечный контейнер C++ вместо того, чтобы пытаться построить его с более низкого уровня, для меня является признаком да-найма. При вашем подходе (1) быстро, но (2), (3) и (4) будет медленно. При отсутствии какой-либо другой информации вы должны упорядочить вещи так, чтобы чтение (включая запросы) данных выполнялось быстрее, чем запись. Ваш подход имеет это наоборот. Хотя иногда это то, что вам нужно - например, когда вы проводите измерения в реальном времени, вы хотите, чтобы этап сброса данных был максимально быстрым за счет чего-либо еще. Для этого приложения ваше решение будет трудно победить!

Бронирование, но ни в коем случае не красные линии:

Целое число не означает int. Если у вас не будет возможности прояснить ситуацию, постройте свой класс на

template<typename Y> std::map<Y, std::size_t>

где Y является целочисленным типом. Обратите внимание на использование std::size_t для счетчика. Он считает количество раз, когда конкретный Y присутствует.

Включите некоторые комментарии программы в следующий раз.

Не используйте using namespace std; , Хотя учебники делают для ясности, профессиональные программисты этого не делают.

Ответ 3

Вы должны использовать map<int,size_t> int для значения, size_t для count.

Если вам нужно реализовать код, вы должны выбрать сбалансированное двоичное дерево, чтобы получить сложность 'log N'.

Итак, у вас есть следующий узел:

struct Node
{
  int key;
  size_t count;
  Node *left, *right; 
  size_t leftNodesCount, rightNodesCount;
};

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

Теперь вы должны реализовать методы Insert, Delete и Balance.

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

Соучастие баланса также "log N". обратите внимание, что после вставок и удалений вы должны обращаться к балансу таким образом, чтобы сохранить соучастие дерева в "log N"