75. Sort Colors

计数排序

计数排序适合排序关键字很有限的情况,如这里只有3个.

class Solution {
    public void sortColors(int[] nums) {
        int[] count={0,0,0};// 存放0,1,2三个元素的频率
        for (int num : nums) {
            assert num >= 0 && num <=2;
            // nums[] = {0,1,2} 自身元素nums[x]与自身频率count[x]是一一对应的
            count[num]++;
        }
        // nums[]元素值{0,1,2}与count[]下标值是一一对应的
        for(int j=0;j<count.length;j++){
            int presum=sum(count,j);
            // 解决了下标递增的问题,这样即使频率有多个也不怕了
            for(int i=presum;i<count[j]+presum;i++)
                nums[i] = j;      
        }
    }
    public static int sum(int[] count,int index){
        int sum =0;
        // count中下标为[0,index)的和是nums[i]中i的下标开始值
        for(int i=0;i< index ;i++)
            sum += count[i];
        return sum;
    }
}

三路快排

能应用的核心还是在于元素种类很有限,可以逐种情况判断:把遍历到的每个元素放到对应的区间0或1或2

class Solution {
    public void sortColors(int[] nums) {
        int zero = -1;
        //[0...zero]放0,初始化为-1是因为我不知道最开始nums[0,0]是几,-1致其其中没有任何元素
        int two = nums.length;
		//[two...nums.length-1] == 2
		//[zero..i] == 1
        for(int i=0;i<two;){
            if(nums[i] == 0){
               swap(nums,++zero,i++);
            }else if(nums[i] == 1){
                i++;
            }else{// nums[i] == 2
                assert nums[i] == 2;
                // 这里i不应该++,因为换过来了一个新的nums[-two]
                swap(nums,i,--two);
            }
        }
    }
    public static void swap(int[] nums,int i,int j){
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

Merge Sorted Array

88.merge-sorted-array

继承自三路快排的区间划分思路: [0…one…m+n|0…two] 中间[one…m+n]部分是空的,与上题不同的是这个部分区间用作了复制(不是有着明确种类划分)

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        if(n== 0){
            return;
        }
 
        int one = m-1;//[0,one]
        int two = n-1;//[0,two]
        // 对nums1,刚开始[m,m+n]是额外给的空间
        // 如果没有这额外空间,我是得额外开辟吧!
        int three=m+n-1;
        // 和链表的merge()很像
        for(;one>=0 && two>=0;three--){
            if(nums2[two]>nums1[one]){
                nums1[three]=nums2[two--];
            }else{
                nums1[three] = nums1[one--];
            }
        }
        // 不需要处理可能还有的剩下的[0,one] 元素,因为这些元素本来就是排序好in-place的
        if(two>=0)
            while(two>=0)
                nums1[three--]=nums2[two--];        
    }
}

继承自快排的快速选择

kth-largest-element-in-an-array

class Solution {
    public int findKthLargest(int[] nums, int k) {
        int lo=0;
        int hi=nums.length-1;//[0,hi]
 
        // 找第k大的精髓是不看数组元素的具体值,只要找到目标第k大元素的索引即可
        // 简单推理一下:
        // 1. 升序排序后,第1大的元素在末尾 `len(nums) - 1`。
		// 2. 升序排序后,第2大的元素就在 `len(nums) - 2` 的位置,以此类推。
		// 3. 泛化上述规律,升序排序后,第 `k` 大的元素在 `len(nums) - k` 的位置。
        int targetIndex = nums.length - k;
        return selectAscendingKthLargest(nums,l,r,targetIndex);
    }
    
    public int selectAscendingKthLargest(int[] nums, int l, int r, int targetIndex) {
		int pivotIndex = ascendingPartition(nums, l, r);
		if (pivotIndex == targetIndex) {
			return nums[pivotIndex];
		} else if (pivotIndex < targetIndex) {
			//如果现在的轴值索引大于目标索引,只需要看轴值左边区间
			return selectAscendingKthLargest(nums,++pivotIndex,r,targetIndex);
		} else {
			return selectAscendingKthLargest(nums,l,--pivotIndex,targetIndex);
		}
	}
	
    /**
    * 返回升序分区后的轴值下标
    * 其实一个单指针就可以解决,不一定要用双指针i、j<b>
    * 但是单指针的优化手段比较少,遇到数组已经部分排序的场景比双指针低
    * @return pivot: 所有小于轴值的元素应该位于其左侧,而所有大于或等于轴值的元素应位于其右侧
    */
    public static int ascendingPartition(int[] nums,int lo,int hi){
        if(lo>=hi) return lo;// 为了防止传进来的已经指针相遇
        
        int pivot = handleAndGetFirstIndexAsPivot(nums,lo,hi);
        
		int i = lo; // i初始化为lo,因为已经留出第一位作为轴值所在的下标位置,所以不需要比较。如果i初始化为 lo-1,轴值参与比较的同时,轴值的下标位置就会丢失,从而不能在最后将轴值放到它应该在的位置上
        int j = hi+1;
        while(true){
	        //i代表的是小于等于pivot的元素,如果当nums[i] 不满足小于等于轴值时,结束向右移动,等待把这个不满足条件的nums[i] 换到右边
            while(i <= hi && nums[++i] < pivot);
            //j代表的是大于等于pivot的元素,当nums[j] 不满足大于等于轴值时,结束向左移动,等待把这个不满足条件的nums[j] 换到左边
            while(j >= lo && pivot <= nums[--j]);
            // 如果左边遍历指针和右边的已经相遇或交叉,表示以下条件已经满足:
			// - 所有位于 `leftSearchIndex` 左侧的元素都已被检查,并且小于轴值
			// - 所有位于 `rightSearchIndex` 右侧的元素(包括轴值本身,如果轴值在最右端的话)都已被检查,并且大于或等于轴值
			// 因此,在双指针相遇或交错的时候终止循环
            if(i>=j ) break;
            
            swap(nums,i,j);
        }
        swap(nums,lo,j);
        return j;
    }
	/**
	* 可以通过随机选择轴值来降低平均时间复杂度
	*/
	public int handleAndGetFirstIndexAsPivot(int[] nums, int l, int r) {
		int randomIndex = l + new Random().nextInt(r - l + 1);
		swap(nums, l, randomIndex);
		return nums[l];
	}
    public static void swap(int[] nums,int j,int i){
        int temp=nums[j];
        nums[j]=nums[i];
        nums[i]=temp;
    }
}