最近在看程杰写的《大话数据结构》,之前也看过他写的《大话设计模式》,虽然书中好多段子都很“尬”,但是整体风格还是很合我的胃口的。

今天看到第四章,在介绍栈的时候提到了逆波兰表示法,这个算法好像上学的时候在 POJ 上面做过。算法我也好久没碰了,今天用Golang实现一下,就当练练手吧。

代码很简单,我写了比较详细的注释,那就直接看代码吧:

package main

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"strconv"
)

type node struct {
	value interface{}
	next  *node
}

// 栈的链式结构实现
type Stack struct {
	top    *node
	length int
}

func NewStack() *Stack {
	return &Stack{nil, 0}
}

func (s *Stack) Len() int {
	return s.length
}

// 获取栈顶元素
func (s *Stack) Peek() interface{} {
	if s.length == 0 {
		return nil
	}
	return s.top.value
}

// 弹出栈顶
func (s *Stack) Pop() interface{} {
	if s.length == 0 {
		return nil
	}
	n := s.top
	s.top = n.next
	s.length--
	return n.value
}

// 压栈
func (s *Stack) Push(value interface{}) {
	n := &node{value, s.top}
	// 从头部插入
	s.top = n
	s.length++
}

func main() {
	var str string
	fmt.Printf("Please enter expression: ")
	scanner := bufio.NewScanner(os.Stdin)
	if scanner.Scan() {
		// 读取表达式
		str = scanner.Text()
	}
	if err := scanner.Err(); err != nil {
		panic(err)
	}
	// 转为字符串数组
	exp := toExp(str)
	fmt.Print("Infix expression: ")
	printExp(exp)
	// 转为后缀表达式
	postfixExp := toPostfix(exp)
	fmt.Print("Postfix expression: ")
	printExp(postfixExp)
	// 计算结果
	value := calValue(postfixExp)
	fmt.Println(fmt.Sprintf("Result: %d", value))
}

// 将数字和操作符转为字符串数组
func toExp(str string) []string {
	s := make([]string, 0)
	var t bytes.Buffer
	n := 0 // 用于判断括号是否成对
	for _, r := range str {
		if r == ' ' {
			// 去掉空格
			continue
		}
		if isDigit(r) {
			// 是数字 就写到缓存中
			t.WriteRune(r)
		} else {
			rs := string(r)
			if !isSign(rs) {
				panic("unknown sign: " + rs)
			}
			if t.Len() > 0 {
				// 遇到符号 把缓存中的数字 输出为数
				// 例如 将缓存中的 ['1', '2', '3'] 输出为 "123"
				s = append(s, t.String())
				t.Reset()
			}
			s = append(s, rs)
			if r == '(' {
				n++
			} else if r == ')' {
				n--
			}
		}
	}
	if t.Len() > 0 {
		// 最后一个操作符后面的数字 如果最后一个操作符是 ")" 那么 t.Len() 为0
		s = append(s, t.String())
	}
	if n != 0 {
		panic("the number of '(' is not equal to the number of ')' ")
	}
	return s
}

func printExp(exp []string) {
	for _, s := range exp {
		fmt.Print(s, " ")
	}
	fmt.Println()
}

// 是否数字
func isDigit(r rune) bool {
	if r >= '0' && r <= '9' {
		return true
	}
	return false
}

// 是否符号
func isSign(s string) bool {
	switch s {
	case "+", "-", "*", "/", "(", ")":
		return true
	default:
		return false
	}
}

