排序
稳定:值相同的元素相对位置不变,2(1), 2(3), 2(2), 1 排完序之后,1, 2(1), 2(3), 2(2),在 2 里面的相对位置是没有变化的,这样如果有与 2 关联的数据,他们的排序也不会发生变化
选择排序 在未排序的数据中不断寻找最小值,将最小值与已排序末尾的后一位交换不稳定,时间复杂度 O(n^2),空间复杂度 O(1)
冒泡排序 每次从后向前遍历,不断交换逆序对,这样每次就会把未排序里最小(最大)的值移到排序末尾稳定,时间复杂度 O(n^2),空间复杂度 O(1)
插入排序 每次从排好序的末尾后一位开始向前遍历,将这个未排序元素不断与前面交换,直到排到他合适的位置稳定,时间复杂度 O(n^2),空间复杂度 O(1) 数组初始越有序,所需时间越小
希尔排序 建立在插入排序的基础上,将数组按间隔长度分成若干组,对每一组分别执行插入排序,然后缩小间隔长度,重复上述步骤,直到间隔长度为 1,此时就是插入排序不稳定,时间复杂度小于等于 O(n^2),空间复杂度 O(1) 初始化一个大的间隔长度来提升数组的整体有序度
快速排序 先随机选一个元素,把小于它的都放在左边,把大于的都放在右边,再递归地排序左右不稳定,时间复杂度 O(nlogn),最坏 O(n^2),空间复杂度 O(logn)
归并排序 先用递归到边界,把两个元素排序作为一组,把左右两半子数组排好序,然后再二叉树的后序遍历时合并两个有序数组 ,最后一直合并直到只剩一组稳定,时间复杂度 O(nlogn),空间复杂度 O(n)
堆排序 用数组构建最大堆后,通过不断将堆顶最大值交换到数组末尾,并缩小堆的范围,同时维护堆性质,最终使整个数组变为升序不稳定,时间复杂度 O(nlogn),空间复杂度 O(1)
计数排序 先建立一个 count 数组,将值映射为索引,存储出现次数,然后再一次 for 循环将 count 数组转换为前缀和数组,每个值就是元素最后出现的位置(保证稳定性),最后从右向左 遍历原数组,将元素放到 count 记录的末尾位置,对应 count 减一,最后遍历完得到的新数组就是排好序的非比较排序,稳定,时间复杂度 O(n + k),空间复杂度 O(n + k),O(k)是开数组要的时间和空间 如果用哈希表的话,哈希表 key 无序就不能直接建立前缀和数组了,还得用排序 keys
桶排序 将待排序数组中的元素使用映射函数分配到若干个桶中,对每个桶进行排序,最后合并这些桶 就是用上面这些排序算法(插入排序)的时候先划分成组分别排序,再合并有序数组,效率会更高桶排序是“按值域分组”,希尔排序是“按位置分组”
基数排序 是计数排序的扩展,就是按位数逐个排序排,先排个位再排十位、百位,拍到最后就完成排序 要用稳定的排序算法,适合用计数排序
哈希表查找逻辑 哈希表的底层是桶数组 (一个存放键值对的数组)
先用 key 通过哈希函数计算出哈希值
再根据哈希值和桶(bucket)数量确定索引位置
若该位置的 key 与目标一致,则返回对应 value
否则按冲突解决策略(如链地址法或开放寻址)继续查找,直到匹配 key 并取出 value。
元素的占到容量的百分比是负载因子(默认 0.75),超过了阈值则扩容
HashMap 的扩容是扩大二倍,原来的哈希值会新增 1 个 bit,既不用重新计算又能随机分散到新的桶中
滑动窗口 left 是左边界的索引,是在窗口内的最左元素 定长窗口直接算
1 int left = right - k + 1 ;
不定长用 while
1 2 3 4 5 6 7 while (n0 > k) { if (nums[left] == 0 ) n0--; left++; }
二分查找
查找 target 第一次出现的索引
ranges::lower_bound C++20 里引入了这个函数,找下界,下面这就是他的具体实现
默认比较器用的 less,就是 mid 值 < target,改用 greater 后就是 mid > target
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int n = nums.size ();int left = 0 , right = n - 1 ;while ( left <= right ) { int mid = (left + right ) / 2 ; if (nums[mid] < target) { left = mid + 1 ; } else right = mid - 1 ; }return left;
lower_bound(nums.begin(),nums.(end),target),升序,找到第一个>=target 的值的元素,并返回该元素的迭代器
lower_bound( begin , end , val , less<type>() )升序,找到第一个>=target 的元素,与上面不写的升序功能相同,官方的自定义比较函数
lower_bound( begin , end , val , greater<type>() )降序,找到第一个<=target 的元素
返回的值是迭代器,若要获取索引,需要用- begin
ranges::lower_bound(nums2.begin(),nums2.end(), nums1[i], ranges::greater{}) - nums2.begin();//减去开始获取索引
upper_bound()也是升序,但是是找到第一个>target 的元素
4 寻找两个正序数组的中位数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 class Solution {public : double findMedianSortedArrays (vector<int >& nums1, vector<int >& nums2) { if (nums1. size () > nums2. size ()) { swap (nums1, nums2); } int n1 = nums1. size (); int n2 = nums2. size (); nums1. insert (nums1. begin (), INT_MIN); nums2. insert (nums2. begin (), INT_MIN); nums1. push_back (INT_MAX); nums2. push_back (INT_MAX); int left = 0 ; int right = n1; while (left <= right) { int mid = left + (right - left) / 2 ; int i = mid; int j = (n1 + n2 + 1 )/2 - i; if (nums1[i] <= nums2[j + 1 ]) { left = mid + 1 ; } else { right = mid - 1 ; } } int i = right; int j = (n1 + n2 + 1 )/2 - i; int first = max (nums1[i], nums2[j]); int second = min (nums1[i + 1 ], nums2[j + 1 ]); return (n1 + n2)%2 ? first : (first + second)/2.0 ; } };
前缀和 s[i]就是前 i 个元素的和 适合求区间和,无论是否有序
1 2 3 4 5 6 7 vector<int > s; s.resize (nums.size () + 1 );for (int i = 0 ; i < nums.size (); i++) { s[i + 1 ] = s[i] + nums[i]; }
找所有和为 k 的子串
1 2 3 4 5 6 7 for (int i = 0 ; i < pre.size (); i++) { auto it = map.find (pre[i] - k); if (it != nullptr ) ans += it -> second; map[pre[i]]++; }
链表 List 翻转链表
1 2 3 4 5 6 7 8 9 10 ListNode* reverseList (ListNode* node) { ListNode* pre = nullptr , *cur = node; while (cur != nullptr ) { ListNode* nxt = cur -> next; cur -> next = pre; pre = cur; cur = nxt; } return pre; }
快慢指针法,查看是否存在环型链表
1 2 3 4 5 6 7 8 9 10 11 12 bool hasCycle (ListNode *head) { ListNode* fast = head, *slow = head; while (fast && fast -> next) { slow = slow -> next; fast = fast -> next -> next; if (fast == slow) return true ; } return false ; }
快慢指针法,找中间节点
1 2 3 4 5 6 7 8 ListNode* middleNode (ListNode* head) { ListNode* slow = head, *fast = head; while (fast != nullptr && fast -> next != nullptr ) { slow = slow -> next; fast = fast -> next -> next; } return slow; }
虚拟头结点,头插法,合并两个有序链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ListNode* mergeTwoLists (ListNode* list1, ListNode* list2) { ListNode dummy (0 ) ; ListNode* tail = &dummy; while (list1 != nullptr && list2 != nullptr ) { if (list1 -> val <= list2 -> val) { tail -> next = list1; list1 = list1 -> next; } else { tail -> next = list2; list2 = list2 -> next; } tail = tail -> next; } tail -> next = list1 ? list1 : list2; return dummy.next; }
二叉树 深度优先搜索(DFS),用递归或者栈实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void Recursion (TreeNode* root, vector<int > &ans) { if (root == nullptr ) return ; Recursion (root->left, ans); ans.push_back (root->val); Recursion (root->right, ans); } stack<TreeNode*> stk;while (root != nullptr || !stk.empty ()) { while (root != nullptr ) { stk.push (root); root = root->left; } root = stk.top (); ans.push_back (stk.top ()->val); stk.pop (); root = root->right; }
广度优先搜索(BFS),用队列实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 queue<TreeNode*> queue; vector<vector<int >> res; queue.push (root);while (!queue.empty ()) { vector<int > vals; for (int n = queue.size (); n > 0 ; n--) { TreeNode* node = queue.front (); if (node->left) queue.push (node->left); if (node->right) queue.push (node->right); vals.push_back (node->val); queue.pop (); } res.emplace_back (vals); }return res;
递归 把原问题化解成一个个和相似的子问题,然后确定好递归的终止条件
数组转换为平衡二叉树
1 2 3 4 5 6 7 8 9 10 11 TreeNode* dfs (vector<int >& nums, int left, int right) { if (left == right) return nullptr ; int m = (left + right) / 2 ; TreeNode* node = new TreeNode (nums[m]); node->left = dfs (nums, left, m); node->right = dfs (nums, m+1 , right); return node; }
二叉搜索树,左子树严格小于根节点,右子树严格大于根节点 中序遍历就是升序
回溯 用递归实现
全排列问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution {public : vector<vector<int >> permute (vector<int >& nums) { vector<vector<int >> ans; int n = nums.size (); vector<int > vec (n) , select (n) ; auto dfs = [&](this auto && dfs, int i) -> void { if (i == n) {位数到n的时候停止 ans.emplace_back (vec); return ; } for (int j = 0 ; j < n; j++) { if (select[j] == 0 ) { vec[i] = nums[j]; select[j] = 1 ; dfs (i + 1 ); select[j] = 0 ; } } }; dfs (0 ); return ans; } };
问:什么时候需要写恢复现场,什么时候不需要写?
答:下面代码中,如果初始化 path 为空列表,就需要写恢复现场。本题由于所有括号长度都是固定的 2n,我们可以创建一个长为 2n 的 path 列表,在递归时直接写入字符(而不是插入字符),这样做无需写恢复现场。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution {public : vector<string> generateParenthesis (int n) { vector<string> ans; string path; auto dfs = [&](this auto && dfs, int l, int r) { if (r == n) { ans.emplace_back (path); return ; } if (l < n) { path.push_back ('(' ); dfs (l + 1 , r); path.pop_back (); } if (l > r) { path.push_back (')' ); dfs (l, r + 1 ); path.pop_back (); } return ; }; dfs (0 , 0 ); return ans; } };string path (2 * n, 0 ) ; path[l + r] = '(' ;dfs (l + 1 , r);
79.单词搜索
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Solution {public : bool exist (vector<vector<char >>& board, string word) { bool flag = false ; int n = word.size (); vector<vector<int >> mark (board.size (), vector <int >(board[0 ].size (), 0 )); auto dfs = [&](this auto && dfs, int i, int x, int y) -> void { if (x<0 || y<0 || x>=board.size () || y>=board[0 ].size ()) return ; if (board[x][y] != word[i] || mark[x][y] == 1 ) return ; mark[x][y] = 1 ; if (i == n - 1 ) { flag = true ; } dfs (i + 1 , x + 1 , y); dfs (i + 1 , x, y + 1 ); dfs (i + 1 , x - 1 , y); dfs (i + 1 , x, y - 1 ); mark[x][y] = 0 ; }; for (int x = 0 ; x < board.size (); x++) { for (int y = 0 ; y < board[0 ].size (); y++) { dfs (0 , x, y); } } return flag; } };
堆(优先队列) 堆(heap)是一种完全二叉树,可分为两种类型
小堆顶:任意节点的值<=其子节点的值
大堆顶:任意节点的值>=其子节节点的值
许多编程语言提供的是优先队列(priority queue),这是一种抽象的数据结构, 定义为具有优先级排序的队列。
完全二叉树适合用数组实现堆的物理结构
快速查找算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 class Solution { int partition (vector<int >& nums, int left, int right) { int i = left + rand () % (right - left + 1 ); int pivot = nums[i]; swap (nums[i], nums[left]); i = left + 1 ; int j = right; while (true ) { while (i <= j && nums[i] < pivot) { i++; } while (i <= j && nums[j] > pivot) { j--; } if (i >= j) { break ; } swap (nums[i], nums[j]); i++; j--; } swap (nums[left], nums[j]); return j; }public : int findKthLargest (vector<int >& nums, int k) { int n = nums.size (); int left = 0 ; int right = n - 1 ; while (true ) { int i = partition (nums, left, right); if (i == n - k) { return nums[i]; } else if (i < n - k) { left = i + 1 ; } else if (i > n - k) { right = i - 1 ; } } } };
动态规划 动态规划(Dynamic Programming,简称 DP)是一种将复杂问题分解为多个简单子问题求解的方法。dp 数组是动态规划中常用的数据结构,用于存储子问题的解,从而避免重复计算。
力扣第 198 题 打家劫舍 一开始用回溯,从 n 开始,每次传 n-1,返回上一个子问题的答案,直到为 0 的时候碰到边界开始归 只保留归的部分,它就能 1 比 1 翻译成下面这样的 dp 数组来递推
用上一次的结果来推下一个结果,这个用于推导的式子叫状态转移方程
记忆化搜索,就是因为相同的入参会有相同的最终结果,所以用一个数组来记录,计算过的结果就直接返回,避免多余的递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution {public : int rob (vector<int >& nums) { int n = nums.size (); vector<int > dp (n + 1 , 0 ) ; dp[0 ] = 0 ; dp[1 ] = nums[0 ]; for (int i = 2 ; i <= n; i++) { dp[i] = max (dp[i - 1 ], dp[i - 2 ] + nums[i - 1 ]); } return dp.back (); } };
对上面进行空间优化 其实不需要数组,推到下一个问题的时候原来的 dp[i - 2]就不需要了 所以可以只用两个变量加一个临时变量来递推
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution {public : int rob (vector<int >& nums) { int n = nums.size (); int dp0 = 0 ; int dp1 = nums[0 ]; for (int i = 2 ; i <= n; i++) { int dp_new = max (dp1, dp0 + nums[i - 1 ]); dp0 = dp1; dp1 = dp_new; } return dp1; } };
力扣第 279 题:完全平方数 用回溯 加记忆化搜索 来实现动态规划
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 int memo[101 ][10001 ];auto init = [] { memset (memo, -1 , sizeof (memo)); return 0 ; }(); class Solution {public : int dfs (int i, int j) { if (i == 0 ) { return j == 0 ? 0 : INT_MAX; } int & res = memo[i][j]; if (res != -1 ) { return res; } if (i * i > j) { res = dfs (i - 1 , j); } else { res = min (dfs (i - 1 , j), dfs (i, j - i * i) + 1 ); } return res; } int numSquares (int n) { return dfs (sqrt (n), n); } };
力扣第 416 题:分割等和子集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class Solution {public : bool canPartition (vector<int >& nums) { int sum = 0 ; for (int i : nums) { sum += i; } if (sum % 2 == 1 ) { return false ; } int target = sum / 2 ; int n = nums.size (); vector dp (2 , vector<bool >(target + 1 , false )) ; dp[0 ][0 ] = true ; for (int i = 1 ; i <= n; i++) { int x = nums[i - 1 ]; for (int c = 0 ; c <= target; c++) { bool flag1 = dp[(i - 1 )%2 ][c]; bool flag2 = false ; if (c - x >= 0 ) { flag2 = dp[(i - 1 )%2 ][c - x]; } dp[i%2 ][c] = flag1 || flag2; } } return dp[n%2 ][target]; } };
多维动态规划 就是加了维度,然后还是一样回溯加记忆化转成递推 但是递推的时候是一行一行推的,依据上一行来推下一行 空间优化可以给第一维都 % 2,就只用两行存储,下一行覆盖上一行
不同路径 力扣第 62 题
记忆化加递推
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution {public : int uniquePaths (int m, int n) { vector memo (m, vector<int >(n, -1 )) ; auto dfs = [&](this auto && dfs, int i, int j) -> int { if (i == 0 && j == 0 ) { return 1 ; } int & res = memo[i][j]; if (res != -1 ) { return res; } int x = 0 , y = 0 ; if (i != 0 ) { x = dfs (i - 1 , j); } if (j != 0 ) { y = dfs (i, j - 1 ); } return res = x + y; }; return dfs (m - 1 , n - 1 ); } };
转成递推
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution {public : int uniquePaths (int m, int n) { vector dp (m, vector<int >(n, 1 )) ; for (int i = 1 ; i < m; i++) { for (int j = 1 ; j < n; j++) { dp[i][j] = dp[i - 1 ][j] + dp[i][j - 1 ]; } } return dp[m - 1 ][n - 1 ]; } };
递推空间优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution {public : int uniquePaths (int m, int n) { vector dp (2 , vector<int >(n, 1 )) ; for (int i = 1 ; i < m; i++) { for (int j = 1 ; j < n; j++) { dp[i%2 ][j] = dp[(i - 1 )%2 ][j] + dp[i%2 ][j - 1 ]; } } return dp[(m - 1 )%2 ][n - 1 ]; } };
最长公共子序列 力扣第 1143 题
递归一比一转换成递推
第一次赋值的时候,由于没有上一组来参考,所以手动扩大 dp 数组,初始化为 0 作为参考
同时也是为了避免出现负数下标,在赋值的时候就不是 i = i - 1,而是 i + 1 = i,所以 dp 数组的空间也要跟着加一
所以在 DP 表中,我们把当前字符对应的状态存在 dp[i+1][j+1]
对第一次赋值的理解
题是要最长公共子序列,i = 0, j = 0 时,给 dp[1][1]赋值,一开始就从取前 1 个字符的字符串 1 和前 1 个字符串 2 开始,看他们的公共子序列长度,两个字符相同就加一,本来要加上dp[0][0]上一个最长子序列,但是已经初始化 0 行 0 列都是 0,所以没影响。
然后先看内部循环,i = 0, j = 1 时,还是取前 1 个字符的字符串 1 和前 2 个字符的字符串 2,看此时的text1[i] == text2[j],也就是字符串 2 新取到的第二个字符是否串 1 的第一个字符相等,如果相等就还是加一,然后加上上一行的结果,但还是由于初始化 0,所以这里还是 0,dp 的 0 行 0 列都是 0,所以第一轮 j 直到加到 m - 1,都是 0 或 1
看第内部第二次循环吧,i = 1, j = [0, m - 1], 这时候还是看是否相等,给dp[i + 1][j + 1]赋值,相等就两个串同时步进,赋成上一行dp[i][j]的结果加一,如果不相等,就是上一个字符截止的串一和上一个字符截止的串 2 中最长子序列长度dp[i + 1][j], dp[i][j + 1]中的大值,这样就得到了串 1 的前两个字符和串 2 每个长度的最长公共子序列长度,第一个匹配,第二个也匹配就是 2,然后从每个字符开始的也都记录了
然后就这么逐渐填,直到填完 m 行 n 列,然后返回 dp[m][n]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution {public : int longestCommonSubsequence (string text1, string text2) { int n = text1. size (); int m = text2. size (); vector dp (n + 1 , vector<int >(m + 1 , 0 )) ; for (int i = 0 ; i < n; i++) { for (int j = 0 ; j < m; j++) { if (text1[i] == text2[j]) { dp[i + 1 ][j + 1 ] = dp[i][j] + 1 ; } else { dp[i + 1 ][j + 1 ] = max (dp[i + 1 ][j], dp[i][j + 1 ]); } } } return dp[n][m]; } };
从两行优化到一行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution {public : int longestCommonSubsequence (string text1, string text2) { int n = text1. size (); int m = text2. size (); vector<int > dp (m + 1 , 0 ) ; for (int i = 0 ; i < n; i++) { int pre = 0 ; for (int j = 0 ; j < m; j++) { int temp = dp[j + 1 ]; if (text1[i] == text2[j]) { dp[j + 1 ] = pre + 1 ; } else { dp[j + 1 ] = max (dp[j], dp[j + 1 ]); } pre = temp; } } return dp[m]; } };