列表
列表和字串是演算法中最常見的資料結構,在列表中又分為一維列表和二維列表。一維列表的資料結構即使很簡單也會有很多複雜的演算法思想。
一維列表
- 刪除排序陣列中的重複項
- 移動非0元素到頭部
- 盛最多水的容器
- 三數之和
- 長度最小的子陣列
- 無重複字元的最長子串
- 字首和
- 合併區間
二維列表
- 二維矩陣最大子矩陣和
- 旋轉影像
- 楊輝三角
- 對角線遍歷
- 矩陣元素查詢
- 容器盛水
刪除排序陣列中的重複項
題目:
給定一個排序陣列,你需要在 原地 刪除重複出現的元素,使得每個元素只出現一次,返回移除後陣列的新長度。
不要使用額外的陣列空間,你必須在 原地 修改輸入陣列 並在使用 O(1) 額外空間的條件下完成。
連結:https://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]
說明:
必須在原陣列上操作,不能拷貝額外的陣列。
儘量減少操作次數。
連結:https://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 軸共同構成的容器可以容納最多的水。
說明:你不能傾斜容器。
連結:https://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
三數之和
題目:
給一個陣列[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
雙指標小結
以上題目能力體現雙指標的使用技巧,通常使用雙指標,可以用在如下場景中:
- 從兩端向中間迭代陣列。
這時你可以使用雙指標技巧:一個指標從頭部開始,而另一個指標從尾部開始。這種技巧經常在排序陣列中使用。
- 同時有一個慢指標和一個快指標。
確定兩個指標的移動策略。與前一個場景類似,你有時可能需要在使用雙指標技巧之前對陣列進行排序,也可能需要運用貪心法則來決定你的運動策略。
長度最小的子陣列
題目:
給定一個含有 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" 是一個子序列,不是子串。
方法:判斷一個子陣列中是否有重複有很多種辦法:
- 使用暴力雙層遍歷
- 使用字典,判斷某元素是否在字典中
- 使用陣列,判斷元素是否在陣列中
方法:使用滑動窗解決:視窗左側不斷加入元素,判斷元素是否存在於視窗中,不在則繼續加入,在則將重複的元素去掉,繼續新增。在這個過程中會有一個最大值出現。
知識點
: 變長滑動視窗
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)
這一道題目還有更精彩的解法,滑動視窗不是隻能用列表實現,還可以用字串,雙指標,字典等實現。
https://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
雙層迴圈使用的比較多,記住一點,雙層迴圈能獲取任意兩個元素的組合。在這個最基礎的操作之上再去看到一些技巧,技巧是對最基礎解法的優化,所以對於一道題如果知道套路就上套路,不知道套路先用最暴力的解法,然後再往套路上靠。