常用演算法示例
目錄
連結串列
刪除連結串列的倒數第N個節點
使用快慢指標,快指標先移動n步。
# Definition for singly-linked list. # class ListNode: # def __init__(self, x): # self.val = x # self.next = None class Solution: def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode: fast = head slow = head i = 1 # 快指標先移動n步 while i <= n: fast = fast.next i += 1 # 說明要刪除的正好是第一個節點 if fast == None: return head.next while fast.next: slow = slow.next fast = fast.next # tmp = slow.next slow.next = slow.next.next return head
合併兩個有序連結串列
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { if(l1 == null) return l2; if(l2 == null) return l1; if(l1.val < l2.val){ l1.next = mergeTwoLists(l1.next, l2); return l1; }else{ l2.next = mergeTwoLists(l1, l2.next); return l2; } } }
上述是使用遞迴的解法,應該還有更好的方法。非遞迴版本:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode(-1);
ListNode cur = dummyHead;
while(l1 != null && l2 != null){
if(l1.val<l2.val){
cur.next = l1;
l1 = l1.next;
cur = cur.next;
}
else{
cur.next = l2;
l2 = l2.next;
cur = cur.next;
}
}
if(l1 == null){
cur.next = l2;
}
else{
cur.next = l1;
}
return dummyHead.next;
}
}
非遞迴解法好像跟容易理解一些。python實現:
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
new_head = ListNode(-1, None)
cur = new_head
while l1 != None and l2 != None:
if l1.val < l2.val:
cur.next = l1
l1 = l1.next
cur = cur.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next
# 處理剩餘的
if l1 == None:
cur.next = l2
else:
cur.next = l1
return new_head.next
環形連結串列
判斷連結串列中是否有環。典型的快慢指標題目。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null)
return false;
ListNode slow = head;
ListNode fast = head.next;
while(fast != slow){
if (fast == null || fast.next == null){
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
python實現:
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def hasCycle(self, head: ListNode) -> bool:
if head is None:
return False
fast = head.next
while fast != None and head != None and fast.next != None:
if fast == head:
return True
fast = fast.next.next
head = head.next
return False
環形連結串列②
給定一個連結串列,返回連結串列開始入環的第一個節點。 如果連結串列無環,則返回 null。
為了表示給定連結串列中的環,我們使用整數 pos 來表示連結串列尾連線到連結串列中的位置(索引從 0 開始)。 如果 pos 是 -1,則在該連結串列中沒有環。
說明:不允許修改給定的連結串列。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
fast = head
slow = head
while True:
# 兩個都為空時直接返回,說明沒有環
if not (fast and fast.next):
return
fast = fast.next.next
slow = slow.next
if fast == slow:
# 說明相遇了
break
fast = head
# 第二次相遇時fast指向的節點就是環的入口節點
while fast != slow:
fast = fast.next
slow = slow.next
return fast
動態規劃
爬樓梯
狀態轉移方程:dp[n] =dp[n-1] + dp[n-2]
上1階臺階:有1種方式
上2階臺階:有1+1和2兩種方式
上n階臺階:到達第n階的方法總數就是到第n-1階和第n-2階的方法數之和。
func climbStairs(n int) int {
if n ==1 {
return 1
}
dp := make([]int, n+1)
dp[1] = 1
dp[2] = 2
for i:=3; i<=n; i++ {
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
}
最大子序和
首先我們分析題目,一個連續子陣列一定要以一個數作為結尾,那麼我們可以將狀態定義成如下:
dp[i]:表示以 nums[i] 結尾的連續子陣列的最大和。
根據狀態的定義,我們繼續進行分析:如果要得到 dp[i],那麼 nums[i] 一定會被選取。並且 dp[i] 所表示的連續子序列與 dp[i-1] 所表示的連續子序列很可能就差一個 nums[i] 。即:
dp[i] = dp[i-1]+nums[i] , if (dp[i-1] >= 0)
但是這裡我們遇到一個問題,很有可能 dp[i-1] 本身是一個負數。那這種情況的話,如果 dp[i] 通過 dp[i-1]+nums[i] 來推導,那麼結果其實反而變小了,因為我們 dp[i] 要求的是最大和。所以在這種情況下,如果 dp[i-1] < 0,那麼 dp[i] 其實就是 nums[i] 的值。即
dp[i] = nums[i] , if (dp[i-1] < 0)
綜上分析,我們可以得到:
dp[i]=max(nums[i], dp[i−1]+nums[i])
得到了狀態轉移方程,但是我們還需要通過一個已有的狀態的進行推導,我們可以想到 dp[0] 一定是以 nums[0] 進行結尾,所以
dp[i] = dp[i-1]+nums[i] , if (dp[i-1] >= 0)
dp[0] = nums[0]
在很多題目中,因為 dp[i] 本身就定義成了題目中的問題,所以 dp[i] 最終就是要的答案。但是這裡狀態中的定義,並不是題目中要的問題,不能直接返回最後的一個狀態 (這一步經常有初學者會摔跟頭)。所以最終的答案,其實我們是尋找:
max(dp[0], dp[1], ..., d[i-1], dp[i])
python實現:
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
# 建立一個dp陣列
n = len(nums)
dp = [0]*n
dp[0] = nums[0]
for i in range(1, n):
dp[i] = max(nums[i], dp[i-1]+nums[i])
return max(dp)
不得不說,這是一道經典的動態規劃題目,必須要掌握。
最長上升子序列
給定一個無序的整數陣列,找到其中最長上升子序列的長度。
示例:
輸入: [10,9,2,5,3,7,101,18]
輸出: 4
解釋: 最長的上升子序列是 [2,3,7,101],它的長度是 4。
說明:
- 可能會有多種最長上升子序列的組合,你只需要輸出對應的長度即可。
因為題目中沒有要求連續,所以LIS可能是連續的,也可能是非連續的。同時,LIS符合可以從其子問題的最優解來進行構建的條件。所以我們可以嘗試用動態規劃來進行求解。首先我們定義狀態:
dp[i] :表示以nums[i]結尾的最長上升子序列的長度
分析:
- 如果nums[i]比前面的所有元素都小,那麼dp[i]等於1(即它本身)
- 如果nums[i]的前面存在比他小的元素nums[j],那麼dp[i]就等於dp[j]+1 (該結論錯誤,比如nums[3]>nums[0],即9>1,但是dp[3]並不等於dp[0]+1)
我們先初步得出上面的結論,但是我們發現了一些問題。因為dp[i]前面比他小的元素,不一定只有一個!
可能除了 nums[j],還包括 nums[k],nums[p] 等等等等。
dp[i] = max(dp[j]+1,dp[k]+1,dp[p]+1,.....)
只要滿足:
nums[i] > nums[j]
nums[i] > nums[k]
nums[i] > nums[p]
...
golang實現:
func lengthOfLIS(nums []int) int {
if len(nums) < 1 {
return 0
}
dp := make([]int, len(nums))
result := 1
for i := 0; i < len(nums); i++ {
dp[i] = 1
for j := 0; j < i; j++ {
if nums[j] < nums[i] {
dp[i] = max(dp[j]+1, dp[i])
}
}
result = max(result, dp[i])
}
return result
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
python實現:
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n = len(nums)
if n<1:
return 0
dp = [0] * n
result = 1
for i in range(n):
dp[i] = 1
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[j]+1, dp[i])
result = max(result, dp[i])
return result
三角形最小路徑和
給定一個三角形,找出自頂向下的最小路徑和。每一步只能移動到下一行中相鄰的結點上。
相鄰的結點 在這裡指的是 下標 與 上一層結點下標 相同或者等於 上一層結點下標 + 1 的兩個結點。
題解:
其實也就等同於,每一步我們只能往下移動一格或者往右移動一格。
題目很明顯就是一個找最優解的問題,並且可以從子問題的最優解進行構建。所以我們通過動態規劃進行求解。首先,我們定義狀態:
dp[i][j]:表示包含第i行第j列元素的最小路徑和
我們很容易想到可以自頂向下進行分析。並且,無論最後的路徑是哪一條,它一定要經過最頂上的元素,即 [0,0]。所以我們需要對 dp[0][0] 進行初始化。
dp[0][0] = [0][0]位置所在的元素值
繼續分析,如果我們要求dp[i][j]
,那麼氣其一定來自於自己頭頂上的兩個元素移動而來的,它們分別是dp[i-1][j-1]
、dp[i-1][j]
。所以狀態轉移方程為:
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
golang的實現:
func minimumTotal(triangle [][]int) int {
if len(triangle) < 1 {
return 0
}
if len(triangle) == 1 {
return triangle[0][0]
}
dp := make([][]int, len(triangle))
for i, arr := range triangle {
dp[i] = make([]int, len(arr))
}
result := 1<<31 - 1
dp[0][0] = triangle[0][0]
dp[1][1] = triangle[1][1] + triangle[0][0]
dp[1][0] = triangle[1][0] + triangle[0][0]
for i := 2; i < len(triangle); i++ {
for j := 0; j < len(triangle[i]); j++ {
if j == 0 {
dp[i][j] = dp[i-1][j] + triangle[i][j]
} else if j == (len(triangle[i]) - 1) {
dp[i][j] = dp[i-1][j-1] + triangle[i][j]
} else {
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
}
}
}
for _,k := range dp[len(dp)-1] {
result = min(result, k)
}
return result
}
func min(a, b int) int {
if a > b {
return b
}
return a
}
python的實現:
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
n = len(triangle)
if n < 1:
return 0
if n == 1:
return triangle[0][0]
dp = [0] * n
for i in range(n):
dp[i] = [0] * len(triangle[i])
result = 10**5
dp[0][0] = triangle[0][0]
dp[1][1] = triangle[1][1] + triangle[0][0]
dp[1][0] = triangle[1][0] + triangle[0][0]
for i in range(2, n):
for j in range(len(triangle[i])):
if j == 0:
dp[i][j] = dp[i-1][j] + triangle[i][j]
elif j == len(triangle[i]) - 1:
dp[i][j] = dp[i-1][j-1] + triangle[i][j]
else:
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
result = min(dp[-1])
return result
最小路徑和
給定一個包含非負整數的 m x n 網格,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和為最小。
說明:每次只能向下或者向右移動一步。
示例:
輸入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
輸出: 7
解釋: 因為路徑 1→3→1→1→1 的總和最小。
題解:題目明顯符合可以從子問題的最優解進行構建,所以我們考慮使用動態規劃進行求解。首先我們定義狀態:
dp[i][j]:表示包含第i行j列元素的最小路徑和
同樣,因為任何一條到達右下角的路徑,都會經過[0][0]
這個元素,所以我們需要對dp[0][0]
進行初始化。
dp[0][0] = [0][0]位置所在的元素值
繼續分析,根據題目給的條件,如果我們要求 dp[i][j] ,那麼它一定是從自己的上方或者左邊移動而來。
進而,我們得到狀態轉移方程:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
同樣,我們需要考慮兩種特殊情況:
- 最上面一行,只能由左邊移動而來(1-3-1)
- 最左邊一行,只能由上面移動而來(1-1-4)
最後,因為我們的目標是從左上角走到右下角,整個網格的最小路徑和其實就是包含右下角元素的最小路徑和。
設:dp的長度為L
最終結果就是:dp[L-1][len(L-1)-1]
綜上,我們就分析完了,我們總共進行了4步:
- 定義狀態
- 總結狀態轉移方程
- 分析狀態轉移方程不能滿足的特殊情況
- 得到最終解
go實現:
func minPathSum(grid [][]int) int {
l := len(grid)
if l < 1 {
return 0
}
dp := make([][]int, l)
for i, arr := range grid {
dp[i] = make([]int, len(arr))
}
dp[0][0] = grid[0][0]
for i := 0; i < l; i++ {
for j := 0; j < len(grid[i]); j++ {
if i == 0 && j != 0 {
dp[i][j] = dp[i][j-1] + grid[i][j]
} else if j == 0 && i != 0 {
dp[i][j] = dp[i-1][j] + grid[i][j]
} else if i !=0 && j != 0 {
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
}
}
}
return dp[l-1][len(dp[l-1])-1]
}
func min(a, b int) int {
if a > b {
return b
}
return a
}
python實現:
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
# m行 * n列
m = len(grid)
n = len(grid[0])
# 初始化dp
dp = [0] * m
for i in range(m):
dp[i] = [0] * n
for i in range(m):
for j in range(n):
if i==0 and j == 0:
dp[i][j] = grid[0][0]
elif i == 0 and j > 0:
dp[i][j] = dp[i][j-1] + grid[i][j]
elif i > 0 and j == 0:
dp[i][j] = dp[i-1][j] + grid[i][j]
else:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
return dp[m-1][n-1]
打家劫舍
你是一個專業的小偷,計劃偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。
給定一個代表每個房屋存放金額的非負整數陣列,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。
示例1:
輸入: [1,2,3,1]
輸出: 4
解釋: 偷竊 1 號房屋 (金額 = 1) ,然後偷竊 3 號房屋 (金額 = 3)。
偷竊到的最高金額 = 1 + 3 = 4 。
示例2:
輸入: [2,7,9,3,1]
輸出: 12
解釋: 偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接著偷竊 5 號房屋 (金額 = 1)。
偷竊到的最高金額 = 2 + 9 + 1 = 12 。
題解:
如何定義dp[i]
?
可以有兩種定義:
- dp[i]: 偷盜 含 第i個房子時,所獲取的最大利益
- dp[i]: 偷盜 至 第i個房子時,所獲取的最大利益
如果我們定義為狀態一,因為我們沒辦法知道獲取最高金額時,小偷到底偷盜了哪些房屋。所以我們需要找到所有狀態中的最大值,才能找到我們的最終答案。即:
max(dp[0],dp[1],.....,dp[len(dp)-1])
如果我們定義為狀態二,因為小偷一定會從前偷到最後(強調:偷盜至第i個房間,不代表小偷要從第i個房間中獲取財物)。所以我們的最終答案很容易確定。即:
dp[i]
如果是狀態一,偷盜含第 i 個房間時能獲取的最高金額,我們相當於要找到偷盜每一間房子時可以獲取到的最大金額。比如下圖,我們要找到 dp[4] ,也就是偷盜 9 這間房子時,能獲取到的最大金額。那我們就需要找到與9不相鄰的前後兩段中能獲取到的最大金額。我們發現題目進入惡性迴圈,因為我們若要找到與9不相鄰的兩端中能偷盜的最大金額,根據 dp[i] 的定義,我們就又需要分析在這兩段中盜取每一間房子時所能獲取的最大利益!想想都很可怕!所以我們放棄掉這種狀態的定義。
如果是狀態二,偷盜至第 i 個房子時,所能獲取的最大利益。那我們可以想到,由於不可以在相鄰的房屋闖入,所以 至i房屋可盜竊的最大值,要麼就是至 i-1 房屋可盜竊的最大值,要麼就是至 i-2 房屋可盜竊的最大值加上當前房屋的值,二者之間取最大值,即:
dp[i] = max(dp[i-2]+nums[i], dp[i-1])
python實現:
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) < 1:
return 0
if len(nums) == 1:
return nums[0]
if len(nums) == 2:
return max(nums[0], nums[1])
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i-2] + nums[i], dp[i-1])
return dp[-1]
python實現的時候一定要注意邊界條件!
java實現:
class Solution {
//找到動態規劃的算式
// dp[i] = max(dp[i-2]+nums[i], dp[i-1])
public int rob(int[] nums) {
int prevMax = 0;
int currMax = 0;
for (int x : nums) {
int temp = currMax;
currMax = Math.max(prevMax + x, currMax);
prevMax = temp;
}
return currMax;
}
}
go實現:
func rob(nums []int) int {
if len(nums) < 1 {
return 0
}
if len(nums) == 1 {
return nums[0]
}
if len(nums) == 2 {
return max(nums[0],nums[1])
}
dp := make([]int, len(nums))
dp[0] = nums[0]
dp[1] = max(nums[0],nums[1])
for i := 2; i < len(nums); i++ {
dp[i] = max(dp[i-2]+nums[i],dp[i-1])
}
return dp[len(dp)-1]
}
func max(a,b int) int {
if a > b {
return a
}
return b
}
字串
反轉字串
編寫一個函式,其作用是將輸入的字串反轉過來。輸入字串以字元陣列 char[] 的形式給出。
比較簡單,原地操作。
class Solution:
def reverseString(self, s):
"""
:type s: List[str]
:rtype: void Do not return anything, modify s in-place instead.
"""
i,j = 0, len(s)-1
while i<j:
s[i],s[j]=s[j],s[i]
i+=1
j-=1
字串中的第一個唯一字元
給定一個字串,找到它的第一個不重複的字元,並返回它的索引。如果不存在,則返回 -1 。
示例:
s = "leetcode"
返回 0.
s = "loveleetcode",
返回 2.
題目不難,直接進行分析。由於字母共有 26 個,所以我們可以宣告一個 26 個長度的陣列(該種方法在本類題型很常用)因為字串中字母可能是重複的,所以我們可以先進行第一次遍歷,在陣列中記錄每個字母的最後一次出現的所在索引。然後再通過一次迴圈,比較各個字母第一次出現的索引是否為最後一次的索引。如果是,我們就找到了我們的目標,如果不是我們將其設為 -1(標示該元素非目標元素)如果第二次遍歷最終沒有找到目標,直接返回 -1即可。
python實現:
class Solution:
def firstUniqChar(self, s: str) -> int:
data_dict = {}
for index, v in enumerate(s):
data_dict[v] = index
for i, v in enumerate(s):
if i == data_dict[v]:
return i
else:
data_dict[v] = -1
return -1
go實現:
func firstUniqChar(s string) int {
var arr [26]int
for i,k := range s {
arr[k - 'a'] = i
}
for i,k := range s {
if i == arr[k - 'a']{
return i
}else{
arr[k - 'a'] = -1
}
}
return -1
}
二叉樹
二叉樹的最大深度
python實現:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def maxDepth(self, root: TreeNode) -> int:
if not root:
return 0
return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1
go實現:
func maxDepth(root *TreeNode) int {
if root == nil {
return 0
}
return max(maxDepth(root.Left), maxDepth(root.Right)) + 1
}
func max(a int, b int) int {
if a > b {
return a
}
return b
}
二叉樹的層序遍歷
給你一個二叉樹,請你返回其按層序遍歷得到的節點值。(即逐層地,從左到右訪問所有節點)
我們先考慮本題的遞迴解法。想到遞迴,我們一般先想到DFS。我們可以對該二叉樹進行先序遍歷
(根左右的順序),同時,記錄節點所在的層次level,並且每一層都定義一個數組,然後將訪問到的節點值放入對應層的陣列中。
go實現:
func levelOrder(root *TreeNode) [][]int {
return dfs(root, 0, [][]int{})
}
func dfs(root *TreeNode, level int, res [][]int) [][]int {
if root == nil {
return res
}
if len(res) == level {
// 需要新加一層slice
res = append(res, []int{root.Val})
} else {
res[level] = append(res[level], root.Val)
}
res = dfs(root.Left, level+1, res)
res = dfs(root.Right, level+1, res)
return res
}
python實現:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
levels = []
if not root:
return levels
def helper(node, level):
# start the current level
if len(levels) == level:
levels.append([])
# append the current node value
levels[level].append(node.val)
# process child nodes for the next level
if node.left:
helper(node.left, level+1)
if node.right:
helper(node.right, level+1)
helper(root, 0)
return levels
驗證二叉搜尋樹
給定一個二叉樹,判斷其是否是一個有效的二叉搜尋樹BST。
假設一個二叉搜尋樹具有如下特徵:
- 節點的左子樹只包含小於當前節點的數。
- 節點的右子樹只包含大於當前節點的數。
- 所有左子樹和右子樹自身必須也是二叉搜尋樹。
思路:離不開遞迴思想。首先看完題目,我們很容易想到 遍歷整棵樹,比較所有節點,通過 左節點值<節點值,右節點值>節點值 的方式來進行求解。但是這種解法是錯誤的,因為對於任意一個節點,我們不光需要左節點值小於該節點,並且左子樹上的所有節點值都需要小於該節點。(右節點一致)所以我們在此引入上界與下界,用以儲存之前的節點中出現的最大值與最小值。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
if not root:
return True
return self.isBST(root, -2**32, 2**32)
def isBST(self, root: TreeNode, min: int, max: int) -> bool:
if root is None:
return True
if min >=root.val or max <=root.val:
return False
return self.isBST(root.left, min, root.val) and self.isBST(root.right, root.val, max)
go實現:
func isValidBST(root *TreeNode) bool {
if root == nil{
return true
}
return isBST(root,math.MinInt64,math.MaxInt64)
}
func isBST(root *TreeNode,min, max int) bool{
if root == nil{
return true
}
if min >= root.Val || max <= root.Val{
return false
}
// 左子樹root.Val應該是最大值,右子樹root.Val應該是最小值
return isBST(root.Left,min,root.Val) && isBST(root.Right,root.Val,max)
}
二叉搜尋樹中的搜尋
給定二叉搜尋樹(BST)的根節點和一個值。你需要在BST中找到節點值等於給定值的節點。返回以該節點為根的子樹。如果節點不存在,則返回NULL。
比較簡單,就是二叉搜素樹的應用。
python實現:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def searchBST(self, root: TreeNode, val: int) -> TreeNode:
if not root:
return None
if val == root.val:
return root
elif val < root.val:
return self.searchBST(root.left, val)
else:
return self.searchBST(root.right, val)
go實現:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func searchBST(root *TreeNode, val int) *TreeNode {
if root == nil {
return root
}
if val == root.Val {
return root
}else if val < root.Val {
return searchBST(root.Left, val)
}else {
return searchBST(root.Right, val)
}
}
java迭代實現:
//迭代
public TreeNode searchBST(TreeNode root, int val) {
while (root != null) {
if (root.val == val) {
return root;
} else if (root.val > val) {
root = root.left;
} else {
root = root.right;
}
}
return null;
}
迭代與遞迴的區別
遞迴:重複呼叫函式自身實現迴圈稱為遞迴;
迭代:利用變數的原值推出新值稱為迭代,或者說迭代是函式內某段程式碼實現迴圈;
刪除二叉搜尋樹中的節點
給定一個二叉搜尋樹的根節點 root 和一個值 key,刪除二叉搜尋樹中的 key 對應的節點,並保證二叉搜尋樹的性質不變。返回二叉搜尋樹(有可能被更新)的根節點的引用。
一般來說,刪除節點可分為兩個步驟:
首先找到需要刪除的節點;
如果找到了,刪除它。
說明: 要求演算法時間複雜度為 O(h),h 為樹的高度。
題解:
首先找到該刪除節點,找到之後會出現三種情況:
- 待刪除節點的左子樹為空,讓待刪除節點的右子樹替代自己。
- 待刪除節點的右子樹為空,讓待刪除節點的左子樹替代自己。
- 如果待刪除的節點的左右子樹都不為空。我們需要找到比當前節點小的最大節點(前驅),來替換自己,或者比當前節點大的最小節點(後繼)來替換自己。
go語言使用後繼節點來實現:
func deleteNode(root *TreeNode, key int) *TreeNode {
if root == nil {
return nil
}
if key < root.Val {
root.Left = deleteNode( root.Left, key )
return root
}
if key > root.Val {
root.Right = deleteNode( root.Right, key )
return root
}
//到這裡意味已經查詢到目標
if root.Right == nil {
//右子樹為空
return root.Left
}
if root.Left == nil {
//左子樹為空
return root.Right
}
// 先取當前節點的右節點,然後一直取該節點的左節點,即比當前節點大的最小節點
minNode := root.Right
for minNode.Left != nil {
minNode = minNode.Left
}
root.Val = minNode.Val
root.Right = deleteMinNode( root.Right )
return root
}
func deleteMinNode( root *TreeNode ) *TreeNode {
if root.Left == nil {
pRight := root.Right
root.Right = nil
return pRight
}
root.Left = deleteMinNode( root.Left )
return root
}
python實現:
class Solution:
def successor(self, root):
"""
One step right and then always left
"""
root = root.right
while root.left:
root = root.left
return root.val
def predecessor(self, root):
"""
One step left and then always right
"""
root = root.left
while root.right:
root = root.right
return root.val
def deleteNode(self, root: TreeNode, key: int) -> TreeNode:
if not root:
return None
# delete from the right subtree
if key > root.val:
root.right = self.deleteNode(root.right, key)
# delete from the left subtree
elif key < root.val:
root.left = self.deleteNode(root.left, key)
# delete the current node
else:
# the node is a leaf
if not (root.left or root.right):
root = None
# the node is not a leaf and has a right child
elif root.right:
root.val = self.successor(root)
root.right = self.deleteNode(root.right, root.val)
# the node is not a leaf, has no right child, and has a left child
else:
root.val = self.predecessor(root)
root.left = self.deleteNode(root.left, root.val)
return root
平衡二叉樹
給定一個二叉樹,判斷它是否是高度平衡的二叉樹。
利用全域性變數的一種解法:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
private boolean result = true;
public boolean isBalanced(TreeNode root) {
maxDepth(root);
return result;
}
public int maxDepth(TreeNode root){
if(root == null) return 0;
int left_deep = maxDepth(root.left);
int right_deep = maxDepth(root.right);
if (Math.abs(left_deep - right_deep) > 1) result = false;
return Math.max(left_deep, right_deep) + 1;
}
}
另一種解法:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isBalanced(TreeNode root) {
if (root == null) return true;
if (!isBalanced(root.left)) return false;
if(!isBalanced(root.right)) return false;
int leftH = dfs(root.left) + 1;
int rightH = dfs(root.right) + 1;
if(Math.abs(leftH - rightH)>1){
return false;
}
else{
return true;
}
}
//計算二叉樹的深度
public int dfs(TreeNode x){
if(x == null) return 0;
if(x.left == null && x.right == null) return 1;
int leftH = dfs(x.left) + 1;
int rightH = dfs(x.right) + 1;
return Math.max(leftH, rightH);
}
}
python解法:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def isBalanced(self, root: TreeNode) -> bool:
if not root:
return True
if not self.isBalanced(root.left) or not self.isBalanced(root.right):
return False
l_depth = self.dfs(root.left) + 1
r_depth = self.dfs(root.right) + 1
if abs(l_depth-r_depth) > 1:
return False
return True
# 計算出二叉樹的高度
def dfs(self, root: TreeNode):
if not root:
return 0
if root.left is None and root.right is None:
return 1
l_depth = self.dfs(root.left) + 1
r_depth = self.dfs(root.right) + 1
return max(l_depth, r_depth)
完全二叉樹
給出一格完全二叉樹,求出該樹的節點個數。
說明:
完全二叉樹的定義如下:在完全二叉樹中,除了最底層節點可能沒填滿外,其餘每層節點數都達到最大值,並且最下面一層的節點都集中在該層最左邊的若干位置。若最底層為第 h 層,則該層包含 1~ 2^h 個節點。
可以直接通過遞迴求出二叉樹的個數:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def countNodes(self, root: TreeNode) -> int:
if root:
return 1 + self.countNodes(root.left) + self.countNodes(root.right)
else:
return 0
但是上述解法對所有二叉樹都有效,沒有充分利用完全二叉樹的特性來簡化計算。
由於題中已經告訴我們這是一顆完全二叉樹,我們又已知了完全二叉樹除了最後一層,其他層都是滿的,並且最後一層的節點全部靠向了左邊。那我們可以想到,可以將該完全二叉樹可以分割成若干滿二叉樹和完全二叉樹,滿二叉樹直接根據層高h計算出節點為2^h-1,*然後*繼續計運算元樹中完全二叉樹節點。那如何分割成若干滿二叉樹和完全二叉樹呢?對任意一個子樹,遍歷其左子樹層高left,右子樹層高right,相等左子樹則是滿二叉樹,否則右子樹是滿二叉樹。
我們看到根節點的左右子樹高度都為3,那麼說明左子樹是一顆滿二叉樹。因為節點已經填充到右子樹了,左子樹必定已經填滿了。所以左子樹的節點總數我們可以直接得到,是2^left - 1,加上當前這個root節點,則正好是2^3,即 8。然後只需要再對右子樹進行遞迴統計即可。
我們看到左子樹高度為3,右子樹高度為2。說明此時最後一層不滿,但倒數第二層已經滿了,可以直接得到右子樹的節點個數。同理,右子樹節點+root節點,總數為2right,即22。再對左子樹進行遞迴查詢。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int countNodes(TreeNode root) {
if (root == null) {
return 0;
}
int left = countLevel(root.left);
int right = countLevel(root.right);
if (left == right) {
return countNodes(root.right) + (1 << left);
} else {
return countNodes(root.left) + (1 << right);
}
}
//計算完全二叉樹的高度
private int countLevel(TreeNode root) {
int level = 0;
while (root != null) {
level++;
root = root.left;
}
return level;
}
}
go實現:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func countNodes(root *TreeNode) int {
if root == nil {
return 0
}
left := countLevel(root.Left)
right := countLevel(root.Right)
if left == right {
return countNodes(root.Right) + (1<<left)
} else {
return countNodes(root.Left) + (1<<right)
}
}
func countLevel(root *TreeNode) int {
level := 0
for root != nil {
level++
root = root.Left
}
return level
}
二叉樹的剪枝
給定二叉樹根結點 root ,此外樹的每個結點的值要麼是 0,要麼是 1。
返回移除了所有不包含 1 的子樹的原二叉樹。
( 節點 X 的子樹為 X 本身,以及所有 X 的後代。)
假設有一棵樹,最上層的是root節點,而父節點會依賴子節點。如果現在有一些節點已經標記為無效,我們要刪除這些無效節點。如果無效節點的依賴的節點還有效,那麼不應該刪除,如果無效節點和它的子節點都無效,則可以刪除。剪掉這些節點的過程,稱為剪枝,目的是用來處理二叉樹模型中的依賴問題。
剪什麼大家應該都能理解。那關鍵是怎麼剪?過程也很簡單,在遞迴的過程中,如果當前結點的左右節點皆為空,且當前結點為0,我們就將當前節點剪掉即可。
go語言實現:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func pruneTree(root *TreeNode) *TreeNode {
if root == nil {
return nil
}
root.Left = pruneTree(root.Left)
root.Right = pruneTree(root.Right)
if root.Left == nil && root.Right == nil && root.Val == 0 {
return nil
}
return root
}
數學
給定一個沒有重複數字的序列,返回其所有可能的全排列。
示例:
輸入: [1,2,3]
輸出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
思路:是回溯演算法。https://leetcode-cn.com/problems/permutations/
滑動視窗
滑動視窗最大值
給定一個數組 nums,有一個大小為 k 的滑動視窗從陣列的最左側移動到陣列的最右側。你只可以看到在滑動視窗內的 k 個數字。滑動視窗每次只向右移動一位。返回滑動視窗中的最大值。
暴力解法:時間複雜度O(LK)。
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int len = nums.length;
if (len * k == 0) return new int[0];
int [] win = new int[len - k + 1];
//遍歷所有的滑動視窗
for (int i = 0; i < len - k + 1; i++) {
int max = Integer.MIN_VALUE;
//找到每一個滑動視窗的最大值
for(int j = i; j < i + k; j++) {
max = Math.max(max, nums[j]);
}
win[i] = max;
}
return win;
}
}
雙向佇列解法:O(N)
我們可以使用雙向佇列,該資料結構可以從兩端以常數時間壓入/彈出元素。
儲存雙向佇列的索引比儲存元素更方便,因為兩者都能在陣列解析中使用。
python實現:
from collections import deque
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
n = len(nums)
if n * k == 0:
return []
if k == 1:
return nums
# i是指元素的下標
def clean_deque(i):
# deq[0]就是下標0,這裡如果i==k時
if deq and deq[0] == i - k:
deq.popleft()
# 迴圈移除佇列中小於當前nums[i]的值
while deq and nums[i] > nums[deq[-1]]:
deq.pop()
deq = deque()
max_idx = 0 # 標記最大值的位置
# 佇列的初始化
for i in range(k):
clean_deque(i)
deq.append(i)
if nums[i] > nums[max_idx]:
max_idx = i
output = [nums[max_idx]]
for i in range(k, n):
clean_deque(i)
deq.append(i)
output.append(nums[deq[0]])
return output
無重複字元的最長子串
給定一個字串,請你找出其中不含有重複字元的 最長子串 的長度。
思路:建立一個滑動視窗,記錄下串口出現過的最大值即可。我們唯一要做的是 --- 儘可能擴大視窗。當一個新元素與原來視窗中的元素一樣時,我們縮小視窗,將出現過的元素以及其左邊的元素統統移除。
python實現:
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
if s == '':
return 0
if len(s) == 1:
return 1
# lookup為視窗內的字串,這個視窗要設計好
# 遍歷的時候主要就是更新這個視窗
lookup = list()
n = len(s)
max_len = 0
cur_len = 0
for i in range(n):
val = s[i]
if val not in lookup:
lookup.append(val)
cur_len+=1
else:
index = lookup.index(val)
lookup = lookup[index+1:] # 移出出現過的元素及其左邊
lookup.append(val)
cur_len = len(lookup)
if cur_len > max_len:
max_len = cur_len
return max_len
找到字串中所有字母異位詞
給定一個字串 s 和一個非空字串 p,找到 s 中所有是 p 的字母異位詞的子串,返回這些子串的起始索引。
字串只包含小寫英文字母,並且字串 s 和 p 的長度都不超過 20100。
說明:
- 字母異位詞指字母相同,但排列不同的字串。
- 不考慮答案輸出的順序。
輸入:
s: "cbaebabacd" p: "abc"
輸出:
[0, 6]
解釋:
起始索引等於 0 的子串是 "cba", 它是 "abc" 的字母異位詞。
起始索引等於 6 的子串是 "bac", 它是 "abc" 的字母異位詞。
輸入:
s: "abab" p: "ab"
輸出:
[0, 1, 2]
解釋:
起始索引等於 0 的子串是 "ab", 它是 "ab" 的字母異位詞。
起始索引等於 1 的子串是 "ba", 它是 "ab" 的字母異位詞。
起始索引等於 2 的子串是 "ab", 它是 "ab" 的字母異位詞。
思路:我們使用雙指標維護一個視窗,該視窗的大小與目標穿保持一致。判斷字母異位詞,我們需要保持視窗中的字母出現次數與目標串中的字母出現次數一致。
排序類
穩定排序:冒泡、插入、歸併、基數
不穩定排序:選擇、快速、希爾、堆
插入排序
i位置是插入的位置,i位置前的(包括i)都是排序好了的。
func main(){
arr := []int{5, 4, 3, 2, 1}
insert_sort(arr)
}
func insert_sort(arr []int) {
for i := 1; i < len(arr); i++ {
for j := i; j > 0; j-- {
if arr[j] < arr[j-1] {
arr[j], arr[j-1] = arr[j-1], arr[j]
}
}
fmt.Println(arr)
}
}
/*
$ go run b.go
[4 5 3 2 1]
[3 4 5 2 1]
[2 3 4 5 1]
[1 2 3 4 5]
*/
按奇偶排序陣列
在一個數組中要求,偶數排在奇數之前,具體順序無要求。
class Solution(object):
def sortArrayByParity(self, A):
"""
:type A: List[int]
:rtype: List[int]
"""
l = len(A)
i,j = 0,l-1
while i<j:
while A[i]%2==0 and i<j:
i+=1
while A[j]%2==1 and i<j:
j-=1
# 交換第一對前面是奇數,後面是偶數的數字對
A[i],A[j] = A[j],A[i]
return A
# 另一種實現
class Solution:
def sortArrayByParity(self, A: List[int]) -> List[int]:
j = 0
for index, v in enumerate(A):
if A[index] % 2 == 0:
A[j], A[index] = A[index], A[j]
j += 1
return A
go語言版本:
func sortArrayByParity(A []int) []int {
j := 0
for i := range A {
if A[i] % 2 == 0 {
A[j], A[i] = A[i], A[j]
j++
}
}
return A
}
位運算系列
2的冪
給定一個整數,編寫一個函式來判斷它是否是2的冪次方。
題解:對於N為2的冪的數,都有N&(N-1)=0,所以這個就是我們的判斷條件。
這個技巧可以記憶下來,在一些別的位運算的題目中也是會用到的。
go實現:
//go
func isPowerOfTwo(n int) bool {
return n > 0 && n&(n-1) == 0
}
python實現:
class Solution:
def isPowerOfTwo(self, n: int) -> bool:
return n >0 and n & (n-1) == 0
位1的個數
編寫一個函式,輸入是一個無符號整數,返回其二進位制表示式中數字位數為 ‘1’ 的個數(也被稱為漢明重量)。
class Solution:
def hammingWeight(self, n: int) -> int:
ans = 0
while n >0:
ans+=n%2
n = n>>1
return ans
java實現:
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int result = 0;
int mask = 1;
for (int i=0;i<32; i++){
if ((n&mask)!=0){
result++;
}
mask = mask << 1;
}
return result;
}
}
只出現一次的數字
給定一個非空整數陣列,除了某個元素只出現一次以外,其餘每個元素均出現兩次。找出那個只出現了一次的元素。
題解:肯定用位運算的方法,兩個相同數字的異或運算為0.
如果a、b兩個值不同,則異或結果為1.如果a、b兩個值相同,異或結果為0.
異或還有以下性質:
- 任意一個數和0異或仍然為自己
- 任意一個數和自己異或是0
- 異或操作滿足交換律和結合律
python實現:
class Solution:
def singleNumber(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
a = 0
for num in nums:
a = a ^ num
return a
只出現一次的數字②
給定一個非空整數陣列,除了某個元素只出現一次以外,其餘每個元素均出現了三次。找出那個只出現了一次的元素。
思路:
- HashMap統計求解,缺點是使用了額外的空間。
- 數學方式:也就是說,把原陣列去重,再乘以3得到的值(求和),剛好是要找的元素的2倍。
- 位運算:定義一種a?a?a =0的運算
方法2的python實現:
class Solution:
def singleNumber(self, nums: List[int]) -> int:
double_target = sum(set(nums))*3 - sum(nums)
# 注意int轉換,因為涉及到觸發,結果以float格式返回
return int(double_target/2)
方法3,是不是就是讓其二進位制的每一位數都相加,最後再對3進行一個取模的過程呢?go實現:
//go
func singleNumber(nums []int) int {
number, res := 0, 0
for i := 0; i < 64; i++ {
//初始化每一位1的個數為0
number = 0
for _, k := range nums {
//通過右移i位的方式,計算每一位1的個數
number + = (k >> i) & 1
}
//最終將抵消後剩餘的1放到對應的位數上
res |= (number) % 3 << i
}
return res
}
缺失數字
給定一個包含 0, 1, 2, ..., n
中 n 個數的序列,找出 0 .. n 中沒有出現在序列中的那個數。
示例:
輸入: [3,0,1]
輸出: 2
輸入: [9,6,4,2,3,5,7,0,1]
輸出: 8
思路:
- 數學解法:n個連續數字的和為(n+1)*n/2
- 位運算:利用 兩個相同的數,使用異或可以相消除 的原理。
方法1的python實現:
class Solution:
def missingNumber(self, nums: List[int]) -> int:
n = len(nums)
total = (n+1)*n/2
total = int(total)
for x in nums:
total -= x
return total
#python
class Solution:
def missingNumber(self, nums: List[int]) -> int:
return sum(range(len(nums) + 1)) - sum(nums)
方法2的go實現:
//Go
func missingNumber(nums []int) int {
result := 0
for i,k := range nums {
result ^= k ^ i
}
return result ^ len(nums)
}
java版本:
//java
class Solution {
public int missingNumber(int[] nums) {
int res = 0;
for(int i = 0; i < nums.length; i++ )
res ^= nums[i] ^ i;
return res ^ nums.length;
}
}
二分法系列
第一個錯誤的版本
你是產品經理,目前正在帶領一個團隊開發新的產品。不幸的是,你的產品的最新版本沒有通過質量檢測。由於每個版本都是基於之前的版本開發的,所以錯誤的版本之後的所有版本都是錯的。
假設你有 n 個版本 [1, 2, ..., n],你想找出導致之後所有版本出錯的第一個錯誤的版本。
你可以通過呼叫 bool isBadVersion(version) 介面來判斷版本號 version 是否在單元測試中出錯。實現一個函式來查詢第一個錯誤的版本。你應該儘量減少對呼叫 API 的次數。
示例:
給定 n = 5,並且 version = 4 是第一個錯誤的版本。
呼叫 isBadVersion(3) -> false
呼叫 isBadVersion(5) -> true
呼叫 isBadVersion(4) -> true
所以,4 是第一個錯誤的版本。
思路:使用二分查詢快速定位出第一個錯誤版本。
python實現:
# The isBadVersion API is already defined for you.
# @param version, an integer
# @return a bool
# def isBadVersion(version):
class Solution:
def firstBadVersion(self, n):
"""
:type n: int
:rtype: int
"""
left = 1
right = n
while left < right:
mid = left + (right-left)//2
if isBadVersion(mid):
right = mid
else:
left = mid + 1
return left
java的實現:
//JAVA
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
int left = 1;
int right = n;
int res = n;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (isBadVersion(mid)) {
res = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return res;
}
}
尋找旋轉排序陣列中的最小值
假設按照升序排序的陣列在預先未知的某個點上進行了旋轉。
( 例如,陣列 [0,1,2,4,5,6,7] 可能變為 [4,5,6,7,0,1,2] )。
請找出其中最小的元素。
你可以假設陣列中不存在重複元素。
輸入: [3,4,5,1,2]
輸出: 1
輸入: [4,5,6,7,0,1,2]
輸出: 0
思路:在二分搜尋中,我們找到區間的中間點並根據某些條件決定去區間左半部分還是右半部分搜尋。但是麻煩的是,我們的陣列被旋轉了,因此肯定不能直接使用二分。那我們需要先觀察一下。
無論怎麼旋轉,我們可以得到一個結論:首元素 > 尾元素
問題似乎變得簡單了,旋轉將原陣列一分為二,並且我們已知了首元素值總是大於尾元素,那麼我們只要找到將其一分為二的那個點(該點左側的元素都大於首元素,該點右側的元素都小於首元素),是不是就可以對應找到陣列中的最小值。
然後我們通過二分來進行查詢,先找到中間節點mid,如果中間元素大於首元素,我們就把mid向右移動;如果中間元素小於首元素,我們就把mid向左移動。
python實現:
class Solution:
def findMin(self, nums: List[int]) -> int:
left = 0
right = len(nums) -1
while left < right:
mid = (left +right) >> 1
if nums[mid] > nums[right]:
left = mid + 1
else:
right = mid
target = nums[left]
return target
這個題目要多看幾個題解,徹底吃透它!
java實現:
class Solution {
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
left = mid + 1;
} else {
right = mid;
}
}
return nums[left];
}
}
尋找旋轉排序陣列中的最小值②
假設按照升序排序的陣列在預先未知的某個點上進行了旋轉。
( 例如,陣列 [0,1,2,4,5,6,7] 可能變為 [4,5,6,7,0,1,2] )。
請找出其中最小的元素。
注意陣列中可能存在重複的元素。
java實現:
class Solution {
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left<right) {
int mid = left + (right-left)/2;
if(nums[mid] > nums[right]){
left = mid + 1;
}
else if (nums[mid] < nums[right])
{
right = mid;
}
else{
right--;
}
}
return nums[left];
}
}
注意邊界條件。