在O(logn)时间内找到第k个最小数字

时间:2013-03-06 01:24:59

标签: algorithm

以下是问题,一个未排序的数组a[n],我需要在kth范围内找到[i, j]最小数字,绝对1<=i<=j<=n, k<=j-i+1

通常我会使用quick-find来完成这项工作,但如果有很多不同范围[i, j]的查询请求,那么速度不够快,我很难找到一个算法来进行查询{ {1}}时间(允许预处理)。

任何想法都表示赞赏。

PS

让我更容易理解问题。允许任何类型的预处理,但查询需要在O(logn)时间内完成。并且会有很多(超过1个)查询,例如找到O(logn)

通过范围[i,j] 我的意思是在原始数组中,没有排序或其他东西。

例如,1st in range [3,7], or 3rd in range [10,17], or 11th in range [33, 52],查询a[5] = {3,1,7,5,9}1st in range [3,4]52nd in range [1,3]53rd in range [0,2]。< / p>

6 个答案:

答案 0 :(得分:7)

如果允许预处理而不计入时间复杂度,只需使用它来构建子列表,以便您可以有效地找到您正在寻找的元素。与大多数优化一样,这会延长时间。

您的预处理步骤是获取n个数字的原始列表,并创建一些新的子列表。

这些子列表中的每一个都是原始的一部分,从n元素开始,扩展为m元素,然后排序。所以你的原始列表:

 {3, 1, 7, 5, 9}

给你:

 list[0][0] = {3}
 list[0][1] = {1, 3}
 list[0][2] = {1, 3, 7}
 list[0][3] = {1, 3, 5, 7}
 list[0][4] = {1, 3, 5, 7, 9}

 list[1][0] = {1}
 list[1][1] = {1, 7}
 list[1][2] = {1, 5, 7}
 list[1][3] = {1, 5, 7, 9}

 list[2][0] = {7}
 list[2][1] = {5, 7}
 list[2][2] = {5, 7, 9}

 list[3][0] = {5}
 list[3][1] = {5,9}

 list[4][0] = {9}

这不是一个便宜的操作(在空间中),所以你可能想在列表上维护一个“脏”标志,这样你只能在修改后第一次执行它操作(插入,删除,更改)。

事实上,即使更多效率,您也可以使用延迟评估。当您启动时以及执行修改操作时,基本上将所有子列表设置为空列表。然后,每当您尝试访问子列表并且它为空时,在尝试从中获取k值之前,计算该子列表(仅限该列表)。

确保在需要时仅评估子列表并缓存以防止不必要的重新计算。例如,如果您从未要求3到6子列表中的值,则永远不会计算它。

用于创建所有子列表的伪代码基本上是(for包含两端的循环):

for n = 0 to a.lastindex:
    create array list[n]
    for m = 0 to a.lastindex - n
        create array list[n][m]
        for i = 0 to m:
            list[n][m][i] = a[n+i]
        sort list[n][m]

懒惰评估的代码有点复杂(但只是一点点),所以我不会为此提供伪代码。

然后,为了找到ki范围内的j最小数字(其中ij是原始索引) ,你只需查看lists[i][j-i][k-1],这是一个非常快速的O(1)操作:

                +--------------------------+
                |                          |
                |                          v
1st in range [3,4] (values 5,9),   list[3][4-3=1][1-1-0] = 5
2nd in range [1,3] (values 1,7,5), list[1][3-1=2][2-1=1] = 5
3rd in range [0,2] (values 3,1,7), list[0][2-0=2][3-1=2] = 7
|             |                         ^    ^    ^
|             |                         |    |    |
|             +-------------------------+----+    |
|                                                 |
+-------------------------------------------------+

以下是一些Python代码,其中显示了这一点:

orig = [3,1,7,5,9]
print orig

print "====="
list = []
for n in range (len(orig)):
    list.append([])
    for m in range (len(orig) - n):
        list[-1].append([])
        for i in range (m+1):
            list[-1][-1].append(orig[n+i])
        list[-1][-1] = sorted(list[-1][-1])
        print "(%d,%d)=%s"%(n,m,list[-1][-1])

print "====="
# Gives xth smallest in index range y through z inclusive.
x = 1; y = 3; z = 4; print "(%d,%d,%d)=%d"%(x,y,z,list[y][z-y][x-1])
x = 2; y = 1; z = 3; print "(%d,%d,%d)=%d"%(x,y,z,list[y][z-y][x-1])
x = 3; y = 0; z = 2; print "(%d,%d,%d)=%d"%(x,y,z,list[y][z-y][x-1])
print "====="

正如所料,输出是:

[3, 1, 7, 5, 9]
=====
(0,0)=[3]
(0,1)=[1, 3]
(0,2)=[1, 3, 7]
(0,3)=[1, 3, 5, 7]
(0,4)=[1, 3, 5, 7, 9]
(1,0)=[1]
(1,1)=[1, 7]
(1,2)=[1, 5, 7]
(1,3)=[1, 5, 7, 9]
(2,0)=[7]
(2,1)=[5, 7]
(2,2)=[5, 7, 9]
(3,0)=[5]
(3,1)=[5, 9]
(4,0)=[9]
=====
(1,3,4)=5
(2,1,3)=5
(3,0,2)=7
=====

答案 1 :(得分:6)

当前解决方案是O((logn)^ 2)。我很确定它可以修改为在O(logn)上运行。该算法优于paxdiablo算法的主要优点是空间效率。该算法需要O(nlogn)空间,而不是O(n ^ 2)空间。

首先,从长度为m和n的两个排序数组中找到第k个最小元素的复杂性是O(logm + logn)。从长度a,b,c,d ..的数组中找到第k个最小元素的复杂度是O(loga + logb + .....)。

现在,对整个数组进行排序并存储它。对数组的前半部分和后半部分进行排序并存储,依此类推。您将有1个排序长度为n的数组,2个排序长度为n / 2的数组,4个排序长度为n / 4的数组,依此类推。所需总内存= 1 * n + 2 * n / 2 + 4 * n / 4 + 8 * n / 8 ... = nlogn。

一旦你有i和j弄清楚子阵列的列表,当连接时,给你范围[i,j]。将有数组的登录号。在其中找到第k个最小数字将花费O((logn)^ 2)时间。

最后一段的示例: 假设数组的大小为8(索引从0到7)。您有以下排序列表:

A:0-7,B:0-3,C:4-7,D:0-1,E:2-3,F:4-5,G:6-7。

现在构造一个带有指向这些数组的指针的树,以便每个节点都包含其直接成分。 A将是root,B和C是其子级等等。

现在实现一个返回数组列表的递归函数。

def getArrays(node, i, j):
    if i==node.min and j==node.max:
        return [node];

    if i<=node.left.max:
        if j<=node.left.max:
            return [getArrays(node.left, i, j)];  # (i,j) is located within left node
        else:
            return [ getArrays(node.left, i, node.left.max), getArrays(node.right, node.right.min, j) ]; # (i,j) is spread over left and right node 
    else:
        return [getArrays(node.right, i, j)]; # (i,j) is located within right node

答案 2 :(得分:3)

预处理:创建一个nxn数组,其中[k] [r]元素是第一个r元素的第k个最小元素(为方便起见,为1个索引)。

然后,给定一些特定范围[i,j]和k的值,执行以下操作:

  1. 在矩阵的[k] [j]槽处找到元素;叫这个x。
  2. 沿着矩阵的i-1列向下查找其中有多少个值小于或等于x(将列0视为具有0个较小的条目)。通过构造,此列将被排序(所有列将被排序),因此可以在日志时间中找到它。调用此值s
  3. 在矩阵的[k + s] [j]槽中找到该元素。这是你的答案。
  4. 例如,给出3 1 7 5 9

    • 3 1 1 1 1
    • X 3 3 3 3
    • X X 7 5 5
    • X X X 7 7
    • X X X X 9

    现在,如果我们要求[2,4]范围内的第二个最小值(再次,1个索引),我首先找到[1,4]范围内的第二个最小值,即3.然后看看第1列,看到有1个元素小于或等于3.最后,我发现在[3] [5]时隙的[1,4]范围内的第3个最小值,根据需要为5。

    这需要n ^ 2个空格和log(n)查找时间。

答案 3 :(得分:1)

这个不需要预处理,但比O(logN)慢一些。它比天真的迭代和计数快得多,并且可以支持对序列的动态修改。

就是这样。假设某些n的{​​{1}}长度为n=2^x。构造一个 segment-tree ,其根节点代表x。对于每个节点,如果它代表节点[0,n-1][a,b],则让它有两个子节点,每个节点代表b>a[a,(a+b)/2]。 (也就是说,做一个递归的除以二)。

然后,在每个节点上,为该段中的数字维护单独的二叉搜索树。因此,对序列的每次修改都需要[(a+b)/2+1,b]查询可以这样完成,让O(logN)[on the segement]*O(logN)[on the BST].在段Q(a,b,x)内成为x的 rank 。显然,如果可以有效地计算[a,b]Q(a,b,x)上的二分搜索可以有效地计算所需的答案(使用额外的x因子。

O(logE)可以计算为:找到构成Q(a,b,x)的最小段数,可以在段树上的[a,b]中完成。对于每个段,在该段的二叉搜索树上查询小于O(logN)的元素数。添加所有这些数字即可获得x

这应该是Q(a,b,x)。那不完全是你所要求的。

答案 4 :(得分:0)

在O(log n)时间内,无法读取数组的所有元素。由于它没有排序,并且没有其他提供的信息,这是不可能的。

答案 5 :(得分:-1)

在最差和普通情况下,你无法比O(n)做得更好。你必须看看每一个元素。