各种旋转

经常有人诟病leetcode为小学奥数,这一指责至少在很多类似脑筋急转弯的题目上是有道理的。利用数据结构和算法解题的题目,往往可以锻炼工程能力以及编码水平。而脑经急转弯类的题目,唯一的用途是让人看完题解后拍大腿。不过这类题目也不是一无是处,在cv之后,的确可以跳出常规思维的圈套。

字符串轮转

我愿意把面试题 01.09. 字符串轮转 拿来做第一个批判和学习的对象。这个题目通常的解法,恐怕要进行n^2的两次循环来暴力解决。可以巧妙的解法则是把字符串1累加(s1+s1),再判断是否包含字符串2:

       public boolean isFlipedString(String s1, String s2) {
           int len1 = s1.length(),len2 = s2.length();
           if(len1!=len2)return false;
           s1+=s1;
           return s1.contains(s2);
   
       }

金风玉露一相逢

其次是 160. 相交链表 II ,这个题目的思路更是诡谲。用常规思路也是让人抓狂。跳出常规思路则很简单。根据 a + all + b = b+ all + a 的小学数学即可解决:

       public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
               ListNode temp1 = headA, temp2 = headB;
               while (temp1 != temp2) {
                   if (temp1 != null) temp1 = temp1.next;//注意这里,要判断temp1,而不是temp1.next,否则可能会死循环
                   else temp1 = headB;
                   if (temp2 != null) temp2 = temp2.next;
                   else temp2 = headA;
               }
               return temp1;
           }

交接重任

面试题 04.01. 节点间通路 ,这个题目的常规解法是回溯法,然后回溯法的一个缺乏是无法提前停止,解题耗时35ms,代码量很大。换成另一种解法,先找终点,找到后,把起点换做终点继续找,直到起点和初始点一致即可。代码量极少,且效率提升到3ms:

           public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target) {
               for(int[] g:graph)   
                    if(g[1]==target && g[0]!=target)
                          if(g[0]==start || findWhetherExistsPath(n,graph,start,g[0]))return true;
               return false;
           }

开心消消乐

面试题 17.05. 字母与数字 ,这个题目用常规的前缀和显然会超时。前缀和的优化思路是加上hash,怎么加hash则是个大学问。这个题目,可以判断字母,遇到字母,总数+1,遇到数字,总数-1。那么任何两个总数相同的区间内部,数字和字母的数量必然相等。这种精巧的思路,不得不让人点赞!代码如下:

           public String[] findLongestSubarray(String[] array) {
                   int left = 0;
                   int ans = 0;
                   int len = array.length;
                   Map<Integer,Integer> dict = new HashMap<>();
                   dict.put(0,-1);
                   int total = 0;
                   for (int i = 0; i < len; ++i) {
                       String cur = array[i];
                       if(Character.isDigit(cur.charAt(0))){
                           total++;
                       }else total--;
                       int prev = dict.getOrDefault(total,Integer.MAX_VALUE);
                       if(prev!=Integer.MAX_VALUE){
                           if(i-prev>ans){
                               ans = i-prev;
                               left = prev + 1;
                           }
                           
                       }else dict.put(total,i);
                       
                     
                   }
           
                   String[] ans_new = new String[ans];
                   System.arraycopy(array, left, ans_new, 0, ans);
           
                   return ans_new;
           
               }

525.连续数组 这个题目算是这类题的母题

旋转、旋转、再旋转

189. 旋转数组 ,这个题目是旋转数组类题目的一个基础题目,比较巧妙的解法是先整体旋转整个数组,再旋转k左边,然后旋转k右边,完全跳出传统思维:

           public void rotate(int[] nums, int k) {
                   int len = nums.length;
                   k%=len;
                   if(k==0)return;
           
                   rotate(nums,0,len-1);//整体旋转  123456789 ->987654321
                   rotate(nums,0,k-1);//左边旋转,相当于转回去 ->789 654321
                   rotate(nums,k,len-1);//右边旋转,相当于转回去 ->789 123456
               }
           
           
               public void rotate(int[] nums, int left,int right) {
                   int temp;
                   while(left<right){
                       temp = nums[left];
                       nums[left] = nums[right];
                       nums[right] = temp;
                       ++left;
                       --right;
           
                   }
               }

先交换再旋转

31. 下一个排列 ,这个题目是看起来跟旋转无关,实际上可以先找到间断点(左小右大),然后把左小的右侧全部旋转。注意,右大是找到准确的右,因为右边本身是从右向左升序的,所以找到第一个大于左边的截止即可,以下用golang来实现:

           func nextPermutation(nums []int)  {
    length:= len(nums)
    if length == 1{
        return
    }
    i:=length - 2
    for i>=0{
        if nums[i] < nums[i+1] {
            break;
        }
        i--
    }

//关键的一步,找到右侧第一个大于左小的数字
    if i >=0 {
        j:= length - 1
        for j>=0{
            if nums[i]<nums[j]{
                break;
            }
            j--
        }
        swap(nums,i,j)

    }

    reverse(nums,i+1,length-1)

}

func swap(nums []int,i int,j int){
    temp:= nums[i]
    nums[i] = nums[j]
    nums[j] = temp
 }

 func reverse(nums []int,left int,right int){
     for left < right{
         swap(nums,left,right)
         left++
         right--
     }
 }



差分之妙用

1109. 航班预订统计 ,这个题目可以用蛮力两轮循环来解决,效率比较低:

                  int[] ans = new int[n];

        for (int[] booking : bookings) {
            for (int i = booking[0]; i <= booking[1]; i++) {
                ans[i - 1] += booking[2];
            }
        }
        return ans;

另一种解决思路是差分,或者说是坐差的逆操作:

        int[] nums = new int[n];
        for (int[] booking : bookings) {
            nums[booking[0] - 1] += booking[2];
            if (booking[1] < n) {
                nums[booking[1]] -= booking[2];
            }
        }
        for (int i = 1; i < n; i++) {
            nums[i] += nums[i - 1];
        }
        return nums;

Is this article useful to you? How about buy me a coffee ?