汇总一些常见的算法,练习练习防老年痴呆。
动态规划
这两个视频不错:动态规划 (第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 。
这个题稍微有点难度了,需要一个二维的数组记录最优值。
第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算法
核心是使用优先队列去遍历图,优先队列用小顶堆实现。
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
};
为什么记录这题呢?大概是我用二分的时候翻车了,简单的二分竟然翻车了