手摸手刷題 - 數組 - 字謎分組、三數之和、無重複字符的最長子串、矩陣置零和遞增的三元子序列

點擊上方藍色字體輕鬆關注前端

前言

上一文咱們知道了數組 - 兩個數組的交集、一週中的第幾天、有效的數獨、除資深之外數組的乘積和存在重複元素。接下來咱們來學習一下關於 字謎分組、三數之和、無重複字符的最長子串、矩陣置零和遞增的三元子序列node


字謎分組

給定一個字符串數組,將字母異位詞組合在一塊兒。字母異位詞指字母相同,但排列不一樣的字符串。web

示例1輸入: ["eat", "tea", "tan", "ate", "nat", "bat"],輸出:[ ["ate","eat","tea"], ["nat","tan"], ["bat"]]

解法1: 排序分類

思路算法

咱們先遍歷數組,把每一個字母異位詞都進行排序。再將排序後的字符串做爲 key,將 key 值同樣的字母異位詞置於同一個數組中。最後,再按照題目所要求的格式返回數組。數組

詳解安全

  1. 建立個空對象 obj,用於後續將字母異位詞分類儲存;
  2. 建立個空數組 arr,用與後續返回結果;
  3. 遍歷數組裏的元素;
  4. 將每一個字母異位詞進行排序,並將排序後的字符串做爲 key,可知 key 值同樣的即爲字母異位詞,將他們置於同一個數組中(即 obj["aet"] = ["ate","eat","tea"]);
  5. 待上述遍歷結束,再遍歷 obj,將 obj 的每個值,push 到 arr 中。
const groupAnagrams = function (strs) { const obj = {}; const arr = []; // 遍歷數組 for (let i = 0; i < strs.length; i++) { // 將每一個字母異位詞進行排序,並將排序後的字符串做爲 key const unit = Array.from(strs[i]).sort().join(''); // 將 key 值同樣的字母異位詞置於同一個數組中 if (!obj[unit]) { obj[unit] = []; } obj[unit].push(strs[i]); } for (const i in obj) { arr.push(obj[i]); } return arr;};

複雜度分析微信

  1. 時間複雜度O(nklogk)

外層 strs for 循環爲O(n),裏面根據字符數據進行排序,查資料得排序時間複雜度爲O(nklogk),k 爲字符串長度最大值,所以爲O(nklogk)。架構

  1. 空間複雜度O(nk)

代碼運行時建立了 obj 對象用於存儲 strs 中的字符串,空間與 strs 長度與字符串長度成正比關係,所以爲O(nk)。app

解法2: 計數分類

思路框架

咱們先遍歷數組,每次都建立一個長度爲 26,元素全是 0 的數組,用於記錄每一個單詞中每一個字符出現的次數;而後將其轉化爲字符串做爲 key,將 key 值同樣的字母異位詞置於同一個數組中。最後,再按照題目所要求的格式返回數組。

詳解

  1. 建立個空對象 obj,用於後續將字母異位詞分類儲存;
  2. 建立個空數組 arr,用與後續返回結果;
  3. 遍歷數組裏的元素;
  4. 每次遍歷都建立一個長度爲 26(跟 26 個英文字母對應),元素全是 0 的數組,用於記錄每一個單詞中每一個字符出現的次數,而後將該數組轉化的字符串做爲 key,可知 key 值同樣的即爲字母異位詞,將他們置於同一個數組中;
  5. 待上述遍歷結束,再遍歷 obj,將 obj 的每個值,push 到 arr 中。
const groupAnagrams = function (strs) { const obj = {}; const arr = []; // 遍歷數組 for (let i = 0; i < strs.length; i++) { // 都建立一個長度爲 26,元素全是 0 的數組,用於記錄每一個單詞中每一個字符出現的次數 const unit = new Array(26).fill(0); for (let j = 0; j < strs[i].length; j++) { const index = strs[i].charCodeAt(j) - 97; unit[index] += 1; } // 將每一個數組轉化的字符串做爲 key const newUnit = JSON.stringify(unit); // 將 key 值同樣的字母異位詞置於同一個數組中 if (!obj[newUnit]) { obj[newUnit] = []; } obj[newUnit].push(strs[i]); } for (const i in obj) { arr.push(obj[i]); } return arr;};

複雜度分析

  1. 時間複雜度O(nk)

外層 strs for 循環爲O(n),內層爲對字符串進行 for 循環O(n),k 爲字符串長度最大值,所以爲O(nk)。

  1. 空間複雜度O(nk)

代碼運行時建立了 obj 對象用於存儲 strs 中的字符串,空間與 strs 長度與字符串長度成正比關係,所以爲O(nk)。

三數之和

給定一個包含 n 個整數的數組 nums,判斷 nums 中是否存在三個元素 a,b,c ,使得 a + b + c = 0 ?找出全部知足條件且不重複的三元組。

注意:答案中不能夠包含重複的三元組。

例如, 給定數組 nums = [-1, 0, 1, 2, -1, -4],
知足要求的三元組集合爲:[ [-1, 0, 1], [-1, -1, 2]]

解法1: 暴力法

思路

咱們首先想到的是三重循環,題目中要求三元組不能重複,所以對每次插入數組對三元組作一下簡單的記錄。

詳解

  1. 直接對數組進行3重遍歷,當三個數的和等於 0 時,插入數組,同時對已經插入對數據作一下記錄
const threeSum = function (nums) { const res = []; const uniqueMap = {}; nums.sort(); for (let i = 0; i < nums.length - 2; i++) { for (let j = i + 1; j < nums.length - 1; j++) { for (let k = j + 1; k < nums.length; k++) { if (nums[i] + nums[j] + nums[k] === 0) { const item = [nums[i], nums[j], nums[k]]; if (!uniqueMap[item.join(',')]) { res.push(item); uniqueMap[item.join(',')] = 1; } } } } } return res;};

複雜度分析

  1. 時間複雜度O( )

套三重循環,所以爲O( )。

  1. 空間複雜度O(n)

額外申請了uniqueMap的空間,所以爲O(n)。

解法2: 雙指針

思路

首先對數組進行排序,便於在插入的時候去重,進行雙指針遍歷時,遇到重複的數就能夠很方便得跳過。

詳解

  1. 先將數組排序,令左指針L = i + 1,右指針R = n - 1,當L <= R時,進行循環
  • 當 nums[i] + nums[L] + nums[R] === 0 時,將三個數插入數組,同時判斷 nums[L] 和 nums[L + 1]是否重複,去重複解以後,同時將 L 和 R 移到下一個位置
  • 若 sumsum 小於 0,說明 nums[L] 過小,L 須要右移,L++
  • 若 sumsum 大於 0,說明 nums[R] 太大,R 須要左移, R--
const threeSum = function (nums) { const res = []; nums.sort((a, b) => a - b); const length = nums.length;
for (let i = 0; i < length; i++) { let left = i + 1; let right = length - 1; while (left < right) { const sum = nums[i] + nums[left] + nums[right]; if (sum === 0) { res.push([nums[i], nums[left], nums[right]]);
const leftValue = nums[left];
// 這兩步是爲了去重 while (left < length && nums[left] === leftValue) { left++; } const rightValue = nums[right]; while (right > left && nums[right] === rightValue) { right--; } } else if (sum < 0) { // 小於 0 說明過小了,須要向右移動 left++; } else { // 太大了,把右邊的指針向左移動 right--; } } while (i + 1 < nums.length && nums[i] === nums[i + 1]) { i++; } } return res;};

複雜度分析

  1. 時間複雜度O( )

數組遍歷O(n),雙指針遍歷O(n),所以複雜度爲O(n) * O(n),所以爲O( )。

  1. 空間複雜度O(1)

指針使用常數大小的額外空間,所以爲O(1)。

無重複字符的最長子串

給定一個字符串,請你找出其中不含有重複字符的 最長子串 的長度。

示例1輸入: "abcabcbb"輸出: 3解釋: 由於無重複字符的最長子串是 "abc",因此其長度爲 3。
示例2輸入: "bbbbb"輸出: 1解釋: 由於無重複字符的最長子串是 "b",因此其長度爲 1。
示例3輸入: "pwwkew"輸出: 3解釋: 由於無重複字符的最長子串是 "wke",因此其長度爲 3。請注意,你的答案必須是 子串 的長度,"pwke" 是一個子序列,不是子串。

解法1

維護一個數組,數組裏從前到後存放着字符。在遍歷過程當中,維持數組中字符不重複,若是重複,則從數組中 shift 出字符, 直到移除那個重複字符爲止,並記錄下數組的最長長度。

思路

用一個數組來儲存當前字符, 在遍歷過程當中不斷地存入不重複字符,遇到重複字符則整理數組達到字符不重複的條件。

詳解

  1. 初始化一個數組和最大值, 從前向後遍歷字符串, 若是該字符不在數組中,則把字符 push 到數組中,而且比較記錄下當前最大值。不然就從頭部向外 pop 字符直到該重複字符被移除, 如此循環直到結束。
const lengthOfLongestSubstring = function (s) { let current = 0; let max = 0; const list = []; const len = s.length; for (; current < len; current++) { if (list.indexOf(s[current]) === -1) { list.push(s[current]); } else { do { list.shift(); } while (list.indexOf(s[current]) !== -1); list.push(s[current]); } max = Math.max(list.length, max); } return max;};

複雜度分析

  1. 時間複雜度O(n)

這樣子須要從前向後遍歷一遍長度爲n的字符串,須要進行n次字符是否重複的比較。所以爲O(n)。

  1. 空間複雜度O(n)

在計算比較過程當中,數組最長有可能存放n個不重複字符串,所以爲O(n)。

解法2

從前到後遍歷字符串,維護一個string,記錄着不重複字符子串。每當遇到重複的字符時候,找到string中重複的字符,並截斷。循環往復直到遍歷最後一個字符.

思路

記錄當前正在遍歷的不重複字串的子集 string , 在遍歷過程當中不斷地添加不重複字符,遇到重複字符則截斷 string 達到 string 內補字符不重複的條件。

詳解

  1. 初始化一個 string 和最大值,
  2. 從前向後遍歷字符串,
  3. 若是該字符不在 string 中,則添加字符到 string 中,並記錄下最大值。
  4. 不然就截斷字符串,如此循環直到結束。
const lengthOfLongestSubstring = function (s) { let num = 0; let max = 0; let subString = ''; for (char of s) { if (subString.indexOf(char) === -1) { subString += char; num++; max = max < num ? num : max; } else { subString += char; subString = subString.slice(subString.indexOf(char) + 1); num = subString.length; } } return max;};

複雜度分析

  1. 時間複雜度O(n)

咱們只遍歷了包含有n個元素的字符串一次。所以爲O(n)。

  1. 空間複雜度O(n)

所需的額外空間取決於子串的長度,子串始終小於等於傳入字符串的長度,該子串最多須要存儲n個元素,所以爲O(n)。

矩陣置零

給定一個 m x n 的矩陣,若是一個元素爲 0,則將其所在行和列的全部元素都設爲 0。請使用原地算法。

示例1輸入: [ [1,1,1], [1,0,1], [1,1,1]]輸出: [ [1,0,1], [0,0,0], [1,0,1]]
示例2輸入: [ [0,1,2,0], [3,4,5,2], [1,3,1,5]]輸出: [ [0,0,0,0], [0,4,5,0], [0,3,1,0]]

進階

  • 一個直接的解決方案是使用O(mn)的額外空間,但這並非一個好的解決方案。
  • 一個簡單的改進方案是使用O(m + n)的額外空間,但這仍然不是最好的解決方案。
  • 你能想出一個常數空間的解決方案嗎?

注意事項

  • 注意邊遍歷邊修改原數組,以避免未遍歷元素受 0 值的影響。

解法1: 記錄 0 的位置

申請額外空間去記錄須要置爲 0 的行號和列號

思路

用一個數組來儲存當前字符, 在遍歷過程當中不斷地存入不重複字符,遇到重複字符則整理數組達到字符不重複的條件。

詳解

  1. 申請O(m + n)的額外空間,聲明兩個數組,所佔空間最大值分別爲m,n分別存放水平方向、垂直方向應該重置爲零的元素下標
  2. 從上到下,從左到右依次遍歷原數組,記錄元素爲 0 的橫座標與縱座標到提早聲明的兩個需置零的數組
  3. 待遍歷結束後,再次遍歷原數組,按照兩個需置零的數組把元素置爲零。
const setZeroes = function (matrix) { const len = matrix.length; const width = matrix[0].length; const vertical = []; const horizontal = []; for (let i = 0; i < len; i++) { for (let j = 0; j < width; j++) { if (!matrix[i][j]) { vertical.push(j); horizontal.push(i); } } } for (let i = 0; i < len; i++) { if (horizontal.indexOf(i) > -1) { matrix[i].fill(0, 0, width); } for (let j = 0; j < vertical.length; j++) { matrix[i][vertical[j]] = 0; } }};

複雜度分析

  1. 時間複雜度O(m * n)

兩個遍歷,運行次數爲是m * n。所以爲O(m * n)。

  1. 空間複雜度O(m + n)

額外空間包括若干個常量和兩個長度分別爲m,n的數組,所以爲O(m + n)。

解法2: 原地算法

思路

咱們不能再額外建立數組來記錄 0 的位置,從原數組自己找突破點,用原數組的第一行和第一列記錄該行或該列需不須要置零,再根據首行首列的標識對元素置零

詳解


  1. 首先從左到右,從上到下遍歷數組中每個元素(列遍歷從第二列開始),若該元素爲 0,則同時設置改行首列元素、首行該列元素爲 0,若首列存在爲 0 元素,則設置標識,意爲首列元素會被所有置零,此目的是區分行元素與首列元素置零的標識,
  2. 而後,從右到左,從下到上遍歷數組,若首行該列或該行首列元素爲 0,則置該元素爲 0,若存在標識,則置首列元素爲 0,爲何不選擇從左到右,從上到下遍歷?是由於首行首列的先根據標誌置零,新的零元素會影響後面的數據。
const setZeroes = function (matrix) { const len = matrix.length; const width = matrix[0].length; let flag = false; for (let i = 0; i < len; i++) { if (!matrix[i][0]) { flag = true; } for (let j = 1; j < width; j++) { if (!matrix[i][j]) { matrix[i][0] = 0; matrix[0][j] = 0; } } } for (let i = len - 1; i >= 0; i--) { for (let j = width - 1; j > 0; j--) { if (!matrix[0][j] || !matrix[i][0]) { matrix[i][j] = 0; } } if (flag) { matrix[i][0] = 0; } }};

複雜度分析

  1. 時間複雜度O(m * n)

兩個遍歷,運行次數爲是m * n。所以爲O(m * n)。

  1. 空間複雜度O(1)

額外空間包括若干個常量,與m,n大小無關,所以爲O(1)。

遞增的三元子序列

給定一個未排序的數組,判斷這個數組中是否存在長度爲 3 的遞增子序列。

數學表達式以下:

若是存在這樣的 i, j, k, 且知足 0 ≤ i < j < k ≤ n-1, 使得 arr[i] < arr[j] < arr[k] ,返回 true ; 不然返回 false 。

說明: 要求算法的時間複雜度爲O(n),空間複雜度爲O(1)。

示例1輸入: [1, 2, 3, 4, 5];輸出: true;
示例2輸入: [5, 4, 3, 2, 1];輸出: false;

解法1: 不連續的遞增

思路

使用 one 和 two 兩個數。保證 one 小於 two,若遍歷到一個數大於 two,則知足3個數遞增。返回true

詳解

  1. 設置兩個變量,one 表明三元子序列的第一個,two表明三元子序列的第二個。

例如:

  • 第一個數做爲 one,第二個數若比第一個數大,這兩個數能夠成三元子序列中的前兩個,因而能夠賦值給two;
  • 第三個數比第二個數小,說明三元子序列可能會從這個數開始
  • one、two 的初始值爲 undefined,任意數與undefined作比較均爲 false。
  • 如今,開始循環遍歷 num,若:
  • num > two,說明能夠構成三元子序列了,返回 true
  • num > one,說明 num 比 two 小(或等於),比 one 大,能夠將 two 更新爲此 num,
  • num < one,則這個 num 能夠成爲三元子序列的最小者,更新 one 爲 num。
/** * @param {number[]} nums * @return {boolean} */const increasingTriplet = function (nums) { if (nums.length < 3) return false; let one, two; for (const num of nums) { if (num > two) { return true; } else if (num > one) { two = num; } else { one = num; } } return false;};

複雜度分析

  1. 時間複雜度O(n)

遍歷了1次含n個元素的空間。所以爲O(n)。

  1. 空間複雜度O(1)

遍歷過程沒有用到新的空間存儲數據,所以爲O(1)。

解法2: 貪心算法

思路

循環遍歷數組,不斷更新數組內出現的最小值與最大值,若是出現的一個大於最大值的數,則表示存在長度爲 3 的遞增子序列。

詳解

  1. 若目標數組 nums 存在遞增的三元子序列,設這三個數爲 a1,a2,a3,則 a3>a2>a1。
  2. 能夠先定義兩個變量 small, big (samll < big) 分別用於存放最小的兩個數字,在js中使用 Number.MAX_SAFE_INTEGER 常量表示最大的安全整數(maxinum safe integer)( 2 53  - 1)。
  3. 遍歷數組,實時捕獲當前最小的兩個數,同時判斷在這兩個數後方是否存在一個數字a3>small && a3>big,若存在,即該數組存在長度爲3的遞增的子序列。
/** * @param {number[]} nums * @return {boolean} */const increasingTriplet = function (nums) { let small = Number.MAX_SAFE_INTEGER; let big = Number.MAX_SAFE_INTEGER; for (let i = 0; i < nums.length; i++) { if (nums[i] <= small) { small = nums[i]; } else if (nums[i] <= big) { big = nums[i]; } else { return true; } } return false;};

複雜度分析

  1. 時間複雜度O(n)

遍歷了1次含n個元素的空間。所以爲O(n)。

  1. 空間複雜度O(1)

遍歷過程沒有用到新的空間存儲數據,所以爲O(1)。

後記

以上就是小編今天給你們分享的內容,喜歡的小夥伴記得收藏轉發,點擊在看推薦給更多的小夥伴。

大前端實踐者,專一於大前端技術領域,分享前端系統架構,框架實現原理,最新最高效的技術實踐!

關注大前端實踐者,學習前端不迷路,歡迎多多留言交流...

                                                

大前端實踐者


本文分享自微信公衆號 - 大前端實踐者(daqianduanJS)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索