【代码随想录Go——数组】

1. 数组

所有题目都来自leetcode。

1.1 什么是数组

数组是存放在连续内存空间上的相同类型数据的集合。
在这里插入图片描述

1.2 数组的特点

  • 数组下标都是从0开始的。
  • 数组内存空间的地址是连续的

正是因为数组的在内存空间的地址是连续的,并且数组的元素是不能删的,只能覆盖。所以我们在删除或者增添中间元素的时候,就难免要移动其他地址上的元素。
在这里插入图片描述

1.3 二维数组

二维数组在内存的空间地址是连续的么?
在C++和C中,二维数组的内存地址是连续的。
在这里插入图片描述

但是在Java等语言中内存地址是不连续的。采用的是指针的方式来指向一行中的一维数组开头地址。
在这里插入图片描述

2.练习题

2.1 二分查找

这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件。
有趣的点:这里通常会把等于的判断放到最后一个,这样通常可以减少if判断的次数。

func search(nums []int, target int) int {
    l,r := 0,len(nums)-1
    for l<=r {
        mid := (l+r)/2
        if nums[mid] < target{
            l = mid + 1
        }else if nums[mid] > target{
            r = mid - 1
        }else{
            return mid
        }
    }
    return -1
}

2.1.1 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

这里我们再理解区分一下左指针和右指针的含义。
左指针:我左边的数都比target小
右指针:我右边的数都比target大
当到达临界状态即l==r时,即这次是最后一次循环
情况一:如果lastNum等于target,返回mid即可
情况二:如果lastNum大于target,此时r需要减一,l保持不变。此时l上的数值是第一个大于要查找的数值,即l就是要插入的位置。
情况三:如果lastNum小于target,此时l需要加一,r保持不变。此时r上的数值是最后一个小于要查找的数值,所以l即是要插入的位置。

func searchInsert(nums []int, target int) int {
    l,r := 0,len(nums)-1
    for l<=r {
        mid := (l+r)/2
        if nums[mid] < target{
            l = mid + 1
        }else if nums[mid] > target{
            r = mid - 1
        }else{
            return mid
        }
    }
    // 这里有两种写法
    return l
    //return r + 1
}

2.1.2 在排序数组中查找元素的第一个和最后一个位置

方法1

当查找到指定元素后开始进行该元素的左右边界查找,该方法在一般情况下比较快。但当数组中的元素的个数全部一样是,时间复杂度为O(N);

func searchRange(nums []int, target int) []int {
    l,r := 0,len(nums)-1
    for l<=r {
        mid := (l+r)/2
        if nums[mid] < target{
            l = mid + 1
        }else if nums[mid] > target{
            r = mid - 1
        }else{
            // 找到一个符合的元素
            // 开始往左右查找边界
            l2,r2 := mid,mid
            for l2>=0&&nums[l2]==target{
                l2-=1
            }
            for r2<len(nums)&&nums[r2]==target{
                r2+=1
            }
            return []int{l2+1,r2-1}
        }
    }
    return []int{-1,-1}
}
方法2

思路转换:
首先需要判断是否有相同的元素
即找到我们数组中第一个相同的元素和最后一个相等的元素
在这里我们使用hasTarget来标记数组中是否有相同的元素。

func searchRange(nums []int, target int) []int {
	l, r := searchFirstEqual(nums, target), searchLastEqual(nums, target)
	if l != -1 && r != -1 {
		return []int{l, r}
	} else if l != -1 {
		return []int{l, l}
	} else if r != -1 {
		return []int{r, r}
	}
	return []int{-1, -1}
}
// 寻找和目标元素第一个相等的下标
func searchFirstEqual(nums []int, target int) int {
	l, r := 0, len(nums)-1
	hasTarget := false
	for l <= r {
		mid := (l + r) / 2
		if nums[mid] < target {
			l = mid + 1
		} else {
			r = mid - 1
		}
		if nums[mid] == target {
			hasTarget = true
		}
	}
	if hasTarget {
		return l
	}
	return -1
}
// 寻找和目标元素最后一个相等的下标
func searchLastEqual(nums []int, target int) int {
	l, r := 0, len(nums)-1
	hasTarget := false
	for l <= r {
		mid := (l + r) / 2
		if nums[mid] > target {
			r = mid - 1
		} else {
			l = mid + 1
		}
		if nums[mid] == target {
			hasTarget = true
		}
	}
	if hasTarget {
		return r
	}
	return -1
}

