编程之美总结

1.1, cpu占用率:如果工作10ms,sleep10ms那么cpu的usage就是50%。同样如果工作个H+ sin(A)秒然后休息个H-sin(A),这样随着A从0-2pi变化(变化量为0.01或者更小),cup的usage就是个sin的图形。
1.2, 当选择数据类型时可以考虑下是否使用int,char,还是几个bit就可以了。节省空间利用率。
1.5, 不变量的完美应用:一个集合中少了一个数,或者两个数怎么办?
          将没有缺少元素的集合的和(或者乘)作为不变量(作为对比物),则当集合缺少某个数后,集合的和(或者乘)与不变量的差即缺少的元素。
         变化: 1. 如一个集合的元素是[1...n],现在这里面少了个数,问这个数是几? 不变量:1+2+3...+n,少了某个数后的和是1+2+...n-k,则k是全求数。
                         2. 如emc的笔试题目: 10 个口袋每个有 100 个金币,其中一个口袋每个金币 9grams ,其余正常的金币都是 10grams 。有个天平,问最少几次可以找出那个口袋
                                同样的不变量题目:10个口袋每个100金币,每个金币10grams,其中有个口袋9grams。那么第一个口袋取1个金币,第二个取2个,...第10个取10个。
这样题目变成了在一个集合中,每个元素的范围是[1...n],现在有一个元素少了,找到这个元素。

1.6, 背包问题:在一个集合中找到一个子集使得该子集的和(或者价值,权重等等)等于某一个条件。
          比如:在一个集合中找到一个子集的和是n就是典型的背包问题。

1.16,24点集合问题。非常好的集合操作问题。24点,4个数构成的集合,通过+,-,*,/将4个数构成24点。
            一个集合,int numbers[4],通过任取两个数a,b进行四则运算,将结果放入numbers集合中。只要将a放结果,b放numbers[3],然后n=n-1即可。将集合元素个数减1。
            二进制表示集合的方法:一个集合n个元素,则该集合有2^n个子集。判断一个集合x是不是另一个集合i的子集的方法:x&i == x, 如果x是i的子集,那么x为1的位i也一定为1,所以相与一定为x。比如一个集合时1111,另一集合时0011,那么0011&1111==0011为真,那么0011为1111的子集。用这个方法可以找到一个集合的所有子集,如:
              for(int i = 1;i<x;i++)
              {
                       if(i & x == i){
                        i是x的子集。
                   }
              }
              所以对于24点题目,集合S个数为4,取出S的子集A,与S-A,得到A与S-A的四则运算结果f(A)与f(S-A),然后计算f(A)与f(S-A)的四则运算结果。这是个分治法,将一个子集分成两个子集,分别求两个子集的解,然后合并两个子集的解。
           关键字:集合操作,分治法,动态规划。

2.1,求二进制数中的1的个数。过于tricky了。没有太大的意义。
2.2,阶乘的末尾0个数。方法有几个:1,模拟法(不现实,n太大,溢出)。2. 找出规律,末尾0是有2,5这两个因子产生的,所以要找到2,5因子个数。
用样二进制的末尾0是由因子2产生的,所以要找到2的个数。
//相关题目:emc的笔试中有个题目说:F ( n) = 50!*5n, 问为了使F(n)末尾的0和F(n+1)末尾的0个数相等,最小的n是多少。
这道题目是求50的阶乘中有多少个2和5。因为2,5两个因子产生了0.而50!里面的2,5因子是固定的,5^n的5的个数在变,所以一旦50!里面的2用完了,0不会增加。

2.3,一个集合中某一个元素超过集合个数的一半则该元素是主元素。
最直接的方法,排序,中间第n/2个数是主元素。
如果一个集合有主元素A,则在集合中任意减去两个不同的元素,则主元素不变。所以可以任意减去两个不同元素,到最后留下的是主元素。
//扩展问题,一个集合中有3个元素的个数都找到1/4,找到这3个元素。
同样主元素问题,3个元素超过1/4,所以任意去除4个不同的元素,则剩下的3个元素一定是主元素。(不可能有4个元素,因为如果4个不同元素的话会被去除)
//在写for循环时,要考虑清楚哪里使用break退出。
//关键方法:递归,分治,集合。



2.4,找到1~n中每个数包含1的个数。要使用数学技巧,过于技巧化。
//关键点:会求余数,n%a的余数是n- (n/a)*a

