運用場景
在汽車服務類的 App 應用開發中,如在獲取4S店保養方案時,有些保養項目之間存在聯動綁定關係。如在選擇A項目時,B項目必須同時選擇。取消A項目時,B項目可能取消,也可能不取消。綁定關係通過服務器返回的一個數組關係來確定(服務器數據示例見下文)。
舉例說明,以某款4S店養車App的使用場景來說,在進入智能方案頁時會顯示推薦項目(推薦項目會默認選中)
如4S店保養項目、4S店清洗養護項目、4S店維修項目。推薦的項目會默認選中。用戶在選擇項目時 會關聯相應的項目。
(某車邦)實現效果:(請原諒圖片大小限制在5M之內,有點模糊,重在達意哈)
我的實現效果:
- 當用戶 取消“機油”選項時,會同時自動取消“機油濾清器”選項。
- 當用戶選擇“機油”選項時,“機油濾清器”選項會同時選中。
- 當用戶選擇養護項目中的”發動機潤滑系統養護“選項時,會提示該選項必須與機油、機油濾清器保養項目同時進行。
- 當用戶取消”發動機潤滑系統養護“選項時,只取消當前項目,已選擇的”機油“、”機油濾清器“選項不取消。
在以上場景的實現中,在解決聯動關係時,用到了遞歸思想。本文重點記錄解決聯動的解決思路。
讓我們首先回顧一下
什麼是遞歸
標準解答:
所謂遞歸,簡單點來說,就是一個函數直接或間接調用自身的一種方法,它通常把一個大型複雜的問題層層轉化爲一個與原問題相似的規模較小的問題來求解。
通俗解答:
我們可以把” 遞歸 “比喻成 “查字典 “,當你查一個詞,發現這個詞的解釋中某個詞仍然不懂,於是你開始查這第二個詞。可惜,第二個詞裏仍然有不懂的詞,於是查第三個詞,這樣查下去,直到有一個詞的解釋是你完全能看懂的,那麼遞歸走到了盡頭,然後你開始後退,逐個明白之前查過的每一個詞,最終,你明白了最開始那個詞的意思。(摘自知乎的一個回答)
其他解答:
所謂遞歸,就是包含遞推和迴歸兩個自然過程,一方面要由外到裏地深入,一方面又要由裏到外地回到原點,這是我的基本看法,遞推過程是決定整個算法的逐步計算過程,而回歸就是將每一步計算的結果慢慢地進行總結,最後就能夠得到我們想要的結果。
我們以階乘爲最簡單的遞歸舉例:求n! = n * (n-1) * (n-2) * …* 1(n>0) (Kotlin版)
fun Factorial(n: Int): Int {
if (n == 0) { //if(n<1)
return 1
}
return n * Factorial(n - 1)
}
遞歸與棧的關係
常常聽到 “遞歸的過程就是出入棧的過程”,這句話怎麼理解?我們以上述代碼爲例,取 n=3,則過程如下:
-
第 1~4 步,都是入棧過程,
Factorial(3)
調用了Factorial(2)
,Factorial(2)
又接着調用Factorial(1)
,直到Factorial(0)
; -
第 5 步,因 0 是遞歸結束條件,故不再入棧,此時棧高度爲 4,即爲我們平時所說的遞歸深度;
-
第 6~9 步,
Factorial(0)
做完,出棧,而Factorial(0)
做完意味着Factorial(1)
也做完,同樣進行出棧,重複下去,直到所有的都出棧完畢,遞歸結束。
**每一個遞歸程序都可以把它改寫爲非遞歸版本。**我們只需利用棧,通過入棧和出棧兩個操作就可以模擬遞歸的過程,二叉樹的遍歷無疑是這方面的代表。
但是並不是每個遞歸程序都是那麼容易被改寫爲非遞歸的。某些遞歸程序比較複雜,其入棧和出棧非常繁瑣,給編碼帶來了很大難度,而且易讀性極差,所以條件允許的情況下,推薦使用遞歸。
什麼時候該用遞歸
當我們遇到一個問題時,我們是怎麼判斷該題用遞歸來解決的?
問題可用遞歸來解決需具備的條件:
-
子問題需與原問題爲同樣的事,且規模更小;
-
程序停止條件。
遞歸的元素總共有三類:
- 初始值
- 結束條件
- 算法
在實際項目中運用
實例代碼(Kotlin):
//關聯的綁定 Ids數組
private var bindItemIds = ArrayList<Int>()
private fun getBindItemIds(
listBindItems: ArrayList<MaintenancePlanEntity.BindItemsBean>,
selectedItemId: Int,
isSelected: Boolean
): ArrayList<Int> {
for (index in 0..listBindItems.size) {
if (index == listBindItems.size) {
//返回空數組
return bindItemIds
}
val item = listBindItems[index]
//選中
if (isSelected) {
if (selectedItemId == item.firstItemId && !bindItemIds.contains(item.bindItemId)) {
bindItemIds.add(item.bindItemId)
return getBindItemIds(listBindItems, item.bindItemId, isSelected)
}
//取消選中
} else {
if (selectedItemId == item.bindItemId && !bindItemIds.contains(item.firstItemId)) {
bindItemIds.add(item.firstItemId)
return getBindItemIds(listBindItems, selectedItemId, isSelected)
}
}
}
return bindItemIds
}
服務器返回綁定關係數據示例:
"bind_items": [
{
//選擇的項目id
"first_id": 2010,
//選擇first_id後必須選擇的id ,
"bind_id": 7
},
{
"first_id": 2011,
"bind_id": 15
},
{
"first_id": 1,
"bind_id": 2
},
{
"first_id": 2001,
"bind_id": 2
},
{
"first_id": 2,
"bind_id": 1
}
]
方法調用:實例代碼(Kotlin)
……(省略部分代碼)
//在選擇與取消時:
val item = list[position]
item.isSelected = !item.isSelected
//獲取當前itemId
val selectedItemId = item.itemId
bindItemIds.clear()
//得到關聯的Ids 數組
val bindItemIds = getBindItemIds(listBindItems, selectedItemId, item.t.isSelected)
for (bindItemId in bindItemIds) {
for (index in list.indices) {
val bean = list[index]
bean?.let {
if (it.itemId == bindItemId) {
it.isSelected = item.isSelected
}
}
}
}
//界面刷新
adapter.notifyDataSetChanged()
//計算關聯後的價格
calculateTotalPrice()
……(省略部分代碼)
遞歸的經典使用場景
-
斐波拉契數列
斐波那契數列指的是這樣一個數列:0,1,1,2,3,5,8,13,21……
數學公式:
F(n) = F(n-1) + F(n-2)
代碼示例:(Kotlin實現)
fun fib(n: Int): Int { if (n <= 2) return 1 return fib(n - 1) + fib(n - 2) }
-
遍歷文件(Kotlin實現)
fun list(fPath: File){
val files = fPath.listFiles()
for (f in files){
println(f.path)
if (f.isDirectory){
list(f)
}
}
}
3.快速排序
快速排序法採用的是“分而治之”的思想,比較適合使用遞歸而且效率也相當不錯。
實例代碼(Kotlin):
fun quickSort(arr: IntArray, left: Int, right: Int) {
if (left < right) {
val mark = arr[left]
var i = left
var j = right
while (i < j) {
// 從後向前查找
while (j > i && arr[j] >= mark) {
j--
}
if (j > i) {
arr[i++] = arr[j]
}
// 從前向後查找
while (i < j && arr[i] < mark) {
i++
}
if (i < j) {
arr[j--] = arr[i]
}
}
arr[i] = mark
quickSort(arr, left, i - 1)
quickSort(arr, i + 1, right)
}
}
總結
在開發中,剛開始可能不知是否需要用遞歸來實現,寫着時發現當函數要不停的調用自己的時候,即你接下來的代碼步驟跟你之前的是一致的時候。你就需要考慮用遞歸來實現了,實現時一定要考慮循環退出的終止條件。考慮好終止條件和通用情況。
參考資料: