对数器的使用
一、对数器的作用
对数器是通过用大量测试数据来验证算法是否正确的一种方式。在算法笔试的时候,我们经常只能确定我们写出的算法在逻辑上是大致正确的,但是谁也不能一次性保证绝对的正确。特别是对于一些复杂的题目,例如贪心算法,我们往往无法在有限时间内用数学公式来推导证明我们程序的正确性。而且在线的OJ一般只会给出有数的几个简单的samples
,可能我们的算法在这些简单的samples
偶然通过了,但是在一些复杂的samples
处却出现了问题。这时我们无法使用复杂的samples
来分析调试我们的代码,人工设计样例来测试代码的效率又太低,而且不能确保考虑各种特殊情况。因此,能随机产生不同情况的数据的对数器就派上了用场。
对数器需要两样东西:
- 绝对正确的方法
- 能产生大量随机样例的随机发生器
看到这里,有些童鞋有疑问了。既然我们知道了绝对正确的方法,直接用这个方法不就好了嘛?
请注意,算法追求的不是解决问题,而是高效率的解决问题。对于一个数组的排序,如果笔试中要求的时间复杂度是O(NlogN),但是你却写了一个冒泡排序的算法交上去了,这时OJ就会提示:Time Limit Exceeded。
而在对数器中,我们要求的绝对正确的算法是没有时间和空间复杂度的限制的,唯一的要求是确保绝对正确。因为只有绝对正确,我们才能通过样例的比对,发现我们的代码是在哪里出了错误。
二、对数器的使用
【使用步骤】
- 有一个你想要测的方法
a
; - 实现一个绝对正确但是复杂度不好的方法
b
; - 实现一个随机样本产生器;
- 实现对比算法
a
和b
的方法; - 把方法
a
和方法b
比对多次来验证方法a
是否正确; - 如果有一个样本使得比对出错,打印样本分析是哪个方法出错;
- 当样本数量很多时比对测试依然正确,可以确定方法
a
已经正确。
其中要注意以下几点:
- 随机产生的样本应该是小数据集,但是要进行多次(10w+)的对比。小数据集是因为方便对比分析,多次比对是要覆盖所有的随机情况。
- 算法
b
要保持正确性。
【示例】
这里使用冒泡排序进行演示
/** * 冒泡排序的对数器 */ public class BubbleSort { /** * 1.有一个你想要测的方法a; * @param arr */ public static void bubbleSort(int[] arr) { if (arr == null || arr.length < 2) { return; } for (int end = arr.length - 1; end > 0; end--) { for (int i = 0; i < end; i++) { if (arr[i] > arr[i + 1]) { swap(arr, i, i + 1); } } } } public static void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } /** * 2.实现一个绝对正确,可以复杂度不是很好的方法b * 可以直接用一些库函数来进行测试 */ public static void rightMethod(int[] arr) { Arrays.sort(arr); } /** * 3.实现一个随机样本产生器 */ public static int[] generateRandomArray(int size, int value) { //Math.random() -> double [0,1) //(int) ((size + 1) * Math.random()) -> [0,size]整数 // 生成长度随机[0, size]的数组 int[] arr = new int[(int) ((size + 1) * Math.random())]; for (int i = 0; i < arr.length; i++) { // 一个随机数减去另一个随机数,生成[-value, value]的随机数 arr[i] = (int) ((value + 1) * Math.random() - value * Math.random()); } return arr; } /** * 4.实现比对的方法 * 判断两个数组是否相等 */ public static boolean isEqual(int[] arr1, int[] arr2) { if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { return false; } if (arr1 == null && arr2 == null) { return true; } if (arr1.length != arr2.length) { return false; } for (int i = 0; i < arr1.length; i++) { if (arr1[i] != arr2[i]) { return false; } } return true; } public static void printArray(int[] arr) { if (arr == null) { return; } for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } public static int[] copyArray(int[] arr) { if (arr == null) { return null; } int[] res = new int[arr.length]; for (int i = 0; i < arr.length; i++) { res[i] = arr[i]; } return res; } public static void main(String[] args) { int testTime = 500000; int size = 10; int value = 100; boolean succeed = true; for (int i = 0; i < testTime; i++) { int[] arr1 = generateRandomArray(size, value); int[] arr2 = copyArray(arr1); int[] arr3 = copyArray(arr1); bubbleSort(arr1); rightMethod(arr2); //6.如果有一个样本使得比对出错,打印样本分析是哪个方法出错; if (!isEqual(arr1, arr2)) { succeed = false; printArray(arr3); break; } } System.out.println(succeed ? "Nice!" : "error----"); int[] arr = generateRandomArray(size, value); printArray(arr); bubbleSort(arr); printArray(arr); } }
【小提示】
很多童鞋进行笔试前,都是背一些记在小本本上的代码,然后匆匆上阵。写出的算法的正确性完全靠OJ的判断,当程序卡在一个2000行的数组样例处出现错误时,就完全傻了......这T喵叫我怎么去进行调试分析啊。而有对数器的小伙伴就不一样了,由于使用的都是小样本,出现错误时也方面进行分析。而且进行了多次测试,确保覆盖了所有的特殊情况。因此笔试前我们可以准备一些对数器模版,如数组排序的对数器,链表的对数器等等。