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

190 阅读3分钟

题目

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

举例:
原数组:[1, 7, -5, 2, -9, 3]
排列后的数组:[-5, -9, 1, 7, 2, 3]
排列后的数组的所有负数位于左边,正数位于右边,且没有改变正数和负数在原数组中的相对位置。

解题思路

这道题是拼多多一面问的问题。一开始我想到的是通过冒泡排序的方式,将负数一个个往前冒,时间复杂度是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

截图了翻手算法文章中核心算法逻辑部分:

代码实现:
其实下面的这个代码实现时间复杂度 O(nlogn),但是空间复杂度是 O(logn),divide 方法递归时用到了临时变量,divide 递归了 logn 层,所以空间复杂度就是 O(logn)。如果大家有更好的解题思路欢迎评论。

public class Solution {

    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--;
        }
    }
}