目录
二分查找基础知识
01. 二分查找知识(一) | 算法通关手册(LeetCode)
二分查找的基本算法思想为:通过确定目标元素所在的区间范围,反复将查找范围减半,直到找到元素或找不到该元素为止。
-
初始化:首先,确定要查找的有序数据集合。可以是一个数组或列表,确保其中的元素按照升序或者降序排列。
leetcode 704 二分查找
题目:Given an array of integers nums
which is sorted in ascending order, and an integer target
, write a function to search target
in nums
. If target
exists, then return its index. Otherwise, return -1
.
You must write an algorithm with O(log n)
runtime complexity.
Constraints:
1 <= nums.length <= 104
-104 < nums[i], target < 104
- All the integers in
nums
are unique. nums
is sorted in ascending order.
读题:target可以出现在任何一个位置上,也可以不存在于list中。
需要双指针考虑全range:最左,最右和中间,各一个指针。
因为数组是从小到大排列好的,所以我们只需要对比middle number和target来移动整体调查阈值(是向左移动还是右移动)。
class Solution:
def search(self, nums: List[int], target: int) -> int:
left,right = 0, len(nums)-1
while left <= right:
middle =left+(right-left)//2
if nums[middle]>target:
right = middle -1 # target在左区间,所以[left, middle - 1]
elif nums[middle]<target:
left=middle+1 # target在右区间,所以[middle + 1, right]
else:
return middle # 数组中找到目标值,直接返回下标
return -1 # 未找到目标值
思路 1:复杂度分析
- 时间复杂度:O(logn)
- 空间复杂度:O(1)
leetcode 27
Remove Element
Given an integer array nums
and an integer val
, remove all occurrences of val
in nums
in-place. The order of the elements may be changed. Then return the number of elements in nums
which are not equal to val
.
Consider the number of elements in nums
which are not equal to val
be k
, to get accepted, you need to do the following things:
- Change the array
nums
such that the firstk
elements ofnums
contain the elements which are not equal toval
. The remaining elements ofnums
are not important as well as the size ofnums
. - Return
k
.
暴力解法 Brute Force method
这个题目暴力的解法就是两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。
def removeElement(self, nums: List[int], val: int) -> int:
n = len(nums)
i = 0
while i < n:
if nums[i] == val:
for j in range(i + 1 , n):
nums[j - 1] = nums[j]
i -= 1
n -= 1
i += 1
return n
双指针法
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针
- 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
- 慢指针:指向更新 新数组下标的位置
def removeElement(self, nums: List[int], val: int) -> int:
# 快指针遍历元素
fast = 0
# 慢指针记录位置
slow = 0
for fast in range(len(nums)):
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
return slow
- 关于二分法和移除元素的共性思考
这两题之间有点类似的,他们都是在不断缩小 left 和 right 之间的距离,每次需要判断的都是 left 和 right 之间的数是否满足特定条件。对于「移除元素」这个写法本质上还可以理解为,我们拿 right 的元素也就是右边的元素,去填补 left 元素也就是左边的元素的坑,坑就是 left 从左到右遍历过程中遇到的需要删除的数,因为题目最后说超过数组长度的右边的数可以不用理,所以其实我们的视角是以 left 为主,这样想可能更直观一点。用填补的思想的话可能会修改元素相对位置,这个也是题目所允许的。
给自己的一个commitment: 希望2024年能越挫越勇,面对问题直面而上,克服对算法和DSA的恐惧,为之后求职必经的oa之路做好准备,加油!
33.搜索旋转排序数组
排序的时间复杂度就是O(n).
class Solution:
def search(self, nums: List[int], target: int) -> int:
left,right=0,len(nums)-1
while left <= right:
mid = (left+right)//2
if nums[mid]==target:
return mid
elif nums[left]<= nums[mid]:
if nums[left]<=target < nums[mid]:
right = mid-1
else:
left= mid+1
else:
if nums[mid]<target <= nums[right]:
left=mid+1
else:
right=mid-1
return -1
5. 最长回文子串
LeetCode 第 5 题:“最长回文子串”题解 | LeetCode 题解
“最长回文子串”题解
方法一:暴力匹配 (Brute Force)
暴力解法虽然时间复杂度高,但是思路清晰、编写简单,因为编写的正确性高,完全可以使用暴力匹配算法检验我们编写的算法的正确性。
class Solution:
def longestPalindrome(self, s):
size = len(s)
if size == 0:
return ''
# 至少是 1
longest_palindrome = 1
longest_palindrome_str = s[0]
for i in range(size):
palindrome_odd, odd_len = self.__center_spread(s, size, i, i)
palindrome_even, even_len = self.__center_spread(s, size, i, i + 1)
# 当前找到的最长回文子串
cur_max_sub = palindrome_odd if odd_len >= even_len else palindrome_even
if len(cur_max_sub) > longest_palindrome:
longest_palindrome = len(cur_max_sub)
longest_palindrome_str = cur_max_sub
return longest_palindrome_str
def __center_spread(self, s, size, left, right):
"""
left = right 的时候,此时回文中心是一条线,回文串的长度是奇数
right = left + 1 的时候,此时回文中心是任意一个字符,回文串的长度是偶数
"""
l = left
r = right
while l >= 0 and r < size and s[l] == s[r]:
l -= 1
r += 1
return s[l + 1:r], r - l - 1
复杂度分析:
- 时间复杂度:O(N2)
- 空间复杂度:O(1)
方法三:动态规划(推荐)
推荐理由:暴力解法太 naive,中心扩散不普适,Manacher 就更不普适了,是专门解这个问题的方法。而用动态规划是可以帮助你举一反三的方法。
class Solution:
def longestPalindrome(self, s: str) -> str:
size = len(s)
if size <= 1:
return s
# 二维 dp 问题
# 状态:dp[l,r]: s[l:r] 包括 l,r ,表示的字符串是不是回文串
# 设置为 None 是为了方便调试,看清楚代码执行流程
dp = [[False for _ in range(size)] for _ in range(size)]
longest_l = 1
res = s[0]
# 因为只有 1 个字符的情况在最开始做了判断
# 左边界一定要比右边界小,因此右边界从 1 开始
for r in range(1, size):
for l in range(r):
# 状态转移方程:如果头尾字符相等并且中间也是回文
# 在头尾字符相等的前提下,如果收缩以后不构成区间(最多只有 1 个元素),直接返回 True 即可
# 否则要继续看收缩以后的区间的回文性
# 重点理解 or 的短路性质在这里的作用
if s[l] == s[r] and (r - l <= 2 or dp[l + 1][r - 1]):
dp[l][r] = True
cur_len = r - l + 1
if cur_len > longest_l:
longest_l = cur_len
res = s[l:r + 1]
# 调试语句
# for item in dp:
# print(item)
# print('---')
return res
动态规划是解决某些类型问题的一种方法,特别是那些可以分解为重复子问题的问题。动态规划通过组合子问题的解来解决主问题,避免了重复计算子问题,从而提高了效率。
在这个“最长回文子串”问题中,动态规划的思想被用来逐步构建回文子串的解决方案。下面我会逐步解释代码中的动态规划过程:
1. 状态定义
在这个问题中,动态规划表 dp
是一个二维数组,其中 dp[l][r]
表示字符串 s
从索引 l
到索引 r
(包含 l
和 r
)的子串是否是回文串。True
表示是回文串,False
表示不是。
2. 初始化
动态规划表 dp
的所有元素最初都被设置为 False
。单个字符总是回文串,但在这个问题的代码中,单字符回文是通过初始化 longest_l
为 1
和 res
为 s[0]
来处理的,而不是在 dp
表中明确设置。
3. 状态转移方程
dp[l][r] = True
的条件是:
s[l]
和s[r]
必须相等,即子串的头尾字符必须相同。- 子串去掉头尾字符后(即
s[l+1:r]
),这个新子串要么是一个长度不超过1
的字符串(这意味着它不需要进一步检查就是回文),要么是另一个回文串(即dp[l+1][r-1]
为True
)。
4. 遍历顺序
这里的遍历是按右边界 r
从 1
开始逐渐扩大,左边界 l
在每个右边界 r
的情况下从 0
遍历到 r
。这保证了在计算 dp[l][r]
时,所有需要的 dp[l+1][r-1]
已经被计算过了,满足了动态规划的“无后效性”,即当前状态只依赖于之前的状态。
5. 更新结果
在每次发现一个更长的回文子串时,更新 longest_l
和 res
来记录当前最长的回文子串的长度和内容。
通过上述步骤,这段代码能够找到并返回输入字符串 s
中的最长回文子串。动态规划在这个问题中的妙处在于它避免了重复检查子串是否是回文,从而大大提高了效率。