双指针法典型例题记录 代码随想录 刷题记录

题目顺序参考的是代码随想录网站上的顺序。

因为已经做过一遍了,所以直接写经验教训和思路了。 

1. 移除元素

采用数组的相关的做法,使用add和remove函数?vector数组本身有啥函数吗?

想到大概率要用到双指针,但是双指针具体如何实现也忘记了。。。只能够大概想到用i和j来记录数组中对应的元素的值。

快指针和慢指针的使用。

本质上还是要回归到对于暴力解法的理解,因为暴力解法就是套两层循环(一旦遇到不符合条件的值就批量移动数组)来解决问题,但是这样做的时间复杂度不行。于是擦用快慢指针的思路一次完成

设置快指针寻找符合条件的值,设置慢指针更新符合条件的值的下标。

2.反转字符串

设置一前一后两个指针,循环实现交换的操作。直到两个指针相遇为止。

思路正确,解答完成。

3.替换数字

做过的题目,看到了其实还是没有立刻就反映出思路,对于问题解决的熟悉度还不够。

核心在于,新的数组的长度与原数组不同了,所以要有重新设定长度的过程。

没有设立额外的字符串空间,但是思路值得借鉴。新的字符串长度定下来后,直接从旧字符串的尾部开始将相关值赋给后方;如果不为数字则正常赋值。

4.翻转字符串里的单词

 重点在于如何处理原来字符串中的空格。

核心在于移除前导空格和尾随空格并且使得每个单词之间的间隙都为一个空格。

移除空格的时候常见方法中是要使用字符串带的erase函数。

s.erase(括号中是下标值)。

(在这个常用方法中默认前导空格和尾随空格只有一个。而中间的空格则根据两个中间值是不是相等且均为空格来判断。)

而双指针法中的移除空格则是采用了一快一慢两个指针来进行操作,过程有一点像“移除元素”中采用的方法。

