【问题标题】:Algorithm to interleave array of characters and digits in-place就地交错字符和数字数组的算法
【发布时间】:2012-10-06 19:13:51
【问题描述】:

有一个类似[a,b,2,c,3,d,4,1] 的数组。必须就地修改成[a,2,b,3,c,4,d,1]之类的数组。

也就是说,从散布字母和数字的原始数组中,修改后的版本应该包含相同的元素,使得数组具有交替的字母和数字,同时保留它们原始的相对顺序。

这可以很容易地完成,通过使用两个指针,并将修改后的版本输出到一个新数组中,但它必须在O(N) 时间和O(1) 空间中就地完成。有没有可能,如果有,怎么做?

【问题讨论】:

  • 我怀疑这个任务是否可以在 O(n) 和 O(1) 内存中解决。问题是它是一种稳定的排序,我们知道任何比较排序的下限都是 O(nlogn)。显然(正如你提到的)它可以在 O(n) 中解决这个问题,但它需要 O(n) 内存,这不是这里的情况

标签: arrays algorithm


【解决方案1】:

O(N) 就地算法是可能的。您可以从将所有字母排序到数组的开头,将所有数字排序到末尾。然后就地转置 2x(N/2) 矩阵,将数组前半部分的所有元素放置到偶数位置,其他元素放置到奇数位置。 This question 解释了如何进行这种转置(实际上,那里描述的算法应该反向应用)。

最棘手的部分是排序。它应该既稳定又就地。

在 O(N2) 时间内排序很简单。插入排序或冒泡排序都可以。

O(N log N) 时间比较复杂。使用稳定的就地合并排序,在this answer 中描述。这个答案给出了一对链接,描述了可以用来组成 O(N) 算法的链接,该算法比稳定的就地合并排序更简单,但仍然相当复杂,所以我只给它一个草图。


将数组分成两个区域。其中之一将用于临时存储算法其他部分所需的一些数据(由于该区域仍应存储数组元素,因此任何附加信息都按这些元素的顺序编码)。其他区域被解释为一个方阵,其中元素应该被排序,首先是行,然后是列。在对两个区域进行排序后,transpose 算法将应用于每个区域,以将字符和数字放在适当的位置。


1。使用部分数组进行临时存储

数组的大部分元素都组织成一个大小为 K 的方阵。 K 是最大的偶数,这样 2 * 8 * K + K2 不大于 N。 临时存储大小M = N - K2,约等于2*8*sqrt( N)。

要为临时存储准备M个元素,首先扫描数组的M个元素并确定哪种元素在这个范围。例如,让它成为“数字”。将数字打包成一个大小为 M/2 的组。为此,迭代地交换字母和数字块,如下例所示:

ddaaaddddadddaaaaad
aaaDDddddadddaaaaad
aaaaDDDDDDdddaaaaad
aaaaaaaaaDDDDDDDDDd

当单组至少收集到M/2个数字时停止。 块交换过程(也可以被称为就地子阵列旋转)可以作为this answer的第一个算法或如下实现:

  1. 反转数字块
  2. 反转数字块
  3. 将两个块颠倒在一起

例子:

123abcd
321abcd
321dcba
abcd123

将数字打包成一组 M/2 需要向上移动 (M/2)2 个数字,最多 M/2 strong>N/2 个字母,所以这可以在 O(N) 时间内完成。

准确来说是O(N log2U)时间,对ASCII字符排序还可以,但是太大了如果我们需要对其他类型的值进行排序(U 是值集的大小)。要将运行时间提高到 O(N * log U * log log U),请从较小的临时存储开始 大小为 2 * K,使用它对日志 U 范围进行排序,然后将这些范围与日志中的 块交换 过程成对合并U 步骤以获得适当大小的临时存储

此时我们有 M/2 大小的数字块,前面是一个字母块(可能更大)。使用块交换过程来制作两个大小相等的块M/2。

为了在临时存储中存储数据位,我们可以交换位置 i 和 i + M/2 的元素。如果位置 i 存储一个字母,位置 i + M/2 存储一个数字,我们有零位。如果数字在前,我们有非零位。 8 个连续位编码单个字符。这意味着,我们可以在临时存储中记住多达 K 个字符。

临时存储中存储位的其他替代方法是它的“数字”一半。每个数字可以存储为“0”..“9”(表示位 0)或“a”..“j”(表示位 1)。如果可能的字母数量至少是数字数量的 3 倍,我们可以在每个值中存储 2 位。为了从每个值中挤出 4 位,我们可以将每 2 位打包成一个字节;这将提供 M/4 个空闲字节;所以 M 可能会减少到 4 * K

2。矩阵行排序

从这一点开始,我们应该重新排列数组的主要区域,使用 临时存储 作为额外的内存。确定,在大小为 2 * K 的子数组中,哪种元素的表示最少。例如,让它成为“数字”。顺序扫描数组,将数字移动到临时存储,并将字母打包成连续序列。当收集到 K 个数字时,我们有 L 个字母顺序,并且 L >= K。将最后 L%K 个字母移动到未占用区域的末尾,并用 临时存储 中的数字填充剩余的未占用区域。然后继续顺序扫描数组。

此过程仅将数组的每个元素复制一次或两次,因此需要 O(N) 时间才能完成。最后,所有字母/数字块都对齐,这样矩阵的每一行都包含一种元素。


3。对矩阵的列进行排序

