算法 - (数组)
数组、链表是编程语言中最常见的数据结构,也是最基础的数据结构。以下会以几道 LeetCode 巩固自己的基础
No.1 两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
func twoSum(nums []int, target int) []int {
tmp := make(map[int]int, len(nums)/2)
for i := 0;i < len(nums);i++ {
if index, ok := tmp[target-nums[i]]; ok {
return []int{index,i}
}
tmp[nums[i]] = i
}
return []int{}
}
No.3 无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。 示例1:输入: s = “abcabcbb” 输出: 3 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
首先需要明确, 无重复字符串是什么意思。 awwke , 最长无重复子串是 wke ,因为 ww 重复了!!
func lengthOfLongestSubstring(s string) int {
if len(s) == 0 {
return 0
}
checkFunc := func(sub string) bool {
m := make(map[byte]struct{})
for i := 0 ; i < len(sub); i++ {
if _, ok := m[sub[i]]; ok {
return false
} else {
m[sub[i]] = struct{}{}
}
}
return true
}
maxLength := 1
for i := 0; i < len(s); i++ {
for j := i; j < len(s); j ++ {
subStr := s[i:j+1]
if checkFunc(subStr) {
if len(subStr) > maxLength {
maxLength = len(subStr)
}
}
}
}
return maxLength
}
看了一下官方题解,原来是要搞个滑动窗口。
- 首先,用一个map表示滑动窗口。
- 不断移动右指针,当出现某个字符不为0时,证明子串里有重复了(map 里面已经存过了)。
- 重复的肯定是左指针指向的值,所以删除左指针所指的值,保持滑动窗口向前滑动。
- 直到计算出最大子串的值
func lengthOfLongestSubstring(s string) int {
right := 0
// 用于存当前窗口中都有哪些字符,当map的value不为1时,表示需要滑动了
storeMap := make(map[byte]int)
max := 0
for left := 0; left < len(s); left++ {
// 左指针向前移动一下,消除窗口中的重复值
if left != 0 {
delete(storeMap, s[left-1])
}
for ; right < len(s); right++ {
// 为0表示是新元素
if storeMap[s[right]] == 0 {
storeMap[s[right]]++
} else {
break
}
}
// 窗口中存在重复字符串了(或者遍历完了数组), 且肯定出现的重复字符是最早加入的(left所指的值)
if max < right-left {
max = right - left
}
}
return max
}
No.4 寻找两个正序数组的中位数
链接:https://leetcode.cn/problems/median-of-two-sorted-arrays/
思路:使用二分查找在较短数组上找分割点,使得左半部分最大值≤右半部分最小值。通过调整分割点位置保证平衡分割,最终根据总长度奇偶性计算中位数。时间复杂度O(log(min(m,n)))。
给定两个大小分别为
m
和n
的正序(从小到大)数组nums1
和nums2
。请你找出并返回这两个正序数组的 中位数 。算法的时间复杂度应该为
O(log (m+n))
。示例 1:
输入:nums1 = [1,3], nums2 = [2] 输出:2.00000 解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4] 输出:2.50000 解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {
// 确保nums1是较短的数组
if len(nums1) > len(nums2) {
nums1, nums2 = nums2, nums1
}
m, n := len(nums1), len(nums2)
left, right := 0, m
for left <= right {
// nums1的分割点
i := (left + right) / 2
// nums2的分割点,保证左半部分元素总数 = (m+n+1)/2
j := (m+n+1)/2 - i
// 边界值处理
maxLeft1 := -1<<31 // 负无穷
if i > 0 {
maxLeft1 = nums1[i-1]
}
minRight1 := 1<<31 - 1 // 正无穷
if i < m {
minRight1 = nums1[i]
}
maxLeft2 := -1<<31 // 负无穷
if j > 0 {
maxLeft2 = nums2[j-1]
}
minRight2 := 1<<31 - 1 // 正无穷
if j < n {
minRight2 = nums2[j]
}
// 检查分割是否正确
if maxLeft1 <= minRight2 && maxLeft2 <= minRight1 {
// 找到正确的分割
if (m+n)%2 == 0 {
// 总长度为偶数
return float64(max(maxLeft1, maxLeft2)+min(minRight1, minRight2)) / 2.0
} else {
// 总长度为奇数
return float64(max(maxLeft1, maxLeft2))
}
} else if maxLeft1 > minRight2 {
// nums1分割点太靠右
right = i - 1
} else {
// nums1分割点太靠左
left = i + 1
}
}
return 0.0
}
No.5 最长回文子串
- 链接:https://leetcode.cn/problems/longest-palindromic-substring/
- 思路:这道题就是一个长串里找回文子串,使用中心扩展法。
给你一个字符串 s,找到 s 中最长的 回文 子串。 示例 1:
输入:s = “babad” 输出:“bab” 解释:“aba” 同样是符合题意的答案。 示例 2:
输入:s = “cbbd” 输出:“bb”
func longestPalindrome(s string) string {
if len(s) <= 1 {
return s
}
longestStr := ""
for i := 0; i < len(s); i++ {
str1 := helper(s, i, i+1) // 考虑 abba 形式
str2 := helper(s, i, i) // 考虑 aba 形式
if len(longestStr) < len(str1) {
longestStr = str1
}
if len(longestStr) < len(str2) {
longestStr = str2
}
}
return longestStr
}
// 查找最长子串
func helper(s string, left, right int) string {
var str string
for ; left >= 0 && right <= len(s)-1 && s[left] == s[right]; left, right = left-1, right+1 {
str = s[left : right+1]
}
return str
}
No.11 盛最多水的容器
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。
- 数组从两边开始向中间凑。选出一个最大的面积。
func maxArea(height []int) int {
maxV := 0
left, right := 0, len(height)-1
for left < right {
if height[left] > height[right] {
maxV = max((right-left) * height[right], maxV)
// 左边更高,右边左移
right --
} else {
maxV = max((right-left) * height[left], maxV)
// 右边更高,左边右移动
left ++
}
}
return maxV
}
No.15 三数之和
- 链接:https://leetcode.cn/problems/3sum/
- 思路:先将数组排序,再2层循环。如[-1,0,1,2,-1,-4 ] 先排序,排序之后 [-4, -1, -1, 0, 1, 2] 数组,再进行一次双重循环,第一次取 -4 ,再对 -4 之后的数组进行一次循环,-4 + 数组头元素 + 数组尾元素 > 0 就证明,数组尾元素过大, < 0 就证明数组头元素太小。逐渐的收拢第二个数组。
func threeSum(nums []int) [][]int {
sort.Ints(nums)
validNums := make(map[[3]int]struct{})
for i := 0; i < len(nums)-2; i++ {
minNum := nums[i]
midNumIndex := i + 1
maxNumIndex := len(nums) - 1
// 中间索引 小于 最大索引
for midNumIndex < maxNumIndex {
midNum := nums[midNumIndex]
maxNum := nums[maxNumIndex]
// 大于0了,证明最大的数太大了
if minNum+midNum+maxNum > 0 {
maxNumIndex--
continue
}
// 小于0了,证明中间数太小了
if minNum+midNum+maxNum < 0 {
midNumIndex++
continue
}
// 只能是等于 0 了, 中间数加点,最大数减点,看看是否还能拼出0
if minNum+midNum+maxNum == 0 {
validNums[[3]int{minNum, midNum, maxNum}] = struct{}{}
midNumIndex++
maxNumIndex--
}
}
}
var res [][]int
for k, _ := range validNums {
res = append(res, []int{k[0], k[1], k[2]})
}
return res
}
No.17 电话号码的数字组合
- 链接: https://leetcode.cn/problems/letter-combinations-of-a-phone-number/
- 思路:使用回溯算法(backtracking)
给定一个仅包含数字
2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
func letterCombinations(digits string) []string {
if len(digits) == 0 {
return []string{}
}
mapping := map[byte]string{
'2': "abc", '3': "def", '4': "ghi", '5': "jkl",
'6': "mno", '7': "pqrs", '8': "tuv", '9': "wxyz",
}
var result []string
var backtrack func(index int, path string)
backtrack = func(index int, path string) {
if index == len(digits) {
result = append(result, path)
return
}
for _, char := range mapping[digits[index]] {
backtrack(index+1, path+string(char))
}
}
backtrack(0, "")
return result
}
No.20 有效的括号
- 链接: https://leetcode.cn/problems/valid-parentheses/description/
- 思路: 使用栈数据结构:遇到左括号入栈,遇到右括号检查栈顶是否匹配并出栈,最后栈为空则有效。时间复杂度O(n),空间复杂度O(n)。
给定一个只包括
'('
,')'
,'{'
,'}'
,'['
,']'
的字符串s
,判断字符串是否有效。有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
package main
import "fmt"
func isValid(s string) bool {
stack := []rune{}
mapping := map[rune]rune{
')': '(',
'}': '{',
']': '[',
}
for _, char := range s {
if char == '(' || char == '{' || char == '[' {
// 左括号入栈
stack = append(stack, char)
} else {
// 右括号
if len(stack) == 0 {
return false
}
// 检查栈顶是否匹配
if stack[len(stack)-1] != mapping[char] {
return false
}
// 匹配成功,出栈
stack = stack[:len(stack)-1]
}
}
// 栈为空说明所有括号都匹配
return len(stack) == 0
}
No.26 删除排序数组中的重复项
func removeDuplicates(nums []int) int {
if len(nums) == 0 {
return 0
}
slow := 1
for fast := 1; fast < len(nums); fast++ {
if nums[fast] != nums[fast-1] {
nums[slow] = nums[fast]
slow++
}
}
return slow
}
思路2: 旋转数组法 旋转数组应该是我比较喜欢的一种方式,比较简单,那就是数组翻转2次,可以将首位数转到末尾曲,重复项判断刚好使用这种方式,对于移动数组类的题是比较通吃的一个方法问题就是时间复杂度比较大
func reversal(nums []int) {
length := len(nums)
for i := 0; length > 1 && i < length/2; i ++ {
nums[i],nums[length-1-i] = nums[length-1-i],nums[i]
}
}
func removeDuplicates(nums []int) int {
length := len(nums)
for i := 0; i < length; i ++ {
if i + 1 < length && nums[i] == nums[i+1] {
reversal(nums[i+1:length])
reversal(nums[i+1:length-1])
length = length - 1
i = i - 1
}
}
return length
}
No.27 移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。
func removeElement(nums []int, val int) int {
slow := 0
for fast := 0; fast < len(nums); fast++ {
if nums[fast] != val {
nums[slow] = nums[fast]
slow++
}
}
return slow
}
No.29 顺时针打印矩阵
螺旋遍历:从左上角开始,按照 向右、向下、向左、向上 的顺序 依次 提取元素,然后再进入内部一层重复相同的步骤,直到提取完所有元素。 示例 1:
输入:array = [[1,2,3],[8,9,4],[7,6,5]] 输出:[1,2,3,4,5,6,7,8,9]

func spiralOrder(matrix [][]int) []int {
if len(matrix) == 0 || len(matrix[0]) == 0 {
return []int{}
}
var (
rows, columns = len(matrix), len(matrix[0])
order = make([]int, rows * columns)
index = 0
left, right, top, bottom = 0, columns -1 , 0 , rows -1
)
for left <= right && top <= bottom {
// 打印最上层(包含top行最后一个)
for col := left; col <= right; col ++ {
order[index] = matrix[top][col]
index++
}
// 打印最右边(包含最右列最后一个)
for row := top + 1; row <= bottom; row ++ {
order[index] = matrix[row][right]
index++
}
//
if left < right && top < bottom {
// 打印最下边(不包含bottom行最后一个和第一个)
for col := right - 1; col > left; col -- {
order[index] = matrix[bottom][col]
index++
}
// 打印最左边(不包含left列最后一个)
for row := bottom; row > top; row -- {
order[index] = matrix[row][left]
index++
}
}
}
left ++
right --
top ++
bottom --
return order
}
No.34 在排序数组中查找元素的第一个和最后一个
链接: https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/
思路:先找到任意一个target位置,然后向两边扩展
在target连续范围很大时可能退化为O(n),但在实际应用中往往表现良好
平均时间复杂度O(log n),最坏O(n)
给你一个按照非递减顺序排列的整数数组
nums
,和一个目标值target
。请你找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值
target
,返回[-1, -1]
。你必须设计并实现时间复杂度为
O(log n)
的算法解决此问题。示例 1:
输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]
//一次二分查找 + 扩展
func searchRangeExpand(nums []int, target int) []int {
if len(nums) == 0 {
return []int{-1, -1}
}
// 先用标准二分查找找到任意一个target的位置
pos := binarySearch(nums, target)
if pos == -1 {
return []int{-1, -1}
}
// 从找到的位置向两边扩展
left, right := pos, pos
// 向左扩展
for left > 0 && nums[left-1] == target {
left--
}
// 向右扩展
for right < len(nums)-1 && nums[right+1] == target {
right++
}
return []int{left, right}
}
func binarySearch(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right-left)/2
if nums[mid] == target {
return mid
} else if nums[mid] < target {
left = mid + 1
} else {
right = mid - 1
}
}
return -1
}
No.39 组合总和
思路:这个 Go 语言实现使用了回溯算法。
核心思想: 回溯算法通过深度优先搜索的方
给你一个 无重复元素 的整数数组
candidates
和一个目标整数target
,找出candidates
中可以使数字和为目标数target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。对于给定的输入,保证和为
target
的不同组合数少于150
个。
func combinationSum(candidates []int, target int) [][]int {
result := [][]int{}
current := []int{}
var backtrack func(start int, remain int)
backtrack = func(start int, remain int) {
if remain < 0 {
return
}
if remain == 0 {
temp := make([]int, len(current))
copy(temp, current)
result = append(result, temp)
return
}
for i := start; i < len(candidates); i++ {
// 选择
current = append(current, candidates[i])
// 递归
backtrack(i, remain-candidates[i])
// 回溯
current = current[:len(current)-1]
}
}
backtrack(0, target)
return result
}
No.42 接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
func trap(height []int) int {
if len(height) <= 2 {
return 0
}
// 初始化左右指针
left, right := 0, len(height)-1
// 初始化左右最大高度
leftMax, rightMax := 0, 0
// 初始化雨水总量
totalWater := 0
for left < right {
// 更新左边的最大高度
if height[left] > leftMax {
leftMax = height[left]
}
if height[right] > rightMax {
rightMax = height[right]
}
// 根据短板决定处理那一侧
if leftMax <= rightMax {
// 左边是短板,计算左边的雨水量
totalWater += leftMax - height[left]
left ++
} else {
// 右边是短板,计算右边的雨水量
totalWater += rightMax - height[right]
right --
}
}
return totalWater
}
No.46 全排列
给定一个不含重复数字的整数数组 nums ,返回其 所有可能的全排列 。可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
这道题用面向对象的方法比较容易理解 回溯法
type PermState struct {
nums []int // 输入数组
currentPerm []int // 当前正在构建的排列
usedNums map[int]bool // 标记哪些数字在使用
allPerms [][]int // 存储所有完成的排列
}
func NewPermState(nums []int) *PermState {
return &PermState{
nums: nums,
currentPerm: []int{},
usedNums: make(map[int]bool),
allPerms: make([][]int, 0),
}
}
func (ps *PermState) Add(i int) {
ps.currentPerm = append(ps.currentPerm, ps.nums[i])
ps.usedNums[i] = true
}
func (ps *PermState) Remove(i int) {
ps.currentPerm = ps.currentPerm[:len(ps.currentPerm)-1]
ps.usedNums[i] = false
}
func (ps *PermState) CanUse(i int) bool {
return !ps.usedNums[i]
}
func (ps *PermState) IsComplete() bool {
return len(ps.currentPerm) == len(ps.nums)
}
func (ps *PermState) Save() {
temp := make([]int, len(ps.currentPerm))
copy(temp, ps.currentPerm)
ps.allPerms = append(ps.allPerms, temp)
}
func (ps *PermState) BuildPerm() {
if ps.IsComplete() {
ps.Save()
return
}
for i := 0; i < len(ps.nums); i ++ {
if ps.CanUse(i) {
ps.Add(i)
ps.BuildPerm()
ps.Remove(i)
}
}
}
func permute(nums []int) [][]int {
ps := NewPermState(nums)
ps.BuildPerm()
return ps.allPerms
}
No.48 旋转图像
- 链接:https://leetcode.cn/problems/rotate-image/description/
- 思路:转置+反转行
- 先沿主对角线转置矩阵
- 再反转每一行
- 时间复杂度O(n²),空间复杂度O(1)
给定一个 n × n 的二维矩阵
matrix
表示一个图像。请你将图像顺时针旋转 90 度。你必须在** 原地** 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
func rotate(matrix [][]int) {
n := len(matrix)
// 第一步:转置矩阵 (沿主对角线翻转)
for i := 0; i < n; i++ {
for j := i + 1; j < n; j++ {
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
}
}
// 第二步:反转每一行
for i := 0; i < n; i++ {
left, right := 0, n-1
for left < right {
matrix[i][left], matrix[i][right] = matrix[i][right], matrix[i][left]
left++
right--
}
}
}
No.49 字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
输入: strs = [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]
输出: [[“bat”],[“nat”,“tan”],[“ate”,“eat”,“tea”]]
解释:
- 在 strs 中没有字符串可以通过重新排列来形成
"bat"
。- 字符串
"nat"
和"tan"
是字母异位词,因为它们可以重新排列以形成彼此。- 字符串
"ate"
,"eat"
和"tea"
是字母异位词,因为它们可以重新排列以形成彼此。
func groupAnagrams(strs []string) [][]string {
anagramMap := make(map[string][]string)
for _, str := range strs {
// 将字符串转换为 rune 切片并排序
sorted := sortString(str)
// 按排序后的字符串作为 key 存入 map
anagramMap[sorted] = append(anagramMap[sorted], str)
}
// 收集结果
result := make([][]string, 0, len(anagramMap))
for _, group := range anagramMap {
result = append(result, group)
}
return result
}
// sortString 对字符串按字典序排序
func sortString(s string) string {
runes := strings.Split(s, "")
sort.Strings(runes)
return strings.Join(runes, "")
}
No.56 合并区间
输入:intervals = [[1,3],[2,6],[8,10],[15,18]] 输出:[[1,6],[8,10],[15,18]] 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
// merge 合并重叠的区间
func merge(intervals [][]int) [][]int {
// 边界情况处理
if len(intervals) <= 1 {
return intervals
}
// 按照区间起始位置排序
sort.Slice(intervals, func(i, j int) bool {
return intervals[i][0] < intervals[j][0]
})
// 初始化结果数组,将第一个区间加入
result := [][]int{intervals[0]}
// 遍历剩余区间
for i := 1; i < len(intervals); i++ {
current := intervals[i]
last := result[len(result)-1]
// 如果当前区间与最后一个合并区间重叠
if current[0] <= last[1] {
// 合并区间:更新结束位置为两者的最大值
last[1] = max(last[1], current[1])
} else {
// 不重叠,直接添加当前区间
result = append(result, current)
}
}
return result
}
No.76 最小覆盖子串
- 链接: https://leetcode.cn/problems/minimum-window-substring/
- 思路:滑动窗口 + 哈希表(经典解法)
- 使用两个哈希表分别记录需要的字符和窗口内的字符
- 右指针扩展窗口,左指针收缩窗口
- 时间复杂度O(|s|+|t|),空间复杂度O(|s|+|t|)
给你一个字符串
s
、一个字符串t
。返回s
中涵盖t
所有字符的最小子串。如果s
中不存在涵盖t
所有字符的子串,则返回空字符串""
。注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。- 如果
s
中存在这样的子串,我们保证它是唯一的答案。示例 1:
输入:s = "ADOBECODEBANC", t = "ABC" 输出:"BANC" 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a" 输出:"a" 解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa" 输出: "" 解释: t 中两个字符 'a' 均应包含在 s 的子串中, 因此没有符合条件的子字符串,返回空字符串。
// 滑动窗口 + 哈希表
func minWindow(s string, t string) string {
if len(s) == 0 || len(t) == 0 || len(s) < len(t) {
return ""
}
// 统计t中每个字符需要的数量
need := make(map[byte]int)
for i := 0; i < len(t); i++ {
need[t[i]]++
}
// 窗口中字符的数量
window := make(map[byte]int)
left, right := 0, 0
valid := 0 // 窗口中满足need条件的字符个数
// 记录最小覆盖子串的起始索引及长度
start, length := 0, len(s)+1
for right < len(s) {
// 扩大窗口
c := s[right]
right++
// 进行窗口内数据的一系列更新
if _, exists := need[c]; exists {
window[c]++
if window[c] == need[c] {
valid++
}
}
// 判断左侧窗口是否要收缩
for valid == len(need) {
// 更新最小覆盖子串
if right-left < length {
start = left
length = right - left
}
// 缩小窗口
d := s[left]
left++
// 进行窗口内数据的一系列更新
if _, exists := need[d]; exists {
if window[d] == need[d] {
valid--
}
window[d]--
}
}
}
if length == len(s)+1 {
return ""
}
return s[start : start+length]
}
No.78 子集
思路:该 Go 语言实现使用了回溯算法。
给你一个整数数组
nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0] 输出:[[],[0]]
func subsets(nums []int) [][]int {
result := [][]int{}
current := []int{}
var backtrack func(start int)
backtrack = func(start int) {
// 将当前子集添加到结果中
temp := make([]int, len(current))
copy(temp, current)
result = append(result, temp)
// 递归生成所有子集
for i := start; i < len(nums); i++ {
// 选择当前元素
current = append(current, nums[i])
// 递归生成以当前元素为基础的子集
backtrack(i + 1)
// 回溯,撤销选择
current = current[:len(current)-1]
}
}
backtrack(0)
return result
}
No.79 单词搜索
- 链接:https://leetcode.cn/problems/word-search/
- 思路:使用回溯算法(Backtracking)来解决这个问题。回溯算法是一种通过递归来探索所有可能路径的搜索方法。
给定一个
m x n
二维字符网格board
和一个字符串单词word
。如果word
存在于网格中,返回true
;否则,返回false
。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

