js经典排序算法

参考链接:

使用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&&current<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. 快速排序

原理:

又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

数组中指定一个元素作为标尺,比它大的放到该元素后面,比它小的放到该元素前面,如此重复直至全部正序排列。

快速排序分三步:

  1. 选基准:在数据结构中选择一个元素作为基准(pivot)
  2. 划分区:参照基准元素值的大小,划分无序区,所有小于基准元素的数据放入一个区间,所有大于基准元素的数据放入另一区间,分区操作结束后,基准元素所处的位置就是最终排序后它应该所处的位置
  3. 递归:对初次划分出来的两个无序区间,递归调用第 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;
     }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值