Abstract: quicksort is an algorithm invented by Donald and the best in average performance. This article compares the standard writing method of quicksort with your own writing method, and finds some hidden programming traps, so it is recorded for learning and communication.
Keywords: C/C ++ Algorithm Program Design quick sorting
1 main idea of quicksort
Select a value from the array element to be sorted as the value elementPartition)Part 2:If the value is smaller than the limit value, put it to the left, and if the value is greater than the limit value, put it to the right.And then recursively process the child Arrays on both sides of the left and right. If the division is even, T (n) = 2 * t (n/2) + O (N ), the average time complexity of the algorithm is O (nlgn). Of course, if it cannot be guaranteed, quicksort degrades to O (N ^ 2.
Unfortunately, the so-called division is actually a swap for arrays. This means that some implementation details need to be considered when converting an idea into code.
2. My first version of quicksort implementation
The Code is as follows: (the partition subfunction is saved)
Void quick_sort (int A [], int length ){
If (length> 1 ){
Int Limit = A [0];
Inti = 1, j = length-1;
While (I <j ){
While (A [I] <strong & I <length) ++ I;
While (j> = 0 & A [J]> values) -- J;
If (I <J & I <length & J> = 1 ){
Swap (A [I], a [J]); // use STD: swap
++ I;
-- J;
}
}
Swap (A [0], a [J]);
Quick_sort (A, J );
Quick_sort (& A [J + 1], length-j-1 );
}
}
Note:
(1) how to describe an array in C language? Generally, you only need the starting address a (OR & A [0]) of the array element and the length.
(2) In some code, when the sequence element is selected, an intermediate value is selected from the third, intermediate, and last three elements, and some use a random function, however, this article is more concerned with "How to Write to quicksort", but does not care about how to choose the sort. Here we simply choose the first a [0] As the sort
(3) Partition part: uses two indexes, I and j, to scan from left to right in the middle. If a [I] <strong or a [J]> continue, until the opposite situation occurs: A [I]> = random and a [J] <= random, then the positions of a [I] And a [J] are exchanged.
Unfortunately, the first version of the code above is wrong! Let's test the code first.
3 test code
Void display (int A [], int length ){
For (INTI = 0; I <length; ++ I) cout <A [I] <",";
Cout <Endl;
}
# Define test (){\
Quick_sort (A, length ());\
Display (A, length ());\
}
# Define length (a) sizeof (a)/sizeof (A [0])
Int main (INT argc, char ** argv ){\
{INTA [] = {1, 2}; test ()}
{INTA [] = {2, 1}; test ()}
{INTA [] = {1, 2, 3}; test ()}
{INTA [] = {3, 2, 1}; test ()}
{INTA [] = {2, 2}; test ()}
{INTA [] = {1, 2, 3, 4}; test ()}
{INTA [] = {4, 3, 2, 1}; test ()}
{INTA [] = {2, 3, 2, 1}; test ()}
{INTA [] = {5, 4, 3, 2, 1}; test ()}
{INTA [] = {1, 2, 3, 4, 5}; test ()}
{INTA [] = {3, 1, 3, 2}; test ()}
}
Output:
2, 1,
1, 2,
1, 3, 2,
2, 1, 3,
2, 2, 2,
1, 2, 4, 3,
1, 2, 3, 4,
1, 2, 3,
1, 3, 2, 4, 5,
1, 2, 3, 5, 4,
1, 2, 3, 3,
Terrible. What's wrong?
4. Let's take a look at the standard writing method.
Code Source: "Computer Algorithm Design and Analysis (version 4th)" (Wang Xiaodong, Electronic Industry Press), and "qsort3" in "programming Pearl.
Int partition (int A [], int P, int R ){
Int I = P, j = R + 1;
Int x = A [p];
While (1 ){
While ([++ I] <X& I <r);
While ([-- J]> X );
If (I> = J)
Break;
Swap (A [I], a [J]);
}
A [p] = A [J];
A [J] = X;
Return J;
}
Void quicksort (int A [], int P, int R ){
If (P <r ){
Intq = partition (A, P, R );
Quicksort (A, P, q-1 );
Quicksort (A, q + 1, R );
}
}
5. Code Analysis
First, it is noted that the array in this implementation is represented by three parameters: Starting address a, starting subscript P, ending subscript R, and a remains unchanged throughout the Code. Q indicates the position where the cursor should be placed after the partition operation. Use the code of this version for testing: No problem. So where is the code of the first version wrong?
OK. Now let's take a look at the implementation before and after to see where the difference is?
1. First, my quick_sort moved the position of I and j further after swap (A [I], a [J]), but this is superfluous and unnecessary;
2. In standard quicksort writing, I and j are initialized to values out of the range! Here intx = A [p]; a row indicates that the sequence is selected in the same way as mine, that is, the first element, but I and j are initialized as P, R + 1, respectively, this is different. But does it matter?
3. Two while loops in the inner layer. My quick_sort is used first, and then ++, -- (mobile operation ). The standard quicksort write rule is first ++, --, and then judgment. Therefore, we can think that both of them are essentially feasible.
However, imagine that all the elements that I and j encounter in the Process of "squeezing" into the middle are equal to the limit? In this case, my code will lead to an endless loop, but the standard quicksort writing is amazing and there is no problem!
4. Check the two while loops in the inner layer again:
A) Conditions for the first while loop (or "Loop Invariant !),
My quick_sort is written as: A [I] <strong & I <Length
The standard quicksort statement is as follows: [++ I] <X& I <r
Depend! Do you see the difference? The constraint I used only requires that I not go beyond the subscript range of the array. The standard writing method enhances this constraint and further requires that I not go beyond the initial position of J !!!
B) then, let's look at the second while loop. Based on the above inference, there should also be [-- J]> X & J> P. Unfortunately, the standard quicksort method directly saves the & J> P part. Although this may make sense, I do not recommend that you do this.-destroying the structural symmetry of the code, that is, losing the beauty of the Code. It is not necessary to do this. J> P is equivalent to j> = p + 1. In my version, j> = 1.
OK. Finally, find the most critical point and modify:
Void quick_sort (int A [], int length ){
If (length> 1 ){
Intpivot = A [0];
Inti = 1, j = length-1;
While (I <j ){
While (A [I] <= Signature& I <length-1) ++ I;
While (J> = 1 &&A [J]> callback) -- J;
If (I <J & I <length & J> = 1 ){
Swap (A [I], a [J]); // use STD: swap
}
}
Swap (A [0], a [J]);
Quick_sort (A, J );
Quick_sort (& A [J + 1], length-j-1 );
}
}
Result: It is still wrong!
What are missing ?! OMG.
First, we noticed that the case with only two elements {1, 2} is incorrect. In this case, we can simulate and execute the program in the brain (you can do it without debugging ): i, j are all 1 at initialization, while (I <j) is not satisfied, skip directly. If swap (A [0], a [J]) is directly used at this time... That's not the opposite! Add a guard/check: if (a [0]> A [J]) Swap (A [0], a [J]);
Void quick_sort (int A [], int length ){
If (length> 1 ){
Intpivot = A [0];
Inti = 1, j = length-1;
While (I <j ){
While (A [I] <= cursor & I <length-1) ++ I;
While (j> = 1 & A [J]> values) -- J;
If (I <J & I <length & J> = 1 ){
Swap (A [I], a [J]); // use STD: swap
++ I;
-- J;
}
}
If (A [0]> A [J])Swap (A [0], a [J]); // fix case {1, 2}
Quick_sort (A, J );
Quick_sort (& A [J + 1], length-j-1 );
}
}
OK, make the changes and test again: No problem now.
In fact, we can look back and find that the modification is just to solve the Boundary Situation of the two elements such as {1, 2} for fast sorting. Because a large array is called for fast sorting and recursion, two elements are returned at a certain point. If the sorting of the two elements is incorrect, it is clear that, the final result is wrong.
In standard writingInt I = P, j = R + 1;The initialization position is actually beyond the scope of the part to be partition. This actually unifies the boundary conditions, so that no special processing is required for the two elements {1, 2. My code places the initialization values of I and J in the boundary position within the scope of the part to be partition, which leads to additional condition judgment and processing.
6 conclusion
Quicksort is not an easy-to-write algorithm. Of course, I do not recommend memorizing the code of these classical algorithms. In that case, I still do not really understand this algorithm. Although the quick sorting code of my own version passes the test, there are a lot of additional check conditions. It seems that it is not as simple as standard writing (beautiful ?)
The following are the lessons learned:
(1) develop perfect test cases for algorithms and Use assertions where necessary;
(2) Pay special attention to the non-variant loop (while loop condition) and the prerequisite for the value assignment operation (if statement), because these are the most important aspects to ensure the correctness of the program.
Leave a homework assignment at the end: if the last element is selected as the marker, what modifications do the quick_sort function need?
Hard-to-write quicksort