常见排序算法总结

前言

校招已经开始 的了。。前端不知道算法会考查到什么地步,但是常见的排序算法是必须要掌握的!
网络上查到很多都是JAVA,phython写的。js的版本有的写的很晦涩。所以我自己来写一遍吧

冒泡排序

思路:将数组从头开始,两两比较,如果左边的比右边大,则交换。
这样一来,一轮下来,最右边的就会是最大的数组元素啦!
然后我们把循环次数减一,再换一遍。然后数组的倒数第二个又是最大的啦!
直到循环的次数减为1,数组的排序就完成啦!

接下来是代码环节!

function bubbleSort(arr) {
        var len = arr.length - 1;
        //循环次数每次减一,直到0,跳出循环
        while(len) {

            for(var i = 0; i < len; i++) {

                if(arr[ i ] > arr[ i + 1 ]) swap(arr,i,i+1);

            }

            len -= 1;

        }
        //swap,交换数组的index1与index2项  在排序中swap函数很常见
        function swap(arr,index1,index2) {
            var tmp = arr[index1];
            arr[index1] = arr[index2];
            arr[index2] = tmp;
        }
    }

选择排序

思路:把数组中的最大元素放到数组的最后,数组的长度减一,然后如此往复,最后数组排序就完成啦。

function selectSort(arr) {

        var result = [];

        while(arr.length) {
            var min = Math.min.apply(null,arr);
            var index = arr.indexOf(min);
            //找到最小元素然后把它从数组中移除
            arr.splice(index,1);
            result.push(min);
        }

        return result;    

    }

插入排序

思路:把数组第一个元素取出,然后将第二个元素与第一个比较排序,接下来将第三个元素取出插入到之前那排序的两个元素中。如此往复,完成排序。
代码:

function insertSort(arr) {
    var result = [];
    //先把第一个元素放进数组
    result.push(arr[0]);
    //然后从第二个数字开始往我们的result里插入
    for(var i = 0; i < arr.length - 1; i ++ ) {
        insert(result,arr[i+1]);
    }

    return result;

    function insert(arr,num) {
        var index;
        for(var i = 0; i < arr.length; i++) {
            //等于时也往同样的位置插入
            if( num >= arr[i] && num <= arr[i+1]) {
                index = i+1;
            }
        }
        if(num > Math.max.apply(null,arr)) index = arr.length;
        if(num < Math.min.apply(null,arr)) index = 0;

        arr.splice(index,0,num);
    }
}

非常遗憾,我们的插入排序确实思路很简单。但是这样子写开辟了额外的数组空间。同时,如果我们要进行希尔排序。这样写会造成麻烦。。
这里重新用比较常见的写法来写一遍:
代码:

function insertSort(arr){

  for(var i = 1; i < arr.length; i++){
      var tmp = arr[i];
      var j = i - 1;
      arr[i] = arr[j];
      while(j >= 0 && tmp < arr[j]){
        arr[j+1] = arr[j];
        j--;
      }
      arr[j+1] = tmp;
  }
}

这样写的精妙之处在于,我们也是先把第一项排除,然后把第二项往前插。但是,这里我们直接取j为我们的前一项,i为当前项。我们直接将当前项变成我们的前一项。然后开始插入,当然,插入的数字我们先要保存下来,那就是当前项的值。(此时数组中没有当前项。。)从j开始往前比较,如果比当前元素小,那么很正常。。还要往前找,这时候把当前项赋给后一项。如果比当前元素大,那么久把tmp赋给j+1处的元素。这样一来,既不需要申请新的空间,也能进行插入了!

好了在此基础之上,我们来写希尔排序。

希尔排序

思路: 希尔排序,是对插入排序的优化。我们先取一个gap,为arr.len除以2取整数。我们根据gap进行分组,对组内的元素进行插入排序。然后gap除以2,再次分组,继续对组内插入排序。当gap=1时,只有一组,最后完成我们的排序。这样子分组,使得插入排序的交换次数大大的减少。但是由于gap的存在,算法不稳定。

代码:

function shellSort(arr) {
    var len = arr.length;
    var gap = Math.floor(len/2);

    while(gap >= 1) {

        for(var i = gap; i < arr.length; i++) {

            var temp = arr[i];
            var j;

            for( j = i - gap; j >= 0 && temp < arr[j]; j -= gap) {

                arr [ j+gap ] = arr[j];                

            }

            arr[ j+gap ] = temp;

        }

        gap = Math.floor(gap / 2);
    }
}

可以看到,之前是对整个数组进行遍历,而这里,我们循环时,不是每次-1,而是-gap值,当然之前插入排序中的1都是gap啦~
在外层循环中,我们从gap开始,这样子就能对整个数组进行分组啦。

快速排序

思路:快速排序其实思路比较简单,数组中找一个枢轴,这个元素影响我们的排序的复杂程度,选择中间数当然是最佳。然后把比他大的放在右边,比他小的放在左边,然后把三者合起来。当然左边与右边的数组要递归处理。

代码:

