反转链表
迭代
时间复杂度:O(n),n是链表的长度。需要遍历链表一次。
空间复杂度:O(1)。
在遍历链表时,将当前节点的next指针改为向前一个节点。 (节点没有引用其前一个节点,因此事先存储其前一个节点。更改引用前需存储后一个节点) 最后返回新的头引用。
var reverseList = function(head) {
let prev = null;
let curr = head;
while(curr){
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
};
递归
假设链表为:
若从节点到已经被反转,而我们正处于。
我们希望的下一个节点指向。所以,
需要注意的是的下一个节点必须指向。如果忽略了这一点,链表中可能会产生环。
var reverseList = function(head){
if(head == null || head.next == null){
return head;
}
const newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
最大子序和
动态规划法
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;
};
分治(线段树)
对于一个区间 [l,r],我们可以维护四个量:
表示 [l,r] 内以l为左端点的最大子段和
表示 [l,r] 内以r为右端点的最大子段和
表示 [l,r] 内的最大子段和
表示 [l,r] 的区间和 就等于「左子区间」的 加上「右子区间」的
对于 [l,r]的 ,存在两种可能,它要么等于「左子区间」的 ,要么等于「左子区间」的 加上「右子区间」的 ,二者取大。
对于 [l,r]的 ,同理,它要么等于「右子区间」的 ,要么等于「右子区间」的 加上「左子区间」的 ,二者取大。
当计算好上面的三个量之后,就很好计算 [l,r]的 了。我们可以考虑 [l,r] 的 对应的区间是否跨越 m。
它可能不跨越 m,也就是说 [l,r]的 可能是「左子区间」的 和 「右子区间」的 中的一个;
它也可能跨越 m,可能是「左子区间」的 和 「右子区间」的 求和。三者取大。
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;
};
合并两个有序链表
递归
list1[0]+merge(list1[1:],list2) list1[0]<list2[0]
list2[0]+merge(list1,list2[1:]) otherwise
两个链表头部值较小的一个节点与剩下元素的 merge 操作结果合并。
如果 l1 或者 l2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 l1 和 l2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。
var mergeTwoLists = function(l1,l2) {
if(l1 == null){
return l2;
}else if (l2 == null){
return l1;
}else if (l1.val < l2.val){
l1.next = mergeTwoLists(l1.next, l2);
}else{
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
};
时间复杂度:O(n + m),其中 n 和 m 分别为两个链表的长度。因为每次调用递归都会去掉 l1 或者 l2 的头节点(直到至少有一个链表为空),函数 mergeTwoList 至多只会递归调用每个节点一次。因此,时间复杂度取决于合并后的链表长度,即 O(n+m)。
空间复杂度:O(n + m),其中 n 和 m 分别为两个链表的长度。递归调用 mergeTwoLists 函数时需要消耗栈空间,栈空间的大小取决于递归调用的深度。结束递归调用时 mergeTwoLists 函数最多调用 n+m 次,因此空间复杂度为 O(n+m)。
迭代
首先,我们设定一个哨兵节点 prehead ,这可以在最后让我们比较容易地返回合并后的链表。我们维护一个 prev 指针,我们需要做的是调整它的 next 指针。然后,我们重复以下过程,直到 l1 或者 l2 指向了 null :如果 l1 当前节点的值小于等于 l2 ,我们就把 l1 当前的节点接在 prev 节点的后面同时将 l1 指针往后移一位。否则,我们对 l2 做同样的操作。不管我们将哪一个元素接在了后面,我们都需要把 prev 向后移一位。
在循环终止的时候, l1 和 l2 至多有一个是非空的。由于输入的两个链表都是有序的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。这意味着我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。
var mergeTwoLists = function(l1,l2){
const prehead = new ListNode(-1);
let prev = prehead;
while (l1 != null && l2 != null){
if(l1.val <= l2.val){
prev.next = l1;
l1 = l1.next;
}else{
prev.next = l2;
l2 = l2.next;
}
prev = prev.next;
}
prev.next = l1 === null? l2 : l1;
return prehead.next;
};
时间复杂度:O(n + m)O(n+m),其中 nn 和 mm 分别为两个链表的长度。因为每次循环迭代中,l1 和 l2 只有一个元素会被放进合并链表中, 因此 while 循环的次数不会超过两个链表的长度之和。所有其他操作的时间复杂度都是常数级别的,因此总的时间复杂度为 O(n+m)O(n+m)。
空间复杂度:O(1)O(1)。我们只需要常数的空间存放若干变量。
合并两个有序数组
直接合并后排序
var merge = function(nums1, m, nums2, n) {
nums1.splice(m, nums1.length - m, ...nums2);
nums1.sort((a, b) => a - b);
};
时间复杂度:O(log(m+n))。 排序序列长度为 m+n,套用快速排序的时间复杂度即可,平均情况为 O(log(m+n))。
空间复杂度:O(log(m+n))。 排序序列长度为 m+n,套用快速排序的空间复杂度即可,平均情况为 O(log(m+n))。
双指针
两个数组看作队列,每次从两个数组头部取出比较小的数字放到结果中。
var merge = function(nums1, m, nums2, n) {
let p1 = 0, p2 = 0;
const sorted = new Array(m + n).fill(0);
var cur;
while (p1 < m || p2 < n) {
if (p1 === m) {
cur = nums2[p2++];
} else if (p2 === n) {
cur = nums1[p1++];
} else if (nums1[p1] < nums2[p2]) {
cur = nums1[p1++];
} else {
cur = nums2[p2++];
}
sorted[p1 + p2 - 1] = cur;
}
for (let i = 0; i != m + n; ++i) {
nums1[i] = sorted[i];
}
};
时间复杂度:O(m+n)。 指针移动单调递增,最多移动 m+n 次,因此时间复杂度为 O(m+n)。
空间复杂度:O(m+n)。 需要建立长度为 m+n 的中间数组sorted。
逆向双指针
在此遍历过程中的任意一个时刻,数组中有 个元素被放入 的后半部,数组中有个元素被放入 的后半部,而在指针 的后面,数组有个位置。由于
等价于 永远成立,因此 后面的位置永远足够容纳被插入的元素,不会产生 的元素被覆盖的情况。
var merge = function(nums1, m, nums2, n){
let p1 = m - 1, p2 = n - 1;
let tail = m + n - 1;
var cur;
while(p1 >= 0 || p2 >= 0){
if(p1 === -1){
cur = nums2[p2--];
} else if (p2 === -1){
cur = nums1[p1--];
} else if (nums1[p1] > nums2[p2]){
cur = nums1[p1--];
} else {
cur = nums2[p2--];
}
nums1[tail--] = cur;
}
};
时间复杂度:O(m+n)。指针移动单调递减,最多移动 m+n 次,因此时间复杂度为 O(m+n)。
空间复杂度:O(1)。直接对数组原地修改,不需要额外空间。
两数之和
暴力枚举
最容易想到的方法是枚举数组中的每一个数 x,寻找数组中是否存在 target - x。
当我们使用遍历整个数组的方式寻找 target - x 时,需要注意到每一个位于 x 之前的元素都已经和 x 匹配过,因此不需要再进行匹配。而每一个元素不能被使用两次,所以我们只需要在 x 后面的元素中寻找 target - x。
ver twoSum = function(nums, target){
const sorted = new Array(2).fill(0);
var n = nums.length;
for(let i = 0; i < n; ++i){
for(let j = i + 1; j < n; ++j){
if(nums[i] + nums[j] == target){
sorted[0] = i;
sorted[1] = j;
return sorted
}
}
}
};
时间复杂度: ),其中 N 是数组中的元素数量。最坏情况下数组中任意两个数都要被匹配一次。
空间复杂度:O(1)。
哈希表
注意到方法一的时间复杂度较高的原因是寻找 target - x 的时间复杂度过高。因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。
使用哈希表,可以将寻找 target - x 的时间复杂度降低到从 O(N) 降低到 O(1)。
这样我们创建一个哈希表,对于每一个 x,我们首先查询哈希表中是否存在 target - x,然后将 x 插入到哈希表中,即可保证不会让 x 和自己匹配。
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; ++i) {
if (hashtable.containsKey(target - nums[i])) {
return new int[]{hashtable.get(target - nums[i]), i};
}
hashtable.put(nums[i], i);
}
return new int[0];
}
}