Go语言实践[回顾]教程10--学习成绩统计的示例【中】

发布于:2022-12-07 ⋅ 阅读:(681) ⋅ 点赞:(0)

Go语言实践[回顾]教程10--学习成绩统计的示例【中】

基于整体需求优化上节代码

  在上一节中,是基于三个基本需求各自独立实现的逻辑,创建了分别完成各自任务的三个函数,然后依次执行。现在我们把上一节代码和需求整体看一下,各需求之间并不是完全独立的,是有一定的关联的。基于需求最后的整体目标,就是输出所有的统计信息,并没有对中间的单独子需求提出单独输出结果的要求。这样我们就可以针对统计及格人数、取前三名分数、整体排序三个子需求的关联性做逻辑调整,使程序运行效率更佳。

  先梳理一下优化的整体逻辑:
  ● 先使用标准库提供正序排序函数对数组进行排序,得到从小到大排列的正序排列的数组和切片
  ● 然后使用 for循环 将正序排序后的切片依次(正序循环)给 用于存放倒序结果的切片 从后向前赋值,这样效率比上一节中要用三个标准库排序函数效率更高,因为不用再排序了,正序结果反着顺序赋一遍值就是倒序结果了。
  ● 在这同一个 for循环 里,直接判断分数是否及格并且及格人数变量没有赋过值?如果成立,说明当前循环的位置以后都是及格的(因为是从小到大的正序排序后的数据),所以用数组长度减去循环计数变量的值,就刚好是后面剩下的元素个数,也就是及格人数,赋值给及格人数变量即可。
  及格人数变量初始化为 0 值,所以第一次判断分数及格能进入判断体内。但是经过刚才的赋值,下一轮循环虽然分数也是及格,但人数变量以不是初始化的值了,第二个条件不成立了,就不会再进入判断体了,及格人数变量就不会再次被修改了。也就是整个循环结束,及格人数变量只会被赋值一次,且是第一次循环到及格的那次。
  ● 上述循环结束后,就得到了一个倒序排列的成绩切片,那我们直接把这个切片中的前三个元素再次切片出来,就是前三名的成绩了。

  下面是根据上面逻辑优化后的代码:

// 统计来自 int 类型数组的数据,文件名 arr_data_count.go
package count

import (
	"fmt"
	"score_count/data"
	"sort"
)

// ArrDataCount  对一维数组进行排序、统计60分及以上人数,前三名分数
func ArrDataCount() {
	// 获取数组长度赋值给 len 以免后面多处使用重复计算
	len := len(data.OnlyScore)

	// 引用数组全部内容给切片 sortArr
	sortArr := data.OnlyScore[:]

	// 使用标准库提供的正序排序函数给切片 sortArr 排序
	sort.Ints(sortArr)

	// 创建一个与源数据数组同样长同样类型的切片,用于存放倒序结果
	reverseArr := make([]int, len)

	passNum := 0 // 用于保存及格人数

	// 将排序好的数组颠倒顺序,使用for循环综合性能更好
	// 统计及格数量刚好也要用for循环,干脆放在一个排序后的循环里
	for i := 0; i < len; i++ {
		v := sortArr[i]
		reverseArr[len-i-1] = v

		if passNum == 0 && v >= 60 {
			passNum = len - i
		}
	}

	// 对倒序排序后的成绩数组直接切片前三个元素就刚好是前三名的成绩
	topThree := reverseArr[:3]

	fmt.Println("及格人数:", passNum)
	fmt.Printf("及格率为: %d%%\n", passNum*100/len)
	fmt.Println("前三分数:", topThree)
	fmt.Println("成绩正序:", sortArr)
	fmt.Println("成绩倒序:", reverseArr)
}

  第22行,使用 make() 函数直接创建一个长度与源数组长度一致的切片,而不是引用源数组的切片,这样就不会影响源数组本身的数据,且可以使用变量定义长度。

  第28行中的 len,替换了上一节代码该位置的 len(data.OnlyScore) 表达式,而是在第13行提前将 len(data.OnlyScore) 的结果赋值给了 len 变量。这样改动的目的也是优化性能。一方面本次优化代码中多处使用了源数组长度,如果都使用 len(data.OnlyScore),那么势必造成多次计算同一个值。另一方面,即使仅在 for 循环的条件一个地方使用,但实际计算次数是与循环的执行次数相同的,也就是每循环依次都要执行一次判断,执行判断就要计算一次。所以提前将结果保存到变量,避免了重复计算。

  第29行,这里的变量 v,创建的原因与前面创建变量 len 一样,就不多说了。

  第32行,使用 && 这个并且关系运算符,要判断两个条件。passNum == 0 这个条件是为了避免循环到所有及格的元素时都进入判断体的,以免造成 passNum 被多次赋值。

  第32行,[:3] 表示从数组或切片的第一个元素开始,连续取到 3 号索引为止,3 号本身不取,生成新的切片,也就是等于取前三个元素生成新的切片。

需求增加按等级分类统计

  现在需求扩展了,需要在原需求基础上增加按照等级分类统计一下每个分类有多少人。分类标准:100分-90分为A级,89分-75分为B级,74分-60分为C级,59分及其以下为D级。

  延续上面的逻辑,我们怎么实现这新增的需求呢?我们先来看实现后的源代码:

// 统计来自 int 类型数组的数据,文件名 arr_data_count.go
package count

import (
	"fmt"
	"score_count/data"
	"sort"
)

// ArrDataCount  对一维数组进行排序、统计60分及以上人数,前三名分数
func ArrDataCount() {
	// 获取数组长度赋值给 len 以免后面多处使用重复计算
	len := len(data.OnlyScore)

	// 引用数组全部内容给切片 sortArr
	sortArr := data.OnlyScore[:]

	// 使用标准库提供的正序排序函数给切片 sortArr 排序
	sort.Ints(sortArr)

	// 创建一个与源数据数组同样长同样类型的切片,用于存放倒序结果
	reverseArr := make([]int, len)

	passNum := 0 // 用于保存及格人数

	levelA, levelB, levelC, levelD := 0, 0, 0, 0

	// 将排序好的数组颠倒顺序,使用for循环综合性能更好
	// 统计及格数量刚好也要用for循环,干脆放在一个排序后的循环里
	for i := 0; i < len; i++ {
		v := sortArr[i]
		reverseArr[len-i-1] = v

		// 利用正序排列特点,综合判断统计各分数段
		if levelD == 0 && v >= 60 {
			passNum = len - i
			levelD = i
		} else if levelC == 0 && v >= 75 {
			levelC = i - levelD
		} else if levelB == 0 && v >= 90 {
			levelB = i - levelC - levelD
			levelA = len - i
		}
	}

	// 对倒序排序后的成绩数组直接切片前三个元素就刚好是前三名的成绩
	topThree := reverseArr[:3]

	fmt.Println("及格人数:", passNum)
	fmt.Printf("及格率为: %d%%\n", passNum*100/len)
	fmt.Println("前三分数:", topThree)
	fmt.Println("成绩正序:", sortArr)
	fmt.Println("成绩倒序:", reverseArr)
	fmt.Println("A 级人数:", levelA)
	fmt.Println("B 级人数:", levelB)
	fmt.Println("C 级人数:", levelC)
	fmt.Println("D 级人数:", levelD)
}

  第26行,声明4个初始值为 0 的 int 类型的变量,用于保存A、B、C、D四个等级的人数。

  第36~43行,是在循环体内综合统计及格人数和四个等级人数的判断逻辑。这段代码是可以用如下代码代替的:

switch {
case v <= 59:
	levelD++
case v >= 60 && v <= 74:
	levelC++
case v >= 75 && v <= 89:
	levelB++
case v >= 90:
	levelA++
}
passNum = levelA + levelB + levelC

  也或者换成下面这段:

switch {
case v <= 59:
	levelD++
case v >= 60 && v <= 74:
	levelC++
	passNum++
case v >= 75 && v <= 89:
	levelB++
	passNum++
case v >= 90:
	levelA++
	passNum++
}

  也或者把这两段 switch case 语句换成 if else 语句实现,都是没问题的,也相对更好理解。但我上面完整的源代码中为什么没有这样写呢,有何区别?
  仔细看它们之间的计算次数差异,完整源代码中使用的方式,计算次数明显要少于后面两种举例可以替换的通常方式。这种差别如果数据量较小的话,几乎没啥影响,在数据量较大时,对性能的影响就相对明显了。所以这是为了提醒大家,同样完成一个需求,不同的编写逻辑和方式,对运行效率的高低是有差别的,所以流程控制、判断逻辑是很重要的,这与使用哪种编程语言关系不大。所以要深入分析,锻炼自己的编程思维,以便可以通过优化代码实现性能的提升。每处提高一点,一个项目的代码累加到一起可能就提高很多,所以在时间允许的情况下,尽量仔细分析代码逻辑,尽力优化到更优。

  第54~57行,是新增加的打印输出。

本节小结

  以下是对本节涉及的 Go 语言编程内容的归纳总结,方便记忆:

  ● make(),用于创建slice(切片)、map(映射,下一节会涉及)和 channel(后面章节讨论),并返回它们的实例,创建数组格式如下:
  make([]类型, 元素个数),如 make([]int, 5) 表示创建一个有5个int类型元素的切片。
  make([]类型, 元素个数, 预留最大长度),如 make([]int, 5, 10) 表示创建一个有5个int类型元素的切片,但动态增加元素个数到10个之内都不会触发重新分配内存(重新分配内存很影响性能),因为创建时在内存中按照总计10个元素的位置预留的。

  ● 当某个计算表达式多次使用时,建议创建一个变量保存该表达式的值,然后使用变量,不再多处使用同一个表达式。这样可以减少计算次数,提高运行效率。这对 for循环 中的条件语句中的判断值更为重要,如果在该位置使用计算表达式,实际执行时就会计算与循环次数相同的次数,提前将计算结果赋值给变量就不会有此情况。
.
.
上一节:Go/Golang语言学习实践[回顾]教程09–学习成绩统计的示例【上】

下一节:Go/Golang语言学习实践[回顾]教程11–学习成绩统计的示例【下】
.

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到