【LeetCode】摆动排序 II [M](快速排序)

166 篇文章 0 订阅

324. 摆动排序 II - 力扣(LeetCode)

一、题目

给你一个整数数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]... 的顺序。

你可以假设所有输入数组都可以得到满足题目要求的结果。

示例 1:
输入:nums = [1,5,1,1,6,4]
输出:[1,6,1,5,1,4]
解释:[1,4,1,5,1,6] 同样是符合题目要求的结果,可以被判题程序接受。

示例 2:
输入:nums = [1,3,2,2,3,1]
输出:[2,3,1,3,1,2]

提示:

  • 1 <= nums.length <= 5 * 104
  • 0 <= nums[i] <= 5000
  • 题目数据保证,对于给定的输入 nums ,总能产生满足题目要求的结果

二、代码

class Solution {
    // 时间复杂度O(N),额外空间复杂度O(1)
    public void wiggleSort(int[] nums) {
        // 过滤无效参数
        if (nums == null || nums.length == 0) {
            return;
        }
        // 数组大小
        int n = nums.length;
        // 第一步:先将整个数组分成两部分,来保证左半部分的任意一个数一定比右半部分的任意一个数小,用于后续我们用来将他们交错排列实现摆动序列
        findIndexNum(nums, 0, n - 1, n >> 1); 
        // 第二步:将排列好的数组进行交错排列,实现摆动序列
        // 1、数组大小为偶数
        if ((n & 1) == 0) {
            // 先进行完美洗牌
            shuffle(nums, 0, n - 1);
            
            // 进行完美洗牌前数组状况:L1 L2 L3 L4 R1 R2 R3 R4
            // 经过完美洗牌后数组状况:R1 L1 R2 L2 R3 L3 R4 L4
            // 我们想要的摆动序列应该是下面这两种的任意一种:
            // 1)L1 R1 L2 R2 L3 R3 L4 R4 ×
            // 2)L4 R4 L3 R3 L2 R2 L1 R1 √
            // 虽然上面两个都符合题目要求,但实际我们只能实现第二种,第一种是不对的。

            // 再将整体逆序即可得到摆动序列,就可以得到第二种序列结果
            reverse(nums, 0, n - 1);

            // 我们无法直接对完美洗牌后的数组两两逆序得到第一种结果序列,因为这样操作后得到的结果提交会报答案错误
            // 做个实验,如果把上一行的code注释掉(reverse过程),然后跑下面注释掉的for循环代码
			// for循环的代码就是两两交换,会发现对数器报错,说明两两交换是不行的, 必须整体逆序
			// for (int i = 0; i < nums.length; i += 2) {
			// 	swap(nums, i, i + 1);
			// }

        // 2、数组大小为奇数    
        } else {
            // 0下标不动,直接从1下标开始进行完美洗牌,即可得到摆动序列
            shuffle(nums, 1, n - 1);
        }
    }

    // 第一步:无序数组中找到第K小的快排方法。这一部分其实比较简单,就是一个类似于荷兰国旗问题的代码
    public void findIndexNum(int[] nums, int l, int r, int index) {
        int num = 0;
        int[] range = null;

        while (l < r) {
            // 随机找一个基准数
            num = nums[l + (int) Math.random() * (r - l + 1)];
            // 将小于num的都放到数组左边,大于的放到数组右边,小于的放在数组中见
            range = partition(nums, l, r, num);

            // 如果index落在了中间范围上,就说明完成了我们想要的操作。这里的index就是中间下标位置,也就是说此时中间相等的部分就是这个数组的中位数
            if (index >= range[0] && index <= range[1]) {
                return;
            } else if (index < range[0]) {
                // 向左部分继续递归,只走一边
                r = range[0] - 1;
            } else {
                // 向右部分继续递归,只走一边
                l = range[1] + 1;
            }
        }
    } 

    // 在数组nums的l~r范围上,将小于num的放左边,等于num的放中间,大于num的放右边
    public int[] partition(int[] nums, int l, int r, int num) {
        // 标记三部分的边界
        int less = l - 1;
        int more = r + 1;
        // 当前遍历到的位置
        int cur = l;
        while (cur < more) {
            if (nums[cur] > num) {
                swap(cur, --more, nums);
            } else if (nums[cur] < num){
                swap(cur++, ++less, nums);
            } else {
                cur++;
            }
        }
        // 返回中间部分的边界位置
        return new int[] {less + 1, more - 1};
    }

    // 交换
    public void swap(int i, int j, int[] nums) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

    // 第二步:完美洗牌——下标循环怼
    public void shuffle(int[] nums, int l, int r) {
        // 切成一块一块的解决,每一块的长度满足(3^k)-1
		while (r - l + 1 > 0) {
            // 当前要处理的长度
			int lenAndOne = r - l + 2;

            // 计算小于等于lenAndOne并且是离lenAndOne最近的,满足(3^k)-1的数
            // 也就是找到最大的k,满足3^k <= lenAndOne
			int bloom = 3;
            // 初始k为1
			int k = 1;
            // 保证3^k <= lenAndOne
            // 要记住这个方法,就是求3次幂的时候直接用循环滚下去,利用之前求出来的结果,只需要再乘一个3就行了,这样效率可以更高一些,比每一轮都重新用Math.pow求快很多
			while (bloom <= lenAndOne / 3) {
				bloom *= 3;
				k++;
			}

            // 此时我们就先处理长度为bloom - 1长度的范围,至于剩下的长度留到后面的循环去弄。bloom - 1满足3^k -1
            // 下面这个流程就是将符合要求的前k个数移动到数组的最前面,下面的流程其实举个具体的例子就能明白了
            // 当前要解决长度为bloom-1的块,一半就是再除2
			int m = (bloom - 1) / 2;
            // [L..R]的中点位置
			int mid = (l + r) / 2;
            // 要旋转的左部分为[L+m...mid], 右部分为arr[mid+1..mid+m]
            // 注意在这里,nums下标是从0开始的
			rotate(nums, l + m, mid, mid + m);

            // 开始下标循环怼
			cycles(nums, l - 1, bloom, k);

             // 解决了前bloom-1的部分,剩下的部分继续处理,将要处理范围的左边界设置为l + bloom - 1,继续循环
			l = l + bloom - 1;
		}
	}

	public void cycles(int[] nums, int base, int bloom, int k) {
		for (int i = 0, trigger = 1; i < k; i++, trigger *= 3) {
            // 根据我们的结论公式,算出来下一个要在什么位置,注意这个位置并不是真实的数组下标位置
			int next = (2 * trigger) % bloom;
			int cur = next;
			int record = nums[next + base];
			int tmp = 0;
			nums[next + base] = nums[trigger + base];
            // 每一轮循环时trigger就相当于这一次下标循环怼的起始位置,只要是循环过程中下标再次回到trigger,就说明这个环已经遍历完一遍了
			while (cur != trigger) {
                // 根据公式计算下一个位置
				next = (2 * cur) % bloom;
                // 下面开始将数移动到要去的位置上
				tmp = nums[next + base];
				nums[next + base] = record;
				cur = next;
				record = tmp;
			}
		}
	}

    // 将l~m和m+1~r两个部分做整体交换   
	public void rotate(int[] arr, int l, int m, int r) {
        // 先对这两个部分自己内部做逆序
		reverse(arr, l, m);
		reverse(arr, m + 1, r);
        // 然后再把这两个部分的整体进行逆序
		reverse(arr, l, r);
	}

    // 对数组arr内的l~r范围进行逆序
	public void reverse(int[] arr, int l, int r) {
		while (l < r) {
			swap(l++, r--,arr);
		}
	}
}

三、解题思路 

这道题就是采用

  1. 无序数组中找到第K小的快排方法(改进的快速排序)
  2. 完美洗牌问题

这两个方法来解决。详细过程都写在注释里了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值