代码随想录刷题-双指针总结篇

双指针

本章节为前面章节出现过的题,只不过都是用双指针进行解题,这里还是重做一下,检查下能不能写出来。具体的题解看我之前发的文章

移除元素

本节对应代码随想录中:代码随想录,对应视频链接为:数组中移除元素并不容易! | LeetCode:27. 移除元素_哔哩哔哩_bilibili

习题

给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例 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],也会被视作正确答案。

我的解法

写的代码和官方题解的基本一致。不过解法还可以优化下,之前写的时候没有写优化解法,这里重新写下。

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int slow = 0;
        for (int i = 0; i < nums.size(); i++)
        {
            if(nums[i]!=val){
                nums[slow++] = nums[i];
            }
        }
        return slow;
    }
};

双指针优化

当移除的元素在开头时,我们要将每个元素都左移一位。如[1,2,3,4,5],val=1。实际上只要将右边的5移到左边的1的位置即可。

可以使用两个指针,分别位于数组的左右两端,如果左指针等于 val 则将右指针指向的元素复制到左指针的位置。直到两个指针重合为止。

需要注意的是如果写成 right = nums.size()-1 那 while 的条件是 <=。原因是最后返回的是 left 值,当 left=right 时,如果此时 nums[left] != val 还是要将 left+1。如 nums={3,2,2,3},val=3 时,当 left=right=1,nums[1]=2时,还是要执行一次 while 让 left+1,从而返回 left=2,如果 while 写的 < 那就会错误的返回 left=1

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int left = 0, right = nums.size()-1;
        // 注意是<=
        while (left <= right) {
            if (nums[left] == val) {
                nums[left] = nums[right];
                right--;
            } else {
                left++;
            }
        }
        return left;
    }
};

反转字符串

本节对应代码随想录中:代码随想录,讲解视频:字符串基础操作! | LeetCode:344.反转字符串_哔哩哔哩_bilibili

习题

题目链接:344. 反转字符串 - 力扣(LeetCode)

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

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

我的解法

左右两个指针不断交换向中间移动即可。交换可以定义一个临时字符 tmp 进行交换,也可以使用异或进行交换。

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

剑指 Offer 05. 替换空格

本节对应代码随想录中:代码随想录,讲解视频:暂无

习题

题目链接:剑指 Offer 05. 替换空格 - 力扣(LeetCode)

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

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

我的解法

写的还不是正确答案,还是新建了一个字符串。我是从前向后填充字符串,所以要新建一个字符串,其实从后往前的话就不用新建字符串了。

class Solution {
   public:
    string replaceSpace(string s) {
        // 先扩充字符串至替换后的长度
        int count = 0;
        int size = s.size();
        for (int i = 0; i < size; i++) {
            if (s[i] == ' ') {
                count++;
            }
        }
        // 填充res字符串
        string res;
        res.resize(size + count * 2);
        int slow = 0;
        for (int i = 0; i < size; i++) {
            if (s[i] != ' ') {
                res[slow++] = s[i];
            } else {
                res[slow++] = '%';
                res[slow++] = '2';
                res[slow++] = '0';
            }
        }
        return res;
    }
};

正确解法

从后往前覆盖字符串,这样就避免了更改原有的字符。一个指针从后向前遍历原来的字符串,另一个指针负责从后向前填充新的字符串

class Solution {
   public:
    string replaceSpace(string s) {
        // 先扩充字符串至替换后的长度
        int count = 0;
        int oldSize = s.size();
        for (int i = 0; i < oldSize; i++) {
            if (s[i] == ' ') {
                count++;
            }
        }
        // 填充新的字符串
        s.resize(oldSize + count * 2);
        int newSize = s.size();
        for (int i = oldSize-1,j=newSize-1; i >= 0; i--) {
            if (s[i] != ' ') {
                s[j--] = s[i];
            } else {
                s[j--] = '0';
                s[j--] = '2';
                s[j--] = '%';
            }
        }
        return s;
    }
};

反转字符串里的单词

本节对应代码随想录中:代码随想录,讲解视频:字符串复杂操作拿捏了! | LeetCode:151.翻转字符串里的单词_哔哩哔哩_bilibili

习题

题目链接:151. 反转字符串中的单词 - 力扣(LeetCode)

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

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

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

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

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

我的解法

我写的代码反转单词和代码随想录的差不多,不过还是和第一次写的差不多,有点我自己的风格。删除空格部分和代码随想录上题解类似,代码随想录是先加空格,再加字符。我是先加字符,再判断条件加空格

