参考链接:
使用JavaScript实现排序算法 (https://blog.csdn.net/LXY224/article/details/79535269)
js十大排序算法 (https://www.cnblogs.com/AlbertP/p/10847627.html)
前言:
常用的内部排序方法有:交换排序(冒泡排序、快速排序)、选择排序(简单选择排序、堆排序)、插入排序(直接插入排序、希尔排序)、归并排序、基数排序(一关键字、多关键字)。
- 时间复杂度指的是一个算法执行所耗费的时间
- 空间复杂度指运行完一个程序所需内存的大小
- 稳定指,如果a=b,a在b的前面,排序后a仍然在b的前面
- 不稳定指,如果a=b,a在b的前面,排序后可能会交换位置
- In-place: 占用常数内存,不占用额外内存
- Out-place: 占用额外内存
- n: 数据规模
- k:“桶”的个数
1.冒泡排序
原理:
冒泡排序每次从数组的最开始索引处与后一个值进行比较,如果当前值比较大,则交换位置。这样一次循环下来,最大的值就会排入到最后的位置。
function BubbleSort(arr){
for(var i =0;i<arr.length;i++){
for(var j=0;j<arr.length-1-i;j++){//因为每次循环都会有一个数被下沉,随着x增大下沉的元素就增加了
if(arr[j]>arr[j+1]){
let temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
return arr;
}
优化:
对冒泡排序的优化很简单,只要在外层循环中加入flag量进行判断——若本轮遍历没有发生任何一次交换,则终止循环。
优化代码如下:
<script>
function BubbleSort(arr){
let loopTimes = 0;
for(var i =0;i<arr.length;i++){
let finished = true;
for(var j=0;j<arr.length-1-i;j++){
if(arr[j]>arr[j+1]){
finished = false;
let temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
loopTimes++;
if(finished){
break;
}
}
return arr;
}
let arr = [2, 3, 9, 4, 5]
let res = bubbleSort(arr)
console.log('loopTimes:' + bubbleSort(arr))
console.log('the Array after sorting:' + arr)
</script>
进一步优化:
冒泡算法还能更进一步优化。使用pos位置量记录本次交换最远元素,避免遍历末尾已排好序的序列,减少遍历的总步数。
function bubbleSort(arr) {
let loopTimes = 0 // 循环计数器
let steps = 0 // 步数
let last = arr.length - 1;
for (let i = 0, len = arr.length; i < len; i++) {
let finished = true // flag
let pos = 0;
for (let j = 0, len = last; j < len; j++) {
if (arr[j] > arr[j + 1]) {
finished = false;
temp = arr[j + 1]
arr[j + 1] = arr[j]
arr[j] = temp
pos = j
}
steps++;
}
last = pos;
loopTimes++;
if (finished)
break;
}
console.log(steps);
return { loopTimes, steps }
}
let arr = [6, 4, 3, 5, 2, 1, 9, 10, 11, 12, 14, 15]
let res = bubbleSort(arr)
console.log('the Array after sorting:' + arr)
console.log('steps:' + res.steps)
console.log('loopTimes:' + res.loopTimes)
2. 选择排序
原理:
首先从原始数组中找到最小的元素,并把该元素放在数组的最前面,然后再从剩下的元素中寻找最小的元素,放在之前最小元素的后面,直到排序完毕。
function selectionSort(arr){
for(var i=0;i<arr.length;i++){
for(var j=i+1;j<arr.length;j++){
if(arr[i]>arr[j]){
let temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
}
}
return arr;
}
3. 插入排序
原理:
对于未排序数据,在一排序序列中从后向前扫描,为其找到相应的位置并插入。从后向前扫描的过程中需要对元素进行向后移位操作。
function insertSort(arr){
for(var i=1;i<arr.length;i++){
var preIndex = i-1;
var current = arr[i];
while(preIndex>=0&¤t<arr[preIndex]){
arr[preIndex+1] = arr[preIndex];
preIndex--;
}
arr[preIndex+1]=current;
}
return arr;
}
4. 希尔排序
原理:
据说是第一个突破O(n^2)的排序操作,可以说是简单插入排序的改进版。它与插入排序的不同在于,它会优先比较距离较远的元素。所以又叫缩小增量排序。核心在于间隔序列的设定,好的间隔序列的设定能够很大程度上的降低排序的时间复杂度。这次我采用的增量是数组长度的一半,然后依次折半。当增量减至1时候,数组被分为了一组,算法停止。
我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。
function shellSort(arr){
var len = arr.length;
for(var gap = Math.floor(arr.length/2);gap>0;gap = Math.floor(gap/2)){
for(var i =0 ; i<gap; i++) //i表示被分了几组
var j = i;
while(j<arr.length-gap){
var k = j;
while(arr[k+gap]<arr[k]&&k>=0){
var temp = arr[k+gap];
arr[k+gap] = arr[k];
arr[k] = temp;
k = k-gap;
}
j = j+gap;
}
}
return arr;
}
既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版》的合著者Robert Sedgewick提出的。
function shellSort(arr){
var len = arr.length,
temp,
gap=1;
while(gap<len/3){ //动态定义间隔序列
gap = gap*3+1;
}
for(gap;gap>0;gap=Math.floor(gap/3)){
for(var i = gap; i<len;i++){
temp = arr[i];
for(var j = i-gap; j>=0&&arr[j]>temp;j-=gap){
arr[j+gap]=arr[j];
}
arr[j+gap] = temp;
}
}
}
5. 归并排序
原理:
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
核心思想:分治。
主题流程:先将一个序列分成很多个不能再分割的子序列,将各个子序列分别排序后再将子序列合并。其实就是重复两个步骤:【1】分【2】合并。
首先是第一个小问题,怎么分?
比如说一个序列:12 ,23,1,44,233,10,9,8。我们先分成两段:12 ,23,1,44 和 233,10,9,8,
发现还能再分成4段:12 ,23 和 1,44------233,10 和 9,8。
再分成8段:12--23--1--44 和233--10--9--8。
这时候开始把子序列进行排序合并,一个元素就是有序的。所以不用排序。
合并成2个一组排序得到:12,23----1,44---10,233---8,9。
再合并成4个一组排序得到:1,12,23,44---8,9,10,233。
最后合并得到最终结果:1,8,9,10,12,23,44,233
归并排序在实现上就可以分为两个函数,一个负责分段,一个负责合并(因为分割后的每个子序列都是有序的,合并就是两个有序数组合并的过程)。
function merge(arr){
if(arr.length<2){
return arr;
}
var mid = Math.floor(arr.length/2);
//floor 向下取值;ceil向上取值,around 正常的四舍五入
var left = arr.slice(0,mid);
var right = arr.slice(mid);
return sort(merge(left),merge(right));
}
function sort(left,right){
var result = [];
var i =0;
var j =0;
while(i<left.lengt&&j<right.length){
if(left[i]<right[j]){
result.push(left[i]);
i++;
}else{
result.push(right[j]);
j++;
}
}
if(i==left.length){
result=result.concat(right.slice(j));
}
if(j==right.length){
result=result.concat(left.slice(i));
}
return result;
}
6. 快速排序
原理:
又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
数组中指定一个元素作为标尺,比它大的放到该元素后面,比它小的放到该元素前面,如此重复直至全部正序排列。
快速排序分三步:
- 选基准:在数据结构中选择一个元素作为基准(pivot)
- 划分区:参照基准元素值的大小,划分无序区,所有小于基准元素的数据放入一个区间,所有大于基准元素的数据放入另一区间,分区操作结束后,基准元素所处的位置就是最终排序后它应该所处的位置
- 递归:对初次划分出来的两个无序区间,递归调用第 1步和第 2步的算法,直到所有无序区间都只剩下一个元素为止。
普遍算法:
function quickSort(arr) {
if (arr.length <= 1) return ;
//取数组最接近中间的数位基准,奇数与偶数取值不同,但不印象,当然,你可以选取第一个,或者最后一个数为基准,这里不作过多描述
var pivotIndex = Math.floor(arr.length / 2);
var pivot = arr.splice(pivotIndex, 1)[0];
//左右区间,用于存放排序后的数
var left = [];
var right = [];
console.log('基准为:' + pivot + ' 时');
for (var i = 0; i < arr.length; i++) {
console.log('分区操作的第 ' + (i + 1) + ' 次循环:');
//小于基准,放于左区间,大于基准,放于右区间
if (arr[i] < pivot) {
left.push(arr[i]);
console.log('左边:' + (arr[i]))
} else {
right.push(arr[i]);
console.log('右边:' + (arr[i]))
}
}
//这里使用concat操作符,将左区间,基准,右区间拼接为一个新数组
//然后递归1,2步骤,直至所有无序区间都 只剩下一个元素 ,递归结束
return quickSort(left).concat([pivot], quickSort(right));
}
var arr = [14, 3, 15, 7, 2, 76, 11];
console.log(quickSort(arr));
/*
* 基准为7时,第一次分区得到左右两个子集[ 3, 2,] 7 [14, 15, 76, 11];
* 以基准为2,对左边的子集[3,2]进行划分区排序,得到[2] 3。左子集排序全部结束
* 以基准为76,对右边的子集进行划分区排序,得到[14, 15, 11] 76
* 此时对上面的[14, 15, 11]以基准为15再进行划分区排序, [14, 11] 15
* 此时对上面的[14, 11]以基准为11再进行划分区排序, 11 [14]
* 所有无序区间都只剩下一个元素,递归结束
*
*/
弊端:
它需要Ω(n)的额外存储空间,跟归并排序一样不好。在生产环境中,需要额外的内存空间,影响性能。
in-place
快速排序一般是用递归实现,最关键是partition分割函数,它将数组划分为两部分,一部分小于pivot,另一部分大于pivot。
采用右基准:
function quickSort(arr) {
// 交换
function swap(arr, a, b) {
var temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
// 分区
function partition(arr, left, right) {
/**
* 开始时不知最终pivot的存放位置,可以先将pivot交换到后面去
* 这里直接定义最右边的元素为基准
*/
var pivot = arr[right];
/**
* 存放小于pivot的元素时,是紧挨着上一元素的,否则空隙里存放的可能是大于pivot的元素,
* 故声明一个storeIndex变量,并初始化为left来依次紧挨着存放小于pivot的元素。
*/
var storeIndex = left;
for (var i = left; i < right; i++) {
if (arr[i] < pivot) {
/**
* 遍历数组,找到小于的pivot的元素,(大于pivot的元素会跳过)
* 将循环i次时得到的元素,通过swap交换放到storeIndex处,
* 并对storeIndex递增1,表示下一个可能要交换的位置
*/
swap(arr, storeIndex, i);
storeIndex++;
}
}
// 最后: 将pivot交换到storeIndex处,基准元素放置到最终正确位置上
swap(arr, right, storeIndex);
return storeIndex;
}
function sort(arr, left, right) {
if (left > right) return;
var storeIndex = partition(arr, left, right);
sort(arr, left, storeIndex - 1);
sort(arr, storeIndex + 1, right);
}
sort(arr, 0, arr.length - 1);
return arr;
}
console.log(quickSort([8, 4, 90, 8, 34, 67, 1, 26, 17]));
采用左基准:
function sort(arr,left,right) {//第一次调用的时候left=0,right=arr.length-1
var standard = arr[left];
var i=left;
var j=right;
if(left>=right)return arr;
while(i<j){
while(arr[j]>=standard&&j>i)j--;
while (arr[i]<=standard&&i<j)i++;
if(i<j){
var temp= arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
}
arr[left] = arr[j];
arr[j]=standard;
sort(arr,left,i-1);
sort(arr,i+1,right);
return arr;
}
7. 堆排序
原理:
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
基本思路:
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
注意:
从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。
var len; //因为声明的多个函数都需要数据长度,所以把len设置成全局变量
function buildMaxHeap(arr){ //建立大顶堆
len = arr.length;
for(var i = Math.floor(len/2);i>=0;i--){
heapify(arr,i);
}
}
function heapify(arr,i){ //堆调整
var left = 2*i+1;
right =2*i+2;
largest = i;
if(left<len&&arr[left]>arr[largest]){
largest = left;
}
if(right<len&&arr[right]>largest){
largest = right;
}
if(largest!=i){
swap(arr,i,largest);
heapify(arr,largest);
}
}
function swap(arr,i,j){
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
function heapSort(arr){
buildMaxHeap(arr);
for(var i = arr.length-1; i>0;i--){
swap(arr,0,i);
len--;
heapify(arr,0);
}
return arr;
}