算法笔记本:十大基础排序算法

第一篇算法笔记

一、导读

本文示例代码使用:JavaScript
本文基础数据结构:JavaScript Array

1、排序定义

  • 对一序列对象根据某种策略进行排序。

2、术语说明

  • 时间复杂度: 一个算法执行所耗费的时间。
  • 空间复杂度: 运行完一个程序所需内存的大小。

3、排序分类

排序算法

4、复杂度小结

算法名称 最佳时间复杂度 平均时间复杂度 最差时间复杂度 空间复杂度
冒泡排序 O(n) O(n2) O(n2) O(1)
选择排序 O(n2) O(n2) O(n2) O(1)
插入排序 O(n) O(n2) O(n2) O(1)
希尔排序 O(n) O(nlogn) O(nlogn) O(1)
快速排序 O(nlogn) O(nlogn) O(n2) O(nlogn)
归并排序 O(nlogn) O(nlogn) O(nlogn) O(n)
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1)
桶排序 O(n+k) O(n+k) O(n2) O(n+k)
计数排序 O(n+k) O(n+k) O(n+k) O(n+k)
基数排序 O(n * k) O(n * k) O(n * k) O(n+k)

二、基于比较的简单排序

冒泡排序、选择排序、插入排序,三者都是基于比较的排序原理,实现都非常易懂。所以笔者在复习排序算法时,都将这三者归为一类。

1、冒泡排序:

算法原理

(1)它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。
(2)直到重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

代码实现
/**
 * @desc   冒泡排序
 * @sort   升序排序
 * @param  {Array} arr
 * @return {Array}
 */
function bubble ( arr ) {
    // 检验:非数组、空数组直接结束函数调用。
    if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
       return [];
    }

    if (arr.length === 1) {
        return arr;
    };    

    let len = arr.length;

    // 外层循环负责控制排序的趟数
    for (let i = 0; i < len - 1; i++) {

       // 内层循环负责执行一趟排序
       for (let j = 0; j < len - 1 - i; j++) {
          if (arr[j + 1] < arr[j]) {
              [arr[j + 1], arr[j]] = [arr[j], arr[j + 1]];
          };
       };

    };

    return arr;
};
算法分析

最佳时间复杂度:O(n)
平均时间复杂度:O(n2)
最差时间复杂度:O(n2)
空间复杂度:O(1)


2、选择排序

算法原理

(1)将数组划分为已排序和未排序两部分,将未排序的元素最小元素一个个取出然后按顺序放置已排序的尾部。
(2)重复进行步骤1,直到未排序部分清空。

代码实现
/**
 * @desc   选择排序
 * @sort   升序排序
 * @param  {Array} arr
 * @return {Array}
 */
function select( arr ) {
    // 检验:非数组、空数组直接结束函数调用。
    if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
       return [];
    }

    if (arr.length === 1) {
        return arr;
    };

    let len = arr.length;

    // 外层循环需要进行 len - 1 趟排序
    for (let i = 0; i < len - 1; i++) {
       let min = i;

       // 内层循环从未排序的数组元素中比较出最小的一个
       for (let j = i + 1; j < len; j++) {
           if (arr[min] > arr[j]) {
               min = j;
           };
       };

       // 将其放在排序后的数组元素的最后
       [arr[min], arr[i]] = [arr[i], arr[min]];
   };

   return arr;
};
算法分析

最佳时间复杂度:O(n2)
平均时间复杂度:O(n2)
最差时间复杂度:O(n2)
空间复杂度:O(1)


3、插入排序

算法原理

(1)它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,通过比较,找到相应位置并插入。

代码实现
/**
 * @desc   插入排序
 * @sort   升序排序
 * @param  {Array} arr
 * @return {Array}
 */
function insert ( arr ) { 
    // 检验:非数组、空数组直接结束函数调用。
    if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
       return [];
    }

    if (arr.length === 1) {
        return arr;
    };

    let len = arr.length;
    for (let i = 1; i < len; i++) {
        let tmp = arr[i];

        let j;
        for (j = i - 1; j >= 0; j--) {
            if (tmp < arr[j]) {
                arr[j+1] = arr[j];
            } else {
                break;
            };
        };

        // 插入取出的数组元素
        arr[j+1] = tmp;
    };

    return arr;
}
算法分析