2.5,找第k大数。有多种方法:
1,典型的select方法可以在平均O(n)时间内找到第k大数。但是最坏可能是O(n^2)。(partition每次只能去除一个元素的情况,即T(n)=T(n-1)+O(n))。
2,用二份法。求得集合的最大值Vmax,最小值Vmin,求的Vmid=(Vmax+Vmin)/2; (这个可能溢出),用Vmid = Vmin + (Vmax-Vmin)/2;比较好。统计大于Vmid的元素个数N,如果N==k则就是Vmid,如果N>K则第k大数比Vmid大,否则比Vmid小。这个方法的时间复杂度为O(n*log((Vmax-Vmin)/2)),当数据均匀分布时,复杂度为O(nlogn)。
上述两个方法都是划分区间的方法,通过缩小区间范围,找到第k大的数所在区间。如果原集合多大不能一次读入内存,那么可以通过文件操作保存中间结果。
3,当集合元素个数巨大,无法全部装入内存,则上述两个方法不是很有效,当k不是很大时,即k<<n时,可以用最小堆保存最大的k个数,复杂度为O(nlogk)。这个方法相对高效,而且复杂度是最坏情况下O(nlogk),但是当k很大时往往不是很理性,所以只适用于k相对较小的情况,并且n相对较大的情况,即k<<n。
4,如果集合的元素都是正整数,而且取值范围不大时,可以用记数排序或者桶排序,bitmap等线性时间排序方法。
记数排序和bitmap相对而言较简单,如果内存够大的话可以利用。但是当内存有限时,桶排序是个相对不错的选择。
桶排序:
1. 可以将集合取值范围划分成M个大小相等区间。如统计各个区间的元素个数。(相当于记数排序,这个只要扫描一遍,用个数组记录各个区间元素个数,O(M)空间,O(n)时间)。
2. 判断第k大元素在哪个区间里面。(O(M)时间)
3. 再次扫描集合,对第k大数所在区间进行记数排序(即M个区间中的每一个区间的取值范围都可以用记数排序表示。)。找到对应的第k大数。
其实这个方法和记数排序时一至的,记数排序是将集合中的每一个值都划分成一个区间,但是这样的话M就太大了,内存不够,所以桶排序就将若干个值并成一个区间,然后在进行排序。             //各种排序算法,要好好复习
//递归,分治,二份,堆排序,桶排序,记数排序,bitmap,文件排序等常用方法。


2.6,对于一个分数A/B,可以通过(A/Gcd(A,B))/(B/Gcd(A,B))进行约分。

2.7,乘2或者除2可以通过左移和右移来实现。

2.8,枚举的范围缩小。同样是枚举,有事枚举结果往往比较方便,如果结果是个相对特定形式的值。如这道题目,结果已知是0,1构成的,所以只要枚举0,1组成的值即可相对简单。

2.9,Fibonacci数列。
//2.6~2.9都是数学相关的题目,有很多数学知识,先暂时不看。
//编程技巧包括,使用移位,缩小枚举的范围,通过空间换时间,记录中间结果,避免重复计算。


2.10,典型的归并排序的思路,将数组两两比较,然后大的和大比,小的和小的比。从而减少了比较次数。
分治法同样如此。将一个数组分成两半。求的左边的最大最小值,再求右边的最大最小值,最后合并,左边的最大值与右边的最大值比较,最小值与最小值比较。

2.11,寻找最近点对。有点难,算法导论上的题目,先放放,回头看。


//这里开始都是和数组相关的题目了。
2.12,找到符合条件的两个数。
典型的查找题目。查找的方法基本上合排序有关,有了排序就想到二份搜索。这是基本思路。当然排序了就是O(nlogn)时间,当然可以通过O(n)空间换时间得到O(n)时间复杂度。(记数排序,hash,桶排序等等线性时间排序方法)。
排序后进行遍历的方法有:顺序遍历,二份遍历,双向遍历(头和末尾一起遍历)。
//扩展问题:一个集合里面选出一个子集使得子集的和等于某个值m。
背包问题,简单的深搜,时间为O(2^n),有点大。可用动规进行优化,复杂度降到O(n*m),n为集合大小,m为和。
当子集的和无法达到m,求最接近m的值。对于深搜,每次到达叶子节点时判断是否最接近需要求的值m。
//深搜,递归,叶子节点判断题目条件。动规优化。减少重复计算,空间换换时间,用空间记录中间结果。

