算法刷题之三一维列表

列表

列表和字符串是算法中最常见的数据结构,在列表中又分为一维列表和二维列表。一维列表的数据结构即使很简单也会有很多复杂的算法思想。

一维列表

  1. 删除排序数组中的重复项
  2. 移动非0元素到头部
  3. 盛最多水的容器
  4. 三数之和
  5. 长度最小的子数组
  6. 无重复字符的最长子串
  7. 前缀和
  8. 合并区间

二维列表

  1. 二维矩阵最大子矩阵和
  2. 旋转图像
  3. 杨辉三角
  4. 对角线遍历
  5. 矩阵元素查找
  6. 容器盛水

删除排序数组中的重复项

题目:
给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-easy/x2gy9m/

方法:只要将不同的数字全部集中到前面就可以了。设置双指针,慢指针用来指向列表前部分里不重复的下标,快指针向后遍历找不重复的数字。找到一个就让慢指针加1,然后将快指针指向的不重复的值赋值给慢指针。
双指针:一种常用的,好用的数据处理方式。在快速排序中,将一个数组分成大小两个部分,用的就是双指针。

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        #时间复杂度为O(n),空间O(1)
        # 慢指针
        i = 1
        
        # 快指针
        for j in range(1, len(nums)):
            if nums[j] != nums[j - 1]:
                nums[i] = nums[j]
                i += 1
        nums[:] = nums[:i]
        return 

地道的写法:

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        if not nums:
            return 0
        i = 0
        for j in range(1, len(nums)):
            if nums[i] != nums[j]:
                i += 1
                nums[i] = nums[j]
        return i + 1

移动非0元素到头部

题目:
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]

说明:
必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。

链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-easy/x2ba4i/
方法:将0全部集中到列表尾部,换一个思路就是将所有非0保持顺序,移动到列表前部。和上一题思想类似,使用双指针,慢指针指向0的下标,快指针向后寻找数值不为0的元素,交换快慢指针的数值,将所有0全部移动到尾部

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        # 慢指针
        i = 0
        
        # 快指针
        j = 0

        while j < len(nums):
            # 不等于快慢指针交换,慢指针遇到0时会停下来,等待快指针指向非0时的交换
            if nums[j] != 0:
                nums[i],nums[j] = nums[j],nums[i]
                i += 1
            j += 1

思想:j遇到0元素不管,i遇到0元素停下。当j再遇到0元素时,和i的元素互相交换,最终完成将非零元素向前移动的目的

def func(arr):

    i = 0
    length = len(arr)

    for j in range(len(arr)):
        if arr[j] != 0:
            arr[i],arr[j] = arr[j],arr[i]
            i = i + 1


arr = [0,1,0,3,12]
func(arr)
print(arr)

盛最多水的容器

提示:
给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

说明:你不能倾斜容器。
链接:https://leetcode-cn.com/problems/container-with-most-water

输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

方法:
首先想到的是暴力解法,两层循环能够找到任意两个柱子之间的距离,乘以高度,取最大值即可。时间复杂度为O(2),
然后使用双指针也能解决这个问题。以左右两边为边界向中间进发,比较两个柱子中较小的一个,并向中间移动,一直到两个柱子相遇。在这个过程中能够找到面积最大的那一对。

class Solution:
    def maxArea(self, height: List[int]) -> int:
        if not height:
            return 0
        n = len(height)
        max_ares = 0

        l = 0
        r = n - 1

        while l < r:
            max_ares = max(max_ares, min(height[l], height[r]) * (r-l))

            if height[l] < height[r]:
                l += 1
            else:
                r -= 1
        return max_ares

关于这一题的详细解法:
https://leetcode-cn.com/problems/container-with-most-water/solution/on-shuang-zhi-zhen-jie-fa-li-jie-zheng-que-xing-tu/

三数之和

题目:
给一个数组[4,2,6,7,4,6],找到其中三个数的和为14的所有组合
方法
三数之和是使用双指针完成遍历,固定住第一个,然后使用双指针一头一尾向中间逼近
因为我们要同时找三个数,所以采取固定一个数,同时用双指针来查找另外两个数的方式。所以初始化时,我们选择固定第一个元素(当然,这一轮走完了,这个蓝框框我们就要也往前移动),同时将下一个元素和末尾元素分别设上 left 和 right 指针。画出图来就是下面这个样子:

当三个数的和大于0时,表明right需要左移以减少和,当小于0时,left需要右移,以增大和。
知识点: 双指针配合循环

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:

        length = len(nums)

        arr = []
        nums.sort()
        for i in range(length - 2):
            if nums[i] > 0:
                continue
            if i > 0 and nums[i] == nums[i-1]:
                continue
            
            L = i + 1
            R = length - 1
            
            while L < R:
                s = nums[i] + nums[L] + nums[R]
                if s == 0:
                    arr.append([nums[i],nums[L],nums[R]])
                    L += 1
                    R -= 1
                    while L < R and nums[L] == nums[L-1]:L += 1
                    while L < R and nums[R] == nums[R+1]:R -= 1
                elif s > 0:
                    R -= 1
                    while L < R and nums[R] == nums[R+1]:R -= 1
                else:
                    L += 1
                    while L < R and nums[L] == nums[L-1]:L += 1
            
        return arr

双指针小结
以上题目能力体现双指针的使用技巧,通常使用双指针,可以用在如下场景中:

  1. 从两端向中间迭代数组。

这时你可以使用双指针技巧:一个指针从头部开始,而另一个指针从尾部开始。这种技巧经常在排序数组中使用。

  1. 同时有一个慢指针和一个快指针。

确定两个指针的移动策略。与前一个场景类似,你有时可能需要在使用双指针技巧之前对数组进行排序,也可能需要运用贪心法则来决定你的运动策略。

长度最小的子数组

题目:
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:
输入:s = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

方法:使用滑动窗户能够完美解决这个问题。窗口右端加入元素,判断窗口中元素总和是否大于target,如果大于计算长度,然后再左端缩小窗口,元素总和小于target,这是右端会继续向下滑动。
知识点:变长滑动窗口

class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:


        left = 0
        res = float("inf")
        cur = 0
        for right in range(len(nums)):
            cur += nums[right]
            while cur >= s:
                print('cur:%d -left: %d' % (cur,nums[left]))
                res = min(res, right-left+1)
                cur = cur - nums[left]
                left += 1
        
        if res != float("inf"):
            return res
        else:
            return 0

无重复字符的最长子串

题目:
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

方法:判断一个子数组中是否有重复有很多种办法:

  1. 使用暴力双层遍历
  2. 使用字典,判断某元素是否在字典中
  3. 使用数组,判断元素是否在数组中

方法:使用滑动窗解决:窗口左侧不断加入元素,判断元素是否存在于窗口中,不在则继续加入,在则将重复的元素去掉,继续添加。在这个过程中会有一个最大值出现。
知识点: 变长滑动窗口

class Solution(object):
    
    def lengthOfLongestSubstring(self, s: str) -> int:
        # 字符串为空则返回零
        if not s:
            return 0

        window = []     # 滑动窗口数组
        max_length = 0  # 最长串长度

        # 遍历字符串
        for c in s:
            # 如果字符不在滑动窗口中,则直接扩展窗口
            if c not in window:
                # 使用当前字符扩展窗口
                window.append(c)
            # 如果字符在滑动窗口中,则
            # 1. 从窗口中移除重复字符及之前的字符串部分
            # 2. 再扩展窗口
            else:
                # 从窗口中移除重复字符及之前的字符串部分,新字符串即为无重复字符的字符串
                window[:] = window[window.index(c) + 1:]
                # 扩展窗口
                window.append(c)

            # 更新最大长度
            max_length = max(len(window), max_length)

        return max_length if max_length != 0 else len(s)

这一道题目还有更精彩的解法,滑动窗口不是只能用列表实现,还可以用字符串,双指针,字典等实现。
https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/solution/python-hua-dong-chuang-kou-xun-xu-jian-jin-de-3ge-/

滑动窗口小结
滑动窗口顾名思义就是对于序列取一段子序列,通常就是有一个大小可变的窗口,左右两端方向一致的向前滑动,右端固定,左端滑动;左端固定,右端滑动。
可以想象成队列,一端在push元素,另一端在pop元素,如下所示:

假设有数组[a b c d e f g h]
一个大小为3的滑动窗口在其上滑动,则有:

[a b c]
  [b c d]
    [c d e]
      [d e f]
        [e f g]
          [f g h]

