面试必备:Javascript 数组7大排序算法【建议收藏】

125 阅读11分钟
  1. 冒泡排序
  2. 选择排序
  3. 插入排序
  4. 快速排序
  5. 归并排序
  6. 希尔排序
  7. 计数排序

整个排序可以将数组切分为有序段,和无序段两个集合,开始时有序段长度为0

稳定排序:排序前后,元素的相对位置不变。

原地排序:不申请多余的空间存储,只利用待排数据的空间进行比较和交换。

冒泡排序

思路:

  • 相邻j和j+1两两比较,大的放后面,重复进行
  • 因为是两两比较,所以除了最后一个(溢出了),所有元素重复两两比较的操作
  • 一次遍历中最大的值会放在无序段的最后面(有序的最前面)
  • 双层for循环
    • 外层for循环:从0开始遍历数组的每一个元素。区间是[0,len - 1]
    • 内层for循环:从索引0开始,比较索引j和索引j+1的值,值大的放后面。
      • 外层一次遍历,尾部就确定了一个数的顺序,所以区间是[0,len-1-i]

eg:

如:nums = [4,3,5,1] 长度是4,最后一个索引是3

  • 第一次遍历:外层i=0 (确定最后一个数)
    • 内层 j=0,4>3,交换,变成[3,4,5,1]
    • 内层 j=1,4<5,不变,依然[3,4,5,1]
    • 内层 j=2,5>1,交换,变成[3,4,1,5]第一次遍历结果
    • 内层 j=3,没有可以交换的了,所以可以结束,也就是j< nums.length-1
  • 第二次遍历:外层i=1(确定倒数第二个数)
    • 内层 j=0,3<4,不变
    • 内层 j=1,4>1,变成[3,1,4,5]第二次遍历结果
    • 内层j=2 循环结束 (j=2的值一定是小于最后一个的,所以索引2和索引3就不需要比较了)
  • 省略...

代码:

function swap(arr, i, j) { // 交换函数
    let temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
function bubbleSort(nums) {
    let len = nums.length;
    for (let i = 0; i < len - 1; i++) {
        for (let j = 0; j < len - 1 - i; j++) {
            if (nums[j] > nums[j + 1]) {
                swap(nums, j, j + 1)
            }
        }
    }
    return nums;
}

选择排序

思路:

  • 假设index0的值是最小的,从无序段第二个开始遍历记录最小值的索引,找到后和交换
  • 一次遍历可以将最小值放在无序区间的头部(有序的尾部)
  • 双层for循环
    • 外层for循环:[0,len-1]
    • 内层for循环:
      • 假设第一个索引的值是最小的,从第二个开始比较,挨个查找,直到找到最小的索引值,然后交换,这样一次遍历就可以确定无序区间第一个位置上是最小的值
        • 如果外层循环已经确定了3个,那就从第4个开始,所以遍历区间是[i+1,len]

eg:

nums=[13,12,11], 长度 = 3

  • 第一次遍历:i=0, 假设索引0的值是最小的 minIndex = 0
    • 内层 j=1,12 < nums[minIndex](此时是nums[0]也就是13),minIndex = 1
    • 内层 j=2,11 < nums[minIndex](此时是nums[1]也就是12),minIndex = 2
    • 因为minIndex变成2了,所以将索引2的值11和索引0的值13交换, 结果[11,12,13](第一次遍历结果
  • 第二次遍历:i=1,此时假设最小的索引是1,minIndex = 1
    • j = 2,index2 的值13 > nums[minIndex=1],所以最小索引不变,还是index1,不需要交换,结果[11,12,13](第二次遍历结果
    • j=3,不符合j<3 结束遍历

代码:

function selectSort(nums) {
    let minIndex = 0; // 假设最小的值是索引为0的那个值
    let len = nums.length;
    for (let i = 0; i < len - 1; i++) {
        minIndex = i; // 每次遍历的第一个数是最小的
        for (let j = i + 1; j < len; j++) { // 无序区间的第二个开始遍历
            if (nums[j] < nums[minIndex]) {
                minIndex = j;
            }
        }
        if (minIndex != i) {
            swap(nums, i, minIndex);
        }
    }
    return nums;
}

插入排序

思路:

  • 假设第一个是排好的顺序,将第二个(无序数组的一个)跟前一个比较,小的话交换位置,然后再次跟前一个比较,小的话再次往前交换,直到相应的位置
  • 默认有序数组是第一个元素,遍历一次可以将第二个元素插入到有序数组对应的位置
  • j是--
  • 双层for循环
    • 外层:从i=1到最后
    • 内层:j = 当前遍历的,也就是i,然后比较j 和j-1,小的放前面

eg:

nums=[7,5,2,11]

  • 第一遍遍历,i=1 假设7是排好的顺序
    • j = 1, 5<前一个7,所以交换[5,7,2,11]第一遍遍历结果
    • j = 0,结束遍历
  • 第二遍遍历,i=2,
    • j = 2,2<前一个7,所以交换[5,2,7,11]
    • j = 1,2<前一个5,所以交换[2,5,7,11]第二遍遍历结果
    • j = 0,结束遍历
  • 第三遍遍历,i= 3,
    • j = 3,11>前一个7,不变
    • j = 2,7<前一个5,不变
    • j = 1,5<前一个2,不变
    • j = 0,结束遍历

代码:

function insertSort(nums) {
    for (let i = 1; i < nums.length; i++) {
        // 假设第一个是排好的顺序
        for (let j = i; j > 0; j--) {
            if (nums[j] < nums[j - 1]) {
                swap(nums, j - 1, j)
            }
        }
    }
    return nums;
}

快速排序

思路:

  1. 要有base case ,否则会死循环,len<=1的时候,就返回本身
  2. 要有基准数,左边的都比它小, 后边的都比它大
  3. for或者while遍历从头到尾
    • 如果是mid,就跳出
    • 如果当前元素小于mid,放在左边数组里
    • 如果当前元素大于mid,放在右边数组里
  4. 递归左数组, 和右数组

如果是while要contiune是直接跳到i++的,会死循环。如果把mid元素也放在左或右数组里,会死循环

eg:

nums=[12,2,5,1,6]

  1. 第一次遍历: nums[mid] = 5
    • leftArr:[2,1]: 走进递归2
    • rightArr:[12,6] :走进递归3
    • return :[...递归[2,1],5,...递归[12,6]]
  2. 递归遍历leftArr: nums[mid]=1
    • leftArr:[]:走进case base
    • rightArr:[2] : 走进case base
    • return :[...[],1,...[2]]
  3. 递归遍历rightArr: nums[mid]=6
    • leftArr:[]:走进case base
    • rightArr:[12] : 走进case base
    • return :[...[],6,...[12]]

代码:

function quickSort(nums) {
    // 递归 
    let len = nums.length;
    if (len <= 1){
        return nums;
    };
    let leftArr = [];
    let rightArr = [];
    let mid = Math.floor(len / 2); // 中间基数
    let i = 0;
    while (i < len) {
        console.log(nums, i, mid)
        if (i === mid) {
            i++;
            continue;
        };
        if (nums[i] < nums[mid]) {
            leftArr.push(nums[i])
        } else {
            rightArr.push(nums[i])
        }
        i++;
        
    }
    return [...quickSort(leftArr), nums[mid], ...quickSort(rightArr)]

}

归并排序

思路:

当数组中只有一个元素时,一定是有序的,两个有序的单个元素数组就可以通过辅助函数merge成一个有序的数组。以此类推,两个有序的数组通过辅助函数merge,也可以得到一个长度为4的有序数组。

所以:

  1. 要有basecase,len<=1, 返回数组本身
  2. 分割数组,从中心基准点分割为左右
  3. 然后merge两个有序数组为一个

eg:

nums=[9,7,4,3,1]; mid = 2

  • 分割数组为[9,7]和[4,3,1]
    • 分割[9,7]为[9]和[7],走进base case 结束
      • 辅助函数merge[9]和[7] 结果为[7,9]
    • 分割[4,3,1]为[4]和[3,1]
      • 分割[3,1]为[3]和[1]
        • 辅助函数merge[3]和[1] 结果为[1,3]
      • 辅助函数merge[4]和[1,3] 结果为[1,3,4]
    • 辅助函数merge[7,9] 和[1,3,4] 为[1,3,4,7,9]

代码:

//  归并排序
function mergeSort(nums) {
    // base case 
    let len = nums.length;
    if (len <= 1) return nums;
    // 分割数组
    let mid = Math.floor(len / 2);
    let leftArr = nums.slice(0, mid);
    let rightArr = nums.slice(mid);

    return merge(mergeSort(leftArr), mergeSort(rightArr))

}
const merge = ((left, right) => {
    let res = [];
    let i = 0;
    let j = 0;
    while (i < left.length && j < right.length) {
        if (left[i] < right[j]) {
            res.push(left[i]);
            i++;
        } else {
            res.push(right[j]);
            j++;
        }
    }
    while (i < left.length) {
        res.push(left[i]);
        i++;
    }
    while (j < right.length) {
        res.push(right[j]);
        j++;
    }
    return res;
})

希尔排序:插入排序的改进

思路:

  • 先将整个待排数组分割成若干个子数组
  • 切分方式可以是数组的一半,也可以是动态定义间隔序列的方式,总之尽可量选择一个较大的间隔值(gap)
  • 然后缩小增量,如从4到2再到1
  • 进行的还是插入排序,只是是gap间隔的插入排序

eg:

nums=[28, 34, 35, 66, 22, 33, 25, 2, 7] image.png

image.png

image.png

代码:

function shellSort(arr) {
    let len = arr.length;
    let gap = Math.floor(len / 2);
    for (; gap > 0; gap = Math.floor(gap / 2)) {
        for (let i = gap; i < len; i++) {
            let temp = arr[i];
            let j = i;
            for (; j >= gap; j -= gap) {
                let another = arr[j - gap];
                if (temp >= another) {
                    break;
                }
                arr[j] = another;
            }
            arr[j] = temp;
        }
    }
    return arr;
}

我还有一种希尔排序的写法,很好理解: 你可以理解为插入排序是间隔为1的排序,而希尔排序是间隔是gap的排序,并且gap需要递减(需要遍历改变) 所以:

  1. for循环改变gap的值。
  2. 插入排序是从i=1开始,那希尔排序就是从i=变量gap开始
  3. 插入排序是j=i;然后j-1>=0;j=j-1,比较j和j-1。那么希尔就是j=i;j-gap>=0;j=j-gap,比较j和j-gap。
  4. 当前j小于前面(插入就是j-1,希尔就是j-gap)的值的话就交换
function shellSort(nums) {
    let len = nums.length;
    for (let gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
        for (let i = gap; i < len; i++) {
            for (let j = i; j >= gap; j = j - gap) {
                if (nums[j] < nums[j - gap]) {
                    swap(nums, j, j - gap)
                }
            }
        }
    }
    return nums;
}

计数排序

思路

  1. 利用数组下标是有序的,只能比较整数
  2. 找出数组中最大值和最小值,确定数组的长度 len= max-min+1
  3. 构建计数数组countArr,定义是数组的下标是元素本身,下标对应的值是元素出现的次数
  4. 两种方式
    • 效率较低:for循环里while循环,构建输出数组
    • 优化:将countArr生成累计数组

如果数组中有负数的话,可以使用min来解决,将nums[i] - min 就可以保证最小的元素在计数数组中的位置值是0

代码

function countSort(nums) { // 只能排序整数
    const min = Math.min(...nums);
    const max = Math.max(...nums);
    const len = nums.length;
    let res = [];
    let countArr = new Array(max - min + 1).fill(0);
    // 先构建一个计数数组
    for (let i = 0; i < len; i++) {
        countArr[nums[i] - min]++;
    }

    // 方式一: 在for循环中使用while,效率比较低 
    for (let i = 0; i < countArr.length; i++) {
        while (countArr[i]) {
            res.push(i + min); // 其实就是push下标
            countArr[i]--;
        }

    }
    return res;

}
    // 1. 更改计数数组为累计数组:累计数组用来确定当前索引也就是当前元素包括自己在内前面累计有了多少个元素 
     for (let i = 1; i < countArr.length; i++) {
         countArr[i] += countArr[i - 1]
    }
    // 2. 从后向前遍历,保证稳定性   
    for (let i = len - 1; i >= 0; i--) {
       // 索引位置应该是累计数组的值-1
        const indexVal = countArr[nums[i] - min];
        res[indexVal - 1] = nums[i];
        countArr[nums[i] - min]--;
    }

总结:

  1. 冒泡排序是从两层循环都是从0开始,比较j和j+1,j大的话就交换。一次遍历将最大的放在最后面
  2. 选择排序外层也是从0开始,记录最小索引。内层是从i+1开始,遍历数组到结尾,重置最小索引值。最小索引值变了的话就交换
  3. 插入排序假定第一个是排好的顺序,所以外层i=1开始。内层j=i开始,j--,比较j和j-1,j小的话交换
  4. 快速排序 是递归的思想,要有basecase 也就是数组长度<=1的时候 返回本身,然后有mid,遍历数组,小于mid的放在左边数组,大的放在右边数组
  5. 归并排序,也是递归。basecase一样是数组长度<=1的时候,返回数组本身。数组中只有一个数的时候一定是有序的。然后需要辅助函数merge,将两个有序数组合并为一个有序数组。
  6. 希尔排序,需要一个gap,是插入排序的改进版,gap会缩小,所以也叫缩小增量排序
  7. 计数排序,需要一个累计数组(也就是前缀和),然后从后向前遍历,保证稳定性,如何存在负数,可以利用min

表格:

排序方式稳定排序原地排序时间复杂度空间复杂度备注
冒泡排序平均:O(n^2)O(1)大规模数据效率低
插入排序平均:O(n^2)O(1)大规模数据效率低
选择排序不是O(n^2)O(1)大规模数据效率低
快速排序不是平均:O(nlogn)O(logn)存储角度空间复杂度是O(1),递归容易溢出
希尔排序不是[O(n^1.5)-O(n^2)]O(1)高度依赖于所选择的间隔序列
归并排序不是O(nlogn)O(n)适用于数据量大且对稳定性有要求
计数排序不是O(n+k)O(k)要求元素是整数