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;
    }
相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax