如何在线性时间内“排序”两种可能值的元素?

6
假设我有一个函数f和一个元素数组。
对于任何元素,该函数返回AB;你可以这样显示元素ABBAABABAA
我需要根据函数对元素进行排序,以便结果为:AAAAAABBBBA值的数量不必等于B值的数量。元素的总数可以是任意的(不固定)。请注意,您不会排序字符,而是对具有单个字符表示的对象进行排序。
还有几件事情:
  • 排序应该以线性时间O(n)完成,
  • 它应该原地执行,
  • 它应该是稳定的排序。
有什么想法吗?
注:如果上述不可能,您是否有牺牲上述要求的算法思路?

4
如果你能区分/比较两个元素,那么你就可以数它们。 - Thomas Jungblut
我已经阅读了它,只是不太清楚:这些键必须相互可比,或者您需要一些相等的度量。无论如何,您可以计算与n个其他元素相等的元素数量。也许您可以告诉我们您确切的用例。 - Thomas Jungblut
@ThomasJungblut 我已经更新了描述,现在更清晰了吗?我的意思是任何 A != B,但 A1 不一定等于 A2 - Sir Bohumil
我猜在这种条件下做到这是不可能的。线性、原地、稳定 - 你可以选择其中任意两个,但不能同时满足三个。 - Andrei Galatyn
1
如果要满足所有的需求,那么很难做到;但是如果不需要原地排序,使用计数排序就可以轻松实现。 - Andrei Galatyn
显示剩余5条评论
7个回答

4
如果必须是线性的且原地排序,可以使用半稳定版本。所谓半稳定是指 A 或 B 可以是稳定的,但不是同时。与 Dukeling 的答案类似,但您需要从同一侧移动两个迭代器:
a = first A
b = first B
loop while next A exists
    if b < a
        swap a,b elements
        b = next B
        a = next A
    else
        a = next A

使用样例字符串ABBAABABAA,你将获得以下结果:
ABBAABABAA
AABBABABAA
AAABBBABAA
AAAABBBBAA
AAAAABBBBA
AAAAAABBBB

在每次操作中,如果您交换了两个元素,它们都会移动;否则,只有一个元素会移动。这样可以保持A的稳定性,但会破坏B的顺序。为了保持B的稳定性,您需要从末尾开始向左依次进行操作。
也许有可能实现完全稳定,但我不知道如何做到。

这非常有趣,谢谢。 - Sir Bohumil

2
一个稳定的排序可能不可能在其他给定的约束条件下实现,因此这里提供了一种类似于quick-sort分区步骤的不稳定排序方法。
  1. 有两个迭代器,一个从左边开始,一个从右边开始。
  2. 当右侧迭代器上有一个B时,将迭代器递减。
  3. 当左侧迭代器上有一个A时,将迭代器递增。
  4. 如果迭代器没有交叉,则交换它们的元素并从2开始重复。

0
如果您的数据结构是链表而不是数组,那么您应该能够满足所有三个约束条件。您只需浏览列表并累积和移动“B”将是微不足道的指针更改。伪代码如下:
sort(list) {
    node = list.head, blast = null, bhead = null
    while(node != null) {
        nextnode = node.next
        if(node.val == "a") { 
            if(blast != null){              
                //move the 'a' to the front of the 'B' list
                bhead.prev.next = node, node.prev = bhead.prev
                blast.next = node.next, node.next.prev = blast
                node.next = bhead, bhead.prev = node
            }
        }
        else if(node.val == "b") { 
            if(blast == null)
                bhead = blast = node
            else //accumulate the "b"s.. 
                blast = node
        }

3

        node = nextnode
    }
}

所以,你可以在数组中做到这一点,但是模拟列表交换的memcopies会使大型数组变得非常缓慢。


0
首先,假设A和B的数组是生成或读入的,我想知道为什么不通过将列表在内存中累积为两个列表并随后合并来完全避免这个问题,从而简单地应用f
否则,我们可以提出一种O(n)时间和O(1)空间的替代解决方案,具体取决于Bohumil先生的最终需求:
遍历列表并原地对每个1,000,000元素的段使用置换循环进行排序(一旦完成此步骤,列表可以通过递归交换内部块(例如ABB AAB->AAABBB)在原地排序,但这可能需要太多时间而没有额外的空间)。再次遍历列表,并使用相同的常量空间,在两个区间树中存储指向每个A和B块的指针。例如,4个段的片段,
ABBAABABAA => AABB AABB AA + pointers to blocks of A's and B's

对于 A 或 B 的顺序访问将立即可用,而随机访问将使用区间树来定位特定的 A 或 B。一种选择是使区间编号为 A 和 B;例如,要查找第 4 个 A,请查找包含 4 的区间。

对于排序,一个由 1,000,000 个四字节元素(3.8MB)组成的数组足以存储索引,每个元素使用一个位来记录交换期间已访问的索引;并且两个临时变量的大小为最大的 A 或 B。对于十亿个元素的列表,最大的组合区间树将编号为 4000 个区间。每个区间使用 128 位,我们可以轻松地为 A 和 B 存储编号区间,并且我们可以使用未使用的位作为块索引(10 位)和在 B 的情况下的偏移量指针(20 位)。4000 * 16 字节 = 62.5KB。我们可以在 4KB 中存储仅包含 B 块偏移量的附加数组。对于一个拥有十亿个元素的列表,总空间不到 5MB。(实际上,空间取决于 n,但因为它与 n 相比极小,所以从实际目的出发,我们可以认为它是 O(1)。)

对于排序百万个元素段的时间将是 - 一次遍历以计数和索引(我们在这里也可以累积间隔和B偏移量),另一次遍历以进行排序。构建区间树的时间复杂度为O(nlogn),但n在这里仅为4000(占十亿列表数量的0.00005)。总时间为O(2n) = O(n)


0
假设, Object_Array[1...N]

Type_A 对象是 A1,A2,...,Ai

Type_B 对象是 B1,B2,...,Bj

i+j = N

FOR i=1 :N
    if Object_Array[i] is of Type_A
       obj_A_count=obj_A_count+1
    else
       obj_B_count=obj_B_count+1
LOOP

根据 obj_A > obj_B,将 obj_Aobj_B 的相应计数填充到结果数组中。


4
除非我遗漏了某些细节,否则这只是一种计数排序,鉴于问题的澄清,这种排序方法行不通。 - Bernhard Barker
@Dukeling 如果我们能够区分两个对象,为什么不能直接数它们呢? - P0W
将结果数组用 obj_A 和 obj_B 填充 - 请想象 type_X 是哈希函数对象的结果之一。 - Sir Bohumil
1
@P0W:你可以计数,但不能用这种方式对项目进行排序并满足所有要求(原地排序和稳定性)。 - Andrei Galatyn

0

以下方法适用于双向链表,可以在线性时间内完成。但是对于涉及到N个插入/删除操作的数组,可能会导致二次时间复杂度。

  1. 找到“排序”后第一个B应该在的位置。通过计算A的数量,可以在线性时间内完成。

  2. 使用3个迭代器:iterA从容器的开头开始,iterB从上述As和Bs相遇的位置开始,iterMiddle从iterB的前一个元素开始。

  3. 使用iterA跳过As,找到第一个B,并将对象从iterA移动到iterB->previous的位置。现在,iterA指向移动元素之后的下一个元素,而移动的元素现在位于iterB之前。

  4. 继续执行步骤3,直到达到iterMiddle。此后,first()和iterB-1之间的所有元素都是A。

  5. 现在将iterA设置为iterB-1。

  6. 使用iterB跳过Bs。当找到A时,将其移动到iterA之后并增加iterA。

  7. 继续执行步骤6,直到iterB到达end()。

这将适用于任何容器的稳定排序。该算法包括O(N)的插入/删除操作,对于具有O(1)插入/删除操作的容器来说是线性时间,但对于数组来说则是O(N^2)。适用性取决于容器是否为数组而不是列表。


适用于链表,但对于数组来说O(n^2)并不是最佳选择。 - Sir Bohumil

-1

这应该可以通过一些动态编程实现。

它的工作方式有点像计数排序,但有一个关键的区别。为a和b分别创建大小为n的数组count_a[n]和count_b[n]。将这些数组填充为在索引i之前有多少个A或B。

仅经过一次循环,我们就可以使用这些数组以O(1)的时间复杂度查找任何元素的正确索引。就像这样:

int final_index(char id, int pos){
    if(id == 'A')
      return count_a[pos];
    else
      return count_a[n-1] + count_b[pos];
}

最后,为了满足总的O(n)要求,交换需要按照一种聪明的顺序进行。一个简单的选择是拥有递归交换过程,直到两个元素都被放置在正确的最终位置之前,不会实际执行任何交换。编辑:实际上这并不是真的。即使是朴素的交换也会有O(n)次交换。但是采用这种递归策略将给您提供所需的绝对最小交换。

请注意,在一般情况下,这将是非常糟糕的排序算法,因为它具有O(n * 元素值范围)的内存要求。


创建大小为n的数组a和b,分别命名为count_a[n]和count_b[n]。这听起来像是O(n)的内存开销。 - Sir Bohumil
仍然是就地修改、稳定和线性的。如果 sizeof(your_object) >> sizeof(int),那么你需要分配一些整数数组也不会太糟糕。 - Santtu Keskinen
2
对我来说,“原地”大致翻译为“O(1)额外空间”。 - Bernhard Barker
2
如果我可以使用O(n)的空间,那么我可以简单地创建两个大小为n的数组,将所有的A按出现顺序复制到第一个数组中,将所有的B复制到第二个数组中,然后合并。更简单了。 - Sir Bohumil
你正在存储什么“元素”?对我来说,你不能为每个“元素”提供32位或任何额外的位数似乎很奇怪。 你甚至可以使用存储ID的同一数组作为额外空间,将其降低到更低的水平,比如32-8=24。这些“元素”必须是非常高效的空间利用! - Santtu Keskinen

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