第二輪 Python 刷題筆記一:數組

經過四十多天緩慢的刷題,現在進度大概是刷了八十多道 LeetCode 題,最近也在吸取過來人的經驗,仍然需要對刷題計劃進行調整。

首先明確下目標,我是有了 Python 基礎,刷題是想掌握更多提升代碼質量的算法、接觸並瞭解更底層的原理和知識點。

結合着目標,便很快找到之前刷題過程中存在的不足:

  1. 經常花費大量時間冥思苦想某道題,最終可能採用辛苦的方法做出來,就這麼提交後沒有繼續跟進和整理,錯過相關更巧妙算法知識的學習。
  2. 之前的模式是刷完題後寫題解,回顧下最初思路,代碼實現加註釋,比對下時間空間表現,時間充裕的話優化下——這就側重點帶偏到“完成任務”了,不過最近開始慢慢在調整
  3. 從最初的按題號順序刷,到之後按專題刷,對題目和類型的選擇都太隨意,缺乏系統和章法。

意識到這些,不妨把之前刷題規劃到第一階段,算是熟悉 LeetCode 題目和培養刷題習慣吧。接下來我們也將結合網上搜羅來的有效建議,對之後刷題模式做一番優化:

  1. 遇到新題目,5-10 分鐘考慮,有思路就代碼實現,沒有頭緒則果斷學習、模仿一直到可以獨立寫出題解
  2. 題解要精簡記錄核心思路以及思路的轉變過程,尤其是再遇到類似的如何去抓到思考點
  3. 專題的選擇更有條理,爭取所有專題過一遍後,常見的題型都能涵蓋,且相互間能加深聯繫
  4. 新一輪刷題過程中,添加時間空間複雜度的分析,也要可以練習優化代碼

以上便是暫時注意到的點,接下來開始第二輪的第一篇筆記~

之所以會對時間空間複雜度有所強調,一來是之前自己會覺得這個很難分析,二來是最近接觸的幾個算法課程開篇都是圍繞複雜度展開的,學下來之後發現並沒有想象中那麼複雜。早學早好,掌握其知識點後,之後可以通過題目不斷加深自己對複雜度的相關理解。

時間複雜度

時間複雜度通常表示爲 O(f(n)),常見的 f(n) 有七種:常數複雜度 O(1)、對數複雜度 O(logn)、線性時間複雜度 O(n)、平方 O(n2)、立方 O(n3)、指數 O(2**n)、階乘 O(n!)。

可能這時候我們第一輪的刷題會發揮作用了,回想之前接觸到了諸多不同的算法:比如二分查找,其時間複雜度是 O(logn):對 8 個數二分查找,通過 log2 8 = 3 次操作便可實現;再比如二叉樹遍歷,其時間複雜度是 O(n),這裏 n 是對應二叉樹中所有節點,遍歷所有節點即與 n 成線性關係。

這裏值得注意的點是在時間複雜度中,常數係數是不考慮的,O(2n) 也是按 O(n) 來計。

空間複雜度

空間複雜度我們先簡單理解:若涉及到數組,考慮數組的長度;若涉及到遞歸,則考慮遞歸的最大深度。二者均有,取其最大值。

後面配合着具體題目我們通過實踐來加深理解。

題目一

LeetCode 第283題:移動零

難度:簡單

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

示例:

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

說明:

必須在原數組上操作,不能拷貝額外的數組;儘量減少操作次數。

自行嘗試

既然要移動所有 0,那麼遍歷整個數組是必須的;但不能拷貝額外數組,遍歷過程中對數組的移動操作就要處理好。最基礎的想法是遍歷遇到 0 時刪除該項,在結尾在補上 0,代碼寫出來這樣:

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        # 因爲刪除元素會改變數組,這裏採用 while 循環來控制遍歷
        i = 0
        # count 用來記錄檢測到 0 的個數,也用來控制 while 的過程
        count=0
        # 當刪除 0 時,數組的座標會前移,最末位座標爲原座標減去已檢測 0 的個數
        while i<len(nums)-count:
        	# 若檢測到 0
            if nums[i]==0:# 移除該位,此操作時間複雜度 O(n)
                nums.pop(i)
                # 結尾添 0,此操作時間複雜度 O(1)
                nums.append(0)
                # 已檢測到的 0 個數 +1
                count+=1
            # 若未檢測到0,移動到下一位
            else:
                i+=1

這是很簡單直接的思路,但要注意到列表中 list.pop(i) 這種操作,刪除掉 i 這位後,其後面的所有位都要前移,若 i 在最前,其時間複雜度是 O(n),若每次遇到 0 都這麼操作,時間複雜度是比較高的。

在此基礎上優化的話,可以檢測到 0 時,交換 0 與下一位非 0 的值。交換值的好處在於不用每次對其它值都進行操作,只在必要時進行調整。但可能會遇到連續出現 0 的情況,這就要有額外的標記來區分是 0 還是非 0 了。

首先就還是借用剛代碼中的 count 變量記錄檢測到的 0 的個數,檢測到 0 時,我們需要把這位換成下一個非 0 的數,此數的座標即其原座標減去其前面 0 的個數。換個理解,也可以是我們不再移動 0,而是將非 0 元素與其前面的 0 依次交換,代碼實現:

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:        
        # 方法二
        count = 0
        for i in range(len(nums)):
            if nums[i]==0:
                count += 1
            # 只有出現 0 才進行換位
            elif count>0:
                nums[i-count],nums[i] = nums[i],0

這裏要注意只有出現了 0 才交換,即代碼中檢測非 0 時還要對 count 0 的個數做個判斷。此外,同樣的思路,我們只關注非 0 元素,將數組中非 0 的元素重新排到數組中,剩餘的位置補上 0,代碼實現:

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:        
        #方法三
        j = 0
        for i in range(len(nums)):
            if nums[i]!=0:
            	# 通過 j 索引,只記錄非 0 元素
                nums[j] = nums[i]
                j+=1
        # 剩餘的位置全部賦爲 0
        for k in range(j,len(nums)):
            nums[k] = 0

以上便是我能想到的思路和代碼,接下來我們看下國外投票最高的 Python 題解代碼。

學習題解

這個條經驗是在算法訓練課裏看到的,在做完題目後,不妨去海外站上看下討論區中投票最高的解法,這也是很好的學習別人優秀代碼的實踐。

# in-place
class Solution:
	def moveZeroes(self, nums):
	    zero = 0  # records the position of "0"
	    for i in range(len(nums)):
	        if nums[i] != 0:
	            nums[i], nums[zero] = nums[zero], nums[i]
	            zero += 1
# 來源:https://leetcode.com/problems/move-zeroes/discuss/72012/Python-short-in-place-solution-with-comments.

看完這個,其實和之前我們想到的利用 j 下標重組非 0 元素是相似的,但更妙的是代碼直接將 nums[i] 和 nums[zero] 的值對調,這怎麼理解呢?

首先,非 0 元素時,zero 和 i 一起遞增,但如果下一位是 0,那麼 nums[zero] 此時爲 0,因爲其最後有個 zero += 1;這樣當再次遇到非 0 元素時,便可通過將 nums[zero] 和 nums[i] 互換,將 0 換到此刻的最末尾。這樣遍歷結束時便實現了將所有 0 後移。

交換值時間複雜度 O(1) 放在遍歷過程中,整體時間複雜度是 O(n),交換過程設計的是真的妙。但如果還是很難想通,不妨改寫成如下:

class Solution:
	def moveZeroes(self, nums):
        # 方法四
        j = 0
        for i in range(len(nums)):
            if nums[i]!=0:
                nums[j]=nums[i] 
                # 若 i j 不等,說明有出現 0,將末位賦爲 0               
                if i!=j:
                    nums[i]=0                
                j+=1

之前代碼中 nums[zero] 作用是將 0 傳遞出去,這裏我們直接將 nums[i] 賦值爲 0 也是同樣效果,但要有個對 i 和 j 的判斷,只有二者不等時才這樣補 0 至末位。

題目二

LeetCode 第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。
你不需要考慮數組中超出新長度後面的元素。

來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array

自行嘗試

注意題目中的數組是排過序的,基礎想法是,遍歷過程中檢測到當前項與前一項相同時,移除該項,但無論是 pop(i) 還是 remove 其時間複雜度都是 O(n),所以我們還是採用對原數組重新賦值的形式,利用額外的 k 索引,只在出現不同元素時 k 才增加、並更新 nums[k] 的值。遍歷結束後,將 k 位之後的元素通過 pop() 來逐位刪掉,注意,pop() 刪最後一位的時間複雜度就是 O(1) 了,因爲不會導致其他位置的改變。落實到代碼:

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        j = 1
        for i in range(len(nums)):
            if i>0 and nums[i]!=nums[i-1]:
                nums[j] = nums[i]
                j+=1
        # 將最後多餘的位刪掉
        for k in range(j,len(nums)):
            nums.pop()
        return len(nums)

這樣,在原數組上操作,時間複雜度控制在 O(n) 級別。

學習題解

觀摩了點贊最高的 Python 題解,就是在剛我們代碼基礎上刪掉後面的 pop() 的代碼,直接返回 j:

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:      
        x = 1
        for i in range(len(nums)-1):
            if(nums[i]!=nums[i+1]):
                nums[x] = nums[i+1]
                x+=1
        return(x)
# 來源:https://leetcode.com/problems/remove-duplicates-from-sorted-array/discuss/302016/Python-Solution

感覺這是比較“雞賊”,充分利用題目規則,因爲題目規則中有個說明:

說明:
爲什麼返回數值是整數,但輸出的答案是數組呢?

請注意,輸入數組是以「引用」方式傳遞的,這意味着在函數裏修改輸入數組對於調用者是可見的。
你可以想象內部操作如下:

// nums 是以“引用”方式傳遞的。也就是說,不對實參做任何拷貝
int len = removeDuplicates(nums);
// 在函數裏修改輸入數組對於調用者是可見的。
// 根據你的函數返回的長度, 它會打印出數組中該長度範圍內的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}
#來源:力扣(LeetCode)
#鏈接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array

換言之,刪不刪後面的元素並不影響題目判斷,所以不刪除可以節省時間獲得更好的表現。

同時,在參考中文題解時,會發現,這也被成爲“雙指針”、“快慢指針”,很明顯,i 是遍歷整個數組的“快指針”,我們額外定義的是有所篩選的“慢指針”,核心並不是指針如何去設計,而是具體過程中如何去操作的考慮。

題目三

LeetCode 第 70 題:爬樓梯

難度:簡單

假設你正在爬樓梯。需要 n 階你才能到達樓頂。

每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢?

注意:給定 n 是一個正整數。

示例 1:

輸入: 2
輸出: 2
解釋: 有兩種方法可以爬到樓頂。
1.  1 階 + 1 階
2.  2 階
示例 2:

輸入: 3
輸出: 3
解釋: 有三種方法可以爬到樓頂。
1.  1 階 + 1 階 + 1 階
2.  1 階 + 2 階
3.  2 階 + 1 階

#來源:力扣(LeetCode)
#鏈接:https://leetcode-cn.com/problems/climbing-stairs

自行嘗試

當積累一定刷題經驗後,便很容易分辨出這是和斐波那契數列相關的題目,因爲題目中呈現的規律是 f(n) = f(n-1) + f(n-2)。對於相關解法,看似省事的可能是遞歸,但其時間複雜度是 O(2^n) 指數級別,所以直接忽略不去考慮。比較常用的可能是如下兩種解法,首先是用列表記錄出現的項:

class Solution:
    def climbStairs(self, n: int) -> int:
    	# 列表記錄前兩項的值
        result = [1,1]
        # 從第三項開始循環
        for i in range(2,n+1):
        	# 當前項爲前兩項之和
            result.append(result[i-2]+result[i-1])
        # 返回所需要的項
        return result[n]

這裏的操作是對數組遍歷,其中的操作是 append()、O(1) 級別的,所以整體下來時間複雜度爲 O(n),額外建立了一個記錄各項的列表,故空間複雜度也爲 O(n)。常見繼續優化的方式就是將這個列表給拿掉,因爲我們每次只要記錄前兩項值即可,不用記錄所有值,代碼實現:

class Solution:
    def climbStairs(self, n: int) -> int:
		# 最初兩項值
        a,b =1,1
        for i in range(n):
        	# 預先將 a+b 賦值給b,充當緩存,下次便可賦到 a 上
            a,b = b,a+b
        return a

這麼下來,時間複雜度是 O(n),空間複雜度就降到了常數級 O(1)。

這個解法可以熟記應用,推薦題解也基本是此寫法或相應的變形。

通過如上的簡單題目熟悉上手,接下來便是中等難度的兩道題整理:

題目四

LeetCode 第 11 題:盛最多水的容器

難度:中等

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

說明:你不能傾斜容器,且 n 的值至少爲 2。

示意圖

示例:
輸入:[1,8,6,2,5,4,8,3,7]
輸出:49

自行嘗試

這題目第一遍刷題時做過,現在就要結合回憶來闡述思路:採用雙指針、雙下標分別從最左和最右側出發,記錄此時左右邊界較小的高度值作爲容器高,座標差爲容器底,計算面積。要想獲取更大的容積,在底在要變小的情況下,只能通過增加高度來實現,所以若指針處的高度不高於容器高,便可移動該指針。若左右指針可以生成更高的容器高,便可刷新容器高度,計算此時容積,若超過原容積則更新最大值、若未超出則繼續移動指針,代碼實現如下:

class Solution:
    def maxArea(self, height: List[int]) -> int:
    	# 左右邊界雙指針
        l,r = 0,len(height)-1
        # 容器高度取其中較小值
        h = min(height[l],height[r])
        # 底
        w = r-l
        # 容積或面積
        s = h*w
        # 指針循環
        while l<r:
        	# 若指針高度小於當前容器高,移動指針
            if height[l]<=h:
                l+=1
            elif height[r]<=h:
                r-=1
            # 若出現指針高度大於容器高
            else:
            	# 更新容器高
                h = min(height[l],height[r])
                # 更新容積最大值
                s = max(s,(r-l)*h)
        # 返回最大容積
        return s

我們也可以分析其複雜度:因爲整個過程是對整個數組的遍歷,其中操作只有移動指針、比較高度和計算的面積,整體下來時間複雜度 O(n);沒有額外的列表數組,所以空間複雜度爲 O(1)。

參考題解

Python 題解中有份思路略不同的:

def maxArea(self, height):
    L, R, width, res = 0, len(height) - 1, len(height) - 1, 0
    for w in range(width, 0, -1):
        if height[L] < height[R]:
            res, L = max(res, height[L] * w), L + 1
        else:
            res, R = max(res, height[R] * w), R - 1
    return res
#來源:https://leetcode.com/problems/container-with-most-water/discuss/6131/O(N)-7-line-Python-solution-72ms

它這裏是對底的長遍歷,從最大的底開始一直到 0,也是對應我們之前雙指針縮小範圍的過程,這裏的設計是,確定了底,來選高,計算面積,最終保留面積的最大值、高度的較小值。

題目五

LeetCode 第 15 題:三數之和

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

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

示例:

給定數組 nums = [-1, 0, 1, 2, -1, -4],

滿足要求的三元組集合爲:
[
  [-1, 0, 1],
  [-1, -1, 2]
]
#來源:力扣(LeetCode)
#鏈接:https://leetcode-cn.com/problems/3sum

自行嘗試

這題之前也做過,但再做卻沒思路了。能想到的就是先對數組排序,遍歷確定第一個數,再其後面的列表元素中遍歷確定第二個數,通過 0 減去二者的和得出第三個數的值,檢測剩餘列表是否存在第三個數。若存在,檢測該組合是否出現過,若未出現,添加到結果中,返回最終結果。這解法擱在之前看到“超出時間限制”就不再多想了,現在正好分析下其時間複雜度:首先遍歷第一個數 O(n),此時遍歷 for 中繼續嵌套 for 來遍歷第二個數,複雜度來到了 O(n^2),在對第三數檢測時,我們取巧可能採用 if third in rest_list 這種形式,但實際其複雜度仍很高,這麼算下來應該是立方級別。此外,我們還需要檢測結果中是否有該組合,若重複則不添加。整體下來,超時是理所當然的。

思考期間,並沒能想到解法,雖然之前參考題解將這題解決了,但現在卻一點印象都沒。

參考題解

剛分析的時間複雜度爲 O(n^3),對第一個數遍歷在所難免,但在接下來確定剩餘兩數過程,可以採用雙指針法,以此將複雜度降到平方級別。同時,我們可以將數組先排序,這樣在移動指針過程中,對重複出現的元素進行跳過,以此規避出現重複結果,從而不用檢測結果中是否包含當前解,降低時間複雜度。

因爲是參考了題解,所以懲罰自己重新默寫代碼來記憶:

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
    	# 先對列表排序,以此方便控制重複情況出現
        nums.sort()
        result = []
        # 第一個數索引爲 i
        for i in range(len(nums)-2):
        	# x y 爲後兩個數的索引,雙指針
            x,y = i+1,len(nums)-1
            # 若最小值爲正或最大值爲負,都不能出現和爲 0,直接返回空列表
            if nums[i]>0 or nums[y]<0:
                return result
            # 若第一個數出現與先前一樣,則跳過
            if i>0 and nums[i]==nums[i-1]:
                continue
            # 雙指針循環
            while x<y:
            	# 當前三者組合
                temp = [nums[i],nums[x],nums[y]]
                # 三者和
                sum_3 = nums[i]+nums[x]+nums[y]
                # 若三數和爲 0
                if sum_3==0:
                	# 加入到結果中
                    result.append(temp)
                    # 若第二、三項出現重複,不斷移動其指針
                    while x<y and nums[x+1]==nums[x]:
                        x+=1
                    while x<y and nums[y-1]==nums[y]:
                        y-=1
                    # 此時再移動下雙指針,使其不與當前項相等
                    x+=1
                    y-=1
                # 若和小於 0 ,移動 x 增加和
                elif sum_3<0:
                    x+=1
                # 若小於 0,左移 y 指針
                else:
                    y-=1
        return result

雖是重寫,但其中還是有犯錯的點,比如 if i>0 and nums[i]==nums[i-1]: 這裏,是檢測與之前一項是否相同,而不能檢測與之後一項是否相同。因爲比如 [-2,-1,-1,0,1,2] 若檢測與之後一項相同跳過的話,就會把 [-1,-1,2] 的解遺失。以及在檢測第二三項重複情況時,while x<y and nums[x+1]==nums[x] 這裏要加個 x<y 的限制以避免 x+1 超出列表索引範圍。

代碼來看的話,時間複雜度降到了 O(n^2),因爲第一層 for 之內的雙指針移動是 O(n) 級別時間複雜度。空間複雜度的話,因爲 result 是題目要求的列表,這個是必需的,理論上是不用考慮在內。

除此之外我們的 temp = [nums[i],nums[x],nums[y]] 看着是在 while 循環中,每次都生成一個列表,那麼整個過程下來豈不佔用了很多空間和無用的列表?這裏正好運用下最近學到的 Python 語言中的垃圾回收機制:採用的是引用計數機制爲主,標記-清除和分代收集兩種機制爲輔的策略。引用計數法中,當對象的別名被賦予新的對象時,舊對象引用計數 -1,在我們代碼中就會被回收掉了,所以這裏不用考慮因此導致的額外空間。

Python 垃圾回收機制介紹:https://testerhome.com/topics/16556

綜合看來,空間複雜度是 O(1) ,接下來我們繼續觀摩下更優題解。

不得不佩服,點贊最高的題解自有其價值所在:

class Solution(object):
	def threeSum(self, nums):
        res = []
        nums.sort()
        length = len(nums)
        for i in range(length-2): #[8]
            if nums[i]>0: break #[7]
            if i>0 and nums[i]==nums[i-1]: continue #[1]
            l, r = i+1, length-1 #[2]
            while l<r:
                total = nums[i]+nums[l]+nums[r]

                if total<0: #[3]
                    l+=1
                elif total>0: #[4]
                    r-=1
                else: #[5]
                    res.append([nums[i], nums[l], nums[r]])
                    while l<r and nums[l]==nums[l+1]: #[6]
                        l+=1
                    while l<r and nums[r]==nums[r-1]: #[6]
                        r-=1
                    l+=1
                    r-=1
        return res
# 來源:https://leetcode.com/problems/3sum/discuss/232712/Best-Python-Solution-(Explained)

這是其代碼,過程與我們的類似,當然,感覺我參考的題解可能最終來源也是這份代碼,其說明中針對代碼中註釋的編號來解讀。

First, we sort the array, so we can easily move i around and know how to adjust l and r.
If the number is the same as the number before, we have used it as target already, continue. [1]
We always start the left pointer from i+1 because the combination of 0~i has already been tried. [2]
Now we calculate the total:
If the total is less than zero, we need it to be larger, so we move the left pointer. [3]
If the total is greater than zero, we need it to be smaller, so we move the right pointer. [4]
If the total is zero, bingo! [5]
We need to move the left and right pointers to the next different numbers, so we do not get repeating result. [6]
We do not need to consider i after nums[i]>0, since sum of 3 positive will be always greater than zero. [7]
We do not need to try the last two, since there are no rooms for l and r pointers.
You can think of it as The last two have been tried by all others. [8]

此外對時間空間複雜度的分析上:

For time complexity
Sorting takes O(NlogN)
Now, we need to think as if the nums is really really big
We iterate through the nums once, and each time we iterate the whole array again by a while loop
So it is O(NlogN+N**2)~=O(N^2)
For space complexity
We didn’t use extra space except the res
So it is O(1).

時間複雜度:數組排序 O(nlogn),第一層遍歷數組 O(n),雙指針遍歷 O(n),總體 O(nlogn) + O(n) * O(n) 約爲 O(n^2),我們之前忽略了最初對列表的排序操作。

注:Python 中的 sort 和 sorted 排序內部實現機制爲 Timsort,最壞時間複雜度爲:O(n logn),空間複雜度爲 O(n)
參考鏈接:https://www.cnblogs.com/clement-jiao/p/9243066.html

但貌似看到的題解都沒對排序的空間複雜度進行考慮,畢竟 sort 也沒產生額外數組。

數組篇小結

因爲是第二輪刷題,可能會更注重題目解法的整理和總結,五道題目中三道簡單、兩道中等難度,題目中多可以運用額外的列表空間或額外的指針來協助解決。結合着算法課程裏面提到過要想降低時間複雜度,要麼提升維度,要麼空間來換取時間。

對數組類題目解決過程中,如果沒思路,就先考慮暴力的窮舉思路,對其中過程可以採取指針優化(比如題目五)。要有雙指針的概念,以及在對數組操作過程中選取時間複雜度低的方法來實施。

這次整理比較長,沒法做到每天一更,但學習效果會可能比之前的總結要更有效,所以近期會繼續以類似的筆記完成第二輪對不同類型題目的整理,鞏固以及提高刷題效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章