最佳时间复杂度:O(n)
平均时间复杂度:O(n2)
最坏时间复杂度:O(n2)
空间复杂度:O(1)


三、基于比较的复杂排序

希尔排序、快速排序、归并排序、堆排序,都是基于比较的排序原理,但实现略有复杂。所以笔者在复习排序算法时,都将这三者归为一类。

1、希尔排序

算法原理:插入排序的改良版本

(1)希尔排序是按一定步长(间隔)将待排序数组分组,对每组使用直接插入排序算法排序;
(2)随着步长逐渐减少,当步长减至1时,排序终止。

代码实现:
/**
 * @desc   插入排序
 * @sort   升序排序
 * @param  {Array}  arr
 * @param  {Number} gap
 * @return {Array}
 */
function insert (arr, gap) {
    // 检验:非数组、空数组直接结束函数调用。
    if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
       return [];
    }

    if (arr.length === 1) {
        return arr;
    };

    let len = arr.length;

    for (let i = gap; i < len; i++) {
        let tmp = arr[i];

        for (var j = i - gap; j >= 0; j -= gap) {
            if (tmp < arr[j]) {
                arr[j+gap] = arr[j];
            } else {
                break;
            };
        };

        arr[j+gap] = tmp;
    };

    return arr;
};

/**
 * @desc   希尔排序
 * @sort   升序排序
 * @param  {Array} arr
 * @return {Array}
 */
function shell( arr ) {
    // 检验:非数组、空数组直接结束函数调用。
    if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
       return [];
    }

    if (arr.length === 1) {
        return arr;
    };

    let len = arr.length;
    let gap = 0;

    while (gap < len/3) {
        gap = 3 * gap + 1;
    };

    for (; gap > 0; gap = Math.floor((gap-1)/3)) {
        console.log(gap);
        arr = insert(arr, gap);
    };

    return arr;
}
算法分析

最佳时间复杂度:O(n)
平均时间复杂度:O(nlogn)
最坏时间复杂度:O(nlogn)
空间复杂度:O(1)


2、快速排序

算法原理

(1)从原始数组中随机取出基准,然后,把比这个数小的放在这个数左边,比这个数大的放在这个数右边,一样大的和这个数放在一起
(2)基准的左右两边各自重复上述过程,直到左边或右边只剩下一个数(或零个数)无法继续为止。

递归实现
/**
 * @desc   快速排序
 * @sort   升序排序
 * @param  {Array} arr
 * @return {Array}
 */
function quick( arr ) {
    // 检验:非数组、空数组直接结束函数调用。
    if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
       return [];
    }

    if (arr.length === 1) {
        return arr;
    };

    let less = [];
    let pivotList = [];
    let more = [];
    let result = [];
    let length = arr.length;

    let pivot = arr[0];
    for (let i = 0; i < length; i++) {
        if (arr[i] < pivot) {
            less.push(arr[i]);
        } else if (arr[i] > pivot) {
            more.push(arr[i]);
        } else {
            pivotList.push(arr[i]);
        };
    };

    less = quick(less);
    more = quick(more);
    result = result.concat(less, pivotList, more);

    return result;
};
迭代实现:同递归版本的显著区别在于,使用 stack 显式维护中间状态
/**
 * @desc   分片
 * @param  {Array}  arr  待分片的数组
 * @param  {Number} low  分片比较的下界
 * @param  {Number} high 分片比较的上界
 * @return {Array}
 */
function partition(arr, low, high) {
    let pivot = arr[high];
    let i = low - 1;

    for (let j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            i++;
            [arr[i], arr[j]] = [arr[j], arr[i]];
        };
    };

    [arr[i+1], arr[high]] = [arr[high], arr[i+1]];

    return i+1;
};

/**
 * @desc   快速排序
 * @sort   升序排序
 * @param  {Array} arr
 * @return {Array}
 */
function quick( arr ) {
    // 检验:非数组、空数组直接结束函数调用。
    if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
       return [];
    }

    if (arr.length === 1) {
        return arr;
    };

    let low = 0;
    let high = arr.length - 1;
    let pivot;
    let stack = [low, high];

    while (stack.length) {
        high = stack.pop();
        low = stack.pop();
        pivot = partition(arr, low, high);

        if (low < pivot - 1) {
            stack.push(low);
            stack.push(pivot - 1);
        };

        if (pivot + 1 < high) {
            stack.push(pivot + 1);
            stack.push(high);
        };
    };

    return arr;
};
算法分析

最佳时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
最差时间复杂度:O(n2)
空间复杂度:O(nlogn)


3、归并排序

算法原理

(1)将序列每相邻两个数字进行归并操作(merge),形成floor(n/2)个序列,排序后每个序列包含两个元素
(2)将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素
(3)重复步骤2,直到所有元素排序完毕

递归实现
/**
 * @desc   归并
 * @param  {Array} left
 * @param  {Array} right
 * @return {Array}
 */
function mergeArray( left, right ) {
    let result = [];

    while ( left.length > 0 && right.length > 0 ) {
        if ( left[0] < right[0] ) {
            result.push(left.shift());
        } else {
            result.push(right.shift());
        };
    };

    return result.concat(left,right);
};

/**
 * @desc   归并排序
 * @sort   升序排序
 * @param  {Array} arr
 * @return {Array}
 */
function merge( arr ) {
    // 检验:非数组、空数组直接结束函数调用。
    if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
       return [];
    }

    let length = arr.length;
    if ( length <= 1 ) {
        return arr;
    };

    let middle = Math.floor(length/2);
    let left = merge(arr.slice(0,middle));
    let right = merge(arr.slice(middle,length));

    return mergeArray(left, right);
};
迭代实现
/**
 * @desc   归并
 * @param  {Array} left
 * @param  {Array} right
 * @return {Array}
 */
function mergeArray( left, right ) {
    let result = [];
    while ( left.length > 0 && right.length > 0 ) {
        if ( left[0] < right[0] ) {
            result.push(left.shift());
        } else {
            result.push(right.shift());
        };
    };

    return result.concat(left,right);
};

/**
 * @desc   归并排序
 * @sort   升序排序
 * @param  {Array} arr
 * @return {Array}
 */
function merge( arr ) {
    // 检验:非数组、空数组直接结束函数调用。
    if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
       return [];
    }

    if (arr.length === 1) {
        return arr;
    };

    // 构造待归并的二维数组
    let work = arr.map(item => [item]);
    if (arr.length % 2 === 1) {
        work.push([]);
    };


    for (let i = arr.length; i > 1; i = (i+1)/2) {
        let j = 0;
        for (let k = 0; k < i; j++, k+=2) {
            work[j] = mergeArray(work[k], work[k+1]);
        };

        work[j] = [];
    };

    return work[0];
};
算法分析

最佳时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
最差时间复杂度:O(nlogn)
空间复杂度:O(n)


4、堆排序

算法原理

(1)将数组构建成大顶堆,将堆顶节点取出放入有序数组中。
(2)重复步骤 1。

代码实现
/**
 * @desc   堆化:将数组构建成堆的形态
 * @param  {Array}  arr 待调整的数组
 * @param  {Number} i   待调整二叉树的顶部节点
 * @param  {Number} len 待调整的数组大小
 * @return {void}
 */
function heapify(arr,  i,  len) {
    // 先取出当前元素 i
    let tmp = arr[i];

    // 从 i 节点的左子节点开始,也就是 2i+1 处开始
    for(let k = i * 2 + 1; k < len; k = k * 2 + 1){
        // 找出左子节点、右子节点中的最大值的下标
        if (k + 1 < len && arr[k] < arr[k + 1]) {
            k++;
        }

        // 如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
        if (arr[k] > tmp) {
            arr[i] = arr[k];
            i = k;
        } else {
            break;
        }
    }

    // 将 tmp 值放到最终的位置
    arr[i] = tmp;
}

