基础算法笔记——双指针

目录

一、引入

二、算法思想

三、例题

第一题

第二题 

第三题

第四题

第五题


一、引入

先看一道例题  例题

给你一个按非递减顺序排序的整数数组 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 已按 非递减顺序 排序

来源:力扣(LeetCode)

对于这道题,我们最先想到的方式是另开一个数组,将该数组的各个数的平方一一存入该数组,之后重新排序。

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        vector<int> num(nums.size()); //开辟新数组

        for(int i = 0; i < nums.size(); ++i)//将原数组的平方处理后一一存入新数组
        {
            num[i] = nums[i] * nums[i];
        }

        sort(num.begin(), num.end(), less());//对新数组排序

        return num;//返回新数组
        
    }
};

对这个解法我们做点改动

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {

        // vector<int> num(nums.size());

        // for(int i = 0; i < nums.size(); ++i)
        // {
        //     num[i] = nums[i] * nums[i];
        // }

        // sort(num.begin(), num.end(), less());

        // return num;

        
        vector<int > num(nums.size());//新的空间
        int i = 0, j = nums.size() - 1;

        for(int k = nums.size() - 1; i <= j;)//遍历数组
        {            
            if(nums[i] * nums[i] < nums[j] * nums[j])
            {
                num[k] = nums[j] * nums[j];
                j--; 
            }
            else
            {
                num[k] = nums[i] * nums[i];
                i++;
            }
            k--;
        }
        return num;
    }
};

在这个解法中,因为包含负数,所以平方数最大值可能出现在头与尾;因此在遍历过程中,逐步比较头一个与最后一个。

二、算法思想

双指针其实是对原有算法的一种优化方式,常见的有快慢指针,对撞指针。

三、例题

第一题

第一题

给定一个数组 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

来源:力扣(LeetCode)

先一般性的解法

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        vector<int > num;

        for(int i = 0, j = 0; i < nums.size(); ++i)//将nums中非0数排入num
        {
            if(nums[i] != 0)
            {
                num.push_back(nums[i]);
                j++;
            }
        }

        for(int i = 0, j = 0; i < nums.size(); ++i)//将nums中0排入num
        {
            if(nums[i] == 0)
            {
                num.push_back(nums[i]);
                j++;
            }
        }

        nums.assign(num.begin(), num.end());//将num复制进nums中
    }
};

双指针解法

我们设想如果两个指针,一个指针在左边,一个在右边查找非0数,如果右边的数字不是0,就将右边的数字换到左边。

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        // vector<int > num;

        // for(int i = 0, j = 0; i < nums.size(); ++i)
        // {
        //     if(nums[i] != 0)
        //     {
        //         num.push_back(nums[i]);
        //         j++;
        //     }
        // }

        // for(int i = 0, j = 0; i < nums.size(); ++i)
        // {
        //     if(nums[i] == 0)
        //     {
        //         num.push_back(nums[i]);
        //         j++;
        //     }
        // }

        // nums.assign(num.begin(), num.end());

        int i = 0; //指向非0数
        int j = 0; //
        while(i < nums.size())//遍历整个数组
        {
            if (nums[i])//右边遇到非0数
            {
                swap(nums[i], nums[j]);
                j++;
            }
            i++;
        }
    }
};

做个动画演示一个整个过程

第二题 

第二题

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 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 码表中的可打印字符

来源:力扣(LeetCode)

这题就不用一般性解法演示了,比较复杂,难度不大

定义两个指针,一个指向字符串的首位,一个指向字符串的末尾,逐步将首位与末尾交换

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

        for(; i <= j; i++, j--)
        {
            swap(s[i], s[j]); //交换头与尾
        }
    }
};

第三题

第三题

给定一个头结点为 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

示例 1:

输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.


示例 2:

输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
 

提示:

给定链表的结点数介于 1 和 100 之间。

来源:力扣(LeetCode)

对于这题我们很容易能想的方法就是直接找到中间的那个数,但是链表又无法直接定位,因此我们可以把该链表转换为数组,再定位到中间的数。

/**
 * 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* middleNode(ListNode* head) {
        vector <ListNode* > L = {head};
        
        while (L.back()->next != NULL)
        {
            L.push_back(L.back()->next);
        }

        return L[L.size() / 2];
    }
};

这个是把链表的数据转换到数组中,再直接输出中间节点。我们做点改进,在原有的链表中,找到中间的节点。

/**
 * 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* middleNode(ListNode* head) {
        
        /*
        * 第一种
        */
        // vector <ListNode* > L = {head};
        
        // while (L.back()->next != NULL)
        // {
        //     L.push_back(L.back()->next);
        // }

        // return L[L.size() / 2]


        /*
        * 第二种
        */
        int n = 0;//初始化
        ListNode* cur = head;

        while(cur != nullptr)//确定节点数量
        {
            ++n;
            cur = cur -> next;
        }

        int i = 0;
        cur = head;//初始化cur

        while (i < n / 2)//找到中间节点
        {
            i++;
            cur = cur->next;
        }

        return cur;
    }
};

再用双指针的方式优化一下,这里用到的是快慢指针。

/**
 * 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* middleNode(ListNode* head) {

        /*
        * 第一种
        */
        // vector <ListNode* > L = {head};
        
        // while (L.back()->next != NULL)
        // {
        //     L.push_back(L.back()->next);
        // }

        // return L[L.size() / 2]


        /*
        * 第二种
        */
        // int n = 0;
        // ListNode* cur = head;

        // while(cur != nullptr)//确定节点数量
        // {
        //     ++n;
        //     cur = cur->next;
        // }

        // int i = 0;
        // cur = head;//初始化cur

        // while (i < n / 2)//找到中间节点
        // {
        //     i++;
        //     cur = cur->next;
        // }

        // return cur;


        /*
        * 第三种
        */
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast != nullptr && fast->next != nullptr)
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
};

