先说快慢指针再谈滑动窗口
快慢指针
快慢指针顾名思义就是在题目中定义两个指针,根据两个指针移动的速度和条件不同。大体上可以分为两类,一类快慢指针 ,主要用于解决链表问题,如判断链表是否有环。一类是左右指针,主要用于解决数组和字符串问题,比如在二分查找中的应用。
这个题目就可以定义零个指针,一个是慢指针slow指向head,一个是快指针fast指向head或者是head.next。区别就是slow一次走一步,fast一次走两步。如果存在环的话,那么快指针一定可以追得上慢指针,就像操场跑步,跑得快的人一定可以追上跑得慢的人。即使一开始跑步快的人落后于跑步慢的人,并且可以实现相遇时多跑几圈的情况。
代码如下:
public class Solution {
public boolean hasCycle(ListNode head) {
if(head==null || head.next==null) return false;
ListNode fast=head.next;
ListNode slow=head;
while(fast!=slow){ //终止条件就是fast==slow,此时正好追得上。
if(fast.next==null || fast.next.next==null) return false;//如果没有环,快指针会提前结束
slow=slow.next;
fast=fast.next.next;
}
return true;
}
}
这个题目和上一个题目的区别是不止是判断是否有环,而且还要返回环的入口。比如示例1,就要返回2,因为2是环的入口。
这个题目要分为两步,第一步和上题相同,判断是否有环,第二步是返回环的入口。
判断是否有环可以使用和上一题相同的代码。
下面讲解一下判断环的入口
如图所示是一个有环的链表,其中a表示不在环上的链表长度,b表示环的链表部分,整个链表长度为a+b。
在判断环相遇的时候,假设此时快指针和慢指针在环内某一点相遇。因为每次慢指针都走一步,快指针走两步。所以f快指针走的长度是慢指针的两倍,即fast=2slow。而且fast=slow+nb,因为fast一直在环内绕圈最后和慢指针相遇,那么一定是走了慢指针的长度加上环长度的整数倍(至于是几倍看具体看链表)。所以可以得到 slow=nb,fast=2nb。
我们在看如果从head头部开始走,走多少步就可以在环的入口,是a+nb,其实n是任意非负整数。联想刚刚的slow指针已经走了nb,只需要再走a步,就可以到入口处,这个和从头部开始走的指针恰好可以在入口处相遇。
那么我们就可以在判断是否有环之后,将快指针重新指向head,然后和慢指针同时每次移动一步,最后相遇几位入口。
代码如下:
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast=head;
ListNode slow=head;
while (true) {
if (fast == null || fast.next == null) return null;
fast = fast.next.next;
slow = slow.next;
if (fast == slow) break;
}
fast = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
}
这个题目可以使用暴力法来做,把所有的子数组都求出来,满足sum>=s的时候更新最小长度,最后返回。代码如下:
class Solution {
public int minSubArrayLen(int s, int[] nums) {
int len=nums.length;
int min=Integer.MAX_VALUE;
for(int i=0;i<len;i++){
int sum=0;
for(int j=i;j<len;j++){
sum+=nums[j];
if(sum>=s){
min=Math.min(min,j-i+1);
break;
}
}
}
return min!=Integer.MAX_VALUE?min:0;
}
}
上面的时间复杂度为n的平方。我们可以优化一下,使用滑动窗口,定义左右指针。这样值需要遍历一次。思路如下:
1,首先定义左右指针同时指向0;
2, 右指针向右加一,sum加num[right],再判断sum是否小于s,如果小于,右指针继续加一,循环判断sum是否大于s,大于或者等于的话,跳出循环;
3,此时判断最小长度,再向右滑动左指针,sum减去nums[l],循环判断是否大于等于s,如果大于,左指针继续加一,如果小于,跳出循环;
4,若右指针没有到重点,则继续判断。
代码如下
class Solution {
public int minSubArrayLen(int s, int[] nums) {
int len=nums.length;
if(len<1) return 0;
int l=0,r=0;
int min=Integer.MAX_VALUE;
int sum=0;
while(r<len){
while(sum<s && r<len) {
sum += nums[r];
r++;
}
while(l>=0 && sum>=s){
min=Math.min(min,r-l);
sum=sum-nums[l];
l++;
}
}
return min == Integer.MAX_VALUE ? 0 : min;
}
}
我们先明确一点,什么时候可以积水,肯定是出现凹形才会积水。当然这个是废话,那么如何体现呢,如果在下标为i时,如何判断是否能积水呢,最暴力的方法就是分别向两边遍历,遍历到左右两边各自的最大值,然后比较,相对较小的数值减去i坐标上的数值,就是i坐标上可以积攒的水。
public int trap(int[] height) {
int res=0;
int len=height.length;
for(int i=1;i<len-1;i++){
int max_left=0,max_right=0;
for(int j=i;j>=0;j--){
max_left=Math.max(max_left,height[j]);
}
for(int j=i;j<len;j++){
max_right=Math.max(max_right,height[j]);
}
res+=Math.min(max_left,max_right)-height[i];
}
return res;
}
遍历数组,对每一个元素,都进行左右遍历,时间复杂度o(n2)
我们可以进行优化,假设我们知道i的数值最高,那么从0-i这段过程中,假设存在一个j,它的最左边存在一个最大值,那么我们就可以确定j上方可以积攒多少水了,因为右边已经存在了最大值了,那么它上面水的高度一定是左边最大值。
class Solution {
public int trap(int[] height) {
int len=height.length;
if(len<2) return 0;
int index=0;
int max=height[0];
for(int i=1;i<len;i++){ //找到最大值
if(height[i]>max){
index=i;
max=height[i];
}
}
int res=0;
int left=height[0];
for(int i=0;i<index;i++){
if(height[i]>left){
left=height[i];
}else{
res+=left-height[i]; //如果发现此时的值比左边最大值小,那么它上方积攒的水的高度即为left-height[i]
}
}
int right=height[len-1];
for(int i=len-1;i>index;i--){ //同上面左边的判定
if(height[i]>right){
right=height[i];
}else{
res+=right-height[i];
}
}
return res;
}
}
也可以使用双指针,如果left_max<right_max成立,那么它就知道自己能存多少水了。无论右边将来会不会出现更大的right_max,都不影响这个结果。
public int trap(int[] height) {
int left=0;
int right=height.length-1;
int left_max=0,right_max=0;
int res=0;
while(left<=right){
if(left_max<right_max){
res+=Math,max(0,left_max-height[left]);
left_max=Maht.max(left_max,height[left]);
left++;
}else{
res+=Math.max(0,right_max-height[right]);
right_max=Math.max(right_max,height[right]);
right++;
}
}
return res;
}
这个是最典型的双指针用法题目,正常思路是定义快慢指针,快指针走两步,慢指针走一步,快指针停下后,慢指针指向的就是中间节点。但是这题有一个细节,就是数据结构中一般head只有next,不保存数,力扣的head是保存数据的。
class Solution {
public ListNode middleNode(ListNode head) {
if(head==null) return null;
ListNode slow=head;
ListNode fast=head;
while(fast!=null&&fast.next!=null ){
slow=slow.next;
fast=fast.next.next;
}
return slow;
}
}
思路就是定义两个指针,一个指向第一个奇数序列,一个指向第一个偶数序列。分别指向下一个偶数或者奇数序列,之后再让奇数指针指向偶数指针的开头。
代码如下:
class Solution {
public ListNode oddEvenList(ListNode head) {
if(head==null || head.next==null) return head;
ListNode odd=head; //奇数指针
ListNode even=head.next; //偶数指针
ListNode evenhead=even; //保存偶数的头指针,以后用于奇数指针指向
while(even!=null && even.next!=null){
odd.next=odd.next.next; //奇数指针指向下一个奇数序列
even.next=even.next.next; //偶数指针指向下一个偶数序列
odd=odd.next; //奇数指针之下下一个
even=even.next;
}
odd.next=evenhead; //连接
return head;
}
}
核心部分也可以这样写
odd.next=even.next;//偶数的下一个节点就是奇数节点
odd=odd.next;
even.next=odd.next;
even=even.next;
这个题目和上一题,可以定义两个指针,分别指向比val大的元素和小的元素。最后再让小的元素指针下一次指向大元素指针的开头。
这个就需要在定义两个指针存放上面指针的开头。
代码如下:
class Solution {
public ListNode partition(ListNode head, int x) {
ListNode min=new ListNode(-1); //设置两个虚拟节点
ListNode max=new ListNode(-1);
ListNode temp1=min; //用于指向比val小的节点
ListNode temp2=max; //用于指向比val大的节点
ListNode node=head; //用于存放比val大的节点的头
while(node!=null){
if(node.val<x){
min.next=node;
min=min.next;
}else{
max.next=node;
max=max.next;
}
node=node.next;
}
min.next=temp2.next; //让min指向max的头节点
max.next=null; //这一步一定要加,我跳的坑
return temp1.next;
}
}
这里一定要说明一下最后一定要让max.next=null.比如【1,3,5,6,2】这个链表,假设 x=3,则可得到两个链表分别是【1,2】和【3,5,6】,但此时右链表的尾部,即6的尾部仍指向2,如果不把右链表的尾部置为None,最后就会得到 1->2->3->5->6->2 这个链表,在节点2处成环。因此必须将右链表尾部手动置为None,实现断链,最后得到 1->2->3->5->6->None。
下面再来看一下两个有联系的题目
示例
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
这个题目有很多的解法,但是我只会用双指针············。首先定义一个空指针pre,cur指向head,然后循环遍历,让cur.next指向pre,然后同时向后移动一位。
代码如下:
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre=null;
ListNode cur=head;
while(cur!=null)
{
ListNode temp=cur.next;
cur.next=pre;
pre=cur;
cur=temp;
}
return pre;
}
}
示例:
给你这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
有示例可知,这个题目是上一题的加强版。上一题是把所有节点都反转,这个题目是每k个反转一下。首先可以定义一个反转的函数,输入某段链表开头和结尾。就可以将其反转。难点就是要记住开始反转的头节点,让上一个返回的尾节点指向它,一直循环,直至剩下的节点数小于K。返回整个节点的头节点。
代码如下:
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if(head==null) return head;
ListNode dummy=new ListNode(-1); //虚设一个节点
dummy.next=head;
ListNode prev=dummy; //记录每次反转的开头节点
ListNode end=dummy; //记录每次反转的尾部节点
while(end.next!=null){
for(int i=0;i<k && end!=null;i++){ //让尾部节点指向K个之后的节点。
end=end.next;
}
if(end==null) break; //这一步一定要加,让剩下数量小于k的节点不进行下面的反转,直接跳出。
ListNode start=prev.next; //继承prev
ListNode end_next=end.next; //记录此时的尾节点的下一个节点
prev.next=reverse(start,end.next); //进行反转,注意这里的尾节点传的是end.next,因为反转链表最后一个没有反转,为了可以反转,需要输入end.next。prev.next指向返回的首节点
start.next=end_next;//让start也就是刚刚反转后成了尾部的节点,让它指向end_next
prev=start; //重新记录
end=prev;
}
return dummy.next;
}
//反转链表
//!!!!这里输入1->2->3 反转成null<-1<-2 3->
public ListNode reverse(ListNode start,ListNode end){
ListNode pre=null;
ListNode cur=start;
while(cur!=end){
ListNode temp=cur.next;
cur.next=pre;
pre=cur;
cur=temp;
}
return pre;
}
}