2.13,典型的cumulative数组的题目。这类题目很多。它是求值题目,不涉及搜索,所以不用排序。
暴力方法就是找到任意的n-1个数,然后求乘积,时间为O(n^2)。很明显有很多重复计算。
同样用空间换时间。记录中间结果,避免重复计算。这里用到了cumulative数组。
类似的题目在“编程珠玑”上的第8章中有队cumulative数组的很好应用,第8章的P11,P10等题目都是cumulative array的用法。

2.14,2,15:动态规划的典型题目:
可以划分子问题,通过子问题求解原问题。关键是找到子问题结构。
在做题的时候要尝试寻找是否有子问题结构。
//整理下:
1维的字串和用dp做。
1维的首尾相连的也用dp作。
有三种方法:
第一种是求最小字段和,然后用sum-最小字段和。
第二种方法是做两次dp,从左向右做一次,从右向左做一次,求dp[n-1]+dp[0]的值是否比max大。
第三种方法是将数组扩大两倍,后面一半内容和前面一半相同,然后当一个数组做dp。

1维数组求两段最大子序列和。
两个dp,左边右边,然后求分支节点。

二维数组求子矩阵问题:
转换为一维的子序列和问题。
如果二维的上下连接,则将二维的长度增大两倍,下面一半复制成上面一半一样。
如果二维的左右也连接,则在dp过程中通一维的首尾连接。

//相似题目,求最小字串和,最大字串乘积。
http://hi.baidu.com/hell74111/blog/item/25e9ef5087c7bb818c543058.html

2.16:最大递增子序列
动规,但是不理想,复杂度为O(n^2)。
优化:使用一个数组记录每个长度的最大字符的最小值。这样就不用一个个查,可以通过根据长度进行二份查找。

2.17:技巧题目。

2.18:背包问题的变形,完美的动规题目。    //这道题目在微软100题的32题用了贪心算法。不过达不到最优解。
普通的背包问题可以知道n个数可以组成的和,但是不知道几个数能组成,所以在该基础上要增加限制。
dp[i][v]表示i个数可以组成和为v。//注,原dp[n][v]表示1-n个数中可以组成v,不限定由几个数组成,现在限定是i个数。
所以在写算法时:
dp[0][0] = 1;
dp[i][v] = 0; v>0
for(int i = 0 ;i< n;i++)    //n个数
{
    for(int j = i;j>=0;j--)    //选择j个数,相对于原来的背包问题多了这一个循环,确定选几个数组成sum。必须是从大到小,因为大的数依赖小的数。
    {
        for(int k = 0;k<w;k++){    //和为k
               if(k >=a[i] && dp[j-1][k-a[i]] == 1){
                    dp[j][k] = 1;
               }
        }
    }
}
//和一个集合中选取一个子集,使得子集合为sum的题目很像,区别在于确定了子集的元素个数。而前者不确定子集元素个数,所以只要知道是不是能够组成,后者需要确实具有k个元素的子集是否存在。
//当然可以用深搜得方法,但是复杂度为O(2^n),太大。
小结:应该说2.12~2.18都是和数组相关的。
数组搜索:排序,二分是基本的方法。或者是双向遍历。复杂度在O(nlogn)。
数组求最值:这类题目不涉及排序,因为一般都不能调整元素的位置。一般都是用动规,或者用cumulative array求和。目的是用空间换时间,保存每次的中间结果,避免重复计算。
当不能排序原数组,可以利用一些辅助数组记录数组信息达到排序数组的目的,如2.16的最长递增序列中用数组记录每个长度递增序列的最大元素的最小值,这样就达到了对每个递增序列排序的目的。
子集问题:深搜或者动规。深搜的话时间复杂度为O(2^n)太大,当n不是很大时可以使用。动规的话是用背包问题的变形解法,时间复杂度为O(n^2*M),如M不是很大,而n相对较大时可以用动规。
背包问题以及背包问题的变形是典型的动规的题目。

2.19,线段树的题目。见算法导论
一维的线段树相对简单,根据每个线段的x坐标排序,合并可连接的线段,然后用二分法搜索,或者用BST保存。然后每次查询都是O(logn)的时间。
二维的相对较难,没有细看。
//二叉搜索树。BST,线段树,看算法导论时再仔细看看,关键是个递归,二分,排序。

2.20,读程序题目。再看ifelse的时候一定要看清了。这道题目两个if写在一起,竟然不是if else的形式,所以两个if 都会执行的。

2.21,数字拆分,用连续的几个数的和表示给定的数。
//时间窗口模型。复杂度O(n)。
2^n都不能用连续几个数的和表示。
数学部分先过。
第二部分小结:这一部分基本上是和数组有关。与数组相关的题目一般有两类,搜索或者求最值。
搜索的话简单方法是排序加遍历。遍历方法有顺序遍历,二分遍历,或者双向遍历。
求最值的方法一般是暴利法,根据题目的要求取的各种可能的值。然后寻找重复计算的部分,进行优化。

3.1,字符串移位包含问题:简单的字符串匹配。

3.2,深搜的非递归版本不错,第一次看到。
如果深搜树有n层,那么用n个指针指向这个n层。每个指针指向这一层的某一个节点。该节点的子节点遍历晚后,该指针移动到他的右边的兄弟节点。当这一层遍历完后,它的上一层的节点移动一个。

3.3,典型的动态规划题目。LCS的变形。LCS是求两个字符串的相似度。其实这道题目和LCS是一样的,LCS求相同字串,这道题目求不相同的字符个数。
类似的动规题目有:求两个字符串的最大连续子串。这个和LCS差不多,但是是连续的子串,而LCS是不连续的。
字符串相关的题目小结:
1. 给两个或一个字符串,查找特定条件的某一个字串,如最长公共子串(LCS),最长公共连续子串。 一般用动态规划。
2. 找相同的子串,或者出现次数最多的子串(其实也是找相同的子串)(这类用后缀表)。包含某一个集合的最短子串(集合问题)(编程之美3.5,微软100题的37)。
3. 字串操作。 逆置,旋转,去除有些特定字符,去除重复字符,等等,比如:
  1. 解析一个字符串,对字符串中重复出现的字符,只在第一次出现时保留。如:abdabbefgf -> abdefg。
  2. 按单词逆序的。比如“This is a sentence",则输出是"sentence a is This”。这道题目和那个字符串旋转有点像。就是一个字符串翻转两次又变成了原来的样子了。
     “This is a sentence"先原地转一次给到“sihT si a ecnetnes"。然后再整体转一次得到"sentence a is This”。有时要考虑直接转不行的时候是否可以转两次得到。
  3. 字符串旋转,很牛逼的题目,编程珠玑上的。
  4.qq截取字符串的面试题: http://www.studyday.net/2010/10/164
  字符串相关面试题: http://www.cnblogs.com/graphics/archive/2011/03/09/1977717.html

3.4,链表操作,详见链表面试题。

3.5,字符串集合包含,见3.3

3.6,链表操作,见链表面试题。

3.7,队列最大值问题。要对栈和队列的操作十分熟悉。
思路:需要保存每个元素到栈底(队列底部)的最大值
对于栈,每个元素只需保存它到栈底的所有元素的最大值,因为栈的栈底是不变的,所以每次插入一个元素的时间为O(1)。
对于队列,因为队列的底部是在不断变化的随着元素的不断插入,所以每当插入一个元素时,该元素前面的所有元素都需要更新,所以插入时间为O(n),不符合要求。
所以书中的做法是用两个栈模拟一个队列,每个栈都记录各种最大值,这样插入一个元素的时间为O(1),而max操作也是O(1),空间为O(n)。
//用两个队列模拟一个栈效率低,需要O(n^2)时间。

3.8~3.10是二叉树的操作题目:
3.8,求二叉树中距离最远的两个节点的距离:
最开始的做法:对于过根节点的两个节点最远的距离是根节点左子树的最大深度以及右子树的最大深度的和。对于不过根节点的两个节点,存在于左子树,或者右子树。整个过程是个递归算法。
缺点是:求过根节点的两个最深节点时需要计算左右子树的深度,但是当求左子树中最大距离时又要再次计算左子树的深度,通过对于右子树也是如此。不多的重复计算每个节点的深度要。
所以可以利用空间换时间的做法,保存每个节点的左边和右边的最大深度。这样就不用重复计算。只要遍历一次就可以知道每个节点的左右子树的最大深度。
通过深搜,递归到叶子节点然后回溯到根节点时更新每个节点的左右子树最大深度。
//原来的是自顶向下,每次都需要求子树的深度。现在是自底向上,每个节点根据子树求的自己左右子树的深度。(动规)。
//这道题目说明了可以通过深度遍历二叉树用O(n)的时间求得每个节点的左右子树深度。需要O(n)空间。
如果不用递归,可以用栈模拟递归过程。算法框架是:
//这几个
stack s;
n = root;
do{
    if(n){
           s.push(n);
         n = n->left;
    }
    else{    
          n = s.pop();
         print n;
         n = n->right;
    }
}while(s.size()>0);