void removeExtraSpaces(string& s) {//去除所有空格并在相邻单词之间添加空格, 快慢指针。
    int slow = 0;   //整体思想参考https://programmercarl.com/0027.移除元素.html
    for (int i = 0; i < s.size(); ++i) { //
        if (s[i] != ' ') { //遇到非空格就处理,即删除所有空格。
            if (slow != 0) s[slow++] = ' '; //手动控制空格,给单词之间添加空格。slow != 0说明不是第一个单词,需要在单词前添加空格。
            while (i < s.size() && s[i] != ' ') { //补上该单词,遇到空格说明单词结束。
                s[slow++] = s[i++];
            }
        }
    }
    s.resize(slow); //slow的大小即为去除多余空格后的大小。

此处代码的精简之处在于,采用慢指针来填补空格的值,并且每次只填补一次。(判断条件中也加入了对于slow不是字符串开头的判断)

主逻辑的判断采用:不为空格就直接退后,将值赋给慢指针。

注意在整套逻辑进行完后要对字符串进行resize的处理,调整其大小。

翻转的部分:

可以视为是整体翻转完后再对空格部分的单个单词进行翻转。

int start = 0; //removeExtraSpaces后保证第一个单词的开始下标一定是0。
        for (int i = 0; i <= s.size(); ++i) {
            if (i == s.size() || s[i] == ' ') { //到达空格或者串尾,说明一个单词结束。进行翻转。
                reverse(s, start, i - 1); //翻转,注意是左闭右闭 []的翻转。
                start = i + 1; //更新下一个单词的开始下标start
            }

这里要注意的是遍历的指针只有i,但是要确定reverse的前方的值还需要引入一个start,来判断单词的前后。

判断单词结束有两种方法:一个是已经到达了单词的末尾,或者是到了空格。这里要注意的是,如果for循环没有直接以<=s.size()为条件,修改后面的判断条件也是可以做的。

for (int i = 0; i < s.size(); i ++) {
            if (i + 1 == s.size() || s[i + 1] == ' ') { //到达空格或者串尾,说明一个单词结束。进行翻转。
                reverse(s, start, i); //翻转,注意是左闭右闭 []的翻转。
                start = i + 2; //更新下一个单词的开始下标start
            }
        }

reverse整体的操作(s, 0, s.size() - 1)。

reverse的整体操作也是在前方定义了一个整体的函数reverse。(采用双指针法则)

void reverse(string& s, int start, int end){ //翻转,区间写法:左闭右闭 []
        for (int i = start, j = end; i < j; i++, j--) {
            swap(s[i], s[j]);
        }
    }

5.反转链表

第一下想的是交换链表中的值,但是这个和数组不同的在于,链表前后关系并不如数组那般好变动。如果需要定位到链表中的特定的值,反倒需要知道前一个链表的值是什么,才能对指针进行操作。

所以最好的办法不是单个交换链表的数值,而是直接改变原有指针的指向。采用一种更为整体的方法来操作。(并没有设定新的空间)

或者是运用熟知的头插法?

头插法比双指针法更容易想到,先介绍头插法:

核心在于遇到新的结点的时候将值插入新的定义的结点的前方:

倒退式更新法,首先定义newnode,该值会通过头结点不断向后退直到变成NULL。定义一个结点保护原有的头结点的next,接着将原来的head的值与新的结点连接起来。

ListNode* nextNode = curr->next;  // 保存下一个节点

curr->next = newHead;    // 头插操作:当前节点指向新链表头

接着更新newHead的值,使得它为curr,满足移动的规则。并且更新原来curr的值(使用保护的值)

 newHead = curr;          // 更新新链表头为当前节点

curr = nextNode;  

双指针法:

原来的链表末尾指向NULL,一个是定义原有的部分为cur,接着再定义一个pre,用于改变原有的链表的指向。pre是cur的后一个值,两者相邻且同时移动。

另外还需要一个临时指针来保存原来的cur后面的结点,用tmp来表示。

while(cur != NULL){           

        tmp = cur -> next;           

        cur -> next = pre;           

        pre = cur;           

        cur = tmp;        

}

注意的一点是,在移动的时候先移动pre的值,再移动cur。因为pre的值与cur相关,如果先移动cur就会导致pre无法定位cur。

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

争取在一次遍历的时候就能够完成结点的删除工作而不用重复进行。

怎么用两个指针一次就找到倒数第N个结点是本题的重点。

核心在于利用好“倒数第N个结点“这个条件,因为单向链表本身没有办法记录到倒数的值,所以就思考能不能用两个指针来表示出一个倒数的差值。想到先让块指针运动N个单位,再快慢指针一起运动直到快指针已经运动到末尾,此时快慢指针的插值就是N。又因为快指针已经运动到了末尾,所以慢指针的位置就是要求的倒数第N个结点。

这里涉及到的一个注意事项是虚拟头结点,因为原来的代码中所带的头结点是有数值的,如果删除的正好是头结点的话就要额外的写出删除的值是头结点的判断逻辑。(删除都要找前一个值是什么)而设置虚拟头结点的好处就是可以直接一视同仁地进行删除操作,并且直接设置dummy head -> next = head(原来的头结点)。

7.链表相交

处理思路就是,分别遍历两个量表的长度,得到两个长度的差值。先让较长的那个字符串运动完这个差值,再同时运动来判断是否有相等的部分。

思路比较简单~

8.环形链表II

这题是运用数学计算与找规律的思维来做的,快指针移动两位,慢指针移动一位,然后通过快慢指针的追及问题找到答案,但是具体的实现的过程有点不清晰。

具体过程的实现:快指针一次移动两个单位,慢指针一次移动一个单位,然后两者一定会在环内相遇。根据数学公式的计算,可以得到由头结点出发移动的距离和相遇点出发 最后移动到的相遇的点即入口点。

这个入口点的计算要从带环的数值开始计算,设立相关的值分析其关系求出。

根据这个能够列出的式子是,快慢指针在移动的过程中有关系式:x + y = y  + z + y,因为两者同时开始运动,所以其运动的距离呈两者运动的倍数关系。

可以列出式子:(x + y) * 2 = x + y + n (y + z)。

9.三数之和

三数之和求解的时候一个很重要的点是,将其中的两个数的和视作一个整体,这样就可以不用动了。但是这个与同时给出三个数组的情况又不一样,因为在给出的数组是同一个的情况下,还需要考虑会不会重复的问题。

原始思路能够想到的就是用两个for循环来计算nums数组中求和的值,最后再用find函数找到相关的值。————这个思路不对啊,这个是满足最上方所提到的分了好几个不同数组的情况来求解。

一个很重要的思路就是先对原有的数组进行排序,至少这样可以判断出来数组的大小情况。

另外值得注意的是,在进行去重的判断的时候,nums[i] == nums[i - 1]的判断是去重的判断。因为如果是直接判断中括号中的i和i + 1,只能起到对于元素本身去重的作用。但是采取nums[i] == nums[i - 1]则可以判断收集的这一组三元组是否出现过。而起到真正的判断去重的作用。

正确的思路是,用一层for循环,定义一个下标left在i + 1的位置上,定义下标right在末尾的位置上。依然在数组中寻找会使数组三者的和为0的值,如果三者和大于0,则移动right的值让其变小一点;如果小于0,则移动left的值让其变大一些。

int left = i + 1,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 ++;//数字过小
                }

至于收获结果这段,如果求得三者的值等于0,则result.pushback()。同时别忘记还要对相应的值进行去重工作。

通过不断的进行值的动态调整可以判断出结果。

else{
            result.push_back(vector<int> {nums[i], nums[left], nums[right]});
            //找到有效解之后也要更新left和right的指针
            while(left < right && nums[left] == nums[left + 1])     
            left ++;//去重要在第一轮判断之后进行
            while(left < right && nums[right] == nums[right - 1])
            right --;//使用去重的判断
            //更新下标的值
            left ++;
            right --;//寻找下一个可能的解

该题目的难点在于其去重部分的判断,以及对于right和left值的相关的理解。

去重部分(i, left, right都有自己的去重的逻辑)的目的是为了找到不一样的三元组来满足相关的条件,而不是找到的三元组本身的元素不重复。

10.四数之和

定义四个指针,其中两个为循环中的变量,与三数之和不同的点或许在于四数之和要套两层循环。

思路正确,但是定义的变动的指针不一样。原来的指针使用的是i和k = i + 1为变动的指针,但是我自己的代码中定义的另一个是倒数着来的。————这样的方式也是OK的,只是要注意定义的值。j作为从末尾循环来的值,定义的时候注意初始值为j = nums.size() - 1,循环值为j > i。

注意:

1.使用push_back的时候不是直接输入相关的值,而是前面带有一个前缀push_back(vector<int> {数组形式})。

2.因为四个数相加的和可能过大,所以要在使用之前加上(long)的前缀。

3.原有的剪枝条件不正确。很重要的一个点是 如果最小的值也大于target,其实并不一定能得到所求集合为空 的结论。因为如果数组中都是负数的话,一堆大的负数相加也会变成更小的数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值