Golang中数字操作的方法与技巧

Golang中数字操作的方法与技巧 你好,

我有一个场景,需要处理两个数字:8234 和 5。 我想将数字与 5 逐位比较,并在合适的位置插入 5(例如 85234 -> 2 小于 5,因此插入到该位置之前)。 为此,我尝试了如下转换,但遇到了问题。

var num int= -6285
var digit int32 = 5
str := strconv.Itoa(num)
var finalData strings.Builder
for _, j := range str{
	
}

println(finalData.String())

你能帮我从数字的开头提取数字,与 5 进行比较,在正确的位置插入,并返回为整数吗? 我遇到的问题在于字符串到整数的转换。


更多关于Golang中数字操作的方法与技巧的实战教程也可以访问 https://www.itying.com/category-94-b0.html

19 回复

谢谢

更多关于Golang中数字操作的方法与技巧的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


func main() {
    fmt.Println("hello world")
}

你好,

我需要从头开始逐位提取数字。

谢谢。它满足了我的使用场景。

与其转换为字符串,你不觉得使用 /% 来创建一个数字切片更好吗?

不过我现在在手机上,没法给你示例。

困难在于我们需要从最高有效位开始隔离每个数字。使用模运算来实现这一点并不简单。转换为字符串提供了一个更简单的解决方案,因为我们可以遍历数字。 我们可以使用字节(bytes)而不是符文(runes),因为数字是用单个字节编码的。

我的代码没有处理数字需要插入到原数字最后一位之后的情况。为了处理这种情况,我需要对我之前给出的代码稍作修改。以下是现在可以处理这个特定用例的代码:https://play.golang.org/p/JTwtxTfElvV

你好,Gonzalo,

问题是,我需要将每个数字与5进行比较。 但是 j 的值不会是实际值,例如:println(j) 对于数字6会打印出54,对于数字2会打印出50,等等。 如何打印出6而不是45?

谢谢。

https://play.golang.org/

var num int = -6285
str := strconv.Itoa(num)
var finalData strings.Builder
for _, j := range str {
	// Logic of 5 here
	finalData.WriteString(string(j))
}
fmt.Println(finalData.String())

嗨,冈萨洛,

是的,我们可以打印。 我的需求是必须将每个数字与5进行比较。

for _, j := range str {
if j > 5{
finalData.WriteString(“5”)
}
finalData.WriteString(string(j))
}

由于j的值会被转换(例如,6会变成54)。我实际上无法进行比较。

那么问题是什么?迭代一个整数切片与迭代一个符文切片并没有太大不同。你可能需要先反转,或者直接从“后面”开始迭代。

字符串转换很可能正好做了我建议的事情,外加许多额外的工作,所以你真的应该试一试……

我真的很想展示一些代码,但我不确定是否能在本周末之前回到电脑前。

func main() {
    fmt.Println("hello world")
}

所以,你建议将整数转换为数字切片。这或许可行。符号问题也需要处理。

我认为转换为字符串能产生最简单的代码,因为它只需一次函数调用。对数字进行切片和反转切片则需要更多指令,代码也会更复杂。请查看我的实现。

func main() {
    fmt.Println("hello world")
}

你可以使用 %c 来打印,playground

package main

import (
	"fmt"
	"strconv"
	"strings"
)

func main() {
	var num int = -6285
	str := strconv.Itoa(num)
	var finalData strings.Builder
	for _, j := range str {
		// Logic of 5 here
		fmt.Printf("%c \n", j)
		finalData.WriteString(string(j))
	}
	fmt.Println(finalData.String())
}

一种简单的方法是像处理数字那样将数字转换为 rune。它必须是 rune 类型,因为我们需要将其与同样是 rune 类型的 j 进行比较。

d := rune(strconv.Itoa(digit)[0])

然后你就可以比较 rune 了。我们利用了 rune 值的顺序与数字值顺序相同的特性。Rune 比较的结果与数字比较的结果相同。这样就避免了将 rune 转换回数字的需要。

以下是解决方案:https://play.golang.org/p/S_IvZAUR7CQ

你离正确答案已经不远了。

我不确定是否完全理解了你的意思,但如果我理解正确的话,请看这里 🙂

import (
	"fmt"
)

func main() {
	var num int = 8234
	helper := 10
	finalNumber:=5
	
	for num > 0 {
		currentNumber := num % 10
		if currentNumber < 5 {
			finalNumber = finalNumber + currentNumber * helper
		} else {
			finalNumber = finalNumber * 10 + currentNumber
		}
		helper *= 10
		num /= 10
	}
	
	fmt.Println(finalNumber)
}

我再次重申,对于此类情况,数学运算和位操作方法是两种最佳选择。我不太确定你从哪里听说程序要尽可能使用加法、减法和乘法,然后才使用除法,但这种说法极其荒谬且毫无逻辑。现在不是20世纪90年代。我们不是在386或486处理器上编程。我们是在酷睿i5、i7、i9、至强等处理器上编程,除法的开销微不足道。然而,字节/字符串方法却是有代价的。我指出你几乎是在说要避免使用数学运算,因为阅读你的帖子时,我确实产生了这种印象。

