【刷题】LeetCode刷题汇总

一、刷题

记录LeetCode力扣刷题,持续更新中…

LeetCode官网:https://leetcode.cn/

题号1:两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。

1. 暴力解法(双循环)

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] list=new int[2];
        for(int i=0; i<nums.length;i++){
            for(int j=i+1; j<nums.length;j++){
                if(nums[i]+nums[j]==target){
                    list[0]=i;
                    list[1]=j;
                }
            }
            
        }
        return list;

    }
}

时间复杂度O(n²)

j的初始值如果为0的话,循环次数会更多且需判断 i!=j

2. HashMap 唯一key

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] list=new int[2];
        Map<Integer, Integer> hashMap = new HashMap<Integer, Integer>();
        for (int i = 0; i < nums.length; ++i) {
            if (hashMap.containsKey(target - nums[i])) {
                list[0]=hashMap.get(target - nums[i]);
                list[1]=i;
            }
            hashMap.put(nums[i], i);
        }
        return list;
    }
}

时间复杂度O(n)

我拿到题目时也想过先确定第一个数,再判断第二数是否在数组中(避免嵌套循环),但是第一时间没找到合适方法,后面看官方解法才想到。官方解法没有提前生成list数组,能提高一点执行用时。

3. 双指针(未通过版)

排序后下标已经乱了,不适合该题但是思路可以

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] list=new int[2];
        int start=0,end=nums.length-1;
        QuickSort(nums,start,end);
        // Arrays.sort(nums);
        while(start<end){
            if(nums[start]+nums[end]>target) end--;
            if(nums[start]+nums[end]<target) start++;
            if(nums[start]+nums[end]==target){
                list[0]=start;
                list[1]=end;
                break;
            }
        }
        return list;

    }

    public void QuickSort(int[] nums,int start,int end){
        if(start<end){
            // 获取分区后的枢纽位置
            int pivotIndex=Partition(nums,start,end);
            // 分别对枢纽左右两边的子数组进行递归排序
            QuickSort(nums, start, pivotIndex - 1);
            QuickSort(nums, pivotIndex + 1, end);
        }
    }
    public int Partition(int[] nums,int start,int end){
        // 单边循环
        int pivot=nums[start]; // 基准元素
        int mask=start; // 标记指针

        for(int i=start+1;i<=end;i++){
            if(nums[i]<nums[mask]){
                mask++;
                // 交换
                int temp=nums[i];
                nums[i]=nums[mask];
                nums[mask]=temp;
            }
        }
        // 交换基准 
        nums[start]=nums[mask];
        nums[mask]=pivot;
        return mask;
        
    }

}

时间复杂度O(nlog₂n)(取决于排序算法O(n)+O(nlog₂n)=O(nlog₂n))

先使用快速排序(或者Arrays.sort())排序好,再用首尾指针去判断
在这里插入图片描述

题号2:两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

1. 同一下标加减

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode head=null,tail=null;

        // 进位
        int carry=0;

        while(l1!=null || l2!=null){
            int a=l1==null?0:l1.val;
            int b=l2==null?0:l2.val;
            int sum=a+b+carry;

            // 插入
            if(head==null){
                head=tail=new ListNode(sum%10);
            }else{
                tail.next=new ListNode(sum%10);
                tail=tail.next;
            }
            carry=sum/10;

            // 往下遍历链表
            if(l1!=null)l1=l1.next;
            if(l2!=null)l2=l2.next;
        }

        // 首位可能还要进位
        if(carry>0) tail.next=new ListNode(carry);

        return head;

    }
}

时间复杂度O(n)

两个链表中同一下标位置的数字可以直接相加(的确没想到这点),最后用尾插法构建链表,注意一下进位即可。

2. 递归

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        return add(l1,l2,0);
    }

    public ListNode add(ListNode l1, ListNode l2, int carry){
        if(l1==null && l2==null && carry==0) return null;
        int sum=carry;
        if(l1!=null){
            sum+=l1.val;
            l1=l1.next;
        }
        if(l2!=null){
            sum+=l2.val;
            l2=l2.next;
        }

        return new ListNode(sum%10,add(l1,l2,sum/10));

    }
}

递归执行循环代码,不用去处理节点的连接。

3. 转数字再求和(未通过版)

