我是如何把簡單題目作成困難的?

你們好,我是 lucifer,衆所周知,我是一個小前端 (不是) 。其實,我是 lucifer 的 1379 號迷妹觀察員,我是一粒納米前端。(不要回答,不要回答,不要回答!!!)前端

這是第一次投稿,因此能夠廢話幾句,說一下我爲何作題和寫題解。剛開始作算法題的時候,只是純粹以爲好玩,因此不只沒有刷題計劃,寫題解也只是隨便記下幾筆,幾個月後本身也看不懂的那種。一次偶然機會發現了 lucifer 的明星題解倉庫,是找到了 onepiece 的感受。受他的啓發,我也開始寫些儘可能能讓人看懂的題解,雖然還趕不上 lucifer,但跟本身比總算是有了些進步。git

身爲迷妹觀察員,lucifer 的 91 天學算法固然是不能錯過的活動,如今活動的第二期正在 🔥 熱進行中,有興趣的同窗瞭解一下呀。言歸正傳,跟着 91 課程我再也不是漫無目的,而是計劃清晰,按照課程安排的專題來作題,這樣不只更有利於瞭解某一類題涉及的相關知識,還能熟悉這類題的套路,再次碰見類似題型也能更快有思路。github

廢話就這麼多,如下是正文部分。等等,還有最後一句,上面的"不要回答"是個三體梗,不知道有沒有人 GET 到我。算法

今天給你們帶來一道力扣簡單題,官方題解只給出了一種最優解。本文比較貪心,打算帶你們用四種姿式來解決這道題。編程

​<!-- more -->數組

題目描述

題目地址:https://leetcode-cn.com/probl...app

給定一個字符串 S 和一個字符 C。返回一個表明字符串 S 中每一個字符到字符串 S 中的字符 C 的最短距離的數組。

示例 1:

輸入: S = "loveleetcode", C = 'e'
輸出: [3, 2, 1, 0, 1, 0, 0, 1, 2, 2, 1, 0]
說明:

字符串 S 的長度範圍爲 [1, 10000]。
C 是一個單字符,且保證是字符串 S 裏的字符。
S 和 C 中的全部字母均爲小寫字母。

解法 1:中心擴展法

思路

這是最符合直覺的思路,對每一個字符分別進行以下處理:優化

  • 從當前下標出發,分別向左、右兩個方向去尋找目標字符 C
  • 只在一個方向找到的話,直接計算字符距離。
  • 兩個方向都找到的話,取兩個距離的最小值。

複雜度分析

咱們須要對每個元素都進行一次擴展操做,所以時間複雜度就是 $N$ * 向兩邊擴展的總時間複雜度。spa

而最壞的狀況是目標字符 C 在字符串 S 的左右兩個端點位置,這個時候時間複雜度是 $O(N)$,所以總的時間複雜度就是 $O(N^2)$指針

  • 時間複雜度:$O(N^2)$,N 爲 S 的長度。
  • 空間複雜度:$O(1)$。

代碼

JavaScript Code

/**
 * @param {string} S
 * @param {character} C
 * @return {number[]}
 */
var shortestToChar = function (S, C) {
  // 結果數組 res
  var res = Array(S.length).fill(0);

  for (let i = 0; i < S.length; i++) {
    // 若是當前是目標字符,就什麼都不用作
    if (S[i] === C) continue;

    // 定義兩個指針 l, r 分別向左、右兩個方向尋找目標字符 C,取最短距離
    let l = i,
      r = i,
      shortest = Infinity;

    while (l >= 0) {
      if (S[l] === C) {
        shortest = Math.min(shortest, i - l);
        break;
      }
      l--;
    }

    while (r < S.length) {
      if (S[r] === C) {
        shortest = Math.min(shortest, r - i);
        break;
      }
      r++;
    }

    res[i] = shortest;
  }
  return res;
};

解法 2:空間換時間

思路

空間換時間是編程中很常見的一種 trade-off (反過來,時間換空間也是)。

由於目標字符 CS 中的位置是不變的,因此咱們能夠提早將 C 的全部下標記錄在一個數組 cIndices 中。

