前言:找了上課時數據結構的教程來看,但是用的語言是c++,所以具體實現在網上搜大神的博客來看,我看到的大神們的博客都寫得特別好,不止講了最基本的思想和演算法實現,更多的是側重於實例運用,一邊看一邊在心裡隱隱歌頌大神的厲害,然後別人的厲害不是我的,所以到底看得各種受打擊+頭昏腦漲,寫這個系列是希望自己能 ...
前言:找了上課時數據結構的教程來看,但是用的語言是c++,所以具體實現在網上搜大神的博客來看,我看到的大神們的博客都寫得特別好,不止講了最基本的思想和演算法實現,更多的是側重於實例運用,一邊看一邊在心裡隱隱歌頌大神的厲害,然後別人的厲害不是我的,所以到底看得各種受打擊+頭昏腦漲,寫這個系列是希望自己能夠總結學到東一塊、西一下的知識,因為水平有限+經驗不足,所以在此只說最基礎的思想,附上我自己的演算法實現(肯定還有更優解),如果要想看進階版的,可以在園裡搜“數據結構”,各種語言實現和進階提升的文章有很多,希望大家都能儘快打敗數據結構這個紙老虎~
參考書是:數據結構(c++版)(第2版) 編者:王紅梅、胡明、王濤
正文:
熱身準備:
1、根據數據元素之間的不同關係,數據結構可以分為以下四種:
(1)集合:數據元素之間的關係就是“屬於同一集合”,除此之外,沒有其他關係。(此關係過於簡單,就不詳述了)
(2)線性結構:數據元素之間存在“一對一”的線性關係。
(3)樹結構:數據元素之間存在“一對多”的層級關係。
(4)圖結構:數據元素之間存在“多對多”的任意關係。
2、數據結構在電腦中的存儲方式,主要有兩種:順序存儲和鏈接存儲。
3、線性結構在電腦中的實現,即線性表。當然根據存儲方式的不同又可以分為:順序表和鏈表。
(1)順序表:用一段地址連續的存儲單元依次存儲線性表的數據元素。通常用一維數組來實現。其數據元素間的關係由下標表現。
(2)鏈表:最常見的是單鏈表。單鏈表是用一組任意的存儲單元存放數據元素。其數據元素間的關係由指針表現。除去單鏈表外,常見的還有迴圈鏈表和雙鏈表,都是在單鏈表的基礎上增加了更多信 息,便於實現某些操作。
開始運動:
棧和隊列都是某些功能受限制的線性表,所以它們有線性表的基本性質,但是更特別,也正是它們的特別之處才使得它們脫穎而出。
棧:限定僅在表尾進行插入和刪除操作的線性表,允許插入和刪除的一端成為棧頂,另一端稱為棧底,不含任何元素的棧稱為空棧。
棧具有FILO(first in last out)即先進後出的特性。
1、棧的基本操作:
push
前置條件:棧已存在
輸入:元素值x
功能:入棧操作,在棧頂插入一個元素x
輸出:如果插入不成功,則拋出異常
後置條件:如果插入成功,則棧頂增加一個元素
pop
前置條件:棧已存在
輸入:無
功能:出棧操作,刪除棧頂元素
輸出:如果刪除成功,返回被刪除元素,否則拋出異常
後置條件:如果刪除成功,則棧頂減少一個元素
getTop
前置條件:棧已存在
輸入:無
功能:取棧頂元素,讀取當前的棧頂元素
輸出:如棧不空,返回當前的棧頂元素
後置條件:棧不變
2、順序棧
function SeqStack(){ this._stack = []; //棧 this._top = -1; //棧頂指針 } SeqStack.prototype = { _push:function(x){
try{ this._stack.push(x); this._top++;
}catch(e){
console.log("壓棧失敗");
} }, _pop:function(){ if(this._top == -1) throw Error("棧空"); this._top--; return this._stack.pop(); }, _getTop:function(){ if(this._top == -1) return "空棧"; return this._stack[this._top]; } }; var s = new SeqStack(); s._push(1); s._push(2); s._push(3); console.log(s._getTop());//輸出3 s._pop(); console.log(s._getTop());//輸出2 s._pop(); s._pop(); console.log(s._getTop());//輸出空棧 s._pop();//拋出異常
特別要說:
(1)因為js是一門非常優秀的語言,(哈哈,沒有打廣告,只是日常告白),js已經實現了push,pop的操作,所以此處撿了個便宜,只是對於獲取棧頂(getTop)增加了實現。之所以在每個方法和屬性前加了下劃線,是因為看不慣sublime特別標註出來的顏色(感覺像是用了保留字一樣),所以如果你看不慣_完全可以不要_。
(2)因為js中的數組是可以自己隨著元素插入而增長的(如果大於你原本預計的長度),所以沒有設置maxSize,如果希望棧是定長的,可以再添加maxSize屬性。
(3)因為js不是強數據類型,所以如果需要棧做更實際化的操作可能會出現元素值x為特定值,沒關係,加判斷就好咯,交給你來,未來的靈魂程式猿(這什麼中二的稱呼)
3、鏈棧
function LinkStack(){ this._top = null; } LinkStack.prototype = { _push:function(x){
try{ var node = {_data:x,_next:null};//至少含有此處兩屬性 node._next = this._top;//當前和下一句不能互換,否則不能實現 this._top = node;
}catch(e){
console.log("壓棧失敗");
} }, _pop:function(){ if(this._top == null) throw Error("棧空"); var node = this._top; this._top = node._next; return node; }, _getTop:function(){ if(this._top == null) return "空棧"; return this._top._data; } }; var s = new LinkStack(); s._push(1); s._push(2); s._push(3); console.log(s._getTop()); console.log(s._pop()); console.log(s._getTop()); console.log(s._pop()); console.log(s._pop()); console.log(s._getTop()); s._pop();
特別要說:
(1)鏈棧如果要求長度的話,會需要遍歷整個棧才能計算(順序棧基於數組有一個length屬性可以獲得長度),所以如果在求長度很常用的情況下,可以再增加一個自定義的length屬性,每次_push時,獲取當前top的length值,加1後賦值給新增node的length屬性,則獲取鏈棧的長度就很容易了。
(2)因為壓入的node定義為了一個對象,所以_top初始化時用了null,非常明確的表明瞭之後壓入的node會是對象,同時判斷棧空時也很明確。建議使用這樣的方法初始化_top
棧先告一段落,繼續來說隊列。
隊列:只允許在一端進行插入,在另一端進行刪除的線性表。允許插入(也稱入隊、進隊)的一端稱為隊尾,允許刪除(也稱出隊)的一端稱為隊頭。
隊列具有FIFO(first in first out)先進先出的特性。(想想棧呢?)
1、隊列的基本操作:
enQueue
前置條件:隊列已存在
輸入:元素值x
功能:入隊操作,在隊尾插入一個元素
輸出:如果插入不成功,拋出異常
後置條件:如果插入成功,隊尾增加一個元素
deQueue
前置條件:隊列已存在
輸入: 無
功能:出隊操作,刪除隊頭元素
輸出:如果刪除成功,返回被刪除元素值,否則,拋出異常
後置條件:如果刪除成功,隊頭減少一個元素
getQueue
前置條件:隊列已存在
輸入:無
功能:讀取隊頭元素
輸出:若隊列不可,返回隊頭元素
後置條件:隊列不變
2、迴圈隊列
高能警報 隊列這裡有個坑,如果是在數組需要預先確定長度的環境里(如使用c++聲明數組,需要預先確實數組長度),當我入隊5個元素,又出隊5個元素,繼續入隊出隊,總有一個時刻,我的隊頭指針會到達數組的邊界,那麼即使前面所有的空間都是空閑的(因為出隊了),但是我此時依然無法再入隊,因為會被判斷“溢出”,這就是所謂了“假溢出”現象。那如果是在js中,當超過原定數組長度就會自動增加,那數組的長度不斷增加,隊頭指針之前的空間也一樣是浪費了的,所以,隊列的順序實現,採取了迴圈隊列。
迴圈隊列是將存儲隊列的數組看成是頭尾相接的迴圈結構,即允許隊列直接從數組中下標最大的位置延續到下標最小的位置。通過取模(%)很容易實現。
function CirQueue(){ this._queue = []; this._front = this._rear = 0;//隊頭、隊尾指針 this._maxSize = 10;//隊列定長為10,可以隨意設置 } CirQueue.prototype = { _enQueue:function(x){ try{ if(this._front == (this._rear+1)%this._maxSize) throw Error("隊滿");//判斷是否隊滿 this._queue.push(x);//入隊 this._rear = (this._rear+1)%this._maxSize;//rear指向空閑空間 }catch(e){ console.log("入隊失敗"); } }, _deQueue:function(){ if(this._front == this._rear) throw Error("隊空");//判斷是否隊空 var q = this._queue[this._front]; this._front = (this._front+1)%this._maxSize; return q; }, _getQueue:function(){ if(this._front == this._rear) throw Error("隊空"); return this._queue[this._front]; } }; var q = new CirQueue(); q._enQueue(1); q._enQueue(2); q._enQueue(3); console.log(q._getQueue());//1 console.log(q._deQueue());//1 console.log(q._getQueue());//2 console.log(q._deQueue());//2 console.log(q._getQueue());//3 console.log(q._deQueue());//3 console.log(q._getQueue());//拋出異常
特別要說:
(1)隊滿的判斷條件是front == (rear+1)%maxSize,因為犧牲了一個空閑空間,以便區別隊空隊滿,否則隊空隊滿的判斷條件都為front == rear,不方便操作。
(2)隊列刪除其實並沒有刪除元素,只是移動了front指針,讓那個元素看起來像是出隊了一樣。反正之後入隊新的元素值會覆蓋之前的值,不會對操作造成影響。
3、鏈隊
function LinkQueue(){ this._front = this._rear = null;//隊頭、隊尾指針 } LinkQueue.prototype = { _enQueue:function(x){ try{ var node = {_data:x,_next:null}; if(this._rear == null){//當隊尾指針為null時,隊空 this._rear = this._front = node; }else{ this._rear._next = node;//將當前rear的下一個元素指向node this._rear = node;//把rear指向當前最後的元素,即node } }catch(e){ console.log("入隊失敗"); } }, _deQueue:function(){ if(this._front == null) throw Error("隊空"); var q = this._front; this._front = this._front._next;//隊頭指針移動 return q._data; }, _getQueue:function(){ if(this._front == null) throw Error("隊空"); return this._front._data; } }; var q = new LinkQueue(); q._enQueue(1); q._enQueue(2); q._enQueue(3); console.log(q._getQueue());//1 console.log(q._deQueue());//1 console.log(q._getQueue());//2 console.log(q._deQueue());//2 console.log(q._getQueue());//3 console.log(q._deQueue());//3 console.log(q._getQueue());//拋出異常
以上棧和隊列的順序存儲和鏈接存儲的基本操作的演算法就完畢啦~完結撒花~說著玩的【嚴肅臉】接下來就是棧和隊列的應用舉例,可以自己寫寫代碼,如果有好的代碼求分享哦~比哈特
棧的應用舉例——表達式求值
表達式求值是編譯程式中一個最基本問題。表達式是由運算對象、運算符和圓括弧組成的式子。運算符從運算對象的個數上分,有單目運算和雙目運算符;從運算類型上分,有算術運算、關係運算、邏輯運算等。在此只討論雙目運算的算術表達式。
題目:中綴表達式3*(4+2)/2-5#的求值。(中綴表達式:運算符在運算對象中間的表達式)
偽代碼:OPND—運算對象棧,OPTR—運算符棧,#結束符
1.將棧OPND、OPTR初始化為空
2.從左到右掃描每一個字元執行以下操作,直到遇到#
2.1若當前字元為運算對象,入OPND棧
2.2若當前字元是運算符且OPTR棧空,則入OPTR棧
2.3若當前字元時運算符且OPTR棧不空,則比較棧頂元素和當前運算符的優先順序
2.3.1 若當前元素為),棧頂元素為(,則從OPTR棧出棧一個元素,繼續2
2.3.1 若棧頂元素優先順序高於或等於當前元素,則從OPND出棧兩個運算對象,從OPTR出棧一個運算符,將計算的結果壓入OPND棧中,繼續2
2.3.2 若棧頂元素優先順序低於當前元素,則將當前元素入棧OPTR,繼續2
3.輸出棧OPND的棧頂元素,即表達式的運算結果
運算符優先順序為:()> * / > + - > #
var opnd = new SeqStack();//此處的SeqStack就是之前的順序棧 var optr = new SeqStack(); var str = "3*(4+2)/2-5#";//可以換成任何你希望的算式,也可以寫成函數,作為參數傳入,或者由用戶輸入,在處理字元串前記得在最後的地方加上#結束符 for(var i=0;i<str.length;i++){ if(isNaN(parseInt(str[i]))){//判斷是否為數值,不為數值則進入if,為數值進入else console.log("optr:"+optr._getTop()); if(optr._getTop() == "空棧" || optr._getTop() == "("){ optr._push(str[i]); console.log("空棧或(:"+optr._getTop()); continue; } if(str[i] == ")"){ while(optr._getTop() != "("){ opnd._push(calculate(opnd._pop(),opnd._pop(),optr._pop())); } optr._pop(); console.log("52:"+optr._getTop()); continue; } var compare = priority(optr._getTop())- priority(str[i]); switch(compare>=0){ case true: console.log("59:"+opnd._getTop()); opnd._push(calculate(opnd._pop(),opnd._pop(),optr._pop())); console.log("61:"+opnd._getTop()); i--;//保證當前元素不會因為i++被跳過 break; case false: optr._push(str[i]);break; } }else{ console.log("opnd:"+opnd._getTop()); opnd._push(parseInt(str[i]));//當前字元解析為數值,壓入opnd棧中 } } document.write(opnd._pop()); //運算符的優先順序 function priority(elem){ switch(elem){ case "(": case ")":elem = 3;break; case "*": case "/":elem = 2;break; case "+": case "-":elem = 1;break; default:elem = 0; } return elem; } //計算 function calculate(num1,num2,sign){ switch(sign){ case "*": return num2*num1; case "/": return num2/num1; case "+": return num2+num1; case "-": return num2-num1; } }
隊列的應用舉例——火車車廂重排
題目:給定任意編號的n節車廂,按照1~n的編號進行排序。現有一個入軌、一個出軌和k個緩衝軌進行排序,緩衝軌位於入軌和出軌直接。
偽代碼:1.對k個緩衝軌初始化。初始化下一個要輸出的車廂號為 nowOut=1。
2.依次取入軌中的車廂編號:
2.1 如果當前車廂編號等於 nowOut,則
2.1.1 輸出該車廂
2.1.2 nowOut++
2.2 當前車廂編號不等於 nowOut,考察每一個緩衝軌 for( i=0;i<k;i++)
2.2.1 取緩衝軌 i 的隊頭元素c
2.2.2 如果c等於nowOut,則
2.2.2.1 將緩衝軌 i 的隊頭元素出隊並輸出
2.2.2.2 nowOut++
2.3 如果入軌和緩衝軌的隊頭元素沒用編號為nowOut的車廂,則
2.3.1 求小於當前車廂編號的最大隊尾元素所在緩衝軌 i
2.3.2 如果 i 存在,則把當前車廂一指該緩衝軌
2.3.3 如果 i 不存在,則判斷是否有空緩衝軌
2.3.3.1 有空緩衝軌,將當前車廂入緩衝軌
2.3.3.2 沒有空緩衝軌,則無法重排車廂,演算法結束
function carriage(k,str){ var queue = [];//存儲緩衝軌的數組,保存的元素是數組對象 for(var j=0;j<k-1;j++){//有一條緩衝軌作為入軌到出軌的通道 queue[j] = new LinkQueue();//鏈隊列 } var nowOut = 1;//初始化需要出列的車廂號 var result = "";//最後出列的順序 for(j=0;j<str.length;j++){//遍歷傳入的數組 if(str[j] == nowOut){//如果當前車廂號等於出列號,則直接出列,並將nowOut加一 result += str[j]; nowOut++; }else{ var frontArr = [];//緩衝列的隊頭元素所在緩衝區和其車廂編號 var rearArr = [];//緩衝區的隊尾元素所在緩衝區和其車廂編號 var emptyQu = [];//存儲空的緩衝區 var calc = false;//如果所以處理的方式都試過,但是並沒有辦法在繼續,則退出演算法 for(var i=0;i<k-1;i++){//遍歷緩衝區,獲取存儲的數據 try{//嘗試獲取隊頭和隊尾數據 frontArr.push({"_index":i,"_data":queue[i]._front._data}); rearArr.push({"_index":i,"_data":queue[i]._rear._data}); }catch(e){//如果緩衝區中沒有數據,則把當前緩衝區的下標存儲在emptyQu中 emptyQu.push(i); } } for(i=0;i<frontArr.length;i++){//查看隊頭信息,是否有隊頭能夠出隊 if(frontArr[i]._data == nowOut){//當前隊頭正是需要出隊的車廂號 result += nowOut; nowOut++; queue[frontArr[i]._index]._deQueue();//出隊 j--;//當前的字元未用,而是在已入列的找到,故需要重新再次判斷 var finded = true;//已經出隊,則當前一輪操作可以退出,繼續下一輪 calc = true;//已經進行操作 break; } } if(window.finded){//退出當前一輪操作 continue; } if(!!rearArr.length){//隊尾數據是否存在 var lowRears = [];//存儲小於當前車廂號的隊尾元素 for(i=0;i<rearArr.length;i++){ if(rearArr[i]._data < str[j]){ lowRears.push(rearArr[i]._data); } } if(!!lowRears.length){//存在小於當前車廂號的隊尾元素 var max = Math.max.apply(Math,lowRears);//找出其中最大者 rearArr.filter(function(elem){//篩選出最大者,並使其進入相應緩衝區中 if(parseInt(elem["_data"]) == max){ queue[parseInt(elem["_index"])]._enQueue(str[j]); } }); calc = true;//已經處理了當前數據 continue; } } if(!!emptyQu.length){//空緩衝區是否存在 queue[emptyQu[0]]._enQueue(str[j]);//將數據入隊 continue; } if(!calc){//如果以上操作都沒能進行,則重排失敗,退出演算法 return "重排失敗"; } } } while(nowOut <= str.length){//將還在緩衝區中的數據依次遍歷,嘗試出列 for(var m=0;m<queue.length;m++){ try{ if(queue[m]._getQueue() == nowOut){ result += nowOut; queue[m]._deQueue(); nowOut++; } }catch(e){}; } } return result;//將最後的結果返回 } document.write(carriage(3,[3,6,9,2,4,7,1,8,5]));//123456789 document.write(carriage(3,[3,10,6,9,2,4,7,1,8,5]));//重排失敗
後話:
啊啊啊啊啊,真的差點崩了,寫了5個小時才寫完(/(ㄒoㄒ)/~~)代碼意外的愛我呢,拉著我各種嘮嗑不准我走,然後我就對它說啊,你要雨露均沾,但是它就寵我就寵我【笑哭】
終於終於,寫完啦!完事開頭難,看來之後應該會越寫越順的,加油咯~代碼的地方,還有很多很多不足之處,不夠精簡,不夠健壯,所以之後會再回過頭來改改的,到時做一個大的總結也是不錯的,今天先這樣咯~晚安安~