“猜手机号游戏”的源码分析:二分查找+面向对象
很多朋友体验了“给哥三十五次机会,哥就能猜中你的手机号”,反馈还不错。有些明眼的朋友,一眼就看出来所用的算法。D瓜哥表示很佩服。另外有一些朋友也问所使用的算法,D瓜哥今天就把源代码和算法全部揭晓。
其实,这个代码很简单。不过也有三个看点:二分查找算法、面向对象编程和对数的计算。下面我们一一讲解。
二分查找算法
基本原理是二分查找算法。首先,我们先简要介绍一下“二分查找算法”。
二分查找又称折半查找,它充分利用了元素间的次序关系,采用分治策略,可在最坏的情况下时间复杂度是O(log2n)。由此看出,二分查找的优点很明显:比较次数少,查找速度快,,平均性能好;不过,也有一些缺点。其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。
二分查找运行时,会首先,假设列表中元素是按升序排列,将列表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将列表分成前、后两个子列表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子列表,否则进一步查找后一子列表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子列表不存在为止,此时查找不成功。
基本原理是这样。不过,我在处理的时候没有列表,直接拿(min+max)/2来显示“猜测结果”。说到这里了,再多说一句。其实是用(min+max)/2的计算有Bug,可能会产生溢出问题。更好的计算方法是:min+(max-min)/2;或是(min+max)>>>1。
面向对象编程
在Javascript中进行“面向对象编程”一直是我提倡的实践。这样,能良好的封装代码,提高代码的可重用性、可读性以及可维护性。以前的文章里面,也多次提到要整几篇文章,讲解一下“面向对象编程”。只是因为各种原因,一直未能兑现。抱歉,这篇也只能讲解个大概。不过,我保证,这方面的内容,一定会写出来的。
扯淡为目的的文章不是好文章。转入正题。因为,我这篇文章只要是分析“猜手机号游戏”的源代码,所以就直接拿源代码讲解面向对象了。
在对象封装方面,我使用了“构造函数模式”。具体代码如下:
/**
* numberScope 需要猜测的数字范围
*/
function GuessNumber(numberScope){
this._maxNumber = -1; // 猜测范围中,最大的数
this._minNumber = 0; // 猜测范围中,最小的数
this._nowNumber = -1; // 目前猜测出来的数
this._count = 0; // 需要猜测的次数
this._times = 0; // 猜测的次数
this._result = 0; // 猜测结果
this._complete = false; // 猜测是否完成
this._success = false; // 猜测是否成功。
this.setNumberScope(numberScope); //设置猜数范围,这里默认从0开始.
}
这样的话,就可以使用new 来创建对象,同时一些必要的值,也可以在new的时候,通过构造函数的参数传进去。创建对象的代码如下:
var guess = new GuessNumber(100);
大家也许注意到了,上面构造函数中的有一行方法调用的代码,如下:
this.setNumberScope(numberScope); //设置猜数范围,这里默认从0开始.
也许大家会奇怪,这个方法是从哪里来的呢?
在对象上添加方法,我使用了“原型模式”。JavaScript中的原型prototype,最大的特点就是“共享”。由于,函数只需要“一套”,所有对象都可以共用一套函数。所以,这种共享特性非常适合用来定义函数。代码如下:
GuessNumber.prototype = {
constructor: GuessNumber,
//设置数字范围
setNumberScope: function(numberScope){
if(numberScope < 0){
alert("数字范围不能小于0!");
return;
}
this._numberScope = numberScope;
this.start(this._minNumber, numberScope);
},
//开始游戏,初始化一些必要的数据。主要是为方便重新设定最小值。所以将最小值作为第一个参数
start: function(minNumber, maxNumber) {
var inMinNum = this._minNumber;
var inMaxNum = this._numberScope;
this._minNumber = -1;
this._maxNumber = 1;
// 当minNumber为0时,需要特殊处理一下。
var minNumberStr = minNumber + "";
if (minNumberStr != "") {
inMinNum = minNumber;
}
if (maxNumber) {
inMaxNum = maxNumber;
this._numberScope = maxNumber;
}
this._complete = false;
this._success = false;
this._times = 0;
this.changeFields(inMaxNum, inMinNum);
this.setCount();
},
// 计算所需猜测的次数
setCount: function() {
this._count = Math.ceil(Math.log(this._maxNumber - this._minNumber) / Math.log(2)) + 1;
},
//猜测的数字太小,调大数字
toLarger: function(){
this.changeFields(this._maxNumber, this._nowNumber);
},
//猜测的数字太大,调小数字
toSmaller: function(){
this.changeFields(this._nowNumber, this._minNumber);
},
//计算数字大小,修改相应值
changeFields: function(maxNumber, minNumber){
if(maxNumber <= minNumber || maxNumber == this._minNumber + 1) {
if(!this._complete) {
this.complete();
}
return;
}
if(this._maxNumber !== maxNumber) {
this._maxNumber = maxNumber;
}
if(this._minNumber !== minNumber) {
this._minNumber = minNumber;
}
this._nowNumber = Math.round((this._maxNumber + this._minNumber) / 2);
this._times ++;
},
……
}
注意,这个的第二行。使用这种语法是非常方便。但是,却从本质上改变了GuessNumber.prototype。致使constructor不再指向GuessNumber函数。这样做是特意将其设置成GuessNumber,以使其能访问到合适的值。
这样,最主要的代码都已经完成。剩下的工作就是调用相应的方法就行。更加详细、完整的代码将在下面展示。
对数的计算
这里讲解一些数学上的知识。不喜欢的,可以忽略过去。
大家也许好奇,为啥我能准确估算出“猜测次数”。其实,我们可以反过来考虑,比我们需要找一个比n(n为正整数)大的数。我们可以从1开始向后数,数到n,再到n+1就可以了。但是这种效率有点低,我们都学过乘方,我们可以从1开始,反复乘以2,假如需要乘以x个2,即2x >= n,这样效率就会很高。再反过来,负负得正,在一个1到2x范围内,我们每次砍去一半,最多砍到第x次,就肯定能找出这个数过来。那么,这个x怎么求呢?
其实,这就要用到数学的对数知识。用数学公式来求解的话,就是x = log2n。在Javascript中,数学相关的计算方法都在Math中。查一下Math的API,我们就会发现只有一个Math.log(n)方法,而且这个方法求的是以10为底n的对数。我们需要求的是以2为底n的对数,不符合我们的需求。肿么办?这时,数学知识再次向我们展示了她的无穷魅力。
可能我们在高中学过对数,了解对数的变换公式。其中一个就能帮助我们:logab = logmb / logma。所以,我们的问题就迎刃而解:x = log10n / log102。在数学中,log10n可以简写成logn,所以 x = log10n / log102 = logn / log2。不过,这样计算出来的结果还会有小数点,这是我们只需要来个“向上取整”就可以了。具体代码如下:
// 计算所需猜测的次数
setCount: function() {
this._count = Math.ceil(Math.log(this._maxNumber - this._minNumber) / Math.log(2)) + 1; //加1,确保次数肯定用。
},
完整代码实现
下面把完整代码贴出来,方便大家阅读。
/**
* numberScope 需要猜测的数字范围
*/
function GuessNumber(numberScope){
this._maxNumber = -1; // 猜测范围中,最大的数
this._minNumber = 0; // 猜测范围中,最小的数
this._nowNumber = -1; // 目前猜测出来的数
this._count = 0; // 需要猜测的次数
this._times = 0; // 猜测的次数
this._result = 0; // 猜测结果
this._complete = false; // 猜测是否完成
this._success = false; // 猜测是否成功。
this.setNumberScope(numberScope); //设置猜数范围,这里默认从0开始.
}
GuessNumber.prototype = {
constructor: GuessNumber,
//设置数字范围
setNumberScope: function(numberScope){
if(numberScope < 0){
alert("数字范围不能小于0!");
return;
}
this._numberScope = numberScope;
this.start(this._minNumber, numberScope);
},
//根据数字长度,设定数字访问
setNumberScopeWithNumberLength: function(numLength){
this.setNumberScope(Math.pow(10, numLength));
},
// 设置最小的数
setMinNumber: function(minNumber) {
this.start(minNumber);
},
//开始游戏,初始化一些必要的数据。主要是为方便重新设定最小值。所以将最小值作为第一个参数
start: function(minNumber, maxNumber) {
var inMinNum = this._minNumber;
var inMaxNum = this._numberScope;
this._minNumber = -1;
this._maxNumber = 1;
// 当minNumber为0时,需要特殊处理一下。
var minNumberStr = minNumber + "";
if (minNumberStr != "") {
inMinNum = minNumber;
}
if (maxNumber) {
inMaxNum = maxNumber;
this._numberScope = maxNumber;
}
this._complete = false;
this._success = false;
this._times = 0;
this.changeFields(inMaxNum, inMinNum);
this.setCount();
},
// 计算所需猜测的次数
setCount: function() {
this._count = Math.ceil(Math.log(this._maxNumber - this._minNumber) / Math.log(2)) + 1;
},
// 获取总的猜测次数
getCount: function() {
if (this._count === 0) {
this.setCount();
}
return this._count;
},
//猜测的数字太小,调大数字
toLarger: function(){
this.changeFields(this._maxNumber, this._nowNumber);
},
//猜测的数字太大,调小数字
toSmaller: function(){
this.changeFields(this._nowNumber, this._minNumber);
},
//计算数字大小,修改相应值
changeFields: function(maxNumber, minNumber){
if(maxNumber <= minNumber || maxNumber == this._minNumber + 1) {
if(!this._complete) {
this.complete();
}
return;
}
if(this._maxNumber !== maxNumber) {
this._maxNumber = maxNumber;
}
if(this._minNumber !== minNumber) {
this._minNumber = minNumber;
}
this._nowNumber = Math.round((this._maxNumber + this._minNumber) / 2);
this._times ++;
},
//提前完成猜数
complete: function() {
this._complete = true;
if (this._maxNumber === this._minNumber) {
this._success = true;
this._result = this._maxNumber;
} else if (this._maxNumber < this._minNumber) {
this._success = false;
this._result = "Sorry! I failed.";
} else {
this._success = true;
this._result = this._nowNumber = Math.round((this._maxNumber + this._minNumber) / 2);
if (this._nowNumber == this._minNumber + 1) {
this._nowNumber = this._minNumber;
}
}
},
//获取结果
getResult: function() {
if(!this._success) {
return "Sorrry! I failed.";
}
return this._result;
},
// 获取猜测的数
getGuess: function() {
if(this._nowNumber == this._minNumber) {
return Math.round((this._maxNumber + this._minNumber) / 2);
}
return this._nowNumber;
},
//获取猜测次数
getTimes: function() {
if(this._times === 0) {
this.start();
}
return this._times;
}
}
剩下的,就是创建对象,事件绑定。另外,GuessNumber主程序输出的是数字,还要根据实际情况进行格式化。这个不多说,具体代码如下:
$(document).ready(function(){
//格式化显示结果
function formatResult(num, type) {
if (type == 1) {
var tmp = new Date(num * 1000 * 3600 * 24);
return tmp.getFullYear()+"年"+(tmp.getMonth()+1)+"月"+tmp.getDate()+"日";
} else {
var numStr = num + "";
var strLeng = numStr.length;
var result = "";
for(var i = strLeng; i >= 0; i -= 4) {
var startIndex = i-4 > 0 ? i-4 : 0;
result = numStr.substring(startIndex, i) + result;
if(i > 4) {
result = "-" + result;
}
}
return result;
}
}
function showResult() {
if(guess._complete){
// 为了简化处理结果,把结果展示暂时写到这里。
if(guess._success) {
alert("您猜测的结果是:" + formatResult(guess.getGuess(), type));
} else {
alert("猜测失败。要不,咱试试其他的?");
}
return;
}
document.getElementById("showNumber").value = formatResult(guess.getGuess(), type);
document.getElementById("showTimes").innerHTML = guess.getTimes();
document.getElementById("showCount").innerHTML = guess.getCount();
}
var guess = new GuessNumber(100);
// 定义个猜数范围数组
var scopeArr = [{min: 0, max: 100, tip: ""}, // 小试牛刀
{min: Math.round(new Date(0).getTime() / (1000*3600*24)),
max: Math.round(new Date().getTime() / (1000*3600*24)),
tip: ""}, // 猜猜生日
{min:Math.pow(10, 10), max: 2*Math.pow(10, 10),
tip: ""} // 猜猜手机号
];
var type = 0; //1 代表日期;其余代表数字
guess.start(scopeArr[type].min, scopeArr[type].max);
showResult();
$("#guessType").change(function(){
type = $(this).val();
guess.start(scopeArr[type].min, scopeArr[type].max);
showResult();
});
//alert("Scope:"+guess._numberScope+",Max:"+guess._maxNumber+",Min:"+guess._minNumber+",Now:"+guess._nowNumber+",Count:"+guess._count);
$("#maxButton").click(function(){
guess.toSmaller();
showResult();
});
$("#minButton").click(function(){
guess.toLarger();
showResult();
});
$("#initButton").click(function(){
guess.start(scopeArr[type].min, scopeArr[type].max);
showResult();
});
});
原来不想贴HTML的代码了,不过想想,既然是完整的,就都贴出来吧。
<div style="background: #E6FAE6; padding-left: 15px; border: 1px dashed #E0E0E0;">
<p>开始猜:最多需要<span id="showCount"></span>次</p>
<p> 选择一个类型:
<select id="guessType">
<option value ="0">小试牛刀-猜一百以内的正整数</option>
<option value ="1">猜猜你的生日</option>
<option value="2">信不信,我能猜出你的手机号</option>
</select></p>
<p> <input type="text" value="" size="24" maxlength="50" readonly="true" id="showNumber" style="font-size:150%;"></p>
<p> <input type="button" value="出错就重来" id="initButton"> <input name="tooMax" type="button" value="太大" id="maxButton" style="font-size:140%;"> 第<span id="showTimes"></span>次 <input name="tooMin" type="button" value="太小" id="minButton" style="font-size:140%;"></p>
</div>
所有代码都贴出来了,从这些代码中也可以看出来,猜测的时候没有和后台进行通信。所以,大家可以放心大胆的试。
稍后,我会把代码发布到Github上,请大家关注我的Github账号:D瓜哥。
参考资料:
原文链接:https://wordpress.diguage.com/archives/80.html
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

特别感谢扣扣网友“谢安”!
在原文中,我错把logab = logmb / logma写成了 logab = logma / logmb。感谢“谢安”的指正!有问题,请留言,D瓜哥会及时改正的。
Math.log(n),这个方法是以Math.E为底数,不是10为底数;
Math.log(Math.E); // 1
Math.log(10); //2.30+
学习了
搜面向对象 搜过来的
纠结了好久 格式化日期下面的 else 是在干嘛
原来是手机号相关的
写的好巧 都塞到了一起