JavaScript篇:一箭双雕:巧用单次循环找出数组中的两大霸主

197 阅读5分钟

        大家好,我是江城开朗的豌豆,一名拥有6年以上前端开发经验的工程师。我精通HTML、CSS、JavaScript等基础前端技术,并深入掌握Vue、React、Uniapp、Flutter等主流框架,能够高效解决各类前端开发问题。在我的技术栈中,除了常见的前端开发技术,我还擅长3D开发,熟练使用Three.js进行3D图形绘制,并在虚拟现实与数字孪生技术上积累了丰富的经验,特别是在虚幻引擎开发方面,有着深入的理解和实践。

        我一直认为技术的不断探索和实践是进步的源泉,近年来,我深入研究大数据算法的应用与发展,尤其在数据可视化和交互体验方面,取得了显著的成果。我也注重与团队的合作,能够有效地推动项目的进展和优化开发流程。现在,我担任全栈工程师,拥有CSDN博客专家认证及阿里云专家博主称号,希望通过分享我的技术心得与经验,帮助更多人提升自己的技术水平,成为更优秀的开发者。

大家好,我是前端开发工程师小杨。今天要分享一个既实用又有趣的算法小技巧——如何用一次循环就找出数组中的两个最大值。这个技巧在面试中经常被问到,在实际开发中也很有用,比如我们需要找出销量最高的两款产品,或者性能消耗最大的两个模块时。

为什么要用单次循环?

很多朋友的第一反应可能是:先排序然后取前两个不就行了?确实可以,但排序的时间复杂度是O(n log n),而我们今天要介绍的方法只需要O(n)的时间复杂度,效率更高。特别是在处理大数据量时,这种优化就显得尤为重要。

基本思路

想象一下你在操场上找跑得最快的两个人。你不会先让所有人跑完记录成绩再排序,而是边看比赛边记录当前的第一名和第二名。这就是我们的算法思路:

  1. 初始化两个变量来保存最大值和第二大值
  2. 遍历数组,逐个比较更新这两个值
  3. 遍历结束后,这两个变量就是我们想要的结果

代码实现

让我们看看具体的JavaScript实现:

function findTwoLargestNumbers(arr) {
  // 初始化两个变量,设置为负无穷以确保任何数都比它们大
  let first = -Infinity;
  let second = -Infinity;

  for (let i = 0; i < arr.length; i++) {
    const current = arr[i];
    
    // 如果当前值大于第一名,则更新第一名,原来的第一名降为第二名
    if (current > first) {
      second = first;
      first = current;
    } 
    // 如果当前值介于第一名和第二名之间,则更新第二名
    else if (current > second && current !== first) {
      second = current;
    }
  }

  return [first, second];
}

// 举个实际例子
const myScores = [98, 23, 42, 85, 77, 85, 99, 90];
const [top1, top2] = findTwoLargestNumbers(myScores);
console.log(`我的最高分是${top1},第二高分是${top2}`);

边界情况处理

好的代码要考虑到各种边界情况:

  1. 数组长度小于2:这时应该返回null或者抛出错误
  2. 所有元素相同:根据需求决定是返回两个相同的值还是认为没有第二大值
  3. 包含NaN或非数字:应该先进行数据清洗

改进后的版本:

function findTwoLargestNumbers(arr) {
  if (!Array.isArray(arr) || arr.length < 2) {
    throw new Error('输入必须是长度至少为2的数组');
  }

  // 使用前两个元素初始化(确保second不大于first)
  let first = Math.max(arr[0], arr[1]);
  let second = Math.min(arr[0], arr[1]);

  // 从第三个元素开始遍历
  for (let i = 2; i < arr.length; i++) {
    const current = arr[i];
    
    if (current > first) {
      second = first;
      first = current;
    } else if (current > second) {
      second = current;
    }
  }

  return [first, second];
}

性能对比

让我们用一个大数组来对比两种方法的性能:

// 生成一个包含100万个 随机数的数组
const hugeArray = Array.from({length: 1000000}, () => Math.floor(Math.random() * 10000000));

// 方法1:排序法
console.time('sort method');
const sorted = [...hugeArray].sort((a, b) => b - a);
const [sortFirst, sortSecond] = sorted.slice(0, 2);
console.timeEnd('sort method');

// 方法2:我们的单次循环法
console.time('single loop');
const [loopFirst, loopSecond] = findTwoLargestNumbers(hugeArray);
console.timeEnd('single loop');

console.log('排序法结果:', sortFirst, sortSecond);
console.log('单次循环结果:', loopFirst, loopSecond);

在我的电脑上测试,排序法耗时约120ms,而单次循环法仅需约5ms,性能提升了20多倍!

实际应用场景

这种算法在实际开发中非常有用,比如:

  1. 电商网站中找出销量最高的两款商品展示
  2. 性能监控中找出消耗资源最多的两个模块
  3. 游戏开发中记录得分最高的两名玩家
  4. 数据分析中找出影响最大的两个因素

变种问题

掌握了这个算法后,我们可以轻松解决一些变种问题:

  1. 找出最小的两个数:只需把比较符号反过来
  2. 找出第N大的数:可以使用类似的思路,但需要维护一个大小为N的数组
  3. 找出最大值和最小值:同样可以在一次循环中完成

总结

今天我们学习了一个既高效又实用的算法技巧。记住,好的算法不在于使用了多么高深的数据结构,而在于如何巧妙地利用基本操作解决问题。单次循环找两个最大值就是一个很好的例子,它展示了如何通过简单的比较和更新操作,将时间复杂度从O(n log n)降到O(n)。

下次面试官问你这个问题时,希望你能自信地给出最优解!如果你有更巧妙的实现方法,欢迎在评论区分享讨论。