二分查找
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 right−left−1。需要用到二轮二分查找
。
先用二分查找找到右边界,当跳出循环即 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)。