Введение
Пузырьковая сортировка/Пример
Сортировка выбором/Пример
Сортировка Шелла/Пример
Сортировка Хоора/Пример
QuickSort/Пример
Сортировка с помощью двоичного дерева/Пример
Сортировка с помощью массива индексов/Пример
Какую сортировку выбрать
Этот текст призван стать кратким обзором нескольких алгоритмов сортировки.
Солидная часть статьи взята из одноименного текста С.Курковского, что-то дописано, что-то поскипано, преобразовано в html, добавлены пара алгоритмов с картинкой, примеры отлажены до рабочего состояния.
Итак, каждый алгоритм сортировки состоит из трех основных частей:
#define less(x,y) (x < y) // функция сравнения элементов #define swap(x,y) {int t=x; x=y; y=t;} // процедура перестановки элементов
Операция сравнения/перестановки выполняется для каждых двух стоящих
рядом элементов.
После первого прохода по массиву "вверху" оказывается
самый "легкий" элемент.
Следующий цикл сортировки выполняется начиная со второго элемента,
в результате чего вторым в массиве оказывается второй наименьший
по величине элемент, и так далее.
Алгоритм прост и крайне неэффективен.
void sort_bubble(int a[], int max) // пузырьковая сортировка { for (int i=0; i<max; i++) for (int j=max-1; j>i; j--) if (less(a[j], a[j-1])) swap(a[j], a[j-1]); }
Во время i-го прохода по массиву выявляется наименьший элемент,
который затем меняется местами с i-м.
Все остальное аналогично пузырьковой сортировке.
void sort_choose(int a[], int max) // сортировка выбором { for (int i=0; i<max; i++) { int k=i; for (int j=i+1; j<max; j++) if (less(a[j], a[k])) k=j; if (i!=k) swap(a[i], a[k]); } }
Основная идея алгоритма заключается в том, чтобы вначале устранить
массовый беспорядок в массиве, сравнивая далеко стоящие друг от друга
элементы.
Интервал между сравниваемыми элементами постепенно уменьшается до
единицы. Это означает, что на поздних стадиях сортировка сводится
просто к перестановкам соседних элементов.
void sort_shell(int a[], int max) // сортировка Шелла { for (int gap = max/2; gap>0; gap/=2) // выбор интервала for (int i=gap; i<max; i++) // проход массива // сравнение пар, отстоящих на gap друг от друга for (int j=i-gap; j>=0 && less(a[j+gap],a[j]); j-=gap) swap(a[j], a[j+gap]); }
Суть метода заключается в том, чтобы найти такой элемент множества, подлежащего сортировке, который разобьет его на два подмножества: те элементы, что меньше делящего элемента, и те, что не меньше его.
Для массива из 10 чисел установим два индекса: на первый (i) и на последний (j) элементы.
10 7 28 49 31 25 17 3 14 43 i <--j step==-1
На каждом шаге будем изменять значение индекса j на значение step (вначале оно равно -1, индекс уменьшается), т.е. двигать j влево. Мы будем делать так в том случае, если j-й элемент больше, чем i-й элемент. Если это условие не выполнено - поменяем местами i-й и j-й элементы массива и сами индексы i и j, а также изменим знак у значения step -- оно станет равным +1:
3 7 28 49 31 25 17 10 14 43 j--> i step==+1
Продолжим процесс: теперь j движется вправо, и изменится условие - сейчас для продолжения процесса необходимо, чтобы j-й элемент был меньше i-го.
Будем продолжать вышеописанный процесс до тех пор, пока индексы i и j не станут одинаковыми.
В этот момент можно утверждать, что i-й (он же и j-й) элемент разделил исходное множество на два подмножества: все элементы, находящиеся слева от делящего элемента, меньше его, и все элементы, находящиеся справа, не меньше делящего (i-го) элемента. Получилось, что i-й элемент стоит на своем месте:
3 7 10 49 31 25 17 28 14 43 ij
Таким образом, мы описали процедуру нахождения расположения одного элемента. Иными словами, мы "отсортировали" один элемент множества. Такая же процедура применима к элементам "левого" и "правого" подмножеств, которые на данный момент еще не отсортированы.
Условие завершения нашей рекурсивной процедуры - совпадение границ, которое эквивалентно тому, что в множестве остался один элемент.
void sort_hoor(int a[], int l, int r) // сортировка Хоора { int i=l, j=r, step=-1, condition=1; if (l>=r) return; // сортировать нечего do { // сортируем с левой границы до правой if (condition == less(a[j],a[i])) { swap(a[i], a[j]); // перестановка чисел swap(i, j); // обмен местами индексов step = -step; // теперь - в другую сторону condition = !condition; // обмен условия на противоположное } j += step; // передвинем индекс } while (j!=i); // пока индексы не сойдутся sort_hoor(a, l, i-1); // левое подмножество sort_hoor(a, i+1, r); // правое }
По существу является разновидностью сортировки Хоора, но программная реализация немного красивее и быстрее.
Основное отличие - за делящий элемент всегда выбирается середина массива.
void sort_quick(int a[], int l, int r) // QuickSort { int i=l, j=r, x=a[(l+r)/2]; do { while (less(a[i],x)) i++; while (less(x,a[j])) j--; if (i<=j) { swap(a[i], a[j]); i++; j--; }; } while (i<j); if (l<j) sort_quick(a,l,j); if (i<r) sort_quick(a,i,r); }
Двоичным деревом назовем упорядоченную структуру данных, в которой каждому элементу -- предшественнику или корню (под)дерева -- поставлены в соответствие по крайней мере два других элемента (преемника). Причем для каждого предшественника выполнено следующее правило: левый преемник всегда меньше, а правый преемник всегда больше или равен предшественнику. Если мы составим такое дерево из букв слова "СОРТИР", сравнивая ASCII коды букв, то получим следующую структуру:
С / \ О Т / \ И Р
Как видно, узел "С" имеет два преемника: левый "О" и правый "Т". Если составить бинарное дерево из элементов неупорядоченного массива, то в общем случае дерево получится достаточно хорошо заполненным (а если массив уже был рассортирован, то дерево выродится в линейный список). Если мы будем обходить дерево по правилу "ЛЕВЫЙ преемник - КОРЕНЬ - ПРАВЫЙ преемник", то, записывая все встречающиеся элементы в массив, мы получим упорядоченное в порядке возрастания множество. На этом и основана идея сортировки деревом.
Использование такой сортировки удобно тогда, когда удобно представлять данные в виде дерева.
Недостаток метода - требуется много памяти. В приведеном примере - дополнительный мегабайт данных на каждые 256k элементов.
/*********** сортировка с помощью двоичного дерева *************/ typedef struct tree { int a; // данные struct tree *left; // левый преемник struct tree *right; // правый преемник } TREE; TREE *add_to_tree(TREE *root, int new_value) { if (root==NULL) // если дошли до корня - создаем новый элемент { root = (TREE*)malloc(sizeof(TREE)); root->a = new_value; root->left = root->right = 0; return root; } if less(root->a, new_value) // добавлем ветвь root->right = add_to_tree(root->right, new_value); else root->left = add_to_tree(root->left, new_value); return root; } void tree_to_array(TREE *root, int a[]) // процедура заполнения массива { static max2=0; // счетчик элементов нового массива if (root==NULL) return; // условие окончания - найден корень tree_to_array(root->left, a); // обход левого поддерева a[max2++] = root->a; tree_to_array(root->right, a); // обход правого поддерева free(root); } void sort_tree(int a[], int max) // собственно сортировка { TREE *root = 0; for (int i=0; i<max; i++) // проход массива и заполнение дерева root = add_to_tree(root, a[i]); tree_to_array(root, a); // заполнение массива }
Это не столько метод, сколько хинт для ускорения любого метода сортировки, когда в качестве данных используются структуры большого размера.
Идея заключается в том, что параллельно с основным массивом данных существует так называемый массив индексов, через который осуществляется перестановка индексов основного массива данных.
struct XPEH a[10000]; // основной массив int index[10000]; // массив индексов {0,1,2,...,9999}
Перед сортировкой массив индексов инициализируется соответствующими порядковыми номерами.
Доступ к массиву данных ведется не как a[i], а как a[index[i]], и функция сравнения теперь выглядит иначе:
#define less(i,j) (a[index[i]].XPEH_member < a[index[j]].XPEH_member)
В результате фактически сортируется массив с индексами, а не с данными.
void sort_quick_index(XPEH a[], int index[], int l, int r) { int i=l, j=r, x=(l+r)/2; // теперь x не элемент а его индекс do { while (less(i,x)) i++; while (less(x,j)) j--; if (i<=j) { if (x==i) x==j; else if (x==j) x==i; swap(index[i], index[j]); // сортируем индексы i++; j--; }; } while (i<j); if (l<j) sort_quick_index(a,index,l,j); if (i<r) sort_quick_index(a,index,i,r); }
А теперь сравните по скорости тысячу операций swap(x,y), где в первом случае x и y - это большие структуры данных, а во втором - числа по 2-4 байта.
Ниже приведена зависимость скорости сортировки от числа элементов массива. Эксперимент проводился на весьма торозном компьютере, так что верна лишь пропорциональность скоростей. Массив заполнялся случайными числами от 0 до N, где N - число элементов массива.
Как видно, QuickSort показывает лучший результат по времени, да и по реализации процедура сортировки весьма проста.
Таким образом лучшее решение - ассемблерный аналог QuickSortа, с использованием массива индексов если структуры данных достаточно большие.
Примечание: не следует исходники сортировок понимать буквально, ибо они далеки от совершенства. В частности, в рекурсивных функциях сортируемый массив как параметр вовсе не передается, и именно так было при построении графика.
И еще: в случае, если функция сравнения less(XPEH a,XPEH b) занимает много времени, можно заранее просчитать "веса" элементов и функцию сравнения определить уже для них.