前端菜鸟学算法(2)

283 阅读6分钟

前言

经过第一阶段的学习,坚定了我学习算法的信息。这篇主要讲栈,队列,二叉树等数据结构的概念和具体应用

不必回头顾盼,但只用力前行!

1.栈

线性表、运算受限、先进后出

image.png 这个题考察字符串的各种逻辑操作,笔者实现如下:

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)
}

image.png 这个题考察队列的理解程度和灵活运用,实现如下:

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.队列

操作受限的线性表,先进先出

image.png 这个题考察线性表的结构和操作方式,实现如下:

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]
    }
}

image.png 这个题考察线性表的理解和灵活应用,实现如下:

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.链表

线性表的一种存储方式(另一种是数组:可以根据偏移实现快速的随机读写。但扩容,增删元素极慢。链表则与之相反),由若干个结点组成,每个结点包含数据域和指针域。 image.png 这个题利用快速排序的思路和链表的结构,实现如下:

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]

image.png 这个题主要考察对链表结构的理解和灵活运用,实现如下:

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里,表现形式为二维数组

image.png 这个题考察螺旋矩阵的结构特性,具体实现如下:

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]

image.png 这个题考察螺旋矩阵的结构特性,具体实现如下:

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里,表现形式为对象

image.png 这个题的思路是:先找到构建一个对称二叉树的方法,再去判断是否是对称二叉树。具体实现如下:

// 节点
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

image.png 这个题的思路是:先找到构建一个二叉搜索树的方法,再去判断。具体实现如下:

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

image.png 这个题的思路是:构建一个类似对应的二叉树(前面方法已有,这里忽略),然后使用先序遍历的方法,拿到每个路径的集合,然后根据条件求值。具体实现如下:

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层必须是满二叉树),任一结点的值是其子树所有结点的最大值(最大堆)或最小值(最小堆)

image.png 这个题的思路是:统计,排序,输出。具体实现如下:

// 使用排序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)
        }
    }
}

image.png 这个题的思路是:求解任意整数的质因数,质因数是否在指定质因数范围内,是否达到指定个数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.贪心算法

是一种算法思想。 定义:又称贪婪算法,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,是在某种意义上的局部最优解。

image.png 这个题的思路是:从低点买入,只要可以赚钱就卖出;不断买卖,追求多次利益。具体实现如下:

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

image.png 这个题的思路是:给钱找零,优先给金额大的零钱,尽量把零钱放在手里(追求多次找零)。具体实现如下:

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.动态规划

是一种算法思想。 包含三个概念:状态转移方程、最优子结构、边界

image.png 这个题的思路是:先对问题建模找到一个通用公式,然后找到最终的边界。具体实现如下:

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

image.png 这个题的思路是: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