Skip to content

题目描述

Wiggle Sort II

Given an integer array nums, reorder it such that nums[0] < nums[1] > nums[2] < nums[3]....

You may assume the input array always has a valid answer.

Follow Up: Can you do it in O(n) time and/or in-place with O(1) extra space?

Example 1:

bash
Input: nums = [1,5,1,1,6,4]
Output: [1,6,1,5,1,4]
Explanation: [1,4,1,5,1,6] is also accepted.

Example 2:

bash
Input: nums = [1,3,2,2,3,1]
Output: [2,3,1,3,1,2]

方法一:排序+双指针

先把数组进行排序,然后再通过双指针进行逐个插入。

这里需要思考的是怎么插入。我们可以将排序好的数组分成两份:前面一半较小的以及后面一半较大的。然后将较大的一半插入到奇数下标中,较小的一半插入偶数下标中,这样一定能保证Wiggle Sort。也就是说奇数下标的数据一定小于等于中位数,偶数下标的数据一定大于中位数!!!

需要注意的是,如果总共有奇数个,中位数的下标在(nums.length - 1) / 2,如果有偶数个,中位数较小的那个数也在(nums.length - 1) / 2。所以左指针取left= (nums.length - 1) / 2开始递减,右指针就取最后一个开始递减。

代码

c
class Solution {
public:
    void wiggleSort(vector<int>& nums) {
        vector<int> tmp = nums;
        sort(tmp.begin(),tmp.end());
        int n = nums.size();
        int l = (n - 1)/2, r = n -1;
        for(int i = 0;i<n;++i){
            if(i%2==0){
                nums[i] = tmp[l--];
            }else{
                nums[i] = tmp[r--];
            }
        }
    }
};

复杂度分析

  • 时间复杂度:\(O(nlogn)\),排序所需的时间复杂度是\(O(nlogn)\),插入\(O(n)\),整体\(O(nlogn)\)
  • 空间复杂度:\(O(n)\),需要额外的空间存放排序的元素。

方法二:桶排序

我们可以把所有的元素分桶然后逐个取出。由于数据大小最大是5000,我们可以开5001个桶来存放元素。然后从大到小依次放回原数组。

同样的思路,我们需要在奇数下标插入比中位数小的数,偶数下标插入比中位数大的数。但是这一次我们不知道中位数在桶中的下标。没关系,我们可以从大到小先每隔一位插一个偶数下标的桶,插完后自然剩下的就是小于等于中位数的值,再从大到小插到奇数下标处。

代码

c
public void wiggleSort(int[] nums) {
  //5001个桶
  int[] bucket = new int[5001];
  for (int num : nums) {
  	bucket[num]++;
  }
  int j = 5000;
  //插空放 较大元素
  for (int i = 1; i < nums.length; i += 2) {
  	while (bucket[j] == 0) {//不是每一个桶都有数,用while过滤
      j--;
  	}
    nums[i] = j;
    bucket[j]--;
  }
  //插空放 较小元素
  for (int i = 0; i < nums.length; i += 2) {
  while (bucket[j] == 0) {
    j--;
  }
  nums[i] = j;
  bucket[j]--;
  }
}

复杂度分析

  • 时间复杂度:\(O(n)\),装桶\(O(n)\),插入\(O(n)\),整体\(O(n)\)
  • 空间复杂度:\(O(c)\),需要额外的桶空间,这里c = 5001

方法三:快选 + 三数排序

本质上,题目要我们实现一种构造方法,能够将 nums 调整为满足「摆动」要求。

具体的构造方法:

  1. 找到 nums 的中位数,这一步可以通过「快速选择」算法来做,时间复杂度为 \(O(n)\),空间复杂度为 \(O(\log{n})\),假设找到的中位数为 x;
  2. 根据 \(nums[i]\)与 x 的大小关系,将 \(nums[i]\)分为三类(小于/等于/大于),划分三类的操作可以采用「三数排序」的做法,复杂度为 \(O(n)\)

这一步做完之后,我们的数组调整为:\([a_1, a_2, a_3, ... , b_1, b_2, b_3, ... , c_1, c_2, c_3]\),即分成「小于 x / 等于 x / 大于 x」三段。

  1. 构造:先放「奇数」下标,再放「偶数」下标,放置方向都是「从左到右」(即可下标从小到大进行放置),放置的值是则是「从大到小」

到这一步之前,我们使用到的空间上界是 \(O(\log{n})\),如果对空间上界没有要求的话,我们可以简单对 nums 进行拷贝,然后按照对应逻辑进行放置即可,但这样最终的空间复杂度为 \(O(n)\)(代码见 P2);如果不希望影响到原有的空间上界,我们需要额外通过「找规律/数学」的方式,找到原下标和目标下标的映射关系(函数 getIdx 中)。

容易证明该构造过程的正确性(即该构造过程必然能顺利进行):由于我们是按照值「从大到小」进行放置,如果构造出来的方案不合法,必然是相邻的两个值为相等(“应当递增实际递减”或者“应当递减实际递增”的情况已被「从大到小」进行放置所否决),而当相邻位置放置了相同的值,即存在某个奇数下标,以及其相邻的偶数下标都放置了相同的值,这等价于该值出现次数超过总个数的一半,这与「题目本身保证数据能够构造出摆动数组」所冲突。

代码

快速选择算法可以用递归表达找第k大的数:

c
int[] nums;
int qselect(int l, int r, int k) {
  if (l == r) return nums[l];
  int x = nums[l + r >> 1], i = l - 1, j = r + 1;
  while (i < j) {
    do i++; while (nums[i] < x);
    do j--; while (nums[j] > x);
    if (i < j) swap(i, j);
  }
  int cnt = j - l + 1;
  if (k <= cnt) return qselect(l, j, k);
  else return qselect(j + 1, r, k - cnt);
}
void swap(int a, int b) {
  int c = nums[a];
  nums[a] = nums[b];
  nums[b] = c;
}

或者非递归方式找第k大的数:

c
int findKthLargest(int[] nums, int k) {
    int lo = 0, hi = nums.length - 1;
    // 索引转化,第k大的数就是第 n - k小的数
    k = nums.length - k;
    while (lo <= hi) {
        // 在 nums[lo..hi] 中选一个分界点
        int p = partition(nums, lo, hi);
        if (p < k) {
            // 第 k 大的元素在 nums[p+1..hi] 中
            lo = p + 1;
        } else if (p > k) {
            // 第 k 大的元素在 nums[lo..p-1] 中
            hi = p - 1;
        } else {
            // 找到第 k 大元素
            return nums[p];
        }
    }
    return -1;
}

//每一次都保证pivot位置的数值是最终正确的位置的数值
int partition(int[] nums, int lo, int hi) {
    if (lo == hi) return lo;
    // 将 nums[lo] 作为默认分界点 pivot
    int pivot = nums[lo];
    // j = hi + 1 因为 while 中会先执行 --
    int i = lo, j = hi + 1;
    while (true) {
        // 保证 nums[lo..i] 都小于 pivot
        while (nums[++i] < pivot) {
            if (i == hi) break;
        }
        // 保证 nums[j..hi] 都大于 pivot
        while (nums[--j] > pivot) {
            if (j == lo) break;
        }
        //退出while循环条件
        if (i >= j) break;
        // 如果走到这里,一定有:
        // nums[i] > pivot && nums[j] < pivot
        // 所以需要交换 nums[i] 和 nums[j],
        // 保证 nums[lo..i] < pivot < nums[j..hi]
        swap(nums, i, j);
    }
    // 将 pivot 值交换到正确的位置
    swap(nums, j, lo);
    // 现在 nums[lo..j-1] < nums[j] < nums[j+1..hi]
    return j;
}

// 交换数组中的两个元素
void swap(int[] nums, int i, int j) {
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}
java
class Solution {
    int[] nums;
    int n;
    int qselect(int l, int r, int k) {
        if (l == r) return nums[l];
        int x = nums[l + r >> 1], i = l - 1, j = r + 1;
        while (i < j) {
            do i++; while (nums[i] < x);
            do j--; while (nums[j] > x);
            if (i < j) swap(i, j);
        }
        int cnt = j - l + 1;
        if (k <= cnt) return qselect(l, j, k);
        else return qselect(j + 1, r, k - cnt);
    }
    void swap(int a, int b) {
        int c = nums[a];
        nums[a] = nums[b];
        nums[b] = c;
    }
    public void wiggleSort(int[] _nums) {
        nums = _nums;
        n = nums.length;
        int x = qselect(0, n - 1, n + 1 >> 1);
        int l = 0, r = n - 1, loc = 0;
        //三数排序
        while (loc <= r) {
            if (nums[loc] < x) swap(loc++, l++);
            else if (nums[loc] > x) swap(loc, r--);//注意这里没有++!!!保证小与x的数有序!
            else loc++;
        }
      
        int[] clone = nums.clone();
        int idx = 1; loc = n - 1;
        while (idx < n) {
            nums[idx] = clone[loc--];
            idx += 2;
        }
        idx = 0;
        while (idx < n) {
            nums[idx] = clone[loc--];
            idx += 2;
        }
    }
}

带上getId,代码变为

java
int getIdx(int x) {
  return (2 * x + 1) % (n | 1);
}
public void wiggleSort(int[] _nums) {
  nums = _nums;
  n = nums.length;
  int x = qselect(0, n - 1, n + 1 >> 1);
  int l = 0, r = n - 1, loc = 0;
  while (loc <= r) {
    if (nums[getIdx(loc)] > x) swap(getIdx(loc++), getIdx(l++));
    else if (nums[getIdx(loc)] < x) swap(getIdx(loc), getIdx(r--));
    else loc++;
  }
}

复杂度分析

  • 时间复杂度:快选的时间复杂度为 \(O(n)\);三数排序复杂度为 \(O(n)\)。整体复杂度为 \(O(n)\)
  • 空间复杂度:我的习惯是不算递归带来的额外空间消耗的,但如果是题目指定 \(O(1)\)空间的话,显然是不能按照习惯来,快选的空间复杂度为 \(O(\log{n})\)。整体复杂度为 \(O(\log{n})\)

用心记录,持续成长