Как эффективно удалить элементы из массива

Превью к эффективному удаления значений из массива

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

Как удалить одно значение из массива?

Начнём с более простого алгоритма — удаления одного элемента по индексу. Его отлично знают все начинающие разработчики: все элементы правее заданного индекса сдвинуть влево на 1 и уменьшить размер массива на единицу:

// удаление из массива array элемента по индексу index
void RemoveValueAt(int *array, int &n, int index) {
    n--; // уменьшаем размер массива

    // сдвигаем элементы правее index влево
    for (int i = index; i < n; i++) {
        array[i] = array[i + 1];
    }
}

Этот алгоритм весьма простой и эффективный. К нему никаких претензий.

Наивный алгоритм удаления значений из массива

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

// удаление из массива array элементов равных value
void RemoveValues(int *array, int &n, int value) {
    for (int i = n - 1; i >= 0; i--) {
        if (array[i] == value) { // если нужно удалить элемент
            RemoveAt(array, n, i); // удаляем его по текущему индексу
        }
    }
}

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

// удаление из массива array элементов равных value
void RemoveValues(int *array, int &n, int value) {
    for (int i = n - 1; i >= 0; i--) {
        if (array[i] == value) { // если нужно удалить элемент
            n--; // уменьшаем размер массива

            // сдвигаем элементы правее i влево
            for (int j = i; j < n; j++) {
                array[j] = array[j + 1];
            }
        }
    }
}

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

Эффективный алгоритм удаления элементов из массива

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

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

// удаление из массива array элементов равных value
void RemoveValues(int *array, int &n, int value) {
    int j = 0; // новое положение в массиве

    for (int i = 0; i < n; i++) {
        if (array[i] != value) { // если не нужно удалять элемент
            array[j++] = array[i]; // помещаем элемент в новое место
        }
    }

    n = j; // обновляем размер массива
}

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

Пример работы данного алгоритма:

Пусть требуется удалить из массива [1, 2, 3, 1, 5, 1, 7] значения, равные 1. Рассмотрим состояние массива на каждой из итераций:

i = 0: j = 0, array = [(1), 2, 3, 1, 5, 1 7], 1 == 1 // пропускаем
i = 1: j = 0, array = [1, (2), 3, 1, 5, 1 7], 2 != 1 // выполняем array[0] = array[1] => j = 1, array = [2, 2, 3, 1, 5, 1, 7]
i = 2: j = 1, array = [2, 2, (3), 1, 5, 1 7], 3 != 1 // выполняем array[1] = array[2] => j = 2, array = [2, 3, 3, 1, 5, 1, 7]
i = 3: j = 2, array = [2, 3, 3, (1), 5, 1 7], 1 == 1 // пропускаем
i = 4: j = 2, array = [2, 3, 3, 1, (5), 1 7], 5 != 1 // выполняем array[2] = array[4] => j = 3, array = [2, 3, 5, 1, 5, 1, 7]
i = 5: j = 3, array = [2, 3, 5, 1, 5, (1) 7], 1 == 1 // пропускаем
i = 6: j = 3, array = [2, 3, 5, 1, 5, 1 (7)], 1 == 1 // выполняем array[3] = array[6] => j = 4, array = [2, 3, 5, 7, 5, 1, 7]

j = 4 // количество элементов массива после удаления

Вместо заключения

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