js 求数组的最大值、最小值

6,132 阅读9分钟

如何实现

求两个数字的最大值,相信大家第一个想到的都是利用内置对象 Math 中的 max 方法

我们先来简单的回顾一下这个方法的相关内容,对 Math.max 不熟悉点 这里

Math.max([value1 [, value2 [, ...])
参数

      value1, value2, ......

      以逗号分割的一组数字

返回值

      返回给定的一组数字中的最大值。

      如果给定的参数中至少有一个参数无法被转换成数字,则会返回 ==NaN==。

      如果没有参数,则结果为 ==- Infinity==。

示例
Math.max(10, 20)    
//  20 

Math.max(3, 1, 2, -9)
//  3

Math.max('10', 8, 5, 3) 
//  10 
//  字符串 '10' 可以强制转换为数字,所以返回 10

Math.max('a', 0, 15)  
//  NaN
//  字符串 'a' 不能转换成数字,所以返回 NaN

Math.max()
//  -Infinity
//  没有参数,所以返回 -Infinity

一般会把一堆数字存放在一个数组里,然后去求这个数组中的最大值,但是数组中并没有提供这样一个方法,那我们就自己写一个吧~

先整理一下思路:

  1. 将数组中第一个元素赋值给一个变量 max ,并且把这个变量作为最大值
  2. 开始遍历数组,从第二个元素开始依次和作为最大值的变量 max 进行比较
  3. 如果当前的元素的值大于变量 max ,就把当前的元素的值赋值给变量 max
  4. 移动到下一个元素,重复步骤 3
  5. 当数组元素遍历结束时,变量 max 就是这个数组中的最大值

代码如下:

var arr = [ 10, 900, -6, 0 , 46, 89, 23];

//  将数组的第一个元素的值赋值给变量 max
var max = arr[0]; 

//  使用 for 循环从数组中的第二个值(即下标为1)开始做遍历
for( var i = 1; i < arr.length ; i++ ) {
    if( arr[i] > max ) {
        max = arr[i];
    }
}

max // 900 此时即可算出数组 arr 中的最大值

可是这样的求最大值的代码只能使用一次,而且只能求变量 arr 的最大值,当需要求多个数组的最大值时,难道需要写多个这种的 for 循环吗 ? 答案肯定是 NO ! 作为程序员的我们,肯定是想写成可复用的代码,于是我们来将这个方法提炼成一个函数,代码如下:

var arr1 = [ 10, 900, -6, 0 , 46, 89, 23];
var arr2 = [ 78, 56, 13, -9 ];
var arr3 = [ 67, 25, 8];

function findMax ( arr ) {
    var max = arr[0]; 
    for( var i = 1; i < arr.length ; i++ ) {
        if( arr[i] > max ) {
            max = arr[i];
        }
    }
    return max  //  返回求出的最大值
}

findMax( arr1 ) //  900
findMax( arr2 ) //  78
findMax( arr3 ) //  67

嗯 ~ 当写成这种函数的形式时就会发现方便多了,我们可以复用这个方法求不同数组中的最大值了,于是乎我将这个函数放进公司的公共组件库中,但是这个方法是否还有可以优化的地方呢 ? 这是一个很值得思考的问题 。

问题如下:
  1. 在 for 循环中每次都需要求 arr.length 的值
  2. 如果传进来的参数 arr 是个空数组,应该返回什么?
  3. 如果传进来的参数 arr 不是一个数组,是一个字符串或者是一个对象,应该如何处理,是否需要对此进行判断
  4. 如果传进来的参数 arr 是一个数组,但是里面的元素不都能转换为数字,例如有对象,含有字母的字符串,又该如何处理

大家都知道,javascript 是一个单线程的语言,程序中只要有一个地方出错,整个程序可能都运行不了了,所以在写程序的时候需要考虑多一些情况,特别是在编写公共组件的时候。

我们可以参考 Math.max 的返回值规则来对以上问题进行处理,我们对这个函数做一下规定:

  1. 这个函数的功能是是求一个==数组==的最大值,当传入的参数不是数组时,例如:对象、数字、字符串等,应当返回 NaN
  2. 当传进来的数组 arr 是个空数组,返回 - Infinity
  3. 当传进来的数组 arr 中含有不能转换为数字的元素时,返回 NaN
  4. 当传进来的数组 arr 中都是可转换成数组的元素时,返回求出的最大值且类型为数字

==注意==:该规定是我自己定下来的,并不适合于所有的业务需求,大家需根据公司的业务来制定相应的规则,例如:当传进来的数组 arr 中含有不能转换为数字的元素时,跳过该元素,继续求最大值,等等

优化后的代码如下:
function findArrayMax ( arr ) {
    // 首先判断参数 arr 是不是一个数组
    if ( Object.prototype.toString.call( arr ) !== '[object Array]' ) {
        // 如果不是数组则返回 NaN
        return NaN
    }
    
    // 将变量 max 的初始值设置为 - Infinity,并将数组的长度用变量保存起来,避免循环时重复求值
    var max = - Infinity, len = arr.length;
        
    // 使用 for 循环遍历数组中的元素
    for( var i = 0; i < len; i++ ) {
        // 如果该元素为 NaN 则该函数直接返回 NaN
        if ( isNaN( arr[i]) ) {
            return NaN
        } else if( arr[i] > max ) {
            max = arr[i];
        }
    }
        
    // 遍历完成后,将返回最大值 max 
    return Number(max)
}

findArrayMax([10, 900, -6, 0 , 46, 89, 23]) // 正常情况 900
findArrayMax([]) // 空数组 - Infinity 
findArrayMax({}) // 不是数组 NaN

看着优化完的代码感觉好像没有规则或逻辑上的问题了,那相对于代码来讲还没有可以优化的地方呢? 在此我提出几个疑问:

  1. 既然规定是数组才可以使用这个函数,那是不是可以在数组 Array 构造函数的原型上添加一个求数组中最大值的方法 max ,这样就可免去判断传进来的参数是否为数组类型的步骤
  2. for 循环中的判断元素是否能强制转换成数字和大小的比较是否太过于繁琐,有没有更好的方法去判断(暂时没想到)
代码继续优化
// 如果 Array 的原型上没有 max 方法的话则添加一个
if( !Array.prototype.max ){  
    Array.prototype.max = function() { 
        var max = - Infinity, len = this.length;
        for( var i = 0; i < len; i++ ) {
            max =  isNaN(this[i]) ? NaN :  this[i] > max ? this[i] : max
        }
        return +max // 隐式转换成数字
    }
}

既然是针对于数组的原型上扩展的方法,那我们是不是可以利用一些数组的内置方法再来优化一下呢?

例如数组的内置方法 forEach ,可以省去求数组的长度,并利用 Math.max 进行两个数最大值的比较

if( !Array.prototype.max ){  
    Array.prototype.max = function() { 
        var max = - Infinity;
        this.forEach(function(val){
            max = Math.max(max, val)
        })
        return max
    }
}

还有没有数组的内置函数可以利用呢? 大家还记得 reduce 方法么? 不记得点这里

if( !Array.prototype.max ){  
    Array.prototype.max = function() { 
        return this.reduce(function(a, b){
            return Math.max(a, b)
        }, - Infinity)
    }
}

// 不使用 Math.max 版
if( !Array.prototype.max ){  
    Array.prototype.max = function() { 
        return this.reduce(function(a, b){
            return a > b ? a : b
        }, - Infinity)
    }
}

还有其他很多很多种方法,例如 对数组进行一次排序,然后取第一个元素或最后一个元素也是可以的,在这里就不再贴代码了,相信大家可以自己写一个出来。

到这里我们用的都是数组有关的知识点,而且比较大小也是两个两个变量进行比较和赋值,有没有更好的办法是不需要两个两个比较的呢,明明 Math.max 方法是可以传一组数字进去的,还有没有 js 内置的方法可以利用的呢?有什么方法是可以把数组拆开的呢?

大家想想平时我们强制性把一个数组转换成字符串会怎么样呢 ? 例如:[1,2].toString() 会是什么结果 ? 答案是 "1,2" 但他是一个字符串,有什么方法是可以把执行字符串 js 代码的呢 ? 没错,就是 eval

利用 eval
Array.prototype.max = function() { 
    return eval("Math.max(" + this + ")");
}

可是这种是有弊端的,例如:数组元素不能转换成数字,[ 1 , 'a' ],这时就会报错说找不到 a 这个变量,那还有没有其他方法呢?

给大家一个提示,fun.apply( ) 和 fun.call( ) 这两个方法是有什么区别呢?

call( ) 方法的作用和 apply( ) 方法类似,只有一个区别,就是 call( ) 方法接受的是若干个参数的列表,而 apply( ) 方法接受的是一个包含多个参数的数组。所以我们可以利用 apply 方法来实现。对 apply 方法不熟悉的可以点这里

利用 apply
Array.prototype.max = function() { 
    return Math.max.apply( null, this );
}

在 ES6中也新增的相关运算符,对 ES6 中的扩展运算符不熟悉的可点这里

利用 ES6 中的扩展运算符
Array.prototype.max = function() { 
    return Math.max(...this)
}

[10, 1, 5].max()    // 10

求数组的最大值就大概这样,求最小值的思路相似,只要把 Math.max 改成 Math.min 即可,在这里就不重复阐述了。

性能问题

工作久了,除了考虑实现方式,当然也要考虑下相关的性能问题: 当数据量少的时候,可能不需要考虑这些问题,只要能实现就好,但是如果数据量是有成千上万或者更多的时候,就需要考虑性能问题了,例如在做数据可视化的时候需要标记出最大值和最小值,当然里面也是涉及到一些算法和复杂度的问题,暂时不在这里展开。我们可以借助一些工具来帮助我们进行分析:

前提:数组中有 10000 个元素,找出最大值

结果完整链接1

结果完整链接2

根据结果发现,自己使用循环实现的方式是最快的,使用 Array.reduce 和 Math.max 的速度是最慢的,想了下,应该是和这些内置函数的实现方式原理等有关,后续继续研究。

这里给我的一点小启发是:

  1. 简单通用的方法可以自己实现就自己实现,不要过分依赖第三方库。
  2. 在设计通用的方法或组件时,多参考别人设计方式和业界中较好的方案,然后设计出适合自己业务或团队的通用方法和组件。
  3. 设计的同时也要考虑性能、算法、复杂度等问题。