快速排序
颜色分类
算法思路(快排思想 - 三指针法使数组分三块):
类比数组分两块的算法思想,这里是将数组分成三块,那么我们可以再添加⼀个指针.
设数组大小为 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 。
算法流程:
- 初始化 i = 0,left = -1, right = nums.length ;
- 当 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 不变(因为交换过来的数是没有被判断过的,因此需要在下轮循环中判断)
- 当循环结束之后:
- [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;
}
}