为什么要学习数据结构和算法?
但我们通过框架和工具进行开发的时候,如何做到优化程序呢?那么数据结构和算法就起到了一定的作用。程序 = 数据结构 + 算法。
在解决一些特殊的问题时候,如果用到特定的数据结构 ,算法相结合,对我们写的代码就可以做到:
1.化繁为简。
2:提高代码的性能。
3.提交面试通过率。
栈
1.基础概念
2.栈的实现
3.leetcode题目。
- 1.基础概念。
- 后进先出
- 只能操作顶部元素,从栈定添加元素。(添加,移除,取值)。
- 添加新元素的一端称为栈定,另一段称为栈底。
- 栈的实现
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题目
- 包含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
};
- 递归反转链表
/**
* @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
}