介绍
考量排序算法优劣的三个标准:
执行效率, 内存消耗, 稳定性
冒泡、插入、选择排序都有一个共同点,那就是将待排序数列分为已排序和未排序两部分。在未排序的部分中查找一个最值,放到已排序数列的恰当位置。
具体到代码层面,外层循环的变量用于分割已排序和未排序数,内层循环的变量用于在未排序数中查找。从思路上看,这三种算法其实是一样的,所以时间复杂度也相同。
希尔排序
function shellSort (arr) {
let len = arr.length
// gap 即为增量
for (let gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
for (let i = gap; i < len; i++) {
let j = i
let current = arr[i]
while (j - gap >= 0 && current < arr[j - gap]) {
arr[j] = arr[j - gap]
j = j - gap
}
arr[j] = current
}
}
return arr
}
var arr = [3, 5, 7, 1, 4, 56, 12, 78, 25, 0, 9, 8, 42, 37]
console.log(shellSort(arr))
冒泡排序
-
排序中交换的次数 = 逆序度 ;
逆序度 = 满有序度 - 有序度,满有序度为n*(n-1)/2
-
空间复杂度为O(1),是原地排序算法;平均时间复杂度为O(n²)。
平均情况下,需要 n (n-1)/4 次交换操作,比较操作肯定要比交换操作多,而复杂度的上限是 O(n2),所以平均情况下的时间复杂度就是 O(n2)。
-
是稳定的排序算法
-
排序的过程是增加有序度(从小到大),减少逆序度,最后达到满有序度
// 冒泡排序,a表示数组,n表示数组大小
public void bubbleSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 0; i < n; ++i) {
// 提前退出冒泡循环的标志位
boolean flag = false;
//此处的n-i-1是关键,要结合理解:每次遍历的下标范围从0~n-1->0~n-2->……->0~1
for (int j = 0; j < n - i - 1; ++j) {
if (a[j] > a[j+1]) { // 交换
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true; // 表示有数据交换
}
}
if (!flag) break; // 没有数据交换,提前退出
}
}
插入排序
-
空间复杂度为O(1),是原地排序算法
-
是稳定排序算法
-
时间复杂度
- 最好O(n)
- 最坏=平均O(n2)
// 插入排序,a表示数组,n表示数组大小
public void insertionSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 1; i < n; ++i) {
int value = a[i];
int j = i - 1;
// 查找插入的位置
for (; j >= 0; --j) {
if (a[j] > value) {
a[j+1] = a[j]; // 数据移动
} else {
break;
}
}
a[j+1] = value; // 插入数据
}
}
1. 概念
打过扑克牌的人应该秒懂, 插入排序是一种最简单直观的排序算法, 它的工作原理是通过构建有序序列, 对于未排序数据, 在已排序序列中从后向前扫描, 找到相应位置并插入
2. 思路
- 将第一待排序序列第一个元素看作一个有序序列, 把第二个元素到最后一个元素当成是未排序序列
- 从头到尾依次扫描未排序序列, 将扫描到的每个元素插入有序序列的适当位置
3. 代码实现
const arr = readline().split(',').map(Number)
function insertSort(arr) {
let len = arr.length
let preIndex //指向
for (let i=1; i<len; i++) {
preIndex = i-1
}
}
冒泡和插入的比较
//冒泡排序中数据的交换操作:
if (a[j] > a[j+1]) { // 交换
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true;
}
//插入排序中数据的移动操作:
if (a[j] > value) {
a[j+1] = a[j]; // 数据移动
} else {
break;
}
选择排序
- 空间复杂度为O(1),是原地排序算法
- 是不稳定的排序算法
- 时间复杂度:最好=最坏=平均O(n2)
1. 概念
典型的时间换空间
选择排序是一种简单直观的排序算法, 无论什么数据进去都是O(n^2)的时间复杂度
所以用到它的时候, 数据规模越小越好, 唯一的好处就是不占用额外的存储空间
2. 思路
首先在未排序序列中找到最小(大)元素, 存放到排序序列的起始位置
再从剩余未排序元素中继续寻找最小(大)元素, 然后放到已排序序列的末尾
重复第二步, 直到所有元素排序完毕
3. 代码实现
// 选择排序
function selectionSort(arr){
// 保存最小值索引
let minIndex, temp;
// 外循环: 获取首个元素(直到倒数第二个元素)
for (let i=0; i<arr.length-1; i++){
// 假设最小值是i, 也就是未排序序列的排头元素
minIndex = i;
// 从i的后面位置找最小值
// 内循环(直到最后一个元素)
for (let j=i+1; j<arr.length; j++){
if (arr[j]<arr[minIndex]) {
minIndex = j;
}
}
// 交换排头元素与最小值
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
return arr;
}
// 输入处理, 将传入的字符串转换为一个数组
const arr = readline().split(',');
// 打印验证
console.log(selectionSort(arr).join(',');
归并排序
- 是一种稳定排序算法
- 时间复杂度在任何情况下都是O(nlogn)
- 空间复杂度为O(n), 不是原地排序算法
// 合并两个有序数组(在两个数组上归并)
function merge_two_sortedArr (arr1, arr2) {
const l1 = arr1.length
const l2 = arr2.length
let p = 0
let q = 0
let res = []
while (p < l1 || q < l2) {
if (p === l1) {
res.push(arr2[q++])
} else if (q === l2) {
res.push(arr1[p++])
} else if (arr1[p] < arr2[q]) {
res.push(arr1[p++])
} else {
res.push(arr2[q++])
}
}
return res
}
let arr1 = [1, 4, 6]
let arr2 = [3, 12, 88]
console.log(merge_two_sortedArr(arr1, arr2))
// 在原数组上进行归并
function merge_in_place (arr, l, mid, r) {
let keep = []
for (let i = l; i <= r; i++) {
keep[i] = arr[i]
}
let p = l
let q = mid + 1
for (let i = l; p <= mid || q <= r; i++) {
if (p === mid + 1) {
arr[i] = keep[q++]
} else if (q === r + 1) {
arr[i] = keep[p++]
} else if (keep[p] < keep[q]) {
arr[i] = keep[p++]
} else {
arr[i] = keep[q++]
}
}
return keep
}
let arr = [1, 4, 5, 2, 3, 6, 7]
merge_in_place(arr, 0, 2, arr.length - 1)
console.log(arr)
// 归并排序
function merge (arr, l, mid, r) {
const keep = []
for (let i = l; i <= r; i++) {
keep[i] = arr[i]
}
for (let p = l, q = mid + 1, k = l; p <= mid || q <= r;) {
if (p === mid + 1) arr[k++] = keep[q++]
else if (q === r + 1) arr[k++] = keep[p++]
else if (keep[p] > keep[q]) arr[k++] = keep[q++]
else arr[k++] = keep[p++]
}
}
function mSort (arr, l, r) {
if (l >= r) return
// 分半
const mid = l + Math.floor((r - l) / 2)
// 归并排序左侧
mSort(arr, l, mid)
// 归并排序右侧
mSort(arr, mid + 1, r)
// 走到这里, l~mid 部分已排序,mid+1~r 部分已排序
// 直接原地归并操作就可以实现数组排序
merge(arr, l, mid, r)
}
function MergeSort (arr) {
mSort(arr, 0, arr.length - 1)
}
const nums = [12, 1, 7, 4, 5, 2, 10, 6, 3, 11, 9, 8, 13]
MergeSort(nums)
console.log(nums);
快速排序
快排是一种原地、不稳定的排序算法, 运用了分治的思想
时间复杂度:
- 最优时间复杂度(分区极其均衡)
- O(nlogn)
- 最坏时间复杂度(分区极其不均衡, 例如对有序数组进行排序)
- O(n^2)
- 平均时间复杂度
- O(n)
在大部分情况下的时间复杂度都可以做到 O(nlogn),只有在极端情况下,才会退化到 O(n2),同时我们也有很多方法将这个概率降到极低
算法:
- 第一步:选择数组的开头元素作为基数
empty = arr[begin] - 第二步:将序列中大于基数的放在基数右边,小于基数的放在基数的左边
- 双while循环
- 第三步:对基数的左边和右边两个序列重复第二步和第三步
quickSort(arr, begin, i - 1)quickSort(arr, i + 1, end)
代码:
function quickSort (arr, begin, end) {
if (begin >= end) return
let i = begin,
j = end,
empty = arr[begin]
// 第二步
while (i < j) {
// 先移动j
while (i < j && arr[j] > empty) {
j--
}
arr[i] = arr[j]
// 再移动i
while (i < j && arr[i] < empty) {
i++
}
arr[j] = arr[i]
}
// 此时i,j在同一位置, i==j
arr[i] = empty
// 第三步
quickSort(arr, begin, i - 1)
quickSort(arr, i + 1, end)
}
let arr = [2, 5, 3, 7, 6, 4, 21, 13, 1]
quickSort(arr, 0, arr.length - 1)
console.log(arr)