用于交错字符和数字数组的算法

时间:2012-10-06 19:13:52

标签: arrays algorithm

有一个像[a,b,2,c,3,d,4,1]这样的数组。必须将其修改为类似[a,2,b,3,c,4,d,1]的数组。

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

可以通过使用两个指针轻松完成,并将修改后的版本输出到新数组中,但必须在O(N)时间和O(1)空间内就地完成。是否可能,如果可能,怎么样?

4 个答案:

答案 0 :(得分:3)

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

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

在O( N 2 )时间排序是微不足道的。插入排序或冒泡排序就可以了。

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


将数组划分为两个区域。其中一个将用于临时存储算法其他部分所需的一些数据(因为该区域仍应存储数组元素,任何其他信息按这些元素的顺序编码)。其他区域被解释为方形矩阵,其中元素应排序,第一行,然后是列。在对两个区域进行排序后,将转置算法应用于每个区域,以便在适当的位置获取字符和数字。


1。从阵列的一部分中进行临时存储

阵列的大多数元素被组织为大小 K 的方阵。 K 是最大偶数,因此2 * 8 * K + K 2 不大于<强>名词即可。 临时存储空间M = N - K 2 的大小,大约等于2 * 8 * sqrt(的ñ)。

要为临时存储准备 M 元素,请扫描数组的第一个 M 元素,并确定哪个元素的表示最少这个范围。例如,让它成为&#34;数字&#34;。将数字打包成一组 M / 2。为此,迭代地交换字母和数字块,如下例所示:

ddaaaddddadddaaaaad
aaaDDddddadddaaaaad
aaaaDDDDDDdddaaaaad
aaaaaaaaaDDDDDDDDDd

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

  1. 反向数字块
  2. 反向数字块
  3. 将两个块一起反转
  4. 示例:

    123abcd
    321abcd
    321dcba
    abcd123
    

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

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

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

    要在临时存储空间中存储数据位,我们可以在位置i和i + M / 2处交换元素。如果位置i存储字母并且位置i + M / 2存储一个数字,则我们有零位。如果数字首先出现,我们有非零位。 8个连续位编码单个字符。这意味着,我们最多可以记住临时存储中的 K 字符。

    临时存储中存储位的其他替代方法是&#34; number&#34;半。每个号码可以存储为&#39; 0&#39; 0&#39; 0 ..&#39; 9&#39; (这意味着第0位),或者作为&#39; a&#39; ..&#39; j&#39; (这意味着第1位)。如果可能的字母数至少是数字位数的3倍,我们可以在每个值中存储2位。为了从每个值中挤出4位,我们可以将每2位数字打包成一个字节;这给出了 M / 4个空闲字节;所以 M 可以减少到4 * K

    enter image description here

    2。对矩阵的行进行排序

    从这一点开始,我们应该重新排列阵列的主要区域,使用临时存储作为附加内存。确定在尺寸为2 * K 的子阵列中最少表示哪种元素。例如,让它成为&#34;数字&#34;。按顺序扫描阵列,将数字移动到临时存储并将字母打包成连续序列。收集 K 数字时,我们按顺序排列L个字母,L&gt; = K 。将最后的L% K 字母移至未占用区域的末尾,并使用临时存储空间中的数字填充未占用区域的剩余部分。然后继续按顺序扫描阵列。

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

    enter image description here


    3。对矩阵的列进行排序

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

    此过程还需要O( N )时间才能完成。最后,阵列的主要区域是完全排序的。现在排序临时存储(将零写入其每个&#34;位&#34;)。

    enter image description here

    4。转置数组

    唯一剩下的步骤是转置 临时存储空间和数组的主要区域,以便字符和数字位于正确的位置。 (注意,不是 K x K 矩阵,但是两个子阵列制作的两个2 x J矩阵都使用算法进行转置,在其中一个链接的答案中提到)

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

    enter image description here


    第二种算法。

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

    1. 使用BASE64编码打包最后1/3的值,它提供 N / 12字节的可用空间(用作临时存储)。
    2. 使用此空格将前1/3的值排序为临时存储空间:确定在 N / 6的子数组中最少表示哪种元素;例如,让它成为&#34;数字&#34 ;;顺序扫描数组,将数字移动到临时存储并将字母打包成连续序列;然后将临时存储复制到未占用的空间;对下一个 N / 6元素重复此操作。
    3. 打开最后1/3的值,打包前1/3的值。
    4. 排序最后2/3的值(排序 N / 6个元素4次)。
    5. 打开前1/3的值。
    6. 此时我们有12组字符(可变大小)。使用块交换过程在它们之间移动这些组的片段,并按正确的顺序(字母,数字......)制作12个大小相等的组。
    7. 转置 6对字符&#39;组。实际上,这里可以使用简单的换位算法。将循环引导算法应用于所有未标记的元素(第7位为零),并在移动元素时,将其第7位设置为1。完成后,清除所有标记。

    8. 第三种算法。

      解决这个问题的其他方法是将所有字母排序到数组的开头,所有数字 - 最后都有(某些修改)就地稳定基数排序,如this pdf中所述。排序后,应用前面提到的转置算法。

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

答案 1 :(得分:1)

这是一个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

答案 2 :(得分:1)

我认为这个问题可以被视为两个序列的稳定就地合并。在这种情况下,应该在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),这会对算法的复杂性设置上限

答案 3 :(得分:0)

另一个更简单的现场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以进行下一次迭代。