本文内容包括以下Go并发/并行的内容:

  1. go
  2. chan

先来看一道并发同步问题:

1114. 按序打印

我们提供了一个类:

public class Foo {
public void first() { print(“first”); }
public void second() { print(“second”); }
public void third() { print(“third”); }
}
三个不同的线程 A、B、C 将会共用一个 Foo 实例。

一个将会调用 first() 方法
一个将会调用 second() 方法
还有一个将会调用 third() 方法
请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/print-in-order
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

这个题目在力扣上并没有给出Go的提交选项!

类似定义class,我们定义一个Go的结构体:

1

关键字go

Go使用名为goroutine的方式来实现并发,routine的IT专业翻译为”例程,例行程序”,goroutine应该是一个Go诞生后的衍生词汇(嗯,我猜的)。main函数也属于一个goroutine

Go实现并发的方式非常的简单,go + 并发语句,代码如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"time"
)
func main() {
go first()
go fmt.Println("second main")
time.Sleep(10) // 如果不添加词句,main协程完毕后其他协程会自动退出而不执行
}
func first() {
fmt.Println("first")
}
func second() {
fmt.Println("second")
}
func third() {
fmt.Println("third")
}

多执行几次,会发现语句打印的顺序是随机的。另外,如果不喜欢使用time.Sleep(10)这种方式,也可以使用sync.WaitGroup来保证其他协程的执行。

go大概有三种使用方式:

(1) go + 函数名/匿名函数

(3) go + 语句或语句块:

1
2
3
go {
/// 代码块
}

chan

go设计者认为在并发模型中,消息机制优于共享内存机制,go中的这种机制被称为channel。

chan, 个人看法,其实并不能说是一种数据类型,因为在内建包(builtin)中并没有写 type chan。chan作为goroutine之间的通信通道,遵循FIFO。一个chan只能传递一种类型,但声明为 chan interface{} 可以传递任意类型。

发送数据与接收数据使用 <-,并且通道是阻塞的,即如果接收方从通道获取数据时,发现通道无数据,则会等待发送方发送数据后才会执行,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main
/**
* 不能直接在函数体外使用 ch := make(chan int), “:=”
* 可以使用var ch = make(chan int)
* 或者函数体外定义,函数体内使用make分配空间
* var ch chan int
* ...
* func ...{
* ch = make(chan int)
* }
*/
var ch chan int

func main() {
ch = make(chan int) // 无缓冲的通道
go parafunc(14)
v := <- ch // 阻塞
// 仍然会延迟5秒打印下述语句
fmt.Println(" v = ", v)
}

func parafunc(i int) {
fmt.Println("test...")
time.Sleep(5 * time.Second) // 延迟5秒
ch <- i // 如果没有接收方,也会阻塞
close(ch) // 关闭通道
}

chan创建时也可以分配长度,如

1
ch := make(chan int, 2) // 此时,ch为缓冲区为2的通道

此时ch为空时,取值阻塞;当ch填满两个元素时,放值阻塞。如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var ch chan int

func main() {
ch = make(chan int, 1)
go parafunc(14)

time.Sleep(10 * time.Second)

fmt.Println("over")
}

func parafunc(i int) {
fmt.Println("test...")
// time.Sleep(5 * time.Second)
fmt.Println("before...")
ch <- i // 因为容量为1,所以并不会阻塞;但是如果ch = make(chan int)时,则会阻塞
fmt.Println("after....")
close(ch)
}

除消息机制外,Go也提供了共享资源加锁机制,包 sync 一些函数可对资源加锁操作。

atomic 提供了一些原子函数:官方链接

1
2
3
4
5
import "sync/atomic"
var count int64
atomic.AddInt64(&count, 1) // 原子操作+1
value := atomic.LoadInt64(&count) // 原子操作:取值
atomic.StoreInt64(&count, 1) // 设置值

sync 包提供了互斥锁:

1
2
3
4
5
6
7
import "sync"

var mutex sync.Mutex

