Leetcode題解 Python & C#:四月挑戰DAY25 Jump Game

給一串非負數列,數列上的元素是最大能跳的次數,從位置[0]出發,回傳是否能到達最後一個索引的布林值。

從位置[0]出發,並記錄剩餘的可跳次數,同時跟當前的元素取最大值。

如果在最後一個索引前可跳次數為零,也代表無法跳到終點,可以提前回傳False。順利遍歷數列的話,則回傳True。

Python

class Solution:
def canJump(self, nums: List[int]) -> bool:
remainJump = 1
nums.pop()
for num in nums:
remainJump -= 1
remainJump = max(remainJump, num)
if remainJump == 0: return False
return True

C#

public class Solution {
public bool CanJump(int[] nums) {
int remainJumps = 1;
int index = 0;
foreach(var num in nums)
{
remainJumps -= 1;
remainJumps = num > remainJumps ? num: remainJumps;
if (remainJumps==0 && index < nums.Length-1)
{
return false;
}
index++;
}
return true;
}
}

Leetcode題解 Python & C#:四月挑戰DAY24 LRU Cache

編寫一個 cache ,採取  Least recent used的方式替換已滿cache。

這道題目,有一個關鍵特點,就是會依照最近的使用次序來決定哪一個要在已滿的情形被先換掉。

所以,要有個能紀錄使用次序的方式。

如果某鍵被使用到,那就把它挪到最後方;如果需要替換,就先刪除第一個鍵。

還要有個記錄對應值的 hashtable ,如果使用 get() ,才能從其中撈出值來。

要實現這樣的功能,python有 OrderedDict 類別可以使用(from collections import OrderedDict),這是我覺得最快速的方式。

即使不用,用 dict 與 list 也能做出一樣的機制,只是慢上許多。

不然還有一種方式,使用 linkedlist 做出來,因為只要改變指向,所以會比起用 list 刪改還快上不少。


Python

class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.cache = {}
self.cachelist = []

def get(self, key: int) -> int:
if key in self.cache:
self.cachelist.remove(key)
self.cachelist.append(key)
return self.cache[key]
else:
return -1

def put(self, key: int, value: int) -> None:
if key in self.cache:
self.cachelist.remove(key)
elif len(self.cachelist) == self.capacity:
del self.cache[self.cachelist[0]]
del self.cachelist[0]
self.cache[key] = value
self.cachelist.append(key)

C#

public class LRUCache {
public int cap;
public Dictionary cache = new Dictionary();
public List cachelist = new List();

public LRUCache(int capacity) {
cap = capacity;
}

public int Get(int key) {
if (cache.ContainsKey(key))
{
cachelist.Remove(key);
cachelist.Add(key);
return cache[key];
}
else
{
return -1;
}
}

public void Put(int key, int value) {
if(cache.ContainsKey(key))
{
cachelist.Remove(key);
}
else if(cachelist.Count == cap)
{
cache.Remove(cachelist[0]);
cachelist.Remove(cachelist[0]);
}
cachelist.Add(key);
cache[key] = value;

}
}

Python 練習 網路爬蟲 scrapy

看到有徵網路爬蟲的工讀生,我也想打個小工,如果時間可以配合的話。

但我已經有段時間沒有寫爬蟲程式,爬來佔硬碟空間,因為沒有想到要的資料,但我電腦每天都會運行爬蟲程式。

再次練習,除了css selector等的選擇器經驗不足,感覺自己寫得還有很多可以強化效率的部分。

這次要針對「缺失值」作更好的處理,因為大部分的爬蟲教學是不考慮爬取失敗的時候。

時代變遷,網頁也會改版,沒有永遠適用的爬蟲。一旦不能用,或是抓取的部分跑掉,這是一定要馬上處理好的事情。

因此,注重例外處理與警示是實戰必寫的。當然不寫例外直接跳出也是一種變向的警示,前提是要留意到。

況且,加載失敗也是會讓本來設定提取的部分失敗,導致爬取意外中止。格式跑掉並非是自身的問題,但是會害到你。

使用框架,可以大幅省去針對例外而寫處理的時間,因此我練習 scrapy。


—-分隔線—-

首先,我建議使用 anaconda ,從官網下載的。

如果是第一次用,從「本機」>「內容」>「進階」>「環境變數」>「系統變數」清單「Path」>「編輯」,新增三個 「(anaconda的位置)」、「(anaconda的位置)\Scripts」、「(anaconda的位置)\Library\bin」」

這樣就可以用「字元提示命令」(cmd)去執行python(anaconda環境)。

在cmd輸入「pip install scrapy」安裝「scrapy」,安裝完成就可以用「scrapy startproject projectname」建立scrapy專案。

(小技巧:如果我想在 A資料夾 建立專案,在「檔案總管」進入 A資料夾 ,在上方路徑輸入 cmd 後按enter, 就能以當前資料夾為工作路徑執行cmd,這樣使用建立專案指令也會建在當前的資料夾

我使用 visual studio 把 scrapy 建立的專案都加進來,這樣方便編輯。

官網教程學習是不錯的。

如果會使用 requests、BeautifulSoup,寫出scrapy spider絕對沒問題是。

這裡提一下scrapy的優勢,在 selector 沒有選到會是 None,而不像 BeautifulSoup 會報錯,用try except會是效率的拖油瓶。(從leetcode的解題觀察到的)

而且 scrapy 的框架有 items ,把資料統一丟到自定義物件內,省版面也美觀。如果沒用scrapy,我大概也會寫個類別接收資料。

異步爬取也是一點,寫 multiprocessing 或 threading,也是一門功夫,既然會想高效,這自然也是得寫的,如果不用scrapy的話。

資料終究要寫進資料庫,那為什麼一開始就不寫好資料庫的管道?這一塊 scrapy 也幫你準備好了,在 pipelines 這裡。

今天我終於開啟了雲端資料庫,我使用mongoDB Altas。我覺得最難的部分總是一開始的陌生,辦好帳號卻不知道要怎麼下一步。

辦好之後,從「SANDBOX」> Closter > 「CONNECT」 可以設定連線代碼,這裡相當貼心,它可以生成可直接用的連線代碼(password要自己改),複製到code中用pymongo模組連線即可。(參考)

成功連線並寫入一筆資料順利在cloud看到,我已心滿意足,最苦惱的不知道要塞什麼資料。

想學 scrapy ,也可以看 youtube,能搜到不錯的教學,所以我也不贅述。

這裡提供一個 xpath 選擇器的進階用法。 response.xpath(‘A | B’),用這個就能抓下ptt文章的內文,包括內的文字,我目前沒看到範例有這樣寫的。

相較於 css ,我覺得 xpath 更精確,但我也才昨天開始接觸。 BeautifulSoup 好像沒有 xpath 可用。

目前最讓我苦手的還是 jsonp 仍舊沒有除了 selenium 以外的解法,關於 jsonp 討論不多,jsonp 屬於動態加載,我好像也是為此才看看 scrapy 。

Leetcode題解 Python & C#:四月挑戰 Bitwise AND of Numbers Range

給一個範圍 [m, n] where 0 <= m <= n <= 2147483647,回傳範圍內所有(整數)的 位元且 結果。

說位元計算是我很少碰到的一塊,不熟悉。

設想一個攤開的情形,答案會是最高位起的連續相同部分取 1, 剩下取 0。

1111011
1100000
11xxxxx => x都會出現0

正因為這是二進位,所以 m, n 之間的數字會使得在最高位連續相同的部分之後的各位置都會有 0 的出現。

因此將著重在如何得到目標部分。

如果 m, n 不相同,m, n往右移一位,直到最高位連續相同的部分時,m == n (※這二者不斷位移後的情形)

由於 m, n 都被動過,到這裡要向左移還原剛右移的部分,因此在剛右移的部分加一變數記錄次數。


Python

class Solution:
def rangeBitwiseAnd(self, m: int, n: int) -> int:
i = 0
while m != n:
m >>= 1
n >>= 1
i += 1
return m << i

C#

public class Solution {
public int RangeBitwiseAnd(int m, int n) {
int times = 0;
while(m != n)
{
m >>= 1;
n >>= 1;
times++;
}
return m << times;
}
}

Leetcode題解 Python & C#:四月挑戰DAY22 Subarray Sum Equals K

給一串整數數列與整數 k ,請回傳有多少個連續子數列總和為 k 。

看到這種 sum 的問題,心中就先想到 k – sum 是否在某一資料(也許 dict 或 list)內,那資料也是某種 sum。

k – sumA = sumB 利用「交換律」來解。

剛好是連續子數列,加上提示有說到「 sum(i,j) = sum(0,j) – sum(0,i)」,這點出了該如何搭配上交換律。

把出現過的 sum 值保存起來,接著再用 k – sum 去找是否它存在於剛剛保存下來的各sum值內。這是核心的想法。

這裡按照實際撰寫的順序。

先把 0 加入到 memo,其值為 1 ,代表 0 出現過一次。

用一個for迴圈,得到 sum(0, i),檢查 k – sum(0,i) 是否在 memo 內,如果有則 result += memo[sum(0,i)]。

接著再把 sum(0,i) 加入 memo 中,使 memo 保存各值的出現次數。

Python

class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
result = 0
nsum = 0
memo = {0:1}
for num in nums:
nsum += num
if nsum-k in memo:
result += memo[nsum-k]
memo[nsum] = memo.get(nsum, 0) +1
return result

C#

public class Solution {
public int SubarraySum(int[] nums, int k) {
int nsum = 0, result = 0;
var memo = new Dictionary();
memo.Add(0,1);
foreach(int num in nums)
{
nsum += num;
if (memo.ContainsKey(nsum - k))
{
result += memo[nsum - k];
}
if (memo.ContainsKey(nsum))
{
memo[nsum]++;
}
else
{
memo.Add(nsum, 1);
}
}
return result;
}
}

倉頡輸入法與打字手勢的一周學習歷程

約莫上周二晚間,我測驗我的中打速度,結果只有七十出頭,相當不滿意。再加上那時我的英打一直有按錯鍵的問題,於是想改正我的打字手勢。

渴望變厲害的性格作祟,喚醒心中強大的精力去改變習慣。

習倉頡並不是平易近人的,我發覺網路是有教材,但是幾乎都蠻舊的,說實在也是可用的。

先記憶字根,光是這點就蠻花費時間的。我建議可以在練習中記憶,這樣也可以幫助到拆解字型取碼。

這裡我是使用《五色倉頡試用版》,達到第一階段「記字根」的基本功。一直打,然後配合對照表去理解。

直到裡面的字已經熟練如何拆解。順利的話,一天就能打出許多字。

之後可以進行生活化的練打,不過一定會碰到不會拆解的字,就使用《Follow Me倉頡字典》查詢。


練習的時候,一定要把每一個不會的用查詢的字再打個幾遍,因為下一次還是會碰到,不會就還是得再查詢。

接著,此時一定要去注意字體組合,大部分的字碼輸入是相當有規則的,例如:「見」、「刂」、「頁」、「寺」,熟悉後,可快上一點。

一開始一定很不順手,集中注意力時,也要順便將字碼的位置用肌肉記憶下來,換句話說,就是得不看鍵盤就能找到字碼的位置。

最好連帶將標準打字手勢擺出來,記憶相對位置。這裡推薦使用倉頡之星,使用按鍵練習模式,練到8秒內可以不用看就能盲打即可。(我從別人的網誌下載的倉頡之星 )

我覺得不必瘋狂練打,一天兩小時就可以休息。也不用透過這些軟體練習到每分鐘三十字,生活常用打熟來,自然也就足以擔當你平時用的中文輸入法。

生活常見的詞彙要一定要去用空白鍵去看選詞的種類,一旦背下來,就能打字速度更上一層。

如果可以每分鐘十字以上,接下來的打字就不可以看鍵盤,太注意自己的手指有沒有按對,反而會拖累自己的打字速度。

只要靜下心,能慢慢按出對的字碼來就可以了。錯誤率小於三成就可以盲打階段。

「絕對不可以看鍵盤﹐因為你要把心思拿去思考如何拆字。」一旦做到,每分鐘又可以多打五個字。

我目前大約打字速度落在每分鐘十七字左右,還是會在每次練打使用字典去查拆解。

據這個網頁說,二到四周就能突破每分鐘三十字,如果有心學習就可以。

最近用上番茄鐘,這兩天開始記錄,如果速度呈直線增長,大概可以在第二周時進入到每分鐘三十字,但可能會早一兩天也說不定。

Leetcode題解 Python & C#:四月挑戰DAY21 Leftmost Column with at Least a One

給一個 BinaryMatrix 類別的物件,該物件有個由 0, 1 組成的二維串列屬性,要找出至少有個1的欄位最小索引,沒有則回傳-1。

該物件有兩個方法,一 get(),只能透過此方法去取得特定位置上的值。二 dimensions(),此方法會回傳列欄大小。

每列的值有經過排序(由小至大)。

如果呼叫 get() 超過1000次,也算失敗。

欄列的大小不超過100。

開始思考要如何找出目標值,除了每排有經過排序,但每欄並沒有提及。

因此除非知道每排最早出現 1 的位置,不然也暫時沒想到更好的方法。

get() 次數限制之下,每排要在 10 次內找出第一個 1 出現位置,最糟要在一百個已經排序的元素中搜查。

使用二分搜查法,50 > 25 > 13 > 7 > 4 > 2 > 1 ,最糟也能在七次內找到第一個 1 的位置。 萬一沒有,索引也會在等於欄數。這樣也能在限制內完成搜查。

Python

class Solution:
def leftMostColumnWithOne(self, binaryMatrix: 'BinaryMatrix') -> int:
rows, cols = binaryMatrix.dimensions()
memo = []
for row in range(rows):
l, r = 0 , cols
while l < r:
m = (r-l)//2 + l
if binaryMatrix.get(row, m) == 0:
l = m + 1
else:
r = m
memo.append(r)
return min(memo) if min(memo) < rows else -1

C#

class Solution {
public int LeftMostColumnWithOne(BinaryMatrix binaryMatrix) {
int rows = binaryMatrix.Dimensions()[0];
int cols = binaryMatrix.Dimensions()[1];
// 使用 Binary search,取得每行上最小的1位置
int minPos = cols;
int r,m,l;
for(int i = 0; i < rows; i++)
{
l = 0;
r = cols;
while(l < r)
{
m = (r-l)/2+l;
if(binaryMatrix.Get(i,m)==0)
{
l = m + 1;
}
else
{
r = m;
}

}
minPos = r < minPos ? r: minPos;
}
return minPos < cols ? minPos: -1;
}
}

Leetcode題解 Python & C#:四月挑戰DAY20 Construct Binary Search Tree from Preorder Traversal

給一串依 preorder 順序遍歷二分樹的值,要還原成二分樹,並回傳根。

這二分樹是嚴格的那種,代表左邊子樹皆小於自身,右邊皆大於自身。

依照 preorder 的順序,先自身,再左邊,再右邊。還原也依此進行。

說到要依順序還原,不管是哪種,都要有保留父節的規劃,因為子節是沒辦法往上回溯的。(或者使用遞迴函數)

※一開始先將 root 生成,最後要回傳它。

這裡使用 stack 的概念來保存父節,一旦目前迴圈的值小於 stack[-1] ,那就添加至左邊,並加入該節至 stack。

如果該節大於 stack[-1],則檢查 stack[-2] 是否小於目前的值,是則不斷刪去 stack[-1] 直到 stack[-2] 大於目前的值或 stack 只剩一個。

然後把該節指派給 stack[-1].rihgt 後刪除 stack[-1],並添加該節至 stack 中。

這做法類似實際 preorder 的途徑。

Python3

class Solution:
def bstFromPreorder(self, preorder: List[int]) -> TreeNode:
root = TreeNode(preorder.pop(0))
stack = [root]
while preorder:
node = TreeNode(preorder.pop(0))
if node.val < stack[-1].val:
stack[-1].left = node
stack.append(node)
else:
while len(stack) >= 2 and stack[-2].val < node.val:
del stack[-1]
stack.pop().right = node
stack.append(node)
return root

C#

public class Solution {
public TreeNode BstFromPreorder(int[] preorder) {
TreeNode root = new TreeNode(preorder[0]);
List stack = new List{root};
for(int i = 1; i < preorder.Length; i++)
{
TreeNode node = new TreeNode(preorder[i]);
if(node.val < stack[stack.Count-1].val)
{
stack[stack.Count-1].left = node;
stack.Add(node);
}
else
{
while(stack.Count>=2 && stack[stack.Count-2].val < node.val)
stack.RemoveAt(stack.Count-1);
stack[stack.Count-1].right = node;
stack.RemoveAt(stack.Count-1);
stack.Add(node);
}
}
return root;
}
}

除了 stack ,也可以使用「遞迴」解決。
Python 搭配二分搜查

class Solution:
def bstFromPreorder(self, preorder: List[int]) -> TreeNode:
if not preorder: return None
def nextNode(l, r):
if l==r: return None
node = TreeNode(preorder[l])
l2, r2 = l+1, r
while l2 < r2:
m = (r2-l2)//2 + l2
if preorder[m] < node.val:
l2 = m+1
else:
r2 = m
node.left = nextNode(l+1, l2)
node.right = nextNode(l2, r)
return node
return nextNode(0, len(preorder))

C# Linq篩選

public class Solution {
public TreeNode BstFromPreorder(int[] preorder) {
if(preorder.Length == 0){return null;}
TreeNode node = new TreeNode(preorder[0]);
node.left = BstFromPreorder((from num in preorder where num < preorder[0] select num).ToArray());
node.right = BstFromPreorder((from num in preorder where num > preorder[0] select num).ToArray());
return node;
}
}

Leetcode題解 Python & C#:四月挑戰DAY19 Search in Rotated Sorted Array

今天有C#,因為我看蠻多ASP的工作缺,練習ASP也得加強C#。

給一串數組,經過排序但旋轉過(向右移動?次),且每個數字不重複,並且要求在時間複雜度 O(log n) 解決。

最直覺地使用 for 迴圈,但時間複雜度會是 O(n) ,不符合要求。

說到 O(log n) ,二分搜尋是最典型的方法。用兩次二分搜尋,一次是找到排序的分界(最大值),二是從分界往左或右找到目標值。O(log 2n) => O(log n)

設左界 l 右界 r,中間為 m = (r-l)/2 + l 。第一次找將比對目標 t 先設為 nums[0] ,如果 nums[m] > t ,更新 t 並移動左界 l = m+1,否則不更新 t 但移動右界 r = m。

直到 l == r,此時為 nums[l] or nums[r] 都是最大值。

若目標值 target 小於 nums[0] ,表示目標值在最大值右方,否則在最大值左方。針對情況移動右界至nums的長度或左界至0。

再用一次二分搜尋法,這次的比對目標就是 target,當 l 等於 r 時沒有找到就回傳 -1 ,其餘為當下的索引 m 。


PYTHON3

class Solution:       
def search(self, nums: List[int], target: int) -> int:
if not nums: return -1
maxNum = nums[0]
n = len(nums)
l, r = 0, n
while l < r:
m = (r-l)//2 + l
if nums[m] >= maxNum:
maxNum = nums[m]
l = m+1
else:
r = m
#
if target < nums[0]: r = n
else: l = 0
#
while l < r:
m = (r-l)//2 + l
if nums[m] == target:
return m
elif nums[m] > target:
r = m
else:
l = m + 1
return -1

C#

public class Solution {
public int Search(int[] nums, int target) {
int r = nums.Length;
if(r==0){return -1;}
int l = 0;
int maxNum = nums[0];
// 先找出最大值的位置
while(l < r){
int m = (r-l)/2+l;
if(nums[m] >= maxNum){
maxNum = nums[m];
l = m+1;
}else{
r = m;
}
}
//從中間開始找
if(target < nums[0]){
r = nums.Length;
}else{
l = 0;
}
//
while(l < r){
int m = (r-l)/2+l;
if(nums[m] == target){
return m;
}else if(nums[m] < target){
l = m+1;
}else{
r = m;
}
}
return -1;
}
}

Leetcode題解 Python:四月挑戰DAY18 Minimum Path Sum

給一串二維數列(格線),每個元素為非零整數,路徑只能往右或往下,找出從左上角到右下角的最短距離。

一看到往右或往下的二種可能性,DP就蠢蠢欲動,而且同個位置上的最短距離是固定的,完全可以使用DP去解。

dfs遞迴函數保存兩個訊息 y, x 記錄回傳值。自身加往下或住右較小的,即當前位置的最小距離。

如果到達邊界,由於 min() 不能比較 int 與 None , 因此得針對邊界的下個距離做處理,這裡使用 [] 把下個距離放入串列再做比較。

class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
rows, cols = len(grid), len(grid[0])

from functools import lru_cache
@lru_cache(None)
def dfs(y,x):
if y == rows or x == cols:return []
d2 = dfs(y+1, x) + dfs(y, x+1)
distance = grid[y][x] + (min(d2) if d2 else 0)
return [distance]

return dfs(0,0)[0]

又或者迴圈所有位置,每個位置加上左方與上方的最小值,等到迴圈結束,此時右下角為最短距離。

Leetcode題解 Python:四月挑戰DAY17 Number of Islands

給一個二維串列,”1″代表是陸地,”0″則為水,周圍(超出範圍)都是水,每個不相連的陸地為一塊島,回傳該二維串列有幾塊島?

這題若用迴圈,要如何判定當前遇到的”1″是獨立的島嶼?是否曾經有找過?

使用 memo 去記錄已經搜尋過的”1″,並且遇到”1″的時候用遞迴函數再往四周找出所有鄰近的”1″

class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
if not grid: return 0
landNums = 0
memo = set()
rows, cols = len(grid), len(grid[0])

def search(y,x):
if y == rows or y < 0 or x == cols or x < 0:
return
if (y,x) in memo: return
else: memo.add((y,x))
if grid[y][x] == "1":
search(y-1,x)
search(y+1,x)
search(y,x-1)
search(y,x+1)

for y in range(rows):
for x in range(cols):
if (y,x) not in memo and grid[y][x] == "1":
search(y,x)
landNums += 1

return landNums

如果想省點空間,把傳入的二維串列當成 memo 使用也是一種選擇,還能稍微加速。

Leetcode題解 Python:四月挑戰DAY16 Valid Parenthesis String

給一字串,只有「 ()* 」組成,( 的右邊要對應一個 ),而 * 可以是 ( 或 ) 或 “”,如果()都有對應則回傳True;否則False。

這題前身是只有 () 的條件,只需要計 ( 與 ) 的數量。

多了一個 * 有三種可能性,光是看到這個條件就直覺想到用DP。

這裡嘗試用計數的方法︰

若 ( 與 * 的數量加總小於 ) ,則可以中途回傳False。

接下來要考慮 **(() 與 ((**) 要如何抓出剩餘沒有配對到的 ( 。

同樣遇到 ( 而計數加一,有沒有能忽略情況一前面的 * 而遇到 ( 時才開始計數?而且情況二也能適用?

如果先 ( 後 *,* 可抵 ( 的計數,如果不夠,( 的計數最小為 0 ,當作 * 為 “” 出現。

那如果是情況二,那 ( 計數被 ) 抵消到 -1 該怎麼看?由於在一開始的條件判斷下已經不會有 ) 找不到左邊對的情況,因此可視為遇到 * 來處理。

整理一下有兩種計數:
1. ( 和 * 的加總去抵消 ) , 一旦小於 0 即 False。
2. ( 去抵消 * 和 ),最小值為 0,最後若有剩,代表 ( 找不到右邊可對。

class Solution:
def checkValidString(self, s: str) -> bool:
c1, c2 = 0, 0
for c in s:
c1 += 1 if c != ')' else -1
c2 += 1 if c == '(' else (-1 if c2 else 0)
if c1 < 0: return False
return c2 == 0