一些可让你装逼、让人眼前一亮的算法技巧总结

今天和你们讲讲,在作算法题时经常使用的一些技巧。对于平时没用过这些技巧的人,或许你能够考虑试着去看看在实践中可否用的上这些技巧来优化问题的解,相信必定会让你有所收获,否则你看我。算法

1. 巧用数组下标

数组的下标是一个隐含的颇有用的数组,特别是在统计一些数字,或者判断一些整型数是否出现过的时候。例如,给你一串字母,让你判断这些字母出现的次数时,咱们就能够把这些字母做为下标,在遍历的时候,若是字母a遍历到,则arr[a]就能够加1了,即 arr[a]++;数组

经过这种巧用下标的方法,咱们不须要逐个字母去判断。bash

我再举个例子:工具

问题:给你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 = 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);
           }
       }
   }
复制代码

利用数组下标的应用还有不少,你们之后在遇到某些题的时候能够考虑是否能够巧用数组下标来优化。ui

2. 巧用取余

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

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 个节点”等问题。对于这种问题,咱们就可使用双指针了,会方便不少。我顺便说下这三个问题怎么用双指针解决吧。code

例如对于第一个问题

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

对于第二个问题

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

对于第三个问题

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

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

4. 巧用移位运算。

有时候咱们在进行除数或乘数运算的时候,例如n / 2,n / 4, n / 8这些运算的时候,咱们就能够用移位的方法来运算了,这样会快不少。

例如:

n / 2 等价于 n >> 1

n / 4 等价于 n >> 2

n / 8 等价于 n >> 3。

这样经过移位的运算在执行速度上是会比较快的,也能够显的你很厉害的样子,哈哈。

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

if(n % 2 == 1){
 dosomething();
}
复制代码

不过咱们用与或运算的话会快不少。例如判断是不是奇数,咱们就能够把n和1相与了,若是结果为1,则是奇数,不然就不会。即

if(n & 1 == 1){
 dosomething();
)
复制代码

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

5. 设置哨兵位

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

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

有时候咱们在操做数组的时候,也是能够设置一个哨兵的,把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 f(int n) {
       if (n <= 2) {
           return n;
       } else {
           return f(n - 1) + f(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 f(int n) {
       if (n <= 2) {
           return n;
       } else {
           if (arr[n] != 0) {
               return arr[n];//已经计算过,直接返回
           } else {
               arr[n] = f(n-1) + f(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 f(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). 是否能够采起递推的方法来自底向上作,减小一味递归的开销。

若是你以为这篇内容对你挺有启发,那么你能够:

一、点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)

二、*关注我,让咱们成为长期关系。

三、关注公众号「苦逼的码农」,里面已有100多篇原创文章,我也分享了不少视频、书籍的资源,以及开发工具,欢迎各位的关注,第一时间阅读个人文章。

相关文章
相关标签/搜索