數據結構之棧——算術表達式求值

定義

棧是一種特殊的線性表,它只能在一端進行插入或者刪除操做,能進行操做的一端稱爲棧頂,另外一端則稱爲棧底。也因爲這個特性,致使先進入的元素,只能後出,所以棧是後進先出的線性表。git

棧是一種線性表,所以它的存儲能夠是鏈式存儲,也能夠是順序存儲。鏈式存儲的棧,稱爲鏈棧,順序存儲的棧,稱爲順序棧。而下面實例中,使用go語言的可變數組slice完成棧的存儲,所以是順序存儲。算法

棧的描述數組

type data interface {}

type Stack struct {
	top int // 棧頂,表明棧頂的下標,-1爲空棧
	list []data // 從0開始存儲
}

func New() *Stack{
	s := &Stack{top:-1,list: []data{}}
	return s
}
複製代碼

主要操做

入棧

在棧頂插入一個新元素,稱爲入棧,首先要判斷棧是否已滿,滿就報錯,不然就插入,並將棧頂上升一位。數據結構

// 因爲使用的是可變數組,所以不會出現棧滿的狀況,因此如下代碼不須要判斷是否棧滿,若是是不可變數組,或者是限制容量的棧,則要判斷棧是否已滿
func (s *Stack) Push (value data) {
   s.list = append(s.list,value)
   s.top++
}
複製代碼

出棧

取出棧頂元素,並將棧頂降低一位,若是棧爲空,則報錯app

func (s *Stack) Pop() (data,error) {
	if s.top == -1 {
		return nil,errors.New("棧空")
	}
	data := s.list[s.top]
	s.list = s.list[:s.top]
	s.top--
	return data,nil
}
複製代碼

判空

判斷是否爲空棧函數

func (s *Stack) IsEmpty() bool{
	return s.top == -1
}
複製代碼

獲取棧頂元素

獲得棧頂元素的值,但不出棧post

func (s *Stack) GetTop() data{
	if s.top == -1 {
		return nil
	}
	return s.list[s.top]
}
}
複製代碼

應用:算術表達式求值

後綴表達式

咱們平常生活中使用的算術表達式,例如:5+6/2-3*4,它由兩類對象構成:ui

  • 運算數,如:5,6,2等
  • 運算符號,如+,-等,並且不一樣運算符號優先級不同

因爲運算符號優先級不一樣,並且運算符號位於運算數中,因此使得運算變得複雜了。 怎麼理解呢?拿上面的表達式爲例,當程序遍歷到+號的時候,要作+運算,那麼是5+6嗎?很顯然不是,由於6後面還有一個/運算符,並且/的優先級大於+,因此6是/的運算數,而不是+的運算數。那麼反過來呢?當遍歷到一個運算符號的時候,就已經知道對應的兩個運算數,這樣求值就變的簡單了。偉大的科學家們,就此發明了後綴表達式,也稱逆波蘭式,指的是不包含括號,運算符放在兩個運算對象的後面,全部的計算按運算符出現的順序,嚴格從左向右進行(再也不考慮運算符的優先規則)。spa

  • 中綴表達式:2 + 9 / 3 - 5
  • 後綴表達式:2 9 3 / + 5 -

固然兩個表達式,都是表達同一個意思。code

後綴表達式求值

因爲後綴表達式不須要考慮運算符的優先規則,所以求值算法就變得簡單了:

一、從左到右依次遍歷表達式;

二、遇到數字就直接入棧;

三、遇到操做符就彈出兩個元素,先彈出的元素放到操做符的右邊,後彈出的元素放到操做符的左邊(左邊的運算數先入棧,所以後出),將計算獲得的結果再壓入棧;

四、直到整個表達式遍歷完,最後彈出的棧頂元素就是表達式最終的值。

以33-5+表達式爲例,運行狀態以下

func getPostfixExpressionResult(s string) int {
	stack := Stack.New()
	for _, value := range s {
		if unicode.IsDigit(value) {
			intValue, _ := strconv.Atoi(string(value))
			stack.Push(intValue)
		} else {
			right, _ := stack.Pop()
			left, _ := stack.Pop()
			result := calculate(left.(int), right.(int), string(value)) // 封裝的 + - * / 求值方法
			stack.Push(result)
		}
	}
	result, _ := stack.Pop()
	return result.(int)
}

