5大排序算法&2大搜索&4大算法思想

5大排序算法&2大搜索&4大算法思想

完整源码地址,开源不易,你的star是我努力的动力!(gitee.com/lintaibai/T...

介绍

介绍数据结构与算法中的的5大排序算法2大搜索算法以及我们刷算法面试题常见的4大算法思想

包含以下内容:

  • 冒泡排序
  • 快速排序
  • 插入排序
  • 归并排序
  • 选择排序
  • 顺序搜索
  • 二分搜素
  • 分而治之
  • 动态规划
  • 贪心算法
  • 回溯算法

5大排序

1.冒泡排序(常考)

认识

冒泡排序(Bubble Sort)是一种非常直观且简单的排序算法,主要用于对一组数据进行排序。它的名字来源于这样一个过程:在每一轮遍历中,最大的元素会"像气泡一样"逐渐上浮到数组的末端,故称为"冒泡排序"。

原理如下
  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个,如果不是相等的就跳过比下面的元素 ,这样依次的循环下去 直到所有的元素都比较完成才结束。
  2. 针对所有的元素重复以上的步骤,除了最后一个。
  3. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
思路
javascript 复制代码
冒泡排序其实核心是对比里面的一层,就是谁大排后面
加一层外面的排序步骤,因为两个对比,所以最后一个不需要对比,对比次数就是数组长度减一
实际手写
plain 复制代码
function bubbleSort(arr) {
    const len = arr.length
    if (len <= 1) return

    for (let i = 0; i < len - 1; i++) {
        for (let j = 0; j < len - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                const temp = arr[j]
                arr[j] = arr[j + 1]
                arr[j + 1] = temp
            }
        }
    }
}

// 功能测试
const arr = [4, 3, 6, 2, 5, 7, 9, 8, 1]
bubbleSort(arr)
console.log(arr) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

整个交换过程比较,分别对比

javascript 复制代码
// 14 34  46 62 65 98 
// 1 3 4 2 5 6 7 8 9 
// 1 3 2 4 56789
优化写法
javascript 复制代码
 // 优化1 写法
  function bubbleSort(arr) {
      const len = arr.length
      if (len <= 1) return
      for (let i = 0; i < len - 1; i++) {
          for (let j = 0; j < len - i - 1; j++) {
              if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
              }
          }
      }
  }

我们可以添加一个计算步数的看看我们的步数走了多少,这个过程可以看出我们部署总共走了36步

javascript 复制代码
function bubbleSort(arr) {
        const len = arr.length
        let stepCount = 0; // 步数计数器
        if (len <= 1) return
        for (let i = 0; i < len - 1; i++) {
            for (let j = 0; j < len - i - 1; j++) {
                stepCount++; // 每次比较都算一步
                if (arr[j] > arr[j + 1]) {
                  [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
                }
            }
        }
        console.log(stepCount,'步数');
        return arr
}

// 输出
36 '步数'
优化逻辑-添加有序标志减少遍历次数

我们发现,虽然我们一直对比,但是后面其实我们已经排序好的并不需要太多的对比,所以我们可以添加一个有序标志

如果一轮遍历中没有发生任何交换,说明数组已经有序,可以提前结束排序

这个时候我们优化一下可以发现,步数已经缩减到了26步

原理

javascript 复制代码
这个标志用来判断在某一轮遍历中是否发生了元素交换
如果一轮遍历中没有发生任何交换,说明数组已经有序,可以提前结束排序

例子:
假设数组是 [1, 2, 3, 4, 5],在第一轮遍历时:

比较1和2,不交换
比较2和3,不交换
比较3和4,不交换
比较4和5,不交换
整个过程中 isSorted 始终为true
因此排序提前结束,不需要进行后续的遍历
javascript 复制代码
// 优化2 标志上一次移动的下标 有序标志 (isSorted)
  function bubbleSort(arr) {
      const len = arr.length;
      let stepCount = 0; // 步数计数器
      if (len <= 1) return
      for (let i = 0; i < len - 1; i++) {
          let isSorted = true // 有序标志
          for (let j = 0; j < len - i - 1; j++) {
              stepCount++; // 每次比较都算一步
              if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
                isSorted = false
              }
          }
          if (isSorted) {
              console.log(isSorted, '交换结束===isSorted');
              break
          } // 如果没有发生交换,说明已经有序
      }
      console.log(stepCount,'步数');
      return arr
  }
// 输出 26 '步数'
优化逻辑-添加无序边界 (sortBorder) 减少每次遍历的比较次数

标记上一轮已经有序的位置 ,我们可以通过记录无序数列的边界位置

下一轮遍历时只需要比较到这个位置即可,因为后面的元素已经有序

javascript 复制代码
例子: 
假设数组是 [3, 4, 2, 1, 5]:

第一轮遍历:

3和4比较,不交换
4和2比较,交换,数组变为 [3, 2, 4, 1, 5],lastSwapIndex = 2
4和1比较,交换,数组变为 [3, 2, 1, 4, 5],lastSwapIndex = 3
4和5比较,不交换
第一轮结束后,sortBorder = 3,因为最后一次交换发生在索引3
此时我们知道,索引3之后的元素(4,5)已经有序
第二轮遍历:

只需要比较到索引3即可,不需要比较到最后
3和2比较,交换,数组变为 [2, 3, 1, 4, 5],lastSwapIndex = 1
3和1比较,交换,数组变为 [2, 1, 3, 4, 5],lastSwapIndex = 2
3和4比较,不交换
第二轮结束后,sortBorder = 2,因为最后一次交换发生在索引2
此时我们知道,索引2之后的元素(3,4,5)已经有序

这个时候输出我们发现,步数已经优化到了18步,也就是刚刚开始时候我们36步的一般,效率提升100%

javascript 复制代码
    // 优化3 标记上一轮已经有序的位置  无序边界 (sortBorder) 
    function bubbleSort(arr) {
        let stepCount = 0; // 步数计数器
        const len = arr.length;
        if (len <= 1) return arr;

        let sortBorder = len - 1; // 初始时无序边界为数组末尾
        let lastSwapIndex = 0; // 记录最后一次交换的位置

        for (let i = 0; i < len - 1; i++) {
            let isSorted = true; // 有序标志

            for (let j = 0; j < sortBorder; j++) {
                stepCount++; // 每次比较都算一步
                if (arr[j] > arr[j + 1]) {
                    [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                    isSorted = false; // 发生了交换,说明还不是完全有序
                    lastSwapIndex = j; // 记录最后一次交换的位置
                }
            }

            sortBorder = lastSwapIndex; // 更新无序边界,边界之后的元素已经有序
            if (isSorted) {
                console.log(isSorted, '交换结束===isSorted');
                break; // 如果没有发生交换,说明已经有序
            }
        }

        console.log(stepCount, '步数');
        return arr;
    }

    // 测试代码
    const testArr = [64, 34, 25, 12, 22, 11, 90];
    console.log('原始数组:', testArr);
    bubbleSort(testArr);


//输出 18 '步数'

添加上我们计算步数以及以及交换和对比的计数

javascript 复制代码
 // 优化3 标记上一轮已经有序的位置  无序边界 (sortBorder) 
    function bubbleSort(arr) {
        let stepCount = 0; // 步数计数器
        let compareCount = 0; // 比较次数计数器
        let swapCount = 0; // 交换次数计数器

        const len = arr.length;
        if (len <= 1) return arr;
        let sortBorder = len - 1; // 初始时无序边界为数组末尾
        let lastSwapIndex = 0; // 记录最后一次交换的位置
        for (let i = 0; i < len - 1; i++) {

            let isSorted = true; // 有序标志

            for (let j = 0; j < sortBorder; j++) {

                stepCount++; // 每次比较都算一步
                compareCount++; // 每次比较都计数

                if (arr[j] > arr[j + 1]) {
                    [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                    isSorted = false; // 发生了交换,说明还不是完全有序
                    lastSwapIndex = j; // 记录最后一次交换的位置
                    swapCount++; // 每次交换都计数
                }
            }
            sortBorder = lastSwapIndex; // 更新无序边界,边界之后的元素已经有序
            if (isSorted) {
                console.log(isSorted, '交换结束isSorted');
                break; // 如果没有发生交换,说明已经有序
            }
        }

        console.log(stepCount, '步数');
        console.log('比较次数:', compareCount);
        console.log('交换次数:', swapCount);
        return arr;
    }

    // 功能测试
    const arr = [1, 4, 3, 6, 2, 5, 7, 9, 8]
    bubbleSort(arr)
    console.log(arr) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

2.快速排序(Quick Sort常考)

认识

快速排序(Quick Sort)是一种经典的排序算法,广泛应用于实际工程中。主要特点是利用分治法 来排序,能够在大多数情况下实现 <font style="color:rgb(36, 41, 47);">O(n log n) 的时间复杂度。

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

plain 复制代码
/**
 * @description 快速排序
 * @author hovinghuang
 */

/**
 * 快速排序 (splice)
 * @param arr 
 * @returns 
 */
function quickSort1(arr: number[]): number[] {
    const len = arr.length
    if (len === 0) return arr

    const midIndex = Math.floor(len / 2)
    const midValue = arr.splice(midIndex, 1)[0]

    const left: number[] = []
    const right: number[] = []

    // 注意: splice 会修改原数组,所以用 arr.length
    for (let i = 0; i < arr.length; i++) {
        const n = arr[i]
        if (n < midValue) {
            left.push(n)
        } else {
            right.push(n)
        }
    }
    return quickSort1(left).concat([midValue], quickSort1(right))
}

/**
 * 快速排序 (slice)
 * @param arr 
 * @returns 
 */
 function quickSort2(arr: number[]): number[] {
    const len = arr.length
    if (len === 0) return arr

    const midIndex = Math.floor(len / 2)
    const midValue = arr.slice(midIndex, midIndex + 1)[0]

    const left: number[] = []
    const right: number[] = []

    for (let i = 0; i < len; i++) {
        if (i === midIndex) continue
        const n = arr[i]
        if (n < midValue) {
            left.push(n)
        } else {
            right.push(n)
        }
    }
    
    return quickSort2(left).concat([midValue], quickSort2(right))
}

// 功能测试
const testArr3 = [3, 2, 5, 1, 8, 7]
console.info('quickSort2:', quickSort2(testArr3))
基本概念

快速排序的核心思想是通过"分治法"将问题拆解为更小的子问题来递归解决。

  1. 分治法:快速排序每次通过选择一个元素作为基准(pivot),将数组分为两部分。左边部分的元素比基准小,右边部分的元素比基准大。然后,分别对左右两部分递归地进行排序。
  2. 选择基准元素:可以选择第一个元素、最后一个元素或中间元素作为基准,或者通过随机选择基准来避免最坏情况。
工作过程
  1. 选择基准元素(Pivot):从待排序的数组中选择一个元素作为基准(可以是第一个元素、最后一个元素,或通过其他方法选择)。
  2. 划分操作:将数组分为两部分。左边部分的元素都小于等于基准元素,右边部分的元素都大于基准元素。基准元素在划分过程中会被放到正确的位置。
  3. 递归排序:对基准元素左边和右边的子数组继续进行快速排序。
例子

假设我们要对以下数组进行排序:

plain 复制代码
[6, 3, 8, 2, 7, 5, 1, 4]
  1. 选择基准元素 :假设选择第一个元素 6 作为基准。
  2. 划分操作:将数组划分为左边部分(小于等于6)和右边部分(大于6)。
plain 复制代码
左边:[3, 2, 5, 1, 4]
基准:[6]
右边:[8, 7]
  1. 对左右子数组递归进行快速排序:
    • 左边 [3, 2, 5, 1, 4] 选择 3 为基准,再进行划分。
    • 右边 [8, 7] 选择 8 为基准,再进行划分。

最终,经过递归排序后,得到排序后的数组:

plain 复制代码
[1, 2, 3, 4, 5, 6, 7, 8]
时间复杂度
  • 最优情况 :当每次基准元素将数组均匀分割时,时间复杂度为 O(n log n),其中 n 是数组的长度。
  • 平均情况 :在大多数实际情况下,快速排序的平均时间复杂度也是 O(n log n)
  • 最坏情况 :当选择的基准总是数组中的最大或最小元素(例如,数组已经排好序或完全逆序),导致每次分割只有一个元素时,时间复杂度退化为 O(n^2)
快速排序的空间复杂度

快速排序的空间复杂度是 O(log n) ,这主要是由递归调用栈所占用的空间决定的。每次划分时,递归深度最多为 log n

如果使用原地排序(即直接在原数组上修改,而不使用额外数组),空间复杂度可以降到 O(log n) 。但如果每次划分时创建新的数组,空间复杂度则是 O(n)

优缺点
优点
  1. 高效性 :在大多数情况下,快速排序的性能非常优越,时间复杂度为 O(n log n),适用于大规模数据排序。
  2. 原地排序:快速排序是一种原地排序算法,不需要额外的存储空间(除了递归栈)。
  3. 分治法:通过递归和分治法来处理复杂问题,适用于大规模数据集。
缺点
  1. 最坏情况性能差 :当数据已经接近有序或是逆序时,快速排序的性能会退化为 O(n^2)。这个问题可以通过随机选择基准元素或使用三数取中的方法来减少。

  2. 不稳定排序:与冒泡排序、插入排序等稳定排序算法不同,快速排序会改变相等元素的顺序。

优化策略
  1. 基准选择优化
    • 随机选择基准:通过随机选择基准元素,可以避免在已经排序或逆序的数组上出现最坏情况。
    • 三数取中法:选择数组首、尾和中间三个元素的中位数作为基准,可以减少最坏情况的概率。
  2. 递归深度限制
    • 当子数组的长度较小时(如小于10),可以切换为其他排序算法,如插入排序,因为插入排序对小规模数据的排序更高效。
  3. 尾递归优化
    • 在递归过程中,可以选择递归较小的子数组,然后迭代处理较大的子数组,减少递归的深度。
快速排序核心
  1. 理解分治法:核心是分治法的思想。通过选择基准将问题划分成两个子问题,并通过递归解决它们。
  2. 实现多种方式:尝试实现快速排序的不同版本,例如使用递归实现、使用随机基准、三数取中法等。
  3. 分析时间复杂度:理解快速排序的时间复杂度分析,尤其是最坏情况和平均情况的区别,以及如何优化。
  4. 与其他排序算法比较:将快速排序与其他排序算法(如冒泡排序、选择排序、归并排序等)比较其优缺点和场景。
写法
基础写法
javascript 复制代码
// 解法1 快速排序
    function quickSort(arr) {
        if (arr.length <= 1) {
            return arr
        }
        // 选择基准元素,这里我们选择第一个元素作为基准
        let pivot = arr[0];
        let left = [];
        let right = [];
        for (let i = 1; i < arr.length; i++) {
            if (arr[i] <= pivot) {
                left.push(arr[i]);
            } else {
                right.push(arr[i]);
            }
        }
        return [...quickSort(left), pivot,...quickSort(right)]
    }
    // 测试数据
    let arr = [6, 3, 8, 2, 7, 5, 1, 4];
    console.log("排序前:", arr);
    let sortedArr = quickSort(arr);
    console.log("排序后:", sortedArr);
整个过程
javascript 复制代码
选择一个基准元素(pivot),这里选择数组的第一个元素
将小于等于基准元素的元素放到左数组
将大于基准元素的元素放到右数组
递归地对左数组和右数组进行同样的排序
最后将排序后的左数组、基准元素和排序后的右数组合并

执行过程:
对于数组 [6, 3, 8, 2, 7, 5, 1, 4]
第一次选择6为基准,分成 [3, 2, 5, 1, 4] 和 [8, 7]
然后对 [3, 2, 5, 1, 4] 选择3为基准,分成 [2, 1] 和 [5, 4]
对 [2, 1] 选择2为基准,分成 [1] 和 []
对 [5, 4] 选择5为基准,分成 [4] 和 []
对 [8, 7] 选择8为基准,分成 [7] 和 []
然后按照相同的方式递归处理其他子数组


输出:
排序前:[6, 3, 8, 2, 7, 5, 1, 4]
排序后:[1, 2, 3, 4, 5, 6, 7, 8]
优化-使用随机基准选择

添加注释以后我们更加容易看出添加基准以后,进行的步数大概平均下来在25步左右

javascript 复制代码
// 写法优化1-使用随机基准选择
   let stepCount = 0; // 全局步数计数器
   console.log(`步骤 ${stepCount}: 进入`);
   function quickSort(arr) {
      if (arr.length <= 1) {
          return arr
      }
      stepCount++; // 基准选择计数
      // 选择基准元素,这里我们选择第一个元素作为基准
      const randomIndex = Math.floor(Math.random() * arr.length);
      [arr[0], arr[randomIndex]] = [arr[randomIndex], arr[0]]; // 交换到第一个位置
      let pivot = arr[0];
      let left = [];
      let right = [];
      for (let i = 1; i < arr.length; i++) {
          stepCount++; // 循环迭代计数
          console.log(`步骤 ${stepCount}: 检查元素 ${arr[i]}, 当前基准: ${pivot}`);
          if (arr[i] <= pivot) {
              left.push(arr[i]);
          } else {
              right.push(arr[i]);
          }
      }
      stepCount++; // 递归调用计数

      return [...quickSort(left), pivot,...quickSort(right)]
  }
优化-使用三数取中法选择基准

这个时候我们优化下来,在14步左右

javascript 复制代码
// 优化2-使用三数取中法选择基准(带步数计算)
     let stepCount = 0; // 全局步数计数器
     function quickSort(arr) {
        if (arr.length <= 1) {
            return arr
        }
        stepCount++; // 基准选择计数
        // 选择基准元素,这里我们选择第一个元素作为基准
        const mid = Math.floor(arr.length / 2);

        // 比较三个数
        if (arr[0] > arr[mid]) {
            [arr[0], arr[mid]] = [arr[mid], arr[0]];
        }
        if (arr[0] > arr[arr.length - 1]) {
            [arr[0], arr[arr.length - 1]] = [arr[arr.length - 1], arr[0]];
        }
        if (arr[mid] > arr[arr.length - 1]) {
            [arr[mid], arr[arr.length - 1]] = [arr[arr.length - 1], arr[mid]];
        }
        let pivot = arr[mid];;
        let left = [];
        let right = [];
        for (let i = 1; i < arr.length; i++) {
            if (i === mid) continue; // 跳过基准元素
            stepCount++; // 循环迭代计数
            console.log(`步骤 ${stepCount}: 检查元素 ${arr[i]}, 当前基准: ${pivot}`);
            if (arr[i] <= pivot) {
                left.push(arr[i]);
            } else {
                right.push(arr[i]);
            }
        }
        stepCount++; // 递归调用计数
        return [...quickSort(left), pivot,...quickSort(right)]
}
优化3-使用原地分区

这里一定得去看看原理分区的概念才能理解这个原地分区,其实就是不借助于额外存储进行位置的不断交换

javascript 复制代码
// 优化3-使用原地分区
    let stepCount = 0; // 全局步数计数器
    function quickSort(arr, left = 0, right = arr.length - 1) {
        if (left < right) {
            stepCount++; // 递归调用计数
            const mid = Math.floor((left + right) / 2);
            // 比较三个数
            if (arr[left] > arr[mid]) {
                [arr[left], arr[mid]] = [arr[mid], arr[left]];
            }
            if (arr[left] > arr[right]) {
                [arr[left], arr[right]] = [arr[right], arr[left]];
            }
            if (arr[mid] > arr[right]) {
                [arr[mid], arr[right]] = [arr[right], arr[mid]];
            }

            [arr[mid], arr[right]] = [arr[right], arr[mid]];
            const pivot = arr[right];
            let i = left - 1;
            for (let j = left; j < right; j++) {
                stepCount++; // 循环迭代计数
                if (arr[j] <= pivot) {
                    i++;
                    [arr[i], arr[j]] = [arr[j], arr[i]];
                }
            }
            [arr[mid], arr[right]] = [arr[right], arr[mid]];
            const partitionIndex = i + 1;
            quickSort(arr, left, partitionIndex - 1);
            quickSort(arr, partitionIndex + 1, right);
        }
        return arr;
    }

    // 测试数据
    let arr = [6, 3, 8, 2, 7, 5, 1, 4];
    console.log("排序前:", arr);
    let sortedArr = quickSort(arr);
    console.log("排序后:", sortedArr);
    console.log("总步数:", stepCount);
优化4-添加小数组插入排序优化

这个时候我们又能感觉到步数明显确实加快了

javascript 复制代码
// 优化4-添加小数组插入排序优化
     let stepCount = 0; // 全局步数计数器
     function quickSort(arr, left = 0, right = arr.length - 1) {

         // 对小数组使用插入排序
        if (right - left < 10) {
            insertionSort(arr, left, right);
            stepCount++;
            return arr;
        }

        if (left < right) {
            stepCount++; // 递归调用计数
            const mid = Math.floor((left + right) / 2);
            // 比较三个数
            if (arr[left] > arr[mid]) {
                [arr[left], arr[mid]] = [arr[mid], arr[left]];
            }
            if (arr[left] > arr[right]) {
                [arr[left], arr[right]] = [arr[right], arr[left]];
            }
            if (arr[mid] > arr[right]) {
                [arr[mid], arr[right]] = [arr[right], arr[mid]];
            }
            [arr[mid], arr[right]] = [arr[right], arr[mid]];
            const pivot = arr[right];
            let i = left - 1;
            for (let j = left; j < right; j++) {
                stepCount++; // 循环迭代计数
                if (arr[j] <= pivot) {
                    i++;
                    [arr[i], arr[j]] = [arr[j], arr[i]];
                }
            }
             [arr[i + 1], arr[right]] = [arr[right], arr[i + 1]];
            const partitionIndex = i + 1;
            quickSort(arr, left, partitionIndex - 1);
            quickSort(arr, partitionIndex + 1, right);
        }
        return arr;
    }


    // 辅助函数:插入排序(带步数计算)
    function insertionSort(arr, left, right) {
        for (let i = left + 1; i <= right; i++) {
            const key = arr[i];
            let j = i - 1;
            while (j >= left && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = key;
        }
        return arr;
    }
优化5-尾递归优化的原理

优化前(普通递归):

plain 复制代码
复制 插入 新文件

factorial(3)
→ 3 * factorial(2)
→ 3 * (2 * factorial(1))
→ 3 * (2 * 1)
→ 3 * 2
→ 6

优化后(尾递归):

plain 复制代码
复制 插入 新文件

factorial(3, 1)
→ factorial(2, 3)
→ factorial(1, 6)
→ 6

这里我们进行一下尾递归优化的写法,核心就是下面的代码

优点:避免递归深度过大,从而引发栈溢出

传统的快速排序

javascript 复制代码
quickSort(arr, left, partitionIndex - 1);  // 递归处理左子数组
quickSort(arr, partitionIndex + 1, right); // 递归处理右子数组

尾递归

javascript 复制代码
 // 先处理较小的子数组,减少递归深度
  if (partitionIndex - left < right - partitionIndex) {
      quickSort(arr, left, partitionIndex - 1);
      left = partitionIndex + 1;
  } else {
      quickSort(arr, partitionIndex + 1, right);
      right = partitionIndex - 1;
  }

尾递归优化完整

javascript 复制代码
 // 优化5 - 尾递归优化
    let stepCount = 0; // 全局步数计数器
    function quickSort(arr, left = 0, right = arr.length - 1) {

        // 对小数组使用插入排序
        if (right - left < 10) {
            insertionSort(arr, left, right);
            stepCount++;
            return arr;
        }

        while (left < right) {
            stepCount++; // 递归调用计数
            const mid = Math.floor((left + right) / 2);

            // 比较三个数
            if (arr[left] > arr[mid]) {
                [arr[left], arr[mid]] = [arr[mid], arr[left]];
            }
            if (arr[left] > arr[right]) {
                [arr[left], arr[right]] = [arr[right], arr[left]];
            }
            if (arr[mid] > arr[right]) {
                [arr[mid], arr[right]] = [arr[right], arr[mid]];
            }

            // 将基准放到right位置
            [arr[mid], arr[right]] = [arr[right], arr[mid]];
            const pivot = arr[right];


            let i = left - 1;
            for (let j = left; j < right; j++) {
                stepCount++; // 循环迭代计数
                if (arr[j] <= pivot) {
                    i++;
                    [arr[i], arr[j]] = [arr[j], arr[i]];
                }
            }
            [arr[i + 1], arr[right]] = [arr[right], arr[i + 1]];


            // 
            const partitionIndex = i + 1;


            // 先处理较小的子数组,减少递归深度
            if (partitionIndex - left < right - partitionIndex) {
                quickSort(arr, left, partitionIndex - 1);
                left = partitionIndex + 1;
            } else {
                quickSort(arr, partitionIndex + 1, right);
                right = partitionIndex - 1;
            }

        }
        return arr;
    }


    // 辅助函数:插入排序(带步数计算)
    function insertionSort(arr, left, right) {
        stepCount++;
        console.log(`步骤 ${stepCount}: 进入insertionSort, 范围: [${left}, ${right}]`);

        for (let i = left + 1; i <= right; i++) {
            stepCount++;
            console.log(`步骤 ${stepCount}: 处理元素arr[${i}](${arr[i]})`);
            const key = arr[i];
            let j = i - 1;

            stepCount++;
            while (j >= left && arr[j] > key) {
                stepCount++;
                console.log(`步骤 ${stepCount}: 移动arr[${j}]到arr[${j + 1}]`);
                arr[j + 1] = arr[j];
                j--;
            }

            stepCount++;
            arr[j + 1] = key;
            console.log(`步骤 ${stepCount}: 将${key}放到位置${j + 1},数组变为:`, arr);
        }

        stepCount++;
        console.log(`步骤 ${stepCount}: 插入排序完成`);
        return arr;
    }

3.插入排序(Insertion Sort)

认识

插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入,类似于我们整理手中的扑克牌 ,一次只处理一个元素,每次都将当前元素插入到已排序部分的正确位置。

plain 复制代码
function insertionSort(arr) {
    for (let i = 1; i < arr.length; i++) {
        const temp = arr[i];
        let j = i;
        while (j > 0) {
            if (arr[j - 1] > temp) {
                arr[j] = arr[j - 1];
            } else {
                break;
            }
            j--;
        }
        arr[j] = temp;
    }
}

// 功能测试
const arr = [4, 3, 6, 2, 5, 7, 9, 8, 1]
insertionSort(arr)
console.log(arr) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
核心思想

将数组分为两个部分:

已排序部分:初始时只包含第一个元素

未排序部分:包含其余所有元素

算法通过从未排序部分取出一个元素,并将其插入到已排序部分的适当位置,从而不断扩大已排序部分,直到整个数组排序完成。

时间复杂度
  • 最坏时间复杂度:O(n²) - 当数组是逆序时
  • 平均时间复杂度:O(n²)
  • 最好时间复杂度:O(n) - 当数组已经有序时
空间复杂度
  • O(1) - 只需要常数级别的额外空间
优缺点
优点
  1. 实现简单,容易理解
  2. 对于小规模数据或基本有序的数据效率较高
  3. 是稳定排序算法(相等元素的相对顺序不变)
  4. 是原地排序算法,不需要额外空间
缺点
  1. 对于大规模数据效率较低
  2. 最坏情况下时间复杂度为O(n²)
适用场景
  1. 数据规模较小
  2. 数据基本有序
  3. 需要稳定排序的场景
  4. 内存受限的场景

4.归并排序(Merge Sort)

认识

归并排序(Merge Sort)是一种基于分治法(Divide and Conquer)的高效排序算法。它将数组分成两半,分别对两半进行排序,然后将排序好的两半合并成一个有序数组。归并排序是稳定排序算法,时间复杂度为O(n log n)。

大致实现如下

plain 复制代码
function mergeSort(arr) {
    if(arr.length === 1) return arr
    
    let mid = Math.floor(arr.length / 2)
    let left = arr.slice(0, mid)
    let right = arr.slice(mid)
    
    return merge(mergeSort(left), mergeSort(right))
}

function merge(a, b) {
    let res = []
    
    while (a.length && b.length) {
        if (a[0] < b[0]) {
            res.push(a[0])
            a.shift()
        } else {
            res.push(b[0])
            b.shift()
        }
    }
    
    if(a.length){
        res = res.concat(a)
    } else {
        res = res.concat(b)
    }
    
    return res
}

// 功能测试
const arr = [4, 3, 6, 2, 5, 7, 9, 8, 1]
console.log(mergeSort(arr)) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

原理

归并排序的核心思想是"分而治之",主要包含两个步骤:

  1. 分:将数组不断二分,直到每个子数组只有一个元素(单个元素自然是有序的)
  2. 治:将有序的子数组两两合并,直到合并成一个完整的有序数组

主要就是分为两步

  • 分割:将待排序的线性表不断地切分成若干个子表,直到每个子表只包含一个元素,这时,可以认为只包含一个元素的子表是有序表。
  • 归并:将子表两两合并,每合并一次,就会产生一个新的且更长的有序表,重复这一步骤,直到最后只剩下一个子表,这个子表就是排好序的线性表。

算法步骤

  1. 分解阶段:
    • 将数组从中间分成两个子数组
    • 递归地对每个子数组进行分解,直到每个子数组只有一个元素
  2. 合并阶段:
    • 将两个有序的子数组合并成一个有序数组
    • 比较两个子数组的元素,按顺序取出较小的元素放入结果数组
    • 如果其中一个子数组已经全部取出,则将另一个子数组的剩余元素直接复制到结果数组

时间复杂度分析

  • 最好时间复杂度:O(n log n)
  • 平均时间复杂度:O(n log n)
  • 最坏时间复杂度:O(n log n)
  • 空间复杂度:O(n) - 需要额外的空间来存储合并的数组

优缺点分析

优点

  1. 时间复杂度稳定:在各种情况下都是O(n log n)
  2. 稳定排序:相等元素的相对顺序保持不变
  3. 适合大规模数据:对于大数据集效率较高
  4. 可并行化:分解阶段可以并行处理

缺点

  1. 空间复杂度高:需要O(n)的额外空间
  2. 小规模数据性能不如插入排序:对于小数组,插入排序可能更高效
  3. 不是原地排序:需要额外的存储空间

JavaScript实现

基础实现
javascript 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>归并排序</title>
</head>

<body>
    <script>
    // 解法1 归并排序
    const merge=(left,right)=>{
        let result = [];
        let leftIndex = 0;
        let rightIndex = 0;
        while (leftIndex < left.length && rightIndex < right.length) {
            if(left[leftIndex]<right[rightIndex]){
                result.push(left[leftIndex])
                leftIndex++;
            }else{
                result.push(right[rightIndex])
                rightIndex++;
            }
        }
        return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
    };
    const  mergeSort=(arr)=> {
        // 数组长度小于等于1,则已经有序
        if(arr.length<=1){
            return arr;
        }
        // 分解阶段:将数组分成两半
        const middle=Math.floor(arr.length / 2);
        const left =arr.slice(0,middle);
        const right =arr.slice(middle);
        return merge(mergeSort(left),mergeSort(right))
    }

    // 示例使用
    const arr = [38, 27, 43, 3, 9, 82, 10];
    console.log("原始数组:", arr);
    console.log("排序后数组:", mergeSort(arr));
    </script>
</body>

</html>
优化1-对小数组使用插入排序

归并排序的方式比较适合于大数组,这种时候小数组我们可以额外使用插入排序进行优化

javascript 复制代码
当数组长度小于某个阈值(如10-20)时,使用插入排序而不是继续递归归并排序
插入排序在小数组上表现通常比归并排序更好

设置阈值以后

javascript 复制代码
// 2 优化-小数组使用插入排序
    const INSERTION_SORT_THRESHOLD = 10;
    const insertionSort = (arr) => {
        for (let i = 1; i < arr.length; i++) {
            let key = arr[i];
            let j = i - 1;
            while (j >= 0 && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = key;
        }
        return arr;
    };
    const merge=(left,right)=>{
        let result = [];
        let leftIndex = 0;
        let rightIndex = 0;
        while (leftIndex < left.length && rightIndex < right.length) {
            if(left[leftIndex]<right[rightIndex]){
                result.push(left[leftIndex])
                leftIndex++;
            }else{
                result.push(right[rightIndex])
                rightIndex++;
            }
        }
        return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
    };
    const  mergeSort=(arr)=> {
        // 数组长度小于等于1,则已经有序
        if(arr.length<=INSERTION_SORT_THRESHOLD){
            return insertionSort([...arr]);
        }
        // 分解阶段:将数组分成两半
        const middle=Math.floor(arr.length / 2);
        const left =arr.slice(0,middle);
        const right =arr.slice(middle);
        return merge(mergeSort(left), mergeSort(right));
    }
优化合并过程

检查左右数组是否已经有序,避免不必要的比较

javascript 复制代码
 // 3-优化合并过程
    // 检查左右数组是否已经有序,避免不必要的比较
    // 这里感觉其实就是各种排序之间的特性进行优化
    const INSERTION_SORT_THRESHOLD = 10;
    const insertionSort = (arr) => {
        for (let i = 1; i < arr.length; i++) {
            let key = arr[i];
            let j = i - 1;
            while (j >= 0 && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = key;
        }
        return arr;
    };
    const merge=(left,right)=>{
        // 检查是否可以直接合并
        if (left[left.length - 1] <= right[0]) {
            return left.concat(right);
        }
        if (right[right.length - 1] <= left[0]) {
            return right.concat(left);
        }
        let result = [];
        let leftIndex = 0;
        let rightIndex = 0;
        while (leftIndex < left.length && rightIndex < right.length) {
            if(left[leftIndex]<right[rightIndex]){
                result.push(left[leftIndex])
                leftIndex++;
            }else{
                result.push(right[rightIndex])
                rightIndex++;
            }
        }
        return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
    };
    const  mergeSort=(arr)=> {
        // 数组长度小于等于1,则已经有序
        if(arr.length<=INSERTION_SORT_THRESHOLD){
            return insertionSort([...arr]);
        }
        // 分解阶段:将数组分成两半
        const middle=Math.floor(arr.length / 2);
        const left =arr.slice(0,middle);
        const right =arr.slice(middle);
        return merge(mergeSort(left), mergeSort(right));
    }
优化-预先分配结果数组空间
javascript 复制代码
// 预先分配结果数组空间
    const INSERTION_SORT_THRESHOLD = 10;
    const insertionSort = (arr) => {
        for (let i = 1; i < arr.length; i++) {
            let key = arr[i];
            let j = i - 1;
            while (j >= 0 && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = key;
        }
        return arr;
    };
    const merge=(left,right)=>{
        // 检查是否可以直接合并
        if (left[left.length - 1] <= right[0]) {
            return left.concat(right);
        }
        if (right[right.length - 1] <= left[0]) {
            return right.concat(left);
        }
         // 预先分配结果数组空间
        const result = new Array(left.length + right.length);
        let leftIndex = 0, rightIndex = 0, resultIndex = 0;

        while (leftIndex < left.length && rightIndex < right.length) {
            result[resultIndex++] = left[leftIndex] < right[rightIndex] 
            ? left[leftIndex++] 
            : right[rightIndex++];
        }
         // 处理剩余元素
        while (leftIndex < left.length) {
            result[resultIndex++] = left[leftIndex++];
        }
        while (rightIndex < right.length) {
            result[resultIndex++] = right[rightIndex++];
        }
        
        return result;
    };
    const  mergeSort=(arr)=> {
        // 数组长度小于等于1,则已经有序
        if(arr.length<=INSERTION_SORT_THRESHOLD){
            return insertionSort([...arr]);
        }
        // 分解阶段:将数组分成两半
        const middle=Math.floor(arr.length / 2);
        const left =arr.slice(0,middle);
        const right =arr.slice(middle);
        return merge(mergeSort(left), mergeSort(right));
    }
优化5-避免不必要的数组复制-使用索引而不是slice来减少内存分配
javascript 复制代码
 const mergeSort = (arr, start = 0, end = arr.length) => {
        // 数组长度小于阈值,使用插入排序
        if (end - start <= INSERTION_SORT_THRESHOLD) {
            return insertionSort(arr.slice(start, end));
        }
        
        const middle = Math.floor((start + end) / 2);
        const left = mergeSort(arr, start, middle);
        const right = mergeSort(arr, middle, end);
        return merge(left, right);
    };
优化-使用迭代而非递归实现
javascript 复制代码
const INSERTION_SORT_THRESHOLD = 10;
    const insertionSort = (arr) => {
        for (let i = 1; i < arr.length; i++) {
            let key = arr[i];
            let j = i - 1;
            while (j >= 0 && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = key;
        }
        return arr;
    };
    const merge = (arr, temp, left, mid, right) => {
        // 检查是否已经有序
        if (arr[mid - 1] <= arr[mid]) {
            return;
        }
        
        // 合并两个有序区间
        let i = left, j = mid, k = left;
        while (i < mid && j < right) {
            temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
        }
        while (i < mid) {
            temp[k++] = arr[i++];
        }
        while (j < right) {
            temp[k++] = arr[j++];
        }
        
        // 将合并结果复制回原数组
        for (let idx = left; idx < right; idx++) {
            arr[idx] = temp[idx];
        }
    };
    const mergeSort = (arr) => {
        const n = arr.length;
        const temp = new Array(n);
        
        // 使用插入排序处理小数组
        for (let i = 0; i < n; i += INSERTION_SORT_THRESHOLD) {
            const end = Math.min(i + INSERTION_SORT_THRESHOLD, n);
            insertionSort(arr, i, end);
        }
        
        // 自底向上的归并排序
        for (let size = INSERTION_SORT_THRESHOLD; size < n; size *= 2) {
            for (let left = 0; left < n - size; left += 2 * size) {
                const mid = left + size;
                const right = Math.min(left + 2 * size, n);
                merge(arr, temp, left, mid, right);
            }
        }
        
        return arr;
    };
最终版本
javascript 复制代码
const INSERTION_SORT_THRESHOLD = 10;

const insertionSort = (arr, start = 0, end = arr.length) => {
    for (let i = start + 1; i < end; i++) {
        let key = arr[i];
        let j = i - 1;
        while (j >= start && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
    return arr;
};

const merge = (left, right) => {
    // 检查是否可以直接合并
    if (left[left.length - 1] <= right[0]) {
        return left.concat(right);
    }
    if (right[right.length - 1] <= left[0]) {
        return right.concat(left);
    }
    
    // 预先分配结果数组空间
    const result = new Array(left.length + right.length);
    let leftIndex = 0, rightIndex = 0, resultIndex = 0;
    
    while (leftIndex < left.length && rightIndex < right.length) {
        result[resultIndex++] = left[leftIndex] < right[rightIndex] 
            ? left[leftIndex++] 
            : right[rightIndex++];
    }
    
    // 处理剩余元素
    while (leftIndex < left.length) {
        result[resultIndex++] = left[leftIndex++];
    }
    while (rightIndex < right.length) {
        result[resultIndex++] = right[rightIndex++];
    }
    
    return result;
};

const mergeSort = (arr) => {
    // 数组长度小于阈值,使用插入排序
    if (arr.length <= INSERTION_SORT_THRESHOLD) {
        return insertionSort([...arr]);
    }
    
    // 分解阶段:将数组分成两半
    const middle = Math.floor(arr.length / 2);
    const left = mergeSort(arr.slice(0, middle));
    const right = mergeSort(arr.slice(middle));
    return merge(left, right);
};

5.选择排序(Selection Sort)

认识

选择排序基本思想是
  • 首先在未排序的数列中找到最小(or最大)元素,然后将其存放到数列的起始位置。
  • 接着,再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾。
  • 以此类推,直到所有元素均排序完毕。

分类:属于选择排序类,是原地排序算法

稳定性:不稳定排序算法(相等元素的相对位置可能改变)

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

O(1),只需要常数级别的额外空间

特点:

交换次数少,每次只交换一次

比较次数多,无论数组初始状态如何

不适合大规模数据排序

选择排序的原理

选择排序的工作过程可以分为以下步骤:

  1. 将数组分为已排序区和未排序区,初始时已排序区为空
  2. 在未排序区中找到最小(或最大)的元素
  3. 将这个元素与未排序区的第一个元素交换位置
  4. 将已排序区的范围扩大一个元素
  5. 重复步骤2-4,直到整个数组排序完成
适用场景

选择排序适用于以下场景:

  1. 小规模数据排序
  2. 内存受限的环境(空间复杂度低)
  3. 对交换操作有较高成本的情况(交换次数少)
  4. 对算法简单性要求高的情况
选择排序的优缺点

优点:

  1. 实现简单,容易理解
  2. 空间复杂度低,只需要O(1)的额外空间
  3. 交换次数少,每次排序只交换一次
  4. 不受数据初始顺序影响,时间复杂度始终是O(n²)

缺点:

  1. 时间复杂度高,不适合大规模数据
  2. 不稳定排序,相等元素的相对位置可能改变
  3. 即使数组已经有序,仍然需要进行O(n²)的比较

实现

plain 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>选择排序</title>
</head>

<body>
    <script>
    // 解法1 选择排序
    const optimizedSelectionSort = (arr) => {
        const n = arr.length;

         // 外层循环控制排序轮数
        for(let i=0;i<n-1;i++){
            // 假设当前未排序区的第一个元素是最小的
            let minIndex = i;

            // 内层循环在未排序区找最小元素的索引
            for(let j=i+1;j<n;j++){
                if(arr[j]<arr[minIndex]){
                    minIndex=j;
                }
            }
            // 如果找到的最小元素不是当前位置,则交换
            if(minIndex!==i){
                [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
            }
        }
        return arr;
    };
    // 示例使用
    const arr = [38, 27, 43, 3, 9, 82, 10];
    console.log("原始数组:", arr);
    console.log("排序后数组:", optimizedSelectionSort(arr));
    </script>
</body>

</html>
优化-双向选择排序,减少比较次数
javascript 复制代码
// 1 优化 -双向选择排序,减少比较次数
    const optimizedSelectionSort = (arr) => {
        const n = arr.length;
        let left = 0;
        let right = n - 1;
        while(left < right){
            let minIndex = left;
            let maxIndex = left;  
             // 优化内层循环,减少比较次数
            for (let i = left + 1; i <= right; i++) {
                // 一次比较同时更新最小和最大值
                if (arr[i] < arr[minIndex]) {
                    minIndex = i;
                } else if (arr[i] > arr[maxIndex]) {
                    maxIndex = i;
                }
            }
            // 交换最小值到左端
            if (minIndex !== left) {
                [arr[left], arr[minIndex]] = [arr[minIndex], arr[left]];
                // 如果最大值原本在left位置,已经被交换到minIndex位置
                if (maxIndex === left) {
                    maxIndex = minIndex;
                }
            }
            // 交换最大值到右端
            if (maxIndex !== right) {
                [arr[right], arr[maxIndex]] = [arr[maxIndex], arr[right]];
            }
            left++;
            right--;
            return arr;
        }
    };
添加提前终止条件

在排序的过程之中,有时候我们会遇到后面已经排序的情况,就可以避免重复比对,这个时候可以提前添加终止条件

javascript 复制代码
// 2 添加提前终止条件
    function optimizedSelectionSort(arr) {
        const n = arr.length;

        let left = 0;
        let right = n - 1;

        while (left < right) {
            let minIndex = left;
            let maxIndex = left;
            let isSorted = true; // 添加标记,假设已排序

            for (let i = left + 1; i <= right; i++) {
                // 如果发现逆序对,标记为未排序
                if (arr[i] < arr[i - 1]) {
                    isSorted = false;
                }

                if (arr[i] < arr[minIndex]) {
                    minIndex = i;
                } else if (arr[i] > arr[maxIndex]) {
                    maxIndex = i;
                }
            }

            // 如果已经有序,提前终止
            if (isSorted) {
                break;
            }

            // 交换最小值到左端
            if (minIndex !== left) {
                [arr[left], arr[minIndex]] = [arr[minIndex], arr[left]];

                if (maxIndex === left) {
                    maxIndex = minIndex;
                }
            }

            // 交换最大值到右端
            if (maxIndex !== right) {
                [arr[right], arr[maxIndex]] = [arr[maxIndex], arr[right]];
            }

            left++;
            right--;
        }

        return arr;
    }
相关推荐
摇滚侠2 小时前
浏览器的打印功能,如果通过HTML5,控制样式
前端·html·html5
喵喵侠w2 小时前
uni-app微信小程序相机组件二次拍照白屏问题的排查与解决
前端·数码相机·微信小程序·小程序·uni-app
超大只番薯2 小时前
在Next.js中实现页面级别KeepAlive
前端
快递鸟2 小时前
第三方物流接口优选:快递鸟物流 API,打破单一快递对接壁垒
前端
Mapmost2 小时前
【高斯泼溅】从一张好照片开始:Mapmost 3DGS建模之图像采集指南
前端
李少兄3 小时前
解决 Chrome 下载 `.crx` 文件被自动删除及“无法安装扩展程序,因为它使用了不受支持的清单版本”问题
前端·chrome
孟祥_成都3 小时前
最好的组件库教程又回来了,升级为 headless 组件库!
前端·面试·架构
Man3 小时前
当我们执行 npm run xxx 的时候实际执行逻辑和流程
前端·javascript·前端框架
曾富贵3 小时前
【eslint】快速配置
前端