迷途小书童

读过几年书,尘世间一枚不起眼的小书童


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

获取Redis中同一前缀key踩坑记录

发表于 2021-08-19 | 分类于 golang踩坑记录

前言

之前有一个订单购买后用户生效保护期的需求,根据当时的需求本人是将key设置好自定义前缀规则,并设置了过期时间然后统一放在了redis上面,后来需求变动,所有的保护期过期之后需要留档处理,这时候就需要把之前设置的同一前缀的保护期数据在过期之前全部转移到数据库留档,就需要将之前设置的所有保护期相关数据全部获取出来。这时的kv大概已经有了300w条左右了。

这里就有两个方案了keys * 和 scan

keys *这个虽然可以实现目的但是最好还是不要使用,因为如果线上数据过大,就会导致单线程的redis阻塞,长时间无法处理后续请求,然后你就等着被捶吧。

scan可以理解是keys 的分批次执行,简单来说就是会有一个游标记录一次扫描的结束位置,然后拿着这个游标作为下一次扫描的起始位置,反复执行,每次执行数据量不大而且每次执行时间也足够短,直到扫描完整个redis库,全程不会造成redis的无响应状况,但是需要声明一点,*这个scan操作是会出现重复key值的,所以需要业务侧做好去重处理哈。本文的重点!为什么说这个scan会出现重复值,又为什么这个scan是不会漏值的?**下面就来说说本人的理解。

实际使用

下面是本人在项目中的一些用法,代码实现功能就是将自己需要的一些kv值遍历出来放进mysql进行存储,由于线上数据量较大,整个过程大约持续了30分钟,但是redis服务全程无异常状况,完美达到目的。

核心代码示例

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
rcnn := redis.Connection()
begin := time.Now().Unix()
firstStrs, cursor := rcnn.Scan(0, "xxxxx_service_xxxxx*", 1000).Val()
fmt.Println(time.Now().Unix() - begin)
for _, value := range firstStrs {
key := value
keyValue := rcnn.Get(key).Val()
ttlSeconds := int64(rcnn.TTL(key).Val().Seconds())
tnow := time.Now().Unix()
memberId, _ := strconv.Atoi((strings.Split(key, "&")[1]))
orderId := strings.Split(keyValue, "&")[0]
pPeriod := &protectperiod.ProtectPeriod{
MemberId: int64(memberId),
OrderId: orderId,
Content: keyValue,
CreatedAt: tnow + ttlSeconds - 3628800,
ExpiredAt: tnow + ttlSeconds,
Status: "0",
Source: "redis",
}
cErr := pPeriod.CreateNewRecord()
if cErr != nil {
fmt.Println(cErr)
}
}
for cursor != 0 {
cbegin := time.Now().Unix()
strs, nextCursor := rcnn.Scan(cursor, "xxxxx_service_xxxxx*", 1000).Val()
fmt.Println(time.Now().Unix() - cbegin)
for _, value := range strs {
key := value
keyValue := rcnn.Get(key).Val()
ttlSeconds := int64(rcnn.TTL(key).Val().Seconds())
tnow := time.Now().Unix()
memberId, _ := strconv.Atoi((strings.Split(key, "&")[1]))
orderId := strings.Split(keyValue, "&")[0]
pPeriod := &protectperiod.ProtectPeriod{
MemberId: int64(memberId),
OrderId: orderId,
Content: keyValue,
CreatedAt: tnow + ttlSeconds - 3628800,
ExpiredAt: tnow + ttlSeconds,
Status: "0",
Source: "redis",
}
cErr := pPeriod.CreateNewRecord()
if cErr != nil {
fmt.Println(cErr)
}
}
cursor = nextCursor
}
fmt.Println("all Done")

原理

好了,在前言中有说到为什么说这个scan会出现重复值,又为什么这个scan是安全不会漏值的? 在讨论这个问题之前,我们需要先了解什么是Rehash,学过java的都知道hashmap,当hashmap中的槽不够用的时候就会在每一个槽产生过长的链表导致效率低下等问题,在hash冲突严重的时候,hashmap就会进行扩容,然后把原来的kv值转移到新的扩容的好hashmap中去,这个过程其实就是rehash。其实在redis中这一过程准确点的叫法是渐进式rehash,点此处了解什么是渐进式rehash, 了解完上述概念之后,我们现在再来看看scan命令为什么是安全不漏值而且可能会出现重复值的。

总结

如果遇到线上redis需要获取大批量的kv值,用scan不会错。切记客户端一定要做好去重处理!!!

Golang的for循环踩坑

发表于 2021-08-16 | 分类于 golang踩坑记录

前言

最近写项目用了太多的for循环,殊不知这么简单的一个操作竟然踩出大坑,记录一波!

for循环使用

下面来看一下项目中的一些用法

实例踩坑效果

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
arr := []int{1, 2, 3}
newArr := []*int{}
for _, v := range arr {
newArr = append(newArr, &v)
}
for _, v := range newArr {
fmt.Println(*v)
}
}
$ go run main.go
3 3 3

看到这个结果的时候我其实是一脸懵逼的,这个问题起初我一个java程序员真是无法理解,后来直到我运行了如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
arr := []int{1, 2, 3}
newArr := []*int{}
for _, v := range arr {
fmt.Println("")
fmt.Printf("origin addr: %p value: %v", &v, v)
newArr = append(newArr, &v)
}
for _, s := range newArr {
fmt.Println("")
fmt.Printf("addr: %p value: %v", s, *s)
}
}
输出结果:
origin addr: 0xc0000a67a8 value: 1
origin addr: 0xc0000a67a8 value: 2
origin addr: 0xc0000a67a8 value: 3
addr: 0xc0000a67a8 value: 3
addr: 0xc0000a67a8 value: 3
addr: 0xc0000a67a8 value: 3

总结

总结就是golang的for循环是通过同一个地址槽来反复接值的!!!

count(1)、count(*)、count(列)有什么区别?

发表于 2021-08-12 | 分类于 数据库

前言

不知不觉入职tencent已经半年了,期间一直都在忙着适应golang还有新的业务新的微服务框架,从java转到golang有所适有所不适吧,但总体来说问题不大,所以就很久没有更新了,但是没关系,工作中遇到的问题小弟我都有一一记录,后面全部补上。今天主要来对sql统计中的count进行一下总结。

count(1) and count(*)

当表的数据量大些时,对表作分析之后,使用count(1)还要比使用count(*)用时多了!

从执行计划来看,count(1)和count()的效果是一样的。但是在表做过分析之后,count(1)会比count()的用时少些(1w以内数据量),不过差不了多少。

如果count(1)是聚索引,id,那肯定是count(1)快。但是差的很小的。

因为count(),自动会优化指定到那一个字段。所以没必要去count(1),用count(),sql会帮你完成优化的 因此:count(1)和count(*)基本没有差别!

count(1) and count(字段)

两者的主要区别是

  1. count(1) 会统计表中的所有的记录数,包含字段为null 的记录。
  2. count(字段) 会统计该字段在表中出现的次数,忽略字段为null 的情况。即不统计字段为null 的记录。

count(*) 和 count(1)和count(列名)区别

执行效果上:

  • count()包括了所有的列,相当于行数,在统计结果的时候,*不会忽略列值为NULL**
  • count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL
  • count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计。

执行效率上:

  • 列名为主键,count(列名)会比count(1)快
  • 列名不为主键,count(1)会比count(列名)快
  • 如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*)
  • 如果有主键,则 select count(主键)的执行效率是最优的
  • 如果表只有一个字段,则 select count(*)最优。

实例查看效果

下面来看看一个列子吧

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
mysql> create table counttest(name char(1), age char(2));
Query OK, 0 rows affected (0.03 sec)

mysql> insert into counttest values -> ('a', '14'),('a', '15'), ('a', '15'), -> ('b', NULL), ('b', '16'), -> ('c', '17'), -> ('d', null), ->('e', '');
Query OK, 8 rows affected (0.01 sec)
Records: 8 Duplicates: 0 Warnings: 0

