大前端 - 算法与数据结构 (leet code)

为什么要学习数据结构和算法?

但我们通过框架和工具进行开发的时候,如何做到优化程序呢?那么数据结构和算法就起到了一定的作用。程序 = 数据结构 + 算法。
在解决一些特殊的问题时候,如果用到特定的数据结构 ,算法相结合,对我们写的代码就可以做到:
1.化繁为简。
2:提高代码的性能。
3.提交面试通过率。

1.基础概念
2.栈的实现
3.leetcode题目。

  • 1.基础概念。
  1. 后进先出
  2. 只能操作顶部元素,从栈定添加元素。(添加,移除,取值)。
  3. 添加新元素的一端称为栈定,另一段称为栈底。

请添加图片描述
请添加图片描述
请添加图片描述

  • 栈的实现
    请添加图片描述
class Stack{
  constructor() {
    // 存储栈内数据
    this.data = []
    // 记录栈的数据个数(相当于数组的length)
    this.count = 0
  }

  /**
   * push: 入栈方法
   * item: 入栈的数据
   */
  push(item) {
    // 方式1: 使用push方法  this.data.push(data)

    // 方法2:
    // this.data[this.data.length] = item


    // 方法3:计数方式
    this.data[this.count] = item
    // 入栈后count自增
    this.count++
  }

  /**
   * pop: 出栈  删除栈顶元素
   */
  pop() {
    // 1.出栈的前提是检测栈中是否有元素。
    if (this.isEmpty()) {
      return
    }
    // 移除栈顶元素
    // 1.方式1 ,使用数组的pop, return this.data.pop()
    // 方式2:计数方式
    const temp = this.data[this.count - 1]
    delete this.data[this.count - 1]
    this.count--
    return temp // 删除之后的数组 [ 'a', 'b', 'empty' ]
    // 还可这样写:
    // const temp = this.data[this.count - 1]
    // delete this.data[--this.count]
    // return temp

  }

  // isEmpty: 检测栈是否为空
  isEmpty() {
    return this.count === 0
  }

  /**
   * top: 获取栈顶元素
   */
  top() {
    if (this.isEmpty()) {
      return
    }
    return this.data[this.count - 1]
  }

  /**
   * size: 获取栈中的元素个数
   */
  size() {
    return this.count
  }

  /**
   * clear :清空栈
   */
  clear() {
    this.data = []
    this.count = 0
  }
}

// 测试
const s = new Stack()
s.push('a')
s.push('b')

console.log(s)
  • leetcode题目
  1. 包含min(最小值)函数的栈
    定义栈的数据结构,在该类型中实现一个能够得到栈的最小元素的min函数在该栈中,调用min,push及pop的时间复杂度。
// 在存储数据的栈外,再新建一个栈,用于存储最小值。

class MinStack {
	constructor() {
    // stackA 用于存储数据
  	// stackB 用于将数据降序存储(栈顶值为最小值)
  	this.stackA = []
  	this.stackB = []
  	this.countB = 0
  	this.countA = 0
  }
	
  /**
   * push 入栈
   */
  push(item) {
    // stackA正常入栈
    this.stackA[this.countA++] = item

    // stackB 如果没有数据,直接入栈
    if (this.countB===0) {
      this.stackB[this.countB++] = item

      // stackB 如果没有数据,直接入栈
      // 如果item的值 <= stackB的最小值,入栈
      if (this.countB===0 || item <= this.min()) {
        this.stackB[this.countB++] = item
      }
    }
  }
  // 获取stackB中的最小值 (stackB的栈顶值)
  min() {
    return this.stackB[this.countB - 1]
  }
  // top :获取stackA的栈顶值。
  top() {
    return this.stackA[this.countA - 1]
  }

  // pop 出栈
  pop() {
    // 先进行stackB的检测

    // 如果stackA的栈顶值===stackB的栈顶值,stackB出栈
    if (this.top() === this.min()) {
      delete this.stackB[--this.countB]
    }

    // stackA出栈
    delete this.stackA[--this.countA]
  }
}

// 测试

var min = new MinStack()
min.push(4)
min.push(2)
min.push(3)
console.log(min)
  • 利用内置方法实现题目 - 获取最小值
class MinStack{
  constructor() {
    this.stack = []
  }
  // 入栈
  push(item) {
    this.stack.push(item)
  }

  // 查看栈顶值
  top() {
    return this.stack[this.stack.length-1]
  }
  // 获取最小值
  min() {
    return Math.min.apply(null, this.stack)
  }

  // 出栈
  pop() {
    return this.stack.pop()
  }
}
// 测试
var min = new MinStack()
min.push(4)
min.push(2)
min.push(3)
console.log(min.min())
  • 每日温度
    请添加图片描述

队列

  • 1.队列的概念

  • 2.队列的实现方式

  • 3.双端队列

  • 4.leetcode题目

  • 1.队列的概念
    请添加图片描述
    请添加图片描述
    请添加图片描述

  • 2.队列的实现 - 基于数组
    请添加图片描述


class Queue {
  constructor() {

    // 用于存储队列数据
    this.queue = []
    this.count = 0
  }

  // 入队
  enQueue(item) {
    this.queue[this.count++] = item
  }

  // 出队
  deQueue() {
    if (this.isEmpty()) {
      return
    }
    // 删除queue中的第一个元素  delete this.queue[0]
    // 利用shift() 移除数组中的第一个元素
    this.count--
    return this.queue.shift()
    
  }

  // 检测对列是否为空
  isEmpty() {
    return this.count === 0
  }
}

// 测试
var q = new Queue()
q.enQueue(2)
// q.deQueue()
console.log('q', q)

  • 2.队列的实现 - 基于对象
class Queue{
  constructor() {
    this.queue = {}
    this.count = 0

    // 用于记录队首的键
    this.head = 0
  }

  // 入队方法
  enQueue(item) {
    this.queue[this.count++] = item
  }
  // 出对
  deQueue() {
    if (this.isEmpty()) {
      return
    }
    var temp = this.queue[this.head]
    delete this.queue[this.head]
    this.head++
    return temp
  }

  // 长度
  length() {
    return this.count - this.head
  }

  // 检测队列是否为空
  isEmpty() {
    return this.length() === 0
  }

  // 清空队列
  clear() {
    this.queue = {}
    this.count = 0
    this.head = 0
  }
}
  • 双端队列
    请添加图片描述
    请添加图片描述

addFront / adaBack. addFront: 对列头部添加,adaBack:队列尾添加
removeFront / removeBack : removeFront对列头部删除 , removeBack: 队列尾删除
frontTop / backTop: frontTop: 获取对列头部元素,backTop:获取队列尾部元素

class DoubleQueue {
  constructor() {
    this.queue = {}
    this.count = 0 // 0后面的个数
    this.head = 0 // 0前面的个数
  }

  // 队首添加
  addFront(item) {
    this.queue[--this.head] = item
  }

  // 队尾添加
  addBack(item) {
    this.queue[this.count++] = item
  }

  // 队首删除
  removeFront() {
    if (this.isEmpty()) return
    const headData = this.queue[this.head]
    delete this.queue[this.head++]
    return headData
  }

  // 队尾删除
  removeBack() {
    if (this.isEmpty()) return
    const backData = this.queue[this.count - 1]
    delete this.queue[this.count - 1]
    this.count--

    /*
      delete this.queue[this.count - 1]
      this.count--
      可以合并为一句: delete this.queue[--this.count]
    */
  }

  // 获取队首值
  fronTop() {
    if (this.isEmpty()) return
    return this.queue[this.head]
  }

  // 获取队尾值
  backTop() {
    if (this.isEmpty()) return
    return this.queue[this.count - 1]
  }

  // 检测对列是否为空
  isEmpty() {
    return this.size === 0
  }

  size() {
    return this.count - this.head
  }


}

const doubleQueu = new DoubleQueue()

doubleQueu.addFront('1')
doubleQueu.addFront('2')
doubleQueu.addBack('x')
doubleQueu.addBack('y')
console.log('doubleQueu', doubleQueu) // { queue: {-1: '1', -2: '2'}, count: 0, head: -1 }
console.log(doubleQueu.fronTop())
console.log(doubleQueu.backTop())
  • 题目
    1.队列的最大值

2.滑动窗口最大值

/**
 * nums: 传入数据
 * k: 滑动窗口宽度
 */
function maxSlidingWindow(nums, k) {
  if (k <= 1) {
    return nums
  }

  const result = []
  const deque = []

  // 1.将窗口第一个位置的数据添加到deque中, 保持递减。
  deque.push(nums[0])

  for (let i = 0; i < k; i++) {
    //  - 存在数据
    //     - 当前数据大于队尾值
    //  - 出队,再重复比较
    while(deque.length && nums[i] > deque[deque.length - 1]) {
      deque.pop()
    }
    deque.push(nums[i])
  }
  // 将第一个位置的最大值添加到result
  result.push(deque[0])

  // 2.遍历后续的数据
  const len = nums.length
  for (let i = 0; i < len; i++) {
    // 同上进行比较
    while(deque.length && nums[i] > deque[deque.length - 1]) {
      deque.pop()
    }
    deque.push(nums[i])

    // 检测当前最大值是否位于窗口外
    if (deque[0] === nums[i-k]) {
      deque.shift()
    }
    // 添加最大值到result中
    result.push(deque[0])
  }
}

链表

  • 基本概面

链表是一种有序的数据结构。
链表在任意位置都可以操作。
可以从首, 尾,中间进行数据操作。为什么不直接使用数据?他们个有优势。

