三叶姐的刷题单

1.DFS

题目 难度 推荐指数
17. 电话号码的字母组合 中等 dfs+回溯
22. 括号生成 中等
37. 解数独 困难
39. 组合总和 中等
40. 组合总和 II 中等
211. 添加与搜索单词 - 数据结构设计 中等
282. 给表达式添加运算符 困难
301. 删除无效的括号 困难
341. 扁平化嵌套列表迭代器 中等
397. 整数替换 中等
403. 青蛙过河 困难
437. 路径总和 III 中等
488. 祖玛游戏 困难
494. 目标和 中等
559. N 叉树的最大深度 简单
563. 二叉树的坡度 简单
589. N 叉树的前序遍历 简单
590. N 叉树的后序遍历 简单
606. 根据二叉树创建字符串 简单
638. 大礼包 中等
653. 两数之和 IV - 输入 BST 简单
677. 键值映射 中等
690. 员工的重要性 简单
778. 水位上升的泳池中游泳 困难
783. 二叉搜索树节点最小距离 简单
869. 重新排序得到 2 的幂 中等
872. 叶子相似的树 简单
938. 二叉搜索树的范围和 简单
987. 二叉树的垂序遍历 困难
993. 二叉树的堂兄弟节点 简单
1239. 串联字符串的最大长度 中等
1609. 奇偶树 中等
1723. 完成所有工作的最短时间 困难
1766. 互质树 困难 1766.互质树
2044. 统计按位或能得到最大值的子集数目 困难 位运算+状态压缩/回溯

2.回溯

题目 难度 推荐指数
17. 电话号码的字母组合 中等 dfs+回溯
37. 解数独 困难
39. 组合总和 中等
40. 组合总和 II 中等
90. 子集 II 中等
131. 分割回文串 中等
212. 单词搜索 II 困难
301. 删除无效的括号 困难
306. 累加数 中等
797. 所有可能的路径 中等
1219. 黄金矿工 中等
剑指 Offer 38. 字符串的排列 中等

3.动态规划

3.1 记忆化搜索

题目 难度 推荐指数
87. 扰乱字符串 困难 不会写
375. 猜数字大小 II 中等 注意推导出dp方程,并且需要逆向遍历
for(int i=n-1;i>=1;i—){
for(int j=i+1;j<=n;j++{
}}
403. 青蛙过河 困难
494. 目标和 中等
552. 学生出勤记录 II 困难
576. 出界的路径数 中等
913. 猫和老鼠 困难
1137. 第 N 个泰波那契数 简单
剑指 Offer 10- I. 斐波那契数列 简单

4.状态压缩

题目 难度 推荐指数

464. 我能赢吗

| 中等 | 464 我能赢吗
状态压缩,枚举所有可能,注意用map存下访问过的状态 | |

473. 火柴拼正方形

| 中等 | 将正方形分成四个数组,每个数组如果值小于边就加上去,最后用dfs+回溯来求解问题的。(在回溯中间如果dfs得到true,就会直接返回得到true的结果。) | |

698. 划分为k个相等的子集

| 中等 | 698. 划分为k个相等的子集
回溯dfs会超时,需要剪枝(怎么剪枝),此外本题目还可以DP |

5.前缀和

题目 难度 推荐指数

661. 图片平滑器

| 简单 | 可以使用二维的前缀和来求解
image.png |

6.手撕排序算法

image.png

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. /*
  4. 1.冒泡排序
  5. 稳定排序
  6. 最佳时间复杂度为o(n)
  7. 平均时间复杂度为o(n^2)
  8. 最差时间复杂度为o(n^2)
  9. 空间复杂度为o(1)
  10. */
  11. //原版本
  12. void bubbleSort(vector<int> &nums)
  13. {
  14. int len = nums.size();
  15. for (int i = 0; i < len - 1; ++i)
  16. {
  17. for (int j = 0; j < len - 1 - i; ++J)
  18. {
  19. if (nums[j] > nums[j + 1])
  20. {
  21. swap(nums[j], nums[j + 1]);
  22. }
  23. }
  24. }
  25. }
  26. //冒泡优化版本
  27. void bubbleSort(vector<int> &nums)
  28. {
  29. int len = nums.size();
  30. bool flag = false;
  31. for (int i = 0; i < len - 1; ++i)
  32. {
  33. for (int j = 0; j < len - 1 - i; ++J)
  34. {
  35. if (nums[j] > nums[j + 1])
  36. {
  37. flag = true;
  38. swap(nums[j], nums[j + 1]);
  39. }
  40. }
  41. if (flag == false)
  42. break;
  43. }
  44. }
  45. /*
  46. 2.选择排序
  47. 选择排序是给每个位置选择当前元素最小的,
  48. 比如给第一个位置选择最小的,
  49. 在剩余元素里面给>二个
  50. 元素选择第二小的,
  51. 依次类推,直到第n-1个元素,第n个元素不用选择了
  52. ,因为只剩下它一个最大的元素了。
  53. 不稳定排序
  54. 最佳时间复杂度为o(n^2)
  55. 平均时间复杂度为o(n^2)
  56. 最差时间复杂度为o(n^2)
  57. 空间复杂度为o(1)
  58. */
  59. void selection(vector<int> &a, int n)
  60. {
  61. int minIndex;
  62. for (int i = 0; i < n; ++i)
  63. {
  64. minIndex = i;
  65. for (int j = i + 1; j < n; ++j)
  66. {
  67. if (a[j] < a[minIndex])
  68. minIndex = j;
  69. }
  70. swap(a[i], a[minIndex]);
  71. }
  72. }
  73. /*
  74. 3.插入排序
  75. 插入排序是在一个已经有序的小序列的基础上,
  76. 一次插入一个元素。
  77. 当然,刚开始这个有序的小序列只有1个元素,
  78. 就是第一个元素。
  79. 比较是从有序序列的末尾开 始,也就
  80. 是想要插入的元素和已经有序的最大者开始比起,
  81. 如果比它大则直接插入在其后面,否则一直往前找直
  82. 到找到它该插入的位置。
  83. 如果碰见一个和插入元素相等的,
  84. 那么插入元素把想插入的元素放在相等元素的后面
  85. 稳定排序
  86. 最佳时间复杂度为o(n)
  87. 平均时间复杂度为o(n^2)
  88. 最差时间复杂度为o(n^2)
  89. 空间复杂度为o(1)
  90. */
  91. void insertionSort(vector<int> &a, int n)
  92. { //{ 9,1,5,6,2,3 }
  93. for (int i = 1; i < n; ++i)
  94. {
  95. if (a[i] < a[i - 1])
  96. { //若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入
  97. int j = i - 1;
  98. int x = a[i]; //复制为哨兵,即存储待排序元素
  99. // a[i] = a[i - 1];
  100. //先后移一个元素,可以不要这一句,跟循环里面的功能重复了
  101. while (j >= 0 && x < a[j])
  102. { //查找在有序表的插入位置,还必须要保证j是>=0的因为a[j] 要合法
  103. a[j + 1] = a[j];
  104. j--; //元素后移
  105. }
  106. a[j + 1] = x; //插入到正确位置
  107. }
  108. }
  109. }
  110. /*
  111. 4.快速排序
  112. 选取第一个数为基准
  113. 将比基准小的数交换到前面,比基准大的数交换到后面
  114. 对左右区间重复第二步,直到各区间只有一个数
  115. 稳定排序
  116. 最佳时间复杂度为o(nlogn)
  117. 平均时间复杂度为o(nlogn)
  118. 最差时间复杂度为o(n^2)
  119. 空间复杂度为o(1ogn)
  120. */
  121. void quickSort(vector<int> &a, int low, int high)
  122. {
  123. if (low >= high) // 结束标志
  124. return;
  125. int first = low; // 低位下标
  126. int last = high; // 高位下标
  127. int key = a[first]; // 设第一个为基准
  128. while (first < last)
  129. {
  130. // 从后往前走,将比第一个小的移到前面
  131. while (first < last && a[last] > key)
  132. last--;
  133. if (first < last)
  134. a[first++] = a[last];
  135. //从前往后走, 将比第一个大的移到后面
  136. while (first < last && a[first] <= key)
  137. first++;
  138. if (first < last)
  139. a[last--] = a[first];
  140. }
  141. a[first] = key;
  142. // 前半递归
  143. quickSort(a, low, first - 1);
  144. // 后半递归
  145. quickSort(a, first + 1, high);
  146. }
  147. /*
  148. 5.希尔排序
  149. 希尔排序就是为了加快速度简单地改进了插入排序,
  150. 交换不相邻的元素以对数组的局部进行排序
  151. 希尔排序的思想是采用插入排序的方法,
  152. 先让数组中任意间隔为 h 的元素有序,
  153. 刚开始 h 的大小可以是
  154. h = n / 2,接着让 h = n / 4,让 h 一直缩小,
  155. 当 h = 1 时,也就是此时数组中任意间隔为1的元素有序,
  156. 此时的数组就是有序的了。
  157. 稳定排序
  158. 最佳时间复杂度为o(n)
  159. 平均时间复杂度为o((nlogn)^2)
  160. 最差时间复杂度为o((nlogn)^2)
  161. 空间复杂度为o(1)
  162. */
  163. void shellSortCore(vector<int> &nums, int gap, int i)
  164. {
  165. int inserted = nums[i];
  166. int j;
  167. // 插入的时候按组进行插入
  168. for (j = i - gap; j >= 0 && inserted < nums[j]; j -= gap)
  169. {
  170. nums[j + gap] = nums[j];
  171. }
  172. nums[j + gap] = inserted;
  173. }
  174. void shellSort(vector<int> &nums)
  175. {
  176. int len = nums.size();
  177. //进行分组,最开始的时候,gap为数组长度一半
  178. for (int gap = len / 2; gap > 0; gap /= 2)
  179. {
  180. //对各个分组进行插入分组
  181. for (int i = gap; i < len; ++i)
  182. {
  183. //将nums[i]插入到所在分组正确的位置上
  184. shellSortCore(nums, gap, i);
  185. }
  186. }
  187. for (auto a : nums)
  188. {
  189. cout << a << "";
  190. }
  191. }
  192. /*
  193. 6.归并排序
  194. 将一个大的无序数组有序,我们可以把大的数组分成两个,
  195. 然后对这两个数组分别进行排序,之后在把
  196. 这两个数组合并成一个有序的数组。
  197. 由于两个小的数组都是有序的,所以在合并的时候是很快的。
  198. 通过递归的方式将大的数组一直分割,
  199. 直到数组的大小为 1,此时只有一个元素,那么该数组就是有序
  200. 的了,之后再把两个数组大小为1的合并成一个大小为2的,
  201. 再把两个大小为2的合并成4的 …
  202. 直到全部
  203. 小的数组合并起来。
  204. 1、把长度为n的输入序列分成两个长度为n/2的子序列;
  205. 2、对这两个子序列分别采用归并排序;
  206. 3、 将两个排序好的子序列合并成一个最终的排序序列
  207. 稳定排序
  208. 最佳时间复杂度为o(nlogn)
  209. 平均时间复杂度为o(nlogn)
  210. 最差时间复杂度为o(nlogn)
  211. 空间复杂度为o(n)
  212. */
  213. void mergeSort(vector<int> &data)
  214. {
  215. int len = data.size();
  216. vector<int> dataTemp(len, 0);
  217. for (int seg = 1; seg < len; seg += seg)
  218. {
  219. for (int start = 0; start < len; start += seg + seg)
  220. {
  221. int low = start, mid = min(start + seg, len), high = min(start + seg + seg, len);
  222. int index = low, start1 = low, end1 = mid, start2 = mid, end2 = high;
  223. while (start1 < end1 && start2 < end2)
  224. {
  225. dataTemp[index++] = data[start1] < data[start2] ? data[start1++] : data[start2++];
  226. }
  227. while (start1 < end1)
  228. {
  229. dataTemp[index++] = data[start1++];
  230. }
  231. while (start2 < end2)
  232. {
  233. dataTemp[index++] = data[start2++];
  234. }
  235. }
  236. swap(data, dataTemp);
  237. }
  238. for (auto a : data)
  239. cout << a << " ";
  240. }
  241. /*
  242. 7.堆排序
  243. 稳定排序
  244. 最佳时间复杂度为o(nlogn)
  245. 平均时间复杂度为o(nlogn)
  246. 最差时间复杂度为o(nlogn)
  247. 空间复杂度为o(1)
  248. */
  249. void heapify(vector<int> &nums, int n, int i) //对有一定顺序的堆,
  250. //当前第i个结点取根左右的最大值(这个操作称heapfiy)
  251. {
  252. int l = i * 2 + 1, r = i * 2 + 2;
  253. int max = i;
  254. if (l < n && nums[l] > nums[max])
  255. max = l;
  256. if (r < n && nums[r] > nums[max])
  257. max = r;
  258. if (max != i)
  259. {
  260. swap(nums[max], nums[i]);
  261. heapify(nums, n, max);
  262. }
  263. }
  264. void heapify_build(vector<int> &nums, int n) //建立大根堆,从树的倒数第二层第一个结点开始,
  265. //对每个结点进行heapify操作,然后向上走
  266. {
  267. int temp = (n - 2) / 2;
  268. for (int i = temp; i >= 0; i--)
  269. heapify(nums, n, i);
  270. for (int i = 0; i < nums.size(); i++)
  271. cout << nums[i] << " ";
  272. cout << endl;
  273. }
  274. void heapify_sort(vector<int> &nums, int n) //建立大根堆之后,每次交换最后一个结点和根节点
  275. (最大值),
  276. //对交换后的根节点继续进行heapify(此时堆的最后一位是最大值,因此不用管他,n变为n-1)
  277. {
  278. heapify_build(nums, n);
  279. for (int i = 0; i < n; i++)
  280. {
  281. swap(nums.front(), nums[n - i - 1]);
  282. heapify(nums, n - i - 1, 0);
  283. }
  284. }
  285. /*
  286. 8.计数排序 Timesort
  287. 计数排序统计小于等于该元素值的元素的个数i,于是该元素就放在目标数组的索引i位(i≥0)。
  288. 计数排序基于一个假设,待排序数列的所有数均为整数,且出现在(0,k)的区间之内。
  289. 如果 k(待排数组的最大值) 过大则会引起较大的空间复杂度,一般是用来排序 0 到 100 之间的数字
  290. 的最好的算法,但是它不适合按字母顺序排序人名。
  291. 计数排序不是比较排序,排序的速度快于任何比较排序算法。
  292. 算法思想:
  293. 1. 找出待排序的数组中最大和最小的元素;
  294. 2. 统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i 项;
  295. 3. 对所有的计数累加(从 C 中的第一个元素开始,每一项和前一项相加);
  296. 4. 向填充目标数组:将每个元素 i 放在新数组的第 C[i] 项,每放一个元素就将 C[i] 减去 1;
  297. 稳定排序
  298. 最佳时间复杂度为o(n)
  299. 平均时间复杂度为o(nlogn)
  300. 最差时间复杂度为o(nlogn)
  301. 空间复杂度为o(n+k)
  302. */
  303. #include <iostream>
  304. #include <vector>
  305. #include <algorithm>
  306. using namespace std;
  307. // 计数排序
  308. void CountSort(vector<int> &vecRaw, vector<int> &vecObj)
  309. {
  310. // 确保待排序容器非空
  311. if (vecRaw.size() == 0)
  312. return;
  313. // 使用 vecRaw 的最大值 + 1 作为计数容器 countVec 的大小
  314. int vecCountLength = (*max_element(begin(vecRaw), end(vecRaw))) + 1;
  315. vector<int> vecCount(vecCountLength, 0);
  316. // 统计每个键值出现的次数
  317. for (int i = 0; i < vecRaw.size(); i++)
  318. vecCount[vecRaw[i]]++;
  319. // 后面的键值出现的位置为前面所有键值出现的次数之和
  320. for (int i = 1; i < vecCountLength; i++)
  321. vecCount[i] += vecCount[i - 1];
  322. // 将键值放到目标位置
  323. for (int i = vecRaw.size(); i > 0; i--) // 此处逆序是为了保持相同键值的稳定性
  324. vecObj[--vecCount[vecRaw[i - 1]]] = vecRaw[i - 1];
  325. }
  326. int main()
  327. {
  328. vector<int> vecRaw = {0, 5, 7, 9, 6, 3, 4, 5, 2, 8, 6, 9, 2, 1};
  329. vector<int> vecObj(vecRaw.size(), 0);
  330. CountSort(vecRaw, vecObj);
  331. for (int i = 0; i < vecObj.size(); ++i)
  332. cout << vecObj[i] << " ";
  333. cout << endl;
  334. return 0;
  335. }
  336. /*
  337. 9.桶排序
  338. 将值为i的元素放入i号桶,最后依次把桶里的元素倒出来。
  339. 算法思想:
  340. 1. 设置一个定量的数组当作空桶子。
  341. 2. 寻访序列,并且把项目一个一个放到对应的桶子去。
  342. 3. 对每个不是空的桶子进行排序。
  343. 4. 从不是空的桶子里把项目再放回原来的序列中。
  344. 最佳时间复杂度为o(n+k)
  345. 平均时间复杂度为o(n+k)
  346. 最差时间复杂度为o(n^2)
  347. 空间复杂度为o(n+k)
  348. */
  349. function bucketSort(arr, bucketSize)
  350. {
  351. if (arr.length == = 0)
  352. {
  353. return arr;
  354. }
  355. var i;
  356. var minValue = arr[0];
  357. var maxValue = arr[0];
  358. for (i = 1; i < arr.length; i++)
  359. {
  360. if (arr[i] < minValue)
  361. {
  362. minValue = arr[i]; // 输入数据的最小值
  363. }
  364. else if (arr[i] > maxValue)
  365. {
  366. maxValue = arr[i]; // 输入数据的最大值
  367. }
  368. }
  369. // 桶的初始化
  370. var DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5
  371. bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
  372. var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
  373. var buckets = new Array(bucketCount);
  374. for (i = 0; i < buckets.length; i++)
  375. {
  376. buckets[i] = [];
  377. }
  378. // 利用映射函数将数据分配到各个桶中
  379. for (i = 0; i < arr.length; i++)
  380. {
  381. buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
  382. }
  383. arr.length = 0;
  384. for (i = 0; i < buckets.length; i++)
  385. {
  386. insertionSort(buckets[i]); // 对每个桶进行排序,这里使用了插
  387. 入排序
  388. for (var j = 0; j < buckets[i].length; j++)
  389. {
  390. arr.push(buckets[i][j]);
  391. }
  392. }
  393. return arr;
  394. }
  395. /*
  396. 10、基数排序
  397. 一种多关键字的排序算法,可用桶排序实现。
  398. 算法思想:
  399. 1. 取得数组中的最大数,并取得位数;
  400. 2. arr为原始数组,从最低位开始取每个位组成radix数组;
  401. 3. 对radix进行计数排序(利用计数排序适用于小范围数的特点)
  402. 最佳时间复杂度为o(nk)
  403. 平均时间复杂度为o(nk)
  404. 最差时间复杂度为o(nk)
  405. 空间复杂度为o(n+k)
  406. */
  407. int maxbit(int data[], int n) //辅助函数,求数据的最大位数
  408. {
  409. int maxData = data[0]; ///< 最大数
  410. /// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
  411. for (int i = 1; i < n; ++i)
  412. {
  413. if (maxData < data[i])
  414. maxData = data[i];
  415. }
  416. int d = 1;
  417. int p = 10;
  418. while (maxData >= p)
  419. {
  420. // p *= 10; // Maybe overflow
  421. maxData /= 10;
  422. ++d;
  423. }
  424. return d;
  425. /* int d = 1; //保存最大的位数
  426. int p = 10;
  427. for(int i = 0; i < n; ++i)
  428. {
  429. while(data[i] >= p)
  430. {
  431. p *= 10;
  432. ++d;
  433. }
  434. }
  435. return d;*/
  436. }
  437. void radixsort(int data[], int n) //基数排序
  438. {
  439. int d = maxbit(data, n);
  440. int *tmp = new int[n];
  441. int *count = new int[10]; //计数器
  442. int i, j, k;
  443. int radix = 1;
  444. for (i = 1; i <= d; i++) //进行d次排序
  445. {
  446. for (j = 0; j < 10; j++)
  447. count[j] = 0; //每次分配前清空计数器
  448. for (j = 0; j < n; j++)
  449. {
  450. k = (data[j] / radix) % 10; //统计每个桶中的记录数
  451. count[k]++;
  452. }
  453. for (j = 1; j < 10; j++)
  454. count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
  455. for (j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
  456. {
  457. k = (data[j] / radix) % 10;
  458. tmp[count[k] - 1] = data[j];
  459. count[k]--;
  460. }
  461. for (j = 0; j < n; j++) //将临时数组的内容复制到data中
  462. data[j] = tmp[j];
  463. radix = radix * 10;
  464. }
  465. delete[] tmp;
  466. delete[] count;
  467. }

7.布隆过滤器

布隆过滤器原理与优点
布隆过滤器是一个比特向量或者比特数组,它本质上是一种概率型数据结构,用来查找一个元素是否在
集合中,支持高效插入和查询某条记录。常作为针对超大数据量下高效查找数据的一种方法。
它的具体工作过程是这样子的:
假设布隆过滤器的大小为m(比特向量的长度为m),有k个哈希函数,它对每个数据用这k个哈希函数
计算哈希,得到k个哈希值,然后将向量中相应的位设为1。在查询某个数据是否存在的时候,对这个数
据用k个哈希函数得到k个哈希值,再在比特向量中相应的位查找是否为1,如果某一个相应的位不为1,
那这个数据就肯定不存在。但是如果全找到了,则这个数据有可能存在。
为什么说有可能存在呢?
因为不同的数据经过哈希后可能有相同的哈希值,在比特向量上某个位置查找到1也可能是由于某个另
外的数据映射得到的。
支持删除操作吗
目前布隆过滤器只支持插入和查找操作,不支持删除操作,如果要支持删除,就要另外使用一个计数变
量,每次将相应的位置为1则计数加一,删除则减一。
布隆过滤器中哈希函数的个数需要选择。如果太多则很快所有位都置为1,如果太少会容易误报。