不改变相对顺序,负数左边正数右边

题目

给定一个只包含正数和负数的数组,不改变正数之间的相对顺序,以及负数之间的相对顺序,重新排列数组,使得负数位于正数之前。

举例:如:[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--;
    }
}