分治-算法

快速排序

颜色分类

算法思路(快排思想 - 三指针法使数组分三块):
类比数组分两块的算法思想,这里是将数组分成三块,那么我们可以再添加⼀个指针.

设数组大小为 n ,定义三个指针 left, i, right :

  • left :⽤来标记 0 序列的末尾,初始化为 -1 ;
  • i :⽤来扫描数组,初始化为 0 ;
  • right :⽤来标记 2 序列的起始位置,初始化为 nums.length。

在 i 往后扫描的过程中,保证:

  • [0, left] 内的元素都是 0 ;
  • [left + 1, i - 1] 内的元素都是 1 ;
  • [i, right - 1] 内的元素是待定元素;
  • [right, nums.length-1] 内的元素都是 2 。

算法流程:

  1. 初始化 i = 0,left = -1, right = nums.length ;
  2. 当 i < right 的时候(因为 right 表示的是 2 序列的左边界,因此当 i 碰到right 的时候,说明已经将所有数据扫描完毕了),⼀直进行下面循环:
  • nums[i] == 0 :说明这个位置的元素需要在 left + 1 的位置上,因此交换 left + 1 与 i 位置的元素,并且让 left++ (指向 0 序列的右边界),i++
  • nums[i] == 1 :说明这个位置应该在 left+1 和 i -1之间,此时⽆需交换,直接让 i++ ,判断下⼀个元素即可;
  • nums[i] == 2 :说明这个位置的元素应该在 right - 1 的位置,因此交换right - 1 与 i 位置的元素,并且让 right-- (指向 2 序列的左边界),i 不变(因为交换过来的数是没有被判断过的,因此需要在下轮循环中判断)
  1. 当循环结束之后:
  • [0, left] 表示 0 序列;
  • [left + 1, right - 1] 表示 1 序列;
  • [right,nums.length - 1] 表示 2 序列。
class Solution {
    void swap(int[] nums, int i, int j) {
            int tmp = nums[i];
            nums[i] = nums[j];
            nums[j] = tmp;
        }
        
    public void sortColors(int[] nums) {
        int left = -1, right = nums.length, i = 0;
        while(i < right) {
            if(nums[i] == 0) swap(nums, ++left, i++);
            else if(nums[i] == 1) i++;
            else swap(nums, --right, i);
        }
    }
}

排序数组

算法思路(数组分三块思想 + 随机选择基准元素的快速排序):
快排最核心的⼀步就是 Partition (分割数据):将数据按照⼀个标准,分成左右两部分。
如果我们将数组划分为 左 中 右 三部分:左边是比基准元素小的数据,中间是与基准元素相同的数据,右边是比基准元素大的数据。然后再去递归的排序左边部分和右边部分即可(可以舍去大量的中间部分)。

class Solution {
    public int[] sortArray(int[] array) {
         qsort(array, 0, array.length - 1);
         return array;
    }

    void qsort(int[] nums, int left, int right) {
        if(left >= right) return;

        //数组分三块
        int key = nums[new Random().nextInt(right - left + 1) + left];
        int l = left - 1, r = right + 1, i = left; 
        while(i < r) {
            if(nums[i] < key) swap(nums, ++l, i++);
            else if(nums[i] == key) i++;
            else swap(nums, --r, i);
        }

        qsort(nums, left, l);
        qsort(nums, r, right);
    }

    void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
}

数组中的第k个最大元素

算法思路(快速选择算法):
当我们把数组「分成三块」之后: a----[left, l], b----[l + 1, r - 1] ,c----[r, right] ,我们可以通过计算每⼀个区间内元素的「个数」,进而推断出我们要找的元素是在「哪⼀个区间」里面的。直接去「相应的区间」去寻找最终结果就好了。

class Solution {
    public int findKthLargest(int[] nums, int k) {
        return qsort(nums, 0, nums.length - 1, k);
    }

    int qsort(int[] nums, int left, int right, int k) {
       
        //1.随机选择一个基准元素
        int key = nums[new Random().nextInt(right - left + 1) + left];

        //2.根据基准元素,使数组分三块
        int l = left - 1, r = right + 1, i = left;
        while(i < r) {
            if(nums[i] < key) swap(nums, ++l, i++);
            else if(nums[i] == key) i++;
            else swap(nums, --r, i);
        }

        //3.分类讨论
        int b = r - l - 1, c = right - r + 1;
        if(c >= k) return qsort(nums, r, right, k);
        else if(b + c >= k) return key;
        else return qsort(nums, left, l, k - b - c); 
    }

    void swap(int[] nums, int left, int right) {
        int tmp = nums[left];
        nums[left] = nums[right];
        nums[right] = tmp;
    }
}

最小的k个数

算法思路(快速选择算法):
在快排中,当我们把数组「分成三块」之后: a----[left, l], b----[l + 1, r - 1] ,c----[r, right] ,我们可以通过计算每⼀个区间内元素的「个数」,进而推断出最小的 k 个数在哪些区间里面,直接去「相应的区间」继续划分数组即可。

class Solution {
    public int[] inventoryManagement(int[] stock, int cnt) {
        qsort(stock, 0, stock.length - 1, cnt);
    
        int[] ret = new int[cnt];
        for(int i = 0; i < cnt; i++) {
            ret[i] = stock[i];
        }
        return ret;
    }

    void qsort(int[] nums, int left, int right, int k) {

        //1.随机选择一个基准元素 + 数组分三块
        int key = nums[new Random().nextInt(right - left + 1) + left];
        int l = left - 1, r = right + 1, i = left;
        while(i < r) {
            if(nums[i] < key) swap(nums, ++l, i++);
            else if(nums[i] == key) i++;
            else  swap(nums, --r, i);
        } 

        //2.分类讨论
        int a = l - left + 1, b = r - l - 1;
        if(a > k) qsort(nums, left, l, k);
        else if(a + b >= k) return;
        else qsort(nums, r, right, k - a - b);      
    }

    void swap(int[] nums, int i, int j) {
            int tmp = nums[i];
            nums[i] = nums[j];
            nums[j] = tmp;
    }
}

归并排序

排序数组

算法思路(归并排序):
归并排序的流程充分体现了「分而治之」的思想,大体过程分为两步:

  • 分:将数组⼀分为⼆为两部分,⼀直分解到数组的长度为 1 ,使整个数组的排序过程被分为
    「左半部分排序」 + 「右半部分排序」;
  • 治:将两个较短的「有序数组合并成⼀个长的有序数组」,⼀直合并到最初的⻓度。
class Solution {
    int[] tmp;
    public int[] sortArray(int[] array) {
        tmp = new int[array.length];
        mergeSort(array, 0, array.length - 1);
        return array;
    }

    void mergeSort(int[] nums, int left, int right) {
        if(left >= right) return;

        //1.根据中间点划分区间
        int mid = (left + right) / 2;

        //2.把左右区间分别排个序
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);

        //3.合并两个有序数组
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right){
            tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];
        }
        //处理没有遍历完的数组
        while(cur1 <= mid) tmp[i++] = nums[cur1++];
        while(cur2 <= right) tmp[i++] = nums[cur2++];

        //4.还原
        for(int j = left; j <= right; j++) {
            nums[j] = tmp[j-left];
        }
    }
}

数组中的逆序对

算法思路(利用归并排序):
用归并排序求逆序对是很经典的⽅法,在合并两个有序序列的过程,快速求出逆序对的数量。
我们将这个问题分解成几个小问题,逐⼀破解这道题。

先解决第⼀个问题,为什么可以利⽤归并排序?
如果我们将数组从中间划分成两个部分,那么我们可以将逆序对产⽣的⽅式划分成三组:
• 两个数从左数组中选择
• 两个数从右数组中选择
• ⼀个数在左数组另⼀个数在右数组
根据排列组合的分类相加原理,三种情况下产⽣的逆序对的总和,正好等于总的逆序对数量。

而这个思路正好匹配归并排序的过程:
• 先排序左数组;
• 再排序右数组;
• 左数组和右数组合⼆为⼀。
因此,我们可以利⽤归并排序的过程,先求出左数组中逆序对的数量,再求出右数组中逆序对的数量,最后求出⼀个选择左边,另⼀个选择右边的逆序对的数量,三者相加即可。

解决第⼆个问题,为什么要这么做?
在归并排序合并的过程中,我们得到的是两个有序的数组。我们可以利⽤数组的有序性,快速统计出逆序对的数量,而不是将所有情况都枚举出来。

  • 排序成升序
class Solution {
    int[] tmp;
    public int reversePairs(int[] record) {
        int n = record.length;
        tmp = new int[n];
        return mergeSort(record, 0, n-1);
    }

    int mergeSort(int[] nums, int left, int right) {
        if(left >= right) return 0;

        int ret = 0;
        //1.选择一个中间点,将数组划分为两部分
        int mid = (left + right) / 2;

        //2.左半部分完成排序后返回的个数  + 右半部分完成排序后返回的个数
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1,right);

        //3.再加上合并时逆序对的个数
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right) {
            if(nums[cur1] <= nums[cur2]) {
                tmp[i++] = nums[cur1++];
            } else {
                ret += mid - cur1 + 1;
                tmp[i++] = nums[cur2++];
            }
        }

        //4.处理剩余元素
        while(cur1 <= mid) tmp[i++] = nums[cur1++];
        while(cur2 <= right) tmp[i++] = nums[cur2++];
		
		//5.还原
        for(int j = left; j <= right; j++) {
            nums[j] = tmp[j - left];
        }

        return ret;
    }
}
  • 排序成降序
class Solution {
    int[] tmp;
    public int reversePairs(int[] record) {
        int n = record.length;
        tmp = new int[n];
        return mergeSort(record, 0, n-1);
    }

    int mergeSort(int[] nums, int left, int right) {
        if(left >= right) return 0;

        int ret = 0;
        //1.选择一个中间点,将数组划分为两部分
        int mid = (left + right) / 2;

        //2.左半部分完成排序后返回的个数  + 右半部分完成排序后返回的个数
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1,right);

        //3.再加上合并时逆序对的个数
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right) {
            if(nums[cur1] <= nums[cur2]) {
                tmp[i++] = nums[cur2++];
            } else {
                ret += right - cur2 + 1;
                tmp[i++] = nums[cur1++];
            }
        }

        //4.处理剩余元素
        while(cur1 <= mid) tmp[i++] = nums[cur1++];
        while(cur2 <= right) tmp[i++] = nums[cur2++];
		
		//5.还原
        for(int j = left; j <= right; j++) {
            nums[j] = tmp[j - left];
        }

        return ret;
    }
}

计算右侧小于当前元素的个数

算法思路(归并排序):
这道题的解法与 求数组中的逆序对 的解法是类似的,但是这道题要求的不是求总的个数,而是要返回⼀个数组,记录每⼀个元素的右边有多少个元素比自己小。
在我们归并排序的过程中,元素的下标是会跟着变化的,因此我们需要辅助数组,来将数组元素和对应的下标绑定在⼀起归并,也就是在归并元素的时候,顺势将下标也转移到对应的位置上。

  • 降序(因为统计后面有多少比自己小)
class Solution {
    int[] ret;
    int[] index;//标记 nums 中当前元素的原始下标
    int[] tmpIndex;
    int[] tmpNums;

    public List<Integer> countSmaller(int[] nums) {
        int n = nums.length;
        ret = new int[n];
        index = new int[n];
        tmpIndex = new int[n];
        tmpNums = new int[n];

        //初始化 index 数组
        for(int i = 0; i < n; i++) {
            index[i] = i;
        }

        mergeSort(nums, 0, n-1);
        List<Integer> l = new ArrayList<>();
        for(int x: ret) 
            l.add(x);
        return l;
    }

    void mergeSort(int[] nums, int left, int right) {
        if(left >= right) return;

        //1.根据中间元素划分区间
        int mid = (left + right) / 2;

        //2.处理左右两个区间
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);

        //3.处理一左一右的情况
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right) { 
            if(nums[cur1] <= nums[cur2]) {
                tmpNums[i] = nums[cur2];
                tmpIndex[i++] = index[cur2++];
            } else {
                ret[index[cur1]] += right - cur2 + 1;
                tmpNums[i] = nums[cur1];
                tmpIndex[i++] = index[cur1++];
            }
        }

        //4.处理剩余的元素
        while(cur1 <= mid) {
            tmpNums[i] = nums[cur1];
            tmpIndex[i++] = index[cur1++];
        }
        while(cur2 <= right) {
            tmpNums[i] = nums[cur2];
            tmpIndex[i++] = index[cur2++];
        }
        
        //5.还原
        for(int j = left; j <= right; j++) {
            nums[j] = tmpNums[j - left];
            index[j] = tmpIndex[j - left];
        }
    }
}

翻转对

题目解析:
翻转对和逆序对的定义⼤同小异,逆序对是前⾯的数要大于后面的数。而翻转对是前面的⼀个数要
大于后面某个数的两倍。因此,我们依旧可以⽤归并排序的思想来解决这个问题。

算法思路(归并排序):
⼤思路与求逆序对的思路⼀样,就是利用归并排序的思想,将求整个数组的翻转对的数量,转换成
三部分:左半区间翻转对的数量,右半区间翻转对的数量,⼀左⼀右选择时翻转对的数量。重点就
是在合并区间过程中,如何计算出翻转对的数量。
与逆序对的问题不同的是,逆序对我们可以⼀边合并⼀边计算,但是这道题要求的是左边元素大于右边元素的两倍,如果我们直接合并的话,是⽆法快速计算出翻转对的数量的。因此我们需要在归并排序之前完成翻转对的统计。

综上所述,我们可以利⽤归并排序的过程,将求⼀个数组的翻转对转换成求 左数组的翻转对数量 + 右数组中翻转对的数量 + 左右数组合并时翻转对的数量。

  • 降序
class Solution {
    int[] tmp;
    public int reversePairs(int[] nums) {
        int n = nums.length;
        tmp = new int[n];

        return mergeSort(nums, 0, n-1);
    }

    int mergeSort(int[] nums, int left, int right) {
        if(left >= right) return 0;

        int ret = 0;
        //1.根据中间元素,将区间分为两部分
        int mid = (left + right) / 2;

        //2.求出左右两个区间的翻转对
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);

        //3.处理一左一右,先计算翻转对
        int cur1 = left, cur2 = mid + 1, i = left;
        while(cur1 <= mid) {
            while(cur2 <= right && nums[cur2]  >= nums[cur1] / 2.0) cur2++;
            if(cur2 > right) break;
            ret += right - cur2 + 1;
            cur1++;
        }

        //4.合并两个有序数组
        cur1 = left;
        cur2 = mid + 1;
        while(cur1 <= mid && cur2 <= right) tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur2++] : nums[cur1++];
        while(cur1 <= mid) tmp[i++] = nums[cur1++];
        while(cur2 <= right) tmp[i++] = nums[cur2++];
		
		//5.还原
        for(int j = left; j <= right; j++) nums[j] = tmp[j];

        return ret;
    }
}
  • 升序
class Solution {
    int[] tmp;
    public int reversePairs(int[] nums) {
        int n = nums.length;
        tmp = new int[n];

        return mergeSort(nums, 0, n-1);
    }

    int mergeSort(int[] nums, int left, int right) {
        if(left >= right) return 0;

        int ret = 0;
        //1.根据中间元素,将区间分为两部分
        int mid = (left + right) / 2;

        //2.求出左右两个区间的翻转对
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);

        //3.处理一左一右,先计算翻转对
        int cur1 = left, cur2 = mid + 1, i = left;
        while(cur2 <= right) {
            while(cur1 <= mid && nums[cur2]  >= nums[cur1] / 2.0) cur1++;
            if(cur1 > mid) break;
            ret += mid - cur1 + 1;
            cur2++;
        }

        //4.合并两个有序数组
        cur1 = left;
        cur2 = mid + 1;
        while(cur1 <= mid && cur2 <= right) tmp[i++] = nums[cur1] <=nums[cur2] ? nums[cur1++] : nums[cur2++];
        while(cur1 <= mid) tmp[i++] = nums[cur1++];
        while(cur2 <= right) tmp[i++] = nums[cur2++];
		
		//5.还原
        for(int j = left; j <= right; j++) nums[j] = tmp[j];

        return ret;
    }
}
  • 26
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值