mysql> select * from counttest;
+------+------+
| name | age |
+------+------+
| a | 14 |
| a | 15 |
| a | 15 |
| b | NULL |
| b | 16 |
| c | 17 |
| d | NULL |
| e | |
+------+------+
8 rows in set (0.00 sec)
mysql> select name, count(name), count(1), count(*), count(age), count(distinct(age)) -> from counttest -> group by name;
+------+-------------+----------+----------+------------+----------------------+
| name | count(name) | count(1) | count(*) | count(age) | count(distinct(age)) |
+------+-------------+----------+----------+------------+----------------------+
| a | 3 | 3 | 3 | 3 | 2 |
| b | 2 | 2 | 2 | 1 | 1 |
| c | 1 | 1 | 1 | 1 | 1 |
| d | 1 | 1 | 1 | 0 | 0 |
| e | 1 | 1 | 1 | 1 | 1 |
+------+-------------+----------+----------+------------+----------------------+
5 rows in set (0.00 sec)

好记性不如烂笔头哈~

goroutine与线程的关系

发表于 2020-12-29 | 分类于 Golang并发编程

前言

学到这里,我们需要了解一下OS线程一般都是有属于自己的线程栈内存(通常为2MB)。一个goroutine栈在刚启动的时候确实很小的(通常是2KB),所以一次性创建10w个goroutine也是没有问题的,随着运行goroutine栈最大可达到1GB。goroutine属于用户态线程,go语言在runtime层面通过实现了一个GPM机制来对goroutine进行调度。

goroutine的调度机制

首先来大致解释下什么是GPM

G:就是goroutine,包含goroutine信息以及与所在P的绑定等信息

P:管理着一组goroutine的队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址以及地址边界),P内部对自己管理的一些列G进行调度,当自己管理的队列消费完了还会去全局队列中取,如果全局队列也消费完了还会去其他的P中取任务

M:是Golang中对os内核线程的抽象,M与内核线程一般就是11对应的关系,goroutine最终都是会在M上进行执行的

我们来稍微理解一下这个GPM模型,本人会觉得与java中的线程池模型还是很相似的,只是本人研究的java线程池中并无全局队列以及去其他队列抢任务的概念出现,了解jJAVA中线程池工作原理戳这里。在GPM中,P和M通常也是11对应的,但这并不是绝对的,可以这么通俗的来理解一个队列P对应一个os线程M,os线程由操作系统cpu运行,然后P来对队列中的任务goroutine进行调度,让每一个goroutine在os线程上不停的来回切换运行(准确一点的表达:P管理着一组G挂载在M上运行,当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G挂载在新建的M上去。当旧的G阻塞完成或者认为其已经死掉时那么就会回收旧的M)。

GOMAXPROCS

​ 在前言中我们说了G的内存大小以及创建数量等问题,那么P由这方面的限制吗?P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。在并发量大的时候会适当增加一些P和M,但是也不会太多,切换太频繁也没有太多必要。

实例查看效果

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核cpu上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里我们来举个例子看看。

例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
package main

import (
"fmt"
"runtime"
"time"
)

func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}

func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}

func main() {
runtime.GOMAXPROCS(1)
go a()
go b()
time.Sleep(time.Second)
}
运行结果:
modora#: go run ./main.go
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9

上面例子中我们设置了runtime.GOMAXPROCS()为1,也就是说GPM模型中最多一个OS线程在运行,可以很明显的看到Agoroutine和Bgoroutine是串行打印,原因也很简单,两个goroutine在一个OS线程上进行调度的。下面我们将runtime.GOMAXPROCS()设置成2,两个goroutine在两个OS线程上进行调度,这里其实可以理解在OS线程层面并行。

例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"
"runtime"
"time"
)

func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}

func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}

func main() {
runtime.GOMAXPROCS(2)
go a()
go b()
time.Sleep(time.Second)
}
执行结果:
modora#: go run ./main.go
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9
A: 9

再次运行上述代码,发现AB变成了交替打印。各种意味自行理解哈~

hexo迁移(Windows -> Mac)

发表于 2020-12-18 | 分类于 Hexo

Hexo迁移(Windows -> Mac)

最近因工作需要换了台mac,需要将windows上面的hexo全数转移至新机,记录一下整个过程。

1.找到自己Windows的hexo根目录

2.在 Mac 安装git和node.js

首先在自己电脑上装好node和git(首先确保brew安装好了)

brew install git

brew install node

3.安装hexo

用node.js来安装,这里可能会安装失败,我这里是切换npm源才成功

npm config set registry https://registry.npm.taobao.org

npm install -g hexo-cli

4.初始化hexo目录

新建一个hexo目录,mkdir blog,cd blog

hexo init
在用hexo s

测试是否成功,打开localhost:4000查看本地

5.生成SSH密钥,关联github

先查看本地的SSH key: cd ~/.ssh
(我是新买的mac,这一步需要直接先走安装)
$ssh-keygen -t rsa -C “youremail@example.com“ 后面那个是注册邮箱

进入.ssh文件夹: cd ~/.ssh,然后打开里面的 id_rsa.pub文件,里面的内容就是 SSH key,复制全部内容;

网页打开 github 的设置:Settings -> SSH and GPG keys,点击绿色的按钮 New SSH key,然后在输入框中输入刚才复制的内容;

保存后,github 会向你的邮箱发送一个验证链接(记得要去登录邮箱验证,不然之后的 hexo d 部署会一直不成功的!);

可以先测试一下是否成功:ssh git@github.com,
看到以下即成功:

1
2
3
4
> PTY allocation request failed on channel 0
> Hi gjincai! You've successfully authenticated, but GitHub does not provide shell access.
> Connection to github.com closed.
>

6.文件配置转移

windows 下的博客根目录 hexo,复制该目录下的:_config.yml, scaffolds, source, themes;
mac 下的博客根目录 hexo,把刚才复制的内容,直接覆盖替换相同的文件文件夹。

7.设置个人信息

git config --global user.name "yourname"

git config --global user.email youremail@example.com

到这就好了,和往常一样hexo g hexo d(或者直接hexo g -d)发布文章吧!结果会提示ERROR Deployer not found: git

安装以下再尝试:npm install hexo-deployer-git --save(若提示有关权限不足的,加sudo,反正我是遇到了)

8.最重要!!!!!!!

以上是通常情况下的解决方案,本人遇到这个问题其实比较棘手并没有那么的顺利:本人旧机Window安装的node是v10.15.3,hexo版本是v3.9.0;要知道hexo最新版本已经是v5.2.0,跟旧版本相比有了很大的改动,比如主题安装,变化还是比较多的。所以最好是确定node版本在12或13或者以下版本,hexo版本最好也保持一致,否则会出现deploy d失败的情况,可以通过nvm安装多版本node解决即可

goroutine

发表于 2020-12-04 | 分类于 Golang并发编程

前言

之前学JAVA并发编程的时候,了解到了cpu内存模型、JMM、竞态资源、同步关键字synchronized、可见性以及指令重排关键字volatile再到后面的AQS源码等等。可以说JAVA的并发编程体系是非常完善的,但据说golang是天生就具备并发性能的语言,今天就来好好学习一下golang并发编程中的第一个重要概念goroutine。

并发与并行

相信学过并发编程的同学一定对这两个概念不陌生,我这里就只举两个非常简单形象的例子来描述一下:

并发:假如你是一个渣男,某一天晚上你用微信轮流不停的和两个女朋友聊天

并行:你和你老弟都是好男人,某一天晚上你们都在用微信和自己女朋友聊天

Java中通过Thread实现并发,GoLang中的并发则通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Golang的运行时调度完成的,而线程是由操作系统调度完成。

golang还提供channel在多个goroutine间进行通信。goroutine和channel是Go语言秉承的CSP(Communicating Sequential Process)并发模式的重要实现基础

goroutine

在JAVA中要实现并发编程的时候,通常需要自己维护一个Threadpool,并且需要自己去包装一个又一个的任务(runnable or callable其实本质还是用户态Thread)往这个池子中丢任务,线程池根据自己的工作机制来运行这个任务或者阻塞这个任务又或者拒绝这个任务,今天我们不讨论JAVA线程池,有兴趣的朋友可以看我另外一篇文章,JAVA中线程池工作原理点这里查看。

goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能 –goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴。

use goroutine

Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

启动单个goroutine

Golang中启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go关键字。如下示例:

1
2
3
4
5
6
7
func hello() {
fmt.Println("Hello stephen!")
}
func main() {
hello()
fmt.Println("main stephen over!")
}

这个示例中hello函数和下面的语句是串行的,执行的结果是打印完Hello stephen!后打印main stephen done!。

下面我们在调用hello函数前面加上关键字go,也就是启动一个goroutine去执行hello这个函数。

1
2
3
4
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main stephen done!")
}

这一次的执行结果只打印了main stephen done!,并没有打印Hello stephen!。为什么呢?在程序启动时,Go程序就会为main()函数创建第一个默认的goroutine,这个goroutine。当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束,main函数所在的goroutine就像是打着降龙十八掌的乔峰,乔峰运功十八条龙(sub goroutines)在空气里穿梭,乔峰收工,十八条龙在空气中消失。这样好理解了吧。所以我们要想办法让main函数等一等hello函数,java中我们可以通过Thread.sleep()或者while(true),而golang这里最简单粗暴的方式就是time.Sleep了。

1
2
3
4
5
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main stephen done!")
time.Sleep(time.Second)
}

执行上面的代码你会发现,这一次先打印main stephen done!,然后紧接着打印Hello stephen !。这里会先打印main stephen done!是因为我们在创建新的goroutine的时候需要花费一些时间,而此时main函数所在的goroutine是继续执行的。

启动多个goroutine(引出WaitGroup)

了解到如何启动单个goroutine后,那么启动多个goroutine同理如下,同样我们需要main goroutine在一系列的sub goroutine执行完之前保持存活状态,这里就引入了一个新的概念WaitGroup,这个东西我在学习java并发编程的时也是有发现类似功能的组件的,比如AQS的直接产物CountDownLatch或者间接产物CyclicBarrier,原理也比较有趣,有兴趣了解其工作原理的戳这里。

1
2
3
4
5
6
7
8
9
10
11
12
13
var wg sync.WaitGroup

func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello stephen!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}

执行上述代码,你会发现执行的打印结果每一次都是不一样的,原因也很简单,因为后台的goroutine的调度是随机的~,好了今天我们初步的了解了goroutine的一些基本概念和基本用法,下一篇来详细讨论一下GMP调度模型,学习一下goroutine调度背后的故事。

Golang练习二

发表于 2020-11-28 | 分类于 Golang

学习Golang的过程中,练习必不可少,俗话说的好 Talk is cheap,show me the code,学完Golang基本语法,是需要适当做一些练习加深理解的,虽然很很很简单!但是本人Java转go,语法还是要多熟悉,秉承多写一遍就多一次理解的原则,开始吧~

  • 练习4

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //练习4
    //打印9*9乘法口诀表
    package main

    import "fmt"

    /*
    * 练习4 打印乘法乘法口诀表
    */

    func test4() {
    for i := 1; i <= 9; i++ {
    for j := 1; j <= 9; j++ {
    if j <= i {
    fmt.Printf("%v * %v = %v ", i, j, i*j)
    }
    }
    fmt.Printf("\n")
    }
    }

    func main() {
    test4()
    }
  • 练习5

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //练习5
    //编写代码统计出字符串中每一个单词出现的次数
    func test5() {
    mymap := make(map[string]int)
    str := "how do you do"
    arr := strings.Split(str, " ")
    for _, v := range arr {
    value, ok := mymap[v]
    if ok {
    mymap[v] = value + 1
    } else {
    mymap[v] = 1
    }
    }
    fmt.Println(mymap)
    }
  • 练习6

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    func calc(index string, a, b int) int {
    ret := a + b
    fmt.Println(index, a, b, ret)
    return ret
    }

    func main() {
    x := 1
    y := 2
    defer calc("AA", x, calc("A", x, y))
    x = 10
    defer calc("BB", x, calc("B", x, y))
    y = 20
    }
    //思考下输出结果?提示一下:
    //defer注册要延迟执行的函数时该函数所有的参数都需要确定其值
    //defer入栈出栈?
  • 练习7

    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
    /*
    你有100枚金币,需要分配给以下几个人:Matthew,Sarah,Augustus,Heidi,Emilie,Peter,Giana,Adriano,Aaron,Elizabeth。
    分配规则如下:
    a. 名字中每包含1个'e'或'E'分1枚金币
    b. 名字中每包含1个'i'或'I'分2枚金币
    c. 名字中每包含1个'o'或'O'分3枚金币
    d: 名字中每包含1个'u'或'U'分4枚金币
    写一个程序,计算每个用户分到多少金币,以及最后剩余多少金币?
    程序结构如下,请实现 ‘dispatchCoin’ 函数
    */
    var (
    coins = 100
    users = []string{
    "Matthew", "Sarah", "Augustus", "Heidi", "Emilie", "Peter", "Giana", "Adriano", "Aaron", "Elizabeth",
    }
    distribution = make(map[string]int, len(users))
    )

    func dispatchCoin() int {
    for _, v := range users {
    var count = 0
    for _, val := range v {
    switch val {
    case 'e':
    count++
    case 'E':
    count++
    case 'i':
    count = count + 2
    case 'I':
    count = count + 2
    case 'o':
    count = count + 3
    case 'O':
    count = count + 3
    case 'u':
    count = count + 4
    case 'U':
    count = count + 4
    }
    }
    distribution[v] = count
    coins = coins - count
    }
    fmt.Println(distribution)
    return coins
    }

    func main() {
    left := dispatchCoin()
    fmt.Println("剩下:", left)
    }

