8种基础排序算法 JavaScript(主要参考算法4)

431 阅读3分钟

下图来自算法-第4版

  1. 冒泡排序(Bubble sort)

顾名思义,🐟 吐泡泡就是越靠近水面越小。

  • 从小到大
  • 依次比较相邻的元素,如果后一个元素小于前一个元素,交换两者位置,每一轮过后对应的剩余未排序部分的最后一个元素就是该轮最大的元素
  • 抛开每一轮完成的最后一个元素,从第一个开始重复以上步骤,直至排序完成
  • 平均时间复杂度:O(n²)、平均空间复杂度:O(1)、稳定性稳定的一匹
const bubbleSort = arr => {
    if (arr.length <= 1) return arr;

    for (let i = 0; i < arr.length; i++) {
        for (let j = 0; j < arr.length - i; j++) {
            // 相邻元素比较大小并且交换位置
            arr[j+1] < arr[j] && ([arr[j], arr[j+1]] = [arr[j+1], arr[j]]);
        }
    }
    return arr;
}
  1. 选择排序(Selection Sort)

一句话概括:依次从数组挑最小的放在前面,挑完了,排序也排好了。

  • 从小到大
  • 从数组中找出最小的元素,然后和第一个元素交换
  • 从剩下的元素中找到最小的元素,再和第二个元素交换
  • 不断重复以上步骤,直至排序完成
  • 平均时间复杂度:O(n²)、平均空间复杂度:O(1)、稳定性不稳定
  • 数据越小越好
const selectionSort = arr => {

    let len = arr.length;
    if (len <= 1) return array;

    for (let i = 0; i <= len - 1; i++) {
        for (let j = i + 1; j <= len - 1; j++) {
            arr[j] < arr[i] && (([arr[i], arr[j]] = [arr[j], arr[i]]))
        }
    }

    return arr;
}
  1. 插入排序(Insertion Sort)

想想斗地主,摸牌整理牌的时候,摸牌之后插入该插入的位置

  • 从小到大

  • 从数组第二个元素开始,和前面已经排序好的元素从后往前依次比较

  • 大于前一元素即找到插入位置,即在该元素后面;小于前一元素即将前一元素与此元素交换位置,继续往前找

  • 找到位置插队进去

  • 不断重复以上步骤,直至排序完成

  • 平均时间复杂度:O(n²)、平均空间复杂度:O(1)、稳定性稳定的一匹

const insertionSort = arr => {

    if (arr.length <= 1) return arr;

    for (let i = 0; i <= arr.length-1; i++) {
        // 从后往前
        for (let j = i+1; j > 0; j--) {
             arr[j] < arr[j-1] && ([arr[j-1], arr[j]] = [arr[j], arr[j-1]]);
        }
    }

    return arr;
}

论选择排序和插入排序的区别:

与选择排序一样,当前索引左边的所有元素都是有序的,但它们的最终位置还不确定,为了给更小的元素腾出空间,它们可能会被移动。但是当索引到达数组的右端时,数组排序就完成了。

和选择排序不同的是,插入排序所需的时间取决于输入中元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比对随机顺序的数组或是逆序数组进行 排序要快得多。

  1. 希尔排序(Shell Sort)

操作类似连连看的操作

希尔排序的思想是使数组中任意间隔为 h 的元素都是有序的。这样的数组被称为 h 有序数组。

  • 确定一个递增序列

  • 下面算法的实现使用了序列 h=1/2(pow(3, k)-1)(也有依次折半的序列),分别比对间隔h的位置上的元素进行调整,后面位置的元素小于前面位置的元素则进行交换

  • 直至该间隔h下的数组每个元素比对完,h=``N``/3 开始递减至 1 ,也是比对对应间隔h位置上的元素

  • h=1情况下比对完则数组排序完成

  • 平均时间复杂度:小于O(n²)、平均空间复杂度:O(1)、稳定性不稳定

const shellSort = nums => {
    if (nums.length <= 1) return nums;

    let h = 1;
    while (h < nums.length / 3) h = 3 * h + 1;

    while (h >= 1) {
        for (let i = h; i < nums.length; i++) {
            for (let j = i; j >= h; j-=h) {
                nums[j] < nums[j - h] && ([nums[j - h], nums[j]] = [nums[j], nums[j - h]]);
            }
        }
        h = Math.floor(h / 3);
    }
    return nums;
}

[S, H, E, L, L, S, O, R, T, E, X, A, M, P, L, E]数组希尔排序轨迹如下:

  1. 归并排序(Merge Sort)

举个🌰,将两个有序的数组归并到一个数组中

原地归并

划分数组各自排序:

  • 依次根据mid进行左右划分

合并已经排序好的数组:

  • 需要一个辅助数组,先把数组全部copy到辅助数组中
  • 左半边用尽(取右半边的元素)、右半边用尽(取左半边的元素)、右半边 的当前元素小于左半边的当前元素(取右半边的元素)以及右半边的当前元素大于等于左半边的当 前元素(取左半边的元素)

const sortArray = nums => mergeSort(nums, 0, nums.length - 1);

const mergeSort = (nums, lo, hi) => {
    if (lo >= hi) return nums;
    let mid = lo + Math.floor((hi - lo) / 2);
    // 对左半边进行递归排序
    mergeSort(nums, lo, mid);
    // 对右半边进行递归排序
    mergeSort(nums, mid + 1, hi);
    // 合并左右半边分别已经排序好的数组
    merge(nums, lo, mid, hi);
    
    return nums;
}

const merge = (nums, lo, mid, hi) => {
    // 简单深拷贝
    let tempArr = JSON.parse(JSON.stringify(nums));
    let i = lo, j = mid + 1;
    for (let k = lo; k <= hi; k++) {
        // 左半边用尽
        if(i > mid) nums[k] = tempArr[j++];
        // 右半边用尽
        else if(j > hi) nums[k] = tempArr[i++];
        // 左右半边都未用尽,依次比较大小
        else if(tempArr[i] > tempArr[j]) nums[k] = tempArr[j++];
        else nums[k] = tempArr[i++];
    }
} 

自顶向下的归并排序

  • 原地归并实际上就是自顶向下的归并排序

自顶向上的归并排序

首先我们进行的是两两归并:

(把每个元素想象成一个大小为 1 的数组),然后 是四四归并(将两个大小为 2 的数组归并成一个有 4 个元素的数组),然后是八八的归并,一直下去。 在每一轮归并中,最后一次归并的第二个子数组可能比第一个子数组要小

  1. 快速排序(Quick Sort)

将数组分为左中右,中是一个切分元素,保证左边的元素小于切分元素,右边的元素大于切分元素

  • 我们选择切分元素为数组的第一个元素

  • 从切分元素的下一个开始即lo,依次比较,如果小于切分元素,继续比较下一个;否则停止

  • 初始从数组的最后一个元素向前即hi,依次比较,如果大于切分元素,继续比较前一个,否则停止

  • 直到arr[lo] > falg 以及 arr[hi] < flag,均不满足切分元素条件,交换两个元素

  • 如果lo和hi相遇,即该轮排序结束,终止整个比较;

  • 最后将切分元素和hi(或者lo,已经相遇,lo === hi)交换,此时切分元素左侧的元素都小于切分元素,右侧的元素都大于切分元素,本轮排序完成

  • 将数组根据切分元素分为左右两部分,依次递归排序

const sortArray = nums => quickSort(nums, 0, nums.length - 1);
const quickSort = (arr, lo, hi) => {
    if(lo >= hi) return arr;

    // 设置数组第一位标志位;
    let flag = arr[lo];

    let i = lo,
        j = hi + 1;

    // 跳过数组第一位标志位
    while(true) {
        while(arr[++i] < flag) if(i === hi) break;
        while (arr[--j] > flag) if (j == lo) break;

        if(i >= j) break;

        // 直到arr[i] > falg 以及 arr[j] < flag,均不满足切分元素条件,交换两个元素
        [arr[i], arr[j]] =  [arr[j], arr[i]];

    }
    // i、j此时已经相遇,将切分元素换到中间,即i或者j的位置
    [arr[lo], arr[j]] =  [arr[j], arr[lo]];

    quickSort(arr, lo, j-1);
    quickSort(arr, j+1, hi);

    return arr;
}
  1. 堆排序(Heap Sort)

堆排序可以分为两个阶段:

  • 在堆的构造阶段中,我们将原始数组重新组织安排进一个堆中;
  • 然后在下沉排序阶段,我们从堆中按递减顺序取出所有元素并得到排序结果。
const sinkToBuildHeap = (nums, i, len) => {
    // 对于父节点i,其子节点分别为 2 * i + 1 和 2 * i + 2;
    for(let j = 2 * i + 1; j < len; j = 2 * j + 1) {
        if(j + 1 < len && nums[j] < nums[j + 1]) { 
            j++;   // 找到两个孩子中较大的一个,再与父节点比较
        }
        // 如果父节点小于子节点则交换;否则跳出循环
        if(nums[i] < nums[j]) {
            [nums[i], nums[j]] = [nums[j], nums[i]]
            i = j;  // 交换后,i的下标变为j
        } else break;
    }
}


// 堆排序
const heapSort = nums => {
    let len = nums.length;
    // 初始化大顶堆,从第一个非叶子结点开始
    for(let i = Math.floor(len / 2 - 1); i >= 0; i--) {
        sinkToBuildHeap(nums, i, nums.length);
    }
    // 排序,每一次for循环找出一个当前最大值,数组长度减一
    for(let i = len - 1; i > 0; i--) {
         // 根节点与最后一个节点交换
        [nums[0], nums[i]] = [nums[i], nums[0]];
        // 从根节点开始调整,并且最后一个结点已经为当前最大值,不需要再参与比较,
        // 所以第三个参数i,即比较到最后一个结点前一个即可
        sinkToBuildHeap(nums, 0, i);
    }
}
    

  1. 树排序(Tree Sort)

  • 递归构建二叉搜索树
  • 利用二叉搜索树的性质进行中序遍历
function TreeNode(val, left, right) {
    this.val = (val===undefined ? 0 : val);
    this.left = (left===undefined ? null : left);
    this.right = (right===undefined ? null : right);
}


/**
 * @param {number[]} nums
 * @return {number[]}
 */
const sortArray = nums => {
    if (nums.length <= 1) return nums;
    let root = new TreeNode(nums[0]), res = [];
    for (let i = 1; i < nums.length; i++) {  // 将所有的元素插入到二叉搜索树中
        buildTree(root, nums[i]);
    }
    inorder(root, res);

    return res;
};

// 构建二叉搜索树
const buildTree = (node, val) => {
    if (!node) return;
    if (val > node.val) {
        if (node.right) buildTree(node.right, val);
        else node.right = new TreeNode(val);
    } else {
        if (node.left) buildTree(node.left, val);
        else node.left = new TreeNode(val);
    }
}

// 中序遍历
const inorder = (root, res) => {
    if (!root) return;
    inorder(root.left, res);
    res.push(root.val);
    inorder(root.right, res);
}