原书链接:https://leetcode-cn.com/leetbook/detail/array-and-string/
数组
二分查找算是比较基础的算法了,这种用于已经排序好的数组中,题目 搜索插入位置便是对于该算法的考察。
因为所查找的target可能不在数组中,这时候对于target位于队列最右端或者最左端的处理就比较关键了。
高赞题解中的解题思路是:
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
size = len(nums)
if size == 0:
return 0
left = 0
# 因为有可能数组的最后一个元素的位置的下一个是我们要找的,
# 故右边界是 len
right = size
while left < right:
# left + right 在 Python 中如果发生整型溢出,
# Python 会自动转成长整形
mid = (left + right) // 2
# 严格小于 target 的元素一定不是解
if nums[mid] < target:
# 下一轮搜索区间是 [mid + 1, right]
left = mid + 1
else:
right = mid
return left
这里面比我的思路好的地方:
- 将right设为
len(nums)
,这样使得最后一个元素可以用于比较; - 设置
left = mid + 1
和right = mid
对于最后数组没有该元素进行了处理,也避免了对于left和right相等的判断。
二维数组
二维数组有点烧脑诶……对角线遍历这道题目自己的解法太过于繁琐,总是超时。
官方题解 题解一的思路和我一样,都是先将对角线上元素加入队列然后依据所在对角线来决定是正序加入res还是逆序加入res。
class Solution:
def findDiagonalOrder(self, matrix: List[List[int]]) -> List[int]:
if not matrix or not matrix[0]:
return []
N, M = len(matrix), len(matrix[0])
result, intermediate = [], []
for d in range(N + M - 1):
intermediate.clear()
r, c = 0 if d < M else d - M + 1, d if d < M else M - 1
while r < N and c > -1:
intermediate.append(matrix[r][c])
r += 1
c -= 1
if d % 2 == 0:
result.extend(intermediate[::-1])
else:
result.extend(intermediate)
return result
官方题解处理比较好的地方是对于遍历过程中有超出范围的值,我的做法是遍历过程中进行检查,而官方解法更加妥当,在初始化时便进行检查,从而避免了无效的遍历:r, c = 0 if d < M else d - M + 1, d if d < M else M - 1
。
字符串
Leetbook中字符串部分比较难的是回文的判断,在最长回文子串这道题目中涉及到多种思路解答,自己是真的菜……
这道题目动态规划、中心扩散、Manacher 算法对于动态规划以及之后的优化讲得特别特别细,自己就不画蛇添足了。
自己要注意的是动态规划一定一定要注意状态转移方程的推导,可以说得到该方程动态规划也就完成了一大半。
附动态规划代码:
class Solution:
def longestPalindrome(self, s: str) -> str:
size = len(s)
if size < 2:
return s
dp = [[False for _ in range(size)] for _ in range(size)]
max_len = 1
start = 0
for i in range(size):
dp[i][i] = True
for j in range(1, size):
for i in range(0, j):
if s[i] == s[j]:
if j - i < 3:
dp[i][j] = True
else:
dp[i][j] = dp[i + 1][j - 1]
else:
dp[i][j] = False
if dp[i][j]:
cur_len = j - i + 1
if cur_len > max_len:
max_len = cur_len
start = i
return s[start:start + max_len]
这里记录之前结果使用了二维数组而不是之前自己见过的一维数组,所以说还是自己功力不够呀。
之后作者介绍了中心扩散的思路,大体为:遍历每一个索引,以这个索引为中心,利用“回文串”中心对称的特点,往两边扩散,看最多能扩散多远。
作者提供的参考代码见下:
class Solution:
def longestPalindrome(self, s: str) -> str:
size = len(s)
if size < 2:
return s
# 至少是 1
max_len = 1
res = 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) > max_len:
max_len = len(cur_max_sub)
res = cur_max_sub
return res
def __center_spread(self, s, size, left, right):
"""
left = right 的时候,此时回文中心是一个字符,
回文串的长度是奇数
right = left + 1 的时候,此时回文中心是一个空隙,
回文串的长度是偶数
"""
i = left
j = right
while i >= 0 and j < size and s[i] == s[j]:
i -= 1
j += 1
return s[i + 1:j], j - i - 1
KMP算法
Knuth–Morris–Pratt(KMP)算法是一种改进的字符串匹配算法,它的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。它的时间复杂度是 O(m+n)。
代码如下:
int match (char* P, char* S){ // KMP 算法
int* next = buildNext(P); // 构造 next 表
int m = (int) strlen (S), i = 0; // 文本串指针
int n = (int) strlen(P), j = 0; //模式串指针
while (j < n && i < m) // 自左向右逐个比对字符
if (0 > j || S[i] == P[j]) // 若匹配,或 P 已移除最左侧
{i++; j++;} // 则转到下一字符
else
j = next[j]; // 模式串右移(注意:文本串不用回退)
delete [] next; // 释放 next 表
return i - j;
}
int* buildNext(char* P) { // 构造模式串 P 的 next 表
size_t m = strlen(P), j = 0; // “主”串指针
int* N = new int[m]; // next 表
int t = N[0] = -1; // 模式串指针
while (j < m - 1)
if ( 0 > t || P[j] == P[t]){ // 匹配
j++; t++;
N[j] = t; // 此句可改进为 N[j] = (P[j] != P[t] ? t : N[t]);
}else // 失配
t = N[t];
return N;
}
KMP算法要说看懂还是比较容易的,但是实际上做的时候还是有很多细节需要注意,附一道KMP算法题:实现 strStr()。
双指针技巧
两数之和 II - 输入有序数组是一道典型需要使用双指针的题目。
题目中给的列表为升序,需要找到两个元素使其和为指定值。其基本思路就是利用双指针,当两指针指向位置元素和大于指定值时right指针移动,反之则left指针移动。
另一类可能使用双指针的策略是快慢指针,这个用的很多,无论是链表中寻找中点还是在列表中需要替换元素等待都是比较常用的。不过这类题目大多在明确使用双指针后编程问题貌似都会简单很多……