美文网首页
2018-06-25

2018-06-25

作者: Like_eb56 | 来源:发表于2018-06-25 15:12 被阅读0次

    线性表中的双指针法是指通过两个指针(游标)来指示线性表中的元素的方法。双指针的使用本身并没有什么神奇之处,但是通过合适地操纵指针的移动,在某些特定问题中却能化腐朽为神奇,大大降低算法的时间复杂度。

    根据对双指针不同的移动规则,整理了常用的双指针方法及其所用来解决的问题:

    双指针法 操作说明 经典问题
    首尾指针 使用两个指针指示线性表的首尾元素 二分查找、NSum
    口袋指针 一个指针指示口袋口,另一个指针用于遍历 数组划分
    窗口指针 两个指针指示窗口范围 窗口蠕动法求解连续子序列问题
    快慢指针 使用两个移动速度不同的指针 环的检测和交点确定
    早晚指针 使用出发时间不同的两个指针 消除距离差额
    双表指针 两个指针分别指示两个线性表当前位置 数组归并

    1. 首尾指针

    • 适用问题:需要同时操作首尾元素,或者需要指示变化中的线性表范围,使用首尾指针经常需要先对数组进行排序;
    • 使用思路:
      • 指针含义:l指示线性表首部元素,r指示线性表尾部元素
      • 移动规则:依条件向内移动,直至相遇

    1.1 反转线性表

    • 问题:[344] 反转字符串,请编写一个函数,其功能是将输入的字符串反转过来;
    • 思路:使用首尾指针交换元素,然后首尾指针向内移动,直至首指针不再小于尾指针;
    • 代码:
    class Solution(object):
        def reverseString(self, s):
            """
            :type s: str
            :rtype: str
            """
            l, r = 0, len(s) - 1
            res = list(s)
            while l < r:
                res[l],res[r] = res[r],res[l]
                l += 1
                r -= 1
            return ''.join(res)
    
    • 分析:空间O(n),时间O(n)

    1.2 盛最多水的容器

    • 问题:[11]盛最多水的容器,给定 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。画 n 条垂直线,使得垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
    • 思路:分治+首尾指针,问题首先划分为两个子问题,使用两侧较小边和不使用两侧较小边,然后取二者中的最大值;使用较小边时,容器两侧容纳最多的水,不使用较小边的子问题可以是原始问题的同类子问题,可以用递归求解,也可以用首尾指针来求,如果使用了较小边l,收集当前最大后l++,反之较小边为r则r--,直至l==r
    image.png
    • 代码:
    class Solution(object):
        def maxArea(self, height):
            """
            :type height: List[int]
            :rtype: int
            """
            l,r = 0,len(height) - 1
            res = 0
            while l < r:
                if height[l] < height[r]:
                    res = max(res,height[l]*(r-l))
                    l += 1
                else:
                    res = max(res,height[r]*(r-l))
                    r -= 1
            return res
    
    • 分析:空间O(1),时间O(n)

    1.3 二分查找

    # 闭区间写法:用[l,r]代表当前查找范围
    def Binary_search(L,key):
        '''
        @L:有序数组
        @key:目标值
        @return:如果查找成功则返回目标元素索引,如果查找失败则返回目标值应插入的位置
        '''
        # 闭区间-初始化窗口
        l,r = 0,len(L)-1 
        # 闭区间-退出条件:窗口内不含元素,l=r+1,目标值在l,r之间
        while l <= r:
            mid = (l + r)//2
            if key == L[mid]:
                return mid
            elif key > L[mid]:
                l = mid + 1
            else:
                r = mid - 1
    
        return l
    

    1.4 2Sum

    1.4.1 无序数组的2Sum

    • 问题:[1] 两数之和,给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。你可以假设每个输入只对应一种答案,且同样的元素不能被重复利用
    • 思路:使用哈希表存储元素残差,如果元素在哈希表则说明和为target,返回
    • 代码:
    class Solution(object):
        def twoSum(self, nums, target):
            """
            :type nums: List[int]
            :type target: int
            :rtype: List[int]
            """
            d = {}
            for i,num in enumerate(nums):
                if num not in d:
                    d[target-num] = i
                else:
                    return [d[num],i]
    
    • 分析:空间O(n),时间O(n)

    1.4.2 有序数组的2Sum

    • 问题:[167] 两数之和 II,给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2
    • 思路:首尾指针法,如果量元素之和小于target则移动右移左指针,如果大于则左移右指针,如果相等,则返回两下标
    • 代码:
    class Solution(object):
        def twoSum(self, numbers, target):
            """
            :type numbers: List[int]
            :type target: int
            :rtype: List[int]
            """
            low,high = 0,len(numbers)-1
            
            while low <= high:
                total = numbers[low] + numbers[high]
                if total == target:
                    return [low+1,high+1]
                elif total < target:
                    low += 1
                else:
                    high -= 1
    
    • 分析:空间O(1),时间O(n)

    1.4.3 3Sum

    • 问题:[15]三数之和,给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。
    • 思路:先排序+2Sum,先对数组进行排序,从前向后每次选取一个不同的元素x作为基准,然后在后面的数组中找到所有不重复的2Sum等于target-x的组合,再合并收集;保证解完备且不重复的核心有两点(选择下一个不同元素中的第一个):①基准选择:每次选择下一个不同元素中的第一个,不同基准不会有重复解,第一个元素作为基准不会漏解②2Sum时:每次移动指针到第一个不重复的位置,也保证了2Sum的完备且不重复
    • 代码:
    class Solution(object):
        def threeSum(self, nums):
            """
            :type nums: List[int]
            :rtype: List[List[int]]
            """
            nums.sort()
            n = len(nums)
            target = 0
            res = []
            for i in xrange(n-2):
                if i == 0 or nums[i] != nums[i-1]:
                    base = nums[i]
                    l,r = i + 1, n - 1
                    while l < r:
                        if nums[l] + nums[r] == target - base:
                            res.append([base,nums[l],nums[r]])
                            l,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 nums[l] + nums[r] < target - base:
                            l += 1
                        else:
                            r -= 1
            return res       
    
    • 分析:空间$O(1)$,时间$O(n^2)$

    1.4.4 Nsum

    • 问题:[18] 四数之和,给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组
    • 思路:排序+递归+2Sum,NSum可以转化为N-1Sum,逐步转化为2Sum,类似的只要每次选取下一个不同元素中的第一个元素,就能保证最终解的完备且不重复
    • 代码:
    class Solution(object):
        def fourSum(self, nums, target):
            """
            :type nums: List[int]
            :type target: int
            :rtype: List[List[int]]
            """
            def NSum(nums, target, N):
                n = len(nums)
                if n < N or nums[0] * N > target or nums[-1] * N < target:
                    return []
                res = []
                if N == 2:
                    l,r = 0, n-1
                    while l < r:
                        if nums[l] + nums[r] < target:
                            l += 1
                        elif nums[l] + nums[r] > target:
                            r -= 1
                        else:
                            res.append([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
                    return res
                else:
                    for i in xrange(n-N+1):
                        # 核心,保证了两轮解中不包含重复解
                        if i == 0 or nums[i] != nums[i-1]:
                            for pre in NSum(nums[i+1:],target-nums[i],N-1):
                                res.append([nums[i]] + pre)  
                    return res
                
            return NSum(sorted(nums),target,4)
    
    • 分析:空间$O(1)$,时间$O(n^{N-1})$

    1.4.5. 最接近的三数之和

    • 问题:[16. 最接近的三数之和]给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。
    • 思路:排序后转化为2Sum,收集绝对差值最小的组合
    • 代码:
    class Solution(object):
        def threeSumClosest(self, nums, target):
            """
            :type nums: List[int]
            :type target: int
            :rtype: int
            """
            n = len(nums)
            if n < 3:
                return None
            
            nums.sort()
            res = None
            delta = float('inf')
            for i in xrange(n-2):
                if i == 0 or nums[i] != nums[i-1]:
                    l, r = i + 1, n-1
                    while l < r:
                        cur = nums[i] + nums[l] + nums[r] - target
                        if abs(cur) < delta:
                            res = [nums[i],nums[l],nums[r]]
                            delta = abs(cur)
                        if cur == 0:
                            return target
                        elif cur > 0:
                            r -= 1
                        else:
                            l += 1
    
            return sum(res)
    

    2. 口袋指针

    • 适用问题:将数组中满足条件的元素移至一端,要求原地修改且满足条件的元素相互位置保持稳定;
    • 使用思路:假想用一只“袋子”盛放满足条件的元素,初始时袋子为空,用l=-1指示袋子口的位置,使用另一个指针从0开始遍历整个数组,如果当前元素满足条件,则袋口+1,将当前元素通过交换放入袋中,当遍历结束时,所有满足条件的元素就都被放入到袋子中了。

    2.1 移动0

    • 问题:[283] 移动零,给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序
    • 思路:口袋指针法,初始口袋口-1,遍历整个数组,如果元素不等于0,袋口扩充,并将该元素和袋口元素交换
    • 代码:
    class Solution(object):
        def moveZeroes(self, nums):
            """
            :type nums: List[int]
            :rtype: void Do not return anything, modify nums in-place instead.
            """
            i = -1
            for j in range(len(nums)):
                if nums[j]:
                    i += 1
                    nums[i],nums[j] = nums[j], nums[i]
    
    • 分析:空间O(1),时间O(n)

    2.2 红白蓝划分

    • 问题:[75] 分类颜色,给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色
    • 思路:两次口袋指针法,先把0放头部,再把1放0后面数组的头部;或者使用首尾两个口袋,一次遍历将0放头口袋,将2放尾口袋
    • 代码:
    class Solution(object):
        """
        思路1:使用前后指针,口袋交换法,将等于0的交换至前面,将等于2的交换至右边
            开区间表示两次装袋
        思路2:双边装袋,略显复杂,还不如遍历两次
        """
        def sortColors(self, nums):
            """
            :type nums: List[int]
            :rtype: void Do not return anything, modify nums in-place instead.
            """
            n = len(nums)
            i,j = -1,n
            k = 0
            while k < j:
                if nums[k] == 0:
                    i += 1
                    nums[k],nums[i] = nums[i],nums[k]
                    k += 1
                elif nums[k] == 2:
                    j -= 1
                    nums[k],nums[j] = nums[j],nums[k]
                else:
                    k += 1
    
    • 分析:空间O(1),时间O(n)

    2.3 快排划分

    • 问题:选取首元素作为枢轴值,将数组中小于等于该值的元素交换至头部,大于该值的交换置数组尾部,返回枢轴值最终的位置
    • 思路:口袋指针法
    • 代码:
    def partition(A,low,high):
        """
        在A[low:high+1]中选取枢轴点,并按首轴值排序
        """
        i = low                         
        for j in xrange(low+1,high+1):
            if A[j] <= A[low]:
                i += 1
                A[i],A[j] = A[j],A[i]
        A[i],A[low] = A[low],A[i]
        return i
    
    • 分析:空间O(1),时间O(n)

    2.4 寻找第k大

    • 问题:[215] 数组中的第K个最大元素,在未排序的数组中找到第 k 个最大的元素
    • 思路:有很多方法可以求解此问题,但最高效的方法是口袋指针法,类似于快排划分,将首元素作为枢轴值,将所有大于等于它的元素交换至数组头部,返回数轴值最终位置pivot,如果枢轴值位置刚好是k-1,则说明该位置的值即为第k大的值,如果i <k-1,在有半部分nums[i+1:]寻找k-i-1大,如果i > k-1,在左半边nums[:i]寻找第k大
    • 代码:
    class Solution(object):
        """
        思路1:先排序,时间O(nlgn)
        思路2:建立大根堆,弹出第k个O(n+klgn)
        思路3:口袋指针法,平均O(n),以首元素为枢轴值,进行划分
            1. 如果数轴位置i==k-1则直接返回i对应的值
            2. 如果i <k-1,在有半部分nums[i+1:]寻找k-i-1大
            3. 如果i > k-1,在左半边nums[:i]寻找第k大
        """
        def findKthLargest(self, nums, k):
            """
            :type nums: List[int]
            :type k: int
            :rtype: int
            """
            n = len(nums)
            if n < 1 or n < k:
                return None
            if n == 1:
                return nums[0]
            
            pivot = self.partition(nums,0,n-1)
            if pivot == k - 1:
                return nums[pivot]
            elif pivot < k - 1:
                return self.findKthLargest(nums[pivot+1:],k-pivot-1)
            else:
                return self.findKthLargest(nums[:pivot],k)
            
        
        def partition(self,nums,low,high):
            bag = low
            for i in range(low+1,high+1):
                if nums[i] >= nums[low]:
                    bag += 1
                    nums[bag],nums[i] = nums[i], nums[bag]
            nums[bag], nums[low] = nums[low],nums[bag]
            return bag
    
    • 分析:最好情形$T(n)=T(n/2)+O(n)$,空间复杂度O(lgn),时间复杂度为O(n)

    3. 窗口指针

    包含剪枝条件的连续子序列问题

    1. 连续子序列问题:求解满足条件f的连续子序列;
    2. 包含剪枝条件:如果在找到了一个首次满足特定条件g的连续子序列之后,发现可以排除掉(l<=x<r,x<=y<r)以及(x=l,y>r)部分的解,就可以左移连续子序列的左侧边界l直至条件g不再满足,即(l'<=x,r<=y)不能排除;
    image.png

    常见的这类问题有:

    1. 满足条件的最长连续子序列问题;
    2. 满足条件的最短连续子序列问题;
    3. 内在的满足剪枝条件的其他类型连续子序列问题;

    窗口蠕动法:将连续子序列看做是一个“窗口”,用左右指针l,r来标识窗口,起初窗口为空(l=0,r=-1),备选解解存在于(x>=l,y>r),每次通过右移右指针来扩展窗口(l,r),如果当前窗口满足上述减枝条件g,则通过右移左指针实现剪枝(l',r),移动过程中收集满足条件f的解,剩余备选解在(x>=l',y>r)中,循环以上过程得到所有满足条件的连续子序列。因为窗口移动的过程通常是由右指针来扩展,由左指针来收缩,很像虫子向前蠕动的过程,因此叫窗口蠕动更形象。

    image.png

    窗口蠕动包含三个步骤:扩展-剪枝-收集(核心是设计出合适的减枝条件)

    1. 初始化空窗口l=0,r=-1
    2. 循环以下过程直至右指针越界
      1. 扩展:右移右指针
      2. 剪枝:如果达到下一个可剪枝的窗口,则右移左指针,直至不能再剪枝
      3. 在窗口满足条件时收集备选解
    3. 返回最终解

    3.1 满足条件的最长连续子序列

    3.1.1 无重复最长子串

    • 问题:[3] 无重复字符的最长子串,给定一个字符串,找出不含有重复字符的最长子串的长度

    • 思路:当窗口首次包含重复时,(r>x>l,r>y>l)(不是最长)以及(x=l,y>r)(包含重复)部分可以剪枝,右移左指针直至包含重复

      • ① 初始化窗口l=0,r=-1
      • ② 循环以下过程直至右指针越界r==n
        • 右移右指针
        • 如果当前窗口包含重复,右移左指针,直至当前窗口不包含重复
        • 更新不包含重复字符的子串的最大长度
      • ③ 返回最大长度
    • 代码:

    from collections import Counter
    def lengthOfLongestSubstring(self, s):
        """
        :type s: str
        :rtype: int
        """
        n = len(s)
        dic = Counter()
        res = 0
        l = 0
        for r in xrange(n):
            dic[s[r]] += 1
            while dic[s[r]] > 1:
                dic[s[l]] -= 1
                l += 1
            res = max(res,r-l+1)
        return res
    
    • 分析:空间O(n),时间O(n)

    3.2 满足条件的最短连续子序列

    3.2.1 和大于s的最短子数组

    • 问题:[209] 长度最小的子数组,给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的子数组。如果不存在符合条件的子数组,返回 0
    • 思路:当窗口和首次大于等于s时,(r>x>l,r>y>l)(小于s)以及(x=l,y>r)(不是最短)部分可以剪枝,右移左指针直至窗口和小于s
      • ① 初始化窗口l=0,r=-1
      • ② 循环以下过程直至右指针越界r==n
        • 右移右指针
        • 如果当前窗口和>=s,更新最短数组的的长度,右移左指针,直至当前窗口<s
      • ③ 返回最大长度
    • 代码:
        def minSubArrayLen(self, s, nums):
            """
            :type s: int
            :type nums: List[int]
            :rtype: int
            """
            n = len(nums)
            cur = 0
            res = n + 1
            
            i = 0
            for j in xrange(n):
                cur += nums[j]
                while cur >= s:
                    res = min(res,j-i+1)
                    cur -= nums[i]
                    i += 1
                    
            return res if res <= n else 0
    
    • 分析:空间O(1),时间O(n)

    3.2.2 最小覆盖子串

    连续序列覆盖子串问题是一类非常经典的问题,值得认真体会,关键点有两个:

    1. 剪枝条件:当已经覆盖时,可以剪枝,l右移至不再覆盖,即第一次need[s[l]]从0变成1的时候
    2. 用count和need标识总需求和每个字符的需求数:count=0时表示已经覆盖,need为负数时表示已超出需求
    • 问题:[76] 最小覆盖子串,给定一个字符串 S 和一个字符串 T,请在 S 中找出包含 T 所有字母的最小子串
    输入: S = "ADOBECODEBANC", T = "ABC"
    输出: "BANC"
    
    • 思路:当窗口首次覆盖子串时,继续扩展r不是最优,(l,r)内不能覆盖,可进行剪枝,右移l直至不再覆盖,不再覆盖不好判断,但不再覆盖的前一位置可以判断。用count统计当前窗口还缺多少个字符,用need记录当前每种字符需求数,如果是负数代表多余个个数。(覆盖问题使用need存放每个元素的需求,使用count存放总需求对于剪枝判断是个很好的技巧,下面我们还会反复遇到)
    • 代码:
        def minWindow(self, s, t):
            """
            :type s: str
            :type t: str
            :rtype: str
            """
            count,need = len(t), Counter(t)
            l, L, R = 0, 0, -1
            for r, c in enumerate(s):
                count -= need[c] > 0
                need[c] -= 1
                while count == 0:
                    if R == -1 or r - l < R - L:
                        L, R = l, r
                    need[s[l]] += 1
                    count += need[s[l]] > 0
                    l += 1
                        
            return s[L:R+1]
    

    -分析:空间O(n),时间O(n)

    3.2.3 最小区间

    • 问题:[632] 最小区间, 你有 k 个升序排列的整数数组。找到一个最小区间,使得 k 个列表中的每个列表至少有一个数包含在其中。我们定义如果 b-a < d-c 或者在 b-a == d-c 时 a < c,则区间 [a,b] 比 [c,d] 小
    输入:[[4,10,15,24,26], [0,9,12,20], [5,18,22,30]]
    输出: [20,24]
    解释: 
    列表 1:[4, 10, 15, 24, 26],24 在区间 [20,24] 中。
    列表 2:[0, 9, 12, 20],20 在区间 [20,24] 中。
    列表 3:[5, 18, 22, 30],22 在区间 [20,24] 中。
    
    • 思路:同最小覆盖子串,核心在于如何判断刚好已覆盖和刚好未覆盖,将所有值按照(值,所属分组号)排序,在排序后的数组上进行窗口滑动,当刚好都覆盖时进行剪枝,同时收集最优结果,右值l直至刚好不再覆盖。
    • 代码:
    from collections import Counter
    def smallestRange(self, nums):
        """
        :type nums: List[List[int]]
        :rtype: List[int]
        """
        count = len(nums)
        need = Counter(range(count))
        p = [(val,i) for i in xrange(count) for val in nums[i]]
        n = len(p)
        p.sort()
        
        l = 0
        res = [p[0][0],p[-1][0]]
        for r in xrange(n):
            count -= need[p[r][1]] > 0
            need[p[r][1]] -= 1
            while count == 0:
                if res[-1] - res[0] > p[r][0] - p[l][0]:
                    res = [p[l][0],p[r][0]]
                need[p[l][1]] += 1
                count += need[p[l][1]] > 0
                l += 1
        return res
    
    • 分析:涉及到排序,空间O(n),时间O(nlgn)

    3.3 其他包含剪枝的连续子序列问题

    3.3.1 包含字符串的排列

    • 问题:[567] 字符串的排列,给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列
    • 思路:窗口蠕动法,只要s2的某个子串的字符统计量等于s1的字符统计量,即返回True。该问题包含减枝条件,当加入的字符统计量超出s1的需求时可以剪枝,右移左指针直至不超,此时判断s1的需求数是否刚好满足,满足则返回True(不多、又刚好满足需求),否则继续扩展窗口。仍然使用need代表每个元素的需求数,用count代表总体需求数。
    • 代码:可以看到,代码与上面两题基本上相同
    def checkInclusion(self, s1, s2):
        """
        :type s1: str
        :type s2: str
        :rtype: bool
        """
        count,need = len(s1),Counter(s1)
        l = 0
        for r,c in enumerate(s2):
            count -= need[c] > 0
            need[c] -= 1
            while need[c] < 0:
                need[s2[l]] += 1
                count += need[s2[l]] > 0
                l += 1
            if count == 0:
                return True
        return False
    
    • 分析:空间O(n),时间O(n)

    3.3.2 乘积小于K的子数组

    • 问题:[713] 乘积小于K的子数组,给定一个正整数数组 nums,找出该数组内乘积小于 k 的连续的子数组的个数
    • 思路:直接应用窗口移动法时,不满足剪枝条件,因为(l<x<r,x<=y<r)这部分不能被剪掉,但如果这一部分已被统计过了,就仍然可以正常剪枝。当窗口满足条件时,我们统计以r结束的满足条件的连续数组个数,当r放入后如果不满足条件,我们就可以进行正常的剪枝了,因为后面的肯定不满足,前面的已经被统计过,此时只需要将l移至下个满足条件的位置即可。
    • 代码:
    def numSubarrayProductLessThanK(self, nums, k):
            """
            :type nums: List[int]
            :type k: int
            :rtype: int
            """
            n = len(nums)
            i,j,count,prod = 0,0,0,1
            while i <= j < n:
                j += 1
                prod *= nums[j-1]
                while i < j and prod >= k:
                    prod /= nums[i]
                    i += 1
                count += j - i
            return count
    
    • 分析:空间O(1),时间O(n)

    4. 快慢指针

    快慢指针:使用移动速度不同的两个指针slow,fast(通常fast移动速度是slow的两倍)来遍历线性表。

    环形检测:如果线性表中存在环,那么快慢指针从同一起点出发,如果能够再次相遇,则说明线性表中存在环。

    寻找中位数:快慢指针同时从线性表首元素出发,当快指针无法前进时,慢指针会指向下中位。

    链表中用的最多,比如[141] 环形链表,[142] 环形链表 II,[234]回文链表,详情参考链表,这里仅讨论在顺序表中的使用

    4.1 寻找重复数

    • 问题:[287]寻找重复数,给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
    不能更改原数组(假设数组是只读的)。
    只能使用额外的 O(1) 的空间。
    时间复杂度小于 O(n2) 。
    数组中只有一个重复的数字,但它可能不止重复出现一次
    
    • 思路:1~n的n+1个整数组成的数组,可以看作是一条静态链表,下标0代表头指针,列表中的值代表next指针,类似于链表中寻找环的交点算法,可以找到数组中的重复元素
    • 代码:
        def findDuplicate(self, nums):
            """
            :type nums: List[int]
            :rtype: int
            """
            n = len(nums)
            slow = fast = 0
            # 首先找到下一个交点
            while True:
                slow = nums[slow]
                fast = nums[nums[fast]]
                if slow == fast:
                    break
            # 排除另一个慢指针,与上一个慢指针同时出发,交点即重复元素
            slow2 = 0
            while True:
                slow = nums[slow]
                slow2 = nums[slow2]
                if slow == slow2:
                    return slow
    
    • 分析:空间O(1),时间O(n^2)

    5. 早晚指针

    早晚指针:在指针移动某位置时,另一个指针从头出发,主要用处在于产生/消除差额。

    使用快慢指针判断环的交点问题中也使用到了早晚指针,另一个经典的应用是寻找链表中倒数第k个节点,原理是当第一个指针移动了k次时,再派出一个指针从头开始同步移动,最后当早指针不能前进时,晚指针指向的就是倒数第k个节点。

    5. 双表指针

    • 指针含义:两个指针分别用于指示两个线性表中的当前位置
    • 指针移动:具体问题具体分析
    • 问题特点:当需要同时操作两个线性表时,典型的有数组归并,链表逆序

    5.1 数组交集

    • 问题:[349] 两个数组的交集,给定两个数组,写一个函数来计算它们的交集
    • 思路:该问题使用集合很容易在O(n)时间复杂度下求解,如果不使用集合,可先对数组排序,用两个指针指示两个数组的出现不同元素的第一个元素,如果相等则收集,直至其中一个遍历完。虽然是一种笨的方法,但用到的方法在很多问题中都会遇到。
    • 代码:
    class Solution(object):
        def intersection(self, nums1, nums2):
            """
            :type nums1: List[int]
            :type nums2: List[int]
            :rtype: List[int]
            """
            nums1.sort()
            nums2.sort()
            m,n = len(nums1),len(nums2)
            res = []
            i = j = 0
            while i < m and j < n:
                if nums1[i] == nums2[j]:
                    res.append(nums1[i])
                    i,j = i + 1, j + 1
                    while i < m and nums1[i] == nums1[i-1]: i+= 1
                    while j < n and nums2[j] == nums2[j-1]: j+= 1
                elif nums1[i] > nums2[j]:
                    j += 1
                else:
                    i += 1
            return res        
    

    5.2 归并排序

    详见链表或者排序

    5.3 链表逆序

    详见链表

    相关文章

      网友评论

          本文标题:2018-06-25

          本文链接:https://www.haomeiwen.com/subject/eqsyyftx.html