題目:
輸入一個數字n 如果n為偶數則除以2,若為奇數則加1或者減1,直到n為1,求最少次數 寫出一個函數
首先,這道題肯定可以用動態規划來解,
n為整數時,n的解為 n/2 的解加1
n為奇數時,n的解為 (n+1)/2 和 (n-1)/2 的解中較小的解加2
通過這個思路,我們可以自底向上依次計算出n的解,代碼如下
public static int getNum(int n) { if(n<1) { return 0; } int[] res = new int[n+1]; res[0] = 0; res[1] = 0; for(int i=2;i<=n;i++) { if((i&1) == 0) { res[i] = res[i/2] + 1; }else { res[i] = Math.min(res[(i+1)/2], res[(i-1)/2]) + 2; } } return res[n]; }
通過上面的思路可以得到問題的解,但是由於是自底向上依次計算n的解,所以有很多不必要的計算,時間效率和空間效率都不高。
比如,當計算n=100時,如果已經知道n=50的解,那么就可以得出n=100的解,所以n=51到n=99都是沒有必要計算的。
如果仍然通過自底向上計算,那么想要忽略51到99這一區間的數字的計算是比較麻煩的,如果是自頂向下計算則容易做到,通過n可以確定只要計算 n/2,(n-1)/2 , (n+1)/2,這三個數就 行,利用遞歸來做代碼如下
public static int getNum2(long n) { if(n<=1) { return 0; } if((n&1) == 0) { return getNum2(n/2) + 1; }else { long a = (n-1) / 2; long b = (n+1) / 2; return Math.min(getNum2(a), getNum2(b)) + 2; } }
遞歸來做這道題簡單明了。
遞歸也有遞歸的壞處,首先遞歸最可能問題就是遞歸深度的問題,很可能造成棧溢出。雖然對這道題來說,幾乎不會出現這個問題,但是在用遞歸做其他問題的時候一定要考慮到這一點。
至於為什么這道題不會造成棧溢出,自己想吧
所有的遞歸算法都可以轉化成非遞歸算法,這道題也一樣,同樣的,遞歸時還有一個小問題,就是它沒有復用子問題的解,對於每個子問題,不管之前是否已經計算過解,都要再重新計算一次,轉化成非遞歸算法時可以一並解決這個問題,代碼如下
public static int getNum3(long n) { MyTask task = new MyTask(n); ForkJoinPool pool = new ForkJoinPool(); int res = pool.invoke(task); pool.shutdown(); return res; } static class MyTask extends RecursiveTask<Integer> { private long number; private static final Map<Long,Integer> map = new ConcurrentHashMap<>(); public MyTask(long number) { super(); this.number = number; } private synchronized void put(Long a,Integer b) { if(map.containsKey(a)) { System.out.println("had existed!"); }else { map.put(a, b); } } private Integer get(Long a) { System.out.println("success"); return map.get(a); } @Override protected Integer compute() { if(number<=1) { put(number, 0); return 0; }else if(map.containsKey(number)) { return get(number); } int res = 0; if((number&1) == 0) { MyTask task = new MyTask(number / 2); try { res = task.fork().get() + 1; put(number, res); } catch (Exception e) { e.printStackTrace(); } return res; }else { MyTask task1 = new MyTask((number-1) / 2); MyTask task2 = new MyTask((number+1) / 2); try { int a = task1.fork().get() + 2; int b = task2.fork().get() + 2; res = Math.min(a,b); put(number, res); } catch (Exception e) { e.printStackTrace(); } return res; } } }
這個解法是將第二種解法的遞歸算法轉化成了非遞歸算法,同時保存了之前已經計算過的子問題的解,並且用到了java中的fork/join框架,至於為什么要用這個框架,原因是,不用它我不知道怎么把這個遞歸算法轉化成非遞歸算法,望各路大神指點指點
對於這道題來說,第二種方式最快,第三種方式其次,最后是第一種方式,而且第一種方式計算的n的最大值,也遠遠小於后兩種。
好了,就先寫到這,人生的第一篇博文就此誕生!慶祝!雖然寫的我自己看了都覺得很爛,但是一步一步來嘛