算法_二分查找&双指针_总结

二分查找

704.二分查找

https://leetcode.cn/problems/binary-search/
数组 二分查找
给定一个 n 个元素有序的(升序)整型数组nums和一个目标值arget,写一个函数搜索nums中的target,如果目标值存在返回下标,否则返回 -1。

二分法的前提条件:数组是有序数组,数组中无重复元素

思路:left, right, middle指针,左闭右闭区间,while循环

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left = 0
        right = len(nums) - 1
        while left <= right:
            middle = (left + right) // 2
            if nums[middle] < target:
                left = middle + 1
            elif nums[middle] > target:
                right = middle - 1
            else:
                return middle
        return -1

278.第一个错误的版本

https://leetcode.cn/problems/first-bad-version/
二分查找 交互
我的思路:把第一个错误版本设为左边界,直接返回左边界。

将左右边界分别初始化为 1 和 n,n 是给定的版本数量。第一个错误版本之前的版本都是正确版本,之后的版本都是错误版本,所以如果左边界是错误版本,那么就是第一个错误版本。判断中间版本是否是错误版本,如果是错误版本,则可能是第一个错误版本也可能不是,因此 right = middle,而不是 middle - 1。如果是正确版本,则 left = middle + 1。

class Solution:
    def firstBadVersion(self, n: int) -> int:
        if n == 1: return 1
        left = 1
        right = n
        while left <= right:
            middle = (left + right) // 2
            if isBadVersion(left): return left 
            if isBadVersion(middle):  # true
                right = middle
            else:                     # false   
                left = middle + 1

复杂度分析

时间复杂度: O ( log ⁡ n ) O(\log n) O(logn),其中 n 是给定版本的数量。
空间复杂度: O ( 1 ) O(1) O(1)。我们只需要常数的空间保存若干变量。

35.搜索插入位置

https://leetcode.cn/problems/search-insert-position/
数组 二分查找
我的思路:如果数组中存在目标值,则直接返回middle索引。如果不存在,则返回的索引位置就是left指向的位置。(等价于right + 1的位置)

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        left = 0
        right = len(nums) - 1
        while left <= right:
            middle = (left + right) // 2
            if nums[middle] < target:
                left = middle + 1
            elif nums[middle] > target:
                right = middle - 1
            else:
                return middle
        return left

!剑指 Offer 53 - I. 在排序数组中查找数字 I

https://leetcode.cn/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/

统计一个数字在排序数组中出现的次数。

示例 1:

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

示例 2:

输入: nums = [5,7,7,8,8,10], target = 6
输出: 0

我的思路:

利用python的库函数,并没有用到算法思路

from collections import Counter
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        res = Counter(nums)
        if target in res:
            return res[target]
        return 0

方法一:!!! 二分查找 左边界&右边界

参考图解数据结构:https://leetcode.cn/leetbook/read/illustration-of-algorithm/58lgr7/

排序数组中的搜索问题,首先想到二分法解决。

统计数字target的出现次数,可转化为:使用二分法分别找到target左边界left右边界right ,易得数字 target的数量为 r i g h t − l e f t − 1 right - left - 1 rightleft1需要用到二轮二分查找

先用二分查找找到右边界,当跳出循环即 j < i 时,i 就是右边界。然后再一轮二分查找,在 [0,j] 区间中找左边界,注意:两次二分查找,当中间值等于 target 时,指针变化不同。

具体参考算法解析:

在这里插入图片描述
在这里插入图片描述

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        # 第一轮二分查找找右边界
        i, j = 0, len(nums) - 1  # 初始化
        while i <= j:
            m = i + (j - i) // 2   # 等价于 m = (i + j) // 2
            if nums[m] <= target:
                i = m + 1
            else:
                j = m - 1
        # 跳出循环,则j < i,即右边界即为i
        right = i

        # 若数组中无 target ,则提前返回
        if j >= 0 and nums[j] != target: return 0

        # 第二轮二分查找找左边界
        i = 0   # 初始化i,j不变
        while i <= j:
            m = i + (j - i) // 2
            if nums[m] < target:
                i = m + 1
            else:
                j = m - 1   # 此时,等于target的时候,j变
        # 跳出循环,此时j为左边界
        left = j
        return right - left - 1

复杂度分析:
时间复杂度 O ( log ⁡ N ) O(\log N) O(logN) : 二分法为对数级别复杂度。
空间复杂度 O ( 1 ) O(1) O(1) : 几个变量使用常数大小的额外空间。

代码简化:

将二分查找右边界 r i g h t right right 的代码 封装至函数 helper() 。
如下图所示,由于数组 numsnums 中元素都为整数,因此可以分别二分查找 target 和 target - 1 的右边界,将两结果相减并返回即可。

在这里插入图片描述
本质上看, helper() 函数旨在查找数字 tar 在数组 nums中的 插入点 ,且若数组中存在值相同的元素,则插入到这些元素的右边。

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        # 分别二分查找 target 和 target - 1 的右边界,将两结果相减并返回即可。
        def helper(target):
            i, j = 0, len(nums) - 1
            while i <= j:
                m = i + (j - i) // 2
                if nums[m] <= target: i = m + 1
                else: j = m - 1
            return i
        return helper(target) - helper(target - 1)

方法二:哈希表

空间复杂度高,数据量大时不如二分查找。

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        res = {}
        for num in nums:
            if num in res:
                res[num] += 1
            else:
                res[num] = 1
        return res[target] if target in res else 0

----------------------------------------------

双指针

977. 有序数组的平方

https://leetcode.cn/problems/squares-of-a-sorted-array/
数组 双指针 排序

给你一个按非递减顺序排序的整数数组nums,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。

方法一:暴力排序

方法二:双指针法

代码随想录思路:逆向思维 (官方解法-方法三:双指针)

数组其实是有序的, 只不过负数平方之后可能成为最大数了。
那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。
此时可以考虑双指针法了, i i i指向起始位置, j j j指向终止位置。
定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置。

如果 A [ i ] ∗ A [ i ] < A [ j ] ∗ A [ j ] A[i] * A[i] < A[j] * A[j] A[i]A[i]<A[j]A[j],那么 r e s u l t [ k ] = A [ j ] ∗ A [ j ] result[k] = A[j] * A[j] result[k]=A[j]A[j]
如果 A [ i ] ∗ A [ i ] > = A [ j ] ∗ A [ j ] A[i] * A[i] >= A[j] * A[j] A[i]A[i]>=A[j]A[j],那么 r e s u l t [ k ] = A [ i ] ∗ A [ i ] result[k] = A[i] * A[i] result[k]=A[i]A[i]

class Solution:
    def sortedSquares(self, nums: List[int]) -> List[int]:
        # 双指针法
        n = len(nums)
        left, right, k = 0, n - 1, n - 1
        result = [0] * n
        while left <= right:
            if nums[left] * nums[left] > nums[right] * nums[right]:
                result[k] = nums[left] * nums[left]
                left += 1
            else:
                result[k] = nums[right] * nums[right]
                right -= 1
            k -= 1
        return result

复杂度分析
时间复杂度: O ( n ) O(n) O(n),其中 n 是数组 nums \textit{nums} nums 的长度。
空间复杂度: O ( 1 ) O(1) O(1)。除了存储答案的数组以外,我们只需要维护常量空间。


189.轮转数组

https://leetcode.cn/problems/rotate-array/
数组 数学 双指针
给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
原地算法

方法一:先反转整个数组,再反转局部区间

本题是右旋转,其实就是反转的顺序改动一下,优先反转整个数组,步骤如下:

  • 反转整个数组
  • 反转区间为前k的数组
  • 反转区间为k到末尾的数组

这是一个重要的思想,可以用到很多需要反转的题目中。

注:本题还有一个小陷阱,题目输入中,如果k大于nums.size了应该怎么办?
例如,1,2,3,4,5,6,7 如果右移动15次的话,是 7 1 2 3 4 5 6 。
所以其实就是右移 k % nums.size() 次,即:15 % 7 = 1

复杂度分析
时间复杂度: O ( n ) O(n) O(n),其中 n 为数组的长度。每个元素被翻转两次,一共 n 个元素,因此总时间复杂度为 O ( 2 n ) = O ( n ) O(2n)=O(n) O(2n)=O(n)
空间复杂度: O ( 1 ) O(1) O(1)

方法二:

方法三:


283.移动零

https://leetcode.cn/problems/move-zeroes/
数组 双指针 原地算法

给定一个数组nums,编写一个函数将所有0移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。

我的思路:定义快慢指针,当数组中元素不为0时,快慢指针同时前进。当元素为0时,快指针前进一步,此时慢指针指向的值为0,快指针继续前进,直到指向的值不为0,快慢指针的值交换(将0移到后面),慢指针前进一步。直到快指针遍历完整个数组。

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        n = len(nums)
        left, right = 0, 0  # 快慢指针
        while right < n:
            if nums[right] != 0:
                # 其实相当于nums[left] == 0,交换0到后面
                nums[left], nums[right] = nums[right], nums[left]
                left += 1
            right += 1

167. 两数之和 II - 输入有序数组

https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/
数组 双指针 二分查找
设计的解决方案必须只使用常量级的额外空间。

方法一:哈希表

达不到 O ( 1 ) O(1) O(1)的空间复杂度

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        res = {}
        for idx, val in enumerate(numbers):
            if target - val not in res:
                res[val] = idx + 1
            else:
                return [res[target - val], idx + 1]

复杂度分析

  • 时间复杂度: O ( N ) O(N) O(N),其中 N 是数组中的元素数量。对于每一个元素 x,我们可以 O ( 1 ) O(1) O(1) 地寻找 target - x。
  • 空间复杂度: O ( N ) O(N) O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。

方法二:二分查找

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        # 二分查找
        n = len(numbers)
        for i in range(n):
            left, right = i+1, n - 1
            while left <= right:
                mid = (left + right) // 2
                if numbers[mid] == target - numbers[i]:
                    return [i+1, mid+1]
                elif numbers[mid] >  target - numbers[i]:
                    right = mid - 1
                else:
                    left = mid + 1
        return [-1,-1]

复杂度分析

  • 时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),其中 n 是数组的长度。需要遍历数组一次确定第一个数,时间复杂度是 O ( n ) O(n) O(n),寻找第二个数使用二分查找,时间复杂度是 O ( log ⁡ n ) O(\log n) O(logn),因此总时间复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn)
  • 空间复杂度: O ( 1 ) O(1) O(1)

方法三:双指针(最优)

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        # 双指针
        n = len(numbers)
        left, right = 0, n - 1
        while left <= right:
            Sum = numbers[left] + numbers[right]
            if Sum == target:
                return [left+1, right+1]
            elif Sum < target:
                left += 1
            else:
                right -= 1
        return [-1,-1]

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n 是数组的长度。两个指针移动的总次数最多为 nn 次。
  • 空间复杂度: O ( 1 ) O(1) O(1)

双指针_字符串

344.反转字符串

https://leetcode.cn/problems/reverse-string/
递归 双指针 字符串

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

双指针

定义两个指针指向字符串的头部和尾部,交换数值。

class Solution:
    def reverseString(self, s: List[str]) -> None:
    	n = len(s)
    	left, right = 0, n - 1
    	while left <= right:
    		s[left], s[right] = s[right], s[left]
    		left += 1
    		right -= 1

557. 反转字符串中的单词 III

https://leetcode.cn/problems/reverse-words-in-a-string-iii/
双指针 字符串
给定一个字符串 s ,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。
原地解法:按最直接的思路,需要一个变量存储空格位置(单词的起始位置),然后根据单词的起始位置,使用双指针反转单词,需要考虑到有多个空格的情况。
但是在 Python 中字符串不可变,以下代码会报错,仅提供思路。

class Solution:
    def reverseWords(self, s: str) -> str:
        n = len(s)
        i = 0
        while i < n:
            # 找到单词的起始位置
            start = i
            while i < n and s[i] != ' ':
                i += 1
            left = start
            right = i - 1
            # 反转单词
            while left < right:
                s[left], s[right] = s[right], s[left]
                left += 1
                right -= 1
            # 如果有多个空格
            while i < n and s[i] == ' ':
                i += 1
        return s

因为在 Python 中字符串是不可变,因此遍历字符串交换每个单词内字符位置的方法不太可行,但是利用** Python 切片**的便利,可以写出更优雅的实现方式。

1、常规思路
将字符串分割成单词列表 然后把每个单词反转切片

class Solution(object):
    def reverseWords(self, s):
        return " ".join(word[::-1] for word in s.split(" "))

