Удалить символы из строки

У меня есть строка вроде

string Text = "012345678901234567890123456789";

и a List<int> с индексами

List<int> Indexes = new List<int>() { 2, 4, 7, 9, 15, 18, 23, 10, 1, 2, 15, 40 };

со следующими ограничениями

  • в списке есть дубликаты
  • список не отсортирован
  • могут быть индексы > Text.length

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

ожидаемый вывод:

035681234679012456789

Существует ли более эффективный способ, чем

foreach (int index in Indexes
                        .OrderByDescending(x => x)
                        .Distinct()
                        .Where(x => x < Text.Length))
{
    Text = Text.Remove(index, 1);
}

Обновление: Ниже приведены контрольные значения текущих ответов (string с 100 000 символов и List<int> с длиной 10.000:

Gallant: 3.322 ticks
Tim Schmelter: 8.602.576 ticks
Sergei Zinovyev: 9.002 ticks
rbaghbanli: 7.137 ticks
Jirí Tesil Tesarík: 72.580 ticks

Ответ 1

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

char temp = '\uFFF0';
StringBuilder sb = new StringBuilder(Text);
for (int i = 0; i < Indexes.Count; i++)
{
    if (Indexes[i] < sb.Length)
    {
        sb[Indexes[i]] = temp;
    }
}

Text = sb.Replace(temp.ToString(), null).ToString();

Кажется, что это где-то между 3-4 раз быстрее, чем создание HashSet, как предложили некоторые другие ответы. http://ideone.com/mUILHg


Если вы не можете сделать вышеприведенное предположение, вы можете построить массив, содержащий этот лишний бит данных, вместо использования уникального символа. Это делает два раунда итерации (так что это немного медленнее), но это все еще эффективность O (n) (поэтому она обычно должна быть быстрее, чем помещать индексы в хэш-карту перед повторением).

bool[] exclude = new bool[Text.Length];
for (int i = 0; i < Indexes.Count; i++)
{
    if (Indexes[i] < exclude.Length)
    {
        exclude[Indexes[i]] = true;
    }
}
StringBuilder sb = new StringBuilder(Text.Length);
for (int i = 0; i < Text.Length; i++)
{
    if (!exclude[i])
    {
        sb.Append(Text[i]);
    }
}
Text = sb.ToString();

Быстрые контрольные показатели: http://ideone.com/3d2uPH

Ответ 2

Здесь более или менее элегантный способ LINQ:

Text = new string(Text.Where((c, index) => !Indexes.Contains(index)).ToArray());

Он использует перегрузку Enumerable.Where, которая проектирует индекс элемента в последовательности.

Если вам нужен самый эффективный, а не самый читаемый способ, и текст действительно большой, вы можете использовать HashSet<int> вместо списка, который не позволяет дублировать и StringBuilder создавать новую строку:

var indexSet = new HashSet<int>(Indexes); // either create from the list(as shown here) or use it without your list
var textBuilder = new StringBuilder(Text.Length);

for(int i = 0; i < Text.Length; i++)
    if (!indexSet.Contains(i))
        textBuilder.Append(Text[i]);
Text = textBuilder.ToString();

Конечно, вы также можете использовать HashSet<int> в подходе LINQ, чтобы сделать его более эффективным.

Ответ 3

Это будет работать быстрее:

string Text = "012345678901234567890123456789";
List<int> Indexes = new List<int>() { 2, 4, 7, 9, 15, 18, 23, 10, 1, 2, 15, 40 };

HashSet<int> hashSet = new HashSet<int>(Indexes);

StringBuilder sb = new StringBuilder(Text.Length);
for (int i = 0; i < Text.Length; ++i)
{
    if (!hashSet.Contains(i))
    {
        sb.Append(Text[i]);
    }
}

string str = sb.ToString();

Ответ 4

Да, см. код ниже (он будет повторяться только один раз над каждой последовательностью):

var map = new short[Text.Length];
foreach (var i in Indexes)
{
    if (i < text.Count)
        map[i] = 1;
}
Text = new string(Text.Where((c, i) => map[i] == 0).ToArray());

Ответ 5

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

string Text = "012345678901234567890123456789";
List<int> Indexes = new List<int>() { 2, 4, 7, 9, 15, 18, 23, 10, 1, 2, 15, 40 };
byte[] contains = new byte[Text.Length];
Indexes.ForEach(p=> {if ( p<Text.Length) contains[p]=1;});
var output = string.Concat(Enumerable.Range(0, Text.Length).Where(p => contains[p] != 1).Select(p => Text[p]));