func exist(board [][]byte, word string) bool {
m := len(board)
if m == 0 {
return false
}
n := len(board[0])
if n == 0 {
return false
}
for i := 0; i < m; i++ {
for j := 0; j < n; j++ {
if backtrack(board, word, i, j, 0) {
return true
}
}
}
return false
}
func backtrack(board [][]byte, word string, row, col, k int) bool {
// 边界条件检查,如果索引越界或字符不匹配,返回 false
if row < 0 || row >= len(board) || col < 0 || col >= len(board[0]) || board[row][col] != word[k] {
return false
}
// 如果所有字符都匹配完毕,说明找到了完整的单词
if k == len(word)-1 {
return true
}
// 临时标记当前单元格,防止重复使用
temp := board[row][col]
board[row][col] = '#'
// 递归搜索四个相邻方向
// 上
if backtrack(board, word, row-1, col, k+1) {
return true
}
// 下
if backtrack(board, word, row+1, col, k+1) {
return true
}
// 左
if backtrack(board, word, row, col-1, k+1) {
return true
}
// 右
if backtrack(board, word, row, col+1, k+1) {
return true
}
// 回溯:恢复当前单元格
board[row][col] = temp
return false
}
No.106 多数元素
- 链接:https://leetcode.cn/problems/majority-element/
- 思路:摩尔投票算法
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3]
输出:3
func majorityElement(nums []int) int {
if len(nums) == 0 {
return 0
}
if len(nums) == 1 {
return nums[0]
}
major := nums[0]
count := 1
for i := 1; i < len(nums); i++ {
if nums[i] == major {
count++
} else {
count--
}
if count < 0 {
count = 1
major = nums[i]
}
}
count = 0
for i := 0; i < len(nums); i++ {
if major == nums[i] {
count += 1
}
}
if count >= len(nums)/2 {
return major
}
return 0
}
No.128 最长连续序列
- 链接: https://leetcode.cn/problems/longest-consecutive-sequence/
- 思路:使用哈希表存储所有数字,只从连续序列的起始点(即不存在num-1的数字)开始计算长度,避免重复计算。时间复杂度O(n),空间复杂度O(n)。
给定一个未排序的整数数组
nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。请你设计并实现时间复杂度为
O(n)
的算法解决此问题。输入:nums = [100,4,200,1,3,2] 输出:4 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
func longestConsecutive(nums []int) int {
if len(nums) == 0 {
return 0
}
// 使用map模拟set,存储所有数字
numSet := make(map[int]bool)
for _, num := range nums {
numSet[num] = true
}
maxLength := 0
// 遍历每个数字
for num := range numSet {
// 只从连续序列的起始点开始计算
if !numSet[num-1] {
currentNum := num
currentLength := 1
// 向右延伸,找连续的数字
for numSet[currentNum+1] {
currentNum++
currentLength++
}
// 更新最大长度
if currentLength > maxLength {
maxLength = currentLength
}
}
}
return maxLength
}
No.136 只出现一次的数字
- 链接: https://leetcode.cn/problems/single-number/
- 思路: 这个 Go 语言实现利用了异或运算(
^
)的特性:- 任何数和 0 异或,结果仍然是它自己:
a ^ 0 = a
。 - 任何数和它自己异或,结果是 0:
a ^ a = 0
。 - 异或运算满足交换律和结合律:
a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b
。
- 任何数和 0 异或,结果仍然是它自己:
给你一个 非空 整数数组
nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
**输入:**nums = [2,2,1]
**输出:**1
func singleNumber(nums []int) int {
result := 0
for _, num := range nums {
result ^= num
}
return result
}
No.146 LRU 缓存
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现
LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
,则应该 逐出 最久未使用的关键字。函数
get
和put
必须以O(1)
的平均时间复杂度运行。
// 双向链表节点
type Node struct {
key, value int
prev, next *Node
}
type LRUCache struct {
capacity int
cache map[int]*Node
head *Node // 虚拟头节点
tail *Node // 虚拟尾节点
}
func Constructor(capacity int) LRUCache {
lru := LRUCache{
capacity: capacity,
cache: make(map[int]*Node),
head: &Node{},
tail: &Node{},
}
// 初始化双向链表
lru.head.next = lru.tail
lru.tail.prev = lru.head
return lru
}
// 添加节点到头部
func (lru *LRUCache) addToHead(node *Node) {
node.prev = lru.head
node.next = lru.head.next
lru.head.next.prev = node
lru.head.next = node
}
// 移除节点
func (lru *LRUCache) removeNode(node *Node) {
node.prev.next = node.next
node.next.prev = node.prev
}
// 移动节点到头部
func (lru *LRUCache) moveToHead(node *Node) {
lru.removeNode(node)
lru.addToHead(node)
}
// 移除尾部节点
func (lru *LRUCache) removeTail() *Node {
lastNode := lru.tail.prev
lru.removeNode(lastNode)
return lastNode
}
func (lru *LRUCache) Get(key int) int {
if node, exists := lru.cache[key]; exists {
// 移动到头部表示最近使用
lru.moveToHead(node)
return node.value
}
return -1
}
func (lru *LRUCache) Put(key int, value int) {
if node, exists := lru.cache[key]; exists {
// 更新值并移动到头部
node.value = value
lru.moveToHead(node)
} else {
newNode := &Node{key: key, value: value}
if len(lru.cache) >= lru.capacity {
// 超出容量,删除尾部节点
tail := lru.removeTail()
delete(lru.cache, tail.key)
}
// 添加新节点到头部
lru.cache[key] = newNode
lru.addToHead(newNode)
}
}
No.169 多数元素
- 链接:https://leetcode.cn/problems/majority-element/
- 思路:Boyer-Moore投票算法(推荐)
- 核心思想:多数元素的出现次数大于其他所有元素出现次数之和
- 维护候选人和计数器,相同+1,不同-1,计数器为0时更换候选人
- 时间复杂度O(n),空间复杂度O(1)
给定一个大小为
n
的数组nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于⌊ n/2 ⌋
的元素。你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3] 输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2] 输出:2
func majorityElement(nums []int) int {
candidate := 0
count := 0
// 第一遍:找候选人
for _, num := range nums {
if count == 0 {
candidate = num
}
if num == candidate {
count++
} else {
count--
}
}
// 由于题目保证存在多数元素,这里可以直接返回candidate
// 如果不保证,需要第二遍验证
return candidate
}
No.200 岛屿数量
给你一个由
'1'
(陆地)和'0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ] 输出:1
func numIslands(grid [][]byte) int {
if len(grid) == 0 || len(grid[0]) == 0 {
return 0
}
rows, cols := len(grid), len(grid[0])
count := 0
// DFS函数,将相连的陆地标记为已访问
var dfs func(int, int)
dfs = func(i, j int) {
// 边界检查和水域检查
if i < 0 || i >= rows || j < 0 || j >= cols || grid[i][j] == '0' {
return
}
// 标记为已访问
grid[i][j] = '0'
// 递归访问四个方向
dfs(i-1, j) // 上
dfs(i+1, j) // 下
dfs(i, j-1) // 左
dfs(i, j+1) // 右
}
// 遍历整个网格
for i := 0; i < rows; i++ {
for j := 0; j < cols; j++ {
if grid[i][j] == '1' {
count++
dfs(i, j) // 将整个岛屿标记为已访问
}
}
}
return count
}
No.209 长度最小的子数组
- 链接:https://leetcode.cn/problems/minimum-size-subarray-sum/description/
- 思路:其题意是在数组中找到满足 连续 、子数组和大于target和 数量最少 三个条件的子数组,由连续就能快速联想并对应到滑动窗口的概念。
func minSubArrayLen(target int, nums []int) int {
ri := 0 // 右指针
minLength := math.MaxInt
if len(nums) == 0 {
return 0
}
curSize := 0
// 移动左指针
for i := 0 ; i < len(nums); i ++ {
if i != 0 {
curSize -= nums[i-1]
}
// 移动右指针。当前Size >= target就退出
for ri < len(nums) && nums[ri] + curSize < target {
curSize += nums[ri]
ri ++
}
// 是我们想要的退出时机,而不是ri遍历完了
if ri < len(nums) && curSize + nums[ri] >= target {
if ri + 1 - i < minLength {
minLength = ri + 1 -i
}
}
}
if minLength == math.MaxInt {
return 0
}
return minLength
}
第二种写法,也是标准的滑动窗口。
func minSubArrayLen(target int, nums []int) int {
var count int = math.MaxInt64
var targetList []int
var right int
for left := 0; left < len(nums); left++ {
// 持续移动窗口的左边,直到窗口中的数组之和不满足和大于target
for len(targetList) > 0 {
// 找到最小
if sum(targetList) >= target && len(targetList) <= count {
count = len(targetList)
} else {
break
}
// 逐渐左移下标
targetList = targetList[1:]
}
// 移动滑动窗口的右边界
for ; right < len(nums); right++ {
targetList = append(targetList, nums[right])
if sum(targetList) < target {
continue
} else {
// 能到这里表示滑动窗口里的值是满足要求的,右指针再往右一格
if len(targetList) <= count {
count = len(targetList)
}
right++
break
}
}
}
if count == math.MaxInt64 {
return 0
}
return count
}
func sum(list []int) int {
all := 0
for _, v := range list {
all += v
}
return all
}
No.215 数组中的第K个最大元素
链接: https://leetcode.cn/problems/kth-largest-element-in-an-array/description/
思路:
- 提供两种解法:
- 快速选择算法:基于快排的分区思想,平均时间复杂度O(n),最坏O(n²)但通过随机化pivot可避免
- 小根堆:维护大小为k的小根堆,时间复杂度O(nlogk),空间复杂度O(k)
快速选择是最优解,将找第k大元素转换为找第(n-k)小元素,通过分区操作逐步缩小搜索范围。
给定整数数组
nums
和整数k
,请返回数组中第**k**
个最大的元素。请注意,你需要找的是数组排序后的第
k
个最大的元素,而不是第k
个不同的元素。你必须设计并实现时间复杂度为
O(n)
的算法解决此问题。示例 1:
输入: [3,2,1,5,6,4], k = 2 输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4 输出: 4
func findKthLargest(nums []int, k int) int {
// 将问题转换为找第 len(nums) - k 小的元素
return quickSelect(nums, 0, len(nums)-1, len(nums)-k)
}
func quickSelect(nums []int, left, right, k int) int {
if left == right {
return nums[left]
}
// 随机选择pivot避免最坏情况
rand.Seed(time.Now().UnixNano())
pivotIndex := left + rand.Intn(right-left+1)
// 分区操作
pivotIndex = partition(nums, left, right, pivotIndex)
if k == pivotIndex {
return nums[k]
} else if k < pivotIndex {
return quickSelect(nums, left, pivotIndex-1, k)
} else {
return quickSelect(nums, pivotIndex+1, right, k)
}
}
func partition(nums []int, left, right, pivotIndex int) int {
pivotValue := nums[pivotIndex]
// 将pivot移到末尾
nums[pivotIndex], nums[right] = nums[right], nums[pivotIndex]
storeIndex := left
// 将小于pivot的元素移到左边
for i := left; i < right; i++ {
if nums[i] < pivotValue {
nums[storeIndex], nums[i] = nums[i], nums[storeIndex]
storeIndex++
}
}
// 将pivot放到正确位置
nums[right], nums[storeIndex] = nums[storeIndex], nums[right]
return storeIndex
}
// 使用小根堆的解法(时间复杂度O(nlogk))
func findKthLargestHeap(nums []int, k int) int {
// 维护大小为k的小根堆
heap := make([]int, 0, k)
for _, num := range nums {
if len(heap) < k {
heap = append(heap, num)
heapifyUp(heap, len(heap)-1)
} else if num > heap[0] {
heap[0] = num
heapifyDown(heap, 0)
}
}
return heap[0]
}
func heapifyUp(heap []int, i int) {
for i > 0 {
parent := (i - 1) / 2
if heap[i] >= heap[parent] {
break
}
heap[i], heap[parent] = heap[parent], heap[i]
i = parent
}
}
func heapifyDown(heap []int, i int) {
n := len(heap)
for {
smallest := i
left, right := 2*i+1, 2*i+2
if left < n && heap[left] < heap[smallest] {
smallest = left
}
if right < n && heap[right] < heap[smallest] {
smallest = right
}
if smallest == i {
break
}
heap[i], heap[smallest] = heap[smallest], heap[i]
i = smallest
}
}
No.239 滑动窗口最大值
链接:https://leetcode.cn/problems/sliding-window-maximum/description/
思路:使用单调递减双端队列维护滑动窗口最大值。队列存储数组下标,队首始终是当前窗口的最大值下标。移除窗口外元素和队尾较小元素,保持单调性。时间复杂度O(n),空间复杂度O(k)。
给你一个整数数组
nums
,有一个大小为k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的k
个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3 输出:[3,3,5,5,6,7] 解释: 滑动窗口的位置 最大值 --------------- ----- [1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7
func maxSlidingWindow(nums []int, k int) []int {
if len(nums) == 0 || k == 0 {
return []int{}
}
result := make([]int, 0, len(nums)-k+1)
// 双端队列,存储数组下标
deque := make([]int, 0)
for i := 0; i < len(nums); i++ {
// 移除窗口外的元素
for len(deque) > 0 && deque[0] <= i-k {
deque = deque[1:]
}
// 维持单调递减队列:移除队尾小于当前元素的下标
for len(deque) > 0 && nums[deque[len(deque)-1]] < nums[i] {
deque = deque[:len(deque)-1]
}
// 当前元素下标入队
deque = append(deque, i)
// 窗口形成后,队首就是当前窗口的最大值
if i >= k-1 {
result = append(result, nums[deque[0]])
}
}
return result
}
No.238 移动零
- 链接: https://leetcode.cn/problems/move-zeroes/
- 思路:使用双指针算法:left指针标记下一个非零元素的放置位置,right指针遍历数组。将非零元素依次移到前面,然后将剩余位置填0。优化版本通过交换减少不必要的赋值操作。时间复杂度O(n),空间复杂度O(1)。
给定一个数组
nums
,编写一个函数将所有0
移动到数组的末尾,同时保持非零元素的相对顺序。请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12] 输出: [1,3,12,0,0]
示例 2:
输入: nums = [0] 输出: [0]
func moveZeroes(nums []int) {
// 双指针:left指向下一个非零元素应该放置的位置
left := 0
// 第一遍:将所有非零元素移到前面
for right := 0; right < len(nums); right++ {
if nums[right] != 0 {
nums[left] = nums[right]
left++
}
}
// 第二遍:将剩余位置填充为0
for left < len(nums) {
nums[left] = 0
left++
}
}
// 优化版本:减少赋值操作
func moveZeroesOptimized(nums []int) {
left := 0
for right := 0; right < len(nums); right++ {
if nums[right] != 0 {
// 只在需要交换时才进行操作
if left != right {
nums[left], nums[right] = nums[right], nums[left]
}
left++
}
}
}
No.245 搜索二维矩阵 ||
- 链接:https://leetcode.cn/problems/search-a-2d-matrix-ii/
- 思路:从右上角开始搜索是最优解。利用矩阵性质:当前元素比目标大就向左,比目标小就向下。时间复杂度O(m+n),空间复杂度O(1)。
编写一个高效的算法来搜索
*m* x *n*
矩阵matrix
中的一个目标值target
。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
func searchMatrix(matrix [][]int, target int) bool {
if len(matrix) == 0 || len(matrix[0]) == 0 {
return false
}
row, col := 0, len(matrix[0])-1
for row < len(matrix) && col >= 0 {
if matrix[row][col] == target {
return true
} else if matrix[row][col] > target {
col-- // 当前值太大,向左移动
} else {
row++ // 当前值太小,向下移动
}
}
return false
}
No.253 会议室
Given an array of meeting time intervals
intervals
whereintervals[i] = [starti, endi]
, return the minimum number of conference rooms required.Input: intervals = [[0,30],[5,10],[15,20]] Output: 2
func minMeetingRooms(intervals [][]int) int {
if len(intervals) == 0 {
return 0
}
// 第 1 步:将所有会议的开始和结束时间转换成事件点
// 创建一个切片,用于存储所有事件。每个事件是一个包含时间点和类型的数组
// 类型为 1 代表会议开始,-1 代表会议结束
events := make([][2]int, 0, len(intervals)*2)
for _, interval := range intervals {
events = append(events, [2]int{interval[0], 1}) // 会议开始事件
events = append(events, [2]int{interval[1], -1}) // 会议结束事件
}
// 第 2 步:对所有事件点进行排序
sort.Slice(events, func(i, j int) bool {
// 首先按时间从小到大排序
if events[i][0] != events[j][0] {
return events[i][0] < events[j][0]
}
// 如果时间相同,开始事件(类型为 1)排在结束事件(类型为 -1)之前
// 这样做是为了在同一个时间点,先释放房间再分配新房间,从而更高效地利用会议室
return events[i][1] > events[j][1]
})
// 第 3 步:扫描所有事件点并统计所需会议室数量
maxRooms := 0 // 记录所需会议室的最大数量
currentRooms := 0 // 当前正在使用的会议室数量
for _, event := range events {
currentRooms += event[1] // 遇到开始事件 (+1),房间数增加;遇到结束事件 (-1),房间数减少
if currentRooms > maxRooms {
maxRooms = currentRooms // 更新峰值
}
}
return maxRooms
}
No.287 寻找重复数
思路:这个问题可以巧妙地使用快慢指针(也称Floyd 判圈算法)来解决,该算法通常用于检测链表中的环。尽管这里是一个数组,但我们可以将它抽象为一个链表。
- 将数组视为链表: 我们可以把数组
nums
看作一个链表,其中每个元素的值代表下一个节点的索引。例如,nums[i]
的值就是下一个节点的索引。由于数组中有 n+1 个元素,而值在 [1,n] 范围内,必然会存在一个重复数。这个重复数会导致一个“环”的产生。
例如,
nums = [1, 3, 4, 2, 2]
:0
->nums[0] = 1
1
->nums[1] = 3
3
->nums[3] = 2
2
->nums[2] = 4
4
->nums[4] = 2
2
->nums[2] = 4
从3
走到2
,然后2
又指向4
,4
又指回2
。这样就形成了一个环:2 -> 4 -> 2 -> ...
。
- 将数组视为链表: 我们可以把数组
给定一个包含
n + 1
个整数的数组nums
,其数字都在[1, n]
范围内(包括1
和n
),可知至少存在一个重复的整数。假设
nums
只有 一个重复的整数 ,返回 这个重复的数 。你设计的解决方案必须 不修改 数组
nums
且只用常量级O(1)
的额外空间。示例 1:
输入:nums = [1,3,4,2,2] 输出:2
func findDuplicate(nums []int) int {
// 快慢指针初始化,从索引0开始
slow, fast := 0, 0
// 第1步:快慢指针相遇
for {
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast {
break
}
}
// 第2步:寻找环的入口
finder := 0
for finder != slow {
finder = nums[finder]
slow = nums[slow]
}
// finder 和 slow 相遇的节点就是重复数
return finder
}
No.394 字符串解码
思路:使用双栈解法:数字栈存储重复次数,字符串栈存储前缀字符串。遇到’[‘时入栈,遇到’]‘时出栈并重复构建字符串。时间复杂度O(n),空间复杂度O(n)。
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为:
k[encoded_string]
,表示其中方括号内部的encoded_string
正好重复k
次。注意k
保证为正整数。你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数
k
,例如不会出现像3a
或2[4]
的输入。示例 1:
输入:s = "3[a]2[bc]" 输出:"aaabcbc"
func decodeString(s string) string {
numStack := []int{}
strStack := []string{}
currentStr := ""
currentNum := 0
for _, char := range s {
if char >= '0' && char <= '9' {
currentNum = currentNum*10 + int(char-'0')
} else if char == '[' {
numStack = append(numStack, currentNum)
strStack = append(strStack, currentStr)
currentNum = 0
currentStr = ""
} else if char == ']' {
num := numStack[len(numStack)-1]
numStack = numStack[:len(numStack)-1]
prevStr := strStack[len(strStack)-1]
strStack = strStack[:len(strStack)-1]
currentStr = prevStr + strings.Repeat(currentStr, num)
} else {
currentStr += string(char)
}
}
return currentStr
}
No.415 字符串相加
- 链接:https://leetcode.cn/problems/add-strings/
- 采用末尾向前递进的方式
func addStrings(num1 string, num2 string) string {
i, j := len(num1)-1, len(num2)-1
carry := 0
result := ""
for i >= 0 || j >= 0 || carry != 0 {
var x, y int
if i >= 0 {
x = int(num1[i] - '0') // 了把字符形式的数字转化成对应的整数值,需要用该字符的 ASCII 码值减去字符 '0' 的 ASCII 码值。
}
if j >= 0 {
y = int(num2[j] - '0')
}
sum := x + y + carry
carry = sum / 10
digit := sum % 10
result = strconv.Itoa(digit) + result
i--
j--
}
return result
}
No.448 找到数组中消失的数字
链接: https://leetcode.cn/problems/find-all-numbers-disappeared-in-an-array/
思路:该 Go 语言实现利用了原地哈希的思想,在不使用额外空间的情况下解决了问题。
核心思想: 数组中的数字都在
[1, n]
范围内。我们可以将数组本身作为一个哈希表来使用。当我们遍历到数字num
时,我们将索引num-1
处的元素标记为负数,以此来表示num
已经出现过。由于可能存在重复数字,我们首先取绝对值来获取正确的索引。
给你一个含
n
个整数的数组nums
,其中nums[i]
在区间[1, n]
内。请你找出所有在[1, n]
范围内但没有出现在nums
中的数字,并以数组的形式返回结果。示例 1:
输入:nums = [4,3,2,7,8,2,3,1] 输出:[5,6]
示例 2:
输入:nums = [1,1] 输出:[2]
func findDisappearedNumbers(nums []int) []int {
for _, num := range nums {
index := abs(num) - 1
if nums[index] > 0 {
nums[index] = -nums[index]
}
}
result := []int{}
for i, num := range nums {
if num > 0 {
result = append(result, i+1)
}
}
return result
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
No.459 重复的子字符串
- 链接:https://leetcode.cn/problems/repeated-substring-pattern/submissions/
- 原题:给定一个非空的字符串
s
,检查是否可以通过由它的一个子串重复多次构成。 - 思路:先第一重循环,找出子串。子串的头已经有了,就是字符串的第一个元素,所以移动下标找末尾元素即可。再到第二个循环中从子串长度开始循环,遍历完整个字符串。
func repeatedSubstringPattern(s string) bool {
// 获取字符串 s 的长度
length := len(s)
// 遍历可能的子字符串长度,子字符串长度范围从 1 到 length/2
for subLen := 1; subLen*2 <= length; subLen++ {
// 检查字符串长度是否能被子字符串长度整除
if length%subLen == 0 {
// 如果能整除,检查是否由该子字符串重复构成
if isRepeatedBySubstring(s, subLen) {
return true
}
}
}
// 没有找到符合条件的子字符串,返回 false
return false
}
// isRepeatedBySubstring 检查字符串 s 是否由长度为 subLen 的子字符串重复构成
func repeatedSubstringPattern(s string) bool {
n := len(s)
if n == 0 {
return false // 空字符串不符合条件
}
// 拼接两个 s
ss := s + s
// 查找 s 是否存在于 ss[1 : 2*n-1] 中
// strings.Contains 效率较高,因为它内部通常会使用优化的字符串查找算法
// 这里的 ss[1:2*n-1] 是为了排除 s 本身在 ss 中的起始位置和结束位置
// 例如 "abab" + "abab" = "abababab"
// 排除首尾字符后是 "bababa"
// 在 "bababa" 中查找 "abab",如果找到则说明是重复子串
return strings.Contains(ss[1:2*n-1], s)
}
解法二:如果将字符串叠加,再移除头部和尾部的字符串,如果是重复的,一定可以在合并字符串中找到原本的自己。
func repeatedSubstringPattern(s string) bool {
return strings.Index((s + s)[1:len(s)*2-1], s) != -1
}
No.438 找到字符串中所有字母异位词
链接: https://leetcode.cn/problems/find-all-anagrams-in-a-string/
思路:滑动窗口 + 匹配计数(最优)
- 维护字符差值数组和不匹配字符种类数,避免每次比较整个数组
- 时间复杂度O(n),空间复杂度O(1),常数因子最小
给定两个字符串
s
和p
,找到s
中所有p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。示例 1:
输入: s = "cbaebabacd", p = "abc" 输出: [0,6] 解释: 起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。 起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
示例 2:
输入: s = "abab", p = "ab" 输出: [0,1,2] 解释: 起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。 起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。 起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
func findAnagramsOptimal(s string, p string) []int {
if len(s) < len(p) {
return []int{}
}
result := []int{}
pLen := len(p)
// 字符差值数组
diff := make([]int, 26)
// 初始化差值数组
for i := 0; i < pLen; i++ {
diff[p[i]-'a']++
diff[s[i]-'a']--
}
// 计算初始不匹配的字符种类数
mismatch := 0
for i := 0; i < 26; i++ {
if diff[i] != 0 {
mismatch++
}
}
// 检查第一个窗口
if mismatch == 0 {
result = append(result, 0)
}
// 滑动窗口
for i := pLen; i < len(s); i++ {
// 添加新字符
newChar := s[i] - 'a'
if diff[newChar] == 0 {
mismatch++
}
diff[newChar]--
if diff[newChar] == 0 {
mismatch--
}
// 移除旧字符
oldChar := s[i-pLen] - 'a'
if diff[oldChar] == 0 {
mismatch++
}
diff[oldChar]++
if diff[oldChar] == 0 {
mismatch--
}
// 检查当前窗口
if mismatch == 0 {
result = append(result, i-pLen+1)
}
}
return result
}
No.560 和位K的子数组
- 链接:https://leetcode.cn/problems/subarray-sum-equals-k/description/
- 思路:使用前缀和+哈希表:如果当前前缀和为sum,存在前缀和为(sum-k),则它们之间的子数组和为k。时间复杂度O(n),空间复杂度O(n)。
给你一个整数数组
nums
和一个整数k
,请你统计并返回 该数组中和为k
的子数组的个数 。子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2 输出:2
示例 2:
输入:nums = [1,2,3], k = 3 输出:2
func subarraySum(nums []int, k int) int {
count := 0
prefixSum := 0
// 前缀和哈希表,key为前缀和,value为出现次数
sumMap := map[int]int{0: 1} // 初始化,前缀和为0出现1次
for _, num := range nums {
prefixSum += num
// 如果存在前缀和为(prefixSum - k),说明存在和为k的子数组
if count, exists := sumMap[prefixSum-k]; exists {
count += count
}
// 更新当前前缀和的出现次数
sumMap[prefixSum]++
}
return count
}
No.581 最短无序连续子数组
- 链接: https://leetcode.cn/problems/shortest-unsorted-continuous-subarray/
- 思路: 这个方法之所以有效,是因为如果一个子数组需要排序才能使整个数组变得有序,那么这个子数组中的所有元素都必须在它们最终的有序位置上。同时,这个子数组两边的元素都必须已经在正确的位置上,所以它们在原始数组和排序后数组中的位置是相同的。因此,通过比较找到第一个和最后一个不匹配的元素,我们就能精确地界定出需要排序的最短子数组的边界。
给你一个整数数组
nums
,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。请你找出符合题意的 最短 子数组,并输出它的长度。
输入:nums = [2,6,4,8,10,9,15] 输出:5 解释:你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。
func findUnsortedSubarray(nums []int) int {
// 创建一个 nums 的副本
sortedNums := make([]int, len(nums))
copy(sortedNums, nums)
// 对副本进行排序
sort.Ints(sortedNums)
n := len(nums)
start, end := -1, -1
// 从左向右找到第一个不匹配的元素
for i := 0; i < n; i++ {
if nums[i] != sortedNums[i] {
start = i
break
}
}
// 如果 start 仍然是 -1,说明原数组已经是有序的
if start == -1 {
return 0
}
// 从右向左找到第一个不匹配的元素
for i := n - 1; i >= 0; i-- {
if nums[i] != sortedNums[i] {
end = i
break
}
}
// 返回子数组的长度
return end - start + 1
}
No.674 最长连续递增序列
- 链接:https://leetcode.cn/problems/longest-continuous-increasing-subsequence/
- 思路:一次遍历,找最长,存下来
func findLengthOfLCIS(nums []int) int {
if len(nums) == 0 {
return 0
}
maxLength := 1
currentLength := 1
for i := 1; i < len(nums); i++ {
// 下一个更大,这里长度+1
if nums[i] > nums[i-1] {
currentLength++
} else {
maxLength = max(maxLength, currentLength)
currentLength = 1
}
}
// 处理最后一个递增子序列的情况
maxLength = max(maxLength, currentLength)
return maxLength
}
No.739 每日温度
思路:使用单调递减栈解法:栈中存储温度的下标,当遇到更高温度时弹出栈中较低温度的下标并计算天数差。时间复杂度O(n),空间复杂度O(n)。
给定一个整数数组
temperatures
,表示每天的温度,返回一个数组answer
,其中answer[i]
是指对于第i
天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用0
来代替。
func dailyTemperatures(temperatures []int) []int {
n := len(temperatures)
result := make([]int, n)
stack := []int{} // 单调栈,存储下标
for i := 0; i < n; i++ {
// 当栈不为空且当前温度大于栈顶对应的温度时
for len(stack) > 0 && temperatures[i] > temperatures[stack[len(stack)-1]] {
// 弹出栈顶元素
prevIndex := stack[len(stack)-1]
stack = stack[:len(stack)-1]
// 计算天数差
result[prevIndex] = i - prevIndex
}
// 当前下标入栈
stack = append(stack, i)
}
return result
}