Talk is cheap,show me the code~

二叉树的遍历

发表于 2020-11-26 | 分类于 数据结构与算法

本文主要就二叉树的遍历方式进行总结和记录,从而了解递归、栈以及队列的应用,通常对于一个二叉树的遍历方式大致分为:

  1. 前序遍历(中 -> 左 -> 右)
  2. 中序遍历(左 -> 中 -> 右)
  3. 后续遍历(左 -> 右 -> 中)
  4. 层次遍历
  • 前序遍历

    • 递归实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      /*
      * 思路:若二叉树非空:
      * 1、访问根节点
      * 2、遍历左子树
      * 3、遍历右子数
      */
      public void PreOrder(Btree T){
      if(T != null){
      show(T)
      PreOrder(T.left)
      PreOrder(T.right)
      }
      }
  • 栈实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /*
    * 思路:若二叉树非空
    * 1、根遍历,拿到一个节点的指针,先判断是否为空,不为空就先访问该结点,然后直接进栈,
    * 2、接着遍历左子树;为空则要从栈中弹出一个节点来,这个时候弹出的结点就是其父亲
    * 3、然后访问其父亲的右子树,直到当前节点为空且栈为空时,算法结束.
    *
    */
    public void PreOrder1(Btree T) {
    stack<Btree> st;
    if (T == NULL){
    return null;
    }
    st.push(T);
    while (!st.empty()){
    Btree T =st.pop();
    show(T)
    if (T.right!=NULL)
    st.push(p.right);
    if (p.left!=NULL)
    st.push(p.left);
    }
    return null;
    }
  • 中序遍历

    • 递归实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      /*
      * 思路:若二叉树非空:
      * 1、遍历左子树
      * 2、访问根节点
      * 3、遍历右子数
      */
      public void MidOrder(Btree T){
      if(T != null){
      MidOrder(T.left)
      show(T)
      MidOrder(T.right)
      }
      }
  • 栈实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /*
    * 思路:借助栈
    * 1、初始时依次扫描根节点的所在左侧节点并将它们一一进栈;
    * 2、出栈一个节点,访问它
    * 3、扫描该节点的右孩子节点并将其出栈
    * 4、依次扫描右孩子节点的所有左侧节点并一一进栈
    * 5、反复 2 - 4 步骤
    */
    public void MidOrder2(Btree T){
    Stack s = new Stack();
    BTree p = T;
    while(p||!s.IsEmpty()){
    if(p){
    s.push(p);
    p = p.left;
    }else{
    p = s.pop();
    show(p);
    p = p.right;
    }
    }
    }
  • 后序遍历

    ​

    • 递归实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      /*
      * 思路:若二叉树非空:
      * 1、遍历左子树
      * 2、遍历右子数
      * 3、访问根节点
      */
      public void PostOrder(Btree T){
      if(T != null){
      PostOrder(T.left)
      PostOrder(T.right)
      show(T)
      }
      }
  • 栈实现

    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
    /*
    * 思路:借助栈
    */
    public void AfterOrder2(Btree root){
    if (root == NULL){
    return;
    }
    Stack<BTree> s = new Stack();
    s.push(root);//根指针入栈
    while (!s.empty())
    {
    //重复将栈顶指针指向结点的左子树压栈,直到栈顶指针指向结点的左子树为空
    while (s.top().left != NULL){
    s.push(s.top().left);
    }
    Btree last = NULL;//上一次遍历过的指针
    //重复检查
    while (!s.empty()){
    if (s.top().right==NULL||last==s.top().right) {
    //如果栈顶指针指向结点的右子树为空或者遍历过
    show(s.top())
    last = s.top();//更新指针last
    s.pop();//栈顶指针出栈
    }
    //如果栈顶指针指向的结点的右子树不为空
    else if(s.top()->R!=NULL){
    s.push(s.top()->R);//将右子树入栈
    break;//退出检查
    }
    }
    }
    }
  • 层次遍历

  • 队列实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /*
    * 思路:借助队列 队列的就是FIFO
    * 1、初始将根节点入队并访问根节点,然后出队
    * 2、如果有左子树,将左子树的根入队
    * 3、如果有右子树,将右子树的根入队
    * 4、然后出队,访问该节点
    * 5、反复出队访问直到队列为空
    * 伪代码
    */
    public void levelOrder(Btree T){
    InitQueue(Q)
    BTree p;
    EnQueue(Q,T)
    while(!IsEmpty(Q)){
    Dequeue(Q,p);
    show(p);
    if(p.left != null){
    EnQueue(Q,p.left);
    }
    if(p.right != null){
    Enqueue(Q,p.right);
    }
    }
    }
  • 总结

    二叉树是数据结构中非常重要的一环,弄懂其相关操作对于后续的学习也是十分重要,Java中的Treemap以及Hashmap中都有红黑树的引入,红黑树就是二叉树的一种特殊存在,通常我们也叫它黑高平衡树,后面再详细描述~