适用范围
1、一般是字符串或者列表
2、一般是要求最值(最大长度,最短长度等等)或者子序列
算法思想
1、在序列中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引闭区间 [left, right] 称为一个窗口。
2、先不断地增加 right 指针扩大窗口 [left, right],直到窗口中的序列符合要求。
3、此时,停止增加 right,转而不断增加 left 指针缩小窗口 [left, right],直到窗口中的序列不再符合要求。同时,每次增加 left前,都要更新一轮结果。
4、重复第 2 和第 3 步,直到 right 到达序列的尽头。
思路其实很简单:第 2 步相当于在寻找一个可行解,然后第 3 步在优化这个可行解,最终找到最优解。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动。

前缀和

给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
输入:nums = [1,3,7,2,8]
输出:2,, [3,7] [2,8]

方法:
想要获取列表中一段子列表,使用双层循环就可以做到。能够获取到任意一段子列表,然后再将这一段nums[1:j]列表的和求解出来,这样时间复杂度会很高是O3,所以使用前缀和每一次内层循环时能够直接算出nums[i:j]的值。
知识点:前缀和

  def subarraySum(self, nums: List[int], k: int) -> int:
        num_times = 0
        for i in range(len(nums)):
            presum = 0
            for j in range(i,len(nums)):
                presum += nums[j]
                if presum == k:
                    num_times += 1
        return num_times

其中的presum += nums[j]就是前缀和,每次内层循环的和都记录下来,避免了计算nums[i:j]
但这种方式还不是最优,时间复杂度为O2,如果想要将时间复杂度降低到O,还需要使用字典这种方式。类似的解法是两数之和

使用一个字典维护前缀和与其出现的次数。

前3项的和为11,k=11。第一项和+sum(3,7) = sum(1,3,7),即sum(1) + 10 = sum(1,3,7),如果存在子数组sum=10,那么sum(1,3,7) - 10 就等于sum(1)。
即:sum(n) - k 在字典中,那么就说明有一个子数组=k

将两个数相加等于固定值,变成使用固定值-其中一个数=差值,判断差值在不在列表中

def subarraySum(self, nums: List[int], k: int) -> int:
        sum_dict = {0:1}
        pre_sum = 0
        num_times = 0
        for i in range(len(nums)):
            pre_sum += nums[i]
            if pre_sum - k in sum_dict:
                num_times += sum_dict[pre_sum-k]
            if pre_sum in sum_dict:
                sum_dict[pre_sum] += 1
            else:
                sum_dict[pre_sum] = 1
       
        return num_times

合并区间

题目:
给出一组区间,请合并所有重叠的区间。
请保证合并后的区间按区间起点升序排列。

示例1
复制
[[10,30],[20,60],[80,100],[150,180]]
返回值
[[10,60],[80,100],[150,180]]

方法:区间问题两边都是极限,解决这一类问题需要固定住一端,然后对另一端判断并处理
知识点:区间处理思想

class Solution:
    def merge(self , intervals ):
        if not intervals:
            return []
        intervals.sort(key=lambda x: x.start)
        res = [intervals[0]]
        
        for i in intervals[1:]:
            if res[-1].end < i.start:
                res.append(i)
            elif res[-1].end <= i.end:
                res[-1].end = i.end
        return res

一维列表小结

对于一维列表的算法求解有很多中技巧,像上面提到的双指针滑动窗口等。在这些技巧之下,需要对一维列表的最基础操作有深刻认识,那就是循环。下面来总结一下一维列表的循环有什么需要掌握的。
单层循环:单层循环能取到列表中每一个值,这没什么特别的。

arr = [5,2,9,6,10,6]

for i in range(len(arr)):
    print(arr[i])

5 2 9 6 10 6

双层循环:双层循环能对列表任意两个元素操作,比如两个元素之和,两个元素之积,甚至是两个元素中间元素的和。

arr = [5,2,9,6]

length = len(arr)

for i in range(length):
    for j in range(i, length):
        print('{}--{}'.format(arr[i], arr[j]))

5--5
5--2
5--9
5--6
2--2
2--9
2--6
9--9
9--6
6--6

双层循环使用的比较多,记住一点,双层循环能获取任意两个元素的组合。在这个最基础的操作之上再去看到一些技巧,技巧是对最基础解法的优化,所以对于一道题如果知道套路就上套路,不知道套路先用最暴力的解法,然后再往套路上靠。

posted @ 2021-06-24 18:05  金色旭光  阅读(553)  评论(0编辑  收藏  举报