数组:在内存中占据一段连续的空间。但是添加,移除会导致后续的元素位移,性能开销大。

请添加图片描述
请添加图片描述

链表:链表的元素在内存中不必是连续的空间。也就是说:添加,移除不会导致元素的位移,不会产生大量的性能开销。也就引出了缺点:无法根据索引快速定位元素。

  • 链表的实现方式
    请添加图片描述
    请添加图片描述
// 节点类
class LinkedNode {
  constructor (value) {
    this.value = value
    // 用于存储下一个节点的引用
    this.next = null
  }
}

// 链表类
class LinkedList {
  constructor () {
    // 链表个数
    this.count = 0
    // 头部指针
    this.head = null
  }
  // 添加节点 (尾)
  addAtTail (value) {
    // 创建新节点
    const node = new LinkedNode(value)
    // 检测链表是否存在数据
    if (this.count === 0) {
      this.head = node
    } else {
      // 找到链表尾部节点,将最后一个节点的 next 设置为 node
      let cur = this.head
      while (cur.next != null) {
        cur = cur.next
      }
      cur.next = node
    }
    this.count++
  }
  // 添加节点(首)
  addAtHead (value) {
    const node = new LinkedNode(value)
    if (this.count === 0) {
      this.head = node
    } else {
      // 将 node 添加到 head 的前面
      node.next = this.head
      this.head = node
    }
    this.count++
  }
  // 获取节点(根据索引)
  get (index) {
    // 合法检测
    if (this.count === 0 || index < 0 || index >= this.count) {
      return
    }
    // 迭代链表,找到对应节点
    let current = this.head
    for (let i = 0; i < index; i++) {
      current = current.next
    }
    return current
  }
  // 添加节点(根据索引添加)
  addAtIndex (value, index) {
    if (this.count === 0 || index >= this.count) {
      return
    }
    // 如果 index <= 0,都添加到头部即可
    if (index <= 0) {
      return this.addAtHead(value)
    }
    // 后面为正常区间处理
    // 当前索引的前一个节点
    const prev = this.get(index - 1)
    // 当前索引的节点
    const next = prev.next

    const node = new LinkedNode(value)
    prev.next = node
    node.next = next

    this.count++
  }
  // 删除(根据索引)
  removeAtIndex (index) {
    if (this.count === 0 || index < 0 || index >= this.count) {
      return
    }
    if (index === 0) {
      this.head = this.head.next
    } else {
      const prev = this.get(index - 1)
      prev.next = prev.next.next
    }
    this.count--
  }
}

// 测试代码
const l = new LinkedList()
l.addAtTail('a')
l.addAtTail('b')
l.addAtTail('c')


  • 链表的多种形式
    请添加图片描述
    请添加图片描述

请添加图片描述

请添加图片描述

请添加图片描述

  • 题目
    1.反转单链表
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
  // 声明变量记录 prev、cur
  let prev = null  // 前一项
  let cur = head // 当前项
  // 当 cur 是节点时,进行迭代
  while (cur) {
    // 先保存当前节点的下一个节点, 因为下面要进行反转,反转之后,原始的next就不存在了。所以要先保存。
    const next = cur.next
    // 反转
    cur.next = prev
    prev = cur
    
    // 移动到下一个节点
    cur = next
  }
  // 返回当前节点
  return prev
};
  1. 递归反转链表
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
  if (head === null || head.next === null) {
    return head
  }
  const newHead = reverseList(head.next)
  // 能够第一次执行这里的节点为 倒数第二个 节点
  head.next.next = head
  // head 的 next 需要在下一次递归执行时设置。当前设置为 null 不影响
  //   - 可以让最后一次(1)的 next 设置为 null
  head.next = null
  return newHead
};

3.环路检测分析 + 实现 (快慢双指针法)
请添加图片描述
slow慢指针每次移动一位,fast快指针每次移动2位, 如果相遇,则说明链表存在环。

请添加图片描述

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var detectCycle = function(head) {
	//合法检测链表是否为空,链表是否只有一个节点,链表为空,链表只有一个节点都不能形成环形链表。
    if (head === null || head.next === null) {
      return null
    }
    // 声明快慢指针
    let slow = head
    let fast = head

    while (fast !== null) {
      // 慢每次指针移动一位
      slow = slow.next
      // 如果满足条件,说明 fast 为尾部结点,不存在环
      if (fast.next === null) {
        return null
      }
      // 快指针每次移动两位
      fast = fast.next.next

      // 检测是否有环
      if (fast === slow) {
        // 找到环的起点位置
        let ptr = head
        while (ptr !== slow) {
          ptr = ptr.next
          slow = slow.next
        }
        // ptr 和 slow 的交点就是环的起始节点
        return ptr
      }
    }
    // while 结束,说明 fast 为 null,说明链表没有环
    return null
};

