leetcodeday19 –删除链表的倒数第 N 个结点

总结方法:对于链表,一般在链表开头先添加一个头元素,该元素指向链表的第一个元素。

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

一般方法:两次for循环

第一次计算长度,第二次重构链表

class Solution:
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        def getLength(head: ListNode) -> int:
            length = 0
            while head:
                length += 1
                head = head.next
            return length
        
        dummy = ListNode(0, head)
        length = getLength(head)
        cur = dummy
        for i in range(1, length - n + 1):
            cur = cur.next
        cur.next = cur.next.next
        return dummy.next

改进版:

双指针

思路与算法

我们也可以在不预处理出链表的长度,以及使用常数空间的前提下解决本题。由于我们需要找到倒数第n 个节点,因此我们可以使用两个指针first 和 second 同时对链表进行遍历,并且 first 比second 超前 n个节点。当first 遍历到链表的末尾时,second 就恰好处于倒数第 n 个节点。

具体地,初始时 first 和 second 均指向头节点。我们首先使用 first 对链表进行遍历,遍历的次数为 n。此时,first 和 second 之间间隔了 n−1 个节点,即 first 比second 超前了 n个节点。

在这之后,我们同时使用 first 和second 对链表进行遍历。当 first 遍历到链表的末尾(即 first 为空指针)时,second 恰好指向倒数第 n 个节点。

根据方法一和方法二,如果我们能够得到的是倒数第 n 个节点的前驱节点而不是倒数第 n 个节点的话,删除操作会更加方便。因此我们可以考虑在初始时将 second 指向哑节点,其余的操作步骤不变。这样一来,当first 遍历到链表的末尾时,second 的下一个节点就是我们需要删除的节点。

# @lc code=start
# 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)
        first = head
        second = dummy
        for i in range(n):
            first = first.next

        while first:
            first = first.next
            second = second.next
        
        second.next = second.next.next
        return dummy.next

复杂度分析

  • 时间复杂度:O(L),其中 L 是链表的长度。
  • 空间复杂度: O(1)。

leetcodeday18 —-四数之和

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。

示例 1:

输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:

输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

思路:三数之和+一层遍历

# @lc code=start
class Solution:
  def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
    n = len(nums)
    nums.sort()
    result=list()
    def threeSum(nums: List[int],targets: int,fst:int) -> List[List[int]]:
        n = len(nums)
        ans = list()
        
        # 枚举 a
        for first in range(n):
            # 需要和上一次枚举的数不相同
            if first > 0 and nums[first] == nums[first - 1]:
                continue
            # c 对应的指针初始指向数组的最右端
            third = n - 1
            target = targets-nums[first]
            # 枚举 b
            for second in range(first + 1, n):
                # 需要和上一次枚举的数不相同
                if second > first + 1 and nums[second] == nums[second - 1]:
                    continue
                # 需要保证 b 的指针在 c 的指针的左侧
                while second < third and nums[second] + nums[third] > target:
                    third -= 1
                # 如果指针重合,随着 b 后续的增加
                # 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
                if second == third:
                    break
                if nums[second] + nums[third] == target:
                    ans.append([fst,nums[first], nums[second], nums[third]])
        
        return ans
    
    for first in range(n):
        if first > 0 and nums[first] == nums[first - 1]:
                continue
        targets= target-nums[first]
        result.extend(threeSum(nums[first+1:],targets,nums[first]))
    return result

leetcodeday17 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:

输入:digits = “23”
输出:[“ad”,”ae”,”af”,”bd”,”be”,”bf”,”cd”,”ce”,”cf”]
示例 2:

输入:digits = “”
输出:[]
示例 3:

输入:digits = “2”
输出:[“a”,”b”,”c”]

提示:

  • 0 <= digits.length <= 4
  • digits[i] 是范围 ['2', '9'] 的一个数字

思路:遍历递归求解

# @lc code=start
class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        if digits=="": 
            return []
        n=len(digits)
        sets={"2":["a","b","c"],"3":["d","e","f"],"4":["g","h","i"],"5":["j","k","l"],"6":["m","n","o"],"7":["p","q","r","s"],"8":["t","u","v"],"9":["w","x","y","z"]}
        def upsort(strs): 
            if len(strs)==1: 
                return sets[strs]
    
            result = upsort(strs[:-1])
            rev=[]
            for i in range(len(result)):
                 for j in sets[strs[-1]]:
                    rev.append(result[i]+j)
            return rev
        return upsort(digits)

leetcodeday16 —最接近的三数之和

这个思路和上一个题差不多,不过简单一些,只需要找出三个数的和。因此给出官方答案。

