zoukankan      html  css  js  c++  java
  • 常用算法解析技巧总结

    算法是程序的灵魂。通常的程序主要是由算法与数据结构组成。算法解法千变万化,学习曲线陡,解题没有固定的模式,这些也是算法的魅力所在。在此总结一下算法的常用技巧。

    1. 巧用数组下标

       数组的下标是一个隐含的很有用的数组,特别是在统计一些数字或者判断一些整型数是否出现过的时候。例如,给你一串字母,让你判断这些字母出现的次数时,就可以把这些字母映射作为下标,在遍历的时候,如果字母a遍历到,则arr[a]就可以加1了,即  arr[a]++,得到的数组值就是字母出现的次数;

      通过这种巧用下标的方法,我们不需要遍历每个字母去判断,时间复杂度是O(n),以空间复杂度换取了时间复杂度。

    例子: 

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

      这道题,如果你是先把这 n 个数先排序,再打印,是不可能O(n)的时间打印出来的。但是数值范围在 0-20。就可以巧用数组下标。把对应的数值作为数组下标,如果这个数出现过,则对应的数组加1。

     代码如下:

    public static void orderPrint(int arr[])
        {
            int[] temp = new int[21];
            //统计出现次数
            for (int i = 0; i < arr.length; i++)
            {
                temp[arr[i]]++;
            }
            //顺序打印
            for (int i = 0; i < 21; i++)
            {
                for (int j = 0; j < temp[i]; j++)
                {
                    System.out.println(i);
                }
            }
        }

    数组下标一般用于统计场景,其他情况也可以考虑是否可以巧用数组下标来优化。

    2. 巧用取余

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

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

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

            for (int i = 0; i < N; i++)
            {
                //使用数组arr[pos]   (刚开始的时候是pos < N)
                pos = (pos + 1) % N;
            }

    3. 巧用双指针

       对于双指针,在做关于单链表的题是特别有用,比如“判断单链表是否有环”、“如何一次遍历就找到链表中间位置节点”、“单链表中倒数第 k 个节点”等问题。对于这种问题,我们就可以使用双指针了,会方便很多。我顺便说下这三个问题怎么用双指针解决吧。

    例如对于第一个问题

       我们就可以设置一个慢指针和一个快指针来遍历这个链表。慢指针一次移动一个节点,而快指针一次移动两个节点,如果该链表没有环,则快指针会先遍历完这个表,如果有环,则快指针会在第二次遍历时和慢指针相遇。

    对于第二个问题 

      一样是设置一个快指针和慢指针。慢的一次移动一个节点,而快的两个。在遍历链表的时候,当快指针遍历完成时,慢指针刚好达到中点。

    对于第三个问题

       设置两个指针,其中一个指针先移动k个节点。之后两个指针以相同速度移动。当那个先移动的指针遍历完成的时候,第二个指针正好处于倒数第k个节点。

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

    4. 巧用哈希表 哈希表最好的情况下空间复杂度可以降低到 O(1),最坏的情况仍然了 O(N)

    5. 巧用移位运算

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

      例如:

        n / 2 等价于 n >> 1

        n / 4 等价于 n >> 2

        n / 8 等价于 n >> 3。 

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

    if (n % 2 == 1)
            {
    
                //TODO
            }

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

    if ((n & 1) == 1)
            {
                //TODO
            }

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

    6. 设置哨兵位

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

       例如我们要删除头第一个节点是时候,如果没有设置一个哨兵位,那么在操作上,它会与删除第二个节点的操作有所不同。但是我们设置了哨兵,那么删除第一个节点和删除第二个节点那么在操作上就一样了,不用做额外的判断。当然,插入节点的时候也一样。

       有时候我们在操作数组的时候,也是可以设置一个哨兵的,把arr[0]作为哨兵。例如,要判断两个相邻的元素是否相等时,设置了哨兵就不怕越界等问题了,可以直接arr[i] == arr[i-1]?了。不用怕i = 0时出现越界。

       具体的应用还有很多,例如插入排序,环形链表等。

    6. 与递归有关的一些优化

    (1).对于可以递归的问题考虑状态保存

     当我们使用递归来解决一个问题的时候,容易产生重复去算同一个子问题,这个时候我们要考虑状态保存以防止重复计算。例如

     问题:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法?

     这个问题用递归很好解决。假设 f(n) 表示n级台阶的总跳数法,则有

      f(n) = f(n-1) + f(n - 2)

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

    public int leap(int n)
        {
            if (n <= 2)
            {
                return n;
            }
            else
            {
                return leap(n - 1) + leap(n - 2);
            }
        }

       不过对于可以使用递归解决的问题,我们一定要考虑是否有很多重复计算。显然对于 f(n) = f(n-1) + f(n-2) 的递归,是有很多重复计算的。这个时候我们要考虑状态保存。例如用hashMap来进行保存,当然用一个数组也是可以的,这个时候就像我们上面说的巧用数组下标了。可以当arr[n] = 0时,表示n还没计算过,当arr[n] != 0时,表示f(n)已经计算过,这时就可以把计算过的值直接返回回去了。因此我们考虑用状态保存的做法代码如下:

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

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

     (2).考虑自底向上

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

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

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

       f(1) = 1;

      f(2) = 2;

       那么我们就可以推出 f(3) = f(2) + f(1) = 3。从而可以推出f(4),f(5)等直到f(n)。因此,我们可以考虑使用自底向上的方法来做。

       代码如下:

    public int leap(int n)
        {
            if (n <= 2)
                return n;
            int f1 = 1;
            int f2 = 2;
            int sum = 0;
            for (int i = 3; i <= n; i++)
            {
                sum = f1 + f2;
                f1 = f2;
                f2 = sum;
            }
            return sum;
        }

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

      当在使用递归解决问题的时候,要考虑以下两个问题:

         (1). 是否有状态重复计算的,可不可以使用备忘录法来优化。

         (2). 是否可以采取递推的方法来自底向上做,减少一味递归的开销。

  • 相关阅读:
    delete records in table A not in table B
    图片上传5-多个图片上传,独立项目Demo和源码
    图片上传5-多个图片上传,独立项目Demo和源码
    商业模式(二):P2P网贷平台,利差和服务费为主的金融玩法
    商业模式(二):P2P网贷平台,利差和服务费为主的金融玩法
    商业模式(一):北京秒针公司,以互联网广告监测服务为主的生财之道
    使用DrawerLayout实现侧拉菜单
    关于ComponentName的使用
    商业模式系列-序
    小的时候.by小雷
  • 原文地址:https://www.cnblogs.com/zengming/p/10853641.html
Copyright © 2011-2022 走看看