LeetCode第一阶段(一)【数组篇】

LeetCode 283 Move Zeros

给定一个数组nums,写一个函数,将数组中所有的0挪到数组的末尾,而维持其他所有非0元素的相对位置。

举例:nums = [0,1,0,3,12],函数运行后的结果为[1,3,12,0,0]

程序初始:传入的是原始数组nums

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """

首先先复习一下对于Py中循环的三种写法:

list = ['python','c++','java','c','go']
# 方法一:直接遍历列表
for i in list:
    print(i)
    print(list.index(i))
    
# 方法二:用range的方式来遍历
for i in range(len(list)):
    print(list[i])
    print(i)

# 方法三:使用enumerate的方法遍历
for i,val in enumerate(list):
    print(val)
    print(i)

直观的思路:

先扫描一遍数组,然后把数组中的非零元素全部拿出来存到一个non_list中,然后将list中和non_list相同index的值替换,再将list将没有填补的位置的元素全部赋成0。

程序实现:

  1. 我们先定义一个变量来存储所有的非零元素,接着扫描一遍列表,选出所有的非零元素:
nonZeroElements = [num for num in nums if(num)]
  1. 用非零元素替代之前list的元素:
# 方法一: 使用zip并行遍历
for i,j in zip(nonZeroElements,nums):
    nums[nums.index(j)] = i

# 方法二:对长度小于nonlist的进行赋值
for i in range(len(nums)):
    if i < len(nonZeroElements):
        nums[i] = nonZeroElements[i]
  1. 最后将之后的元素赋为0
for i in range(len(nums)-len(nonZeroElements)):
    nums[len(nonZeroElements)+i] = 0
# 如果第二步用方法二赋值的话,可以直接加个else判断
class Solution:
    def moveZeroes(self, nums) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        nonZeroElements = [num for num in nums if(num)]
        for i,j in zip(nonZeroElements,nums):
            nums[nums.index(j)] = i
        for i in range(len(nums)-len(nonZeroElements)):
            nums[len(nonZeroElements)+i] = 0
        return nums

复杂度分析

对于上述方法,循环执行了n次,故时间复杂度为O(n);因为也用了一个辅助空间数组,故空间复杂度为O(n);

优化

在上个算法中,最明显的是我们使用了一个新的list,即多了一个辐助的空间。那么我们可不可以不使用空间,而在原地直接对非0元素进行移动呢?

那么直接的思路是,少使用一个空间,多使用一个索引,使用另外一个索引k,让[0…k)中保存所有遍历过的k个非0元素。

操作的过程是:通过索引i遍历这个数组,当索引i遇到非零的元素时,将其指向的值赋值给k索引的位置,再将k索引后移一位,直到索引i遍历完所以值为止,最后将索引k位置及之后的元素都赋值为0即可。

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        k = 0
        for i in range(len(nums)):
            if(nums[i]):
                nums[k] = nums[i]
                k += 1
        for i in range(len(nums)-k):
            nums[k] = 0
            k += 1
        return nums

复杂度分析,其循环执行了n次,故时间复杂度为O(n),而其没有用辐助空间数组,故其空间复杂度为O(1);

再优化

继续考虑上述方案,将值遍历完后,我们发现最后还要再对索引k位置及之后的元素再赋值为0,那么可不可以在循环的过程中就直接把0放在后面呢?

那么在刚才的思路上,我们只需把赋值给换成互换:遍历到第i个位置的元素后,我们不是把i位置的非零元素赋值给k,而是将i位置的非零元素和k位置的零元素互换,从而避免了最后的赋值。

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        k = 0
        for i in range(len(nums)):
            if(nums[i]):
                nums[i],nums[k] = nums[k],nums[i]
                k += 1
        return nums

复杂度分析,其循环执行了n次,故时间复杂度为O(n),而其没有用辐助空间数组,故其空间复杂度为O(1);

对可能输入的数据来进行优化。很多优化本身,就是对特殊情况进行优化,如果输入的数组都是非零元素,那么对于上述情况,我们每次遍历都是对相同位置的元素进行了互换,因此我们还可以在其中加入判断条件,避免这种自身元素的互换。

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        k = 0
        for i in range(len(nums)):
            if(nums[i]):
                if (i!=k):
                    nums[i],nums[k] = nums[k],nums[i]
                    k += 1
                else:                  
                    k += 1
        return nums

LeetCode 27 Remove Element

问题

给定一个数组nums和一个数指val,将数组中所有等于val的元素删除,并返回剩余元素的个数。

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

例:nums = [3,2,2,3],val = 3; 返回2,且nums中前两个元素为2;

前言

peace,如果在面试中遇到了删除类问题,务必要问清楚细节,显示你考虑问题的全面,切忌上手就做!:

  1. 怎么来定义删除?从数组中直接去除,还是放在数组末尾?
  2. 删除过后剩余元素的排列是否要保证原有的顺序?
  3. 是否有空间复杂度的需求,用不用开辟新的额外空间?

题目的要求是空间复杂度为O(1),删除后剩余元素排列的顺序可以改变,即最后返回元素的个数即可。

1. 不使用remove

解决这个的思路和昨天的问题一样,如果想不额外再创造空间,那么首先想到的就是多个指针,然后通过指针索引来赋值或交换的操作,来达到目的。

那么我们依然使用双指针的情况,用[0…i)来表示删除过了指定值的数组。用j对list进行遍历:开始i和j指向元素相同,如果当前j指的值为指定的值,我们不用管它,继续遍历j,当j遍历到非指定元素值时,将这个元素赋值给i指向的元素,接着为了准备判断下一个指定值的元素,让i向后移动一位。
实现如下:

class Solution(object):
    def removeElement(self, nums, val):
        i = 0;
        for j in range(len(nums)):
            if (nums[j] != val):
                nums[i] = nums[j];
                i += 1
        return i; 

因为遍历了n次,其时间复杂度为O(n),空间复杂度为O(1);

优化

正如上一篇中所说的,很多优化本身,就是对特殊情况进行优化,比如对可能输入的数据来进行优化。如果输入的数据是需要删除数据在首尾的情况:

比如:[1,2,3,5,4],要删除4;[4,1,2,3,5]要删除4;

使用上种方法,对于第一种情况,我们就做了很多次自身的复制;对于第二种情况更惨,我们让每个不是指定值得元素都进行了左移。那么如何避免多余的赋值,从而进行优化呢?

根据上一篇中的优化思路–为了避免最后对0再次遍历,采用了交换元素的思想,这样让0元素直接都在了末尾。

因为题中不用考虑顺序,那么在这里,我们可以使用首尾两个指针,同样令[0…i)来表示删除过了指定值的数组长度.(i表示末尾指针;j表示头指针索引)

使用j对list进行遍历,如果开始j指向的值是我们要的指定值,那直接将j与i进行交换,让指定的值排在最后,再让末尾指针i向前移动一位;如果不是指定的值,那么只需让头指针j向后移动即可。

这里要注意的时,当交换完毕后,我们仍然需要对当前交换完的值进行判断,所以这里最好使用while的写法,当i和j相同指向同一个元素时,循环跳出。

class Solution(object):
    def removeElement(self, nums, val):
        i = 0
        j = len(nums)
        while i < j:
            if (nums[i] == val):
                nums[i] = nums[j - 1]
                j -= 1
            else:
                i += 1
        return n

2. 使用remove

对于Py,可以取巧使用remove来进行删除,即遍历一遍list,哪个元素和提供的元素相同的话,则直接从list中移除。

复习下Py常用的三种删除的写法:

  1. remove删除

list.remove()进行删除,方法内传入指定要删除元素的值,当list中没有该元素时会报错,无返回值

nums = [1,3,2,0]
nums.remove(3)
[1,2,0]
  1. del删除

del是通过传入索引,根据位置来进行删除,无返回值

nums = [1,3,2,0]
del nums[3]
[1,3,2]
  1. pop删除

pop也是根据传入的索引进行删除,其返回值为被删除的元素。无参数值传入时默认删除最后一个

nums = [1,3,2,0]
nums.pop(1)
3
[1,2,0]

在Py的删除操作中,会有一个坑,我们按照思路进行删除,如下:

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        for i in nums:
            if i == val:
                nums.remove(val)
        return len(nums)

但是这样用for循环遍历在提交后会报错,错误样例为:

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

删除时,并没有把结尾的2删除干净,原因是因为我们用py对list进行遍历时,每次remove会改变list的长度,导致最后一个元素会没有遍历到,从而会对结尾需要删除的情况判断出错。因此对这种情况用while判断在不在list中即可。

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        while val in nums:
            nums.remove(val)
        return len(nums)

下一题:

LeetCode 26 Remove Duplicated from Sorted Array

LeetCode 80 Remove Duplicated from Sorted Array 2

LeetCode 26 Duplicated from Sorted Array

问题描述

给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。

例如给定nums=[1,1,2],结果应该会返回2,且nums的前两个元素为1和2;

PRE

显然,这依然是一个删除类的问题,对于删除类问题,依旧问三个问题

  1. 怎么来定义删除?从数组中直接去除,还是放在数组末尾?
  2. 删除过后剩余元素的排列是否要保证原有的顺序?
  3. 是否有空间复杂度的需求,用不用开辟新的额外空间?

一. 放在数组末尾

思路

不需要额外的空间,即空间复杂度为O(1),同样我们可以考虑使用双指针,因为是排序好的数组,故不存在相邻元素不想等,但和第三个位置上元素相等的情况,只需比较相邻元素相等即可。

假定[0…i)表示移除后的数组,i和j指向数组的先后位置:

如果两个位置元素相同,则将j继续向后移动,继续和i比较;如果两个位置元素不相同,则先将i向后移动一个位置,然后将j指向的值赋给i,再将j向后移动作比较;
(如果之前是相同的,那么此时i与i-1是相同的值,所以将不同的值替换;如果之前不同,那i和j都是指向同一个元素,同一个元素赋值也ok)

实现

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        i,j = 0,1
        while j < len(nums):
            if nums[i] == nums[j]:
                j += 1
            else:
                i += 1
                nums[i] = nums[j]
                j += 1
        return i+1

在刚开始写时,因为觉得py没法像c++一样可以在for循环内直接设定起始值,因而使用while麻烦了一些。实际上也可以使用使用py中的for循环:

py中的for循环的range写法中,有三种传参数的类型:

1个参数:stop

2个参数:start, stop

3个参数:start, stop, ste

因此可以使用for替代while,通过传入参数来控制开始和结束的遍历,这样可以少考虑变量的执行顺序:

实现

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

复杂度分析

遍历的次数为n,故时间复杂度为O(n);只对自身进行操作,故空间复杂度为O(1);

二. 从数组中删除

思路

上篇我们说过,在py中通过for循环来删除元素时,会出现索引错位的问题,上篇中使用的是while进行解决。在这里,我们可以仍然使用for循环,但是采用逆序遍历的方式来防止索引错位。

具体方法就是通过range方法,有len-1处start,0处为结尾,步长为-1,因为此时遍历的为索引,所以可以用pop或del方法对元素进行删除。

实现

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

复杂度

遍历次数为n,但是py中list的方法pop(i)的时间复杂度为O(n),(如果是pop最后一个元素的话时间复杂度为O(1));

故总时间复杂度为O(n^2);只对自身进行操作,故空间复杂度为O(1);

LeetCode 80 Remove Duplicated from Sorted Array II

题目描述

给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。

如nums = [1,1,1,2,2,3],结果返回5,且nums的前五个元素为:1,1,2,2,3

复杂度

这次题目和上道差不多,只是去重的规则改变了一下,变成了相同的值可以有两个。

ok,那考虑下,如何实现限制最多两次的重复出现。

同样,考虑问题时先不要去考虑边界,先从中间的值入手。如果开始遍历i时,当i-1与i+1的元素值不同,则表示至多有两个相同元素,此时可以继续后移比较,后移多少位呢?同样,刚才的判断只是证明了i-1与i+1不同,但是i和i+1还是可能相同,所以和刚才情况一样,只需后移动一位即可。

再考虑边界的情况,之前的写法我们默认可以有一个重复值,故i从0位置开始;此时可以有两个重复值,故i从1位置开始,另一个索引j在i的后一位,则从2位置开始。

实现

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

复杂度

遍历了n次,时间复杂度为O(n);在自身上完成,空间复杂度为O(1);

LeetCode 75 Sort Colors

题目描述

给定一个包含红色、白色和蓝色,一共n个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

此题中,我们使用整数 0、1 和 2 分别表示红色、白色和蓝色。

注意:
不能使用代码库中的排序函数来解决这道题。

前言

n个元素的数组,且取值只有三种可能,故可能不是根据一般的排序来进行考虑,但是,如果面试上遇到了这个问题实在没有优化思路,首先应该考虑的是把问题先解决掉,再考虑用更好的方法,可以用普通排序算法先去解决。

首先我们想到的是快排,但是快排的时间复杂度平均为O(nlogn),能不能使用一种排序在O(n)内先解决问题?

一. 计数排序

思想

首先扫描一遍数组,统计0,1,2分别有多少个,再将0,1,2依次来放回数组,最后返回。

实现

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        count = {
    
    0:0,1:0,2:0}
        for i in range(len(nums)):
            count[nums[i]] += 1
        index = 0
        for i in range(count[0]):
            nums[index] = 0
            index += 1
        for i in range(count[1]):
            nums[index] = 1
            index += 1
        for i in range(count[2]):
            nums[index] = 2
            index += 1
        return nums

时间复杂度

时间复杂度,遍历了n次,时间复杂度为O(n);

对于空间复杂度,开了一个字典,为O(k)级别,k为元素的取值范围;

二. 三路快排思想扩展

在计数排序中,第一次遍历统计元素频率,第二次遍历来统计数组,用到了两次遍历,那有没有只扫描数组一遍,来执行上述操作呢?

只扫描一遍就可以元素进行排序,这里容易想到的就是快排,因为快排的思想是:每次从当前考虑的数组中选择一个元素,以这个元素为基点,之后想办法把该元素放在它在排好序后应该所处的位置上。此时该元素就都具有了一个性质,在该元素之前的都是比这个元素小,之后都是比这个元素大,从而完成了对该元素的排序。

然而上述题目中的元素有三个值:0,1,2,而快排只是将其分为两拨,一拨小于指定值,一波大于指定值。

这里我们可以考虑下快排的扩展排序,回忆自己的基础知识,对快排的优化中,有随机初始化阈值,有二路快排,三路快排。而在三路快排中,其分组是将小于阈值,等于阈值,大于阈值分成了三波,而这里恰恰只有三个值,因此挪用三路快排的思想进行使用。

思路

因为只有三个元素,所以只对所有元素执行一次三路快排即可。

选取一个标定点,对于这个标定点,有很多个和这个标定点相等的元素值,所以将整个数组分为了三份,分别是小于V,等于V,和大于V。之后在小于V和大于V的地方,继续执行这种三路快排。

  1. 数组元素为0:

设置一个索引zero,索引从0到zero的数组元素一直保持为0;

  1. 数组元素为2
    设置一个索引叫做two,在数组中从two到n-1,一直保持为2;

  2. 数组元素为1
    当然我们也有一个遍历索引i,当索引i从zero+1到i-1的索引处,我们将其赋值为1;

同快排中考虑如何把阈值元素放在指定的排序位置上一样,当我们遍历到某一个元素e时,我们如何操纵这一元素e,使得数组一直维持上述的规则?

假设要遍历到的元素e有三种情况:

当e为1时,我们直接将其纳入到属于1的空间:zero+1到i-1,继续i++;

当e为2时,我们可以拿出two索引位置之前的元素,让其与2这个元素相交换,紧接着two指针–;

当e为0时,zero为值为0区间的最后一个元素,故将zero+1的1与这个e交换,再将zero++;

实现

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        zero = -1   # nums[0...zeros] == 0
        two = len(nums) # nums[two...n-1] == 2
        i = 0
        while i < two:
            if nums[i] == 1:
                i += 1
            elif nums[i] == 2:
                two -= 1
                nums[i],nums[two] = nums[two],nums[i]
            else: # nums[i] == 0
                zero += 1
                nums[zero],nums[i] = nums[i],nums[zero]
                i += 1
        return nums

时间复杂度

因为整体只是遍历了一遍就完成了排序,故其时间复杂度为O(n);在本地完成操作,空间复杂度为O(1);

LeetCode 88 Merge Sorted Array

题目描述

给定两个有序整数数组nums1 和 nums2,将 nums2 合并到nums1中,使得num1 成为一个有序数组。

说明:

初始化nums1 和 nums2 的元素数量分别为m 和 n。
你可以假设nums1有足够的空间(空间大小大于或等于m + n)来保存 nums2 中的元素。

示例:

输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3

输出:[1,2,2,3,5,6]

核心:

空间复杂度为O(1)

初始化

前言

这个问题其实就是做归并排序中的归并那一步,ok,那么回忆下归并排序是怎么做的?

当要排序一个数组时,归并排序做的是,首先把数组分为一半,然后把左边的数组给排序,再把右边的数组给排序,之后再将他们归并起来(merge)。对左边的数组排序时,再分别将左边和右边的数组分成一半,然后对每一个部分先排序再归并。当分到某一个细度时,即一个元素时,只需对其归并即可。

归并的过程中,使用三个索引,来在数组内进行追踪,跟踪的位置,两个排好序的数组当前要考虑的元素,其中i,j指向的是当前正在指向的元素,k指向的是这两个元素最终应该放到的归并数组的位置,k的定义不表示归并结束后放置的位置,而表示下一个需要放的位置。

归并排序和快排的时间复杂度一样,都为O(nlogn),具体原因是,对于递归类函数的时间复杂度判断中,总体的时间复杂度为:递归的深度乘以每个递归函数的时间复杂度。

对于归并排序来说,其每次都是二分,递归的深度相当于是求2的几次方等于总长度,故其递归深度为log_2N,根据换底公式,其为log以任意数为底的n乘以一个常数,而其遍历n次,时间复杂度可表示为O(nlogn)。

对于空间复杂度,常规的归并排序需要一个存储空间的数组,故空间复杂度为O(n);

思路

对于这道题目而言,使用了归并排序的归并操作,其题目要求是将nums2合并到nums1中,使得nums1成为一个有序数组,即要求用**空间复杂度为O(1)**来进行解决。

在题目中有:

  1. m和n表示的是nums1和nums2中已经初始化的元素数量,而并非nums1和nums2的空间大小,也就是说nums1中空间足够大,但其中m个空间设置了该设的值,故我们可以将nums1的空间作为归并排序中的那个‘额外’的数组;
  2. 合并后的总的有效数目为m+n;
  3. 都是从0开始,所以nums1和nums2的最后一个元素的索引分别是m-1和n-1,合并后的nums1的最后一个元素的索引应该是m+n-1;

如何将其作为额外的数组呢,首先我们先考虑特殊情况,就是如果nums2的数组中的值都比nums1要小,那么nums2的值就都排入了那个额外的数组中,因此需要将nums1中需要留出前n个位置,而原来的位置放在原来的位置加n处。例如:nums1中有2个元素,m=2,nums2中有3个元素,n=3,那么需要把nums1中的第一个位置元素放在第四个位置上,第二个位置元素放在第五个位置上。于是有这样的初始化:

for i in range(m):
    nums1[i+n] = nums1[i]

但是这样顺序的做初始化会出现一个问题,就是当m>n时,顺序的这样迭代,会将原来的值给占掉,所以需要采用从后向前的方式来初始化,将索引为m-1的赋给索引为n+m-1,最后将索引为0的赋值给索引为n。初始化如下:

for i in range(n+m-1,n-1,-1):
    nums1[i] = nums1[i - n]

初始化完毕后,就按正常归并的思路走,设定一个索引k,用来指向每一个比较好的元素,再用i和j来表示左数组和右数组,分别对左数组和右数组的每个值进行比较,谁小就将值赋给k所在的位置,然后相应位置++,且k++,如果位置超出了自己数组的大小,就将另一数组中的所有值直接赋值即可。

class Solution:
    def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        """
        Do not return anything, modify nums1 in-place instead.
        """
        for i in range(n+m-1,n-1,-1):
            nums1[i] = nums1[i - n]
        i = n;  
        j = 0;  
        k = 0;  
        while k < n + m:
            if i >= n+m :
                nums1[k] = nums2[j]
                k += 1
                j += 1
            elif j >= n: 
                nums1[k] = nums1[i]
                k += 1
                i += 1
            elif nums1[i] < nums2[j]:
                nums1[k] = nums1[i]
                k += 1
                i += 1
            else:
                nums1[k] = nums2[j]
                k += 1
                j += 1

归并排序

当要排序一个数组时,归并排序做的是,首先把数组分为一半,然后把左边的数组给排序,再把右边的数组给排序,之后再将他们归并起来。对左边的数组排序时,再分别将左边和右边的数组分成一半,然后对每一个部分先排序再归并。当分到某一个细度时,即一个元素时,只需对其归并即可。

多使用了存储的空间,使用了O(n)的空间复杂度;

把两个排好序的数组合并为一个排序好的数组;

使用三个索引,来在数组内进行追踪,跟踪的位置,两个排好序的数组当前要考虑的元素;

i,j指向的是当前正在指向的元素,k指向的是这两个元素最终应该放到的归并数组的位置,k的定义不表示归并结束后放置的位置,而表示下一个需要放的位置。

维持好算法中的变量的定义,在循环中一直满足;

LeetCode 215 Kth Largest Element in an Array

在一个整数序列中寻找第k大的元素:

如:给定数组[3,2,1,5,6,4], k = 2 , 结果为 5

首先最直观的想法就是排序,先排序好,再从排序好的数组中进行选择,好的排序算法的时间复杂度为O(nlogn)的算法,那能否可以在时间复杂度O(n)将问题解决呢?

完全可以,在这里,我们可以使用快排的思路,在O(n)的时间复杂度内将问题解决。

利用快排每一轮将选定的枢轴元素放在正确的排序位置上的性质,将正确位置的索引返回与k进行比较,如果k比其大,那么就递归在比返回位置大的那块寻找;如果k比其小,那么就递归在比返回值小的那块寻找。

因为避免了原有快排两边递归的情况,只选择了一侧进行递归,所以时间复杂度为O(n),而不用乘上logn。

在用上述的方法之前,我们先再来认识下快排。(在前面解决 LeetCode 75 问题时使用了三路快排的思想)

前言–快速排序

[外链图片转存失败(img-OCmnm6sC-1568598941725)(5675D93521A6447DBCAA3FA9B32676DB)]

初始–从一段开始

[外链图片转存失败(img-eGbEgrBv-1568598941726)(C1E1559ABF47464599C4B28CFB5326B2)]

适用输入值无重复元素

快速排序是每次从当前考虑的数组中选择一个元素,以这个元素为基点,之后想办法把该元素放在它在排好序后应该所处的位置上。

比如对数组:[4,6,2,3,1,5,7,8]排序,首先先要把4这个元素放在已经排序好的位置上,此时该元素就都具有了一个性质:即4之前的所有元素都是小于4的,4之后的所有元素都是大于4的。

接下来所做的事情,就是对当前排好序的元 素4之前和之后的部分,继续递归的进行上述过程,直至每个元素都排好序。

ok,那么问题就是如何将遍历的元素放在已经排序好的位置上,以及如何定义元素排好序了呢?

通常是选择第一个元素v当作基准点,索引记作l,之后遍历未访问的元素,在遍历的过程中,逐渐的整理,再使用j这个索引位置来记录小于v和大于v的分界点,当前访问的元素叫做i。

这样,[l+1,j]都是小于v,[j+1,i-1]都是大于v,那么i这个元素,该怎么变化才能使得整个数组仍保持这样的性质;

当i指的元素比v还要大,让i++,直接放在大于v的区间中;

当i指的元素比v小,则将j所指的后一个元素与i指的元素进行交换,再让j++,i++,进而考察下一个;

快速排序是不断的将数组一分为二,需要找到一个标定点,对标定点的左边和右边分别进行排序。

代码如下:

def partition(nums,l,r):
    v = nums[l]
    # nums[l+1...j] < v  nums[j+1...i) > v
    j = l
    for i in range(l+1,r+1):
        if(nums[i] < v):
            j += 1
            nums[j], nums[i] = nums[i], nums[j]
    
    nums[l],nums[j] = nums[j],nums[l]
    return j
def quickSort(nums, l, r):
    if l >= r:
        return
    p = partition(nums, l, r)
    quickSort(nums, l, p-1)
    quickSort(nums, p+1, r)
    return nums
    
nums = [7,6,5,3]
lis = quickSort(nums,0,len(nums)-1)
print(nums)

快速排序也是对数组一分为二的过程,对于快排,找到一个标定点,将左右两个数组分别排序,快排可能分的是不平均的,
当整个数组近乎有序时,这样会造成时间复杂度过大,此时可以随机的选择一个元素作为枢纽值,而不必用第一个元素作为枢轴。

时间复杂度的期望值是nlogn。

优化–二路快排

二路快排是教科书及参考资料的标准写法,其将小于v和大于v放在数组的两端,把等于v的元素分散到了左右两部分,这样不会有等于v的元素不会集中于某一侧,而是将它们平分开来。

[外链图片转存失败(img-X1KkPk4h-1568598941727)(14A8596E181446EFAB05BE3F9DE48733)]

实现代码如下:

def partition(nums,l,r):
    v = nums[l]
    # arr[l+1,,,i)] <=v arr(j,,,r] >= v
    i = l + 1
    j = r
    while 1:
        while(i <= r and nums[i] < v):
            i += 1
        while(j >= l+1 and nums[j] > v):
            j -= 1
        if(i > j):
            break
        nums[i],nums[j] = nums[j],nums[i]
        i += 1
        j -= 1
    nums[l],nums[j] = nums[j],nums[l]
    return j
def quickSort(nums, l, r):
    if l>= r:
        return
    p = partition(nums, l, r)
    quickSort(nums,l,p-1)
    quickSort(nums,p+1,r)
    return nums

nums = [7,6,5,3]
lis = quickSort(nums,0,len(nums)-1)
print(nums)

优化–三路快排

当对于输入值的情况有大量相等的元素时,交换元素同样需要消耗,那么就可以将整个数组氛围三部分:<v; =v; >v;这样,当递归交换时,就可以只对等于v之前与等于v之后的段落进行交换,如下:

[外链图片转存失败(img-lp0jpEMC-1568598941728)(286EA13F19154ABF80950D468140B72E)]

实现代码如下:

def partition(nums,l,r):
    if l >= r:
        return
    v = nums[l]
    # arr[l+1,,,lt] < v
    lt = l
    # arr[gt,,,r] > v
    gt = r + 1
    # arr[lt+1,,,i) == v
    i = l + 1

    while(i < gt):
        if nums[i] < v:
            nums[i],nums[lt+1] = nums[lt+1],nums[i]
            lt += 1
            i += 1
        elif nums[i] > v:
            nums[i],nums[gt-1] = nums[gt-1],nums[i]
            gt -= 1
        else:
            i += 1
    nums[l],nums[lt] = nums[lt],nums[l]
    partition(nums,l,lt-1)
    partition(nums, gt, r)
    return nums

arr = [1,3,2,4,1,6]
arr = partition(arr,0,len(arr)-1)
print(arr)

实现一:先排序再求k索引

先对指定数组nums进行快排,排序完后再取k的索引。如果是从小到大得排序,那么应该取得元素是-k;

class Solution:
    def partition(self,nums,l,r):
        if l >= r:
            return nums
        v = nums[l]
        # arr[l+1,,,lt] < v
        lt = l
        # arr[gt,,,r] > v
        gt = r + 1
        # arr[lt+1,,,i) == v
        i = l + 1

        while(i < gt):
            if nums[i] < v:
                nums[i],nums[lt+1] = nums[lt+1],nums[i]
                lt += 1
                i += 1
            elif nums[i] > v:
                nums[i],nums[gt-1] = nums[gt-1],nums[i]
                gt -= 1
            else:
                i += 1
        nums[l],nums[lt] = nums[lt],nums[l]
        self.partition(nums,l,lt-1)
        self.partition(nums, gt, r)
        return nums
    def findKthLargest(self, nums, k) -> int:
        nums = self.partition(nums,0,len(nums)-1)
        return nums[-k]

实现二:只与k那部分递归

对返回的索引,与给定的k值进行比较,但在这里需要注意的问题有两个:

一是:原来返回的是数组的索引值,而k值是个数,故不能直接与k进行比较,需要将返回的索引值加1与k比较,此时递归的值也需要变化,之前p-1的,要变为p-2,而p+1的要变为p。

二是:如果是从小到大得排序,那么应该取得元素是len-k+1;
实现一:

class Solution:
    def partition(self, nums,l,r):
        v = nums[l]
        # nums[l+1...j] < v  nums[j+1...i) > v
        j = l
        for i in range(l+1,r+1):
            if(nums[i] < v):
                j += 1
                nums[j], nums[i] = nums[i], nums[j]
        nums[l],nums[j] = nums[j],nums[l]
        return j+1
    def quickSort(self, nums, l, r, k):
        if l >= r:
            return nums[l]
        p = self.partition(nums, l, r)
        if p == k:
            return nums[p-1]
        elif k < p:
            return self.quickSort(nums, l, p-2, k)
        else:
            return self.quickSort(nums, p, r, k)

    def findKthLargest(self, nums, k) -> int:
        num = self.quickSort(nums,0,len(nums)-1,len(nums)-k+1)
        return num

实现二:

class Solution:
    def partition(self,nums,l,r):
        v = nums[l]
        # arr[l+1,,,i)] <=v arr(j,,,r] >= v
        i = l + 1
        j = r
        while 1:
            while(i <= r and nums[i] < v):
                i += 1
            while(j >= l+1 and nums[j] > v):
                j -= 1
            if(i > j):
                break
            nums[i],nums[j] = nums[j],nums[i]
            i += 1
            j -= 1
        nums[l],nums[j] = nums[j],nums[l]
        return j + 1
    def quickSort(self, nums, l, r, k):
        if l >= r:
            return nums[l]
        p = self.partition(nums, l, r)
        if p == k:
            return nums[p-1]
        elif k < p:
            return self.quickSort(nums, l, p-2, k)
        else:
            return self.quickSort(nums, p, r, k)
    def findKthLargest(self, nums, k) -> int:
        num = self.quickSort(nums,0,len(nums)-1,len(nums)-k+1)
        return num

当然取最大k元素还有经典的堆排序,没有深入看,就先不解释这个方法了。

日常如果遇到这个问题时,其实大可不必这么麻烦,附一个py的简单实现:

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        nums.sort()
        return nums[-k]

LeetCode 167 Two Sum 2- Input array is sorted

题目描述

给定一个已按照升序排列的有序数组,找到两个数使得它们相加之和等于目标数。

函数应该返回这两个下标值 index1 和 index2,其中 index1必须小于index2。

说明:

返回的下标值(index1 和 index2)不是从零开始的。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。

如:numbers = [2,7,11,15],target = 9

返回数字2,7得索引为1,2(索引从1开始计算)

这道题目比较简单,而且题目也最简化了情况:只有唯一解、不可以用相同的元素。但是因为面试中面试官可能不会带有这么多的条件,练习时也要发散的来思考一下,表明自己的细节完整:如果问题没有解怎么办?如果问题有多个解怎么办,按照什么顺序返回?如果可以单个元素重复的话,又该怎么考虑?

暴力搜索 O(n^2)

最直观解法,即暴力解法,使用双层遍历,i从0到len-1,j从i+1到len-1,这样经历了两层循环,故时间复杂度为:O(n^2)

(两层循环…就不写了)

显然,暴力解法没有充分利用原数组的性质–有序,谈到有序数组,首先必须想到什么?二分搜索!于是第二种思路就可以按照二分搜索对时间复杂度进行优化。

二分搜素思路优化:O(nlogn)

依次来遍历每一个元素i,对于每一个i,都在剩余的有序数组中,使用二分查找的思路,寻找target-nums[i],找到即返回,否则继续遍历。遍历为n,二分搜索为logn,总体的时间复杂度为:nlogn。

这里简单的复习一下二分搜素:

二分搜素

在一个有n个元素的有序数组中,寻找target的值,并且将找到的索引以int的形式返回。

在二分查找中,我们要在一个范围中不停的寻找target,然后这个范围逐渐的根据中间元素的不同来进行缩小。直到我们最终找到了这个target。

我们通过l,r两个边界来限定这个范围,在初始化时,首先的问题是就是这两个边界设定什么样的初值。

具体的边界设置为什么,不应该是靠猜测,而应该严格的限定清楚l和r两个变量的实际意义。

在这里我定义我是从[l,r]的闭区间的范围里去寻找target,

那么初始值设定为左边界0,右边界n-1。

有了这样的定义后,代码也要一直满足l和r相应的定义,下面开始定义循环,对循环的理解是,只要还有要查找值的话,那么我们就在这个循环中继续查找。

那么第二个问题就是应该写为l < r,还是l <= r?

这又是一个边界问题,遇到边界问题,一定要清楚自己最初的定义。定义是在[l,r]这个闭区间的范围内去寻找target,当l=r时,此时的区间l到r,就相当于是只有一个元素的区间,这个区间仍然是一个有效的区间,所以此时还应该查找下去。所以在while中的查找条件应该是l <= r。

代码如下:

def binarySearch(arr, target):
   l,r = 0,len(arr)-1
   while(l <= r):
       mid = int((l+r)/2)
       if(arr[mid] == target):
           return mid
       elif(target > arr[mid]):
           # 此时带查找元素target,应该在mid元素右侧
           # 那么此时应该是mid还是mid+1?此时更新左边界,我们就要回到左边界的定义中去,arr[mid]不是我们寻找的target,那么target就应该在[mid+1,,,r]中,
           l = mid + 1
       else:
           # target在[l,,,mid-1]中
           r = mid - 1
   return -1

我记得上学期课的郭老师曾经在讲设计模式时说:记住变与不变,对于算法来说也是如此,要非常清晰的定义l和r两个变量的意义是什么,在下面的循环中,就去不断的去维护这个意义,保护这个循环不变量的意义。

当然也可以改写,如果初始r值为n,相应的对于r的意义是[l,r)寻找target;

def binarySearch(arr, target):
   l,r = 0,len(arr)
   while(l < r):
       mid = int((l+r)/2)
       if(arr[min] == target):
           return mid
       elif(target > arr[mid]):
           # 此时带查找元素target,应该在mid元素右侧
           # 那么此时应该是mid还是mid+1?此时更新左边界,我们就要回到左边界的定义中去,arr[mid]不是我们寻找的target,那么target就应该在[mid+1,,,r)中,
           l = mid + 1
       else:
           # target在[l,,,mid)中
           r = mid
   return -1

在对mid的求值时,我们使用的是(l+r)/2求中间值的方式,如果用C语言的话,两个int都足够大时,可能会出现整型溢出。虽然在py中不会出现整型溢出的问题,但是为了效率,我们也应该避免使用加法,优化求中间值元素的小套路就是:

mid = l + (r-l)/2

对于二分搜索的时间复杂度,可以这样理解:第一次在n个元素中寻找,第二次在n/2个元素中寻找…直至最后在1个元素中寻找,这本质上就是在问–n经过几次"除以2"的操作后变成了1?那么自然答案是log以2为底,n的对数。那为什么不写log2n,而都表示为logn呢,这里就是用了数学中的对数换底公式,不管以几为底,都能表示为某一个为底的log再乘以一个常数,而常数忽略,故统一写为logn。

实现题目

class Solution:
    def twoSum(self, numbers, target):
        for i in range(len(numbers)-1):
            l = i + 1
            r = len(numbers) - 1
            while (l <= r):
                mid = int(l + (r - l) / 2)
                if numbers[mid] == target - numbers[i]:
                    return [i + 1, mid + 1]
                elif target-numbers[i] > numbers[mid]:
                    l = mid + 1
                else:
                    r = mid - 1

对撞指针–O(n)

在前几个题目中,为了避免额外开辟新的数组空间,于是用了两个指针,来做交换删除之类的操作,同样这里,为了少一层循环,我们也可以考虑多用一个指针索引。

寻找两个索引,两个索引代表的数字和是target,因为数组有序,故其一定是一左一右存在,那么就从从最左侧选择i,从最右侧选择j:

  1. 如果第一个加上最后一个小于target,那么此时让i这个索引++,由于整个数组有序,故这个位置会比原来位置的值更大;
  2. 如果大于target,那么j–;由于有序,得到的结果会比上一次更小。

那么小套路–对撞指针,就是指的是用两个索引,向中间的方向不断前行,就能找到所给的答案。

因为不可以是重复元素,所以循环条件l < r

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        l,r = 0,len(numbers)-1
        while l < r:
            if numbers[l] + numbers[r] == target:
                return [l+1,r+1]
            elif numbers[l] + numbers[r] < target:
                l += 1
            else:
                r -= 1

只遍历一遍,时间复杂度为O(n),空间复杂度为O(1);

本科的时候在学浙大翁恺老师的程序设计课时,记得他说过:学程序题目就是要学其中的小套路。那么接下来的一篇,将把对撞指针相关的题目过一遍,时间会稍微有些长。

下一题:

LeetCode 125 Valid Palindrome

LeetCode 344 Reverse String

LeetCode 345 Reverse Vowels of a String

LeetCode 11 Container with most water

LeetCode 125 Valid Palindrome

题目描述

给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。

说明:本题中,我们将空字符串定义为有效的回文串。

示例 1:

输入: “A man, a plan, a canal: Panama”

输出: true

示例 2:

输入: “race a car”

输出: false

思路

对于回文串问题,依然是对撞指针的思想,设前后两个指针,一个指针从头进行,一个指针从尾部进行。依次判断两个指针的字符是否相等,当两个指针相遇的时候循环停止跳出。

字符串问题中常要考虑的几个问题是:

1.空字符串怎么处理?

题目中已经将空字符串定义为有效的回文串,故不用特殊判断处理,默认循环完返回true就可以;

2.大小写问题

题目中忽略字母的大小写,而忽略字母大小写的套路做法是:将对应的字母统一转化为大写,再比较大写的字母是否相同。

3.跳过非法字符

Python中有字符串方法isalnum(),用来检测字符串是否由字母和数字组成,是的话返回true,如果没有库的话,可以用判断字符的ascii码来实现。

代码如下:

class Solution:
    def isPalindrome(self, s: str) -> bool:
        i,j = 0,len(s)-1
        while i < j:
            # 每次while都需要判断下i<j
            while i<j and not s[i].isalnum():
                i += 1
            while i<j and not s[j].isalnum():
                j -= 1
            if s[i].upper() != s[j].upper():
                return False
            # while循环中要改变循环变量
            i += 1
            j -= 1
        return True

时间复杂度

只遍历一遍数组,时间复杂度为:O(n);开辟的为常量空间,空间复杂度为O(1);

LeetCode 344 Reverse String

题目描述

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

思路代码

同样,使用对撞指针的思路,一个指针从前到后,一个指针从后向前,然后两个指针相互交换,当两个指针相同时跳出循环。

class Solution:
    def reverseString(self, s: List[str]) -> None:
        """
        Do not return anything, modify s in-place instead.
        """
        i,j = 0,len(s)-1
        while i < j:
            s[i],s[j] = s[j],s[i]
            i += 1
            j -= 1
        return s

时间复杂度为O(n),空间复杂度为O(1);

题目中的小坑

对于Python,当然也可以使用list的切片来解决:

class Solution:
    def reverseString(self, s: List[str]) -> None:
        """
        Do not return anything, modify s in-place instead.
        """
        s = s[::-1]
        return s

当然这样做时错的,因为题目的要求是原地修改输入数组,而这样做相当于是将改变的值赋给了局部变量s,而没有改变传入的s变量值。因为s是一个list,所以可以通过python s[0::] = s[::-1]来改变,为什么要求s是list,因为s如果是字符串的话,将不能原地修改,str是不可变对象。

基础知识点:

  1. 倒序遍历数组:s[::-1];
  2. 原地改变数组变量:s[0::] = s[::-1]
  3. py中可变对象:list,set,dict;不可变对象:字符串,元组
  4. 遇到需要交换元素时,需要考虑是类型的可变不可变;

LeetCode 345 Reverse Vowels of a String

题目描述

编写一个函数,以字符串作为输入,反转该字符串中的元音字母。

示例 :
输入: “hello”
输出: “holle”

思路

同样,对撞指针,从前到后,从后到前,遇到每一个相同的元素进行交换。这时要注意,传入的为字符串,属于不可变对象,故要将字符串转化为list,交换后,再将list拼接为字符串。

基础知识点:

1.str->list: list(str)

2.list->str:’’.join(list)

代码

class Solution:
    def reverseVowels(self, s: str) -> str:
        i,j = 0,len(s)-1
        v = ['A','E','I','O','U'] 
        lis_s = list(s)
        while i < j:
            while i<j and lis_s[i].upper() not in v:
                i += 1
            while i<j and lis_s[j].upper() not in v:
                j -= 1
            lis_s[i],lis_s[j] = lis_s[j],lis_s[i]
            i += 1
            j -= 1
        return ''.join(lis_s)

时间复杂度为O(n),空间复杂度为O(n)

LeetCode 11 Container with most water

给出一个非负整数数组a1,a2,a3…an,每一个整数表示一个竖立在坐标轴x位置的一堵高度为ai的’墙’,选择两堵墙,和x轴构成的容器可以容纳最多的水。

说明:你不能倾斜容器,且 n 的值至少为 2。

示例:

输入: [1,8,6,2,5,4,8,3,7]
输出: 49

[外链图片转存失败(img-YPbh3Fes-1568598941729)(A131D80D21544AC9860297F8581D3359)]

‘伪对撞指针’–O(n^2)

定义两个指针,一个从前向后,一个从后向前。矩形面积的长为两指针相减,矩形面积的高为两个指针比较后较小的值。首先固定i循环从0到len-1,再对j从len-1到i进行移动,比较每一次的面积值,最后返回。

class Solution:
    def maxArea(self, height: List[int]) -> int:
        maxarea = 0
        for i in range(len(height)):
            j = len(height)-1
            while i < j:
                maxarea = max(maxarea, min(height[i], height[j]) * (j - i));
                j -= 1
        return maxarea

时间复杂度为:n(n-1)/2,为O(n),空间复杂度只开辟了变量,故为O(1);

对撞指针精髓

这种做法,显然没有get到对撞指针的精髓,对撞指针并不是两次遍历,而是通过首尾指针在满足某种条件下,只移动一边的指针,最终指针重合跳出,达到O(n)的目的,那么可以在只移动一侧指针就能达到目的吗???

对撞指针–O(n)

矩形的面积由长和高决定,为了使面积最大化,我们需要考虑两条线段之间的区域更大,要么长度越长,要么是高度更高。但是由于面积取决于边长短的那一端,假设为m,所以要想得到比当前更大的面积,边长短的那一端必须舍弃,因为如果不舍弃,高最大也是m,而随着指针的移动宽会一直减小,因此面积只会越来越小。

说到这,可能还是有疑惑,这样移动会不会错过最优解的情况?

不会, 这相当于就是有一个数列{a0,a1,…an-1}一共n个(n>=2)。 取出任意两个值,然后result = min(ax,ay) * (y-x)

写出数列中任意两个元素的所有组合,一般的写法就是排列组合,但显然对于min(ax,ay) * (y-x)

在第一轮的筛选中,假设左边的a0是短元素(如果是右边也一样,这里以左边为例),显然(a0,an-1)的组合的值大于其他任何以a0为起始的组合的值,因为(a0,an-2)此时即使an-2的值大于an-1,但高度以低值为准,故高度还是a0,长度还减小了1。

故只能对值小的a0进行移动,此时虽然长度减小,但是期间可能出现高度大的值,有result大的可能性。

代码

class Solution:
    def maxArea(self, height: List[int]) -> int:
        i,j = 0,len(height)-1
        maxarea = 0
        while i < j:
            maxarea = max(maxarea, min(height[i], height[j])*(j - i))
            if height[i] > height[j]:
                j -= 1
            else:
                i += 1
        return maxarea

时间复杂度为O(n),空间复杂度为O(1)

下一题:

LeetCode 209 Minimum Size Subarray Sum

LeetCode 209 Minimum Size Subarray Sum

双索引的套路,除了对撞指针以外,即两个索引不再是通过首尾指针在满足某种条件下,只移动一边的指针,最终指针重合跳出,以达到只遍历一次O(n)的目的。

还有一类滑动窗口的套路,其两个索引不是遍历,而像表示的是一个窗口,让这个窗口不停的滑动,在这个数组中游走, 最终来找到我们希望求得的问题的解。

题目描述

给定一个整型数组和一个数字s,找到数组中最短的一个连续子数组,使得连续子数组的数字和sum>=s,返回这个最短的连续子数组的长度值。

例如:给定数组[2,3,1,2,4,3],s=7,答案为[4,3],返回2

分析

求解子数组问题时,需要先搞清楚以下几个问题:

  1. 连续不连续,如果是连续,有没有元素大小的情况?

题目中是连续的子数组,且不用考虑元素的大小情况。

  1. 没有解的情况需不需要考虑?

题目中没有解的情况,默认返回为0。怎么判断没有解,可以在开始设定初始值,最后判断如果初始值不变的话,就返回0.

  1. 返回的是什么?是子数组,还是子数组长度?

如果是只返回长度值,就不用考虑多组解的情况。如果是返回最短子数组,就要考虑多组解的情况–如果是要只返回某一个解,那么这个解有什么限定?如果是要返回多个解,那么这多个解按什么顺序来返回?

双指针O(n^2):

思路

遍历所有的连续子数组,计算其和sum,验证sum >= s,就拿这个当前sum的长度(长度由索引取得),与初始值进行比较,取最小的进行记录。

设置初始长度值时,之前我们比较最大时,将初始值设置为0或-1,因为最小也就是0,而在这里比较最小,反过来考虑,将初始值设置为len或len-1,因为题目中可能出现无解的情况,故考虑到最后一步判断,将初始值设置为len-1;

遍历全部连续子数组的小套路:首先遍历L从0到len-1,接着遍历R从L到len-1。

在遍历j时计算sum值,如果sum值>=s时,满足要求,此时比较求出最小的len值,接着break掉j的循环,因为再往后长度只会越大。

代码如下

class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:
        res = len(nums) + 1
        
        for l in range(len(nums)):
            sum = 0
            for r in range(l,len(nums)):
                sum += nums[r]
                if sum >= s:
                    res = min(res, r-l+1)
                    break
        if res == len(nums) + 1:
            return 0
        return res 

因为遍历两次,故时间复杂度为O(n^2);

空间复杂度为常量空间,故为O(1)

滑动窗口O(n)

思路

在上述思路中,其实包含了大量的重复计算,对于nums[i,j]到nums[i+1,j]的和,只需要减去nums[i]即可,但是刚刚的操作是又通过遍历一遍数组来实现。

那么如何避免这种重复操作呢?

滑动窗口的思想,就是开始定义了子数组[i,j],通过对这个子数组的sum全局值进行计算。

如果子数组的和还不到s,就往后多看一个数据,直到sum和大于s,就可以把其长度记录,此时如果再移动j,长度只会变大,故就可以从i这一端缩小这个数组。

这时连续子数组的和就会小一些。当sum和小于s时,就再去移动j,找到一个连续子数组,使其和大于s,如此循环。

整个上述过程,都保持着一个窗口,这个窗口的长度并不是固定的,但是是被i和j这两个索引所定义的,这个窗口不停的向前滑动,来寻找满足题意的连续子数组。

代码

class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:
        # [l,r]为滑动窗口,初始滑动窗口的值为0
        l,r = 0,-1
        sum = 0
        # 记录当前寻找到得最小长度,初始化为最大值,不可能取到这个值
        res = len(nums) + 1
        # 滑动窗口得左边界小于len,那么右边界也可以取值
        while l < len(nums):
            '''
            对于数组问题而言,一旦用方括号来取值得话,一定要注意数组越界的问题:
            需要保证r+1后还在取值范围中,因此在条件中需要进行限定
            '''
            if r+1 < len(nums) and sum < s:
                r += 1
                sum += nums[r]
            else:
                sum -= nums[l]
                l += 1
            if sum >= s:
                # 因为是连续闭空间,右边界减去左边界后,还要+1
                res = min(res, r-l+1)
        # 有可能遍历了整个数组都没有解的情况
        if res == len(nums) + 1:
            return 0
        return res

只遍历了一遍数组,故时间复杂度为O(n);

空间复杂度为常量空间,为O(1)

美团笔试题

题目描述

给定两个字符串,输出最短的包含全部字符的字符串

样例–>

输入:第一个字符串abac,第二个字符串 cab

输出:cabac

思路

题目中要求:最短的包含全部字符的字符串,怎么保证最短?其实也就是找公共子串最长。如何去找两字符串的最长公共子串,我这里用了滑动窗口的思路,规定[l,r)为最长公共子串,那么遍历其中任意一字符串,如果[l,r)如果在另一字符串里,r索引右移,看能否子串更长;如果没有在,就将l右移动。

但是这里值得注意的是,找到最长公共子串就结束了吗?

样例中,最长子串正好是第一个字符串的开头,以及第二个字符串的末尾,于是两个字符串得以拼接。

如果字符串是‘dabac’和‘cab’,即使它们的公共子串是’ab’,其拼接起来的字符串也不是以ab为连接的,而是通过公共字符子串‘c’来进行连接,形成‘dabacab’。那么这就需要在判断子串长度的这个条件下,另外加上判断条件,保证子串是两个字符串的前或者后,不是的话,即使其最长,也无法判定为连接要素。

最后就是如果公共子串如果既不是前也不是后,那么就直接返回两个字符串相加的结果,因为题目中没有说明,就默认为返回s1+s2

def minSubArrayLen(s1, s2):
    l,r = 0,0
    res = ''
    # 定义[l,,,r)为最长子串
    while l < len(s2):
        if r <= len(s2) and s2[l:r] in s1:
            sum = s2[l:r]
            r += 1
        else:
            l += 1
        '''
        第一个条件是为了确定最长子串;
        二三个条件是为了保证公共子串是首尾
        '''
        if len(sum) > len(res) and (s1.index(sum) == 0 or s1.index == len(s1) - len(sum)) and (s2.index(sum) == 0 or s2.index(sum)== len(s2) - len(sum)):
            print(res)
            res = sum
    print(res)
    if s1.index(res) == 0 and s2.index(res) == len(s2)-len(res):
        return s2 + s1[len(res):]
    elif s2.index(res) == 0 and s1.index(res) == len(s1)-len(res):
        return s1 + s2[len(res):]
    else:
        return s1 + s2

LeetCode 3 Longest SubString Without Repeating Characters

问题描述

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

如:‘abcabcbb’,则结果为:‘abc’

如:‘bbbbb’,则结果为’b’

如:‘pwwkew’,则结果为’wke’

分析

仍然是字符串问题,求解时,需要先搞清楚:

  1. 子串需不需要连续

根据案例三,题目中是连续的子数组,不能有隔的字符

  1. 没有解的情况需不需要考虑?

题目中不存在没有解的情况,至少为1.

  1. 返回的是什么?是子数组,还是子数组长度?

如果是只返回长度值,就不用考虑多组解的情况。如果是返回最短子数组,就要考虑多组解的情况–如果是要只返回某一个解,那么这个解有什么限定?如果是要返回多个解,那么这多个解按什么顺序来返回?

题目中返回的是最长子串的长度

  1. 重复字母的大小写是否要考虑?

题目没有说明,暂不考虑

思路

同样,这也是一个典型的可以使用滑动窗口的问题,首先定义最长子串为s[i,j],在这个子串中,没有重复的字母,如果s[i,j]这个数组中没有重复字母,那么将j继续向后移动,看下一个字符是否和当前的字符串产生相同的字符存在。如果没有,就找到了一个更长的子串没有重复的字母,即s[i,j+1]。如果下一个字符和当前字符串数组中产生了重复的字母,那此时字符串就没办法再往后扩展了,就记录下此时字符串数组的长度,再将i向后移动,直到把这个重复的字符给刨除出去,此时j就可以包含刚刚重复的字母。

那么在这里,如何来判定下一个字符和当前的字符串中没有重复的字符呢?

用py实现的话当然很简单,看要判定的字符在不在当前的字符数组里,用in即可。(上一篇美团的题目中有体现)

如果不能用python中的in方法的话,怎么整呢?

可以设置一个数组,数组的k索引位置存的就是ASCII为k的字符在子串中出现的频率。可以用其面对下一个字符,查
找其在数组中出现的频率值为多少,如果为0就没有重复,继续移动;如果为1即产生了一个重复的字符。

代码实现

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        l,r = 0,-1
        res = 0
        while l < len(s):
            if r+1 < len(s) and not s[r+1] in s[l:r+1]:
                r += 1
            else:
                l += 1
            res = max(res,r-l+1)
        return res

循环一遍,时间复杂度为O(n);空间复杂度为O(1)

LeetCode 438 Find All Anagrams in a String

题目描述

给定一个字符串s和一个非空字符串p,找到s中所有是p的字母异位词的子串,返回这些子串的起始索引。

字符串只包含小写英文字母,并且字符串s和 p的长度都不超过 20100

说明:

字母异位词指字母相同,但排列不同的字符串。
不考虑答案输出的顺序。

示例:

如:s = “cbaebabacd” p = “abc”,返回[0,6]

解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的字母异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的字母异位词。

如:s = “abab” p = “ba” 返回[0,1,2 ]

解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的字母异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的字母异位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的字母异位词。

包含字母一样,只是有可能顺序不同。

分析

仍然是字符串问题,求解时,需要先搞清楚:

  1. 子串需不需要连续

根据案例,s子串需要是p的字母异位词,故必须连续,不能有其他不属于p的字母。

  1. 没有解的情况需不需要考虑?

没有解返回的是空列表。

  1. 返回的是什么?是子数组,还是子数组长度?

返回子数组的开始索引,有多组解的情况不用考虑顺序。

  1. 重复字母的大小写是否要考虑?

题目中说明字符串只有英文小写字母,故不需要考虑。

思路

猜你喜欢

转载自blog.csdn.net/qq_29027865/article/details/100878169