文章目录
- 1. 快速排序
- 2. 归并排序
- 3. 数组切分问题(Partition Array)
- 3.1 [LintCode-31. Partition Array](https://www.lintcode.com/problem/partition-array/description)
- 3.2 [LintCode373. Partition Array by Odd and Even](https://www.lintcode.com/problem/partition-array-by-odd-and-even/description)
- 3.3 [LintCode49.Sort Letters by Case](https://www.lintcode.com/problem/sort-letters-by-case/description)
- 3.4 [LintCode144-Interleaving Positive and Negative Numbers](https://www.lintcode.com/problem/interleaving-positive-and-negative-numbers/description)
- 4. QuickSelect问题
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];
}
}