面试-题集

非递归遍历二叉树

http://hxraid.iteye.com/blog/507806#comments

C代码  

1.  #include<stdio.h>  

2.  #include<malloc.h>  

3.    

4.  typedef struct binode{  

5.      char data;  

6.      struct binode *lchild;  

7.      struct binode *rchild;  

8.  }BiNode,*BiTree;  

9.  /**************************** 

10.  *输入创建二叉树: abd##ef###c## 

11.  *其实输入按照先序顺序,#表示叶子节点 

12.  *****************************/  

13. void create(BiTree t){  

14.     char ch=(char)getchar();  

15.     if(ch=='#'){  

16.         t->data='#';  

17.         t->lchild=NULL;  

18.         t->rchild=NULL;  

19.     }  

20.     else{  

21.         t->data=ch;  

22.         t->lchild=(BiTree)malloc(sizeof(BiNode));  

23.         create(t->lchild);  

24.         t->rchild=(BiTree)malloc(sizeof(BiNode));  

25.         create(t->rchild);  

26.     }    

27. }  

28. //先序遍历  

29. void preTraverse(BiTree t){  

30.     BiTree p=t;  

31.     BiTree stack[20]; //使用栈来替代递归方法  

32.     int top=0;  

33.     while(top>=0){  

34.        while(p->data!='#'){  

35.            printf("%c ",p->data);  

36.            stack[top++]=p;  

37.            p=p->lchild;  

38.        }  

39.        if(top>0)  

40.           p=stack[--top]->rchild;  

41.        else   

42.           top=-1;  

43.          

44.     }  

45. }  

46. //中序遍历,和先序差不多  

47. void midTraverse(BiTree t){  

48.     BiTree p=t;  

49.     BiTree stack[20];  

50.     int top=0;  

51.     while(top>=0){  

52.         while(p->data!='#'){  

53.            stack[top++]=p;  

54.            p=p->lchild;  

55.         }  

56.         if(top>0){  

57.            p=stack[--top];  

58.            printf("%c ",p->data);  

59.            p=p->rchild;  

60.         }else  

61.            top=-1;  

62.     }  

63. }  

64. //后序遍历,稍微复杂一点  

65. void afeTraverse(BiTree t){  

66.     BiTree p=t,q=t;  

67.     BiTree stack[20];  

68.     int top=0;  

69.     while(top>=0){  

70.         while(p->data!='#'){  

71.            stack[top++]=p;  

72.            p=p->lchild;  

73.         }  

74.         if(q->rchild==p){  

75.            printf("%c ",q->data);  

76.            q->data='#'; //遍历完的节点直接做叶子标记  

77.            --top;   

78.         }  

79.         if(top>0){  

80.            p=stack[top-1];  

81.            q=p;  

82.            p=p->rchild;  

83.              

84.         }  

85.     }  

86. }  

87. //测试  

88. int main(){  

89.     BiTree root=(BiTree)malloc(sizeof(BiNode));  

90.     create(root);  

91.     afeTraverse(root);  

92.     printf("\n");  

93.     return 1;  

94. }  

 

【迅雷】多线程同步循环打印

/****************************************************************
 * 迅雷笔试题:
 * 有三个线程ID分别是A、B、C,请有多线编程实现,在屏幕上循环打印10次ABCABC… 
 * 
 * @author: heartraid
 *****************************************************************/

代码1:

Java代码  

1.  /** 

2.   * 锁码:公共数据区 

3.   * 码值:码值为A,表示应该由A线程来执行,B,C线程等待 

4.   *      码值为B,C同理。 

5.   */  

6.  class LockCode{  

7.      /**当前锁码码值,初始码值为A,表示最初由A线程运行*/  

8.      private char code='A';  

9.      /**单例模式*/  

10.     private LockCode(){  

11.     }  

12.       

13.     public static LockCode newInstance(){  

14.         return new LockCode();  

15.     }  

16.     /** 

17.      * 循环设置锁码 

18.      * 每一次调用,锁码按照A-B-C-A-...-的顺序循环往复 

19.      */  

20.     public void setCode(){  

21.         this.code=(char)(this.code+1);  

22.         if(this.code=='D')  

23.             this.code='A';  

24.     }  

25.     /** 

26.      * 得到锁码 

27.      */  

28.     public char getCode(){  

29.         return this.code;  

30.     }  

31. }  

32. /** 

33.  * 完成打印工作的线程类 

34.  */  

35. class PrintRunnable implements Runnable{  

36.     /**需要打印的字符*/  

37.     private char character='?';  

38.     /**公共锁码*/  

39.     private LockCode lockCode=null;  

40.       

41.     PrintRunnable(char c,LockCode l){  

42.         this.character=c;  

43.         this.lockCode=l;  

44.     }  

45.     /** 

46.      * 线程执行 

47.      */  

48.     public void run() {  

49.         int loopCount=1;  

50.         while(loopCount<=10){  

51.             synchronized(lockCode){//线程同步操作锁码  

52.                 try{  

53.                     //如果当前运行的线程并不等于当前锁码的码值,则改线程等待  

54.                     //比如当前运行线程是A,但是码值为B,则A线程等待。  

55.                     while(lockCode.getCode()!=this.character)  

56.                         lockCode.wait();  

57.                     //码值匹配成功,打印字符  

58.                     System.out.print(this.character);  

59.                     //循环10次记数  

60.                     loopCount++;  

61.                     //设置码值,让下一个线程可以运行  

62.                     lockCode.setCode();  

63.                     //让其他所有等待线程激活  

64.                     lockCode.notifyAll();  

65.                 }catch(InterruptedException e){    

66.                     e.printStackTrace();    

67.                 }    

68.             }  

69.         }  

70.     }  

71.       

72. }  

73. /** 

74.  * 测试 

75.  */  

76. public class ThreadLoopPrint {  

77.       

78.     public static void main(String[] args) {  

79.         LockCode lockCode=LockCode.newInstance();//公共锁码  

80.         Thread ta=new Thread(new PrintRunnable('A',lockCode));  

81.         Thread tb=new Thread(new PrintRunnable('B',lockCode));  

82.         Thread tc=new Thread(new PrintRunnable('C',lockCode));  

83.         ta.start();  

84.         tb.start();   

85.         tc.start();  

86.     }  

87. }  

 代码2:

Java代码  

1.  /** 

2.   *此代码和上面的代码有一个很大的相同点,就是都利用公共数据区中的数据变化来决定线程工作还是阻塞等待。公共数据区利用了类静态变量。因此代码简洁。 

3.   * 

4.   *@author jiangtao 

5.   *@date 2010-2-27 

6.   *@version 1.0 

7.   */  

8.  public class MyThread extends Thread{  

9.    

10.     public static String[] NAMES = new String[] { "A", "B", "C" };  

11.   

12.     public static int POS = 0;  

13.   

14.     private static final long DURATION = 1000;  

15.       

16.     private int count = 10;  

17.   

18.     public MyThread (String name) {  

19.         this.setName(name);  

20.     }  

21.   

22.     @Override  

23.     public void run() {  

24.         while (count > 0) {  

25.             if (this.getName().equals(NAMES[POS])) {  

26.                 this.print();  

27.                 this.count--;  

28.             }  

29.             try {  

30.                 Thread.sleep(DURATION);  

31.             } catch (InterruptedException e) {  

32.                 e.printStackTrace();  

33.             }  

34.         }  

35.     }  

36.   

37.     private synchronized void print() {  

38.         System.out.print(this.getName());  

39.         POS = (POS >= NAMES.length - 1 ? 0 : ++POS);  

40.     }  

41.       

42.     public static void main(String[] args) {  

43.         new MyThread ("A").start();  

44.         new MyThread ("B").start();  

45.         new MyThread ("C").start();  

46.     }  

47. }  

 

 

【腾讯】连续数打乱判断出少了哪些数?

http://hxraid.iteye.com/blog/618153

1. 问题:100个连续 的数打乱 之后,随机取出1个数 ,问如何最快速 的判断出少了哪一个?

 

分析:对于所有100个连续的数,只要除余100。一定在0~99之间。一般来说,比较常规的做法就是先排序(利用Hash表定位),在循环查找。当然时间复杂度是O(2n)。现在介绍一种很牛的O(n)做法:求二进制异或运算。

 

异或运算: 0^0=1^1=0;   0^1=1^0=1。0~99个数全部异或的结果只能是0。如果缺少一个数,那么全部异或的结果正好就是那个数。为什么呢?我们做个小实验:假如有四个数:  0001  0010,0101, 0110 排列成一个matrix.

                                        bits:      1     2    3      4

                                                     0     0    0      1

                                                     0     0    1      0

                                                     0     1    0      1

                                                     0     1    1      0

                                    全部异或:  0     0    0      0

我们可以下结论了,要全部异或的结果为0,那么所有bit位上的1的个数必须为偶数。 反过来说:如果其中有一个数不存在了(比如0001),那么少0的的bit位上的值不变(因为1的个数还是偶数),而少1的bit位上的值就变成了1(1的个数为奇数了)。

 

这样0~99的道理也就一样了,所以异或的结果就是少的那个值。代码如下:

Java代码  

1.  int data=0;  

2.  for(int i=1;i<=99;i++){  

3.      if(i==78)  //少78  

4.            continue;  

5.      data=data^i;  

6.  }  

7.  System.out.println(data);  

 

2. 问题:100个连续 的数打乱 之后,随机取出2个数 ,问如何最快速 的判断出少了哪两个? (注意少2个数了)

 

分析:常用的做法可以先创建一个100个结构的Hash表,然后循环一次将所有数哈希100之后的位置上置1。然后顺序循环100次查找这个Hash表。前后需要O(2n)的时间。然而有没有更快速的做法呢?当然,直接操作bit.

 

假设我们有32个连续打乱的数字(0~31)缺少两个数2和11,希望把标记1标记在一个32位上。也就是一个整形变量,标记完之后就成为了:

                 bits position    31   30  29  28  .......  11   10   ....  2    1    0

                         int   a=     1     1    1    1   .......   0     1    ....  0    1    1    (缺少数的bit位上为0)

至于如何标记成为a,我们可以看看下面的小段代码:

Java代码  

1.  long bits=0;  

2.  for(int i=0;i<32;i++){  

3.      long bitMove=1;  

4.      if(i==2||i==11)  

5.          continue;  

6.      bitMove=bitMove<<i;  

7.      bits=bits | bitMove;  

8.      System.out.println(bits+"  :  "+Long.toBinaryString(bits));  

9.  }  

 

此时我们将数字a每8位作为一个数字b,如果b==255, 则说明全部8位都是1(没有缺少数字)。如果b!=255,则说明有某些位是0(有数字缺少),然后再在不等于255的8 bits上顺序查找等译0的位数即可。这样就相当于原来需要顺序查找32 bits(查找32次)。而现在只需要先查找4个8位的块,然后再需找某个8位块中的bit(也就是需要4+8=12次)即可。这就是分块查找的基本原理了。

 

通过一个32个连续的数,我们发现了敲门。这样对于100个连续的数呢?很简单,我们需要4个32位就够了。注意:由于最高位如果是1的话,整形数据将会变成负数,不方便我们的计算。因此我们用long数据来存储32个位数。

 

代码如下:

Java代码  