Golang练习一

发表于 2020-11-26 | 分类于 Golang

学习Golang的过程中,练习必不可少,俗话说的好 Talk is cheap,show me the code,学完Golang基本语法,是需要适当做一些练习加深理解的,虽然很很很简单!但是本人Java转go,语法还是要多熟悉,秉承多写一遍就多一次理解的原则,开始吧~

  • 练习1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //练习1
    //编写代码分别定义一个整型、浮点型、布尔型、字符串型变量,使
    //用fmt.Printf()搭配%T分别打印出上述变量的值和类型。
    package main

    import (
    "fmt"
    )

    func test1() {
    a := 10
    b := 1.0
    c := false
    d := "哈哈哈"
    fmt.Printf("a的类型为%T\n", a)
    fmt.Printf("b的类型为%T\n", b)
    fmt.Printf("c的类型为%T\n", c)
    fmt.Printf("d的类型为%T\n", d)
    }

    func main() {
    test1()
    }
  • 练习2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //练习2
    //编写代码统计出字符串"hello沙河小王子"中汉字的数量
    package main

    import (
    "fmt"
    "unicode"
    )

    func test2() {
    s := "hello沙河小王子"
    count := 0
    for _, c := range s {
    if unicode.Is(unicode.Han, c) {
    count = count + 1
    }
    }
    fmt.Printf("字符串包含%v个中文", count)
    }

    func main() {
    test2()
    }
  • 练习3

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //练习3
    //求数组[1, 3, 5, 7, 8]所有元素的和
    package main

    import (
    "fmt"
    "unicode"
    )

    func test3() {
    arr := [...]int{1, 3, 5, 7, 9}
    count := 0
    for _, v := range arr {
    count = count + v
    }
    fmt.Printf("result:%v", count)
    }

    func main() {
    test3()
    }
  • 练习4

    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
    //练习4
    //找出数组中和为指定值的两个元素的下标,
    //比如从数组[1, 3, 5, 7, 8]中找出和为8的两个元素的下标分别为(0,3)和(1,2)。
    package main

    import "fmt"

    func test4() (int, int) {
    result := 8
    resultmap := make(map[int]int)
    arr := [...]int{1, 3, 5, 7, 9}
    for index, v := range arr {
    gapv := result - v
    va, ok := resultmap[gapv]
    if ok {
    return index, va
    }
    resultmap[v] = index
    }
    fmt.Println(resultmap)
    return 0, 0
    }

    func main() {
    res1, res2 := test4()
    fmt.Printf("%v,%v", res1, res2)
    }

