前言
经过第一阶段的学习,坚定了我学习算法的信息。这篇主要讲栈,队列,二叉树等数据结构的概念和具体应用
不必回头顾盼,但只用力前行!
1.栈
线性表、运算受限、先进后出
这个题考察字符串的各种逻辑操作,笔者实现如下:
function bong(arr) {
let ret = []
for(let i of arr) {
if(i === 'C') {
if(ret.length) {
ret.pop()
}
}else if(i === 'D') {
let pre = ret.slice(-1)
ret.push(pre * 2)
}else if(i === '+') {
let pre1 = ret.slice(-1)
let pre2 = ret.slice(-2, -1)
ret.push(pre1[0] + pre2[0])
}else {
ret.push(i)
}
}
return ret.reduce((acc, num) => (acc + num), 0)
}
这个题考察队列的理解程度和灵活运用,实现如下:
function maxRectMethod(arr) {
let result = []
let reg = /1{2,}/g
arr = arr.map(item => {
let str = item.join('')
let r = reg.exec(str)
let rs = []
while(r) {
rs.push([r.index, r.index + r[0].length - 1])
r = reg.exec(str)
}
return rs
})
let maxRect = (arr, result, row) => {
let top = arr.pop()
let next = arr.pop()
// 记录第一行的每一个起始点和截止点
// 记录第二行点每一个起始点和截止点
// 记录交叉点的起始索引
// 记录交叉点的截止索引
let tt, nn, start, end
let width = 1 // 交叉点宽度
let maxW = 1 // 最大宽度
row++
for(let i = 0, il = top.length; i < il; i++) {
tt = top[i]
for(let j = 0, jl = next.length; j < jl; j++) {
nn = next[j]
width = Math.min(tt[1], nn[1]) - Math.max(tt[0], nn[0])
if(width > maxW) {
maxW = width
start = Math.max(tt[0], nn[0])
end = Math.min(tt[1], nn[1])
}
}
}
if(start === undefined || end === undefined) { // 如果没有找到交叉点
if(row < 3) { // 真的没有交叉点
return false
}else {
width = top[0][1] - top[0][0] + 1
if(width > 1) {
result.push((row - 1) * width)
}
}
}else {
arr.push([[start, end]])
maxRect(arr, result, row++)
}
}
while(arr.length > 1) {
maxRect([].concat(arr), result, 1)
arr.pop()
}
let max = 0
let item = result.pop()
while(item) {
if(item > max) {
max = item
}
item = result.pop()
}
return max || -1
}
2.队列
操作受限的线性表,先进先出
这个题考察线性表的结构和操作方式,实现如下:
class circleQueue {
constructor(k) {
this.list = Array(k)
this.front = 0 // 队首指针
this.rear = 0 // 队尾指针
this.max = k // 队列的长度
}
enQueue(num) {
if(this.isFull()) {
return false
}else {
this.list[this.rear] = num
this.rear = ++this.rear % this.max
return true
}
}
deQueue() {
let num = this.list[this.front]
this.list[this.front] = ''
this.front = ++this.front % this.max
return num
}
isEmpty() {
return this.front === this.rear && this.list[this.front] === ''
}
isFull() {
return this.front === this.rear && !!this.list[this.front]
}
Front() {
return this.list[this.front]
}
Rear() {
return this.list[(this.rear || this.max) - 1]
}
}
这个题考察线性表的理解和灵活应用,实现如下:
function taskSchedule(arr, n) {
if(!n) {
return arr.length
}
let keyMap = {}, q = ''
arr.forEach(item => {
if(keyMap[item]) {
keyMap[item] ++
}else {
keyMap[item] = 1
}
});
for(;;) {
let keys = Object.keys(keyMap)
if(!keys.length) {
break
}
let tmp = []
for (let i = 0; i <= n; i++) {
let max = 0
let key, position
keys.forEach((k, idx) => {
if(keyMap[k] > max) {
max = keyMap[k]
key = k
position = idx
}
})
if(key) {
tmp.push(key)
keys.splice(position, 1)
keyMap[key] --
keyMap[key] < 1 && (delete keyMap[key])
}else {
break
}
}
q += tmp.join('').padEnd(n + 1, '-')
}
q = q.replace(/-+$/g, '')
return q.length
}
3.链表
线性表的一种存储方式(另一种是数组:可以根据偏移实现快速的随机读写。但扩容,增删元素极慢。链表则与之相反),由若干个结点组成,每个结点包含数据域和指针域。
这个题利用快速排序的思路和链表的结构,实现如下:
class Node {
constructor(value) {
this.val = value
this.next = null
}
}
// 链表结构
class NodeList {
constructor(arr) {
// 链表头
let head = new Node(arr.shift())
let next = head
arr.forEach(item => {
next.next = new Node(item)
next = next.next
})
return head
}
}
// 交换两个节点的值
let swap = (p, q) => {
let t = p.val
p.val = q.val
q.val = t
}
// 寻找基准元素的节点
let partion = (begin, end) => {
let val = begin.val
let p = begin
let q = begin.next
while(q !== end) {
if(q.val < val) {
p = p.next
swap(p, q)
}
q = q.next
}
swap(p, begin) // 让基准元素跑到中间去
return p
}
function sort(begin, end) {
if(begin !== end) {
let part = partion(begin, end)
sort(begin, part)
sort(part.next, end)
}
}
let head = new NodeList([4, 1, 2, 3, 7, 9, 10, 12, 6])
sort(head, null)
let ret = []
let next = head
while(next) {
ret.push(next.val)
next = next.next
}
console.log(ret) // [1, 2, 3, 4, 6, 7, 9, 10, 12]
这个题主要考察对链表结构的理解和灵活运用,实现如下:
class Node {
constructor(val) {
this.val = val
this.next = null
}
}
// 创建环形链表
function createChain(arr, pos) {
let head = new Node(arr.shift())
let next = head
arr.forEach((item, index) => {
next.next = new Node(item)
next = next.next
if(index === arr.length - 1) {
next.next = findNodeByPos(head, pos)
}
})
return head
}
function findNodeByPos(chain, pos) {
let current = chain
let count = 0
while(current) {
if(count === pos) {
break
}
current = current.next
count++
}
return current
}
// 判断链表是否有环
function hasCycle(chain) {
let slow = chain.next
let fast = chain.next.next
while(slow !== fast) {
if(fast === null || fast.next === null) {
return false
}
slow = slow.next
fast = fast.next.next
}
return true
}
hasCycle(createChain([1,2,3,4,5], 5)) // false
hasCycle(createChain([1,2,3,4,5], -1)) // false
hasCycle(createChain([1,2,3,4,5], 1)) // true
4.矩阵
矩阵在js里,表现形式为二维数组
这个题考察螺旋矩阵的结构特性,具体实现如下:
function screw(arr) {
let r = []
let map = (part) => {
let remain = []
part.forEach((row, rowIndex) => {
if(rowIndex === 0) {
r = [...r, ...row]
}else if(rowIndex === part.length - 1) {
r = [...r, ...row.reverse()]
}else {
r = [...r, row.pop()]
remain.push(row.shift())
}
});
r.push(...remain.reverse())
part.splice(0, 1)
part.splice(part.length - 1, 1)
if(part.length) {
map(part)
}
}
map(arr)
return r
}
let arr1 = [ [1, 2, 3, 4],
[12, 13, 14, 5],
[11, 16, 15, 6],
[10, 9, 8, 7],
]
let arr2 = [ [1, 2, 3, 4],
[10, 11, 12, 5],
[9, 8, 7, 6],
]
screw(arr1) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
screw(arr2) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
这个题考察螺旋矩阵的结构特性,具体实现如下:
function rotate(matrix) {
let len = matrix.length
let temp
for (let row = 0; row < len / 2; row++) {
for (let col = 0; col < len; col++) {
temp = matrix[row][col]
matrix[row][col] = matrix[len - row - 1][col]
matrix[len - row - 1][col] = temp
}
}
return fold(matrix)
}
function fold(matrix) {
let len = matrix.length
let temp
for (let row = 0; row < len; row++) {
for (let col = 0; col < row; col++) {
temp = matrix[row][col]
matrix[row][col] = matrix[col][row]
matrix[col][row] = temp
}
}
return matrix
}
let arr = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
rotate(arr) // [ [ 7, 4, 1 ], [ 8, 5, 2 ], [ 9, 6, 3 ] ]
5.二叉树
二叉树在js里,表现形式为对象
这个题的思路是:先找到构建一个对称二叉树的方法,再去判断是否是对称二叉树。具体实现如下:
// 节点
class Node {
constructor(val) {
this.val = val
this.left = null
this.right = null
}
}
// 树
class Tree {
constructor(arr) {
let nodeList = []
let root
for (let i = 0; i < arr.length; i++) {
let node = new Node(arr[i])
nodeList.push(node)
if(i > 0) {
// 计算当前节点属于哪一层
let n = Math.floor(Math.sqrt(i + 1))
// 记录当前层的起始点
let q = Math.pow(2, n) - 1
// 记录上一层的起始点
let p = Math.pow(2, n - 1) -1
// 找到当前节点的父节点
let parent = nodeList[p + Math.floor((i - q) / 2)]
// 将当前节点和上一层的父节点做关联
if(parent.left) {
parent.right = node
}else {
parent.left = node
}
}
}
root = nodeList.shift()
nodeList.length = 0
return root
}
static isSymmetry(root) {
if(!root) return true
let walk = (left, right) => {
if(!left && !right) {
return true
}
if((left && !right) || (!left && right) || (left.val !== right.val)) {
return false
}
return walk(left.left, right.right) && walk(left.right, right.left)
}
return walk(root.left, root.right)
}
static isSameTree(node1, node2) { // 额外的,判断两个二叉树是否相等
if(node1 === null && node2 === null) {
return true
}
let walk = (n1, n2) => {
if(!n1 && !n2) {
return true
}
if((n1 && !n2) || (!n1 && n2) || (n1.val !== n2.val)) {
return false
}
return walk(n1.left, n2.left) && walk(n1.right, n2.right)
}
return walk(node1, node2)
}
}
let root = new Tree([1, 2, 2, 3, 4, 4, 3])
Tree.isSymmetry(root) // true
let root1 = new Tree([1, 2, 2, 3, 4, 5, 3])
Tree.isSymmetry(root1) // false
这个题的思路是:先找到构建一个二叉搜索树的方法,再去判断。具体实现如下:
class Node {
constructor(val) {
this.val = val
this.left = null
this.right = null
}
}
class Tree {
constructor() {
this.head = null
}
insert(target, node) {
if(target.val > node.val) {
if(target.left === null) {
target.left = node
}else {
this.insert(target.left, node)
}
}else {
if(target.right === null) {
target.right = node
}else {
this.insert(target.right, node)
}
}
}
build(arr) {
for (let i = 0; i < arr.length; i++) {
let node = new Node(arr[i])
if(!this.head) {
this.head = node
}else {
this.insert(this.head, node)
}
}
}
static isValidBST(root) {
let walk = (node) => {
if(node.left === null && node.right === null) {
return true
}
if((node.left && node.left.val > node.val) || (node.right && node.right.val < node.val)) {
return false
}
return walk(node.left) && walk(node.right)
}
return walk(root)
}
}
let tree = new Tree()
tree.build([10, 20, 25, 16, 6, 17, 11, 8, 5])
Tree.isValidBST(tree.head) // true
let head = new Node(3)
head.left = new Node(4)
head.right = new Node(2)
Tree.isValidBST(head) // false
这个题的思路是:构建一个类似对应的二叉树(前面方法已有,这里忽略),然后使用先序遍历的方法,拿到每个路径的集合,然后根据条件求值。具体实现如下:
function pathSum(root, sum, matrix = []) {
let walk = (node, arr = []) => {
if(node) {
arr.push(node.val)
if(node.left === null && node.right === null) { // 叶子结点时
matrix.push(arr)
}
walk(node.left, [...arr]) // arr.slice() arr.concat()
walk(node.right, [...arr])
}
}
walk(root)
let findSum = (arr) => {
return _ => arr.reduce((acc,item) => acc + item) === _
}
return matrix.filter(arr => findSum(arr)(sum))
}
let tree = {
"val": 5,
"left": {
"val": 4,
"left": {
"val": 11,
"left": {
"val": 7,
"left": null,
"right": null
},
"right": {
"val": 2,
"left": null,
"right": null
}
},
"right": null
},
"right": {
"val": 8,
"left": {
"val": 13,
"left": null,
"right": null
},
"right": {
"val": 4,
"left": null,
"right": {
"val": 1,
"left": null,
"right": null
}
}
}
}
pathSum(tree, 22) // [5, 4, 11, 2]
6.堆
堆在js里,表现形式为数组。 定义:必须是完全二叉树(n-1层必须是满二叉树),任一结点的值是其子树所有结点的最大值(最大堆)或最小值(最小堆)
这个题的思路是:统计,排序,输出。具体实现如下:
// 使用排序api 普通解法
function charSort(str) {
let arr = str.split('')
let calObj = {}
arr.forEach(char => {
if(!calObj[char]) {
calObj[char] = 1
}else {
calObj[char] ++
}
})
let matrix = Object.entries(calObj)
matrix.sort((pre,next) => pre[1] < next[1] ? 1 : -1)
return matrix.reduce((acc, item) => {
acc += item[0].repeat(item[1])
return acc
}, '')
}
// map结构,heap sort的解法
function charSortByHeap(str) {
let arr = str.split('')
let map = new Map()
for (let i = 0, len = arr.length; i < len; i++) {
if(map.has(arr[i])) {
let count = map.get(arr[i])
map.set(arr[i], count + 1)
}else {
map.set(arr[i], 1)
}
}
let heap = new Heap(Array.from(map.values()))
let hArr = heap.sort()
let sArr = []
while(hArr.length) {
let top = hArr.pop()
for(let [k, v] of map) {
if(v === top) {
sArr.push(k.repeat(v))
map.delete(k)
break
}
}
}
return sArr.join('')
}
// 步骤:先构建最大堆,拿到最大值,和叶子结点互换,去掉最大值的结点。循环往复,就能一次次取到剩余最大堆的最大值,从而实现排序
class Heap {
constructor(arr) {
this.arr = arr
}
/**
* 从小到大的数据
* @returns arr
*/
sort() {
let arr = this.arr
let n = arr.length
if(n <= 1) {
return arr
}else {
for (let i = Math.floor(n / 2); i >= 0; i--) {
Heap.maxHeapify(arr, i, n)
}
for (let j = 0; j < n; j++) {
Heap.swap(arr, 0, n - 1 - j)
Heap.maxHeapify(arr, 0, n - 1 - j - 1)
}
return arr
}
}
// 交换两个元素
static swap(arr, a, b) {
if(a === b) return
let t = arr[a]
arr[a] = arr[b]
arr[b] = t
}
/**
* 构建最大堆的过程
* @param {*} arr 数组
* @param {*} i 某个结点的索引
* @param {*} size 数组长度
*/
static maxHeapify(arr, i, size) {
// 左结点(索引)
let l = i * 2 + 1
// 右结点(索引)
let r = i * 2 + 2
let max = i
// 父结点和l结点进行比较
if(l <= size && arr[l] > arr[max]) {
max = l
}
// 最大结点和r结点进行比较
if(r <= size && arr[r] > arr[max]) {
max = r
}
if(max !== i) {
Heap.swap(arr, i, max)
Heap.maxHeapify(arr, max, size)
}
}
}
这个题的思路是:求解任意整数的质因数,质因数是否在指定质因数范围内,是否达到指定个数n。具体实现如下:
class Ugly {
constructor(n, primes) {
this.n = n
// this.primes = primes // 普通写法
this.primes = new Heap(primes)
}
getAll() {
// 超级丑数列表
let ret = [1]
let i = 2
let primes = this.primes
while(ret.length < this.n) {
let arr = Ugly.getPrimes(i)
let k = 0
let l = arr.length
for (; k < l; k++) {
// if(!primes.includes(arr[k])) { // 普通写法
// break
// }
if(!primes.find(arr[k])) {
break
}
}
// k === l 两种情况:1.当前这个数没有质因数 2.所有质因数都在指定列表中
if(k === l) {
if(l === 0) {
// if(primes.includes(i)) { // 普通写法
// ret.push(i)
// }
if(primes.find(i)) {
ret.push(i)
}
}else {
ret.push(i)
}
}
i++
}
return ret[this.n - 1]
}
// 计算指定正整数n的质因数
static getPrimes(n) {
let prime = (n) => {
// 存储所有的质因数
let arr = []
for (let i = 2; i < n / 2 + 1; i++) {
if(n % i === 0 && !prime(i).length) {
arr.push(i)
}
}
return arr
}
return prime(n)
}
}
class Heap {
constructor(arr) {
this.arr = arr
this.max = arr.length
this.sort()
}
/**
* 从小到大的数据
* @returns arr
*/
sort() {
let arr = this.arr
let n = arr.length
if(n <= 1) {
return arr
}else {
for (let i = Math.floor(n / 2); i >= 0; i--) {
Heap.maxHeapify(arr, i, n)
}
// 在查找里,不需要排序,只需构建一次最大堆,故删掉
// for (let j = 0; j < n; j++) {
// Heap.swap(arr, 0, n - 1 - j)
// Heap.maxHeapify(arr, 0, n - 1 - j - 1)
// }
return arr
}
}
find(val, i = 0) {
let arr = this.arr
if(val > arr[i] || i > this.max) {
return false
}else if(val === arr[i]) {
return val
}else {
return this.find(val, 2 * i + 1) || this.find(val, 2 * i + 2)
}
}
// 交换两个元素
static swap(arr, a, b) {
if(a === b) return
let t = arr[a]
arr[a] = arr[b]
arr[b] = t
}
/**
* 构建最大堆的过程
* @param {*} arr 数组
* @param {*} i 某个结点的索引
* @param {*} size 数组长度
*/
static maxHeapify(arr, i, size) {
// 左结点(索引)
let l = i * 2 + 1
// 右结点(索引)
let r = i * 2 + 2
let max = i
// 父结点和l结点进行比较
if(l <= size && arr[l] > arr[max]) {
max = l
}
// 最大结点和r结点进行比较
if(r <= size && arr[r] > arr[max]) {
max = r
}
if(max !== i) {
Heap.swap(arr, i, max)
Heap.maxHeapify(arr, max, size)
}
}
}
7.贪心算法
是一种算法思想。 定义:又称贪婪算法,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,是在某种意义上的局部最优解。
这个题的思路是:从低点买入,只要可以赚钱就卖出;不断买卖,追求多次利益。具体实现如下:
function maxProfit(prices) {
let profit = 0
for (let i = 0, len = prices.length; i < len - 1; i++) {
profit += Math.max(0, prices[i + 1] - prices[i])
}
return profit
}
maxProfit([7,1,5,3,6,4]) // 7
maxProfit([1,2,3,4,5]) // 4
maxProfit([7,6,4,3,1]) // 0
这个题的思路是:给钱找零,优先给金额大的零钱,尽量把零钱放在手里(追求多次找零)。具体实现如下:
function lookChange(bills) {
let hand = []
while(bills.length) {
let m = bills.shift()
if(m === 5) {
hand.push(m)
}else {
let change = m - 5
hand.sort((pre, next) => next - pre)
for (let i = 0; i < hand.length; i++) {
if(hand[i] <= change) {
change -= hand[i]
hand.splice(i, 1)
i-- // 删除了元素,数组长度发生了变化,要维持刚才的i不变
}
if(change === 0) {
break
}
}
if(change !== 0) {
return false
}else {
hand.push(m)
}
}
}
return true
}
lookChange([5,5,5,10,20]) // true
lookChange([5,5,10,10,20]) // false
8.动态规划
是一种算法思想。 包含三个概念:状态转移方程、最优子结构、边界
这个题的思路是:先对问题建模找到一个通用公式,然后找到最终的边界。具体实现如下:
function uniquePath(arr, m, n) {
let dp = (m, n) => {
if(m === 2 && n === 2) { // 边界
return (arr[1][1] === 1 || (arr[0][1] + arr[1][0] === 2)) ? 0 : (arr[1][0] === 1 || arr[0][1] === 1) ? 1 : 2
}else if(m < 2 || n < 2){
if(m < 2) { // 单行有1就返回0, 没有1返回1
return arr[m - 1].includes(1) ? 0 : 1
}else { // 单列中不能有障碍物(1),有就是0 没就返回1
for (let i = 0; i < m; i++) {
if(arr[i][0] === 1) {
return 0
}
}
return 1
}
}else {
return dp(m - 1, n) + dp(m, n - 1)
}
}
return dp(m, n)
}
let arr1 = [ [0,0,0],
[0,1,0],
[0,0,0]
]
let arr2 = [ [0,1],
[0,0]
]
uniquePaths(arr1, 3, 3) // 2
uniquePaths(arr2, 2, 2) // 1
这个题的思路是:F(src, dist, k) = F(src, dist - 1, k - 1) + F(dist - 1, dist, 1)。具体实现如下:
function findCheapestPrice(n, flights, src, dst, k) {
let cheap = (src, dst, k) => {
// 找到dst的前一站
let prev = flights.filter(item => item[1] === dst)
let min = Math.min.apply(null, prev.map(item => {
// 从dst往前找,找到了起始城市
if(item[0] === src && k > -1) {
return item[2]
}else if(k === 0 && item[0] !== src){
return Number.MAX_SAFE_INTEGER
}else {
return item[2] + cheap(src, item[0], k - 1)
}
}))
return min
}
return cheap(src, dst, k) || -1
}
let fights = [
[0, 1, 100],
[1, 2, 100],
[0, 2, 500]
]
let n = fights.length
findCheapestPrice(n, fights, 0, 2, 1) // 200
findCheapestPrice(n, fights, 0, 2, 0) // 500