20200925—遞迴與分治

Sibyl_今天學習了嗎發表於2020-10-18

今日學習

IDEA使用

1.一鍵生成getter和setter

​ 選中屬性右擊Generate

2.與替換相關

​ 重新命名檔案、方法、屬性等(rename):SHIFT+F6

​ 重構類、方法(change signarture):CTRL+F6

​ ctrl + r: 當前檔案內容替換,指的是在當前開啟的檔案中替換匹配的字元,只操作一個檔案。

​ ctrl + shift + r: 在路徑中替換,指的是在選定的目錄下或者類包下,查詢要被替換的字元,再在第二個輸入框中輸入要替換的字元,點選彈出框的右下角的replace或者replaceall即可

一、遞迴與分治

1. 什麼是遞迴

某個函式直接或間接地呼叫自身,將原問題地求解轉換為許多性質相同,但規模更小的子問題。

求解時只需要關注如何把原問題劃分為符合條件的子問題,而不需要過分關注這個子問題時如何被解決的。

遞迴程式碼最重要的兩個特徵:結束條件和自我呼叫。

自我呼叫是在解決子問題;結束條件定義了最簡子問題的答案。

int fun(傳入數值){
	if(終止條件)
    	return 最小子問題的解;
    return func 縮小規模;
    }
	

2. 遞迴的缺點:

遞迴時利用堆疊來實現的。每當進入一個函式呼叫,棧就會增加一層棧幀,每次函式返回,棧就會減少一層棧幀。而棧不是無限大的,當遞迴層數過多,就會造成棧溢位的後果。

3.例項

給一課二叉樹,和一個目標值,節點上的值有正有負,返回樹中和等於目標值的路徑條數,讓你編寫 pathSum 函式:

/* 來源於 LeetCode PathSum III: https://leetcode.com/problems/path-sum-iii/ */
root = [10,5,-3,3,2,null,11,3,-2,null,1],
sum = 8

      10
     /  \
    5   -3
   / \    \
  3   2   11
 / \   \
3  -2   1

Return 3. The paths that sum to 8 are:

1.  5 -> 3
2.  5 -> 2 -> 1
3. -3 -> 11

編碼時的注意事項:在java中,int型陣列不能賦值為空,int陣列在定義時就已經被預設初始化為0;要想達到類似效果,可以將int陣列轉為Integer陣列。

sorry。在此之前,讓我們先來複習一下二叉樹

package com.seu.recursion;


/**
 * @author SJ
 * @date 2020/9/25
 */
public class Node {
    public int getData() {
        return data;
    }

    public void setData(int data) {
        this.data = data;
    }

    private int data;

    public Node getLeftChild() {
        return leftChild;
    }

    public void setLeftChild(Node leftChild) {
        this.leftChild = leftChild;
    }

    private Node leftChild;

    public Node getRightChild() {
        return rightChild;
    }

    public void setRightChild(Node rightChild) {
        this.rightChild = rightChild;
    }

    private Node rightChild;

    public Node(int data) {
        this.data = data;
        this.leftChild = null;
        this.rightChild = null;
    }


}

package com.seu.recursion;

/**
 * @author SJ
 * @date 2020/9/25
 */
public class Tree {
    public Node getRoot() {
        return root;
    }

    public void setRoot(Node root) {
        this.root = root;
    }

    private Node root;

    public Tree(int[] nums, int index) {
        this.root = createTree(nums, index);
    }


    public Node createTree(int[] nums, int index) {
        Node node = null;
        if (index < nums.length) {
            node = new Node(nums[index]);//每次進來先建節點
            node.setLeftChild(createTree(nums, index * 2 + 1));
            node.setRightChild(createTree(nums, index * 2 + 2));
        }

        return node;
    }

    public void preOrderTraverse(Node root) {
        if (root != null) {
            System.out.println(root.getData());
            preOrderTraverse(root.getLeftChild());
            preOrderTraverse(root.getRightChild());

        }
    }
}

package com.seu.recursion;

/**
 * @author SJ
 * @date 2020/9/25
 */
public class Test {
    public static void main(String[] args) {
        int[] nums = {10, 5, -3, 1};
        Tree tree = new Tree(nums, 0);
        tree.preOrderTraverse(tree.getRoot());

    }
}

執行結果:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" ...
10
5
1
-3

Process finished with exit code 0

二叉樹的建立和遍歷也是很好的關於遞迴的例子。

寫遞迴時,**明白一個函式的作用並且相信他能完成這個任務,千萬不要試圖跳進細節,**否則就會陷入無窮的細節無法自拔,人腦壓不了幾個棧!!別想太多!

好,我們接著來解決上面的問題。

package com.seu.solution;

/**
 * @author SJ
 * @date 2020/9/25
 */
public class TreeNode {
    public Integer data;
    public TreeNode left;
    public TreeNode right;

