leetcode刷题记录总结-5.双指针专题

文章目录

一、过滤保序

27.移除元素

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

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

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

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以**「引用」**方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

示例 1:

输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

示例 2:

输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

提示:

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 50
  • 0 <= val <= 100

题解

题解1:暴力解法
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int size = nums.size(); // 记录维护的有效元素大小
        // 1.遍历每个元素,进行处理
        for(int i = 0; i < size; i++) {
            // 2.遇到目标元素,后面所有元素进行移动覆盖
            if(nums[i] == val) {
                for(int j = i + 1; j < size; j++) {
                    nums[j - 1] = nums[j];
                }
                // 3.注意,后面元素移动后,i位置也要向前移动,继续处理i位置新的数
                i--;
                size--;
                
            }
        }
        return size; // 不能返回nums.size(),只是覆盖,元素个数没有改变
        //nums.resize(size); // 可以重构nums数组实际容量
        //return nums.size();
    }
};
题解2:双指针法
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
       // 1.定义左右指针,右指针寻找元素,左指针保存满足条件的元素
       int l = 0, r = 0;
       for(; r < nums.size(); r++) {
           if(nums[r] != val) { // 排除等于val的元素
               nums[l++] = nums[r];
           }
       }
       return l;
    }
};

26. 删除有序数组中的重复项

给你一个 升序排列 的数组 nums ,请你** 原地** 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致

由于在某些语言中不能改变数组的长度,所以必须将结果放在数组nums的第一部分。更规范地说,如果在删除重复项之后有 k 个元素,那么 nums 的前 k 个元素应该保存最终结果。

将最终结果插入 nums 的前 k 个位置后返回 k

不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

判题标准:

系统会用下面的代码来测试你的题解:

int[] nums = [...]; // 输入数组
int[] expectedNums = [...]; // 长度正确的期望答案

int k = removeDuplicates(nums); // 调用

assert k == expectedNums.length;
for (int i = 0; i < k; i++) {
    assert nums[i] == expectedNums[i];
}

如果所有断言都通过,那么您的题解将被 通过

示例 1:

输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。

示例 2:

输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。

提示:

  • 1 <= nums.length <= 3 * 104
  • -104 <= nums[i] <= 104
  • nums 已按 升序 排列

题解

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        // 1.定义左右指针
        int l = 0; // 左指针指向新数组下标
        int r = 1; // 右指针寻找原数组元素,过滤重复元素
        // 2.过滤保序,防止数组越界,先特殊处理下标为0的元素
        nums[l++] = nums[0]; 
        for(; r < nums.size(); r++) {
            if(nums[r] != nums[r - 1]) nums[l++] = nums[r];
        }
        return l;
    }
};

283. 移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

示例 2:

输入: nums = [0]
输出: [0]

提示:

  • 1 <= nums.length <= 104
  • -231 <= nums[i] <= 231 - 1

**进阶:**你能尽量减少完成的操作次数吗?

题解

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
       int l = 0, r = 0;
       for(; r < nums.size(); r++) {
           if(nums[r] != 0) nums[l++] = nums[r];
       } 
       while(l < nums.size()) nums[l++] = 0;
    }
};

844. 比较含退格的字符串-易错

给定 st 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true# 代表退格字符。

**注意:**如果对空文本输入退格字符,文本继续为空。

示例 1:

输入:s = "ab#c", t = "ad#c"
输出:true
解释:s 和 t 都会变成 "ac"

示例 2:

输入:s = "ab##", t = "c#d#"
输出:true
解释:s 和 t 都会变成 ""

示例 3:

输入:s = "a#c", t = "b"
输出:false
解释:s 会变成 "c",但 t 仍然是 "b"

提示:

  • 1 <= s.length, t.length <= 200
  • st 只含有小写字母以及字符 '#'

进阶:

  • 你可以用 O(n) 的时间复杂度和 O(1) 的空间复杂度解决该问题吗?

题解

题解1:重构字符串
class Solution {
public:
    bool backspaceCompare(string s, string t) {
        return build(s) == build(t);
    }
private:
    string build(string& s) {
        string res;
        for(char c : s) {
            if(c != '#') res.push_back(c);
            else if(!res.empty()) res.pop_back();
        }
        return res;
    }
};
题解2:双指针法
  • 其实,只要遇到空格字符,会删除空格前面的字符,与空格后面字符无关,每次逆序遍历即可
  • 难点是:当有多个空格时,如何将对应的前面普通字符删干净再比较
    • 因此,需要维护个变量,记录普通字符后面的空格
    • 只有将后面的空格都抵消完后,才比较前面有效的字符

注意:不能是s[l--] == '#',或skipS-- > 0

  • 因为,if else判断,有可能不走这个分支,但在判断条件时,将变量改了
  • 因此,在判断条件里改变变量的习惯不好,除非确定一定走这个分支才行
class Solution {
public:
    bool backspaceCompare(string s, string t) {
        int l = s.size() -1, r = t.size() -1; // 逆序遍历的左右指针
        int skipS = 0, skipT = 0; // 记录有效字符前面的空格数
        while(l >= 0 || r >= 0) { // 两组字符数量可以不相等,可能一方先走完
            // 1.处理s字符串-有效字符前的退格符对普通字符的消除工作
            while(l >= 0) {
                if(s[l] == '#') { // 不能是s[l--] == '#'
                    l--; // 注意,判断时不能先减1,只有真遇到空格才行
                    skipS++; // 统计有效字符前空格数量
                }else if(skipS > 0) {// 不能是skipS-- > 0
                    skipS--;
                    l--;  // 消除普通字符,有多少空格消除多少
                }else break;// 直到遇到有效字符,即前面没有空格的普通字符,跳循环
            }
            // 2.处理t字符串-有效字符前的退格符对普通字符的消除工作
            while(r >= 0) {
                if(t[r] == '#') { // 注意,判断时不能先减1,只有真遇到空格才行
                    r--;
                    skipT++;
                }else if(skipT > 0) {
                    skipT--;
                    r--;
                }else break;
            }
            // 3.比较两个消除后留下的有效字符是否相等
            if(l >= 0 && r >= 0) { // 都没有走完,才能计较有效字符
                if(s[l] != t[r]) return false;
            } else if(l >= 0 || r >= 0) return false;//有效字符数目不同,定不等
            // 4.即相等,也没有走完,继续下轮循环判断
            l--; r--;
        }
        return true;
    }
};

977. 有序数组的平方

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

示例 1:

输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

示例 2:

输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]

提示:

  • 1 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • nums 已按 非递减顺序 排序

进阶:

  • 请你设计时间复杂度为 O(n) 的算法解决本问题

题解

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        // 大的肯定在两边,因此,从两边向中间处理数据
        int i = 0, j = nums.size() -1;// 原数组两边下标
        int n = nums.size();
        vector<int> res(n);
        int k = n -1;
        while(i <= j) {
            if(nums[i] * nums[i] > nums[j] * nums[j]) {
                res[k--] = nums[i] * nums[i];
                i++;
            } else {
                res[k--] = nums[j] * nums[j];
                j--;
            }              
        }
        return res;
    }
};

二、重构字符串

344. 反转字符串

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

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

示例 1:

输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]

示例 2:

输入:s = ["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]

提示:

  • 1 <= s.length <= 105
  • s[i] 都是 ASCII 码表中的可打印字符

题解

class Solution {
public:
    void reverseString(vector<char>& s) {
        for(int i = 0, j = s.size()-1; i < j; i++, j--) swap(s[i], s[j]);
    }
};

剑指 Offer 05. 替换空格

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

示例 1:

输入:s = "We are happy."
输出:"We%20are%20happy."

限制:

0 <= s 的长度 <= 10000

题解

解法1:开额外空间,进行拼接
class Solution {
public:
    string replaceSpace(string& s) {
        string res;
        for(char c : s) {
            if(c == ' ') res += "%20";
            else res += c; 
        }
        return res;
    }
};
题解2:不开额外空间,双指针法
  • 注意:循环判断的终止条件:
    • while(i < j) { // 不能相等,否则,一直循环下去,跳不出循环
    • 或者:while(i <= j) && i >= 0
class Solution {
public:
    string replaceSpace(string& s) {
        // 1.先记录多少个空格,扩容空间
        int nOld = s.size();
        int cnt = 0; 
        for(char c : s) {
            if(c == ' ') cnt++;
        }
        s.resize(nOld + cnt * 2);
        int nNew = s.size();
        // 2.从后往前添加元素
        int i = nOld -1, j = nNew -1; // 左指针指向旧字符,右指针指向新字符
        while(i < j) { // 不能相等,否则,一直循环下去,跳不出循环
            if(s[i] != ' ') s[j--] = s[i--];
            else {
                s[j--] = '0';s[j--] = '2';s[j--]= '%';
                i--;
            }
        }
        return s;
    }
};

151. 反转字符串中的单词

给你一个字符串 s ,请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

**注意:**输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。

示例 1:

输入:s = "the sky is blue"
输出:"blue is sky the"

示例 2:

输入:s = "  hello world  "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。

示例 3:

输入:s = "a good   example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。

提示:

  • 1 <= s.length <= 104
  • s 包含英文大小写字母、数字和空格 ' '
  • s至少存在一个 单词

**进阶:**如果字符串在你使用的编程语言中是一种可变数据类型,请尝试使用 O(1) 额外空间复杂度的 原地 解法。

题解

方式1:直接用库函数,
  • 将字符串分割得到字符数组,然后进行拼接
  • c++标准库中split函数,
  • C/C++中的Split函数是strtok()其函数原型如下:
    char * strtok (char * str, const char * delimiters);
class Solution {
    public String reverseWords(String s) {
        s = s.trim();
        StringBuilder s2 = new StringBuilder();
        String[] st = s.split(" ");
        for(int i = st.length - 1; i >= 0; i--) {
            if(st[i] != "") { // 去除重复的空格字符,空格字符被切分后变成空字符
                if(i != 0) s2.append(st[i] + " ");
                else s2.append(st[i]);
            }         
        }
        return s2.toString();
    }
}
方式2:不使用库函数
自定义trim函数-使用额外空间
  • 1.先去除首尾多余空格
  • 2.去除多余空格,并将整个字符串反转
  • 3.再将每个单词字符串反转
class Solution {
public:
    string reverseWords(string s) {
       // 1.去除首尾空格
       s = trim(s); // 可以先去除首尾空格字符,调用erase函数,自动维护size大小
       // 2.去除多余空格,反转整个字符串,使用双指针法
       int i = 0; 
       string res; // 重构,会影响原字符串字符
       for(int j = s.size() - 1; j >= 0; j--) {
           if(s[j] != ' ') { // 过滤保序-不能直接加字符,还要加空格
               while(j >= 0 && s[j] != ' ') res += s[j--];
               if(j != -1) res += ' ';
           }
       }  
       // 2.反转每个单词字符串
       int start = 0;
       for(int end = 0; end <= res.size(); end++) {
           if(end == res.size() || res[end] == ' ') {
               reverse(res, start, end);// 左闭右开
               start = end + 1;
           }
       }
       return res;
    }
private:
    string& trim(string &s) {
        if(s.empty()) return s;
        s.erase(0, s.find_first_not_of(" "));
        s.erase(s.find_last_not_of(" ") + 1);
        return s;
    }
    void reverse(string& s, int start, int end) {
        for(int i = start, j = end - 1; i < j; i++, j--) swap(s[i], s[j]);
    }
};
先去除多余空格在反转-不开额外空间
  • 1.先去除空格
  • 2.将整个字符串反转
  • 3.再将每个单词字符串反转
class Solution {
public:
    string reverseWords(string s) {
        // 1.先去除多余空格-使用双指针法,先不翻转,可以在原空间处理
        //s = trim(s); // 可以先去除首尾空格字符,调用erase函数,自动维护size大小
        int i = 0;
        for(int j = 0; j < s.size(); j++) {
             // 如果不先调用trim,就要先添加空格,最后一个空格不易找到
            if(s[j] != ' ') {
                if(i != 0) s[i++] = ' ';
                // 循环进行条件,处理合法字符
                while(j < s.size() && s[j] != ' ') s[i++] = s[j++]; 
           }
       }
       s.resize(i);// 重构s的实际元素大小
       // 2.翻转整个字符串
       reverse(s, 0, s.size()); // 前闭后开
       // 3.翻转每个单词
       int start = 0;
       for(int end = 0; end <= s.size(); end++) {
           if(end == s.size() || s[end] == ' ') { // 终止边界,非法字符触发事件
                reverse(s, start, end);
                start = end + 1; // 左指针指向新的单词开始为止,跳过空格字符
           }
       }
       return s;
    }
private:
    string& trim(string &s) {
        if(s.empty()) return s;
        s.erase(0, s.find_first_not_of(" "));
        s.erase(s.find_last_not_of(" ") + 1);
        return s;
    }
    void reverse(string& s, int start, int end) {
        for(int i = start, j = end - 1; i < j; i++, j--) swap(s[i], s[j]);
    }
};

三、重构链表

206. 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

img

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:

img

输入:head = [1,2]
输出:[2,1]

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中节点的数目范围是 [0, 5000]
  • -5000 <= Node.val <= 5000

**进阶:**链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

题解

双指针法
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        // 初始化三个指针,分别指向前,中,后节点
        ListNode pre = null;
        ListNode cur = head;
        ListNode next;
        while(cur != null) {
            // 1.先记录下个节点
            next = cur.next;
            // 2.反转指向
            cur.next = pre;
            // 3.移动指针,进行下轮反转
            pre = cur;
            cur = next;
        }
        return pre;
    }
   
}
递归法
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
       return recur(nullptr, head);
    }
private:
    ListNode* recur(ListNode* pre, ListNode* cur) {
        if(cur == nullptr) return pre;
        ListNode* next = cur->next;
        cur->next = pre;
        return recur(cur, next);
    }
};

19. 删除链表的倒数第 N 个结点

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

示例 1:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SuedzZdm-1674910076455)(assets/remove_ex1.jpg)]

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

**进阶:**你能尝试使用一趟扫描实现吗?

题解

  • 因为无法知道链表中一共有多少个元素,如果想使用一趟扫描,需要使用双指针
  • 双指针可以利用差值,找到要删除节点的前置节点
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        // 1.定义头节点,和左右指针
        ListNode* dummyHead = new ListNode(0, head);
        ListNode* left = dummyHead; // 左指针指向要删除节点的前一个位置
        ListNode* right = head;     // 右指针探路,要比左提前一步,左是前置节点
        while(n-- && right != NULL) right = right->next;//差值就是n
        // 2.右指针探路,直到遍历到链表末尾,左指针就是要删除的前一个节点
        while(right != nullptr) {
            left = left->next;
            right = right->next;
        }
        // 3.left指向要删除节点前面,进行删除节点操作
        left->next = left->next->next;// 因题中要求n>=1,故left一定不会越界
        return dummyHead->next;
    }
};

面试题 02.07. 链表相交

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null

图示两个链表在节点 c1 开始相交**:**

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rgBFHWj8-1674910076456)(assets/160_statement.png)]

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

示例 1:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TY543qlE-1674910076457)(assets/160_example_1.png)]

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A[4,1,8,4,5],链表 B[5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例 2:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KAItMZgy-1674910076462)(assets/160_example_2.png)]

输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A[0,9,1,2,4],链表 B[3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tSIA8ZM1-1674910076463)(assets/160_example_3.png)]

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A[2,6,4],链表 B[1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null

提示:

  • listA 中节点数目为 m
  • listB 中节点数目为 n
  • 0 <= m, n <= 3 * 104
  • 1 <= Node.val <= 105
  • 0 <= skipA <= m
  • 0 <= skipB <= n
  • 如果 listAlistB 没有交点,intersectVal0
  • 如果 listAlistB 有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]

**进阶:**你能否设计一个时间复杂度 O(n) 、仅用 O(1) 内存的解决方案?

题解

题解1:O(n)空间
  • 可以直接使用集合记录节点,重复的节点就是相交的节点
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        unordered_set<ListNode *> visited;
        // 1.遍历A
        while(headA != NULL) {
            visited.insert(headA);
            headA = headA->next;
        }
        // 2.遍历B,判断有无环
        while(headB != NULL) {
            if(visited.find(headB) != visited.end()) return headB;
            headB = headB->next;
        }
        return NULL;
    }
};
题解2:双指针法-O(1)空间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wGfBaHHE-1674910076464)(assets/image-20221120214525644.png)]

证明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VDmug6sq-1674910076464)(assets/image-20221120214712154.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eaJ1E4xE-1674910076465)(assets/image-20221120214733231.png)]

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if (headA == nullptr || headB == nullptr) return NULL;
        // 1.定义两个指针分别指向两个链表的头
        ListNode* pA = headA;
        ListNode* pB = headB;
        // 2.a+c+b = b+c+a
        while(pA != pB) {
            pA = pA == nullptr ? headB : pA->next;
            pB = pB == nullptr ? headA : pB->next;
        }
        return pA;
    }
};

141. 环形链表

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false

示例 1:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eDTHWCQg-1674910076467)(assets/circularlinkedlist.png)]

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R5aDojfK-1674910076467)(assets/circularlinkedlist_test2.png)]

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IszcONgM-1674910076468)(assets/circularlinkedlist_test3.png)]

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

提示:

  • 链表中节点的数目范围是 [0, 104]
  • -10^5 <= Node.val <= 10^5
  • pos-1 或者链表中的一个 有效索引

题解

  • 1.可以用**哈希表记录链表遍历过的节点,**如果有重复,说明有环,空间O(n)

  • 2.也可以原地解决,使用快慢指针,如果有环,快指针一定能与慢指针相遇

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OUJpFwM6-1674910076469)(assets/34c6bd80278a4c05a713f7aa279d4f31.png)]

public class Solution {
    public boolean hasCycle(ListNode head) {
        if(head == null || head.next == null) return false;
        ListNode slow = head;
        ListNode fast = head;
        while(fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow) return true;
        }
        return false;
    }
}

142. 环形链表 II

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

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N9r910gJ-1674910076470)(assets/circularlinkedlist-166895576765318.png)]

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RZDRPLBP-1674910076471)(assets/circularlinkedlist_test2-166895576765720.png)]

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZLfeVxxa-1674910076472)(assets/circularlinkedlist_test3-166895576765722.png)]

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

提示:

  • 链表中节点的数目范围在范围 [0, 10^4]
  • -10^5 <= Node.val <= 10^5
  • pos 的值为 -1 或者链表中的一个有效索引

题解

  • 1.此题若不开额外空间,需要找规律,列数学表达式

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vTxVABAb-1674910076473)(assets/e384a94b6efa48cda6b361c9792a33f8.png)]

public class Solution {
    public ListNode detectCycle(ListNode head) {
        if(head == null || head.next == null) return null;
        ListNode slow = head.next; // 先走一步,不然进不了循环
        ListNode fast = head.next.next;
        // 1. 先找到快慢指针的相遇点
        while(fast != slow && fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        // 2. 判断是否有环
        if(fast != slow) return null;
        // 3. 如果有环,根据公式,计算出环的起点
        while(head != slow) {
            head = head.next;
            slow = slow.next;
        }
        return head;
    }
}

四、几个数之和

1.两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]

示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]

提示:

  • 2 <= nums.length <= 104
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109
  • 只会存在一个有效答案

**进阶:**你可以想出一个时间复杂度小于 O(n2) 的算法吗?

题解

用HashMap
  • 注意,求下个位置时,不能包含当前的位置
class Solution {
    public int[] twoSum(int[] nums, int target) {
        // 1.开哈希记录每个数对应的小标
        Map<Integer, Integer> map = new HashMap<>();
        for(int i = 0; i < nums.length; i++) map.put(nums[i], i);
        // 2.遍历一遍数值,查找答案
        for(int i = 0; i < nums.length; i++) {
            // 注意,排除自身的数值
            int key = target - nums[i];
            if(map.containsKey(key) && map.get(key) != i) {
                return new int[]{i, map.get(target - nums[i])};
            }
        }
        return new int[]{-1, -1};
    }
}
代码优化:
  • 不能先放入反向表,否则会对同一个位置进行判断,因此要先判断再加入map
  • 即从后往前判断,每次从前面已经添加的数据中查找,这样可以避免对同一位置判断
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> map;        
        for(int i = 0; i < nums.size(); i++) {
            // 1.先找之前满足条件的值
            auto it = map.find(target - nums[i]);// 自动推断为std::map<int, int>::iterator类型,是一个指针
            if(it != map.end()) return {it->second, i};
            // 2.再将当前值创建反向表,避免找到当前值
            map[nums[i]] = i;
        }
        return {};      
    }
};

454. 四数相加 II

给你四个整数数组 nums1nums2nums3nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:

  • 0 <= i, j, k, l < n
  • nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

示例 1:

输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0

示例 2:

输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1

提示:

  • n == nums1.length
  • n == nums2.length
  • n == nums3.length
  • n == nums4.length
  • 1 <= n <= 200
  • -2^28 <= nums1[i], nums2[i], nums3[i], nums4[i] <= 2^28