2.1.3 x的平方根

给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
Note:不解释了,应该都看得懂。

func mySqrt(x int) int {
    l,r := 0,x
    for l<=r{
        mid := (l+r)/2
        if mid*mid<x{
            l=mid+1
        }else if mid*mid>x{
            r=mid-1
        }else{
            return mid
        }
    }
    return l-1
}

2.1.4 有效的完全平方数

给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。
不能使用任何内置的库函数,如 sqrt 。

func isPerfectSquare(num int) bool {
    l,r := 0,num
    for l<=r{
        mid := (l+r)/2
        if mid*mid<num{
            l=mid+1
        }else if mid*mid>num{
            r=mid-1
        }else{
            return true
        }
    }
    return false
}

2.2 移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

func removeElement(nums []int, val int) int {
    // 用来记录遇到的val元素个数
    count := 0
    for i:=0;i<len(nums);i++{
        if nums[i]==val{
            count+=1
        }else{
            //将元素向前移动指定长度,长度为遇到过的val值的个数
            //这里也可以理解i-count为一个指针,所以也可以理解为双指针法
            nums[i-count]=nums[i]
        }
    }
    return len(nums)-count
}

2.2.1 删除排序数组中的重复项

给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。

方法1(容易理解)
相对上一题来说我们可以用到set集合,来判断当前元素是否已经出现过。

type void struct{}
var member void

func removeDuplicates(nums []int) int {
    m := make(map[int]void)
    // 用来记录遇到的val元素个数
    count := 0
    for i:=0;i<len(nums);i++{
        if _,ok := m[nums[i]];ok{
            count+=1
        }else{
            //将元素向前移动指定长度,长度为遇到过的val值的个数
            //这里也可以理解i-count为一个指针,所以也可以理解为双指针法
            nums[i-count]=nums[i]
            m[nums[i]] = member
        }
    }
    return len(nums)-count
}

方法2(更快,不需要map)

func removeDuplicates(nums []int) int {
    // 用来记录不一样的元素个数
    count := 1
    for i:=1;i<len(nums);i++{
        if nums[i]==nums[i-1]{
            continue
        }
        nums[count]=nums[i]
        count++
    }
    return count
}

2.2.2 移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。

依据2.2中移除0元素,之后再最后添加指定个数的0即可。

func moveZeroes(nums []int)  {
    start:=removeElement(nums,0)
    for i:=start;i<len(nums);i++{
        nums[i]=0
    }
}

func removeElement(nums []int, val int) int {
    // 用来记录遇到的val元素个数
    count := 0
    for i:=0;i<len(nums);i++{
        if nums[i]==val{
            count+=1
        }else{
            //将元素向前移动指定长度,长度为遇到过的val值的个数
            //这里也可以理解i-count为一个指针,所以也可以理解为双指针法
            nums[i-count]=nums[i]
        }
    }
    return len(nums)-count
}

2.2.3 比较含退格的字符串

给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true 。# 代表退格字符。
注意:如果对空文本输入退格字符,文本继续为空。

这题目是简单的模拟题:

func backspaceCompare(s string, t string) bool {
	vec_s := make([]byte, 200)
	vec_t := make([]byte, 200)
	ps := 0
	for i := 0; i < len(s); i++ {
		if s[i] == '#' {
			if ps > 0 {
				ps -= 1
			}
		} else {
			vec_s[ps] = s[i]
			ps += 1
		}
	}
	pt := 0
	for i := 0; i < len(t); i++ {
		if t[i] == '#' {
			if pt > 0 {
				pt -= 1
			}
		} else {
			vec_t[pt] = t[i]
			pt += 1
		}
	}

	if ps != pt {
		return false
	}
	for i := 0; i < ps; i++ {
		if vec_s[i] != vec_t[i] {
			return false
		}
	}

2.2.4 有序数组的平方

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

常用思路,平方完成后进行排序。O(nlogn)
新思路:找到正负数的交界,按绝对值进行归并排序。再求平方

func sortedSquares(nums []int) []int {
	size := len(nums)
	plusIndex := 0
	for i := 0; i < size; i++ {
		if nums[i] >= 0 {
			break
		}
		plusIndex += 1
	}
	fmt.Printf("正负数的交界位置:%d\n", plusIndex)
	// 归并排序
	p1, p2 := plusIndex-1, plusIndex
	res := make([]int, size)
	index := 0
	for p1 >= 0 && p2 < size {
		if -nums[p1] <= nums[p2] {
			res[index] = nums[p1] * nums[p1]
			p1 -= 1
		} else {
			res[index] = nums[p2] * nums[p2]
			p2 += 1
		}
		index += 1
	}

	for p1 >= 0 {
		res[index] = nums[p1] * nums[p1]
		index += 1
		p1 -= 1
	}

	for p2 < size {
		res[index] = nums[p2] * nums[p2]
		index += 1
		p2 += 1
	}
	return res
}

2.3 长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的连续子数组[numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回0。
在这题中我们使用滑动窗口来解决这个问题,这里复杂度看似是O(N^2),实则是O(N)

func minSubArrayLen(target int, nums []int) int {
    l,r,size :=0,0,len(nums)
    sum,minLen,nowLen := 0,1000000,0
    for r<size{
        sum+= nums[r]
        r+=1
        nowLen += 1
        if sum>=target{
            if nowLen<minLen{
                minLen = nowLen
            }
            for sum>target{
                sum -= nums[l]
                l += 1
                nowLen -= 1
                if sum>=target && nowLen<minLen{
                    minLen = nowLen
                }
            }
        }
    }
    if minLen==1000000{
        return 0
    }else{
        return minLen
    }
}

2.3.1 水果成篮

你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类 。
你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

  • 你只有 两个 篮子,并且每个篮子只能装单一类型的水果。每个篮子能够装的水果总量没有限制。
  • 你可以选择任意一棵树开始采摘,你必须从每棵树(包括开始采摘的树)上恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
  • 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。

给你一个整数数组 fruits ,返回你可以收集的水果的最大数目。


func totalFruit(fruits []int) int {
    fruitsBasket := make([]int,100001)
    l,r,nums := 0,0,len(fruits)
    basketUseNum,maxBasketNum := 0,2
    maxNum,nowNum := 0,0
    for r<nums{
        fruitTree := fruits[r]
        if fruitsBasket[fruitTree]==0{
            //是新水果
            //查看是否有新篮子用来装这类水果
            for basketUseNum==maxBasketNum{
                // 开始清空一个篮子
                fruitTree2 := fruits[l]
                fruitsBasket[fruitTree2] -= 1
                nowNum -= 1
                if(fruitsBasket[fruitTree2]==0){
                    basketUseNum -= 1
                }
                l += 1
            }
            //使用一个空栏装新水果
            basketUseNum += 1
        }
        fruitsBasket[fruitTree] += 1
        nowNum += 1
        if(nowNum>maxNum){
            maxNum=nowNum
        }
        r += 1
    }
    return maxNum
}

2.3.2 最小覆盖子串

这里巧妙的利用了needSafiCharNum来标记了需要满足的字符种类,避免每次需要去遍历数组来查看是否覆盖要求。
小小炫耀一下做了一道hard。O(∩_∩)O哈哈~

const INF = -1000000

func minWindow(s string, t string) string {
	nums := make([]int, 128)
	needSafiCharNum := genNeedCharVec(t, nums)
	minLeft, minRight, minLen := -1, -1, 100001
	l, r, nowLen := 0, 0, 0
	for r < len(s) {
		char := int(s[r])
		nowLen += 1
		r += 1
		if nums[char] != INF {
			// 表示这个字符是我子串里面需要的一个字符
			nums[char] -= 1
			if nums[char] == 0 {
				//表面这类字母所需的数量已经满足要求
				needSafiCharNum -= 1
			}
			if needSafiCharNum == 0 {
				//表面现在区域内的内容已经满足
				if minLen > nowLen {
					minLen = nowLen
					minLeft = l
					minRight = r
				}
				//开始控制l前进
				for needSafiCharNum == 0 {
					char2 := int(s[l])
					l += 1
					nowLen -= 1
					if nums[char2] != INF {
						//表面这个字母是我们需要的字母
						//退除后需要+1
						nums[char2] += 1
						if nums[char2] == 1 {
							//表面我们又缺少了一类字符
							needSafiCharNum += 1
							break
						}
					}
					//否则更新一下
					if minLen > nowLen {
						minLen = nowLen
						minLeft = l
						minRight = r
					}
				}
			}
		}
	}
    if minLeft==-1 || minRight== -1{
		return ""
	}
	return s[minLeft:minRight]
}

func genNeedCharVec(t string, vec []int) int {
	charNum := 0
	for i := 0; i < len(vec); i++ {
		vec[i] = INF
	}
	for _, char := range t {
		if vec[int(char)] == INF {
			vec[int(char)] = 0
			charNum += 1
		}
		vec[int(char)] += 1
	}
	return charNum
}

在这里插入图片描述

2.4 螺旋矩阵II

给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
示例:
输入: 3
输出: [ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ]

func generateMatrix(n int) [][]int {
    startx,starty := 0,0
    arr := make([][]int,n)
    for i:=0;i<n;i++{
        arr[i] = make([]int,n)
    }
    loop := (n+1)/2
    offset,num := 0,1
    // 初始化完成
    for loop>0{
        i,j := startx,starty
        //行i不动,列j增大
        for ;j<n-offset;j++{
            arr[i][j]=num
            num+=1
        }
        //列j不动,行i增大
        j-=1
        for i=i+1;i<n-offset;i++{
            arr[i][j]=num
            num+=1
        }
        //行不动,列减小
        i-=1
        for j=j-1;j>=starty;j--{
            arr[i][j]=num
            num+=1
        }
        j+=1
        //列不动,行动
        for i=i-1;i>startx;i--{
            arr[i][j]=num
            num+=1
        }
        startx += 1
        starty += 1
        offset += 1
        loop -= 1
    }
    return arr
}

2.4.1 螺旋矩阵/螺旋遍历二维数组

给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

设置每轮开始的起点,每轮中的一个方向走完需要调整i,j的位置。

func spiralOrder(matrix [][]int) []int {
	rows := len(matrix)
	// 特例判定
	if rows==0{
        return make([]int, 0)
    }
	cols := len(matrix[0])
	sum, count := rows*cols, 0
    //x表示行,y表示列
	startx, starty := 0, 0
	offset := 0

	res := make([]int, rows*cols)
	for sum > count {
		i, j := startx, starty
		//行数不变 列数在变
		for j = starty; j < cols-offset; j++ {
			res[count] = matrix[startx][j]
			count += 1
		}
		if sum == count {
			break
		}
		j -= 1
		//列数不变是j行数变,注意i的起始位置
		for i = startx+1; i < rows-offset; i++ {
			res[count] = matrix[i][j]
			count += 1
		}
		if sum == count {
			break
		}
		i -= 1
		j -= 1
		//行数不变 i 列数变 j--
		for ; j >= starty; j-- {
			res[count] = matrix[i][j]
			count += 1
		}
		if sum == count {
			break
		}
		j += 1
		i -= 1
		//列不变 行变
		for ; i > startx; i-- {
			res[count] = matrix[i][j]
			count += 1
		}
		startx += 1
		starty += 1
		offset += 1
	}
	return res
}

3. 总结

数组的经典题目

  • 二分法
    • 二分法是算法面试中的常考题,建议通过这道题目,锻炼自己手撕二分的能力。
  • 双指针法(快慢指针)
    • 双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。
  • 滑动窗口
    • 滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。
  • 模拟行为
    • 模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,十分考察大家对代码的掌控能力。
  • 35
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值