将重复项移动到已排好序的数组的末尾

11

我在一次面试中被问到了这个问题。有一个带重复元素的已排序数组,目标是返回只含唯一元素的数组,后面跟着重复元素,且保持原来的顺序。例如[1, 1, 2, 3, 4, 4, 5]应该变成[1, 2, 3, 4, 5, 1, 4]

我使用了额外的空间(O(n)空间)和线性时间(O(n)时间)来解决这个问题,但我不确定这是否是最好的答案,理想情况下不使用线性空间。

我在stack overflow上搜索到了类似的问题,但并不完全相同。例如,有一个问题是将数组排序并将重复元素移动到末尾,但在我的情况下,数组已经排好序,目标只是将重复元素移到末尾。


每个数字是否可以有多个重复项?比如说 - [1,1,1,1,2,3,4,5]?这种情况可能出现吗,或者每个数字最多只能有两个条目? - zenwraight
3
什么问题? - nicomp
1
@zenwraight 我们应该将重复元素移动到末尾,而不是丢弃任何元素。在你的例子中 [1,1,1,1,2,3,4,5] -> [1, 2, 3, 4, 5, 1, 1, 1] - apadana
@apadana,我现在明白你的问题了,并更新了我的答案,你能看一下吗? - Mike Q
7个回答

4

如果你的数值范围有限,那么可以在O(n)时间和O(1)空间内找到解决方案。

确定数组中的最大值。选取某个常量C>arraymax,例如,对于你的数组,C=10

扫描数组,将唯一的值压缩并计算每个值的重复次数。如果值VK>0个副本,则写入V+C*K而不是该值。

在下一次扫描中,查找具有重复项的值,提取重复项的数量,并在压缩的唯一值之后写入它们。

def dedup(lst):
    mx = max(lst) + 1
    dupcnt = 0
    delcnt = 0
    start = 0
    for i in range(1, len(lst) + 1):
        if i == len(lst) or (lst[i] != lst[start]):
            lst[start - delcnt] = lst[start] + dupcnt * mx
            delcnt += dupcnt
            start = i
            dupcnt = 0
        else:
            dupcnt += 1
    dupidx = len(lst) - delcnt
    for i in range(0, len(lst) - delcnt):
        dupcnt = lst[i] // mx
        if dupcnt:
           lst[i] %= mx
           for j in range(dupidx, dupidx+dupcnt):
              lst[j] = lst[i]
           dupidx += dupcnt
    return lst

print(dedup([1,2,2,2,3,4,4,5]))
>>> [1, 2, 3, 4, 5, 2, 2, 4]

遍历数组两次不会变成n^2吗?这就是为什么我没有用这种方法的原因。 - Mike Q
@Mike Q 不需要双重遍历,只需要~2*N操作=O(N)。嵌套循环的算法通常具有二次行为。 - MBo
@MBo 我要承认我在这方面可能有点生疏,但我觉得有趣的是我可以通过将它们堆叠到一个扩展的 for 循环的末尾并在之后清理来只用一次遍历列表完成它... 那不会更快吗? - Mike Q
@Mike Q,根据你的描述,你的方法会立即重新分配数组,导致隐含的二次复杂度而使工作变慢。你可以记住删除的项,移动其他项并将删除的项放在末尾单元格中 - 显式二次运行时间。因此,这种方法适用于链表而不是数组。 - MBo
@MBo,实际上查找php函数array_push时,它基本上执行与您的示例相同数量的工作,您的优势在于它更清晰地说明了正在发生的事情。 - Mike Q
显示剩余4条评论

3

您需要有2-3个指针(索引):

  • i: 下一个唯一元素将放置在此位置
  • j: 列表上的线性遍历指针
private static void fix(int[] nums) {

    int i = 0;
    int j = 0;

    while (j < nums.length) {

        int k;

        for (k = j + 1; (k < nums.length) && (nums[k] == nums[j]); k++) {}

        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;

        j = k;
        i++;

    }
}

1
冒昧地说一下...一个时间复杂度为O(n log n),空间复杂度为O(1)的方法是:
  1. 扫描数组以找到每个值的第一个元素,并将该元素直接交换到正确的位置。(例如,当你到达第四个不同的值时,将该值的第一个元素与位置#4交换。)
    • 这一步需要O(n)时间和O(1)额外空间。
    • 完成此步骤后,数组由所有唯一元素按正确顺序组成,后面是所有重复元素的垃圾顺序。
  2. 使用堆排序对重复项进行排序。
    • 此步骤需要O(n log n)时间和O(1)额外空间。

堆排序不是稳定的,所以如果键不是整个数据,它将无法保持顺序。 - n. m.
@ruakh 堆排序有什么特别之处,还是它可以像快速排序一样使用任何排序算法? - apadana
@ruakh 交换是怎么进行的?用数组末尾的某个东西交换唯一元素吗?我没太明白交换部分。 - apadana
1
@apadana:关于“堆排序是否有什么特别之处[...]?”的问题:它的时间复杂度为O(n log n),额外空间复杂度为O(1)。相比之下,快速排序的时间复杂度为O(n²)。 (尽管n.m.评论说它不稳定,但是在这一点上,稳定的排序算法实际上并没有帮助,因为问题在于元素已经被打乱。)关于“交换是如何发生的?”的问题:您只需跟踪已经交换到位的元素数量,以便您知道将下一个元素交换到哪个位置。 - ruakh
我喜欢这个答案。如果重复值的数量显着大于唯一值,则还有一个补充答案:保留指向重复部分开始之前的指针(该指针从数组末尾开始),并在向后遍历数组时遇到重复元素时不断交换该位置中的元素和重复元素,然后对唯一值的部分进行堆排序。 - גלעד ברקן

0

更新:我误读了您的意图,您关心的是空间问题,这是“指针”的PHP版本。由于它已经排序,我们只需要遍历一次循环,对吧?如果不是,我们可能会将重复排序嵌入到排序本身中。

function findRepeating(&$arr)
{
    $size = count($arr);
    $previous = -99999;
    for ($i = 0; $i < $size; $i++) {
        if ($i>0)
            $previous = $arr[$i-1];

        if ($arr[$i] == $previous) {
            array_push($arr,$arr[$i]); //push to end
            unset($arr[$i]); //then remove current one
        }
    }
    var_dump($arr);
}

我们基本上只是取当前数组的大小,当我们发现重复项时,将其推到数组的末尾,扩展一下它的大小,这可以通过unset()来抵消。
array(7) {
  [0]=>
  string(1) "1"
  [2]=>
  string(1) "2"
  [3]=>
  string(1) "3"
  [4]=>
  string(1) "4"
  [6]=>
  string(1) "5"
  [7]=>
  string(1) "1"
  [8]=>
  string(1) "4"
}

在低级语言中,您可以轻松地移动指针,因为您知道结束值,所以只需在此之后添加重复项并随着偏移量增加即可。无论是否使用数组,这都是完全可实现的,我们只是交换它们。我的示例是使用PHP编写的,因此我不会进行洗牌操作,而是扩展数组,因此我仅临时使用单个额外空间。

感谢您的回答。根据php文档,array_push()将数组视为堆栈,并将传递的变量推入数组的末尾。数组的长度增加了所推入变量的数量。由于数组大小发生了变化,因此它似乎不是面试的合适解决方案。 - apadana
PHP并不容易支持指针,但无论如何,您可以看到这是一个很好的小妥协,因为它只在任何给定时间添加一个附加元素,而且在“移动”后立即删除。 - Mike Q

0

对于如何处理多个重复项或者你确切的问题不是很清楚,但我猜测你想要确保满足O(1)空间复杂度,无论时间复杂度如何,所以我会尝试回答这个问题。

使用数组,O(1)空间复杂度,O(N^2)时间复杂度:

你可以通过将重复元素交换到末尾来原地进行操作。你可以通过保持一个“当前”指针并简单地检查“下一个”元素是否与“当前”相同来找到重复元素。在最坏情况下,这需要O(n^2)时间。例如:

[1,1,2,3,4,4,5] # "cur" is index 0 (element 1), and "next" is index 1 (element 1). Swap "next" to end.
[1,2,1,3,4,4,5] # swapping
[1,2,3,1,4,4,5] # swapping
...             # Tedious swapping
[1,2,3,4,4,5,1] # Done swapping. Increment "cur".
[1,2,3,4,4,5,1] # "cur" is index 1 (element 2), and "next" is index 2 (element 3). Increment "cur"
...             # Boring (no duplicates detected)
[1,2,3,4,4,5,1] # "cur" is index 3 (element 4), and "next" is index 4 (element 4). Swap "next" to end.
[1,2,3,4,5,4,1] # swapping
[1,2,3,4,5,1,4] # Done swapping. Increment "cur"
...             # No more duplicates
# Done

顺便提一下,在实践中,为了节省空间而牺牲时间通常是不值得的。内存很便宜,但慢的响应时间可能会失去用户,这是很昂贵的。一个值得注意的例外是嵌入式系统,其中内存可能很紧张,输入很短(在小输入上渐近运行时间不相关)。

使用链表,O(1) 空间,O(N) 时间:

如果您有一个链表而不是数组,您可以很容易地在 O(n) 时间和 O(1) 空间内完成此操作。 当你被迫“移动”元素时,链表比数组更具优势,因为它们可以移动指针而不是将所有元素移动一个位置。 cur/next 策略与上面的数组类似。以下是一个示例:

1->1->2->3->4->4->5 # "cur" is first element (value 1), and "next" is second element (value 1). Swap "next" to the end.

1
 \
1->2->3->4->4->5    # Move "cur"'s pointer to "next"'s next element.

1->2->3->4->4->5->1 # Set "next"'s pointer to null, set tails pointer to "next"

...                 # Boring stuff with no duplicates

1->2->3->4->4->5->1 # "cur" is fourth element (value 4), and "next" is fifth element (value 4). Swap fifth element to end.

         4
          \
1->2->3->4->5->1    # Move "cur"'s pointer to "next"'s next element.

1->2->3->4->5->1->4 # Set "next"'s pointer to null, set tails pointer to "next"

...                 # No more duplicates
# Done (hopefully it's clear moving and element to the end is O(1) instead of O(n))

如果您可以在O(n)时间和O(1)空间内将数组转换为链表,则问题解决了。然而,这是不可能的。 链表每个元素占用的空间比数组多,因此仅通过程序中存在链表,我认为O(1)空间就会被破坏。

虽然这是一个面试问题,但指出链表更适合高效地解决此问题可能是值得的,无论问题陈述如何。通常,面试官喜欢看到您能够正确应用数据结构,有时他们也会接受输入类型的更改。

聪明的数据结构和愚蠢的代码比另一种方式要好得多。--Eric S Raymond


谢谢你的回答。我认为我们可以通过将重复项交换到列表末尾,并对数组的唯一部分(第一部分)进行排序,从而实现比O(n^2)更好的效果,这将是O(n log n)。 - apadana
是的,观察得很好。我有机会时会稍后更新。 - Matt Messersmith
你可以使用数组来实现,同时如果你知道最后一个值是'5',那么你就不必进行不必要的排序,只需要记录该位置/值,并在到达该位置或当前值小于前一个值时停止。此外,你只需将重复项附加到'5'值+偏移量或其他位置即可。 - Mike Q

0
这里是将重复的字符串放在数组末尾的C代码。 指示器数组用于指示字符串重复的索引。 例如:如果s [0] == s [1],则将指标[1]分配为0,因为在此索引处重复了字符串。 然后使用指示器数组将重复的字符串交换到数组中最后一个有效位置。
例如:如果我们发现indicator [1] = 0,则意味着在索引1处有一个重复的字符串,我们需要将其移动到数组的末尾,但是如果数组的最后一个元素也是重复的!那么我们应该向前移动到数组倒数第二个元素。
    void put_dublicates_to_last(char**s, int n)
{
    int i = 0, j = 0, flag = 0,counter=0;
    int* indicator = malloc(n * sizeof(int));
    char * temp;
    for (i = 0; i < n; i++)
        indicator[i] = -1;
    for (i = 0; i < n; i++)
    {
        for (j = i + 1; j < n; j++)
        {
            if (strcmp(s[i], s[j]) == 0)
            {
                //swap with the last element
                counter++;
                indicator[j] = 0;
            }
        }
    }
    printf("counter is %d\n", counter);
    //use the indicator to swap with the last elements 
    for (i = 0; i < n; i++)
    {
        for (j = n; j >= 0; j--)
        {
            if (indicator[i] == 0)
            {
                if (indicator[j] != 0)
                {
                    //swap
                    temp = s[i];
                    s[i] = s[j-1];
                    s[j-1] = temp;
                    flag = 1;
                }
            }
            if (flag)
            {
                flag = 0;
                break;
            }

        }

    }

    for (i = 0; i < n; i++)
        printf("%s\n", s[i]);
}

0

如果我们不介意数组中重复元素的稳定性和排序性,可以用单指针和另一个指针来找到下一个最大值。

算法

  • 启动指针并遍历数组,只要当前元素大于前一个元素且小于下一个元素就继续遍历
  • 一旦发现此模式中断,请停止增量并查找一个比当前数字大的数字
  • 将该数字与下一个更大的数字交换
  • 继续搜索,直到在数组中找不到更大的数字为止
  • 如果达到此条件,请退出循环并返回数组
public static void main(String[] args) {
        // TODO Auto-generated method stub              
        int[] arr = {11, 12, 12, 13, 14, 14, 14, 14,  15};
        rearrangeSort(arr);     
        for(int a : arr) {
            System.out.print(a + " ");
        }       
    }   
    public static void rearrangeSort(int[] arr){
        int unique = 1;
        int find = 0;
        while(unique < arr.length) {
            if(unique == 1 && (arr[unique - 1] == arr[unique])){
                find = findMax(arr, arr[unique], unique);
                swap(arr, unique, find);                
            }else if(unique == 1 && (arr[unique] == arr[unique + 1])){
                find = findMax(arr, arr[unique], unique);
                swap(arr, unique + 1, find);                
            }           
            if(unique > 0 && (arr[unique - 1] < arr[unique]) && (arr[unique] < arr[unique + 1])){
                unique++;
            }
            find = findMax(arr, arr[unique], unique);           
            if(find == 0) {break;}
            swap(arr, unique+1, find);
        }                   
    }       
    public static int findMax(int[] arr, int target, int index){
        while(index < arr.length) {
            if(arr[index] > target) {return index;}
            index++;
        }
        return 0;
    }       
    public static void swap(int[] arr, int idx1, int idx2){
        int temp = arr[idx1];
        arr[idx1] = arr[idx2];
        arr[idx2] = temp;       
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接