class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
dummyhead = ListNode(None)
dummyhead.next = head
cur = dummyhead
for i in range(n+1):
cur = cur.next
tail = dummyhead
while cur:
tail = tail.next
cur = cur.next
tail.next = tail.next.next
return dummyhead.next
删除某一节点只需将该节点前置节点指向后置节点,为方便操作头节点,加入一个伪头节点。用一领跑指针从伪头节点出发遍历整个链表,用另一个跟随指针记录其之后的第n个节点,该节点即为自领跑指针指向节点开始的倒数第n个元素的前置节点。当前置节点遍历到链表尾,即为None时,跟随指针恰好指向需删除的倒数第n个节点的前置节点,这是即可方便的删除目标节点。时间复杂度O(n)。
大佬解法:
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
ans = []
def dfs(left, right, prefix):
if left == 0 and right == 0:
ans.append(prefix)
return
if left > 0:
dfs(left-1, right, prefix+'(')
if right > left:
dfs(left, right-1, prefix+')')
dfs(n, n, '')
return ans
使用回溯算法,在保证括号集合一直有效的情况下添加左右括号,即当仍有左括号剩余时可以添加左括号,当右括号数量大于左括号数量时,可以添加右括号。在这种情况下,当剩余左右括号数量均为零时,将结果加入答案中。
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
ans = []
if n == 0:
return ['']
for i in range(n):
for left in self.generateParenthesis(i):
for right in self.generateParenthesis(n-i-1):
ans.append('({}){}'.format(left,right))
return ans
动态规划法,尝试建立n对括号组合与n-1对括号组合之间的关系。我们把第一个左括号和一个可以移动的右括号看成新增的括号,则右括号把括号组合分为左右两部分([左])[右]
。左右两部分括号对之和为n-1,这样新括号组合可以由左右两子部分的不同括号组合组合而成。
class Solution:
def swapPairs(self, head: ListNode) -> ListNode:
if not head or not head.next:
return head
p1, p2 = head, head.next
head = p2
while p2.next and p2.next.next:
cur = p1
p1.next = p2.next.next
p1 = p2.next
p2.next = cur
p2 = p1.next
p1.next = p2.next
p2.next = p1
return head
用双指针,一个指针p1遍历单数节点,一个指针p2遍历双数节点,同时用指针cur来辅助节点间的链接的更替。利用此方法处理掉所有成对的节点,若最后存在落单的节点,再将结果与最后一个节点相连。时间复杂度O(n),空间复杂度O(n)。
大佬解法:
class Solution:
def swapPairs(self, head: ListNode) -> ListNode:
if not head or not head.next:
return head
new_head = head.next
head.next = self.swapPairs(new_head.next)
new_head.next = head
return new_head
迭代算法,每次将两节点调换指向,剩余节点利用迭代方法完成排序。时间复杂度度O(n),空间复杂度O(n),为递归时的堆栈空间。
大佬解法:
class Solution:
def nextPermutation(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n, flag = len(nums), False
if n <= 1:
return nums
for i in range(n-1, 0, -1):
if nums[i] > nums[i-1]:
flag = True
break
if flag:
for j in range(n-1, i-1, -1):
if nums[j] > nums[i-1]:
break
nums[i-1], nums[j] = nums[j], nums[i-1]
nums[i:] = nums[i:][::-1]
else:
nums[:] = nums[::-1]
首先从后往前遍历,找到需要交换的元素。具体方法为从后往前遍历,找到第一对元素满足nums[i]大于nums[i-1],此时nums[i-1]右侧数列为递减数列,该递减数列已无法增大,因此需要从这个递减数列中找出最小的大于nums[i-1]的数nums[j]与nums[i-1]互换。我们再次从数列末尾往前遍历找到nums[j],将其与nums[i-1]互换后,nums[i-1]右侧的数列依然为递减数列,我们希望该数列取值最小,因此我们将该数列倒转,即为最小的升序数列。
大佬解法:
class Solution:
def search(self, nums: List[int], target: int) -> int:
if not nums:
return -1
n = len(nums)
left, right = 0, n-1
while left<=right:
mid = (left+right)//2
if target == nums[mid]:
return mid
if nums[left] <= nums[mid]:
if (nums[left] <= target) and (target < nums[mid]):
right = mid-1
else:
left = mid+1
else:
if (nums[mid] < target) and (target <= nums[right]):
left = mid+1
else:
right = mid-1
return -1
利用二分查找法。因为将旋转排序数组二分为[l,mid]、[mid+1,r]左右两部分时,必有一部分为有序数组。我们可以通过判断nums[l]<=nums[mid]
是否成立来判断左区间是否为有序数组,若成立则左区间为有序数组,若不成立则右区间为有序数组,取等号是为了处理当只剩两个元素时l=mid的情况。我们能方便的由有序数组部分判断目标值是否在有序数组内,借此调整左右指针的指向。不断向下进行迭代,先判断哪部分为有序数组,再判断目标值的位置,直到找到目标。时间复杂度O(n)。
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
n = len(nums)
if n < 1:
return [-1,-1]
left, right = 0, n-1
first, last = -1, -1
while left<=right:
mid = (left+right)//2
if nums[mid] > target:
right = mid-1
elif nums[mid] < target:
left = mid+1
else:
right = mid
first = right
if left == right:
break
left, right =0, n-1
while left<=right:
mid = (left+right+1)//2
if nums[mid] > target:
right = mid-1
elif nums[mid] < target:
left = mid+1
else:
left = mid
last = left
if left == right:
break
return [first, last]
利用二分法分别查找目标元素的第一和最后位置。对于第一个位置,我们将中间数设定为(left+right)//2,这表示当剩余列表中有双数个元素,即中间数有两个时,将选择左侧的中间数。中间数大于或小于目标值时,我们按正常的二分法进行查找,当中间数等于目标值时,不是停止搜索,而是将右指针移动到当前中间值位置继续搜索,同时记录当前第一个目标值位置(right指针位置),由于中间数倾向于选择左侧的元素,到左右指针重合为止,第一个位置将一直向左侧更新。对于最后一个位置,我们将中间数设定为(left+right+1)//2,这表明中间数倾向于选择右侧元素,类似的,我们找到相应中间位置后,将左指针移动过来,继续搜索,直到找到最右侧的目标元素。时间复杂度O(logn)。
我的解法:
class Solution:
def isValidSudoku(self, board: List[List[str]]) -> bool:
rows, columns, blocks = [[] for _ in range(9)], [[] for _ in range(9)], [[[] for _ in range(3)] for _ in range(3)]
for i in range(9):
for j in range(9):
if board[i][j].isdigit():
if board[i][j] not in rows[i]:
rows[i].append(board[i][j])
else:
return False
if board[i][j] not in columns[j]:
columns[j].append(board[i][j])
else:
return False
bi, bj = i//3, j//3
if board[i][j] not in blocks[bi][bj]:
blocks[bi][bj].append(board[i][j])
else:
return False
return True
创建三个列表分别用来记录各行、各列以及各方块出现的数字,遍历一遍盘面,如果是数字则分别判断它们是否已经在对应的行、列或方块中出现过,如果没有则将其添加进对应列表,如果有则返回False。
我的解法:
class Solution:
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
if not candidates:
return []
ans, n = [], len(candidates)
#candidates = sorted(candidates)
for i in range(n):
if candidates[i]>target:
continue
dif = target-candidates[i]
if dif == 0:
ans.append([candidates[i]])
else:
for sub_ans in self.combinationSum(candidates[i:], dif):
if sub_ans:
ans.append(sub_ans+[candidates[i]])
return ans
递归算法,因为题目中明确数组中无重复元素,因此我们可以不需要对数组进行排序。总体思想为将原问题分解成可迭代的子问题。我们遍历列表中的元素,若元素大于目标值则直接跳过,若小于目标值,则计算与目标值之间的差,将差作为目标值,包括自身的剩余数组作为新的查找数组带回函数递归;如果得到的子结果集不为空,则将子结果集与本元素合并后添加进本层的结果中。排序可能有助于加快搜索速度,因为对于排序数组来说,当以一个数为起始的数组无法搜索到结果时,以比它更大的数为起点的数组也不可能搜索到结果,可以直接截断搜索。
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
def helper(candidates, target):
if not candidates:
return []
n, ans = len(candidates), []
for i in range(n):
if candidates[i]>target:
continue
if i>=1 and candidates[i]==candidates[i-1]:
continue
if candidates[i]==target:
ans.append([candidates[i]])
else:
dif = target-candidates[i]
if i < n-1:
for sub_ans in helper(candidates[i+1:], dif):
if sub_ans:
ans.append([candidates[i]]+sub_ans)
else:
break
return ans
candidates = sorted(candidates)
ans = helper(candidates, target)
return ans
与上一题解法类似,不同的是由于列表中可能存在相同的元素,故需要考虑到重复结果的问题。需要首先对列表进行排序,因为之后要递归调用原函数,而排序只需对原始列表进行一次即可,递归中不需要再次排序,故定义一个helper函数实现所需的结果查找功能,而在helper函数外完成第一次排序。为了防止出现重复结果,在遍历选择第一个元素时,必须保证选择的元素与上一个元素不同,即当发现选择的元素与上一个元素相同时,直接跳过以该元素为第一个元素的搜索,直接搜索下一个元素。
我的解法:
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
if not nums:
return []
n = len(nums)
if n == 1:
return [nums]
ans = []
for i in range(n):
if i < n-1:
for sub_ans in self.permute(nums[:i]+nums[i+1:]):
ans.append([nums[i]]+sub_ans)
else:
for sub_ans in self.permute(nums[:i]):
ans.append([nums[i]]+sub_ans)
return ans
运用递归算法,从列表中取出一个数字,那么列表中剩余数字仍然面临全排列的问题,因此可以迭代求解。需要注意的是如何表示除一个元素外的剩余数组。对于最后一个元素而言,可以表示为nuns[:i],对于其他元素则表示为nums[:i]+nums[i+1:]。
大佬解法:
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
def backtrack(depth=0):
if depth == n:
ans.append(nums[:])
for i in range(depth,n):
nums[depth], nums[i] = nums[i], nums[depth]
backtrack(depth+1)
nums[i], nums[depth] = nums[depth], nums[i]
ans = []
backtrack()
return ans
回溯搜索算法,定义backtrack函数,函数参数depth为当前正在排列的深度,若深度等于列表长度n则将结果记入结果列表中,注意因为之后需要对同一列表进行回溯操作,这里要使用拷贝nums[:],不然最后所有结果都会变回原数组的样子。对于nums和depth,depth左边的树都已经使用过,我们遍历选择depth及其右边的一个数作为depth的数,对于depth+1层,因为要确保使用过的数位于左侧,我们将num[depth]与选择的nums[i]互换,继续递归使用backtrack(depth+1),之后再将互换复原。
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
def backtrack(depth=0):
if depth==n:
ans.append(nums[:])
showed = {
}
for i in range(depth,n):
if showed.get(nums[i]):
continue
else:
showed[nums[i]] = 1
nums[i], nums[depth] = nums[depth], nums[i]
backtrack(depth+1)
nums[depth], nums[i] = nums[i], nums[depth]
ans = []
backtrack()
return ans
同样是回溯算法,但是引入剪枝,对于同一层,记录已经出现过的数字,若遍历到之前出现过的数字,就直接跳过。
我的解法:
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
n = len(matrix)
store = {
i:[] for i in range(n)}
for i in range(n):
for k in range(i+1,n):
store[k] = [matrix[k][n-1-i]]+store[k]
to_be_added = matrix[i][:n-i]+store[i]
for j in range(n):
matrix[j][n-1-i] = to_be_added[j]
首先根据观察,发现第i行会转换到第n-i-1列,那么我们从第一行开始转换。需要注意的是,如果我们从前往后的将行元素转换到旋转后的列时,行中靠后的元素可能会被新的列元素替换,造成信息丢失,因此我们要创建一个store字典来记录每一行丢失的信息,第i行丢失的信息为其最后i个元素。我们在做行旋转成列时,需要将行末尾已经丢失的信息替换为字典中记录的正确信息,再进行旋转。
我的解法:
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
ans = {
}
for s in strs:
letter = [0]*26
for si in s:
letter[ord(si)-ord('a')] += 1
letter = tuple(letter)
if not ans.get(letter):
ans[letter] = [s]
else:
ans[letter].append(s)
return list(ans.values())
将字符串转化为26位的字母数量编码,或者将字符串转为排序数组都能解决这个问题。
class Solution:
def myPow(self, x: float, n: int) -> float:
ans_set = {
1:x}
def pow_(x,n):
if ans_set.get(n):
return ans_set[n]
else:
ans = pow_(x,n//2)*pow_(x,n-n//2)
ans_set[n] = ans
return ans
return pow(x,n)
第一反应是这不就是x**n吗,然后想到这里面会有很多重复运算,比如24相当于小22x22,即22计算了两次。因此我们利用一个字典来储存已经计算过的次方,将原高次方逐渐分解为低次方计算。考虑到奇次方的情况,我们把原次方分解为n//2和n-n//2,递归求解即可得到答案,时间复杂度O(logn)。