1、有序数组的平方
描述:给你一个按 非递减顺序
排序的整数数组 nums,返回每个数字的平方组成的新数组,要求也按 非递减顺序
排序。
示例:
示例 1:
输入:nums = [-4, -1, 0, 3, 10]
输出:[0, 1, 9, 16, 100]
解释:平方后,数组变为[16, 1, 0, 9, 100]
排序后,数组变为[0, 1, 9, 16, 100]
示例 2:
输入:nums = [-7, -3, 2, 3, 11]
输出:[4, 9, 9, 49, 121]
方法一:直接排序
思路与算法:
最简单的方法就是将数组 nums 中的数平方后直接排序。
代码:
var sortedSquares = function (nums) {
var newNums = [];
// 所有数依次平方
nums.forEach((value) => {
newNums.push(value * value);
});
// 排序
newNums = newNums.sort(function (a, b) {
return a - b;
});
// 返回排序后的数组
return newNums;
};
var nums = [-4, -1, 0, 3, 10];
var nums1 = [-7, -3, 2, 3, 11];
console.log(sortedSquares(nums)); // [0, 1, 9, 16, 100]
console.log(sortedSquares(nums1)); // [4, 9, 9, 49, 121]
复杂度分析:
- 时间复杂度:O(nlog n),其中 n 是数组 nums 的长度。
- 空间复杂度:O(log n)。除了存储答案的数组以外,我们需要O(log n) 的栈空间进行排序。
方法二:双指针
思路: 方法一没有利用「数组nums 已经按照升序排序」这个条件。
首先审题:(非严格)递增数组,求各数字平方后的(非严格)递增顺序。
思考:什么数的平方最小?什么数的平方最大?0的平方最小,绝对值最大的数平方最大。
那么由以上2个问题就可以产生2种解法,
- 第一种是找平方最大的数,即找绝对值最大的数,要么是最大正数,要么是最小负数。
更进一步的,我们可以思考:
- 如果全正,那么最大平方就是最右(最大正数);
- 如果全负,那么最大平方就是最左(最小负数);
- 如果有正有负,那么就是最左或者最右里绝对值较大的那个;
因此我们可以得出结论,不论正负,平方最大值一定会是最左和最右之一,这样就避免了对正负性的讨论。
现在采用第一种方法,我们可以使用两个指针分别指向位置 0 和 n−1,每次比较两个指针对应的数,选择较大的那个逆序放入答案并移动指针。这种方法无需处理某一指针移动至边界的情况,我们可以仔细思考其精髓所在。
代码:
// 解法2:找到平方后最大的数,从两头向中间遍历
// 无论数字的正负,平方之后最大的数,一定是最左(最小负数)或者最右(最大整数)之一
var sortedSquares = function (nums) {
var newNums = [];
// 左指针
var left = 0;
// 右指针
var right = nums.length - 1;
// newNums数组的指针,每次将最大的数写入数组的后面
var current = nums.length - 1;
while (left <= right) {
// 分别计算最左和最右的平方
var leftNum = nums[left] * nums[left];
var rightNum = nums[right] * nums[right];
// 取较大者
// 左侧较大时
if (leftNum >= rightNum) {
newNums[current] = leftNum;
// 左指针右移
left++;
} else { // 右侧较大时
newNums[current] = rightNum;
// 右指针左移
right--;
}
// newNums数组的指针左移
current--;
}
// 返回排序后的数组
return newNums;
};
var nums = [-4, -1, 0, 3, 10];
var nums1 = [-7, -3, 2, 3, 11];
console.log(sortedSquares(nums)); // [0, 1, 9, 16, 100]
console.log(sortedSquares(nums1)); // [4, 9, 9, 49, 121]
复杂度分析:
- 时间复杂度:O(n),其中 nn 是数组 nums 的长度。
- 空间复杂度:O(1)。除了存储答案的数组以外,我们只需要维护常量空间。
2、旋转数组
描述: 给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数,最后返回移动后的数组。
示例:
示例 1:
输入: nums = [1, 2, 3, 4, 5, 6, 7], k = 3
输出: [5, 6, 7, 1, 2, 3, 4]
解释:
向右旋转 1 步: [7, 1, 2, 3, 4, 5, 6]
向右旋转 2 步: [6, 7, 1, 2, 3, 4, 5]
向右旋转 3 步: [5, 6, 7, 1, 2, 3, 4]
示例 2:
输入:nums = [-1, -100, 3, 99], k = 2
输出:[3, 99, -1, -100]
解释:
向右旋转 1 步: [99, -1, -100, 3]
向右旋转 2 步: [3, 99, -1, -100]
思路:
我们可以使用额外的数组来将每个元素放至正确的位置。用 n 表示数组的长度,我们遍历原数组,将原数组下标为 i 的元素放至新数组下标为 (i+k) mod n
的位置,最后将新数组拷贝至原数组即可。
代码:
var rotate = function (nums, k) {
const n = nums.length;
const newNums = new Array(n);
for (let i = 0; i < n; i++) {
newNums[(i + k) % n] = nums[i];
}
for (let i = 0; i < n; i++) {
nums[i] = newNums[i];
}
return nums;
};
let nums1 = [1, 2, 3, 4, 5, 6, 7];
let k1 = 3;
console.log(rotate(nums1, k1)); // [5, 6, 7, 1, 2, 3, 4]
let nums2 = [-1, -100, 3, 99];
let k2 = 2;
console.log(rotate(nums2, k2)); // [3, 99, -1, -100]
复杂度分析:
- 时间复杂度:O(n),其中 n 为数组的长度。
- 空间复杂度:O(n)。
3、移动零
描述: 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
要求:
- 必须在原数组上操作,不能拷贝额外的数组。
- 尽量减少操作次数。
示例:
示例:
输入: [0, 1, 0, 3, 12]
输出: [1, 3, 12, 0, 0]
方法一: 使用两次遍历
思路:
- 第一次遍历,用 count 记录非零元素的个数,只要是非零的通通都赋值给 nums[count] 。
- 非零元素统计完了,剩下的都是 0 了,所以第二次遍历把末尾的元素都赋值为 0 即可。
代码:
var moveZeroes = function (nums) {
let count = 0; // 记录非零个数
// 第一次遍历,用 count 记录非零元素的个数,只要是非零的通通都赋值给 nums[count]
for (let i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
nums[count++] = nums[i];
}
}
// 非零元素统计完了,剩下的都是 0 了,所以第二次遍历把末尾的元素都赋值为 0 即可
for (let i = count; i < nums.length; i++) {
nums[i] = 0;
}
// 返回移动后的数组
return nums;
};
const nums = [0, 1, 0, 3, 12];
console.log(moveZeroes(nums)); // [1, 3, 12, 0, 0]
复杂度分析:
- 时间复杂度:O(n),其中 n 为序列长度。
- 空间复杂度:O(1)。只需要常数的空间存放若干变量。
方法二: 使用双指针
思路及解法:
使用双指针,左指针指向当前已经处理好的序列的尾部,即非零元素指针,右指针指向待处理序列的头部。右指针不断向右移动,即遍历数组,每次右指针指向非零数,则将左右指针对应的数交换,同时左指针右移。
注意到以下性质:
- 左指针左边均为非零数;
- 右指针左边直达左指针处均为零。
因为每次交换,都是将左指针的零与右指针的非零数交换,且非零数的相对顺序并未改变。
代码:
var moveZeroes = function (nums) {
// 左指针 即非零元素指针
let left = 0;
// 右指针 即遍历指针
let right = 0;
while (right < nums.length) {
if (nums[right] != 0) {
let temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
}
right++;
}
// 返回移动后的数组
return nums;
};
const nums = [0, 1, 0, 3, 12];
console.log(moveZeroes(nums)); // [1, 3, 12, 0, 0]
// 代码运行过程如下:
// left = 0, right = 0 [0, 1, 0, 3, 12]
// left = 0, right = 1 [1, 0, 0, 3, 12]
// left = 1, right = 2 [1, 0, 0, 3, 12]
// left = 1, right = 3 [1, 3, 0, 0, 12]
// left = 2, right = 4 [1, 3, 12, 0, 0]
// 循环结束
复杂度分析:
- 时间复杂度:O(n),其中 n 为序列长度。每个位置至多被遍历两次。
- 空间复杂度:O(1)。只需要常数的空间存放若干变量。
4、两数之和——输入有序数组
描述: 给定一个已按照 升序排列
的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。
函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length 。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例:
示例 1:
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
示例 2:
输入:numbers = [2,3,4], target = 6
输出:[1,3]
示例 3:
输入:numbers = [-1,0], target = -1
输出:[1,2]
利用输入数组有序的性质,有以下两种方法:
方法一:二分查找
思路:
在数组中找到两个数,使得它们的和等于目标值,可以首先固定第一个数,然后寻找第二个数,第二个数等于目标值减去第一个数的差。利用数组的有序性质
,可以通过二分查找的方法寻找第二个数。为了避免重复寻找,在寻找第二个数时,只在第一个数的右侧寻找。
代码:
// 解法1:二分查找
var twoSum = function (numbers, target) {
for (let i = 0; i < numbers.length; i++) {
let low = i + 1; // 在寻找第二个数时,只在第一个数的右侧寻找
let high = numbers.length - 1;
while (low <= high) {
let mid = Math.floor(low + (high - low) / 2);
if (numbers[mid] == target - numbers[i]) {
return [i + 1, mid + 1];
} else if (numbers[mid] > target - numbers[i]) {
high = mid - 1;
} else {
low = mid + 1;
}
}
}
return [-1, -1];
};
let numbers = [2, 7, 11, 15];
let target = 9;
console.log(twoSum(numbers, target)); // [1, 2]
let numbers1 = [1, 2, 3, 4, 4, 9, 56, 90];
let target1 = 8;
console.log(twoSum(numbers1, target1)); // [4, 5]
复杂度分析:
- 时间复杂度:O(nlog n),其中 n 是数组的长度。需要遍历数组一次确定第一个数,时间复杂度是 O(n),寻找第二个数使用二分查找,时间复杂度是 O(log n),因此总时间复杂度是 O(nlog n)。
- 空间复杂度:O(1)。
方法二:使用双指针
思路:
初始时两个指针分别指向第一个元素位置和最后一个元素的位置。每次计算两个指针指向的两个元素之和,并和目标值比较。
- 如果两个元素之和等于目标值,则发现了唯一解。
- 如果两个元素之和小于目标值,则将左侧指针右移一位。
- 如果两个元素之和大于目标值,则将右侧指针左移一位。
- 移动指针之后,重复上述操作,直到找到答案。
由于题目确保有唯一的答案,因此使用双指针一定可以找到答案。
代码:
// 解法2:双指针
var twoSum = function (numbers, target) {
let low = 0; // 第一个元素的位置
let high = numbers.length - 1; // 最后一个元素的位置
while (low < high) {
let sum = numbers[low] + numbers[high];
if (sum == target) {
return [low + 1, high + 1];
} else if (sum < target) {
low++;
} else {
high--;
}
}
return [-1, -1];
}
let numbers = [2, 7, 11, 15];
let target = 9;
console.log(twoSum(numbers, target)); // [1, 2]
let numbers1 = [1, 2, 3, 4, 4, 9, 56, 90];
let target1 = 8;
console.log(twoSum(numbers1, target1)); // [4, 5]
复杂度分析:
- 时间复杂度:O(n),其中 n 是数组的长度。两个指针移动的总次数最多为 n 次。
- 空间复杂度:O(1)。
5、反转字符串
描述: 编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改
输入数组、使用 O(1) 的额外空间解决这一问题。
你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
示例:
示例 1:
输入:["h", "e", "l", "l", "o"]
输出:["o", "l", "l", "e", "h"]
示例 2:
输入:["H", "a", "n", "n", "a", "h"]
输出:["h", "a", "n", "n", "a", "H"]
方法: 使用双指针
代码:
var reverseString = function (s) {
let left = 0;
let right = s.length - 1;
while (left <= right) {
// 数组解构赋值
[s[left], s[right]] = [s[right], s[left]];
left++;
right--;
}
return s;
};
let arr = ["h", "e", "l", "l", "o"];
console.log(reverseString(arr)); // ["o", "l", "l", "e", "h"]
let arr1 = ["H", "a", "n", "n", "a", "h"];
console.log(reverseString(arr1)); // ["h", "a", "n", "n", "a", "H"]
复杂度分析:
- 时间复杂度:O(N),其中 N 为字符数组的长度。一共执行了 N/2 次的交换。
- 空间复杂度:O(1)。只使用了常数空间来存放若干变量。
6、反转字符串中的单词
描述: 给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。
示例:
示例:
输入:"Let's take LeetCode contest"
输出:"s'teL ekat edoCteeL tsetnoc"
思路: 使用额外空间
开辟一个新字符串。然后从头到尾遍历原字符串,直到找到空格为止,此时找到了一个单词,并能得到单词的起止位置。随后,根据单词的起止位置,可以将该单词逆序放到新字符串当中。如此循环多次,直到遍历完原字符串,就能得到翻转后的结果。
需要注意的是,原地解法在某些语言(比如 Java,JavaScript)中不适用,因为在这些语言中 String 类型是一个不可变的类型。
代码:
var reverseWords = function (s) {
const arr = [];
let i = 0;
while (i < s.length) {
let start = i;
while (i < s.length && s.charAt(i) != ' ') {
i++;
}
for (let p = start; p < i; p++) {
arr.push(s.charAt(start + i - 1 - p));
}
while (i < s.length && s.charAt(i) == ' ') {
i++;
arr.push(' ');
}
}
return arr.join('');
};
let str = "Let's take LeetCode contest";
console.log(reverseWords(str)); // s'teL ekat edoCteeL tsetnoc
复杂度分析:
- 时间复杂度:O(N),其中 N 为字符串的长度。原字符串中的每个字符都会在 O(1) 的时间内放入新字符串中。
- 空间复杂度:O(N)。我们开辟了与原字符串等大的空间。
7、链表的中间结点
描述: 给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
示例:
示例 1:
输入:[1, 2, 3, 4, 5]
输出:此列表中的结点 3(序列化形式:[3, 4, 5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是[3, 4, 5]) 。
注意,我们返回了一个 ListNode 类型的对象 ans,
这样:ans.val = 3, ans.next.val = 4, ans.next.next.val = 5,
以及 ans.next.next.next = NULL.
示例 2:
输入:[1, 2, 3, 4, 5, 6]
输出:此列表中的结点 4(序列化形式:[4, 5, 6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
方法一:数组
思路和算法:
链表的缺点在于不能通过下标访问对应的元素。因此我们可以考虑对链表进行遍历,同时将遍历到的元素依次放入数组 A 中。如果我们遍历到了 N 个元素,那么链表以及数组的长度也为 N,对应的中间节点即为 A[N/2]。
代码:
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val)
this.next = (next === undefined ? null : next)
}
// 方法一:数组
var middleNode = function (head) {
let A = [head];
while (A[A.length - 1].next != null) {
A.push(A[A.length - 1].next);
}
// Math.trunc() 方法会将数字的小数部分去掉,只保留整数部分。
return A[Math.trunc(A.length / 2)];
};
复杂度分析:
- 时间复杂度:O(N),其中 N 是给定链表中的结点数目。
- 空间复杂度:O(N),即数组 A 用去的空间。
方法二:单指针法
思路和算法:
我们可以对方法一进行空间优化,省去数组 A。
我们可以对链表进行两次遍历。第一次遍历时,我们统计链表中的元素个数 N;第二次遍历时,我们遍历到第 N/2 个元素(链表的首节点为第 0 个元素)时,将该元素返回即可。
代码:
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val)
this.next = (next === undefined ? null : next)
}
// 方法二:单指针法
var middleNode = function (head) {
let n = 0;
let current = head;
// 计算链表中元素的个数
while (current != null) {
n++;
current = current.next;
}
let k = 0;
current = head;
while (k < Math.trunc(n / 2)) {
k++;
current = current.next;
}
return current;
};
复杂度分析:
- 时间复杂度:O(N),其中 N 是给定链表的结点数目。
- 空间复杂度:O(1),只需要常数空间存放变量和指针。
方法三:快慢指针法
思路和算法:
我们可以继续优化方法二,用两个指针 slow 与 fast 一起遍历链表。slow 一次走一步,fast 一次走两步。那么当 fast 到达链表的末尾时,slow 必然位于中间。
代码:
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val)
this.next = (next === undefined ? null : next)
}
// 方法三:快慢指针法
var middleNode = function (head) {
let slow = head;
let fast = head;
// 当 fast 走到末尾,slow 则走到了中间位置
while (fast && fast.next) {
slow = slow.next; // 走一步
fast = fast.next.next; // 走两步
}
return slow;
};
复杂度分析:
- 时间复杂度:O(N),其中 NN 是给定链表的结点数目。
- 空间复杂度:O(1),只需要常数空间存放 slow 和 fast 两个指针。
8、删除链表的倒数第N个结点
描述: 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
示例 3:
输入:head = [1,2], n = 1
输出:[1]
方法一:使用双指针
思路和算法:
我们可以设想假设设定了双指针 slow 和 fast 的话,当 fast 指向末尾的 NULL,slow 与 fast 之间相隔的元素个数为 n 时,那么删除掉 slow 的下一个指针就完成了要求。
- 设置虚拟节点 dummyHead 指向 head。
- 设定双指针 slow 和 fast,初始都指向虚拟节点 dummyHead。
- 移动 fast,直到 slow 和 fast 之间相隔的元素个数为 n。
- 同时移动 slow 和 fast,直到 fast 指向的为 NULL。
- 将 slow 的下一个节点指向下下个节点(即删除了倒数第N个结点)。
代码:
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val)
this.next = (next === undefined ? null : next)
}
var removeNthFromEnd = function (head, n) {
// 设置虚拟节点dummy,指向head
let dummy = new ListNode(0, head);
let slow = dummy;
let fast = head;
// 移动fast,直到 fast 和 slow 之间相隔n个元素
for (let i = 0; i < n; i++) {
fast = fast.next;
}
// 同时移动 slow 和 fast, 直到 fast 指向 null
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
// slow 此时指向要删除的节点的前一个节点
// slow 的下一个节点指向下下个节点,删除节点
slow.next = slow.next.next;
// 让 ans 指向 head
let ans = dummy.next;
// 返回链表
return ans;
};
复杂度分析:
- 时间复杂度:O(L),其中 L 是链表的长度。
- 空间复杂度:O(1)。