1

I was trying to write some sorting algorithms in prolog and find their complexity when I started thinking whether or not they are going to have a different complexity just because they are written in a logical language.

Take for example quicksort. It has an average complexity of nlogn and it's code(not complete) goes like this:

quicksort([Head|Tail], SortedList) :-
    split(Head, Tail, Left, Right),
    quicksort(Left, SortedLeft),
    quicksort(Right, SortedRight),
    append(SortedLeft, [Head|SortedRight], SortedList).

split has n. quicksort logn. Which gives the (average) nlogn. But what about append? It also has linear complexity. So is it overall (n^2)logn?

Doesn't the fact that in prolog we can only access the elements of a list in a linear way hurt the complexity of our programs. In that sense isn't it better to use another language like C for example?

false
  • 10,264
  • 13
  • 101
  • 209
Manos Ntoulias
  • 513
  • 1
  • 4
  • 21
  • 2
    You can view `append/3` as part of the cost of `split/4`. But nevertheless, `append/3` can be avoided altogether by using grammars see http://en.wikipedia.org/wiki/Prolog#Quicksort – false Jun 01 '14 at 13:20

1 Answers1

3

When determining the complexity of algorithms, you should take the same steps as in other languages, and keep in mind that Prolog programs have a procedural/imperative reading as well as a logical one. The main difference comes from the possibility of backtracking, which in this case isn't very relevant.

The complexity of this program, assuming none of the steps backtracks, is given by a recurrence

T(n) ≤ 2T(n/2) + splitT(n) + appendT(n)

You've already observed that append/3 takes linear time, so if split/4 takes linear time as well,

splitT(n) + appendT(n) = Θ(n)

and you get T(n) ≤ 2T(n/2) + Θ(n), which is the same recurrence as for imperative quicksort.

Doesn't the fact that in prolog we can only access the elements of a list in a linear way hurt the complexity of our programs. In that sense isn't it better to use another language like C for example?

As I've shown, this is unrelated to the complexity problem that you presented. Yes, for many problems idiomatic C programs are faster than idiomatic Prolog programs. For other problems, idiomatic Prolog programs are a few lines of code whereas the corresponding C programs would include half a Prolog interpreter.

What is relevant here is that in C, quicksort can be written with O(lg n) ''space'' complexity since you only need to represent the stack and can modify an array in-place. A Prolog list, by contrast, is immutable, so your program will build a new list and has linear space complexity. Whether that's good or bad depends on the use case.

Fred Foo
  • 355,277
  • 75
  • 744
  • 836
  • 1
    It's the worst case which makes quicksort daunting. And I believe also its nonstability (not sure how to fix this with reasonable effort) – false Jun 01 '14 at 13:21
  • @false The worst case is better in the [introsort](https://en.wikipedia.org/wiki/Introsort) variant, but for that you need heaps and those are non-trivial in Prolog too. – Fred Foo Jun 01 '14 at 13:50
  • Actually mergesort is the preferred sorting algorithm for linked lists. I never really understood smoothsort and I've never encountered an implementation in production code. – Fred Foo Jun 01 '14 at 14:00
  • 1
    I just checked [wikipedia](https://en.wikipedia.org/wiki/Smoothsort) to realize that this is something different from what I remembered. What I meant was a refined version of mergesort where you first scan for a prefix that can be sorted linearly: So [1,-1,2,-2,3,-4|...] would be *one* linear scan. This is possible in Prolog but not in functional languages. – false Jun 01 '14 at 14:04
  • 2
    [Some reference](http://stackoverflow.com/questions/8429479/sorting-a-list-in-prolog/8430692#8430692) of this refined mergesort. – false Jun 01 '14 at 14:21
  • Cool. But I don't see why you can't do that in FP: `sortedPrefix (x:y:xs) = if x <= y then ((x:srt), rest) else ([x], (y:ys)) where srt, rest = sortedPrefix (y:ys)` (add base cases). – Fred Foo Jun 01 '14 at 15:21
  • 1
    How does this produce [-4,-3,-2,-1,1,2,3] out of [1,-1,2,-2,3,-4]? – false Jun 01 '14 at 15:26
  • No, but it takes off a pre-sorted prefix and returns that along with the tail. A repetition of this will produce `[[1], [-1,2], [-2,3], [-4]]`. I'm convinced that clever exploitation of a lazy language will also give the optimization you describe; I've seen tail queues in Haskell. – Fred Foo Jun 01 '14 at 15:33