mutex.Lock()
.... // 临界区
mutext.Unlock()

解决同步问题

我们回到问题上,如何保持三个方法的同步呢,我们可以使用chan,代码如下。思路是使用两个通道,保持三个方法的同步。当然也可以用三个通道,告知外部程序,三个方法均执行完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import "time"

type Foo struct {
ch1 chan int
ch2 chan int
}

func (f Foo) first() {
print("first")
f.ch1 <- 1 // 存入任何值均可
}

func (f Foo) second() {
<- f.ch1
print("second")
f.ch2 <- 2 // 存入任何值均可

}

func (f Foo) third() {
<- f.ch2
print("third")
}

func main() {
f := Foo{make(chan int), make(chan int)}
go f.first()
go f.second()
go f.third()
time.Sleep(10 * time.Second)
}

补充:

  1. 在默认情况下(不使用sync.WaitGroup, 不使用chan阻塞),main的goroutine在存在其他goroutine为执行的情况下也会自动退出,导致有的goroutine无法执行,即所有 goroutine 在 main() 函数结束时会一同结束。所以第一次执行的时候,不添加time.Sleep(10)会发生什么都打印不出来的情况。

  2. := 仅能在函数体内使用,函数外应用如下方式定义参数:

    1
    var test = "testing"

最后的最后,我有个疑问,根据我看到的代码,结构体好像使用指针居多,即我声明方法的习惯是:

1
2
3
func (f Foo) first() {
....
}

但是看到很多人都下面这样用:

1
2
3
func (f *Foo) first() {
....
}

这样有什么好处吗?

19. 删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

进阶:你能尝试使用一趟扫描实现吗?

这个算是很经典的一道双指针题目了。目前为止我们见过的双指针题目包括:

第11题“盛最多水的容器”,以及第15题“三数之和”。第11题与第15题均属于通过双指针来快速缩小搜索范围

本题算是双指针在链表上的基本操作了。

记得研究生面试的时候遇到过一个双指针问题,大概是判断一个链表有没有环的问题,当时没答出来,所以对链表的双指针印象深刻。

其实能想起使用双指针,这题就算解决了一半了。

本题涉及到Go的知识:

  1. 自定义类型

    “结构?对象?”,whatever

    (1) 如何初始化:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    type ListNode struct {
    Val int
    Next *ListNode
    }

    head := ListNode{13, nil}
    h := new(ListNode) // 返回的即是一个指针
    // head := make(ListNode, 1) // 编译不通过
    h1 := ListNode{Val: 14, Next: &ListNode{15, nil}}

    注意的是:**&ListNode 低层仍然会调用new(ListNode)**

    (2) 如何使用或取值:与Java, C类似,使用‘.’取值即可。

  2. 指针

    这一个算是在测试的时候发现的,关键字new返回的是结构体的指针

    指针与结构体的测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func main()  {
i := 12
myprint(&i)

// head := make(ListNode, 1) // 编译不通过
head := ListNode{13, nil}
myprintListNode(&head)

h := new(ListNode) // 返回的即是一个指针
h.Val = 14
myprintListNode(h)

h1 := ListNode{Val: 14, Next: &ListNode{15, nil}}
myprintListNode(&h1)

// &ListNode 低层仍然会调用new(ListNode)
}

func myprint(p *int) {
fmt.Println("&p值为:", &p) // 0xc000154018
fmt.Println("p值为:", p) // 0xc000128058
fmt.Println("*p值为:", *p) // 12
}

func myprintListNode(listnode *ListNode) {
fmt.Println("&listNode值为:", &listnode) // 0xc000006040
fmt.Println("listNode值为:", listnode) // &{13 <nil>}
fmt.Println("*listNode值为:", *listnode) // {13 <nil>}
}

回到题目上:

易错点:

(1) 链表有可能删除head节点,因此额外添加一个节点(你要抬杠不用添加当然也可以,但是需要额外的判断,判断链表为空并且判断链表的长度为n时的情况)

(2) 测试打印链表,其实如何初始化测试这个我有点疑虑,不知道如何快速初始化一个接口链表。除了for循环,想来没有方便的办法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package main

import "fmt"

func main() {
h := new(ListNode)
p := h
for i := 1; i<6; i++ {
temp := new(ListNode)
temp.Val = i
p.Next = temp
p = p.Next
}
printList(h.Next)
fmt.Println("-------")
ll := removeNthFromEnd(h.Next, 2)
printList(ll)
}

// 打印链表
func printList(head *ListNode) {
for i := head; i != nil; i = i.Next {
fmt.Println(i.Val)
}
}

type ListNode struct {
Val int
Next *ListNode
}

/**
* 基本策略:双指针,两个指针的间隔为n+1,当后一个指针遍历到链表尾部时,则前一个指针的后面一个元素就是到处第n个元素
*/
func removeNthFromEnd(head *ListNode, n int) *ListNode {
if head == nil {
return head
}

/**
* 最初没有设想到head被删除的所有情况,只想到n=1与链表长度为1的情况,
* head被删除的所有情况是:链表长度为n
* 在开始纠结了好久删除head的情况:其实额外添加一个节点能让问题简化
*/
h := new(ListNode)
h.Next = head

/**
* 初始化
*/
p, tail := h, head
/**
* 之所以将i不作为临时变量,是通过判断i与tail,判断在tail指针移动n步的过程中,是先tail为空,还是i先为n
*/
i := 0
for ; i < n && tail != nil; i++ {
tail = tail.Next
}
if tail == nil && i == n {
// 即链表长度为n
return head.Next
} else if tail == nil && i < n {
// 链表长度大于n
return head
}

/**
* 这里的问题:循环终止条件应该是到了尾部
*/
for tail != nil {
tail = tail.Next
p = p.Next
}

// 移除p的下一个节点
p.Next = p.Next.Next

return h.Next
}

总结:

  1. 链表head之前添加一个节点会方便许多,包括方便初始化与边界条件

第一章 关于Go语言的介绍

编译速度快,内置并发,自带垃圾回收器。

开发速度:

Go编译器只会关注直接被引用的库。而Java,C,C++要遍历依赖链中的所有库。很多Go程序可以在1秒内编译完成。

Go能捕获类型错误。

并发

goroutine类似线程,占用内存更少;channel允许用户在不同的goroutine之间同步发送具有类型的消息。即并发编程模型倾向于在goroutine之间发送消息,而不是让多个goroutine争夺同一数据的使用权。

gotourine是一个并行执行的函数,单一的操作系统线程可以执行多个goroutine。goroutine代码简洁。

1
go log("error happen") // 调度log函数作为独立的goroutine去运行

channel,通道,一种数据结构,让goroutine之间进行安全的数据通信,避免共享内存访问问题。保证同一时刻只会有一个goroutine修改数据。特别注意,通道不提供跨goroutine的数据访问保护机制,如果通道传输数据的副本,那么每个goroutine各自对自己的副本做修改是安全的;如果是指向数据的指针,如果读和写是由不同的goroutine完成的,每个goroutine仍旧需要额外的同步动作。

类型系统

无继承,使用“组合”设计模式,将一个类型嵌入到另一个类型,就能复用全部功能。允许用户对行为进行建模,而不是对类型进行建模。不需要声明某个类型实现了某个接口,编译器会判断一个类型的实例是否符合正在使用的接口。

用户自定义类型与C语言的结构很像。

接口用于描述类型的行为,如果一个类型实现了一个接口的所有方法,那么这个类型的实例就可以存储在这个接口类型的实例中,不需要额外声明。Go语言的接口只倾向于定义一个单一的动作。

你好,Go

goplayground 可再现运行与分享Go程序:

http://play.golang.org 需科学上网

https://goplay.space/

https://goplay.tools/

第二章 快速开始一个Go程序

程序架构