Talk is cheap,show me the code~

如何实现一个带括号的四则混合运算表达式求值

发表于 2020-11-25 | 分类于 数据结构与算法

本题其实相对比较简单,只要了解如何利用stack将中缀表达式转为后缀表达式(去括号的过程),然后对后缀表达式进行求值即可!这里不对stack的结构和原理进行讲解,可自行查阅相关数据,也比较简单。

  • 代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //实现 (1+2)*4*(5+1)/2+3 求值
    public class ComputerReview {
    public static void main(String[] args){
    Scanner sc = new Scanner(System.in);
    String s = sc.nextLine();
    //1去括号,中缀转后缀
    ArrayList<String> backExpList = tansToBackExp(s);
    //2后缀计算
    int result = compute(backExpList);
    System.out.println(result);
    }
    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
    //这一步就是利用stack的特性将中缀表达式转换为后缀表达式同时去除掉括号的过程
    //实现方式有很多种,这里只是我的思路
    private static ArrayList tansToBackExp(String s) {
    ArrayList<String> al = new ArrayList();
    Stack<String> stack = new Stack<>();
    for(int i = 0 ; i < s.length() ; i++){
    String c = s.charAt(i)+"";
    if(c.matches("\\d")){
    al.add(c);
    }else {
    if(c.equals("(")){
    stack.push(c);
    }else if(c.equals("+")||c.equals("-")){
    while(!stack.isEmpty()){
    if(stack.peek().equals("(")){
    break;
    }
    al.add(stack.pop());
    }
    stack.push(c);
    }else if(c.equals("*")||c.equals("/")){

    while(!stack.isEmpty()){
    if(stack.peek().equals(" (")||stack.peek().equals("+")||stack.peek().equals("-")){
    break;
    }
    if(c.equals("*")||c.equals("/")){
    al.add(stack.pop());
    }
    }
    stack.push(c);
    }else {
    while(!stack.isEmpty()){
    if(!stack.peek().equals("(")){
    al.add(stack.pop());
    }else{
    //这一步很关键
    stack.pop();
    break;
    }
    }
    }
    }
    }
    while(!stack.isEmpty()){
    al.add(stack.pop());
    }
    return al;
    }
    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
    //这一步相对来说就比较简单了,直接对转换后的后缀表达式求值,还是利用栈的特性
    //遍历后缀表达式,遇到数字就压栈处理,遇到表达式就出栈计算结果然后将结果再压栈,
    //最后栈中剩下的数字就是最终的结果
    private static Integer compute(ArrayList<String> al) {
    Stack<String> stack = new Stack<String>();
    for(int i = 0 ; i < al.size(); i++){
    String s = al.get(i);
    if(s.matches("\\d")){
    stack.push(s);
    continue;
    }
    String str2 = stack.pop();
    String str1 = stack.pop();
    if("+".equals(s)){
    stack.push(Integer.parseInt(str1)+Integer.parseInt(str2)+"");
    }else if("-".equals(s)){
    stack.push(Integer.parseInt(str1)-Integer.parseInt(str2)+"");
    }else if("*".equals(s)){
    stack.push(Integer.parseInt(str1)*Integer.parseInt(str2)+"");
    }else if("/".equals(s)){
    stack.push(Integer.parseInt(str1)/Integer.parseInt(str2)+"");
    }
    }
    return Integer.parseInt(stack.peek());
    }
  • 总结

    栈是一个很重要的数据结构,FILO、LIFO的策略值得我们好好理解学习,jvm中的栈以及本地方法栈也都是采用了这样的数据结构

12345
史蒂芬猴

史蒂芬猴

dora is my precious

49 日志
27 分类
38 标签
GitHub
© 2023 史蒂芬猴
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
访客数 人 总访问量 次