class Solution {
   public:
    string reverseWords(string s) {
        // 整体反转
        reverse(s.begin(), s.end());
        // 去除空格
        int now = 0;
        for (int i = 0; i < s.size(); i++) {
            if (s[i] != ' ') {
                s[now++] = s[i];
            } else {
                if (i < s.size() - 1 && s[i + 1] != ' ' && now != 0) {
                    s[now++] = ' ';
                }
            }
        }
        s.resize(now);
        // 反转单词
        int last = 0;
        for (int i = 0; i <= s.size(); i++) {
            if (s[i] == ' ' || i == s.size()) {
                reverse(s.begin() + last, s.begin() + i);
                last = i + 1;
            }
        }
        return s;
    }
};

反转链表

本节对应代码随想录中:代码随想录,讲解视频:帮你拿下反转链表 | LeetCode:206.反转链表 | 双指针法 | 递归法_哔哩哔哩_bilibili

习题

题目链接:206. 反转链表 - 力扣(LeetCode)

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

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

我的解法

还是写的麻烦了,定义了一个虚拟头结点,但这样的话,就还要让原来的 head 的 next 指向 nullptr。其实可以直接让左边的指针等于 nullptr 而不是虚拟头结点,这样就不用再执行 head->next = nullptr; 这个操作了。

class Solution {
   public:
    ListNode* reverseList(ListNode* head) {
        if(head==nullptr){
            return nullptr;
        }
        ListNode* dummyHead = new ListNode(0, head);
        ListNode* left = dummyHead;
        ListNode* right = head;
        while (right != nullptr) {
            ListNode* tmp = right->next;
            right->next = left;
            left = right;
            right = tmp;
        }
        head->next = nullptr;
        return left;
    }
};

双指针的解法正确答案如下

class Solution {
   public:
    ListNode* reverseList(ListNode* head) {
        ListNode* right = head;
        ListNode* left = nullptr;
        while (right != nullptr) {
            ListNode* tmp = right->next;
            right->next = left;
            left = right;
            right = tmp;
        }
        return left;
    }
};

这道题更好的解法是递归法,由于这章节主要讲的是双指针的使用,这里就不再写递归法的代码了

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

本节对应代码随想录中:代码随想录,讲解视频:链表遍历学清楚! | LeetCode:19.删除链表倒数第N个节点_哔哩哔哩_bilibili

习题

题目链接:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

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

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

我的解法

写的代码和正确答案基本一致,就不解释原因了

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        // 0 1    1
        ListNode* dummyHead = new ListNode(0,head);
        ListNode* left = dummyHead;
        ListNode* right = dummyHead;
        // 先让右指针向右移n个位置
        for (int i = 0; i < n; i++)
        {
            right = right->next;
        }
        // 向右遍历链表
        while(right->next!=nullptr){
            right = right->next;
            left = left->next;
        }
        // 删除节点
        ListNode* tmp = left->next;
        left->next = tmp->next;
        delete tmp;
        return dummyHead->next;
    }
};

相交链表

本节对应代码随想录中:代码随想录,暂无讲解视频

习题

题目链接:160. 相交链表 - 力扣(LeetCode)

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

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

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

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

自定义评测:
评测系统的输入如下(你设计的程序不适用此输入):

  • intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
  • listA - 第一个链表
  • listB - 第二个链表
  • skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
  • skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数
    评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA 和 headB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被视作正确答案。

示例 1:

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at ‘8’
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
— 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。

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

我的解法

我的解法基本和正确题解差不多。只不过和第一次写的时候一样,在移动长的链表的时候我还是计算两个链表移动的长度然后分别移动对应的长度。代码随想录中是让第一个链表变成最长的链表,然后移动最长的链表,两种写法只是思路不同

class Solution {
   public:
    ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
        // 记录两个链表的长度
        ListNode* cur = headA;
        int sizeA = 0;
        while (cur != nullptr) {
            sizeA++;
            cur = cur->next;
        }
        cur = headB;
        int sizeB = 0;
        while (cur != nullptr) {
            sizeB++;
            cur = cur->next;
        }
        // 让长的移动到和短的一个起点
        ListNode* curA = headA;
        ListNode* curB = headB;
        int moveA = sizeA > sizeB ? sizeA - sizeB : 0;
        int moveB = sizeB > sizeA ? sizeB - sizeA : 0;
        for (int i = 0; i < moveA; i++)
        {
            curA = curA->next;
        }
        for (int i = 0; i < moveB; i++)
        {
            curB = curB->next;
        }
        // 同时向后遍历
        while(curA!=nullptr){
            if(curA==curB){
                return curA;
            }
            curA = curA->next;
            curB = curB->next;
        }
        return nullptr;
    }
};

环形链表 II

本节对应代码随想录中:代码随想录,讲解视频:把环形链表讲清楚! 如何判断环形链表?如何找到环形链表的入口? LeetCode:142.环形链表II_哔哩哔哩_bilibili