快慢指针是利用两个指针的速度不一致,比如这题中,快指针的速度是慢指针的两倍,快指针到末尾的时候,恰好慢指针到达链表的中间。

第四题

第四题

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

示例 1:

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]


示例 2:

输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释: 
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
 

提示:

1 <= nums.length <= 105
-231 <= nums[i] <= 231 - 1
0 <= k <= 105

来源:力扣(LeetCode)

class Solution {
public:
    void rotate(vector<int>& nums, int k) {

        //int i = 0; int j = nums.zise() - k;

        vector<int > num(nums.size());//开辟一个数组空间
        
        int temp;

        for(int i = 0; i < nums.size(); i++)
        {
            if(i < nums.size() - k)
            {
                num[(i + k) % nums.size()] = nums[i];//对nums.size()取模,防止溢出
            }
            else 
            {
                temp = nums.size() - i - k;
                temp = fabs(temp);
                temp = temp % nums.size();//同样取模,防止溢出
                num[temp] = nums[i];
            }
        }
        nums.assign(num.begin(), num.end());
    }
};

后面看官方解答,似乎并不需要分类,于是乎

class Solution {
public:
    void rotate(vector<int>& nums, int k) {

        //int i = 0; int j = nums.zise() - k;

        vector<int > num(nums.size());//开辟一个数组空间
        
        int temp;

        for(int i = 0; i < nums.size(); i++)
        {
            // if(i < nums.size() - k)
            // {
            //     num[(i + k) % nums.size()] = nums[i];//对nums.size()取模,防止溢出
            // }
            // else 
            // {
            //     temp = nums.size() - i - k;
            //     temp = fabs(temp);
            //     temp = temp % nums.size();//同样取模
            //     num[temp] = nums[i];
            // }
            num[(i + k) % nums.size()] = nums[i];
        }
        nums.assign(num.begin(), num.end());
    }
};

这个也是可以AC的。

class Solution {
public:
    void rotate(vector<int>& nums, int k) {
         //int i = 0; int j = nums.zise() - k;

        // vector<int > num(nums.size());//开辟一个数组空间
        
        // int temp;

        // for(int i = 0; i < nums.size(); i++)
        // {
        //     // if(i < nums.size() - k)
        //     // {
        //     //     num[(i + k) % nums.size()] = nums[i];//对nums.size()取模,防止溢出
        //     // }
        //     // else 
        //     // {
        //     //     temp = nums.size() - i - k;
        //     //     temp = fabs(temp);
        //     //     temp = temp % nums.size();//同样取模
        //     //     num[temp] = nums[i];
        //     // }
        //     num[(i + k) % nums.size()] = nums[i];
        // }
        // nums.assign(num.begin(), num.end());

        int i = 0, j = nums.size() - 1;

        while(i <= j)//将所有的数调转顺序。
        {
            swap(nums[i], nums[j]);
            i++;
            j--;
        }

        i = 0;
        j = k % nums.size() - 1;//取模,防止数据溢出

        while(i <= j)//调转前数列的顺序
        {
            swap(nums[i], nums[j]);
            i++;
            j--;
        }

        i = k % nums.size();//取模,防止数据溢出
        j = nums.size() - 1;

        while(i <= j)//调转后序列的顺序
        {
            swap(nums[i], nums[j]);
            i++;
            j--;
        }
    }
};

用双指针的方法,我们可以这样,将整个数列翻转,之后以k为界,分别将前后数列翻转。

官方解答

class Solution {
public:
    void reverse(vector<int>& nums, int start, int end) {
        while (start < end) {
            swap(nums[start], nums[end]);
            start += 1;
            end -= 1;
        }
    }

    void rotate(vector<int>& nums, int k) {
        k %= nums.size();
        reverse(nums, 0, nums.size() - 1);
        reverse(nums, 0, k - 1);
        reverse(nums, k, nums.size() - 1);
    }
};

 

第五题

第五题

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

示例 1:

输入:s = "Let's take LeetCode contest"
输出:"s'teL ekat edoCteeL tsetnoc"


示例 2:

输入: s = "God Ding"
输出:"doG gniD"
 

提示:

1 <= s.length <= 5 * 104
s 包含可打印的 ASCII 字符。
s 不包含任何开头或结尾空格。
s 里 至少 有一个词。
s 中的所有单词都用一个空格隔开。

来源:力扣(LeetCode)

在写这题的时候,我们想的就是,针对每一个单词分别调换。

class Solution {
public:
    string reverseWords(string s) {
        //string str;//新的空间,用于储存已经反转好的单词
        int i;
        int begin = 0, end = 0;//交换时的开始位置与结束位置
        int sLength = s.length();//字符串长度
        int count = 0;//定义遍历字母的数量
        while(count < sLength) //count的值不可以随意的变动,它表示的是遍历的字母位置
        {
            begin = count;//将i初始化至当前位置

            while(s[count] != ' ' && count < sLength)//确定j的位置,每个单词的结尾
            {
                count++;
            }
            
            end = count - 1;

            while(begin <= end)//颠倒单词的字母顺序
            {
                swap(s[begin], s[end]);
                begin++;
                end--;
            }

            while(s[count] == ' ' && count < sLength)//定位到下一个单词
            {
                count++;
            }
        }
        return s;
    }
};

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值