LeetCode算法题2:双指针


前言

      Leetcode算法系列:https://leetcode-cn.com/study-plan/algorithms/?progress=njjhkd2

      简单总结一下双指针相关的算法题:

一、有序数组的平方

      题目链接:https://leetcode-cn.com/problems/squares-of-a-sorted-array/

      题目描述:给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

      思路1:因为有可能存在负数,所以从数组的两端开始计算,因为数组元素平方的最大值不是在开头,就是在末尾,由此可得下面解法:

class Solution {
    public int[] sortedSquares(int[] nums) {
    	int[] res=new int[nums.length];
    	int start=0,end=nums.length-1;

	    for(int ri=end;ri>=0;ri--){ //从后往前    从两边向中间收缩。
    	    int t1=nums[start]*nums[start];
        	int t2=nums[end]*nums[end];
	        if(t1<t2){
    	        res[ri]=t2;
        	    end--;
	        }
    	    else{
        	    res[ri]=t1;
            	start++;
	        }
    	}
    	return res;

    }
}

      相反的,如果想从最小值的两边开始扩散则不太容易。因为在这样一个非降序排列的数组中不太好直接找到绝对值最小的那个值。那么可以考虑先对原数组求平方,然后再找最小值。由此得到思路2。

      思路2:从最小值开始往两边扩散有一种更好的方法:先对原数组求平方之后,再找到最小值然后扩散即可。可参考算法如下:

    public int[] sortedSquares(int[] nums) {
        int len=nums.length-1,i;
        for(i=0;i<=len;i++)
            nums[i]=nums[i]*nums[i];
        
        int minIndex=0;
        for(i=1;i<=len;i++)
            if(nums[minIndex]>nums[i])
                minIndex=i;
        
        int[] ans=new int[len+1];
        i=0;
        ans[i++]=nums[minIndex];
        int left=minIndex-1,right=minIndex+1;
        while(left>=0&&right<=len){
            if(nums[left]<nums[right]){
                ans[i++]=nums[left];
                left--;
            }
            else{
                ans[i++]=nums[right];
                right++;
            }    
        }
        while(left>=0){
            ans[i++]=nums[left];
            left--;
        }
        while(right<=len){
            ans[i++]=nums[right];
            right++;
        }
        return ans;
    }

      这两种做法的时间复杂度和空间复杂度都为O(n)。

二、轮转数组

      题目链接:https://leetcode-cn.com/problems/rotate-array/

      题目描述:给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

      思路:假设数组长度为 len,k=len 或 =0 时,原数组不会产生变化,所以需要取模来避免不必要的运算。 令 k = k%len 。有一种比较巧妙的解法是进行两次逆序,参考如下:

class Solution {
    public void rotate(int[] nums, int k) {
        int len=nums.length,k1=k%len;
        int tmp;
        int i=0,j=len-1;
        while(i<j){    // 逆序(第一次)
            tmp=nums[i];
            nums[i]=nums[j];
            nums[j]=tmp;
            i++;
            j--;
        }
        for(i=0,j=k1-1;i<j;i++,j--){    //分别逆序(左半部分第二次)
            tmp=nums[i];
            nums[i]=nums[j];
            nums[j]=tmp;
        }
        for(i=k1,j=len-1;i<j;i++,j--){   //分别逆序(右半部分第二次)
            tmp=nums[i];
            nums[i]=nums[j];
            nums[j]=tmp;
        }
    }
}

      时间复杂度为O(n),空间复杂度为O(1)。

      思路2:更直观的,如果我们新建一个数组,这道题立马就变得简单,但是空间复杂度也变为O(n)了。

 	public void rotate(int[] nums, int k) {
    	int len=nums.length,k1=k%len;               // 注意:k 个元素轮转k次为其本身。
        if(k1==0)
            return;
        int index=len-k1;  //index 为起始下标。
        int[] tmp=Arrays.copyOf(nums,len);
        int i,j=0;
        for(i=index;i<len;i++)
            nums[j++]=tmp[i];
        for(i=0;i<index;i++)
            nums[j++]=tmp[i];
	}

      思路3:想象一下这个数组是一个循环数组(参考循环队列),k 表示所有元素向右移动 k 次,这个时候最终元素的位置就需要对数组长度取模了,经过一丢丢的运算,下标为 index 的数的最终位置为 (index+k)%len,len 为数组长度。参考算法如下(空间复杂度仍为O(n)):

	public void rotate(int[] nums, int k) {
		int len=nums.length,k1=k%len;
        int[] tmp=Arrays.copyOf(nums,len);
        for(int i=0;i<len;i++){
            nums[(i+k1)%len]=tmp[i];
	}

      思路4:将 3 的空间复杂度优化降低为O(1),缺点是算法会稍微显得有些复杂,参考如下:

	public void rotate(int[] nums, int k) {
		 //注意:不重复的几轮交替转换一定会搞定,一定不会存在重复交换的情况。
        int len=nums.length,k1=k%len;
        int count=0;
        int indexNew,valueOld,valueNew;
        for(int i=0;count!=len;i++){  // i一定不会增加到len。
            indexNew=(i+k1)%len;
            valueOld=nums[i];
            valueNew=nums[indexNew];
            while(i!=indexNew){
                nums[indexNew]=valueOld;
                valueOld=valueNew;
                indexNew=(indexNew+k1)%len;
                valueNew=nums[indexNew];
                count++;
            }
            nums[i]=valueOld;    //最后收尾。
            count++;
	}

三、移动零

      题目链接:https://leetcode-cn.com/problems/move-zeroes/

      题目描述:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
      请注意 ,必须在不复制数组的情况下原地对数组进行操作。

      思路1:从头开始遍历数组,当遇到一个 0 时,将0之后的所有元素均往前移动一位,最后一个数置为0;下一次再遇到 0,依然将此 0 之后的所有元素往前移动一位,也将最后一个数置为0。 算法的时间复杂度为O(n2)。参考算法:

    public void moveZeroes(int[] nums) {
        int len=nums.length;
        int i,j,countZero=0;   
        for(i=0;i<len-countZero;){
            if(nums[i]==0){
                countZero++;
                for(j=i;j<len-1;j++)
                    nums[j]=nums[j+1];
                nums[j]=0;
            }
            else
                i++;  
        }
   }

      思路2:双指针,首先,一个指针 a 标记数组中出现的第一个零,另一个指针 b 标记在 a 之后的第一个非零数,因为可能有连续的 0 存在,所以 b 并不总等于 a+1。
      然后,重复执行 “交换 a 和 b 所指向的值,再令 a++,此时 a 所指向的元素必为0;增加 b 的值,直到它指向下一个非零数(或者指向数组尾,此时算法结束)”。时间复杂度为O(n)。参考算法:

    public void moveZeroes(int[] nums) {
		int len=nums.length,a=0,b=0;
        while(a<len&&nums[a]!=0)
            a++;
        b=a+1;
        while(b<len&&nums[b]==0)
            b++;
        while(b<len){
            nums[a]=nums[b];
            nums[b]=0;
            a++;
            while(b<len&&nums[b]==0)
                b++;
        } 
   }

      思路2 也可以用下面的算法来实现:

    public void moveZeroes(int[] nums) {
        int indexNow = 0,indexNum = 0,len = nums.length;
        
        while(indexNum<m){
            if(nums[indexNum] != 0) 
                nums[indexNow++] = nums[indexNum];
            ++indexNum;
        }
        
        for(int i = indexNow; i < m; i++)
            nums[i] = 0;
   }

四、两数之和 II - 输入有序数组

      题目链接:https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/

      题目描述:给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length 。

      以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。

      你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。

      你所设计的解决方案必须只使用常量级的额外空间。

      思路1:二分查找:从第一个数开始,在之后的数中查找另一个数,使得二者之和等于target,存在即返回,否则遍历数组直至结束。时间复杂度为O(nlogn)。参考算法:

    public int[] twoSum(int[] numbers, int target) {
        for (int i = 0; i < numbers.length; ++i) {
            int low = i + 1, high = numbers.length - 1;
            while (low <= high) {
                int mid = (high - low) / 2 + low;
                if (numbers[mid] == target - numbers[i]) 
                    return new int[]{i + 1, mid + 1};
                else if (numbers[mid] > target - numbers[i]) 
                    high = mid - 1;
                else 
                    low = mid + 1;
            }
        }
        return new int[]{-1, -1};
   }

      思路2:双指针,由于此数组为非递减,令指针 index1 指向数组第一个元素,令指针 index2 指向数组第二个元素,当这两处元素之和大于 target 时,只能减小 index2;小于 target 时,只能增大 index1;等于 target 时,算法结束。时间复杂度为O(n)。参考算法:

    public int[] twoSum(int[] numbers, int target) {
        int index1=0,index2=numbers.length-1;
        int tmp;
        while(index1<index2){
            tmp=numbers[index1]+numbers[index2];
            if(tmp==target)
                return new int[]{index1+1,index2+1};
            else if(tmp<target)
                index1++;
            else
                index2--;
        }
        return null;
   }

五、反转字符串

      题目链接:https://leetcode-cn.com/problems/reverse-string/

      题目描述:编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。

      不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

      思路:双指针,首尾元素交换即可。时间复杂度为O(n)。参考算法:

    public void reverseString(char[] s) {
        int index1=0,index2=s.length-1;
        char tmp;
        while(index1<index2){
            tmp=s[index1];
            s[index1]=s[index2];
            s[index2]=tmp;
            index1++;
            index2--;
        }
    }

六、反转字符串中的单词 III

      题目链接:https://leetcode-cn.com/problems/reverse-words-in-a-string-iii/

      题目描述:给定一个字符串 s ,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。

      思路:双指针,一个指针每次指向字符串中单词的开头,另一个指针指向单词的末尾,反转即可。时间复杂度为O(n),空间复杂度为O(1)。参考算法:

public String reverseWords(String s) {
    char[] ss=s.toCharArray();
    int index1=0,index2;
    int len=ss.length;
    char tmp;
    for(int i=0;i<=len;i++){
        if(i==len||ss[i]==' '){  //将上面的代码稍微优化一下,减少了原来的代码量。
            index2=i-1;
            while(index1<index2){
                tmp=ss[index2];
                ss[index2]=ss[index1];
                ss[index1]=tmp;
                index2--;
                index1++;
            }
            index1=i+1;    
        }
    }
    return new String(ss); 
}

七、链表的中间结点

      题目链接:https://leetcode-cn.com/problems/middle-of-the-linked-list/

      题目描述:给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

      思路1:简单的做法,遍历两次,第一次得到链表的长度;第二次返回链表的中间结点。参考算法:

    public ListNode middleNode(ListNode head) {
        int len=0;
        ListNode tmp;
        for(tmp=head;tmp!=null;tmp=tmp.next)
            len++;
        int mid=len/2,i=0;

        for(tmp=head;i<mid;i++)
            tmp=tmp.next;
            
        return tmp;
	}

      思路2:双指针,两个指针之间的关系为:一个指针 index2 一次跑两步,另一个指针 index1 一次跑一步。这里需要注意比如结点总数为 2 或者 3 时,返回的中间节点是相同的。参考算法:

    public ListNode middleNode(ListNode head) {
        ListNode index1,index2;
        index1=index2=head;
        while(index2!=null&&index2.next!=null){ //节点总数为3时,index1仍为总数为2的结果,没有发生改变。
            index2=index2.next.next;
            index1=index1.next;
        }
        return index1;
	}

八、删除链表的倒数第 N 个结点

      题目链接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/

      题目描述:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

      思路1:基于上题的经验,直接采用双指针来做,仅需要修改两个指针之间的关系为:保持两个指针之间的距离恒定为 n。这样,当指针 index2 指向链表尾时,index1 为待删除结点的前一个结点,然后进行删除操作。但是还需要考虑删除第一个结点的情况,所以在刚开始时,我创建了一个 dummy 结点,它的 next 指向头节点,这样就方便多了。参考算法:

    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy,index1,index2;
        dummy=new ListNode(-1,head);//dummy 结点
        
        index1=dummy;
        index2=head;
        int count=0;
        while(index2!=null){
            index2=index2.next;
            count++;
            if(count>n)
                index1=index1.next;
        }
        index1.next=index1.next.next;
        return dummy.next;
	}

      注,不管是通过计算链表长度的方式,还是通过建立辅助栈的解法,都没有双指针来的简洁。


总结

      无。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值