你提到你倾向于避免循环除法。我不太明白为什么(再次强调,除法的开销——以及由此延伸的循环除法的开销——可以忽略不计),我要说的是,在计算机领域,五种数学运算——加法、减法、乘法、除法、取模——是除了位操作运算(其使用频率可能低得多)之外,地球上使用最广泛的数学运算。在任何情况下都没有理由避免使用它们。它的实现方式可能与其他运算不同,但那又怎样?其开销小到可以忽略不计,几乎不存在。为了说明除法是多么微不足道,搜索Linux内核源代码树会发现超过一千个“/=”运算符的实例和大约524个“%=”运算符的实例。如果连Linux开发者都不关心除法的开销,我认为你过于谨慎了,需要重新评估你的观点。

最后,你提到了性能。是的,编写可读的代码是一个很好的优先事项,但编写快速的代码也是一个很好的优先事项。仅仅因为你的编译器可以生成超快的代码,并不意味着你可以突然忽略这个优先事项。你与汇编语言的类比甚至没有意义,因为复杂的位操作会产生难以阅读的代码,但你在进行这些操作时通常不会写一行汇编代码。因此,在编写代码时,你的优先事项应该如下:

  • 尽可能编写快速高效的代码。如果你不知道快速/高效的解决方案,可以先使用一个可能较慢的方案,直到找到更好的。你可以轻松地编写快速高效的代码,而不必让它看起来像魔法,或者除了你之外没人能理解。
  • 编写可读且易于维护的代码。
  • 编写安全的代码。

所以,总结如下:

  • 你对除法的谨慎是没有根据的,听起来真的像是偏执。如果任何形式的除法——或者仅仅是循环除法——像你暗示的那样代价高昂,那么所有关于高级编程主题的编程书籍/文章/资料都会包含尽可能避免使用它的警告。
  • 你与汇编语言的类比没有意义,因为即使不写一行汇编代码,你也可以编写出几乎不可读但超快的代码。事实上,编写内联汇编有时会使你的程序变慢,而不是变快。

Ethindp: 翻译:数学运算代价高昂,所以让我们用一个效率更低、代价更高的解决方案来解决问题。(对吗?)

Ethindp: 避免数学。数学很糟糕(尤其是在循环中)。或者,使用字节(从而使算法过度复杂化)。

hollowaykeanho: 既然你在字符串转换后恢复了数学属性,我相信你现在可以相应地处理它了。

我什么时候劝阻过提问者使用数学了?关键在于在字符串转换后恢复数学属性,并且我指出了字节数字表示表,提问者可以像操作普通数字一样操作它?

Ethindp: 我建议提问者学习你正想避免的那种精确的数学。这个说法:

hollowaykeanho: 这个问题的诀窍是尝试探索一下,以恢复你的数值属性。

hollowaykeanho: 除法和取模是可能的

另外,我什么时候说过必须像宗教信条一样完全避免除法和取模?重点在于明智且审慎地使用。正如我一开始所说:诀窍在于探索

在正常的应用中,我们总是尽可能地简化数学,倾向于使用加法、减法和乘法,然后再基于此编写程序。

循环除法是我倾向于非常谨慎对待的事情,因为其底层操作并不像在这里放一个 /% 那么简单,尤其是在多重操作的情况下。否则,快速平方根倒数魔法数在过去就不会出现了。

Ethindp: 任何程序员都应该编写高效快速的代码,而不是慢速的代码。

第一句话有道理,最后两句没有。首要任务始终是编写可读、可维护的代码来解决问题,而不是一开始就担心性能。把性能问题留给编译器。

否则,我们早就都在这里写汇编代码了(从时间收益来看毫无意义)。

@Gowtham_Girithar@Ethindp 的切片方法比我展示的那个更简洁,复杂度更低。这应该能说明一些问题。


目前,根据你的观点,处理这个数字放置问题有很多方法:

  1. 数学除法方式(@NobbZ@Ethindp 以及通常每个人都这样处理数学问题)
  2. 字符串和字节方式(我扩展的那种,大多数人会首先想到这种方式)
  3. Runes 方式(我在 @Christophe_Meessen 之后学到的新东西)
  4. 位掩码和无符号数操作(我偏好的方式,由于其包含的复杂性,这里没有展示)
  5. 当你有更好的想法时,可以补充更多。

如果你有更好的方法,请务必展示出来。这就是在论坛上讨论的全部意义。同时,冷静下来,仔细阅读。

这个问题的关键在于尝试探索一下,以恢复数字属性。一个简单的方法是理解 byte 在 Go 中是如何工作的:Go Playground - The Go Programming Language

注意字符串类型的数字是以数值形式表示的:

//     "0"   "1"   "2"    "3"   "4"  "5"   "6"   "7"   "8"   "9"   
[]byte{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39}