这是先前程序的简化版本。对于每个矩阵列,将数字移动到临时存储并将字母打包成连续的行,然后将数字从临时存储复制到未占用的行。

这个过程也需要 O(N) 时间来完成。最后,数组的主要区域完全排序。现在对临时存储进行排序(将零写入其每个“位”)。

4。转置数组

剩下的唯一步骤是转置 临时存储 和数组的主要区域,以便字符和数字位于适当的位置。 (请注意,不是 K x K 矩阵,而是由两个子数组组成的两个 2 x J 矩阵用算法转置,在链接答案之一中提到) .

这个过程以及整个算法需要 O(N) 时间。


第二种算法。

由于 ASCII 只包含 10 个数字和 52 个字母,我们可以通过以下方式显着简化算法:

  1. 使用 BASE64 编码打包最后 1/3 的值,从而提供 N/12 字节的可用空间(用作临时存储)。
  2. 将该空间用作临时存储的值的前1/3排序:确定在大小为N/6的子数组中代表的元素最少;例如,让它成为“数字”;顺序扫描数组,将数字移动到临时存储,并将字母打包成连续序列;然后将临时存储复制到空闲空间;对接下来的 N/6 个元素重复此操作。
  3. 解压缩最后 1/3 的值,压缩前 1/3 的值。
  4. 对最后 2/3 的值进行排序(对 N/6 个元素进行 4 次排序)。
  5. 解压前 1/3 的值。
  6. 此时我们有 12 组字符(大小可变)。使用 block exchange 程序在它们之间移动这些组的片段,并按照正确的顺序(字母、数字、...)组成 12 个大小相同的组。
  7. 转置 6 对字符组。实际上,这里可以使用简单的转置算法。将循环引导算法应用于所有未标记的元素(第 7 位为零),当元素移动时,将其第 7 位设置为 1。完成后,清除所有标记。

第三种算法。

解决此问题的其他方法是使用this pdf 中描述的就地稳定基数排序将所有字母排序到数组的开头,所有数字排序到结尾(一些修改)。排序后,应用前面提到的transpose算法。

这里最棘手的是这个基数排序算法的压缩部分。我们不能从每个值中保存一位。相反,我们应该使用算术编码。

【讨论】:

  • 嗯,那肯定是一口,所以我需要一些时间来回答你。但是,您正在使用辅助存储。如果确实如此,我还不如使用 2 个数组来按顺序存储字母和数字,最后将它们放回原始数组中。对吗?
  • 不,我不使用额外的存储空间。我从原始数组的一部分中制作了额外的存储空间。算法需要 O(1) 额外空间。
【解决方案2】:

这是一个 O(N²) 的实现

def is_digit(s):
    if isinstance(s, str):
        return False
    return True

def interleave(items):
    if len(items) < 2:
        return

    loc = 1
    max_loc = len(items)
    prev_is_digit = is_digit(items[0])

    while loc < max_loc:
        if is_digit(items[loc]) == prev_is_digit:
            # proceed through the list looking for the next item that has the 
            # opposite type
            for i in xrange(loc + 1, max_loc + 1):
                if is_digit(items[i]) != prev_is_digit:
                    # found the opposite type
                    val = items[i]

                    # once the item is found, shift everything up to the item
                    # over by one
                    for j in xrange(i, loc, -1):
                        items[j] = items[j - 1]

                    # move the item to It's new place
                    items[loc] = val
                    break
            loc += 2
        else:
            # invert the type we are looking for
            prev_is_digit = not prev_is_digit
            loc += 1


items = ["a","b",2,"c",3,"d",4,1]
interleave(items)
print items

【讨论】:

  • 似乎工作正常。请解释逻辑!当然不是O(N),但至少是到位的!
【解决方案3】:

我相信这个问题可以看作是两个序列的稳定就地合并。鉴于此,它应该可以在 O(n) 时间和 O(1) 空间内解决,因为等大小数组的稳定就地合并可以在 O(n) 时间内完成(参见,例如,http://comjnl.oxfordjournals.org/content/38/8/681.abstract)。

但是,执行此操作的算法完全不简单(并且可以为 CS 本科生制作一个体面的小型研究项目)。

在任何情况下,在 O(n log n) 时间和 O(1) 时间内对数组进行稳定排序绝对是可能的(例如,参见http://www.springerlink.com/content/d7348168624070v7/?MUD=MP),这为算法的复杂性设定了上限。

【讨论】:

    【解决方案4】:

    另一个更简单的就地 O(n2) 解决方案如下: 1)使用2个ptrs:src和dest 2)你想找到第一个非数字字符并将其放在目的地 3) 如果 dest 的 char 已经是非数字,则查找第一个数字并将其与 dest+1 loc 交换

    public static void permute_alt(char arr[]) {
        int src=0, dest=0;
    
        while (dest < arr.length) {
            if (isChar(arr[dest])) {   //We have a non-digit at dest idx .Now lets look for
                                           // digit and put it in dest+1 place
                dest++;
                src=dest;
                while (!Character.isDigit(arr[src])) {
                    src++;
                }
                swap(arr, src, dest);
                dest++;
            } else {
                src=dest;
                while (!isChar(arr[src])) {
                    src++;
                }
                swap(arr, src, dest);
                dest++;
    
            }
        }
    
    }
    

    src ptr 被重置为 dest+1 或 dest 以进行下一次迭代。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-04-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-21
      • 2017-03-18
      • 1970-01-01
      • 2020-07-25
      相关资源
      最近更新 更多