复习几个算法题

123 阅读5分钟

汇总一些常见的算法,练习练习防老年痴呆。

动态规划

这两个视频不错:动态规划 (第1讲), 动态规划(第2讲)

动态规划主要找到子问题关系,有时候一个元素分选与不选两种选择的思路很重要。

最大子数组和

nums = [-2,1,-3,4,-1,2,1,-5,4],连续子数组 [4,-1,2,1] 的和最大,为 6 。

dp[i] 表示在第 i 个元素时的最大和,则 dp[i] 为 nums[i] 和 nums[i] + dp[i - 1] 中的最大值。注意dp[1] 可能比 dp[4] 大,所以要对 dp 数组求最大值才是结果

var maxSubArray = function(nums) {
    let dp = [nums[0]]
    let max = nums[0]
    for (let i = 1; i < nums.length; i++) {
        // nums[i] 是否独立成组
        dp[i] = Math.max(nums[i], nums[i] + dp[i - 1])
        max = Math.max(max, dp[i])
    }
    return max
};
// 优化点:dp只与前一个有关,因此不用数组,常量即可
var maxSubArray = function(nums) {
    let preOpt = nums[0]
    let max = nums[0]
    for (let i = 1; i < nums.length; i++) {
        // nums[i] 是否独立成组
        let curOpt = Math.max(nums[i], nums[i] + preOpt)
        max = Math.max(max, curOpt)
        preOpt = curOpt
    }
    return max
};

青蛙跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

f(n)为跳上n级的跳法,跳到第n级就两种选择,从 n-1 级跳,或从 n-2 级跳,那 f(n)=f(n-1)+f(n-2),一个斐波那契数列

var numWays = function(n) {
    let dp = []
    dp[0] = 1
    dp[1] = 1
    for (let i = 2; i <= n; i++) {
        dp[i] = (dp[i - 1] + dp[i - 2]) % 1000000007
    }
    return dp[n]
};

var numWays = function(n) {
    let left = 1, right = 1
    for (let i = 2; i <= n; i++) {
        let tmp = right
        right = (left + right) % 1000000007
        left = tmp
    }
    return right
};

最长连续递增序列

输入:nums = [1,3,5,4,7]。输出:3。解释:最长连续递增序列是 [1,3,5], 长度为3。

可以用dp,dp[i]为在第 i 个元素的最长递增序列长度,和最大子数组和有点像。写起来简单得不考虑dp。

var findLengthOfLCIS = function(nums) {
    let max = 1, count = 1
    for (let i = 1; i < nums.length; i++) {
        if (nums[i] > nums[i - 1]) {
            count++
            max = max > count ? max : count
        } else {
            count = 1
        }
    }
    return max
};

最长公共子序列

输入:text1 = "ace", text2 = "abcde" 。输出:3。解释:最长公共子序列是 "ace" ,它的长度为 3 。

这个题稍微有点难度了,需要一个二维的数组记录最优值。

longest-common-subsequence.png

第0行表示 "a" 和 "a" "ab" "abc"的最长公共子序列,很明显是1。第0列则是 "a" 和 "a" "ac" "ace" 最长公共子序列,很明显也是1。

继续能看出f(1,1) "ab" "ac" = 1, f(1,2) "abc" "ac" = 2, f(2,1) "ab" "ace" = 1

重点看f(2,2) "abc" "ace" = 2,怎么来呢,此时text1[2]和text2[2]不相等,那么最长子序列必然在 “abc” "ac" 和 "ab" "ace" 中,也就是f(1,2) f(2,1)的最大值。可以得到当text1[i]!==text2[j], opt[i][j] = Math.max(opt[i][j - 1], opt[i - 1][j])

回过头看 f(1,2),此时 text1[1]===text2[2], "abc" "ac" 的最长子序列长度就等于 "ab" "a" 的长度+1,即 text1[1]===text2[2], opt[i][j] = opt[i - 1][j - 1] + 1

第0行和第0列需要特殊处理

var longestCommonSubsequence = function(text1, text2) {
    let opt = []
    for (let i = 0; i < text1.length; i++) {
        opt[i] = []
        for (let j = 0; j< text2.length; j++) {
            if (i === 0 || j === 0) {
                if (text1[i] === text2[j]) {
                    opt[i][j] = 1
                    continue
                }
                if ( i === 0 && j === 0) {
                    opt[i][j] = 0
                    continue
                }
                if (j === 0) {
                    opt[i][j] = opt[i - 1][j]
                }
                if (i === 0) {
                    opt[i][j] = opt[i][j - 1]
                }
            } else {
                if (text1[i] === text2[j]) {
                    opt[i][j] = opt[i - 1][j - 1] + 1
                } else {
                    opt[i][j] = Math.max(opt[i][j - 1], opt[i - 1][j])
                }
            }

        }
    }
    return opt[text1.length - 1][text2.length - 1]
};

最长回文子串

输入:s = "babad"。输出:"bab"。解释:"aba" 同样是符合题意的答案。

opt[i][j]表示子串i到j是否是回文,是则opt[i][j]=1,否则=0

子问题的关系就表现为如果opt[i][j]是回文,那opt[i+1][j-1]亦是回文,bab是回文则a是回文,baab是回文则aa是回文。

反过来讲当 s[i]=s[j] 时,s[i][j] 是不是回文取决于 s[i+1][j-1]

那就很好写了

var longestPalindrome = function (s) {
  let opt = []
  let maxL = 0, maxI = 0, maxJ = 0
  for (let j = 0; j < s.length; j++) {
    for (let i = 0; i <= j; i++) {
      if (!opt[i]) opt[i] = []
      if (i === j) {
        opt[i][j] = 1
        continue
      }
      if (s[i] !== s[j]) opt[i][j] = 0
      else opt[i][j] = i+1 < j-1 ? opt[i + 1][j - 1] : 1
      if (opt[i][j] && j - i > maxL) {
        maxI = i
        maxJ = j
        maxL = j - i
      }
    }
  }
  return s.substring(maxI, maxJ + 1)
}

聪明的你也不一定能发现这种算法复杂度有点高,除非你听过马拉车(Manacher)

他是这样的,一个字符串你往空隙添加#,无论原字符串长度是奇还是偶,新字符串长度就会变成奇数,如bab变成b#a#b,baba变成b#a#b#a。

这样有什么好处呢,bab回文中心在1,baab回文中心在1.5,若从中心扩散寻找回文就不好找。加了#后回文中心下标就是整数,来看代码

var longestPalindrome = function(s) {
    let maxP = s[0]
    let newS = s[0]
    for (let i = 1; i < s.length; i++) {
        newS += `#${s[i]}`
    }
    for(let i=1; i < newS.length; i++) {
        let curP = newS[i] === '#' ? '' : newS[i]
        let m = i -1, n = i + 1
        while(m >= 0 && n < newS.length && newS[m] === newS[n]) {
            if (newS[m] !== '#') {
                curP = newS[m] + curP + newS[n]
            }
            m--
            n++
        }
        if (curP.length > maxP.length) {
            maxP = curP
        }
    }
    return maxP
};

虽然还是两层循环,但有终止,复杂度就低一点点

堆很实用,图的最短路径也会用到堆

用数组存堆,则下标为n的元素两个子元素为:2n+1 2n+2

那么堆的最后一个父元素就是:Math.floor((n - 2) / 2),也即是 Math.floor((arr.length - 2) / 2)

构建堆只需要从最后一个父元素往前进行堆化,需要注意每次堆化发生交换后需要对交换元素进行堆化

数组中的第K个最大元素

输入: [3,2,1,5,6,4], k = 2。输出: 5

使用一个大小为k的小顶堆,遍历数组放入堆,堆顶就是第k大元素

var swap = (nums, i, j) => {
    let tmp = nums[i]
    nums[i] = nums[j]
    nums[j] = tmp
}
var heapify = (nums, i, k) => {
    let left = 2*i + 1
    let right = left + 1
    let min = i
    if (left < k && nums[left] < nums[min]) {
        min = left
    }
    if (right < k && nums[right] < nums[min]) {
        min = right
    }
    if (min !== i) {
        swap(nums, i, min)
        heapify(nums, min, k)
    }
}
var buildHeap = (nums, k) => {
    let lastParent = Math.floor((k - 2) / 2)
    for (let i = lastParent; i >= 0; i--) {
        heapify(nums, i, k)
    }
}
var findKthLargest = function(nums, k) {
    buildHeap(nums, k)
    for (let i = k; i < nums.length; i++) {
        if (nums[i] > nums[0]) {
            nums[0] = nums[i]
            heapify(nums, 0, k)
        }
    }
    return nums[0]
};

图的最短路径

参考:BFS和DFS算法(第3讲)—— 从BFS到Dijkstra算法

核心是使用优先队列去遍历图,优先队列用小顶堆实现。

map.png

const graph = {
    A: {
        B: 5,
        C: 1
    },
    B: {
        A: 5,
        D: 1,
        C: 2
    },
    C: {
        A: 1,
        B: 2,
        D: 4,
        E: 8
    },
    D: {
        E: 3,
        F: 6,
        B: 1,
        C: 4
    },
    E: {
        C: 8,
        D: 3
    },
    F: {
        D: 6
    }
}
const swap = (arr, i, j) => {
    let tmp = arr[i]
    arr[i] = arr[j]
    arr[j] = tmp
}
const heapify = (arr, i) => {
    let left = 2*i + 1
    let right = left + 1
    let min = i
    if (left < arr.length && arr[left].dis < arr[min].dis) {
        min = left
    }
    if (right < arr.length && arr[right].dis < arr[min].dis) {
        min = right
    }
    if (min !== i) {
        swap(arr, min, i)
        heapify(arr, min)
    }
}
const dijkastra = (graph, start) => {
    let queue = [{
        name: start,
        dis: 0,
        parent: null
    }]
    let seen = {
    }
    while (queue.length) {
        let node = queue.shift()
        if (seen[node.name]) continue
        else seen[node.name] = node
        let subNodes = graph[node.name]
        Object.keys(subNodes).forEach(key => {
            if (!seen[key]) {
                // 从 0 heapify 得从 0 添加
                queue.unshift({
                    name: key,
                    dis: node.dis + subNodes[key],
                    parent: node
                })
                if (queue.length > 1) {
                    heapify(queue, 0)
                }
            }
        })
    }
    return seen
}

链表

这个结构简单复习,很少用到

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

反转链表

和数组交换有点像,使用 prev cur 不断前移调换,prev 从 head 前一个节点开始(null)

var reverseList = function(head) {
    let prev = null
    let cur = head
    while(cur) {
        let tmp = cur.next
        cur.next = prev
        prev = cur
        cur = tmp
    }
    return prev
};

删除链表的倒数第 N 个结点

使用类似于滑动窗口的想法,分快慢两个下标,快的先走k个节点,然后快慢一起走,快下标到终点删除慢下标节点即可。

var removeNthFromEnd = function(head, n) {
    let slow = head
    let fast = head
    while(n) {
        fast = fast.next
        n--
    }
    if (!fast) {
        return head.next
    }
    while (fast && fast.next) {
        slow = slow.next
        fast = fast.next
    }
    slow.next = slow.next.next
    return head
};

删除链表中的节点

很流氓地修改值不改对象即可

var deleteNode = function(node) {
    node.val = node.next.val
    node.next = node.next.next
};

其他

两数之和

输入:nums = [2,7,11,15], target = 9。输出:[2,7] 或者 [7,2]

思路就是遍历,两层循环,每次都看0到j有没有满足的,不过,用一个map记录遍历过程复杂度就降低了

var twoSum = function(nums, target) {
    let indexMap = {}
    for (let i = 0; i < nums.length; i++) {
        const targetI = target - nums[i] 
        if (indexMap[targetI]) return [targetI, nums[i]]
        indexMap[nums[i]] = 1
    }
    return []
};

如果数组是有序的,有其他做法:从两端遍历

var twoSum = function(nums, target) {
    let i = 0, j = nums.length - 1;
    while(i < j) {
        const re = nums[i] + nums[j]
        if (re > target) {
            j--
        } else if (re < target) {
            i++
        } else {
            return [nums[i], nums[j]]
        }
    }
};

三数之和

三数之和可以做成一层循环下求解两数之和,此时复杂度是n^2,可以先排序能得到更多循环跳出,排序是nlogn的,优化成立。有序的,内层循环的两数之和从两端遍历。

如果是最接近的三数之和,和三数之和差不多,只不过要做最小差的记录

统计一个数字在排序数组中出现的次数

输入: nums = [5,7,7,8,8,10], target = 8。输出: 2

直接遍历很简单

var search = function(nums, target) {
    let count = 0
    for (let i = 0; i < nums.length; i++) {
        if (nums[i] === target) {
            count++
        } else if (nums[i] > target) {
            break
        }
    }
    return count
};

二分查找可以优化复杂度

const bi = (nums, val) => {
    let left = 0,right = nums.length - 1;
    while (left <= right) {
        let mid = Math.floor((left + right) / 2)
        if (val === nums[mid]) {
            return mid
        } else if (val < nums[mid]) {
            right = mid - 1
        } else {
            left = mid + 1
        }
    }
    return -1
}
var search = function(nums, target) {
    const targetI = bi(nums, target) 
    if (targetI === -1) return 0
    let count = 1
    for (let i = targetI - 1; i >= 0; i--) {
        if (nums[i] === target) count++
        else break
    }
    for (let i = targetI + 1; i < nums.length; i++) {
        if (nums[i] === target) count++
        else break
    }
    return count
};

为什么记录这题呢?大概是我用二分的时候翻车了,简单的二分竟然翻车了