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. }
|
/**************************************************************** 代码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个并打乱次序,如何找出那两个数?(不准用位图) 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次。
|
http://hxraid.iteye.com/blog/627859 题目:有一个江洋大盗,他每次写信都是从一张报纸上剪下单词,再把单词贴在信上。假如某张报纸的单词保存在vector<string>paper 中,而信的单词保存在vector<string>letter 中,写一个算法程序判别该报纸可不可以生成该信?
对比一些方法: 这里假设paper:(m个单词,每个单词平均d个字母),letter:(n个单词,每个单词平均d个字母)。对于中文词语而言,一般2~4个字左右,对于英文单词,d就要大一点,但基本上也是常数。因此d远远小于m,下面比较的时候就不考虑单词中每个字符间的比较了。
|
http://hxraid.iteye.com/blog/649831 题目:在一个文件中有 10G 个整数,乱序排列,要求找出中位数。内存限制为 2G。只写出思路即可(内存限制为 2G的意思就是,可以使用2G的空间来运行程序,而不考虑这台机器上的其他软件的占用内存)。
分析: 既然要找中位数,很简单就是排序的想法。那么基于字节的桶排序是一个可行的方法 (请见《桶排序 》):
整个过程的时间复杂度在O(n)的线性级别上(没有任何循环嵌套)。但主要时间消耗在第一步的第二次内存-磁盘数据交换上,即10G数据分255个文件写回磁盘上。一般而言,如果第二步过后,内存可以容纳下存在中位数的某一个文件的话,直接快排就可以了。关于快排的效率,可以看看我博客中的数据《基于比较的内部排序总结 》。
|
数据规模分析
不考虑操作系统的区别,通常将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. for( int i = 0; i < RES_ARR_SIZE; ++i ){ 5. int idx = i; 6. //选择最大的元素 7. for( int 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. for( int 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. }
另外,快慢指针在解决单链表环问题的时候是非常有用的,具体请参见《★经典问题—链表中的环问题 》
|
问题是这样的:一共有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取. 当然,他们俩都足够聪明,总会采取最优的策略。
分治法: 穷举出所有可能的取石头方案。
算法思想,假设两个玩家分别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)的额外空间。
|