面试官:你有几种方法实现该数组的排序?

64 阅读8分钟

引言:

排序是编程中最基础也最核心的操作之一。从JavaScript的内置sort()方法到经典的冒泡排序,再到高效的归并与快速排序等,不同的排序算法在时间复杂度、空间复杂度和适用场景上各有优劣。本文将系统且深入解析6种常用排序方法,帮助你在实际开发中做出最优选择。

一、Sort()

sort()是JavaScript数组的原地排序方法,它会直接修改原数组,并返回排序后的数组引用。

🔍 要注意的是,要在方法内传入函数,如果未传入函数,可能会导致输出结果出现错误。因为sort()会将数组元素转换为字符串,然后按照Unicode编码顺序进行排序。

示例:

const arr = [10, 2, 30, 1]; 
console.log(arr.sort()); // 输出:[1, 10, 2, 30] 
// 原因:"10" 的 Unicode 编码小于 "2"

1. 数字排序

示例:

const nums = [10, 2, 30, 1]; 
// 升序排序 
nums.sort((a, b) => {
  return a - b
});
console.log(nums); // [1, 2, 10, 30] 

// 降序排序 
nums.sort((a, b) =>{
  return b - a
  }); 
console.log(nums); // [30, 10, 2, 1]

2. 字符串排序

示例:

const strs = ["apple", "banana", "cherry", "date"];
strs.sort((a, b) => {
    return a.length - b.length
});
console.log(strs);

输出结果:

image.png

输出结果显示sort()方法实现了让字符串按长度排序的效果

3.对象数组排序

示例:

const users = [
  { name: "Alice", age: 25 },
  { name: "Bob", age: 20 },
  { name: "Charlie", age: 30 }
];
// 按年龄升序
users.sort((a, b) => {
    return a.age - b.age
}); 
console.log(users);

输出结果:

image.png

二、冒泡排序

冒泡排序的核心思想是通过重复遍历待排序数组,比较相邻元素并交换位置,将最大(或最小)的元素逐步“冒泡”到数组末端。这个过程如同水中气泡上升,因此成为“冒泡排序”。

实现步骤可简述为:

🔍 遍历数组:从第一个元素开始,依次比较相邻的两个元素。

🔍 交换元素:如果前一个元素大于(或小于)后一个元素(升序排序),则交换它们的位置。

🔍 减少范围:每完成一轮遍历,最大的元素会被“推”到当前未排序部分的末尾,下一轮遍历的范围可以减少一个元素。

🔍重复过程:持续上述操作,直到整个数组排序完成。

实现代码: image.png

输出结果:

image.png 因为使用了两个for循环嵌套实现功能,因此时间复杂度为O(n^2)空间复杂度为O(1)

三、选择排序

选择排序是一种基础的排序算法,其核心思想是通过不断选择剩余元素中的最小值(或最大值),将其放到已排序序列的末尾

实现代码:

image.png 输出结果:

image.png 实现步骤可简述为:

🔍 构建寻找最小值的函数getSmall:传入两个参数start和end,用于标记传入数组的首位值下标和末尾值的下标。定义一个temp用于标记最小值的下标,初始化为start的值,再使用for循环遍历传入的数组,如果遍历到的元素小于当前temp下标代表的值,便将该元素下标赋值给temp,直至循环结束,我们便得到最小值的下标,最后return temp。

🔍 交换数组元素:使用for循环来交换数组元素,定义一个min等于使用getSmall()得到最小值的下标,从第一个元素开始,依次和最小值交换,直到循环结束,便可得到有序排列的数组。

该方法依旧嵌套了两个for循环,所以时间复杂度为O(n^2),原地排序无需额外的空间,所以空间复杂度为O(1)

四、插入排序

插入排序是一种简单直观的排序算法,核心思想是将数组分为“已排序部分”和“未排序部分”,逐步将未排序部分的元素插入到已排序部分的正确位置。

实现步骤:

🔍 初始化:将数组的第一个元素视为“已排序部分”(索引0)。

🔍 遍历未排序部分:从索引1开始,依次取出每个元素 current

🔍 查找插入位置:从“已排序部分”的末尾(索引 i-1)向前比较:

  • 如果 current 小于当前元素,将当前元素后移一位(腾出位置)。
  • 如果 current 大于等于当前元素,停止比较(找到插入位置)。

🔍 插入元素:将 current 插入到停止位置的下一位。

🔍 重复:直到所有元素处理完毕。

实现代码:

image.png

  • 时间复杂度:O(n²)(最坏情况),但对于接近有序的数组,时间复杂度可优化至O(n)。
  • 空间复杂度:O(n)(因使用了额外的 newArr 存储结果)。

五、归并排序

归并排序是一种基于分治思想的经典排序算法,由约翰·冯·诺依曼在1945年提出。它通过将问题拆解为更小的子问题并递归解决,最终合并结果得到有序序列,具有稳定且高效的特点。

核心思想:分而治之

归并排序的核心逻辑可以概括为三步:

  1. 分解(Divide) :将未排序的数组从中间拆分为两个等长的子数组,递归执行直到子数组长度为1(此时视为有序)。
  2. 合并(Merge) :将两个已排序的子数组合并为一个有序数组。
  3. 递归(Recurse) :重复上述过程,直到所有子数组合并为完整的有序数组。

第一步:分解

将数组从中间拆分成两个子数组,重复执行直到所有子数组的长度均为1。所以我们可以构建一个函数用于实现该功能。

实现代码:

image.png 注: Math.floor(len) 为向下取整,Math.ceil(len) 为向上取整

第二步:排序

当数组被分成长度为1的单个数组时,就对它们进行排序。

实现代码:

image.png 该函数用于对两个数组进行排序,创建一个空数组和两个指针,空数组res用于存储排序后的数组,两个指针分别指向两个数组的第一个元素并进行比较,将较小的数存入数组,且指针后移一位;若有一个数组已经遍历完成,就将另一个数组的剩余部分拼接到res数组的后面,即可完成两个数组的排序。

完整代码示例:

const arr = [8,7,6,5,4,3,2,1]
function mergeSort(arr) {
  const len = arr.length
  if (len <= 1) {
    return arr
  }
  // 分割
  const mid = Math.floor(len / 2)
  const leftArr = mergeSort(arr.slice(0, mid))  // [8]
  const rightArr = mergeSort(arr.slice(mid, len)) // [7]
  // 合并两个有序数组,让它还是有序的
  return mergeArr(leftArr, rightArr)//时间o(n)
}

function mergeArr(arr1, arr2) {
  let res = []
  let i = 0
  let j = 0

  while (i < arr1.length && j < arr2.length) {
    if (arr1[i] <= arr2[j]) {
      res.push(arr1[i])
      i++
    } else {
      res.push(arr2[j])
      j++
    }
  }

  if (i < arr1.length) {
    res = res.concat(arr1.slice(i))
  } else {
    res = [...res, ...arr2.slice(j)]
  }

  return res
}
console.log(mergeSort(arr));
  • 时间复杂度:最优、最坏、平均均为 O(n log n) ,递归分解的时间为O(log n),合并的时间为O(n)。
  • 空间复杂度O(n) ,合并过程需额外的临时数组存储结果

六、快速排序

快速排序的执行流程可分为三个关键步骤:

第一步:选择基准

从数组中选一个元素作为基准(如第一个、最后一个或随机元素,我们的示例采用中间的元素),作为分区的“分界线”。

第二步:分区操作

通过双指针遍历数组,将小于基准的元素移到左侧,大于基准的移到右侧,基准最终位于正确的排序位置(无需额外空间)。

第二步: 递归排序

对基准左侧和右侧的子数组递归重复上述步骤,直到子数组长度为1(默认有序)。

实现代码:

image.png

  • 时间复杂度平均O(n log n),最坏O(n²)。
  • 空间复杂度O(n²) ,合并过程需额外的临时数组存储结果。

总结:

排序问题是非常经典的算法问题,在我们面试的过程中也经常涉及到,尤其是归并排序快速排序,几乎是必考题,需熟练掌握。

排序算法在计算机科学中扮演着至关重要的角色,不同算法因其独特特性适用于各异场景。Sort()函数提供便捷通用排序方式,适用于多种数据类型简单排序。冒泡、选择、插入排序虽原理直观,时间复杂度较高,适合小规模数据排序。而归并排序与快速排序基于分治思想,时间复杂度均为O(n log n),在大数据量排序中表现出色。归并排序稳定且性能稳定,快速排序平均性能佳但不稳定且最坏情况时间复杂度为O(n²)。在实际应用中,需依据数据规模、是否要求稳定性等因素,合理选择排序算法,以实现高效的数据处理与管理。