两个链表求和
每一对儿的节点处理都是一样的,并且不断的向后遍历,因此看到这个问题的时候可以用循环也可以用递归。有一点不同的是,子问题的入参需要预先处理,即
- 先要求和
- 然后判断进位
- 将进位传递给子问题
还有一点非常有意思,一般情况下,一个链表遍历到None的时候,就直接返回了,例如l2
比l1
要短,因此肯定是先遍历完l2
,通常情况下只需要把l1
拼接起来即可,但是这里却是不断的补充0元素,这样就可以把两个链表虚拟成一样长了。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
def add(l1,l2,carry):
if l1 is None and l2 is None and carry == 0: # 停止条件很有意思,一定要两个链表都遍历完,否者就补充0
return None
v1 = l1.val if l1 else 0
v2 = l2.val if l2 else 0
val = v1 + v2 + carry
l1 = l1.next if l1 else None
l2 = l2.next if l2 else None
node=ListNode(val%10)
node.next = add(l1,l2,val // 10)
return node
res = add(l1,l2,0)
return res
单个链表遍历求和
- 递去:原问题是对链表所有元素求和,即
sum([1,2,3,4,5,6,7])
,子问题是对链表部分求和,即sum([2,3,4,5,6,7])
。为了求解sum([2,3,4,5,6,7])
,我们需要先求出子问题sum([3,4,5,6,7])
,不断的递去
,直到问题规模无法缩减,即到达了终止条件。 - 归来:当到达终止条件之后,我们已经有了最最最最小子问题的解了,然后不断的返回,在这样就可以得到最最最小子问题的解,然后层层返回,不断得到子问题的解,一直到最后一层。
可以将整个递归过程展开来看
最终实现的代码
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def listSum(self, head: ListNode) -> List[int]:
if head is None:
return 0
s = self.listSum(head.next) + head.val # 子问题
return s # 其他逻辑
数组分治求和
数组求和的递归是我自己瞎想的,主要是想要使用归并的思想,我们依然把原问题划分为子问题,进而缩小问题规模。但是这里我们需要调用两次子问题,我们把数组分为两个部分,然后分别求解出左边数组的和,右边数组的和,之后将其相加就是总的和
这个问题也很好的说明了递归的一个特点,那就是缩小问题规模,子问题与原问题是等价的,我们求解出子问题之后,需要再进行一些逻辑操作。现在突然想到一个问题,子问题与原问题等价吗?
- 逻辑上肯定是相同的,只是问题规模缩小了。
- 那些逻辑操作则是解决问题的
arr = [-1,2,3,4,5,6]
def mergeAdd(arr, l,r):
if l >= r:
return arr[l]
m = (l + r) // 2
lsum = mergeAdd(arr, l, m)
rsum = mergeAdd(arr, m+1, r)
return lsum + rsum
res = mergeAdd(arr,0,len(arr)-1)
print(res)
- 终止条件,当左游标大于等于右游标的时候,说明只有一个元素了,此时直接返回值即可
- 子问题和重复逻辑,原来求解的是
0
到len(arr)-1
所有元素的和,现在求解成子问题0
到m
和子问题m+1
到len(arr)-1
和,调用了两次子问题,之后将求解的和相加即是总的和
反转字符串
通常面对递归,我都会先思考子问题,然后根据子问题去求解原问题,这道题有两点不同
- 子问题的输入需要预先做处理,因此需要考虑子问题的入参,所以需要先处理再递归
- 子问题会返回子链表的head节点,因此需要next操作,即
node.next=add(l1,l2,c)
,并且需要返回原问题的节点return node
字符串也可以考虑使用递归
- 假如子问题已经完成了反转
- 两侧的字符行交换
- 当
l>=r
停止即可
先递归再处理
s = list("abcdefgh")
def dfs(arr, l, r):
if l >= r: # 递归的终止条件
return
dfs(arr,l+1,r-1) # 缩减问题的规模,重复操作
arr[l],arr[r] = arr[r],arr[l] # 重复操作部分。交换左右的两个字符
dfs(s,0,len(s)-1)
print(s)
先处理再递归
这个递归很简单,但是我在实现的时候发现了另一种想法,就是先进行交换,然后再去处理子问题,这种方式也是对的。先后递归再处理和先处理再递归结果竟然是一样的。
打印二叉树的时候
- 先打印再递归对应的是先序遍历
- 先递归再打印的是中序遍历
不同顺序对应的结果也是不同的,为什么这里的结果是相同的呢?其实在这里我想错了,先交换后递归和先递归后交换虽然结果相同,但是中间操作是不同的。
- 先交换后递归是从两侧往中间改变的
- 先递归后交换是从中间往两侧改变的
二叉树遍历的时候不管先序还是中序也都是把所有节点遍历一遍。
s = list("abcdefgh")
def dfs(arr, l, r):
if l >= r: # 递归的终止条件
return
arr[l],arr[r] = arr[r],arr[l] # 重复操作部分。交换左右的两个字符
dfs(arr,l+1,r-1) # 缩减问题的规模,重复操作
dfs(s,0,len(s)-1)
print(s)
反转链表
这个问题跟移除链表指定元素非常的相像,我在下图直接给了一种方法,但是遇到了一个问题
移除指定元素的时候我们是直接返回head节点的,但是这个问题要求的是反转链表,需要将原始链表最末尾的元素作为返回,也就是说当我们递归到停止条件之后将最后一个元素返回即可,并且每次递归返回都是这个元素
看了别人的解法之后,我重新梳理了一下
其中有一点也比较有意思,既然sr每次的返回的结果都是一样的(都是在最后一个元素),那么是不是放在哪个位置都可以,肯定不是的,例如下面这种位置就是不对的
head.next.next = head
head.next = None
last = self.reverseList(head.next) # 此时head.next=None,有问题的
下面是最终代码
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
if head is None or head.next is None:
return head
last = self.reverseList(head.next) # 为了返回最后一个元素,我们不断去递归深入下去,直到终止条件
head.next.next = head
head.next = None
return last # 不管前面怎么处理,就直接返回最后一个节点
回文链表
这个问题其实非常的好理解,比较头节点和尾节点的值,然后不断向中间靠近即可。如果都相等说明就是回文链表,否则不是
这个题一看我就想到了递归,如果是数组的话,非常的好写,比较两侧的字符是否相等,如果不相等直接返回False,否则返回子问题的结果。
arr = [8,4,2,2,4,8]
def dfs(arr, l, r):
if l >= r: return True
if arr[l] != arr[r]: return False
return dfs(arr,l+1,r-1)
res = dfs(arr,0,len(arr)-1)
print(res)
但是当前是链表,没有办法使用L和R来进行比较,而且我还对递归产生了错误的理解,我一直觉得dfs(head)
表示的是整条链表,那么子问题dfs(head.next)
肯定是表示除了头节点之后链表,这让我怎么都想不明白该如何使用递归,即是看了很多个文章,依然糊里糊涂的
我实在是想不出来怎么做,就去看了一下别人写的代码,发现自己对递归,对递归子问题都思考的太少了
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def isPalindrome(self, head: Optional[ListNode]) -> bool:
realHead = head
def dfs(recHead):
nonlocal realHead
if recHead is None:
return True
sr = dfs(recHead.next)
r = (realHead.val == recHead.val)
realHead = realHead.next
return sr and r
res = dfs(head)
return res
分析过后,我才恍然大悟,原来这里的子问题定义的是子链表的每个节点是否与之前对应的每个节点相等,而不是去判断子链表是否是一个回文链表。
然后我就想起之前看到的一句话,递归问题的解就是递归问题的定义。只有定义好递归问题才能更好的解决。比如这个问题,定义的就是子链表是否与前面对应节点的值都相等。
移除链表元素
- 首先假设子问题已经解决了,即子链表已经移除了指定的元素
- 当前元素该如何做才能拼接上子链表,并且可以解决原问题
从下图可以看出,假设我们已经得到了子问题的返回解sr
,那么对当前元素该如何处理呢?这里分了两种情况
- 当x=val时,我们要把这个元素删除,那么直接返回子问题的解即可
- 当x!=val时,我们直接把子问题的解拼接在head后面,然后返回head即可
最终的代码实现
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
if head is None:
return head
sr = self.removeElements(head.next,val)
if head.val == val:
return sr
else:
head.next = sr
return head
删除链表的倒数第 N 个结点
链表的递归我感觉更像是递归遍历,不通过for循环操作进行遍历,直接使用递归的原理进行遍历,再删除倒数第N个节点的时候,通过递归会顺向遍历到尾部,然后再从下往上返回,这个时候我们就可以计数了,记到第N+1个的时候,我们可以进行操作。
这其中有两个小技巧,一个是定义一个dummy的节点放到head的前面,这样如果要删除head的结点依然可以返回dummy.next,第二个是使用nonlocal这个关键字,这个关键字定义了内部函数可以操作函数以外的变量。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
dummy = ListNode(0,head)
def dfs(head):
nonlocal n
if head is None:
return
dfs(head.next) # 之前不需要任何处理,一直到末尾元素,并且不需要返回
n -= 1
if n == -1:
head.next = head.next.next
dfs(dummy)
return dummy.next
两两交换链表中的节点
首先假设子问题已经解决了,由于是两两交换节点,因此我们需要预留出两个节点出来才行,之前使用递归对链表求解的时候都是只需要操作head节点即可,但是这里要求两个节点交换,因此需要head和head.next两个节点,因此子问题的输入就是head.next.next了
最后写成代码如下
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head is None or head.next is None:
return head
sr = self.swapPairs(head.next.next)
t = head.next
t.next = head
head.next = sr
return t
汉诺塔问题
原问题是把N个圆盘由A移动到C,子问题可以变为把N-1个圆盘由A移动到B,然后在把这个N-1个圆盘移动到C。从下图可以看到共调用了两次子问题
最终的代码也非常的简单
class Solution:
def hanota(self, A: List[int], B: List[int], C: List[int]) -> None:
"""
Do not return anything, modify C in-place instead.
"""
def move(n,A,B,C): # 原问题
if n == 1:
C.append(A.pop())
return
move(n-1, A, C, B) # 子问题
C.append(A.pop()) # 逻辑
move(n-1, B, A, C) # 子问题
n = len(A)
move(n,A,B,C)
还有一种打印版本的汉诺塔
def move(n,A,B,C):
if n == 1:
print('{}->{}'.format(A,C))
return
move(n-1,A,C,B)
print('{}->{}'.format(A,C))
move(n-1,B,A,C)
move(3,'A','B','C')