[toc]
# 4 寻找两个正序数组的中位数
类似于二分,可以递归解,每次可以排除 K/2 个元素,以 A,B K/2 为初始条件递归, K=(M+N)/2
1. A[K/2]
< B[K/2]
那么总共小于 B [K/2] 的元素个数严格小于 K, 即 A [K/2] 之前的数据全部不可能是中位数
2. A[K/2]
> B[K/2]
同上,B [K/2] 之前的不可能是中位数
3. A[K/2] == B[K/2]
则表示 A/B 之中最小的那个必是中位数 1/2 每次去掉 K/2 个元素后的剩余数组可以当做同样的子问题来处理,即剩余元素中寻找中位数
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 : double findMedianSortedArrays (vector<int >& nums1, vector<int >& nums2) { int tot = nums1.size () + nums2.size (); if (tot%2 == 0 ){ int left = find (nums1,0 ,nums2,0 ,tot/2 ); int right = find (nums1,0 ,nums2,0 ,tot/2 + 1 ); return (left + right) / 2.0 ; }else { return find (nums1,0 ,nums2,0 ,tot/2 + 1 ); } } int find (vector<int >& nums1,int i,vector<int >& nums2,int j,int k) { if (nums1.size () - i > nums2.size () - j) return find (nums2,j,nums1,i,k); if (k==1 ){ if (nums1.size ()==i) return nums2[j]; else return min (nums1[i],nums2[j]); } if (nums1.size ()==i){ return nums2[j+k-1 ]; } int si = min (i+k/2 ,(int )nums1.size ()),sj = j+k-k/2 ; if (nums1[si-1 ] > nums2[sj - 1 ]){ return find (nums1,i,nums2,sj,k-(sj-j)); }else return find (nums1,si,nums2,j,k-(si-i)); } };
# 5. 最长回文串
方法一:枚举中心点,双指针扩展
方法二:枚举边界点,双指针向内缩
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution {public : string longestPalindrome (string s) { string res; for (int i=0 ;i<s.size ();++i){ int l = i-1 ,r=i+1 ; while (l >= 0 && r < s.size () && s[l] == s[r]) l--,r++; if (res.size () < r-l-1 ) res = s.substr (l+1 ,r-l-1 ); l = i,r = i+1 ; while (l >= 0 && r < s.size () && s[l] == s[r]) l--,r++; if (res.size () < r-l-1 ) res = s.substr (l+1 ,r-l-1 ); } return res; } };
# 10. 正则表达式匹配 (闫式 DP 分析法)
状态表示 (经验) 1.1 f (i,j)
集合: s (1i) 与 p (1 j) 的匹配方案 存储的值 (bool): 是否存在一个合法方案
状态计算 (推理) 2.1 情况 p [j]≠’*’ => (s [i] 是否等于 p [j] p [j] = .
) s 前 i-1 个字符和 p 前 j-1 个字符也要匹配
p [j] = *
=> 考虑 *
匹配了多少个字符,直接枚举 *
的个数 (这部分可以用完全背包优化,而且由于子问题已解决,只需要考虑最多两个字符的情况)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution {public : bool isMatch (string s, string p) { int n =s.size (),m = p.size (); s = ' ' + s,p = ' ' + p; vector<vector<bool >> f (n+1 ,vector <bool >(m+1 )); f[0 ][0 ] = true ; for (int i=0 ;i<=n;++i){ for (int j=1 ;j<=m;++j){ if (j+1 <=m && p[j+1 ] == '*' ) continue ; if (i && p[j]!='*' ){ f[i][j] = f[i-1 ][j-1 ] && (s[i]==p[j] p[j]=='.' ); }else if (p[j]=='*' ){ f[i][j] = f[i][j-2 ] i && f[i-1 ][j] && (s[i] == p[j-1 ] p[j-1 ] == '.' ); } } } return f[n][m]; } };
# 22 括号生成
性质:
- 1. 任意前缀中 ‘(‘数量大于等于’)’
- 2. 左右括号总数量相等
只要满足上面两个,就是一个合法序列 扩展:
n 个左括号和右括号可能的合法匹配序列是卡塔兰数
# 23. 合并 K 个升序链表
思路和合并两个升序链表一致,只是循环来做的话,时间复杂度是 O (N*K), 但是可以优化,即使用堆进行优化,首次将 K 个元素放到堆中,每次取第一个元素后位移指针,加入,继续取。这样
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 class Solution {public : struct Cmp { bool operator () (ListNode* a,ListNode* b) { return a->val > b->val; } }; ListNode* mergeKLists (vector<ListNode*>& lists) { priority_queue<ListNode*,vector<ListNode*>, Cmp> heap; auto dummy = new ListNode (-1 ),tail = dummy; for (auto l : lists) if (l) heap.push (l); while (heap.size ()){ auto t = heap.top (); heap.pop (); tail = tail->next = t; if (t->next) heap.push (t->next); } return dummy->next; } };
# 25 K 个一组反转链表
暴力
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution {public : ListNode* reverseKGroup (ListNode* head, int k) { auto dummy = new ListNode (-1 ); dummy->next = head; for (auto p = dummy;;){ auto q = p; for (int i=0 ;i<k && q;++i){ q=q->next; } if (!q) break ; auto a = p->next,b = a->next; for (int i=0 ;i<k-1 ;++i){ auto c = b->next; b->next = a; a = b,b = c; } auto c= p->next; p->next = a,c->next = b; p = c; } return dummy->next; } };
# 28 找出字符串中第一个匹配项的下标 (KMP)
# KMP Next 数组
Next 数组之前一直没学明白,最近看明白了 首先我们需要考虑一点,如果每次匹配一个字符就失败,直到所有字符匹配完全成功 的匹配次数实际上最多 N 次,即遍历一次字符串,这个本身是合理的时间复杂度。但是最差的情况是什么呢?AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB 匹配 AAAAAAAAAAAAAAAAB 会发现,后面一个字符串,每匹配一次,就要完整的遍历一次,直到最后一次匹配才会成功,这个时间复杂度就退化到了 O (N*M)
而 Next 数组实际上是优化这种情况的,将字符串直接移动到合适的位置,减少最多次数的匹配
Next 的含义是:所有 P (1~i) 相等的前缀和后缀中,长度的最大值,单位是长度
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 strStr (string s, string p) { int n= s.size (),m = p.size (); s = ' ' + s,p = ' ' + p; vector<int > next (m+1 ) ; for (int i=2 ,j=0 ;i<=m;++i){ while (j&&p[i]!=p[j+1 ]) j = next[j]; if (p[i]==p[j+1 ]) j++; next[i] = j; } for (int i=1 ,j=0 ;i<=n;++i){ while (j&&s[i]!=p[j+1 ]) j = next[j]; if (s[i]==p[j+1 ]) j++; if (j==m) return i-m; } return -1 ; } };
# 31. 下一个排列
找规律,下一个 permutation 序列
前到后降序的不用管
第一个前到最后一个后升序的需要交换,交换后从第一个前到最后都是降序的
反转子区间 (子问题)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution {public : void nextPermutation (vector<int >& nums) { int k = nums.size () - 1 ; while (k>0 && nums[k-1 ] >=nums[k]) k--; if (k<=0 ){ reverse (nums.begin (),nums.end ()); }else { int t = k; while (t < nums.size () && nums[t]>nums[k-1 ]) t++; swap (nums[t-1 ],nums[k-1 ]); reverse (nums.begin ()+k,nums.end ()); } } };
# 32. 最长有效括号
两个重要的性质来自 22题
:
1 2 3 - 1. 任意前缀中 '('数量大于等于')' - 2. 左右括号总数量相等 只要满足上面两个,就是一个合法序列
可以证明当右括号数量严格小于等于左括号数量时才是合法序列
遍历时使用栈维护序列,右括号 pop, 左括号 push, 如果右括号时,栈内没有元素了,证明此时左括号小于了右括号数量,重新开始计数
找到所有区间内最大的连续串即可
证明
1. 连续序列必然存在在左括号大于右括号的子序列当中
2. 不可能存在跨越两个子序列的情况
如上图,A 点前面左括号严格大于等于右括号,A 点时左括号小于右括号 (已知)
假设存在一个区间:从 B 出发跨越 A 点,且可能存在合法子串 (左括号严格大于等于右括号)
则 B 到 A 点的左括号必然大于等于右括号
由于 A 点到前面是左括号小于右括号,由于 2, 所以 B 点到前面那段的左括号必然小于右括号才能使已知成立
第 3 条与性质 1
相悖,所以假设不成立
即得到结论:
连续序列必然存在在左括号大于右括号的子序列当中
不可能存在跨越两个子序列的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution {public : int longestValidParentheses (string s) { stack<int > stk; int res = 0 ; for (int i = 0 ,start = -1 ;i<s.size ();++i){ if (s[i] == '(' ) stk.push (i); else { if (stk.size ()){ stk.pop (); if (stk.size ()){ res=max (res,i-stk.top ()); }else { res = max (res,i - start); } }else { start = i; } } } return res; } };
# 33. 搜索旋转数组
二分的概念可以扩展到具有相同性质的序列的二分
如,通常的二分查找都是根据序列的升降来判断的,在这道题中,可以针对序列是否大于 nums [0] 这一概念来进行二分,直到找到两个旋转段的分界点
二分找到两个旋转段的分界点
判断 target 可能在哪个段上,对段进行二分
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 class Solution {public : int search (vector<int >& nums, int target) { int l=0 ,r = nums.size ()-1 ; while (l<r){ int mid = (l + r + 1 )>>1 ; if (nums[mid] >= nums[0 ]) l = mid; else r = mid - 1 ; } if (target >=nums[0 ]) l = 0 ; else l = r + 1 ,r = nums.size () - 1 ; while (l<r){ int mid = (l + r) >>1 ; if (nums[mid] >= target ) r = mid; else l = mid + 1 ; } if (nums[r] == target) return r; return -1 ; } };
# 34. 在排序数组中查找元素的第一个和最后一个位置
这道题也可以类似于上一题,找两个二段性
第一个区间 <= target
第二个区间 >= target
根据这两个二段性来进行二分
# 35. 搜索输入的位置
二分二段性条件是被选中位置大于等于 target
需要特判 0 点
另外,根据这道题发现了一个需要注意的问题
1. 破坏 l,r 中点的只能有一个,就是 l=mid+1 或者 r=mid-1 2. 最后结果最好用未破坏中点的那个 l=mid、r=mid
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution {public : int searchInsert (vector<int >& nums, int target) { if (nums[0 ]>=target) return 0 ; int l = 0 ,r = nums.size ()-1 ; while (l<r){ int mid = (l+r+1 ) >> 1 ; if (nums[mid] >= target) r = mid-1 ; else l = mid; } return l+1 ; } };