1.递归的考察
请说明下面这段代码的输出结果。 2023.08.25
1
2
3
4
5
6
7
8
9
10
11
|
func main() {
test(4)
}
func test(n int) {
if n > 2 {
n--
test(n)
}
fmt.Println("n=", n)
}
|
解析:
1.第1次调用:test(4)
n=4,n>2,进入 if 语句块:
n–:n=3
test(3):调用test(3)
2.第2次调用:test(3)
n=3,n>2,进入 if 语句块:
n–:n=2
test(2):调用test(2)
3.第3次调用:test(2)
n=2,n>2,不满足 if 条件,往下执行输出语句:
fmt.Println(“n=”, n):输出n=2
【注意】到这里,整个递归调用的过程并没有结束,而是回到前一个递归层次(这是很容易出错的地方)。
4.回到第2次调用,继续执行下面的输出语句:
fmt.Println(“n=”, n):输出n=2(注意,这里的局部变量n值为2)
再回到前一个递归层次。
5.回到第1次调用,继续执行下面的输出语句:
fmt.Println(“n=”, n):输出n=3
这已经是第1次调用了,整个递归调用结束。
综上,代码的输出结果为:
注意,虽然 test
函数在递归时修改了 n
的值,但每个递归调用都有自己的局部变量副本,因此修改不会影响到其他调用。
2.闭包的使用
2023.08.25
- 编写一个函数 makeSuffix(suffix string) 可以接收一个文件后缀名(比如jpg),并返回一个闭包。
- 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg) ,则返回 文件名.jpg,如果已经有.jpg后缀,则返回原文件名。
- 要求使用闭包的方式完成。
- strings.HasSuffix:func HasSuffix(s, suffix string) bool,判断s是否有后缀字符串suffix。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func main() {
mySuffix := makeSuffix(".jpg") // 先获得一个闭包
fmt.Println(mySuffix("hello.jpg")) // hello.jpg
fmt.Println(mySuffix("winter")) // winter.jpg
fmt.Println(mySuffix("pic.png")) // pic.png.jpg
}
func makeSuffix(suffix string) func(string) string {
return func(name string) string {
if strings.HasSuffix(name, suffix) {
return name
} else {
return name + suffix
}
}
}
|
3.冒泡排序
2023.08.29
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 冒泡排序:每轮使1个数在正确的位置上,需要双层循环
// 外层循环:控制循环的轮数,每轮排好1个数
// 内层循环:从外层循环的i值开始,表示第i个元素是即将需要参与排序的数
func bubbleSort(arr *[10]int) {
for i := 0; i < len(arr)-1; i++ { // 控制循环的轮数
for j := i; j > 0; j-- { // // 控制实际参与排序的数字
if arr[j] < arr[j-1] {
temp := arr[j]
arr[j] = arr[j-1]
arr[j-1] = temp
}
}
}
}
|
4.快速排序
2023.08.29
2023.10.14 补
需要额外分配空间的版本:
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 quickSort(arr []int) []int {
if len(arr) <= 1 {
return arr
}
pivot := arr[0] // 选择第一个元素作为基准
var left, right []int
for i := 1; i < len(arr); i++ {
if arr[i] < pivot {
left = append(left, arr[i])
} else {
right = append(right, arr[i])
}
}
left = quickSort(left)
right = quickSort(right)
return append(append(left, pivot), right...)
}
func main() {
// 待排序的切片
arr := []int{64, 34, 25, 12, 22, 11, 90}
fmt.Println("未排序的数组:", arr)
arr = quickSort(arr)
fmt.Println("排序后的数组:", arr)
}
|
这种方式比较好理解,但需要额外分配空间,不是原地进行交换,空间和时间上的性能较差。
推荐使用下面这种原地(In-Place)交换的方式,具有更高的性能。
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 main() {
arr := []int{3, 8, 1, 5, 4, 7, 6, 9, 2}
QuickSort2(0, len(arr)-1, arr)
fmt.Println(arr)
}
func QuickSort2(low, high int, arr []int) {
if low < high {
pivotIndex := partition(low, high, arr) // 选择分区点并获取分区点的索引
QuickSort2(low, pivotIndex, arr) // 递归排序分区点左侧的子数组
QuickSort2(pivotIndex+1, high, arr) // 递归排序分区点右侧的子数组
}
}
// partition 将数组分成两部分,左侧的元素都小于或等于基准元素,右侧的元素都大于基准元素。
// 这是快速排序算法中的关键步骤,它在原地(in-place)完成,不需要额外的内存空间。
func partition(low, high int, arr []int) int {
pivot := arr[(low+high)/2] // 选择最中间的元素作为基准
i, j := low, high
for {
for arr[i] < pivot {
i++
}
for arr[j] > pivot {
j--
}
if i >= j {
return j
}
arr[i], arr[j] = arr[j], arr[i]
i++
j--
}
}
|
5.二分查找
2023.08.29
递归实现:
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
|
package main
import "fmt"
func main() {
arr := [...]int{1, 2, 3, 4, 5, 6, 7, 8}
binarySearch(&arr, 0, len(arr)-1, 8)
}
func binarySearch(arr *[8]int, leftIndex int, rightIndex int, findElem int) {
// 当leftIndex大于rightIndex时,表示未找到指定元素
if leftIndex > rightIndex {
fmt.Println("找不到")
return
}
middleIndex := (leftIndex + rightIndex) / 2
middleElem := arr[middleIndex]
if findElem < middleElem {
binarySearch(arr, leftIndex, middleIndex-1, findElem)
} else if findElem > middleElem {
binarySearch(arr, middleIndex+1, rightIndex, findElem)
} else {
fmt.Println("找到了,下标为:", middleIndex)
}
}
|
非递归实现:(2023.09.03补)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 二分查找,一定要注意区间的定义,是左闭右闭还是左闭右开。
// 这里我们采用左闭右闭。
func search(nums []int, target int) int {
left := 0
right := len(nums) - 1
for left <= right {
middle := (left + right) / 2
if target < nums[middle] {
right = middle - 1
} else if target > nums[middle] {
left = middle + 1
} else {
return middle
}
}
return -1
}
|
6.自定义排序
2023.08.30
使用 sort.Sort(data Interface),实现对 Student 结构体切片的排序。
Student 结构体有三个字段:name string, age int, score int。
Student 结构体切片中存有多个 Student 对象,将这些对象按照 age 字段升序排列后遍历输出。
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
|
package main
import (
"fmt"
"math/rand"
"sort"
"strconv"
)
type Student struct {
name string
age int
score int
}
// StuSlice 定义一个切片类型,存放多个学生
type StuSlice []Student
// 实现Interface接口中的三个方法:Len、Less、Swap
func (s StuSlice) Len() int {
return len(s)
}
func (s StuSlice) Less(i, j int) bool {
return s[i].age < s[j].age
}
// Swap 将切片中的元素进行交换
func (s StuSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func main() {
s1 := StuSlice{}
for i := 0; i < 10; i++ {
s1 = append(s1, Student{"stu" + strconv.Itoa(i),
int(float32(rand.Intn(101))*0.1 + 15), // 生成15-25之间的随机数
rand.Intn(101)}) // 生成0-100之间的随机数
}
for i := 0; i < 10; i++ {
fmt.Printf("%v\n", s1[i])
}
fmt.Println("-----排序后-----")
sort.Sort(s1)
for i := 0; i < 10; i++ {
fmt.Printf("%v\n", s1[i])
}
}
|
7.连续因子
2023.09.03
一个正整数 N 的因子中可能存在若干连续的数字。例如 630 可以分解为 3×5×6×7,其中 5、6、7 就是 3 个连续的数字。给定任一正整数 N,要求编写程序求出最长连续因子的个数,并输出最小的连续因子序列。
输入格式:
输入在一行中给出一个正整数 N(1<N<231)。
输出格式:
首先在第 1 行输出最长连续因子的个数;然后在第 2 行中按 因子1*因子2*……*因子k
的格式输出最小的连续因子序列,其中因子按递增顺序输出,1 不算在内。
输入样例:
输出样例:
自己做的本题解答:
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
|
func main() {
var num int
fmt.Scanln(&num)
count, factorSlice := test(num)
fmt.Println(count)
var resultStr string
for i := 0; i < len(factorSlice); i++ {
if i < len(factorSlice)-1 {
resultStr += strconv.Itoa(factorSlice[i]) + "*"
} else {
resultStr += strconv.Itoa(factorSlice[i])
}
}
fmt.Println(resultStr)
}
func test(num int) (int, []int) {
// 先遍历出所有小于根号num的因子
var factor []int
for i := 2; i*i < num; i++ {
if num%i == 0 {
factor = append(factor, i)
// fmt.Printf("%d ", i)
}
}
// 然后遍历因子列表,统计最大的连续因子的数量
count := 1
maxLength := 1
for i := 0; i < len(factor)-1; i++ {
if factor[i+1] == factor[i]+1 {
count++
} else {
if count > maxLength {
maxLength = count
}
count = 1
}
}
// 然后再次遍历因子列表,求得连续的因子序列
var resultSlice []int
for i := 0; i < len(factor); i++ {
if factor[i]+maxLength-1 == factor[i+maxLength-1] {
resultSlice = factor[i : i+maxLength]
break
}
}
return maxLength, resultSlice
}
|
个人总结:在求解过程中,进行了3次for循环,总觉得这不是最优解,应该还可以更进一步优化。
8.查验身份证
2023.09.04
L1-016 查验身份证
一个合法的身份证号码由17位地区、日期编号和顺序编号加1位校验码组成。校验码的计算规则如下:
首先对前17位数字加权求和,权重分配为:{7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2};然后将计算的和对11取模得到值Z
;最后按照以下关系对应Z
值与校验码M
的值:
1
2
|
Z:0 1 2 3 4 5 6 7 8 9 10
M:1 0 X 9 8 7 6 5 4 3 2
|
现在给定一些身份证号码,请你验证校验码的有效性,并输出有问题的号码。
输入格式:
输入第一行给出正整数N(≤100)是输入的身份证号码的个数。随后N行,每行给出1个18位身份证号码。
输出格式:
按照输入的顺序每行输出1个有问题的身份证号码。这里并不检验前17位是否合理,只检查前17位是否全为数字且最后1位校验码计算准确。如果所有号码都正常,则输出All passed
。
输入样例1:
1
2
3
4
5
|
4
320124198808240056
12010X198901011234
110108196711301866
37070419881216001X
|
输出样例1:
1
2
3
|
12010X198901011234
110108196711301866
37070419881216001X
|
输入样例2:
1
2
3
|
2
320124198808240056
110108196711301862
|
输出样例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
36
37
38
39
40
41
42
43
44
45
46
|
package main
import (
"fmt"
"reflect"
"strconv"
)
func main() {
var numCount int
allPass := true
fmt.Scanln(&numCount)
var idCardList = make([]string, numCount)
for i := 0; i < numCount; i++ {
var idCard string
fmt.Scanln(&idCard)
idCardList[i] = idCard
}
for _, idCard := range idCardList {
if !identity(idCard) {
fmt.Println(idCard)
allPass = false
}
}
if allPass {
fmt.Println("All passed")
}
}
func identity(str string) bool {
lenStr := len(str)
lastStr := string(str[lenStr-1]) // 最后一个字符
sum := 0
weightArray := []int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}
mArray := []string{"1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"}
for i := 0; i < lenStr-1; i++ { // 注意,这里不包括最后一位
indexNum, err := strconv.Atoi(string(str[i])) // 注意:这里的str[i]为byte(uint8),直接转为int得到的是其ascii码的值,如数字1得到的是49。所以要先转换成string,然后再将string转换成int。
if err != nil {
return false
}
sum += indexNum * weightArray[i] // 每位的值分别与权重相乘
}
z := sum % 11
return mArray[z] == lastStr // 最后一位与加权运算得到的结果是否一致
}
|
几个需要注意的细节点:
-
字符串中的数字截取
如:
1
2
3
|
str := "123456"
fmt.Println(str[0]) // 49
fmt.Println(reflect.TypeOf(str[0])) // uint8,即byte
|
输出的结果为49
,为什么呢?
因为str[0]
截取字符串得到的类型是byte
类型(uint8)
。当打印值的时候,打印的是其ASCII
码值。
所以如果我们想从由数字组成的字符串中截取每一个数字的话,就不能直接使用int()
函数去转成int
类型,因为这样只会得到这个字符的ASCII
码值。正确的做法为:
先将str[0]
转换为string
类型,然后再将该string
通过strconv.Atoi()
函数转换为int
类型。即:
1
2
3
4
5
|
num, err := strconv.Atoi(string(str[0]))
if err != nil {
return
}
fmt.Println(num)
|
-
strconv.Atoi()函数的使用
strconv.Atoi()函数的作用是将由数字组成的字符串转换成int类型。其有两个返回值,第一个返回值为转换后得到的值,第二个返回值为 error。
注意,一定要是由数字(必须是整数,小数也不行)的字符串,否则第一个参数会返回0。
建议将第二个参数接收过来,并用if语句去判断是否出错,否则可能得到的不是我们想要的结果。如:
1
2
|
num, _ := strconv.Atoi("12.23")
fmt.Println(num) // 0
|
这里我们将error隐式处理了,但是返回的结果却不是我们想要的,而且也没有报错信息(这是比较致命的)。这无疑给代码的排错增加了难度。
所以建议一定要对error进行处理。
9.字符串转换为整数
2023.09.25
题目描述:实现一个函数 Atoi(s string) (int64, error)
,该函数将一个字符串 s
解析为一个带符号的整数,并返回解析后的整数值和错误。如果解析失败,应返回适当的错误。
要求:
- 函数的输入参数
s
是一个字符串,表示要解析的整数。
- 函数应能处理正整数和负整数。
- 函数的返回值应为解析后的整数和一个错误。如果解析成功,错误应该为
nil
,否则应包含错误信息。
- 不考虑超出
int64
范围越界的问题。
示例:
1
2
3
4
5
6
7
8
|
val, err := Atoi("123")
// val 应该为 123,err 应该为 nil
val, err := Atoi("-456")
// val 应该为 -456,err 应该为 nil
val, err := Atoi("12a34")
// val 应该为 0,err 应该包含合适的错误信息
|
请实现上述要求并编写 Atoi
函数的完整代码。
参考解答:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func Atoi(s string) (int64, error) {
s0 := s
if s[0] == '-' || s[0] == '+' {
s = s[1:]
}
var n int64
for _, ch := range []byte(s) { // 将s转为byte的切片,并遍历
ch -= '0' // 将一个ch(Unicode码点)转换为相应的整数值。如果ch包含字符'3',执行ch-='0'之后,ch将变成整数3。
if ch > 9 { // 若ch大于9,则表示该字符非法
return 0, errors.New("`" + string(ch+'0') + "`为非法字符,类型转换出错!")
}
n = n*10 + int64(ch)
}
if s0[0] == '-' {
n = -n
}
return n, nil
}
|
10.单例模式
2023.10.07
手写一个单例模式的代码,并测试创建实例的函数创建的实例对象都是同一个。
参考代码:
使用Once:
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
|
type Singleton struct {}
var singleInstance *Singleton
var once sync.Once
func GetSingletonObj() *Singleton {
once.Do(func() { // 只执行一次
fmt.Println("Create Obj")
singleInstance = new(Singleton)
})
return singleInstance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
obj := GetSingletonObj()
fmt.Printf("%x\n", unsafe.Pointer(obj)) // 可以看到,这里打印的地址值是相同的,说明只创建了一个实例对象。
wg.Done()
}()
}
wg.Wait()
}
|
不使用Once,使用双重互斥锁+饿汉式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
var mu sync.Mutex
var instance *Singleton
type Singleton struct {
// 在这里定义单例的属性
Value int
}
// GetInstance 返回单例实例
func GetInstance() *Singleton {
if instance == nil {
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &Singleton{
Value: 42, // 这里可以初始化单例的属性
}
}
}
return instance
}
|
11.循环定时任务
2023.10.07
创建一个可以循环执行的定时任务,要求可以指定任务的次数,当达到指定执行次数时,程序退出。
参考代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
var (
wg sync.WaitGroup
taskNum = 5
)
func main() {
ticker := time.NewTicker(time.Second) // 使用time.NewTicker()的方式,每隔1s执行一次定时任务
count := 0
wg.Add(1)
go func() {
defer wg.Done()
defer ticker.Stop()
for {
count++
t := <-ticker.C
fmt.Println(t)
if count >= taskNum { // 控制定时任务执行的次数
return
}
}
}()
wg.Wait()
fmt.Println("程序结束!")
}
|
12.对象池
2023.10.07
实现一个简单的对象池的功能,要求:
- 通过
NewObjPool(numOfObj int)
函数设置连接池的大小,并初始化连接池。
- 通过
GetObj(timeout time.Duration)
可以获取一个连接池对象,timeout
参数为指定的超时时间(当操作指定时间还没有获取到连接池对象时,报超时的错误)。
- 通过
ReleaseObj(obj *ReusableObj)
函数可以将一个连接池对象放入连接池中。
- 测试连接池的所有功能。
思考使用什么数据结构可以更好地实现连接池?
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
|
type ReusableObj struct {}
type ObjPool struct {
bufChan chan *ReusableObj
}
func NewObjPool(numOfObj int) *ObjPool {
objPool := ObjPool{}
objPool.bufChan = make(chan *ReusableObj, numOfObj)
for i := 0; i < numOfObj; i++ {
objPool.bufChan <- &ReusableObj{}
}
return &objPool
}
func (p *ObjPool) GetObj(timeout time.Duration) (*ReusableObj, error) {
select {
case ret := <-p.bufChan:
return ret, nil
case <-time.After(timeout): // 超时控制
return nil, errors.New("time out")
}
}
func (p *ObjPool) ReleaseObj(obj *ReusableObj) error {
select {
case p.bufChan <- obj:
return nil
default:
return errors.New("overflow")
}
}
func main() {
pools := NewObjPool(10)
for i := 0; i < 10; i++ { // 在不放回的情况下,如果取的数量超过了连接池中连接的数量,会报超时的错误。
if v, err := pools.GetObj(time.Second); err != nil {
panic(err)
} else {
fmt.Println("v:", unsafe.Pointer(v))
// 对连接进行的操作....
if err := pools.ReleaseObj(v); err != nil { // 操作完成后,将连接放回连接池
panic(err)
}
}
}
fmt.Println("Done")
}
|
13.生成随机字符串
2023.10.07
定义一个GetRandomString(n int)函数,生成随机的字符串(包含数字和小写英文字母),可以指定字符串的位数n。
参考代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func main() {
result := GetRandomString(4) // 生成长度为4的随机字符串
fmt.Println("result:", result)
}
func GetRandomString(n int) string { // 这里的n代表生成的字符串长度
str := "0123456789abcdefghijklmnopqrstuvwxyz" // 可选的字符集
var result []byte
r := rand.New(rand.NewSource(time.Now().UnixNano())) // 指定随机种子
for i := 0; i < n; i++ {
result = append(result, str[r.Intn(len(str)-1)])
}
return string(result)
}
|
14.删除数组中的重复元素
2023.10.07
定义函数removeDuplicates(array []int)
,删除数组中的重复元素。
参考代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
func main() {
array := []int{1, 6, 6, 8, 6, 1, 2, 2, 2}
res := removeDuplicates(array)
fmt.Println(res) // [1 6 8 2]
}
// 删除重复元素
func removeDuplicates(array []int) []int {
mapData := make(map[int]bool)
var result []int
for _, value := range array {
_, exist := mapData[value] // 使用map探测重复元素
if !exist {
mapData[value] = true
result = append(result, value)
}
}
return result
}
|
15.反射的最佳实践
2023.10.07
使用反射的方式来获取结构体的tag标签,遍历字段的值,修改字段值,调用结构体方法。
具体的结构体和结构体方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
type Mankind struct {
Name string `json:"name"`
Age int `json:"age"`
Gender string `json:"gender"`
}
func (m *Mankind) Introduce() {
fmt.Printf("My name is %v, %v years old!\n", m.Name, m.Age)
}
func main() {
m1 := Mankind{"Jerry", 25, "male"}
// 定义名为callStructure的函数,完成题目要求的功能。
}
func callStructure() {
|
参考代码:
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
|
type Mankind struct {
Name string `json:"name"`
Age int `json:"age"`
Gender string `json:"gender"`
}
func (m *Mankind) Introduce() { // 使用指针接收者的方法,在反射调用时和普通的方法略有不同
fmt.Printf("My name is %v, %v years old!\n", m.Name, m.Age)
}
func main() {
m1 := Mankind{"Jerry", 25, "male"}
callStructure(&m1) // 要修改属性值,这里必须传入地址
}
func callStructure(m *Mankind) { // 指针方式接收形参
structValue := reflect.ValueOf(m).Elem() // “解引用”
// 遍历属性和标签
for i := 0; i < structValue.NumField(); i++ {
field := structValue.Field(i)
fieldName := structValue.Type().Field(i).Name
tag := structValue.Type().Field(i).Tag.Get("json")
fmt.Printf("field[%v] is %v, value is %v, tag is %v\n", i, fieldName, field, tag)
// 设置属性值
if field.CanSet() {
if fieldName == "Name" { // 根据字段名修改字段值
field.SetString("Alice")
}
if fieldName == "Age" {
field.SetInt(28)
}
if fieldName == "Gender" {
field.SetString("female")
}
}
}
fmt.Println("属性值修改成功!")
// 调用结构体方法
// method := structValue.MethodByName("Introduce") // 这样写无法调用指针接收者方法Introduce
method := reflect.ValueOf(m).MethodByName("Introduce") // 由于Introduce是指针接收者方法,所以这里应该用指针(解引用之前)来调用该方法。
if method.IsValid() {
method.Call(nil)
} else {
fmt.Println("方法调用失败!")
}
}
|
16.单向环形队列
2023.10.11
使用数组模拟实现一个单向的环形队列,只要不超过队列的最大长度,就可以一直利用数组队列的空间,不断地进行Push和Pop操作,要求实现三个方法:Push(添加元素)、Pop(取出元素)、ShowQueue(显示队列所有元素)。
参考代码:
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
type CircleQueue struct {
maxSize int
array [5]int
head int // head指向下一个弹出元素的位置
tail int // tail指向下一个添加元素的位置
}
// Push 向队列中添加元素
func (queue *CircleQueue) Push(data int) (err error) {
if (queue.tail+1)%queue.maxSize == queue.head { // 队列已满
return errors.New("queue full")
}
queue.array[queue.tail] = data
queue.tail = (queue.tail + 1) % queue.maxSize
return
}
// Pop 从队列中弹出元素
func (queue *CircleQueue) Pop() (data int, err error) {
if queue.head == queue.tail { // 队列为空
return -1, errors.New("queue empty")
}
data = queue.array[queue.head]
queue.head = (queue.head + 1) % queue.maxSize
return data, nil
}
func (queue *CircleQueue) ShowCircleQueue() {
queueLen := (queue.tail - queue.head + queue.maxSize) % queue.maxSize // 队列中元素个数
if queueLen == 0 {
fmt.Println("queue is empty")
return
}
originalHead := queue.head
for i := 1; i <= queueLen; i++ { // 遍历队列长度个元素
fmt.Printf("array[%d]=%d\t", i, queue.array[originalHead])
originalHead = (originalHead + 1) % queue.maxSize
}
fmt.Println()
}
func main() {
// 先创建一个队列
queue := CircleQueue{
maxSize: 5,
head: 0,
tail: 0,
}
// 添加数据
var key string
var val int
fmt.Println("1. 输入push 表示添加数据到队列")
fmt.Println("2. 输入pop 表示从队列中获取数据")
fmt.Println("3. 输入show 表示显示队列")
fmt.Println("4. 输入exit 表示退出")
for {
fmt.Printf("请输入指令:")
if _, err := fmt.Scanln(&key); err != nil {
panic(err)
}
keyMap := map[string]struct{}{"pop": struct{}{}, "push": struct{}{}, "show": struct{}{}, "exit": struct{}{}}
if _, exist := keyMap[key]; !exist { // 输入的指令不正确
fmt.Println("请输入正确的指令(add/get/show/exit)!")
continue
}
switch key {
case "push":
fmt.Printf("输入需要添加到队列的数据:")
_, err := fmt.Scanln(&val)
if err != nil {
panic(err)
}
if err := queue.Push(val); err != nil {
fmt.Println(err.Error())
} else {
fmt.Println("加入队列成功!")
}
case "pop":
data, err := queue.Pop()
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Printf("取出元素:%d\n", data)
}
case "show":
fmt.Printf("当前队列为:")
queue.ShowCircleQueue()
case "exit":
os.Exit(0)
}
}
}
|
17.约瑟夫问题
2023.10.12
题目描述:
你需要实现一个Josephus环,该环包含一定数量的成员。每个成员都有一个唯一的ID。
要求:
- 编写函数
InitRing
,来初始化Josephus环。
- 编写函数
GetRingElems
,打印出Josephus环中所有的成员ID。
- 编写函数
PlayGame
,以模拟Josephus问题的解决过程。
具体要求如下:
- 从环中的第
startNo
号成员开始,数数,数到第 countNo
号成员后,将其从环中淘汰。
- 淘汰后,重新从下一个成员开始数,继续数
countNo
个成员,再淘汰。
- 重复上述过程,直到只剩下一个成员为止。
三个函数的签名如下:
1
2
3
4
5
6
|
func InitRing(num int) *JosephusRing{} // num:初始化成员的数量
func GetRingElems(first *JosephusRing) {} // first:Josephus环的第一个成员的指针
func PlayGame(first *JosephusRing, startNo int, countNo int) {}
// first:Josephus环的第一个成员的指针
// startNo:开始计数的成员的ID号(从1开始)。
// countNo:每次数数的步数。
|
参考代码:
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
80
81
82
83
84
85
86
87
88
89
|
type JosephusRing struct {
id int
next *JosephusRing
}
func InitRing(num int) *JosephusRing { // 指定成员的个数进行初始化环
first := &JosephusRing{}
last := &JosephusRing{}
if num < 1 {
fmt.Println("成员数量至少为1!")
return nil
}
for i := 1; i <= num; i++ {
rData := &JosephusRing{
id: i,
}
if i == 1 { // 第一个成员
first = rData // first指向链表的第一个元素,此后就不再改变
last = rData // 通过移动last来实现链表元素的增长,初始值为第一个元素
last.next = first // 将新加入的元素指向first
} else {
last.next = rData // 将新的元素加入链表,并将last指向新的元素
rData.next = first
last = rData
}
}
return first
}
func GetRingElems(first *JosephusRing) {
if first.next == nil {
fmt.Println("链表为空,没有任何成员!")
return
}
last := first
fmt.Println()
for {
fmt.Printf("[id=%d]--->", last.id)
if last.next == first { // 遍历到最后一个元素了
break
}
last = last.next
}
}
func PlayGame(first *JosephusRing, startNo int, countNo int) {
if first.next == nil {
fmt.Println("链表为空!")
return
}
last := first // 需要一个last指针指向最后(也就是first之前的那个元素),因为要删除first结点,必须要找打它的前一个结点
for { // 将last指向最后的元素
if last.next == first {
break
}
last = last.next
}
// 让first和last都移动startNo-1次(移动到起始位置)(因为startNo表示的是开始的编号,但first已经指向第1个元素了,所以只需要移动startNo-1次)
for i := 1; i <= startNo-1; i++ {
first = first.next
last = last.next
}
// 游戏开始,开始数数(countNo)
// first始终指向的是要删除的成员
for {
// 移动countNo-1次(因为成员自身也算一次)
for i := 1; i <= countNo-1; i++ {
first = first.next
last = last.next
}
// 删除first指向的结点
fmt.Printf("\nid=%d的元素被删除!", first.id)
first = first.next
last.next = first
// 循环的终止条件:链表中只有一个元素(first=last)
if last == first {
break
}
}
fmt.Printf("\nid=%d的元素被删除!", first.id)
}
func main() {
first := InitRing(5)
GetRingElems(first)
PlayGame(first, 2, 3)
}
|
18.选择排序
2023.10.14
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
/**
从小到大排序
选择排序:每轮选择一个无需中的最小值,n-1轮后,数组有序。
技巧:先写内层循环,然后再嵌套一个外层循环。
先假设我们只需要找一次最小值,具体操作如下:
1.假设arr[0]就是最小元素(min=arr[0]);(注意:需要使用一个变量index记录下标0,后面交换的时候要用到)
2.遍历数组的每个元素,依次与min进行比较,如果有比min值更小的,则更新min的值和下标index;
3.交换arr[0]和arr[index]的值。
这样就完成了一次最小值的排序,我们只需要在外面嵌套一层for循环,将0改成循环变量j即可。这样就大大简化了我们写代码的过程。
*/
func main() {
arr := []int{3, 8, 1, 5, 4, 7, 6, 9, 2}
result := SelectionSort(arr)
fmt.Println(result)
}
func SelectionSort(arr []int) []int {
newArr := arr
for j := 0; j < len(arr)-1; j++ {
//minArr := newArr[j] // 之前用了一个变量来存最小值,后面发现可以优化一下,只需要记录下标minIndex就可以了,所以将这里注释掉
minIndex := j
for i := j + 1; i < len(arr); i++ { // 找出最小值的下标
if newArr[i] < newArr[minIndex] {
minIndex = i // 更新minIndex
}
}
newArr[minIndex], newArr[j] = newArr[j], newArr[minIndex]
}
return newArr
}
|
19.插入排序
2023.10.14
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
|
package main
import "fmt"
/**
* Description:
技巧:也是和选择排序一样,先写内层循环,然后再嵌套一层循环。
* @Author Hollis
* @Create 2023-10-12 18:20
*/
func main() {
arr := []int{3, 8, 1, 5, 4, 7, 6, 9, 2}
result := InsertSort(arr)
fmt.Println(result)
}
func InsertSort(arr []int) []int {
newArr := arr
for i := 1; i <= len(newArr)-1; i++ {
// 先确定第二个元素应该插入的位置
insertVal := newArr[i]
insertIndex := i - 1
for insertIndex >= 0 && newArr[insertIndex] > insertVal {
newArr[insertIndex+1] = newArr[insertIndex]
insertIndex--
}
if insertIndex+1 != i {
newArr[insertIndex+1] = insertVal
}
}
return newArr
}
|
20.Hash表
2023.10.15
实现一个简单的哈希表,然后要求你编写插入、查找和删除键值对的操作。
问题描述:
你需要实现一个简单的哈希表(不要使用Go的内置map
),该哈希表支持以下操作:
Insert(key string, value int)
:插入一个键值对,如果键已存在,则更新其值。
Get(key string) int
:获取给定键对应的值,如果键不存在,则返回 -1。
Delete(key string)
:从哈希表中删除给定键的键值对。
你的哈希表应该能够处理哈希碰撞,可以使用开放寻址法或链表法等方法来解决碰撞问题。你可以使用已经实现的哈希函数或自己编写一个哈希函数。
示例使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func main() {
myHashMap := Constructor() // 创建哈希表实例
myHashMap.Insert("apple", 1) // 插入键值对
myHashMap.Insert("banana", 2)
myHashMap.Insert("cherry", 3)
fmt.Println(myHashMap.Get("apple")) // 输出: 1
fmt.Println(myHashMap.Get("banana")) // 输出: 2
myHashMap.Delete("banana") // 删除键值对
fmt.Println(myHashMap.Get("banana")) // 输出: -1
}
|
参考代码:
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
|
type MyHashMap struct {
data [1000][]KeyValue
}
type KeyValue struct {
key string
value int
}
/** Initialize your data structure here. */
func Constructor() MyHashMap {
return MyHashMap{}
}
/** value will always be non-negative. */
func (this *MyHashMap) Insert(key string, value int) {
index := hash(key)
for i, kv := range this.data[index] {
if kv.key == key {
this.data[index][i].value = value
return
}
}
this.data[index] = append(this.data[index], KeyValue{key, value})
}
/** Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key */
func (this *MyHashMap) Get(key string) int {
index := hash(key)
for _, kv := range this.data[index] {
if kv.key == key {
return kv.value
}
}
return -1
}
/** Removes the mapping of the specified value key if this map contains a mapping for the key */
func (this *MyHashMap) Delete(key string) {
index := hash(key)
for i, kv := range this.data[index] {
if kv.key == key {
this.data[index] = append(this.data[index][:i], this.data[index][i+1:]...)
return
}
}
}
func hash(s string) int {
h := 0
for _, ch := range s {
h = 31*h + int(ch)
}
return h % 1000
}
|
哈希值更新:对于每个字符 ch
,哈希值 h
会按以下方式更新:
h
乘以 31:将当前的哈希值乘以 31,这个选择的基数通常是质数,帮助分散哈希值。
- 加上字符的ASCII值:将当前字符
ch
的ASCII值(整数表示)加到哈希值 h
上。
21.归并排序
2023.10.16
递归实现
归并排序-递归实现的基本思想:先选取数组中最中间的值作为middle,将数组分为左右两个数组。
然后将左右两个数组分别进行归并排序,然后定义一个和原来数组容量相同的数组help,用两个指针p1和p2,
分别指向左右两个数组的最开始元素(需要借助middle),取出二者中更小的元素存入数组help中,移动p1和p2,直到p1和p2越界,最后,使用help数组覆盖原来的arr数组,得到最后有序的数组。
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
|
func main() {
arr := []int{3, 8, 1, 5, 4, 7, 6, 9, 2}
help := make([]int, len(arr))
MergeSort(arr, help, 0, len(arr)-1)
fmt.Println(arr)
}
func MergeSort(arr, help []int, left, right int) {
if left >= right {
return
}
middle := left + (right-left)>>1 // 求中点
MergeSort(arr, help, left, middle) // 左部分有序
MergeSort(arr, help, middle+1, right) // 右部分有序
Merge(arr, help, left, middle, right) // 将左右数组合并到help数组中(middle参数用来分割左右数组)
}
func Merge(arr, help []int, left, middle, right int) { // 左:left...middle,右:middle+1...right。help为辅助辅助,存放合并后的值。
p1 := left
p2 := middle + 1
i := left // 两个数组中比较出来的数字,应该放在help数组中的位置
for ; p1 <= middle && p2 <= right; i++ {
if arr[p1] <= arr[p2] {
help[i] = arr[p1]
p1++
} else {
help[i] = arr[p2]
p2++
}
}
for ; p1 <= middle; i++ { // 将p1或p2中剩下的元素添加到help数组中
help[i] = arr[p1]
p1++
}
for ; p2 <= right; i++ {
help[i] = arr[p2]
p2++
}
for i := left; i <= right; i++ {
arr[i] = help[i]
}
}
|
22.和等于k的最长子数组长度
2023.10.17
力扣:和等于 k 的最长子数组长度
给定一个数组nums和一个目标值k,找到累加和等于k的最长连续子数组长度。如果不存在任意一个符合要求的子数组,则返回 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
|
// maxSubArrayLen 函数接受一个整数数组 nums 和一个目标值 k,并返回累加和等于 k 的最长连续子数组的长度
func maxSubArrayLen(nums []int, k int) int {
prefixSum := make(map[int]int) // prefixSum 是一个映射,用于存储累加和和对应的索引
maxLen := 0 // maxLen 用于跟踪最长子数组的长度
sum := 0 // sum 用于跟踪当前的累加和
for i := 0; i < len(nums); i++ {
sum += nums[i]
// 如果当前累加和等于目标值 k,整个数组就是一个候选子数组
if sum == k {
maxLen = i + 1
} else if index, ok := prefixSum[sum-k]; ok {
// 如果存在之前的累加和等于当前累加和减去 k,说明这两个位置之间的元素和为 k
maxLen = max(maxLen, i-index)
}
// 如果当前累加和不在前缀和映射中,将其添加进去
if _, ok := prefixSum[sum]; !ok {
prefixSum[sum] = i
}
}
return maxLen
}
// max 函数用于比较两个整数并返回较大的那个
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
nums := []int{1, -1, 5, -2, 3}
k := 3
result := maxSubArrayLen(nums, k)
fmt.Println("最长连续子数组的长度为:", result)
}
|
23.荷兰国旗问题
2023.10.17
给定一个数组arr,和一个整数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数的右边。
要求额外空间复杂度O(1),时间复杂度O(N)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
func main() {
arr := []int{3, 1, 2, 8, 5, 2, 7, 1, 3, 6, 3}
num := 3
partition(arr, num)
fmt.Println(arr)
}
func partition(arr []int, num int) {
low, mid, high := 0, 0, len(arr)-1
for mid <= high {
if arr[mid] < num {
arr[low], arr[mid] = arr[mid], arr[low]
low++
mid++
} else if arr[mid] == num {
mid++
} else {
arr[mid], arr[high] = arr[high], arr[mid]
high--
}
}
}
|
在这个示例中,我们使用三个指针:low、mid和high,它们分别表示数组中小于num的区域、等于num的区域和大于num的区域。
我们从中间开始遍历数组中的每个元素,并根据其值将元素放入相应的区域。具体来说:
如果当前元素小于num,将其与low位置的元素交换,然后将low和mid分别向前移动一位。
如果当前元素等于num,只将mid向前移动一位。
如果当前元素大于num,将其与high位置的元素交换,然后将high向后移动一位。
这个过程将数组分成三个区域,分别包含小于、等于和大于num的元素,满足题目要求的条件。
24.找到第K大的数
2023.10.18
给你一个整数数组nums和一个正数k,请你返回第k大的数。要求时间复杂度O(n)。
可以把这个问题转化为:确定排完序(升序)后,第 len(num)-k 位置上的数,这个数就是我们的最后结果。
但我们不需要真的去将数组nums排好序,可以借鉴快速排序中的partition函数(左边的数比pivot小,右边的数比pivot大)的思想。
如果pivot刚好等于了我们的目标位置 len(num)-k,则说明这个位置上的数就是我们要找的数。
如果pivot小于目标位置 len(num)-k,说明我们要找的数在pivot的右侧,反之则在左侧。(只需要找一侧就可以了,这样就提高了查找的效率)
参考代码:
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
|
func main() {
nums := []int{3, 2, 1, 5, 6, 4}
k := 6
result := findKthLargest(nums, k)
fmt.Printf("第 %d 大的数是:%d\n", k, result)
}
func findKthLargest(nums []int, k int) int {
return quickSelect(nums, 0, len(nums)-1, len(nums)-k)
}
func quickSelect(nums []int, left, right, index int) int {
for {
if left > right { // 遍历完了都没找到的情况
return 0
}
pivotIndex := partition2(nums, left, right)
if pivotIndex == index {
return nums[pivotIndex]
}
if pivotIndex < index {
left = pivotIndex + 1
} else {
right = pivotIndex - 1
}
}
}
func partition2(nums []int, left, right int) int {
l := left
r := right
pivot := l + (r-l)>>1
for {
for nums[l] < nums[pivot] {
l++
}
for nums[r] > nums[pivot] {
r--
}
if l >= r {
return r
}
nums[l], nums[r] = nums[r], nums[l]
l++
r--
}
}
|
25.大根堆
2023.10.18
创建一个 MaxHeap
结构体,其中包含最大堆的堆切片和堆大小。它包括了插入元素、删除最大元素、堆化(上浮和下沉)操作。在 main
函数中,使用最大堆来插入元素并删除最大元素(依次输出堆中最大的元素)。
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
80
81
82
83
84
85
|
type MaxHeap struct {
data []int // 堆的元素存储在切片中
size int // 堆的大小
}
// NewMaxHeap 创建一个新的最大堆
func NewMaxHeap() *MaxHeap {
return &MaxHeap{}
}
// Insert 将元素插入到最大堆
func (h *MaxHeap) Insert(key int) {
h.data = append(h.data, key) // 将新元素添加到堆的末尾
h.size++ // 堆大小加一
h.heapifyUp(h.size - 1) // 对新元素执行上浮操作,以保持最大堆性质
}
// ExtractMax 删除并返回最大元素
func (h *MaxHeap) ExtractMax() (int, error) {
if h.size == 0 {
return 0, fmt.Errorf("heap is empty")
}
max := h.data[0] // 最大元素是根节点
last := h.data[h.size-1] // 获取最后一个元素
h.size-- // 堆大小减一
if h.size > 0 {
h.data[0] = last // 用最后一个元素替代根节点
h.heapifyDown(0) // 对根节点执行下沉操作,以保持最大堆性质
}
return max, nil
}
// heapifyUp 堆化操作,自下而上,用于插入操作
func (h *MaxHeap) heapifyUp(index int) {
for index > 0 {
parent := (index - 1) / 2 // 计算父节点索引
if h.data[index] <= h.data[parent] {
break // 如果当前节点不大于父节点,堆性质已经满足,退出循环
}
// 交换当前节点和父节点的值
h.data[index], h.data[parent] = h.data[parent], h.data[index]
index = parent // 更新索引
}
}
// heapifyDown 堆化操作,自上而下,用于删除操作
func (h *MaxHeap) heapifyDown(index int) {
for {
leftChild := 2*index + 1 // 左子节点索引
rightChild := 2*index + 2 // 右子节点索引
largest := index // 假设当前节点是最大的
// 寻找左右子节点中的最大者
if leftChild < h.size && h.data[leftChild] > h.data[largest] {
largest = leftChild
}
if rightChild < h.size && h.data[rightChild] > h.data[largest] {
largest = rightChild
}
if largest == index {
break // 如果当前节点已经是最大的,堆性质已经满足,退出循环
}
// 交换当前节点和最大子节点的值
h.data[index], h.data[largest] = h.data[largest], h.data[index]
index = largest // 更新索引(largest的位置指向的是和原来index互换值的位置,现在我们需要将index设置为该值,方便进行向下面的子树遍历)
}
}
func main() {
maxHeap := NewMaxHeap()
maxHeap.Insert(10)
maxHeap.Insert(5)
maxHeap.Insert(20)
maxHeap.Insert(3)
maxHeap.Insert(30)
fmt.Println("Max Heap:")
for maxHeap.size > 0 {
max, _ := maxHeap.ExtractMax()
fmt.Printf("%d ", max)
}
}
|