# key()-less grouping tests, including divide-and-conquer (DVC) # From: "Robbert van Dalen" # posted to xsl-list on 22-Mar-2003. from Xml.Xslt import test_harness # The author mistakenly thought that key() could not be used on # result tree fragments that had been converted to node-sets, so # he devised an efficient DVC-based algorithm and several examples # that led up to it. # Input XML (taken from Michael Kay's book) # source_1 = """ """ # The following example will give you an idea of grouping # tree-fragments without using the key() function. # (partly copied from Michael Kay's book) # # 1) First the cities are sorted on the @country attribute. After this, cities # that share the same @country value will be following each other, which is a # property we can exploit in step 2. # # 2) Then the template that matches city nodes will be called N times if there are # N cities to be grouped. For each city node in the sorted set the # 'following-sibling::*[1]' node(s) are matched. If they're not equal, the city # node will mark a new group. # # As Michael Kay already pointed out in his book, the efficiency of this approach # depends on the implementation of 'following-sibling::*[1]'. If this expression # has time complexity O(1) then the overall time complexity of getting all the # groups will be O(N) (leaving sorting out of the equation). # # 3) Strangely enough, the last step is actually the most problematic. Let's say # the second step gave us 3 groups. Then, for each group, the expression # '$sorted-tree-fragment[country = $country] will be evaluated with time # complexity O(N). # # So, does this mean the overall time complexity will be 3*N = O(N)? # The answer is definitely no! It does hold for a small number of groups, but if # we have N/2 groups then time complexity will be O(N^2). # Selecting nodes with XPATH expressions is usually OK, but in this example we # want to select the K cities that share the same @country value in O(K) time, not # O(N) time. # # So the question we really want to anwer is: 'how can we efficiently select a # subset of nodes without traversing them all?'. The anwser is: 'this all depends # on the selection criterium.' # Still, if the selection criterium isn't too complex, we can still hope for a # better solution. # One solution is that we don't use XPATH expressions to select nodes, but rather # walk through the nodes by using recursive calls. # sheet_1 = """ """ expected_1 = """ """ # GROUPING USING RECURSION # # One idea to reduce time complexity of the previous example is by slightly # modifying the match='city' template [...see original post for more...] # # The time complexity of the recursive solution can be proven to be O(N) but with # the recursion depth also to be O(N). # # Unfortunately, most XSLT implementations have a maximum recursion depth (~1000) # so this is not a general solution. # source_2 = source_1 sheet_2 = """ """ expected_2 = expected_1 # DVC AND THE BINARY TREE # # Dimitre Novatchev was one of the first to mention Divide and Conquer (DVC) # algorithms to reduce recursion depth. Because most XSLT implementations out # there still do not support tail-recursion elimination, DVC is the way to go if # you want to process a lot of nodes. # # The idea behind DVC is that to attack a big problem, you should divide it into a # number of smaller problems. # # Not surprisingly, dividing a problem into just 2 subproblems is enough to reduce # recursion depth to be O(log2(N)). # # The following example will give you an idea of how this works: # source_3 = """ """ sheet_3 = """ """ # The result is what is called a binary tree representation. At first this # representation doesn't seem all that useful. Later we will see that specialised # binary trees can be (re-)used to implement almost any recursive function without # exceeding the maximum recursion depth. # expected_3 = """ """ # Let's sum all the @v values with the use of the binary (fragment) tree: # # [...] the overall 'copy' complexity is O(log2(N)*N). # # Although the number of recursive calls is O(N) the XSLT processor still spends # at least O(log2(N)*N) time because it must copy (and select) half of the nodes # for the each recursive call (twice). # # Copying nodes should be avoided as much as possible because it slows down # recursion considerably. # source_4 = expected_3 sheet_4 = """ """ expected_4 = "36" # MODIFIED DVC ALGORITHM: RANGE PARTITIONING # # The following implementation of a binary partition doesn't copy a list of nodes # but just one node at each recursive call. It uses the so called 'sibling' axis # to walk through the list. Because there are O(N) recursive calls, this means # that O(N) nodes are copied. Does this mean that the overall time complexity will # be O(N) too? The answer is: probably yes, but at worst it will be O(N^2). # # Let's compare overall time complexity with the possible implementations of # 'following-sibling::[w]' # # following-sibling::*[w] | total time # _____________________________________ # O(1) | O(N) # O(w) | O(log2(N)*N) # O(N) | O(N^2) # # So at worst it will be quadratic. So the question still remains if it is # theoretically possible to do binary partitioning without copying to much nodes. # Nevertheless, experiments with XALAN have shown that the implementation is not # quadratic. # source_5 = source_3 sheet_5 = """ """ expected_5 = """ """ # GROUPING WITH A BINARY TREE # # The new and improved grouping algorithm is more or less the same as the first # one except where using ranges to select nodes which are in the same group. # Thus: # # 1) we sort the nodes for a given key # 2) then compute the ranges of nodes which have the same key # 3) and then select the (sorted) nodes for each range. # # To efficiently select a range of nodes we will be using the binary tree. # # Here's the whole solution: # source_6 = source_1 sheet_6 = """ """ # note this is missing the group ids as compared to the first example # expected_6 = """ """ def Test(tester): source = test_harness.FileInfo(string=source_1) sheet = test_harness.FileInfo(string=sheet_1) test_harness.XsltTest(tester, source, [sheet], expected_1, title='grouping without keys') source = test_harness.FileInfo(string=source_2) sheet = test_harness.FileInfo(string=sheet_2) test_harness.XsltTest(tester, source, [sheet], expected_2, title='grouping using recursion') source = test_harness.FileInfo(string=source_3) sheet = test_harness.FileInfo(string=sheet_3) test_harness.XsltTest(tester, source, [sheet], expected_3, title='generate a binary tree') source = test_harness.FileInfo(string=source_4) sheet = test_harness.FileInfo(string=sheet_4) test_harness.XsltTest(tester, source, [sheet], expected_4, title='sum nodes in a binary tree') source = test_harness.FileInfo(string=source_5) sheet = test_harness.FileInfo(string=sheet_5) test_harness.XsltTest(tester, source, [sheet], expected_5, title='generate a binary tree with range partitioning') source = test_harness.FileInfo(string=source_6) sheet = test_harness.FileInfo(string=sheet_6) test_harness.XsltTest(tester, source, [sheet], expected_6, title='efficient divide-and-conquer based grouping') return