算法训练营Day9

#JAVA #REVIEW 

开源学习资料

Feeling and experiences:

今日复习之前的知识,双指针的用法

根据代码来复习与回顾:

移除元素:

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

class Solution {
    public int removeElement(int[] nums, int val) {
        // 快慢指针
        int slowIndex = 0;
        for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) {
            if (nums[fastIndex] != val) {
                nums[slowIndex] = nums[fastIndex];
                slowIndex++;
            }
        }
        return slowIndex;
    }
}

此题运用到的是快慢指针,其本质是要完成元素的移动,后面覆盖前面。

模拟:最开始慢指针和快指针都指向第一个元素,如果该元素不等于val,则一起移动。

当出现了指向的元素等于val,这时候慢指针会停下来,而快指针则继续遍历(注意:此时慢指针指向的元素==val)

而快指针在遍历的时候,遇到不等于val的元素就会把这个元素给慢指针。

所以,当我们遍历完,慢指针指向的元素一定不等于val,返回慢指针,就相当于返回了移除元素后的数组长度。(本题很简单,具体可以手动模拟)

反转字符串:

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。

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

你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。

class Solution {
    public void reverseString(char[] s) {
        int l = 0;
        int r = s.length - 1;
        while(l < r){
            char temp = s[l];
            s[l] = s[r];
            s[r] = temp;
            l++;
            r--;
        }
    }
}

本题就更加就简单了,也是昨天的一道题,简单的交换,相向双指针的运用。

替换数字:

给定一个字符串 s,它包含小写字母和数字字符,请编写一个函数,将字符串中的字母字符保持不变,而将每个数字字符替换为number。

例如,对于输入字符串 "a1b2c3",函数应该将其转换为 "anumberbnumbercnumber"。

import java.util.Scanner;

class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        String s = in.nextLine();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            if (Character.isDigit(s.charAt(i))) {
                sb.append("number");
            }else sb.append(s.charAt(i));
        }
        System.out.println(sb);
    }
}

本题有些特殊,可以用到StringBuilder就非常简单,只要把元素依次append到该类的对象中,遇到数字就替换添加成“number”即可。

因为Java不能直接对字符串进行处理,必须用辅助空间

所以在Java中用不到双指针。

在C/C++中,运用双指针,可以在字符串后开辟空间,然后从后向前替换数字字符。

翻转字符串里的单词:

给定一个字符串,逐个翻转字符串中的每个单词。

示例 1:
输入: "the sky is blue"
输出: "blue is sky the"

示例 2:
输入: "  hello world!  "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

示例 3:
输入: "a good   example"
输出: "example good a"
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

class Solution {
    public String reverseWords(String s) {
        // trim方法 删除首尾空格
        s = s.trim();                                    
        int j = s.length() - 1, i = j;
        StringBuilder res = new StringBuilder();
        while (i >= 0) {
            while (i >= 0 && s.charAt(i) != ' ') i--;     
            res.append(s.substring(i + 1, j + 1) + " "); 
            while (i >= 0 && s.charAt(i) == ' ') i--;     
            j = i;                                       
        }
        return res.toString().trim();                    
    }
}

该题在做的时候出现了很多次错误, 最好还是手动模拟。

用到的两个指针,我感觉像是在你追我赶,一个指针记录记录先走的那个指针的出发位置,等那个指针停下了,记录了结果就直接瞬移过去。(反复这样执行)

反转链表:

题意:反转一个单链表。

示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode cur = head;
        ListNode temp = null;
        while (cur != null) {
            temp = cur.next;// 保存下一个节点
            cur.next = prev;
            prev = cur;
            cur = temp;
        }
        return prev;
    }
}

这道题也可以形象理解为你追我赶,但要注意的是,为了防止节点的丢失,要用临时变量来保存

算法训练营Day9_第1张图片

删除链表的倒数第N个节点 :

算法训练营Day9_第2张图片

int getLength(struct ListNode* head) {
    int length = 0;
    while (head) {
        ++length;
        head = head->next;
    }
    return length;
}

struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
    struct ListNode* dummy = malloc(sizeof(struct ListNode));
    dummy->val = 0, dummy->next = head;
    int length = getLength(head);
    struct ListNode* cur = dummy;
    for (int i = 1; i < length - n + 1; ++i) {
        cur = cur->next;
    }
    cur->next = cur->next->next;
    struct ListNode* ans = dummy->next;
    free(dummy);
    return ans;
}

该题最开始想到的不是用双指针,而是遍历两次链表,第一次找链表的长度,第二次再来处理要删除的节点。

而双指针的用法,一个指针从头往后遍历,一个指针指向null。

这样就使得在操作的时候不用遍历两次链表了

环形链表II:

题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *detectCycle(struct ListNode *head) {
    //先设置一个快指针,和一个慢指针
    struct ListNode* fast = head;
    struct ListNode* slow = head;
   

    //找到slow , fast 相遇点
   while(fast != NULL && fast->next != NULL) {
        //设置它们的速度,fast一下走两节点,slow一下走一个节点
    fast = fast->next->next;//代表速度为2
    slow = slow->next;//代表速度为1

    if(fast == slow){
        //找到了相遇点
        //则从这个相遇点设置一个指针速度为1,头节点再设置一个指针速度为1,让它们开始运动
        struct ListNode* index01 = fast;
        struct ListNode* index02 = head;
        //当这两个指针相遇了,则该相遇点为入口
        while(index01 != index02){
        index01 = index01->next;
        index02 = index02->next;
        }
        return index01;
        
    }
    }
    return NULL;

}

这是一道非常有趣的题,巧妙运用了数学知识,解决了跳出循环的问题。

三数之和:

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

注意: 答案中不可以包含重复的三元组。

class Solution {
    public List> threeSum(int[] nums) {
        List> result = new ArrayList<>();
        Arrays.sort(nums);
	// 找出a + b + c = 0
        // a = nums[i], b = nums[left], c = nums[right]
        for (int i = 0; i < nums.length; i++) {
	    // 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
            if (nums[i] > 0) { 
                return result;
            }

            if (i > 0 && nums[i] == nums[i - 1]) {  // 去重a
                continue;
            }

            int left = i + 1;
            int right = nums.length - 1;
            while (right > left) {
                int sum = nums[i] + nums[left] + nums[right];
                //当前三个数的和大于0了,则让右指针左移
                if (sum > 0) {
                    right--;
                    //小了就让左指针右移
                } else if (sum < 0) {
                    left++;
                } else {
                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));
		    // 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
                    while (right > left && nums[right] == nums[right - 1]) right--;
                    while (right > left && nums[left] == nums[left + 1]) left++;
                    
                    right--; 
                    left++;
                }
            }
        }
        return result;
    }
}

需要注意的就是去重问题,还有剪枝。

四数之和:

题意:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

注意:

答案中不可以包含重复的四元组。

class Solution {
    public List> fourSum(int[] nums, int target) {
    //创建一个集合来收集满足条件的答案
    List> list = new ArrayList<>();
    
    int len = nums.length;
    //对数组进行升序排列
    Arrays.sort(nums);

    for(int i = 0;i < len;i++){
        //特殊情况进行判断
        if(nums[i]>0 && nums[i]>target){
            return list;
        }
        //对a进行去重操作
        if(i>0 && nums[i-1] == nums[i]){
            //如果后一个数与前一个数相同,则跳过该数
            continue;
        }
        for(int j = i+1;j < len;j++){
           // 第二层循环也如此做
              if (j>i+1 && nums[j - 1] == nums[j]) {  // 对nums[j]去重
                    continue;
                }
                //设置双指针:左指针left。右指针right
                 int left = j+1; 
                 int right = len-1;
            while(lefttarget){
                //说明大了
                right--;
            }
            else if(sum 

此题的思路和三数之和的思路一样。

注意:这两道题,没有用到map集合来做的原因就是题目要求三元or四元组不能重复,如果用map集合,键值对匹配来做的话,就太过于繁琐了。

总结:

在什么时候会用到双指针的思想?

结合以上练习的题目,可以归纳出:

在数组中:有很多要求原地移除,交换的题目,大多且基本都可以用到双指针,这样可以大大提高效率,减少for循环的嵌套。

在链表中:环形链表这类题中,双指针的运用可以满足条件跳出循环,找到出口,也就是可以用在找环形出口上。

滑动窗口:经典的双指针运用,快慢指针的配合。

组合问题:用来组合匹配寻找目标值。

在代码随想录中,有更详细的总结:双指针总结篇

慢品人间烟火色

闲观万事岁月长 ~

Fighting!

你可能感兴趣的:(算法,leetcode,数据结构)