T21 合并两个有序链表
题目链接:leetcode-cn.com/problems/me…
哨兵技巧
这是一道基于「单向链表」的「模拟」题,做这道题我们主要的目的是复习「哨兵技巧」的运用。忘记的同学请戳这里。
题目比较简单,需要注意的地方有两点:①对传入空链表的特殊化处理;②while循环中if语句条件的设计。
我们直接过一遍代码:
function mergeTwoLists(L1, L2) {
if (L1 === null) return L2;
if (L2 === null) return L1;
let ans = new ListNode(); //创建哨兵节点
let tempNode = ans;
while (L1 !== null || L2 !== null) {
if(L2 !== null && (L1 === null || L1.val >= L2.val)) {
tempNode = tempNode.next = L2;
L2 = L2.next;
} else {
tempNode = tempNode.next = L1;
L1 = L1.next;
}
}
return ans.next;
}
T26 删除有序数组中的重复项
题目链接:leetcode-cn.com/problems/re…
双指针法
朴素解法:先扫再移
为了便于入手,我们先不考虑优化,直接从最简单的想法开始:
- 定义双指针
i和j,并通过指针i对数组元素进行枚举;- 令
j的初值为j = i + 1,通过j寻找满足nums[j] ≠ nums[i]的第一个位置;- 定义移动指针
k并令其初值为k = i + 1,使用k将后方数组元素向前移动,移动间隔delta = j - i - 1.
代码如下:
function removeDuplicates(nums) {
let i = 0;
let border = nums.length;
while(i < border) {
let num = nums[i];
/* 搜索最后一位与nums[i]重复的数字的位置 */
let j = i + 1;
while(j < border && nums[j] === num) {
j++;
}
/* 数组元素移位 */
let delta = j - i - 1;
border -= delta;
for(let k = i + 1; k < border; k++) {
nums[k] = nums[k + delta];
}
/* 更新i,进入下一轮循环以校验下一位元素是否重复 */
i++;
}
return (nums.length = border);
}
Tip: 实际上,题目中并没有要求我们删除多余的元素,因此最后的返回语句直接写成
return border;就行了。但出于便于调试的目的,我们这里仍然保留对有效区间外元素的删除操作,下文的代码我们也将进行这样的操作。
优化解法:边扫边改(双指针法)
前述的「朴素解法」中我们使用了三个循环来实现题目要求,比较耗时。能不能用更少的循环进行解答?
答案是肯定的。我们可以这么考虑:
首先,无论如何去重,数组nums中的第一位nums[0]肯定是保持不动的,我们对数组的遍历只需要从i = 1开始即可。
然后为了理解方便,我们不妨引入一个假想的目标数组N,并且令指针j始终指向N的末端,以便于我们向N中写入数据。我们称j为「写入位置」。现在,我们只需要将原数组nums中需要保留的元素依次写入N[j]中,再让j向右移动即可。
如何知道哪些元素是需要保留的呢?别忘了,我们题目的要求可是要给原数组nums去重。同时也很重要的是,它可是有序数组!也就是说,如果原数组中出现重复的数字,那么它们肯定是挨在一起的。
现在你应该明白过来了。
是的,如果原数组中出现重复的数字,那么当它们中的第一个数字被写入N[j]后,当指针i指向它们中的第二个数字时,一定会有nums[i] = N[j - 1](现在的j已经往后移动了)。因此我们的任务便是阻止当这种情况发生时,N[j]被更新成nums[i]。
但现在你的心中一定有疑惑:题目中不允许我们使用额外的空间,这个算法还是不可行啊!
事实上,把上文中的N全部替换成nums就行了。很容易理解:指针j不可能超越指针i。因此它们俩的工作并不会发生冲突,我们可以实现对原数组「边扫边改」的效果!
代码如下:
function removeDuplicates(nums) {
let j = 1;
for(let i = 1; i < nums.length; i++) {
if (nums[i] !== nums[j - 1]) {
nums[j++] = nums[i];
}
}
//这个结论是容易归纳的,请自己尝试一下!
return (nums.length = j);
}
这种「优化解法」的思想本源与前面我们介绍的「朴素解法」是类似的。
我们可以这么理解:我们只是在前面的基础上将「扫描」(遍历数组检查重复)和「移动」(在这里是对有效区间内元素的逐个修改)两个步骤合二为一,将三个循环缩减到了一个循环,实现了时间的节约。
拓展:最多允许重复k次
下面我们对原题进行一般化的推广:给定存在重复元素的有序数组,使得数组中的元素最多允许重复k次。
朴素解法:先扫再移
同样地,我们先从「朴素解法」入手。
"最多允许重复k次的问题",本质上还是数组的移位问题。容易想到的办法,仍然是确定连续数字的区间,然后分情况讨论是否要进行位移。
实现这个思路只需要对原题的「朴素解法」代码进行修改即可。
修改后的代码如下:
function removeDuplicates(nums, k) {
let i = 0;
let border = nums.length;
while(i < border) {
let num = nums[i];
let j = i + 1;
while(j < border && nums[j] === num) {
j++;
}
if(j - i + 1 > k) { //如果区间长度超过k,则需要进行移位
let delta = j - (i + k);
border -= delta;
for(let m = i + k; m < border; m++) {
nums[m] = nums[m + delta];
}
i = i + k;
} else { //如果区间长度没有超过k,直接让i跳转到下一区间即可
i = j;
}
}
return (nums.length = border);
}
优化解法:边扫边改
初次尝试(多指针法)
同样地,下面让我们来探索如何实现「边扫边改」的优化解法。下面是一个比较容易入手的想法:
首先我们可以模仿「删除有序数组中的重复项」中的「双指针法」引入指针i、j,再引入指针p用于标记首次出现当前数字的数组下标。nums[p]便是我们遍历数组过程中的参照对象。
我们通过指针i逐位遍历数组,对nums[i]和nums[j]进行比较,之后的操作需要进入如下的分类讨论:
- 若当前数字
nums[i]与比较数字nums[p]相同,且当前数字总数不超过k,则直接写入nums[i]并更新i、j;- 若当前数字
nums[i]与比较数字nums[p]相同,且当前数字总数超过k,则先暂时保持j和p不动,只更新i以检查下一个数字;- 若当前数字
nums[i]与比较数字nums[p]不同,则表示出现新数字. 令p = i以更新参照对象,同时写入新数字nums[i]并更新i、j.
同时类似于原题中运用「双指针法」解题时我们直接保留了nums[0],在这里由于我们要保留k个相同数字,那么对于原数组前k个数字,我们一定可以直接保留。进而我们可以进一步压缩遍历的次数。
代码如下:
function removeDuplicates(nums, k) {
let p = 0; //记录比较位置
let j = k; //记录写入位置
for(let i = k; i < nums.length; i++) { //遍历数组进行逐位检查
if(nums[i] === nums[p] && i - p < k) {
nums[j++] = nums[i];
}
else if (nums[i] !== nums[p]) {
p = i;
nums[j++] = nums[i];
}
}
return (nums.length = j);
}
再次改进(双指针法)
事实上,我们还可以再次改进前述的解法,使之成为真正的「双指针法」。因为即使没有比较指针p,我们也可以实现相同的功能。
下面请你自行归纳:对于任意数组元素nums[i](其中i ≥ k),当该元素与nums[j - k]不相同时,则需要保留(即令nums[j] = nums[i]并更新j),反之则需要放弃。
代码如下:
function removeDuplicates(nums, k) {
let j = k; //记录写入位置
for(let i = k; i < nums.length; i++) { //遍历数组进行逐位检查
if(nums[i] !== nums[j - k]) {
nums[j++] = nums[i];
}
}
return (nums.length = j);
}
对比最初的「朴素解法」,现在我们只需要两个变量i、j外加一次循环就能实现相同的功能,不得不感叹算法的神奇!
T27 移除元素
题目链接:leetcode-cn.com/problems/re…
双指针法
本题本质上是前面的「删除有序数组中的重复项」的变式。由于朴素解法的代码编写较为繁琐,让我们来直接参照前面的优化解法(「双指针法」)进行解题。
代码如下:
function removeElement(nums, val) {
let j = 0;
for(let i = 0; i < nums.length; i++) {
if(nums[i] !== val) {
nums[j++] = nums[i];
}
}
return (nums.length = j);
}
走到这里,相信你已经发现了:题目中所谓的"元素的顺序可以改变"其实只是一个误导。只要我们把握了「双指针法」解「数组内元素删除问题」的精髓——通过指针i进行数组的遍历和数据校验,并通过另一个指针j标记数据的写入位置,这类题型就都可以轻松解决。
写在文末
我是来自在校学生编程兴趣小组江南游戏开发社的PAK向日葵,我们目前正在致力于开发自研的非营利性网页端同人游戏《植物大战僵尸:旅行》,以锻炼我们的前端应用开发能力。
我们诚挚邀请您体验我们的这款优秀作品,如果您喜欢TA的话,欢迎向您的同事和朋友推荐。如果您有技术方面的问题希望与我们探讨,欢迎直接与我联系。您的支持是我们最大的动力!