当前位置:  开发笔记 > 编程语言 > 正文

算法:从数组中删除重复整数的有效方法

如何解决《算法:从数组中删除重复整数的有效方法》经验,为你挑选了10个好方法。

我在接受微软采访时遇到了这个问题.

给定一个随机整数数组,在C中编写一个算法,删除重复的数字并返回原始数组中的唯一数字.

例如输入:{4, 8, 4, 1, 1, 2, 9} 输出:{4, 8, 1, 2, 9, ?, ?}

需要注意的是,预期的算法不应该首先对数组进行排序.当一个元素被移除后,以下元素也必须向前移动.无论如何,元素尾部元素向前移动的元素值可以忽略不计.

更新:必须在原始数组中返回结果,并且不应使用辅助数据结构(例如哈希表).但是,我想订单保存不是必需的.

更新2:对于那些想知道为什么这些不切实际的约束的人来说,这是一个面试问题,在思考过程中讨论所有这些约束,看看我如何能够提出不同的想法.



1> ejel..:

我女朋友建议的解决方案是合并排序的变体.唯一的修改是在合并步骤中,只是忽略重复的值.这个解决方案也是O(n log n).在这种方法中,排序/复制删除被组合在一起.但是,我不确定这是否会产生任何影响.


很好的建议,但你需要一些记账来跟踪每个合并输出的结束.我实际上曾经这样做了一次,并且在合并时消除了重复,这使得它更快.
当人们过度称赞女性程序员时...这就是我们这么少的确切原因......
一篇描述女友建议的文章如下:http://dc-pubs.dbs.uni-leipzig.de/files/Bitton1983Duplicaterecordeliminationin.pdf
目前尚不清楚O(N/2)额外空间是否算作问题中禁止的"辅助数据结构" - 我不知道该限制是否旨在规定O(1)额外空间,或仅仅是为了规定答案不应该依赖于一个大的数据结构实现.也许标准合并是好的.但如果没有,最重要的提示:不要试图在面试中写一个就地合并排序,除非你*真的*知道你在做什么.

2> dsimcha..:

我之前已经发布了这个,但是我会在这里重现它,因为它非常酷.它使用散列,构建类似哈希集的东西.它保证在腋窝空间中是O(1)(递归是尾调用),并且通常是O(N)时间复杂度.算法如下:

    取数组的第一个元素,这将是哨兵.

    尽可能重新排序数组的其余部分,使每个元素位于与其散列对应的位置.完成此步骤后,将发现重复项.设置它们等于哨兵.

    将索引等于哈希的所有元素移动到数组的开头.

    将除了数组的第一个元素之外的所有等于sentinel的元素移动到数组的末尾.

    正确散列的元素和重复元素之间的剩余部分将是由于冲突而无法放置在与其散列相对应的索引中的元素.递归处理这些元素.

这可以显示为O(N),在散列中没有提供病理场景:即使没有重复,在每次递归时也将消除大约2/3的元素.每个递归级别为O(n),其中小n是剩余元素的数量.唯一的问题是,在实践中,当重复很少时,即快速排序,即大量碰撞时,它比较慢.然而,当存在大量重复时,它的速度非常快.

编辑:在D的当前实现中,hash_t是32位.关于此算法的所有内容都假定在完整的32位空间中将存在非常少的哈希冲突(如果有的话).然而,碰撞可能在模数空间中频繁发生.然而,对于任何合理大小的数据集,这种假设很可能都是正确的.如果密钥小于或等于32位,则它可以是它自己的散列,这意味着完全32位空间中的冲突是不可能的.如果它更大,你根本无法将它们放入32位内存地址空间,因为它是一个问题.我假设在D的64位实现中hash_t将增加到64位,其中数据集可以更大.此外,如果这确实是一个问题,可以在每个递归级别更改散列函数.

这是D编程语言中的一个实现:

void uniqueInPlace(T)(ref T[] dataIn) {
    uniqueInPlaceImpl(dataIn, 0);
}