习题

题目链接:142. 环形链表 II - 力扣(LeetCode)

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

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

不允许修改链表。

示例 1:
在这里插入图片描述

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

进阶:你是否可以使用 O(1) 空间解决此题?

我的解法

写的代码思路和正确答案一致,不过可以不用定义虚拟头结点,另外可以将 while 循环中的 if 判断放到 while 循环的判断里面。

至于思路方面,判断有环就是两个指针一个一次走两步,一个一次走一步,若相遇说明有环。相遇的时候,另一个指针从 head 开始向后每次和 slow 指针一样走一步,当他们相遇的时候,所处节点就是环的入口,这个直接记住就行了,推导方面我之前的博客中解释了原理。

class Solution {
   public:
    ListNode* detectCycle(ListNode* head) {
        ListNode* dummyHead = new ListNode(0, head);
        // 判断是否有环
        ListNode* fast = dummyHead;
        ListNode* slow = dummyHead;
        while (1) {
            if (fast->next == nullptr || fast->next->next == nullptr) {
                return nullptr;
            }
            fast = fast->next->next;
            slow = slow->next;
            // 相遇说明有环
            if(fast==slow){
                ListNode* res =dummyHead;
                while(res!=slow){
                    res = res->next;
                    slow = slow->next;
                }
                return res;
            }
        }
    }
};

三数之和

本节对应代码随想录中:代码随想录,讲解视频:梦破碎的地方!| LeetCode:15.三数之和_哔哩哔哩_bilibili

习题

题目链接:15. 三数之和 - 力扣(LeetCode)

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != 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] 。
注意,输出的顺序和三元组的顺序并不重要。

我的解法

在写这道题的时候,理所当然的对数组排序,然后用哈希表进行求解。但由于要去重,用哈希表去重会很麻烦,最终也没写出来,看了代码随想录题解后自己又写了一遍,果然还是要多刷几遍题才能加深印象

哈希表去重的麻烦在于,若是用 map 记录位置,但结果如0 0 0这种,查找0可能得到的是第一个0的位置,这样就不确定找到的0与前两个 for 循环得到的0是否是同一个元素。而用双指针查找到三元组后移动指针就能轻松解决重复的问题

class Solution {
   public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        vector<vector<int>> res;
        for (int i = 0; i < nums.size(); i++) {
            // 对a去重
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            int left = i + 1;
            int right = nums.size() - 1;
            while (left < right) {
                if (nums[i] + nums[left] + nums[right] > 0) {
                    right--;
                } else if (nums[i] + nums[left] + nums[right] < 0) {
                    left++;
                } else {
                    res.push_back({nums[i], nums[left], nums[right]});
                    // 对b和c去重
                    while (left < right && nums[left] == nums[left + 1]) {
                        left++;
                    }
                    while (left < right && nums[right] == nums[right - 1]) {
                        right--;
                    }
                    // 两个指针向内收缩
                    left++;
                    right--;
                }
            }
        }
        return res;
    }
};

四数之和

本节对应代码随想录中:代码随想录,讲解视频:难在去重和剪枝!| LeetCode:18. 四数之和_哔哩哔哩_bilibili

习题

题目链接:18. 四数之和 - 力扣(LeetCode)

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

0 <= a, b, c, d < n
a、b、c 和 d 互不相同
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]]

我的解法

四数之和和三数之和类似,只要掌握三数之和,四数之和就是加层 for 循环,最后的两个数的查找还是用双指针进行查找,能优化一个量级。不过我的解法中加上剪枝(当前最小的数都大于 target 且大于0)不符合条件时 break 会更快点

class Solution {
   public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        sort(nums.begin(), nums.end());
        vector<vector<int>> res;
        for (int i = 0; i < nums.size(); i++) {
            // 对a去重
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            for (int j = i + 1; j < nums.size(); j++) {
                // 对b去重
                if (j > i + 1 && nums[j] == nums[j - 1]) {
                    continue;
                }
                int left = j + 1;
                int right = nums.size() - 1;
                while (left < right) {
                    if ((long)nums[i] + nums[j] + nums[left] + nums[right] > target) {
                        right--;
                    } else if ((long)nums[i] + nums[j] + nums[left] + nums[right] < target) {
                        left++;
                    } else {
                        res.push_back(
                            {nums[i], nums[j], nums[left], nums[right]});
                        // 对c和d去重
                        while (left < right && nums[left] == nums[left + 1]) {
                            left++;
                        }
                        while (left < right && nums[right] == nums[right - 1]) {
                            right--;
                        }
                        left++;
                        right--;
                    }
                }
            }
        }
        return res;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值