本文整理了常见的有关排序的算法,包含以下七种:
- 冒泡排序(bubble sort)
- 选择排序(selection sort)
- 插入排序(insertion sort)
- 归并排序(merge sort)
- 快速排序(quick sort)
- 堆排序(heap sort)
- 计数排序(counting sort)
一、冒泡排序
其关键在于第n轮排序结束之后,使最大的数字出现在数组的长度 - n
索引处,一共需要进行数组的长度 - 1
轮排序。代码如下:
function bubbleSort(arr) {
if (arr == null || arr.length <= 1) return arr;
let len = arr.length;
for (let i = 0; i < len - 1; i++) {
// len - 1轮排序
for (let j = 0; j < len - 1 - i; j++) {
// 将最大的数移动到末尾
if (arr[j] > arr[j + 1]) {
// 数组元素交换
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
return arr;
}
二、选择排序(selection sort)
与冒泡不同的是,选择排序需要在第n轮选出最小的元素放在数组的n索引位。
function selectionSort(arr) {
if (arr == null || arr.length <= 1) return arr;
let len = arr.length;
let minIndex; //最小元素的索引
for (let i = 0; i < len; i++) {
minIndex = i;
for (let j = i + 1; j < len; j++) {
// 找出最小的元素的索引
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
}
return arr;
}
其实,可以直接用i
来代替变量minIndex
,代码如下:
function selectionSort(arr) {
if (arr == null || arr.length <= 1) return arr;
let len = arr.length;
for (let i = 0; i < len; i++) {
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[i]) {
[arr[i], arr[j]] = [arr[j], arr[i]]
}
}
}
return arr;
}
三、插入排序(insertion sort)
想象我们在打扑克牌时,假设手中的牌已经是有序的,当再次从牌堆中抓起一张牌,会将新抓起的牌插入到合适的位置,使手中的牌始终保持有序。现在假设数组的第一个元素已经是排序好的(相当于手中的牌),接下来关键在于如何告知程序将一个新的元素插入已经有序的区域中。
function insertionSort(arr) {
if (arr == null || arr.length <= 1) return arr;
let len = arr.length;
for (let i = 1; i < len; i++) {
// 当前的元素大于等于上一个元素时,无需处理,继续遍历
// 当前的元素小于上一个元素时,交换位置,索引前移,继续while循环
let j = i; //保存一下当前的位置,便于while循环中使用,避免污染外层循环的变量i
while (j - 1 >= 0 && arr[j] < arr[j - 1]) {
[arr[j - 1], arr[j]] = [arr[j], arr[j - 1]]
j--;
}
}
return arr;
}
四、归并排序(merge sort)
归并排序遵循分治模式。大致分为三个步骤:
- 分解:分解带排序的n个元素的序列成各具n/2个元素的两个子序列
- 解决:使用归并排序递归地排序两个子序列
- 合并:合并两个已经排序的子序列。
当待排序的序列长度为1时,可以认为已经排好序,递归“开始回升”。
function mergeSort(arr) {
if (arr == null || arr.length <= 1) return arr;
let mid = Math.floor(arr.length / 2);
let left = arr.slice(0, mid);
let right = arr.slice(mid);
return merge(mergeSort(left), mergeSort(right));
}
// 合并两个序列
function merge(left, right) {
let result = [];
while (left.length && right.length) {
if (left[0] < right[0]) {
result.push(left.shift())
} else {
result.push(right.shift())
}
}
result.push(...left);
result.push(...right);
return result;
}
五、快速排序(quick sort)
在快速排序中,也使用到了分治法。其关键在于,指定一个基准元素(pivot),在每轮排序结束后,使小于基准的元素排列在基准元素的一侧,大于基准的元素排列在基准元素的另一侧。然后分别对两侧递归使用快速排序。有两种实现方法:
- 单边循环法
假设基准元素(pivot)为数组第一位,声明一个mark指针(用于维护一个小于pivot的区域),起始指向数组第一位(表示当前暂无元素小于pivot),从左向右循环数组。若当前循环位置的元素大于pivot,继续向后循环,若当前循环位置的元素小于pivot,将mark指针右移(增加了一个小于pivot的元素),当前元素和mark指针所在的元素交换。循环结束,将pivot交换到mark指针所在的位置。这样就完成了一次循环。
function quickSort(arr, startIndex, endIndex){
if (arr == null || arr.length <= 1 || startIndex >= endIndex) {
return arr;
}
let mid = partition(arr, startIndex, endIndex);
quickSort(arr, startIndex, mid - 1);
quickSort(arr, mid + 1, endIndex);
return arr;
}
function partition(arr, startIndex, endIndex){
let pivot = startIndex;
let mark = startIndex;
for(let i = startIndex; i <= endIndex; i++){
if(arr[i] < arr[pivot]){
mark ++;
[arr[i], arr[mark]] = [arr[mark], arr[i]]
}
}
[arr[pivot], arr[mark]] = [arr[mark], arr[pivot]]
return mark;
}
- 双边循环法
假设基准元素(pivot)为数组第一位,声明两个指针left和right。起始分别指向数组最低索引位和最高索引位。从right指针开始循环,当right指针所指的元素大于pivot时,继续循环,当right指针所指的元素小于pivot时,停止循环,切换至left指针,当left指针所指的元素小于等于pivot时,继续循环,当left指针所指的元素大于pivot时,停止循环,切换至right指针,。left指针所指的元素和right指针所指的元素交换。
function quickSort(arr, startIndex, endIndex) {
if (arr == null || arr.length <= 1 || startIndex >= endIndex || startIndex > arr.length - 1) {
return arr;
}
let mid = partition(arr, startIndex, endIndex);
quickSort(arr, startIndex, mid - 1);
quickSort(arr, mid + 1, endIndex);
return arr;
}
function partition(arr, startIndex, endIndex) {
let pivot = startIndex;
let left = startIndex;
let right = endIndex;
while (left < right) {
// 当right指针所指的元素大于pivot时,继续循环
while (left < right && arr[right] > arr[pivot]) {
right--;
}
// 当left指针所指的元素小于等于pivot时,继续循环
while (left < right && arr[left] <= arr[pivot]) {
left++;
}
if (left < right) {
// left指针所指的元素和right指针所指的元素交换
[arr[left], arr[right]] = [arr[right], arr[left]]
}
}
// 此处left === right, 将基准元素和left指针所在的元素交换,一次分治完成
[arr[pivot], arr[left]] = [arr[left], arr[pivot]]
return left;
}
六、堆排序(heap sort)
以最大堆为例。首先解决建立最大堆(根节点大于左右节点)的问题。然后从后往前循环数组,第n次循环时,交换堆顶和数组的n索引位的元素,重新进行堆顶调整。
let len;
function buildMaxHeap(arr) {
if (arr == null || arr.length <= 1) return arr;
len = arr.length;
let mid = Math.floor(len / 2);
for (let i = mid; i >= 0; i--) {
heapify(arr, i)
}
}
function heapify(arr, index) {
let left = index * 2 + 1;
let right = index * 2 + 2;
let largest = index;
if (left < len && arr[largest] < arr[left]) {
largest = left;
}
if (right < len && arr[largest] < arr[right]) {
largest = right;
}
if (largest !== index) {
[arr[largest], arr[index]] = [arr[index], arr[largest]];
heapify(arr, largest);
}
}
function heapSort(arr) {
buildMaxHeap(arr);
for (let i = len - 1; i > 0; i--) {
[arr[0], arr[i]] = [arr[i], arr[0]];
len--;
heapify(arr, 0);
}
}
七、计数排序(counting sort)
前提:数组的元素均为整数;元素的最大值和最小值差距较小。
假设数组arr中有n个随机整数,取值范围为0 - range, 那么可以根据这个有限范围,建立一个长度为n的数组countArr,元素初始值均为0。然后开始遍历数组arr,每一个元素按照其值对号入座(比如arr[i]的值为5时,相应countArr[5 ]的值加1),便利结束后countArr数组每一个下标对应的值就代表数组arr中对应数值出现的次数。有了这个统计结果,排序就变得非常容易了。直接遍历数组countArr,输出数组元素的下标值,元素的值为几就输出几次。
let range = 11;
function countingSort(arr) {
if (arr == null || arr.length <= 1) return arr;
let result = [];
let countArr = new Array(range);
countArr.fill(0);
let elem;
for (let i = 0; i < arr.length; i++) {
elem = arr[i];
countArr[elem]++;
}
for (let j = 0; j < countArr.length; j++) {
while (countArr[j]) {
result.push(j);
countArr[j]--;
}
}
return result;
}