演算法刷題之三一維列表

列表

列表和字元串是演算法中最常見的數據結構,在列表中又分為一維列表和二維列表。一維列表的數據結構即使很簡單也會有很多複雜的演算法思想。

一維列表

  1. 刪除排序數組中的重複項
  2. 移動非0元素到頭部
  3. 盛最多水的容器
  4. 三數之和
  5. 長度最小的子數組
  6. 無重複字元的最長子串
  7. 前綴和
  8. 合併區間

二維列表

  1. 二維矩陣最大子矩陣和
  2. 旋轉影像
  3. 楊輝三角
  4. 對角線遍歷
  5. 矩陣元素查找
  6. 容器盛水

刪除排序數組中的重複項

題目:
給定一個排序數組,你需要在 原地 刪除重複出現的元素,使得每個元素只出現一次,返回移除後數組的新長度。
不要使用額外的數組空間,你必須在 原地 修改輸入數組 並在使用 O(1) 額外空間的條件下完成。
鏈接://leetcode-cn.com/leetbook/read/top-interview-questions-easy/x2gy9m/

方法:只要將不同的數字全部集中到前面就可以了。設置雙指針,慢指針用來指向列表前部分里不重複的下標,快指針向後遍歷找不重複的數字。找到一個就讓慢指針加1,然後將快指針指向的不重複的值賦值給慢指針。
雙指針:一種常用的,好用的數據處理方式。在快速排序中,將一個數組分成大小兩個部分,用的就是雙指針。

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        #時間複雜度為O(n),空間O(1)
        # 慢指針
        i = 1
        
        # 快指針
        for j in range(1, len(nums)):
            if nums[j] != nums[j - 1]:
                nums[i] = nums[j]
                i += 1
        nums[:] = nums[:i]
        return 

地道的寫法:

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        if not nums:
            return 0
        i = 0
        for j in range(1, len(nums)):
            if nums[i] != nums[j]:
                i += 1
                nums[i] = nums[j]
        return i + 1

移動非0元素到頭部

題目:
給定一個數組 nums,編寫一個函數將所有 0 移動到數組的末尾,同時保持非零元素的相對順序。

示例:
輸入: [0,1,0,3,12]
輸出: [1,3,12,0,0]

說明:
必須在原數組上操作,不能拷貝額外的數組。
盡量減少操作次數。

鏈接://leetcode-cn.com/leetbook/read/top-interview-questions-easy/x2ba4i/
方法:將0全部集中到列表尾部,換一個思路就是將所有非0保持順序,移動到列表前部。和上一題思想類似,使用雙指針,慢指針指向0的下標,快指針向後尋找數值不為0的元素,交換快慢指針的數值,將所有0全部移動到尾部

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        # 慢指針
        i = 0
        
        # 快指針
        j = 0

        while j < len(nums):
            # 不等於快慢指針交換,慢指針遇到0時會停下來,等待快指針指向非0時的交換
            if nums[j] != 0:
                nums[i],nums[j] = nums[j],nums[i]
                i += 1
            j += 1

思想:j遇到0元素不管,i遇到0元素停下。當j再遇到0元素時,和i的元素互相交換,最終完成將非零元素向前移動的目的

def func(arr):

    i = 0
    length = len(arr)

    for j in range(len(arr)):
        if arr[j] != 0:
            arr[i],arr[j] = arr[j],arr[i]
            i = i + 1


arr = [0,1,0,3,12]
func(arr)
print(arr)

盛最多水的容器

提示:
給你 n 個非負整數 a1,a2,…,an,每個數代表坐標中的一個點 (i, ai) 。在坐標內畫 n 條垂直線,垂直線 i 的兩個端點分別為 (i, ai) 和 (i, 0) 。找出其中的兩條線,使得它們與 x 軸共同構成的容器可以容納最多的水。

說明:你不能傾斜容器。
鏈接://leetcode-cn.com/problems/container-with-most-water

