排序算法
直接插入排序
直接插入排序的思路是遍歷過程中保持被遍歷過的元素有序,每遍歷到一個新元素時如不能保持已遍歷元素有序,則爲該新元素尋找合適的插入位置插入以保持遍歷元素的有序性,當完成遍歷時所有元素均有序。
假設初始列表items = [4, 2, 3, 1, 5],非降序的直接插入排序過程如下:
- 初始已遍歷內容爲[4],從i = 1開始,items[i = 1] = 2,[4] => 插入2 => [2, 4]
- items[i = 2] = 3,[2, 4] => 插入3 => [2, 3, 4]
- items[i = 3] = 1,[2, 3, 4] => 插入1 => [1, 2, 3, 4]
- items[i = 4] = 5,[1, 2, 3, 4] => 插入5 => [1, 2, 3, 4, 5]
代碼實現:
function insertSort(items) {
let i, j, tmp;
for (i = 1; i < items.length; i++) {
if (items[i] < items[i - 1]) {
tmp = items[i];
items[i] = items[i - 1];
for (j = i - 1; j >= 0 && items[j] > tmp; j--) {
items[j + 1] = items[j];
}
items[j + 1] = tmp;
}
}
return items;
}
冒泡排序
經典排序算法,可總結成:“兩兩比較,兩兩交換”,每一輪比較和交換可以將一個最大或最小元素移動到對應的位置。
假設初始列表items = [4, 2, 3, 1, 5],非降序的冒泡排序過程如下:
- [(2, 4), 3, 1, 5] => [2, (3, 4), 1, 5] => [2, 3, (1, 4), 5] => [2, 3, 1, (4, 5)]
- [(2, 3), 1, 4, 5] => [2, (1, 3), 4, 5] => [2, 1, (3, 4), 5]
- [(1, 2), 3, 4, 5] => [1, (2, 3), 4, 5]
- [(1, 2), 3, 4, 5] => [1, 2, 3, 4, 5]
流程裏中括號內的小括號是進行比較和交換的元素,整個排序看起來像是目標元素從列表中向上“冒出”的過程,所以被稱爲冒泡排序。
代碼實現:
function bubbleSort(items) {
let i, j, tmp;
for (i = 0; i < items.length - 1; i++) {
for (j = 1; j < items.length - i; j++) {
if (items[j] < items[j - 1]) {
tmp = items[j];
items[j] = items[j - 1];
items[j - 1] = tmp;
}
}
}
return items;
}
選擇排序
選擇排序的思路是在遍歷過程中依次挑選出最大、次大、第三大…元素,並將其移動到對應的位置上,當然也可以反過來用挑選最小、次小、第三小…的方式。
假設初始列表items = [4, 2, 3, 1, 5],非降序的選擇排序過程如下:
- i = 0,找到最小元素的下標爲3,交換後:[1, 2, 3, 4, 5]
- i = 1,…,[1, 2, 3, 4, 5]
- i = 2,…,[1, 2, 3, 4, 5]
- i = 3,…,[1, 2, 3, 4, 5]
代碼實現:
function selectSort(items) {
let i, j, tmp;
for (i = 0; i < items.length - 1; i++) {
let k = i;
for (j = i + 1; j < items.length; j++) {
if (items[k] > items[j]) {
k = j;
}
}
tmp = items[k];
items[k] = items[i];
items[i] = tmp;
}
return items;
}
希爾排序
希爾排序是對直接插入排序的一種優化改進,做法是先挑選一個遞減的delta序列,隨後遍歷delta序列時從待排序序列中按步長爲delta的方式挑選子序列(共delta個)進行直接插入排序,直到delta = 1進行最後一次排序後,整個序列有序。
假設初始列表items = [4, 2, 6, 3, 1, 5, 8, 7],非降序的希爾排序排序過程如下:
- 選擇delta序列[3, 2, 1]
- delta = 3,對子序列[4, 3, 8]、[2, 1, 7]、[6, 5]分別排序得到[3, 4, 8]、[1, 2, 7]、[5, 6],此時items = [3, 1, 5, 4, 2, 6, 8, 7]
- delta = 2,對子序列[3, 5, 2, 8]、[1, 4, 6, 7]分別排序得到[2, 3, 5, 8]、[1, 4, 6, 7],此時items = [2, 1, 3, 4, 5, 6, 8, 7]
- delta = 1,對序列[2, 1, 3, 4, 5, 6, 8, 7]排序得到[1, 2, 3, 4, 5, 6, 7, 8]
代碼實現:
function shellSort(items) {
let delta = [];
let n = items.length;
do {
delta.push(n = Math.floor(n / 2));
} while (n > 1);
let i, j, k, l, tmp;
for (i = 0; i < delta.length; i++) {
j = delta[i];
for (k = j; k < items.length; k++) {
if (items[k] < items[k - j]) {
tmp = items[k];
for (l = k - j; l >= 0 && tmp < items[l]; l -= j) {
items[l + j] = items[l];
}
items[l + j] = tmp;
}
}
}
return items;
}
快速排序
快速排序的思路是先從序列中選擇一個元素作爲“軸中數”,然後分別將大於軸中數的元素放到右側,小於的元素放在左側,接着對左側和右側的數再次採用快速排序,快速排序的終止條件是待排序序列長度不大於1。
假設初始列表items = [4, 2, 3, 1, 5],非降序的快速排序過程如下:
- [4, 2, 3, 1, 5]:4 => [(1, 2, 3), 4, (5)]
- [1, 2, 3]:1、[5]:5 => [1, (2, 3), 4, 5]
- [2, 3]:2 => [1, 2, (3), 4, 5]
- [3]:3 => [1, 2, 3, 4, 5]
上述過程,中括號接冒號後面的值表示所選的軸中數(序列第一個元素),代碼如下:
function quickSort(items) {
return _sort(items, 0, items.length - 1);
function _sort(items, low, high) {
if (low >= high) {
return items;
}
let i = low;
let j = high;
let pivot = items[low];
while (i < j) {
while (i < j && items[j] >= pivot) j--;
items[i] = items[j];
while (i < j && items[i] <= pivot) i++;
items[j] = items[i];
}
items[i] = pivot;
_sort(items, low, i - 1);
_sort(items, i + 1, high);
return items;
}
}
堆排序
對於長度爲n的序列,大根堆的元素滿足:Ai >= A2i, 2i+1(2i + 1 < n),小根堆的元素滿足:Ai <= A2i, 2i+1(2i + 1 < n)。可將序列轉換爲一顆完全二叉樹,大根堆的約束是所有非葉子結點不小於其左右孩子結點,小根堆則是不大於。
堆排序過程是先將序列調整成大根堆或小根堆,然後取走堆的根元素並用最後面的元素填充到根元素位置,隨後反覆調整堆符合大(小)根堆的定義以及取走根元素與填充最後元素到根元素位置,直到堆爲空。
假設初始列表items = [4, 2, 6, 3, 7, 1, 5],他對應的完全二叉樹如下:
|------4------|
| |
|---2---| |---6---|
| | | |
3 7 1 5
非降序的堆排序過程如下:
- 從items[floor(items.length / 2) - 1] = items[2] = 6開始向前調整,使其逐漸成爲最大堆
- [4, 2, 6, 3, 7, 1, 5] => 6與1、5比較,不需要交換 => [4, 2, 6, 3, 7, 1, 5]
- [4, 2, 6, 3, 7, 1, 5] => 2與3、7比較,2與7交換 => [4, 7, 6, 3, 2, 1, 5]
- [4, 7, 6, 3, 2, 1, 5] => 4與7、6比較,4與7交換 => [7, 4, 6, 3, 2, 1, 5],每一次交換需要考慮交換後的元素作爲根的堆是否滿足大根堆定義,不滿足則需要繼續交換,這裏4與7交換後,4作爲根的堆左右孩子分別爲3、2,故無需調整
經過上面4步後,整個序列已經符合大根堆的定義,其對應的完全二叉樹如下:
|------7------|
| |
|---4---| |---6---|
| | | |
3 2 1 5
接着進行後續步驟:
- 取走7,將5放在7的位置,並調整5作爲根的堆成爲大根堆,[7, 4, 6, 3, 2, 1, 5] => [5, 4, 6, 3, 2, 1]、[7] => [6, 4, 5, 3, 2, 1]、[7]
- [6, 4, 5, 3, 2, 1]、[7] => 取走6用1替代 => [1, 4, 5, 3, 2]、[6, 7] => 調整1作爲根的堆成爲大根堆 => [5, 4, 1, 3, 2]、[6, 7]
- [5, 4, 1, 3, 2]、[6, 7] => 取走5用2替代 => [2, 4, 1, 3]、[5, 6, 7] => 調整2作爲根的堆成爲大根堆 => [4, 3, 1, 2]、[5, 6, 7]
- …與上類似
- 最終得到[]、[1, 2, 3, 4, 5, 6, 7]
代碼如下:
function heapSort(items) {
let len = items.length;
for (let i = Math.floor(len / 2) - 1; i >= 0; i--) {
_fixHeap(items, i, len - 1);
}
for (let i = len - 1; i > 0; i--) {
let tmp = items[0];
items[0] = items[i];
items[i] = tmp;
_fixHeap(items, 0, i - 1);
}
return items;
function _fixHeap(items, s, m) {
let tmp = items[s];
for (let i = 2 * s + 1; i <= m; i = i * 2 + 1) {
if (i < m && items[i] < items[i + 1]) {
i++;
}
if (tmp >= items[i]) {
break;
}
items[s] = items[i];
s = i;
}
items[s] = tmp;
}
}
歸併排序
歸併排序的過程是多次將兩個有序序列合併爲一個有序序列,直到最後只剩下一個序列。
假設初始列表items = [4, 2, 6, 3, 1, 5, 8, 7],非降序的歸併排序排序過程如下(正向描述):
- [4, 2, 6, 3, 1, 5, 8, 7] => 8個有序序列兩兩合併 => [(2, 4), (3, 6), (1, 5), (7, 8)]
- [(2, 4), (3, 6), (1, 5), (7, 8)] => 4個有序序列兩兩合併 => [(2, 3, 4, 6), (1, 5, 7, 8)]
- [(2, 3, 4, 6), (1, 5, 7, 8)] => 2個有序序列合併 => [(1, 2, 3, 4, 5, 6, 7, 8)]
代碼採用的是遞歸方式,如下:
function mergeSort(items) {
if (items.length <= 1) {
return items;
}
let mid = Math.floor(items.length / 2);
let left = mergeSort(items.slice(0, mid));
let right = mergeSort(items.slice(mid));
let result = [];
let i = 0;
let j = 0;
while (i < left.length || j < right.length) {
if (i === left.length) {
result.push(right[j++]);
continue;
}
if (j === right.length) {
result.push(left[i++]);
continue;
}
if (left[i] < right[j]) {
result.push(left[i++]);
}
else {
result.push(right[j++]);
}
}
return result;
}