JS/TS算法---雙指針(包含滑動窗口環形鏈表)


什么是雙指針(對撞指針、快慢指針)

雙指針,指的是在遍歷對象的過程中,不是普通的使用單個指針進行訪問,而是使用兩個相同方向(快慢指針)或者相反方向(對撞指針)的指針進行掃描,從而達到相應的目的。
換言之,雙指針法充分使用了數組有序這一特征,從而在某些情況下能夠簡化一些運算。

用法

對撞指針(首尾指針,左右指針)

對撞指針是指在有序數組中,將指向最左側的索引定義為左指針(left),最右側的定義為右指針(right),然后從兩頭向中間進行數組遍歷。
對撞數組適用於有序數組,也就是說當你遇到題目給定有序數組時,應該第一時間想到用對撞指針解題。

  1. 求和
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]
    } 
  }
}
  1. 數組反轉
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" 是一個子序列,不是子串。

這是一個簡化版的滑動窗口題型。解題思路可以參考專題中關於雙指針滑動窗口的相關介紹。

按照專題中的思路,這里考慮兩點:

  1. 擴充右邊界后,何時能使滑動窗口內的元素滿足要求。根據題意,當滑動窗口的hash map中出現字符個數大於1的情況時,說明窗口中字串有重復字符,此時考慮開始縮小左邊界。
  2. 何時更新返回結果。在本題中,當滑動窗口中所有元素個數都為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)) 時間復雜度的解法。

這道題是求滿足條件的子數組最小長度。對於求子串、子數組的最優解問題,我們首先想到是否能用滑動窗口或是動態規划來解。這道題要求子數組連續,那么其實用滑動窗口就夠了。

那么就來到了經典的問題填空環節:

  1. 擴充右邊界后,何時能使滑動窗口內的元素滿足要求。根據題意,當滑動窗口內元素總和 >= target 時,考慮開始縮小左邊界。
  2. 何時更新返回結果。在本題中,當滑動窗口內元素總和 >= 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:

img

輸入:head = [3,2,0,-4], pos = 1
輸出:true
解釋:鏈表中有一個環,其尾部連接到第二個節點。

示例 2:

img

輸入:head = [1,2], pos = 0
輸出:true
解釋:鏈表中有一個環,其尾部連接到第一個節點。

示例 3:

img

輸入: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:

img

輸入:head = [3,2,0,-4], pos = 1
輸出:返回索引為 1 的鏈表節點
解釋:鏈表中有一個環,其尾部連接到第二個節點。
示例 2:

img

輸入:head = [1,2], pos = 0
輸出:返回索引為 0 的鏈表節點
解釋:鏈表中有一個環,其尾部連接到第一個節點。
示例 3:

img

輸入:head = [1], pos = -1
輸出:返回 null
解釋:鏈表中沒有環。

判斷鏈表是否有環

可以使用快慢指針法,分別定義 fast 和 slow 指針,從頭結點出發,fast指針每次移動兩個節點,slow指針每次移動一個節點,如果 fast 和 slow指針在途中相遇 ,說明這個鏈表有環。

為什么fast 走兩個節點,slow走一個節點,有環的話,一定會在環內相遇呢,而不是永遠的錯開呢

首先第一點:fast指針一定先進入環中,如果fast指針和slow指針相遇的話,一定是在環中相遇,這是毋庸置疑的。

那么來看一下,為什么fast指針和slow指針一定會相遇呢?

可以畫一個環,然后讓 fast指針在任意一個節點開始追趕slow指針。

會發現最終都是這種情況, 如下圖:

142環形鏈表1

fast和slow各自再走一步, fast和slow就相遇了

這是因為fast是走兩步,slow是走一步,其實相對於slow來說,fast是一個節點一個節點的靠近slow的,所以fast一定可以和slow重合。

動畫如下:

141.環形鏈表

如果有環,如何找到這個環的入口

此時已經可以判斷鏈表是否有環了,那么接下來要找這個環的入口了。

假設從頭結點到環形入口節點 的節點數為x。 環形入口節點到 fast指針與slow指針相遇節點 節點數為y。 從相遇節點 再到環形入口節點節點數為 z。 如圖所示:

142環形鏈表2

那么相遇時: 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同時移動,每次移動一個節點, 那么他們相遇的地方就是 環形入口的節點。

動畫如下:

142.環形鏈表II(求入口)

那么 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)中如下的地方:

142環形鏈表5

首先slow進環的時候,fast一定是先進環來了。

如果slow進環入口,fast也在環入口,那么把這個環展開成直線,就是如下圖的樣子:

142環形鏈表3

可以看出如果slow 和 fast同時在環入口開始走,一定會在環入口3相遇,slow走了一圈,fast走了兩圈。

重點來了,slow進環的時候,fast一定是在環的任意一個位置,如圖:

142環形鏈表4

那么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

};


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM