Table of Contents
对 Go 数组和相关算法进行了学习,并做了笔记。 相关链接:代码随想录
Go 数组
定义相关
数组是存放在连续内存空间上的相同类型数据的集合。
数组是具有相同类型的一组已编号且长度固定的数据项序列。这种类型可以是任意的原始类型(int、string或者自定义类型)。
数组元素可以通过索引来读取或修改。
数组定义: var a [len]int,例如 var a [5]int
数组的大小len必须是常量,且是类型的组成部分(数组的类型不仅由 元素类型 决定,还由 长度 决定,例如[5]int 和 [10]int 是 两种完全不同的类型)。
数组大小一旦定义后不可变。
内置函数 len 和 cap 都会返回数组大小
需要注意:
- 数组的下标都是从0开始的
- 数组内存空间的地址是连续的
正是因为数组在内存空间的地址是连续的,所以在“删除”元素时需要移动其他元素的地址。数组的元素不能删,只能覆盖。
遍历数组:
for i := 0; i < len(a); i++ { //... } for index, v := range a { //... }访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic
数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
值拷贝会出现性能问题,通常会使用 slice 或数组指针。
Go 数组支持 ”==”、”!=” 操作符,因为数组是值类型,内存总是被初始化为对应类型的0值。
指针数组 [n]*T,数组指针 *[n]T 。
数组的初始化
// 全局:var numbers [5]int //初始值为0var arr0[5]int = [5]int{1, 2, 3}var arr1 = [5]int{1, 2, 3, 4, 5} // 使用初始化列表来初始化数组的元素var arr2 = [...]int{1, 2, 3, 4, 5, 6} // 数组长度不确定,使用...代替数组长度,编译器会根据元素个数自行推断数组长度var str = [5]string{3: "hello world", 4: "kira"} // 如果设置了数组长度,可以通过指定下标来初始化元素
// 局部:a := [3]int{1, 2} // 未初始化的元素值为0b := [...]int{1, 2, 3, 4} // 通过初始化值来确定数组长度c := [5]int{2:100, 4:200} // 使用下标来初始化元素d := [...]struct { name string age string // 这里是数组元素类型struct}{ {"user1", 10}, {"user2", 20}, // 2个元素}Go 向函数中传递数组
Go 语言中的数组是值类型,因此将数组传递给函数时,实际上传递的是数组的副本。
向函数传递数组参数,需要在函数定义时声明形参为数组。
func aFunc(arr [10]int) { ...}// 或者func bFunc(arr []int) { ...}如果想要在函数内修改原始数组,需要传递数组的指针
package main
import "fmt"
// 函数接受一个数组作为参数func modifyArray(arr [5]int) { for i := 0; i < len(arr); i++ { arr[i] = arr[i] * 2 }}
// 函数接受一个数组的指针作为参数func modifyArrayWithPointer(arr *[5]int) { for i := 0; i < len(*arr); i++ { (*arr)[i] = (*arr)[i] * 2 }}
func main() { // 创建一个包含5个元素的整数数组 myArray := [5]int{1, 2, 3, 4, 5}
fmt.Println("Original Array:", myArray)
// 传递数组给函数,但不会修改原始数组的值 modifyArray(myArray) fmt.Println("Array after modifyArray:", myArray)
// 传递数组的指针给函数,可以修改原始数组的值 modifyArrayWithPointer(&myArray) fmt.Println("Array after modifyArrayWithPointer:", myArray)}输出结果
Original Array: [1 2 3 4 5]Array after modifyArray: [1 2 3 4 5]Array after modifyArrayWithPointer: [2 4 6 8 10]多维数组
一个二维数组 a 为三行四列,二维数组中的元素可通过 a[ i ][ j ] 来访问。
// 全局 var arr0 [5][3]int var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}} // 局部 a := [3][4]int{ {0, 1, 2, 3} , /* 第一行索引为 0 */ {4, 5, 6, 7} , /* 第二行索引为 1 */ {8, 9, 10, 11}, /* 第三行索引为 2 */ } b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。多维数组遍历
package main
import ( "fmt")
func main() {
var f [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
for k1, v1 := range f { for k2, v2 := range v1 { fmt.Printf("(%d,%d)=%d ", k1, k2, v2) } fmt.Println() }}输出结果
(0,0)=1 (0,1)=2 (0,2)=3 (1,0)=7 (1,1)=8 (1,2)=9704.二分查找
给定一个
n个元素有序的(升序)整型数组nums和一个目标值target,写一个函数搜索nums中的target,如果target存在返回下标,否则返回-1。你必须编写一个具有
O(log n)时间复杂度的算法。示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9输出: 4解释: 9 出现在 nums 中并且下标为 4示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2输出: -1解释: 2 不存在 nums 中因此返回 -1提示:
- 你可以假设
nums中的所有元素是不重复的。n将在[1, 10000]之间。nums的每个元素都将在[-9999, 9999]之间。
使用二分查找的前提:有序数组;无重复元素。
边界处理是重要问题。
主流写法有2类,左闭右闭[left, right]和左闭右开[left, right)。
左闭右闭区间[left, right]
x >> n相当于除以 2^n 并向下取整。
mid := left + (right-left)>>1是常见的二分中点写法,直接相加除以二可能会导致数值溢出。
left := 0
right := len(num) - 1
*判断 left < right 时有没有=,需要看区间[left, right]。
左闭右闭,left == right 在这个区间内有意义,所以需要 <=
*更新区间边界,以下是伪代码
if num[mid] == target
return mid
else if num[mid] > target
更新右边界,如下图所示。因为区间[left, right]右闭包含 right,并且上面的判断条件 mid != target,所以
right = mid - 1 。
else // num[mid] < target
更新左边界,左闭包含 left,left != target, 所以
left = mid + 1
func search(nums []int, target int) int { // 左闭右闭 [left, right] // 初始化左右边界 left := 0 right := len(nums) - 1
for left <= right { mid := left + (right - left) >> 1 if nums[mid] == target { return mid } else if nums[mid] < target { // 更新左边界,mid 不是 target,所以left应该为 mid+1 left = mid + 1 } else { // 更新右边界, mid > targer,所以right 应该为 mid-1 right = mid - 1 } } return -1}左闭右开区间[left, right)
left := 0
right := len(num)
由于右开, right 应当是数组元素综述+1才不会漏元素
*条件判断时,left < right ,因为 left == right 在区间[left, right)中没有意义。
*更新区间边界
if num[mid] == target
return mid
else if num[mid] > target
更新右边界。num[mid] != target,又因为右开,所以
right = mid
else // num[mid] < target
更新左边界, 因为左闭,应当包含边界,而 num[mid] != target 所以
left = mid + 1
func search(nums []int, target int) int { // 左闭右开 [left, right) // 初始化左右边界 left := 0 right := len(nums)
for left < right { mid := left + (right - left) >> 1 if nums[mid] == target { return mid } else if nums[mid] < target { // 更新左边界,mid 不是 target,所以left应该为 mid+1 left = mid + 1 } else { // 更新右边界, mid > targer,所以right 应该为 mid-1 right = mid } } return -1}遗留:什么是循环不变量?
27. 移除元素
给你一个数组
nums和一个值val,你需要 原地 移除所有数值等于val的元素。元素的顺序可能发生改变。然后返回nums中与val不同的元素的数量。假设
nums中不等于val的元素数量为k,要通过此题,您需要执行以下操作:
- 更改
nums数组,使nums的前k个元素包含不等于val的元素。nums的其余元素和nums的大小并不重要。- 返回
k。用户评测:
评测机将使用以下代码测试您的解决方案:
int[] nums = [...]; // 输入数组int val = ...; // 要移除的值int[] expectedNums = [...]; // 长度正确的预期答案。// 它以不等于 val 的值排序。int k = removeElement(nums, val); // 调用你的实现assert k == expectedNums.length;sort(nums, 0, k); // 排序 nums 的前 k 个元素for (int i = 0; i < actualLength; i++) {assert nums[i] == expectedNums[i];}如果所有的断言都通过,你的解决方案将会 通过。
示例 1:
输入:nums = [3,2,2,3], val = 3输出:2, nums = [2,2,_,_]解释:你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2输出:5, nums = [0,1,4,0,3,_,_,_]解释:你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。注意这五个元素可以任意顺序返回。你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。提示:
0 <= nums.length <= 1000 <= nums[i] <= 500 <= val <= 100
暴力解法
时间复杂度是O(n^2)
两层 for 循环,一个for循环遍历数组元素 ,第二个for循环更新数组。
一旦第一层 for 循环找到 val ,就用其后面的元素把它往前一位覆盖掉,并且数组大小 len-1
func removeElement(nums []int, val int) int { len := len(nums) for i := 0; i < len; i++ { if nums[i] == val { for j := i + 1; j < len; j++ { nums[j-1] = nums[j] } i-- len-- } } return len}快慢指针法
时间复杂度:O(n);空间复杂度:O(1)
没有改变元素的相对位置
- 快指针:寻找新数组元素(非val的元素)
- 慢指针:指向要更新的元素,新数组的下标。
快指针一旦找到新数组元素,就更新慢指针的元素,直到找完就得到了全部是非 val 元素的新数组。
slowIndex 刚好就是新数组大小。
func removeElement(nums []int, val int) int { var slowIndex int = 0 len := len(nums) for fastIndex := 0; fastIndex < len ; fastIndex++ { if nums[fastIndex] != val { nums[slowIndex] = nums[fastIndex] slowIndex++ } } return slowIndex}双向指针法
有点像二分查找,这里按照左闭右闭来写。
双向指针法,left找 val,right找非 val,一旦都找到,就 nums[left] = nums[right] 随后 left++ ; right— (都向中间靠拢) 当 left > right 时,说明left左侧没有 val,left 指针左侧即是最终的数组,left 就是数组大小。
过程:
func removeElement(nums []int, val int) int { left := 0 right := len(nums) - 1 // 左闭右闭区间,所以是 <= for left <= right { // 寻找左侧val,找到退出循环 for left <= right && nums[left] != val { left++ } // 寻找右侧非val,找到退出循环 for left <= right && nums[right] == val { right-- } // 各自都找到后开始覆盖 if left < right { nums[left] = nums[right] left++ right-- } } return left}977.有序数组的平方
给你一个按 非递减顺序 排序的整数数组
nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。示例 1:
输入:nums = [-4,-1,0,3,10]输出:[0,1,9,16,100]解释:平方后,数组变为 [16,1,0,9,100]排序后,数组变为 [0,1,9,16,100]示例 2:
输入:nums = [-7,-3,2,3,11]输出:[4,9,9,49,121]提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums已按 非递减顺序 排序进阶:
- 请你设计时间复杂度为
O(n)的算法解决本问题
暴力解法 直接平方排序
O(n + nlogn) 或是 O(nlogn)
func sortedSquares(nums []int) []int { for i, val := range(nums) { nums[i] *= val } sort.Ints(nums) return nums}双指针法
O(n)
数组是有序的,平方之后的数组,最大的数分别在两端,越往中间越小。
创建新数组 ans,以及k(数组的最大下标)。
两端的下标 left, right(左闭右闭)。nums[left]^2 和 nums[right]^2 做比较。
大的填入 ans[k],然后大的指针—,k—,另一个不动留着下一次比较。即:
如果A[i] * A[i] < A[j] * A[j] 那么ans[k--] = A[j] * A[j]; j--。
如果A[i] * A[i] >= A[j] * A[j] 那么ans[k--] = A[i] * A[i]; i++。
下图在最后应该是 i 和 j 相等,都到元素0再判断平方大小,动画可能少了一张图。
func sortedSquares(nums []int) []int { len := len(nums) var left, right, k int = 0, len-1, len-1 ans := make([]int, len) for left <= right { lsq, rsq := nums[left]*nums[left], nums[right]*nums[right] if lsq > rsq { left++ ans[k] = lsq k-- } else { right-- ans[k] = rsq k-- } } return ans}