如何測試洗牌程序

發表于:2015-08-18來源:uml.org.cn作者:不詳點擊數: 標簽:軟件測試
我們有一個程序,叫ShuffleArray(),是用來洗牌的,我見過N多千變萬化的ShuffleArray(),但是似乎從來沒人去想過怎么去測試這個算法。所以,我在面試中我經常會問應聘者如何測試 Shuffle

我希望本文有助于你了解測試軟件是一件很重要也是一件不簡單的事。

我們有一個程序,叫ShuffleArray(),是用來洗牌的,我見過N多千變萬化的ShuffleArray(),但是似乎從來沒人去想過怎么去測試這個算法。所以,我在面試中我經常會問應聘者如何測試 ShuffleArray(),沒想到這個問題居然難倒了很多有多年編程經驗的人。對于這類的問題,其實,測試程序可能比算法更難寫,代碼更多。而這個問題正好可以加強一下我在《我們需要專職的QA嗎?》中我所推崇的——開發人員更適合做測試的觀點。

我們先來看幾個算法(第一個用遞歸二分隨機抽牌,第二個比較偷機取巧,第三個比較通俗易懂

遞歸二分隨機抽牌

有一次是有一個朋友做了一個網頁版的撲克游戲,他用到的算法就是想模擬平時我們玩牌時用手洗牌的方式,是用遞歸+二分法,我說這個程序恐怕不對吧。他覺得挺對的,說測試了沒有問題。他的程序大致如下(原來的是用Javascript寫的,我在這里憑記憶用C復現一下):

  //遞歸二分方法 
  const size_t MAXLEN = 10; 
  const char TestArr[MAXLEN] = {'A','B','C','D','E','F','G','H','I','J'};
  static char RecurArr[MAXLEN]={0}; 
  static int cnt = 0; 
  void ShuffleArray_Recursive_Tmp(char* arr, int len) 
  {     
     if(cnt > MAXLEN || len <=0){         
        return;     
     }       
     int pos = rand() % len;     
     RecurArr[cnt++] = arr[pos];     
     if (len==1) return;     
     ShuffleArray_Recursive_Tmp(arr, pos);     
     ShuffleArray_Recursive_Tmp(arr+pos+1, len-pos-1); 
  }   
  void ShuffleArray_Recursive(char* arr, int len) 
  {     
     memset(RecurArr, 0, sizeof(RecurArr));     
     cnt=0;     
     ShuffleArray_Recursive_Tmp(arr, len);     
     memcpy(arr, RecurArr, len); 
  }   
  void main() 
  {     
      char temp[MAXLEN]={0};     
      for(int i=0; i<5; i++) 
	  {         
         strncpy(temp, TestArr, MAXLEN);         
         ShuffleArray_Recursive((char*)temp, MAXLEN);     
      } 
  }

隨便測試幾次,還真像那么回事:

  第一次:D C A B H E G F I J 
  第二次:A G D B C E F J H I 
  第三次:A B H F C E D G I J 
  第四次:J I F B A D C E H G 
  第五次:F B A D C E H G I J

快排Hack法

讓我們再看一個hack 快排的洗牌程序(只看算法,省去別的代碼):

  int compare( const void *a, const void *b ) 
  {  
     return rand()%3-1; 
   }   
  void ShuffleArray_Sort(char* arr, int len) 
  {  
     qsort( (void *)arr, (size_t)len, sizeof(char), compare ); 
   }

運行個幾次,感覺得還像那么回事:

  第一次:H C D J F E A G B I 
  第二次:B F J D C E I H G A 
  第三次:C G D E J F B I A H 
  第四次:H C B J D F G E I A 
  第五次:D B C F E A I H G J

看不出有什么破綻。

大多數人的實現

下面這個算法是大多數人的實現,就是for循環一次,然后隨機交換兩個數

  void ShuffleArray_General(char* arr, int len) 
  {  
     const int suff_time = len;     
	 for(int idx=0; idx<suff_time; idx++) 
	 {      
	    int i = rand() % len;         
		int j = rand() % len;         
		char temp = arr[i];         
		arr[i] = arr[j];         
		arr[j] = temp;     
	  } 
   }

跑起來也還不錯,洗得挺好的。

  第一次:G F C D A J B I H E 
  第二次:D G J F E I A H C B 
  第三次:C J E F A D G B H I 
  第四次:H D C F A E B J I G 
  第五次:E A J F B I H G D C 

但是上述三個算法哪個的效果更好?好像都是對的。一般的QA或是程序員很有可能就這樣把這個功能Pass了。但是事情并沒有那么簡單……

如何測試

在做測試之前,我們還需要了解一下一個基本知識——PC機上是做不出真隨機數的,只能做出偽隨機數。真隨機數需要硬件支持。但是不是這樣我們就無法測試了呢,不是的。我們依然可以測試。

我們知道,洗牌洗得好不好,主要是看是不是夠隨機。那么如何測試隨機性呢?

試想,我們有個隨機函數rand()返回1到10中的一個數,如果夠隨機的話,每個數返回的概率都應該是一樣的,也就是說每個數都應該有10分之1的概率會被返回。

一到概率問題,我們只有一個方法來做測試,那就是用統計的方式。也就是說,你調用rand()函數100次,其中,每個數出現的次數大約都在10次左右。(注意:我用了左右,這說明概率并不是很準確的)不應該有一個數出現了15次以上,另一個在5次以下,要是這樣的話,這個函數就是錯的。

舉一反三,測試洗牌程序也一樣,需要通過概率的方式來做統計,是不是每張牌出現在第一個位置的次數都是差不多的。

于是,這樣一來上面的程序就可以很容易做測試了。

下面是測試結果(測試樣本1000次——列是每個位置出現的次數,行是各個字符的統計,出現概率應該是1/10,也就是100次):

遞歸隨機抽牌的方法

很明顯,這個洗牌程序太有問題。算法是錯的!

       1    2    3    4    5    6    7    8    9    10 
  ---------------------------------------------------- 
  A | 101  283  317  208   65   23    3    0    0    0 
  B | 101  191  273  239  127   54   12    2    1    0 
  C | 103  167  141  204  229  115   32    7    2    0 
  D | 103  103   87  128  242  195  112   26    3    1 
  E | 104   83   62   67  116  222  228   93   22    3 
  F |  91   58   34   60   69  141  234  241   65    7 
  G |  93   43   35   19   44  102  174  274  185   31 
  H |  94   28   27   27   46   68   94  173  310  133 
  I | 119   27   11   30   28   49   64   96  262  314 
  J |  91   17   13   18   34   31   47   88  150  511 

快排Hack法

看看對角線(從左上到右下)上的數據,很離譜!所以,這個算法也是錯的。

        1    2    3    4    5    6    7    8    9    10
  ----------------------------------------------------- 
  A |   74  108  123  102   93  198   40   37   52  173 
  B |  261  170  114   70   49   28   37   76  116   79 
  C |  112  164  168  117   71   37   62   96  116   57 
  D |   93   91  119  221  103   66   91   98   78   40 
  E |   62   60   82   90  290  112   95   98   71   40 
  F |   46   60   63   76   81  318   56   42   70  188 
  G |   72   57   68   77   83   39  400  105   55   44 
  H |   99   79   70   73   87   34  124  317   78   39 
  I |  127  112  102   90   81   24   57   83  248   76 
  J |   54   99   91   84   62  144   38   48  116  264 

大多數人的算法

我們再來看看大多數人的算法。還是對角線上的數據有問題,所以,還是錯的。

        1    2    3    4    5    6    7    8    9    10 
  ----------------------------------------------------- 
  A |  178   98   92   82  101   85   79  105   87   93 
  B |   88  205   90   94   77   84   93   86  106   77 
  C |   93   99  185   96   83   87   98   88   82   89 
  D |  105   85   89  190   92   94  105   73   80   87 
  E |   97   74   85   88  204   91   80   90  100   91 
  F |   85   84   90   91   96  178   90   91  105   90 
  G |   81   84   84  104  102  105  197   75   79   89 
  H |   84   99  107   86   82   78   92  205   79   88 
  I |  102   72   88   94   87  103   94   92  187   81 
  J |   87  100   90   75   76   95   72   95   95  215  

正確的算法

下面,我們來看看性能高且正確的算法—— Fisher_Yates算法

  void ShuffleArray_Fisher_Yates(char* arr, int len) 
  {  
     int i = len, j;     
	   char temp;       
	   if ( i == 0 ) return;     
	   while ( --i ){       
	      j = rand() % (i+1);         
		  temp = arr[i];         
		  arr[i] = arr[j];         
		  arr[j] = temp;     
	   } 
   }

這個算法不難理解,看看測試效果(效果明顯比前面的要好):

        1    2    3    4    5    6    7    8    9    10 
  ----------------------------------------------------- 
  A |  107   98   83  115   89  103  105   99   94  107 
  B |   91  106   90  102   88  100  102   97  112  112 
  C |  100  107   99  108  101   99   86   99  101  100 
  D |   96   85  108  101  117  103  102   96  108   84 
  E |  106   89  102   86   88  107  114  109  100   99 
  F |  109   96   87   94   98  102  109  101   92  102 
  G |   94   95  119  110   97  112   89  101   89   94 
  H |   93  102  102  103  100   89  107  105  101   98 
  I |   99  110  111  101  102   79  103   89  104  102 
  J |  105  112   99   99  108  106   95   95   99   82 

但是我們可以看到還是不完美。因為我們使用的rand()是偽隨機數,不過已經很不錯的。最大的誤差在20%左右。

我們再來看看洗牌100萬次的統計值,你會看到誤差在6%以內了。這個對于偽隨機數生成的程序已經很不錯了。

        1       2     3       4      5      6      7      8     9      10 
  ------------------------------------------------------------------------- 
  A | 100095  99939 100451  99647  99321 100189 100284  99565 100525  99984 
  B |  99659 100394  99699 100436  99989 100401  99502 100125 100082  99713 
  C |  99938  99978 100384 100413 100045  99866  99945 100025  99388 100018 
  D |  99972  99954  99751 100112 100503  99461  99932  99881 100223 100211 
  E | 100041 100086  99966  99441 100401  99958  99997 100159  99884 100067 
  F | 100491 100294 100164 100321  99902  99819  99449 100130  99623  99807 
  G |  99822  99636  99924 100172  99738 100567 100427  99871 100125  99718 
  H |  99445 100328  99720  99922 100075  99804 100127  99851 100526 100202 
  I | 100269 100001  99542  99835 100070  99894 100229 100181  99718 100261 
  J | 100268  99390 100399  99701  99956 100041 100108 100212  99906 100019  

如何寫測試案例

測試程序其實很容易寫了。就是,設置一個樣本大小,做一下統計,然后計算一下誤差值是否在可以容忍的范圍內。比如:

樣本:100萬次

最大誤差:10%以內

平均誤差:5%以內 (或者:90%以上的誤差要小于5%)

注意

其實,以上的測試只是測試了牌在各個位置的概率。這個還不足夠好。因為還可能會現在有Patten的情況。如:每次洗牌出來的都是一個循環順序數組。這完全可以滿足我上面的測試條件。但是那明顯是錯的。所以,還需要統計每種排列的出現的次數,看看是不是均勻。但是,如果這些排列又是以某種規律出現的呢?看來,這沒完沒了了。

測試的確是一個很重要,并不簡單的事情。謝謝所有參與討論的人。

附錄

之前忘貼了一個模擬我們玩牌洗牌的算法,現補充如下:

  void ShuffleArray_Manual(char* arr, int len) 
  {  
     int mid = len / 2;       
	 for (int n=0; n<5; n++)
	 {       
	     //兩手洗牌         
		 for (int i=1; i<mid; i+=2)
		 {         
		     char tmp = arr[i];             
			 arr[i] = arr[mid+i];             
			 arr[mid+i] = tmp;         
		  }           
		 //隨機切牌         
		 char *buf = (char*)malloc(sizeof(char)*len);           
		 for(int j=0; j<5; j++) 
		 {           
		    int start= rand() % (len-1) + 1;             
			int numCards= rand()% (len/2) + 1;               
			if (start + numCards > len )
			{              
			   numCards = len - start;             
			 }               
			memset(buf, 0, len);             
			strncpy(buf, arr, start);             
			strncpy(arr, arr+start, numCards);             
			strncpy(arr+numCards, buf, start);         
		 }         
		 free(buf);       
	  } 
   }

我們來看看測試結果:(10萬次)效果更好一些,誤差在2%以內了。

        1       2     3       4      5      6      7      8     9      10 
  ------------------------------------------------------------------------- 
  A |  10002   9998   9924  10006  10048  10200   9939   9812  10080   9991 
  B |   9939   9962  10118  10007   9974  10037  10149  10052   9761  10001 
  C |  10054  10100  10050   9961   9856   9996   9853  10016   9928  10186 
  D |   9851   9939   9852  10076  10208  10003   9974  10052   9992  10053 
  E |  10009   9915  10050  10037   9923  10094  10078  10059   9880   9955 
  F |  10151  10115  10113   9919   9844   9896   9891   9904  10225   9942 
  G |  10001  10116  10097  10030  10061   9993   9891   9922   9889  10000 
  H |  10075  10033   9866   9857  10170   9854  10062  10078  10056   9949 
  I |  10045   9864   9879  10066   9930   9919  10085  10104  10095  10013 
  J |   9873   9958  10051  10041   9986  10008  10078  10001  10094   9910  

原文轉自:http://www.uml.org.cn/Test/201302201.asp

国产97人人超碰caoprom_尤物国产在线一区手机播放_精品国产一区二区三_色天使久久综合给合久久97