回溯法理論基礎 LeetCode 77. 組合 LeetCode 216.組合總和III LeetCode 17.電話號碼的字母組合

目錄

回溯法理論基礎

回溯法

回溯法的效率

用回溯法解決的問題

如何理解回溯法

回溯法模板

LeetCode 77. 組合

回溯算法的剪枝操作

LeetCode 216.組合總和III

LeetCode 17.電話號碼的字母組合


回溯法理論基礎

回溯法

回溯法也可以叫做回溯搜索法,它是一種搜索的方式。回溯一般與遞歸相輔相成,并在遞歸后進行使用。回溯法是一種暴力搜索方法,你可以想象你現在在走迷宮,當你走到一個死路后,是否需要回退,而回退的這個過程就是回溯。

回溯法的效率

正如上面所說,回溯法是暴力搜索方法,其本質是窮舉,窮舉所有可能,然后選出我們想要的答案。如果想讓回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是窮舉的本質。

用回溯法解決的問題

那么既然回溯法并不高效為什么還要用它呢? 因為沒得選,一些問題能暴力搜出來就不錯了,撐死了再剪枝一下,還沒有更高效的解法。
那什么問題適合用回溯法進行解決?

  • 組合問題:N個數里面按一定規則找出k個數的集合
  • 切割問題:一個字符串按一定規則有幾種切割方式
  • 子集問題:一個N個數的集合里有多少符合條件的子集
  • 排列問題:N個數按一定規則全排列,有幾種排列方式
  • 棋盤問題:N皇后,解數獨等等

需要特別說明下組合和排列的問題,組合是沒有順序的,排列是有順序,比如一對男女朋友,這是一個組合,但你問到他們之間誰先表白,這就變成排列了。

如何理解回溯法

回溯法解決的問題都可以抽象為樹形結構

因為回溯法解決的都是在集合中遞歸查找子集,集合的大小就構成了樹的寬度,遞歸的深度就構成了樹的深度。有遞歸,就必須要有終止條件,所以必然是一棵高度有限的樹(N叉樹)。

回溯法模板

回溯法也有模板,在將其模版前,復習下遞歸的模板。

  1. 終止條件
  2. 遞歸順序
  3. 輸入參數和輸出參數

回溯法模板。

1.回溯函數終止條件

既然回溯可以抽象為樹形結構,那么也像遞歸二叉樹那樣存在終止條件。什么時候達到了終止條件,樹中就可以看出,一般來說搜到葉子節點了,也就找到了滿足條件的一條答案,把這個答案存放起來,并結束本層遞歸。

回溯函數終止條件偽代碼如下:

if (終止條件) {存放結果;return;
}

2.回溯搜索的遍歷過程

在上面我們提到了,回溯法一般是在集合中遞歸搜索,集合的大小構成了樹的寬度,遞歸的深度構成的樹的深度。

上圖是特意舉例集合大小和孩子的數量是相等的。size為4的集合有4個子集合。

回溯函數遍歷過程偽代碼如下:

for (選擇:本層集合中元素(樹中節點孩子的數量就是集合的大小)) {處理節點;backtracking(路徑,選擇列表); // 遞歸回溯,撤銷處理結果
}

for循環就是遍歷集合區間,可以理解一個節點有多少個孩子,這個for循環就執行多少次。

backtracking這里自己調用自己,實現遞歸

大家可以從圖中看出for循環可以理解是橫向遍歷,backtracking(遞歸)就是縱向遍歷,這樣就把這棵樹全遍歷完了,一般來說,搜索葉子節點就是找的其中一個結果了

3.回溯函數模板返回值以及參數

回溯算法中函數返回值一般為void。

回溯算法需要的參數可不像二叉樹遞歸的時候那么容易一次性確定下來,所以一般是先寫邏輯,然后需要什么參數,就填什么參數。

回溯函數偽代碼如下:

void backtracking(參數)

回溯算法模板框架如下:

void backtracking(參數) {if (終止條件) {存放結果;return;}for (選擇:本層集合中元素(樹中節點孩子的數量就是集合的大小)) {處理節點;backtracking(路徑,選擇列表); // 遞歸回溯,撤銷處理結果}
}

涉及回溯算法的題都可以基于上述模板進行實現,回溯算法模板 +?題目特性?→?解決回溯算法問題

LeetCode 77. 組合

為什么要用回溯算法,你用兩個for循環也可以做到,第一個循環從左到右逐個遍歷,第二個從第一個循環的下標的下一個位置開始遍歷,這樣的話也可以做到,時間復雜度是O(N^2),那如果是k越大呢?此時不就需要更多個循環了嗎,那此時時間復雜度就是O(N^k),顯然時間復雜度太高。

回溯方法的做法其實就是用遞歸來替代循環。

思路:

  1. 通過for循環橫向遍歷起始處理節點(紅箭頭)
  2. 通過遞歸深度遍歷可能出現的組合(藍箭頭)
  3. 遞歸后進行回溯
  4. 當組合長度 == k?時,為終止條件

手撕Code

class Solution(object):def combine(self, n, k):""":type n: int:type k: int:rtype: List[List[int]]"""self.result = []    ## 二維數組self.path = []self.backtracking(n, k, 1)return self.resultdef backtracking(self, n, k, start_index):"""n 記錄需要處理到的最大數字k 記錄需要輸出的數組數目start_index 記錄當前處理的數字, 遍歷范圍是1到n"""### 終止條件if len(self.path) == k:# self.result.append(self.path)     ### 這里是將指針給append了,self.path的內容是一直在變的,如果你將指針append的話,上一輪的遞歸的結果并沒有進行保存,因為上一輪指向的數組已經變了。self.result.append(list(self.path))return### 遞歸邏輯for _ in range(start_index, n+1):     ### 左閉右開self.path.append(start_index)     ### 當前處理節點。處理同一層的邏輯self.backtracking(n, k, start_index + 1)    ### 遞歸,往深度遍歷。self.path.pop()                   ### 遞歸后進行回溯start_index += 1                  ### 處理完當前節點后,處理下一個節點

易錯點:

不能是self.result.append(self.path) ,這部分append只是當前self.path指針所指向的當前數組元素,如果self.path當前指向的數組是[1,3],上一輪self.result已經將[1,2]給存進去了,此時如果你要將[1,3]數組進行append,你應該append是這個數組,而不應該是這個指針,即你想要的結果是[ [1,2], [1,3] ],如果你是append這個指針的話,只會添加當前指針指向的數組,因為你存儲的其實是指向當前數組的指針,即[ self.path, self.path ],再進行下一輪的append操作后,其實你獲取的是[ [1,3], [1,3] ]。指針指向的是不斷在變的數組,如果你存指針的話,最終的self.result只會是存儲操作完畢后該指針下的元素,沒有記錄整個過程。

總結:當你在終止條件 if len(self.path) == k: 中執行 self.result.append(self.path) 時,你并沒有將 self.path 當前的內容復制一份,而是將 self.path 這個列表對象本身的引用添加到了 self.result 中。

回溯算法的剪枝操作

體現在for循環范圍的限制。如還是組合問題,現在k變了,變為4。

那么我們可以看到由于元素只有4個,因此只有從元素1開始的遍歷才能得到最終符合數目為k的組合,而其他子樹的遍歷是不需要的,因為肯定數目是不符合的。那如果k是3呢?是不是只有從1開始和從2開始進行遍歷才是有效的,且其子遍歷中,有些分支也可以進行去掉,從而減少遍歷的操作,這是與通過多個循環暴力搜索法相比所體現出的優勢。

那要如何優化以實現剪枝呢?其實答案就體現在for循環范圍的控制。優化過程如下:

i = start_index,表示處理節點的起始位置。

  1. 已經選擇的元素個數:path.size();
  2. 所需需要的元素個數為: k - path.size();
  3. 列表中剩余元素(n-i) >= 所需需要的元素個數(k - path.size())
  4. 在集合n中至多要從該起始位置 : i <= n - (k - path.size()) + 1,開始遍歷。

根據3得到的是 i <= n - (k - path.size()) ,為什么要+1?因為包括起始位置,我們要是一個左閉的集合。

這個優化條件 i <= n - (k - path.size()) + 1 實際上是在說: "當前的起始位置 i 必須小于或等于這樣一個值:從這個值開始,到最大的元素 n 結束,能夠恰好湊齊我們所需要的 k - path.size() 個元素,并且還考慮到起始位置 i 本身也算一個元素。"? ? ? ??

n-i 是?指包含起始位置之后的列表元素個數,當n-i = k -?path.size()時,此時有臨界條件,i = n - (k - path.size),這表示i為這個值時,當前元素+后續元素剛好是符合k -?path.size()的。但我們要的臨界條件是i到了什么值時,后續遍歷操作可以不用執行,因此要加1,1是表明i = n - (k - path.size)時,后序的元素可以進行遍歷,再往后的就不用了。

舉個例子,n = 4,k = 3, 目前已經選取的元素為0(path.size為0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。表示最大的符合條件在當第二個元素開始時。因此,從2開始。

class Solution(object):def combine(self, n, k):""":type n: int:type k: int:rtype: List[List[int]]"""self.result = []    ## 二維數組self.path = []self.backtracking(n, k, 1)return self.resultdef backtracking(self, n, k, start_index):"""n 記錄需要處理到的最大數字k 記錄需要輸出的數組數目start_index 記錄當前處理的數字, 遍歷范圍是1到n"""### 終止條件if len(self.path) == k:# self.result.append(self.path)     ### 這里是將指針給append了,self.path的內容是一直在變的,如果你將指針append的話,上一輪的遞歸的結果并沒有進行保存,因為上一輪指向的數組已經變了。self.result.append(list(self.path))returnfor _ in range(start_index, (n -(k-len(self.path))+1) +1):      ### 剪枝操作self.path.append(start_index)     self.backtracking(n, k, start_index + 1)   self.path.pop()                  start_index += 1                 

LeetCode 216.組合總和III

思路

  • 與LeetCode組合類似,只不過這里是求和,判斷是否有滿足數目的path == sum
  • 思路基本一致,注意循環中不要忘了對start_index進行修改。

手撕Code

class Solution(object):def combinationSum3(self, k, n):""":type k: int:type n: int:rtype: List[List[int]]"""self.result = []self.path = []self.nums = []for i in range(1,10):self.nums.append(i)         ### 創建一個數組用于存儲用于遍歷的數字self.backtracking(0, k, n)return self.resultdef backtracking(self, start_index, k, n):if len(self.path) == k:sum = 0for i in range(len(self.path)):sum += self.path[i]if sum == n:self.result.append(list(self.path))returnfor _ in range(start_index, len(self.nums)-(k-len(self.path)) +1):self.path.append(self.nums[start_index])self.backtracking(start_index+1, k, n)self.path.pop()start_index += 1

LeetCode 17.電話號碼的字母組合

思路1 ——?樸實無華

  • 分級處理思想,之前操作是都是在同一個數組內,現在是多個數組,那只要修改回溯函數對應的輸入和處理方法就行。具體地,輸入兩個數組,第一個數組進行橫向遍歷,第二個數據進行遍歷。為什么在一個集合里使用start_index,是為了實現去重操作;在多個集合的操作,多個集合無交集的情況下,直接從0開始遍歷,不用考慮去重操作。
  • 針對k=2時,傳入兩個數組就可以直接處理。而針對k=3,需要將k=2時的處理結果拿出來后,作為新的數組,與第三個數組輸入到回溯函數中,從而實現k=3的計算邏輯。
  • k=4也是類似,就是逐步將前面的輸出結果作為新的輸入與下一個進行計算。

上述這種思路,跟用循環進行實現沒什么區別了,沒體現回溯的特點。另外,在處理數字為1的時候要注意,字符串不是像數組那般可以進行修改,因此當遇到1時,直接跳過就行,可以用一個new_digits來存儲對舊digits進行判斷修改后的結果。

class Solution(object):def letterCombinations(self, digits):""":type digits: str:rtype: List[str]"""### 采用一個Hashmap存儲對應數字和字母的關系### 對輸入的數字進行判斷已獲得其對應的字母hash_map = dict()hash_map[2] = ['a','b','c']hash_map[3] = ['d','e','f']hash_map[4] = ['g','h','i']hash_map[5] = ['j','k','l']hash_map[6] = ['m','n','o']hash_map[7] = ['p','q','r', 's']hash_map[8] = ['t','u','v']hash_map[9] = ['w','x','y', 'z']### 構造一個函數,第一個數組作為橫向遍歷,第二個數組作為縱向遍歷self.result = []self.path   = [] k = len(digits)# left = right = 0                #### python中的字符串是無法直接修改的,因此碰到1的話,應該是選擇跳過,而不是進行修改# while right < k:    #     if digits[left] != '1':#         left += 1#         right += 1#     elif digits[left] == '1':#         right += 1#         digits[left] = digits[right] cur = 0digits_list = []while cur < k:if digits[cur] != '1':digits_list.append(digits[cur])cur += 1new_digits = ''.join(digits_list)k = len(new_digits)if k == 0:return self.resultif k == 1:return hash_map[int(digits[0])]first_str = hash_map[int(digits[0])]second_str = hash_map[int(digits[1])]if k == 2:self.backtracking(0, first_str, second_str, k)if k == 3:self.backtracking(0, first_str, second_str, 2)first_second_str = list(self.result)self.result = []third_str = hash_map[int(digits[2])]self.backtracking(0, first_second_str, third_str, 2)if k == 4:self.backtracking(0, first_str, second_str, 2)first_second_str = list(self.result)self.result = []third_str = hash_map[int(digits[2])]first_str = self.backtracking(0, first_second_str, third_str, 2)first_second_third_str = list(self.result)self.result = []forth_str = hash_map[int(digits[3])]self.backtracking(0, first_second_third_str, forth_str, 2)return self.resultdef backtracking(self, start_index, first_str, second_str, k):"""first_str : 第一個字符串數組,橫向遍歷second_str : 第二個字符串數組,深度遍歷k : path的長度 == digits.length"""if len(self.path) == k:path_str = ''.join(self.path)self.result.append(path_str)returnfor _ in range(start_index, len(first_str)):self.path.append(first_str[start_index])self.backtracking(0, second_str, None, k)self.path.pop()start_index += 1 

思路2:

  • 關鍵還是在對回溯的理解,回溯的結構可以抽象成一顆樹,在單個集合中,你是將單個元素分到樹的每一層中。現在是多個集合,其實也就是把單個集合分到樹的每一層中。
  • 在了解上述思想后,我們就可以設計index了,index表示的是digits的長度,其長度決定了有幾個集合,有N個集合就決定了有N-1個回溯遞歸的深度(因為第一個是橫向遍歷)。

Code

class Solution(object):def letterCombinations(self, digits):""":type digits: str:rtype: List[str]"""self.letterMap = [              ### 一個一維數組,數字的大小剛好對應去在數組中去獲得該數字對應字符串的下標"",     # 0"",     # 1"abc",  # 2"def",  # 3"ghi",  # 4"jkl",  # 5"mno",  # 6"pqrs", # 7"tuv",  # 8"wxyz"  # 9]self.result = []self.path = []new_digits = []for dig in digits:      ### 判斷數字是否在[2,9]之內,如果有不存在的需要進行刪除if dig != '1' or dig != '0':new_digits.append(dig)new_digits = "".join(new_digits)length = len(new_digits)        ### 數字的長度index = 0                       ### 樹的深度if length == 0:return self.resultself.backtracking(index, new_digits, length)return self.resultdef backtracking(self, index, new_digits, length):if index == length:self.result.append("".join(list(self.path)))return                         ### 終止條件cur = int(new_digits[index])       ### 當前處理的數字cur_str = self.letterMap[cur]       ### 當前處理的數字 對應的字符串for i in range (len(cur_str)):self.path.append(cur_str[i])self.backtracking(index+1, new_digits, length)self.path.pop()# index += 1           

注意,在self.path.pop()之后,index不需要+1,與start_index進行區分開來。start_index + 1是為了獲得一個數組中后續的元素。而在不同的集合中,你index是操作不同集合,index為0傳進后是第一層,如何往下進行深度遍歷操作,已經在index+1作為輸入參數進行輸入時已經確定了,后序遞歸結束時,重新執行for循環,進行橫向遍歷,再一步遞歸去深度遍歷。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/83480.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/83480.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/83480.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

自己手寫tomcat項目

一&#xff1a;Servlet的原理 在Servlet(接口中)有&#xff1a; 1.init():初始化servlet 2.getServletConfig()&#xff1a;獲取當前servlet的配置信息 3.service():服務器&#xff08;在HttpServlet中實現&#xff0c;目的是為了更好的匹配http的請求方式&#xff09; 4.g…

蘭亭妙微:用系統化思維重構智能座艙 UI 體驗

蘭亭妙微設計專注于以產品邏輯驅動的界面體驗優化&#xff0c;服務領域覆蓋AI交互、智能穿戴、IoT設備、智慧出行等多個技術密集型產業。我們倡導以“系統性設計”為方法論&#xff0c;在用戶需求與技術邊界之間找到最優解。 此次智能駕駛項目&#xff0c;我們為某車載平臺提供…

ubuntu安裝google chrome

更新系統 sudo apt update安裝依賴 sudo apt install curl software-properties-common apt-transport-https ca-certificates -y導入 GPG key curl -fSsL https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor | sudo tee /usr/share/keyrings/google-chrom…

技術測評:小型單文件加密工具的功能解析

最近在測試一款名為OEMexe的文件加密工具&#xff0c;發現它確實有一些獨特之處值得分享。這款軟件體積非常小巧&#xff0c;僅209KB&#xff0c;屬于綠色單文件版程序&#xff0c;無需安裝即可直接運行。 主要特點 多格式支持&#xff1a;能夠處理多種常見文件格式&#xff0…

Java-Objects類高效應用的全面指南

Java_Objects類高效應用的全面指南 前言一、Objects 類概述二、Objects 類的核心方法解析2.1 requireNonNull系列方法&#xff1a;空指針檢查的利器2.2 equals方法&#xff1a;安全的對象比較2.3 hashCode方法&#xff1a;統一的哈希值生成2.4 toString方法&#xff1a;靈活的對…

計網| 網際控制報文協議(ICMP)

目錄 網際控制報文協議&#xff08;ICMP&#xff09; 一、ICMP 基礎特性 二、ICMP 報文分類及作用 差錯報告報文 詢問報文 網際控制報文協議&#xff08;ICMP&#xff09; ICMP&#xff08;Internet Control Message Protocol&#xff0c;網際控制報文協議&#xff09;是 …

微服務初步學習

系統架構演變過程 一、單體架構 前后端都在一個項目中&#xff0c;包括我們現在的前后端分離開發&#xff0c;都可以看作是一個單體項目。 二、集群架構 把一個服務部署多次&#xff0c;可以解決服務不夠的問題&#xff0c;但是有些不必要的功能也跟著部署多次。 三、垂直架…

Web安全基礎:深度解析與實戰指南

一、Web安全體系架構的全面剖析 1.1 分層防御模型(Defense in Depth) 1.1.1 網絡層防護 ??防火墻技術??: 狀態檢測防火墻(SPI):基于連接狀態跟蹤,阻斷非法會話(如SYN Flood攻擊)下一代防火墻(NGFW):集成IPS、AV、URL過濾(如Palo Alto PA-5400系列)配置示例…

使用大語言模型從零構建知識圖譜(上)

從零到一&#xff1a;大語言模型在知識圖譜構建中的實操指南 ©作者|Ninja Geek 來源|神州問學 將你的 Pandas data frame 利用大語言模型轉換為知識圖譜。從零開始構建自己的基于大語言模型的圖譜構建器&#xff0c;實際使用 Langchain 的 LLMGraphTransformer &#xff…

18.自動化生成知識圖譜的多維度質量評估方法論

文章目錄 一、結構維度評估1.1 拓撲結構評估1.1.1 基礎圖論指標1.1.2 層級結構指標 1.2 邏輯一致性評估1.2.1 形式邏輯驗證1.2.2 約束滿足度 二、語義維度評估2.1 語義一致性評估2.1.1 標簽語義分析2.1.2 關系語義評估 2.2 語義表示質量2.2.1 嵌入質量2.2.2 上下文語義評估 三、…

go 集成base64Captcha 支持多種驗證碼

base64Captcha 是一個基于 Go 語言開發的驗證碼生成庫&#xff0c;主要用于在 Web 應用中集成驗證碼功能&#xff0c;以增強系統的安全性。以下是其主要特點和簡介&#xff1a; base64Captcha主要功能 驗證碼類型豐富&#xff1a;支持生成多種類型的驗證碼&#xff0c;包括純…

制作大風車動畫

這個案例的風車旋轉應用了圖形變換來實現&#xff0c;速度和縮放比例應用slider來實現&#xff0c;其中圖片的速度&#xff0c;圖片大小的信息通過State來定義變量管理&#xff0c;速度和和縮放比例的即時的值通過Prop來管理。 1. 案例效果截圖 2. 案例運用到的知識點 2.1. 核…

代碼隨想錄算法訓練營第四十二四十三天

LeetCode/卡碼網題目: 42. 接雨水84. 柱狀圖中最大的矩形98. 所有可達路徑 其他: 今日總結 往期打卡 42. 接雨水 跳轉: 42. 接雨水 學習: 代碼隨想錄公開講解 問題: 給定 n 個非負整數表示每個寬度為 1 的柱子的高度圖&#xff0c;計算按此排列的柱子&#xff0c;下雨之后能…

SEO 優化實戰:ZKmall模板商城的 B2C商城的 URL 重構與結構化數據

在搜索引擎算法日益復雜的今天&#xff0c;B2C商城想要在海量信息中脫穎而出&#xff0c;僅靠優質商品和營銷活動遠遠不夠。ZKmall模板商城以實戰為導向&#xff0c;通過URL 重構與結構化數據優化兩大核心策略&#xff0c;幫助 B2C 商城實現從底層架構到搜索展示的全面升級&…

Linux自有服務

自有服務概述 概述 自有服務&#xff0c;即不需要用戶獨立去安裝的軟件的服務&#xff0c;而是當系統安裝好之后就可以直接使用的服務&#xff08;內置&#xff09; 顯示服務 顯示服務 命令&#xff1a;systemctl \[選項] 選項參數 list-units --type service --all&#x…

ZYNQ Overlay硬件庫使用指南:用Python玩轉FPGA加速

在傳統的FPGA開發中,硬件設計需要掌握Verilog/VHDL等硬件描述語言,這對軟件開發者而言門檻較高。Xilinx的PYNQ框架通過Overlay硬件庫徹底改變了這一現狀——開發者只需調用Python API即可控制FPGA的硬件模塊,實現硬件加速與靈活配置。本文將深入探討ZYNQ Overlay的核心概念、…

JavaScript入門【1】概述

1.JavaScript是什么? <font style"color:rgb(38,38,38);">Javascript &#xff08;簡稱“JS”&#xff09;是?種直譯式腳本語?&#xff0c;?段腳本其實就是?系列指令&#xff0c;計算機通過這些指令來達成?標。它?是?種動態類型的編程語?。JS?來在?…

c++從入門到精通(五)--異常處理,命名空間,多繼承與虛繼承

異常處理 棧展開過程&#xff1a; 棧展開過程沿著嵌套函數的調用鏈不斷查找&#xff0c;直到找到了與異常匹配的catch子句為止&#xff1b;也可能一直沒找到匹配的catch&#xff0c;則退出主函數后查找過程終止。棧展開過程中的對象被自動銷毀。 在棧展開的過程中&#xff0c…

自適應稀疏核卷積網絡:一種高效靈活的圖像處理方案

自適應稀疏核卷積網絡&#xff1a;一種高效靈活的圖像處理方案 引言 在深度學習的大潮中&#xff0c;計算機視覺技術取得了長足的進步。其中&#xff0c;卷積神經網絡&#xff08;CNN&#xff09;作為圖像處理的核心工具&#xff0c;極大地推動了各類圖像識別任務的效果提升。…

Nginx:利用 FreeSSL 申請(Https)免費證書的技術指南

1、簡述 在現代互聯網應用中,使用 HTTPS 連接是確保數據傳輸安全的基本需求。SSL/TLS 證書能夠加密客戶端與服務器之間的通信,防止中間人攻擊等安全隱患。而許多開發者和小型企業可能會擔心 SSL 證書的費用問題。幸運的是,FreeSSL 提供了一個簡單易用的平臺,允許我們申請免…