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)。
- 模拟行为
- 模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,十分考察大家对代码的掌控能力。