双指针是解数组类型题的最常见解法。
- 比如有头尾分别有指针,然后依次向中间靠拢
- 还有一种是快慢指针,两个指针都从左边起,一个走的快,一个走得慢
删除数组中的重复项
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地修改输入数组 并在使用 O(1) 额外空间的条件下完成。
说明:为什么返回数值是整数,但输出的答案是数组呢?请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。你可以想象内部操作如下:// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝int len = removeDuplicates(nums);// 在函数里修改输入数组对于调用者是可见的。// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。for (int i = 0; i < len; i++) {print(nums[i]);}
示例 1:输入:nums = [1,1,2]输出:2, nums = [1,2]解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。示例 2:输入:nums = [0,0,1,1,1,2,2,3,3,4]输出:5, nums = [0,1,2,3,4]解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
这题在题目要求上有限制,就是你可以改变nums里的元素的值,却不能对nums进行重新赋值。
而且返回的数字n,就代表了 nums 前n项,也就是数组里所有的不重复项。
// 如果没有不允许对nums赋值的要求,可以用for循环// 两轮循环,相同元素则把元素改为undefined,不同元素则更新i的值var removeDuplicates = function(nums) {for (let i = 0; i < nums.length; i++) {for (let j = i+1; j < nums.length; j++) {if (nums[i] === nums[j]) {nums[j] = undefined} else {i = jbreak}}}nums = nums.filter(e => e) // 过滤掉undefined的元素 (可惜不满足题意)return nums.length};
这道题目的就是让你,把数组的前n位元素,改成数组里所有不重复的元素值。
双指针法:
让两个指针快慢不同的向右移动
- 慢指针是i,快指针是j
- 如果nums[i] 等于 nums[j] 说明是相同的元素,j继续走,i还在原位
- 如果nums[i] 不等于 nums[j] 说明是不相同的元素,那么nums[i++] = nums[j],j继续向前走
/*** @param {number[]} nums* @return {number}*/var removeDuplicates = function (nums) {let i = 0;for (let j = 1; j < nums.length; j++) {if (nums[j] !== nums[i]) {nums[++i] = nums[j] // 先+再用。因为不是要更新nums[i], 而是要更新当前位的下一位。同时i也要递增1}}return i+1 // length,要加1}
合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
示例 1:输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3输出:[1,2,2,3,5,6]解释:需要合并 [1,2,3] 和 [2,5,6] 。合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。示例 2:输入:nums1 = [1], m = 1, nums2 = [], n = 0输出:[1]解释:需要合并 [1] 和 [] 。合并结果是 [1] 。示例 3:输入:nums1 = [0], m = 0, nums2 = [1], n = 1输出:[1]解释:需要合并的数组是 [] 和 [1] 。合并结果是 [1] 。注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。
方法一: 首先用js的排序肯定是可以的, 但是排序算法的时间复杂度会比较高
var merge = function(nums1, m, nums2, n) {nums1.splice(m, nums.length-1, ...nums2) // 把nums1 后面的0 切割掉,换成nums2里的元素nums1.sort((a, b) => a - b)}
方法二:新建一个数组,然后分别比较两个数组元素的大小。
我自己写的这个其实用的也是双指针法:
/*** @param {number[]} nums1* @param {number} m* @param {number[]} nums2* @param {number} n* @return {void} Do not return anything, modify nums1 in-place instead.*/var merge = function(nums1, m, nums2, n) {const arr = []let i = 0;let j = 0;while (i < m || j < n) { // 只要没有全都走完,就继续if (i >= m) { // nums1 m之后的元素都是占位元素,不参与比较arr.push(nums2[j])j++continue // 跳出本次循环,继续下次循环}if (j >= n) {arr.push(nums1[i])i++continue}// 谁小谁的指针向右移if (nums1[i] > nums2[j]) {arr.push(nums2[j])j++} else {arr.push(nums1[i])i++}}// arr就是排序后的数组// 题目要求是直接改变 nums1,所以替换nums1 元素for (let n = 0; n < arr.length; n++) {nums1[n] = arr[n]}};
上面的方法里还是使用了临时变量,因为如果直接操作nums1,那nums1中元素会在取值前被覆盖。其实这题里nums1 后面一部分的元素是用来占位的,就是用来存放元素的。
仍旧使用双指针法,但是从尾部开始向头部进行遍历
// 双指针法,从后往前var merge = function(nums1, m, nums2, n) {let i = m - 1;let j = n - 1;let p = nums1.length - 1while (p >= 0) {if (i < 0) {nums1[p--] = nums2[j--] // 注意这里的p-- 和 j--, 偷懒写法,先使用p的值, 然后p = p - 1continue // 记得要跳出本次循环}if (j < 0) {nums1[p--] = nums1[i--]continue}if (nums1[i] > nums2[j]) {nums1[p--] = nums1[i--]} else {nums1[p--] = nums2[j--]}}}
验证回文串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
示例 1:输入: "A man, a plan, a canal: Panama"输出: true解释:"amanaplanacanalpanama" 是回文串示例 2:输入: "race a car"输出: false解释:"raceacar" 不是回文串
解题思路就是俩指针从两边向中间靠拢。注意要把输入的字符串处理下。
/*** @param {string} s* @return {boolean}*/var isPalindrome = function(s) {const str = s.replace(/(\W|_)/g, '').toLowerCase()let p1 = 0let p2 = str.length - 1while(p2 >= p1) {if (str[p1] !== str[p2]) return falsep1++p2--}return true};
回文链表
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
示例 1:
输入:head = [1,2,2,1] 输出:true
示例 2:
输入:head = [1,2] 输出:false
进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
/*** @param {ListNode} head* @return {boolean}*/// 方法一,将链表变数组,判断数组是否是对称的var isPalindrome = function(head) {const arr = []while (head) {arr.push(head.val)head = head.next}let p1 = 0let p2 = arr.length - 1while (p1 <= p2) {if (arr[p1] !== arr[p2]) return falsep1++p2--}return true};
这种方法开辟了个数组,空间复杂度O(n),这题也可以使用快慢指针的方式。
首先,我们要找到链表的中间点,然后对后半部分进行反转。再比较前半部分和反转后的部分是否节点值一致。
那么该如何找到中间节点呢?
可以遍历完链表,得知链表中有多少个个节点,记录下,然后一直next到中间节点的位置。这样有点麻烦。
为了获取中间节点,可以使用两个指针,一个一次走一步,一个一次走两步,这样当走两步的到达结尾(不一定是最后一个节点)时,走一步的正好在中间位置。 (同样的方法,可以获取链表或数组 的 1/2、3/1、1/4位置的节点)
// 获取中间节点const getMidNode = function (head) {let fast = headlet slow = headwhile(fast.next !== null && fast.next.next !== null) { // 注意这里的判断,fast.next 和 fast.next.next存在,保证fast还没有到结尾。有fast.next 没有fast.next.next,说明是偶数个节点的链表fast = fast.next.nextslow = slow.next}return slow}
注意链表这种结构的特征,每一个next,都是相当于后面整个链表的。 next -> next -> next -> …… next里存放的是下一个节点对象的内存地址。
如果说 链表1中某个节点 === 链表2中的某个节点, 说明这俩节点后的链表是同一条。就像车道汇合。
拿到了链表的中间节点, 那么就从中间节点开始,反转从中间节点之后的链表部分。 反转链表之前有过的
反转链表这个其实也像是快慢指针的形式
// 反转链表const reverseList = function (listNode) {let prev = nulllet head = listNodewhile (head) {const next = head.nexthead.next = prevprev = head // 往后移head = next // 往后移}return prev}
现在已经得到了反转后的链表了,那就开始比较两个链表的节点值是不是相等。
注意,这里比较的是原始链表,和 后半部分反转后的链表,只需要当遍历完后半部分就可以结束了。
var compare = function (n1, n2) {while (n2) {if (n1.val !== n2.val) return falsen1 = n1 ? n1.next : nulln2 = n2 ? n2.next : null}return true}
因为前面我们改变过原链表的后半部分,最好还是还原一下
var isPalindrome = function(head) {if (head === null) return trueconst halfListNode = getMidNode(head)const lastHafListNode = reverseList(halfListNode.next)const result = compare(head, lastHafListNode) // 先执行halfListNode.next = reverseList(lastHafListNode) // 再还原原链表return result}
所以完整流程就是
进阶版
// 1. 获取中间节点。 知识点: 快慢指针找中间点const getMidNode = function (head) {let fast = headlet slow = headwhile(fast.next !== null && fast.next.next !== null) { // 注意这里的判断,fast.next 和 fast.next.next存在,保证fast还没有到结尾。有fast.next 没有fast.next.next,说明是偶数个节点的链表fast = fast.next.nextslow = slow.next}return slow}// 2. 反转链表。 知识点: 双指针const reverseList = function (listNode) {let prev = nulllet head = listNodewhile (head) {const next = head.nexthead.next = prevprev = head // 往后移head = next // 往后移}return prev}// 3. 俩链表比较。知识点:迭代var compare = function (n1, n2) {while (n2) {if (n1.val !== n2.val) return falsen1 = n1 ? n1.next : nulln2 = n2 ? n2.next : null}return true}// 4.判断回文。 知识点:要先执行compre,然后再执行 halfListNode.next = , 否则 lastHafListNode 的值会变化。 变量lastHafListNode中保存的都是对象的引用地址var isPalindrome = function(head) {if (head === null) return trueconst halfListNode = getMidNode(head)const lastHafListNode = reverseList(halfListNode.next)const result = compare(head, lastHafListNode) // 先执行halfListNode.next = reverseList(lastHafListNode) // 再还原原链表return result}
上述过程分成了4步骤,还是稍显麻烦,有没有更简洁一点的办法呢?
其实在找中间点 和 反转链表的过程 可以合并在一起,在找的过程中,将前半部分反转了,然后跟后半部分比较
进阶pro版
var isPalindrome = function(head) {let slow = headlet fast = headlet prev = nullwhile (fast && fast.next ) {fast = fast.next.next; // 一次走两步[slow.next, slow, prev ] = [prev, slow.next, slow]; // //在前半个循环中找到中间,并且慢指针顺手将所过地区翻转}//出了while,fast或者fast.next已经null了,且前半段已经反转,下面 slow pre 开始配合if (fast) slow = slow.next// 比较slow 和 prev, slow是后半段,prev是反转后的前半段while(slow) {if (prev.val !== slow.val) return falseprev = prev.nextslow = slow.next}return true}
删除链表中的节点
请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点。传入函数的唯一参数为 要被删除的节点 。
现有一个链表 — head = [4,5,1,9],它可以表示为:
示例 1:输入:head = [4,5,1,9], node = 5输出:[4,1,9]解释:给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.示例 2:输入:head = [4,5,1,9], node = 1输出:[4,5,9]解释:给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.提示:链表至少包含两个节点。链表中所有节点的值都是唯一的。给定的节点为非末尾节点并且一定是链表中的一个有效节点。不要从你的函数中返回任何结果。
这道题只给了一个参数,就是被删除的节点。 可能第一反应是,我应该知道这个链表的前一个节点啊,然后
prev.val = prev.next.next.val
prev.next = rev.next.next
这样就越过了当前的这个节点。但是没给链表 这个参数,怎么越过当前节点呢?
但是一个节点里有两个信息,val 和next, 只要将当前节点的val 和next 修改成别的了,那么当前节点其实就可以看成是不存在的了。
所以解法很简单。
/*** @param {ListNode} node* @return {void} Do not return anything, modify node in-place instead.*/var deleteNode = function(node) {node.val = node.next.valnode.next = node.next.next};
移动0
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:输入: [0,1,0,3,12]输出: [1,3,12,0,0]说明:必须在原数组上操作,不能拷贝额外的数组。尽量减少操作次数。
思路: 这道题跟前面的 合并两个有序数组 是类似的,也是双指针解法。
快慢两个指针,如果快指针指向的元素不为0, 而慢指针为0, 则交换两个元素的位置,并且将快慢指针都向后移动一位。
只不过快指针在每次迭代的时候都往后移动,而慢指针只在 快指针不为0慢指针为0 和 慢指针本身不为0 的情况下向右移动1位。
/*** @param {number[]} nums* @return {void} Do not return anything, modify nums in-place instead.*/var moveZeroes = function(nums) {let i = 0;let j = 1;while(j < nums.length) {if (nums[j] !== 0 && nums[i] === 0) {[nums[j], nums[i]] = [nums[i], nums[j]]i++} else if (nums[i] !== 0) {i++}j++}};// 其实if 和 else if可以合并下// 但是一旦这么写,要注意i和j都是从0开始的。// 这样就避免 原始数组[1,2] 被改成了 [2,1]的问题var moveZeroes = function(nums) {let i = j = 0;while(j < nums.length) {if(nums[j] !== 0){[nums[i], nums[j]] = [nums[j], nums[i]]i++}j++}return nums};
反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 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 i = 0;let j = s.length - 1;while (i <= j) {[s[i], s[j]] = [s[j], s[i]];i++;j--}};
两个数组的交集2
给定两个数组,编写一个函数来计算它们的交集。
示例 1:输入:nums1 = [1,2,2,1], nums2 = [2,2]输出:[2,2]示例 2:输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]输出:[4,9]说明:输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。我们可以不考虑输出结果的顺序。进阶:如果给定的数组已经排好序呢?你将如何优化你的算法?如果 nums1 的大小比 nums2 小很多,哪种方法更优?如果 nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?
判断交集。。其实最先应该想到的是 hashMap。。。哎,虽然是在归纳双指针类型的题,但是不应该被套路套住。
方法1: hashMap
hashMap的解法比较直观易懂
用hashmap需要注意的是,这里要求数组里
输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。
那就要记录第一个数组的每个元素的出现次数,然后在遍历第二个数组时,遇到相同的再减1。 当减到0时,即使以后第二个数组里还有相同元素,也不再添加。这样就保证了两个数组里,以少的那个为准。
另外为了效率,一开始对两个数组进行了调换,hashMap里存的是小的数组信息。
var intersect = function(nums1, nums2) {if (nums1.length > nums2.length) {[nums1, nums2] = [nums2, nums1]}const arr = []const map = new Mapfor (let i = 0; i < nums1.length; i++) {if (map.has(nums1[i])) {map.set(nums1[i], map.get(nums1[i]) + 1)} else {map.set(nums1[i], 1)}}for (let j = 0; j < nums2.length; j++) {if (map.has(nums2[j]) && map.get(nums2[j]) > 0) {arr.push(nums2[j])map.set(nums2[j], map.get(nums2[j]) - 1)}}return arr}
方法2: 双指针
这题也可以用双指针解法。
这题是要求求出两个数组的交集,只要是有相同的元素,就是交集,而不仅仅是有连续的元素才算是交集。这个要首先明确。
难点在于,如何移动两个指针呢?
比如说两个数组 [9,4,3,1] 和 [3,2,1],它们有重叠的元素 3, 1 但是指针该怎么移动呢?如果说同时移动,肯定不行。如果第一个先移动,直到碰到相同的,再进行移动,也不行。
这里有个技巧,就是把两个数组按照从小到大的顺序排序。
然后两个数组从0开始,如果元素不相等,则把值较小的元素的指针向右移动一位。如果相等,则俩指针都移动。
因为迭代的条件是任意一个数组走完,而且谁小谁移动,就保证了 重复元素 是按照数组中的最小出现次数出现。
/*** @param {number[]} nums1* @param {number[]} nums2* @return {number[]}*/var intersect = function(nums1, nums2) {let i = 0let j = 0nums1 = nums1.sort((a, b) => a - b)nums2 = nums2.sort((a, b) => a - b)const arr = []while (i < nums1.length && j < nums2.length) { // 任意一个数组走完if (nums1[i] === nums2[j]) {arr.push(nums1[i])i++j++} else {if (nums1[i] < nums2[j]) {i++} else {j++}}}return arr};
