【数据结构】排序与检索

排序与检索

179. 最大数

题目描述

给定一组非负整数,重新排列它们的顺序使之组成一个最大的整数。
示例:

输入: [3,30,34,5,9]
输出: 9534330

说明: 输出结果可能非常大,所以你需要返回一个字符串而不是整数。

题解

解决思想: 其实很简单,把整数换成字符,再进行自定义的排序即可。自定义排序:每一对数在排序的比较过程中,我们比较两种连接顺序哪一种更好。

假设:一对整数字符a和b,有 a+b>b+a,b+c>c+b,那么一定有 a+c>b+c

数组排好序后,最“重要”的数字会在最前面,形成字符返回即可。需要注意如果数组只包含 0 ,我们直接返回结果 0 。

复杂度分析

  • 时间复杂度:O(nlgn)

尽管我们在比较函数中做了一些额外的工作,但是这只是一个常数因子。所以总的时间复杂度是由排序决定的,在 Python 和 Java 中都是 O(nlgn)

  • 空间复杂度:O(n)

这里,我们使用了 O(n) 的额外空间去保存 nums 的副本。尽管我们就地进行了一些额外的工作,但最后返回的数组需要 O(n) 的空间。因此,需要的额外空间与 nums 大小成线性关系。

Java代码

class Solution {
    public String largestNumber(int[] nums) {
		
        String[] asStri = new String[nums.length];
        //将int类型数组转换成字符串类型数组
		for(int i=0;i<nums.length;i++) {
			asStri[i]=String.valueOf(nums[i]);
		}
		//自定义规则,对字符数组进行排序
		Arrays.sort(asStri, new Comparator<String>() {
			@Override
			public int compare(String a, String b) {
				//寻找两个字符之间合适的排序位置
				String o1=a+b;
				String o2=b+a;
				//从大到小排序
				return o2.compareTo(o1);
			}
		});
		
		//处理特殊值
		//如果数组中最大值为0,那么输出0
		if(asStri[0].equals("0")) {
			return "0";
		}
		//将排序后的数组和为一个字符串
		String LargestStr="";
		for (String string : asStri) {
			LargestStr += string;
		}
		//System.out.println(LargestStr);
        return LargestStr;		        
    }
}

324. 摆动序列

题目描述

给定一个无序的数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]… 的顺序。
示例:

输入: nums = [1, 5, 1, 1, 6, 4]
输出: 一个可能的答案是 [1, 4, 1, 5, 1, 6]

说明: 你可以假设所有输入都会得到有效的结果。

进阶: 你能用 O(n) 时间复杂度和 / 或原地 O(1) 额外空间来实现吗?

解法1:排序

首先,我们可以很容易想到一种简单的解法:将数组进行排序,然后从中间位置进行等分(如果数组长度为奇数,则将中间的元素分到前面),然后将两个数组进行穿插

例如:
对于数组[1, 5, 2, 4, 3],我们将其排序,得到[1, 2, 3, 4, 5],然后将其分割为[1, 2, 3]和[4, 5],对两个数组进行穿插,得到[1, 4, 2, 5, 3]。

但是这一方法有一个问题
对于数组[1, 2, 2, 3],按照这种做法求得的结果仍为[1, 2, 2, 3]。如果题目不要求各元素严格大于或小于相邻元素,即,只要求nums[0] <= nums[1] >= nums[2] <= nums[3]…,那么这一解法是符合要求的,但题目要求元素相互严格大于或小于,那么需要稍微做一点改进。

为了方便阅读,我们在下文中定义较小的子数组为数组A,较大的子数组为数组B。显然,出现上述现象是因为nums中存在重复元素。实际上,由于穿插之后,相邻元素必来自不同子数组,所以A或B内部出现重复元素是不会出现上述现象的。所以,出现上述情况其实是因为数组A和数组B出现了相同元素,我们用r来表示这一元素。而且我们可以很容易发现,如果A和B都存在r,那么r一定是A的最大值,B的最小值,这意味着r一定出现在A的尾部,B的头部。其实,如果这一数字的个数较少,不会出现这一现象,只有当这一数字个数达到原数组元素总数的一半,才会在穿插后的出现在相邻位置。以下举几个例子进行形象地说明:

例如,对于数组[1,1,2,2,3,3],分割为[1,1,2]和[2,3,3],虽然A和B都出现了2,但穿插后为[1,2,1,3,2,3],满足要求。而如果2的个数再多一些,即[1,1,2,2,2,3],分割为[1,1,2]和[2,2,3],最终结果为[1,2,1,2,2,3],来自A的2和来自B的2出现在了相邻位置。

出现这一问题是因为重复数在A和B中的位置决定的,因为r在A尾部,B头部,所以如果r个数太多(大于等于(length(nums) + 1)/2),就可能在穿插后相邻。要解决这一问题,我们需要使A的r和B的r在穿插后尽可能分开。一种可行的办法是将A和B反序:

例如,对于数组[1,1,2,2,2,3],分割为[1,1,2]和[2,2,3],分别反序后得到[2, 1, 1]和[3, 2, 2],此时2在A头部,B尾部,穿插后就不会发生相邻了。

当然,这只能解决r的个数等于(length(nums) + 1)/2的情况,如果r的个数大于(length(nums) + 1)/2,还是会出现相邻。但实际上,这种情况是不存在有效解的,也就是说,这种数组对于本题来说是非法的。

此时我们得到了第一个解法:

使用排序将数组等分为小大两个子数组AB,将数组A反序后,将两个数组进行穿插。
由于需要使用排序,所以时间复杂度为O(NlogN),由于需要存储A和B,所以空间复杂度为O(N)。

解法2:快速选择+快速3向切分

上一解法之所以时间复杂度为O(NlogN),是因为使用了排序。但回顾解法1,我们发现,我们实际上并不关心A和B内部的元素顺序,**只需要满足A和B长度相同(或相差1),且A中的元素小于等于B中的元素,且r出现在A的头部和B的尾部即可。**实际上,由于A和B长度相同(或相差1),所以r实际上是原数组的中位数,下文改用mid来表示。因此,我们第一步其实不需要进行排序,而只需要找到中位数即可。而寻找中位数可以用快速选择算法实现,时间复杂度为O(n)。

找到中位数后,我们需要利用3-way-partition算法将中位数放在数组中部,同时将小于中位数的数放在左侧,大于中位数的数放在右侧。

至此,原数组被分为3个部分,左侧为小于中位数的数,中间为中位数,右侧为大于中位数的数。之后的做法就与解法1相同了:我们只需要将数组从中间等分为2个部分,然后反序,穿插,即可得到最终结果。

快速选择

快速选择算法 是快速排序算法的变形, 通常用来在未排序的数组中寻找第k小/第k大的元素

  • 快速选择的总体思路与快速排序一致,选择一个元素作为基准来对元素进行分区, 将小于和大于基准的元素分在基准左边和右边的两个区域。不同的是,快速选择并不递归访问双边,而是只递归进入一边的元素中继续寻找。 这降低了平均时间复杂度为O(n),不过最坏情况仍然是O(n2)。

算法原理:
回想快速排序的 partition() 方法,它会将数组的 a[lo]至 a[hi] 重新排列并返回一个整数 j 使得 a[lo…j-1] <= a[j] <=a[j+1…hi] 。那么,如果 k = j,问题就解决了。如果 k < j,我们就需要切分左子数组(令 hi = j-1);如果 k > j,我们则需要切分右子数组(令 lo = j+1)。这个循环保证了数组中 lo 左边的元素都小于等于 a[lo…hi],而 hi 右边的元素都大于等于 a[lo…hi]。我们不断地切分直到子数组中只含有第 k 个元素,此时 a[k] 含有最小的(k+1)个元素,a[0…k-1] <= a[k]<=a[k+1…hi] 。

实现步骤:

  1. 打乱数组。
  2. 对数组进行一次切分,得到整数 j,满足a[lo…j-1] <= a[j] <=a[j+1…hi] 。
  3. 果 k = j,返回 nums[k]。如果 k < j,令 hi = j-1;如果 k > j,令 lo = j+1。
  4. 当满足 lo < hi 时,重复以上步骤。

至于为何这个算法是线性级别的,是因为假设每次都正好将数组二分,那么比较的总次数为(N+N/2+N/4+N/8…),直到找到第 k 的元素,这个和显然小于 2N。和快速排序一样,这里也需要一点数学知识来得到比较的上界,它比快速排序略高。这个算法和快速排序的另一个共同点是这段分析依赖于使用随机的切分元素,因此它的性能保证也来自于概率。

