一、如何分析一个排序算法
1.1 执行效率
1.1.1 最好、最坏、平均时间复杂度
在分析算法的好坏时,要分别说出最好、最坏、平均时间复杂度的同时,也要说出最好、最坏时间复杂度对应排序的原始数据是什么样的。
1.1.2 复杂度系数、常数、低阶
时间复杂度反应的是数据规模 n 很大的时候的一个增长趋势,它表示的时候会忽略系数、常数、低阶 ,小规模数据除外。
1.1.3 比较次数和移动次数
基于比较的排序算法,在分析算法效率时,我们要考虑到元素的比较和元素的移动。
1.2 内存消耗
算法的内存消耗可以通过空间复杂度来衡量,排序算法也不例外。我们引用一个名词叫做“原地排序”,就是指特定空间复杂度是 O(1) 的排序算法。
1.3 稳定性
如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变,就叫做“稳定排序”。
二、冒泡排序
2.1 算法思想
每次冒泡对相邻的两个元素进行比较,看是否满足大小关系,不满足就进行互换,一次冒泡会让至少一个元素移动到它应该在的位置。有 n 个数据,需要重复 n 次。
2.2 问题思考
2.2.1 是否为原地排序
冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1),是一个原地排序算法。
2.2.2是否为稳定排序
在冒泡排序中,只有交换才可以改变两个元素的前后顺序。为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法。
2.2.3最好、最坏以及平均时间复杂度
最好的情况是数据已经排好序,我们只进行一次冒泡排序就可以了,最好时间复杂度为 O(n) 。最坏的情况是,要排序的数据刚好是倒序排列的,我们只进行 n 此冒泡操作,所以最坏的时间复杂度为 O(n²),平均时间复杂度为 O(n²)。
2.3 代码实现
// 冒泡排序法:
// 比较相邻的元素。如果第一个比第二个大,就交换
// 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数。
// 针对所有元素重复以上的步骤,除了最后一个
// 重复步骤1-3,直到排序完成。
function bubbleSort(arr){
var len = arr.length;
for(var i = 0;i < len;i++){
for(var j = i + 1;j <= len;j++){
if(arr[i]>arr[j]){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
当某次冒泡过程已经没有数据交换,说明数据已经达到完全有序了,不用再执行后续冒泡操作。
算法优化代码:
//冒泡排序优化
function bubbleSort(arr){
var len = arr.length;
for(var i = 0;i < len;i++){
boolean flag = false;//判断进行了数据交换的标志位
for(var j = i + 1;j <= len;j++){
if(arr[i]>arr[j]){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
flag = true;
}
}
if(!flag){//没有数据交换,提前退出
break;
}
}
return arr;
}
三、插入排序
3.1 算法思想
我们将元素分为两个区间,未排序区间和已排序区间。我们要做的就是在未排序区间取出元素与已排序区间元素进行比较插入到适当位置,以此类推,直到未排序区间元素为空为止(顺序为从后向前比较)。
3.2 问题思考
3.2.1 是否为原地排序
插入排序的运算并不需要额外的存储空间,所以空间复杂度是 O(1),是一个原地排序算法。
3.2.2 是否为稳定排序
在插入排序中,对于值相同的元素,我们会将后边出现的元素插入到前边出现的元素的后边,所以插入排序是稳定排序。
3.2.3 最好、最坏以及平均时间复杂度
最好的情况就是数据元素已经排好序,最好的时间复杂度为 O(1) ,如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,需要移动大量的数据,最坏的时间复杂度是 O(n²)。我们在数组中插入数据的平均时间复杂度为 O(n),对于插入排序来说我们每次就相当于数组插入一个新的数据,循环执行n次插入数据,所以平均时间复杂度为 O(n²)。
3.3 代码实现
// 插入排序
// 原理:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入
// 步骤:
// 从第一个元素开始,该元素可以认为已经被排序
// 取出下一个元素,在已排序的元素序列中从后向前扫描
// 如果该元素大于新元素,则将该元素移到下一个位置
// 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
// 将新元素插入到该位置后
// 重复步骤2-5
function insertionSort(arr){
var len = arr.length;
var preIndex , temp;
for(var i = 1; i < len;i++){
for(var j = i;j < len;j--){
preIndex = j - 1;
if(arr[j]<arr[preIndex]){
temp = arr[j];
arr[j] = arr[preIndex];
arr[preIndex] = temp;
}
}
}
return arr;
}
四、选择排序
4.1 算法思想
和插入排序有点相似,将在未排序期间寻找到最小的数据,并将其放到已排好区间的元素的尾部。
4.2 问题思考
4.2.1 是否为原地排序
因为,数组中的两个元素需要相互交换,需要用一个变量来存储交换值,选择排序的空间复杂度为O(1),所以,是一种原地排序算法。
4.2.2 是否为稳定排序
选择排序每次都要找到剩余未排序元素的最小值,并和前边的元素交换位置,这样破坏了稳定性。所以说,选择排序是一种不稳定的排序算法。
4.2.3 最好、最坏以及平均时间复杂度
选择排序的最好情况就是已经是一组有序数据,最好的时间复杂度为 O(1),最坏的情况就是 O(n²)。平均时间复杂度就是 O(n²)。
4.3 代码实现
// 选择排序
// 原理:首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后再从剩余未排序元素中继续找最小元素,放到已排序序列的末尾。
// n-1趟后,所有元素排序完毕。
//
// 初始状态:无序区为R[1...n],有序区为空
// 第i趟排序开始时,有序区和无序区分别为R[1..i-1],R[i..n]。
// n-1趟排序后,数组有序化了。
function selectionSort(arr){
var len = arr.length;
var minIndex , temp;
for(var i = 0;i < len - 1;i++){
minIndex = i;
for(var j = i + 1;j < len;j++){
if(arr[minIndex]>arr[j]){
minIndex = j;
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
return arr;
}
五、实际应用中,为什么插入排序应用最为广泛?
冒泡排序不管怎么优化,元素交换的次数是一个固定值,是原始数据的逆序度。
插入排序是同样的,不管怎么优化,元素移动的次数也等于原始数据的逆序度。
从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个。
参考文章:https://www.cnblogs.com/onepixel/articles/7674659.html
https://yq.aliyun.com/articles/669203