1.  public class Test{  

2.        

3.      public static void main(String[] args){  

4.          //需要4个32位数据,用long存储为了避开int存储可能带来的负数。  

5.          //intBits[0]的最低32位标识数据0~31  

6.          //intBits[1]的最低32位标识数据32~63,  

7.          //intBits[2]的最低32位标识数据64~95,  

8.          //intBits[3]的最低4位标识数据96~99  

9.          long[] intBits={0L,0L,0L,0L};   

10.           

11.         //用100bit位标识是否存在0~99的数据  

12.         //此时需要循环的次数为100次,时间复杂度O(n),n为连续的data数量。  

13.         int loop=0;  

14.         for(int data=0;data<=99;data++){  

15.             long bitMove=1;  

16.             if(data==2||data==11) //缺少数据2和11  

17.                 continue;  

18.             bitMove=bitMove<<(data-32*(data/32));   

19.             intBits[data/32]=intBits[data/32] | bitMove;  

20.             loop++;  

21.         }  

22.         //中间打印标识结果  

23.         System.out.print("标识结果(循环"+loop+"次):");  

24.         for(int i=3;i>=0;i--)  

25.             System.out.print(Long.toBinaryString(intBits[i]));  

26.         System.out.println();  

27.           

28.         //分块查找,每8bit一块,一共需要100/8=13块  

29.         //其中如果8bit全部是1,则该数据等于2^8-1=255  

30.         //前3个intBits都全部位数都用来标识数据  

31.         loop=0;  

32.         int zeroSize=0;  

33.         for(int i=0;i<4;i++){  

34.             long bits=intBits[i];  

35.             long eightBits=0;  

36.             int eightSize=0;  

37.             while((eightBits=bits%256)!=0){  

38.                 System.out.println("第"+((eightSize+1)+i*4)+"个8bits块:"+Long.toBinaryString(eightBits));  

39.                 if(i<3&&eightBits!=255){  

40.                     for(int j=0;j<8;j++){  

41.                         loop++;  

42.                         if(eightBits%2==0){  

43.                             zeroSize=j+eightSize*8+i*32;  

44.                             System.out.println("   zero size="+zeroSize);  

45.                         }  

46.                         eightBits=eightBits>>1;  

47.                     }  

48.                 }  

49.                 bits=bits/256;  

50.                 eightSize++;  

51.                 loop++;  

52.             }  

53.         }  

54.               

55.         System.out.println("标识后查找需要循环"+loop+"次");  

56.   

57.     }     

58. }  

 这段代码只需要循环98+29=117次,少于一般情况下的200次。

 

 

3. 问题:有1到10w这10w个数,去除2个并打乱次序,如何找出那两个数?(不准用位图) 

  我想用分块查找的思想,首先开辟一个a[100][1001]存储结构,遍历10W个数存放这个结构

Java代码  

1.  int[][] a=new[100][1001];  

2.    for(int i=0;i<99998){  

3.          a[i/1000-1][i%1000+1]=1;  

4.          a[i/1000-1][0]++;  //记1000个数是否已经满了。  

5.    }  

6.    

7.    for(int j=0;j<100;j++){  

8.        if(a[j][0]<1000){ //这1000个数里面有数少了  

9.             for(int k=1;k<1000;i++){  

10.                if(a[j][k]!=1){  

11.                     System.out.println("少了的呢个数就是:"+(j*1000+k));  

12.                }  

13.            }  

14.       }  

15.   }  

 

性能分析: 首先循环99998次为数组设置标志位。然后双重循环(其实真正需要双循环的是有2次,因为就少了2个数),因此双重循环的次数因该是: 100(块查找次数)+2*1000=2100次。
        一共循环99998+2100  因此效率是O(n+m) 其中n为待查找数字的个数(10W),m为分割的块数与快内查找的次数。如果n远远大于m,则查询效率接近于O(n)。因此这种查找数据越多效率越好。

 

 

 

【腾讯】报纸与信件的字符匹配效率问题

http://hxraid.iteye.com/blog/627859

题目:有一个江洋大盗,他每次写信都是从一张报纸上剪下单词,再把单词贴在信上。假如某张报纸的单词保存在vector<string>paper 中,而信的单词保存在vector<string>letter 中,写一个算法程序判别该报纸可不可以生成该信?

 

对比一些方法: 这里假设paper:(m个单词,每个单词平均d个字母),letter:(n个单词,每个单词平均d个字母)。对于中文词语而言,一般2~4个字左右,对于英文单词,d就要大一点,但基本上也是常数。因此d远远小于m,下面比较的时候就不考虑单词中每个字符间的比较了。


(1) 蛮力匹配:
    把n中每个单词与m中的每个单词一一比较。时间复杂度为:O(m*n)。估计谁也不会选这种。

(2) 二分查找:要求,对paper建有序索引。
    paper排序的时间复杂度最好为O(m*logm)。
    对letter中的所有单词二分查找的时间复杂度为O(n*logm)
    总的时间复杂度为O((m+n)*logm). 比蛮力查找效率要好不少。

    缺点:如果换了新报纸,所有的单词都必须重新排序,需要O(m*logm)的时间来建立索引。

(3) trie树: 要求,对paper建trie索引树

    paper建立trie树的时间复杂度为O(m)。
    对letter中所有单词在trie树中查找的时间复杂度为O(n).
    总的时间复杂度为O(m+n)。效率绝对要比二分查找好。

    优势: 如果换了新报纸,重新建立一颗trie树的时间O(m)也小于二分查找建立有序索引的时间O(m*logm)要小的多。

    缺点:建立trie树所需要的空间代价是很大的。如果是中文词语的trie树,那么放进全部加载进内存是很可怕的,需要把trie树用B树的方法存储在磁盘上。详见:《Trie树 》

(4) Hash表: 要求,对paper建立Hash结构

    建立Hash表结构的时间复杂度为O(m),注意需要计算每个单词的HashCode的时候,很可能要遍历单词中的每个字母。
    对letter中所有单词在Hash结构中查找的时间复杂度为O(n)。当然,这是在没有任何散列冲突的理想情况下。选择好HashCode的计算方式和散列表的大小,可以将冲突降到很低。因此我们这里不考虑冲突。
    总的时间复杂度为O(m+n)。由此可见,Hash表结构与Trie树的效率都是相当的客观。

    缺点:如果换报纸。为了考虑冲突的可能性,Hash结构的大小可能需要重新考虑。这一点很麻烦。当然,存储空间上应该会比Trie要好一些,但实际应用上并不比Trie方便。


