算法笔记:将数组按符号排序
题目:
给定一个int数组,长度为n,数组中每个元素为随机整数,可能为负数,可能为0,可能为正数,要求将数组按照符号排序,所有的负数在左边,正数在右边,零在中间,负数和负数之间不需要有序,正数和正数之间也不需要有序。
数据约束:
0 < n <= 300000000
例子:
输入:
[0, 3, 5, -10, -1]
输出:
下面任意一个都是合法输出:
[-10, -1, 0, 3, 5]
[-1, -10, 0, 3, 5]
[-10, -1, 0, 5, 3]
[-1, -10, 0, 5, 3]
这道题中的难点是出现了三种类型的数值,不能简单地采用两个指针从两端向中间收缩的遍历方式,除非使用额外的空间(这个空间还不是临时的,而是在函数执行完毕仍然可能被占用的)。
即新申请一个长度为n的int数组,因为int数组中元素默认值为0,相当于不用考虑0的情况,将三种情况转为了两种情况,就比较容易处理了,只需要将原数组中的值按照负数放在新数组的左端,正数放在新数组的右端即可,这种方式时间复杂度为O(n),只需要遍历一遍数组即可,但是需要额外的至少4n字节的内存空间。
实现代码如下:
package org.cc11001100.alg.sortBySymbol; /** * 需要额外空间的按符号排序,时间复杂度是O(n),但是需要额外的内存空间n * * @author CC11001100 */ public class SortBySymbolSolutionNeedMoreMemory implements SortBySymbol { // 拷贝到一个新的数组中 public int[] sortBySymbol(int[] nums) { int[] result = new int[nums.length]; int left = 0, right = result.length - 1; for (int i = 0; i < nums.length; i++) { if (nums[i] < 0) { result[left++] = nums[i]; } else if (nums[i] > 0) { result[right--] = nums[i]; } } return result; } public static void main(String[] args) { SortBySymbol sortBySymbol = new SortBySymbolSolutionNeedMoreMemory(); // int[] nums = RandomIntArrayGenerator.random(100000000); // long start = System.currentTimeMillis(); // nums = sortBySymbol.sortBySymbol(nums); // long cost = System.currentTimeMillis() - start; // System.out.println(cost); // System.out.println(SortBySymbolJudge.judge(nums)); // for (int i = 0; i < nums.length; i++) { // System.out.println(nums[i]); // } SortBySymbolJudge.judge(sortBySymbol); } }
上面的代码只是实现了基本的要求,我们肯定能够做到更好对吧,对于用到了额外空间的解法,我们应该想一下是否可以不使用额外空间就解决这个问题呢?
上面说了,这个问题复杂就复杂在要在一个数组中操纵三种类型的数据,指针不够用,那么我们可以将这个问题转化一下,比如可以将问题分为两步:
1. 先按照负数和非负数(包括零和正数)进行排序,这一步将负数的顺序排好了,但是零和正数还是无序的。
2. 再按照零和正数排序,排完之后整体有序了。
这样每一步都是两种类型的数据,使用两个指针完全够用了,而且不需要使用到额外的内存了,缺点就是需要多遍历一遍数组,时间复杂度是O(2n),约掉常数复杂度仍然是O(n)。
代码实现:
package org.cc11001100.alg.sortBySymbol; /** * 不需要额外空间,但是需要扫描两次数组,时间复杂度是O(2n),但是不需要额外空间 * * @author CC11001100 */ public class SortBySymbolSolutionTwoTimesSort implements SortBySymbol { // 将整个排序过程看做是两个步骤,先按负数和非负数排序,然后将非负数部分按照零和正数排序 public int[] sortBySymbol(int[] nums) { int left = negativeAndNotNegativeSort(nums); zeroAndPositiveSort(nums, left); return nums; } // 负数和非负数排序 private int negativeAndNotNegativeSort(int[] nums) { int left = 0, right = nums.length - 1; while (left <= right) { if (nums[left] >= 0) { right = findFirstNegativeIndexFromRight(nums, right); if (right < left) { break; } swap(nums, left, right); left++; right--; } else { left++; } } return left; } private int findFirstNegativeIndexFromRight(int[] nums, int right) { while (right >= 0) { if (nums[right] < 0) { return right; } right--; } return -1; } private void swap(int[] nums, int a, int b) { int t = nums[a]; nums[a] = nums[b]; nums[b] = t; } // 零和正数排序 private void zeroAndPositiveSort(int[] nums, int left) { int right = nums.length - 1; while (left <= right) { if (nums[left] != 0) { right = findFirstZeroIndexFromRight(nums, right); if (right < left) { break; } swap(nums, left, right); left++; right--; } else { left++; } } } private int findFirstZeroIndexFromRight(int[] nums, int right) { while (right >= 0) { if (nums[right] == 0) { return right; } else { right--; } } return -1; } public static void main(String[] args) { SortBySymbol sortBySymbol = new SortBySymbolSolutionTwoTimesSort(); // int[] nums = RandomIntArrayGenerator.random(100000000); // long start = System.currentTimeMillis(); // sortBySymbol.sortBySymbol(nums); // long cost = System.currentTimeMillis() - start; // System.out.println(cost); // System.out.println(SortBySymbolJudge.judge(nums)); // for (int i = 0; i < nums.length; i++) { // System.out.println(nums[i]); // } SortBySymbolJudge.judge(sortBySymbol); } }
写一个类测试代码是否正确,首先为了测试方便将两个类的类型统一:
package org.cc11001100.alg.sortBySymbol; public interface SortBySymbol { int[] sortBySymbol(int[] nums); }
然后是生成测试数据的类:
package org.cc11001100.alg.sortBySymbol; /** * 用于生成测试数据 * * @author CC11001100 */ public class RandomIntArrayGenerator { public static int[] random(int length) { int[] result = new int[length]; for (int i = 0; i < result.length; i++) { int n = (int) (Math.random() * 100); int t = n % 5; if (t < 2) { // n ∈ [0, 1] n *= -1; } else if (t == 2) { // n == 2 n = 0; } else { // n ∈ [3, 4] // n = n; } result[i] = n; } return result; } public static void main(String[] args) { int[] nums = random(20); for (int i = 0; i < nums.length; i++) { System.out.println(nums[i]); } } }
然后是测评类:
package org.cc11001100.alg.sortBySymbol; /** * 用于评测SortBySymbol输出结果是否正确 * * @author CC11001100 */ public class SortBySymbolJudge { public static boolean judge(int[] nums) { int index = 0; if (nums[index] < 0) { while (index < nums.length) { if (nums[index] >= 0) { break; } index++; } } if (index < nums.length && nums[index] == 0) { while (index < nums.length) { if (nums[index] > 0) { break; } index++; } } while (index < nums.length) { if (nums[index] <= 0) { return false; } index++; } return index == nums.length; } public static void judge(SortBySymbol sortBySymbol) { int[] numLengths = new int[]{ 1, 2, 3, 5, 10, 1000, 10000, 100000, 1000000, 10000000, 100000000, 100000000, 100000000, 200000000, 200000000, 200000000, 300000000 }; for (int i = 0; i < numLengths.length; i++) { int[] nums = RandomIntArrayGenerator.random(numLengths[i]); long start = System.currentTimeMillis(); nums = sortBySymbol.sortBySymbol(nums); long cost = System.currentTimeMillis() - start; boolean judgeResult = judge(nums); System.out.println("num length=" + numLengths[i] + ", cost=" + cost + "ms, judge=" + judgeResult); } } }
测评结果:
可以看到,对于使用额外内存的解法当数据量足够大时它就撑不住了,很多问题就是这样,没有CPU资源可以跑慢一点,终究还是能够跑起来的,但是没有内存资源根本跑都跑不起来。
.