/**
 * @desc   堆排序
 * @sort   升序排序
 * @param  {Array}  arr 待调整的数组
 * @return {Array}
 */
function heapSort(arr) {
    // 检验:非数组、空数组直接结束函数调用。
    if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
       return [];
    }

    if (arr.length === 1) {
        return arr;
    };

    // 构建大顶堆
    for (let i = parseInt(arr.length/2 - 1); i >= 0; i--) {
        // 从第一个非叶子节点从下至上,从右至左调整结构
        heapify(arr, i, arr.length);
    }

    // 堆排序
    for (let j = arr.length - 1; j > 0; j--) {
        // 交换堆顶元素与末尾元素
        [arr[0], arr[j]] = [arr[j], arr[0]];

        // 重新对堆进行调整
        heapify(arr, 0, j);
    }

    return arr;
}
算法分析

最佳时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
最差时间复杂度:O(nlogn)
空间复杂度:O(1)


四、基于统计的排序算法

桶排序、计数排序、基数排序,三者都是基于统计的排序原理。所以笔者在复习排序算法时,都将这三者归为一类。

1、桶排序

算法原理

(1)将待排序数组中的数,按照一定策略分组,依次放入大小不一的桶中。
(2)然后对每个桶内的元素进行排序。
(3)输出排序结果。

代码实现

/**
 * @desc   桶排序
 * @sort   升序排序
 * @param  {Array} arr
 * @return {Array}
 */
function bucket ( arr ) {
    // 检验:非数组、空数组直接结束函数调用。
    if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
       return [];
    }

    if (arr.length === 1) {
        return arr;
    };

    let max = Math.max.apply(Math, arr);
    let min = Math.min.apply(Math, arr);
    let len = arr.length
    let bucket = [];
    let result = [];

    // 遍历原数组,将数组元素归入相应桶内
    for(let k = 0; k < len; k++){
       index = parseInt(( arr[k] - min ) / len );
       bucket[index] = !bucket[index] ? [] : bucket[index];
       bucket[index].push( arr[k] );
    };

    // 根据计数数组输出排序后的数组
    for(let l = 0; l < bucket.length; l++){
        if (!!bucket[l]) {
            // 桶排序内部需要使用其他排序来给桶排列
            bucket[l].sort((a, b) => a - b);
            result = result.concat( bucket[l] );
        };
    };

    return result;
};
算法分析

最佳时间复杂度:O(n)
平均时间复杂度:O(n+k)
最差时间复杂度:O(n2)
空间复杂度:O(n+k)


2、计数排序

算法原理

(1)将待排序数组中的数,生成计数分布数组。
(2)利用数组的有序性,从头遍历数组,输出排序结果。

代码实现
/**
 * @desc   计数排序
 * @sort   升序排序
 * @param  {Array} arr
 * @return {Array}
 */
function count ( arr ) {
    // 检验:非数组、空数组直接结束函数调用。
    if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
       return [];
    }

    if (arr.length === 1) {
        return arr;
    };

    // 计数排序使用数组建立频率分布表,对负数进行排序,需要特殊处理一下。
    let min = Math.min.apply(Math, arr);
    if (min < 0) {
        arr = arr.map(item => item + 1 - min);
    };

    let max = Math.max.apply(Math, arr);
    let count = [];
    let result = [];

    // 建立计数数组
    for(let j = 0; j < max + 1; j++){
        count.push(0);
    };

    // 遍历原数组,形成原数组元素的频数分布
    arr.forEach((item) => count[item] += 1);

    // 根据计数数组输出排序后的数组
    for (let l = 0; l < count.length; l++) {
        for (let m = 0; m < count[l]; m++) {
            result.push(l);
        };
    };

    if (min < 0) {
        result = result.map(item => item + min - 1);
    };

    return result;
};
算法分析

待排序的数据范围很大(0 ~ k),计数排序需要大量内存,是典型的以空间换时间的排序算法。

