应届生必须掌握的十大排序算法

697 阅读8分钟

冒泡排序

思路/原理:比较相邻的两个元素,如果前一个比后一个大,则交换位置,每一趟会将最小或最大的元素“浮”到顶端,最终达到完全有序。就好像一串气泡一样,最终从小到大或从大到小依次排下来。

function bubbleSort(arr) {            
    var len = arr.length;            
    // i表示第几趟排序            
    for(var i = 0; i < len - 1; i++) {                
    // j表示交换次数                
        for(var j = 0; j < len - 1 - i; j++) {                    
            if(arr[j] > arr[j + 1]) {                        
                // var temp = arr[j];                        
                // arr[j] = arr[j + 1];                        
                // arr[j + 1] = temp;                        
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];                    
            }                                        
        }            
    }            
    return arr;        
}        
console.log(bubbleSort([3, 1, 8, 10, 5]));

时间复杂度: 平均情况O(n²)  最坏情况O(n²)  最好情况O(n)

空间复杂度: O(1)

稳定性: 稳定

由于冒泡排序只在相邻元素大小不符合要求时才调换他们的位置,它并不改变相同元素之间的相对顺序,因此它是稳定的排序算法。

复杂性: 简单

选择排序

思路/原理: 每一趟从待排序的记录中选出最小的元素,顺序放在已排好序的序列最后,直到全部记录排序完毕。

function selectSort(arr) {            
    var len = arr.length;            
    // 控制位置            
    for(var i = 0; i < len - 1; i++) {                            
        // 假如这个是最小的                
        var min = i;                
        // 与后面的数字进行比较 寻找最小值的索引                
        for(var j = i + 1; j < len; j++) {                    
            if(arr[j] < arr[min]) {                        
                min = j;                    
            }                
        }                
        // 如果当前的值的索引和最小值的索引不相等 交换位置                
        if(i !== min) {                    
            [arr[i], arr[min]] = [arr[min], arr[i]];                
        }                        
    }            
    return arr;        
}

时间复杂度: 平均情况O(n²) 最坏情况O(n²) 最好情况O(n²)

空间复杂度: O(1)

稳定性: 不稳定

Tips: 选择排序每次交换的元素都有可能不是相邻的 因此它有可能打破原来值为相同的元素之间的顺序 比如数组[2,2,1,3] 正向排序时 第一个数字2将与数字1交换 那么两个数字2之间的顺序将和原来的顺序不一致 虽然它们的值相同 但它们相对的顺序却发生了变化 我们将这种现象称作 不稳定性 .

选择排序的简单和直观名副其实 这也造就了它"出了名的慢性子" 无论是哪种情况 哪怕原数组已排序完成 它也将花费将近n²/2次遍历来确认一遍 即便是这样 它的排序结果也还是不稳定的 唯一值得高兴的是 它并不耗费额外的内存空间

插入排序

思路/原理: 将排序分为排序与未排序,将未排序的数依次向排序数进行比较。

function insertSort(arr) {            
    var len = arr.length;            
    for (var i = 1; i < len; i++) {                
        var temp = arr[i]; // 要插入的元素                
        // 与前面的排序好的数组进行比较                 
        var j = i - 1;                
        while(j >= 0 && temp < arr[j]) {                    
            // 如果当前元素小于他们 则他们往后运动 并交换当前元素和他们的位置 否则不动                    
            arr[j + 1] = arr[j];                       
            j --;                
        }                 
        arr[j + 1] = temp;            
    }            
    return arr        
}        
console.log(insertSort([2, 1, 4, 8, 3]));                        

时间复杂度: 平均情况O(n²) 最坏情况O(n²) 最好情况O(n)

空间复杂度: O(1)

稳定性: 稳定

Tips: 由于直接插入排序每次只移动一个元素的位置 并不会改变值相同的元素之间的排序 因此它是一种稳定排序

归并排序

思路/原理: 归并排序建立在归并操作之上 它采取分而治之的思想 将数组拆分为两个子数组 分别排序 最后才将两个子数组合并 拆分的两个子数组 再继续递归拆分为更小的子数组 进而分别排序 直到数组长度为1 直接返回该数组为止

// 采用分而治之的思想 把数组以递归的方式分为左右相等的两个数组        
function mergeSort(arr) {            
    var len = arr.length;            
    // 递归出口            
    if(len < 2) {            
        return arr;            
    }            
    var mid = parseInt(len / 2),                
    left = arr.slice(0, mid),                
    right = arr.slice(mid);            
    return merge(mergeSort(left), mergeSort(right));            
}        
// 把两个数组进行排序        
function merge(left, right) {            
    var res = [];            
    while(left.length && right.length) {                
        if(left[0] < right[0]) {                    
            res.push(left.shift());                
        }else {                    
            res.push(right.shift());                
        }              
    }                        
    // 把剩余的数组连接                
    while(left.length) {                
        res.push(left.shift());            
    }            
    while(right.length) {                
        res.push(right.shift());            
    }            
    return res;        
}

时间复杂度: 平均情况O(n log n) 最坏情况O(n log n) 最好情况O(n log n)

空间复杂度: O(n)

稳定性: 稳定

Tips: 由于直接插入排序每次只移动一个元素的位置 并不会改变值相同的元素之间的排序 因此它是一种稳定排序

快速排序

思路/原理: 在数据集之中 选择一个元素作为"基准"(pivot),所有小于"基准"的元素,都移到"基准"的左边,所有大于"基准"的元素,都移到"基准"的右边,对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。