求和后,用尾插法重组链表,但超过int、long的取值范围,不支持这种解法

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode m1=l1.next,m2=l2.next;
        int a=l1.val,b=l2.val,speed=10;
        while(m1!=null){
            a+=m1.val*speed;
            m1=m1.next;
            speed*=10;
        }
        speed=10;
        while(m2!=null){
            b+=m2.val*speed;
            m2=m2.next;
            speed*=10;
        }

        int c=a+b;

        ListNode head=new ListNode();
        ListNode tail=head;

        head.val=c%10;
        c/=10;
        // 尾插法
        while(c!=0){
            ListNode r=new ListNode();
            r.val=c%10;
            tail.next=r;
            tail=r;
            c/=10;
        
        }
        
        return head;

    }
}

头插法:一个指针
在这里插入图片描述

尾插法:双指针
在这里插入图片描述

题号3:无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

1. 暴力解法

class Solution {
    public int lengthOfLongestSubstring(String s) {
        if (s == null)
            return 0;

        int start=0, end = 0,max = 0;
        Set<Character> set = new HashSet<>();
        while(end<s.length()){
            while(set.contains(s.charAt(end))){
                set.remove(s.charAt(start));
                start++;
            }
            set.add(s.charAt(end));
            max=Math.max(max,end-start+1);
            
            end++;
        }

        return max;

    }
}

时间复杂度O(n²)

依次从每一个字符开始的,枚举所有子串,依次筛选出最长的子串。例如abcbc,子串有[a、ab、abc、abcb、abcbc]、[b、bc、bcb、bcbc]、[c、cb、cbc],共12个。暴力解法也是基于双指针,两指针不是一起移动,而是分开移动的,start固定,end向右移动,移动完再回退到start位置重新遍历。

2. 滑动窗口

滑动窗口算法的基本思想是维护一个窗口(可能限制窗口大小),通过移动窗口的两个边界来处理问题。它是一种常用的双指针算法,可以优化暴力枚举的时间复杂度,使得算法的执行效率更高。

class Solution {
    public int lengthOfLongestSubstring(String s) {
        if (s.length()<2)
            return s.length();

        int start=0, end = 0,max = 0;
        // Set<Character> set = new HashSet<>();
        // for(;end<s.length();end++){
        //     while(set.contains(s.charAt(end))){
        //         set.remove(s.charAt(start)); //存在就从左边开始删,一直删到不存在为止
        //         start++;
        //     }
        //     set.add(s.charAt(end)); //不存在保存
        //     max=Math.max(max,end-start+1);
        // }

        Map<Character,Integer> map = new HashMap<>();
        while(end<s.length()){
            //存在start不用再慢慢加加,直接一步到位
            if(map.containsKey(s.charAt(end))) start=Math.max(start,map.get(s.charAt(end))+1); //不能回退
            map.put(s.charAt(end),end);
            max=Math.max(max,end-start+1);

            end++;
        }
    

        return max;

    }
}

时间复杂度O(n)

start和end只会往一个方向移动,避免重复的循环。为了更好理解滑动窗口,接下来我将一步步优化算法:

以abcbc为例,它有12个子串。end依次判断a、ab、abc子串,当end移动到第二个b时,end需要回退start的位置,再去判断子串b、bc。
在这里插入图片描述
第一次优化:这时候已经重复判断了,abc子串不重复,也意味着b、bc不重复,能减少判断了几个子串。这时候何不如end不后退去重复判断,只需要把a移除,start前进即可。
在这里插入图片描述
第二次优化:到这里已经算是滑动窗口算法了,但在第一步我们已经判断过一次b重复了,第二步时又判断了一次b重复,这有点冗余了,所以可以让start直接跳到重复字符的下一个字符。
在这里插入图片描述
这里就解释为什么用HashMap不用HashSet,HashSet需要while去一直移动到重复字符的下一个,而HashMap有记录字符串下标,所以可以直接跳过去。

特别注意HashMap中保存的字符可能是以前出现的,不在窗口中,所以start要用max选取最大的。例如"abba",start移动到第二个b,end移动到第二个a时,map里面就存在第一个a。

题号4:寻找两个正序数组的中位数

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。

1. 双指针

不满足题设的时间复杂度

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        if (nums1.length == 0) return findMedian(nums2);
        if (nums2.length == 0) return findMedian(nums1);

        int p1 = 0, p2 = 0, i = 0;

        int[] merge = new int[nums1.length + nums2.length];

        // 两个数组都还有值没比较
        while (p1 < nums1.length && p2 < nums2.length) merge[i++] = nums1[p1] <= nums2[p2] ? nums1[p1++] : nums2[p2++];
        // 只有第一个数组还有值
        while (p1 < nums1.length) merge[i++] = nums1[p1++];
        // 只有第一个数组还有值
        while (p2 < nums2.length) merge[i++] = nums2[p2++];

        return findMedian(merge);

    }

    public double findMedian(int[] nums) {
        int length = nums.length;
        if (length % 2 == 0) {
            return (nums[(length - 1) / 2] + nums[length / 2]) / 2.0;
        } else {
            return nums[length / 2];
        }
    }
}

时间复杂度O(m+n),空间复杂度O(m+n)

利用两个指针依次比较元素,将两个数组合并到一个新数组中,然后根据奇数,还是偶数,返回中位数。
在这里插入图片描述
优化:

这里其实我们可以不用将两个数组真的合并,只需要找到中位数在哪,从而优化空间复杂度。总长度是奇数的话,只要找到一个中间数【length/2】,偶数的话,只要找到两个数【length/2的数和它前一个数(length-1)/2】,即可得到中位数。

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {

        int p1 = 0, p2 = 0, pre_median = 0, median = 0;
        int length = nums1.length + nums2.length;

        for (int i = 0; i <= length / 2; i++) {
            pre_median = median; // 记录排序的前一个结果
            if (p1 < nums1.length && (p2 >= nums2.length || nums1[p1] < nums2[p2])) {
                median = nums1[p1++];
            } else {
                median = nums2[p2++];
            }
        }

        if (length % 2 == 0) {
            return (pre_median + median) / 2.0;
        } else {
            return median;
        }

    }
}

时间复杂度O(m+n),空间复杂度O(1)

2. 二分查找(双指针+递归)

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {

        int length = nums1.length + nums2.length;

        if (length % 2 == 0) {
            return (getKth(nums1,0,nums2,0,length/2) + getKth(nums1,0,nums2,0,length/2+1)) / 2.0;
        } else {
            return getKth(nums1,0,nums2,0,length/2+1);
        }

    }
    //找到第k小的元素(递归)
    //s1是第一个数组的起点下标,s2是第二个数组的起点下标
    private int getKth(int[] nums1,int s1,int[] nums2,int s2,int k){
        //注意跳出条件
        if(s1==nums1.length) return nums2[s2+k-1];
        if(s2==nums2.length) return nums1[s1+k-1];
        if(k==1) return Math.min(nums1[s1],nums2[s2]);

        //移动起始下标
        int new_s1=Math.min(nums1.length,s1+k/2)-1;
        int new_s2=Math.min(nums2.length,s2+k/2)-1;

        //移动距离new_s-s+1
        if (nums1[new_s1] > nums2[new_s2]){
            return getKth(nums1,s1,nums2,new_s2+1,k-(new_s2-s2+1));
        }else{
            return getKth(nums1,new_s1+1,nums2,s2,k-(new_s1-s1+1));
        }

    }
}

时间复杂度 O(log(m+n)),空间复杂度 O(1)

如果对时间复杂度的要求有 log,通常都需要用到二分查找。第一个解法是一个一个地查找,但由于数组是有序的就可以一半一半地排除,减少次数。

二分查找是在有序数组中查找某一特定元素,将数组一分为二,查找元素与中间元素相比较,小于就在左边找,大于就在右边找,重复上述过程,不断缩小范围,每次剔除一半,效率更高。
在这里插入图片描述

原问题可以转换成求数组第k小的数,官方解法说明有点难理解,在我看来,只在中位数的前半部分(总共k个数,length/2)使用二分查找,例如上图的27之前的数,因为与后半部分无关,每次查找剔除一半元素(k/2个数),k逐渐变小直至为1。那这k/2个数怎么去确定呢?这就是比较难理解的点了。
在这里插入图片描述
由于两个数组没有一个数组那么直观,每次剔除元素,每一个数组都剔除一点太混乱,最好就是只在单个数组里面剔除这k/2个数。以上图为例,A和B个数组,总共14个数,需要找到第7小的数和第8小的数,以找第7小的数来说,需要先剔除k/2=3个数,即将两个数组的指针指向下标k/2-1,判断这个数的大小,3<=4,这就说明A数组的前2个数,可能有<=3的数,也可能有>3的数,就算全部<=3,加上B的前三个数,总共也才k/2-1+k/2=k-1(k为偶数时)或者k-2个数,不可能是第k个数,所以可以将B前3个剔除。

这里自己理一遍就会清楚一点。

橙色是剔除元素,k减去3(k不是每次都剔除k/2),指针下标往前(k/2-1),继续重复上一步。
在这里插入图片描述
这里有几个注意点:

当其中一个数组不够k/2时:它的指针只指向最后一个元素,此时剔除个数就不足k/2
当其中一个数组为空时:直接返回另外一个数组的第k元素
当 k=1时:直接返回两个数组首元素的最小值

4. 中位数特性

这里是一个特殊方法,依据的是中位数的特性,它可以将数组平分成相等的两个部分,按照之前的思路,先以一个数组来平分,一共有m+1种切法:

在这里插入图片描述

再以两个数组【数组A,长度m,数组B,长度n】来说,分别在 i 和 j 处【i∈[0,m]、j ∈[0,n] 】切:

在这里插入图片描述

i 的左边加上 j 的左边就相对于一个大数组的左半部分,i 的右边加上 j 的右边就相对于一个大数组的右半部分,这两部分具有一些特性:

总长度偶数时:左半部分元素个数=右半部分元素个数、左半部分的最大值max<=右半部分的最小值min(中位数=(max+min)/2)
总长度奇数时:左半部分元素个数+1=右半部分元素个数、左半部分的最大值max<=右半部分的最小值min(中位数=max)

此时难点就在于 i 、j 怎么划分?

由第一个特性可知:i+j=m−i+n−j(当 m+n 为偶数)或 i+j=m−i+n−j+1(当 m+n 为奇数),可将这两个合并得到 i+j= (m+n+1)/2,因为m+n不管是偶数还是奇数加不加1都不影响 i+j 的结果(int类型),则 j = (m+n+1) / 2 - i,这里m必须小于n 【如果 i = m,j= (n-m+1)/2,j 可能为负数】。流程上就可以先确定 i 再确定 j 。

有第二个特性可知:重点在 i 和 j 左右两边的四个数,为了保证 max ( A [ i - 1 ] , B [ j - 1 ]) <= min ( A [ i ] , B [ j ]),因为 A [ i - 1 ] <= A [ i ],B [ i - 1 ] <= B [ i ] 这是必然的,所以只需要保证 B [ j - 1 ] < = A [ i ] 和 A [ i - 1 ] <= B [ j ]
在这里插入图片描述

还需注意切到边界的情况,这种可以单独去处理也可以规定A[−1]=B[−1]=负无穷,A[m]=B[n]=正无穷(不会对左半部分的最大值产生影响;不会对右半部分的最小值产生影响)而不用单独处理。

这里还可以再简化一下条件,因为 i 递增,j 就会递减,所以在数组A中,一定存在一个最大的 i 满足 A [ i - 1 ] <= B [ j ],而i+1(它的下一个数)就一定满足 A [ i ] > B[ j+1 ],与原条件一致,第二条大小关系就不需要单独去判断了。(就算A里面所有数都大于或小于B里面的数,i=0/m 就可以用无穷去判断,不影响结果)

结论:i 用二分查找去取,而 j 由 i 的值确定,遵从 :

m<n
j = (m+n+1) / 2 - i
存在最大的 i 满足A [ i−1 ] ≤ B [ j ]
边界值为无穷

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        if(nums1.length>nums2.length){
            return findMedianSortedArrays(nums2,nums1); //保证m<n
        }
        int m=nums1.length,n=nums2.length;

        int start=0,end=m; //二分法确定i

        // median1:左半部分的最大值, median2:右半部分的最小值
        int median1 = 0, median2 = 0;

        while(start<=end){
            int i = (start+end)/2;
            int j = (m+n+1) /2-i;

            int nums_i_1= i==0 ? Integer.MIN_VALUE : nums1[i-1];
            int nums_i = i==m? Integer.MAX_VALUE : nums1[i];
            
            int nums_j_1= j==0 ? Integer.MIN_VALUE : nums2[j-1];
            int nums_j = j==n? Integer.MAX_VALUE : nums2[j];

            if(nums_i_1<=nums_j){
                median1=Math.max(nums_i_1,nums_j_1);//奇数时中位数在左边
                median2=Math.min(nums_i,nums_j);
                start =i+1; //i往前走

            }else{
                end= i-1; //i退后
            }
            
        }

        return (m+n)%2==0 ? (median1+median2) / 2.0 : median1;


    }
}

