在线算法
在线算法是指它可以以序列化的方式一个个的处理输入,也就是说在开始时并不需要已经知道所有的输入。
最大连续子序列和(53)
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6
1.动态规划算法
状态转移方程:
f(i)=max{f(i−1)+nums[i],nums[i]}
考虑到 f(i)只和 f(i-1)相关,于是可以只用一个变量pre 来维护对于当前 f(i)从而让空间复杂度降低到 O(1)
https://leetcode.cn/problems/maximum-subarray/solution/zui-da-zi-xu-he-by-leetcode-solution/
var maxSubArray = function(nums) {
let pre = 0, maxAns = nums[0];
nums.forEach((x) => {
pre = Math.max(pre + x, x);
maxAns = Math.max(maxAns, pre);
});
return maxAns;
};
如果要输出该序列
用一个变量arr去记录最大和连续序列的起始与结束索引。随着最大值max的更新,结束索引也更新。当当前和小于等于零,更新开始索引。
var maxSubArray = function (nums) {
let arr = [0, 0];
let max = -101;
let cur = 0;
let temp = 0;
for (let i = 0; i < nums.length; i++) {
if (cur <= 0) {
cur = 0;//连续性断了,重新开始
temp = i;
}
cur += nums[i];
if (cur > max) {
arr[0] = temp;
max = cur;
arr[1] = i;
}
}
//arr[0]为最大子序和的起始索引,arr[1]为最大子序和的结束索引
};
2.分治算法
分治算法将大问题拆分为小问题,再组合起来得到大问题的解。
分:以数组中点为界限,拆成两个数组left,right;
合:
-
这里求一个数组的最大连续和mSum,那么这个最大连续和区间可能出现在left、right、横跨left,right两个数组。
-
left数组的最大连续和、right数组的最大连续和通过递归可以得到,递归结束条件为数组中只有一个元素。
-
当最大连续和区间横跨left,right两个数组时,最大连续和=(以left数组右端点作为区间结束端点时的最大连续和)+(以right数组左端点作为区间开始端点时的最大连续和)。
-
因此mSum为max(left数组的最大连续和、right数组的最大连续和、横跨时的最大连续和)
function Status(l, r, m, i) {
this.lSum = l;
this.rSum = r;
this.mSum = m;
this.iSum = i;
}
const pushUp = (l, r) => {
const iSum = l.iSum + r.iSum;
const lSum = Math.max(l.lSum, l.iSum + r.lSum);
const rSum = Math.max(r.rSum, r.iSum + l.rSum);
const mSum = Math.max(Math.max(l.mSum, r.mSum), l.rSum + r.lSum);
return new Status(lSum, rSum, mSum, iSum);
}
const getInfo = (a, l, r) => {
if (l === r) {
return new Status(a[l], a[l], a[l], a[l]);
}
const m = (l + r) >> 1;
const lSub = getInfo(a, l, m);
const rSub = getInfo(a, m + 1, r);
return pushUp(lSub, rSub);
}
var maxSubArray = function(nums) {
return getInfo(nums, 0, nums.length - 1).mSum;
};
最大连续子序列乘积(156)
输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
1.动态规划算法
如果我们用 f*max(i) 来表示以第 i 个元素结尾的乘积最大子数组的乘积, 考虑元素的正负性,如果第i个元素是正数,则希望该元素能够乘上一个大的正值,如果第i个元素是负数,则希望该元素能乘上一个小的负数。所以除了记录最大乘积,还需要记录最小乘积,毕竟当nums[i]是负数的时候,有可能翻盘。
转移方程:
f
m
a
x
(
i
)
=
m
a
x
(
f
m
a
x
(
i
−
1
)
∗
n
u
m
s
[
i
]
,
f
m
i
n
(
i
−
1
)
∗
n
u
m
s
[
i
]
,
n
u
m
s
[
i
]
)
fmax(i)=max{(fmax(i−1)*nums[i], fmin(i−1)*nums[i],nums[i] )}
fmax(i)=max(fmax(i−1)∗nums[i],fmin(i−1)∗nums[i],nums[i])
f m i n ( i ) = m i n ( f m a x ( i − 1 ) ∗ n u m s [ i ] , f m i n ( i − 1 ) ∗ n u m s [ i ] , n u m s [ i ] ) fmin(i)=min{(fmax(i−1)*nums[i], fmin(i−1)*nums[i], nums[i])} fmin(i)=min(fmax(i−1)∗nums[i],fmin(i−1)∗nums[i],nums[i])
var maxProduct = function (nums) {
let max=1, min=1;
let result=nums[0];
for(let i =0;i<nums.length;i++){
let temp = max;
max=Math.max(max*nums[i],min*nums[i],nums[i]);
min=Math.min(temp*nums[i],min*nums[i],nums[i]);
result=Math.max(max,result);
}
return result;
};
最大子串和
对于一个包含负值的数字串array[1…n],要找到他的一个子串array[i…j](0<=i<=j<=n),使得在array的所有子串中,array[i…j]的和最大。(子串不要求连续,只要元素在原数组中的顺序一致)
Kadane算法
首先,对于array[1…n],如果array[i…j]就是满足和最大的子串,那么对于任何k(i<=k<=j),我们有array[i…k]的和大于0。因为如果存在k使得array[i…k]的和小于0,那么我们就有array[k+1…j]的和大于array[i…j],这与我们假设的array[i…j]就是array中和最大子串矛盾。
其次,我们可以将数组从左到右分割为若干子串,使得除了最后一个子串之外,其余子串的各元素之和小于0,且对于所有子串array[i…j]和任意k(i<=k<j),有array[i…k]的和大于0。(此时我们要说明的是,满足条件的和最大子串,只能是上述某个子串的前缀,而不可能跨越多个子串。我们假设array[p…q],是array的和最大子串,且array[p…q],跨越了array[i…j],array[j+1…k]。根据我们的分组方式,存在i<=m<j使得array[i…m]的和是array[i…j]中的最大值,存在j+1<=n<k使得array[j+1…n]的和是array[j+1…k]的最大值。由于array[m+1…j]使得array[i…j]的和小于0。此时我们可以比较array[i…m]和array[j+1…n],如果array[i…m]的和大于array[j+1…n]则array[i…m]>array[p…q],否array[j+1…n]>array[p…q],无论谁大,我们都可以找到比array[p…q]和更大的子串,这与我们的假设矛盾,所以满足条件的array[p…q]不可能跨越两个子串。对于跨越更多子串的情况,由于各子串的和均为负值,所以同样可以证明存在和更大的非跨越子串的存在。对于单元素和最大的特例,该结论也适用)。
根据上述结论,我们就得到了Kadane算法的执行流程,从头到尾遍历目标数组,将数组分割为满足上述条件的子串,同时得到各子串的最大前缀和,然后比较各子串的最大前缀和,得到最终答案。我们以array={−2, 1, −3, 4, −1, 2, 1, −5, 4}为例,来简单说明一下算法步骤。通过遍历,可以将数组分割为如下3个子串(-2),(1,-3),(4,-1,2,1,-5,4),这里对于(-2)这样的情况,单独分为一组。各子串的最大前缀和为-2,1,6,所以目标串的最大子串和为6。
function Kadane(array, length, left, right)
{
let cur_max = max = left = right = cur_left = cur_right = 0;
for(let i = 0; i < length; ++i)
{
cur_max += array[i];//记录前缀和
if(cur_max > 0)
{
cur_right = i;
if(max < cur_max)
{
max = cur_max;//更新最大值
left = cur_left;//移动子串的指针
right = cur_right;
}
}
else
{
//分割子串
cur_max = 0;
cur_left = cur_right = i + 1;
}
}
return max;
}
k次串联后最大子序列和(1191)
最大子序和变形
以arr=[1,2,-1],k=3为例
1 | 2 | -1 | 1 | 2 | -1 | 1 | 2 | -1 |
当k=2时若最大连续和数组横跨两个序列,此时最大连续和为arr的最大前缀和(2)+arr的最大后缀和(3)=5。若不横跨两个序列,则最大连续和就是arr的最大连续和=3。所以二次串联后的最大连续和数组为【1,2,-1,1,2】。
k>2时就是吧【1,2,-1】往前补位,也只是k为2的结果最后加上(k-2)*sum,实际上是上一个情况的变种。
1(最大后缀和2索引) | 2(后缀) | -1(后缀) | 1(前缀) | 2(最大前缀和3索引) | -1 |
var modulo = (10 ** 9) + 7;
/**
* @param {number[]} arr
* @param {number} k
* @return {number}
*/
var kConcatenationMaxSum = function (arr, k) {
let prefix = arr[0]
let preMax = Math.max(0, prefix);
let postfix = arr[arr.length - 1];
let postMax = Math.max(0, postfix);
let subSum = Math.max(0, arr[0]);
let subMax = Math.max(0, subSum);
// 求最大前缀和以及最大后缀和以及最大子串和
for (let i = 1; i < arr.length; i++) {
subSum = Math.max(0, subSum + arr[i]) % modulo;
prefix = prefix + arr[i];
postfix = postfix + arr[arr.length - i - 1];
if (subSum > subMax) {
subMax = subSum;
}
if (prefix > preMax) {
preMax = prefix
}
if (postfix > postMax) {
postMax = postfix
}
}
let max = 0;
if (k > 1) {
max = (postMax + preMax) % modulo;
if (prefix > 0 && k > 2) {
max = (max + prefix * (k - 2)) % modulo;
}
}
if(subMax*k % modulo == max) return max;//添加这个是因为leetcode增加了一个很大的数组
return max < subMax ? subMax : max;
};
插入排序算法
对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
// 插入排序改进:二分插入排序
function BinaryInsertSort(arr, len)
{
let key, left, right, middle;
for (let i=1; i<len; i++)
{
key = a[i];
left = 0;
right = i-1;
//通过二分法先确定待排元素要插入的位置,
//普通二分法每次比较后都要进行移位
while (left<=right)
{
middle = (left+right)/2;
if (a[middle]>key) right = middle-1;
else left = middle+1;
}
for(let j=i-1; j>=left; j--) a[j+1] = a[j];//将排序元素逐步向后挪
a[left] = key;
}
}