在O(n log n)时间生成n个长度为k个反转数的数组的算法?

时间:2019-02-03 18:55:35

标签: java algorithm performance sorting inversion

我正在编写一种算法,该算法将返回具有确定的长度和求反数(数对,其中左边的数字大于右边的数字)的数组。即数组[3,1,4,2]包含三个取反(3,1),(3,2)和(4,2)。因此在实践中,当给定长度为n = 3且求反数k = 3时,该算法应生成一个数组[3、1、4、2](或满足这些要求的另一个数组)。

由于反转次数也是必须按升序对数组进行排序的交换数,因此我通过创建从1到n-1的数组并在其中使用插入排序算法来解决此问题。反向进行k次交换。

这种方法对于较小的输入也很好,但是该算法应该能够有效地生成最大为n = 10 ^ 6和k = n(n-1)/ 2的数组,并且介于两者之间,因此在O(n log n)时间而不是O(n ^ 2)中工作。下面是代码:

import java.util.*;

public class Inversions {

    public int[] generate(int n, long k) {        

        // Don't mind these special cases

        if (n == 1) {

            int[] arr = {1};

            return arr;
        }

        if (k == 0) {

            int[] arr = new int[n];

            for (int i = 0; i < n; i++) {

                arr[i] = 1;                    
            }

            return arr;
        }

        int[] arr = new int[n];

        for (int i = 0; i < n; i++) {

            arr[i] = i + 1;
        } 

        int inversions = 0;
        int i = 0;    

        while (inversions < k && i < n) {                                    

            int j = i - 1;                        

            while (j >= 0 && arr[j] < arr[j + 1] && inversions < k) {

                int helper = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = helper;
                inversions++;
                j--;
            }     

            i++;
        }

        return arr;
    }
}

以及用于测试不同输入数组的主类:

public class Main {

    public static void main(String[] args) {

        Inversions in = new Inversions();
        int[] arr1 = in.generate(4,3);
        int[] arr2 = in.generate(4,0);
        int[] arr3 = in.generate(4,6);        

        System.out.println(Arrays.toString(arr1)); // [3,1,4,2]
        System.out.println(Arrays.toString(arr2)); // [1,1,1,1]
        System.out.println(Arrays.toString(arr3)); // [4,3,2,1]
    }
}

该算法不会返回与样本结果完全相同的数组,而是通过所有测试,但输入大小很大的测试除外。我还尝试了合并排序的其他变体,因为它可以在O(n log n time)下工作,但无济于事。

如果你们有一些想法,那就太好了。如果您不熟悉Java,没关系,欢迎使用伪代码或任何其他建议!

8 个答案:

答案 0 :(得分:4)

如果反转数组中初始的 m 个元素,则会创建 m(m-1)/ 2 个反转。

如果反转初始的 m + 1 个元素,则会创建 m(m + 1)/ 2 个反转。

两者之间的差异仅为 m

所以:

  1. 生成排序数组
  2. 找到最大的 m ,使 m(m-1)/ 2 <= k
  3. 反转数组中的前 m 个元素以创建 m(m-1)/ 2 个反转
  4. 将下一个元素向前移动 k-m(m-1)/ 2 个位置,以创建其余所需的反演。

这需要 O(n)时间,这比您所需的时间要好。

答案 1 :(得分:1)

如果k> = n-1,则将元素n-1首先放入数组中,以便将其与n-1个元素取反;否则,将其放在数组的最后,以便将其与0个元素反转。继续使用这种 greedy 方法来确定其余元素的位置。

这是一个实现了generate()的解决方案,它只需一点点数学就可以在线性时间内运行。

public class Inversions {

  public static int[] generate(int n, long k) {

    int[] array = new int[n];
    
    // locate k in various sums of (n-1), (n-2), ..., 1
    int a = (int) Math.sqrt((n * (n - 1) - 2 * k)); // between the sum of [(n-1)+...+(n-a)] and the sum of [(n-1)+...+(n-a-1)]
    int b = n - 1 - a; // counts of (n-1), (n-2), ..., (n-a)
    int c = (int) (k - n * b + (b * b + b) / 2); // spillover = k - [(n-1)+(n-b)]*b/2;

    // put elements in the array
    for (int i = 0; i < b; i++) {
        array[i] = n - 1 - i;
    }
    for (int i = b; i < n - 1 - c; i++) {
        array[i] = i - b;
    }
    array[n - 1 - c] = n - 1 - b;
    for (int i = n - c; i < n; i++) {
        array[i] = i - b - 1;
    }

    return array;

  }

  public static void main(String[] args) {

    int n = Integer.parseInt(args[0]);
    long k = Long.parseLong(args[1]);

    for (int i = 0; i < n; i++) {
        StdOut.print(generate(n, k)[i] + " ");
    }

  }
}

答案 2 :(得分:0)

另一种O(n)算法:从排序数组开始。交换第一个和最后一个元素时,将得到x = 2 * (n-2) + 1倒置。考虑将这两个元素固定并仅在其余阵列上工作。如果x太大,请考虑使用较小的数组。只要需要,就重复一次。

未经测试的代码:

for (int first=0, last = n-1; remainingInversions>0; ) {
    int x = 2 * (last-first-1) + 1;
    if (x <= remainingInversion) {
        first++;
        last--;
        remainingInversion -= x;
    } else {
        last--; // consider a smaller array
    }
}

答案 3 :(得分:0)

实际上,每次将最后一个元素与之前的元素交换时,反转的数量都会增加。这是一个Java解决方案:

public static int[] generate(int n, long k) {
    int[] arr = new int[n];

    for(int i = 0; i < n; i++) {
        arr[i] = i;
    }

    long inversions = 0;
    int j = (n-1);
    int s = 0;

    while(inversions < k) {
        int temp = arr[j];
        arr[j] = arr[j-1];
        arr[j-1] = temp;

        inversions++;
        j--;
        if(j == s) {
            j = (n-1);
            s++;
        }
    }

    return arr;
}

答案 4 :(得分:0)

我在Python中实现了具有 O(n)复杂性的实现。

它基于两个规则。

  1. 反转大小为m的数组将得到m*(m-1)/2倒数。
  2. 将元素移动m个位置,将创建m倒置。
def get_m(k):
    m=0
    while m*(m-1)/2<=k:
        m+=1
    else:
        m-=1
    return m

def generate(l, k):
    """
    Generate array of length l with k inversions.
    """
    # Generate a sorted array of length l
    arr = list(range(0,l))
    
    # If no inversions are needed, return sorted array. 
    if k==0:
        return arr
    
    # Find largest m such that m*(m-1)/2 <= k
    m=get_m(k)

    # Reverse first m elements in the array which will give m*(m-1)/2 inversions
    arr = arr[m-1::-1]+arr[m:]

    # Calculate for any remaining inversions
    remaining_k = k-(m*(m-1)/2)

    # For remaining inversions, move the last element to its left by remaining_k
    if remaining_k>0:
        arr.insert(int(len(arr)-remaining_k - 1), arr[-1])
        arr = arr[:-1]
    return arr

if __name__ == '__main__':
    l = int(sys.argv[1])
    k = int(sys.argv[2])
    arr = generate(l, k)
    print(arr)

答案 5 :(得分:0)

@zhong yang:在预期范围0 <= k <= n(n-1)/ 2上很好地工作,但是如果k超出此范围,则抛出异常或null会更好范围而不是返回一些数组!

答案 6 :(得分:0)

有一个非常简单的方法来创建n个反转... 那就是将最后一个元素移到最前面。 由于使用了额外的内存,它的效率不高,但是我会做这样的事情:

创建一个长度为n的两倍的数组。 如果我们使用Integer []而不是int [],则从一开始到中间都填充一个前哨(即null)。 从中间填充,升序。 然后执行以下操作... 我确定我有一个错误和其他错误,但是总体思路已在下面的代码中捕获。

int start = 0;
int mid = arr.length / 2;
int end = arr.length - 1;

while (v > 0)
{
    if (v < (end - mid))
    {
        arr[start++] = arr[mid + v];
        arr[mid + v] = null;
    }
    else
    {
        arr[start++] = arr[end];
        v -= (end - mid);
        end--;
    }
}

因此,我们有一个数组,其中填充了起始值,一堆空值,然后是原始增量值(其中一个可能已变为空值)和一个“结束”指针,该指针指向原始区域的中间。 / p>

所以最后一步是从0-> endPos(忽略空值)复制到最终数组。

答案 7 :(得分:0)

逻辑并不难。例如,我们有 10 个数字 [0,1,2,3,4,5,6,7,8,9] 来生成 18 个倒数。首先在0前插入9,--->[9,0,1,2,3,4,5,6,7,8],产生9个反转。还剩下 9 个反转,所以我们在 0 之前插入 8 个,---->[9,8,0,1,2,3,4,5,6,7],所以我们得到了额外的 8 个反转。最后,还剩 1 个反转,我们在 6---->[9,8,0,1,2,3,4,5,7,6] 之前插入 7。在这种情况下,我只使用数组。该程序以 O(n) 复杂度运行。以下代码仅考虑 n 个数字 (0,1,2.....n-1) 及其反转。

    public static int[] generate(int n, long k) {
    int[] a = new int[n];
    int[] b = new int[n];
    for (int i = 1; i < n; i++) {
        a[i] = 1 + a[i - 1];
    }
    if (n == 0 || k == 0) return a;
    else {
        int i = 0;
        while (k > 0) {
            if (k > n - i - 1) {
                b[i] = a[n - 1 - i];
            }
            else {
                //auxilary array c to store value 
                int[] c = new int[(int) (k + 1)];
                for (int j = i; j < n - 1 - k; j++) {
                    b[j] = j - i;
                }
                for (int j = (int) (n - 1 - k); j < n; j++) {
                    c[j - (int) (n - 1 - k)] = j - i;
                }
                b[(int) (n - 1 - k)] = c[(int) k];
                for (int j = (int) (n - k); j < n; j++) {
                    b[j] = c[j - (int) (n - k)];
                }
                break;
            }
            k = k - (n - 1 - i);
            i++;

        }
        return b;
    }
}