数据结构与算法(栈、队列、链表)


栈是一种特殊的线性表,仅能够在栈顶进行操作,有着先进后出的特性

 

 

我们先定义一个简单的 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))

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM