花了十幾天频轿,把《算法》看了一遍然后重新 AC 了一遍 LeetCode 的題逆日,收獲頗豐余黎。這次好好記錄下心得。
我把所有做題的代碼都放在 github 上以供參考鸟雏。
項目地址:https://github.com/violetjack/LeetCodeACByJS
題目地址:https://leetcode.com/problemset/top-interview-questions/
說來慚愧享郊,之前寫的《LeetCode 邏輯題分享》其實自己動手做的比較少,都是看解決方案孝鹊。更加關(guān)鍵的是我沒有系統(tǒng)地去學習過算法(自學的編程)炊琉。所以導致以下幾個問題:
- 看題不懂方法論,理解他人方案困難又活。
- 解題方法通過看別人的方案去歸納苔咪,照著抄。(其實都是有系統(tǒng)的算法寫法的)
- 很多題目看了答案只是知其然而不知其所以然柳骄。
- 很多答案(討論區(qū)的方案)是有錯誤的团赏,卻把它當正確答案來發(fā)。
之后耐薯,我看了《算法(第4版)》一書舔清,重新去做并且試著去 AC 題目,問題又是一堆堆的曲初。所以這次比第一次刷題時間要久不少体谒。
各類題的解決方案
話不多說,系統(tǒng)整理下解題的一些算法和解決方案
二叉樹
二叉樹大多使用遞歸的方式左右兩個元素向下遞歸臼婆。比如:
計算二叉樹最大深度
var maxDepth = function (root) {
if (root == null) return 0
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right))
};
將二叉樹以二維數(shù)組形式表現(xiàn)
var levelOrder = function(root) {
let ans = []
helper(root, ans, 0)
return ans
};
function helper(node, ans, i){
if (node == null) return
if (i == ans.length) ans.push([])
ans[i].push(node.val)
helper(node.left, ans, i + 1)
helper(node.right, ans, i + 1)
}
都是通過遞歸方式逐層向下去查找二叉樹數(shù)據(jù)抒痒。
可能性問題
這類題一般是告訴你一組數(shù)據(jù),然后求出可能性颁褂、最小值或最大值故响。比如:
給定幾種面額的硬幣和一個總額,使用最少的硬幣湊成這個總額颁独。
var coinChange = function (coins, amount) {
let max = amount + 1
let dp = new Array(amount + 1)
dp.fill(max)
dp[0] = 0
for (let i = 1; i < max; i++) {
for (let j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1)
}
}
}
return dp[amount] > amount ? -1 : dp[amount]
};
使用了動態(tài)規(guī)劃(DP)被去,將從 0 到目標額度所需的最小硬幣數(shù)都列出來。
求出從矩陣左上角走到右下角奖唯,且只能向右向下移動惨缆,一共有多少種可能性。
var uniquePaths = function (m, n) {
const pos = new Array(m)
for (let i = 0; i < m; i++) {
pos[i] = new Array(n)
}
for (let i = 0; i < n; i++) {
pos[0][i] = 1
}
for (let i = 0; i < m; i++) {
pos[i][0] = 1
}
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
pos[i][j] = pos[i - 1][j] + pos[i][j - 1]
}
}
return pos[m - 1][n - 1]
};
這題就是使用了動態(tài)規(guī)劃逐步列出每一格的可能性丰捷,最后返回右下角的可能性坯墨。
獲取給定數(shù)組連續(xù)元素累加最大值
var maxSubArray = function (nums) {
let count = nums[0], maxCount = nums[0]
for (let i = 1; i < nums.length; i++) {
count = Math.max(count + nums[i], nums[i])
maxCount = Math.max(maxCount, count)
}
return maxCount
};
上面這題通過不斷對比最大值來保留并返回最大值。
其實病往,可能性問題使用動態(tài)規(guī)劃要比使用 DFS捣染、BFS 算法更加簡單而容易理解。(我使用 DFS 經(jīng)常報 TLE)
查找
一般遇到的查找問題停巷,如查找某個值一般會用到一下方法:
- 排序算法(排序便于查找)
- 二分查找
- 索引移動查找(這個方法名自己想的耍攘,大概就這個意思~)
查找橫向和縱向都遞增的二維矩陣中的某個值
var searchMatrix = function (matrix, target) {
if (matrix.length == 0) return false
let row = 0, col = matrix[0].length - 1
while (true) {
if (matrix[row][col] > target && col > 0) {
col--
} else if (matrix[row][col] < target && row < matrix.length - 1) {
row++
} else if (matrix[row][col] == target) {
return true
} else {
break
}
}
return false
};
先將位置定位在右上角榕栏,通過改變位置坐標來找到目標值。使用了索引移動查找法來找到結(jié)果蕾各。
找到數(shù)組中最左邊和最右邊的某個數(shù)字所在位置
var searchRange = function (nums, target) {
let targetIndex = binarySearch(nums, target, 0, nums.length - 1)
if (targetIndex == -1) return [-1, -1]
let l = targetIndex, r = targetIndex
while(l > 0 && nums[l - 1] == target){
l--
}
while(r < nums.length - 1 && nums[r + 1] == target){
r++
}
return [l, r]
};
function binarySearch(arr, val, lo, hi) {
if (hi < lo) return -1
let mid = lo + parseInt((hi - lo) / 2)
if (val < arr[mid]) {
return binarySearch(arr, val, lo, mid - 1)
} else if (val > arr[mid]) {
return binarySearch(arr, val, mid + 1, hi)
} else {
return mid
}
}
這題使用二分法來查找到某個目標數(shù)字的索引值扒磁,然后索引移動法分別向左和向右查找字符。獲取左右兩側(cè)的索引值返回式曲。
回文
所謂回文妨托,就是正著讀反著讀是一樣的。使用索引兩邊向中間移動的方式來判斷是否為回文吝羞。
找到給定字符串中某段最長的回文
var longestPalindrome = function (s) {
let maxLength = 0, left = 0, right = 0
for (let i = 0; i < s.length; i++) {
let singleCharLength = getPalLenByCenterChar(s, i, i)
let doubleCharLength = getPalLenByCenterChar(s, i, i + 1)
let max = Math.max(singleCharLength, doubleCharLength)
if (max > maxLength) {
maxLength = max
left = i - parseInt((max - 1) / 2)
right = i + parseInt(max / 2)
}
}
return s.slice(left, right + 1)
};
function getPalLenByCenterChar(s, left, right) {
// 中間值為兩個字符兰伤,確保兩個字符相等
if (s[left] != s[right]){
return right - left
}
while (left > 0 && right < s.length - 1) {
left--
right++
if (s[left] != s[right]){
return right - left - 1
}
}
return right - left + 1
}
路徑題
路徑題可以使用深度優(yōu)先(DFS)和廣度優(yōu)先(BFS)算法來做。我比較常用的是使用 DFS 來做钧排。通過遞歸將走過的路徑進行標記來不斷往前找到目標路徑敦腔。如:
通過給定單詞在二維字母數(shù)組中查找是否能使用鄰近字母組成這個單詞(212題)
let hasWord = false
var findWords = function (board, words) {
var ans = []
for (let word of words) {
for (let j = 0; j < board.length; j++) {
for (let i = 0; i < board[0].length; i++) {
if (board[j][i] == word[0]) {
hasWord = false
DFS(word, board, 0, j, i, "")
if (hasWord) {
if (!ans.includes(word))
ans.push(word)
}
}
}
}
}
return ans
};
function DFS(word, board, index, j, i, subStr) {
if (word[index] == board[j][i]) {
subStr += board[j][i]
board[j][i] = "*"
if (j < board.length - 1)
DFS(word, board, index + 1, j + 1, i, subStr)
if (j > 0)
DFS(word, board, index + 1, j - 1, i, subStr)
if (i < board[0].length - 1)
DFS(word, board, index + 1, j, i + 1, subStr)
if (i > 0)
DFS(word, board, index + 1, j, i - 1, subStr)
board[j][i] = word[index]
}
if (index >= word.length || subStr == word) {
hasWord = true
}
}
由于 DFS 是一條路走到黑,如果每個元素都去使用 DFS 來找會出現(xiàn)超時的情況恨溜。如果條件允許(如查找遞增數(shù)組)可以通過設(shè)置緩存來優(yōu)化 DFS 查找超時問題会烙。
獲取二維矩陣中最大相鄰遞增數(shù)組長度。
const dirs = [[0, 1], [1, 0], [0, -1], [-1, 0]]
var longestIncreasingPath = function (matrix) {
if (matrix.length == 0) return 0
const m = matrix.length, n = matrix[0].length
let max = 1
let cache = new Array(m)
for (let i = 0; i < m; i++){
let child = new Array(n)
child.fill(0)
cache[i] = child
}
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
let len = dfs(matrix, i, j, m, n, cache)
max = Math.max(max, len)
}
}
return max
}
function dfs(matrix, i, j, m, n, cache){
if (cache[i][j] != 0) return cache[i][j]
let max = 1
for (let dir of dirs){
let x = i + dir[0], y = j + dir[1]
if(x < 0 || x >= m || y < 0 || y >= n || matrix[x][y] <= matrix[i][j]) continue;
let len = 1 + dfs(matrix, x, y, m, n, cache)
max = Math.max(max, len)
}
cache[i][j] = max
return max
}
將已使用 DFS 查找過的長度放入緩存筒捺,如果有其他元素走 DFS 走到當前值,直接返回緩存最大值即可纸厉。
鏈表
鏈表從 JS 的角度來說就是一串對象使用指針連接的數(shù)據(jù)結(jié)構(gòu)系吭。合理使用 next
指針改變指向來完成對鏈表的一系列操作。如:
鏈表的排序:
var sortList = function (head) {
if (head == null || head.next == null) return head
let prev = null, slow = head, fast = head
while (fast != null && fast.next != null) {
prev = slow
slow = slow.next
fast = fast.next.next
}
prev.next = null;
let l1 = sortList(head)
let l2 = sortList(slow)
return merge(l1, l2)
};
function merge(l1, l2) {
let l = new ListNode(0), p = l;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
p.next = l1;
l1 = l1.next;
} else {
p.next = l2;
l2 = l2.next;
}
p = p.next;
}
if (l1 != null)
p.next = l1;
if (l2 != null)
p.next = l2;
return l.next;
}
使用了自上而下的歸并排序方法對鏈表進行了排序颗品。使用 slow.next
和 fast.next.next
兩種速度獲取鏈表節(jié)點肯尺,從而獲取中間值。
鏈表的倒序
var reverseList = function(head) {
let ans = null,cur = head
while (cur != null) {
let nextTmp = cur.next
cur.next = ans
ans = cur
cur = nextTmp
}
return ans
};
排序
排序和查找算是算法中最重要的問題了躯枢。常用的排序算法有:
- 插入排序
- 選擇排序
- 快速排序
- 歸并排序
- 計數(shù)排序
更多排序算法的知識點可參考《JS家的排序算法》则吟,文章作者圖文并茂的講解了各種排序算法,很容易理解锄蹂。
舉幾個排序算法的栗子:
求數(shù)組中第K大的值
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function (nums, k) {
for (let i = 0; i <= k; i++) {
let max = i
for (let j = i; j < nums.length; j++) {
if (nums[j] > nums[max]) max = j
}
swap(nums, i, max)
}
return nums[k - 1]
};
function swap(arr, a, b) {
let tmp = arr[a]
arr[a] = arr[b]
arr[b] = tmp
}
使用了選擇排序排列了前 K 個值得到結(jié)果氓仲。
對有重復值的數(shù)組 [2,0,2,1,1,0]
排序
var sortColors = function (nums) {
sort(nums, 0, nums.length - 1)
};
function sort(arr, lo, hi) {
if (hi <= lo) return
let lt = lo, i = lo + 1, gt = hi;
let v = arr[lo]
while (i <= gt) {
if (arr[i] < v) swap(arr, lt++, i++)
else if (arr[i] > v) swap(arr, i, gt--)
else i++
}
sort(arr, lo, lt - 1)
sort(arr, gt + 1, hi)
}
function swap(arr, a, b) {
let x = arr[a]
arr[a] = arr[b]
arr[b] = x
}
這種有重復值的使用三向切分的快速排序是非常好的解決方案。當然得糜,計數(shù)排序法可是不錯的選擇敬扛。
還有之前提到的鏈表的排序使用的是歸并排序。
算術(shù)題
算術(shù)題看似簡單朝抖,但是遇到最大的問題就是:如果使用累加啥箭、累成這種常熟級別的增長,遇到很大的數(shù)字會出現(xiàn) TLE (超出時間限制)治宣。所以急侥,我們要用指數(shù)級別的增長來找到結(jié)果砌滞。如:
計算 x 的 n 次方
var myPow = function (x, n) {
if (n == 0) return 1
if (n < 0) {
n = -n
x = 1 / x
}
return (n % 2 == 0) ? myPow(x * x, parseInt(n / 2)) : x * myPow(x * x, parseInt(n / 2));
};
一開始我使用了 x*x 這么乘上 n 次,但是遇到 n 太大就直接超時了坏怪。使用以上方案:29 = 2 * 44 = 2 * 82 = 2 * 64 = 128
直接從常熟級變化變?yōu)橹笖?shù)級變化贝润,這一點在數(shù)學運算中是需要注意的。
求 x 的平方根
var mySqrt = function (x) {
let l = 0, r = x
while (true) {
let mid = parseInt(l + (r - l) / 2)
if (mid * mid > x) {
r = mid - 1
} else if (mid * mid < x) {
if ((mid + 1) * (mid + 1) > x) {
return mid
}
l = mid + 1
} else {
return mid
}
}
};
這題使用二分法來找到結(jié)果陕悬。
二進制問題
二進制問題题暖,一般使用按位運算符和二進制轉(zhuǎn)換 Number.parseInt()
和 Number.prototype.toString()
來解決。
將一個32位數(shù)字的二進制進行倒序
var reverseBits = function(n) {
var t = n.toString(2).split("");
while(t.length < 32) t.unshift("0"); // 插入足夠的 0
return parseInt(t.reverse().join(""), 2);
};
常用算法
講了這么多捉超,其實除了常用的排序胧卤、搜索,其他最常用的就是 DP拼岳、DFS枝誊、BFS 這三個算法了∠е剑可以這么說:掌握了排序和這三個算法叶撒,可以 AC 大多數(shù)的算法問題。這么牛逼的算法了解一下耐版?
簡單說說幾種排序和查找
- 冒泡排序:遍歷數(shù)組祠够,對比元素和后面相鄰元素,如果當前元素大于后面元素粪牲,調(diào)換位置古瓤。這樣從頭遍歷到尾,獲取最后一位排序玩的元素腺阳。然后在 1 到 n - 1 中再次重復以上步驟落君。直到最后第一和第二個元素對比大小。是一種從后往前的排序亭引。
- 選擇排序:遍歷數(shù)組绎速,找到最小的元素位置,與第一個元素調(diào)換位置焙蚓,然后縮小范圍從第二個元素開始遍歷纹冤,如此重復到最后一個元素」汗可以從后往前也可以從前往后排序赵哲。
function sort(arr) {
const len = arr.length
for (let i = 0; i < len; i++) {
let min = i
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[min]) min = j
}
swap(arr, i, min)
console.log(arr)
}
return arr
}
- 插入排序:遍歷數(shù)組,選中某一個元素君丁,與前面相鄰元素對比枫夺,如果當前元素小于之前元素,調(diào)換位置绘闷,繼續(xù)對比直到當前元素前的元素小于當前元素(或者到最前面)橡庞,如此對所有元素排序一遍较坛。是一種從前往后的排序。
function sort(arr) {
const len = arr.length
for (let i = 1; i < len; i++) {
for (let j = i; j > 0 && arr[j] < arr[j - 1]; j--) {
swap(arr, j, j - 1)
console.log(arr)
}
}
return arr
}
- 希爾排序:類似于插入排序扒最,選中一個元素與元素前 n 個元素進行比大小和調(diào)換位置丑勤。之后再縮小 n 的值。這種方法可以減少插入排序中最小值在最后面吧趣,然后需要一個一個調(diào)換位置知道最前面這類問題法竞。減少調(diào)換次數(shù)。是一種從前往后的排序强挫。
- 歸并排序:在《算法》中提到了兩種歸并排序:一種是自上而下的歸并排序岔霸。將數(shù)組不斷二分到最小單位(1到2個元素)將他們進行排序,之后將前兩個和后兩個元素對比俯渤,如此往上最后完成整個數(shù)組的排序呆细。還有一種自下而上的歸并排序是直接將數(shù)組分割為若干個子數(shù)組進行排序然后合并。
let aux = new Array(arr.length)
function sort(arr, lo, hi) {
if (hi <= lo) return
let mid = lo + (parseInt((hi - lo) / 2))
sort(arr, lo, mid)
sort(arr, mid + 1, hi)
merge(arr, lo, mid, hi)
}
function merge(arr, lo, mid, hi) {
let i = lo, j = mid + 1
for (let k = lo; k <= hi; k++) {
aux[k] = arr[k]
}
for (let k = lo; k <= hi; k++) {
if (i > mid) arr[k] = aux[j++]
else if (j > hi) arr[k] = aux[i++]
else if (aux[j] < aux[i]) arr[k] = aux[j++]
else arr[k] = aux[i++]
}
console.log(arr)
}
- 快速排序:選定第一個值為中間值八匠,然后將小于中間值的元素放在中間值的左側(cè)而大于中間值的元素放在中間值右側(cè)絮爷,然后對兩側(cè)的元素分別再次切割,直到最小單位梨树。
function sort(arr, lo, hi) {
if (hi <= lo + 1) return
let mid = partition(arr, lo, hi) // 切分方法
sort(arr, lo, mid)
sort(arr, mid + 1, hi)
}
function partition(arr, lo, hi) {
let i = lo, j = hi + 1
let v = arr[lo]
while(true) {
while(arr[++i] < v) if (i == hi) break
while(v < arr[--j]) if (j == lo) break
if ((i >= j)) break
swap(arr, i, j)
console.log(arr)
}
swap(arr, lo, j)
console.log(arr)
return j
}
- 三向切分的快速排序:類似于快速排序坑夯,優(yōu)化點在于如果某個元素等于切分元素,元素位置不變抡四。最后小于切分元素的到左邊柜蜈,等于切分元素的根據(jù)數(shù)量放在中間,大于切分元素的放在右邊床嫌。適用于有大量相同大小元素的數(shù)組。
function sort(arr, lo, hi) {
if (hi <= lo) return
let lt = lo, i = lo + 1, gt = hi;
let v = arr[lo]
while (i <= gt) {
if (arr[i] < v) swap(arr, lt++, i++)
else if (arr[i] > v) swap(arr, i, gt--)
else i++
console.log(arr)
}
sort(arr, lo, lt - 1)
sort(arr, gt + 1, hi)
}
- 堆排序:堆排序可以說是一種利用堆的概念來排序的選擇排序胸私。使用優(yōu)先隊列返回最大值的特性逐個返回當前堆的最大值厌处。
- 計數(shù)排序:就是將數(shù)組中所有元素的出現(xiàn)次數(shù)保存在一個數(shù)組中,然后按照從小到大返回排序后的數(shù)組岁疼。
- 桶排序:其實就是字符串排序的 LSD 和 MSD 排序阔涉。LSD 使用索引計數(shù)法從字符串右邊向左邊移動,根據(jù)當前值進行排序捷绒。而 MSD 是從左到右使用索引計數(shù)法來排序瑰排,在字符串第一個字符后,將字符串數(shù)組分為若干個相同首字符串的數(shù)組各自進行第二暖侨、第三次的 MSD 排序椭住。
- 二分查找: 對有序數(shù)組去中間值與目標值相比對。如果目標值小于中間值字逗,取前一半數(shù)組繼續(xù)二分京郑。如果目標值大于中間值宅广,取后一半數(shù)組繼續(xù)二分。如果目標值等于中間值些举,命中跟狱!
DP
關(guān)于動態(tài)規(guī)劃,可以看下詳解動態(tài)規(guī)劃——鄒博講動態(tài)規(guī)劃一文户魏,其中講了路徑驶臊、硬幣、最長子序列叼丑。都是 LeetCode 中有的題目关翎。
我的理解:動態(tài)規(guī)劃就是下一狀態(tài)可以根據(jù)上一狀態(tài),或之前幾個狀態(tài)獲取到的一種推理過程幢码。
DFS
深度優(yōu)先搜索(DFS)就是選中某條從條件1到條件2的某條可能性進行搜索笤休,之后返回搜索其他一條可能性,如此一條條升入症副。舉個栗子店雅,如果有5條路,那么 DFS 算法就是只排出一個斥候先走一條路走到底去偵察贞铣,如果走不通那么返回走下一條路徑闹啦。
DFS(頂點v)
{
標記v為已遍歷;
for(對于每一個鄰接v且未標記遍歷的點u)
DFS(u);
}
DFS 使用的是遞歸的方式進行搜索的辕坝。
示例:在二維字母矩陣中查找是否能夠使用相鄰字母組成目標單詞窍奋。
var exist = function (board, word) {
for (let y = 0; y < board.length; y++) {
for (let x = 0; x < board[0].length; x++) {
if (find(board, word, y, x, 0)) return true
}
}
return false
};
function find(board, word, y, x, d) {
if (d == word.length) return true
if (y < 0 || x < 0 || y == board.length || x == board[y].length) return false;
if (board[y][x] != word[d]) return false
let tmp = board[y][x]
board[y][x] = "*"
let exist = find(board, word, y, x + 1, d + 1)
|| find(board, word, y, x - 1, d + 1)
|| find(board, word, y + 1, x, d + 1)
|| find(board, word, y - 1, x, d + 1)
board[y][x] = tmp
return exist
}
BFS
廣度優(yōu)先搜索(BFS)就是將從條件1到條件2的所有可能性都列出來同步搜索的過程。適用于查找最短路徑酱畅。舉個栗子琳袄,如果有5條路,那么 BFS 算法就是分別向5條路排出斥候去偵察纺酸。
BFS()
{
輸入起始點窖逗;
初始化所有頂點標記為未遍歷;
初始化一個隊列queue并將起始點放入隊列餐蔬;
while(queue不為空)
{
從隊列中刪除一個頂點s并標記為已遍歷碎紊;
將s鄰接的所有還沒遍歷的點加入隊列;
}
}
BFS是使用數(shù)組存儲下一頂點的方式樊诺。
示例:每次改變一次字母仗考,通過給定數(shù)組中的單詞,從單詞 A 變?yōu)閱卧~ B词爬。(127題)
/**
* @param {string} beginWord
* @param {string} endWord
* @param {string[]} wordList
* @return {number}
*/
var ladderLength = function (beginWord, endWord, wordList) {
if (!wordList.includes(endWord)) return 0
let set = new Set(),
visited = new Set(),
len = 1
set.add(beginWord)
visited.add(beginWord)
while (set.size != 0) {
let tmp = new Set([...set])
for (let w of tmp) {
visited.add(w)
set.delete(w)
if (changeOneChar(w, endWord))
return len + 1
for (let word of wordList){
if (changeOneChar(w, word) && !visited.has(word)){
set.add(word)
}
}
}
len++
}
return 0
};
function changeOneChar(a, b) {
let count = 0
for (let i = 0; i < a.length; i++)
if (a[i] != b[i])
count++
return count == 1
}
最后
寫下 AC 一遍題目之后的收獲秃嗜。
- 知道了方法論,做起題來輕松了不少。
- 遇到問題多找輪子痪寻,一定有某種方法論可以用螺句。
- 不要耍小聰明用一些奇巧淫技,思路不對再怎么繞都是浪費時間橡类。
- 不要想著自己造輪子(特別是算法方面)蛇尚,絕大多數(shù)問題前輩一定有更好更完善的方案在。自己造輪子費時費事又沒太大意義顾画。
- 看答案和自己做是兩回事取劫,自己動手實現(xiàn)了才能算是會了。
- 算法之所以存在研侣,就是用來適應(yīng)某些場景谱邪、解決某類問題的。在對的場景選擇對的算法才能體現(xiàn)算法的價值庶诡,不要濫用算法惦银。
- 沒必要把所有算法都精通,但起碼在遇到問題時可以找到最優(yōu)算法解決問題末誓。即知道算法的存在及其用途扯俱,按需深入學習。
其實刷算法題還是很有趣的事情喇澡,之后計劃把 LeetCode 題庫中的所有問題都刷一遍~
PS:本文以及相關(guān)項目中有任何錯誤或者可以改進的地方迅栅,還請?zhí)岢觥9餐M步~