排序算法与直觉

一般来说,我们首先学到的算法就是排序。排序是很典型的场景具化的需求,即现实中我们需要排序,编程时就进行对应的排序,简称模拟。比如学生早操排队、分数成绩排名,都是最简单的排序。也因来源于生活场景,所以将生活的直觉转化为代码操作,也就有了最初的排序算法。起泡排序,来源于最无脑的一一比较排列;选择or插入排序,来源于我们打扑克的洗牌以及变形;归并排序,很像小组先排序,然后班内排序,之后年级排序,最后全校排序。这些排序算法,都很符合生活的直觉,因此,这些算法对初学者来说很容易理解和掌握。
另一类排序算法则反生活直觉,如快速排序、堆排序。快速排序要求两个指针向中间不停移动,堆排序需要构建大(小)顶堆,这些操作,与上一段提到的几种排序相反,你很难向一般的非编程人员描述清楚并指望他们理解。仔细想想,为何上一段的几种算法,他们会容易理解?无非是他们建立了生活常识,若对无生活常识的人,比如一个未入学的孩童,他未必能理解很简单的起泡排序。对于软件工程师来说,双指针、树(堆)也就是常识,也就是编程的常识。有了这些常识,遇到问题才会产生编程的直觉。
我们不断的学习知识,就是为了培养我们的直觉。

三数之和

15. 三数之和 是一个较能反应编程直觉的题目,有经验的工程师应当能反应出用双指针可以快速解决;同理,他应当反应出双指针处理这类问题的基础是数据有序,这样可以固定一侧然后推动左右两个指针夹逼的同时,亦可进行剪枝。

public List<List<Integer>> threeSumsOptimize(int[] nums) {

      List<List<Integer>> lists = new ArrayList<>();
      if (nums.length < 3) return lists;
      Arrays.sort(nums);
      if (nums[0] > 0) return lists;//最小值肯定要小于等于=0
      int n = nums.length, left, right;
      for (int i = 0; i < n - 1; i++) {
          if (i > 0 && nums[i] == nums[i - 1]) continue;//排序+比较来去重复

          left = i + 1;
          right = n - 1;
          while (right > left) {

              if (nums[i] + nums[left] + nums[right] == 0) {
                  List<Integer> integers = new ArrayList<>();
                  integers.add(nums[i]);
                  integers.add(nums[left]);
                  integers.add(nums[right]);
                  lists.add(integers);
                  while (right > left && nums[left] == nums[left + 1]) left++;
                  while (right > left && nums[right] == nums[right - 1]) right--;

                  left++;
                  right--;
              } else if (nums[i] + nums[left] + nums[right] > 0) right--;
              else left++;
          }


      }

      return lists;

  }

四数之和

18. 四数之和 是上一个题目的升级版本。做过上一个题目,产生的直觉进行加强,左右两端固定,中间两个指针推动,推动结束再移动右侧,右侧移动结束,再移动左侧,实现四轴运转:

  class Solution:
      def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
          lists = []
          lens = len(nums)
          if lens < 4:
              return lists
          nums.sort()
          for i in range(0, lens - 3):
              if nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target:
                  break
              if i > 0 and nums[i] == nums[i - 1]:
                  continue
              j = lens - 1
              while j > 2:
                  if j < lens - 1 and nums[j] == nums[j + 1]:
                      j = j - 1
                      continue
                  left = i + 1
                  right = j - 1
                  while left < right:
                      sum = nums[i] + nums[j] + nums[left] + nums[right]
                      if sum == target:
                          lists.append([nums[i], nums[j], nums[left], nums[right]])
                          while nums[left] == nums[left + 1] and left < right:
                              left = left + 1
                          left = left + 1
                          while nums[right] == nums[right - 1] and left < right:
                              right = right - 1
                          right = right - 1
                      elif sum < target:
                          left = left + 1
                      else:
                          right = right - 1
                  j = j - 1
          return lists

这类题目用python做有天然优势, lists.append([nums[i], nums[j], nums[left], nums[right]]) 这行代码用java需要写得极为繁琐

番外篇之最接近的三数之和

16. 最接近的三数之和 是三数之和的变形,融合了dp的思想。

  class Solution {
      public int threeSumClosest(int[] nums, int target) {
          int len = nums.length;
          int min = Integer.MAX_VALUE;
          Arrays.sort(nums);
           int sum,left,right,temp,current = Integer.MAX_VALUE;
          for(int i=0;i<len;++i){
              if(i>0 && nums[i]==nums[i-1])continue;
  
              left = i+1;right = len-1;
              while(left<right){
                  sum = nums[i] + nums[left] + nums[right];
                  if(sum==target)return target;
                  temp = Math.abs(sum - target);
                  if(temp< current){
                      current = temp;
                      min = sum;
                  }
                  if(sum <target){
                         while(left<right &&nums[left+1]==nums[left])left++;
                  left++;
                  } else{
                      while(left<right &&nums[right-1]==nums[right])right--;
                      right--;
                  }
              }
          }
          return min;
  
      }
  }

番外篇之存在重复元素 III

220. 存在重复元素 III 这个题目反而更像两数之和,不过对数据结构更加依赖,Treemap满足了可以快速查找到高于目标值(set.floor)和低于目标值(set.ceiling)的最近的数据,而set本身也是比较好的维护滑动窗口的数据结构

 public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
     TreeSet<Long> set = new TreeSet<>();
     int len = nums.length;
     for (int i = 0; i < len; i++) {

         Long  floor = set.floor((long)nums[i] + (long)t);//防止溢出

         if(floor!=null &&  floor >= ((long)nums[i] - (long)t))
             return true;

         set.add((long)nums[i]);

         if(i>=k){//用这种下标方式删除来维护滑动窗口的效果更好
             set.remove((long)nums[i-k]);
         }


     }

     return false;


 }

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