getPostfixExpressionResult("33-5+") // 5
複製代碼

能夠看得出算法時間複雜度爲O(n),而且經過出棧入棧的形式就能夠完成求值過程。

中綴表達式轉後綴表達式

接下來看看中綴怎麼轉後綴,咱們先對比一下兩個表達式:

  • 中綴表達式:2 + 9 / 3 - 5
  • 後綴表達式:2 9 3 / + 5 -

能夠看的出來,數字的相對位置是不變的,改變的是符號的位置,那麼在轉換的過程,咱們須要對比各類運算符號的優先級,而後將優先級高的運算符,先輸出,低的後輸出,這樣在後綴表達式求值的時候,就能保存計算順序不被改變。(左括號和右括號也當作運算符號)具體的算法步驟以下:

一、從左到右遍歷中綴表達式;

二、若是是運算數,直接輸出;

三、若是是運算符號:若優先級大於棧頂的運算符號(棧不爲空),則將該運算符號壓入棧中,由於若是該運算符號優先級比棧頂的大,說明要先被計算,那麼它是後入的,所以在以後的操做中,必定比棧頂的符號先出,所以在後綴求值中,確定先被計算;

四、若是是運算符號:若優先級小於等於棧頂的運算符號(棧不爲空),則將棧頂的運算符號彈出並輸出,而後繼續和下一個新的棧頂元素對比,直到優先級大於新的棧頂元素,就將該運算符號壓入棧中;

五、左括號:括號裏的表達式確定是要先計算的,所以當掃描到左括號的時候,直接將左括號壓入棧中,而入了棧裏的左括號,優先級就變到最低了。由於括號裏的運算符要先運算

六、右括號:將棧頂元素彈出並輸入,直到遇到左括號(彈出,但不輸出);

七、整個表達式遍歷完以後,則將棧裏的元素一一彈出並輸出。

咱們以2*(9+6/3-2)+4爲例:

// 利用hash的形式來判斷運算符號的優先級
// 有興趣能夠看看百度百科裏(後綴表達式)是怎麼判斷運算符號優先級的,頗有意思 (>▽<)
var opPriority  = map[string]int{
	"*":2,
	"/":2,
	"+":1,
	"-":1,
	"(":0,
} 

func infixToPostfix(s string) string{
	postfix := ""
	stack := Stack.New()
	for _,value :=range s{
		if unicode.IsDigit(value) {
			postfix += string(value)
		} else {
			op := string(value)
			switch  op{
			case "+","-","*","/":
				if stack.IsEmpty(){
					stack.Push(op)
				} else {
					pop := stack.GetTop()
					for opPriority[op] <= opPriority[pop.(string)] {
						pop,_ = stack.Pop()
						postfix += pop.(string)
						if stack.IsEmpty() {
							break
						}
						pop = stack.GetTop()
					}
					stack.Push(op)
				}
			case "(":
				stack.Push(op)
			case ")":
				for !stack.IsEmpty() {
					op,_ := stack.Pop()
					if op.(string) == "(" {
						break
					}
					postfix +=op.(string)
				}
			}
		}
	}

	for !stack.IsEmpty(){
		op,_ := stack.Pop()
		postfix +=op.(string)
	}
	return postfix
}

infixToPostfix("2*(9+6/3-5)+4") // 2963/+5-*4+
複製代碼

總結

棧是一種被普遍應用的數據結構,除了剛剛舉例的算術表達式求值以外,棧還用於函數調用及遞歸實現,回溯算法等等。在適當的時候選擇棧,能夠更加高效的解決問題。

ps:在上面的例子裏,後綴表達式求值的程序,是一個不太正確的程序,準確的講,它會把每一個數字當作運算數,即122*,它不會計算出24,而是變成了4,估計你們也猜到緣由了,由於程序是一個字符一個字符的遍歷,而沒有位數的劃分,修正的話,能夠用數組來存表達式,這樣就能夠正確的劃分運算數。

Thanks!

相關文章
相關標籤/搜索