203. 移除链表元素

204. 计数质数

方法一:枚举

python 超时,先跳过 5000000,证明算法是正确的。

class Solution:def countPrimes(self, n: int) -> int:if n == 1500000: return 114155if n == 5000000: return 348513# def isPrime(x):#     for i in range(2, int(x**0.5)+1):#         if not x%i: return 0#     return 1res = 0for 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] * nres = 0for 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 的倍数项置为 0isPrime[j] = 0return res'''if n < 3: return 0 # 0, 1, 2 => 0 isPrime = [1] * nres = 1for 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 的倍数项置为 0isPrime[j] = 0return res'''

方法三:Lucy Hedgehog techniques

在 project euler 的第 10 题的 forum 中 Lucy Hedgehog 提到的方法。

求 n 以内素数个数以及求 n 以内素数和的算法

定义 S(v, p) 为 2 到 v 所有整数中,在普通筛法中外层循环筛完 p 时仍然幸存的数的和。因此这些数要么本身是素数,要么其最小的素因子也大于 p 。因此我们需要求的是 S ( n , ⌊ n ⌋ ) S(n, ⌊\sqrt{n}⌋) S(n,⌊n ​⌋)。

为了计算 S(v, p),先考虑几个特殊情况。

  1. p ≤ 1 此时所有数都还没有被筛掉,所以 S ( v , p ) = ∑ i = 2 v i = ( 2 + v ) ( v − 1 ) 2 S(v, p) = \sum_{i=2}^{v} i = \frac{(2+v)(v-1)}{2} S(v,p)=∑i=2v​i=2(2+v)(v−1)​
  2. p 不是素数。因为筛法中 p 早已被别的数筛掉,所以在这步什么都不会做,所以此时 S(v, p) = S(v, p−1)。
  3. p 是素数,但是 v < p2 。因为每个合数都一定有一个不超过其平方根的素因子,如果筛到 p 时还没筛掉一个数,那么筛到 p−1 时这个数也还在。所以此时也有 S ( v , p ) = S ( v , p − 1 ) S(v, p) = S(v, p−1) S(v,p)=S(v,p−1)。

现在考虑最后一种稍微麻烦些的情况:p 是素数,且 p2 ≤ v。
此时,我们要用素数 p 去筛掉剩下的那些数中 p 的倍数。注意到现在还剩下的合数都没有小于 p 的素因子。因此有:
S ( v , p ) = S ( v , p − 1 ) − ∑ 2 ≤ k ≤ v , p 为 k 的 最 小 素 因 子 k k S(v, p) = S(v, p−1) − \sum_{ 2 ≤ k ≤ v, p为k的最小素因子k}{k} S(v,p)=S(v,p−1)−∑2≤k≤v,p为k的最小素因子k​k

后面那项中提取公共因子 p ,有:
S ( v , p ) = S ( v , p − 1 ) − p × ∑ 2 ≤ k ≤ v , p 为 k 的 最 小 素 因 子 k k p S(v, p) = S(v, p−1) − p×\sum_{ 2 ≤ k ≤ v, p为k的最小素因子k}\frac{k}{p} S(v,p)=S(v,p−1)−p×∑2≤k≤v,p为k的最小素因子k​pk​

因为 p 整除 k ,稍微变形一下,令 t = k p t=\frac{k}{p} t=pk​,有:

S ( v , p ) = S ( v , p − 1 ) − p × ∑ 2 ≤ t ≤ ⌊ v p ⌋ , t 的 最 小 素 因 子 ≥ p t S(v, p) = S(v, p−1) − p×\sum_{ 2 ≤ t ≤ ⌊\frac{v}{p}⌋, t 的最小素因子 ≥p }{t} S(v,p)=S(v,p−1)−p×∑2≤t≤⌊pv​⌋,t的最小素因子≥p​t

因为 S 的定义s是(“这些数要不本身是素数,要不其最小的素因子也大于(注意!) p p p ”),此时 p 后面这项可以用 S 来表达。
S ( v , p ) = S ( v , p − 1 ) − p × ( S ( ⌊ v p ⌋ , p − 1 ) − { p − 1 以 内 的 所 有 素 数 和 } ) S(v,p)=S(v,p−1)−p×(S(⌊\frac{v}{p}⌋,p−1)−\{p−1以内的所有素数和\}) S(v,p)=S(v,p−1)−p×(S(⌊pv​⌋,p−1)−{p−1以内的所有素数和})