程序入口main函数,保存在main包中。一个包定义一组编译过的代码,包名即命名空间。

17. 电话号码的字母组合

一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

使用Go解决该问题涉及知识包括:

  1. Go中map的初始化与使用

    map官方文档

    map可以使用make或者常量来初始化:

    1
    2
    3
    4
    5
    6
    7
    a := make(map[string]string)
    a["key"] = "value"
    fmt.Println(a)
    b := map[string]string {
    "keyb": "valueb",
    }
    fmt.Println(b)
  2. for 循环控制

    用习惯for-range后,真的比 for-i简洁好看好多。不仅可以遍历数组,切片,也可以遍历map,字符串。不需要某个参数时,可以将以_代替。

    对于数组而言: 第一个参数表示索引,第二参数表示取值。对于map而言:第一个参数表示键,第二参数表示取值。

    1
    2
    3
    for _, letter := range letters {
    // loop
    }
  3. append 切片添加元素

    append可以向切片添加元素,并返回修改后的切片。

    1
    2
    slice = append(slice, elem1, elem2)
    slice = append(slice, anotherSlice...)
  4. Go的指针:

    这是我第一次使用Go的指针,按照C语言的方式使用,大概也就按照**&取地址,*取值**来操作。不多阐述。

回到题目上,这个题目个人认为官方将其复杂化了,或许是为了覆盖回溯算法这一考点吧。

算法1:三层循环,逐个追加。

其中的初始化 result := []string{""} 是我非常骄傲的一行代码 ^_^

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func letterCombinations(digits string) []string {
if len(digits) == 0 {
return []string{}
}
numToLetterMap := map[string]string{
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz",
}
result := []string{""}
for _, v := range digits {
letters := numToLetterMap[string(v)]
temp := make([]string, 0)
for _, str := range result {
for _, letter := range letters {
temp = append(temp, str + string(letter))
}
}
result = temp
}
return result
}

算法2: 回溯(递归)。借鉴了官方的算法逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func letterCombinations(digits string) []string {
if len(digits) == 0 {
return []string{}
}
numToLetterMap := map[string]string{
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz",
}
result := []string{}
backtrack(digits, 0, "", &result, numToLetterMap)
return result
}

func backtrack(d string, index int, combination string, result *[]string, m map[string]string) {
if index == len(d) {
*result = append(*result, combination)
} else {
num := string(d[index])
letters := m[num]
for i := 0; i < len(letters); i++ {
backtrack(d, index+1, combination+string(letters[i]), result, m)
}
}
}

使用Go刷了力扣17道题目,Go的基础知识了解的差不多了,但是完整的Go逻辑却还没有建立,因此想看一点Go书籍,把知识串起来,后续大概会发一下阅读笔记之类的。

16. 最接近的三数之和

给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。

1
2
3
输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/3sum-closest
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

直接做这种近似的题目,我是比较虚的,还好,我们有第15题的经验。

第15题三数之和为0的策略是:对数组进行从小到大排序,对于每个元素,使用双指针查找此元素后是否有两个元素与它之和为0

因此,对于本题,我们修改为:对数组进行从小到大排序,对于每个元素,使用双指针查找此元素后两个元素与它之和与target之差的绝对值最小

涉及到Go的小知识:

  1. math包: Go中的数学工具包

    math包 是Go中的数学工具包,包括一些常用常量与数学函数,常量包括π,自然对数e,最大最小整数,绝对值函数,求sin/cos/tan/log函数等,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const (
    MaxInt8 = 1<<7 - 1
    MinInt8 = -1 << 7
    MaxInt16 = 1<<15 - 1
    MinInt16 = -1 << 15
    MaxInt32 = 1<<31 - 1
    MinInt32 = -1 << 31
    MaxInt64 = 1<<63 - 1
    MinInt64 = -1 << 63
    MaxUint8 = 1<<8 - 1
    MaxUint16 = 1<<16 - 1
    MaxUint32 = 1<<32 - 1
    MaxUint64 = 1<<64 - 1
    )

    我们之前是如何定义最大32位整数的呢:

    1
    2
    3
    4
    5
    const MAX_UINT32 = ^uint32(0)
    const MIN_UINT32 = 0
    // 最大无符号去掉符号位
    const MAX_INT32 = int(MAX_UINT32 >> 1)
    const MIN_INT32 = - MAX_INT32 - 1

    本题我们使用到了math中的32位最大整数,以及求绝对值的函数

  2. sort对于数组排序的快捷用法:

    除第15题中,我们提到的sort.slice(slice []type, func)外,还有sort.Ints(x []int), sort.Float64(x []float64)等对数组便捷正序排序的函数。

回到题目上,容易出错的地方在

  1. 使用math.Abs函数,该函数是针对类型float64的,所以传参与返回参数均需要显式类型转换。
  2. 要看清题目最后返回的,并不是最小差距,而是三者之和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func threeSumClosest(nums []int, target int) int {
result := 0
distance := math.MaxInt32
sort.Ints(nums)
// fmt.Println(nums)
for i := 0; i < len(nums); i++ {
l := i+1
r := len(nums) - 1

for l < r {
sum := nums[i] + nums[l] + nums[r]
temp := sum - target
if temp == 0 {
return sum
}
// fmt.Printf("%d, %d, %d\n", nums[i], nums[l], nums[r])
// fmt.Printf("a[%d] + a[%d] + a[%d] = %d, temp = %d\n", i, l, r, nums[i] + nums[l] + nums[r], temp)
// math.Abs: func Abs(x float64) float64 参数与返回值均为float64,所以需要做显示类型转换
tempAbs := int(math.Abs(float64(temp)))
// fmt.Println(tempAbs)
if tempAbs < distance {
distance = tempAbs
result = sum
// fmt.Printf("after change, distance = %d, result = %d\n", distance, result)
}
if temp > 0 {
r --
}
if temp < 0 {
l ++
}
}
}
return result
}

15. 三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

1
2
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/3sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

  1. Go中的包-sort

    sort包官网

    talk is cheap, show me the code,先看最基础的代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import (
    "fmt"
    "sort" // 需引入包
    )

    a := []int{-1,0,1,2,-1,-4}
    sort.Slice(a, func (i, j int) bool { // 最基础的使用方式
    return a[i] < a[j]
    })
    fmt.Println(a)

    熟悉Java排序的小伙伴应该不会陌生,使用方式与Java中的Collection.sort类似,第二参数为func,定义数组元素之间的大小关系。因此,Go也可以对结构体数组进行排序。

  2. 结构体 struct:

    有C语言基础与面向对象基础的开发者应该很容易理解,比如我们可以将一个“人”包装为一个结构体:

    定义:

    1
    2
    3
    4
    type Person struct {
    name string
    age int
    }

    初始化:

    1
    2
    3
    4
    p := Person{"kevinQ", 30}
    fmt.Println(p) // {kevinQ 30}
    p2 := Person{name: "kevinQw"}
    fmt.Println(p2) // {kevinQw 0}

    结构体的使用:

    1
    2
    p.age = 20
    fmt.Println(p) // {kevinQ 20}

    sort如何对结构体数组进行排序呢,比如如何按照年龄对人员进行排序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    type SortByAge []Person
    func (array SortByAge) Len() int { return len(array) }
    func (array SortByAge) Swap(i, j int) { array[i], array[j] = array[j], array[i] }
    func (array SortByAge) Less(i, j int) bool { return array[i].age < array[j].age }

    func main() {
    p := Person{"old", 30}
    // fmt.Println(p)
    p2 := Person{name: "young", age: 20}
    pArray := []Person{p, p2}
    fmt.Println(pArray) // [{old 30} {young 20}]

    sort.Sort(SortByAge(pArray))
    fmt.Println(pArray) // [{young 20} {old 30}]
    }

    sort官方示例中还有另外三种包括SortKeys, SortMutiKeys, SortWrapper技术,以及各个方法示例,建议大家多code几遍,理解一下。

  3. append:添加元素

    append官方解释:给切片添加元素。

    1
    2
    3
    slice = append(slice, elem1, elem2)
    slice = append(slice, anotherSlice...)
    slice = append([]byte("hello "), "world"...)