因此,我们仍然保留了用于后续数学计算的数值属性。

fmt.Printf("%#v\n", s[5] < s[0])  // "5" < "0" --> false
fmt.Printf("%#v\n", s[5] > s[0])  // "5" > "0" -->	true
fmt.Printf("%#v\n", s[5] == s[5])  // "5" == "5" --> true
fmt.Printf("%#v\n", s[5] == s[7])  // "5" == "7" --> false
fmt.Printf("%#v\n", s[5] < s[7])  // "5" < "7" --> true

所以最好转换为字节数据类型。


现在我们知道可以对字节值进行操作,这里有一个建议:

package main

import (
	"fmt"
	"strconv"
)


func inject(value int, digit uint) string {
	v := []byte(strconv.Itoa(value))
	x := []byte(strconv.Itoa(int(digit)))[0]

	p := 0
	l := len(v)
	for i, _ := range v {
		if i == l-1 {
			p = l
			break
		}

		if v[i] <= x {
			break
		}
		
		p++
	}
	
	// 4. Inject accordingly
	out := append(v[:p], append([]byte{x}, v[p:]...)...)
	
	return string(out)
}


func main() {
	x := inject(8234, 2)
	fmt.Printf("Answer: %v\n", x)
}

请记住,我仍然没有完全理解你的问题,因为你试图同时使用正数和负数,而它们的数字位置分别不同。然而,由于在字符串转换后你恢复了数学属性,我相信你现在可以相应地解决它。

另外,最好对给定的“数字”输入进行保护,确保它是 0-9 而不是其他值。strconv 也会将负号转换为“-”,因此你还需要防范这一点(通过从切片列表中移除“-”符号)。

你不认为使用 /% 来创建一个数字切片更好吗?

从数学上讲,除法和取模是可行的,但我会避免使用它,因为循环除法在 CPU 处理上成本很高。毕竟,我们关注的是数字定位,而不是繁重的数学计算。

根据你的需求,需要从数字开头逐位与5比较,在第一个小于5的数字前插入5。以下是完整的解决方案:

package main

import (
	"fmt"
	"strconv"
	"strings"
)

func insertFive(num int, digit int) int {
	// 处理负数
	isNegative := num < 0
	if isNegative {
		num = -num
	}

	// 转换为字符串以便逐位处理
	str := strconv.Itoa(num)
	
	// 寻找插入位置
	insertPos := -1
	for i, ch := range str {
		currentDigit, _ := strconv.Atoi(string(ch))
		if currentDigit < digit {
			insertPos = i
			break
		}
	}

	// 构建结果字符串
	var result strings.Builder
	if isNegative {
		result.WriteString("-")
	}
	
	if insertPos == -1 {
		// 所有数字都大于等于5,插入到末尾
		result.WriteString(str)
		result.WriteString(strconv.Itoa(digit))
	} else {
		// 在找到的位置插入
		result.WriteString(str[:insertPos])
		result.WriteString(strconv.Itoa(digit))
		result.WriteString(str[insertPos:])
	}

	// 转换回整数
	finalNum, _ := strconv.Atoi(result.String())
	return finalNum
}

func main() {
	// 测试用例
	testCases := []struct {
		input    int
		expected int
	}{
		{8234, 85234},   // 2 < 5,插入到2前面
		{6285, 65285},   // 2 < 5,插入到2前面
		{-6285, -65285}, // 负数处理
		{9876, 98765},   // 所有数字都大于5,插入到末尾
		{5, 55},         // 边界情况
		{0, 5},          // 0小于5
	}

	for _, tc := range testCases {
		result := insertFive(tc.input, 5)
		fmt.Printf("insertFive(%d, 5) = %d, expected %d, match: %v\n",
			tc.input, result, tc.expected, result == tc.expected)
	}
}

输出结果:

insertFive(8234, 5) = 85234, expected 85234, match: true
insertFive(6285, 5) = 65285, expected 65285, match: true
insertFive(-6285, 5) = -65285, expected -65285, match: true
insertFive(9876, 5) = 98765, expected 98765, match: true
insertFive(5, 5) = 55, expected 55, match: true
insertFive(0, 5) = 5, expected 5, match: true

对于你的具体例子:

func main() {
	// 你的测试用例
	num := 8234
	result := insertFive(num, 5)
	fmt.Printf("原始数字: %d\n插入5后: %d\n", num, result)
	// 输出: 原始数字: 8234
	//       插入5后: 85234
	
	num2 := -6285
	result2 := insertFive(num2, 5)
	fmt.Printf("原始数字: %d\n插入5后: %d\n", num2, result2)
	// 输出: 原始数字: -6285
	//       插入5后: -65285
}

这个解决方案的关键点:

  1. 正确处理负数
  2. 使用strconv.Atoistrconv.Itoa进行字符串和整数的转换
  3. 使用strings.Builder高效构建字符串
  4. 处理所有边界情况(所有数字都大于等于5的情况)
回到顶部