輸入:[1,8,6,2,5,4,8,3,7]
輸出:49
解釋:圖中垂直線代表輸入數組 [1,8,6,2,5,4,8,3,7]。在此情況下,容器能夠容納水(表示為藍色部分)的最大值為 49。

方法:
首先想到的是暴力解法,兩層循環能夠找到任意兩個柱子之間的距離,乘以高度,取最大值即可。時間複雜度為O(2),
然後使用雙指針也能解決這個問題。以左右兩邊為邊界向中間進發,比較兩個柱子中較小的一個,並向中間移動,一直到兩個柱子相遇。在這個過程中能夠找到面積最大的那一對。

class Solution:
    def maxArea(self, height: List[int]) -> int:
        if not height:
            return 0
        n = len(height)
        max_ares = 0

        l = 0
        r = n - 1

        while l < r:
            max_ares = max(max_ares, min(height[l], height[r]) * (r-l))

            if height[l] < height[r]:
                l += 1
            else:
                r -= 1
        return max_ares

關於這一題的詳細解法:
//leetcode-cn.com/problems/container-with-most-water/solution/on-shuang-zhi-zhen-jie-fa-li-jie-zheng-que-xing-tu/

三數之和

題目:
給一個數組[4,2,6,7,4,6],找到其中三個數的和為14的所有組合
方法
三數之和是使用雙指針完成遍歷,固定住第一個,然後使用雙指針一頭一尾向中間逼近
因為我們要同時找三個數,所以採取固定一個數,同時用雙指針來查找另外兩個數的方式。所以初始化時,我們選擇固定第一個元素(當然,這一輪走完了,這個藍框框我們就要也往前移動),同時將下一個元素和末尾元素分別設上 left 和 right 指針。畫出圖來就是下面這個樣子:

當三個數的和大於0時,表明right需要左移以減少和,當小於0時,left需要右移,以增大和。
知識點: 雙指針配合循環

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:

        length = len(nums)

        arr = []
        nums.sort()
        for i in range(length - 2):
            if nums[i] > 0:
                continue
            if i > 0 and nums[i] == nums[i-1]:
                continue
            
            L = i + 1
            R = length - 1
            
            while L < R:
                s = nums[i] + nums[L] + nums[R]
                if s == 0:
                    arr.append([nums[i],nums[L],nums[R]])
                    L += 1
                    R -= 1
                    while L < R and nums[L] == nums[L-1]:L += 1
                    while L < R and nums[R] == nums[R+1]:R -= 1
                elif s > 0:
                    R -= 1
                    while L < R and nums[R] == nums[R+1]:R -= 1
                else:
                    L += 1
                    while L < R and nums[L] == nums[L-1]:L += 1
            
        return arr

雙指針小結
以上題目能力體現雙指針的使用技巧,通常使用雙指針,可以用在如下場景中:

  1. 從兩端向中間迭代數組。

這時你可以使用雙指針技巧:一個指針從頭部開始,而另一個指針從尾部開始。這種技巧經常在排序數組中使用。

  1. 同時有一個慢指針和一個快指針。

確定兩個指針的移動策略。與前一個場景類似,你有時可能需要在使用雙指針技巧之前對數組進行排序,也可能需要運用貪心法則來決定你的運動策略。

長度最小的子數組

題目:
給定一個含有 n 個正整數的數組和一個正整數 s ,找出該數組中滿足其和 ≥ s 的長度最小的 連續 子數組,並返回其長度。如果不存在符合條件的子數組,返回 0。
示例:
輸入:s = 7, nums = [2,3,1,2,4,3]
輸出:2
解釋:子數組 [4,3] 是該條件下的長度最小的子數組。

方法:使用滑動窗戶能夠完美解決這個問題。窗口右端加入元素,判斷窗口中元素總和是否大於target,如果大於計算長度,然後再左端縮小窗口,元素總和小於target,這是右端會繼續向下滑動。
知識點:變長滑動窗口