而後遍歷字符串 S 中的每一個字符,到 cIndices 中找到距離當前位置最近的下標,計算距離。

複雜度分析

和上面方法相似,只是向兩邊擴展的動做變成了線性掃描 cIndices,所以時間複雜度就是 $N$ * 線性掃描 cIndices的時間複雜度。

  • 時間複雜度:$O(N*K)$,N 是 S 的長度,K 是字符 C 在字符串中出現的次數。因爲 $K <= N$。所以時間上必定是優於上面的解法的。
  • 空間複雜度:$O(K)$,K 爲字符 C 出現的次數,這是記錄字符 C 出現下標的輔助數組消耗的空間。

實際上,因爲 cIndices 是一個單調遞增的序列,所以咱們可使用二分來肯定最近的 index,時間能夠優化到 $N*logK$,這個就留給各位來解決吧。若是對二分不熟悉的,能夠看看我往期的《二分專題》

代碼

JavaScript Code

/**
 * @param {string} S
 * @param {character} C
 * @return {number[]}
 */
var shortestToChar = function (S, C) {
  // 記錄 C 字符在 S 字符串中出現的全部下標
  var cIndices = [];
  for (let i = 0; i < S.length; i++) {
    if (S[i] === C) cIndices.push(i);
  }

  // 結果數組 res
  var res = Array(S.length).fill(Infinity);

  for (let i = 0; i < S.length; i++) {
    // 目標字符,距離是 0
    if (S[i] === C) {
      res[i] = 0;
      continue;
    }

    // 非目標字符,到下標數組中找最近的下標
    for (const cIndex of cIndices) {
      const dist = Math.abs(cIndex - i);

      // 小小剪枝一下
      // 注:由於 cIndices 中的下標是遞增的,後面的 dist 也會愈來愈大,能夠排除
      if (dist >= res[i]) break;

      res[i] = dist;
    }
  }
  return res;
};

解法 3:貪心

思路

其實對於每一個字符來講,它只關心離它最近的那個 C 字符,其餘的它都無論。因此這裏還能夠用貪心的思路:

  1. 從左往右 遍歷字符串 S,用一個數組 left 記錄每一個字符 左側 出現的最後一個 C 字符的下標;
  2. 從右往左 遍歷字符串 S,用一個數組 right 記錄每一個字符 右側 出現的最後一個 C 字符的下標;
  3. 而後同時遍歷這兩個數組,計算距離最小值。

優化 1

再多想一步,其實第二個數組並不須要。由於對於左右兩側的 C 字符,咱們也只關心其中距離更近的那一個,因此第二次遍歷的時候能夠看狀況覆蓋掉第一個數組的值:

  1. 字符左側沒有出現過 C 字符
  2. i - left > right - i (i 爲當前字符下標,left 爲字符左側最近的 C 下標,right 爲字符右側最近的 C 下標)

若是出現以上兩種狀況,就能夠進行覆蓋,最後再遍歷一次數組計算距離。

優化 2

若是咱們是直接記錄 C 與當前字符的距離,而不是記錄 C 的下標,還能夠省掉最後一次遍歷計算距離的過程。

複雜度分析

上面我說了要開闢一個數組。而實際上題目也要返回一個數組,這個數組的長度也剛好是 $N$,這個空間是不可避免的。所以咱們直接利用這個數組,而不須要額外開闢空間,所以這裏空間複雜度是 $O(1)$,而不是 $O(N)$,具體能夠看下方代碼區。

  • 時間複雜度:$O(N)$,N 是 S 的長度。
  • 空間複雜度:$O(1)$。

代碼

JavaScript Code

/**
 * @param {string} S
 * @param {character} C
 * @return {number[]}
 */