题解

  • 因为用四个数值在四个数组中,因此,可以不用考虑重复的问题
  • 可以将四数之和,两两分组,分组计算,两组处理的逻辑与两数之和类似
class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        map<int, int> map;
        // 1.先计算记录前两个元素和
        for(int i : nums1) {
            for(int j : nums2) map[i + j]++;
        }
        int ans = 0;
        // 2.查询后面两个数和,并统计
        for(int i : nums3) {
            for(int j : nums4) {
                // size_type count( const Key& key ) const,返回1或0,可用于判断
                if(map.count(-i-j)) {// size_type count( const Key& key ) const
                    ans += map[-i-j];
                }
            }
        }
        return ans;
    }
};

15.三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

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

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1][-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0

示例 3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0

提示:

  • 3 <= nums.length <= 3000
  • -105 <= nums[i] <= 105

题解

题解1:排序+滑动窗口-三层模板
  • 其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。

1.先排序,优化三重循环比较的次数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qWvJBx6R-1674910076474)(assets/image-20221210155538055.png)]

2.双指针,一个指向二重循环,一个指向三重循环

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rKI10Tmu-1674910076476)(assets/image-20221210155828898.png)]

  • 使用双指针,都只向一侧移动,复杂度降了一个等级

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lq6DpevU-1674910076477)(assets/image-20221210160204194.png)]

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);
        int n = nums.length;
        // 1. 枚举a
        for(int ia = 0; ia < n; ia ++) {
            // 1.1 要保证枚举的数不重复
            if(ia > 0 && nums[ia] == nums[ia - 1]) continue;
            // 2. 创建b,c组成的滑动窗口的指针
            int ic = n - 1;  // 第三元数据,右指针
            int target = -nums[ia];    // 滑动窗口的内容
            for(int ib = ia + 1; ib < n; ib ++) { // 第二元数,左指针
                // 同样,先保证枚举元素不重复
                if(ib > ia + 1 && nums[ib] == nums[ib - 1]) continue;
                // 维护滑动窗口, 左指针已经定了,移动右指针,注意边界
                while(ib < ic && nums[ib] + nums[ic] > target) --ic;
                if(ib == ic) break; // 后续b增加,更不会有等于target的值
                // 如果找到满足条件的值,记录答案
                if(nums[ib] + nums[ic] == target) {
                    List<Integer> list = new ArrayList<>();
                    list.add(nums[ia]);list.add(nums[ib]);list.add(nums[ic]);
                    res.add(list);
                }
            }
        }
        return res;
    }
}
题解2:排序+滑动窗口-利用两数之和模板
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> ans;
        // 1.排序,为了去重
        sort(nums.begin(), nums.end());
        // 2.遍历第一层元素,后面两重可以用两数和模板求出
        for(int i = 0; i < nums.size(); i++) {
            // 3. 记住;每层循环都要进行去重截断处理
            if(i > 0 && nums[i] == nums[i - 1]) continue;
            // 4.将target-nums[i]的值作为两数之和模板参数,取出满足条件元素
            for(vector<int> opt : twoSum(nums, i + 1, -nums[i])) {
                ans.push_back({nums[i], opt[0], opt[1]});
            }
        }
        return ans;
    }
private:
    // 两数之和模板-双指针解法-将两重循环优化成一层循环
    vector<vector<int>> twoSum(vector<int>& nums, int start, int target) {
        vector<vector<int>> ans;
        // 1.定义左右指针
        int i = start;          // 左指针指向左边界(递增),向右移,和增大
        int j = nums.size() -1; // 右指针指向右边界(递减),向左移,和减少
        for(; i < nums.size(); i++) {
            // 2.先去重-截断
            if(i > start && nums[i] == nums[i - 1]) continue;
            // 3.移动右指针,一直保存在左指针右边,直到<=target为止
            while(i < j && nums[i] + nums[j] > target) j--;
            // 4.截断-当停止条件是左右指针相遇时,左不能再往右走,一定没有合适元素了
            if(i == j) break;
            // 5.当停止条件是等于target时,记录满足条件的下标
            if(nums[i] + nums[j] == target) ans.push_back({nums[i], nums[j]});
        }
        return ans;
    }
};

18. 四数之和-一个数组元素

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • abcd 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