void uniqueInPlaceImpl(T)(ref T[] dataIn, size_t start) {
    if(dataIn.length - start < 2)
        return;

    invariant T sentinel = dataIn[start];
    T[] data = dataIn[start + 1..$];

    static hash_t getHash(T elem) {
        static if(is(T == uint) || is(T == int)) {
            return cast(hash_t) elem;
        } else static if(__traits(compiles, elem.toHash)) {
            return elem.toHash;
        } else {
            static auto ti = typeid(typeof(elem));
            return ti.getHash(&elem);
        }
    }

    for(size_t index = 0; index < data.length;) {
        if(data[index] == sentinel) {
            index++;
            continue;
        }

        auto hash = getHash(data[index]) % data.length;
        if(index == hash) {
            index++;
            continue;
        }

        if(data[index] == data[hash]) {
            data[index] = sentinel;
            index++;
            continue;
        }

        if(data[hash] == sentinel) {
            swap(data[hash], data[index]);
            index++;
            continue;
        }

        auto hashHash = getHash(data[hash]) % data.length;
        if(hashHash != hash) {
            swap(data[index], data[hash]);
            if(hash < index)
                index++;
        } else {
            index++;
        }
    }


    size_t swapPos = 0;
    foreach(i; 0..data.length) {
        if(data[i] != sentinel && i == getHash(data[i]) % data.length) {
            swap(data[i], data[swapPos++]);
        }
    }

    size_t sentinelPos = data.length;
    for(size_t i = swapPos; i < sentinelPos;) {
        if(data[i] == sentinel) {
            swap(data[i], data[--sentinelPos]);
        } else {
            i++;
        }
    }

    dataIn = dataIn[0..sentinelPos + start + 1];
    uniqueInPlaceImpl(dataIn, start + swapPos + 1);
}



3> Byju..:

一个更有效的实现

int i, j;

/* new length of modified array */
int NewLength = 1;

for(i=1; i< Length; i++){

   for(j=0; j< NewLength ; j++)
   {

      if(array[i] == array[j])
      break;
   }

   /* if none of the values in index[0..j] of array is not same as array[i],
      then copy the current value to corresponding new position in array */

  if (j==NewLength )
      array[NewLength++] = array[i];
}

在该实现中,不需要对阵列进行排序.此外,如果找到重复元素,则不需要在此之后将所有元素移位一个位置.

此代码的输出是array [],其大小为NewLength

这里我们从数组中的第二个elemt开始,并将它与数组中的所有元素进行比较,直到这个数组.我们持有一个额外的索引变量'NewLength'来修改输入数组.NewLength变量初始化为0.

数组[1]中的元素将与数组[0]进行比较.如果它们不同,那么数组[NewLength]中的值将被数组[1]修改并增加NewLength.如果它们相同,则不会修改NewLength.

所以,如果我们有一个数组[1 2 1 3 1],那么

在'j'循环的第一次传递中,数组[1](2)将与array0进行比较,然后2将被写入数组[NewLength] = array [1],因此数组将为[1 2],因为NewLength = 2

在'j'循环的第二次传递中,数组[2](1)将与array0和array1进行比较.这里因为数组[2](1)和array0是相同的循环将在这里打破.因为NewLength = 2,所以数组将是[1 2]

等等


