文章目录
q3无重复字符的最长子串
题解
使用滑动窗口求解,设置左右指针来确定窗口的边界,其中右指针就是循环变量的位置. 窗口的变化取决于当前遍历到的字符之前有没有出现过,如果出现过,那么左指针移动到字符上一个位置+1.
我们设置了一个cache数组,用于保存每个元素最后一次出现的后一位,以下面一组元素来举例:
i: 0 1 2 3 4 5 6
元素: a b c a d a e
cache的值变化 左边界(l) 子串长度
遍历到 0 : 0 0 0 0 0 0 0 0 1
遍历到 1 : 1 0 0 0 0 0 0 0 2
遍历到 2 : 1 2 0 0 0 0 0 0 3
遍历到 3 : 1 2 3 0 0 0 0 1 3
遍历到 4 : 4 2 3 4 0 0 0 1 4
遍历到 5 : 4 2 3 4 5 0 0 4 2
遍历到 6 : 6 2 3 6 5 6 0 4 3
我们可以发现,子串的长度实际就是i - l + 1
.
要注意每次遍历到一个元素的时候,cache数组总是在最后才更新值,假设当前刚好遍历到一个重复的元素a
,刚好能让左指针先跳转到前面那个a
的后一个位置,再计算子串的长度,最后才更新cache数组的值.
func lengthOfLongestSubstring(s string) (maxLen int) {
l := 0
cache := make([]int, 128)
for i := 0; i < len(s); i++ {
// 如果当前元素之前出现过,就需要移动l指针
l = max(l, cache[s[i]])
// 更新最大子串长度
maxLen = max(maxLen, i - l + 1)
// 更新cache数组的值
cache[s[i]] = i + 1
}
return
}
func max(a, b int) int {
if a > b {
return a
} else {
return b
}
}
q11盛最多水的容器
题解
使用双指针从两边向中间遍历,同时更新最大值.
func maxArea(height []int) (Max int) {
left, right := 0, len(height) - 1
Max = (right -left) * min(height[left], height[right])
// 左右指针从两边向中间遍历,更新最大值
for left < right {
// 哪边更小哪边的指针就向中间走,寻找更高的垂直线
if height[left] < height[right] {
left++
} else {
right--
}
// 更新最大值
area := (right -left) * min(height[left], height[right])
Max = max(Max, area)
}
return
}
func min(a, b int) int {
if a < b {
return a
} else {
return b
}
}
func max(a, b int) int {
if a > b {
return a
} else {
return b
}
}
q15三数之和
题解
首先排序,然后用三指针遍历,注意需要去重。
步骤:
- 对数组进行排序。
- 开始循环遍历。
- 首先判断左指针指向的数是否大于0,大于0就break。
- 对左指针进行去重,nums[i] == nums[i - 1],就continue。
- 定义中间指针和右指针:j, k := i+1, len(nums)-1。
- 在内置循环中进行遍历,判断
nums[i]+nums[j]+nums[k]
是否等于0,同时对j和k进行去重。
func threeSum(nums []int) (res [][]int) {
// 对数组进行排序
sort.Ints(nums)
// 循环遍历
for i := 0; i < len(nums)-2; i++ {
// 最左边的数大于0,结果肯定大于0
if nums[i] > 0 {
break
}
// 对最左边的数进行去重
if i > 0 && nums[i] == nums[i-1] {
continue
}
// 定义中间指针和右指针
j, k := i+1, len(nums)-1
for j < k {
if nums[i]+nums[j]+nums[k] == 0 {
res = append(res, []int{nums[i], nums[j], nums[k]})
// 对中指针和右指针去重
n1, n2 := nums[j], nums[k]
for j < k && nums[j] == n1 {
j++
}
for j < k && nums[k] == n2 {
k--
}
} else if nums[i]+nums[j]+nums[k] < 0 {
j++
} else {
k--
}
}
}
return
}
q16最接近的三数之和
题解
先排序,然后用双指针操作。
func threeSumClosest(nums []int, target int) int {
// 先排序
sort.Ints(nums)
// 初始化最大值
n, sumClose := len(nums), nums[0]+nums[1]+nums[2]
// i从0到n - 3遍历
// 剩下两个数在i+1到n - 1的范围遍历
for i := 0; i < n-2; i++ {
// 去重
if i > 0 && nums[i] == nums[i-1] {
continue
}
// 遍历剩下两个数
j, k := i+1, n-1
for j < k {
sum := nums[i] + nums[j] + nums[k]
// 正好相加等于target
if sum == target {
return target
}
// 更新最接近的数
if math.Abs(float64(int64(sum-target))) < math.Abs(float64(int64(sumClose-target))) {
sumClose = sum
}
// 偏大就左移右指针,偏小就左移右指针
if sum > target {
k--
} else {
j++
}
}
}
return sumClose
}
q26 删除有序数组中的重复项
题解
这道题使用快慢指针来求解,首先循环遍历数组,快指针就是循环变量,使用一个变量p来缓存上一个遍历到的变量,与本次遍历到的变量做比较,如果相等就忽略,如果大于p,就覆盖到慢指针指向的那个位置,同时向后移动慢指针。
func removeDuplicates(nums []int) int {
// 慢指针
slow := 0
// 用于保存上一个变量
p := -10001
for i, _ := range nums {
// 当前变量比上一个变量大,说明没有重复,直接覆盖到原数组
if nums[i] > p {
nums[slow] = nums[i]
// 备份当前变量,用于下一次比较
p = nums[slow]
slow++
}
}
return slow
}
q31 下一个排列
题解
该题的算法思路是:
我们的目标就是找出两个数:右边尽可能小的大数和左边尽可能大的小数,将这两个数交换,然后将交换后的位置后面的数组升序排序,即可得到答案。下面是算法步骤:
- 从后向前查找第一个相邻升序的元素对 (i,j),满足 A[i] < A[j]。此时 [j,end) 必然是降序。
- 在 [j,end) 从后向前查找第一个满足 A[i] < A[k] 的 k。A[i]、A[k] 分别就是左边的最大数、右边的最小数。
- 将 A[i] 与 A[k] 交换。
- 可以断定这时 [j,end) 必然是降序,逆置 [j,end),使其升序。
如果在步骤 1 找不到符合的相邻元素对,说明当前 [begin,end) 为一个降序顺序,则直接跳到步骤 4。
func nextPermutation(nums []int) {
n := len(nums)
i := n - 2
// 查找第一个相邻的顺序对(i<j,nums[i]<nums[j])
// 顺序对的左边那个数就是左半边数组的最小值
for i >= 0 && nums[i] >= nums[i + 1] {
i--
}
// 查找右半边数组的较小值
// 右半边数组一定是逆序数组
if i >= 0 {
// 从n - 1开始找右半边数组中找比nums[i]大的最小数
j := n - 1
for j >= 0 && nums[i] >= nums[j] {
j--
}
nums[i], nums[j] = nums[j], nums[i]
}
reverse(nums[i + 1:])
}
// 翻转数组
func reverse(nums []int) {
for i, n := 0, len(nums); i < n / 2; i++ {
nums[i], nums[n - i - 1] = nums[n - i - 1], nums[i]
}
}
q42 接雨水
题解
使用双指针求解。这道题的策略是要将两边尽量高的柱体向中间移,计算出外围高柱体与移动过程中碰见的低柱体的高度差。
func trap(height []int) (ans int) {
// 定义两个指针
l, r := 0, len(height)-1
// 如果数组的大小不够,直接返回
if r < 2 {
return
}
// 循环遍历高度图
for l < r {
// 策略是要将两边尽量高的柱体向中间移,计算出外围高柱体与移动过程中碰见的低柱体的高度差
// 如果左边界比右边界低,移动左指针
if height[l] < height[r] {
// 将外围的高柱体向中间移动
if height[l] > height[l + 1] {
// 统计高度
ans += height[l] - height[l + 1]
height[l + 1] = height[l]
}
l++
} else {
if height[r] > height[r - 1] {
// 统计高度
ans += height[r] - height[r - 1]
height[r - 1] = height[r]
}
r--
}
}
return
}
q76 最小覆盖子串
题解
题目要求目标子串需要覆盖t串,然后要求子串的长度最小,可以使用滑动窗口求解。如何来判断目标子串包含了t串呢,我们可以使用一个数组tArr记录下t串中的每个字母的出现次数,然后另一个数组sArr记录下s串中每个字母出现的次数。当我们遍历tArr数组的时候,如果都能满足tArr[i] <= sArr[i]
的话,那么当前子串就包含了t串。
在满足了以上的情况后,我们可以尝试着缩小滑动窗口的大小了。我们的策略是向右移动左边界,如果移动了左边界以后,子串仍然能够包含t串,那么就视为滑动窗口被成功缩小了。
func minWindow(s string, t string) string {
// tArr统计t串中字符的个数, sArr统计s串中字符的个数
sArr, tArr := make([]int, 128), make([]int, 128)
slen, tlen := len(s), len(t)
// 首先用tArr数组统计t串的字符个数
for i := 0; i < tlen; i++ {
tArr[t[i]-'A']++
}
// left,right分别表示滑动窗口左边界和右边界的下标
// length表示滑动窗口的大小
left, right, length := 0, -1, slen+1
// 遍历s数组,同时统计s串的字符个数,看看有没有将t串覆盖
// 如果s串覆盖了t串,就尝试着减小窗口的大小
for i, j := 0, 0; j < slen; j++ {
sArr[s[j]-'A']++
// 在s串覆盖t串的条件下,减小窗口的大小
for equal(sArr, tArr) {
// 更新窗口大小
if length > j-i+1 {
length = j - i + 1
left = i
right = j
}
// 尝试着剔除窗口左边界的元素
sArr[s[i]-'A']--
i++
}
}
return s[left : right+1]
}
// 判断s串有没有覆盖t串
func equal(arr1 []int, arr2 []int) bool {
for i := 0; i < 128; i++ {
if arr1[i] < arr2[i] && arr2[i] != 0 {
return false
}
}
return true
}
q121 买卖股票的最佳时机
题解
一次性遍历数组即可求解,因为最低价格必须在前面,最高价格必须在后面,所以遍历的时候,使用minValue存储前面出现过的股票的最低价格,然后与当前遍历到的股票价格相减,得到利润,使用maxValue存储最大利润即可。
func maxProfit(prices []int) int {
minValue := math.MaxInt64
maxValue := 0
for i := 0; i < len(prices); i++ {
// minValue记录股票的最低价格
if prices[i] < minValue {
minValue = prices[i]
// maxValue记录股票的最大价格差,即最大利润
} else if prices[i] - minValue > maxValue {
maxValue = prices[i] - minValue
}
}
return maxValue
}
q209 长度最小的子数组
题解
使用双指针来求解,右指针不断向后遍历,同时记录中间元素的和,当和加起来大于等于目标值,就将左指针右移来尝试得到更短的子数组。
func minSubArrayLen(target int, nums []int) (ans int) {
n := len(nums)
// 初始化最小值
ans = math.MaxInt32
slow, fast := 0, 0
// 用于累加和
sum := 0
for fast < n {
sum += nums[fast]
// 如果sum大于等于target,开始向右移动慢指针,得到更短的子数组
for sum >= target {
// 更新最小长度
ans = min(ans, fast-slow+1)
// 减去最左边的值
sum -= nums[slow]
// 右移慢指针
slow++
}
// 后移快指针
fast++
}
// 如果没有满足的子数组,就返回0
if ans == math.MaxInt32 {
return 0
}
return
}
func min(a, b int) int {
if a < b {
return a
} else {
return b
}
}
q283 移动零
题解
使用双指针解法,准备left和right指针,如果right指针遍历到的数不是0,就与left指针指向的数做交换,然后同时往后移动两个指针:
func moveZeroes(nums []int) {
left, right, n := 0, 0, len(nums)
for right < n {
if nums[right] != 0 {
nums[left], nums[right] = nums[right], nums[left]
left++
}
right++
}
}
q438 找到字符串中所有字母异位词
题解
这道题可以使用滑动窗口+哈希表的方法来解决,既然是异位词,那就不用考虑单词的顺序,只需要每种单词的数量满足就可以了。所以我们可以开两个hash表,一个用来储存模式串中每种单词的数量,另一个用来维护滑动窗口中每种单词的数量,这个滑动窗口的大小刚好是模式串的长度,最后一边遍历目标串,一边比较两个hash表即可:
golang中的数组是可以直接比较的,相同长度,相同类型的数组在比较的时候是相同的,所以我们可以使用数组来替换哈希表。
步骤:
- 比较模式串和匹配串的长度。
- 定义两个用于记录字母个数的数组:
cntP, cntS := [26]int{}, [26]int{}
。 - 从
len(p)
位置开始循环初始化匹配串中各位字母的个数。 - 遍历模式串,累加上新遍历到的字符,然后剔除滑动窗口左边的字符,再与匹配串数组做比较。
func findAnagrams(s string, p string) (ans []int) {
// 如果模式串比匹配串短,直接返回
if len(s) < len(p) {
return nil
}
// 定义用于记录字母个数的数组
cntP, cntS := [26]int{}, [26]int{}
// 首先记录匹配串中各位的字母个数
for i := 0; i < len(p); i++ {
cntP[p[i]-'a']++
cntS[s[i]-'a']++
}
if cntP == cntS {
ans = append(ans, 0)
}
// 遍历模式串
for i := len(p); i < len(s); i++ {
// 统计上当前遍历到的字符
cntS[s[i]-'a']++
// 减去滑动串口左边的字符
cntS[s[i-len(p)]-'a']--
if cntS == cntP {
ans = append(ans, i-len(p)+1)
}
}
return
}
q6230 长度为 K 子数组中的最大和
题解
这道题需要借助于哈希表,哈希表用于判断窗口中的值是否重复。
步骤:
- 构建长度为k-1的窗口,维护哈希表+累加sum值。
- 从k-1下标开始遍历数组,首先维护哈希表+累加sum值,然后更新最大值,最后剔除窗口最左边的值。
func maximumSubarraySum(nums []int, k int) int64 {
// 定义用于临时累加的变量和最大值变量
ans, sum := 0, 0
// 定义哈希表
hashTable := make(map[int]int)
// 初始化哈希表
for _, v := range nums[:k-1] {
hashTable[v]++
sum += v
}
// 从k - 1开始遍历数组
for i := k - 1; i < len(nums); i++ {
// 累加sum 和 维护哈希表
sum += nums[i]
hashTable[nums[i]]++
// 更新最大值
if len(hashTable) == k && sum > ans {
ans = sum
}
// 剔除最左边的值
x := nums[i-k+1]
hashTable[x]--
// 及时清楚0
if hashTable[x] == 0 {
delete(hashTable, x)
}
sum -= x
}
return int64(ans)
}
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
题解
这道题使用双指针遍历,定义两个指针分别指向数组的两端,左指针向右遍历,右指针向左遍历,将数组左边的偶数与数组右边的奇数做交换。
func exchange(nums []int) []int {
// 定义双指针
left, right := 0, len(nums)-1
for left < right {
// 遍历左半边数组,奇数就向后移动
for left < right && nums[left]%2 != 0 {
left++
}
// 遍历右半边数组,偶数就前后移动
for left < right && nums[right]%2 == 0 {
right--
}
nums[left], nums[right] = nums[right], nums[left]
}
return nums
}
剑指 Offer 48. 最长不含重复字符的子字符串
题解
这道题与 q3无重复字符的最长子串 相同。
func lengthOfLongestSubstring(s string) (maxLength int) {
l := 0
cache := make([]int, 128)
for i, _ := range s {
l = max(l, cache[s[i]])
maxLength = max(maxLength, i-l+1)
cache[s[i]] = i + 1
}
return
}
func max(a, b int) int {
if a > b {
return a
} else {
return b
}
}
剑指 Offer 57 - II. 和为s的连续正数序列
题解
使用双指针解法,定义两个指针,每次循环分别统计两个指针直接的元素和,如果和大于目标值就将左指针向后移动,表示减少一位数字,小于目标值就将右指针向后移动一位,表示增加一位数字,如果等于目标值就循环遍历指针之间的数构造结果集。
其中两个指针之间的元素和我们采用等差数列之和的求法:(首项+尾项)* 项数 / 2.
func findContinuousSequence(target int) (res [][]int) {
l, r := 1, 2
for l < r {
// 等差数列求和
sum := (l + r) * (r - l + 1) / 2
if sum == target {
var row []int
for i := l; i <= r; i++ {
row = append(row, i)
}
res = append(res, row)
l++
} else if sum > target {
// 减少一位数字
l++
} else {
// 增加一位数字
r++
}
}
return
}
剑指 Offer 63. 股票的最大利润
题解
这道题的解法与买卖股票的最佳时机一样。
func maxProfit(prices []int) int {
minValue, maxValue := math.MaxInt32, math.MinInt32
for _, v := range prices {
if v < minValue {
minValue = v
} else if v-minValue > maxValue {
maxValue = v - minValue
}
}
return maxValue
}