Кнут залишив це як вправу (т. 3, 5.2.5). Існують місцеві види злиття. Вони повинні ретельно реалізовуватися.
По-перше, наївне на місці злиття, як описано тут , не є правильним рішенням. Це знижує продуктивність до O (N 2 ) .
Ідея полягає в сортуванні частини масиву, використовуючи решту як робочу зону для злиття.
Наприклад, як наступна функція злиття.
void wmerge(Key* xs, int i, int m, int j, int n, int w) {
while (i < m && j < n)
swap(xs, w++, xs[i] < xs[j] ? i++ : j++);
while (i < m)
swap(xs, w++, i++);
while (j < n)
swap(xs, w++, j++);
}
Він займає масив xs
, два відсортовані підмасиви представлені у вигляді діапазонів [i, m)
і [j, n)
відповідно. Робоча зона починається з w
. Порівняйте зі стандартним алгоритмом злиття, наведеним у більшості підручників, цей обмінюється вмістом між відсортованим підмасивом та робочою зоною. В результаті попередня робоча зона містить об'єднані відсортовані елементи, тоді як попередні елементи, що зберігаються в робочій області, переміщуються до двох підмасивів.
Однак необхідно виконати два обмеження:
- Робоча зона повинна знаходитися в межах масиву. Іншими словами, він повинен бути достатньо великим, щоб вміщувати елементи, обмінювані в них, не викликаючи при цьому ніякої зовнішньої помилки.
- Робочу зону можна перекривати будь-яким з двох відсортованих масивів; однак, він повинен забезпечити, щоб жоден з не занурених елементів не був перезаписаний.
За допомогою визначеного алгоритму злиття легко уявити рішення, яке може сортувати половину масиву; Наступне питання - як поводитися з рештою несортованої частини, що зберігається в робочій області, як показано нижче:
... unsorted 1/2 array ... | ... sorted 1/2 array ...
Одна інтуїтивна ідея полягає в рекурсивному сортуванні ще половини робочої зони, таким чином, лише 1/4 елементи ще не відсортовані.
... unsorted 1/4 array ... | sorted 1/4 array B | sorted 1/2 array A ...
Ключовим моментом на цьому етапі є те, що ми повинні рано чи пізно об'єднати відсортовані 1/4 елементи B з відсортованими 1/2 елементами A.
Чи залишилася робоча зона, яка містить лише 1/4 елемента, достатньо велика для злиття A і B? На жаль, це не так.
Однак друге обмеження, згадане вище, дає нам натяк на те, що ми можемо використовувати його, організувавши робочу зону для перекриття з будь-яким підмасивом, якщо ми зможемо забезпечити послідовність злиття, щоб неперезаписані елементи не були перезаписані.
Насправді, замість сортування другої половини робочої зони, ми можемо сортувати першу половину та розмістити робочу зону між двома відсортованими масивами так:
... sorted 1/4 array B | unsorted work area | ... sorted 1/2 array A ...
Ця настройка ефективно упорядковує робочу зону, що перекривається підмасивом A. Ця ідея запропонована в [Jyrki Katajainen, Tomi Pasanen, Jukka Teuhola. `` Практичний місцевий злиття ''. Nordic Journal of Computing, 1996].
Тож єдине, що залишилося - це повторити вищезазначений крок, що зменшує робочу зону з 1/2, 1/4, 1/8, ... Коли робоча зона стане достатньо маленькою (наприклад, лише два елементи), ми можемо перейти до тривіального сортування вставки, щоб закінчити цей алгоритм.
Ось реалізація в ANSI C на основі цієї роботи.
void imsort(Key* xs, int l, int u);
void swap(Key* xs, int i, int j) {
Key tmp = xs[i]; xs[i] = xs[j]; xs[j] = tmp;
}
/*
* sort xs[l, u), and put result to working area w.
* constraint, len(w) == u - l
*/
void wsort(Key* xs, int l, int u, int w) {
int m;
if (u - l > 1) {
m = l + (u - l) / 2;
imsort(xs, l, m);
imsort(xs, m, u);
wmerge(xs, l, m, m, u, w);
}
else
while (l < u)
swap(xs, l++, w++);
}
void imsort(Key* xs, int l, int u) {
int m, n, w;
if (u - l > 1) {
m = l + (u - l) / 2;
w = l + u - m;
wsort(xs, l, m, w); /* the last half contains sorted elements */
while (w - l > 2) {
n = w;
w = l + (n - l + 1) / 2;
wsort(xs, w, n, l); /* the first half of the previous working area contains sorted elements */
wmerge(xs, l, l + n - w, n, u, w);
}
for (n = w; n > l; --n) /*switch to insertion sort*/
for (m = n; m < u && xs[m] < xs[m-1]; ++m)
swap(xs, m, m - 1);
}
}
Де раніше визначено wmerge.
Повний вихідний код можна знайти тут, а детальне пояснення можна знайти тут
До речі, ця версія не найшвидший сорт злиття, тому що їй потрібно більше операцій своп. Згідно з моїм тестом, це швидше, ніж у стандартній версії, яка виділяє додаткові місця у кожній рекурсії. Але це повільніше, ніж оптимізована версія, яка заздалегідь подвоює початковий масив і використовує його для подальшого злиття.