目录
3、合并K个升序链表 - 暴力合并两个链表升级版 / 最小堆(优先队列)
1、合并两个有序链表 - 链表 + 暴力 / 递归
(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)递归
思路:
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 + 剪枝
先求所有括号组合情况,我们发现可以用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个升序链表 - 暴力合并两个链表升级版 / 最小堆(优先队列)
(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、在排序数组中查找元素的第一个和最后一个位置 - 二分
二分模板:
- 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 + 剪枝优化
思路:
这题看数据范围知道是跟全排列类似的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、接雨水 - 单调栈
思路:
刚开始我写的时候用的模拟,思路大概是先预处理出最低点和最高点,然后再遍历最低点逐个计算,但是这种遇到样例二这种就算不了,看了题解之后,发现单调栈这个方法更具有普遍性,而我做的模拟只能不断测试数据进行修改
单调栈是思路是保持栈内单调递减,当遇到当前柱子高度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;
}
}