var shortestToChar = function (S, C) {
  var res = Array(S.length);

  // 第一次遍歷:從左往右
  // 找到出如今左側的 C 字符的最後下標
  for (let i = 0; i < S.length; i++) {
    if (S[i] === C) res[i] = i;
    // 若是左側沒有出現 C 字符的話,用 Infinity 進行標記
    else res[i] = res[i - 1] === void 0 ? Infinity : res[i - 1];
  }

  // 第二次遍歷:從右往左
  // 找出如今右側的 C 字符的最後下標
  // 若是左側沒有出現過 C 字符,或者右側出現的 C 字符距離更近,就更新 res[i]
  for (let i = S.length - 1; i >= 0; i--) {
    if (res[i] === Infinity || res[i + 1] - i < i - res[i]) res[i] = res[i + 1];
  }

  // 計算距離
  for (let i = 0; i < res.length; i++) {
    res[i] = Math.abs(res[i] - i);
  }
  return res;
};

直接計算距離:

JavaScript Code

/**
 * @param {string} S
 * @param {character} C
 * @return {number[]}
 */
var shortestToChar = function (S, C) {
  var res = Array(S.length);

  for (let i = 0; i < S.length; i++) {
    if (S[i] === C) res[i] = 0;
    // 記錄距離:res[i - 1] + 1
    else res[i] = res[i - 1] === void 0 ? Infinity : res[i - 1] + 1;
  }

  for (let i = S.length - 1; i >= 0; i--) {
    // 更新距離:res[i + 1] + 1
    if (res[i] === Infinity || res[i + 1] + 1 < res[i]) res[i] = res[i + 1] + 1;
  }

  return res;
};

Python Code:

class Solution:
    def shortestToChar(self, S: str, C: str) -> List[int]:
        pre = -len(S)
        ans = []

        for i in range(len(S)):
            if S[i] == C: pre = i
            ans.append(i - pre)
        pre = len(S) * 2
        for i in range(len(S) - 1, -1, -1):
            if S[i] == C: pre = i
            ans[i] = min(ans[i], pre - i)
        return ans

解法 4:窗口

思路

C 當作分界線,將 S 劃分紅一個個窗口。而後對每一個窗口進行遍歷,分別計算每一個字符到窗口邊界的距離最小值,並在遍歷的過程當中更新窗口信息便可。

複雜度分析

因爲更新窗口裏的「搜索」下一個窗口的操做總共只須要 $N$ 次,所以時間複雜度仍然是 $N$,而不是 $N^2$。

  • 時間複雜度:$O(N)$,N 是 S 的長度。
  • 空間複雜度:$O(1)$。

代碼

JavaScript Code

/**
 * @param {string} S
 * @param {character} C
 * @return {number[]}
 */
var shortestToChar = function (S, C) {
  // 窗口左邊界,若是沒有就初始化爲 Infinity
  let l = S[0] === C ? 0 : Infinity,
    // 窗口右邊界
    r = S.indexOf(C, 1);

  const res = Array(S.length);

  for (let i = 0; i < S.length; i++) {
    // 計算字符到當前窗口左右邊界的最小距離
    res[i] = Math.min(Math.abs(i - l), Math.abs(r - i));

    // 遍歷完了當前窗口的字符後,將整個窗口右移
    if (i === r) {
      l = r;
      r = S.indexOf(C, l + 1);
    }
  }

  return res;
};

小結

本文給你們介紹了這道題的四種解法,從直覺思路入手,到使用空間換時間的策略,再到貪心算法思想,最後是一個簡單直白,同時複雜度也是最優的思路。

對於剛開始作題的人來講,"作出來"是首要任務,但若是你有餘力的話,也能夠試試這樣"一題多解",多鍛鍊一下本身。

但不管怎樣,只要你對算法感興趣,必定要考慮關注 lucifer 這個算法燈塔哦。不要嫌我囉嗦,真話不囉嗦。

更多題解能夠訪問:https://github.com/suukii/91-days-algorithm

end

你們對此有何見解,歡迎給我留言,我有時間都會一一查看回答。更多算法套路能夠訪問個人 LeetCode 題解倉庫:https://github.com/azl3979858... 。 目前已經 37K star 啦。你們也能夠關注個人公衆號《力扣加加》帶你啃下算法這塊硬骨頭。

我整理的 1000 多頁的電子書已限時免費下載,你們能夠去個人公衆號《力扣加加》後臺回覆電子書獲取。

相關文章
相關標籤/搜索