3

This is what I've done:

(define qsort
  (lambda (l)
    (let ((lesser '()))
      (let ((greater '()))
        (cond
          ((null? l) '())
          (else (map (lambda (ele)
                       (if (> (car l) ele)
                           (set! lesser (cons ele lesser))
                           (set! greater (cons ele greater)))) (cdr l))
                (append (qsort lesser) (cons (car l) (qsort greater))))
        )))))

I noticed that when provided with an already sorted list, it becomes extremely sluggish. After some searching, I found that if the "pivot" is selected in a random manner, the performance can be improved. However the only way I know to achieve this is by list-ref, and it seems to be O(n). To make matters even worse, I have to implement a cdr-like function to remove n-th element in the list, which might also be extremely inefficient.

Maybe I'm in the wrong direction. Could you give me some advice?

Will Ness
  • 70,110
  • 9
  • 98
  • 181
babel92
  • 767
  • 1
  • 11
  • 21
  • 1
    true quicksort runs on arrays, with in-place partitioning. e.g. see http://docs.racket-lang.org/r5rs-std/r5rs-Z-H-9.html#%_sec_6.3.6. – Will Ness Dec 19 '13 at 18:50
  • 1
    btw *mergesort* is worst-case O(n log n) i.e. it won't degenerate on sorted data; and lists are natural for it. Bottom-up variant is more amenable for making it work iteratively; and any non-trivial implementation of that starts with partitioning the input list into monotonic "runs", automatically converting the descending into ascending runs, so will run in O(n) time on sorted data. – Will Ness Dec 19 '13 at 19:45
  • 1
    As I did [here](http://stackoverflow.com/questions/19082032/quicksort-in-lisp#comment28209236_19082032), I'll point out the [Sheep Trick from _The Pitmanual_](http://www.maclisp.info/pitmanual/funnies.html#sheep_trick). It still uses the first element as a pivot though. Getting a random pivot would be O(n). – Joshua Taylor Dec 19 '13 at 20:18
  • @JoshuaTaylor Hi. I don't quite have a grasp on common lisp syntax... But if I understand it correctly, the sheep and goat are my lesser and greater. I think my program is nearly the same as it. – babel92 Dec 19 '13 at 20:43
  • They're very similar, yes. The sheep and goats are an allusion to [Matthew 25:31–46](http://www.biblegateway.com/passage/?search=Matthew+25:31-46&version=NASB) where the goats are on the left and the sheep are on the right. – Joshua Taylor Dec 19 '13 at 20:51
  • Also, it's not that far off from Scheme syntax, since Scheme also has [`do` for iteration](http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-7.html#%_sec_4.2.4). You're copying a lot of list structure, though, since `(append (qsort lesser) ...)` make a _copy_ of `(qsort lesser)`. – Joshua Taylor Dec 19 '13 at 20:52
  • @JoshuaTaylor Ah got it. I'm going to make it neater. Thank you:) – babel92 Dec 19 '13 at 21:08

3 Answers3

5

true quicksort runs on random-access arrays, with in-place partitioning. e.g. see this.

you can start by converting your list to vector with list->vector, then implementing the quicksort by partitioning the vector with mutating swaps, in C fashion.

Randomizing it is easy: just pick a position randomly, and swap its contents with the first element in range being sorted, before each partition step. When you're done, convert it back with vector->list.

Efficient implementation of quicksort may run without recursion, in a loop, maintaining a stack of bigger parts boundaries, always descending on the smaller ones (then, when at the bottom, switching to the first part in the stack). Three-way partitioning is always preferable, dealing with equals in one blow.

Your list-based algorithm is actually an unraveled treesort.

see also:

Community
  • 1
  • 1
Will Ness
  • 70,110
  • 9
  • 98
  • 181
3

Although there's already an accepted answer, I thought you might appreciate a Scheme translation of the Sheep Trick from The Pitmanual. Your code is actually quite similar to it already. Scheme does support do loops, but they're not particularly idiomatic, whereas named lets are much more common, so I've used the latter in this code. As you've noted, choosing the first element as the pivot cause perfomance problems if the list is already sorted. Since you have to traverse the list on each iteration, there might be some clever thing you could do to pick the pivots for the left and right sides for the recursive calls in advance.

(define (nconc l1 l2)
  ;; Destructively concatenate l1 and l2. If l1 is empty,
  ;; return l2.  Otherwise, set the cdr of the last pair
  ;; of l1 to l2 and return l1. 
  (cond
    ((null? l1)
     l2)
    (else 
     (let loop ((l1 l1))
        (if (null? (cdr l1))
            (set-cdr! l1 l2)
            (loop (cdr l1))))
     l1)))

(define (quicksort lst)
  (if (null? lst) lst
      (let ((pivot (car lst))
            (left '())
            (right '()))
        (let loop ((lst (cdr lst))) ; rebind to (cdr lst) since pivot wasn't popped
          (if (null? lst)
              (nconc (quicksort left)
                     (cons pivot
                           (quicksort right)))
              (let ((tail (cdr lst)))
                (cond
                  ((< (car lst) pivot)
                   (set-cdr! lst left)
                   (set! left lst))
                  (else
                   (set-cdr! lst right)
                   (set! right lst)))
                (loop tail)))))))
(quicksort (list 9 1 8 2 7 3 6 4 5))
;=> (1 2 3 4 5 6 7 8 9)

Scheme does support do, so if you are interested in that (it does make the Common Lisp and Scheme version very similar), it looks like this:

(define (quicksort lst)
  (if (null? lst) lst
      (do ((pivot (car lst))
           (lst (cdr lst))   ; bind lst to (cdr lst) since pivot wasn't popped
           (left '())
           (right '()))
        ((null? lst)
         (nconc (quicksort left)
                (cons pivot
                      (quicksort right))))
        (let ((tail (cdr lst)))
          (cond
            ((< (car lst) pivot)
             (set-cdr! lst left)
             (set! left lst))
            (else
             (set-cdr! lst right)
             (set! right lst)))
          (set! lst tail)))))
(display (quicksort (list 9 1 8 2 7 3 6 4 5)))
;=> (1 2 3 4 5 6 7 8 9)
Joshua Taylor
  • 84,998
  • 9
  • 154
  • 353
  • Hi. I found out that DrRacket doesn't support set-cdr! anymore. Is it equivalent to (cons (car lst) left)? – babel92 Dec 19 '13 at 21:27
  • 1
    @babel92 Racket is similar, but it's not the same as Scheme. Scheme has supports `set-cdr!` and `set-car!`. In DrRacket you can select the language by starting your file with, e.g., `#lang r5rs`. But you're right, Racket defaults to non-mutable pairs now. – Joshua Taylor Dec 19 '13 at 21:31
2

A truly efficient implementation of Quicksort should be in-place and implemented using a data structure that can be accessed efficiently by index - and that makes immutable linked lists a poor choice.

The question asks whether Quicksort can be efficiently implemented with Scheme - the answer is yes, as long as you don't use lists. Switch to using a vector, which is mutable and has O(1) index-based access over its elements, like an array in C-like programming languages.

If your input data comes in a linked list, you can always do something like this, it'll probably be faster than directly sorting the list:

(define (list-quicksort lst)
  (vector->list
   (vector-quicksort ; ToDo: implement this procedure
    (list->vector lst))))
Óscar López
  • 232,561
  • 37
  • 312
  • 386
  • This can still be done fairly efficiently with mutable lists (see my comment about the "Sheep Trick" on the main question). Memory locality will still be an issue, of course, and it's still a problem to get a random pivot. – Joshua Taylor Dec 19 '13 at 20:21
  • @JoshuaTaylor why is it called the "sheep trick"? seems like I'm missing a cultural reference here :) – Óscar López Dec 19 '13 at 21:15
  • 1
    As I mentioned in a comment on the main question, the sheep and goats are an allusion to [Matthew 25:31–46](http://www.biblegateway.com/passage/?search=Matthew+25:31-46&version=NASB) where the goats are on the left and the sheep are on the right. :) – Joshua Taylor Dec 19 '13 at 21:33
  • 1
    Although, perhaps the original author of that code held to [universal reconciliation](https://en.wikipedia.org/wiki/Universal_reconciliation), since the sheep and goats all end up in the same list after all. – Joshua Taylor Dec 19 '13 at 21:37