// 中缀表达式转后缀表达式
func toPostfix(exp []string) []string {
	result := make([]string, 0)
	s := NewStack()
	for _, str := range exp {
		if isSign(str) {
			// 若是符号
			if str == "(" || s.Len() == 0 {
				// "(" 或者 栈为空 直接进栈
				// 括号中的计算 需要单独处理 相当于一个新的上下文
				// 如果栈为空 需要先进栈 和后续操作符比较优先级之后 才能决定计算顺序
				s.Push(str)
			} else {
				if str == ")" {
					// 若为 ")" 依次弹出栈顶元素并输出 直到遇到 "("
					for s.Len() > 0 {
						if s.Peek().(string) == "(" {
							s.Pop()
							break
						}
						result = appendStr(result, s.Pop().(string))
					}
				} else {
					// 判断其与栈顶符号的优先级
					// 如果栈顶是 "(" 说明是新的上下文 不能相互比较优先级
					for s.Len() > 0 && s.Peek().(string) != "(" && signCompare(str, s.Peek().(string)) <= 0 {
						// 当前符号的优先级 不大于栈顶元素 弹出栈顶元素并输出
						// 优先级高的操作 需要先计算
						// 优先级相同 因为栈中的操作是先放进去的 也需要先计算
						result = appendStr(result, s.Pop().(string))
					}
					// 当前符号入栈
					s.Push(str)
				}
			}
		} else {
			// 若是数字就输出
			result = appendStr(result, str)
		}
	}
	for s.Len() > 0 {
		result = appendStr(result, s.Pop().(string))
	}
	return result
}

func appendStr(slice []string, str string) []string {
	if str == "(" || str == ")" {
		// 后缀表达式 不包含括号
		return slice
	}
	return append(slice, str)
}

// 比较符号优先级
func signCompare(a, b string) int {
	return getSignValue(a) - getSignValue(b)
}

// 优先级越高 值越大
func getSignValue(a string) int {
	switch a {
	case "(", ")":
		return 2
	case "*", "/":
		return 1
	default:
		return 0
	}
}

// 通过后缀表达式 计算值
func calValue(exp []string) int {
	s := NewStack()
	for _, str := range exp {
		if isSign(str) {
			// 如果是符号 弹出栈顶的两个元素 进行计算
			// 因为栈结构先进后出 所以先弹出b
			b := getInt(s)
			a := getInt(s)
			var n int
			switch str {
			case "+":
				n = a + b
			case "-":
				n = a - b
			case "*":
				n = a * b
			case "/":
				n = a / b
			}
			// 计算结果压栈
			s.Push(n)
		} else {
			// 数字直接压栈 也可以在这里做类型转换 使栈中的值均为int
			s.Push(str)
		}
	}
	// 栈顶元素 为最终结果
	return getInt(s)
}

// 弹出栈顶元素 并转为int
func getInt(s *Stack) int {
	v := s.Pop()
	switch v.(type) {
	case int: // push进去的计算结果为int
		return v.(int)
	case string: // exp中的数据为string
		if i, err := strconv.Atoi(v.(string)); err != nil {
			panic(err)
		} else {
			return i
		}
	}
	panic(fmt.Sprintf("unknown value type: %T", v))
}

运行结果:

Please enter expression: 9+(3-1)*3+10/2
Infix expression: 9 + ( 3 - 1 ) * 3 + 10 / 2 
Postfix expression: 9 3 1 - 3 * + 10 2 / + 
Result: 20

总结

逆波兰表示法解决了计算机不容易处理中缀表达式的问题。之前策划提出过能否在技能配置表中,使用字符串来配置每个技能的伤害计算公式,以方便更好的定制技能效果。这个需求其实就可以使用逆波兰表示法解决。不过后来被我以性能较差为由给拒绝了。看一下benchmark吧,这只是一个简单的例子,如果真的应用在技能系统里,还需要动态解析玩家属性等操作,差距会更大一些。

package main

import (
	"testing"
)

var exp []string

func init() {
	str := "1+(2*3+4-5*6/2+7)-2"
	exp = toExp(str)
}

func BenchmarkRPN(b *testing.B) {
	var n int
	for i := 0; i < b.N; i++ {
		n = calValue(toPostfix(exp))
	}
	b.Log(n)
}

func BenchmarkDirect(b *testing.B) {
	var n int
	for i := 0; i < b.N; i++ {
		n = 1 + (2*3 + 4 - 5*6/2 + 7) - 2
	}
	b.Log(n)
}


运行结果:

500000	      2787 ns/op
--- BENCH: BenchmarkRPN-8
	main_test.go:19: 1
	main_test.go:19: 1
	main_test.go:19: 1
	main_test.go:19: 1
2000000000	         0.29 ns/op
--- BENCH: BenchmarkDirect-8
	main_test.go:27: 1
	main_test.go:27: 1
	main_test.go:27: 1
	main_test.go:27: 1
	main_test.go:27: 1
	main_test.go:27: 1