(5) 归并查找:要求,对letter和paper都建立有序索引。

    对letter和paper排序的时间复杂度分别为O(m*logm)和O(n*logn)
    归并查找的时间复杂度为O(m+n)
    总的时间复杂度为O(m*logm+n*logn+m+n)

总结:对于这几种方法而言,我更加青睐于trie树。因为我相信报纸中的单词数量基本上是保持稳定的,不可能达到海量级别。Trie树的空间代价其实并不算什么。

 

 

【腾讯】10G整数文件中寻找中位数

http://hxraid.iteye.com/blog/649831

题目:在一个文件中有 10G 个整数,乱序排列,要求找出中位数。内存限制为 2G。只写出思路即可(内存限制为 2G的意思就是,可以使用2G的空间来运行程序,而不考虑这台机器上的其他软件的占用内存)。

 

 

分析: 既然要找中位数,很简单就是排序的想法。那么基于字节的桶排序是一个可行的方法 (请见《桶排序 》):

思想:将整形的每1byte作为一个关键字,也就是说一个整形可以拆成4个keys,而且最高位的keys越大,整数越大。如果高位keys相同,则比较次高位的keys。整个比较过程类似于字符串的字典序。

第一步:把10G整数每2G读入一次内存,然后一次遍历这536,870,912个数据。每个数据用位运算">>"取出最高8位(31-24)。这8bits(0-255)最多表示255个桶,那么可以根据8bit的值来确定丢入第几个桶。最后把每个桶写入一个磁盘文件中,同时在内存中统计每个桶内数据的数量,自然这个数量只需要255个整形空间即可。

代价:(1) 10G数据依次读入内存的IO代价(这个是无法避免的,CPU不能直接在磁盘上运算)。(2)在内存中遍历536,870,912个数据,这是一个O(n)的线性时间复杂度。(3)把255个桶写会到255个磁盘文件空间中,这个代价是额外的,也就是多付出一倍的10G数据转移的时间。 

第二步:根据内存中255个桶内的数量,计算中位数在第几个桶中。很显然,2,684,354,560个数中位数是第1,342,177,280个。假设前127个桶的数据量相加,发现少于1,342,177,280,把第128个桶数据量加上,大于1,342,177,280。说明,中位数必在磁盘的第128个桶中。而且在这个桶的第1,342,177,280-N(0-127)个数位上。N(0-127)表示前127个桶的数据量之和。然后把第128个文件中的整数读入内存。(平均而言,每个文件的大小估计在10G/128=80M左右,当然也不一定,但是超过2G的可能性很小)。

代价:(1)循环计算255个桶中的数据量累加,需要O(M)的代价,其中m<255。(2)读入一个大概80M左右文件大小的IO代价。 

注意,变态的情况下,这个需要读入的第128号文件仍然大于2G,那么整个读入仍然可以按照第一步分批来进行读取。 

第三步:继续以内存中的整数的次高8bit进行桶排序(23-16)。过程和第一步相同,也是255个桶。

第四步:一直下去,直到最低字节(7-0bit)的桶排序结束。我相信这个时候完全可以在内存中使用一次快排就可以了。

 

整个过程的时间复杂度在O(n)的线性级别上(没有任何循环嵌套)。但主要时间消耗在第一步的第二次内存-磁盘数据交换上,即10G数据分255个文件写回磁盘上。一般而言,如果第二步过后,内存可以容纳下存在中位数的某一个文件的话,直接快排就可以了。关于快排的效率,可以看看我博客中的数据《基于比较的内部排序总结 》。

 

 

【腾讯】1亿个数据取前1万大的整数

数据规模分析

 

不考虑操作系统的区别,通常将C++中的一个整型变量认为4bytes。那么1亿整型需要400M左右的内存空间。当然,就现代PC机而言,连续开辟400M的内存空间还是可行的。因此,下面的讨论只考虑在内存中的情况。为了讨论方便,假设M=1亿,N=1万。

 

用大拇指想想

略微考虑一下,使用选择排序。循环1万次,每次选择最大的元素。源代码如下:

Cpp代码  

1.  //解决方案1,简单选择排序  

2.  //BigArr[]存放1亿的总数据、ResArr[]存放1万的总数据  

3.  void solution_1(int BigArr[], int ResArr[] ){  

4.         forint i = 0; i < RES_ARR_SIZE; ++i ){  

5.                int idx = i;  

6.                //选择最大的元素  

7.                forint j = i+1; j < BIG_ARR_SIZE; ++j ){  

8.                       if( BigArr[j] > BigArr[idx] )  

9.                              idx = j;  

10.               }  

11.               //将最大元素交换到开始位置  

12.               ResArr[i] = BigArr[idx];  

13.               std::swap( BigArr[idx], BigArr[i] );  

14.        }  

15. }  

性能分析: 哇靠!时间复杂度为O(M*N)。 有人做过实验《从一道笔试题谈算法优化(上) 》,需要40分钟以上的运行时间。太悲剧了......

 

当然,用先进的排序方法(比如快排),时间复杂度为O(M*logM)。虽然有很大的改进了,据说使用C++的STL中的快排方法只需要32秒左右。确实已经达到指数级的优化了,但是否还能够优化呢?

 

稍微动下脑子

我们只需要1万个最大的数,并不需要所有的数都有序,也就是说只要保证的9999万个数比这1万个数都小就OK了 。我们可以通过下面的方法来该进:

 

(1) 先找出M数据中的前N个数。确定这N个数中的最小的数MinElement。

