对于很多精妙的题目,常常在想这些出题的童鞋是怎么想到的,因为这些题目确实能够很深入的考察出一个面试者的逻辑,算法和思维功底。本文章主要是总结自己在做这些面试题和了解相应解法的过程中思考的一些东西,感觉这些东西在很多题目中都出现过,非常值得抽象出来专门理解。

  一 双指针遍历

  所谓双指针,是利用两个指针对一个有序数组进行遍历,查找出符合要求的数据集合。相信大家都接触到了这种思维模式的解题方法,只是没有注意到罢了。下面举几个例子吧。

  例1:给定一个数组a[n],求数组中是否存在两个数的和等于给定值sum并输出?

编程之美 2.12 快速寻找满足条件的两个数

  这个问题很常见,我当年在面试微软实习生的时候就被问到了此题,解决方法有很多种,这里我就不赘述,我讲的是用双指针遍历法的。首先数组不一定有序,对数组排序是必须的。那么便来到了这样一个场景:对有序数组如何遍历来求得符合要求的数据集合?双指针的解决方法如下:定义两个指针(i 和 j),分别指向数组头和尾,那么会出现如下三种情况:

  1. 如果a[i]+a[j] == sum,那么很显然,只要输出这两个数,并把指针i+1和j-1指向下一个数即可。(这里不输出重复的组合)
  2. 如果a[i]+a[j] > sum,说明当前遍历的数值偏大,所以可以把j-1以减小和的值,在继续比较。
  3. 如果a[i]+a[j] < sum,说明当前遍历的数值偏小,同样为了加大和可以把i+1。

总的时间复杂度取决于排序即O(nlogn)。

例2:这题来自编程之美2.21只考加法的面试题,原题大致意思是写一个程序,对于一个32位整数,输出它所有可能的连续自然数之和的算式,要求是这些连续自然数之和要等于原数。例如3 = 2+1; 9 = 4+5,9 = 2+3+4等。

这题有两种解法, 其中一种便是双指针法,还有一种比较巧妙,是我同学在面试阿里云计算的时候想到的,利用了数学方法,简单来说是求出一个公式来。这里只说双指针的解法。

这里需要一个转化,把求n中所有可能的连续自然数之和归约为在数组{1,2,3,...,n}中找所有连续子序列和等于n的问题。这里同样也是这样一个场景:对有序数组如何遍历来求得符合要求的数据集合?这时的双指针可以不是一头一尾了,而是两个都指向头部,这样可以以高效的顺序遍历我们要找的所有集合。初始设i=j=1,这里同样会出现三种情况:

  1. sum[i,j] == sum, 直接输出i到j的值,并把i+1,j+1,因为只是i+1肯定是不等的,因为和小了,同样j+1只会使和变大,所以两个都要往前加(注意这里指针不用考虑减小,因为这在以前就考虑过了)
  2. sum[i,j] < sum,说明偏小,那么提高j来使得和变大才有可能相等
  3. sum[i,j] > sum,说明偏大,那么提高i来使得和变小才有可能相等

这样,代码就出来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<span style="font-size: 16px;">//计算连续和为n的所有子序列
void GetAnswer(int n)
{
    int i=1;
    int j=1;
 
    while(i<=n/2 && j <= n)
    {
         
        int sum = (j+i)*(j-i+1)/2;
 
        if(sum == n)
        {
            for(int k=i; k <= j; k++)
                cout<<k<<" ";
            cout<<endl;
            j++;
            i++;
        }
        else if(sum < n)
        {//sum[i..j]<n,只能提高j以增大sum
            j++;
        }
        else//sum[i..j]>n,只能提高i以减小sum
            i++;
    }
}
</span>

这里的思想本质上与上面例题1是相同的,这也是我抽象出这种思维模型的原因,当遇到有序数组或者归约到有序数组时,利用双指针遍历的方法是求得我们需要的数据集合的一种相对比较高效的方法。

二 排除以减少解空间大小

相信这种方法大家都听过,但是实际使用的时候却时常忘了去考虑这种思维模式,我这里举的都是很巧妙的例子,也是我遇到的,感觉绝对值得把这种思考方法总结出来。

 例1 此种解法很值得一说的题目来自编程之美2.3 寻找发帖水王,大致意思是:

 某论坛有一个“水王”,经常发帖,据说该“水王”发帖数目超过了帖子总数的一半,那么如何在id发帖列表中快速的查找到这一“水王”?

这道题非常好的体现了排除法的非凡效果,如果直接去求这个水王,方法也不少,例如按发帖数排序,但是这至少是O(nlogn)时间复杂度,实际上最好的算法却是尽可能的去减少解空间,把不可能的去除掉,留下的自然是要求的的解。对于这道题,就是每次去除两个不同id的发帖,由于默认水王发帖超过一半,那么去除任何两个不同id后仍然是超过一半的,why?可以这样想,开始满足的公式是 x>y/2,那么减去两个不同id后,最坏情况,这两个不同id中有一个是水王的id,则(x-1)/(y-2) > (y/2-1)/(y-2) > 1/2,即仍然是大于二分之一的, 所以可以不断的这样做直到最终剩下水王id为止。编程之美上给出了一个非常精妙的程序,这里不赘述。

 相同的例子还有这里

实际上这类思想的应用场景可以认为如果看到要求的东西占总体数量一半以上情况的时候,可以考虑排除法。当然还有其他情况,例如正面求解屡试不行的时候,也可以考虑这样的方法。

三 蓄水池抽样求概率模型

想起这个是因为多次碰到类似概率题要用到它,例如10月16号百度北京的笔试题中有它,然后同学面试阿里云被问到的题目中有它,我发现不仅仅是当不知道n多大的时候,即便有时候知道n多大,也可以使用这个模型,详细的关于模型的知识可以查看wiki也看以看我这篇文章

我同学在面试阿里云的时候被问到这样一个概率的问题:

给你一个n个长度的链表,以及一个函数,这个函数50%的概率返回0,50%的概率则返回1,问如何用这些条件从这n个链表中随机的抽取k个节点。

利用蓄水池抽样可以有这样一个解法,首先,这个函数可以产生0和1,那么我们可以通过构造多个二进制位(调用多次这个函数)并只取其中某些情况来构造任意概率,例如假设我要构造1/4,那么我调用两次这个函数,如果出现00,我认为发生,如果不是则不发生;又假设要构造3/5,我调用三次这个函数,并假定3种出现为无效出现,例如000,001,010。如果出现这种则再次调用函数生成,同时我们指定某三种为出现,其余不出现,这样可以构造一个生成概率3/5的生成器。有了这个,我们利用蓄水池抽样思想,先指定前k个节点为所求,并把指针指向第k+1个节点,此时以k/(k+1)决定是否与前k个选定的节点替换,替换时随机选择,并以此类推直至结尾。最后剩余的节点即为所求。

 四:动态规划:

编程之美:2.14 求数组的子数组之和的最大值和最小值(动态规划)

五。分而治之:

编程之美的2.17,数组循环移位 & 字符串逆转 Hello world => world Hello

总的来说,我觉得这三个思想都是经常可以利用的,有些问题没有见过这些思想是挺难在面试时当场想出来的,相反如果通过面试题提取出某些抽象可复用的思想,那么以后任何变形的面试题,都可以归约至这些解法从而解决,相信那个时候面试官一定会对你刮目相看。

posted on 2013-12-19 12:55  higirle  阅读(3472)  评论(0编辑  收藏  举报