好一个.我有一个改进的建议.第二个嵌套循环可以更改为for(j = 0; j
4> carl..:

如果您正在寻找优越的O符号,那么使用O(n log n)排序对数组进行排序,然后进行O(n)遍历可能是最佳路径.没有排序,你看O(n ^ 2).

编辑:如果你只是做整数,那么你也可以做基数排序来获得O(n).


ChrisW:如果你假设没有碰撞,哈希集/字典只有O(1).(我不是说我不会用它来解决这个问题 - 我可能会 - 声称它们真的是O(1)只是一个谬论.)
您可能希望详细说明"遍历",因为天真的擦除方法可能会导致大量重复的O(n ^ 2).
实际上,由于您事先了解了数组的大小,因此可以保证O(1).然后,您可以将冲突与您使用的额外内存进行权衡.

5> mocj..:

怎么样:

void rmdup(int *array, int length)
{
    int *current , *end = array + length - 1;

    for ( current = array + 1; array < end; array++, current = array + 1 )
    {
        while ( current <= end )
        {
            if ( *current == *array )
            {
                *current = *end--;
            }
            else
            {
                current++;
            }
        }
    }
}

应该是O(n ^ 2)或更少.


大声笑,尽管对数组进行排序并对排序数据进行排序肯定会更快.排序应该由API提供,并且不会过早优化.
他们甚至可能会检查你是否没有沉迷于过早的优化,除非他们也给你运行时限制!:-)
这是一个简单的解决方案,很可能是面试问题所寻求的.
不应该是while(current <= end)而不是while(current 为什么这被认为是正确的答案?如果没有必要保留订单,那么最好只使用合并排序O(nlogn)然后删除O(n)中的重复元素...总复杂度 - O(nlogn)比这个解决方案好得多.

6> Jack V...:

1.在O(n log n)时间内使用O(1)额外空间

这是可能的,例如:

首先进行就地O(n log n)排序

然后遍历列表一次,将每个返回的第一个实例写入列表的开头

我相信ejel的合作伙伴是正确的,最好的方法是使用简化的合并步骤进行就地合并排序,这可能是问题的意图,如果你是例如.编写一个新的库函数来尽可能有效地完成这项工作而无法改进输入,并且有些情况下,根据输入的类型,在没有散列表的情况下这样做会很有用.但我实际上没有检查过这个.

2.在O(n)时间内使用O(大量)额外空间

声明一个零的数组足以容纳所有整数

穿过阵列一次

为每个整数设置相应的数组元素为1.

如果它已经是1,则跳过该整数.

这仅在若干可疑假设成立时才有效:

可以廉价地将内存归零,或者与它们的数量相比,整数的大小很小

您很高兴向您的操作系统询问256 ^ sizepof(int)内存

如果它是巨大的,它会真正有效地缓存它

这是一个糟糕的答案,但如果你有很多输入元素,但它们都是8位整数(或者甚至可能是16位整数),那么它可能是最好的方法.

O(小) - 多余的空间,O(n) - 时间

作为#2,但使用哈希表.

4.明确的方式

如果元素的数量很少,如果其他代码编写速度更快,读取速度更快,则编写适当的算法是没有用的.

例如.遍历数组中的每个独特元素(即第一个元素,第二个元素(第一个元素的副本已删除)等)删除所有相同的元素.O(1)额外空间,O(n ^ 2)时间.

例如.使用执行此操作的库函数.效率取决于您容易获得的效率.



7> Dario..:

嗯,它的基本实现非常简单.浏览所有元素,检查其余元素是否有重复,并将其余元素移到它们上面.

这是非常低效的,您可以通过输出或排序/二叉树的辅助数组来加速它,但似乎不允许这样做.



8> Jeff B..:

如果你愿意牺牲记忆,你可以在一次遍历中做到这一点.您可以简单地计算是否在哈希/关联数组中看到了整数.如果您已经看过一个数字,请在移动时移除它,或者更好的是,将未看到的数字移动到新数组中,避免在原始数组中移位.

在Perl中:

foreach $i (@myary) {
    if(!defined $seen{$i}) {
        $seen{$i} = 1;
        push @newary, $i;
    }
}


我不明白为什么这个答案得到最多的投票.它是用perl编写的,并且使用了C中没有的重要功能,正如问题所要求的那样.
在编辑问题之前,这是一个好主意.你的哈希表的想法显然违反了规则.
问题是c代码,而不是perl.使用perl可以获得哈希表并免费"推送".如果我可以在scala中执行此操作,您只需调用input.removeDuplicates,但我怀疑这对面试官来说是可以接受的:)

9> fbrereto..:

如果您被允许使用C++,则呼叫std::sort后跟呼叫std::unique将为您提供答案.时间复杂度为排序的O(N log N)和唯一遍历的O(N).

如果C++不在桌面上,则没有任何东西能够保持这些相同的算法不能用C语言编写.


它并没有说你得到它后你不能对数组进行排序...不使用O(N)外部存储器排序是在O(N log N)或更好的情况下进行它的唯一方法.

10> dsh..:

函数的返回值应该是唯一元素的数量,它们都存储在数组的前面.如果没有这些附加信息,您甚至不知道是否有任何重复.

外循环的每次迭代都处理数组的一个元素.如果它是唯一的,它将保留在数组的前面,如果它是重复的,它将被数组中最后一个未处理的元素覆盖.该解决方案在O(n ^ 2)时间内运行.

#include 
#include 

size_t rmdup(int *arr, size_t len)
{
  size_t prev = 0;
  size_t curr = 1;
  size_t last = len - 1;
  while (curr <= last) {
    for (prev = 0; prev < curr && arr[curr] != arr[prev]; ++prev);
    if (prev == curr) {
      ++curr;
    } else {
      arr[curr] = arr[last];
      --last;
    }
  }
  return curr;
}

void print_array(int *arr, size_t len)
{
  printf("{");
  size_t curr = 0;
  for (curr = 0; curr < len; ++curr) {
    if (curr > 0) printf(", ");
    printf("%d", arr[curr]);
  }
  printf("}");
}

int main()
{
  int arr[] = {4, 8, 4, 1, 1, 2, 9};
  printf("Before: ");
  size_t len = sizeof (arr) / sizeof (arr[0]);
  print_array(arr, len);
  len = rmdup(arr, len);
  printf("\nAfter: ");
  print_array(arr, len);
  printf("\n");
  return 0;
}

推荐阅读
和谐啄木鸟
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有