основы
Базовая версия Quicksort довольно проста и может быть реализована всего несколькими строками кода:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public static void basicQuickSort( long arr[], int beginIdx, int len) { if ( len <= 1 ) return ; final int endIdx = beginIdx+len- 1 ; // Pivot selection final int pivotPos = beginIdx+len/ 2 ; final long pivot = arr[pivotPos]; Utils.swap(arr, pivotPos, endIdx); // partitioning int p = beginIdx; for ( int i = beginIdx; i != endIdx; ++i) { if ( arr[i] <= pivot ) { Utils.swap(arr, i, p++); } } Utils.swap(arr, p, endIdx); // recursive call basicQuickSort(arr, beginIdx, p-beginIdx); basicQuickSort(arr, p+ 1 , endIdx-p); } |
Код выглядит довольно простым и легко читаемым. Выбор сводки тривиален и не требует каких-либо объяснений. Процесс разделения можно проиллюстрировать с помощью следующего рисунка:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public static void basicQuickSort( long arr[], int beginIdx, int len) { if ( len <= 1 ) return ; final int endIdx = beginIdx + len - 1 ; final int pivotIdx = getPivotIdx(arr, beginIdx, len); final long pivot = arr[pivotIdx]; Utils.swap(arr, pivotIdx, endIdx); int p = partition(arr, beginIdx, len, pivot); Utils.swap(arr, p, endIdx); basicQuickSort(arr, beginIdx, p-beginIdx); basicQuickSort(arr, p+ 1 , endIdx-p); } public static int partition( long [] arr, int beginIdx, int len, long pivot) { final int endIdx = beginIdx + len - 1 ; int p = beginIdx; for ( int i = beginIdx; i != endIdx; ++i) { if ( arr[i] <= pivot ) { Utils.swap(arr, i, p++); } } return p; } public static int getPivotIdx( long arr[], int beginIdx, int len) { return beginIdx+len/ 2 ; } |
Теперь давайте посмотрим, как он выполняет алгоритм сортировки Java 1.6. Для теста я сгенерирую массив, используя следующий цикл:
1
2
3
4
5
6
7
8
|
static Random rnd = new Random(); private static long [] generateData() { long arr[] = new long [ 5000000 ]; for ( int i = 0 ; i != arr.length; ++i) { arr[i] = rnd.nextInt(arr.length); } return arr; } |
Затем я запускаю каждый JDK 6 Arrays.sort () и basicQuickSort () по 30 раз и в качестве результата взял среднее время выполнения. Новый набор случайных данных был создан для каждого запуска. Результат этого упражнения:
обр [я] = rnd.nextInt (arr.length) | |
Java 6 Arrays.sort | 1654ms |
basicQuickSort | 1431ms |
Не так уж плохо. Теперь посмотрим, что произойдет, если во входных данных есть еще несколько повторяющихся элементов. Чтобы сгенерировать эти данные, я просто разделил аргумент nextInt () на 100:
обр [я] = rnd.nextInt (arr.length) | обр [я] = rnd.nextInt (arr.length / 100) | |
Java 6 Arrays.sort | 1654ms | 935ms |
basicQuickSort | 1431ms | 2570ms |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public static int getPivotIdx( long arr[], int beginIdx, int len) { if ( len <= 512 ) { int p1 = beginIdx; int p2 = beginIdx+(len>>> 1 ); int p3 = beginIdx+len- 1 ; if ( arr[p1] > arr[p2] ) { int tmp = p1; p1 = p2; p2 = tmp; } if ( arr[p2] > arr[p3] ) { p2 = p3; } if ( arr[p1] > arr[p2] ) { p2 = p1; } return p2; } else { int p1 = beginIdx+(len/ 4 ); int p2 = beginIdx+(len>> 1 ); int p3 = beginIdx+(len-len/ 4 ); int p4 = beginIdx; int p5 = beginIdx+len- 1 ; if ( arr[p1] > arr[p2] ) { int tmp = p1; p1 = p2; p2 = tmp; } if ( arr[p2] > arr[p3] ) { int tmp = p2; p2 = p3; p3 = tmp; } if ( arr[p1] > arr[p2] ) { int tmp = p1; p1 = p2; p2 = tmp; } if ( arr[p3] > arr[p4] ) { int tmp = p3; p3 = p4; p4 = tmp; } if ( arr[p2] > arr[p3] ) { int tmp = p2; p2 = p3; p3 = tmp; } if ( arr[p1] > arr[p2] ) { p2 = p1; } if ( arr[p4] > arr[p5] ) { p4 = p5; } if ( arr[p3] > arr[p4] ) { p3 = p4; } if ( arr[p2] > arr[p3] ) { p3 = p2; } return p3; } } |
Вот результаты после улучшений в стратегии выбора сводных данных:
обр [я] = rnd.nextInt (arr.length) | обр [я] = rnd.nextInt (arr.length / 100) | |
Java 6 Arrays.sort | 1654ms | 935ms |
basicQuickSort | 1431ms | 2570ms |
basicQuickSort с «лучшей» опорой | 1365ms | 2482ms |
После реализации такого алгоритма функция разбиения становится намного сложнее. В этой реализации результатом разбиения является длина двух связанных разделов:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
public static long partition( long [] arr, int beginIdx, int endIdx, long pivot) { int i = beginIdx- 1 ; int l = i; int j = endIdx+ 1 ; int r = j; while ( true ) { while (arr[++i] <> pivot){} if ( i >= j ) break ; Utils.swap(arr, i, j); if ( arr[i] == pivot ) { Utils.swap(arr, i, ++l); } if ( arr[j] == pivot ) { Utils.swap(arr, j, --r); } } // if i == j then arr[i] == arr[j] == pivot if ( i == j ) { ++i; --j; } final int lLen = j-l; final int rLen = r-i; final int pLen = l-beginIdx; final int exchp = pLen > lLen ? lLen: pLen; int pidx = beginIdx; for ( int s = 0 ; s <= exchp; ++s) { Utils.swap(arr, pidx++, j--); } final int qLen = endIdx-r; final int exchq = rLen > qLen ? qLen : rLen; int qidx = endIdx; for ( int s = 0 ; s <= exchq; ++s) { Utils.swap(arr, qidx--, i++); } return ((( long )lLen)<< 32 )|rlen; } |
Выбор точки поворота также должен быть изменен, но, скорее, для удобства идея остается абсолютно неизменной. Теперь он возвращает фактическое значение pivot, а не index:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
public static long getPivot( long arr[], int beginIdx, int len) { if ( len <= 512 ) { long p1 = arr[beginIdx]; long p2 = arr[beginIdx+(len>> 1 )]; long p3 = arr[beginIdx+len- 1 ]; return getMedian(p1, p2, p3); } else { long p1 = arr[beginIdx+(len/ 4 )]; long p2 = arr[beginIdx+(len>> 1 )]; long p3 = arr[beginIdx+(len-len/ 4 )]; long p4 = arr[beginIdx]; long p5 = arr[beginIdx+len- 1 ]; return getMedian(p1, p2, p3, p4, p5); } } |
И вот основной метод, который также немного изменился:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public static void threeWayQuickSort( long [] arr, int beginIdx, int len) { if ( len < 2 ) return ; final int endIdx = beginIdx+len- 1 ; final long pivot = getPivot(arr, beginIdx, len); final long lengths = threeWayPartitioning(arr, beginIdx, endIdx, pivot); final int lLen = ( int )(lengths>> 32 ); final int rLen = ( int )lengths; threeWayQuickSort(arr, beginIdx, lLen); threeWayQuickSort(arr, endIdx-rLen+ 1 , rLen); } |
теперь давайте сравним это с Java 6 sort:
обр [я] = rnd.nextInt (arr.length) | обр [я] = rnd.nextInt (arr.length / 100) | |
Java 6 Arrays.sort | 1654ms | 935ms |
basicQuickSort | 1431ms | 2570ms |
basicQuickSort с «лучшей» опорой | 1365ms | 2482ms |
Трехстороннее разбиение Quicksort | 1330ms | 829ms |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
public static void threeWayQuickSort( long [] arr, int beginIdx, int len) { if ( len < 2 ) return ; if ( len < 17 ) { InsertionSort.sort(arr, beginIdx, len); return ; } final int endIdx = beginIdx+len- 1 ; final long pivot = getPivot(arr, beginIdx, len); final long lengths = threeWayPartitioning(arr, beginIdx, endIdx, pivot); final int lLen = ( int )(lengths>> 32 ); final int rLen = ( int )lengths; threeWayQuickSort(arr, beginIdx, lLen); threeWayQuickSort(arr, endIdx-rLen+ 1 , rLen); } |
и снова запустите тест:
обр [я] = rnd.nextInt (arr.length) | обр [я] = rnd.nextInt (arr.length / 100) | |
Java 6 Arrays.sort | 1654ms | 935ms |
basicQuickSort | 1431ms | 2570ms |
basicQuickSort с «лучшей» опорой | 1365ms | 2482ms |
Трехстороннее разбиение Quicksort | 1330ms | 829ms |
Трехстороннее разбиение Quicksort с сортировкой Insertion | 1155ms | 818ms |
теперь стандартная библиотека выглядит просто ужасно. Похоже, теперь все сказано и сделано. Но на самом деле это еще не конец истории, и есть о чем поговорить.
Двойной поворот Quicksort
Двигаясь вперед, я обнаружил, что Java 7 намного более продвинута и работает намного быстрее, чем версия Java 6, и превосходит все предыдущие тесты:
обр [я] = rnd.nextInt (arr.length) | обр [я] = rnd.nextInt (arr.length / 100) | |
Java 6 Arrays.sort | 1654ms | 935ms |
Java 7 Arrays.sort | 951ms | 764ms |
basicQuickSort | 1431ms | 2570ms |
basicQuickSort с «лучшей» опорой | 1365ms | 2482ms |
Трехстороннее разбиение Quicksort | 1330ms | 829ms |
Трехстороннее разбиение Quicksort с сортировкой Insertion | 1155ms | 818ms |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
public static void dualPivotQuicksort( long arr[], int beginIdx, int len) { if ( len < 2 ) return ; final int endIdx = beginIdx+len- 1 ; long pivot1 = arr[beginIdx]; long pivot2 = arr[endIdx]; if ( pivot1 == pivot2 ) { final long lengths = QuickSort.threeWayPartitioning(arr, beginIdx, endIdx, pivot1); final int lLen = ( int )(lengths>> 32 ); final int rLen = ( int )lengths; dualPivotQuicksort3(arr, beginIdx, lLen); dualPivotQuicksort3(arr, endIdx-rLen+ 1 , rLen); } else { if ( pivot1 > pivot2 ) { long tmp = pivot1; pivot1 = pivot2; pivot2 = tmp; Utils.swap(arr, beginIdx, endIdx); } int l = beginIdx; int r = endIdx; int p = beginIdx; while ( p <= r ) { if ( arr[p] < pivot1 ) { Utils.swap(arr, l++, p++); } else if ( arr[p] > pivot2 ) { while ( arr[r] > pivot2 && r > p ) { --r; } Utils.swap(arr, r--, p); } else { ++p; } } if ( arr[l] == pivot1 ) ++l; if ( arr[r] == pivot2 ) --r; dualPivotQuicksort3(arr, beginIdx, l-beginIdx); dualPivotQuicksort3(arr, l, r-l+ 1 ); dualPivotQuicksort3(arr, r+ 1 , endIdx-r); } } |
Первый код поднимает два центра. Если стержни одинаковы, это означает, что у нас есть только один стержень, и в этом случае мы можем использовать трехсторонний метод для разбиения. Если стержни разные, то процесс разбиения будет выглядеть так:
Сканирующий указатель «p» движется от начала массива. Если текущим элементом является «<> pivot1», то r-й элемент заменяется на p-й, а указатель «r» перемещается к следующему элементу назад. Все останавливается, когда «р» становится меньше, чем «р». После разбиения массив будет выглядеть так:
- Википедия Quicksort
- Dual-Pivot QuickSort
- Quicksort — оптимальная презентация Роберта Седжвика и Джона Бентли
- MIT лекция о быстрой сортировке