算法——双指针

1. 什么是双指针算法?

双指针算法是一种常用于解决数组或链表中的问题的技巧。它涉及使用两个指针(索引或引用),通常分别称为“快指针”和“慢指针”或“左指针”和“右指针”,以协同进行遍历或搜索。

该算法的核心思想是通过移动这两个指针来实现特定的目标,例如寻找一对元素的和、判断是否存在某种关系或在特定条件下移动其中一个指针。双指针算法通常能够在O(n)的时间复杂度内解决问题,具有较好的效率。 

2. 双指针算法的常见形式

常见的双指针算法有以下几种类型:

  1. 对撞指针: 在数组两端分别设立左右指针,通过向中间移动这两个指针来解决问题。例如,寻找数组中的两个元素,它们的和等于给定值。

  2. 快慢指针: 一个指针移动速度较快,另一个移动速度较慢。这常用于解决链表中的问题,如判断链表是否有环,找到链表的中间节点等。

  3. 滑动窗口(有专题描述:算法——滑动窗口): 使用两个指针维护一个窗口,通过移动窗口的左右边界解决问题。这类问题常见于字符串和数组处理,例如找到最短的包含所有字符的子串。

  4. 同向双指针: 两个指针方向相同,通过控制其中一个指针的位置来处理问题。例如,在一个有序数组中查找两个数,使它们的和等于给定值。

3. 应用实例

1. 移动零

题目链接:283. 移动零 - 力扣(LeetCode)

解析:看到这道题我们这样解决,即定一个左端点在第一个0处,右指针向右寻找第一个非0元素,找到后交换这两个元素,直到遍历整个数组,代码如下

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

2. 复写零

题目链接:1089. 复写零 - 力扣(LeetCode)

解析:对于这道题,我们分析一下可以发现,要想在原数组上做到复写0,我们需要从右向左复写,不然会覆盖到后面的数字,那么我们如何找到最后一个修改后数组的位置呢?那么在这里我们可以定义一个快指针dest表示慢指针所指元素是否到达数组结尾,定义一个慢指针cur表示修改后数组中末尾元素,代码如下

class Solution 
{
public:
    void duplicateZeros(vector<int>& arr) 
    {
        int n = arr.size();
        int cur = 0;
        int dest = -1;

        while (dest < n-1)
        {
            // 若cur对应元素为0则表示需要复写,dest+=2
            if (arr[cur] != 0)
            {
                dest++;
            }
            else
            {
                dest += 2;
            }
            if (dest < n-1)
                cur++;
        }

        // 从右向左开始复写
        while (cur >= 0)
        {
            // 若dest超出数组范围,预处理末尾元素即可
            if (dest > n-1)
            {
                arr[n-1] = 0;
                dest-=2;
                cur--;
                continue;
            }

            // 非0元素复写一次,0元素复写2次
            if (arr[cur] != 0)
            {
                arr[dest] = arr[cur];
                dest--;
                cur--;
            }
            else
            {
                arr[dest] = 0;
                arr[dest-1] = 0;
                dest -= 2;
                cur--;
            }
        }
    }
};

3. 快乐数

题目链接:202. 快乐数 - 力扣(LeetCode)

解析:我们首先分析一下19是如何变为1的,即

随后我们再来分析一下2是为何变成不了1的,即

可以看到,最后整个生成快乐数的过程形成了一个环,因此我们可以使用快慢指针判断链表是否有环的思想来解决这道题(参考:OR36链表的回文结构),代码如下

class Solution {
public:
    int transform(int n)
    {
        int ret = 0;
        while (n)
        {
            int num = (n % 10) * (n % 10);
            ret += num;

            n /= 10;
        }

        return ret;
    }

    bool isHappy(int n) 
    {
        int slow = transform(n);
        int fast = transform(transform(n));

        while (slow != fast)
        {
            slow = transform(slow);        
            fast = transform(transform(fast));
        }

        if (fast == 1) return true;
        else return false;
    }
};

对于这道题来说,我们应该还有一个疑问,为什么不会到达1的时候一定会形成一个环呢?因为int类型最大值为为‭‭2 147 483 647‬‬, 所以平方和最大的数是1 999 999 999,平方和为1 + 81*9 = 724。任何数的平方和都在1到724之间,724次循环之内一定有重复的,即一定会进入循环区间,从而形成环。

4. 盛多最水的容器

题目链接:11. 盛最多水的容器 - 力扣(LeetCode)