class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:


        left = 0
        res = float("inf")
        cur = 0
        for right in range(len(nums)):
            cur += nums[right]
            while cur >= s:
                print('cur:%d -left: %d' % (cur,nums[left]))
                res = min(res, right-left+1)
                cur = cur - nums[left]
                left += 1
        
        if res != float("inf"):
            return res
        else:
            return 0

無重複字元的最長子串

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

示例 1:

輸入: “abcabcbb”
輸出: 3
解釋: 因為無重複字元的最長子串是 “abc”,所以其長度為 3。

示例 2:

輸入: “bbbbb”
輸出: 1
解釋: 因為無重複字元的最長子串是 “b”,所以其長度為 1。

示例 3:

輸入: “pwwkew”
輸出: 3
解釋: 因為無重複字元的最長子串是 “wke”,所以其長度為 3。
請注意,你的答案必須是 子串 的長度,”pwke” 是一個子序列,不是子串。

方法:判斷一個子數組中是否有重複有很多種辦法:

  1. 使用暴力雙層遍歷
  2. 使用字典,判斷某元素是否在字典中
  3. 使用數組,判斷元素是否在數組中

方法:使用滑動窗解決:窗口左側不斷加入元素,判斷元素是否存在於窗口中,不在則繼續加入,在則將重複的元素去掉,繼續添加。在這個過程中會有一個最大值出現。
知識點: 變長滑動窗口

class Solution(object):
    
    def lengthOfLongestSubstring(self, s: str) -> int:
        # 字元串為空則返回零
        if not s:
            return 0

        window = []     # 滑動窗口數組
        max_length = 0  # 最長串長度

        # 遍歷字元串
        for c in s:
            # 如果字元不在滑動窗口中,則直接擴展窗口
            if c not in window:
                # 使用當前字元擴展窗口
                window.append(c)
            # 如果字元在滑動窗口中,則
            # 1. 從窗口中移除重複字元及之前的字元串部分
            # 2. 再擴展窗口
            else:
                # 從窗口中移除重複字元及之前的字元串部分,新字元串即為無重複字元的字元串
                window[:] = window[window.index(c) + 1:]
                # 擴展窗口
                window.append(c)

            # 更新最大長度
            max_length = max(len(window), max_length)

        return max_length if max_length != 0 else len(s)

這一道題目還有更精彩的解法,滑動窗口不是只能用列表實現,還可以用字元串,雙指針,字典等實現。
//leetcode-cn.com/problems/longest-substring-without-repeating-characters/solution/python-hua-dong-chuang-kou-xun-xu-jian-jin-de-3ge-/

滑動窗口小結
滑動窗口顧名思義就是對於序列取一段子序列,通常就是有一個大小可變的窗口,左右兩端方向一致的向前滑動,右端固定,左端滑動;左端固定,右端滑動。
可以想像成隊列,一端在push元素,另一端在pop元素,如下所示:

假設有數組[a b c d e f g h]
一個大小為3的滑動窗口在其上滑動,則有:

[a b c]
  [b c d]
    [c d e]
      [d e f]
        [e f g]
          [f g h]

適用範圍
1、一般是字元串或者列表
2、一般是要求最值(最大長度,最短長度等等)或者子序列
演算法思想
1、在序列中使用雙指針中的左右指針技巧,初始化 left = right = 0,把索引閉區間 [left, right] 稱為一個窗口。
2、先不斷地增加 right 指針擴大窗口 [left, right],直到窗口中的序列符合要求。
3、此時,停止增加 right,轉而不斷增加 left 指針縮小窗口 [left, right],直到窗口中的序列不再符合要求。同時,每次增加 left前,都要更新一輪結果。
4、重複第 2 和第 3 步,直到 right 到達序列的盡頭。
思路其實很簡單:第 2 步相當於在尋找一個可行解,然後第 3 步在優化這個可行解,最終找到最優解。左右指針輪流前進,窗口大小增增減減,窗口不斷向右滑動。

前綴和

給定一個整數數組和一個整數 k,你需要找到該數組中和為 k 的連續的子數組的個數。
示例 1 :
輸入:nums = [1,1,1], k = 2
輸出: 2 , [1,1] 與 [1,1] 為兩種不同的情況。
輸入:nums = [1,3,7,2,8]
輸出:2,, [3,7] [2,8]

方法:
想要獲取列表中一段子列表,使用雙層循環就可以做到。能夠獲取到任意一段子列表,然後再將這一段nums[1:j]列表的和求解出來,這樣時間複雜度會很高是O3,所以使用前綴和每一次內層循環時能夠直接算出nums[i:j]的值。
知識點:前綴和

  def subarraySum(self, nums: List[int], k: int) -> int:
        num_times = 0
        for i in range(len(nums)):
            presum = 0
            for j in range(i,len(nums)):
                presum += nums[j]
                if presum == k:
                    num_times += 1
        return num_times

其中的presum += nums[j]就是前綴和,每次內層循環的和都記錄下來,避免了計算nums[i:j]
但這種方式還不是最優,時間複雜度為O2,如果想要將時間複雜度降低到O,還需要使用字典這種方式。類似的解法是兩數之和

使用一個字典維護前綴和與其出現的次數。

前3項的和為11,k=11。第一項和+sum(3,7) = sum(1,3,7),即sum(1) + 10 = sum(1,3,7),如果存在子數組sum=10,那麼sum(1,3,7) – 10 就等於sum(1)。
即:sum(n) – k 在字典中,那麼就說明有一個子數組=k

將兩個數相加等於固定值,變成使用固定值-其中一個數=差值,判斷差值在不在列表中

def subarraySum(self, nums: List[int], k: int) -> int:
        sum_dict = {0:1}
        pre_sum = 0
        num_times = 0
        for i in range(len(nums)):
            pre_sum += nums[i]
            if pre_sum - k in sum_dict:
                num_times += sum_dict[pre_sum-k]
            if pre_sum in sum_dict:
                sum_dict[pre_sum] += 1
            else:
                sum_dict[pre_sum] = 1
       
        return num_times

合併區間

題目:
給出一組區間,請合併所有重疊的區間。
請保證合併後的區間按區間起點升序排列。

示例1
複製
[[10,30],[20,60],[80,100],[150,180]]
返回值
[[10,60],[80,100],[150,180]]

方法:區間問題兩邊都是極限,解決這一類問題需要固定住一端,然後對另一端判斷並處理
知識點:區間處理思想

class Solution:
    def merge(self , intervals ):
        if not intervals:
            return []
        intervals.sort(key=lambda x: x.start)
        res = [intervals[0]]
        
        for i in intervals[1:]:
            if res[-1].end < i.start:
                res.append(i)
            elif res[-1].end <= i.end:
                res[-1].end = i.end
        return res

一維列表小結

對於一維列表的演算法求解有很多中技巧,像上面提到的雙指針滑動窗口等。在這些技巧之下,需要對一維列表的最基礎操作有深刻認識,那就是循環。下面來總結一下一維列表的循環有什麼需要掌握的。
單層循環:單層循環能取到列表中每一個值,這沒什麼特別的。

arr = [5,2,9,6,10,6]

for i in range(len(arr)):
    print(arr[i])

5 2 9 6 10 6

雙層循環:雙層循環能對列表任意兩個元素操作,比如兩個元素之和,兩個元素之積,甚至是兩個元素中間元素的和。

arr = [5,2,9,6]

length = len(arr)

for i in range(length):
    for j in range(i, length):
        print('{}--{}'.format(arr[i], arr[j]))

5–5
5–2
5–9
5–6
2–2
2–9
2–6
9–9
9–6
6–6

雙層循環使用的比較多,記住一點,雙層循環能獲取任意兩個元素的組合。在這個最基礎的操作之上再去看到一些技巧,技巧是對最基礎解法的優化,所以對於一道題如果知道套路就上套路,不知道套路先用最暴力的解法,然後再往套路上靠。

Tags: