棧
棧是一種特殊的線性表,僅能夠在棧頂進行操作,有着先進后出的特性
我們先定義一個簡單的 Stack 類
function Stack(){ var items = []; // 使用數組存儲數據 }
棧有以下幾個方法:
- push:添加一個元素到棧頂
- pop:彈出棧頂元素
- top:返回棧頂元素(不是彈出)
- isEmpty:判斷棧是否為空
- size:返回棧里元素的個數
- clear:清空棧
function Stack(){ var items = []; // 使用數組存儲數據 // 從棧頂添加元素,也叫壓棧 this.push = function(item){ items.push(item) } // 彈出棧頂元素 this.pop = function(){ return items.pop() } // 返回棧頂元素 this.top = function(){ return items[items.length - 1] } // 判斷棧是否為空 this.isEmpty = function(){ return items.length === 0 } // 返回棧的大小 this.size = function(){ return items.length } // 清空棧 this.clear = function(){ items = [] } }
練習1:合法括號
下面的字符串中包含小括號,請編寫一個函數判斷字符串中的括號是否合法,所謂合法,就是括號成對出現
sdf(sd(qwe(qwe)wwe)req)ewq 合法
(sd(qwewqe)asdw(swe)) 合法
()()sd()(sd()sw))( 不合法
思路:
用for循環遍歷字符串中的每一個字符,對每個字符做如下操作:
- 遇到左括號,就把左括號壓入棧中
- 遇到右括號,判斷棧是否為空,為空說明沒有左括號與之對應,字符串不合法;如果棧不為空,則把棧頂元素移除
當for循環結束之后,如果棧是空的,就說明所有的左右括號都抵消掉了,則字符串合法;如果棧中還有元素,則說明缺少右括號,字符串不合法。
// 判斷字符串里的括號是否合法 function isLeagle(string){ var stack = new Stack() for(var i=0; i<string.length; i++){ var item = string[i] // 遇到左括號入棧 if(item === '('){ stack.push(item) }else if(item === ')'){ // 遇到右括號,判斷棧是否為空 if(stack.isEmpty()){ return false }else{ stack.pop() // 彈出左括號 } } } // 如果棧為空,說明字符串合法 return stack.isEmpty() } console.log(isLeagle('sdf(sd(qwe(qwe)wwe)req)ewq')) // true console.log(isLeagle('(sd(qwewqe)asdw(swe))')) // true console.log(isLeagle('()()sd()(sd()sw))(')) // false
練習2:逆波蘭表達式
逆波蘭表達式,也叫后綴表達式,它將復雜表達式轉換為可以依靠簡單的操作得到結果的表達式,例如: (a+b)*(c+d) 轉換為 ab+cd+*
從中綴表達式轉換成后綴表達式的邏輯:
(1)如果遇到 數字 ,就壓入棧
(2)如果遇到 運算符,就彈出棧頂的兩個數字,對這兩個數字進行當前運算符的運算,然后將運算結果壓入棧中
for循環結束之后,棧里只有一個元素,這個元素就是整個表達式的計算結果
示例:
["4", "13", "5", "/", "+"] 等價於 (4 + (13 / 5)) = 6 ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"] 等價於 ((10 * (6 / ((9 + 3) * -11 ))) + 17) +5
請編寫函數 calc_exp(exp) 實現逆波蘭表達式計算,exp的類型是數組
// 計算后綴表達式 function calc_exp(exp){ var stack = new Stack() for(var i=0; i<exp.length; i++){ var item = exp[i] if(["+","-","*","/"].indexOf(item) >= 0){ var value1 = stack.pop() var value2 = stack.pop() var exp_str = value2 + item + value1 // 計算並取整 var res = parseInt(eval(exp_str)) // 計算結果壓入棧頂 stack.push(res.toString()) }else{ stack.push(item) } } return stack.pop() } console.log(calc_exp(["4", "13", "5", "/", "+"])) // 6
練習3:實現一個有min方法的棧
實現一個棧,除了常見的push,pop方法以外,提供一個min方法,返回棧里最小的元素,且時間復雜度為 O(1)
function MinStack(){ var data_stack = new Stack() // 存儲數據 var min_stack = new Stack() // 存儲最小值 this.push = function(item){ data_stack.push(item) // min_stack為空,或者棧頂元素大於item if(min_stack.isEmpty() || item < min_stack.top()){ min_stack.push(item) }else{ min_stack.push(min_stack.top()) } } this.pop = function(){ data_stack.pop() min_stack.pop() } this.min = function(){ return min_stack.top() } } var minStack = new MinStack() minStack.push(3) minStack.push(5) minStack.push(8) minStack.push(1) console.log(minStack.min()) // 1
練習4:使用棧,完成中序表達式轉后續表達式
輸入:["12", "+", "3"] 輸出:["12", "3", "+"] (1 + (4 + 5 + 3) -3) + (9 + 8) 輸入:["(", "1", "+", "(", "4", "+", "5", "+", "3", ")", "-", "3", ")", "+", "(", "9", "+", "8", ")"] 輸出:["1", "4", "5", "+", "3", "+", "+", "3", "-", "9", "8", "+", "+"] (1 + (4 + 5 + 3) / 4 - 3) + (6 + 8) * 3 輸入:["(", "1", "+", "(", "4", "+", "5", "+", "3", ")", "/", "4", "-", "3", ")", "+", "(", "6", "+", "8", ")", "*", "3"] 輸出:["1", "4", "5", "+", "3", "+", "4", "/", "+", "3", "-", "6", "8", "+", "3", "*", "+"]
1、如果是數字,直接放入到postfix_list中
2、遇到左括號入棧
3、遇到右括號,把棧頂元素彈出,直到遇到左括號,最后彈出左括號
4、遇到運算符,把棧頂的運算符彈出,直到棧頂的運算符優先級小於當前運算符,把彈出的運算符加入到 postfix_list,當前的運算符入棧
5、for循環結束后,棧里可能還有元素,都彈出放入到 postfix_list 中
var priority_map = { "+": 1, "-": 1, "*": 2, "/": 2 } function infix_exp_2_postfix_epx(exp){ var stack = new Stack() var postfix_list = [] for(var i=0; i<exp.length; i++){ var item = exp[i] // 如果是數字,直接放入到postfix_list中 if(!isNaN(item)){ postfix_list.push(item) }else if(item === "("){ // 遇到左括號入棧 stack.push(item) }else if(item === ")"){ // 遇到右括號,把棧頂元素彈出直到遇到左括號 while(stack.top() !== '('){ postfix_list.push(stack.pop()) } stack.pop() // 左括號出棧 }else{ // 遇到運算符,把棧頂的運算符彈出,直到棧頂的運算符優先級小於當前運算符 while(!stack.isEmpty() && ["+","-","*","/"].indexOf(stack.top()) >= 0 && priority_map[stack.top()] >= priority_map[item]){ // 把彈出的運算符加入到postfix_list postfix_list.push(stack.pop()) } // 當前的運算符入棧 stack.push(item) } } // for循環結束后,棧里可能還有元素,都彈出放入到 postfix_list中 while(!stack.isEmpty()){ postfix_list.push(stack.pop()) } return postfix_list } console.log(infix_exp_2_postfix_epx(["12", "+", "3"])) console.log(infix_exp_2_postfix_epx(["(", "1", "+", "(", "4", "+", "5", "+", "3", ")", "-", "3", ")", "+", "(", "9", "+", "8", ")"])) console.log(infix_exp_2_postfix_epx(["(", "1", "+", "(", "4", "+", "5", "+", "3", ")", "/", "4", "-", "3", ")", "+", "(", "6", "+", "8", ")", "*", "3"]))
隊列
隊列是一種特殊的線性表,它只允許你在隊列的頭部刪除元素,在隊列的末尾添加新元素(先進先出)
定義一個簡單的 Queue 類
function Queue(){ var items = [] // 存儲數據 }
隊列的方法如下:
- enqueue:從隊列尾部添加一個元素
- dequeue:從隊列頭部刪除一個元素
- head:返回頭部的元素(不是刪除)
- size:返回隊列的大小
- clear:清空隊列
- isEmpty:判斷隊列是否為空
- tall:返回隊列尾節點
function Queue(){ var items = [] // 存儲數據 // 向隊列尾部添加一個元素 this.enqueue = function(item){ items.push(item) } // 移除隊列頭部的元素 this.dequeue = function(){ return items.shift() } // 返回隊列頭部的元素 this.head = function(){ return items[0] } // 返回隊列尾部的元素 this.tail = function(){ return items[items.length - 1] } // 返回隊列大小 this.size = function(){ return items.length } // clear this.clear = function(){ items = [] } // 判斷隊列是否為空 this.isEmpty = function(){ return items.length === 0 } }
練習1:約瑟夫環(普通模式)
有一個數組a[100] 存放 0 - 99 ;要求每隔兩個數刪掉一個數,到末尾時循環至開頭繼續進行,求最后一個被刪掉的數。
思路分析:
前10個數是 0 1 2 3 4 5 6 7 8 9 ,所謂每隔兩個數刪掉一個數,其實就是把 2 5 8 刪除掉
算法步驟:
1、從隊列頭部刪除一個元素, index + 1
2、如果 index % 3 === 0 ,就說明這個元素是需要刪除的元素;如果不等於0,就是不需要被刪除的元素,則把它添加到隊列的尾部
// 准備好數據 var arr_list = [] for(var i=0; i<100; i++){ arr_list.push(i) } function del_ring(arr_list){ // 把數組里的元素都放入到隊列中 var queue = new Queue() for(var i=0; i<arr_list.length; i++){ queue.enqueue(arr_list[i]) } var index = 0 while(queue.size() !== 1){ // 彈出一個元素,判斷是否需要刪除 var item = queue.dequeue() index += 1 // 每隔兩個就要刪除掉一個,那么不是被刪除的元素就放回到隊列尾部 if(index % 3 !== 0){ queue.enqueue(item) } } return queue.head() } console.log(del_ring(arr_list))
練習2:斐波那契數列(普通模式)
使用隊列計算斐波那契數列的第n項
思路分析:
斐波那契數列的前兩項是 1 1 ,此后的每一項都是該項前面兩項之和,即 f(n) = f(n-1) + f(n-2)
先將兩個 1 添加到隊列中,之后使用 while 循環,用index計數,循環終止的條件是 index < n - 2
- 使用 dequeue 方法從隊列頭部刪除一個元素,該元素為 del_item
- 使用 head 方法獲得隊列頭部的元素,該元素為 head_item
- del_item + head_item = next_item,將 next_item 放入隊列,注意,只能從尾部添加元素
- index + 1
當循環結束時,隊列里面有兩個元素,先用dequeue刪除頭部元素,剩下的那個元素就是我們想要的答案
function fibonacii(n){ if(n <= 2){ return 1 } var queue = new Queue() var index = 0 // 先放入斐波那契序列的前兩個數值 queue.enqueue(1) queue.enqueue(1) while(index < n-2){ // 循環次數:因為要求的是第n項,但是前面已經放入兩個 1 了 // 出隊列一個元素 var del_item = queue.dequeue() // 取隊列頭部元素 var head_item = queue.head() var next_item = del_item + head_item // 將計算結果放入隊列 queue.enqueue(next_item) index++ } queue.dequeue() return queue.head() }
小結:
在socket中,當大量客戶端向服務器發起連接,而服務器忙不過來的時候,就會把這些請求放入到隊列中,先來的先處理,后來的后處理,隊列滿時,新來的請求直接拋棄掉
練習3:用隊列實現棧(困難模式)
用兩個隊列實現一個棧
思路:
隊列是先進先出,而棧是先進后出,兩者對數據的管理模式剛好是相反的,但是卻可以用兩個隊列實現一個棧
兩個隊列分別命名為 queue_1,queue_2,實現的思路如下:
- push:實現push方法時,如果兩個隊列都為空,那么默認向 queue_1 里添加數據,如果有一個不為空,則向這個不為空的隊列里添加數據
- top:兩個隊列,或者都為空,或者有一個不為空,只需要返回不為空的隊列的尾部元素即可
- pop:pop方法要刪除的是棧頂,但這個棧頂元素其實是隊列的尾部元素。每一次做pop操作時,將不為空的隊列里的元素依次刪除並放入到另一個隊列中,直到遇到隊列中只剩下一個元素,刪除這個元素,其余的元素都跑到之前為空的隊列中了
在具體的實現中,定義兩個額外的變量,data_queue 和 empty_queue,data_queue 始終指向那個不為空的隊列,empty_queue 始終指向那個為空的隊列
function QueueStack(){ var queue_1 = new Queue() var queue_2 = new Queue() var data_queue = null // 放數據的隊列 var empty_queue = null // 空隊列,備份使用 // 確認哪個隊列放數據,哪個隊列做備份空隊列 var init_queue = function(){ // 都為空,默認返回 queue_1 if(queue_1.isEmpty() && queue_2.isEmpty()){ data_queue = queue_1 empty_queue = queue_2 }else if(queue_1.isEmpty()){ data_queue = queue_2 empty_queue = queue_1 }else{ data_queue = queue_1 empty_queue = queue_2 } } this.push = function(item){ init_queue() data_queue.enqueue(item) } this.top = function(){ init_queue() return data_queue.tail() } /** * pop方法要彈出棧頂元素,這個棧頂元素,其實就是queue的隊尾元素 * 但是隊尾元素是不能刪除的,我們可以把data_queue里的元素(除了隊尾元素)都移入到empty_queue中 * 最后移除data_queue的隊尾元素並返回 * data_queue 和 empty_queue 交換了身份 */ this.pop = function(){ init_queue() while(data_queue.size() > 1){ empty_queue.enqueue(data_queue.dequeue()) } return data_queue.dequeue() } }
鏈表
鏈表是物理存儲單元上非連續的、非順序的存儲結構,由一系列節點組成,這里所提到的鏈表,均指單鏈表。
節點
節點包含兩部分,一部分是存儲數據的數據域,一部分是存儲指向下一個節點的指針域。
var Node = function(data){ this.data = data this.next = null } var node1 = new Node(1) var node2 = new Node(2) var node3 = new Node(5) node1.next = node2 node2.next = node3 console.log(node1.data) // 1 console.log(node1.next.data) // 2 console.log(node1.next.next.data) // 5
首尾節點
鏈表中的第一個節點是首節點,最后一個節點是尾結點
有頭鏈表和無頭鏈表
無頭鏈表是指第一個節點既有數據域,又有指針域,第一個節點即是首節點又是頭節點
有頭鏈表是指第一個節點只有指針域,而沒有數據域
定義鏈表類
function LinkList(){ // 定義節點 var Node = function(data){ this.data = data this.next = null } var length = 0 // 長度 var head = null // 頭節點 var tail = null // 尾結點 }
鏈表的方法:
- append:添加一個新的元素
- insert:在指定位置插入一個元素
- remove:刪除指定位置的節點
- remove_head:刪除首節點
- remove_tail:刪除尾結點
- indexOf:返回指定元素的索引
- get:返回指定索引位置的元素
- head:返回首節點
- tail:返回尾結點
- length:返回鏈表長度
- isEmpty:判斷鏈表是否為空
- clear:清空鏈表
- print:打印整個鏈表
append方法:
// 在尾部添加一個節點 this.append = function(data){ // 創建新節點 var new_node = new Node(data) if(head === null){ head = new_node tail = new_node }else{ tail.next = new_node tail = new_node } length++ return true }
print方法:
this.print = function(){ var curr_node = head while(curr_node){ console.log(curr_node.data) curr_node = curr_node.next } }
insert方法:
append只能在鏈表的末尾添加元素,而insert可以在指定位置插入一個元素,新增數據的方式更加靈活,insert方法需要傳入參數 index,指明要插入的索引位置。該方法的關鍵是找到索引為 index-1 的節點,只有找到這個節點,才能將新的節點插入到鏈表中。和索引相關的方法,先要考慮索引的范圍是否合法,然后考慮索引的邊界情況。
- 如果 index === length ,那么可以直接調用 append方法
- 如果 index > length 或者 index<0,索引錯誤,返回null
- 如果index===0,創建新節點 new_node,這個新的節點索引是0,因此是鏈表的首節點,讓 new_node.next = head , head = new_node
- 如果index>0 且index<length,同樣創建新節點 new_node,變量 curr_node 指向 head,insert_index = 1,表示 new_node 應該插入的位置,使用一個 while循環,循環條件是 insert_index < index,在循環體內,insert_index 加一, curr_node = curr_node.next,這樣當循環結束的時候,curr_node 就是new_node的上一個節點,讓 new_node 指向curr_node的下一個節點,而curr_node指向new_node
this.insert = function(index, data){ if(index < 0 || index > length){ return false }else if(index === length){ return this.append(data) }else{ var new_node = new Node(data) // new_node 成為新的頭結點 if(index === 0){ new_node.next = head head = new_node }else{ var insert_index = 1 var curr_node = head while(insert_index < index){ insert_index++ curr_node = curr_node.next } // 獲取 next節點(這時還沒將新節點插入) var next_node = curr_node.next // 改變當前節點的next,指向新節點(這時斬斷了原鏈表,將前半部分的next指向新節點) curr_node.next = new_node // 將新節點的next指向原鏈表的后半部分(將斷開的鏈表重新連上) new_node.next = next_node } } length++ return true }
remove方法
this.remove = function(index){ if(index < 0 || index >= length){ return null }else{ var del_node = null if(index === 0){ del_node = head head = head.next }else{ var del_index = 0 var pre_node = null // 被刪除節點的前一個節點 var curr_node = head // curr_node就是那個要被刪除的節點 while(del_index < index){ // 當要刪除的index和傳入的index相同時跳出循環 del_index++ pre_node = curr_node // 當前節點賦值給pre_node curr_node = curr_node.next // 當前節點往后挪動一位 } del_node = curr_node // 被刪除節點的前一個節點指向被刪除節點的后一個節點 pre_node.next = curr_node.next // 如果被刪除的節點是尾結點 if(curr_node.next = null){ tail = pre_node // 更新tail節點 } } length-- del_node.next = null return del_node.data } }
get方法
// 返回指定位置節點的值 this.get = function(index){ if(index < 0 || index >= length){ return null } var node_index = 0 var curr_node = head while(node_index < index){ node_index++ curr_node = curr_node.next } return curr_node.data }
indexOf方法
// 返回指定元素的索引,如果沒有,返回-1 // 有多個相同元素,返回第一個 this.indexOf = function(data){ var index = -1 var curr_node = head while(curr_node){ index++ if(curr_node.data === data){ return index }else{ curr_node = curr_node.next } } return -1 }
練習1:基於鏈表實現的Stack和Queue
function Stack(){ var linkList = new LinkList() // 從棧頂添加元素 this.push = function(item){ linkList.append(item) } // 彈出棧頂元素 this.pop = function(){ return linkList.remove_tail() } // 返回棧頂元素 this.top = function(){ return linkList.tail() } // 返回棧的大小 this.size = function(){ return linkList.length() } // 判斷是否為空 this.isEmpty = function(){ return linkList.isEmpty() } // 清空棧 this.clear = function(){ linkList.clear() } }
練習2:翻轉鏈表
使用迭代和遞歸兩種方法翻轉鏈表,下面的代碼已經准備好了上下文環境,請實現函數 reverse_iter 和 reverse_digui
var Node = function(data){ this.data = data this.next = null } var node1 = new Node(1) var node2 = new Node(2) var node3 = new Node(3) var node4 = new Node(4) var node5 = new Node(5) node1.next = node2 node2.next = node3 node3.next = node4 node4.next = node5 function print(node){ var curr_node = node while(curr_node){ console.log(curr_node.data) curr_node = curr_node.next } } // 迭代翻轉 function reverse_iter(head){ } // 遞歸翻轉 function reverse_digui(head){ }
思路分析:
在考慮算法時,多數情況下考慮邊界情況會讓問題變得簡單,但邊界情況往往不具備普適性,因此,也要嘗試考慮中間的情況,假設鏈表中間的某個點為 curr_node ,它的前一個節點是 pre_node,后一個節點是 next_node,現在把思路聚焦到這個curr_node節點上,只考慮在這一個點上進行翻轉
curr_node.next = pre_node
只需要這簡單的一個步驟就可以完成對 curr_node 節點的翻轉,對於頭結點來說,它沒有上一個節點,讓 pre_node=null ,表示它的上一個節點是空節點。
在遍歷的過程中,每完成一個節點的翻轉,都讓 curr_node = next_node,找到下一個需要翻轉的節點,同時,pre_node 和 next_node 也跟隨 curr_node 一起向后滑動
// 迭代翻轉 function reverse_iter(head){ if(!head){ return null } var pre_node = null // 前一個節點 var curr_node = head // 當前要翻轉的節點 while(curr_node){ var next_node = curr_node.next // 下一個節點 curr_node.next = pre_node // 對當前節點進行翻轉 pre_node = curr_node // pre_node向后滑動 curr_node = next_node // curr_node向后滑動 } // 最后要返回 pre_node,當循環結束時,pre_node指向翻轉前鏈表的最后一個節點 return pre_node } print(reverse_iter(node1))
遞歸翻轉鏈表思路分析
遞歸的思想,精髓之處在於甩鍋,你做不到的事情,讓別人去做,等別人做完了,你在別人的基礎上繼續做
甩鍋一共分為四步:
1、明確函數的功能,既然是先讓別人去做,那你得清楚的告訴他做什么。函數 reverse_digui(head) 完成的功能,是從head開始翻轉鏈表,函數返回值是翻轉后的頭結點
2、正式甩鍋,進行遞歸調用
var new_head = reverse_digui(head.next)
原本是翻轉以head開頭的鏈表,可是你不會啊,那就先讓別人從head.next開始翻轉鏈表,等他翻轉玩,得到的 new_head 就是翻轉后的頭節點
3、根據別人的結果,計算自己的結果
在第二步中,已經完成了從 head.next 開始翻轉鏈表,現在,只需要把 head 鏈接到新鏈表上就可以了,新鏈表的尾結點是 head.next,執行 head.next.next = head,這樣,head 就完成了新鏈表的尾結點
// 遞歸翻轉 function reverse_digui(head){ // 如果head為null if(!head){ return null } // 從下一個節點開始進行翻轉 var new_head = reverse_digui(head.next) head.next.next = head // 把當前節點連接到新鏈表上 head.next = null return new_head }
4、找到遞歸的終止條件
遞歸必須有終止條件,否則就會進入死循環,函數最終要返回新鏈表的頭,而新鏈表的頭正是舊鏈表的尾,所以,遇到尾結點,直接返回尾結點,這就是遞歸終止的條件
// 遞歸翻轉 function reverse_digui(head){ // 如果head為null if(!head){ return null } if(head.next === null){ return head } // 從下一個節點開始進行翻轉 var new_head = reverse_digui(head.next) head.next.next = head // 把當前節點連接到新鏈表上 head.next = null return new_head } print(reverse_digui(node1))