大约两个月前一位朋友问我一道他同事的面试题目:一个含有无重复元素的集合,找出它所有的子集。例如{1,2}的所有集合是{}, {1}, {2}, {1, 2}.
当时我预料到了这道题目的算法时间复杂度为O(2^n), 但是并没有写出代码来。前两天无意间又试着做了一下这道题目,然后接受查找的资料,原来这是一个专门的算法问题。学名为powerset.
代码如下:
1 /* 2 * 该算法的基本原理是:将集合分成前后两个部分: 3 * 前一部分是已经固定了的,后一部分为即将固定的, 4 * 然后将即将固定的添加到已经固定的集合的后面。 5 * 然后采用递归的方式,欲求{1,n-1},必先求{2, n-2}, 6 * 之后仿效类推,直到先求出{n-1, 1} 7 */ 8 public static <T> Set<Set<T>> getPowerSet(Set<T> originalSet) { 9 Set<Set<T>> sets = new HashSet<Set<T>>(); 10 if (originalSet.isEmpty()) { 11 sets.add(new HashSet<T>()); 12 return sets; 13 } 14 List<T> list = new ArrayList<T>(originalSet); 15 T head = list.get(0); 16 Set<T> rest = new HashSet<T>(list.subList(1, list.size())); 17 for (Set<T> set : getPowerSet(rest)) { 18 Set<T> newSet = new HashSet<T>(); 19 newSet.add(head); 20 newSet.addAll(set); 21 sets.add(newSet); 22 sets.add(set); 23 } 24 return sets; 25 }
这个算法采用了泛型的方式,可以求整型、字符串和字符数组的子组合问题。
然后自己还写了个判断一个数字是否是素数的程序,之所以再写了一个是因为自己以前写的算法时间表复杂度为O(n^1/2),这个还是有可以优化的地方,比如下面这种算法:
1 public static boolean isPrime(long n) { 2 if(n < 2) { 3 return false; 4 } 5 if(n == 2 || n== 3){ 6 return true; 7 } 8 if(n%2 == 0 || n%3==0){ 9 return false; 10 } 11 long sqrt=(long) (Math.sqrt(n)+1); 12 for (long i = 6L; i <=sqrt; i+=6L) { 13 if(n%(i-1) == 0 || n%(n+1)==0){ 14 return false; 15 } 16 } 17 return true; 18 }
这种算法使用了判断诸如2, 3, 5, 7, 11, 13...这些比较小的素数是否是n的因子来判断n是否是素数,明显地提升了算法的时间复杂度。
还有一道题目是求数组的最大和的问题。常见的解法是采用动态规划跟记忆算法,采用O(n^2)的时间来循环求解,但是这里有一个复杂度为O(n)的解法,代码如下:
1 /*该算法的时间复杂度为: O(n)*/ 2 public static int maxSubsum(int[] array) { 3 int maxSum=0; 4 int tempSum=0; 5 for (int i = 0; i < array.length; i++) { 6 tempSum+=array[i]; 7 if(tempSum > maxSum){ 8 maxSum=tempSum; 9 }else if (tempSum < 0) { 10 tempSum=0; 11 } 12 } 13 return maxSum; 14 }
的确,这种解法真的非常巧妙。
今天对这个解法进行了一些功能扩充,就是要求将具有最大各的子数组的起始位置找出来,具体代码如下:
1 /* 该算法的时间复杂度为: O(n) */ 2 public static int maxSubArraySum2(int[] array) { 3 if (array == null) { 4 return -1; 5 } 6 int maxSum = 0; 7 int tempSum = 0; 8 int start = 0, end = 0, curStart = 0; 9 for (int i = 0; i < array.length; i++) { 10 if (tempSum < 0) { 11 tempSum = array[i]; 12 curStart = i; 13 } else { 14 tempSum += array[i]; 15 } 16 if (tempSum > maxSum) { 17 maxSum = tempSum; 18 start = curStart; 19 end = i; 20 } 21 } 22 System.out.println("Start---" + start + ", End---" + end); 23 return maxSum; 24 }
思路是一样的,只是因为要找出子数组的索引,所以对代码的基本结构进行了一些调整,但逻辑跟时间复杂度并没有发生变化。
还有一个算法题目是:自己实现一个power函数。在java的Math库的,有pow(double base, double exp)方法,返回base的exp次方。一般有两种算法,先看一下只对int进行处理的循环和递归算法:
1 /* when exp is bigger than zero */ 2 public static int ipow1(int base, int exp) { 3 int result = 1; 4 while (exp != 0) { 5 if ((exp & 1) == 1) { 6 result *= base; 7 } 8 exp >>= 1; 9 base *= base; 10 } 11 return result; 12 } 13 14 /* when exp is bigger than zero */ 15 public static long ipow2(long base, long exp) { 16 if (exp == 0) { 17 return 1; 18 } 19 if (exp == 1) { 20 return base; 21 } 22 23 if (exp % 2 == 0) { 24 long half = ipow2(base, exp / 2); 25 return half * half; 26 } else { 27 long half = ipow2(base, (exp - 1) / 2); 28 return base * half * half; 29 } 30 }
以上的两种方法都是在base和exp不小于0的情况下进行的讨论。
后者是递归的实现方式:分成了exp是奇数还是偶数两种情况进行讨论。关于递归,有一个经典名言:“In order to understand recursion, you must first understand recursion.”。就像GNU的定义一样,自己去理解吧。
下面看一下对base和exp可以是任意数值(exp是int)的情况下循环方式的实现:
1 /* when exp or base could be smaller than zero */ 2 public static double ipow3(double base, int exp) { 3 if (base == 0.0D && exp < 0) { 4 throw new ArithmeticException("Dividend can't be zero"); 5 } 6 int tmpExp = Math.abs(exp); 7 double temBase = Math.abs(base); 8 double tempResult = 1; 9 while (tmpExp != 0) { 10 if ((tmpExp & 1) == 1) { 11 tempResult *= temBase; 12 } 13 tmpExp >>= 1; 14 temBase *= temBase; 15 } 16 boolean isBaseNegative = base < 0; 17 boolean isExpNegative = exp < 0; 18 boolean isExpEven = (exp % 2 == 0); 19 if (!isBaseNegative && !isExpNegative) { 20 return tempResult; 21 } else if (!isBaseNegative && isExpNegative) { 22 return 1 / tempResult; 23 } else if (isBaseNegative && !isExpNegative) { 24 if (isExpEven) { 25 return tempResult; 26 } else { 27 return -tempResult; 28 } 29 } else { 30 if (isExpEven) { 31 return 1 / tempResult; 32 } else { 33 return -1 / tempResult; 34 } 35 } 36 }
这里增加了对base和exp为负数时的讨论,以及base=0而exp<0的异常抛出。
暂时只想到了这么多,以后再想到了会及时的添加上去。