function quickSort(arr) {
    if(arr.length) {
        var pivotIndex = Math.floor(arr.length/2);
        var pivot = arr[pivotIndex];
        var bigArr = [];
        var smallArr = [];
        var sameArr = [];
        for(var i = 0; i < arr.length; i ++ ) {

            if(arr[ i ] > pivot) {
                bigArr.push(arr[i]);
            }else if(arr[ i ] < pivot) {
                smallArr.push(arr[i]);
            }else{
                //因为可能有重复元素,所以用三路快排
                sameArr.push(arr[i]);
            }    
        }

        var sortLeft = quickSort(smallArr);
        var sortRight = quickSort(bigArr);

        return sortLeft.concat(sameArr).concat(sortRight);

    }else{
        //其实快排也算是分治思想的运用如果没有元素则返回空
        return [];
    }

}

归并排序

思路:归并排序是分治思想的运用。同时也运用到了递归。

代码:

function mergeSort(arr) {
    if(arr.length == 1) {
        //若只有一个元素则直接返回
        return arr;
    }else{
        var midIndex = Math.floor(arr.length/2);
        var leftPart = arr.slice(0,midIndex);
        var rightPart = arr.slice(midIndex);
        //若有两个我们将其合并,当然需要递归处理子数组
        return merge( mergeSort(leftPart), mergeSort(rightPart));
    }
}

function merge(arr1,arr2) {
    var result = [];
    //知道其中一个数组内元素完全被推出,则合并完毕
    while(arr1.length && arr2.length) {

        if(arr1[0] < arr2[0]) {
            result.push(arr1.shift());
        }else {
            result.push(arr2.shift());
        }

    }
    //我们并不知道哪个为空,所以都concat
    return result.concat(arr1,arr2);    
}

堆排序

思路:堆排序需要借助堆这个数据结构。堆是一个完全二叉树,且堆的父节点的值比子节点要大(大根堆)。
我们先利用数组来构建大根堆(父大于子,父小于子是小根堆),然后首位互换数组长度减一后再次调整堆。知道完成排序。
代码:

//调整函数,将数组调整为大根堆,从所选的节点,递归调整其所有的子节点
function adjustHeap(arr, i, len){
    //首先假设传入的index节点的值最大
    var maxIndex = i;
    // 根据树的结构,右节点是 2(i+1),做节点比又节点小一
    var right = 2 * ( i + 1 );
    var left = right - 1;

    //传入节点可能没有子节点,没有右节点,或者有左右子节点
    //如果没有子节点那么maxIndex = i
    //如果有左节点,若他比较大,那么maxIndex替换为left,否则不变
    if( left < len && arr[left] > arr[i] ) {

        maxIndex = left;

    }
    //然后如果有右节点,与之前的大的节点比较,若大于maxIndex
    //替换为right。否则不变
    if( right < len && arr[right] > arr[maxIndex] ) {

        maxIndex = right;

    }
    //如果maxIdex被替换了,说明要对无序数组进行调整来构建堆
    if( maxIndex !== i ) {
        //swap函数,排序中常用
        swap(arr, maxIndex, i);
        //替换完后由于原先maxIndex处的节点发生变化,所以要重新调整
        //它自己的子节点。故递归处理
        adjustHeap(arr, maxIndex, len);

    }

}

function swap(arr,index1,index2){
   var temp=arr[index1];
   arr[index1]=arr[index2];
   arr[index2]=temp;
}        

//构建堆,这里的lastTrunkIndex很重要,他是最后一个树单元的父序号
//从它开始往前,依次进行调整,最终构建整个堆
function buildHeap(arr){
  var lastTrunkIndex = Math.floor(arr.length / 2) - 1;
  for(var i = lastTrunkIndex ; i >= 0; i--){
    adjustHeap(arr, i, arr.length);
  }
}


function heapSort(arr){
    buildHeap(arr); 
    //构建完堆以后,进行换位于换位后堆的调整,由于顶部发生了变化所以传入0调整整个堆
    for(var i = arr.length-1; i > 0; i--){   //从数组的尾部进行调整 尾部的序号当然是len - 1啦~
        var swap = arr[i];
        arr[i] = arr[0];
        arr[0] = swap;
        adjustHeap(arr, 0, i); //将最大的元素进行调整,将最大的元素调整到堆顶
    }
}

计数排序

思路:计数排序不是比较排序。他的思路如下:获得数组中的最大数,将其加一。构建数组,每一位为0。表示,该位无数字。
之后遍历我们的数组,往直前的数组中填充。
之后直前的数组中,有数字的表示,该位有数字,个数为填充数。

代码:

function countSort(arr) {
    //构建技术的数组
    var countArr = [];
    var result = [];

    var max = Math.max.apply(null,arr);
    for(var i = 0; i <= max; i++ ) {

        countArr.push(0);

    }

    for(var i = 0; i < arr.length; i ++) {

        var item = arr[i];

        countArr[item] += 1;

    }

    for(var i = 0; i < countArr.length; i++) {

        var hasItem = countArr[i] !== 0 ? true : false;
        var isDuplicate = countArr[i] !== 1 ? true : false;

        if(hasItem) {

            if(!isDuplicate) {

                result.push(i);

            }else{
                //如果该位大于1,则重复推入
                for(var j = 0; j < count[i]; j ++) {
                    result.push(i);
                }
            }
        }
    }
    return result;
}