回到题目上,我们采用的策略是,对数组进行从小到大排序,对于每个元素,使用双指针查找此元素后是否有两个元素与它之和为0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import "sort"

func threeSum(nums []int) [][]int {
result := make([][]int, 0)
if nums == nil || len(nums) < 3 {
return result
}
// 排序,后来查看答案可以更简便的使用sort.Ints
sort.Slice(nums, func (i, j int) bool {
return nums[i] < nums[j]
})
// fmt.Println(nums)
for i := 0; i < len(nums); i++ {
if nums[i] > 0 {
break
}
if i > 0 && nums[i] == nums[i-1] {
// 下一个数字与此数字相同
continue
}
// fmt.Printf("正在检查:a[%d]= %d\n", i, nums[i])
// fmt.Printf
l := i+1
r := len(nums) - 1
// fmt.Printf("l = %d, r = %d, a[l] = %d, a[r] = %d\n", l, r, nums[l], nums[r])
for l < r {
sum := nums[i] + nums[l] + nums[r]
if sum == 0 {
temp := []int{nums[i], nums[l], nums[r]}
result = append(result, temp)
// 查找下一个
for l < r && nums[l] == nums[l+1] {
l ++
}
l ++
for l < r && nums[r] == nums[r-1] {
r --
}
r --
}
if sum > 0 {
r --
}
if sum < 0 {
l ++
}

}
}
return result
}

只学过C,C++,Java,Python,JavaScript,一点点Rust,表示第一次见到这么个关键字,一头慌慌哒的雾水~~

个人是这么理解的:const内部的递增行号,const每行复制上一行的表达形式。我也在学习阶段,不一定准确。

官方给出的iota的测试用例链接为:https://golang.org/test/iota.go,熟悉这些测试用例,iota的知识点就应该算是掌握了。

例如,官方测试用例中的两个样例:重点关注下述代码中的F

1
2
3
4
5
6
7
8
9
10
11
const (
A = 1 << iota // iota = 0
B // iota = 1, 1 << iota 即 1 << 1 = 2
C // iota = 2, 1 << iota 即 1 << 2 = 4
D // iota = 3, 1 << iota 即 1 << 3 = 8
E = iota * iota // iota = 4, 16
F // iota = 5, 复制上一行的形式,iota * iota = 25
G // iota = 6, 复制上一行的形式,iota * iota = 36
)


再比如:

1
2
3
4
const (
s = string(iota + 'a') // iota = 0, s = "a"
t // t = string(iota + 'a') // iota = 1, t = "b"
)

需要注意的几点:

  1. iota表示当前内部的行号, 从0开始,即使第一行没有写明iota。

    1
    2
    3
    4
    const (
    a = 1 // 1, iota = 0
    b = iota // iota=1
    )
  2. iota复制上一行的表达式,除iota本身外,其他均为明确的数字:

    上述两点,可以看以下示例:重点关注示例中的d

    1
    2
    3
    4
    5
    6
    const (
    a = 1 // 1, iota = 0
    b = iota << a // iota=1, 实际表达式为 1 << 1 = 2
    c = iota << b // iota=2, 实际表达式为 2 << 2 = 4
    d // iota = 3, 模仿上一行的表达式为 iota << 2 即 3 << 2 = 12
    )
  3. iota在下一行增长

    1
    2
    3
    4
    5
    const (
    abit, amask = 1 << iota, 1<<iota - 1 // iota= 0, abit = 1, amask = 0
    bbit, bmask = 1 << iota, 1<<iota - 1 // iota= 1, bbit = 2, bmask = 1
    cbit, cmask = 1 << iota, 1<<iota - 1 // iota= 2, cbit = 4, cmask = 3
    )