你可以按 任意顺序 返回答案 。

示例 1:

输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

示例 2:

输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

提示:

  • 1 <= nums.length <= 200
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109

题解

同样,使用排序加双指针
  • 最后两个数依旧使用双指针,可以减少复杂度
  • 只是前面两个数需要两重循环

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xnYue3RF-1674910076477)(assets/image-20221218145450936.png)]

注意:int数据相加,会有可能越界,因此,要转为long类型

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> res = new ArrayList<>();
        if(nums == null || nums.length < 4) return res;
        int n = nums.length;
        Arrays.sort(nums);
        // 处理前两重循环
        for(int i = 0; i < n; i ++) {
            // 注意,每层循环,都要去重操作
            if(i > 0 && nums[i] == nums[i - 1]) continue;
            for(int j = i + 1; j < n; j ++) {
                if(j > i + 1 && nums[j] == nums[j - 1]) continue;
                // 寻找剩下的两个数,滑动窗口封装的结果
                long two = (long)nums[i] + nums[j];
                for(ArrayList<Integer> opt : 
                    twoSum(nums, j + 1, target-two)) {
                    ArrayList<Integer> arr = new ArrayList<>();
                    arr.add(nums[i]); arr.add(nums[j]);
                    arr.add(opt.get(0)); arr.add(opt.get(1));
                    res.add(arr);
                }
            }
        }
        return res;
    }

    // 1.最后两重循环用双指针,封装好
    private ArrayList<ArrayList<Integer>> twoSum(int[] nums, int start, long target) {
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        // 1.确定右指针
        int right = nums.length - 1; 
        // 2.循环遍历左指针
        for(int i = start; i < nums.length; i ++) {
            // 3. 先去重
            if(i > start && nums[i] == nums[i - 1]) continue;
            // 4. 维护滑动窗口内容,先移动右指针
            while(i < right && (long)nums[i] + nums[right] > target) --right;
            // 5. 截断:后续左边界右移,内容变大,一定找不到<=target的数
            if(i == right) break; // 截断,直接结束循环判断
            // 6. 找到满足条件,记录下来
            if((long)nums[i] + nums[right] == target) {
                ArrayList<Integer> arr = new ArrayList<>();
                arr.add(nums[i]); arr.add(nums[right]);
                res.add(arr);
            }
        }
        return res;
    }
}
还可以进行一些剪枝操作:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TUcvHtuW-1674910076479)(assets/image-20221218150229824.png)]

注意:int默认是有符号整型,可能会越界

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> ans;
        int n = nums.size();
        if(n < 4) return ans;
        // 1.排序
        sort(nums.begin(), nums.end());
        // 2.遍历前两重循环
        for(int i = 0; i < n - 3; i++) {
            // 3. 截断
            if(i > 0 && nums[i] == nums[i - 1]) continue;
            if((long)nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) 
                break;
            if((long)nums[i] + nums[n - 1] + nums[n - 2] + nums[n - 3] < target)
                continue; // 右边界不满足,往左移动会更小,当前i循环一定不满足
            for(int j = i + 1; j < n - 2; j++) {
                if(j > i + 1 && nums[j] == nums[j - 1]) continue;
                if((long)nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) 
                    break;
                if((long)nums[i] + nums[j] + nums[n - 1] + nums[n - 2] < target)
                    continue;
                // 4.后两重用两数之和模板
                long two = (long)target - nums[i] - nums[j];
                for(vector<int>& opt : twoSum(nums, j + 1, two)) {
                    ans.push_back({nums[i], nums[j], opt[0], opt[1]});
                }
            }
        }
        return ans;
    }
private:
    vector<vector<int>> twoSum(vector<int>& nums, int start, long target) {
        vector<vector<int>> ans;
        // 1.定义左右指针
        int i = start, j = nums.size() - 1;
        // 2.遍历左指针,移动右指针
        for(; i < nums.size(); i++) {
            // 3.先去重,截断
            if(i > start && nums[i] == nums[i - 1]) continue;
            // 4.移动右指针
            while(j > i && (long)nums[i] + nums[j] > target) --j;
            // 5.截断
            if(i == j) break;
            // 6.记录
            if((long)nums[i] + nums[j] == target) ans.push_back({nums[i], nums[j]});
        }
        return ans;
    }
};
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值