桶排序

思路: 桶排序是计数排序的变种,我们自己设定桶的个数。也就是采集数组的个数,如果把桶的个数细分为最大数+1,那么与计数排序无异了。
下面以四通为例:(每个桶的内部可以采用其他排序方式,这里我们用快排)

代码:

function bucketSort(arr,bucketNum) {
    var bucketNum = bucketNum;
    var bigBucket = [];

    for(var i = 0; i < bucketNum; i++) {

        bigBucket[i] = [];

    }
    var max = Math.max.apply(null,arr);
    var min = Math.min.apply(null,arr);
    //计算每个桶的范围
    var range = ( max - min + 1 ) / bucketNum;

    for(var i = 0; i < arr.length; i++) {
        //计算自己该丢到哪个桶内
        var bucketIndex = Math.floor( (arr[i] - min) / range  );
        bigBucket[bucketIndex].push(arr[i]);

    }

    var blank = []; 

    for(var i = 0; i < bucketNum; i++) {
        //merge所有桶内的元素。
        var result = blank.concat(quickSort(bigBucket[i]));
        blank = result;

    }

    console.log(result);
    return result;
}

基数排序

思路:基数排序是桶排序的变种。我们以数字的位来让数字入桶。
LSD是从小的位开始,往高位来,每次入桶后,merge,然后再入桶,直到最高位。然后merge,完成排序。
而MSD是从高位开始,入桶后,遍历每个桶,对每个桶内的数组再次使用桶排序,递归结束的条件是,每个桶内的元素个数为1或者0,则返回merge。
我们把所有的merge合并,最后得到排序好的数组。

代码: (LSD)

function radixSort(arr) {
    var bucket = new Array(10);
    //初始化桶,桶的每一位置0
    initBucket();
    //数组入桶,按个位
    splitArr(arr,-1);
    //合并
    mergeBucket();
    //再次初始化桶
    initBucket();
    //入桶
    splitArr(arr,-2,-1);
    //合并
    mergeBucket();
    //往复,知道百位入桶
    initBucket();
    splitArr(arr,-3,-2);
    //合并
    mergeBucket();

    //得到结果
    return arr;


    function splitArr(arr,index1,index2) {
        for(var i = 0; i < arr.length; i++) {

            var num = getLoc(arr[i], index1, index2);

            bucket.forEach(function(item,index) {
                if(num == index) bucket[index].push(arr[i]);
            });

        }
    }

    function mergeBucket() {

         var blank = []; 

        for(var i = 0; i < 10; i++) {

            var result = blank.concat(bucket[i]);
            blank = result;

        }

        arr = blank;

    }


    function getLoc(number,index1,index2) {
        var numArr = number.toString().split('');
        var result = numArr.slice(index1,index2);
        return Number(result);
    }

    function initBucket() {
        for(var i = 0; i < 10; i++) {

            bucket[i] = [];

        }
    }

}

MSD:

  function radixSort2(arr) {
      var result = [];
      splitArr(arr,3,result);
      console.log(result);
      return result;
 }

function splitArr(arr,pos,result) {
    //每次都初始化一个10位桶
    var bucket = initBucket();

    for(var i = 0; i < arr.length; i++) {

            var num = getLoc(arr[i], pos);

            bucket.forEach(function(item,index) {
                if(num == index) bucket[index].push(arr[i]);
            });

    }

    var isFinish = bucket.every(function(item) {

        return item.length == 1 || item.length == 0;

    });
    //如果桶的每一位为0或1则递归结束,往result上push我们的item,此处用concat会导致错误
    if(isFinish) {

        var newArr = mergeBucket(bucket);
        newArr.forEach(function(item) {
            result.push(item);
        });

    }else{
        //如果递归未结束,那么取出不为1和0的桶内数组再次桶排序,但是这次位数要-1,当然Pos必须大于1.
        bucket.forEach(function(item) {

            if(item.length >= 1) {

                if(pos > 1) splitArr(item,pos - 1,result);

            }
        });
    }
}

function initBucket() {
    var bucket = [];
    for(var i = 0; i < 10; i++) {
        bucket[i] = [];
    }
    return bucket;
}

function getLoc(number,pos) {
    var result;
    var numArr = number.toString().split('');
    if(pos == 1) {
        result = numArr.slice(-1);
    }else if(pos == 2) {
        result = numArr.slice(-2,-1);
    }else if(pos == 3) {
        result = numArr.slice(-3,-2);
    }
    return Number(result);
}

function mergeBucket(bucket) {
    var blank = []; 

    for(var i = 0; i < 10; i++) {

        var result = blank.concat(bucket[i]);
        blank = result;

    }
    return blank;
}

结语

下一篇讨论算法的复杂度。不废话over!