The problem
I have been playing with one of my toy projects and ended up having to shuffle text many times in a file that is around 10MB long. A naïve implementation chopping a string and gluing it together again ended up being painfully slow:
\[ O(n \cdot k) \]where n is the length of the string and k is the number of shuffling operations.
A solution
It turns out that the problem can be efficiently solved using splay trees, where each node holds:
- the starting offset in the source string
- the length of the substring it represents
- the offset from the beginning of the substring taking into account all of the children on the left-hand-side
- total length of the substring taking into account all the children on the right-hand side
The find operation looks for a node containing the offset and splits it in two, if the node does not start with the offset.
1static Node *Find(uint32_t offset, Node *root)
2{
3 if(!root)
4 return 0;
5 if(offset >= root->offset && offset < (root->offset + root->length))
6 return root;
7 if(offset < root->offset)
8 return Find(offset, root->left);
9 return Find(offset-root->offset-root->length, root->right);
10}
11
12static Node *STFind(uint32_t offset, Node *root)
13{
14 Node *n = Find(offset, root);
15 Splay(n);
16
17 if(n->offset < offset) {
18 Node *newNode = new Node();
19 newNode->start = n->start;
20 newNode->length = offset - n->offset;
21 n->start = newNode->start + newNode->length;
22 n->length -= newNode->length;
23 newNode->left = n->left;
24 update(newNode);
25 n->left = newNode;
26 update(n);
27 }
28 return n;
29}
The actual shuffling boils down to splitting and merging sub-trees and producing the result to traversing the whole tree in-order.
1void process(int i, int j, int k)
2{
3 Node *n, *t, *left, *right = 0;
4 STSplit(left, n, i, pRoot);
5 if(j+1 < pS.length())
6 STSplit(n, right, j+1-i, n);
7 t = STMerge(left, right);
8 if(k < (t->offset + t->total_length))
9 STSplit(left, right, k, t);
10 else {
11 right = 0;
12 left = t;
13 }
14 pRoot = STMerge(left, n);
15 pRoot = STMerge(pRoot, right);
16}
The amortized complexity is:
\[ O(k \cdot log(k) + n) \]See rope-splay.cpp.
A test
It all runs pretty nicely for a test case with 10M characters and 100k shuffle operations:
]==> ./testgen 10000000 100000 > test
]==> time ./rope-naive < test > test-o-naive
./rope-naive < test > test-o-naive 386.17s user 66.19s system 99% cpu 7:32.86 total
]==> time ./rope-splay < test > test-o-splay
./rope-splay < test > test-o-splay 0.71s user 0.04s system 99% cpu 0.752 total
]==> diff test-o-splay test-o-naive
If you like this kind of content, you can subscribe to my
newsletter,
follow me on Twitter,
or subscribe to my RSS channel.