最佳时间复杂度:O(n+k)
平均时间复杂度:O(n+k)
最差时间复杂度:O(n+k)
空间复杂度:O(n+k)


3、基数排序

算法原理

(1)将待排序数组中的数,根据二进制基数,从高位或低位,迭代生成桶大小不一致的桶。
(2)然后从小到大遍历各个桶,输出排序结果。

代码实现
/**
 * @desc   基数排序
 * @sort   升序排序
 * @param  {Array} arr
 * @return {Array}
 */
function base( arr ) {
    // 检验:非数组、空数组直接结束函数调用。
    if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
       return [];
    }

    if (arr.length === 1) {
        return arr;
    };

    // 基数排序使用数组建立频率分布表,对零和负数进行排序,需要特殊处理一下。
    let min = Math.min.apply(Math, arr);
    if (min <= 0) {
        arr = arr.map(item => item + 1 - min);
    };

    let maxDigit = Math.max.apply(Math, arr).toString().length;
    let quenes = [];
    let mod = 10;
    let dev = 1;

    for(let b = 0; b < maxDigit; b++, dev *= 10, mod *= 10){
        // 将对应元素分配到对应的桶内
        for(let i = 0, len = arr.length; i < len; i++){
            let bucket = parseInt( (arr[i] % mod) / dev );
            quenes[bucket] = !quenes[bucket] ? [] : quenes[bucket];
            quenes[bucket].push( arr[i] );
        };

        // 输出本趟排序结果
        let index = 0;
        for(let j = 0; j < quenes.length; j++){
            if (!!quenes[j]) {
                for(let k = 0, len = quenes[j].length; k < len; k++){
                    arr[index++] = quenes[j].shift();
                };
            };
        };
    };

    if (min <= 0) {
        arr = arr.map(item => item + min - 1);
    };

    return arr;
};
算法分析

k 为数组中的最大数的二进制位数。

最佳时间复杂度:O(n * k)
平均时间复杂度:O(n * k)
最差时间复杂度:O(n * k)
空间复杂度:O(n+k)

全部评论

相关推荐

点赞 评论 收藏
分享
04-16 10:27
已编辑
美团_Saas_后端开发
今天周一休息,突发奇想写一篇阶段总结。如题,我已经去了一个和Java彻底毫无关联的行业。曾经我以为自己能在计算机行业发光发热,拿到美团offer那会感觉自己天都亮了。没想到刚入行一年多就当了逃兵。从最开始的热爱到现在一看到代码就厌恶,不知道自己经历了什么。所以我去干什么了?答案是:在成都当了租房销售。上班那会压力大了就念叨着去干租房中介,但是一直下不去这个决心,想着自己学了四年多的计算机知识,终究还是不甘心。终于在某一天准备八股文的时候,看着无数篇和工作内容关系不大的理论知识,那一刻下定决心,决定尝试一下销售行业,也算是给自己一个交代。后面阴差阳错的投了成都自如去当租房管家,没想到面试很顺利,在当天一百多个面试的人里面,我成为了为数不多通过的几个幸运儿之一。目前已经培训通过,正式入职,也开了单,有压力但是每天过得很开心,真心喜欢那种和人交流的感觉,哪怕是最后没有选择找我租房。说这些也是想告诉那些大三,大四正在找Java实习而焦虑的同学:你们现在还年轻,选择很多,容错率也很高,可以尽情去尝试自己喜欢的行业和工作。不用因为某一次的面试没通过或者简历石沉大海而焦虑,更不用因为身边人都在挤编程的独木桥就强迫自己跟风。也算是自己的碎碎念吧,也希望自己能在新的领域取得一点小成就。也祝牛油工作顺利!
沉淀小子:干啥都不丢人啊,生存是必须要的,销售很考验一个人综合素质能力的,好的销售人脉和资源可不比写字楼的白领差啊
点赞 评论 收藏
分享
Gardenia06...:刚开始学是这样的,可以看看左神和灵神都讲的不错
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务