*x 对数组/列表中的元素进行 *排名 只是为了找出数组/列表中严格小于x的元素。
x
因此,对列表进行排名只是获得列表中所有元素的排名。
例如,,rank [51, 38, 29, 51, 63, 38] = [3, 1, 0, 3, 5, 1]即有3个元素小于51,以此类推。
rank [51, 38, 29, 51, 63, 38] = [3, 1, 0, 3, 5, 1]
可以在O(NlogN)中对列表进行排名。基本上,我们可以在记住每个元素的原始索引的同时对列表进行排序,然后查看每个元素的编号。
这里的问题是 如何在列表中对后缀进行排名O(NlogN)?
O(NlogN)
对列表的后缀进行排名意味着:
用于清单[3; 1; 2],等级[[3; 1; 2]; [1; 2]; [2]
请注意,元素可能并不不同。
编辑
我们不需要打印所有后缀的所有元素。您可以想象我们只需要打印出一个列表/数组,其中每个元素都是后缀的等级。
例如,等级后缀_of_ [3; 1; 2] =等级[[3; 1; 2]; [1; 2]; [2]] = [2; 0; 1],而您只打印了[2; 0; 1]。
编辑2
让我在这里更清楚地解释什么是 所有后缀, 以及什么意味着 对所有后缀进行排序/排序 。
假设我们有一个数组/列表[e1; e2; e3; e4; e5]。
那么[e1; e2; e3; e4; e5]的所有后缀是:
[e1; e2; e3; e4; e5] [e2; e3; e4; e5] [e3; e4; e5] [e4; e5] [e5]
例如,[4; 2; 3; 1; 0]的所有后缀均为
[4; 2; 3; 1; 0] [2; 3; 1; 0] [3; 1; 0] [1; 0] [0]
超过5个后缀的排序意味着按字典顺序排序。在所有后缀之上排序,您会得到
[0] [1; 0] [2; 3; 1; 0] [3; 1; 0] [4; 2; 3; 1; 0]
顺便说一句,如果您无法想象如何在其中对5个列表/数组进行排序,只需考虑按字典顺序对字符串进行排序。
“ 0” <“ 10” <“ 2310” <“ 310” <“ 42310”
似乎对所有后缀进行排序实际上是对原始数组的所有元素进行排序。
但是,请注意,并非所有元素都可以区分,例如
对于[4; 2; 2; 1; 0],所有后缀为:
[4; 2; 2; 1; 0] [2; 2; 1; 0] [2; 1; 0] [1; 0] [0]
那么命令是
[0] [1; 0] [2; 1; 0] [2; 2; 1; 0] [4; 2; 2; 1; 0]
正如MBo正确指出的那样,您的问题是构造输入列表的后缀数组的问题。快速而复杂的算法实际上是线性时间,但是由于您只是针对O(n log n),所以我将尝试提出一个更易于实现的简单版本。
O(n log n)
O(n log² n)
让我们以序列[4, 2, 2, 1]为例。它的后缀是
[4, 2, 2, 1]
0: 4 2 2 1 1: 2 2 1 2: 2 1 3: 1
我用原始序列中的后缀索引了后缀。最终,我们希望按字典顺序对这组后缀进行快速排序。我们知道可以在恒定空间中使用其后缀索引来表示每个后缀,并且可以O(n log n)使用合并排序,堆排序或类似算法在比较中进行排序。因此问题仍然存在,我们如何快速比较两个后缀?
假设我们要比较后缀[2, 2, 1]和[2, 1]。我们可以填充负无穷大值的值,以更改比较结果:[2, 2, 1, -∞]和[2, 1, -∞, -∞]。
[2, 2, 1]
[2, 1]
[2, 2, 1, -∞]
[2, 1, -∞, -∞]
现在,这里的主要思想是以下分而治之的观察:而不是逐个字符地比较序列,直到找到两个不同的位置,我们可以将两个列表一分为二并按字典顺序比较两半:
[a, b, c, d] < [e, f, g, h] <=> ([a, b], [c, d]) < ([e, f], [g, h]) <=> [a, b] < [e, f] or ([a, b,] = [e, f] and [c, d] < [g, h])
本质上,我们已经将比较序列的问题分解为比较较小序列的两个问题。这导致以下算法:
步骤1 :对长度为1的子字符串(连续的子序列)进行排序。在我们的示例中,长度为1的子字符串为[4], [2], [2], [1]。每个子字符串都可以由原始列表中的起始位置表示。我们通过简单的比较排序对它们进行排序并得到[1], [2], [2], [4]。我们通过在列表的排序列表中分配其排名的每个位置来存储结果:
[4], [2], [2], [1]
[1], [2], [2], [4]
position substring rank 0 [4] 2 1 [2] 1 2 [2] 1 3 [1] 0
重要的是,我们将相同的等级分配给相等的子字符串!
步骤2: 现在我们要对长度为2的子字符串进行排序。实际上只有3个这样的子字符串,但是如果需要的话,我们通过填充负无穷大为每个位置分配一个。这里的窍门是,我们可以使用上面的“分而治之”的想法以及在步骤1中分配的等级来进行快速比较(这实际上不是必需的,但稍后将变得很重要)。
position substring halves ranks from step 1 final rank 0 [4, 2] ([4], [2]) (2, 1) 3 1 [2, 2] ([2], [2]) (1, 1) 2 2 [2, 1] ([2], [2]) (1, 0) 1 3 [1, -∞] ([1], [-∞]) (0, -∞) 0
步骤3: 您猜对了,现在我们对长度为4(!)的子字符串进行排序。这些恰好是列表的后缀!这次我们可以使用分治法和第二步的结果:
position substring halves ranks from step 2 final rank 0 [4, 2, 2, 1] ([4, 2], [2, 1]) (3, 1) 3 1 [2, 2, 1, -∞] ([2, 2], [1, -∞]) (2, 0) 2 2 [2, 1, -∞, -∞] ([2, 1], [-∞,-∞]) (1, -∞) 1 3 [1, -∞, -∞, -∞] ([1,-∞], [-∞,-∞]) (0, -∞) 0
大功告成!如果我们的初始序列具有size 2^k,那么我们将需要一些k步骤。或者反过来说,我们需要一些log_2 n步骤来处理一系列size n。如果其长度不是2的幂,则仅填充负无穷大。
2^k
k
log_2 n
n
对于实际的实现,我们只需要记住算法每一步的序列“最终排名”。
C ++中的实现可能看起来像这样(使用编译-std=c++11):
-std=c++11
#include <algorithm> #include <iostream> using namespace std; int seq[] = {8, 3, 2, 4, 2, 2, 1}; const int n = 7; const int log2n = 3; // log2n = ceil(log_2(n)) int Rank[log2n + 1][n]; // Rank[i] will save the final Ranks of step i tuple<int, int, int> L[n]; // L is a list of tuples. in step i, // this will hold pairs of Ranks from step i - 1 // along with the substring index const int neginf = -1; // should be smaller than all the numbers in seq int main() { for (int i = 0; i < n; ++i) Rank[1][i] = seq[i]; // step 1 is actually simple if you think about it for (int step = 2; step <= log2n; ++step) { int length = 1 << (step - 1); // length is 2^(step - 1) for (int i = 0; i < n; ++i) L[i] = make_tuple( Rank[step - 1][i], (i + length / 2 < n) ? Rank[step - 1][i + length / 2] : neginf, i); // we need to know where the tuple came from later sort(L, L + n); // lexicographical sort for (int i = 0; i < n; ++i) { // we save the rank of the index, but we need to be careful to // assign equal ranks to equal pairs Rank[step][get<2>(L[i])] = (i > 0 && get<0>(L[i]) == get<0>(L[i - 1]) && get<1>(L[i]) == get<1>(L[i - 1])) ? Rank[step][get<2>(L[i - 1])] : i; } } // the suffix array is in L after the last step for (int i = 0; i < n; ++i) { int start = get<2>(L[i]); cout << start << ":"; for (int j = start; j < n; ++j) cout << " " << seq[j]; cout << endl; } }
输出:
6: 1 5: 2 1 4: 2 2 1 2: 2 4 2 2 1 1: 3 2 4 2 2 1 3: 4 2 2 1 0: 8 3 2 4 2 2 1
的复杂度为O(log n * (n + sort)),这是O(n log² n)在此实现中的原因,因为我们使用了一种比较复杂性O(n log n)
O(log n * (n + sort))
如果我们设法在O(n)每个步骤中进行排序,那么我们将O(n log n)受到限制。因此,基本上我们必须对一对序列进行排序(x, y),其中0 <= x, y < n。我们知道,我们可以O(n)使用计数sort在给定范围内及时对整数序列进行排序。我们可以将对解释(x, y)为z = n * x + y以n 为底的数字。现在我们可以看到如何使用LSD基数排序对。实际上,这意味着我们通过y使用计数排序增加对数对进行排序,然后 再次 使用计数排序对增加进行排序x。由于计数排序是稳定的,因此这给了我们对中的字典顺序2 * O(n) = O(n)。因此,最终的复杂度是O(n log n)。
O(n)
(x, y)
0 <= x, y < n
z = n * x + y
y
2 * O(n) = O(n)
如果您有兴趣,可以在我的Github存储库中找到O(n log² n)该方法的实现。该实现有27行代码。整洁,不是吗?