    public TreeNode(Integer data) {
        this.data = data;
        this.left = null;
        this.right = null;
    }

}

package com.seu.solution;

/**
 * @author SJ
 * @date 2020/9/25
 */
public class Tree {
    public TreeNode root;

    public Tree(Integer[] nums) {
        this.root = createTree(nums, 0);

    }


    public TreeNode createTree(Integer[] nums, int index) {
        TreeNode root = null;
        if (index < nums.length && nums[index] != null) {
            //這裡要新建節點,老忘記
            root = new TreeNode(nums[index]);
            root.left = createTree(nums, 2 * index + 1);
            root.right = createTree(nums, 2 * index + 2);
        }
        return root;
    }

}

package com.seu.solution;


/**
 * @author SJ
 * @date 2020/9/25
 */
public class PathSum {
    public static void main(String[] args) {
        Integer[] sums = {10, 5, -3, 3, 2, null, 11, 3, -2, null, 1};
        Tree tree = new Tree(sums);
        int num = pathSum(tree.root, 8);
        System.out.println(num);

    }

    //以不同頂點開頭的和為sum的總數
    public static int pathSum(TreeNode root, int sum) {
        int isMe = 0;
        if (root == null)
            return 0;

        return count(root.right, sum) + count(root.left, sum) + count(root, sum);


    }

    //以某一頂點開頭的和為sum的總數
    public static int count(TreeNode root, int sum) {
        if (root == null)
            return 0;
        int isMe = (root.data == sum) ? 1 : 0;
        return count(root.left, sum - root.data) + count(root.right, sum - root.data) + isMe;
    }


}

結果

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" ...
3

Process finished with exit code 0

例子就到這裡。

4.分治演算法

歸併排序,典型的分治演算法;分治,典型的遞迴結構。

分治演算法可以分三步走:分解 -> 解決 -> 合併

  1. 分解原問題為結構相同的子問題。
  2. 分解到某個容易求解的邊界之後,進行第歸求解。
  3. 將子問題的解合併成原問題的解。

歸併排序像不像是二叉樹的後續遍歷?因為我們分治演算法的套路是 分解 -> 解決(觸底) -> 合併(回溯) 啊,先左右分解,再處理合並,回溯就是在退棧,就相當於後序遍歷了。

附上我之前敲的歸併排序的程式碼:

import java.util.Arrays;

public class MergeSort {
    /**
     * 二路歸併排序
     * 包含1.兩個有序陣列合併成一個有序陣列
     *    2.遞迴
     */
    public static void main(String[] args) {
        int[] nums={1,4,2,3};
        System.out.println("原陣列為" +
                Arrays.toString(nums));
        mergeSort(nums,0,nums.length-1);

        System.out.println("排序後為:"+Arrays.toString(nums));
    }
    public static void merge(int[] nums, int start, int middle, int end){
        //首先從start~middle、從middle+1~end是了兩個分別有序的序列
        //需要一個和nums等大的陣列來輔助
        int[] temp = new int[nums.length];
        //p1和p2用來在原陣列上挪動比較大小
        //k用來在新陣列上挪動存放資料
        int p1=start, p2=middle+1, k=start;
        //兩組都還有剩餘的情況
        while (p1<=middle && p2<=end){
            if (nums[p1]<=nums[p2])
                temp[k++]=nums[p1++];
            else
                temp[k++]=nums[p2++];

        }
        //第二組的數已經全部插入新陣列,第一組還有剩餘
        while (p1<=middle)
            temp[k++]=nums[p1++];
        //第一組數已經全部插入新陣列,第二組還有剩餘
        while (p2<=end)
            temp[k++]=nums[p2++];


        //注意只能將已經排好序的複製回原陣列,而不是從0-結尾開始複製
        for (int i = start; i <= end; i++) {
            nums[i]=temp[i];
        }
        //return nums;

    }
    public  static void mergeSort(int[] nums, int start, int end){
        if (start<end)
        {
            int middle = (start+end)/2;
            mergeSort(nums,start,middle);
            mergeSort(nums,middle+1,end);
            merge(nums,start,middle,end);
        }
    }

    

}

然後是演算法書上的規範程式碼:有很多值得學習的地方,不該犯的錯誤我都犯了

public class Merge {
    // 不要在 merge 函式裡構造新陣列了,因為 merge 函式會被多次呼叫,影響效能
    // 直接一次性構造一個足夠大的陣列,簡潔,高效
    private static Comparable[] aux;

     public static void sort(Comparable[] a) {
        aux = new Comparable[a.length];
        sort(a, 0, a.length - 1);
    }

    private static void sort(Comparable[] a, int lo, int hi) {
        if (lo >= hi) return;
        int mid = lo + (hi - lo) / 2;
        sort(a, lo, mid);
        sort(a, mid + 1, hi);
        merge(a, lo, mid, hi);
    }