PS: 我本地执行程序时,将文件名命名为了iota_test.go,执行 go run oita_test.go 报错为:go run: cannot run *_test.go files (iota_test.go)。

据说是因为_test后缀结尾的会被认为是测试文件。未涉及到这部分,暂且略过。

PS: 果然//对齐才好看。

14. 最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 ""

1
2
3
4
5
6
输入:strs = ["flower","flow","flight"]
输出:"fl"

输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-common-prefix
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

第一想法,首先取最短子串Si,然后逐位递减,依次判断子串是否是公共前缀。

涉及到Go的知识包括:

  1. Go数组基本操作:如何遍历数组查找最短子串?for的三种方式:我分别称呼为正常for, 替换while的for,for-range

    (1)正常for:

    1
    2
    3
    4
    a := []int{0, 1, 2, 3, 4}
    for i := 0; i < len(a); i++ {
    fmt.Println(a[i])
    }

    (2)替换while的for:

    1
    2
    3
    4
    5
    6
    numLessThan3 := 0
    for numLessThan3 < 3 {
    numLessThan3++
    fmt.Println(numLessThan3)
    }
    fmt.Println("over...")

    (3)for-range:可针对数组,切片,map等进行遍历操作,当不需要某个值时可用“_”代替。了解了一种新类型channel,暂略不表。

    对于数组而言: 第一个参数表示索引,第二参数表示取值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 数组
    a := []int{034, 122, 222, 3222, 2234}
    for i, v := range a {
    fmt.Println(i)
    fmt.Println(v)
    }
    for _, v := range a {
    //fmt.Println(i)
    fmt.Println(v)
    }

    对于map而言:第一个参数表示键,第二参数表示取值。

    1
    2
    3
    4
    5
    6
    7
    8
    // map
    a := make(map[string]string, 2)
    a["k1"] = "value1"
    a["k2"] = "value2"
    for k, v := range a {
    fmt.Println(k)
    fmt.Println(v)
    }
  2. 如何从一个字符串中截取部分作为一个新的字符串:切片,类似与python中的切片,又略有不同, Go不支持倒数切片。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    a := []int{0, 1, 2, 3, 4}
    fmt.Println(a) // [0 1 2 3 4]
    fmt.Println(a[0:2]) // [0 1]
    fmt.Println(a[:2]) // [0 1]
    fmt.Println(a[2:4]) // [2 3]
    // fmt.Println(a[2:-1]) invalid slice index -1 (index must be non-negative)
    fmt.Println(a[2:]) // [2 3 4]

    // 倒数切片
    // fmt.Println(a[-2:]) invalid slice index -2 (index must be non-negative)
  3. 如何判断一个字符串是另外一个字符串的前缀:

    我们可以通过循环判断字符串的每个字符是否相同,也可以通过strings.HasPrefix(s, prefix string)来判断。

回到问题上,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import (
"fmt"
"strings"
)

func main() {
strs := []string{"a", "a23", "abc"}
fmt.Println(longestCommonPrefix(strs))
}

func longestCommonPrefix(strs []string) string {
// 我第一次跑的时候遗忘了边界条件
if len(strs) == 0 {
return ""
}

// 查找最短子串
shortestStr, shortestLength := strs[0], len(strs[0])
for _, value := range strs {
if len(value) < shortestLength {
shortestLength = len(value)
shortestStr = value
}
}


for i := shortestLength; i > 0; i-- {
if isCommonPrefix(shortestStr[:i], strs) {
return shortestStr[:i]
}
}

return ""
}


func isCommonPrefix(prefix string, strs []string) bool {
// 逐位判断,或者使用strings.HasPrefix函数
for _, value := range strs {
if !strings.HasPrefix(value, prefix) {
return false
}
}
return true
}

最后问个问题:fmt.Println(034) 会输出什么?

提示一下:0-开头的数字会被认为是8进制。