JavaScript算法——归并排序、快速排序、希尔排序、堆排序、冒泡排序、插入排序

冒泡排序

基本思想

  • 它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。
  • 走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
  • 这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  3. 针对所有的元素重复以上的步骤,除了最后一个。

代码实现

function bubbleSort(arr) {
    // 获取数组长度,以确定循环次数。
    let len = arr.length;
    // 遍历数组len次,以确保数组被完全排序
    for (let i = 0; i < len; i++) {
        // 遍历数组的前len-i项,忽略后面的i项(已排序部分)
        for (let j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
            }
        }
    }
    return arr
}

具体分析

  1. 冒泡排序是原地排序算法吗 ?
    答:冒泡排序是一个原地排序算法,过程只涉及相邻数据的交换操作,只需要常量级的临时空间
  2. 冒泡排序是稳定的排序算法吗 ?
    答:对于相同数值的元素我们并没有进行比较和交换位置,所以相同数值元素的相对位置不会发生改变,因此冒泡排序算法是稳定的排序算法。
  3. 冒泡排序的时间复杂度是多少 ?
    在完全有序的情况下,最好的时间复杂度是O(n),只需要1次冒泡。而在极端情况完全逆序,时间复杂度为O(n^2).

插入排序

基本思想

  • 将数组中的所有元素依次跟前面已经排好的元素相比较,如果选择的元素比已排序的元素小,则交换,直到全部元素都比较过。
  • 第一层循环:遍历待比较的所有数组元素
  • 第二层循环:将本轮选择的元素与前面已经排好序的元素相比较,若当前元素小,则进行交换

代码实现

function insertionSort(arr) {
    let len = arr.length;
    for (let i = 1; i < len; i++) {
        let j = i;
        let temp = arr[i];
        while (j > 0 && arr[j - 1] > temp) {
            arr[j] = arr[j - 1];
            j--;
        }
        arr[j] = temp;
    }
    return arr;
}

具体分析

  1. 插入排序是原地排序算法吗 ?
    答:不牵涉额外得到其他空间。所以是原地排序算法
  2. 插入排序是稳定的排序算法吗 ?
    答:在插入排序算法中,对于相同数值的元素可以选择插入到已排序区间等值元素的后边,所以相同数值元素的相对位置不会发生改变,因此插入排序算法是稳定的排序算法。
  3. 冒泡排序的时间复杂度是多少 ?
    在完全有序的情况下,插入排序每个未排序区间元素只需要比较1次,所以时间复杂度是O(n)。而在极端情况完全逆序,时间复杂度为O(n^2),就等于每次都把未排序元素插入到数组第一位

快速排序

优点:速度快,效率高
缺点:需要另外声明两个数组,浪费了内存空间资源

基本思想

  • 先找到一个基准点(一般指数组的中部),然后数组被该基准点分为两部分,依次与该基准点数据比较,如果比它小,放左边;反之,放右边。
  • 左右分别用一个空数组去存储比较后的数据。
  • 最后递归执行上述操作,直到数组长度 <= 1

代码实现

方案一

// 方案1
function quickSort1(arr) {
	if (arr.length < 2) {
		return arr;
	}
	//取基准点
	const midIndex = Math.floor(arr.length / 2);
	//取基准点的值,splice(index,1) 则返回的是含有被删除的元素的数组。
	const valArr = arr.splice(midIndex, 1);
	console.log(valArr)
	const midIndexVal = valArr[0];
	const left = [];//存放比基准点小的数组
	const right = [];//存放比基准点大的数组
	for (let i = 0; i < arr.length; i++) {
		if (arr[i] < midIndexVal) {
			left.push(arr[i])//比基准点小的放在左边数组
		} else {
			right.push(arr[i])//比基准点大的放在右边数组
		}
	}
	//递归执行以上操作,对左右两个数组进行操作,直到数组长度为 <= 1
	return quickSort1(left).concat(midIndexVal, quickSort1(right));
}

方案二

// 方案2
function quickSort2(arr) {
    if (arr.length < 2) { return arr }
    let left = 0,
        right = arr.length - 1;
    main(arr, left, right);
    return arr

    function main(arr, left, right) {
        // 递归结束的条件,直到数组只包含一个元素。
        if (arr.length === 1) {
            // 由于是直接修改arr,所以不用返回值。
            return;
        }
        // 获取left指针,准备下一轮分解。
        let index = partition(arr, left, right);
        if (left < index - 1) {
            // 继续分解左边数组。
            main(arr, left, index - 1);
        }
        if (index < right) {
            // 分解右边数组。
            main(arr, index, right);
        }
    }

    // 数组分解函数。
    function partition(arr, left, right) {
        // 选取中间项为参考点。
        let pivot = arr[Math.floor((left + right) / 2)];
        // 循环直到left > right。
        while (left <= right) {
            // 持续右移左指针直到其值不小于pivot。
            while (arr[left] < pivot) {
                left++;
            }
            // 持续左移右指针直到其值不大于pivot。
            while (arr[right] > pivot) {
                right--;
            }
            // 此时左指针的值不小于pivot,右指针的值不大于pivot。
            // 如果left仍然不大于right。
            if (left <= right) {
                // 交换两者的值,使得不大于pivot的值在其左侧,不小于pivot的值在其右侧。
                [arr[left], arr[right]] = [arr[right], arr[left]];
                // 左指针右移,右指针左移准备开始下一轮,防止arr[left]和arr[right]都等于pivot然后导致死循环。
                left++;
                right--;
            }
        }
        // 返回左指针作为下一轮分解的依据。
        return left;
    }
}

具体分析

  1. 快速排序是原地排序算法吗 ?
    答:因为 partition() 函数进行分区时,不需要很多额外的内存空间,所以快排是原地排序算法。
  2. 快速排序是稳定的排序算法吗 ?
    答:和选择排序相似,快速排序每次交换的元素都有可能不是相邻的,因此它有可能打破原来值为相同的元素之间的顺序。因此,快速排序并不稳定。
  3. 快速排序的时间复杂度是多少 ?
    1. 极端的例子:如果数组中的数据原来已经是有序的了,比如 1,3,5,6,8。如果我们每次选择最后一个元素作为 pivot,那每次分区得到的两个区间都是不均等的。我们需要进行大约 n 次分区操作,才能完成快排的整个过程。每次分区我们平均要扫描大约 n / 2 个元素,这种情况下,快排的时间复杂度就从 O(nlogn) 退化成了 O(n^2)。
    2. 最佳情况:T(n) = O(nlogn)。
    3. 最差情况:T(n) = O(n^2)。
    4. 平均情况:T(n) = O(nlogn)。

归并排序

基本思想

  • 排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。
  • 归并排序采用的是分治思想。
  • 分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。
    在这里插入图片描述

代码实现

function mergeSort(arr) {
    //采用自上而下的递归方法
    const len = arr.length;
    if (len < 2) {
        return arr;
    }
    let middle = Math.floor(len / 2),
        left = arr.slice(0, middle),
        right = arr.slice(middle);
    return merge(mergeSort(left), mergeSort(right));

    function merge(left, right) {
        const result = [];
        while (left.length && right.length) {
            // 注意: 判断的条件是小于或等于,如果只是小于,那么排序将不稳定.
            if (left[0] <= right[0]) {
                result.push(left.shift());
            } else {
                result.push(right.shift());
            }
        }
        while (left.length) {
            result.push(left.shift())
        }
        while (right.length) {
            result.push(right.shift());
        }
        return result
    }
}

具体分析

  1. 归并排序是原地排序算法吗 ?
    1. 这是因为归并排序的合并函数,在合并两个有序数组为一个有序数组时,需要借助额外的存储空间。
    2. 实际上,尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)。
    3. 所以,归并排序不是原地排序算法。
  2. 归并排序是稳定的排序算法吗 ?
    答:merge 方法里面的 left[0] <= right[0] ,保证了值相同的元素,在合并前后的先后顺序不变。归并排序是一种稳定的排序方法。
  3. 归并排序的时间复杂度是多少 ?
    1. 从效率上看,归并排序可算是排序算法中的佼佼者。假设数组长度为 n,那么拆分数组共需 logn 步, 又每步都是一个普通的合并子数组的过程,时间复杂度为 O(n),故其综合时间复杂度为 O(nlogn)。
    2. 最佳情况:T(n) = O(nlogn)。
    3. 最差情况:T(n) = O(nlogn)。
    4. 平均情况:T(n) = O(nlogn)。
      在这里插入图片描述

希尔排序

基本思想

  • 先将整个待排序的记录序列分割成为若干子序列。
  • 分别进行直接插入排序。
  • 待整个序列中的记录基本有序时,再对全体记录进行依次直接插入排序。

代码实现

function shellSort(arr) {
    let len = arr.length,
        temp, gap = 1;
    while (gap < len / 3) { gap = gap * 3 + 1 };
    for (gap; gap > 0; gap = Math.floor(gap / 3)) {
        for (let i = gap; i < len; i++) {
            temp = arr[i];
            let j = i - gap;
            for (; j >= 0 && arr[j] > temp; j -= gap) {
                arr[j + gap] = arr[j]
            }
            arr[j + gap] = temp
        }
    }
    return arr
}

具体分析

  1. 希尔排序是原地排序算法吗 ?
    答:希尔排序过程中,只涉及相邻数据的交换操作,只需要常量级的临时空间,空间复杂度为 O(1) 。所以,希尔排序是原地排序算法。
  2. 希尔排序是稳定的排序算法吗 ?
    1. 单次直接插入排序是稳定的,它不会改变相同元素之间的相对顺序
    2. 但在多次不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,可能导致相同元素相对顺序发生变化。
    3. 因此,希尔排序不稳定。
  3. 希尔排序的时间复杂度是多少 ?
    最佳情况:T(n) = O(n logn)。
    最差情况:T(n) = O(n (log(n))2)。
    平均情况:T(n) = 取决于间隙序列。

堆排序

堆的定义

堆其实是一种特殊的树。只要满足这两点,它就是一个堆。
堆是一个完全二叉树。

  • 完全二叉树:除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。
  • 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。

也可以说:堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。这两种表述是等价的。
对于每个节点的值都大于等于子树中每个节点值的堆,我们叫作大顶堆。
对于每个节点的值都小于等于子树中每个节点值的堆,我们叫作小顶堆。
在这里插入图片描述

基本思想

  • 将初始待排序关键字序列 (R1, R2 … Rn) 构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素 R[1] 与最后一个元素 R[n] 交换,此时得到新的无序区 (R1, R2, … Rn-1) 和新的有序区 (Rn) ,且满足 R[1, 2 … n-1] <= R[n]。
  • 由于交换后新的堆顶 R[1] 可能违反堆的性质,因此需要对当前无序区 (R1, R2 … Rn-1) 调整为新堆,然后再次将 R[1] 与无序区最后一个元素交换,得到新的无序区 (R1, R2 … Rn-2) 和新的有序区 (Rn-1, Rn)。不断重复此过程,直到有序区的元素个数为 n - 1,则整个排序过程完成。

代码实现

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

    function swap(arr, i, j) {
        let temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }


    // 将 i 结点以下的堆整理为大顶堆,注意这一步实现的基础实际上是:
    // 假设结点 i 以下的子堆已经是一个大顶堆,heapify 函数实现的功能是实际上是:找到 结点 i 在包括结点 i 的堆中的正确位置。
    // 后面将写一个 for 循环,从第一个非叶子结点开始,对每一个非叶子结点都执行 heapify 操作,所以就满足了结点 i 以下的子堆已经是一大顶堆
    function heapify(arr, i, length) {
        let temp = array[i]; // 当前父节点
        // j < length 的目的是对结点 i 以下的结点全部做顺序调整
        for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
            temp = arr[i]; // 将 array[i] 取出,整个过程相当于找到 array[i] 应处于的位置
            if (j + 1 < length && arr[j] < arr[j + 1]) {
                j++; // 找到两个孩子中较大的一个,再与父节点比较
            }
            if (temp < arr[j]) {
                swap(arr, i, j); // 如果父节点小于子节点:交换;否则跳出
                i = j; // 交换后,temp 的下标变为 j
            } else {
                break
            }
        }
    }
}

具体分析

  1. 堆排序是原地排序算法吗 ?
    整个堆排序的过程,都只需要极个别临时存储空间,所以堆排序是原地排序算法。
  2. 堆排序是稳定的排序算法吗 ?
    因为在排序的过程,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序。
    所以,堆排序是不稳定的排序算法。
  3. 堆排序的时间复杂度是多少 ?
    堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn),所以,堆排序整体的时间复杂度是 O(nlogn)。
    最佳情况:T(n) = O(nlogn)。
    最差情况:T(n) = O(nlogn)。
    平均情况:T(n) = O(nlogn)。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值