计算数组位置的越来越小的值

时间:2013-09-24 23:10:33

标签: java arrays algorithm optimization data-structures

我有以下需要优化的问题。对于给定数组(允许重复键),对于数组中的每个位置i,我需要计算i右侧的所有较大值,以及i左侧的所有较小值。如果我们有:

1 1 4 3 5 6 7i = 3(值3),i左侧较小值的计数为1(无重复键),右侧为较大值的数量为3

这个问题的强力解决方案是~N^2,我可以通过一些额外的空间来计算较大值的较小值,从而将复杂度降低到~(N^2)/2。 我的问题是:有没有更快的方法来完成它?也许NlgN?我想有一个数据结构,我不知道哪个允许我更快地进行计算。

编辑:谢谢大家的回复和讨论。你可以找到两个好的解决方案,下面的问题。总是很高兴从stackoverflow中的开发人员那里学习。

4 个答案:

答案 0 :(得分:2)

尝试用于解决RMQ的分段树数据结构。 它会准确地给你n log n。

通常会查看RMQ problem,您的问题可能会减少。

答案 1 :(得分:2)

您要求O(n log n),我在技术上给您一个O(n)解决方案。

正如@SayonjiNakate暗示的那样,使用分段树的解决方案(我在实现中使用Fenwick树)在O(n log M)时运行,其中M是数组中可能的最大值。假设M是常量(嘿它受int的大小限制!),算法在O(n)中运行。对不起,如果你觉得我在报告复杂性时作弊,但是嘿,从技术上来说这是真的! = d

首先,请注意问题“左边较小元素的数量”通过反转和否定数组等同于“右边较大元素的数量”的问题。所以,在我下面的解释中,我只描述了“左边较小元素的数量”,我称之为“lesser_left_count”。

lesser_left_count的

算法

