七种比较排序算法的实现思路

699 阅读5分钟

本文正在参加「金石计划」

前言

在四月的某一次面试中,我居然败在了一道快速排序面试题上,在我的印象中,除了上数据结构课程时,老师和我讲过C语言版七种排序的方法后,我就再没系统的区分这七种排序的方法,所以我觉得有必要再总结一下七种排序的实现思路了,同时也分享给其他和我一样不能区分的小伙伴。

交换类排序

1. 冒泡排序

冒泡排序的核心动作比较两个元素大小并交换,时间复杂度为 On2。

主要步骤:

  1. 拿到数组的 j 个元素与 j+1个元素比较大小
  2. 如果前者大于后者则交换位置,反之。
  3. 重复以上步骤直到i为数组第一个元素。
let arr = [49, 38, 65, 97, 76, 13, 27, 49]
let bubbleSort = (arr) => {
    let flag =1, temp; // flag 用于记录某趟排序中是否有元素交换的动作
    let i = arr.length-1;
    while(i >= 0 && flag ==1) {
        flag = 0;  
        for( j = 0; j<=i; j++) {
            if(arr[j] > arr[j+1]) {
                temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
                flag =1;
            }
        }
        i--;
    }
    return arr
}
console.log(bubbleSort(arr));

2. 快速排序

快速排序的核心动作是通过多次比较与交换来完成排序,快速排序(Quick Sort)是从冒泡排序算法演变而来的,实际上是在冒泡排序基础上的递归分治法。时间复杂度为Onlogn。

主要步骤是:

  1. 在数据集之中,选择一个元素作为基准值。
  2. 将大于或等于基准值的数据集中在右边,小于分界值的数据集中到数组的左边
  3. 左边和右边的数据在重复上述步骤独立排序, 进行递归操作,直到所有子集只剩一个元素
let arr = [49, 38, 65, 97, 76, 13, 27, 49]
const quickSort = (array) => {
    const sort = (arr, left, right) => {
     if (left >= right) { // 递归出口:左边的索引大于等于右边的索引
      return
     }
    let i = left
    let j = right
    const baseVal = arr[j] // 取最后一个数为基准值
    while (i < j) {//把所有比基准值小的数放在左边大的数放在右边
     while (i < j && arr[i] <= baseVal) {
      i++
     }
     arr[j] = arr[i] // 将较大的值放在右边
     while (j > i && arr[j] >= baseVal) { //找到一个比基准值小的数交换
      j--
    }
     arr[i] = arr[j] // 将较小的值放在左边
    }
    arr[j] = baseVal // 将基准值放至中央位置完成一次循环
    sort(arr, left, j-1) // 将左边的无序数组重复上面的操作
    sort(arr, j+1, right) // 将右边的无序数组重复上面的操作
    }
    const newArr = array.concat()
    sort(newArr, 0, newArr.length - 1)
    return newArr
}
console.log(quickSort(arr)); // [13, 27, 38, 49,49, 65, 76, 97]

插入类排序

3. 插入排序

3.1 简单插入排序

简单插入排序的核心动作就是寻找被插入的元素的合适位置进行插入,时间复杂度为 On2。

主要步骤是:

  1. 拿到数组的第 i 个元素,并将记录为temp
  2. 从 i-1 == j 的位置向前查找比 temp 大的元素并记录成item
  3. 将 item 向后面移一个位置
  4. 将 temp 放在 item 的位置
  5. 重复上述步骤
let arr = [49, 38, 65, 97, 76, 13, 27, 49]
let insertSort = function (arr) {
    let j,item
    for(let i =1; i < arr.length; i++) {
        item = arr[i]
        j = i-1
        while( j>=0 && item < arr[j]) {
            arr[j+1] = arr[j]
            j--  
            arr[j+1] = item
        }
    }
    return arr
}
console.log(insertSort(arr)); // [13, 27, 38, 49, 49, 65, 76, 97]

3.2 折半插入排序

折半插入排序的核心动作与简单插入排序相同,只是在查找插入位置时使用二分查询确定插入位置,时间复杂度最坏情况为On2, 最好情况下为Onlogn。

 let arr = [49, 38, 65, 97, 76, 13, 27, 49]
let halfinsertSort = (arr) =>{
    let j, temp, low, high, mid
    for(let i =1; i < arr.length; i++) {
        temp = arr[i]
        low = 0;
        high = i -1;
        while(low <= high) {
            mid = Math.floor((low +high)/2)
            if(temp < arr[mid]) high = mid - 1;
            else low = mid + 1;
        }
       for( j = i-1; j >= low; j--) {
        arr[j+1] = arr[j];
       }
       arr[low] = temp; 
    }
    return arr
}
console.log(halfinsertSort(arr)); // [13, 27, 38, 49, 49, 65, 76, 97]

4. 希尔排序

希尔排序又称缩小增量排序法,是对插入排序法的一种改进, 希尔排序的核心动作是确定一个间隔数,再交换位置同时缩小间隔数,直到间隔数为1,时间复杂度在Onlogn到On之间。

主要步骤:

  1. 取一个小于n的整数gap作为第一个间隔数
  2. 将元素间隔为gap的值进行比较并交换
  3. 缩小间隔数,直到间隔数为1

let arr = [49, 38, 65, 97, 76, 13, 27, 49]
var len = arr.length;
for (var gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
    for (var i = gap; i <= len; i += gap) {
        for (var j = i - gap; j >= 0 && arr[j] > arr[gap + j]; j -= gap) {
            var temp = arr[j];
            arr[j] = arr[gap + j];
            arr[gap + j] = temp;
        }
    }
}
console.log(arr); // [13, 27, 38, 49,49, 65, 76, 97]

选择类排序

5. 选择排序

选择排序的核心动作找到最小的元素与最前面的元素交换位置,时间复杂度为 On2。

主要步骤是:

  1. 拿到数组的第 i 个元素
  2. 从 i +1 == j 的位置后找到值最小的元素记录下标为min
  3. 将 min位置的元素与 i位置的元素进行替换。
  4. 重复上述步骤
let arr = [49, 38, 65, 97, 76, 13, 27, 49]
let selectSort = (arr) => {
    let temp;
    for(let i=0; i<arr.length; i++) {
        let min = i;
        for(let j=i+1; j<arr.length; j++) {
            if(arr[j] < arr[min]) min = j
        }
        if (min != i) {
            temp = arr[min]
             arr[min] = arr[i]
             arr[i] = temp
        }
    }
    return arr
}
console.log(selectSort(arr)); // [13, 27, 38, 49, 49, 65, 76, 97]

6. 堆排序

堆排序的核心动作就是将堆中的最大值移到根节点,时间复杂度为Onlogn。

主要步骤:

  1. 首先将待排序序列构建成一个大顶堆(构建一个二叉树)。
  2. 将堆的末端子节点作调整,使得子节点永远小于父节点
  3. 重复第二步。
function adjustHeap(arr,i,length){
    var notLeafNodeVal = arr[i]; //非叶子节点的值
    //k = 2*k+1表示再往左子节点找
    for(var k=i*2+1;k<length;k=2*k+1){
        if(k+1<length && arr[k]<arr[k+1]){
            k++;   //将k++,此时为当前节点的右孩子节点的索引
        }
        if(arr[k]>notLeafNodeVal){
            arr[i] = arr[k]; //将当前节点赋值为孩子节点的值
            i = k;//将i赋值为孩子的值,再看其孩子节点是否有比它大的
        }else{
            break;  //只要上面的不大于,下面的必不大于
        }
    }
    arr[i] = notLeafNodeVal;
}

function heapSort(arr){
    for(var i=parseInt(arr.length/2)-1;i>=0;i--){
        adjustHeap(arr,i,arr.length);
    }
    for(var j=arr.length-1;j>0;j--){
        var temp = arr[j];
        arr[j] = arr[0];
        arr[0] = temp;
        adjustHeap(arr,0,j); //交换堆积的第一个元素与最后那个元素的位置
    }
    return arr
}
let arr = [49, 38, 65, 97, 76, 13, 27, 49]
console.log(heapSort(arr)); // [13, 27, 38, 49, 49, 65, 76, 97]

归并类排序

7. 归并排序

归并排序核心动作是对序列的元素进行逐层折半分组,然后从最小分组开始比较排序,合并成一个大的分组,逐层进行,最终所有的元素都是有序的。

主要步骤:

  1. 将数组分为由n个长度为一的子序列
  2. 将相邻的子序列两两成对的进行合并,同时得到长度为n/2的子序列。
  3. 重复第二步,直到子序列长度为n。
function merge(left, right){
    var 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).concat(right);
}
function mergeSort(arr){
    if(arr.length == 1){
        return arr;
    }
var middle = Math.floor(arr.length/2),
    left = arr.slice(0, middle),
    right = arr.slice(middle);
    
    return merge(mergeSort(left), mergeSort(right));
}
let arr = [49, 38, 65, 97, 76, 13, 27, 49]
console.log(mergeSort(arr));