以猜數字遊戲引出的分治演算法的理解與思考

智慧zhuhuix發表於2020-06-12

一、背景

分治演算法是計算機五大常用演算法之一,也是在JAVA程式設計中經常用到的演算法之一。對於分治演算法的理解,往往會停留在一些枯燥的概念上,比如“分而治之”,“問題原子分解”等。該文將會通過一個猜數字的遊戲入手,引出對於分治演算法基本思想的思考。

二、猜數字遊戲

2.1 遊戲規則
  • 由電腦生成一個在【1-100】之間的隨機整數;
  • 人類每輪只能猜測一個數字;
  • 電腦根據人類給出的數字進行反饋:
    -- 人類給出的數字比電腦給出的數字大,則反饋“比這個數字要大”;
    -- 人類給出的數字比電腦給出的數字小,則反饋“比這個數字要小”;
    -- 人類給出的數字等同於電腦給出的數字,則反饋“猜中了”。
  • 不限猜數輪次,以猜中為準
2.2 猜數字遊戲原始碼
  • 根據遊戲規則,我們先形成編碼:
/**
 * 猜數字遊戲
 *
 * @author zhuhuix
 * @date 2020-06-11
 */
public class Guess {

    public static void main(String[] args) throws IOException {
        int num = generateRandomInteger(1, 100);
        int guessNum = 0;
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        System.out.println("已生成一個【1-100】的整數,請開始猜數...");
        while (num != guessNum) {

            String s = bufferedReader.readLine();
            if (!s.matches("[0-9]+")) {
                System.out.println("請輸入一個整數..");
                continue;
            }
            guessNum=Integer.parseInt(s);
            if (guessNum > 100 || guessNum < 1) {
                System.out.println("請輸入一個【1-100】整數..");
                continue;
            }
            if (guessNum<num){
                System.out.println("Sorry,比這個數字要大,請繼續...");
            }
            if (guessNum>num){
                System.out.println("Sorry,比這個數字要小,請繼續...");
            }

        }

        System.out.println("恭喜您猜中了!!!");

    }

    /**
     * 產生一個在規定範圍內的隨機數
     *
     * @param left  起始數字
     * @param right 終止數字
     * @return 隨機數
     */
    private static int generateRandomInteger(int left, int right) {
        Random random = new Random();
        return left + random.nextInt(right - left + 1);
    }
}

2.3 猜數字遊戲技巧
  • 看看人類是怎麼猜的:
    --有沒有發現規律?
    在這裡插入圖片描述
  • 人類猜數字的技巧:
    -- 先猜50這個中位數,
    -- 根據電腦“大”或“小”的反饋將數字範圍對半拆分
    --迴圈重複以上分解過程,直至找到對應的數字為止
    在這裡插入圖片描述

三、分治演算法

3.1 思想與策略

將一個難以直接解決的大問題,分割成一些規模較小的相同問題,以便各個擊破,分而治之。

  • 注意:是相同問題,就象猜數字遊戲一樣,原來的問題是【1-100】的數,分割成的是【1-49】,【50-100】,【50-75】...這些縮小了規模的數,問題的性質始終沒變
3.2 適用的特徵
  • 問題縮小到一定規模就容易解決:比如猜數字遊戲數字範圍從【1-100】縮小到【56-62】,就容易猜中。
  • 問題縮小規模後形成的子問題是相互獨立的。
  • 問題規模不管怎麼縮小,性質不能改變。
  • 利用該問題分解出的子問題的解可以合併為該問題的解
3.3 分治演算法的典型應用
3.3.1 歸併排序的原理

歸併排序就是一種典型的分治演算法:將N個數字的一個大規模表分成1個數字的N個小規模表,再通過數字從小到大的順序從1個數字的N個小規模表合併成N個數字的一個大規模表
在這裡插入圖片描述

3.3.2 自頂向下的歸併排序原始碼
/**
 * 整型陣列排序統一介面定義
 *
 * @author zhuhuix
 * @date 2020-06-06
 */
public interface Sort  <T extends Comparable<? super T>> {

    /**
     * 整型排序
     * @param arr 待排序陣列
     */
   void sort(T[] arr);
}

/**
 * 歸併排序
 *
 * @author zhuhuix
 * @date 2020-06-11
 */
public class MergeSort<T extends Comparable<? super T>> implements Sort<T> {
    @Override
    public  void sort(T[] arr) {
        mergeSort(arr,0,arr.length-1);
    }

    private void mergeSort(T[]arr,int l,int r){
    	// 遞迴退出條件:分到最小規模為止
        if (r-l<=0){
            return;
        }
        // 取到當前規模的中值
        int mid = (l+r)/2;
       // 中值的左邊遞迴分解
        mergeSort(arr,l,mid);
        // 中值的右邊遞迴分解
        mergeSort(arr,mid+1,r);
        // 排序合併
       if (arr[mid].compareTo(arr[mid + 1]) > 0) {
            merge(arr, l, mid, r);
        }
    }

    private void merge(T[]arr,int l,int mid,int r){
        T[]aux=Arrays.copyOf(arr,r-l+1);
        for (int i = l; i <= r; i++) {
            aux[i - l] = arr[i];
        }
        int i = l, j = mid + 1;
        for (int k = l; k <= r; k++) {

            if (i > mid) {
                arr[k] = aux[j - l];
                j++;
            } else if (j > r) {
                arr[k] = aux[i - l];
                i++;
            } else if (aux[i - l].compareTo(  aux[j - l])<0) {
                arr[k] = aux[i - l];
                i++;
            } else {
                arr[k] = aux[j - l];
                j++;
            }
        }
    }

 }

四、總結

  1. 分治演算法的難點就是”如何分“:每個分解出來的子問題需獨立存在;比如整數陣列排序時需要從N個數分到1個數...
  2. 分治演算法一般會涉及遞迴程式;
  3. 分治演算法在從小規模問題合併成大規模問題的過程中,一般需要開闢輔助空間處理。

相關文章