Fork me on GitHub

大约两个月前一位朋友问我一道他同事的面试题目:一个含有无重复元素的集合,找出它所有的子集。例如{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     }
View Code

这个算法采用了泛型的方式,可以求整型、字符串和字符数组的子组合问题。

然后自己还写了个判断一个数字是否是素数的程序,之所以再写了一个是因为自己以前写的算法时间表复杂度为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     }
View Code

这种算法使用了判断诸如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     }
View Code

的确,这种解法真的非常巧妙。

今天对这个解法进行了一些功能扩充,就是要求将具有最大各的子数组的起始位置找出来,具体代码如下:

 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     }
View Code

思路是一样的,只是因为要找出子数组的索引,所以对代码的基本结构进行了一些调整,但逻辑跟时间复杂度并没有发生变化。

还有一个算法题目是:自己实现一个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     }
View Code

以上的两种方法都是在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     }
View Code

 这里增加了对base和exp为负数时的讨论,以及base=0而exp<0的异常抛出。

暂时只想到了这么多,以后再想到了会及时的添加上去。

posted on 2014-06-27 10:09  SilentKnight  阅读(1584)  评论(0编辑  收藏  举报