时间复杂度是O( log min(m,n))),空间复杂度 O(1)

二、解法总结

1. 嵌套循环

暴力解法最常用,不考虑时间复杂度。

2. 双指针

首尾指针,快慢指针,一个指针一个方向,减少循环次数,例如滑动窗口、二分查找算法。

3. 递归

例如遍历,线性结构用迭代,非线性结构用递归。递归执行循环代码,注意递归跳出条件。

4. 原地置换

用数组代替哈希表的作用,例题题号:LCR 120 :寻找文件副本

三、常用API(Java)

//引用类型
Integer
Integer.MIN_VALUE/MAX_VALUE //正负无穷 
Character

//长度
list.length
str.length()
arrList.size()
set.size()
map.size()

//集合
int[] list=new int[length]
int[] list={1,2,3}
char[] charList={'a'}
ArrayList<T> arrList= new ArrayList<>()
Set<T> set = new HashSet<>()
Map<T,T> map = new HashMap<>()

//添加元素
arrList.add(element)
arrList.add(index,element)
set.add(element))
map.put(key,value)

//判断元素是否存在或相等
str.equals("")
set.contains(element)
map.containsKey(key)
map.containsValue(value)

//获取元素
list[index]
str.charAt(index)
arrList.get(index)
set.get(index)
map.get(key)
map.values() //所有value的集合

//修改元素
arrList.set(index,element)

//删除元素
arrList.remove(index/element)
arrList.clear()
set.remove(element)
set.clear()
map.remove(key)
map.clear()

//查找元素
arrList.indexOf(element) //第一次出现的下标
str.indexOf("") //第一次出现的下标

//字符串方法
str1.concat(str2) //拼接
str.startsWith/endsWith("") //判断是否以这个字符串开头或结束
str.substring(start, end) //截取,不含end
str.split("") //分割成字符串数组
str.replace("old""new") //替换
String.join("分隔符", str1, str2) //合并
str.toUpperCase/toLowerCase() //大小写
str.trim() //去空白
str.getBytes() //转字节数组

//遍历
for (T element: arr) {} //增强for
//建立一个迭代器,并且将List中的元素放进迭代器中
Iterator it = lists.listIterator();
//迭代器的头指针是空的
while (it.hasNext()){
     //每次next,指针都会指向下一个元素
     T element = it.next();
}

//Math
Math.max/min(a,b)
Math.abs(a)
Math.round(a) //四舍五入为整数

//排序
Arrays.sort()
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Python 是一种流行的高级编程语言,因其简洁易读的语法和广泛的应用领域而受到开发者喜爱。LeetCode 是一个在线编程平台,专门用于算法和技术面试的准备,提供了大量的编程题目,包括数据结构算法、系统设计等,常用于提升程序员的编程能力和解决实际问题的能力。 在 Python 中刷 LeetCode 题目通常涉及以下步骤: 1. **注册账户**:首先在 LeetCode 官网 (https://leetcode.com/) 注册一个账号,这样你可以跟踪你的进度和提交的代码。 2. **选择语言**:登录后,在个人主页设置中选择 Python 作为主要编程语言。 3. **学习和理解题目**:阅读题目描述,确保你理解问题的要求和预期输出。题目通常附有输入示例和预期输出,可以帮助你初始化思考。 4. **编写代码**:使用 Python 编写解决方案,LeetCode 提供了一个在线编辑器,支持实时预览和运行结果。 5. **测试和调试**:使用给出的测试用例来测试你的代码,确保它能够正确地处理各种边界条件和特殊情况。 6. **提交答案**:当代码完成后,点击 "Submit" 提交你的解法。LeetCode 会自动运行所有测试用例并显示结果。 7. **学习他人的代码**:如果遇到困难,可以查看社区中的其他优秀解法,学习他人的思路和技术。 8. **反复练习**:刷题不是一次性的事情,通过反复练习和优化,逐渐提高解题速度和代码质量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

full courage

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值