下面这个是后续遍历的:
stack s;
while(s)
{
n = s.top();
if(n->v == 1){s.pop();}
if(n->right)s.push(n->right);
if(n->left)s.push(n->left);
}
非递归版本的搜索可以参考: http://www.cnblogs.com/way_testlife/archive/2010/10/07/1845264.html

3.9是考察二叉树的前中后续遍历的概念。根据前序可以很快找到根节点,然后用根节点在中序遍历中找到左右子树的范围,递归的建立左右子树即可。
//扩展题目,当节点的字母有重复,构造出来的二叉树是不唯一的,需要将所有都输出。方法是在中序遍历中找到所有等于前序遍历的根节点的节点,然后递归的简历二叉树。
//貌似前序后后续不能确定二叉树吧。

3.10,二叉树的层次遍历。用队列模拟层次遍历,关键是记录下每一层的节点个数。
当某一层遍历结束后队列的长度则是下一层的节点个数。第一层节点个数为1,当第一层遍历结束后,队列中的长度为第二层的节点数,这样一层层遍历就可以确定每一层的节点数。
书中用两个指针,cur和last,cur指向每一层的第一个节点,last指向每一层的最后个节点,当cur>last时,这一层遍历结束,last移动队列尾部,相当于last-cur永远等于每一层的节点个数。
二叉树小结:
二叉树的题目基本上都是递归的算法,所以看到二叉树首先想到递归。比如:
1. 典型的求一个树的深度等于max(左子树的深度,右子树的深度)+1。
2. 求二叉树的节点个数 = (左子树的节点数+右子树的节点数)+1。
3. 二叉树翻转的题目,就是每个节点的左右节点交换,然后递归处理左子树,右子树。
4. 找某一个节点:当前节点,在左边子树找,在右边子树找。
3. 深搜:v(左子树),v(右子树)。求叶子路径,前中后序遍历都是通过深搜完成。基本上和二叉树相关的都很深搜有关。

二叉搜索树的一些特征:
1. 一颗树的最大值是它的最右边的一个节点,最小值是它最左边的一个节点。
2. 二叉搜索树的中序遍历是对二叉树的一个遍历。
3. 二叉搜索树的中序遍历中,每个节点的前继是没有右子树的(左子树的最右下节点),每个节点的后继是没有左子树的(右子树的最左下节点)。
所以可以用每个节点的右指针指向它的后继,每个节点的左指针指向前继。这样就可以将一个二叉树改变成一个双向链表了。
二叉搜索树的中序遍历相当有用,因为中序遍历是有序结构,所以相当于对一个集合进行排序输出。

二叉树递归的基本遍历方法包括深度搜索,广度搜索。
和二叉树相关的题目有很多都是二叉搜索树。
1. 比如建立完美二叉搜索树。
2. 考察前后中序遍历特征的,比如求节点的前继,后继。前中后序遍历等。一般都是在前中后序遍历的基础上进行变形。
a. 如微软面试100的第一题,利用中序遍历中每个节点的前继是没有右节点的特征,可以将每个节点的右指针指向每个节点的后继。同样每个节点的后继是没有左节点的,所以每个节点的左节点可以指向前继。
b. 如编程之美的3.9题,利用前序和中序的特征建立一个二叉树。同样微软100的第9题,判断一个序列是否为二叉排序树的后续遍历的结果,考察后续遍历与二叉排序树的特征,即左子树的所有节点的值都小于右子树所有节点的值。根据这个特征就可以判断一个序列是否为二叉排序树的后序遍历了。

还有的题目和二叉树的搜索相关,一般都是用深搜,如找最近公共父节点,找两个距离最远的节点,找最长叶子节点路径,再比如找逆序对的题目,用二叉排序树保存节点,相当于求每个节点的左子树节点数。找某个特定值,比如在搜索二叉树中找大于某个值的最小值。

总之,对于二叉树而已,一定要考虑其递归特征,一般三步骤:1.处理当前节点,2.处理左子树,3.处理右子树。
二叉树搜索关键是利用其排序特性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值