不改变相对顺序,负数左边正数右边
题目
给定一个只包含正数和负数的数组,不改变正数之间的相对顺序,以及负数之间的相对顺序,重新排列数组,使得负数位于正数之前。
举例:如:[1, 7, -5, 2, -9, 3] 变成 [-5, -9, 1, 7, 2, 3] 使得所有负数位于左边,正数位于右边,且没有改变正数,以及负数在原始数组中的相对位置。
解题思路
这道题是拼多多 1 面问的问题。一开始我想到的是通过冒泡排序的方式,将负数一个个往前冒,时间复杂度是 O(n2)。
后来面试官要求解题方法的时间复杂度是 O(nlogn),空间复杂度是 O(1),这个就把我难倒了,但是从时间复杂度上我猜测到应该是用归并或者快排的思路去做,但是归并的空间复杂度是O(n),所以我就想用快排,但是快排不是稳定O(nlogn),而且快排来做也没有思路。
面试官提示用归并来实现,但是我没有想出来怎么能让 merge 操作不用到额外的空间。后来我线下去看了下解题方案,没有现成的答案,但是我找到了一篇将关于《翻手算法》的文章,利用线性代数的转置思路求解,可以实现时间复杂度O(n),空间复杂度O(1)。
翻手算法的思路是这样的,举例,比如我们要把数组[1,2,3,4,5],转换成[4,5,1,2,3],也就是将 4,5和 1,2,3换一下位置。算法步骤:
步骤 1:将 1,2 转置下,变为 2,1。数组变为:[2,1,3,4,5]
步骤 2:将 3,4,5 转置下,变为5,4,3。数组变为:[2,1,5,4,3]
步骤 3:将整个数组转置下,就变为:[3,4,5,1,2]
转置的意思就是将对应的数前后颠倒下。1,2 → 2,1 3,4 → 4,3 1,2,3 → 3,2,1 1,2,3,4 → 4,3,2,1
更详细的解释可以看下这篇文章:https://blog.csdn.net/KeepThinking_/article/details/8771873,截图了文章中核心部分。
代码实现
`
public static void main(String[] args) {
int[] nums = {1, 7, -5, 2, -9, 3};
divide(nums, 0, nums.length - 1);
System.out.println(Arrays.toString(nums));
}
/**
* 归并算法的分治思想,将 nums 数组拆分成两部分,然后分别对两部分进行归并。
* divide 方法的空间复杂度为 O(1),时间复杂度为 O(logn)
*/
public static void divide(int[] nums, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
divide(nums, left, mid);
divide(nums, mid + 1, right);
merge(nums, left, mid, right);
}
}
/**
* 将归并后的部分,两两比较翻转。
* merge 方法的空间复杂度为 O(1),时间复杂度为 O(n)
*/
public static void merge(int[] nums, int left, int mid, int right) {
// 找到第1部分的第一个正数位置
int leftPoint = -1;
for (int i = left; i < mid + 1; i++) {
if (nums[i] > 0) {
leftPoint = i;
break;
}
}
// leftPoint == -1 表示第1部分全是负数,那么就不用处理了,天然满足左负右正。
boolean isAllNegative = leftPoint == -1;
if (isAllNegative) {
return;
}
// 找到第2部分最后一个负数位置
int rightPoint = -1;
for (int i = mid + 1; i <= right; i++) {
if (nums[i] < 0) {
rightPoint = i;
}
}
// rightPoint == -1 表示第2部分全是正数,那么就不用处理了,天然满足左负右正。
boolean isAllPositive = rightPoint == -1;
if (isAllPositive) {
return;
}
// 翻转第1部分的正数部分
turnHand(nums, leftPoint, mid);
// 翻转第2部分的负数部分
turnHand(nums, mid + 1, rightPoint);
// 翻转第1部分的正数部分和第2部分的负数部分
turnHand(nums, leftPoint, rightPoint);
}
/**
* 翻手,反转数组,调换数组中元素的前后位置
*/
public static void turnHand(int[] nums, int startPoint, int endPoint) {
while (startPoint < endPoint) {
int temp = nums[startPoint];
nums[startPoint] = nums[endPoint];
nums[endPoint] = temp;
startPoint++;
endPoint--;
}
}