什么是雙指針(對撞指針、快慢指針)
雙指針,指的是在遍歷對象的過程中,不是普通的使用單個指針進行訪問,而是使用兩個相同方向(快慢指針)或者相反方向(對撞指針)的指針進行掃描,從而達到相應的目的。
換言之,雙指針法充分使用了數組有序這一特征,從而在某些情況下能夠簡化一些運算。
用法
對撞指針(首尾指針,左右指針)
對撞指針是指在有序數組中,將指向最左側的索引定義為左指針(left),最右側的定義為右指針(right),然后從兩頭向中間進行數組遍歷。
對撞數組適用於有序數組,也就是說當你遇到題目給定有序數組時,應該第一時間想到用對撞指針解題。
- 求和
function sum(arr,target){
let left = 0,right = arr.length-1
while(left<right){
if(arr[left]+arr[right]>target){
right--
}else if(arr[left]+arr[right]<target){
left++
}else if(arr[left]+arr[right]==target){
return [left,right]
}
}
}
- 數組反轉
function reverse(arr){
let left = 0, right = arr.length-1
while(left < right){
[arr[left],arr[right]] = [arr[right],arr[left]]
left++
right--
}
return arr
}
快慢指針
快慢指針也是雙指針,但是兩個指針從同一側開始遍歷數組,將這兩個指針分別定義為快指針(fast)和慢指針(slow),兩個指針以不同的策略移動,直到兩個指針的值相等(或其他特殊條件)為止,如fast每次增長兩個,slow每次增長一個。
1.字符串壓縮
function compressString(S){
let newS = '', i = 0, j = 0
while(j < S.length - 1){
if(S[j]!==S[j+1]){
newS += S[i]+(j-i+1)
i = j+1
}
j++
}
newS += S[i]+(j-i+1)
return newS.length<S.length?S
}
leecode題詳解
左右指針
幾數之和
[1] 兩數之和---排序+雙指針
給定一個整數數組 nums 和一個整數目標值 target,請你在該數組中找出 和為目標值 target 的那 兩個 整數,並返回它們的數組下標。
你可以假設每種輸入只會對應一個答案。但是,數組中同一個元素在答案里不能重復出現。
你可以按任意順序返回答案。
示例 1:
輸入:nums = [2,7,11,15], target = 9
輸出:[0,1]
解釋:因為 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
輸入:nums = [3,2,4], target = 6
輸出:[1,2]
示例 3:
輸入:nums = [3,3], target = 6
輸出:[0,1]
提示: 只會存在一個有效答案
進階:你可以想出一個時間復雜度小於 O(n^2) 的算法嗎?
先排序 O(NlogN),並記錄原來的位置,題目說了確定有唯一答案,所以用左右指針縮小搜索范圍 O(N)
function twoSum(nums: number[], target: number): number[] {
const nextNums = nums.map((val, idx) => ({
val,
idx,
}));
nextNums.sort((a, b) => {
return a.val - b.val;
});
const n = nums.length;
let [left, right] = [0, n - 1];
while (left < right) {
const tmp = nextNums[left].val + nextNums[right].val;
if (tmp > target) {
right--;
} else if (tmp < target) {
left++;
} else {
break;
}
}
return [nextNums[left].idx, nextNums[right].idx];
}
[15] 三數之和
給你一個包含 n 個整數的數組 nums,判斷 nums 中是否存在三個元素 a,b,c ,使得 a + b + c = 0 ?請你找出所有和為 0 且不重復的三元組。
注意:答案中不可以包含重復的三元組。
示例 1:
輸入:nums = [-1,0,1,2,-1,-4]
輸出:[[-1,-1,2],[-1,0,1]]
示例 2:
輸入:nums = []
輸出:[]
示例 3:
輸入:nums = [0]
輸出:[]
這道題是1.Two Sum的升級版,需要三個數的和為 0。那么我們可以想到,這三個數中的最小數必定為負數,並且另兩個數的和等於這個數的相反數。
因此我們需要對數組從小到大進行排序,之后遍歷一遍數組,每次固定住最小的那個數字nums[i],將它的相反數作為 target。
之后的解法就與Two Sum的解法完全一致了,使用首尾指針,由於另外兩個數一定比最小數大,因此首次循環首尾指針范圍在當前位置i+1到數組尾。
根據以上推導的結論,若這個nums[i]>0,或者尾指針指向的數字<0,則可以直接結束循環了。
跟Two Sum稍有不同的是,當找到 target 的一組解后不能立即結束循環,因為有可能存在多組和為 target 的解。並且數字組成完全相同的解不能放入結果中,需要做好去重操作。
function threeSum(nums: number[]): number[][] {
//數組排序 a-b升序 b-a降序
nums = nums.sort((a, b) => a - b);
const res: number[][] = [];
//length-2即可
for (let i = 0; i < nums.length - 2; i++) {
const min = nums[i];
// 如果數組的最小值都>0,則一定不存在 a + b + c = 0
if (min > 0) break;
// 去掉重復情況
if (i > 0 && min === nums[i - 1]) continue;
const target = 0 - min;
// 設置雙指針
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
if (nums[left] + nums[right] === target) {
res.push([min, nums[left], nums[right]]);
// 去除重復情況
while (left < right && nums[left + 1] === nums[left]) left += 1;
while (left < right && nums[right - 1] === nums[right]) right -= 1;
// 指針移動到下一組情況
left += 1;
right -= 1;
} else if (nums[left] + nums[right] > target) {
right -= 1;
} else {
left += 1;
}
}
}
return res;
}
[19] 四數之和
給你一個由 n 個整數組成的數組 nums ,和一個目標值 target 。請你找出並返回滿足下述全部條件且不重復的四元組 [nums[a], nums[b], nums[c], nums[d]] (若兩個四元組元素一一對應,則認為兩個四元組重復):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意順序 返回答案 。
示例 1:
輸入:nums = [1,0,-1,0,-2,2], target = 0
輸出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
輸入:nums = [2,2,2,2,2], target = 8
輸出:[[2,2,2,2]]
提示:
1 <= nums.length <= 200
-10^9 <= nums[i] <= 10^9
-10^9 <= target <= 10^9作為 [1] 兩數之和 與 [15] 三數之和 的再一次升級版,這一次我們可以整理出這一類問題的通用套路了。
實際上 nSum 問題的通用解法為:先通過遍歷數組選定 N 元組中最小的那個數,之后再通過遍歷選定第二小的數……當然這一過程可以用遞歸實現。
直到剩余 2 個數未確定,此時調用 2Sum 方法,通過空間換時間,將 O(n^2) 的時間復雜度縮小為 O(n)。
在進入下一級遞歸之前,我們需要做好一定的剪枝來提升性能。例如,最小的 4 個數都小於 target,直接退出;或者最大的 4 個數都大於 target, 直接跳過。再例如,當發現當前值與下一個值相同時,說明有重復元素,同樣跳過。
最終,nSum 的時間復雜度為 O(n^(N-1))。所以隨着 N 的增大,這一算法的優越度也變得越來越低了……因為 N=5 以上以后,一個 O(n^4) 的算法已經足以讓很多用例直接超時了。
因此,掌握常規算法,並了解其衍生的題型改造就已經足夠了,出 5Sum,6Sum 的題並無必要。
function fourSum(nums: number[], target: number): number[][] {
nums = nums.sort((a, b) => a - b);
const len = nums.length;
const res: number[][] = [];
for (let i = 0; i < len - 3; i++) {
// 最小的 4 個數都小於 target,直接退出;或者最大的 4 個數都大於 target, 直接跳過
if (nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) break;
if (nums[i] + nums[len - 1] + nums[len - 2] + nums[len - 3] < target) continue;
// 去掉重復情況
if (i > 0 && nums[i - 1] === nums[i]) continue;
// 接下來就是 3Sum 問題了
threeSumCase(i, nums[i], target, nums, res);
}
return res;
function threeSumCase(start: number, first: number, target: number, nums: number[], res: number[][]) {
const len = nums.length;
for (let i = start + 1; i < len - 2; i++) {
const second = nums[i];
// 最小的 4 個數都小於 target,直接退出;或者最大的 4 個數都大於 target, 直接跳過
if (first + second + nums[i + 1] + nums[i + 2] > target) break;
if (first + second + nums[len - 1] + nums[len - 2] < target) continue;
// 去掉重復情況
if (i > start + 1 && nums[i - 1] === nums[i]) continue;
// 接下來就是 2Sum 問題了
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
const sum = first + second + nums[left] + nums[right];
if (sum === target) {
res.push([first, second, nums[left], nums[right]]);
// 去除重復情況
while (left < right && nums[left + 1] === nums[left]) left += 1;
while (left < right && nums[right - 1] === nums[right]) right -= 1;
// 指針移動到下一組情況
left += 1;
right -= 1;
} else if (sum > target) {
right -= 1;
} else {
left += 1;
}
}
}
}
}
滑動窗口
在力扣上刷題時經常可以看到這樣的題,求XXX的子串、子數組、子序列等等,這類題一般使用滑動窗口來解決。
情況一:尋找最長的
①初始化左右指針left和right,左右指針之間的內容就是窗口,定義一個變量result記錄當前的滑動窗口的結果,定義一個變量bestResult記錄當前滑動窗口下的最優結果
②right要向右逐位滑動循環
③每次滑動后,記錄當前滑動的結果。如果當前的結果符合條件,則更新最優的結果,然后right要繼續向右滑動;如果當前的結果不符合條件,那么要讓left逐步收縮
④當right到達結尾時停止滑動
初始化left,right,result,bestResult
while (右指針沒有到結尾) {
窗口擴大,加入right對應元素,更新當前result
while (result不滿足要求) {
窗口縮小,移除left對應元素,left右移
}
更新最優結果bestResult
right++;
}
返回bestResult
情況二:尋找最短的
①初始化左右指針left和right,左右指針之間的內容就是窗口,定義一個變量result記錄當前的滑動窗口的結果,定義一個變量bestResult記錄當前滑動窗口下的最優結果
②right要向右逐位滑動循環
③每次滑動后,記錄當前滑動的結果。如果當前的結果符合條件,則更新最優的結果,然后right要繼續向右滑動;如果當前的結果不符合條件,那么要讓left逐步收縮
④當right到達結尾時停止滑動
初始化left,right,result,bestResult
while (右指針沒有到結尾) {
窗口擴大,加入right對應元素,更新當前result
while (result不滿足要求) {
更新最優結果bestResult
窗口縮小,移除left對應元素,left右移
}
right++;
}
返回bestResult
[3] 無重復字符的最長子串
給定一個字符串,請你找出其中不含有重復字符的 最長子串 的長度。
示例 1:
輸入: "abcabcbb"
輸出: 3
解釋: 因為無重復字符的最長子串是 "abc",所以其長度為 3。
示例 2:
輸入: "bbbbb"
輸出: 1
解釋: 因為無重復字符的最長子串是 "b",所以其長度為 1。
示例 3:
輸入: "pwwkew"
輸出: 3
解釋: 因為無重復字符的最長子串是 "wke",所以其長度為 3。
請注意,你的答案必須是 子串 的長度,"pwke" 是一個子序列,不是子串。
這是一個簡化版的滑動窗口題型。解題思路可以參考專題中關於雙指針滑動窗口的相關介紹。
按照專題中的思路,這里考慮兩點:
- 擴充右邊界后,何時能使滑動窗口內的元素滿足要求。根據題意,當滑動窗口的hash map中出現字符個數大於1的情況時,說明窗口中字串有重復字符,此時考慮開始縮小左邊界。
- 何時更新返回結果。在本題中,當滑動窗口中所有元素個數都為1,則可以認為當前子串為無重復字符串,此時可以比對並更新結果。
function lengthOfLongestSubstring(s: string): number {
const window: Record<string, number> = {};
let res = 0;
let left = 0;
let right = 0;
while (right < s.length) {
// 擴大右邊界
const ch = s[right];
right++;
// 更新滑動窗口元素
window[ch] = window[ch] ? window[ch] + 1 : 1;
// 當滑動窗口中該字符個數大於1,此時字串不合法,需要縮小左邊界直到使該字符唯一
while (window[ch] > 1) {
// 縮左邊界
const dropCh = s[left];
left++;
// 更新滑動窗口元素
window[dropCh] -= 1;
}
// 更新合法情況的結果
res = Math.max(res, right - left);
}
return res;
}
[209] 長度最小的子數組-------尋找最短的
給定一個含有 n 個正整數的數組和一個正整數 target 。
找出該數組中滿足其和 ≥ target 的長度最小的 連續子數組 [numsl, numsl+1, ..., numsr-1, numsr],並返回其長度。如果不存在符合條件的子數組,返回 0 。
示例 1:
輸入:target = 7, nums = [2,3,1,2,4,3]
輸出:2
解釋:子數組 [4,3] 是該條件下的長度最小的子數組。
示例 2:
輸入:target = 4, nums = [1,4,4]
輸出:1
示例 3:
輸入:target = 11, nums = [1,1,1,1,1,1,1,1]
輸出:0
提示:
進階:
如果你已經實現 O(n) 時間復雜度的解法, 請嘗試設計一個 O(n log(n)) 時間復雜度的解法。
這道題是求滿足條件的子數組最小長度。對於求子串、子數組的最優解問題,我們首先想到是否能用滑動窗口或是動態規划來解。這道題要求子數組連續,那么其實用滑動窗口就夠了。
那么就來到了經典的問題填空環節:
- 擴充右邊界后,何時能使滑動窗口內的元素滿足要求。根據題意,當滑動窗口內元素總和 >= target 時,考慮開始縮小左邊界。
- 何時更新返回結果。在本題中,當滑動窗口內元素總和 >= target 時,比對記錄值與當前sum結果,保存最小值。
這樣一套操作下來,整個問題就沒有任何難點可言了。
function minSubArrayLen(target: number, nums: number[]): number {
let res: number = nums.length + 1;
let left = 0;
let right = 0;
let sum = 0;
while (right < nums.length) {
sum += nums[right];
right += 1;
while (sum >= target) {
res = Math.min(res, right - left);
sum -= nums[left];
left++;
}
}
// 不存在時返回0
return res === nums.length + 1 ? 0 : res;
}
環形鏈表
[141] 環形鏈表
給你一個鏈表的頭節點 head ,判斷鏈表中是否有環。
如果鏈表中有某個節點,可以通過連續跟蹤 next 指針再次到達,則鏈表中存在環。 為了表示給定鏈表中的環,評測系統內部使用整數 pos 來表示鏈表尾連接到鏈表中的位置(索引從 0 開始)。注意:pos 不作為參數進行傳遞 。僅僅是為了標識鏈表的實際情況。
如果鏈表中存在環 ,則返回 true 。 否則,返回 false 。
示例 1:
輸入:head = [3,2,0,-4], pos = 1
輸出:true
解釋:鏈表中有一個環,其尾部連接到第二個節點。示例 2:
輸入:head = [1,2], pos = 0
輸出:true
解釋:鏈表中有一個環,其尾部連接到第一個節點。示例 3:
輸入:head = [1], pos = -1
輸出:false
解釋:鏈表中沒有環。
快慢指針
function hasCycle(head: ListNode | null): boolean {
if (head === null || head.next === null) return false;
// 快慢指針
let slow = head;
let fast = head;
while (fast !== null) {
// 慢指針每次移動一位
slow = slow.next;
// 如果滿足條件,說明 fast 為尾部結點,不存在環
if (fast.next === null) return false;
// 快指針每次移動兩位
fast = fast.next.next;
// slow 和 fast 相等,說明內存地址相同,有環
if (slow === fast) return true;
}
return false;
};
[142] 環形鏈表 II
給定一個鏈表的頭節點 head ,返回鏈表開始入環的第一個節點。 如果鏈表無環,則返回 null。
如果鏈表中有某個節點,可以通過連續跟蹤 next 指針再次到達,則鏈表中存在環。 為了表示給定鏈表中的環,評測系統內部使用整數 pos 來表示鏈表尾連接到鏈表中的位置(索引從 0 開始)。如果 pos 是 -1,則在該鏈表中沒有環。注意:pos 不作為參數進行傳遞,僅僅是為了標識鏈表的實際情況。
不允許修改 鏈表。
示例 1:
輸入:head = [3,2,0,-4], pos = 1
輸出:返回索引為 1 的鏈表節點
解釋:鏈表中有一個環,其尾部連接到第二個節點。
示例 2:
輸入:head = [1,2], pos = 0
輸出:返回索引為 0 的鏈表節點
解釋:鏈表中有一個環,其尾部連接到第一個節點。
示例 3:
輸入:head = [1], pos = -1
輸出:返回 null
解釋:鏈表中沒有環。
判斷鏈表是否有環
可以使用快慢指針法,分別定義 fast 和 slow 指針,從頭結點出發,fast指針每次移動兩個節點,slow指針每次移動一個節點,如果 fast 和 slow指針在途中相遇 ,說明這個鏈表有環。
為什么fast 走兩個節點,slow走一個節點,有環的話,一定會在環內相遇呢,而不是永遠的錯開呢
首先第一點:fast指針一定先進入環中,如果fast指針和slow指針相遇的話,一定是在環中相遇,這是毋庸置疑的。
那么來看一下,為什么fast指針和slow指針一定會相遇呢?
可以畫一個環,然后讓 fast指針在任意一個節點開始追趕slow指針。
會發現最終都是這種情況, 如下圖:

fast和slow各自再走一步, fast和slow就相遇了
這是因為fast是走兩步,slow是走一步,其實相對於slow來說,fast是一個節點一個節點的靠近slow的,所以fast一定可以和slow重合。
動畫如下:

如果有環,如何找到這個環的入口
此時已經可以判斷鏈表是否有環了,那么接下來要找這個環的入口了。
假設從頭結點到環形入口節點 的節點數為x。 環形入口節點到 fast指針與slow指針相遇節點 節點數為y。 從相遇節點 再到環形入口節點節點數為 z。 如圖所示:

那么相遇時: slow指針走過的節點數為: x + y, fast指針走過的節點數:x + y + n (y + z),n為fast指針在環內走了n圈才遇到slow指針, (y+z)為 一圈內節點的個數A。
因為fast指針是一步走兩個節點,slow指針一步走一個節點, 所以 fast指針走過的節點數 = slow指針走過的節點數 * 2:
(x + y) * 2 = x + y + n (y + z)
兩邊消掉一個(x+y): x + y = n (y + z)
因為要找環形的入口,那么要求的是x,因為x表示 頭結點到 環形入口節點的的距離。
所以要求x ,將x單獨放在左面:x = n (y + z) - y ,
再從n(y+z)中提出一個 (y+z)來,整理公式之后為如下公式:x = (n - 1) (y + z) + z 注意這里n一定是大於等於1的,因為 fast指針至少要多走一圈才能相遇slow指針。
這個公式說明什么呢?
先拿n為1的情況來舉例,意味着fast指針在環形里轉了一圈之后,就遇到了 slow指針了。
當 n為1的時候,公式就化解為 x = z,
這就意味着,從頭結點出發一個指針,從相遇節點 也出發一個指針,這兩個指針每次只走一個節點, 那么當這兩個指針相遇的時候就是 環形入口的節點。
也就是在相遇節點處,定義一個指針index1,在頭結點處定一個指針index2。
讓index1和index2同時移動,每次移動一個節點, 那么他們相遇的地方就是 環形入口的節點。
動畫如下:

那么 n如果大於1是什么情況呢,就是fast指針在環形轉n圈之后才遇到 slow指針。
其實這種情況和n為1的時候 效果是一樣的,一樣可以通過這個方法找到 環形的入口節點,只不過,index1 指針在環里 多轉了(n-1)圈,然后再遇到index2,相遇點依然是環形的入口節點。
function detectCycle(head: ListNode | null): ListNode | null {
if(head===null||head.next===null) return null;
//快慢指針
let slow = head;
let fast = head;
while(fast!==null&&fast.next!==null){
//慢指針移動一次
slow = slow.next;
fast = fast.next.next;
//進入環內
if(slow===fast) {
slow = head;
while(slow!==fast){
slow = slow.next;
fast = fast.next;
}
return slow
}
}
return null;
};
補充
在推理過程中,大家可能有一個疑問就是:為什么第一次在環中相遇,slow的 步數 是 x+y 而不是 x + 若干環的長度 + y 呢?
即文章鏈表:環找到了,那入口呢? (opens new window)中如下的地方:

首先slow進環的時候,fast一定是先進環來了。
如果slow進環入口,fast也在環入口,那么把這個環展開成直線,就是如下圖的樣子:

可以看出如果slow 和 fast同時在環入口開始走,一定會在環入口3相遇,slow走了一圈,fast走了兩圈。
重點來了,slow進環的時候,fast一定是在環的任意一個位置,如圖:

那么fast指針走到環入口3的時候,已經走了k + n 個節點,slow相應的應該走了(k + n) / 2 個節點。
因為k是小於n的(圖中可以看出),所以(k + n) / 2 一定小於n。
也就是說slow一定沒有走到環入口3,而fast已經到環入口3了。
這說明什么呢?
在slow開始走的那一環已經和fast相遇了。
那有同學又說了,為什么fast不能跳過去呢? 在剛剛已經說過一次了,fast相對於slow是一次移動一個節點,所以不可能跳過去。
好了,這次把為什么第一次在環中相遇,slow的 步數 是 x+y 而不是 x + 若干環的長度 + y ,用數學推理了一下,算是對鏈表:環找到了,那入口呢? (opens new window)的補充。
快慢指針
劍指 Offer 22. 鏈表中倒數第k個節點
輸入一個鏈表,輸出該鏈表中倒數第k個節點。為了符合大多數人的習慣,本題從1開始計數,即鏈表的尾節點是倒數第1個節點。
例如,一個鏈表有 6 個節點,從頭節點開始,它們的值依次是 1、2、3、4、5、6。這個鏈表的倒數第 3 個節點是值為 4 的節點。
示例:
給定一個鏈表: 1->2->3->4->5, 和 k = 2.
返回鏈表 4->5.
function getKthFromEnd(head: ListNode | null, k: number): ListNode | null {
let slow = head;
let fast = head;
//快指針先走k步
while (k-- > 0) fast = fast.next;
//快指針走到頭停止
while (fast !== null) {
fast = fast.next;
slow = slow.next;
}
//輸出慢指針
return slow;
};
講解:原地算法(In-Place Algorithm)
原地算法:在計算機科學中,一個原地算法(in-place algorithm)是一種使用小的,固定數量的額外之空間來轉換資料的算法。當算法執行時,輸入的資料通常會被要輸出的部分覆蓋掉。不是原地算法有時候稱為非原地(not-in-place)或不得其所(out-of-place)。
通俗的說法:就是一個算法,除了可以運用輸入數據本身已開辟的空間外,就只可以用極小的輔助空間來進行運算了,一般 額外空間復雜度為 O(1),也就是一個變量。(特殊情況除外)
[283] 移動零
給定一個數組 nums,編寫一個函數將所有 0 移動到數組的末尾,同時保持非零元素的相對順序。
請注意 ,必須在不復制數組的情況下原地對數組進行操作。
示例 1:
輸入: nums = [0,1,0,3,12]
輸出: [1,3,12,0,0]
示例 2:輸入: nums = [0]
輸出: [0]
兩次遍歷
var moveZeroes = function (nums) {
let j = 0;
for (let i = 0; i < nums.length; i++) {
if (nums[i] !== 0) {//遇到非0元素,讓nums[j] = nums[i],然后j++
nums[j] = nums[i];
j++;
}
}
for (let i = j; i < nums.length; i++) {//剩下的元素全是0
nums[i] = 0;
}
return nums;
};
雙指針一次遍歷
思路:定義left、right指針,right從左往右移動,遇上非0元素,交換left和right對應的元素,交換之后left++
復雜度:時間復雜度O(n),空間復雜度O(1)
var moveZeroes = function(nums) {
let left=0,right=0
while(right<nums.length){
if(nums[right]!==0){//遇上非0元素,交換left和right對應的元素
swap(nums,left,right)
left++//交換之后left++
}
right++
}
};
function swap(nums,l,r){
let temp=nums[r]
nums[r]=nums[l]
nums[l]=temp
}
[26] 刪除有序數組中的重復項
給定一個排序數組,你需要在原地刪除重復出現的元素,使得每個元素只出現一次,返回移除后數組的新長度。
不要使用額外的數組空間,你必須在原地修改輸入數組並在使用 O(1) 額外空間的條件下完成。
示例 1:
給定數組 nums = [1,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。
你不需要考慮數組中超出新長度后面的元素。
本來只是一個基本的數組去重動作,但是題目要求我們使用原地算法(空間復雜度O(1)),並且不需要考慮超出長度后面的元素,也就是說我們只需要保證前K個數是有序去重的即可。
我們就能想到雙指針,解題流程如下:
創建一個慢指針 i,指向數組第2位數字,再創建一個快指針 j,指向數組第2位。
遍歷數組,從數組第二位開始
若 nums[j] 和 nums[j-1] 不等,把 nums[i] 改為 nums[j],再i++。
圖解如下:
function removeDuplicates(nums: number[]):number {
let i = 1;
for(let j=1; i<nums.length; j++){
if(nums[j]!=nums[j-1]){
nums[i] = nums[j]
i++;
}
}
return i
};
兩個數組指針
[88] 合並兩個有序數組
給你兩個按 非遞減順序 排列的整數數組 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 中。提示:
nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-109 <= nums1[i], nums2[j] <= 109進階:你可以設計實現一個時間復雜度為 O(m + n) 的算法解決此問題嗎?
function merge(nums1: number[], m: number, nums2: number[], n: number) {
let e = nums1.length - 1;//指向nums1末尾
let mi = m - 1;//指向nums1最后
let ni = n - 1;//指向nums2最后
while (mi >= 0 && ni >= 0) {
if (nums1[mi] > nums2[ni]) {
//最大值移動到末尾
nums1[e] = nums1[mi]
mi--
} else {
nums1[e] = nums2[ni]
ni--
}
//末尾指針左移
e--
}
//nums2為空
while (mi >= 0) {
nums1[e] = nums1[mi]
mi--
e--
}
//nums1為空(均為0)
while (ni >= 0) {
nums1[e] = nums2[ni]
ni--
e--
}
return nums1
};