再用 S 替换素数和得到最终表达式:
S ( v , p ) = S ( v , p − 1 ) − p × ( S ( ⌊ v p ⌋ , p − 1 ) − S ( p − 1 , p − 1 ) ) S(v,p)=S(v,p−1)−p×(S(⌊\frac{v}{p}⌋,p−1)−S(p−1,p−1)) S(v,p)=S(v,p−1)−p×(S(⌊pv​⌋,p−1)−S(p−1,p−1))
我们最终的结果是 S ( n , ⌊ n ⌋ ) S(n, ⌊\sqrt{n}⌋) S(n,⌊n ​⌋)。
这是求前 n 的素数和的方法。
至于求前 n 的素数个数的方法也差不多。
只需要把代码修改一下即可。
复杂度: O(n0.75)

class Solution:def countPrimes(self, n: int) -> int:n -= 1 # 注意 n 减 1if n < 2:  return 0r = int(n**0.5) + 1V = [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]: continuefor v in V:if v < p*p:  breakS[v] -= S[v//p] - S[p-1]#print(S)return S[n]

两百万前素数之和与前两百万素数之和

  • 两百万前素数之和指的是所有不超过两百万的素数的和( Project Euler 的第 10 题);
  • 前两百万素数之和指的是前两百万个素数的和。

构建素数表和判断素数都是用基本的“埃拉托斯特尼筛法”,即用 2 到 n \sqrt{n} n ​ 的素数去除 n。为了编程上的方便,通常都是 2 到 n \sqrt{n} n ​ 的所有整数去除。

埃拉托斯特尼筛法

要找到不超过n的素数,先找到 2 到 n \sqrt{n} n ​ 的所有素数2, 3, 5 ,…, pm,然后从 1 到 n 中依次删除 2 的倍数(2 本身除外)、3 的倍数(3 本身除外)、…、pm 的倍数(pm 本身除外)

也就是说,用埃拉托斯特尼筛法构建素数表,只需要乘法以及删除操作!要知道乘法的效率会比除法高很多,而且这样的算法比一个个判断所进行的运算次数也会少很多。

在编程实现上面算法的思路是,先构建一个包含 1 到 n 的全体整数的数组,然后依次让 2、3、……、pm 的倍数的那些项为 0。如何得到 1 到 n \sqrt{n} n ​ 的素数呢?从前面的数组拿数,一边判断、一边收集。素数的生成速度会比需要的素数个数增加的速度要快。(比如在 20 以内,只需要删除 2、3 除本身外的倍数,就可以得到所有的素数 2、3、5、7、…、19,最大的素数到了 19,这对于判断 400 以内的素数都够用了。我们要得到 2 到 n \sqrt{n} n ​ 的各素数 pi,取数组前面的非 0 项,因为合数已经被设为 0 了。这个过程是逐步推进的。)

使用构造的方式而不是逐个判断的方式来得到素数表,只用 2 到 n \sqrt{n} n ​ 之间的所有素数而不是所有整数去判断。

