热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

程序员如何巧妙学习算法技巧?

作者|帅地责编|胡巍巍今天和大家讲讲,在做算法题时常用的一些技巧。对于平时没用过这些技巧的人,或许你可以考虑试着去看看在实践中能否用的上这些技巧来优化问

640?wx_fmt=gif

640?wx_fmt=jpeg

作者 | 帅地

责编 | 胡巍巍

今天和大家讲讲,在做算法题时常用的一些技巧。对于平时没用过这些技巧的人,或许你可以考虑试着去看看在实践中能否用的上这些技巧来优化问题的解。


640?wx_fmt=png

 巧用数组下标


数组的下标是一个隐含的很有用的数组,特别是在统计一些数字,或者判断一些整型数是否出现过的时候。

例如,给你一串字母,让你判断这些字母出现的次数时,我们就可以把这些字母作为下标,在遍历的时候,如果字母a遍历到,则arr[a]就可以加1了,即arr[a]++。

通过这种巧用下标的方法,我们不需要逐个字母去判断。

我再举个例子:

问题:给你n个无序的int整型数组arr,并且这些整数的取值范围都在0-20之间,要你在 O(n) 的时间复杂度中把这n个数按照从小到大的顺序打印出来。

对于这道题,如果你是先把这n个数先排序,再打印,是不可能O(n)的时间打印出来的。

但是数值范围在0-20。我们就可以巧用数组下标了。把对应的数值作为数组下标,如果这个数出现过,则对应的数组加1。

代码如下:

public void f(int arr[]{

        int[] temp = new int[21];
        for (int i &#61; 0; i < arr.length; i&#43;&#43;) {
            temp[arr[i]]&#43;&#43;;
        }
        //顺序打印
        for (int i &#61; 0; i < 21; i&#43;&#43;) {
            for (int j &#61; 0; j < temp[i]; j&#43;&#43;) {
                System.out.println(i);
            }
        }
    }

利用数组下标的应用还有很多&#xff0c;大家以后在遇到某些题的时候可以考虑是否可以巧用数组下标来优化。


640?wx_fmt&#61;png

巧用取余


有时候我们在遍历数组的时候&#xff0c;会进行越界判断&#xff0c;如果下标差不多要越界了&#xff0c;我们就把它置为0重新遍历。特别是在一些环形的数组中&#xff0c;例如用数组实现的队列。往往会写出这样的代码&#xff1a;

for (int i &#61; 0; i < N; i&#43;&#43;) {
        if (pos < N) {
         //没有越界
         // 使用数组arr[pos]
        else {
          pos &#61; 0;//置为0再使用数组
           //使用arr[pos]
         }
        pos&#43;&#43;;
   }

实际上我们可以通过取余的方法来简化代码。

for (int i &#61; 0; i < N; i&#43;&#43;) {
   //使用数组arr[pos]   (我们假设刚开始的时候pos < N)
   pos &#61; (pos &#43; 1) % N;
}


640?wx_fmt&#61;png

巧用双指针


对于双指针&#xff0c;在做关于单链表的题是特别有用&#xff0c;比如“判断单链表是否有环”、“如何一次遍历就找到链表中间位置节点”、“单链表中倒数第k个节点”等问题。

对于这种问题&#xff0c;我们就可以使用双指针了&#xff0c;会方便很多。我顺便说下这三个问题怎么用双指针解决吧。

例如对于第一个问题&#xff0c;我们就可以设置一个慢指针和一个快指针来遍历这个链表。

慢指针一次移动一个节点&#xff0c;而快指针一次移动两个节点&#xff0c;如果该链表没有环&#xff0c;则快指针会先遍历完这个表&#xff0c;如果有环&#xff0c;则快指针会在第二次遍历时和慢指针相遇。

对于第二个问题&#xff0c;一样是设置一个快指针和慢指针。慢的一次移动一个节点&#xff0c;而快的两个。

在遍历链表的时候&#xff0c;当快指针遍历完成时&#xff0c;慢指针刚好达到中点。

对于第三个问题&#xff0c;设置两个指针&#xff0c;其中一个指针先移动k个节点。之后两个指针以相同速度移动。

当那个先移动的指针遍历完成的时候&#xff0c;第二个指针正好处于倒数第k个节点。

你看&#xff0c;采用双指针方便多了吧。所以以后在处理与链表相关的一些问题的时候&#xff0c;可以考虑双指针哦。


640?wx_fmt&#61;png

巧用移位运算


有时候我们在进行除数或乘数运算的时候&#xff0c;例如n / 2&#xff0c;n / 4, n / 8这些运算的时候&#xff0c;我们就可以用移位的方法来运算了&#xff0c;这样会快很多。

例如&#xff1a;

  • n / 2等价于n >> 1&#xff1b;

  • n / 4等价于n >> 2&#xff1b;

  • n / 8等价于n >> 3。

这样通过移位的运算在执行速度上是会比较快的&#xff0c;也可以显得你很厉害的样子&#xff0c;哈哈。

还有一些&(与)、|(或)的运算&#xff0c;也可以加快运算的速度。例如判断一个数是否是奇数&#xff0c;你可能会这样做&#xff1a;

if(n % 2 &#61;&#61; 1){
  dosomething();
}

不过我们用与或运算的话会快很多。例如判断是否是奇数&#xff0c;我们就可以把n和1相与了&#xff0c;如果结果为1&#xff0c;则是奇数&#xff0c;否则就不会。即&#xff1a;

if(n & 1 &#61;&#61; 1){
  dosomething();
)

具体的一些运算技巧&#xff0c;还得需要你们多在实践中尝试着去使用&#xff0c;这样用久后就会比较熟练了。


640?wx_fmt&#61;png

设置哨兵位


在链表的相关问题中&#xff0c;我们经常会设置一个头指针&#xff0c;而且这个头指针是不存任何有效数据的&#xff0c;只是为了操作方便&#xff0c;这个头指针我们就可以称之为哨兵位了。

例如我们要删除头第一个节点是时候&#xff0c;如果没有设置一个哨兵位&#xff0c;那么在操作上&#xff0c;它会与删除第二个节点的操作有所不同。

但是我们设置了哨兵&#xff0c;那么删除第一个节点和删除第二个节点那么在操作上就一样了&#xff0c;不用做额外的判断。当然&#xff0c;插入节点的时候也一样。

有时候我们在操作数组的时候&#xff0c;也是可以设置一个哨兵的&#xff0c;把arr[0]作为哨兵。

例如&#xff0c;要判断两个相邻的元素是否相等时&#xff0c;设置了哨兵就不怕越界等问题了&#xff0c;可以直接arr[i] &#61;&#61; arr[i-1]?了。不用怕i &#61; 0时出现越界。

当然我这只是举一个例子&#xff0c;具体的应用还有很多&#xff0c;例如插入排序&#xff0c;环形链表等。


640?wx_fmt&#61;png

与递归有关的一些优化


  • 对于可以递归的问题考虑状态保存

当我们使用递归来解决一个问题的时候&#xff0c;容易产生重复去算同一个子问题&#xff0c;这个时候我们要考虑状态保存以防止重复计算。例如我随便举一个之前举过的问题

问题&#xff1a;一只青蛙一次可以跳上1级台阶&#xff0c;也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法&#xff1f;

这个问题用递归很好解决。假设f(n)表示n级台阶的总跳数法&#xff0c;则有f(n) &#61; f(n-1)&#43;f(n - 2)。

递归的结束条件是当0 <&#61; n <&#61; 2时, f(n) &#61; n。因此我们可以很容易写出递归的代码&#xff1a;

    public int f(int n) {
        if (n <&#61; 2) {
            return n;
        } else {
            return f(n - 1) &#43; f(n - 2);
        }
    }

不过对于可以使用递归解决的问题&#xff0c;我们一定要考虑是否有很多重复计算。显然对于f(n) &#61; f(n-1) &#43; f(n-2)的递归&#xff0c;是有很多重复计算的。如&#xff1a;

640?wx_fmt&#61;jpeg

就有很多重复计算了。这个时候我们要考虑状态保存。例如用hashMap来进行保存&#xff0c;当然用一个数组也是可以的&#xff0c;这个时候就像我们上面说的巧用数组下标了。

可以当arr[n] &#61; 0时&#xff0c;表示n还没计算过&#xff0c;当arr[n] !&#61; 0时&#xff0c;表示f(n)已经计算过&#xff0c;这时就可以把计算过的值直接返回回去了。因此我们考虑用状态保存的做法代码如下&#xff1a;

//数组的大小根据具体情况来&#xff0c;由于int数组元素的的默认值是0
    //因此我们不用初始化
    int[] arr &#61; new int[1000];
    public int f(int n) {
        if (n <&#61; 2) {
            return n;
        } else {
            if (arr[n] !&#61; 0) {
                return arr[n];//已经计算过&#xff0c;直接返回
            } else {
                arr[n] &#61; f(n-1) &#43; f(n-2);
                return arr[n];
            }
        }
    }

这样&#xff0c;可以极大提高算法的效率。也有人把这种状态保存称之为备忘录法。

  • 考虑自底向上

对于递归的问题&#xff0c;我们一般都是从上往下递归的&#xff0c;直到递归到最底&#xff0c;再一层一层着把值返回。

不过&#xff0c;有时候当n比较大的时候&#xff0c;例如当n &#61; 10000时&#xff0c;那么必须要往下递归10000层直到n <&#61;2才将结果慢慢返回&#xff0c;如果n太大的话&#xff0c;可能栈空间会不够用。

对于这种情况&#xff0c;其实我们是可以考虑自底向上的做法的。例如我知道&#xff1a;

  • f(1)&#61;1&#xff1b;

  • f(2)&#61;2。

那么我们就可以推出f(3)&#61; f(2)&#43;f(1)&#61;3。从而可以推出f(4),f(5)等直到f(n)。

因此&#xff0c;我们可以考虑使用自底向上的方法来做。

代码如下&#xff1a;

public int f(int n) {
        if(n <&#61; 2)
            return n;

        int f1 &#61; 1;
        int f2 &#61; 2;
        int sum &#61; 0;

        for (int i &#61; 3; i <&#61; n; i&#43;&#43;) {
            sum &#61; f1 &#43; f2;
            f1 &#61; f2;
            f2 &#61; sum;
        }
        return sum;
    }

我们也把这种自底向上的做法称之为递推。


640?wx_fmt&#61;png

总结一下


当你在使用递归解决问题的时候&#xff0c;要考虑以下两个问题。

&#xff08;1&#xff09;是否有状态重复计算的&#xff0c;可不可以使用备忘录法来优化。

&#xff08;2&#xff09;是否可以采取递推的方法来自底向上做&#xff0c;减少一味递归的开销。

今天就先讲到这里&#xff0c;之后有时间再来多谢一些其他的。如果觉得不错&#xff0c;不妨点个赞。

作者&#xff1a;帅地&#xff0c;一个热爱编程的在校生&#xff0c;我的世界不只有coding&#xff0c;还有writing。目前维护订阅号「苦逼的码农」&#xff0c;专注于写算法与数据结构、Java、计算机网络。

声明&#xff1a;本文为作者个人投稿&#xff0c;版权归其所有。

推荐阅读&#xff1a;

  • 谷歌基情录&#xff1a;TensorFlow、Hadoop、MapReduce 都靠他们诞生&#xff01;

  • 金立卒于 16 岁

  • 向 Android 4.0 彻底说再见&#xff01;

  • 数读|DApp现状揭底: 80%活不过一周; 大量游戏营收不到0.5 ETH; EOS已成"博彩链"

  • 我地铁都在努力改 Bug&#xff0c;为什么还要裁掉我&#xff1f;

  • 程序员加班很严重吗&#xff1f;看看国外程序员怎么怼老板&#xff01;

  • 程序员为啥365天都背电脑包&#xff1f;这答案我服&#xff01;

  • “男医生&#xff0c;女护士&#xff1f;”消除偏见&#xff0c;Google有大招

640?wx_fmt&#61;gif

640?wx_fmt&#61;gif



推荐阅读
  • 2018年人工智能大数据的爆发,学Java还是Python?
    本文介绍了2018年人工智能大数据的爆发以及学习Java和Python的相关知识。在人工智能和大数据时代,Java和Python这两门编程语言都很优秀且火爆。选择学习哪门语言要根据个人兴趣爱好来决定。Python是一门拥有简洁语法的高级编程语言,容易上手。其特色之一是强制使用空白符作为语句缩进,使得新手可以快速上手。目前,Python在人工智能领域有着广泛的应用。如果对Java、Python或大数据感兴趣,欢迎加入qq群458345782。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • 判断数组是否全为0_连续子数组的最大和的解题思路及代码方法一_动态规划
    本文介绍了判断数组是否全为0以及求解连续子数组的最大和的解题思路及代码方法一,即动态规划。通过动态规划的方法,可以找出连续子数组的最大和,具体思路是尽量选择正数的部分,遇到负数则不选择进去,遇到正数则保留并继续考察。本文给出了状态定义和状态转移方程,并提供了具体的代码实现。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • Android JSON基础,音视频开发进阶指南目录
    Array里面的对象数据是有序的,json字符串最外层是方括号的,方括号:[]解析jsonArray代码try{json字符串最外层是 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
author-avatar
jingjing20111201
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有