function quickSort(arr) {            
    if (arr.length <= 1) {                
        return arr;            
    }            
    var pivotIndex = Math.floor(arr.length / 2);            
    var pivot = arr.splice(pivotIndex, 1)[0];            
    var left = [];            
    var right = [];            
    for (var i = 0; i < arr.length; i++) {                
        if (arr[i] < pivot) {                    
            left.push(arr[i]);                
        } else {                    
            right.push(arr[i]);                
        }            
    }            
    return quickSort(left).concat([pivot], quickSort(right));        
};        

时间复杂度: 平均情况O(n log n)  最坏情况O(n²)  最好情况O(n log n)

空间复杂度: O(log n) 

快速排序排序效率非常高 虽然它运行最糟糕时将达到O(n²)的时间复杂度 但通常 平均来看 它的时间复杂为O(nlogn) 比同样为O(nlogn)时间复杂度的归并排序还要快 快速排序似乎更偏爱乱序的数列 越是乱序的数列 它相比其他排序而言 相对效率更高

之前在 捋一捋JS的数组 一文中就提到: Chrome的v8引擎为了高效排序 在排序数据超过了10条时 便会采用快速排序 对于10条及以下的数据采用的便是插入排序

稳定性: 不稳定

Tips: 同选择排序相似 快速排序每次交换的元素都有可能不是相邻的 因此它有可能打破原来值为相同的元素之间的顺序 因此 快速排序并不稳定    

堆排序

思路/原理: 将待排序序列构造成一个大顶堆 此时 整个序列的最大值就是堆顶的根节点 将其与末尾元素进行交换 此时末尾就为最大值 然后将剩余n-1个元素重新构造成一个堆 这样会得到n个元素的次小值 如此反复执行 便能得到一个有序序列了

// 构建大顶堆
function heapify(arr, len, index) {    
    var largest = index,         
    left = index * 2 + 1,        
    right = index * 2 + 2;    
    if(left < len && arr[left] > arr[largest]) {        
        largest = left;    
    }        
    if(right < len && arr[right] > arr[largest]) {        
        largest = right;    
    }    
    // 交换 递归出口    
    if(largest !== index) {        
        [arr[index], arr[largest]] = [arr[largest], arr[index]];                
        // 重新调整下面的位置 构建大顶堆        
        heapify(arr, len, largest);    
    }
}
// 排序
function heapSort(arr) {    
    var len = arr.length;        
    // 从第一个非叶子节点开始构建大顶堆 从下往上 从右往左    
    for(var i =  Math.floor(len / 2 - 1); i >= 0; i--) {         
        heapify(arr, len, i);    
    }    
    for(var i = len - 1; i >= 0; i--) {                
        // 把最大值压入最下面 位置交换        
        [arr[0], arr[i]] = [arr[i], arr[0]];        
        heapify(arr, i, 0);                  
    }    
    return arr;
}

时间复杂度: 平均情况O(n log n) 最坏情况O(n log n)  最好情况O(n log n)

空间复杂度: O(1)

稳定性: 不稳定

计数排序

思路/原理:找出待排序的数组中最大和最小的元素 统计数组中每个值为i的元素出现的次数 存入数组countArr的第i项 对所有的计数累加(从countArr中的第一个元素开始 每一项和前一项相加)反向遍历原数组: 将每个元素i放在新数组的第countArr(i)项 每放一个元素就将countArr(i)减去1

function countSort(arr) {    
    var len = arr.length;    
    // 先找出最小值和最大值    
    var min = arr[0],        
    max = arr[0];    
    for(var i = 1; i < len; i++) {        
        if(arr[i] < min) {            
            min = arr[i];        
        }        
        if(arr[i] > max) {            
            max = arr[i];        
        }    
    }        
    
    // 计数数组的长度    
    var d = max - min + 1;    
        
    // 统计数次出现的次数    
    var countArr = [];    
    for(var i = 0; i < len; i++) {        
        if(!countArr[arr[i] - min]) {            
            countArr[arr[i] - min] = 1;        
        }else {            
            countArr[arr[i] - min] ++;         
        }    
    }            
    
    // 累加    
    var sum = 0;    
    for(var i = 0; i < d; i++) {        
        // 把undefined转化为0        
        if(!countArr[i]) {            
            countArr[i] = 0;        
        }        
        sum += countArr[i];        
        countArr[i] = sum;    
    }    
    console.log(countArr);          
    
    // 从统计数组最后一位向前递减    
    var newArr = [];    
    for(var i = len - 1; i>= 0; i--) {        
        newArr[ countArr[arr[i] - min] - 1 ] = arr[i];        
        countArr[arr[i] - min] --;    
    }    
    return newArr;
}

时间复杂度: 平均情况O(n+k)  最坏情况O(n+k) 最好情况O(n+k)

空间复杂度: O(k)

稳定性: 稳定

使用它处理一定范围内的整数排序时 时间复杂度为O(n+k) 其中k是整数的范围 它几乎比任何基于比较的排序算法都要快( 只有当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序 如归并排序和堆排序) 稳定

计数排序的缺点:

1. 当数列最大最小值差距过大时 并不适用计数排序比如给定20个随机整数 范围在0到1亿之间 这时候如果使用计数排序 需要创建长度1亿的数组 不但严重浪费空间 而且时间复杂度也随之升高
2. 当数列元素不是整数 并不适用计数排序 如果数列中的元素都是小数 比如25.213 或是0.00000001这样子 则无法创建对应的统计数组 这样显然无法进行计数排序

上述缺点桶排序可以做出弥补


 

未完待续。。。。。。。。。。。。。。。。。。。。。。。。。

你的点赞是我持续输出的动力 希望能帮助到大家 互相学习 有任何问题下面留言 一定回复