时间复杂度: O ( n ) O(n) O(n) 。其中 n 是字符串的长度。
空间复杂度: O ( 1 ) O(1) O(1)

补充:代码随想录字符串

541.反转字符串II

https://leetcode.cn/problems/reverse-string-ii/
字符串 双指针


剑指 Offer 05. 替换空格

https://leetcode.cn/problems/ti-huan-kong-ge-lcof/

class Solution:
    def replaceSpace(self, s: str) -> str:
        return "%20".join(word for word in s.split(" "))

151. 反转字符串中的单词


双指针_链表

876. 链表的中间结点

https://leetcode.cn/problems/middle-of-the-linked-list/

给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。

参考官方题解:

方法一:数组

链表的缺点在于不能通过下标访问对应的元素。因此我们可以考虑对链表进行遍历,同时将遍历到的元素依次放入数组 A 中。如果我们遍历到了 N 个元素,那么链表以及数组的长度也为 N,对应的中间节点即为 A[N/2]。

class Solution:
    def middleNode(self, head: ListNode) -> ListNode:
        A = [head]
        while A[-1].next:
            A.append(A[-1].next)
        return A[len(A) // 2]
  • 时间复杂度:O(N),其中 N 是给定链表中的结点数目。
  • 空间复杂度:O(N),即数组 A 用去的空间。

方法二:单指针法

我们可以对方法一进行空间优化,省去数组 A。

我们可以对链表进行两次遍历。第一次遍历时,我们统计链表中的元素个数 N;第二次遍历时,我们遍历到第 N/2 个元素(链表的首节点为第 0 个元素)时,将该元素返回即可。

class Solution:
    def middleNode(self, head: ListNode) -> ListNode:
        n, cur = 0, head
        while cur:
            n += 1
            cur = cur.next
        k, cur = 0, head
        while k < n // 2:
            k += 1
            cur = cur.next
        return cur
  • 时间复杂度:O(N),其中 N 是给定链表的结点数目。
  • 空间复杂度:O(1),只需要常数空间存放变量和指针。

方法三:快慢指针法

我们可以继续优化方法二,用两个指针 slow 与 fast 一起遍历链表。slow 一次走一步,fast 一次走两步。那么当 fast 到达链表的末尾时,slow 必然位于中间。

class Solution:
    def middleNode(self, head: ListNode) -> ListNode:
        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        return slow

19. 删除链表的倒数第 N 个结点

https://leetcode.cn/problems/remove-nth-node-from-end-of-list/

定义虚拟头节点

常用题解:使用双指针,快慢指针

# 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: Optional[ListNode], n: int) -> Optional[ListNode]:
        # 定义一个虚拟头节点
        dummyNode = ListNode()
        dummyNode.next = head

        slow = fast = dummyNode
        # 快指针先走n步
        for i in range(n):
            fast = fast.next
        
        while fast.next != None:
            slow = slow.next
            fast = fast.next

        # fast走到结尾后,slow的下一个节点为倒数第n个节点
        slow.next = slow.next.next
        return dummyNode.next

代码随想录

# 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:
        head_dummy = ListNode()
        head_dummy.next = head

        slow, fast = head_dummy, head_dummy
        while(n!=0): #fast先往前走n步
            fast = fast.next
            n -= 1
        while(fast.next!=None):
            slow = slow.next
            fast = fast.next
        #fast 走到结尾后,slow的下一个节点为倒数第N个节点
        slow.next = slow.next.next #删除
        return head_dummy.next
  • 时间复杂度:O(L),其中 L 是链表的长度。
  • 空间复杂度:O(1)。

官方题解方法一:计算链表长度

一种容易想到的方法是,我们首先从头节点开始对链表进行一次遍历,得到链表的长度 LL。随后我们再从头节点开始对链表进行一次遍历,当遍历到第 L-n+1个节点时,它就是我们需要删除的节点。

为了与题目中的 n 保持一致,节点的编号从 1 开始,头节点为编号 1 的节点。

为了方便删除操作,我们可以从哑节点开始遍历 L-n+1 个节点。当遍历到第 L-n+1 个节点时,它的下一个节点就是我们需要删除的节点,这样我们只需要修改一次指针,就能完成删除操作。

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
  • 时间复杂度:O(L),其中 L 是链表的长度。
  • 空间复杂度:O(1)。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Pythonistas29xs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值