我们的想法是能够找到小于特定数字的总数。

  1. 定义大小最高为tree的数组MAX_VALUE,其中会为看到的数字存储值1,否则会存储0

  2. 然后,当我们遍历数组时,当我们看到数字num时,只需将值1指定给tree[num]更新操作) 。然后,对于数字num,lesser_left_count是从1num-1总和操作)的总和到目前为止,因为当前位置左边的所有较小数字都会已设置为1

  3. 简单吧?如果我们使用Fenwick tree,则可以在O(log M)时间内完成更新和求和操作,其中M是数组中的最大可能值。由于我们迭代数组,如果我们将O(n log M)视为常量(在我的代码中我将其设置为O(n),则总时间为M,或仅2^20-1 = 1048575,因此它是O(20n),即O(n)

    朴素解决方案的唯一缺点是它使用了大量内存,因为M变得更大(我在代码中设置了M=2^20-1,这需要大约4MB的内存)。这可以通过将数组中的不同整数映射到较小的整数(以保持顺序的方式)来改进。通过对数组进行排序,可以简单地在O(n log n)中完成映射(确定,这会产生复杂性O(n log n),但正如我们所知n < M,您可以将其视为O(n) )。因此,数字M可以重新解释为“数组中不同元素的数量”

    因此内存不再是任何问题,因为如果在这个改进之后你确实需要大量内存,那就意味着你的数组中有许多不同的数字,以及{的时间复杂度{1}}已经太高而无法在普通机器中计算出来。

    为了简单起见,我没有在代码中包含这种改进。

    哦,由于Fenwick树仅适用于正数,我将数组中的数字转换为最小值1.请注意,这不会改变结果。

    Python代码:

    O(n)

    将产生:

    Original array:    [1, 1, 3, 2, 4, 5, 6]
    Lesser left count: [0, 0, 1, 1, 3, 4, 5]
    Greater right cnt: [5, 5, 3, 3, 2, 1, 0]
    

    或者如果你想要Java代码:

    MAX_VALUE = 2**20-1
    f_arr = [0]*MAX_VALUE
    
    def reset():
        global f_arr, MAX_VALUE
        f_arr[:] = [0]*MAX_VALUE
    
    def update(idx,val):
        global f_arr
        while idx<MAX_VALUE:
            f_arr[idx]+=val
            idx += (idx & -idx)
    
    def cnt_sum(idx):
        global f_arr
        result = 0
        while idx > 0:
            result += f_arr[idx]
            idx -= (idx & -idx)
        return result
    
    def count_left_less(arr):
        reset()
        result = [0]*len(arr)
        for idx,num in enumerate(arr):
            cnt_prev = cnt_sum(num-1)
            if cnt_sum(num) == cnt_prev: # If we haven't seen num before
                update(num,1)
            result[idx] = cnt_prev
        return result
    
    def count_left_right(arr):
        arr = [x for x in arr]
        min_num = min(arr)
        if min_num<=0:                       # Got nonpositive numbers!
            arr = [min_num+1+x for x in arr] # Convert to minimum 1
        left = count_left_less(arr)
        arr.reverse()                        # Reverse for greater_right_count
        max_num = max(arr)
        arr = [max_num+1-x for x in arr]     # Negate the entries, keep minimum 1
        right = count_left_less(arr)
        right.reverse()                      # Reverse the result, to align with original array
        return (left, right)
    
    def main():
        arr = [1,1,3,2,4,5,6]
        (left, right) = count_left_right(arr)
        print 'Array: ' + str(arr)
        print 'Lesser left count: ' + str(left)
        print 'Greater right cnt: ' + str(right)
    
    if __name__=='__main__':
        main()
    

    会产生相同的结果。

答案 2 :(得分:2)

这是一个相对简单的解决方案,O(N lg(N))不依赖于有限多个整数中的条目(特别是,它应该适用于任何有序的数据类型)。

我们假设输出存储在两个数组中; lowleft[i]最后会包含x[j]j < i的不同值x[j] < x[i]的数量,而highright[i]最后会包含不同值的数量{ {1}} x[j]j > i

创建一个平衡的树数据结构,在每个节点中维护以该节点为根的子树中的节点数。这是相当标准的,但我认为不是Java标准库的一部分;做AVL树可能最容易。节点中值的类型应该是数组中值的类型。

现在首先通过数组迭代前进。我们从一个空的平衡树开始。对于我们遇到的每个值x[j] > x[i],我们将其输入到平衡树中(在该树的末尾附近有x[i]个条目,因此此步骤需要O(N)次。在搜索要输入O(lg(N))的位置时,我们会在每次采用正确的子树时,通过将所有左子树的大小相加来跟踪小于x[i]的值的数量,并添加将是x[i]左子树的大小。我们将此号码输入x[i]

如果值lowleft[i]已经在树中,我们继续进行该循环的下一次迭代。如果值x[i]不在那里,我们输入它并重新平衡树,注意正确更新子树大小。

此循环的每次迭代都需要x[i]个步骤,总计O(lg(N))。我们现在从一个空树开始,并做同样的事情,通过数组迭代向后,找到树中每个O(N lg(N))的位置,并且每次记录所有子树的大小到新节点的权限为x[i]。因此总复杂度为highright[i]

答案 3 :(得分:0)

这是一个应该给你O(NlgN)的算法:

  1. 迭代列表一次并构建key => indexList的地图。因此,对于永远的键(数组中的元素),您存储该键在该数组中的所有索引的列表。这将采用O(N)(遍历列表)+ N*O(1)(将N个项目附加到列表)步骤。所以这一步是O(N)。第二步要求对这些列表进行排序,因为我们从左到右迭代列表,所以列表中新插入的索引总是比已经存在的所有其他索引大。

  2. 再次迭代列表,并为每个元素搜索索引列表中所有键,这些键大于当前索引之后的第一个索引的当前元素。这将为您提供当前元素右侧的元素数量,这些元素的数量大于当前元素。由于索引列表已排序,您可以进行二进制搜索,其中O(k * lgN)步骤k是大于当前键的键数。如果键的数量有一个上限,那么就big-O而言这是一个常数。这里的第二步是搜索所有较小的键,并在列表中找到当前索引之前的第一个索引。这将为您提供当前较小的元素的数量。与上述相同的推理是O(k * lgN)

  3. 因此,假设密钥数量有限,如果我没有弄错的话,这应该给你O(N) + N * 2 * O(lgN)整体O(NlgN)

    修改:伪代码:

    int[] list;
    map<int => int[]> valueIndexMap;
    foreach (int i = 0; i < list.length; ++i) {       // N iterations
       int currentElement = list[i];                     // O(1)
       int[] indexList = valueIndexMap[currentElement];  // O(1)
       indexList.Append(i);                              // O(1)
    }
    
    foreach (int i = 0; i < list.length; ++i) {  // N iterations
        int currentElement = list[i];            // O(1)
        int numElementsLargerToTheRight;
        int numElementsSmallerToTheLeft;
        foreach (int k = currentElement + 1; k < maxKeys; ++k) {  // k iterations with k being const
           int[] indexList = valueIndexMap[k];                                            // O(1)
           int firstIndexBiggerThanCurrent = indexList.BinaryFindFirstEntryLargerThan(i); // O(lgN)
           numElementsLargerToTheRight += indexList.Length - firstIndexBiggerThanCurrent;  // O(1)
        }
        foreach (int k = currentElement - 1; k >= 0; --k) {  // k iterations with k being const
           int[] indexList = valueIndexMap[k];                                            // O(1)
           int lastIndexSmallerThanCurrent = indexList.BinaryFindLastEntrySmallerThan(i); // O(lgN)
           numElementsSmallerToTheLeft += lastIndexSmallerThanCurrent;                    // O(1)
        }
    }
    

    更新:如果有人有兴趣,我会在with a C# implementation附近修补一下;