树与二叉树

请添加图片描述
请添加图片描述
请添加图片描述
左子节点: 以B为例:D是B 的左子节点,E是B 的右子节点。
左子树:以B为例: DEGH都是B的左子树。

  • 树形结构是一种非线形的数据结构。
  • 树中的每个部分称为节点,节点间存在分支结构与层次关系。
  • 每个树形结构凑具有一个根节点。
  • 根据节点之前的关系,也存在:父节点,子节点,兄弟节点的概念。
  • 不含子节点的节点称为:叶节点。
  • 子树:多某个节点与其后代节点的整体称呼。
  • 深度: 树中最深的节点称为高度,也就是深度。

请添加图片描述
请添加图片描述

  • 二叉树
    请添加图片描述请添加图片描述
    请添加图片描述

  • 二叉树的遍历
    请添加图片描述
    请添加图片描述
    请添加图片描述

  • 二叉树的前序遍历

非递归

// 前序遍历
const preOrderTravlersal = function(root) {
	// 用于存储遍历的结果
	const res = []
	const preOrder = (root) => {
		// 当前结果为空时,无需进行递归
		if(!root) return
		// 记录根节点的值
		res.push(root.val)
		// 前序遍历左子树
		preOrder(root.left)
		// 前序遍历右子树
		preOrder(root.right)
		
	}
	preOrder(root) // 调用函数
	return res // 返回结果
}

// 测试
var root = [1,2,3,4,5,null,6,7,8,null,null, 9]
preOrderTravlersal(root)
  • 迭代法实现前序遍历
// 迭代法实现前序遍历
const preOrderTravlersal = function(root) {
	const res = []
	const stack = [] // 存储当前不处理的右子节点
	// 外层循环控制循环的次数(整体的把控)
	while(root || stack.length) {
		// 控制左子节点的处理
		while(root) {
			// 1.右子节点入栈
			stack.push(root.right) 
			// 2.记录根节点
			res.push(root.val)
			// 3.下一步处理左子节点
			root = root.left
		}
		// 左子树处理完毕,将stack出栈,处理右子树
		root = stack.pop()
	}
	return res
}

// 测试
var root = [1,2,3,4,5,null,6,7,8,null,null, 9]
consoele.log(preOrderTravlersal(root))
  • 二叉树的最大深度
const maxDepth = function (root) {
	if(!root) return 0
	return Math.max(maxDepth(root.left), maxDepth(root.left)) + 1
}
  • 二叉树的层序遍历(bfs)
    请添加图片描述
    广度优先:是一种按照层级,逐层的输出
	// 在操作的时候需要使用队列的思想,先入先出。
	const bfs = function (root) {
		const res = []
		if (!root) return
		//声明队列用于存储后续的数据
		const queue = []
		queu.push(root.val)
	
		// 遍历队列
		whiel(queue.length) {
			// 针对本轮操作创建新的数组,存储本层的结果
			res.push([])
			let len = queue.length
			for (let i = 0; i < len; i++) {
				// 将本次操作的结果出队列
				const ndoe = queue.shift()
				res[res.length - 1].push(node.val)
				// 检测是否存在左右子节点,如果有,则入队即可
				if (node.left) {queue.push(node.left) }
				if (node.right) {queue.push(node.rght) }
			}
		}
		return res
	}

// 输出结果
[
	[1]
	[2,3]
	[4]
]
  • 二叉树 - 二叉搜索树
    请添加图片描述

验证二叉搜索树

const isValidBST = function (root) {
	return helper(root, -Infinity, Infinity)
}

const helper = function (root, lower, upper) {
	if(!root) { return true }
	// 检测当前节点是否超出边界
	if (root.val) {
		if(root.val > upper || root.val < lower) {
			return false
		}
	}
	// 当前节点通过检测,再检测右节点
	return helper(root.left, lower, root.val) && helper(root.right, root.val, upper) 
}

验证二叉搜索树 (方法2)

二叉搜索树的中序遍历时升序的。
请添加图片描述

// 二叉树的中序遍历
const inorderTraversal = function (root) {
	const res= []
	const stack = []
	// 外层控制总体的流程,
	// 内层控制每一轮的流程
	while(root || stack.length) {
		while(root) {
			stack.push(root)
			root = root.left
		}
		// 取出最左侧的节点
		root = stack.pop()
		res.push(root.val)
		root = root.right
	}
	return res	
}

验证二叉搜索树

var isValidVBST = function(root) {
	const stack = []
	// 声明一个变量,记录当前操作的节点,用于与下次获取的节点对比
	const oldNode = -Infinity
	while(root || stack.length) {
		while(root) {
			stack.push(root.val)
			root = root.left
		}
		root = stack.pop()
		if (root.val <= oldNode) {
			return false
		}
		oldNode = root.val
		root = root.right
	}
	return true
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值