【面试】leetcode一题多解之颜色分类

312 阅读5分钟

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg

这是leetcode面试刷题一题多解系列的第二篇,大家一起来做一个荷兰国旗问题,也是一个限定范围的排序问题,帮助大家练习数组原地的移动操作,加强大家对于指针以及对于循环不变量的理解,对于编码的边界处理亦会有很大的帮助。

题目

今天跟大家一起看一道经典的 leetcode 算法题,颜色分类。

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。 必须在不使用库内置的 sort 函数的情况下解决这个问题。

来源:力扣(LeetCode) 链接:leetcode.cn/problems/so… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

题解1---统计排序

这是最简单也是最直接的一种思路,就是找创建一个大小为3的空数组,用来存储统计原数组0,1,2的个数,再循环遍历该数组,将原数组各位上依次放置对应的数。时间复杂度是 O(N),空间复杂度 O(1)。

const sortColors = function (nums) {
  // tatalArray = [0, 0, 0]
  let len = nums.length, totalArray = Array.from({length:3}, _ => 0);
  // 分别统计0,1,2的个数
  for (let i = 0; i < len; i++) {
    totalArray[nums[i]] += 1
  }
  // 每个数字再数组nums的起始位置
  let start = 0;
  // 循环totalArray的每一项,对nums进行赋值
  for(let i = 0; i < totalArray.length; i ++) {
    // 每个数字再nums的结束位置
    let end = totalArray[i] + start;
    // 依次赋值对应的i值
    while(start < end) {
      nums[start] = i;
      start++;
    }
  }
  return nums;
}

题解2---单指针法

从题解可知,数字只有0,1,2如果将0移动到最前面,1移动到0后面,那么剩下的自然就是2了,所以我们需要一个指针来标识放置0和放置1的位置,然后在数组中原地移动元素(不需要另外开辟空间),自然就可以进行排序了。时间复杂度 O(N) ,空间复杂度O(1)。

 var sortColors = function (nums) {
   // 本题解的核心,用来标识交换的位置
   let pr = 0,
     len = nums.length;
   // 循环数组,将0全部移动到最前面
   for (let i = 0; i < len; i++) {
     if (nums[i] === 0) {
       [nums[i], nums[pr]] = [nums[pr], nums[i]]
       pr++
     }
   }
   // 从pr开始,再讲所有的1移动到0的后面
   for (let j = pr; j < len; j++) {
     if (nums[j] === 1) {
       [nums[j], nums[pr]] = [nums[pr], nums[j]]
       pr++
     }
   }
   // 以上操作结束,排序完成
   return nums;
 }

题解3---双指针法

单指针法排序对数组分别进行了两次遍历,我们能否进行一次遍历就将数组排好序呢?因为是有3数字的数组,如果我们想要在一次遍历的过程中将0,1,2排序,就需要两个指针,依次标识0与1的边界和1与2的边界时间复杂度 O(N) ,空间复杂度O(1)。

 var sortColors = function (nums) {
   // 设置变量zero和two分别标识放置0和1,2和1的边界[0,zero]是0的初始区间,
   // [nums.length,nums.length-1]是2的初始区间,刚开始均没有值
   let zero = -1,
     two = nums.length,
     i = 0;
   // 终止条件,two为2的最左边位置
   while (i < two) {
     // 遍历如果是0,则zero指针右移,然后i和zero交换位置,zero指向下一个和0交换的位置
     if (nums[i] === 0) {
       zero++;
       [nums[i], nums[zero]] = [nums[zero], nums[i]];
       i++;
     // 如果等于1,i++即可,不用做其他处理
     } else if (nums[i] === 1) {
       i++;
     } else {
       //如果是2,two指针左移,然后i和zero交换位置,zero指向下一个和2交换的位置
       two--;
       [nums[i], nums[two]] = [nums[two], nums[i]];
     }
   }
   return nums;
 };

上面代码,左右两个指针分别去标识0的区域和2的区域,剩下中间的是1的区间,不断的移动两个指针就将原数组进行排序。

题解4---双指针法

这是题解3的另外一种写法,思想和题解3完全一致,就是做了一些细微的调整,将边界条件修改了下,while改成for循环,目的是进行编码的练习,提供给大家以供参考,同样是时间复杂度 O(N) ,空间复杂度O(1)。

var sortColors = function (nums) {
   let length = nums.length;
   // 对于边界条件进行了修改[0,zero)是0的区间,(two, length-1]是2的区间,
   // 同样初始都没有值,注意和题解3作对比,只就是常说的循环不变量
   let zero = 0,
     two = length - 1;
  // 用了for循环,i自然加1,所以就不需要对等于1进行额外的处理  
   for (let i = 0; i <= two; ++i) {
     if ( nums[i] == 2) {
       [nums[i], nums[two]] = [nums[two], nums[i]];
       --two;
     }
     // 注意这里没有用else,因为上面和nus[two]进行了交换后的值如果是0则依然需要处理
     if (nums[i] == 0) {
       [nums[i], nums[zero]] = [nums[zero], nums[i]];
       ++zero;
     }
   }
   return nums;
 };

题解4是对于题解3的另外一种实现,重点是为了说明一个在编码的过程中非常重要的知识点,那就是循环不变量,就是你在初始定义了变量的含义,那么在整个过程中都要保证已确定变量的含义是不能变的,两种题解指针的初始值是不同的例如,在题解三中[0,zero]是0的初始区间,[nums.length,nums.length-1]是2的初始区间,而在题解四中[0,zero) 是0的区间,(two, length-1]是2的区间但是你只要保证了循环不变量,最终的结果都是正确的。