    private static void merge(Comparable[] a, int lo, int mid, int hi) {
        int i = lo, j = mid + 1;
        for (int k = lo; k <= hi; k++)
            aux[k] = a[k];
        for (int k = lo; k <= hi; k++) {
            if      (i > mid)              { a[k] = aux[j++]; }
            else if (j > hi)               { a[k] = aux[i++]; }
            else if (less(aux[j], aux[i])) { a[k] = aux[j++]; }
            else                           { a[k] = aux[i++]; }
        }
    }

    private static boolean less(Comparable v, Comparable w) {
        return v.compareTo(w) < 0;
    }
}

5.對於常見的幾種演算法的理解

簡單闡述一下遞迴,分治演算法,動態規劃,貪心演算法這幾個東西的區別和聯絡

遞迴是一種程式設計技巧,一種解決問題的思維方式;分治演算法和動態規劃很大程度上是遞迴思想基礎上的(雖然動態規劃的最終版本大都不是遞迴了,但解題思想還是離不開遞迴),解決更具體問題的兩類演算法思想;貪心演算法是動態規劃演算法的一個子集,可以更高效解決一部分更特殊的問題。

分治演算法,以最經典的歸併排序為例,它把待排序陣列不斷二分為規模更小的子問題處理,這就是 “分而治之” 這個詞的由來。顯然,排序問題分解出的子問題是不重複的,如果有的問題分解後的子問題有重複的(重疊子問題性質),那麼就交給動態規劃演算法去解決!

6.練習

6.1本題來源https://leetcode-cn.com/problems/recursive-mulitply-lcci/

package com.seu.solution;

import java.util.Scanner;

/**
 * @author SJ
 * @date 2020/9/25
 */
//遞迴乘法。 寫一個遞迴函式,不使用 * 運算子, 實現兩個正整數的相乘。可以使用加號、減號、位移,但要吝嗇一些。
public class Multiple {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int a=scanner.nextInt();
        int b=scanner.nextInt();
        System.out.println(multiply(a,b));

    }
    public static int multiply(int A, int B) {
        int temp1=(A>B)?A:B;
        int temp2=(A<B)?A:B;
        if (A==0||B==0)
            return 0;
        else if (A==1)
            return B;
        else if (B==1)
            return A;
        else
        return temp1+multiply(temp1,temp2-1);

    }
}

提交通過

6.2本題來源https://leetcode-cn.com/problems/chuan-di-xin-xi/

這道題要用到深度優先搜尋,好,我們先來學習一下搜尋。

搜尋

DFS(深度優先搜尋)

DFS 為圖論中的概念,在 搜尋演算法 中,該詞常常指利用遞迴函式方便地實現暴力列舉的演算法,與圖論中的 DFS 演算法有一定相似之處,但並不完全相同。

深搜模板:

int ans = 最壞情況, now;  // now為當前答案
void dfs(傳入數值) {
  if (到達目的地) ans = 從當前解與已有解中選最優;
  for (遍歷所有可能性)
    if (可行) {
      進行操作;
      dfs(縮小規模);
      撤回操作;
    }
}

考慮這個例子:

//把正整數n分解為3個不同的正整數,如6=1+2+3,排在後面的數必須大於等於前面的數,輸出所有方案

對於這個問題,如果不知道搜尋,應該怎麼辦呢?

當然是三重迴圈,參考程式碼如下:

for (int i = 1; i <= n; ++i)
  for (int j = i; j <= n; ++j)
    for (int k = j; k <= n; ++k)
      if (i + j + k == n) printf("%d=%d+%d+%d\n", n, i, j, k);

分解的層數一旦變多,多重迴圈就做不了了。那分解成小於等於m個整數呢?

此刻我們需要考慮遞迴搜尋。

該類搜尋演算法的特點在於,將要搜尋的目標分成若干“層”,每層基於前幾層的狀態進行決策,直到達到目標狀態。

換題:將正整數n分解成小於等於m個正整數之和,且排在後面的數必須大於等於前面的數,並輸出所有方案。

package com.seu.solution;

/**
 * @author SJ
 * @date 2020/9/25
 */
public class Test {

    static int m=3;
    static int[] arr=new int[103];  // arr 用於記錄方案
    public static void dfs(int n, int i, int a) {
        if (n == 0) {
            for (int j = 1; j <= i - 1; ++j)
                System.out.print(arr[j]+" ");
            System.out.println();
        }
        if (i <= m) {
            for (int j = a; j <= n; ++j) {
                arr[i] = j;
                dfs(n - j, i + 1, j);  // 請仔細思考該行含義。
            }
        }
    }

    public static void main(String[] args) {
        dfs(10, 1, 1);
    }


}

先這樣吧。

相關文章