Loading

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数之和

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'));
posted @ 2020-11-06 20:50  ZXYFrank  阅读(82)  评论(0编辑  收藏  举报