给你一个长度为 n 的整数数组 nums和 一个目标值 target。请你从 nums中选出三个整数,使它们的和与 target 最接近。

返回这三个数的和。

假定每组输入只存在恰好一个解。

示例 1:

输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。

排序 + 双指针
思路与算法

题目要求找到与目标值 target 最接近的三元组,这里的「最接近」即为差值的绝对值最小。我们可以考虑直接使用三重循环枚举三元组,找出与目标值最接近的作为答案,时间复杂度为 O(N^3)。然而本题的 N 最大为 1000,会超出时间限制。

那么如何进行优化呢?我们首先考虑枚举第一个元素 a,对于剩下的两个元素 b 和 c,我们希望它们的和最接近 target−a。对于 b 和 c,如果它们在原数组中枚举的范围(既包括下标的范围,也包括元素值的范围)没有任何规律可言,那么我们还是只能使用两重循环来枚举所有的可能情况。因此,我们可以考虑对整个数组进行升序排序,这样一来:

假设数组的长度为 n,我们先枚举 a,它在数组中的位置为 i;

为了防止重复枚举,我们在位置 [i+1,n) 的范围内枚举 b 和 c。

当我们知道了 b 和 c 可以枚举的下标范围,并且知道这一范围对应的数组元素是有序(升序)的,那么我们是否可以对枚举的过程进行优化呢?

答案是可以的。借助双指针,我们就可以对枚举的过程进行优化。我们用 p_b和 p_c
分别表示指向 b 和 c的指针,初始时,p_b指向位置 i+1,即左边界;p_c指向位置 n-1,即右边界。在每一步枚举的过程中,我们用 a+b+c 来更新答案,并且:

如果 a+b+c≥target,那么就将 p_c向左移动一个位置;

如果 a+b+c<target,那么就将 p_b向右移动一个位置。这是为什么呢?我们对 a+b+c≥target 的情况进行一个详细的分析:

如果 a+b+c≥target,并且我们知道 p_b到 p_c这个范围内的所有数是按照升序排序的,那么如果 p_c不变而 p_b向右移动,那么 a+b+c 的值就会不断地增加,显然就不会成为最接近 target 的值了。因此,我们可以知道在固定了 p_c的情况下,此时的 p_b就可以得到一个最接近target 的值,那么我们以后就不用再考虑 p_c了,就可以将 p_c向左移动一个位置。

同样地,在 a+b+c<target 时:

如果 a+b+c<target,并且我们知道 p_b到 p_c这个范围内的所有数是按照升序排序的,那么如果 p_b不变而 p_c向左移动,那么 a+b+c 的值就会不断地减小,显然就不会成为最接近 target 的值了。因此,我们可以知道在固定了 p_b的情况下,此时的 p_cp就可以得到一个最接近 target 的值,那么我们以后就不用再考虑 p_b了,就可以将 p_b向右移动一个位置。实际上,p_b和 p_c就表示了我们当前可以选择的数的范围,而每一次枚举的过程中,我们尝试边界上的两个元素,根据它们与 target 的值的关系,选择「抛弃」左边界的元素还是右边界的元素,从而减少了枚举的范围。这种思路与盛最多水的容器 中的双指针解法也是类似的。

小优化

本题也有一些可以减少运行时间(但不会减少时间复杂度)的小优化。当我们枚举到恰好等于target 的 a+b+c时,可以直接返回 target 作为答案,因为不会有再比这个更接近的值了。

另一个优化与 三数之和的官方题解 中提到的类似。当我们枚举 a, b, c中任意元素并移动指针时,可以直接将其移动到下一个与这次枚举到的不相同的元素,减少枚举的次数。

class Solution:
    def threeSumClosest(self, nums: List[int], target: int) -> int:
        nums.sort()
        n = len(nums)
        best = 10**7
        
        # 根据差值的绝对值来更新答案
        def update(cur):
            nonlocal best
            if abs(cur - target) < abs(best - target):
                best = cur
        
        # 枚举 a
        for i in range(n):
            # 保证和上一次枚举的元素不相等
            if i > 0 and nums[i] == nums[i - 1]:
                continue
            # 使用双指针枚举 b 和 c
            j, k = i + 1, n - 1
            while j < k:
                s = nums[i] + nums[j] + nums[k]
                # 如果和为 target 直接返回答案
                if s == target:
                    return target
                update(s)
                if s > target:
                    # 如果和大于 target,移动 c 对应的指针
                    k0 = k - 1
                    # 移动到下一个不相等的元素
                    while j < k0 and nums[k0] == nums[k]:
                        k0 -= 1
                    k = k0
                else:
                    # 如果和小于 target,移动 b 对应的指针
                    j0 = j + 1
                    # 移动到下一个不相等的元素
                    while j0 < k and nums[j0] == nums[j]:
                        j0 += 1
                    j = j0

        return best

