2017 校招面试笔试记录
2017 年校招面试笔试记录,面试方向是 Android
# 1.音视频不同步的原因:
# 一般是客户端的问题。
由于音视频包都会带上时间戳,出现不同步的根本原因是客户端取出当前数据包解码放入缓冲队列,进行渲染时***(在这做同步)不能从缓冲队列中同时找到当前时间戳*的视频解码数据和音频解码数据,所以只能先取其中一个拿个渲染。
(注:拿去渲染后,如果时间戳同步,渲染一般不会出现不同步的情况,硬件处理相对成熟);
渲染不同步,可以说是缓冲队列设置太小,没有考虑网络不好数据包传输慢或者需要丢包重传的情况。 当然还有可能是解码的问题,音视频其中之一的解码效率太慢,而缓冲队列设置合适,稍微不好一点的网络情况导致解码数据包(音频or视频)来不及放入缓冲队列。
# 如果是服务端的问题,比较不好处理。
音视频线程数据采集速度不同步(和采集卡有关),封装协议时就已经出现同一时间戳的音视频包实际是不同步的情况。
# 2. synchronized 实例方法与静态方法的区别
先上结论:
1.同一个实例,所有非静态同步方法会加锁同步,锁的对象是类实例;即先等待其中一个实例方法运行结束才能运行下一个实例方法(不管实例方法是否是同一个)
2.同步实例方法的锁对象是类实例,所以不同类实例之间可以并行的运行实例方法
3.同步静态方法的锁对象是Class类本身,所以静态方法间的运行是串行的
4.类的静态方法和类实例的实例方法同时运行,由于锁对象不同,运行是并行的
public class TestSynchronized {
public synchronized static void synstaMethod1(){
System.out.println("i am synstaMethod1 start");
timeTask();
System.out.println("i am synstaMethod1 end");
}
public synchronized static void synstaMethod2() {
System.out.println("i am synstaMethod2 start");
timeTask();
System.out.println("i am synstaMethod2 end");
}
public synchronized void synMethod1() {
System.out.println("i am synMethod1 start");
timeTask();
System.out.println("i am synMethod1 end");
}
public void synMethod2() {
synchronized (this) {
System.out.println("i am synMethod2 start");
timeTask();
System.out.println("i am synMethod2 end");
}
}
public static void timeTask() {
double i = 0;
while (true) {
i += 1;
if (i > 1000000000)
break;
}
}
public static void timeTask2() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//下面这方法不好测试 会另开线程
public static void timeTask3(String methodName) {
Timer timer = new Timer();
// 3s后运行该任务
timer.schedule(new TimerTask() {
public void run() {
System.out.printf("i am %s end\n",methodName);
timer.cancel();
}
}, 1000);
}
}
测试方法:
package com.france;
//话说JUnit做多线程测试有诸多不便..还是用Main来测试把..
public class Main {
static TestSynchronized obj1 = new TestSynchronized();
static TestSynchronized obj2 = new TestSynchronized();
// 测试:同一个类实例的实例方法
// 结论:同一个实例,所有非静态同步方法会加锁同步,锁的对象是类实例;即先等待其中一个实例方法运行结束才能运行下一个实例方法(不管实例方法是否是同一个)
// output:
// i am synMethod1 start
// i am synMethod1 end
// i am synMethod2 start
// i am synMethod2 end
// i am synMethod2 start
// i am synMethod2 end
@org.junit.Test
public static void testSameObjectUnstatic() {
// 先运行完其中的一个实例方法完再运行下一个实例方法
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
obj1.synMethod1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
obj1.synMethod2();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
obj1.synMethod2();
}
}).start();
}
// 测试:不同类实例的实例方法
// 结论:同步实例方法的锁对象是类实例,所以不同类实例之间可以并行的运行实例方法
// output:
// i am synMethod1 start
// i am synMethod2 start
// i am synMethod2 end
// i am synMethod1 end
@org.junit.Test
public static void testDifferentObjectUnstatic() {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
obj2.synMethod1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
obj1.synMethod2();
}
}).start();
}
// 测试:类的静态方法
// 结论:同步静态方法的锁对象是Class类本身,所以静态方法间的运行是串行的
// 注意与该方法的调用者(Class或者类实例)无关
// output:
// i am synstaMethod1 start
// i am synstaMethod1 end
// i am synstaMethod2 start
// i am synstaMethod2 end
@org.junit.Test
public static void testClassStatic() {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
TestSynchronized.synstaMethod1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
TestSynchronized.synstaMethod2();
}
}).start();
}
// 测试:类的静态方法和类实例的实例方法
// 结论:类的静态方法和类实例的实例方法同时运行,由于锁对象不同,运行是并行的
// 注意与该方法的调用者(Class或者类实例)无关
// output:
// i am synstaMethod1 start
// i am synMethod2 start
// i am synstaMethod1 end
// i am synMethod2 end
@org.junit.Test
public static void testClassAndObject() {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
TestSynchronized.synstaMethod1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
obj1.synMethod2();
}
}).start();
}
public static void main(String[] args) {
// TODO Auto-generated method stub
testClassAndObject();
}
}
# 3.Android 事件分发
参考:(荐)http://blog.csdn.net/duo2005duo/article/details/51604119 http://www.jianshu.com/p/6ebdb78f579e
1.一次Touch有UP,MOVE(>=0次),UP/Cancel,事件至上而下传递
# ViewGroup的dispatchTouchEvent方法分析
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
//1.只有在非拦截的情况的下寻找target
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// 防止onInterceptTouchEvent()的时候改变Action
ev.setAction(MotionEvent.ACTION_DOWN);
// 遍历子View,第一个消费这个事件的子View的为Target
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
//当然只遍历可见的,并且没有在进行动画的。
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
//up或者cancel的时候清空DisallowIntercept
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// 如果没有target,则把自己当成View,向自己派发事件
final View target = mMotionTarget;
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
ev.setLocation(xf, yf);
return super.dispatchTouchEvent(ev);
}
// 如果有Target,拦截了,则对Target发送Cancel,并且清空Target
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
//up 或者 cancel清空Target
if (isUpOrCancel) {
mMotionTarget = null;
}
//如果有Target,并且没有拦截,则向Target派发事件,这个事件会转化成Target的坐标系
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
return target.dispatchTouchEvent(ev);
}
2.点击某个View控件,系统计算位置然后找到根ViewGroup;
ACTION_DOWN且ViewGroup非拦截
时向下分发:如果ViewGroup的某个子View消费掉(View的dispatchTouchEvent返回true),则不再分发并且进行向上传递:ViewGroup遍历子View,将第一个消费这个事件的子View置为Target
3.如果ViewGroup没有target,则把自己当成View,向自己派发事件:向下分发时没有子View消费
4.如果ViewGroup有Target,并且拦截了,则对Target发送Cancel,并且清空Target。需要分析onInterceptTouchEvent
方法
5.如果ViewGroup有Target,并且没有拦截,则向Target派发事件。比如DOWN时设置子View为Target 然后现在ACTION_MOVE,则向该子VIEW派发该事件
# View的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
//1.停止嵌套滑动
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
stopNestedScroll();
}
//2.安全监测
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//3.如果当前View使能(setEnabled(true)),则调用Touch监听器
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//4.如果Touch监听器返回false或者没有调用Touch监听器,则返回调用onTouchEvent()
if (!result && onTouchEvent(event)) {
result = true;
}
}
//停止嵌套滑动
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
分析3.
第一个if语句要同时满足li != null 、 li.mOnTouchListener != null 、 (mViewFlags & ENABLED_MASK) == ENABLED 、 li.mOnTouchListener.onTouch(this, event);
第一个一般不会为null,所以走第二个条件,刚刚上面说到mOnTouchListener是我们在setOnTouchListener的时候传递过来的,所以只要设置了TouchListener,这个条件也会成立;
第三个条件是判断控件是否是enable的,一般控件默认都是enable的,除非你明确的设置成disable;
接着判断第四个条件,就是回调mOnTouchListener的onTouch方法,这个方法是我们自己重写的,如果我们返回false,这个条件就不成立,否则我们返回true,该条件就成立,然后执行result=true
。
如果第一个if不满足就会执行onTouchEvent
方法
public boolean onTouchEvent(MotionEvent event) {
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP://抬起事件
//巴拉巴拉巴拉....
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();//去调用onClick方法
}
......
break;
case MotionEvent.ACTION_DOWN://按下事件
//巴拉巴拉巴拉....
break;
case MotionEvent.ACTION_CANCEL://取消事件
//巴拉巴拉巴拉....
break;
case MotionEvent.ACTION_MOVE://移动事件
//巴拉巴拉巴拉....
break;
}//switch语句执行完毕
return true;//如果控件可以点击,也就是说可以进入到这个if语句里面,那么总会返回true
}//if语句结束
return false;//如果控件不可以点击,就进不到上面的if语句里面,那么总会返回false
}
这个条件是判断控件是否可以支持点击事件,如果支持点击事件进入语句体,开始执行switch语句,可以看到switch执行完毕之后总是会返回true
onTouchEvent
方法总结:
1.不管View使能与否,只要clickable或者longclickable,就一定消费事件(返回true)
2.如果View不使能,并且clickable或者longclick,就只会消费事件但不做其他任何操作
3.如果View使能,先看看TouchDelegate消费与否,如果不消费再给自己消费
4.处理包括focus,press,click,longclick
# Demo:
tv.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "onTouch: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "onTouch: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "onTouch: ACTION_UP");
break;
}
return false;
}
});
点击TextView,log如下:
06-04 04:38:46.835 15690-15690/cn.wang.permissiondemo I/ThirdActivity: onTouch: ACTION_DOWN
执行流程(TextView的CLICKABLE=false): 1.ACTION_DOWN:ViewGroup向下分发,找到某个View尝试进行消费 2.该View执行TouchListener.onTouch 处理了ACTION_DOWN,但是返回false,继续运行该View的onTouchEvent发现CLICKABLE是false然后方法返回false. 3.遍历子View后,此时ViewGroup没有找到子View可以消费事件,将自己当成View进行派发事件(super.dispatchTouchEvent),该dispatchTouchEvent默认返回false,就这样不断向上传递 4.ACTION_MOVE:ViewGroup没有Target,故还是自己当成View进行派发事件,并返回false 总的说,打印了ACTION_DOWN只是一开始View进行尝试消费时运行的..
# 4.内存泄漏检测工具
# 先说下经常出现内存泄漏的几种情况及解决
# 1.Activity的Context传给单例->改为传ApplicationContext
# 2.在某个类(Activity)中创建了一个非静态内部类(默认会持有外部类(Activity)的引用),并且创建了该类的单例(全局静态变量),该变量在某个方法中被实例化;
注:该变量生命周期与应用一致.也就是说,在应用生命周期中,该变量会持有Activity的引用导致不能进行回收
改进->将该非静态内部类该为静态内部类或单独提出封装成一个单例
# 3.通过这样创建的Handler handler=new Handler(){...}非静态匿名内部类持有Activity的引用. 当Activity销毁 而Handler里面有消息未完全处理完时,无法正常回收Activity.
改进->覆盖Handler类被静态实现被弱引用Context,被注意移除消息队列中消息(否则虽避免Activity内存泄漏但是Handler)
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
private TextView mTextView ;
private static class MyHandler extends Handler {
private WeakReference<Context> reference;
public MyHandler(Context context) {
reference = new WeakReference<>(context);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = (MainActivity) reference.get();
if(activity != null){
activity.mTextView.setText("");
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView)findViewById(R.id.textview);
loadData();
}
private void loadData() {
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}
# 4.资源未关闭造成的内存泄漏
BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap,注册的系统监听器等资源的使用,应该在Activity销毁时及时关闭或者注销
使用完Bitmap后先调用recycle(),再赋为null
# 5.线程造成的内存泄漏
将异步任务和Runnable写成非静态匿名内部类,那么他们持有Activity的引用 如果Activity销毁时任务还没完成将导致Activity的资源无法正常回收
正确的做法还是使用静态内部类的方式
# 检测工具
MAT(比较复杂,面试的时候就提到有安装该插件 但是具体没用过) LeakCanary(onCreate里加一句代码,然后出现泄漏会发通知,点通知跳转到具体原因的页面,面试就讲这个) ndk内存泄漏检测工具?Valgrind, 有点复杂
# 5.View绘制过程
- 自上而下,先设置父视图,再循环设置子视图;
- 先通过measure计算视图大小,再通过layout计算视图位置,再调用draw绘制;
- 子视图的measure由父子视图共同决定,layout方法是传入四个位置的坐标,draw时先绘制background,然后绘制当前view的内容,再遍历view; 总过程:measure(viewgroup->childview)->layout(viewgroup->childview)->draw(viewgroup->childview);
# 6.Android的两种广播注册方式
代码中注册,与程序生命周期有关 Manifest.XML中注册,在系统中常驻:始终处于活动状态 耗电
# 7.ThreadLocal 的用法
提供线程内部的局部变量,本线程随时可取,隔离其他线程
# 8.Java的几种引用
软引用对象在内存不足会被回收;
弱引用只要进行GC就会被回收;
虚引用对象任何时候都会被回收(不仅是GC);
强引用对象不会被回收,一般不用的时候需要代码显示将其置null,不然宁愿OOM也不会回收;
另外讲下GC分主GC和次GC,次GC比较平滑,主GC满足一定条件才会调用
System.gc()
:调用该方法,JVM底层建议进行主GC,大部分情况会进行非一定,增加了间接性停顿的次数..
# 9.Android数据库升级
传入的数据库版本比之前的大,会调用SQLiteOpenHelper#onUpgrade()方法,我们在该方法做如下操作:
1.将要修改的表A重命名为A_temp
2.先建表A,并把A_temp的数据传入A
3.删除A_temp
# 10.HandlerThread
另开线程,封装了创建Looper,Handle去处理MessageQueue的操作
# 11.onSaveInstanceState和onRestoreInstanceState
# onSaveInstanceState触发时机:存在非用户主动销毁Activity的情况下会触发,如:按HOME,切换程序,转屏,进ActivityB,关屏幕等,系统在内存不足时会销毁Activity(转屏是一定会销毁如果没设置的话)
# onRestoreInstanceState调用时机:Activity确实被销毁了
# 12.Hashtable HashSet HashMap 细节
HashSet里面维护了一个HashMap map变量,增删改查都是对map对象的key进行操作,map的key即set的value HashMap里面定义了静态内部类Node<K,V>和transient Node<K,V>[] table,K由key和hash(key)确定;非并发操作 Hashtable 维护了private transient Entry<?,?>[] table,其上的操作synchronized->并发,允许null值
# 13.java8 ConcurrentHashMap源码 分段锁是什么
insert链表头是采用CAS汇编指令集判断数据一致性,每个链表头加锁 链表>8才则tree化,桶数量>64也tree化
# 14.bitmap压缩
inJustDecodeBounds
设置为true 不会分配bitmap内存 但是可以获取宽高
然后设置inSampleSize
为我们想要的压缩比(和获取的宽高有关,取小的比例?)
源300,200 目标100,100 一般是压缩2 变为 150,100 而不是100,67..不过这也要看应用
再设置 inJustDecodeBounds
为false 然后return BitmapFactory.decodeResource(res, resId, options);
# 15.Android中的几种设计模式(待补充)
# 工厂
BitmapFactory.decodeResource.. 生产出bitmap
# 适配器
不同的数据提供者通过同一接口提供给同一客户:Adapter
# 创建者模式
复杂对象的构建和表现分离
# 抽象工厂
MediaPlayerFactory:ijk项目的player有好几种实现,后面具体生成哪个Player就看用户选择了
# 原型
Bitmap.copy(Config.RGB_xxx)方法修改部分数据然后进行对象的clone AlertDialog.Builder
# 16.[笔试]扔硬币正反概率都是0.5,直到连续的2次都为正面即结束,问结束次数的期望
解:这题如果一直穷举,列数列,找规律 难度很大,会在其中陷入循环.... 所以我们设法得到一个等式。设次数的期望为E.我们现在开始抛第一枚硬币,有以下情况:
- 第一枚硬币为负,那么需要重新扔,所占比例的期望为
0.5*(1+E)
- 第一枚硬币为正,第二枚为负,那么需要重新扔,所占比例的期望为
0.25*(2+E)
- 一正二正,结束,
0.25*2
得到
E= 0.5*(1+E)+ 0.25*(2+E)+ 0.25*2
解得E=6
也可以写代码跑下,随着样例越大答案会趋近于6
# 改为 负正 即结束 问期望
情况为以下,注:当出现连续的-时我们只考虑下次是否为+即可,否则说明为-我们继续判断下下次
+:
0.5*(E+1)
-+:
0.25*2
--+:
0.125*3
...(-...-)[n个]+
:(0.5)^(n+1)*(n+1)
E=0.5E+0.5+0.252+...+(0.5)^(n+1)(n+1) 高中知识:错位相减法 0.5E=0.51+0.252+...+(0.5)^(n+1)(n+1) 0.25E=0.251+0.1252+...+(0.5)^(n+2)(n+1) 两式相减: 0.25E=0.5+0.25+0.125+...+0.5^(n+1)-(0.5)^(n+2)(n+1) 等比数列求和:0.5+0.25+0.125+...+0.5^(n+1) a1=0.5 q=0.5 等比数列前n项和(其实不用算也能得到是趋于1的无穷...E=4): Sn=0.5(1-0.5^n)/(1-0.5)=1-0.5^n 故0.25E=1-0.5^n-0.5^(n+2)(n+1) E=4(1-0.5^n-0.5^(n+2)*(n+1))
n→∞ 得E=4
# 改成 出现+-甲赢,--乙赢,问甲赢的概率
只要投硬币出现+说明后面甲一定赢了,乙赢的情况必须是一开始就是-- 很容易求得甲赢的概率为0.5+0.5*0.5=0.75
# 改成 出现--+说明甲赢 出现-++说明乙赢,问甲赢的概率
# ~~ 思路1:分别求--+和-++的期望 ~~
[不可行 例:-+和--期望分别是4 6 但是两人赢的概率是一样的] 说明两个事件是相关的,不能分别求。
# ~~ 思路2:那么分别求不出现-++情况条件下出现--+的期望和不出现--+情况条件下出现-++的期望呢,条件概率。~~
# 思路3:直接算
顺便复习**可计算理论
**
求 表示不含-++且以--+结尾的-+字符串的`正则表达式
先画DFA
DFA转正则
最后的正则表达式如下 \+*-(\+-|--)*-\+
所以不含-++且以--+结尾的概率为
+*-
:概率为1
(\+-|--)*-\+
:以下概率的和
先找几项
-+:0.5^2
---+:0.5^2*0.5^2
+--+:0.5^2*0.5^2
-----+:0.5^4*0.5^2
--+--+: 0.5^3 以--+-开头的不用再往后搜索了
+-+--+:0.5^4*0.5^2
+----+:0.5^4*0.5^2
...
把图画出来就好做了:
a:--(--)*-+:0.5^2*(0.5^2+0.5^4+...+0.5^2n) =0.25*(0.25(1-0.25^n)/0.75)=
b:--(--)*+-:0.5*(0.5^2+0.5^4+...+0.5^2n)=0.5*(0.25(1-0.25^n)/0.75)
s=a+b 表示以--开头 =0.75*(0.25(1-0.25^n)/0.75)
c:+-(+-)*-+:0.5^2*(0.5^2+0.5^4+...+0.5^2n)=0.25*(0.25(1-0.25^n)/0.75)
d:+-(+-)*s:(0.5^2+0.5^4+...+0.5^2n)*s=(0.25(1-0.25^n)/0.75)* 0.75*(0.25(1-0.25^n)/0.75)
e:-+:0.5^2=0.25
所得概率=a+b+c+d+e=0.25+(0.25(1-0.25^n)/0.75)*(1+ 0.75*(0.25(1-0.25^n)/0.75))
=0.25+1/3*(1+3/4*1/3)=2/3
故甲赢概率是2/3 乙赢概率是1/3