解析:分析一下题意我们可以发现,我们可以定义左右指针,分别从两端向中间遍历,哪一端低就像反方向移动一位(若左边低则left++,右边低则right--),每一次将本次计算出的面积与上一次相比较,直到遍历到最后得到最终结果,代码如下

class Solution 
{
public:
    int maxArea(vector<int>& h) 
    {
        int left = 0;
        int right = h.size()-1;
        int max = 0;
        while (left < right)
        {
            if (h[left] <= h[right])
            {
                int tmp = h[left] * (right - left);
                if (tmp > max) max = tmp;

                left++;
            }
            else
            {
                int tmp = h[right] * (right - left);
                if (tmp > max) max = tmp;

                right--;
            }
        }
        
        return max;
    }
};

5. 有效三角形的个数

题目链接:611. 有效三角形的个数 - 力扣(LeetCode)

解析:要想判断三个数是否能组成三角形,我们知道需要满足以下条件:

1. 任意两边之和大于第三边

2. 任意两边之差小于第三边

那么我们先将数组排序,可以固定一条边(此边最小),将这个数右边的数作为一个区间,借助双指针来判断这三个数是否能够组成三角形,代码如下

class Solution {
public:
    int triangleNumber(vector<int>& nums) 
    {
        sort(nums.begin(), nums.end());
        int count = 0;
        int n = nums.size();
        for (int i = n-1; i >= 2; i--)
        {
            int left = 0;
            int right = i-1;

            while (left < right)
            {
                if (nums[left] + nums[right] > nums[i])
                {
                    count += right - left;

                    right--;
                }
                else
                {
                    left++;
                }
            }
        }

        return count;
    }
};

6. 和为s的两个数

题目链接:LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)

解析:分析题目后,我们可以使用双指针从数组的两端向中间寻找,由于数组是升序的,因此当结果 < target时,left++即可,当结果> target时,right--即可,代码如下

class Solution {
public:
    vector<int> twoSum(vector<int>& price, int target) 
    {
        int n = price.size();

        int left = 0, right = n-1;
        while (left < right)
        {
            if (price[left] + price[right] < target) left++;
            else if (price[left] + price[right] > target) right--;
            else break;
        }

        return {price[left], price[right]};
    }
};

7. 三数之和

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

解析:这道题要求找到三数之和为0的三元组,那么我们可以先将数组排序,每次固定一个最小的数(最左端),在它右边的剩余区间中借助双指针找到符合nums[left] + nums[ight] == nums[min]的数,代码如下

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) 
    {
        // 借助set去重
        set<vector<int>> res;
        sort(nums.begin(), nums.end());
        int n = nums.size();
        for (int i = 0; i < n-2; i++)
        {
            int left = i+1;
            int right = n-1;
            while (left < right)
            {
                if (nums[left] + nums[right] < -nums[i]) left++;
                else if (nums[left] + nums[right] > -nums[i]) right--;
                else
                {
                    vector<int> tmp = {nums[i], nums[left++], nums[right--]};
                    res.insert(tmp);
                }
            }
        }

        vector<vector<int>> ret;
        for (auto& e : res) ret.push_back(e);

        return ret;
    }
};

8. 四数之和

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

