【Leetcode热题】打卡 day11——20(更新至17)

目录

1、合并两个有序链表 - 链表 + 暴力 / 递归

(1)暴力

(2)递归

2、括号生成 - dfs + 剪枝

3、合并K个升序链表 - 暴力合并两个链表升级版 / 最小堆(优先队列)

(1)暴力 - 合并两链表升级版

(2)最小堆(优先队列)

4、下一个排列 - 思维+排序

5、在排序数组中查找元素的第一个和最后一个位置 - 二分

6、组合总和 - dfs + 剪枝优化

7、接雨水 - 单调栈


1、合并两个有序链表 - 链表 + 暴力 / 递归

21. 合并两个有序链表

74bf0321244640d3b9a43b223f78d17e.png

(1)暴力

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode dummy=new ListNode();
    ListNode cur=dummy;
    
    while(l1!=null&&l2!=null)
    {
        if(l1.val<=l2.val)
        {
            cur.next=l1;
            l1=l1.next;
        }else{
            cur.next=l2;
            l2=l2.next;
        }
        cur=cur.next;
    }
    cur.next= l1==null? l2:l1;
    return dummy.next;
    }
}

(2)递归

思路:

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

388e08487c6b422fbbe320bc29f85cc1.png

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        
        if(l1==NULL) return l2;
        if(l2==NULL) return l1;
        
        if(l1->val<=l2->val) 
        {
            l1->next=mergeTwoLists(l1->next,l2);
            return l1;
        }
        else {
            l2->next=mergeTwoLists(l1,l2->next);
            return l2;
        } 
    }
};

2、括号生成 - dfs + 剪枝

22. 括号生成

4bce3612b6d045008a4393f28b2c2f65.png

a0d596d29e9944b7816bf808be83d5db.png

先求所有括号组合情况,我们发现可以用dfs列出(画出dfs树)

然后我们发现可以进行剪枝:

  • 当左括号数>n时,像 (()( 、((()这种肯定不行
  • 当dfs过程中右括号数>左括号数时,再深入括号也定然不匹配(先左括号再右括号才能组成完整的)

排除上述两种情况,则剩下的就是匹配的括号情况

class Solution {
public:
    vector<string> res;
    vector<string> generateParenthesis(int n) {
        string str;
        dfs("",0,0,n);
        return res;
    }

    void dfs(string cur,int left,int right,int n)
    {
        if(left > n || left < right) return; // 如果左括号超了,或右括号比左括号多,该分支肯定不行

        if(cur.size() == n*2)
        {
            res.push_back(cur);
            return;
        }
        
        dfs(cur+"(",left+1,right,n);

        dfs(cur+")",left,right+1,n);
    }
};

3、合并K个升序链表 - 暴力合并两个链表升级版 / 最小堆(优先队列)

23. 合并 K 个升序链表

da35fb2bb98540e4947aa1b99d10f833.png

(1)暴力 - 合并两链表升级版

思路:

把K个链表合并转换为每次合并两个链表 


class Solution {
    
    public ListNode mergeKLists(ListNode[] lists) {
    int n=lists.length;
    ListNode res=null;
    for(int i=0;i<n;i++)
       res=vmerge(lists[i],res);
    return res;
    }
    
    public ListNode vmerge(ListNode a,ListNode b)
    {
        ListNode dummy=new ListNode(0);
        ListNode cur=dummy,l1=a,l2=b;
        while(l1!=null&&l2!=null)
        {
            if(l1.val<=l2.val)
            {
                cur.next=l1;
                l1=l1.next;
            }else{
                cur.next=l2;
                l2=l2.next;
            }
            cur=cur.next;
        }
        cur.next= l1!=null? l1:l2;
        return dummy.next;
    }
   
}

(2)最小堆(优先队列)

思路:

最小堆性质是:每次取出的堆顶元素都保证是最小值

我们可以利用这个性质,先把所有链表的头结点存进最小堆中

然后不断取堆顶元素,并存入取出元素的下一个元素

循环取出堆顶直至堆空,也就成功获取一条从小到大的顺序链表

// c++
class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        auto cmp=[](ListNode* a,ListNode* b) {return a->val > b->val;};
        priority_queue<ListNode*,vector<ListNode*>,decltype(cmp)> pq;

        for(ListNode* x:lists)
            if(x) pq.push(x);

        ListNode* dummy=new ListNode();
        ListNode* cur=dummy;

        while(!pq.empty())
        {
            ListNode* t=pq.top();
            pq.pop();
            if(t->next) pq.push(t->next);
            cur->next=t;
            cur=cur->next;
        }
        return dummy->next;
    }
};
// java
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
        for (ListNode head : lists) 
            if (head != null) pq.offer(head);
        
        ListNode dummy = new ListNode();
        ListNode cur = dummy;
        while (!pq.isEmpty()) 
        {
            ListNode node = pq.poll();
            if (node.next != null) pq.offer(node.next);
            
            cur.next = node;
            cur = cur.next;
        }
        return dummy.next;
    }
}

4、下一个排列 - 思维+排序

思路:

题目要求寻找比当前序列大,且最接近当前序列值的新序列,即就是求向左逼近该数的新数

为了保证新序列在比原序列大的情况下尽可能小,所以我们从后向前寻找一个大值与前面进行交换(从后向前是因为尽量改变低位的值以保证值最小)

怎么寻找交换的位置呢?其实应该从后向前找第一个升序对(i,j),因为只有升序交换后,值才可能变大

解决了值变大的问题,再而要控制数尽可能小。也就是在交换位置之后寻找一个比交换位置的数大的最小数,与之交换

从后向前找第一个升序对(i,j),所以说 [j,end] 区间必定是降序的,记录i的下标为k。

在[j,end]区间内从后向前找第一个比nums[k]大的数,交换它与nums[k]的位置(改变低位的值保证整体值最小)

交换后的[j,end]区间必定是降序的,将[j,end]翻转(k位置的值变大了,为了保证值尽可能小,后面必须是升序)

class Solution {
    public void nextPermutation(int[] nums) {
    int n=nums.length;
    int k=-1;
        
    //step1:先从后往前找第一个升序对(i,j),记录i的坐标为k    
    for(int i=n-2;i>=0;i--)
    {
        int j=i+1;
        if(nums[i]<nums[j]) 
        {
            k=i;
            break;
        }
    }
    
    //如果这个序列是降序,则需要翻转
    if(k==-1)
    {
        swap(0,n-1,nums);
        return;
    }
    
    //step2:从后向前找[j,end]中找第一个比nums[k]大的数nums[i],交换两者位置
    for(int i=n-1;i>k;i--)
    {
        if(nums[i]>nums[k])
        {
            int t=nums[i];
            nums[i]=nums[k];
            nums[k]=t;
            break;
        }
    }
    
    for(int i=0;i<n;i++)
      System.out.print(nums[i]+" ");
    
    //step3:此时[j,end]绝对是升序的,翻转成降序
    swap(k+1,n-1,nums);
    
    }
    
    public void swap(int a,int b,int[] nums)
    {
        int n=nums.length;
        for(int i=a,j=b;i<j;i++,j--)
        {
            int t=nums[i];
            nums[i]=nums[j];
            nums[j]=t;
        }
    }
}

5、在排序数组中查找元素的第一个和最后一个位置 - 二分

34. 在排序数组中查找元素的第一个和最后一个位置

二分模板

  • l + r >> 1 —— 先 r = mid 后 l = mid+1 —— 寻找左边界 —— 大于某个数的最小值
  • l+r+1>>1 —— 先 l = mid 后 r = mid-1 —— 寻找右边界 —— 小于某个数的最大值

思路:

由于数组有序,因此先二分出第一个出现的位置st,然后再二分出比target大的数(target+1)的位置ed,返回答案【st,ed】

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int st=find(nums,target);
        int ed=find(nums,target+1);
        if(st==nums.length||nums[st]!=target) return new int[]{-1,-1};
        return new int[] {st,ed-1};
    }

    public int find(int[] nums,int target)
    {
        int l=0,r=nums.length;
        while(l<r)
        {
            int mid=l+r>>1;
            if(nums[mid]>=target) r=mid;
            else l=mid+1;
        }
        return l;
    }
}

6、组合总和 - dfs + 剪枝优化

39. 组合总和

思路:

这题看数据范围知道是跟全排列类似的dfs暴力枚举+剪枝

但是我不太擅长dfs,感觉回溯的思路有点反人类,因此根据题解学习的该题

首先我的思路是,像树形dfs一样枚举所有情况,然后如果之和>target则进行剪枝

我们发现这样会出现重复的子集,比如说在3这个分支下出现过【3,4……】,而4这个分支下会出现【4,3……】出现了重复,因此我们需要去重

因此我们规定只需要升序的序列,因为其他顺序肯定被之前升序的序列枚举过,只是顺序不一样,进行去重剪枝。这里我们先对choice数组排序,再按顺序枚举choice的元素,每次找组合只往它及之后找,我们规定一个当前枚举位置st,每次dfs时循环st后面的数(for(i=st;;i++))

class Solution {
    List<List<Integer>> res=new ArrayList<>();

    public List<List<Integer>> combinationSum(int[] c, int target) {
        Arrays.sort(c);
        List<Integer> cur=new ArrayList<>(); //用于记录每一个方案
        int st=0; //设置遍历起点
        dfs(c,target,cur,st);
        return res;
    }

    public void dfs(int[] c,int target,List<Integer> cur,int st)
    {
        if(target==0) //子集和==target时记录方案
        {
            res.add(new ArrayList<>(cur));
            return;
        }

        //从st开始遍历 避免生成重复子集
        for(int i=st;i<c.length;i++)
        {
            if(target-c[i]<0) break;
            cur.add(c[i]);
            dfs(c,target-c[i],cur,i);
            cur.remove(cur.size()-1); //撤销选择,回到之前的状态
        }

    }
}

7、接雨水 - 单调栈

42. 接雨水

思路:

刚开始我写的时候用的模拟,思路大概是先预处理出最低点和最高点,然后再遍历最低点逐个计算,但是这种遇到样例二这种就算不了,看了题解之后,发现单调栈这个方法更具有普遍性,而我做的模拟只能不断测试数据进行修改

单调栈是思路是保持栈内单调递减,当遇到当前柱子高度cur比栈顶高时,cur可以作为右边界,和前方的柱子进行雨水结算,cur依次与栈内比它小的柱子进行结算

具体做法是,如果遇到cur比栈顶高,则将栈顶出栈作为bottom(注意栈顶元素抛出),然后依次将栈内的元素作为左边界,计算接雨水体积,循环出栈,直至保持栈内单调,然后入栈(比较抽象,配上图会比较好理解)

class Solution {
    public int trap(int[] h) {
        Deque<Integer> stk=new LinkedList<>();
        int sum=0;
        for(int r=0;r<h.length;r++)
        {
            while(!stk.isEmpty()&&h[stk.peek()]<h[r])
            {
                int bottom=stk.pop();
                if(stk.isEmpty()) break; //如果没有左边界 则盛不了水
                int l=stk.peek();
                int H=Math.min(h[r],h[l])-h[bottom];
                sum+=H*(r-l-1);
            }
            stk.push(r);
        } 
        return sum;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值