快速排序、归并排序、数组切分(Partition Array)、Quick Select算法总结

1. 快速排序

思路

  • 先整体有序,后局部有序
  • 先将整个大数组以某个pivot划分成左右有序的状态,然后缩小区间,重复这个过程
  • 具体的实现细节均在代码中

代码

class LeetCode912 {
    public List<Integer> sortArray(int[] nums) {
        List<Integer> result = new ArrayList<>();
        if (nums == null || nums.length == 0){
            return result;
        }
        int start = 0;
        int end = nums.length - 1;
        quickSort(nums,start,end);

        for (int num : nums) {
            result.add(num);
        }
        return result;
    }
    //0. 这是个递归函数,它的定义是 在闭区间[start,end]中,把所有小于 nums[(start+end)/2]的数都放在它左边
    //   把所有大于这个的数都放在它右边。然后不断的去缩小这个区间
    //   也就是先整体有序 后局部有序
    private void quickSort(int[] nums, int start, int end) {
        //1. 递归出口
        if (start >= end){
            return;
        }
        //1.1 缓存区间端点 后面缩小区间的时候还需要这两个端点的信息
        int left = start;
        int right = end;

        //2. get value not index
        int pivot = nums[(end + start)/2];
        //2.1 接下来的操作就是以pivot为锚点 切分数组
        //    注意这里的细节 left <= right,为何要等于呢?
        //    就是为了跳出while后,能达到这样的状态:start......right,left,.....end
        while (left <= right){
            //2.2 找到第一个应该在 pivot右边的数
            while (left <= right && nums[left] < pivot ){
                left++;
            }
            //2.3 找到第一个应该在 pivot左边的数
            while (left <= right && nums[right] > pivot){
                right--;
            }
            //2.4 找到后,完成交换
            if (left <= right){
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
                left++;
                right--;
            }
        }
        //3. 跳出while,完成了本次区间内的切分,接下来缩小区间
        //3.1 此时left和right的状态一定是  start......right,left,.....end
        quickSort(nums, start, right);
        quickSort(nums, left, end);
    }
}

2. 归并排序

思路

  • 先局部有序,后整体有序
  • 先一直把数组二分下去,直到找到最小有序的部分,那么就开始向上合并两个有序的部分
  • 显然最开始最小的有序部分就是单个数字本身嘛
  • 既然需要合并两个有序的数组,那么肯定就额外需要另一个数组来腾挪,这也是归并相对于快排劣势的一点,需要额外的开辟一个数组的额外空间
  • 细节都在代码里

代码

class LeetCode912 {
    public List<Integer> sortArray(int[] nums) {
        List<Integer> result = new ArrayList<>();
        if (nums == null || nums.length == 0){
            return result;
        }
        int start = 0;
        int end = nums.length - 1;
        //用于合并两个有序数组
        int[] temp = new int[nums.length];
        mergeSort(nums,start,end,temp);
        for (int num : nums) {
            result.add(num);
        }
        return result;
    }
    //0. 这是一个递归函数,其定义是:将闭区间[start,end]划分为两半
    private void mergeSort(int[] nums, int start, int end, int[] temp) {
        //3.出
        //3.1 当切分到只剩一个元素的时候,就无需再继续往下了,此时已经是最小的有序区间
        if (start >= end){
            return;
        }
        //1.分
        //  就是分治法的味道,先分下去
        int mid = (end - start)/2 + start;
        mergeSort(nums, start, mid, temp);
        mergeSort(nums, mid+1, end, temp);
        //2.合
        //  从上述递归中出来后,可以认为 区间:[start,mid]和[mid+1,end]已经是有序了
        //  那么接下来只需要合并这两个有序区间即可
        merge(nums,start,mid,end,temp);
    }
    //此函数可认为是合并两个有序数组
    private void merge(int[] nums, int start, int mid, int end, int[] temp) {
        //1. 两有序数组的起始点
        int left = start;
        int right = mid +1;
        //2. 有序数组的index
        int index = 0;
        //3. 这里都是index,所以可以取到
        while (left <= mid && right <= end){

            if (nums[left] < nums[right]){
                temp[index++] = nums[left++];
            }else {
                temp[index++] = nums[right++];
            }
        }
        //3.1 double check
        while (left <= mid){
            temp[index++] = nums[left++];
        }
        while (right <= end){
            temp[index++] = nums[right++];
        }

        //4. 现在数组nums中[start,end]都是有序的,但是呢,这一部分的值还暂存在temp的[0,index]区间内
        //4.1 现在要把这个有序的部分赋值给nums的[start,end]部分
        //4.2 要注意这里index不能取等,因为你想,最后一个元素赋值给temp后,index完成了一次自加操作
        for (int i = 0; i < index; i++) {
            nums[start++] = temp[i];
        }
    }
}

3. 数组切分问题(Partition Array)

3.1 LintCode-31. Partition Array

题意

  • 给定数组和一个数k,要求把数组中<k的数放在左边,>=k的数放在右边,返回第一个大于等于k的数的索引

思路

  • 这不就是快排中每一次划分的算法嘛
  • 只要快排理解了,这一道题就很简单了

代码

 public int partitionArray(int[] nums, int k) {
        // write your code here

        if (nums == null || nums.length == 0){
            return 0;
        }

        int left = 0;
        int right = nums.length - 1;
        while (left <= right){

            while (left <= right && nums[left] < k){
                left++;
            }
            while (left <= right && nums[right] >= k){
                right--;
            }

            if (left <= right){
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
            }
        }
        // start ... right left ... end
        return left;
    }

3.2 LintCode373. Partition Array by Odd and Even

题意

  • 给定数组
  • 将数组中的奇数放在前面(左边)
  • 将数组中的偶数放在后面(右边)

思路

  • 和上题完全一样嘛,只是条件变了

代码

 public void partitionArray(int[] nums) {

        if (nums == null || nums.length == 0){
            return;
        }

        int left = 0;
        int right = nums.length - 1;
        while (left <= right){

            //0. 找到第一个应该在 右侧的偶数
            while (left <= right && nums[left] %2 == 1 ){
                left++;
            }
            //1. 找到第一个应该在 左侧的奇数
            while (left <= right && nums[right] % 2 == 0){
                right--;
            }
            //2.交换
            if (left <= right){
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
                left++;
                right--;
            }
        }

    }

3.3 LintCode49.Sort Letters by Case

题意

  • 给定字符数组
  • 把小写字母放在前面
  • 把大写字母放在后面

思路

  • 和上题完全一致,只是判断条件变成了字母是否为大小写

代码

 public void sortLetters(char[] chars) {
        // write your code here

        if (chars == null || chars.length == 0){
            return;
        }
        int left = 0;
        int right = chars.length - 1;
        while (left <= right){
            //0. 找到第一个应该 在右侧的大写字母
            while (left <= right && Character.isLowerCase(chars[left])){
                left++;
            }
            //1. 找到第一个应该 在左侧的小写字母
            while (left <= right && Character.isUpperCase(chars[right])){
                right--;
            }
            if (left <= right){
                char temp = chars[left];
                chars[left] = chars[right];
                chars[right] = temp;
                left++;
                right--;
            }
        }

    }

3.4 LintCode144-Interleaving Positive and Negative Numbers

题意

  • 给定数组
  • 要求将数组划分为正负相间的样式

思路

  • 关键在于处理正负数数量的影响
  • 1.首先把所有的负数放在左边 正数放在右边
  • 2.然后统计数量
  • 3.数量多的代表第一个数和最后一个数都是它
  • 4.然后按照步长为2前后交换即可

代码

 public void rerange(int[] A) {
        // write your code here
        if (A == null || A.length == 0){
            return;
        }
        int left = 0;
        int right = A.length - 1;
        //0. 先把所有的负数放在左侧 正数放在右侧
        while (left <= right){
            while (left <= right && A[left] < 0){
                left++;
            }
            while (left <= right && A[right] > 0){
                right--;
            }
            if (left <= right){
                int t = A[left];
                A[left] = A[right];
                A[right] = t;
                left++;
                right--;
            }
        }

        //1. 统计正负数的数量
        int posNum = 0;
        int negNum = 0;
        for (int num : A) {

            if (num > 0){
                posNum++;
            }
            if (num < 0){
                negNum++;
            }
        }
        //2. 根据正负数数量来决定完成划分后谁位于首位和尾位
        //2.1 数量多的那一方 占据首尾
        //2.2 left和right代表交换的起始位置
        if (posNum > negNum){
            //2.3 那么第一个数和最后一个数都是正数
            left = 0;
            right = A.length - 2;
        }else if (posNum < negNum){
            //2.4 那第一个数和最后一个数都是负数
            left = 1;
            right = A.length -1;
        }else {
            //2.5 一样多,那就按照负正负正的次序进行排列即可
            left = 0;
            right = A.length - 1;
        }
        //3. 随后进行交换,注意步长为2
        while (left <= right){
            int t = A[left];
            A[left] = A[right];
            A[right] = t;
            left+=2;
            right-=2;
        }
    }

4. QuickSelect问题

4.1 LeetCode215. Kth Largest Element in an Array

题意

  • 给定无序数组和一个数k,要求找到改数组中第k大的数

思路

  • 利用数组划分和快速排序思想
  • 不断的缩小第k大可能存在的区间
  • 具体细节都在代码注释中

代码

class LeetCode215 {
    public int findKthLargest(int[] nums, int k) {
        if (nums == null || nums.length == 0){
            return -1;
        }
        
        return quickSelect(nums,0,nums.length - 1,k);
        
    }
    //0. 思想就在于通过切分数组 不断的压缩 k-th可能在的区间
    private int quickSelect(int[] nums, int start, int end, int k) {
        //2.递归出口
        //2.1 为何相等的时候就要退出了呢,因为此时区间中就只有一个数了嘛,那这个数肯定就是要找的
        if (start == end){
            return nums[start];
        }


        int left = start;
        int right = end;
        int pivot = nums[(left + right)/2];
        //0. 以pivot为界,把数组元素切分为两部分,左侧大于它,右侧小于它
        //0.1 注意这里和快排不同,因为这里是求第k大
        while (left <= right){

            //0.2 找到第一个应该在右边的数
            while (left <= right && nums[left] > pivot){
                left++;
            }
            while (left <= right && nums[right] < pivot){
                right--;
            }
            //0.3 交换
            if (left <= right){
                int t = nums[left];
                nums[left] = nums[right];
                nums[right] = t;
                left++;
                right--;
            }
        }
        //1. 完成划分,现在left和right的位置关系有两种可能
        //1.1 start....right,left....end
        //1.2 start.....right,A,left...end 这种隔了一个的状态是由于上面while中的if导致的
        //1.3 当if中二者 left = right后,满足添加,执行if内的代码,然后会 left++,right--,就会导致这种刚好错开一个的情况
        //1.4 我们要找第k大,那么就需要判断 第k大 可能会落在哪个区间
        //1.5 现在一个好消息在于区间[start,right]中的数都大于区间[left,end]
        //1.6 那么需要判断k是否落在这两个区间内,第k大是基于1的,而上述的left这些都是索引,基于0,所以需要k-1
        if (start + k - 1 <= right){
            //1.7 第k大在左半区间,所以扔掉右边的一半
            return quickSelect(nums, start, right, k);
        }
        if (start + k - 1 >= left){
            //1.8 第k大在右半区间,所以扔掉左边的一半
            //1.8.1 左边一半的数量是多少呢
            return quickSelect(nums,left,end,k - (left - start));
        }
        //1.9 如果落在中间,即情况1.2
        return nums[right+1];
    }
}

4.2 LintCode80. Median

题意

  • 给定未排序数组,求其中位数

思路

  • 只要分析清楚中位数是第几大的数
  • 那么问题就转换为了在无序数组中求第k大的数问题
  • 分奇数和偶数不难分析出中位数都是第n/2 - 1大的数

代码

public class Solution {
  public int median(int[] nums) {

        //0. 简单分析发现,不管数字个数是奇数还是偶数,其中位数都是第(n/2) + 1大的数
        int k = nums.length/2 + 1;
        //1. 那么剩余的工作就是在一个未排序的数组中找第k大的数
        return findKthLargest(nums, k);
    }

    public int findKthLargest(int[] nums, int k) {
        if (nums == null || nums.length == 0){
            return -1;
        }

        return quickSelect(nums,0,nums.length - 1,k);

    }
    //0. 思想就在于通过切分数组 不断的压缩 k-th可能在的区间
    private int quickSelect(int[] nums, int start, int end, int k) {
        //2.递归出口
        //2.1 为何相等的时候就要退出了呢,因为此时区间中就只有一个数了嘛,那这个数肯定就是要找的
        if (start == end){
            return nums[start];
        }


        int left = start;
        int right = end;
        int pivot = nums[(left + right)/2];
        //0. 以pivot为界,把数组元素切分为两部分,左侧大于它,右侧小于它
        //0.1 注意这里和快排不同,因为这里是求第k大
        while (left <= right){

            //0.2 找到第一个应该在右边的数
            while (left <= right && nums[left] > pivot){
                left++;
            }
            while (left <= right && nums[right] < pivot){
                right--;
            }
            //0.3 交换
            if (left <= right){
                int t = nums[left];
                nums[left] = nums[right];
                nums[right] = t;
                left++;
                right--;
            }
        }
        //1. 完成划分,现在left和right的位置关系有两种可能
        //1.1 start....right,left....end
        //1.2 start.....right,A,left...end 这种隔了一个的状态是由于上面while中的if导致的
        //1.3 当if中二者 left = right后,满足添加,执行if内的代码,然后会 left++,right--,就会导致这种刚好错开一个的情况
        //1.4 我们要找第k大,那么就需要判断 第k大 可能会落在哪个区间
        //1.5 现在一个好消息在于区间[start,right]中的数都大于区间[left,end]
        //1.6 那么需要判断k是否落在这两个区间内,第k大是基于1的,而上述的left这些都是索引,基于0,所以需要k-1
        if (start + k - 1 <= right){
            //1.7 第k大在左半区间,所以扔掉右边的一半
            return quickSelect(nums, start, right, k);
        }
        if (start + k - 1 >= left){
            //1.8 第k大在右半区间,所以扔掉左边的一半
            //1.8.1 左边一半的数量是多少呢
            return quickSelect(nums,left,end,k - (left - start));
        }
        //1.9 如果落在中间,即情况1.2
        return nums[right+1];
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值