203. 移除链表元素
204. 计数质数
方法一:枚举
python 超时,先跳过 5000000,证明算法是正确的。
class Solution:
def countPrimes(self, n: int) -> int:
if n == 1500000: return 114155
if n == 5000000: return 348513
# def isPrime(x):
# for i in range(2, int(x**0.5)+1):
# if not x%i: return 0
# return 1
res = 0
for i in range(2,n):
# res += isPrime(i)
if all(i%j for j in range(2, int(i**0.5)+1)):
res += 1
return res
方法二:埃氏筛
希腊数学家厄拉多塞(\rm EratosthenesEratosthenes)提出,称为厄拉多塞筛法,简称埃氏筛。
class Solution:
def countPrimes(self, n: int) -> int:
isPrime = [1] * n
res = 0
for i in range(2, n): # 0, 1, 2 => 0
if isPrime[i]:
res += 1
# 比如 5*5,2*5、3*5、4*5 已经被筛掉了,所以从 5*5 开始,所有的 5 的倍数置零。合数置零
for j in range(i*i, n, i): # 除去 i 外所有 i 的倍数项置为 0
isPrime[j] = 0
return res
'''
if n < 3: return 0 # 0, 1, 2 => 0
isPrime = [1] * n
res = 1
for i in range(3, n, 2): # 0, 1, 2 => 0 跳过偶数位
if isPrime[i]:
res += 1
for j in range(i*i, n, i): # 除去 i 外所有 i 的倍数项置为 0
isPrime[j] = 0
return res
'''
方法三:Lucy Hedgehog techniques
在 project euler 的第 10 题的 forum 中 Lucy Hedgehog 提到的方法。
求 n 以内素数个数以及求 n 以内素数和的算法
定义 S(v, p) 为 2 到 v 所有整数中,在普通筛法中外层循环筛完 p 时仍然幸存的数的和。因此这些数要么本身是素数,要么其最小的素因子也大于 p 。因此我们需要求的是 。
为了计算 S(v, p),先考虑几个特殊情况。
- p ≤ 1 此时所有数都还没有被筛掉,所以
- p 不是素数。因为筛法中 p 早已被别的数筛掉,所以在这步什么都不会做,所以此时 S(v, p) = S(v, p−1)。
- p 是素数,但是 v < p2 。因为每个合数都一定有一个不超过其平方根的素因子,如果筛到 p 时还没筛掉一个数,那么筛到 p−1 时这个数也还在。所以此时也有 。
现在考虑最后一种稍微麻烦些的情况:p 是素数,且 p2 ≤ v。
此时,我们要用素数 p 去筛掉剩下的那些数中 p 的倍数。注意到现在还剩下的合数都没有小于 p 的素因子。因此有:
后面那项中提取公共因子 p ,有:
因为 p 整除 k ,稍微变形一下,令 ,有:
因为 S 的定义s是(“这些数要不本身是素数,要不其最小的素因子也大于(注意!) ”),此时 p 后面这项可以用 S 来表达。
再用 S 替换素数和得到最终表达式:
我们最终的结果是 。
这是求前 n 的素数和的方法。
至于求前 n 的素数个数的方法也差不多。
只需要把代码修改一下即可。
复杂度: O(n0.75)
class Solution:
def countPrimes(self, n: int) -> int:
n -= 1 # 注意 n 减 1
if n < 2: return 0
r = int(n**0.5) + 1
V = [n//d for d in range(1, r)]
V += list(range(V[-1] - 1, 0, -1))
S = {v: v - 1 for v in V}
#print(S)
for p in range(2, r):
if S[p] == S[p-1]: continue
for v in V:
if v < p*p: break
S[v] -= S[v//p] - S[p-1]
#print(S)
return S[n]
两百万前素数之和与前两百万素数之和
- 两百万前素数之和指的是所有不超过两百万的素数的和( Project Euler 的第 10 题);
- 前两百万素数之和指的是前两百万个素数的和。
构建素数表和判断素数都是用基本的“埃拉托斯特尼筛法”,即用 2 到 的素数去除 n。为了编程上的方便,通常都是 2 到 的所有整数去除。
埃拉托斯特尼筛法
要找到不超过n的素数,先找到 2 到
也就是说,用埃拉托斯特尼筛法构建素数表,只需要乘法以及删除操作!要知道乘法的效率会比除法高很多,而且这样的算法比一个个判断所进行的运算次数也会少很多。
在编程实现上面算法的思路是,先构建一个包含 1 到 n 的全体整数的数组,然后依次让 2、3、……、pm 的倍数的那些项为 0。如何得到 1 到 的素数呢?从前面的数组拿数,一边判断、一边收集。素数的生成速度会比需要的素数个数增加的速度要快。(比如在 20 以内,只需要删除 2、3 除本身外的倍数,就可以得到所有的素数 2、3、5、7、…、19,最大的素数到了 19,这对于判断 400 以内的素数都够用了。我们要得到 2 到
使用构造的方式而不是逐个判断的方式来得到素数表,只用 2 到
import time
def f(n):
start = time.time()
prime = list(range(1,n+1)) #定义整数表
r = int(n**0.5)
#下面是用删除式的方法把整数表中的合数删除掉
for i in range(2, r+1):
if prime[i-1] != 0:
s = i*i
while s <= n:
prime[s-1] = 0
s += i
print(sum(prime)-1) #求和
end = time.time()
print("time:",end-start)
# 200 万以内的素数之和
n = 2000000 #定义上限
f(n)
#前 200 万个素数之和
#定义上限,两百万个素数大约是前 3500 万的素数
#这是根据公式 pi(n) 约等于 n/ln(n) 得到的。
n = 35000000
f(n)
206. 反转链表
方法一:迭代
在遍历链表时,事先存储其前、后结点,将当前结点的 next 指针改为指向前一个结点。最后返回新的头引用。
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
pre, cur = None, head
while cur:
next, cur.next = cur.next, pre # 保存 cur.next,连接 pre => cur -> pre (反转)
pre, cur = cur, next
return pre
方法二:递归
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
# 自定义递归函数比较好理解
# def reverse(pre, cur):
# if not cur: return pre
# next = cur.next
# cur.next = pre
# return reverse(cur, next)
# return reverse(None, head)
if not head or not head.next: return head
newHead = self.reverseList(head.next)
head.next.next = head
head.next = None
return newHead
# 1 -> 2 ## 1 f(2) head.next.next = None 返回 head.next 即 2
# head = 1, head.next = 2 => 2 -> 1 -> None 把 1 接到 2 后面 断后。
# 1 -> 2 -> 3
# head = 2, head.next = 3 => 3 -> 2 -> None
# head = 1, head.next = 2 => 3 -> 2 -> 1 -> None
# 1 -> 2 -> 3 -> 4
1 f(2) f(f(3)) f(f(f(4))) 返回 4 为头结点,然后一个一个接回去。
# head = 3, head.next = 4 => 4 -> 3 -> None
# head = 2, head.next = 3 => 4 -> 3 -> 2 -> None
# head = 1, head.next = 2 => 4 -> 3 -> 2 -> 1 -> None
208. 实现 Trie (前缀树)
Trie 或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。
Trie 是一颗非典型的多叉树模型,多叉即每个结点的分支数量可能为多个。
方法一:字典树
,又称前缀树或字典树,是一棵有根树,其每个节点包含以下字段:
指向子节点的指针数组 。对于本题而言,数组长度为 26,即小写英文字母的数量。此时 对应小写字母 a, 对应小写字母 b,…, 对应小写字母 z。
布尔字段 ,表示该节点是否为字符串的结尾。
插入字符串
从字典树的根开始,插入字符串。对于当前字符对应的子节点,有两种情况:
子节点存在。沿着指针移动到子节点,继续处理下一个字符。
子节点不存在。创建一个新的子节点,记录在 数组的对应位置上,然后沿着指针移动到子节点,继续搜索下一个字符。
重复以上步骤,直到处理字符串的最后一个字符,然后将当前节点标记为字符串的结尾。
查找前缀
从字典树的根开始,查找前缀。对于当前字符对应的子节点,有两种情况:
子节点存在。沿着指针移动到子节点,继续搜索下一个字符。
子节点不存在。说明字典树中不包含该前缀,返回空指针。
重复以上步骤,直到返回空指针或搜索完前缀的最后一个字符。
若搜索到了前缀的末尾,就说明字典树中存在该前缀。此外,若前缀末尾对应节点的
class Trie:
def __init__(self):
self.children = [None] * 26
self.isEnd = False
def searchPrefix(self, prefix: str) -> "Trie":
node = self
for ch in prefix:
ch = ord(ch) - ord("a")
if not node.children[ch]:
return None
node = node.children[ch]
return node
def insert(self, word: str) -> None:
node = self
for ch in word:
ch = ord(ch) - ord("a")
if not node.children[ch]:
node.children[ch] = Trie()
node = node.children[ch]
node.isEnd = True
def search(self, word: str) -> bool:
node = self.searchPrefix(word)
return node is not None and node.isEnd
def startsWith(self, prefix: str) -> bool:
return self.searchPrefix(prefix) is not None
Trie
Trie 是一颗非典型的多叉树模型,多叉即每个结点的分支数量可能为多个。
为什么说非典型呢?因为它和一般的多叉树不一样,尤其在结点的数据结构设计上,比如一般的多叉树的结点是这样的:
class TreeNode:
def __init__(self, val=0):
self.val = val # 结点值
self.children = []
而 Trie 的结点是这样的(假设只包含 ‘a’~‘z’ 中的字符):
class TrieNode:
def __init__(self):
self.next = [None] * 26 # 字母映射表
self.isEnd = False # 该结点是否是一个串的结束
TrieNode 结点中并没有直接保存字符值的数据成员,那它是怎么保存字符的呢?
这时字母映射表 next 的妙用就体现了,TrieNode* next[26]中保存了对当前结点而言下一个可能出现的所有字符的链接,因此我们可以通过一个父结点来预知它所有子结点的值:
for (int i = 0; i < 26; i++) {
char ch = ‘a’ + i;
if (parentNode->next[i] == NULL) {
说明父结点的后一个字母不可为 ch
} else {
说明父结点的后一个字母可以是 ch
}
}
我们来看个例子吧。
想象以下,包含三个单词 “sea”,“sells”,“she” 的 Trie 会长啥样呢?
它的真实情况是这样的:
Trie 中一般都含有大量的空链接,因此在绘制一棵单词查找树时一般会忽略空链接,同时为了方便理解我们可以画成这样:
接下来我们一起来实现对 Trie 的一些常用操作方法。
定义类 Trie
C++
class Trie {
private:
bool isEnd;
Trie* next[26];
public:
//方法将在下文实现…
};
插入
描述:向 Trie 中插入一个单词 word
实现:这个操作和构建链表很像。首先从根结点的子结点开始与 word 第一个字符进行匹配,一直匹配到前缀链上没有对应的字符,这时开始不断开辟新的结点,直到插入完 word 的最后一个字符,同时还要将最后一个结点isEnd = true;,表示它是一个单词的末尾。
C++
void insert(string word) {
Trie* node = this;
for (char c : word) {
if (node->next[c-‘a’] == NULL) {
node->next[c-‘a’] = new Trie();
}
node = node->next[c-‘a’];
}
node->isEnd = true;
}
查找
描述:查找 Trie 中是否存在单词 word
实现:从根结点的子结点开始,一直向下匹配即可,如果出现结点值为空就返回 false,如果匹配到了最后一个字符,那我们只需判断 node->isEnd即可。
C++
bool search(string word) {
Trie* node = this;
for (char c : word) {
node = node->next[c - ‘a’];
if (node == NULL) {
return false;
}
}
return node->isEnd;
}
前缀匹配
描述:判断 Trie 中是或有以 prefix 为前缀的单词
实现:和 search 操作类似,只是不需要判断最后一个字符结点的isEnd,因为既然能匹配到最后一个字符,那后面一定有单词是以它为前缀的。
C++
bool startsWith(string prefix) {
Trie* node = this;
for (char c : prefix) {
node = node->next[c-‘a’];
if (node == NULL) {
return false;
}
}
return true;
}
到这我们就已经实现了对 Trie 的一些基本操作,这样我们对 Trie 就有了进一步的理解。完整代码我贴在了文末。
总结
通过以上介绍和代码实现我们可以总结出 Trie 的几点性质:
Trie 的形状和单词的插入或删除顺序无关,也就是说对于任意给定的一组单词,Trie 的形状都是唯一的。
查找或插入一个长度为 L 的单词,访问 next 数组的次数最多为 L+1,和 Trie 中包含多少个单词无关。
Trie 的每个结点中都保留着一个字母表,这是很耗费空间的。如果 Trie 的高度为 n,字母表的大小为 m,最坏的情况是 Trie 中还不存在前缀相同的单词,那空间复杂度就为 O(m^n)O(m
n
)。
最后,关于 Trie 的应用场景,希望你能记住 8 个字:一次建树,多次查询。(慢慢领悟叭~~)
全部代码
C++
class Trie {
private:
bool isEnd;
Trie* next[26];
public:
Trie() {
isEnd = false;
memset(next, 0, sizeof(next));
}
void insert(string word) {
Trie* node = this;
for (char c : word) {
if (node->next[c-'a'] == NULL) {
node->next[c-'a'] = new Trie();
}
node = node->next[c-'a'];
}
node->isEnd = true;
}
bool search(string word) {
Trie* node = this;
for (char c : word) {
node = node->next[c - 'a'];
if (node == NULL) {
return false;
}
}
return node->isEnd;
}
bool startsWith(string prefix) {
Trie* node = this;
for (char c : prefix) {
node = node->next[c-'a'];
if (node == NULL) {
return false;
}
}
return true;
}
};
最后
至此,您已经掌握了 Trie 树的实现以及对它的一些基本操作,感谢您的观看!
212. 单词搜索 II
方法一:回溯 + 字典树
前缀树(字典树)是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。前缀树可以用 的时间复杂度完成如下操作,其中
向前缀树中插入字符串 ;
查询前缀串 是否为已经插入到前缀树中的任意一个字符串
前缀树的实现可以参考「208. 实现 Trie (前缀树) 的官方题解」。
逐个遍历二维网格中的每一个单元格;然后搜索从该单元格出发的所有路径,找到其中对应
遍历二维网格中的所有单元格。
深度优先搜索所有从当前正在遍历的单元格出发的、由相邻且不重复的单元格组成的路径。因为题目要求同一个单元格内的字母在一个单词中不能被重复使用;所以我们在深度优先搜索的过程中,每经过一个单元格,都将该单元格的字母临时修改为特殊字符(例如 #),以避免再次经过该单元格。
如果当前路径是 中的单词,则将其添加到结果集中。如果当前路径是 中任意一个单词的前缀,则继续搜索;反之,如果当前路径不是 中任意一个单词的前缀,则剪枝。可以将 中的所有字符串先添加到前缀树中,而后用 的时间复杂度查询当前路径是否为
注意如下情况:
因为同一个单词可能在多个不同的路径中出现,所以我们需要使用哈希集合对结果集去重。
在回溯的过程中,不需要每一步都判断完整的当前路径是否是
from collections import defaultdict
class Trie:
def __init__(self):
self.children = defaultdict(Trie)
self.word = ""
def insert(self, word):
cur = self
for c in word:
cur = cur.children[c]
cur.is_word = True
cur.word = word
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
trie = Trie()
for word in words:
trie.insert(word)
def dfs(now, i1, j1):
if board[i1][j1] not in now.children:
return
ch = board[i1][j1]
now = now.children[ch]
if now.word != "":
ans.add(now.word)
board[i1][j1] = "#"
for i2, j2 in [(i1 + 1, j1), (i1 - 1, j1), (i1, j1 + 1), (i1, j1 - 1)]:
if 0 <= i2 < m and 0 <= j2 < n:
dfs(now, i2, j2)
board[i1][j1] = ch
ans = set()
m, n = len(board), len(board[0])
for i in range(m):
for j in range(n):
dfs(trie, i, j)
return list(ans)
方法二:删除被匹配的单词
假设给定一个所有单元格都是 a 的二维字符网格和单词列表 [“a”, “aa”, “aaa”, “aaaa”] 。当使用方法一来找出所有同时在二维网格和单词列表中出现的单词时,需要遍历每一个单元格的所有路径,会找到大量重复的单词。
为了缓解这种情况,可以将匹配到的单词从前缀树中移除,来避免重复寻找相同的单词。因为这种方法可以保证每个单词只能被匹配一次;所以我们也不需要再对结果集去重了。
from collections import defaultdict
class Trie:
def __init__(self):
self.children = defaultdict(Trie)
self.word = ""
def insert(self, word):
cur = self
for c in word:
cur = cur.children[c]
cur.is_word = True
cur.word = word
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
trie = Trie()
for word in words:
trie.insert(word)
def dfs(now, i1, j1):
if board[i1][j1] not in now.children:
return
ch = board[i1][j1]
nxt = now.children[ch]
if nxt.word != "":
ans.append(nxt.word)
nxt.word = ""
if nxt.children:
board[i1][j1] = "#"
for i2, j2 in [(i1 + 1, j1), (i1 - 1, j1), (i1, j1 + 1), (i1, j1 - 1)]:
if 0 <= i2 < m and 0 <= j2 < n:
dfs(nxt, i2, j2)
board[i1][j1] = ch
if not nxt.children:
now.children.pop(ch)
ans = []
m, n = len(board), len(board[0])
for i in range(m):
for j in range(n):
dfs(trie, i, j)
return ans
213. 打家劫舍 II
198. 打家劫舍 环状排列首尾两间不能同晚被偷窃,把问题简化成两个单排房子问题:
- 不偷窃第一间(即 ),最大金额是 p1 ;
- 不偷窃最后一间(即 ),最大金额是 p2。
综合偷窃最大金额:
动态规划
(不包含 ,等于 ;包含 nums[i],等于 )
返回
class Solution:
def rob(self, nums: List[int]) -> int:
def f(nums):
n = len(nums)
dp = [0] * (n+1)
dp[1] = nums[0]
for i in range(1, n):
dp[i+1] = max(dp[i], dp[i-1] + nums[i])
return dp[-1]
if len(nums) == 1: return nums[0]
return max(f(nums[1:]), f(nums[:-1]))
简化空间复杂度:
只与 和
class Solution:
def rob(self, nums: [int]) -> int:
def f(nums):
cur, pre = 0, 0
for num in nums:
cur, pre = max(pre + num, cur), cur
return cur
return max(f(nums[:-1]),f(nums[1:])) if len(nums) != 1 else nums[0]
215. 数组中的第K个最大元素
1985. 找出数组中的第 K 大整数1738. 找出第 K 大的异或坐标值 给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
方法一:排序
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
# nums.sort(reverse = True)
# return nums[k-1]
# nums.sort()
# return nums[-k]
return sorted(nums)[-k]
方法二:
快速排序和堆排序的标准代码
方法一:基于快速排序的选择方法
先对原数组排序,再返回倒数第 k 个位置,这样平均时间复杂度是 ,但其实我们可以做的更快。
首先我们来回顾一下快速排序,这是一个典型的分治算法。我们对数组
分解: 将数组 「划分」成两个子数组 ,使得 中的每个元素小于等于 ,且 小于等于$ a[q + 1 \cdots r]$ 中的每个元素。其中,计算下标 q 也是「划分」过程的一部分。
解决: 通过递归调用快速排序,对子数组 和 进行排序。
合并: 因为子数组都是原址排序的,所以不需要进行合并操作, 已经有序。
上文中提到的 「划分」 过程是:从子数组 中选择任意一个元素 x 作为主元,调整子数组的元素使得左边的元素都小于等于它,右边的元素都大于等于它, x 的最终位置就是 q。
由此可以发现每次经过「划分」操作后,我们一定可以确定一个元素的最终位置,即 x 的最终位置为 q,并且保证 中的每个元素小于等于 ,且 小于等于 中的每个元素。所以只要某次划分的 q 为倒数第 k 个下标的时候,我们就已经找到了答案。 我们只关心这一点,至于 和
因此我们可以改进快速排序算法来解决这个问题:在分解的过程当中,我们会对子数组进行划分,如果划分得到的 q 正好就是我们需要的下标,就直接返回 ;否则,如果 q 比目标下标小,就递归右子区间,否则递归左子区间。这样就可以把原来递归两个区间变成只递归一个区间,提高了时间效率。这就是「快速选择」算法。
我们知道快速排序的性能和「划分」出的子数组的长度密切相关。直观地理解如果每次规模为 n 的问题我们都划分成 1 和 n - 1,每次递归的时候又向 n - 1 的集合中递归,这种情况是最坏的,时间代价是 。我们可以引入随机化来加速这个过程,它的时间代价的期望是 O(n),证明过程可以参考「《算法导论》9.2:期望为线性的选择算法」。
class Solution {
public:
int quickSelect(vector<int>& a, int l, int r, int index) {
int q = randomPartition(a, l, r);
if (q == index) {
return a[q];
} else {
return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index);
}
}
inline int randomPartition(vector<int>& a, int l, int r) {
int i = rand() % (r - l + 1) + l;
swap(a[i], a[r]);
return partition(a, l, r);
}
inline int partition(vector<int>& a, int l, int r) {
int x = a[r], i = l - 1;
for (int j = l; j < r; ++j) {
if (a[j] <= x) {
swap(a[++i], a[j]);
}
}
swap(a[i + 1], a[r]);
return i + 1;
}
int findKthLargest(vector<int>& nums, int k) {
srand(time(0));
return quickSelect(nums, 0, nums.size() - 1, nums.size() - k);
}
};
我们也可以使用堆排序来解决这个问题——建立一个大根堆,做 k - 1k−1 次删除操作后堆顶元素就是我们要找的答案。在很多语言中,都有优先队列或者堆的的容器可以直接使用,但是在面试中,面试官更倾向于让更面试者自己实现一个堆。所以建议读者掌握这里大根堆的实现方法,在这道题中尤其要搞懂「建堆」、「调整」和「删除」的过程。
友情提醒:「堆排」在很多大公司的面试中都很常见,不了解的同学建议参考《算法导论》或者大家的数据结构教材,一定要学会这个知识点哦!_
class Solution {
public:
void maxHeapify(vector<int>& a, int i, int heapSize) {
int l = i * 2 + 1, r = i * 2 + 2, largest = i;
if (l < heapSize && a[l] > a[largest]) {
largest = l;
}
if (r < heapSize && a[r] > a[largest]) {
largest = r;
}
if (largest != i) {
swap(a[i], a[largest]);
maxHeapify(a, largest, heapSize);
}
}
void buildMaxHeap(vector<int>& a, int heapSize) {
for (int i = heapSize / 2; i >= 0; --i) {
maxHeapify(a, i, heapSize);
}
}
int findKthLargest(vector<int>& nums, int k) {
int heapSize = nums.size();
buildMaxHeap(nums, heapSize);
for (int i = nums.size() - 1; i >= nums.size() - k + 1; --i) {
swap(nums[0], nums[i]);
--heapSize;
maxHeapify(nums, 0, heapSize);
}
return nums[0];
}
};
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
def adju_max_heap(nums_list, in_node): # 从当前内部节点处修正大根堆
""""in_node是内部节点的索引"""
l, r, large_idx= 2*in_node+1, 2*in_node+2, in_node # 最大值的索引默认为该内部节点
if l < len(nums_list) and nums_list[large_idx] < nums[l]:
# 如果左孩子值大于该内部节点的值,则最大值索引指向左孩子
large_idx = l
if r < len(nums_list) and nums_list[large_idx] < nums[r]:
# 如果执行了上一个if语句,此时最大值索引指向左孩子,否则还是指向该内部节点
# 然后最大值索引指向的值和右孩子的值比较
large_idx = r
# 上述两个if就是得到(内部节点,左孩子,右孩子)中最大值的索引
if large_idx != in_node: # 如果最大值在左孩子和右孩子中,则和内部节点交换
nums_list[large_idx], nums_list[in_node] = nums_list[in_node], nums_list[large_idx]
# 如何内部节点是和左孩子交换,那就递归修正它的左子树,否则递归修正它的右子树
adju_max_heap(nums_list, large_idx)
def build_max_heap(nums_list): # 由列表建立大根堆
""""从后往前遍历所有内部节点,其中最后一个内部节点的公式为len(nums_list)//2 - 1"""
for in_node in range(len(nums_list)//2 - 1, -1, -1):
adju_max_heap(nums_list, in_node)
def find_kth_max(nums_list, k): # 从列表中找到第k个最大的
build_max_heap(nums_list) # 先建立大根堆
for _ in range(k-1):
nums_list[0], nums_list[-1] = nums_list[-1], nums_list[0] # 堆头和堆尾交换
nums_list.pop() # 删除堆尾
adju_max_heap(nums_list, 0) # 从堆头处开始修正大根堆
return nums_list[0]
return find_kth_max(nums, k)
216. 组合总和 III
方法一:回溯
class Solution:
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
def backtrack(idx, m, combinate):
if m == 0 and len(combinate) == k:
res.append(combinate)
return
if m < 0 or len(combinate) >= k: return
for i in range(idx, 10):
backtrack(i + 1, m - i, combinate + [i])
res = []
backtrack(1, n, [])
return res