【问题标题】:Remove elements from one array whose Indexes are present in another aray从一个数组中删除其索引存在于另一个数组中的元素
【发布时间】:2015-01-02 13:38:59
【问题描述】:

一个包含 N 个对象的数组 A1。另一个数组 A2 包含表示第一个数组的索引的数字。您需要从 A1 中删除 A2 中存在索引的元素并生成压缩数组。例如:

A1 = [ a, b, c, d, e, f, g ] // N elements and N is large
A2 = [ 5, 1 ] // k elements and k is small (and constant)
Answer = [ a, c, d, e, g, _, _ ]

我写的 C# 代码如下:

public class CompactingArray
{
    private Compact(array A1 , array A2)
    {
        var hash = new Hashset<int>(A2);
        foreach(int c in hash)
        {
            A1.remove(c,1);
        }

        Console.WriteLine(A1);
    }
}

我需要 O(n) 复杂度代码并且不使用任何内置函数。请建议一个不使用任何内置函数的 C# 代码。

【问题讨论】:

  • 不,在一次采访中被问到,我使用内置函数 remove 给出了答案,但它们不需要内置函数和线性复杂度。
  • 如果我们回答,我们能得到这份工作吗?

标签: c# arrays algorithm


【解决方案1】:

如果kA2 中的元素个数是“小而恒定”,那么一个 O(N*k) 复杂度的普通算法(对于A1 中的每个元素,看看它的索引是否在A2) 将被视为线性:

int writingPosition = 0;
for (int i = 0 ; i != N ; i++) {
    boolean found = false;
    // Since k is constant, this loop is considered constant-time
    for (int j = 0 ; j != k ; j++) {
        if (A2[j] == i) {
            found = true;
            break;
        }
    }
    if (!found) {
        A1[writingPosition++] = A1[i];
    }
}
while (writingPosition != N) {
    A1[writingPosition++] = "_";
}

但是,这不是最佳选择。为了提高性能,您可以对A2 进行排序(对其进行排序是一个恒定时间操作)。对A2 进行排序后,您可以创建一个int current=0,一个指向A2 的索引,然后将A1 数组从零遍历到N,并跳过来自A2[current] 的索引。在循环到N 的每次迭代中,您只需要查看“A2”的一个元素,因此总体上该算法也是线性的。

实现与上述类似,但不是使用嵌套循环并检查if (!found),而是检查A2[current] == i,并相应地调整current

【讨论】:

  • 是的,如果A2 总是被排序,那么复杂度将是O(n),只有一个循环。在阅读代码下的描述之前,请参阅我写的答案;-)
【解决方案2】:

这里是解决方案。

        Char[] A1 = { 'a', 'b', 'c', 'd', 'e', 'f', 'g' };
        int[] A2 = { 5, 1 };

        int k = A2.Length;

        int N = A1.Length;

        for (int i = 0; i < k; i++)
        {
            A1[A2[i]] = '\0'; // place null charcater here
        }

        Char[] copy = new char[N];

        for (int i = 0,j=0; i < N; i++) // place all values in sorted order
        {
            if (A1[i] != '\0')
                copy[j++] = A1[i];
        }
        for (int i = (N-k); i < N;i++ )
        {
            copy[i] = '-';
        }
        Console.WriteLine(copy);

【讨论】:

  • 结果缺少最后两个_
【解决方案3】:

结果数组的顺序重要吗?如果没有,您可以围绕以下几行做一些事情:

char[] a = { 'a', 'b', 'c', 'd', 'e', 'f', 'g' };
int[] z = { 5, 1 };

int zlen = z.Length;
int amax = a.Length - 1;

for (int i = 0; i < zlen; i++)
    a[z[i]] = a[amax - i];

最后,您必须调整结果数组(a 本身)的大小,以从数组末尾删除 zlen 元素。该解决方案只是将要删除的元素冒泡到数组的末尾。无需顺序移除。添加对索引边界等的适当检查。

【讨论】:

  • 这也不起作用。它产生{ 'a', 'g', 'c', 'd', 'e', 'g', 'g' }
  • @t3chb0t:很明显,您没有阅读围绕您“测试”的代码的文本。
【解决方案4】:

如果A2 已排序,则O(n) 的复杂性是可能的,因为这样你就可以只用一个循环来单独索引每个数组:

var A1 = new string[] { "a", "b", "c", "d", "e", "f", "g" }; // N elements and N is large

var A2 = new int[] { 1, 5 }; // k elements and k is small (and constant)

A2 = A2.OrderBy(x => x).ToArray();

var A3 = new string[A1.Length];

int m = 0; // To check it runs only n times.
int leftItemCount = A1.Length - A2.Length;
for (int i = 0, j = 0, k = 0, l = leftItemCount; i < leftItemCount || m < items.Length; i++)
{
    m++;
    if (j < A2.Length && k == A2[j])
    {
        j++;
        k++;
        A3[l++] = "_";
        i--;
        continue;
    }
    A3[i] = A1[k];
    k++;
}

// Answer = [ a, c, d, e, g, _, _ ] for { 1, 5 }
// Answer = [ a, d, e, g, _, _, _ ] for { 1, 2, 5 }

测试代码:

static void ArrayTests()
{
    // Item array lengths.
    for (int i = 45; i < 256; i++)
    {
        var items = Enumerable.Range(0, i).Select(x => x.ToString()).ToArray();

        // Number of tests per array.
        for (int j = 0; j < 100; j++)
        {
            // Items to remove.
            Random rnd = new Random(DateTime.Now.Millisecond);
            var remove = new int[rnd.Next(1, i)];
            HashSet<int> indexes = new HashSet<int>();
            for (int k = 0; k < remove.Length; k++)
            {
                int index = 0;
                do
                {
                    index = rnd.Next(0, i);
                } while (indexes.Contains(index));
                indexes.Add(index);
                remove[k] = index;
            }
            remove = remove.OrderBy(x => x).ToArray();
            var result = ArrayTest(items, remove);
        }
    }
}

static string[] ArrayTest(string[] items, int[] remove)
{
    var A3 = new string[items.Length];

    int m = 0;
    int leftItemCount = items.Length - remove.Length;
    for (int i = 0, j = 0, k = 0, l = leftItemCount; i < leftItemCount || m < items.Length; i++)
    {
        m++;
        if (j < remove.Length && k == remove[j])
        {
            j++;
            k++;
            A3[l++] = "_";
            i--;
            continue;
        }
        A3[i] = items[k];
        k++;
    }            
    Debug.Assert(m == items.Length);
    return A3;
}

【讨论】:

  • k, 不是常数,k 可以是例如 = n 或更糟 k=n^2 甚至更糟 (k=2^n) 毫不费力,所以正式,你可以' t 忽略它作为一个常数。因此,对具有 k 项的数组进行排序不是恒定的,如果 k 不大,大多数编程语言不使用 Quicksort,在最坏的情况下为 O(n^2),但平均情况为 O(nlogn)复杂度,他们使用像 Bubblesort 这样的 O(n^2) 算法,所以你的算法复杂度也取决于 k。考虑到它是恒定的,我认为这是不正确的。
  • 我已经用不同的ks 对其进行了测试,并且循环始终使用O(n) 运行并且到目前为止产生了正确的结果。我知道如果k 是我在代码上方编写的,那么这是正确的。
  • 无意争论,我的朋友。您是否使用 k=O(n^2) 项甚至 k=O(2^n) 进行了测试?一个循环可以是指数级的,甚至可以产生正确的结果。
  • 我已经添加了测试代码。无论数组有多长,它都不会超过O(n) 次。有一个 m &lt; 条件,但它仅确保在最后一项被删除时替换最后一项。