Название: Алгоритмическое мышление при решении задач (Шамшев А. Б.)

Жанр: Информационные системы и технологии

Просмотров: 1407


Задача 5: удаление элементов из массива по значению (1 способ решения).

В этой задаче неявно подразумевается, что у нас есть массив (пусть будет массив  целых  чисел  (пусть  с  именем  data))  и  удаляемое  значение (целочисленное число (пусть с именем delValue)).

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

рассмотрим  в  начале  конкретные  данные.  Пусть  массив  будет  заполнен числами 2,4,2,8,2,2,3,7. И надо удалить элементы, равные 2.

Запишем массив в виде таблицы, в первой строке которой будут написаны

значения элементов, а во второй – индексы элементов (напомним, что для того, что            бы     обратиться     к     элементу     массива,     необходимо     написать имяМассива[индекс]).

Массив data до удаления:

2  4  2  8  2  2  3  7

0  1  2  3  4  5  6  7

Для сокращения записи в дальнейшем будем писать массив в следующем виде: элементМассива (индексЭлемента), т. е. {2(0), 4(1), 2(2), 8(3), 2(4), 2(5),

3(6), 7(7)}.

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

 

Для каждого элемента массива

Начало действия

Если текущий элемент равен удаляемому, то

НачалоЕсли

Удалить текущий элемент

ОкончаниеЕсли

Окончание действия.

 

С учетом фразы «Для каждого элемента массива» в решении появится цикл for (int i = 0; i < data.Length; i++) {…}. Так же учтем, что алгоритм удаления элемента по индексу нам уже известен. Поэтому уточним алгоритм на псевдокоде

 

Для каждого индекса от 0 до размер массива - 1

Начало действия

Если элемент с текущим индексом равен удаляемому, то

НачалоЕсли

Удалить элемент по текущему индексу.

ОкончаниеЕсли

Окончание действия.

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

public static void delByIndex(ref int[] data, int delIndex) {

int[] newData = new int[data.Length - 1];

for (int i = 0; i < delIndex; i++) {

newData[i] = data[i];

}

for (int i = delIndex; i < newData.Length; i++) {

newData[i] = data[i + 1];

}

data = newData;

}

 

public static void delByValue(ref int[] data, int delValue) {

for (int i = 0; i < data.Length; i++) {

if (data[i] == delValue) {

delByIndex(ref data, i);

}

}

}

Рассмотрим, как будет работать алгоритм на массиве {2(0), 4(1), 2(2), 8(3),

2(4), 2(5), 3(6), 7(7)}.

Первые  команды,  которые  будут  выполнены  в  процедуре  delByValue,

следующие:

int i = 0;

if (data[i] == delValue) {

delByIndex(ref data, i);

}

i++;

if (data[i] == delValue) {

Запишем их в виде, более удобном для человека

int i = 0;

if (data[0] == delValue) {

delByIndex(ref data, 0);

}

i = i + 1;

if (data[1] == delValue) {

………………………………

}

Начнем рассматривать, что происходит с данными. В начале i=0 и мы

просматриваем нулевой элемент массива {2(0), 4(1), 2(2), 8(3), 2(4), 2(5), 3(6),

7(7)}. Его необходимо удалить (т. к. он равен удаляемому значению). После удаления массив  будет  {4(0),  2(1),  8(2),  2(3),  2(4),  3(5),  7(6)}.  После  этого

выполняется i=i+1. И мы будем просматривать первый элемент массива, т. е.

{4(0),  2(1),  8(2),  2(3),  2(4),  3(5),  7(6)}.  И  обратите  внимание  –  мы  НЕ

ПРОСМАТРИВАЛИ четверку, которая после удаления стала нулевым элементом массива. А это ошибка.

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

«всплывет» (не будет удален второй элемент). Причина этой ошибки кроется в том, что мы не учли важное следствие удаления по индексу – смещение индексации элементов после удаляемого.

Если  мы  удалили  элемент,  то  все  элементы  после  него  сами  к  нам

«пододвигаются» и поэтому нам не надо переходить на следующий элемент. Однако операция i++ будет выполнена в любом случае (как действие в конце итерации цикла), поэтому ее можно только «компенсировать». Если мы уменьшим i на 1, а потом увеличим на 1, то i не изменится. Поэтому алгоритм на псевдокоде будет выглядеть следующим образом:

 

Для каждого индекса от 0 до размер массива - 1

Начало действия

Если элемент с текущим индексом равен удаляемому, то

НачалоЕсли

Удалить элемент по текущему индексу.

Уменьшить текущий индекс на 1.

ОкончаниеЕсли

Окончание действия.

 

Реализация этого алгоритма будет следующая:

public static void delByValue(ref int[] data, int delValue) {

for (int i = 0; i < data.Length; i++) {

if (data[i] == delValue) { delByIndex(ref data, i); i--;

}

}

}

Следует отметить, что в delByIndex есть уменьшение размера массива, и как следствие условие в условии i < data.Length нельзя заменить data.Length на константу (в данном случае 8).

Однако  существует  другой,  более  оптимальный  метод  решения  этой задачи. В псевдокоде мы писали «Для каждого элемента массива». Но при этом

не сказано, как обходятся элементы массива – от начала к концу массива или от конца к началу.

Рассмотрим, что будет, если мы будем обходить массив от конца к началу.

Последний элемент в массиве размером data.Length имеет индекс data.Length –

1, поэтому надо начинать с него. Мы будем двигаться к началу массива, т. е. текущий  индекс  будет   уменьшаться  (i--).   Мы   должны  просмотреть  все элементы, включая нулевой, поэтому условие окончания прохода по циклу будет (i >= 0).

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

Поэтому алгоритм будет выглядеть следующим образом:

for (int i = data.Length - 1; i >= 0; i--) {

if (data[i] == delValue) {

delByIndex(ref data, i);

}

}

Очевидно,  что  данный  алгоритм  будет  выполняться  быстрее          первой версии.

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

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

Однако минусом является то, что при каждом удалении создается копия почти всего массива. Т. е. если у нас K удалений, то K раз будет создана копия

N элементов (хотя если быть более точным, то в начале  будет  создана копия

N-1 элементов, потом N-2 элементов и т. д.).