(2) 将  (N+1) —— M个数循环与MinElement比较,如果比MinElement还小,则不处理。如果比MinElement大,则与MinElement交换,然后重新找出N个数中的MinElement。

Cpp代码  

1.  //解决方案2  

2.  void solution_2( T BigArr[], T ResArr[] ){  

3.         //取最前面的一万个  

4.         memcpy( ResArr, BigArr, sizeof(T) * RES_ARR_SIZE );  

5.         //标记是否发生过交换  

6.         bool bExchanged = true;  

7.         //遍历后续的元素  

8.         forint i = RES_ARR_SIZE; i < BIG_ARR_SIZE; ++i ){  

9.                int idx;  

10.               //如果上一轮发生过交换  

11.               if( bExchanged ){  

12.                      //找出ResArr中最小的元素  

13.                      int j;  

14.                      for( idx = 0, j = 1; j < RES_ARR_SIZE; ++j ){  

15.                             if( ResArr[idx] > ResArr[j] )  

16.                                    idx = j;  

17.                      }  

18.               }  

19.               //这个后续元素比ResArr中最小的元素大,则替换。  

20.               if( BigArr[i] > ResArr[idx] ){  

21.                      bExchanged = true;  

22.                      ResArr[idx] = BigArr[i];  

23.               }else  

24.                      bExchanged = false;  

25.        }  

26. }  

 

性能分析: 最坏的时间复杂度为O((M-N)*N)。咋一看好像比快排的时间复杂度还高。但是注意是最坏的,实际上,并不是每次都需要付出一个最小值O(N)的代价的。因为,如果当前的BigArr[i]<ResArr[idx]的话,就不需要任何操作,则1——N的最小值也就没有变化了。下一次也就不需要付出O(N)的代价去寻找最小值了。当然, 如果M基本正序的话,则每次都要交换最小值,每次都要付出一个O(N)代价。最坏的情况比快排还要差。

 

就平均性能而言,改进的算法还是比快排要好的,其运行时间大约在2.0秒左右。

 

 

使劲动下脑子

上面的解决方案2还有一个地方不太好。当BigArr[i]>ResArr[idx]时,则必须交换这两个数,进而每次都需要重新计算一轮N个数的最小值。只改变了一个数就需要全部循环一次N实在是不划算。能不能下一次的最小值查找可以借助上一次的比较结果呢?

 

基于这样一个想法,我们考虑到了堆排序的优势(每一次调整堆都只需要比较logN的结点数量)。因此我们再做一次改进:

 

(1) 首先我们把前N个数建立成小顶堆,则根结点rootIdx。

(2) 当BigArr[i]>ResArr[rootIdx]时,则交换这两个数,并重新调整堆,使得根结点最小。

 

性能分析:显然,除了第一次建堆需要O(N)时间的复杂度外,每一次调整堆都只需要O(logN)的时间复杂度。因此最坏情况下的时间复杂度为O((M-N)*logN),这样即使在最坏情况下也比快排的O(M*logM)要好的多了。

 

另外:实际上也可以使用二分查找的思想,第一次找N中的最小值的时候将N排序。以后每次替换最小值,都使用二分查找在logN代价下找到当前N的最小值即可。与使用堆的过程如出一辙。

 

 

【腾讯】快速找到未知长度单链表的中间节点

普通的方法很简单,首先遍历一遍单链表以确定单链表的长度L。然后再次从头节点出发循环L/2次找到单链表的中间节点。算法复杂度为O(L+L/2)=O(3L/2)。

 

能否再优化一下这个时间复杂度呢?有一个很巧妙的方法:设置两个指针* fast、*slow都指向单链表的头节点。其中* fast的移动速度是* slow的2倍。当* fast指向末尾节点的时候,slow正好就在中间了。

 

C源代码如下:

Java代码  

1.  void locate(LinkedList *head){  

2.        LinkedList *fast, *slow;  

3.        fast=slow=head;  

4.        while(fast->next!=NULL){  

5.                //fast的移动速度是slow的2倍  

6.                if(fast->next->next!=Null){  

7.                        fast=fast->next->next;  

8.                        slow=slow->next;  

9.                }else{  

10.                     fast=fast->next;  

11.               }  

12.       }  

13. }  

 

另外,快慢指针在解决单链表环问题的时候是非常有用的,具体请参见《★经典问题—链表中的环问题 》

 

 

【Google】25匹马的角逐

问题是这样的:一共有25匹马,有一个赛场,赛场有5个赛道,就是说最多同时可以有5匹马一起比赛。假设每匹马都跑的很稳定,不用任何其他工具,只通过马与马之间的比赛,试问最少 得比多少场才能知道跑得最快的5匹马。

 

注意: "假设每匹马都跑的很稳定" 的意思是在上一场比赛中A马比B马快,则下一场比赛中A马依然比B马快。

 

稍微想一下,可以采用一种 竞标赛排序(Tournament Sort)的思路。 见《选择排序 》

 

(1) 首先将25匹马分成5组,并分别进行5场比赛之后得到的名次排列如下:

              A组:  [A1  A2  A3   A4  A5]

              B组:  [B1  B2  B3   B4  B5]

              C组:  [C1  C2  C3  C4  C5]

              D组:  [D1  D2  D3  D4  D5]

              E组:  [E1  E2  E3   E4  E5]

      其中,每个小组最快的马为[A1、B1、C1、D1、E1]。

(2) 将[A1、B1、C1、D1、E1]进行第6场,选出第1名的马,不妨设 A1>B1>C1>D1>E1. 此时第1名的马为A1。

(3) 将[A2、B1、C1、D1、E1]进行第7场,此时选择出来的必定是第2名的马,不妨假设为B1。因为这5匹马是除去A1之外每个小组当前最快的马。

(3) 进行第8场,选择[A2、B2、C1、D1、E1]角逐出第3名的马。

(4) 依次类推,第9,10场可以分别决出第4,5名的吗。

 

因此,依照这种竞标赛排序思想,需要10场比赛是一定可以取出前5名的。

 

 

仔细想一下,如果需要减少比赛场次,就一定需要在某一次比赛中同时决出2个名次,而且每一场比赛之后,有一些不可能进入前5名的马可以提前出局。 当然要做到这一点,就必须小心选择每一场比赛的马匹。我们在上面的方法基础上进一步思考这个问题,希望能够得到解决。

 

(1) 首先利用5场比赛角逐出每个小组的排名次序是绝对必要的。

(2) 第6场比赛选出第1名的马也是必不可少的。假如仍然是A1马(A1>B1>C1>D1>E1)。那么此时我们可以得到一个重要的结论:有一些马在前6场比赛之后就决定出局的命运了(下面绿色字体标志出局)。

       A组:  [A1  A2  A3   A4  A5]

       B组:  [B1  B2  B3   B4  B5 ]

       C组:  [C1  C2  C3  C4  C5 ]

       D组:  [D1  D2  D3  D4  D5 ]

       E组:  [E1  E2  E3   E4  E5 ]

(3) 第7场比赛是关键,能否同时决出第2,3名的马呢?我们首先做下分析:

     在上面的方法中,第7场比赛[A2、B1、C1、D1、E1]是为了决定第2名的马。但是在第6场比赛中我们已经得到(B1>C1>D1>E1),试问?有B1在的比赛,C1、D1、E1还有可能争夺第2名吗? 当然不可能,也就是说第2名只能在A2、B1中出现。实际上只需要2条跑道就可以决出第2名,剩下C1、D1、E1的3条跑道都只能用来凑热闹的吗?

     能够优化的关键出来了,我们是否能够通过剩下的3个跑道来决出第3名呢?当然可以,我们来进一步分析第3名的情况?

     ● 如果A2>B1(即第2名为A2),那么根据第6场比赛中的(B1>C1>D1>E1)。 可以断定第3名只能在A3和B1中产生。

     ● 如果B1>A2(即第2名为B1),那么可以断定的第3名只能在A2, B2,C1 中产生。

     好了,结论也出来了,只要我们把[A2、B1、A3、B2、C1]作为第7场比赛的马,那么这场比赛的第2,3名一定是整个25匹马中的第2,3名。

     我们在这里列举出第7场的2,3名次的所有可能情况:

     ①  第2名=A2,第3名=A3

     ②  第2名=A2,第3名=B1

     ③  第2名=B1,第3名=A2

     ④  第2名=B1,第3名=B2

     ⑤  第2名=B1,第3名=C1

 

(4)  第8场比赛很复杂,我们要根据第7场的所有可能的比赛情况进行分析。

      ①  第2名=A2,第3名=A3。那么此种情况下第4名只能在A4和B1中产生。

           ● 如果第4名=A4,那么第5名只能在A5、B1中产生。

           ● 如果第4名=B1,那么第5名只能在A4、B2、C1中产生。

           不管结果如何,此种情况下,第4、5名都可以在第8场比赛中决出。其中比赛马匹为[A4、A5、B1、B2、C1]

      ②  第2名=A2,第3名=B1。那么此种情况下第4名只能在A3、B2、C1中产生。

           ● 如果第4名=A3,那么第5名只能在A4、B2、C1中产生。

           ● 如果第4名=B2,那么第5名只能在A3、B3、C1中产生。

           ● 如果第4名=C1,那么第5名只能在A3、B2、C2、D1中产生。

           那么,第4、5名需要在马匹[A3、B2、B3、C1、A4、C2、D1]七匹马中产生,则必须比赛两场才行,也就是到第9场角逐出全部的前5名。

      ③  第2名=B1,第3名=A2。那么此种情况下第4名只能在A3、B2、C1中产生。

           情况和②一样,必须角逐第9场

      ④  第2名=B1,第3名=B2。 那么此种情况下第4名只能在A2、B3、C1中产生。

           ● 如果第4名=A2,那么第5名只能在A3、B3、C1中产生。

           ● 如果第4名=B3,那么第5名只能在A2、B4、C1中产生。

           ● 如果第4名=C1,那么第5名只能在A2、B3、C2、D1中产生。

            那么,第4、5名需要在马匹[A2、B3、B4、C1、A3、C2、D1]七匹马中产 生,则必须比赛两场才行,也就是到第9场角逐出全部的前5名。

        ⑤  第2名=B1,第3名=C1。那么此种情况下第4名只能在A2、B2、C2、D1中产生。

            ● 如果第4名=A2,那么第5名只能在A3、B2、C2、D1中产生。

            ● 如果第4名=B2,那么第5名只能在A2、B3、C2、D1中产生。

            ● 如果第4名=C2,那么第5名只能在A2、B2、C3、D1中产生。

            ● 如果第4名=D1,那么第5名只能在A2、B2、C2、D2、E2中产生。

             那么,第4、5名需要在马匹[A2、B2、C2、D1、A3、B3、C3、D2、E1]九匹马中 产 生,因此也必须比赛两场,也就是到第9长决出胜负。

 

 

总结:最好情况可以在第8场角逐出前5名,最差也可以在第9场搞定。

 

 

找一个数组中的主元素

问题:在一个规模为N的数组array[N]中,所谓主元素就是出现次数大于N/2的元素,例如 

                                           3.3.4.2.4.4.2.4.4  有一个主元素为4。

          给出一个算法,如果过半元素存在,就找出来,否则给出报告,要求给出O(N)的算法。

 

 

常规想法

 

(1) 穷举:找出元素中每一个数在数据中的数量。时间复杂度O(N^2)

(2) 排序:先对数组快排,然后重头开始遍历一遍计算每个数的数量。时间复杂度O(N*logN+N)

 

经典算法    裁剪数组算法, 时间复杂度为O(2N ) 

     思想: 如果一个数组array[N],其中有两个元素e1和e2。

      (1) e1不等于e2

      假如e1是数组array[N]的主元素,e2不是。那么e1在array[N]中的数量en>N/2。此时去掉array[N]中的e1和e2两个元素(e1!=e2)。那么新数组array[N-2]中e1的数量为en-1,并且en-1>N/2-1=(N-2)/2。即e1还是新数组array[N-2]的主元素。

      假如e1和e2都不是数组array[N]的主元素,那么去掉e1、e2以后。那么新数组的大小将变成N-2。此时很有可能出现一个新数组的主元素X,此主元素X的数量正好等于X=(N-2)/2+1。但是该主元素就不是原数组的主元素了,因为X= (N-2)/2+1=N/2。那么这样的主元素X我们叫做伪主元素。因此需要通过和裁剪掉元素比较,来确定是否是真正的主元素。

      (2) e1等于e2

      这种情况下不能进行裁剪。只能继续找不同的两个数裁减掉

 

C代码  

1.  #include<stdio.h>    

2.  #include<malloc.h>  

3.   // 算法源代码,裁剪当前数组中相邻的不同元素    

4.   void main(){    

5.      int pArr[6]={4,4,4,4,6,6};    

6.      int arrLength=6;  //数组长度  

7.      int element=pArr[0];    

8.      int value=1;  //记录剪裁过程中遇到相同元素的个数  

9.      int delNum=0; //记录裁剪数组的元素个数  

10.     int *dArr=(int *)malloc(arrLength*sizeof(int)); //记录被剪裁的数组元素  

11.     int dTop=0; //当前剪裁数组的索引位置  

12.        

13.     for(int i=1;i<arrLength;i++){    

14.        if(value==0){    

15.            element=pArr[i];    

16.        }    

17.        if(pArr[i]==element) //如果当前数组相邻的元素相等    

18.            value++;    

19.        else if(value>0){//如果当前数组相邻的元素不等,则需要裁剪得到新的数组    

20.            dArr[dTop++]=element;  

21.            dArr[dTop++]=pArr[i];  

22.            delNum+=2;    

23.            value--;    

24.        }    

25.     }    

26.     //如果裁剪之后出现了主元素,那么这个主元素有可能是个伪主元素  

27.     if(value>(arrLength-delNum)/2){  

28.         //与裁剪掉的数组元素一一比较  

29.         for(int j=0;j<delNum;j++)  

30.             if(element==dArr[j]) value++;             

31.         //确定真正的主元素  

32.         if(value>arrLength/2)  

33.             printf("主元素为:%d\n",element);    

34.     }    

 

 

<游戏> 取石子

Tang和Jiang非常喜欢玩一种有趣的小游戏: 有N个石子,两人轮流从中取出1个, 3个或4个石子,当石子被取空时,游戏结束。最后一个取石子的人获胜, 第一次总是Tang取. 当然,他们俩都足够聪明,总会采取最优的策略。 

Input
每行会有一个正整数N(N<=100000), 代表石子的个数, N=0 代表输入结束
Output
输出获胜人的名字。

Sample Input
1 //石头数量为1 
2
0
Sample Output
Tang //石头数量为1的时候,总是Tang会赢
Jiang

 

 

分治法: 穷举出所有可能的取石头方案。

 

      算法思想,假设两个玩家分别pid编号为0和1。f(n, pid)表示当前一轮获胜的玩家编号(如果f=0,表示获胜玩家是0),其中n表示当前一轮的石头总数,pid表示当前一轮的玩家标号。

 

      考虑分治算法的思想,当前一轮的胜负结果取决于对下一轮各种情况的结果的统计。这个统计有两种情况:

      1、当前一轮玩家pid=0,那么他取石头的可能性为1,3或4。则下一轮玩家pid=1的情况有三种: f(n-1, 1),f(n-3,1),f(n-4,1)。如果这三种情况的f()函数值至少有一个是0,不妨假设f(n-1, 1)=0,根据题目条件的"他 们俩都足够聪明,总会采取最优的策略。 " ,那么当前一轮pid=0的玩家一定会选择取1个石头,结果也一定是pid=0赢。

           因此当pid=0时, f(n ,0)=f(n-1, 1)&f(n-3, 1)&f(n-4, 1)

      2、当前一轮玩家pid=1,那么下一轮三种情况下只要有一个f()函数值为1,则结果一定是pid=1赢。即

           当pid=1是, f(n ,1)=f(n-1, 0)|f(n-3, 0)|f(n-4, 0)

 

      下面是源代码:

Java代码  

1.  public class RecurStonePlay {  

2.      private static final String[] PLAYER={"Tang","Jiang"};  

3.      /** 

4.       * 当前轮到第pIdx个PLAYER从剩下的stoneNum块石头中取石头获胜的情况 

5.       * @param stoneNum 当前石头总数 

6.       * @param pIdx 取石头的人的ID 

7.       * @return 在当前这种情况下,能够取胜的PLAYER的ID 

8.       */  

9.      public static int turn(int stoneNum,int pIdx){  

10.           

11.         //当前只有1,3,4块石头时,则当前PLAYER[pIdx]能够取胜  

12.         if(stoneNum==1||stoneNum==3||stoneNum==4) return pIdx;  

13.         //当前只有2块石头时,则PLAYER[(pIdx+1)%2]能取胜  

14.         if(stoneNum==2) return (pIdx+1)%2;  

15.           

16.         //如果当前是PLAYER[0]取石头,则只要取1,3,4块三种情况中一种情况下能够取胜,则PLAYER[0]获胜。  

17.         //使用&运算,如果有一个0,则结果为0  

18.         if(pIdx==0)  

19.             return turn(stoneNum-1,(pIdx+1)%2)&turn(stoneNum-3,(pIdx+1)%2)&turn(stoneNum-4,(pIdx+1)%2);  

20.         //与上面情况相反,如果有一个1,则结果为1  

21.         else  

22.             return turn(stoneNum-1,(pIdx+1)%2)|turn(stoneNum-3,(pIdx+1)%2)|turn(stoneNum-4,(pIdx+1)%2);  

23.     }  

24.       

25.        //测试  

26.     public static void main(String[] args) {  

27.                 //石头总数为5块,PLAYER[0]开始先玩  

28.         System.out.println(PLAYER[RecurStonePlay.turn(5,0)]);  

29.     }  

30. }  

 

上面的方法时间复杂度太高,那么 是否能够通过对当前一轮石头总数的判断,可以知道是当前玩家赢(先手赢),还是下一轮的玩家赢(后手赢)呢? 

 

我们假设先手玩家是Player1,后手玩家是Player2。用上面的程序运行1-30个石头,并输出赢的情况(其中0代表Player1赢,1代表Player2赢)。

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  29  30

0  1   0  0  0  0  1   0  1    0    0    0    0    1    0    1     0    0    0    0    1     0    1     0    0    0    0      1    0    1

 

我们可以发现,凡是mod 7 余2或0的石头数目,都是后手赢,其他情况都是先手赢。 我们来证明一下:

 

(1) stoneNum=1,2,3,4时就不证明了。

(2) 当stoneNum=2的时候,是Player2赢。我们能够想到,如果Player1抽取石头后,能使得Player2玩的时候手头上的石头数量为2。那么Player1一定赢。也就是说(2+1=3),(2+3=5),(2+4=6)的石头数量一定导致Player1赢。

(3) 当stoneNum=7的时候,Player1无论抽1,3,4块石头中的任意情况,都会使得Player2玩的时候手头上的石头数量为6,4,3。这三种石头数量都是当前玩家赢(Player2赢)。因此7块石头一定是Player2赢。

(4) 当stoneNum=7的时候,情况与(2)相同。因此(7+1=8),(7+3=10),(7+4=11)的石头数量一定是Player1赢。

(5) 当stoneNum=9的时候,情况与(3)相同。因此9块石头一定是Player3赢。

(6) 依次下去,我们就能够得出这个结论:

 

策略:如果当前石头数量stoneNum%7==2||stoneNum%7==0,那么一定是后手赢。除此之外是先手赢。

 

<算法书>子数组换位问题

子数组换位问题

 

      设a[0:n-1]是一个有n个元素的数组,k(0<=k<=n-1)是一个非负整数。 试设计一个算法将子数组a[0:k]与a[k+1,n-1]换位。要求算法在最坏情况下耗时O(n),且只用到O(1)的辅助空间  (来自《计算机算法设计与分析》- 王晓东 - 第三章 - 递归与分治策略 - 课后习题 )

 

初步思考:最简单的方法就是循环(n-k-1)次,将a数组的末尾数字插入到a[0]之前。

具体做法:(1) 首先开辟一个额外空间temp用于存放每一次a数组的末尾数据。

               (2) temp <- a[n-1]

               (3) 将a[0: n-2] 每个数据都依次向后移动一位赋值给a[1: n-1]。

               (4) a[0] <- temp

               (5) 循环执行(2) -(4) 步 (n-k+1)次。

代价分析: 时间代价—— O((n-1)*(n-k+1))  即O(n^2)数量级;空间代价——O(1)

 

我们仔细想想还有没有更快的办法呢?试想一下,如果a[0 : k] 与 a[k+1 : n-1] 正好长度相等,则可以直接一一对应交换即可。 当然,这道题的难点就在于k并不一定是a数组的中间位置。即便如此,但是仍然可以交换:

 

     如果a[0 : k].length< a[k+1 : n-1].length, 则可以将a[0 : k] 与 a[k+1 : n-1] 中最后一部分大小相同的数据交换:

                                              |--------  a[k+1 : n-1] -----------| 

                              a[0:k]       a[k+1 : n-k-2]      a[n-k-1 : n-1]  

     其中  a[0:k] 与  a[n-k-1 : n-1]  长度相同,因此完全可以一一对应交换成:

                              |------  a[0 : n-k-2] -------|

                             a[0:k]        a[k+1 : n-k-2]    a[n-k-1 : n-1] 

     交换完成以后,则a[n-k-1 : n-1] 已经交换到位,而a[0 : n-k-2 ]还需要进一步这样递归交换。

 

源代码如下:

C代码  

1.  #include<stdio.h>  

2.    

3.  //交换数组的两段大小相等的范围的对应数据  

4.  //a[low1] <->a[low2]  a[low1+1]<->a[low2+1]  ... a[high1] <-> a[high2]  

5.  void swap(int a[],int low1,int high1,int low2,int high2){  

6.    

7.      int temp;  

8.      while(low1<=high1){  

9.          temp=a[low1];  

10.         a[low1]=a[low2];  

11.         a[low2]=temp;  

12.         low1++;  

13.         low2++;  

14.     }  

15. }  

16.   

17. //利用分治算法, 每次选择最小的数组进行换位  

18. void patition(int a[], int low, int k, int high){  

19.   

20.     if(low<high){  

21.         if((k-low+1)==(high-k))  

22.             swap(a,low,k,k+1,high);  

23.         else if((k-low+1)<(high-k)){  

24.             swap(a,low,k,low+high-k,high);  

25.             patition(a,low,k,low+high-k-1);  

26.         }  

27.         else{  

28.             swap(a,low,high+low-k-1,k+1,high);  

29.             patition(a,high+low-k,k,high);  

30.         }  

31.     }  

32.   

33. }  

34. //测试  

35. int main(){  

36.     int a[]={0,1,2,3,4,5,6,7,8,9,10,11,12,13};  

37.     patition(a,0,4,13);  

38.     for(int i=0;i<14;i++){  

39.         printf("%d  ",a[i]);  

40.     }  

41.     return 0;  

42. }  

         这样的时间复杂度为O(n),而且交换数据的时候只需要O(1)的额外空间。

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值