# 快速选择实现
public class QuickSelection {
	public static int find(Comparable[] nums,int k) {
		//0.先对数组进行打乱
		shuffle(nums);
		int lo=0;
		int hi=nums.length-1;
		while(hi>lo) {
			//1.选择一个元素作为基准来对元素进行分区
			int j = partition(nums,lo,hi);
			if (j==k) return nums[k];
			else if(j>k) hi=j-1; //切分左子树组
			else if(j<k) lo=j+1; //切分右子树组
		}
		return nums[k];
	}
	private static void shuffle(Comparable[] a) {
		List<Comparable> list= new ArrayList<Comparable>();
		for(int i=0;i<a.length; i++) {
			list.add(a[i]);
		}
		System.out.println(Arrays.toString(a));
		
		//使用Collections
		Collections.shuffle(list);
		for(int i=0;i<a.length; i++) {
			a[i]=list.get(i);
		}
	}


	//对于某个 j,a[j] 已经排定;
	// a[lo] 到 a[j-1] 中的所有元素都不大于 a[j];
	// a[j] 到 a[hi] 中的所有元素都不小于 a[j]。
	private static int partition(Comparable[] a, int lo, int hi) {
		// 将数组切分为a[lo..i-1], a[i], a[i+1..hi]
		int i=lo,j=hi+1;   //左右指针
		Comparable v = a[lo];
		while(true) {
			//扫描左右
			while(less(a[++i],v)) if(i==hi) break; //找到一个大于v的值
			while(less(v,a[--j])) if(j==lo) break; //找到一个小于v的值
			if(i>=j) break;
			exch(a,i,j); //交换	
		}
		exch(a,lo,j);  // 将v放入正确位置a[j]
		return j;     // a[lo..j-1] <= a[j] <= a[j+1..hi] 达成
	}
	
	// 判断元素 v 是否比 w 小
	public static boolean less(Comparable v,Comparable w) {
		return v.compareTo(w)<0;
	}
	// 交换数组a中指定位置的元素
	public static void exch(Comparable[] a, int i, int j) {
		Comparable t= a[i];a[i]=a[j];a[j]=t;
	}

使用三向切分

我们仅需要使用一次3-way-partition算法将中位数放在数组中部,同时将小于中位数的数放在左侧,大于中位数的数放在右侧。

算法原理及实现:
https://blog.csdn.net/weixin_42033436/article/details/107304184

public class WiggleSort {
	public static void main(String[] args) {
		Integer[] a= {2,1,6,3,5,2,9,4};
		wiggleSort(a);
	}
	public static void wiggleSort(Comparable[] nums) {
		//1.先找到中位数
		int k=(0+nums.length-1)/2;
		Comparable v  = QuickSelection.find(nums, k);
		//System.out.println(v);
		
		//2.利用快速3向排序
		//exch() less()和之前写法一样
		int lt=0,i=1,gt=nums.length-1; //初始化三个指针
		
		while(i<=gt) {
			int cmp=nums[i].compareTo(v);
			
			if(cmp<0) exch(nums,lt++,i++);       //指针lt 使得 a[lo..lt-1] 中的元素都小于 v
			else if (cmp>0) exch(nums,i,gt--);   //指针 gt 使得 a[gt+1..hi] 中的元素都大于 v
			else  i++;                        //指针 i 使得 a[lt..i-1] 中的元素都等于 v
		} // 现在 a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi]成立	
		
		//System.out.println(Arrays.toString(nums));
	    
	    //3.分为两个数组,并反序,穿插
		Integer[] A=new Integer[k+1],B=new Integer[nums.length-1-k];
		System.arraycopy(nums, 0, A, 0, k+1);
		System.arraycopy(nums, k+1, B, 0, nums.length-1-k);
		//System.out.println(Arrays.toString(A));
		//System.out.println(Arrays.toString(B));
		
		for(int m=0;m<=A.length-1;m++) {
			nums[2 * m] = A[A.length - 1 - m];
		}
		for(int m=0;m<=B.length-1;m++) {
			nums[2 * m+1] = B[B.length - 1 - m];
		}
		System.out.println(Arrays.toString(nums));
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值