LeetCode 1-20
万事开头难
坚持比我们想象的要重要的多!
4. 寻找两个正序数组的中位数
简单确立思路完全无法代替编码,这才是计算机工程师的重要工作
🔑findkth
——折半剔除——单侧二分
我的想法是通过改变 base
/ offset
来剔除两数组左侧的元素
在自己做的时候纠结过if的分支问题
cut = k/2;
if(nums1[base1 + cut - 1] > nums2[base2 + cut]){
base1 += cut;
k -= cut;
};
// 此处 base1 可能变化,所以cut也应该变化
// 实际上等价于continue
if(nums2[base2 + cut - 1] > nums1[base1 + cut]){
base2 += cut;
k -= cut;
};
实际上不可能出现左侧同时大于右侧的情况
x1,x2
y1,y2
有x1<=x2 && y1 <=y2
x1 >= y2 就有 x2 >= x1 >= y2 >= y1
int
是不会越界的: 循环外 bound
每次折半剔除,如果 k>= 1
最终一定会得到 k = 1
,此时折半剔除无法及进行下去
❗我们要明确算法的目的,不能为了实现算法而写
算法的最终目的是解决问题,最实用的算法并不一定是最优雅的算法
剔除的目的,是缩小区间,而事实上,
k =1
就是足够退化的情况
❗当k 到达 0,1,2等值时,引入特判反而可能减少冗余逻辑
class Solution {
int findkth(int[] nums1, int[] nums2,int k){
int len1 = nums1.length;
int len2 = nums2.length;
// base 代表某个数组左侧已经被排除的元素个数
int base1 = 0,base2 = 0;
// k 的含义是在两个数组当中分别大于 base 的元素当中需要找到第k 小元素
while(true){
// 以下两种情况代表在之前某一个时候base 已经被bound 住了
// 或者 base = 0 && len = 0 的退化情况
if(base1 == len1){
return nums2[base2 + k - 1];
}
if(base2 == len2){
return nums1[base1 + k - 1];
}
if(k == 1){
// 基本的退化情况,base 一定不会越界
return Math.min(nums1[base1],nums2[base2]);
}
// 需要剔除的情况
int half = k/2;
// 循环外 bound,其实边界判断就这么简单
int next1 = Math.min(len1 - 1,base1 + half - 1);
int next2 = Math.min(len2 - 1,base2 + half - 1);
// len 提供的bound 保证不越界
if(nums1[next1] <= nums2[next2]){
// 注意先计算k,再更新base
k -= next1 - base1 + 1;
base1 = next1 + 1;
// 注意,因为half 可能被bound,所以k 的剔除值需要另行计算
}else{
k -= next2 - base2 + 1;
base2 = next2 + 1;
}
}
}
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int len1 = nums1.length;
int len2 = nums2.length;
// 中位数的处理技巧s
return (findkth(nums1,nums2,(len1 + len2 + 1)/2) + findkth(nums1,nums2,(len1 + len2 + 2)/2)) / 2.0;
}
}
💡换个角度思考:数组联动 & 二分法 & 评测函数
由于直接操作较短数组,该方法的复杂度可以降至 O(log(min(m+n)))
二分法的核心在于区间评判策略 & 区间收缩策略
难点在于理解根据分支决定取中间数是上取整还是下取整,以避免死循环。
二分法最经典的应用就是 l = 0;r = len - 1
,在左闭右闭区间上操纵数组,改变候选区间大小
本题的这个解法比较特殊:划分区间
class Solution {
int INF = 0x3f3f3f3f;
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int len1 = nums1.length;
int len2 = nums2.length;
if(len2 < len1){
return findMedianSortedArrays(nums2,nums1);
}
// len2 <= len1
int k = (len1 + len2)/2;
int l = 0,r = len1;
// i 代表左侧已经有的元素个数
int i = (l + r) /2;
// j 代表较长数组当中需要的元素
int j = k - i;
int l1 = -3;
int l2 = -3;
int r1 = -3;
int r2 = -3;
while(l < r){
// 上取整,i 是取不到 0 的
int mid = (l + r + 1)/ 2;
i = mid;
// 下面对于是二分界的评测
j = k - mid;
// 直接 if(nums1[i] > nums2[j - 1]){} 这样写明显可能越界
// 显式地写出哨兵来
l1 = i == 0? -INF:nums1[i-1];
l2 = j == 0? -INF:nums2[j-1];
r1 = i >= len1? INF:nums1[i];
r2 = j >= len2? INF:nums2[j];
// System.out.println("l1 = "+l1+" l2 = "+l2+" r1 = "+r1+" r2 = "+r2);
if(l1 >= r2){
r = mid -1;
}
else if(l2 >= r1){
l = mid;
}else{
l = mid;
break;
}
}
i = l;j = k - l;
// System.out.println("i = "+i+" j = "+j);
l1 = i == 0? -INF:nums1[i-1];
l2 = j == 0? -INF:nums2[j-1];
r1 = i >= len1? INF:nums1[i];
r2 = j >= len2? INF:nums2[j];
int max = Math.max(l1,l2);
int min = Math.min(r1,r2);
if((len1 + len2) % 2 == 1){
return min;
}else{
return (min + max) / 2.0d;
}
}
}
这里两次显式书写了哨兵
其实循环体内的哨兵是多余的
因为这里写到的是 mid = (l+r+1)/2
,也就是上取整(配合 r = mid - 1
),因此,0不会在遍历过程中被取到,只可能带着0返回
二分查找的正确性是由区间长度的严格递减实现的
mid = (l+r)/2 & l = mid+1 & r = mid
mid = (l+r+1)/2 & l = mid & r = mid -1
以上的代码换成这样的逻辑也可以AC
在我的代码当中我写了一个不太常规的break
if(l1 >= r2){
r = mid -1;
}
else if(l2 >= r1){
l = mid;
}else{
// 这里是指正好找到了符合要求的位置
l = mid;
break;
}
问题来了:为什么题解里面一个if之后就可以把其他的所有情况都写进else 呢
情况 | 动作 |
---|---|
l1>r2 && l2 > r1 | 不可能出现! |
l1>r2 && l2 <=r1 | i-=? |
l1<=2 && l2 > r1 | i+=? |
l1<=r2 && l2 <= r1 | 满足条件 |
只需要保证 `
应该是写明白才好
if(l1>r2 && l2<=r1){
r = mid;
}else if(l1<=r2 && l2>r1){
l = mid + 1;
}else if(l1<=r2 && l2<=r1){
l = mid;
break;
}
findkth
的二分查找实现
采用了findkth
的思想,并且采用了在较短数组当中直接二分的方法,可以利用早停将复杂度降至 O(log(min(m+n)))
5. 最长回文子串
不要大意!
aaabbbb
回文串有两种形式
7. 整数反转
如何快速在数据范围之内判断一个整数有没有溢出
我自己想到的是判断是否可逆
class Solution {
public int reverse(int x) {
if(x == Integer.MIN_VALUE) return 0;
// 负数表示的范围更大,不必担心溢出
if(x<0) return -(reverse(-x));
int ans = 0;
int pre = 0;
while(x!=0){
if(ans*10 / 10 != ans){
return 0;
}
pre = ans * 10;
ans = ans*10 + x%10;
if(ans - pre != x%10){
return 0;
}
x /= 10;
}
return ans;
}
}
其实还可以直接判断上溢和下溢
class Solution {
public int reverse(int x) {
int ans = 0;
while(x!=0){
// 先分离
int pop = x%10;
// 再判断
if(ans > Integer.MAX_VALUE/10 || ans == Integer.MAX_VALUE/10 && pop >= 7){return 0;}
if(ans < Integer.MIN_VALUE/10 || ans == Integer.MIN_VALUE/10 && pop <= -8){return 0;}
// 再组合
ans = ans*10 + pop;
x/=10;
}
return ans;
}
}
13. 罗马数字转整数
class Solution:
def romanToInt(self, s: str) -> int:
d = {'I':1, 'IV':3, 'V':5, 'IX':8, 'X':10, 'XL':30, 'L':50, 'XC':80, 'C':100, 'CD':300, 'D':500, 'CM':800, 'M':1000}
return sum(d.get(s[max(i-1, 0):i+1], d[n]) for i, n in enumerate(s))
举个例子,遍历经过 IV的时候先记录 I 的对应值 11 再往前移动一步记录 IV的值 3,加起来正好是 IV 的真实值 4。max 函数在这里是为了防止遍历第一个字符的时候出现 [-1:0]的情况
可能读取一个或者两个字符,如果发现可以采用增量策略,可以采用读取 s[cur-1,cur]的方式
15. 三数之和
18. 四数之和
k数之和
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
return nSum(nums,3,0);
}
public List<List<Integer>> nSum(int[] nums,int n, int tar){
if(nums.length == 0 || n<2){
return new ArrayList<List<Integer>>();
}
// n>2时,nlogn的排序复杂度就不再占主导,可以通过双指针的方法降低复杂度
Arrays.sort(nums);
// 调用底层函数
return _nSum(nums,n,0,nums.length-1,tar);
}
public List<List<Integer>> _nSum(int[] nums,int n, int start, int end, int tar){
System.out.println("start: "+start+" end = "+end);
List<List<Integer>> ans = new ArrayList<List<Integer>>();
if(n == 2){
// 需要提取两个数
// 已排序,复杂度可以降至n,但是和哈希不同,不需要O(n)的额外空间
int r = end;
for(int l = start;l<end && l<r;l++){
// 考察每一个元素
if(nums[l] + nums[r]<tar){
// 左侧数字过小
continue;
}
// 移动右侧指针
while(nums[l] + nums[r] > tar && l < r - 1)r--;
if(nums[l] + nums[r] == tar){
List<Integer> list = new ArrayList<Integer>();
list.add(nums[l]);
list.add(nums[r]);
ans.add(list);
}
// 跳过重复元素
while(l<=end - 1 && nums[l] == nums[l+1])l++;
}
}else{
// n>2,递归调用
for(int i = start;i<=end-n+1;i++){
// 以每一个元素为最左侧元素,提取剩下的n-1个元素
List<List<Integer>> ret = _nSum(nums,n-1,i+1,end,tar-nums[i]);
for(List<Integer> r:ret){
r.add(nums[i]);
ans.add(r);
}
// 跳过重复元素
while(i+1<=end && nums[i] == nums[i+1])i++;
}
}
return ans;
}
}
“不重复”:排序 && 跳过相同元素
注意判断是否越界
// 跳过重复元素
while(l<=end - 1 && nums[l] == nums[l+1])l++;
// 跳过重复元素
while(i+1<=end && nums[i] == nums[i+1])i++;
9. 回文数
判断回文数的好方法
int tail = 0;
while(x > tail){
tail = tail * 10 + x % 10;
x /= 10;
}
return x == tail || x == tail / 10;
但是上面这段代码是有错误的
输入
10
,输出为false
10,100 这样的用例之所以会出错,是因为最后 tail = 0-9,x = 0 导致
而代码逻辑会误判为奇数个数位对应的情况,实际上,这种情况下因为没有前导零,所以这样的回文比较是没有意义的
if(x<0 || (x % 10 ==0 && x!=0))return false;Q
应该添加上这样的特判
12. 整数转罗马数字
这是贪心算法的典型例题,和找钱其实是一个道理
class Solution {
public String intToRoman(int num) {
int[] steps= {1000,900,500,400,100,90,50,40,10,9,5,4,1};
String[] s = {"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"};
StringBuilder ans = new StringBuilder();
for(int i = 0;i<steps.length;i++){
while(num >= steps[i]){
ans.append(s[i]);
num -= steps[i];
}
}
return ans.toString();
}
}
16. 最接近的三数之和
class Solution {
int INF = 0x3f3f3f3f;
int ans,_sum,sum;
public int threeSumClosest(int[] nums, int target) {
ans = INF;
_sum = 0;
findCloeset(nums,target,3);
return sum;
}
public void findCloeset(int[] nums, int target,int n){
Arrays.sort(nums);
_findClosest(nums,n,target,0,nums.length-1);
}
public void _findClosest(int[] nums, int n, int target, int start, int end){
int len = end - start + 1;
// System.out.println("start = " + start + " end = " + end);
if(n < 2){
// 无效解
return;
}
if(n > 2){
// 遍历每一个元素,作为区间起始点
for(int i = start; i <= end - n + 1;){
_sum = _sum + nums[i];
_findClosest(nums,n-1,target - nums[i],i+1,end);
_sum = _sum - nums[i];
// 跳过重复元素
while(i + 1 <= end && nums[i+1] == nums[i]) i++;
i++;
}
}else if(n == 2){
// 双指针
int i = start,j = end;
// System.out.println("\ti = " + i + " j = " + j + " sum = "+ _sum);
while(i<j){
int c = nums[i]+nums[j];
// System.out.println("\t\ttar = "+target+" c = "+c);
// 此时因为target是期待的标准数据,任何的递归最终都会到达这个递归基
// 从而,这里的绝对值之差就是最终答案之差
if(Math.abs(c - target) < Math.abs(ans)){
ans = c - target;
sum = _sum + c;
}
if(c == target){
return;
}else if(c < target){
// 现有结果比较小,更好的结果按照必然出现在右侧[i+1,j]
while(i + 1 <= end && nums[i+1] == nums[i] && i<j-1){i++;}
i++;
}else{
// 结果必然出现在左侧区间[i,j-1]
while(j - 1 >= start && nums[j-1] == nums[j] && j - 1 > i){j--;}
j--;
}
}
}
}
}
347. 前 K 个高频元素 & 215. 数组中的第K个最大元素
⭐kth-max / kth-min / kth-frequent : 快速排序 均摊 O(N)
分治法(Divide&Conquer),把一个大的问题,转化为若干个子问题(Divide),每个子问题“都”解决,大的问题便随之解决(Conquer)。这里的关键词是“都”。从伪代码里可以看到,快速排序递归时,先通过partition把数组分隔为两个部分,两个部分“都”要再次递归。
分治法有一个特例,叫减治法
减治法(Reduce&Conquer),把一个大的问题,转化为若干个子问题(Reduce),这些子问题中“只”解决一个,大的问题便随之解决(Conquer)。这里的关键词是“只”。
class Solution{
int[] ret;
int reti;
public int[] topKFrequent(int[] nums, int k) {
Map<Integer,Integer> freq = new HashMap<Integer,Integer>();
for(int i:nums){
freq.put(i,freq.getOrDefault(i,0)+1);
}
List<int[]> counts= new ArrayList<int[]>();
for(Integer key : freq.keySet()){
int count = freq.get(key);
counts.add(new int[]{count,key});
}
ret = new int[k];
reti = 0;
qsort(counts,0,freq.size() - 1,k);
return ret;
}
void qsort(List<int[]> counts,int start,int end,int k){
if(k<=0)return;
int pick = (int) (Math.random() * (end - start)) + start;
System.out.println("start = "+start+" pick = "+pick+" end = "+end);
Collections.swap(counts,pick,start);
int pivot = counts.get(start)[0];
int par = start;
for(int r = start + 1;r<=end;r++){
if(counts.get(r)[0] >= pivot){
// 保证左侧的数都是大于等于pivot的
Collections.swap(counts,r,par + 1);
par ++;
}
}
Collections.swap(counts,par,start);
if(par-start+1>k){
// 数量足够了,缩小范围
qsort(counts,start,par,k);
}else{
// 这里的所有元素都可以被纳入答案
for(int i = start;i<=par;i++){
ret[reti++] = counts.get(i)[1];
}
int left = k - (par - start + 1);
qsort(counts,par + 1,end,left);
}
}
}
⭐ 计数排序 / 桶排序 / 基数排序
数据范围有限。可以优先考虑桶排序
0 <= frequency <= nums.length
20. 有效的括号
一定注意判空
if(stack.empty() || stack.peek()!=tar.getOrDefalut(c,'\0'));
本文来自博客园,作者:ZXYFrank,转载请注明原文链接:https://www.cnblogs.com/zxyfrank/p/13938878.html