资讯专栏INFORMATION COLUMN

Underscore 源码(三)随机洗牌算法

silencezwm / 1074人阅读

摘要:随机洗牌算法说实话,以前理解数组的排序,都是将数组按照一定的逻辑由大到小或者由小到大排序,我自己是没有碰到过随机打乱数组排序的问题。然后里用的是所谓的洗牌算法,很高效。总结又是三个知识点,分别是随机洗牌分组和函数的实现,没什么复杂的。

这是第三篇关于 Underscore 的源码解读,最近一段时间学的东西很少,自己太忙了,一方面忙着找实习,晚上回去还要写毕业论文。毕业论文真的很忧伤,因为是两年半,九月份就要交一个初稿,一般都是暑假写,如果暑假出去实习,是没时间点,所以要现在写一个版本出来。

随机洗牌算法

说实话,以前理解数组的排序,都是将数组按照一定的逻辑(由大到小或者由小到大)排序,我自己是没有碰到过随机打乱数组排序的问题。今天看到这个问题,先是一愣,为什么要把一个数组给随机乱序,仔细想想这种应用场合还是很多的,比如随机地图、随机洗牌等等,都是随机乱序的应用。

如果这是一个面试题,将一个数组随机乱序,我的解决办法如下:

function randomArr( arr ){
  var ret = [],
    rand,
    len,
    newA = arr.slice(); // 为了不破坏原数组
  while(len = newA.length){
    rand = Math.floor(Math.random() * len);
    ret.push(newA.splice(rand, 1)[0]);
  }
  return ret;
}

这是一个解决办法,但是却不是一个高效的解决办法,首先,空间复杂度来讲,新建了两个数组(若不考虑对原数组的改变,可以只用一个返回数组),如果能在原数组上直接操作,那真的是太好了,其次时间复杂度来讲,splice 函数要对数组进行改变,复杂度可以算作 n,所以总的时间复杂度应该为 O(n^2)

然后 _ 里用的是所谓的洗牌算法,很高效。洗牌算法的思路有个很大的不同,用交换数组元素的方式替换原来的 splice,因为 splice 太坑了,然后对上面的代码进行改进:

function randomArr2( arr ){
  var rand, temp;
  for(var i = arr.length - 1; i >= 0; i--){
    rand = Math.floor(Math.random() * ( i + 1 ));

    // 交互 i 和 rand 位置的元素
    temp = arr[i];
    arr[i] = arr[rand];
    arr[rand] = temp;
  }
  return arr;
}

改进后看起来就好多了,也比较好理解,还有一个循环的方式是从 0 到 n,randmo 函数没次取值到范围为 i~n,方法也大体是相同的。然而在 _ 中的 shuffle 方法的循环却是从左到右,看了半天才明白,代码如下:

  // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
  _.shuffle = function(obj) {
    var set = isArrayLike(obj) ? obj : _.values(obj);
    var length = set.length;
    var shuffled = Array(length); // 新建一个 kength 长度的空数组
    for (var index = 0, rand; index < length; index++) {
      rand = _.random(0, index); // 获取 0~index 之间的随机数

      // shuffled[index] 是 undefined 的,先将 index 处的值替换成 rand 处的值
      // 再将 rand 处的值替换成 set 集合处的 index
      if (rand !== index) shuffled[index] = shuffled[rand];
      shuffled[rand] = set[index];
    }
    return shuffled;
  };

_.shuffle 使用的是从左到右的思路,以至于我都无法判断出这是不是随机数组,向右前进一位,找到 0 ~ index 之间的一个随机数,插入新元素,如果还按照之前的解题思路,在原数组的基础上进行修改,则可以得到:

function randomArr3(arr){
  var rand, temp;
  for(var i = 0; i < arr.length; i++){
    // 这里的 random 方法和 _.random 略有不同
    rand = Math.floor(Math.random() * ( i + 1 ));
    if(rand !== i){
      temp = arr[i];
      arr[i] = arr[rand];
      arr[rand] = temp;
    }
  }
  return arr;
}

_.random 方法是一个非常实用的方法,而我在构造 randomArr3 函数的时候,是想得到一个 0 ~ i 之间的一个随机数,来看看 _.random 是如何方便的实现的:

_.random = function(min, max) {
  if (max == null) {
    max = min;
    min = 0;
  }
  // 和我最终的思路是一样的
  return min + Math.floor(Math.random() * (max - min + 1));
};
group 原理

有时候需要对从服务器获得的 ajax 数据进行统计、分组,这个时候就用到了 _ 中的 group 函数。

与 group 相关的函数有三个,它们分别是 groupByindexBycountBy,具体使用可以参考下面的代码:

// groupBy 用来对数据进行分组
_.groupBy([3, 4, 5, 1, 8], function(v){ return v % 2 });
// {0:[4,8],1:[3,5,1]}

// 还可以用数据的属性来区分
_.groupBy(["red", "yellow", "black"], "length");
// {3:["red"],5:["black"],6:["yellow"]}

// 从某一个属性入手,相同的会替换
_.indexBy([{id: 1,name: "first"}, {id: 2, name: "second"}, {id: 1, name: "1st"}], "id");
// {1:{id:1,name:"1st"},2:{id:2,name:"second"}}

// 用来统计数量
_.countBy([3, 4, 5, 1, 8], function(v){ return v % 2 == 0 ? "even" : "odd" });
// {odd: 3, even: 2}

这三个函数的实现都非常类似,在 _ 中有进行优化,即常见的函数闭包:

var group = function(behavior) {
  return function(obj, iteratee, context) { // 函数闭包
    var result = {}; // 要返回的对象
    iteratee = cb(iteratee, context);
    _.each(obj, function(value, index) {
      var key = iteratee(value, index, obj);
      // 针对 group、index、count,有不同的 behavior 函数
      behavior(result, value, key);
    });
    return result;
  };
};

_.groupBy = group(function(result, value, key) {
  // 每个属性都是一个数组
  if (_.has(result, key)) result[key].push(value); else result[key] = [value];
});

_.indexBy = group(function(result, value, key) {
  // 对于相同的前者被后者替换
  result[key] = value;
});

_.countBy = group(function(result, value, key) {
  // 每个属性是数量
  if (_.has(result, key)) result[key]++; else result[key] = 1;
});

对于 groupBycountBy 处理模式几乎是一摸一样的,只是一个返回数组,一个返回统计值而已。

关于统计,这三个函数用的还真的不是很多,循环很好构造,处理函数也很好构造,如果数据是数组,直接 forEach 循环一遍添加一个处理函数就 ok 了,不过 _ 最大的优点就是省事吧,这种重复利用函数、函数闭包的思路,是值的借鉴的。

bind 函数

call、apply、bind 这三个函数也算是 js 函数中三剑客,经常能看到面试题,让实现 bind 函数,我就有一个疑问,难道支持 apply 的浏览器,不支持 bind 函数:

实现 bind 函数:

Function.prototype.bind = Function.prototype.bind || 
  function(context){
    var slice = Array.prototype.slice
    var args = slice.call(arguments, 1),
      self = this;
    return function(){
      self.apply(context, args.concat(slice.call(arguments)));
    }
  }

有时候也会看到有人这样写:

Function.prototype.bind = Function.prototype.bind || 
  function(context){
    var self = this;
    return function(){
      self.apply(context, arguments);
    }
  }

下面这种写法显然是低级新手玩家的手准,因为对于 bind 函数,有个很大的优点就是提前预定参数,如果懂了这个,就不会犯这个错误。

来看看 _ 里高级玩家的写法:

_.bind = function(func, context) {
  // 原生 bind 还是好,此函数总结
  if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
  // 报个错
  if (!_.isFunction(func)) throw new TypeError("Bind must be called on a function");
  var args = slice.call(arguments, 2); // 先存变量
  var bound = function() {
    return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
  };
  return bound;
};

var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
  // 还是用 apply 来回调,args 已经是拼接好的
  if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
  // 后面好想是针对 constructor 的,表示看不懂
  var self = baseCreate(sourceFunc.prototype);
  var result = sourceFunc.apply(self, args);
  if (_.isObject(result)) return result;
  return self;
};

_ 里类似 bind 的函数还有 _.partial_.bindAll,只是使用的时候大同小异,就不多做介绍了,总之记住一句话,闭包无敌。

总结

又是三个知识点,分别是随机洗牌、分组和 bind 函数的实现,没什么复杂的。

说到闭包,其实面试的时候问得最多的就是这个问题了,有啥优点,有啥缺点,如果是现场面,我直接手写一串代码,对面试官说:看,这就是闭包:

function add(m){
  var fn = function(n){
    return add( m + n );
  }
  fn.toString = function(){
    return m;
  }
  return fn;
}

add(2)(3)(4).toString(); //9

好像不对,这不是闭包,是柯里化。

参考

Fisher–Yates shuffle
JavaScript 数组乱序
Underscore.js (1.8.3) 中文文档
浅谈 underscore 内部方法 group 的设计原理

欢迎来我的博客交流。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83108.html

相关文章

  • 也谈前端面试常见问题之『数组乱序』

    摘要:看完部分的源码,首先迫不及待想跟大家分享的正是本文主题数组乱序。这是一道经典的前端面试题,给你一个数组,将其打乱,返回新的数组,即为数组乱序,也称为洗牌问题。关于数组乱序,正确的解法应该是,复杂度。 前言 终于可以开始 Collection Functions 部分了。 可能有的童鞋是第一次看楼主的系列文章,这里再做下简单的介绍。楼主在阅读 underscore.js 源码的时候,学到...

    tracy 评论0 收藏0
  • Underscore源码解析(

    摘要:本文同步自我得博客前两天在微博上看到的微博推荐了我的前两篇文章,有点意外和惊喜。没看过前两篇博客的朋友可以戳这里源码解析一源码解析二上一篇文章介绍了的个函数的具体实现细节,今天将继续介绍其他的函数。 本文同步自我得博客:http://www.joeray61.com 前两天在微博上看到SF的微博推荐了我的前两篇文章,有点意外和惊喜。作为一个菜鸟,真的是倍受鼓舞,我写博客的动力也更充足了...

    Prasanta 评论0 收藏0
  • 随机问题之洗牌算法

    摘要:百度文库洗牌算法提到一种换牌思路随机交换两个位置,共交换次,越大,越接近随机。洗牌插牌法优化版,可以用数学归纳法证明,这种洗牌是均匀的。每次生成一张最大的牌,与随机的某张牌换位子抽牌抽牌优化换牌插牌插牌优化文章转载自随机问题之洗牌算法 洗牌算法是我们常见的随机问题,在玩游戏、随机排序时经常会碰到。它可以抽象成这样一个问题。 得到一个M以内的所有自然数的随机顺序数组。 在百度搜洗牌算法,...

    instein 评论0 收藏0
  • 数组随机排序:洗牌算法(Fisher–Yates shuffle)

    摘要:代码实现代码一测试用例输出其中,代码二测试用例输出其中,参考资料洗牌算法学习笔记数组随机排序洗牌算法给数组随机排序洗牌算法原理 原理及步骤 1.定义一个数组(shuffled),长度(length)是原数组(arr)长度2.取 0 到 index (初始0) 随机值 rand, shuffled[index] = shuffled[rand], shuffled[rand] = arr...

    张金宝 评论0 收藏0
  • 洗牌算法

    摘要:描述拷贝数组从扫描数组,选择一个随机数新数组的新数组的新数组的原始数组重复第步,直到末尾最终的新数组就是随机的参考三种洗牌算法 洗牌算法 Fisher-Yates Shuffle Fisher–Yates 随机置乱算法,通俗说就是生成一个有限集合的随机排列。 描述: 写下从 1 到 N 的数字 取一个从 1 到剩下的数字(包括这个数字)的随机数 k 从低位开始,得到第 k 个还没有被...

    omgdog 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<