20200925—遞迴與分治
今日學習
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.分治演算法
歸併排序,典型的分治演算法;分治,典型的遞迴結構。
分治演算法可以分三步走:分解 -> 解決 -> 合併
- 分解原問題為結構相同的子問題。
- 分解到某個容易求解的邊界之後,進行第歸求解。
- 將子問題的解合併成原問題的解。
歸併排序像不像是二叉樹的後續遍歷?因為我們分治演算法的套路是 分解 -> 解決(觸底) -> 合併(回溯) 啊,先左右分解,再處理合並,回溯就是在退棧,就相當於後序遍歷了。
附上我之前敲的歸併排序的程式碼:
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);
}
}
先這樣吧。
相關文章
- 遞迴與分治之大整數乘法遞迴
- 遞迴與分治演算法練習遞迴演算法
- 遞迴、分治和動態規劃遞迴動態規劃
- 遞迴 & 分治演算法深度理解遞迴演算法
- 歸併排序(C++_分治遞迴)排序C++遞迴
- 分治與遞迴-找k個臨近中位數的數遞迴
- 計算機演算法設計與分析筆記(二)——遞迴與分治計算機演算法筆記遞迴
- 遞迴與回溯法遞迴
- 迭代與遞迴--你被遞迴搞暈過嗎?遞迴
- 演算法小專欄:遞迴與尾遞迴演算法遞迴
- 順序表應用7:最大子段和之分治遞迴法遞迴
- 連結串列與遞迴遞迴
- 揹包問題的遞迴與非遞迴演算法遞迴演算法
- 遞迴和尾遞迴遞迴
- 二叉樹的四種遍歷(遞迴與非遞迴)二叉樹遞迴
- 函式遞迴與生成式函式遞迴
- 快速排序【遞迴】【非遞迴】排序遞迴
- 10-17 c遞迴與遞推初識遞迴
- [20180531]函式呼叫與遞迴.txt函式遞迴
- 遍歷二叉樹的遞迴與非遞迴程式碼實現二叉樹遞迴
- 遞迴遞迴
- 二十一、氣泡排序演算法——JAVA實現(遞迴與非遞迴)排序演算法Java遞迴
- 二叉樹——後序遍歷的遞迴與非遞迴演算法二叉樹遞迴演算法
- 什麼是遞迴?遞迴和迴圈的異同遞迴
- 反轉連結串列(遞迴與棧)遞迴
- go 遞迴Go遞迴
- JavaScript遞迴JavaScript遞迴
- 理解遞迴遞迴
- 分而治之-遞迴遞迴
- 遍歷二叉樹-------遞迴&非遞迴二叉樹遞迴
- 資料結構與演算法:遞迴資料結構演算法遞迴
- 遞迴和遞推總結遞迴
- 遞迴呼叫 VS 迴圈呼叫遞迴
- 遞迴總結遞迴
- SQL 遞迴思想SQL遞迴
- 遞迴函式遞迴函式
- 談談遞迴遞迴
- 遞迴問題遞迴