今天的文章主要是从宏观角度聊聊递归这种算法思想,文章主要内容如下:5 U; ]" I% c/ r2 P7 p
通过数组求和看递归的基本性质 - 链表的天然递归性
3 j; E% K) Q, w4 m$ W* `+ V7 k" Y5 m Q, _, |8 H; R
01 通过数组求和看递归的基本性质 现在给出一个数组arr={1,3,6},问如何用递归方式求出数组中所有元素的总和。
! O5 r% `& M+ @9 m
9 P$ p; d/ {% J& V: f+ s( `5 c$ w/ I
递归思想介绍; t- |7 H# x1 i. g5 o; a% M0 b
假设现在A拿到了这个问题,但是A只能计算数组中的第一个元素值和剩余元素总和的和,那这时怎么办呢?3 @, k- v6 r. t: v4 w
A就想,我可以先把数组中第一个元素1的值记录下来,然后让B计算数组中剩余元素的总和。
5 X4 ]3 K, ~( R
于是,B就收到一个任务,需要计算数组{3,6}的总和,同样的B和A一样也只能计算数组中的第一个元素值和剩余元素总和的和。因此,B记录下他拿到的数组中的第一个元素3的值,然后让C去计算数组中剩余元素的总和。
3 ]! G6 |# O+ A& Z9 f
这时,C收到一个任务,是计算数组{6}的总和,同样的C和A一样也只能计算数组中的第一个元素值和剩余元素总和的和,并且除了第一个元素外,C不知道数组中还剩余哪些元素。于是,C将数组中的第一个元素6的值记录下来,然后让D去计算数组中剩余元素的总和。
' E! H2 }: D% A" V: t- q. X# R' m
8 V$ ^; S4 {% f% ^/ e3 uD在收到任务后,一看数组是空的,就知道数组中元素总和是0。
& |+ _ E1 U1 q
8 {- I% K& l% q* g, Z; m
然后D就告诉C,剩余元素的总和是0。于是C将自己记录的元素6和D告诉他的剩余元素的总和0相加后得到6,他就把这个结果告诉了B。
( P. K5 Q0 m5 s9 A3 V5 r+ _
3 t* G: A+ K5 R! @7 ]- T$ NB在收到C的结果6后,同样的将自己记录的元素3和其相加得到9,于是他就把这个结果告诉了A。
) P2 `( ^# M P% l' j0 F
A在拿到B告诉他的结果9后,将其和自己记录的元素1相加得到结果10。这时A就知道数组中所有元素的总和是10。
& {7 B# D% K/ b
上述过程中A拿到求数组中所有元素和这个问题后,将其告诉B,B再告诉C,C再告诉D,这个过程我们可以称之为”递“。
' B3 B# _9 }0 A- A* H然后,D将结果告诉C,C再告诉B,B在告诉A的这个过程我们可以称之为”归“。/ K1 g9 s$ @9 y: P. w
7 M4 C3 [# Y, Z1 U7 ~. U: f a i( Q$ {' n. N H& K
递归要满足的条件% F4 u6 ~- U5 P2 ^8 F
在上述问题的描述中,其实就包含了递归这种算法思想的一些基本条件:
; ]! T! m8 p' D9 t5 G& X4 d一是一个问题可以分解为多个更小的子问题并且这多个更小的子问题的求解思路完全一样
+ R! a/ m3 _+ A2 N( b+ K& ]对于数组求和这个问题来说,A要知道自己拿到的数组的总和这个问题,可以分解为B要知道除去数组第一个元素外剩余元素总和这样一个问题。
" _, y1 k* C/ p! @& A7 G同时,对于A和B来说,他们求解问题的思路是一样的,即只计算自己拿到的数组中第一个元素值和数组中剩余元素总和的和,而不管数组中剩余元素总和的和是如何计算的。
0 H+ M2 Q6 R2 R7 K% e) g( U2 j二是存在一个终止条件,可以让递归停下来。; O# n2 e" E }2 Z2 `
比如数组求和的终止条件就是数组中没有剩余元素需要计算了。
% C, M* b, _; P) `5 Q数组求和的递归思路具体如下图:) ~6 Y0 ~% i6 i
* u/ V6 j c" T' S+ Q$ {
( u) F) R2 I1 j! D! u) ?& V; g. \* G& p- [: o* X& s
递归代码的编写% |+ o; g4 w7 |
递归代码的编写一是要知道递归终止条件,对于数组求和这个问题来说,其终止条件是数组中没有元素需要计算了,代码表示是:
* t8 j$ [6 t; \" f K
: Y: V( b+ h c+ p# ^- // begin 表示从数组中哪个索引位置开始
m/ r# [: }! t# ]- L% m; X2 p$ P/ H - // 当begin == arr.length表示数组中没有剩余元素7 y6 }+ r1 m; j3 F* `
- if (begin == arr.length) {% e F" ^- @1 g$ w$ ]
- return 0;
1 }& G( A) t! F - }
复制代码
# a8 z$ i& k/ Z( N' K+ j" Y二是要知道递归递推公式,就数组求和这个问题来说,递推公式是: - 3 u+ T- }1 s ^4 t, Z0 \
- arr[begin] + sum(begin + 1, arr);
复制代码 # Y$ H9 X. T8 C( H6 P7 T, l' c
有了终止条件和递推公式,就可以写出递归求解数组中所有元素和的代码了,实现如下: - 6 w9 H* N1 U; V& p3 Z2 ~
- public int sum (int[] arr) {
- v0 ^: Z! @ X: Y ] Y7 N ] - // 调用递归函数,初始从0开始. t' `+ |, @6 |: H# O! z
- return sum(0, arr);
. p5 L- w7 @- g- k - }; y+ ~+ [+ p( G2 N, K
# u! D" P/ e# b& o+ L: h7 m8 C3 O- // 递归函数$ U( I# }( _4 l. h- s2 K
- private int sum(int begin, int[] arr) {
* g1 s! k# W7 y& O - // begin 表示从数组中哪个索引位置开始
8 q9 r! m# z: p8 e - // 当begin == arr.length表示数组中没有剩余元素9 ^' t& }7 u. c! b9 l
- if (begin == arr.length) {$ Q" I/ ] O& t' y, F
- return 0;
; b# x# Y, p4 M& B% w& E - }
5 M2 ^: q5 p- ?; f: e
+ w& o" x) Z- Z$ U) C6 S1 z- e- // 我只计算我拿到的数组中第一个元素的值
9 I4 m4 o) S# p: Y7 e) e3 h( y* [ - // 和数组中剩余元素总和的和
1 Y" n, ^% q# e& T3 k - return arr[begin] + sum(begin + 1, arr); // 递归调用
6 Q$ S6 J) }# j9 n0 ~5 F - }
复制代码 ! _4 J) j1 }. v& q3 K- F
这里我们在第7行sum(int begin, int[] arr)这个方法中调用了sum(int begin, int[] arr)方法它自己,即16行的sum(begin + 1, arr),这一点可能会让你觉得困惑。 我们可以这样理解,方法sum(int begin, int[] arr)是计算数组中从索引begin开始所有元素的总和,而该方法的计算规则是我计算的是当前数组起始位置begin所对应的元素值和数组中剩余元素的总和的和。那么,我就需要有个方法可以告诉我数组中剩余元素的总和是多少。 这时,刚好有个方法fun(int begin, int[] arr),只要我告诉它数组是什么样的,以及从哪个索引位置开始计算,它就会告诉我数组中剩余元素的总和。只不过这个方法fun(int begin, int[] arr)所需的参数和我自己一样而已。于是,在代码中看起来就是方法sum(int begin, int[] arr)调用了它自己。 * o3 P4 V; d$ j5 b* }+ G
02 链表的天然递归性 接着我们看下如何用递归思想解答LeetCode中#203.移除链表元素这个问题。题目描述: 删除链表中等于给定值 val 的所有节点。
i2 I* x6 C" F3 ~0 E8 \
示例: 输入: 1->2->6->3->4->5->6, val = 6 输出: 1->2->3->4->5 LeetCode 思路分析
) J' [# X/ |" [( }为了方便演示,我将题目示例中给定的链表进行了简化。如下图:
, d; r# q5 {4 r( R
' l& Q* @0 G4 a先看下”递“的过程,对于A来说,他在拿到给定的链表后,记录下了头节点,然后将头结点之后的链表给了B。
0 ?$ M; n0 C; p9 W1 Z6 k
; ?: B8 c% K, x3 c8 c/ D+ \. ]
同样的B在拿到给定的链表后,记录下了头节点,然后将头结点之后的链表给了C。 H2 w: h7 I8 l7 Z* Q5 H5 z
1 B6 D4 l# V) o" LC在拿到给定的链表后,记录下了头节点,然后将头结点之后的链表给了D。; J$ f( w! ?1 D3 J ~7 n/ M
) O6 p& m3 v5 ?5 \% J7 j) C
D在拿到给定的链表后,记录下了头节点,然后将头结点之后的链表给了E。- |3 F, w8 T1 H7 g8 M$ ]# P
" [& M( c( L: Q% Y这时E拿到的链表是一个空链表。) I5 x, }" n# X# u% ~; i
( i* p. A9 I, r& H: vE拿到空链表之后,就将其返回给了D,这时D就知道它的链表是3—>null。 D! T8 P- @" i, C$ g% E. {1 h
+ z; t& t* K5 x% T# X, XD将自己得到的结果3—>null返回给了C,这时C就知道它的链表是6—>3—>null。
% Q2 k. `6 H# |6 x/ g接着C需要将自己计算的结果返回给B,但这时C发现自己拿的链表的头结点是和要移除的元素val=6相等的,因此C返回B的结果应该是去掉头结点6后的剩余部分3—>null。
. t7 `$ X9 _+ D, o7 J
' s y# ^- C: f. S
B在拿到C返回的结果后,就知道它的链表是2—>3—>null。然后,它将这个结果告诉了A。
. j( S1 T+ O! \' E$ m
A在拿到B返回的结果后,就知道它的链表是1—>2—>3—>null,也就是移除链表中元素val=6之后的结果。
8 U" A4 A% n1 k6 s6 y7 l对于上述的递归过程,可以简化为两个步骤:
2 J7 m5 D% C- r- `9 P2 P. l一是记录下链表当前头结点head,然后调用移除链表中结点值val=6的方法;
+ F. g0 |. X: y二是在拿到调用方法之后的结果时,判断一下当前记录的头结点的值是不是等于val=6,如果不等,则将调用方法之后的结果挂在所记录的头结点之后,返回;如果相等,则直接将调用方法之后的结果返回。链表这种数据结构,我们说它具有天然递归性,就在于对于一个链表,我们可以将其分解为头结点和一个子链表。然后,对于子链表同样可以分解为一个头结点和一个子链表。这满足的是递归思想中的:一个问题可以分解为多个子问题并且这多个子问题求解思路是一样的这一条件。( H4 L0 i& ^1 J) ]) I
此外,对于链表分解到最后时,头结点为null,这满足的是递归思想中的:存在递归终止条件,让递归停下来这一条件。
8 B! p% c/ @" y: m5 d7 ]9 j代码实现
0 ]# i s# [$ _0 c5 `$ f T
1 O' m7 l- q. O4 c4 l- public ListNode removeElements(ListNode head, int val) {6 `, }! x# M9 ^0 {4 {8 F: L4 x; T
- // 递归终止条件: a2 b3 F/ P8 u
- if (head == null) {
, m3 w- b z2 r8 R5 g - return null;
4 I0 N# O0 p0 L8 E - }
* w( G( B5 V9 ?- o
6 W$ {+ o g- y% _! ]- // 调用方法removeElements将当前结点之后的链表部分中的% ]- Z a/ G: W
- // 和val值相等的结点删除" H+ W7 f* ]/ c/ j$ s
- ListNode result = removeElements(head.next, val);
+ p& G( ^1 M* w b
9 }- j& F6 o) B& E- // 如果当前结点head的值等于val则返回result
/ x# M* m7 Q7 \4 ?7 {0 Q - if (head.val == val) {
( \0 O/ ]# }* d. R( F2 a - return result;
2 w7 ]2 t+ N6 [8 l9 U - } else {
* x: J, Z( S# m. A9 | - // 如果当前结点head的值不等于val则
4 ^( {; m1 y6 M0 ^ - // 将result挂在当前结点head之后返回
, ~& o8 F- n6 \" ?8 i4 E - head.next = result;, x# H `' d8 D2 M
- return head;
+ E5 H x ?1 E - }
. H9 f0 R) v% l$ A - }
复制代码 ' ]2 @1 ^/ P" P2 F- m* c& L( D6 c" v+ Z
6 a9 y* D) ]7 J5 l' U |