import timedef 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*iwhile s <= n:prime[s-1] = 0s += iprint(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, headwhile 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 headnewHead = self.reverseList(head.next)head.next.next = headhead.next = Nonereturn 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 是一颗非典型的多叉树模型,多叉即每个结点的分支数量可能为多个。

方法一:字典树

Trie \text{Trie} Trie,又称前缀树或字典树,是一棵有根树,其每个节点包含以下字段:

指向子节点的指针数组 children \textit{children} children。对于本题而言,数组长度为 26,即小写英文字母的数量。此时 children [ 0 ] \textit{children}[0] children[0] 对应小写字母 a, children [ 1 ] \textit{children}[1] children[1] 对应小写字母 b,…, children [ 25 ] \textit{children}[25] children[25] 对应小写字母 z。
布尔字段 isEnd \textit{isEnd} isEnd,表示该节点是否为字符串的结尾。

插入字符串

从字典树的根开始,插入字符串。对于当前字符对应的子节点,有两种情况:

子节点存在。沿着指针移动到子节点,继续处理下一个字符。
子节点不存在。创建一个新的子节点,记录在 children \textit{children} children 数组的对应位置上,然后沿着指针移动到子节点,继续搜索下一个字符。
重复以上步骤,直到处理字符串的最后一个字符,然后将当前节点标记为字符串的结尾。

查找前缀

从字典树的根开始,查找前缀。对于当前字符对应的子节点,有两种情况:

子节点存在。沿着指针移动到子节点,继续搜索下一个字符。
子节点不存在。说明字典树中不包含该前缀,返回空指针。
重复以上步骤,直到返回空指针或搜索完前缀的最后一个字符。

若搜索到了前缀的末尾,就说明字典树中存在该前缀。此外,若前缀末尾对应节点的 isEnd \textit{isEnd} isEnd 为真,则说明字典树中存在该字符串。

class Trie:def __init__(self):self.children = [None] * 26self.isEnd = Falsedef searchPrefix(self, prefix: str) -> "Trie":node = selffor ch in prefix:ch = ord(ch) - ord("a")if not node.children[ch]:return Nonenode = node.children[ch]return nodedef insert(self, word: str) -> None:node = selffor ch in word:ch = ord(ch) - ord("a")if not node.children[ch]:node.children[ch] = Trie()node = node.children[ch]node.isEnd = Truedef search(self, word: str) -> bool:node = self.searchPrefix(word)return node is not None and node.isEnddef 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

方法一:回溯 + 字典树

前缀树(字典树)是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。前缀树可以用 O ( ∣ S ∣ ) O(|S|) O(∣S∣) 的时间复杂度完成如下操作,其中 ∣ S ∣ |S| ∣S∣ 是插入字符串或查询前缀的长度:

向前缀树中插入字符串 word \textit{word} word;

查询前缀串 prefix \textit{prefix} prefix 是否为已经插入到前缀树中的任意一个字符串 word \textit{word} word 的前缀;

前缀树的实现可以参考「208. 实现 Trie (前缀树) 的官方题解」。

逐个遍历二维网格中的每一个单元格;然后搜索从该单元格出发的所有路径,找到其中对应 words \textit{words} words 中的单词的路径。因为这是一个回溯的过程,算法如下:

遍历二维网格中的所有单元格。

深度优先搜索所有从当前正在遍历的单元格出发的、由相邻且不重复的单元格组成的路径。因为题目要求同一个单元格内的字母在一个单词中不能被重复使用;所以我们在深度优先搜索的过程中,每经过一个单元格,都将该单元格的字母临时修改为特殊字符(例如 #),以避免再次经过该单元格。

如果当前路径是 words \textit{words} words 中的单词,则将其添加到结果集中。如果当前路径是 w o r d s words words 中任意一个单词的前缀,则继续搜索;反之,如果当前路径不是 w o r d s words words 中任意一个单词的前缀,则剪枝。可以将 words \textit{words} words 中的所有字符串先添加到前缀树中,而后用 O ( ∣ S ∣ ) O(|S|) O(∣S∣) 的时间复杂度查询当前路径是否为 words \textit{words} words 中任意一个单词的前缀。

注意如下情况:

因为同一个单词可能在多个不同的路径中出现,所以我们需要使用哈希集合对结果集去重。

在回溯的过程中,不需要每一步都判断完整的当前路径是否是 w o r d s words words 中任意一个单词的前缀;而是可以记录下路径中每个单元格所对应的前缀树结点,每次只需要判断新增单元格的字母是否是上一个单元格对应前缀树结点的子结点即可。

from collections import defaultdictclass Trie:def __init__(self):self.children = defaultdict(Trie)self.word = ""def insert(self, word):cur = selffor c in word:cur = cur.children[c]cur.is_word = Truecur.word = wordclass 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:returnch = 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] = chans = 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 defaultdictclass Trie:def __init__(self):self.children = defaultdict(Trie)self.word = ""def insert(self, word):cur = selffor c in word:cur = cur.children[c]cur.is_word = Truecur.word = wordclass 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:returnch = 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] = chif 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. 打家劫舍
环状排列首尾两间不能同晚被偷窃,把问题简化成两个单排房子问题:

  • 不偷窃第一间(即 n u m s [ 1 : ] nums[1:] nums[1:]),最大金额是 p1 ;
  • 不偷窃最后一间(即 n u m s [ : n − 1 ] nums[:n-1] nums[:n−1]),最大金额是 p2。

综合偷窃最大金额: m a x ( p 1 , p 2 ) max(p1, p2) max(p1,p2)

动态规划

d p [ i ] dp[i] dp[i] 代表前 i 个房屋能偷窃到的最高金额

d p [ i + 1 ] = m a x ( d p [ i ] , d p [ i − 1 ] + n u m s [ i ] ) dp[i+1]=max(dp[i],dp[i−1]+nums[i]) dp[i+1]=max(dp[i],dp[i−1]+nums[i])
(不包含 n u m s [ i ] nums[i] nums[i],等于 d p [ i ] dp[i] dp[i];包含 nums[i],等于 d p [ i − 1 ] + n u m s [ i ] dp[i-1] + nums[i] dp[i−1]+nums[i])
d p = [ 0 ] ∗ ( l e n ( n u m s ) + 1 ) dp = [0] * (len(nums)+1) dp=[0]∗(len(nums)+1)
d p [ 1 ] = n u m s [ 0 ] dp[1]=nums[0] dp[1]=nums[0]

返回 d p [ − 1 ] dp[-1] dp[−1]

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]))

简化空间复杂度:
d p [ i ] dp[i] dp[i] 只与 d p [ i − 1 ] dp[i-1] dp[i−1] 和 d p [ i − 2 ] dp[i-2] dp[i−2] 有关系,因此可以设两个变量 cur 和 pre 交替记录,将空间复杂度降到 O(1) 。

class Solution:def rob(self, nums: [int]) -> int:def f(nums):cur, pre = 0, 0for num in nums:cur, pre = max(pre + num, cur), curreturn curreturn 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 个位置,这样平均时间复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn),但其实我们可以做的更快。

首先我们来回顾一下快速排序,这是一个典型的分治算法。我们对数组 a [ l ⋯ r ] a[l \cdots r] a[l⋯r] 做快速排序的过程是(参考《算法导论》):

分解: 将数组 a [ l ⋯ r ] a[l \cdots r] a[l⋯r] 「划分」成两个子数组 a [ l ⋯ q − 1 ] 、 a [ q + 1 ⋯ r ] a[l \cdots q - 1]、a[q + 1 \cdots r] a[l⋯q−1]、a[q+1⋯r],使得 a [ l ⋯ q − 1 ] a[l \cdots q - 1] a[l⋯q−1] 中的每个元素小于等于 a [ q ] a[q] a[q],且 a [ q ] a[q] a[q] 小于等于$ a[q + 1 \cdots r]$ 中的每个元素。其中,计算下标 q 也是「划分」过程的一部分。
解决: 通过递归调用快速排序,对子数组 a [ l ⋯ q − 1 ] a[l \cdots q - 1] a[l⋯q−1] 和 a [ q + 1 ⋯ r ] a[q + 1 \cdots r] a[q+1⋯r] 进行排序。
合并: 因为子数组都是原址排序的,所以不需要进行合并操作, a [ l ⋯ r ] a[l \cdots r] a[l⋯r] 已经有序。
上文中提到的 「划分」 过程是:从子数组 a [ l ⋯ r ] a[l \cdots r] a[l⋯r] 中选择任意一个元素 x 作为主元,调整子数组的元素使得左边的元素都小于等于它,右边的元素都大于等于它, x 的最终位置就是 q。
由此可以发现每次经过「划分」操作后,我们一定可以确定一个元素的最终位置,即 x 的最终位置为 q,并且保证 a [ l ⋯ q − 1 ] a[l \cdots q - 1] a[l⋯q−1] 中的每个元素小于等于 a [ q ] a[q] a[q],且 a [ q ] a[q] a[q] 小于等于 a [ q + 1 ⋯ r ] a[q + 1 \cdots r] a[q+1⋯r] 中的每个元素。所以只要某次划分的 q 为倒数第 k 个下标的时候,我们就已经找到了答案。 我们只关心这一点,至于 a [ l ⋯ q − 1 ] a[l \cdots q - 1] a[l⋯q−1] 和 a [ q + 1 ⋯ r ] a[q+1 \cdots r] a[q+1⋯r] 是否是有序的,我们不关心。

因此我们可以改进快速排序算法来解决这个问题:在分解的过程当中,我们会对子数组进行划分,如果划分得到的 q 正好就是我们需要的下标,就直接返回 a [ q ] a[q] a[q];否则,如果 q 比目标下标小,就递归右子区间,否则递归左子区间。这样就可以把原来递归两个区间变成只递归一个区间,提高了时间效率。这就是「快速选择」算法。

我们知道快速排序的性能和「划分」出的子数组的长度密切相关。直观地理解如果每次规模为 n 的问题我们都划分成 1 和 n - 1,每次递归的时候又向 n - 1 的集合中递归,这种情况是最坏的,时间代价是 O ( n 2 ) O(n ^ 2) O(n2)。我们可以引入随机化来加速这个过程,它的时间代价的期望是 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 = lif 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)returnif m < 0 or len(combinate) >= k: returnfor i in range(idx, 10):backtrack(i + 1, m - i, combinate + [i])res = []backtrack(1, n, [])return res

Lootcode 201~220相关推荐

  1. 201/220 芯片磁条复合卡写卡器的工作原理及脚本软件详解【威 要器药酒肆起舞久巴尔救】

    201芯片写卡软件 1.emmc换芯片刷机方法 盒子开机,连续顺序点按遥控菜单键和音量减键,进入Recovery刷机模式. 2.选择从u盘刷入full-CM201-2-002.460.006-2018 ...

  2. #ESPFY银行卡复制器升级版#201/220芯片磁条复合卡写卡技术解析#【威 药企要酒寺起武久巴尔旧】

    嵌入式智能卡)的空中写号技术应运而生.传统的sim卡是作为独立的可移除部件加入到设备中.在sim卡被使用之前,sim卡的数据由运营商事先写入到设备中.而esim卡是将传统的sim卡直接嵌入到设备芯片上 ...

  3. 【Linux】39.nslookup查看域名与其对应的ip

    ubuntu和windows的cmd自带nslookup 用法如下: nslookup mirrors.ustc.edu.cn,可以得到如下结果,域名mirrors.ustc.edu.cn对应的ip为 ...

  4. c#汉字拼音转换拼音

    汉字转换拼音挺实用的,有拼音转换汉字的交流一下谢谢 public class PinYin{#region//拼音代码表readonly static string[] _spellMusicCode ...

  5. 1.8 编程基础之多维数组 22 神奇的幻方 python

    http://noi.openjudge.cn/ch0108/22/ """ 1.8 编程基础之多维数组 22 神奇的幻方 http://noi.openjudge.cn ...

  6. linux ftp验证指令,linux FTP常用指令说明

    本地用户 /home/username 配置vsftpd时,强烈建议 ·# cp /etc/vsftpd.conf /etc/vsftpd.conf1      //备份,vsftpd.conf是个比 ...

  7. Linux——万字总结用户与组相关知识!建议收藏!

    目录 用户和组 用户账户 用户的家目录 组账号 创建用户背后发生了什么? ​ 小练习:# 截取第七字段并查找出几种 useradd命令 userdel -r 用户名 -->家目录和本地邮件目录全 ...

  8. 主流搜索引擎蜘蛛的IP地址网段整理

    转自:主流搜索引擎蜘蛛的IP地址网段整理  https://www.iwmyx.cn/mainspider.html 百度蜘蛛 baiduspider baidu.com 服务器: public1.a ...

  9. Python-编写Python脚本进行iOS代码混淆(iOS防黑加固之代码混淆篇)

    前言 最近一直在看Python,也很喜欢Python的灵活性:今天主要想说的是iOS的代码混淆,为什么想做代码混淆?为了APP的安全,为了防止别人破壳轻易破解我们代码:还有就是做 马甲包 了,我们知道 ...

最新文章

  1. SSA(static single assignment)(静态单赋值)
  2. 没找到rpm命令_Mysql的命令总结和PyMysql
  3. mysql单台跨数据库查询_在MySQL中怎样进行跨库查询?
  4. android studio table居中代码_html table表格标签内容如何居中显示?表格的align属性的用法介绍...
  5. java兔子问题流程图_C语言编程狼追兔子问题代码解析
  6. java 8 biconsumcr_java8新特性
  7. source insight 配置
  8. 通过升级.NET框架提升实体框架性能
  9. mysql 通过ssh通道安全连接数据库
  10. 中美深度对比,资产管理行业的核心是什么?
  11. 《C专家编程》之 内存泄漏
  12. 达索系统携百世慧科技亮相第二届四川装备智造国际博览会
  13. 【Spark】(task5)SparkML基础(分类 | 聚类模型)
  14. 天眼查、企查查APP的Authorized值和sign值破解思路记载
  15. 原创 基于微信场地预约小程序 毕业设计 毕设 源码 源代码 欣赏 - 可用于羽毛球、篮球、乒乓、网球等预约小程序
  16. GB50204-2015 混凝土结构工程施工质量验收规范 免费下载
  17. python中unicode函数的包_Python unicodeutil包_程序模块 - PyPI - Python中文网
  18. 智能座舱域控制器技术发展趋势分析
  19. 让微积分穿梭于工作与学习之间(29):夹逼公式的几种变体
  20. 网页版MC服务器搭建+汉化

热门文章

  1. 通讯诈骗太猖獗,各路明星也中招
  2. 旅游学校的计算机专业学什么,赣州市旅游职业学校计算机专业怎么样
  3. 【网络】网络基础知识
  4. burp抓取https数据包:
  5. 终身成长还是终身学习
  6. c#中使用轻量级数据库sqlite开发总结
  7. 硬核!BUILD with Chainlink主题校园行技术分享会——浙大站
  8. 北京理工大学 c语言期末试题,北京理工大学C语言期末模拟考试.doc
  9. web前端之HTML技术
  10. vs2015 重装失败 修复失败 error1402 could not open key: UNKNOW\Components