解析:对于四数之和我们可以采取和三数之和类似的处理方法,即先将数组排序,固定一个最小的数(最左端数),在剩下的区间内再去固定另一个最小值,再借助双指针寻找并判断四数是否满足对应关系,代码如下

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) 
    {
        sort(nums.begin(), nums.end());
        set<vector<int>> res;
        int n = nums.size();
        for (int i = 0; i < n - 3; i++)
        {
            for (int j = i+1; j < n-2; j++)
            {
                int left = j+1;
                int right = n-1;
                while (left < right)
                {
                    int a = nums[i];
                    int b = nums[j];
                    int c = nums[left];
                    int d = nums[right];

                    long long t_left = nums[left] + nums[right];
                    long long t_right = target;
                    t_right -= nums[i];
                    t_right -= nums[j];
                    
                    if (t_left < t_right) left++;
                    else if (t_left > t_right) right--;
                    else 
                    {
                        vector<int> tmp = {a,b,c,d};
                        res.insert(tmp);

                        left++;
                        right--;
                    }
                }
            }
        }

        vector<vector<int>> ret;
        for (auto& e : res) ret.push_back(e);

        return ret;
    }
};
  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java是一种广泛使用的面向对象的编程语言,由Sun Microsystems公司于1995年5月正式发布。它的设计目标是“一次编写,到处运行(Write Once, Run Anywhere)”,这意味着开发者可以使用Java编写应用程序,并在支持Java的任何平台上无需重新编译即可运行,这得益于其独特的跨平台性,通过Java虚拟机(JVM)实现不同操作系统上的兼容。 Java的特点包括: 面向对象:Java全面支持面向对象的特性,如封装、继承和多态,使得代码更易于维护和扩展。 安全:Java提供了丰富的安全特性,如禁止指针运算、自动内存管理和异常处理机制,以减少程序错误和恶意攻击的可能性。 可移植性:Java字节码可以在所有安装了JVM的设备上执行,从服务器到嵌入式系统,再到移动设备和桌面应用。 健壮性与高性能:Java通过垃圾回收机制确保内存的有效管理,同时也能通过JIT编译器优化来提升运行时性能。 标准库丰富:Java拥有庞大的类库,如Java SE(Java Standard Edition)包含基础API,用于开发通用应用程序;Java EE(Java Enterprise Edition)提供企业级服务,如Web服务、EJB等;而Java ME(Java Micro Edition)则针对小型设备和嵌入式系统。 社区活跃:Java有着全球范围内庞大的开发者社区和开源项目,持续推动技术进步和创新。 多线程支持:Java内建对多线程编程的支持,使并发编程变得更加简单直接。 动态性:Java可以通过反射、注解等机制实现在运行时动态加载类和修改行为,增加了程序的灵活性。 综上所述,Java凭借其强大的特性和广泛的适用范围,在企业级应用、互联网服务、移动开发等领域均扮演着举足轻重的角色,是现代软件开发不可或缺的重要工具之一。
中英文书籍+源码+课后题详解 --------------------------------------- Kyle Loudon是美国加州洛斯加托斯Jeppesen Dataplan公司的一名软件工程师,主管图形接口开发小组,主攻航迹规划软件的研发,这些软件主要用于商业航空公司、私营航空部门和其他一些航空制造业。在来到Jeppesen之前,Kyle在IBM公司是一名系统程序员。在技术上,Kyle主要对操作系统、网络、人机交互等领域感兴趣。1992年,Kyle在普渡大学拿到了计算机科学学士学位,并取得了法语的第二学位,同时他还被选入斐陶斐荣誉学会(美国大学优等生之荣誉学会)。他在普渡大学计算机系教了三年的计算机课程。在这期间,他完成了他个人的第一本书《Understanding Computers》,这本书用理论结合实践的方式介绍计算机的方方面面。如今,尽管他继续工作在硅谷的软件业,但他仍然坚韧不拔地在追求一个更高的学位。 除了计算机,Kyle多年来喜欢打网球、教网球。他还喜欢山地骑行、滑冰,偶尔也和朋友们一起参加高尔夫课程。另外,Kyle还喜欢各种形式的戏剧、美食,以及某些风格的音乐和艺术;他期望成为钢琴家和艺术家,但希望渺茫。现在,他是一个拥有美国联邦航空局颁发的商业飞行员执照的飞行员。 《算法精解:C语言描述》是数据结构和算法领域的经典之作,十余年来,畅销不衰!全书共分为三部分:第一部分首先介绍了数据结构和算法的概念,以及使用它们的原因和意义,然后讲解了数据结构和算法中最常用的技术——指针和递归,最后还介绍了算法的分析方法,旨在为读者学习这本书打下坚实的基础;第二部分对链表、栈、队列、集合、哈希表、堆、图等常用数据结构进行了深入阐述;第三部分对排序、搜索数值计算、数据压缩、数据加密、图算法、几何算法等经典算法进行了精辟的分析和讲解。 本书的众多特色使得它在同类书中独树一帜:具体实现都采用正式的C语言代码而不是伪代码,在很多数据结构和算法的实现过程中,有大量细节问题是伪代码不能解决的;每一章都有精心组织的主题和应用;全部示例来自真实的应用,不只是一般的练习;对每种数据结构、算法和示例都进行了详细分析;每一章的末尾都会有一系列问题和对应的回答,旨在强调这一章的重要思想…… 本书中的代码尤为值得强调:所有实现都采用C语言编写,所有代码都优先用于教学目的,所有代码都在4种平台上经过完整测试,头文件记录了所有公共的接口,命名规则适用于全书所有的代码,所有的代码都包含大量注释……

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值