栈
栈是一种特殊的线性表,仅能够在栈顶进行操作,有着先进后出的特性
我们先定义一个简单的 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))