今天我們來聊聊遞歸喝汽水問題

君子食無求飽,居無求安,敏於事而慎於言,就有道而正焉,可謂好學也已

再識帝龜

大家好,我是帝龜,好久不見!!!
在這裏插入圖片描述
關於我的基本介紹,大家可以到以下鏈接中找尋我的身影:

面試題警告

可能是由於本帝龜平時非常喜歡喝汽水,所以面試官似乎經常喜歡用喝汽水的問題當做面試題來考考大家對於本帝龜的熟悉程度。如這是我朋友最近幾天碰到的面試題:

1元錢一瓶汽水,喝完之後兩個空瓶換一瓶汽水。 問:若你有N元錢,你最多能喝多少瓶汽水?

又或者說,我們來看一道華爲的面試題:

一個人買汽水,一塊錢一瓶汽水,三個瓶蓋可以換一瓶汽水,兩個空瓶可以換一瓶汽水,問20塊錢可以買多少汽水?

題1解

我們先來看題1:

1元錢一瓶汽水,喝完之後兩個空瓶換一瓶汽水。 問:若你有N元錢,你最多能喝多少瓶汽水?

看到這個題目,可能大家覺得非常簡單,然後就飛快的在草稿紙上計算了起來:

錢數(元) - 	   瓶數
  1            	1
  2            	3=2+1
  3            	5=3+1+1
  4            	7=4+2+1
  ...			...
  n			    2n-1

經過以上分析,無論是通過列出一元一次方程還是通過觀察得出這是一個首項爲1,公差爲2的等差數列,都可以輕而易舉地得出:當你有N元錢時,如果你不怕撐的話,最多能喝2N-1瓶汽水。

但是如果讓你用代碼解,你會怎麼解呢?

我們試着來分析一下這個題目(暫時先不考慮換不完問題):
首先我有N元錢,買了N瓶汽水,喝完汽水剩下N個空瓶;
然後用N個空瓶,換了N/2瓶汽水,喝完汽水剩下N/2個空瓶;
然後用N/2個空瓶,換了(N/2)/2瓶汽水,喝完汽水剩下(N/2)/2個空瓶;

最後用2個空瓶,換了一瓶汽水,喝完汽水剩下一個空瓶(換不了,扔掉)

這樣試着一分析,這似乎是一個循環往復,週而復始的問題:拿到汽水,再用空瓶換汽水。而且當只有一瓶汽水的時候兌換結束,這不就是一箇中止條件嗎?好的,分析完畢,我們可以刷刷刷寫下以下代碼:

/**
     * 計算最多能喝多少瓶汽水,並返回這個值
     * @param n 表示有n瓶汽水
     * @return
     */
    private static int soda(int n) {

        if (n == 1) {
            // 遞歸終止條件,當還有一瓶飲料的時候只能喝一瓶,不能再換了
            return 1;
        }else if(n % 2 == 0) {
            // 偶數瓶,這時空瓶剛好能夠換完
            return n + soda(n/2);
        }else {
            // 奇數瓶,這一次剩下的一個空瓶子剛好和最後一次的一個空瓶子能換一瓶飲料,所以要+1
            return n + 1 + soda(n/2);
        }
    }

至於爲什麼奇數瓶時要+1,我們可以試着畫一下當有3瓶飲料時的兌換圖:
在這裏插入圖片描述
要是還是無法理解的話,我們再試着畫一下當有5瓶飲料時的兌換圖:
在這裏插入圖片描述
這樣就可以發現當有奇數瓶時再複雜的情況無非也就是3瓶時的疊加。
我們再試着在main方法裏面調用一下,來檢驗一下結果:

public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        for(;;) {
            System.out.print("請輸入你的金額:");
            //從鍵盤輸入我的錢數
            int money = scanner.nextInt();
            // 第一次的錢能喝的汽水瓶數(一元錢一瓶汽水)
            int n = money / 1;
            // 我能喝的汽水數
            int sum = soda(n);
            System.out.println("當我有" + money + "元時,總共能喝" + sum + "瓶飲料");
        }
    }

運行main方法,控制檯結果如下:
在這裏插入圖片描述
簡直完美!!!當然之前在我的這篇博客: - 遞歸和循環之間不得不說的故事中就提到過,遞歸能解決的問題循環一般也能解決,如:

	/**
     * 用循環來計算最多能喝多少瓶汽水
     * @param n
     * @return
     */
    private static int soda2(int n) {

        // 總共能喝的飲料瓶數
        int sum = n;

        // 當n==1時循環結束,所以n>1
        for (; n > 1; n = n/2) {
            if (n % 2 == 0) {
                sum += n/2;
            }else {
                // 此次的飲料瓶數是奇數瓶就補1
                sum += (n/2 + 1);
            }
        }
        
        return sum;
    }

我們將上面main方法中計算喝汽水的方法改爲這個方法:

// 我能喝的汽水數
int sum = soda2(n);

運行main方法,從控制檯得到結果如下:
在這裏插入圖片描述
結果是一樣的。不過我突然想到,既然之前一開始已經推導出了:當你有N元錢時,如果你不怕撐的話,最多能喝2N-1瓶汽水。 那我還這麼麻煩幹甚?
在這裏插入圖片描述
所以我們可以很輕鬆的寫出第三種方法:

	/**
     * 直接用推導出的數學公式計算
     * @param n
     * @return
     */
    private static int soda3(int n) {
        return 2 * n - 1;
    }

再調用測試一下:

// 我能喝的汽水數
int sum = soda3(n);

運行main方法:
在這裏插入圖片描述
如此簡單的代碼,結果竟然是一模一樣的,讓我歎服。數學不愧是一切科學的基礎,被譽爲科學的皇后。以後要是讓我看見數學好的大哥:
在這裏插入圖片描述

大招

經過上述的分析,在考慮到本博主數學不好不能保證:每次遇到問題都能推導出數學公式的前提下,有這樣兩個問題:

  • 奇數瓶時的+1不太好理解,導致代碼可讀性較差
  • 作爲本文主角的遞歸似乎顯得有些弱雞,和循環的解法相比似乎沒有什麼兩樣,而且遞歸的空間複雜度要更高又要入棧,又要出棧的能不高嗎?

難道遞歸真的如此弱雞嗎?這個時候,我突然想起了某位名人說過的話:

To Iterate is Human, to Recurse, Divine.(人理解迭代,神理解遞歸)

竊以爲,遞歸的一大優勢就在於描述:能夠用簡單、有限的語句,描述複雜、龐大的問題。而後採用分而治之的思想,將一個大問題拆解成一個個元問題並加以解決。好了,我要放大招了!

我們再試着回顧一下之前對於這個喝汽水問題的分析:

首先我有N元錢,買了N瓶汽水,喝完汽水剩下N個空瓶;
然後用N個空瓶,換了N/2瓶汽水,喝完汽水剩下N/2個空瓶;
然後用N/2個空瓶,換了(N/2)/2瓶汽水,喝完汽水剩下(N/2)/2個空瓶;

然後用2個空瓶,換了一瓶汽水
最後喝完汽水剩下一個空瓶(換不了,扔掉)

有什麼新的發現嗎?

是的,我們可以發現在每次週而復始的過程中:不但有汽水這個變量,還有空瓶這個變量。 我們試着引入空瓶這個變量,刷刷刷:

	/**
     * 用遞歸計算出最多能喝多少瓶汽水
     * @param n 飲料瓶數
     * @param bottle 空瓶數
     * @return
     */
    private static int soda1(int n, int bottle) {

        // 兌換剩下的空瓶數
        bottle %= 2;
        // 喝完飲料剩下的空瓶數
        bottle += n;

        if (bottle < 2) { // 如果空瓶數<2,停止兌換
            return n;
        } else {
            return n + soda1(bottle/2, bottle);
        }
    }

試着來調用測試一下:

// 我能喝的汽水數
int sum = soda1(n, 0);

運行main方法:
在這裏插入圖片描述
這下才是真的完美,代碼可讀性也比之前要強很多了。而如果要用循環來控制兩個變量來解決這個問題的話,那麼代碼很可能又會變得非常複雜。

題2解

看到這裏,大家可能已經忘了題目二是啥了。不過莫得事,我們先來回顧一下題二:

一個人買汽水,一塊錢一瓶汽水,三個瓶蓋可以換一瓶汽水,兩個空瓶可以換一瓶汽水,問20塊錢可以買多少汽水?

我們可以先試着像題1一樣在草稿紙上計算一下,看看能不能找到規律:

錢數(元) - 	   瓶數
  1            	1
  2            	5 = 2 + 1(2個空瓶) + 1(3個瓶蓋) + 1(2個空瓶)
  3            	? = 3 + 1(2個空瓶) + 1(3個瓶蓋) + ???
  ...			...
  20		    ???
  ...			...
  n			    ???

好吧,這個問題確實有點複雜,我計算到3塊錢的時候就已經喫不消了,不愧是華爲大佬們出的面試題。但是我不能服輸,3塊錢的最多喝飲料瓶數我就算跪着畫圖我也要強行算完。如下圖,我們用空白的三角箭頭(▷)表示上次兌換剩下來的瓶蓋或者瓶子,用全黑的三角箭頭(▶)表示兌換成功的飲料:
在這裏插入圖片描述

通過上圖,我們可以很輕鬆的計算出3塊錢的時候最多喝的飲料數爲:3+2+1+2+1+1+1=11(瓶)

確實很輕鬆 哇,這也太難了吧。這個時候我們無論是用循環,還是說利用數學推導出數學公式(可能是我數學太菜)來計算出這個問題都很難。不過,本文的主角遞歸這下可就派上大用場了。有了之前的題1的講解,相信大家理解以下代碼就會變得很容易:

import java.util.Scanner;

/**
 * @author guqueyue
 * @Date 2020/6/25
 * 一個人買汽水,一塊錢一瓶汽水,三個瓶蓋可以換一瓶汽水,兩個空瓶可以換一瓶汽水
 * 問20塊錢可以買多少汽水?
 **/
public class SoDaWater2 {
    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);

        for(;;) {
            System.out.print("請輸入你的金額:");
            //從鍵盤輸入我的錢數
            int money = scanner.nextInt();
            // 第一次的錢能喝的汽水瓶數(一元錢一瓶汽水)
            int n = money / 1;
            // 我能喝的汽水數
            int sum = sodaWater(n, 0, 0);
            System.out.println("當我有" + money + "元時,總共能喝" + sum + "瓶飲料");
        }
    }

    /**
     * 用遞歸計算n元錢最多能喝多少瓶汽水
     * @param n 飲料瓶數
     * @param bottle 空瓶數
     * @param cap 瓶蓋數
     * @return
     */
    private static int sodaWater(int n, int bottle, int cap) {

        // 兌換剩下的空瓶數
        bottle %= 2;
        // 喝完飲料剩下的空瓶
        bottle += n;

        // 兌換剩下的瓶蓋數
        cap %= 3;
        // 喝完飲料剩下的瓶蓋
        cap += n;

        if (bottle < 2 && cap < 3) { // 如果空瓶數<2 並且 瓶蓋數<3, 那麼停止兌換
            return n;
        }else {
            return n + sodaWater(bottle/2 + cap/3, bottle, cap);
        }
    }

}

運行main方法,測試一下:
在這裏插入圖片描述
我們可以得出:當有20塊錢,如果我不怕撐的話,我最多能喝113瓶飲料。

最後

如果你看到這裏的話,希望我的這篇博客能夠給你帶來啓發或者幫助。
我的完美主義拖延症導致了我的這篇博客從端午前寫到了端午後。今天已經是端午節假期的第二天了,就不祝大家端午安康,祝大家端午假期快樂,沒有bug!
在這裏插入圖片描述

創作不易, 非常歡迎大家的點贊、評論和關注(^_−)☆
你的點贊、評論以及關注是對我最大的支持和鼓勵,而你的支持和鼓勵是我繼續創作高質量博客的動力 !!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章