转置卷积、微步卷积、空洞卷积

1、转置卷积 又可以称为 反卷积(数据从低维到高维)

转置卷积是一个将低维特征转换到高维特征。为什么叫做转置卷积呢?其实就是引入了转置的思想。

  • 假设我们现在有一个p维的向量Z,然后有个d维的向量X,p<d.
  • 这样就会出现 Z = W·X,其中W的维度为(p,d),叫做转换矩阵.
  • 现在,我们要从Z通过相似的方法来得到X,这样我们不难想到:X= W.T · X 其中W.T的维度是(d,p),但是这两个W并不是同一个值,而是具有转置的形式而已。

上面的例子是一维向量的情况,在卷积操作中,也可以借用这个思想,从低维到高维的转变可以在形式上看成是转置操作。

  • 比如我们现在对一个4 * 4的输入做3 * 3的卷积操作(m=3核的大小,stride=1,padding=0),得到一个2 * 2的特征映射
  • 如果我们想对这个2 * 2特征映射进行3 * 3卷积,并反过来得到4 * 4的输出,就可以用到转置卷积:

如上图所示,对2 * 2的特征映射先做(m-1) padding得到6 * 6的输入,然后对其进行3*3的卷积操作,从而得到4 * 4的特征映射。 同样,这个两个3 * 3的卷积参数不是一致的,都是可学习的。

2、微步卷积(步长不为1的转置卷积(反卷积))

微步卷积其实是一个转置卷积的一个特殊情况,就是卷积操作的stride ≠ 1。因为在现实中,为了大幅度降低特征维数,卷积的步长会大于1。同样,为了大幅度提高特征维度,我们也可以用通过卷积来实现,这种卷积stride < 1 ,所以叫做微步卷积。

  • 如果卷积操作stride>1,其对应的转置卷积步长为1/s :就是在输入特征之间插入s – 1个0,来使得步长变’小’。
  • 例如,我对一个5 * 5的输入做3 * 3的卷积操作(m=3, padding=0,但是stride=2),从而我得到的特征输出为2 * 2.
  • 现在对其进行微步卷积:

跟转置卷积一样,先对2 * 2的输入做(m-1)padding ,然后再在特征之间插入stride -1个0,从而得到一个7 * 7的特征输入,然后对其做3 * 3 的卷积操作,得到5 * 5的特征输出。

如何计算反卷积:

当输入的矩阵高宽为n,核大小为k,padding为p,stride为s

  • 当输入的矩阵高宽为 n ,核大小为 k ,padding为 p , stride为 s 。
  • 转置卷积作用后的尺寸变化: \(n^{1}=s n+k-2 p-s\) 。如果想让高宽成倍增加,那么 \(k=2 p+s\) 。
  • 卷积作用后的尺寸变化: \(n^{1}=\left\lfloor\frac{n-k+2 p+s}{s}\right\rfloor\) 。如果想让高宽成倍减少,那么 \(k=2 p+1\)。

1、当填充为0步长为1时

将输入填充 k − 1 。(k是 卷积核大小)
将核矩阵上下,左右翻转。
之后正常做填充为0(无填充),步幅为1的卷积。

2 当填充为 p 步幅为1时

将输入填充 k − p − 1 。
将核矩阵上下,左右翻转。
之后正常做填充为0,步幅为1的卷积。

3 当填充为 p pp 步幅为s ss时

在行和列之间插入s − 1 行和列。
将输入填充 k − p − 1。
将核矩阵上下,左右翻转。
之后正常做填充为0,步幅为1的卷积。

3、空洞卷积(膨胀卷积)

通常来说,对于一个卷积层,如果希望增加输出单元的感受野,一般由三个方式:

  1. 增加卷积核大小
  2. 增加层数
  3. 进行pooling操作

其中1和2都会增加参数量,而3会丢失特征信息。这样我们就可以引入‘空洞卷积’的概念,它不增加参数量,同时它也可以增加输出的感受野。
它主要是通过给卷积核插入空洞来增加其感受野大小,如果卷积核每两个元素之间插入d-1个空洞,那么卷积核的有效大小为:M = m + (m-1)*(d-1)

leetcodeday15 —三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

示例 2:

输入:nums = []
输出:[]

示例 3:

输入:nums = [0]
输出:[]

提示:

  • 0 <= nums.length <= 3000
  • -105 <= nums[i] <= 105

思路双指针方法:依次对每个元素进行如下判断:-s[i]==s[j]+s[m],其中 s[j]+s[m] 要遍历剩下的元素(采用双指针)


# @lc code=start
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
            if set(nums)=={0} and len(nums)>=3:
                return [[0,0,0]]
                   
            sets=set()
            lens=len(nums)
            nums =sorted(nums)
            result=[]
            for i in range(lens):
                newlist=nums[0:i]+nums[i+1:]
                
                m=0
                j=lens-1-1
                if nums[i] in sets:
                    continue
                else:
                    sets.add(nums[i])
                while m<j:
                    if newlist[m]+newlist[j]==-nums[i] and sorted([newlist[m],newlist[j],nums[i]])not  in result:
            
                        result.append(sorted([newlist[m],newlist[j],nums[i]]))
                        m=m+1
                    elif newlist[m]+newlist[j]>-nums[i]:
                        j=j-1
                    else:
                        m=m+1
            return result

# @lc code=end

就差超时了……

方法一:排序 + 双指针
题目中要求找到所有「不重复」且和为 0 的三元组,这个「不重复」的要求使得我们无法简单地使用三重循环枚举所有的三元组。这是因为在最坏的情况下,数组中的元素全部为 0,即

[0, 0, 0, 0, 0, …, 0, 0, 0]
任意一个三元组的和都为 00。如果我们直接使用三重循环枚举三元组,会得到 O(N^3)个满足题目要求的三元组(其中 N是数组的长度)时间复杂度至少为 O(N^3)。在这之后,我们还需要使用哈希表进行去重操作,得到不包含重复三元组的最终答案,又消耗了大量的空间。这个做法的时间复杂度和空间复杂度都很高,因此我们要换一种思路来考虑这个问题。

「不重复」的本质是什么?我们保持三重循环的大框架不变,只需要保证:

第二重循环枚举到的元素不小于当前第一重循环枚举到的元素;

第三重循环枚举到的元素不小于当前第二重循环枚举到的元素。

也就是说,我们枚举的三元组 (a, b, c) 满足 a≤b≤c,保证了只有 (a,b,c) 这个顺序会被枚举到,而 (b,a,c)、(c,b,a) 等等这些不会,这样就减少了重复。要实现这一点,我们可以将数组中的元素从小到大进行排序,随后使用普通的三重循环就可以满足上面的要求。

同时,对于每一重循环而言,相邻两次枚举的元素不能相同,否则也会造成重复。举个例子,如果排完序的数组为

[0, 1, 2, 2, 2, 3]

我们使用三重循环枚举到的第一个三元组为 (0, 1, 2),如果第三重循环继续枚举下一个元素,那么仍然是三元组 (0, 1, 2),产生了重复。因此我们需要将第三重循环「跳到」下一个不相同的元素,即数组中的最后一个元素 3,枚举三元组 (0, 1, 3)。

nums.sort()
for first = 0 .. n-1
    // 只有和上一次枚举的元素不相同,我们才会进行枚举
    if first == 0 or nums[first] != nums[first-1] then
        for second = first+1 .. n-1
            if second == first+1 or nums[second] != nums[second-1] then
                for third = second+1 .. n-1
                    if third == second+1 or nums[third] != nums[third-1] then
                        // 判断是否有 a+b+c==0
                        check(first, second, third)

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/3sum/solution/san-shu-zhi-he-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

改进:

nums.sort()
for first = 0 .. n-1
    if first == 0 or nums[first] != nums[first-1] then
        // 第三重循环对应的指针
        third = n-1
        for second = first+1 .. n-1
            if second == first+1 or nums[second] != nums[second-1] then
                // 向左移动指针,直到 a+b+c 不大于 0
                while nums[first]+nums[second]+nums[third] > 0
                    third = third-1
                // 判断是否有 a+b+c==0
                check(first, second, third)

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        n = len(nums)
        nums.sort()
        ans = list()
        
        # 枚举 a
        for first in range(n):
            # 需要和上一次枚举的数不相同
            if first > 0 and nums[first] == nums[first - 1]:
                continue
            # c 对应的指针初始指向数组的最右端
            third = n - 1
            target = -nums[first]
            # 枚举 b
            for second in range(first + 1, n):
                # 需要和上一次枚举的数不相同
                if second > first + 1 and nums[second] == nums[second - 1]:
                    continue
                # 需要保证 b 的指针在 c 的指针的左侧
                while second < third and nums[second] + nums[third] > target:
                    third -= 1
                # 如果指针重合,随着 b 后续的增加
                # 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
                if second == third:
                    break
                if nums[second] + nums[third] == target:
                    ans.append([nums[first], nums[second], nums[third]])
        
        return ans