本题是典型的数组排序,要求时间复杂度在O(nlogn),
借此复习一下quick sort, merge sort和桶排序。
思路:
1.quick sort
quick sort的平均时间复杂度在O(n), 但是最坏的情况是O(n2),
它的主要思想是在数组中选一个pivot,
一般3种选法:选最左边的数字,选最右边的数字,随机数字。
下面的图是选最后一个数字作为pivot.
然后进行操作,目的是让所有比pivot小的数字都在pivot左边,
所有比pivot大的数字都在它的右边。
具体怎么操作呢,看下面的流程:
假设把比pivot小的数字放在 i 的位置, 刚开始 i 在-1的位置,
j 从左到右遍历,注意我们现在的目的是把比pivot小的数字都换到左边(i 的位置),
所以遇到arr[ j ] > pivot时不需要操作,继续往右走,
当遇到arr[ j ] <= pivot时,把arr[ j ]换到 i 的位置,i每次先往右移一格再换(因为刚开始是-1)。
这样,比pivot小的数字会慢慢地换到比它大的数字左边。
当 j 到达 pivot前面一个数字时,停止,这时i 和 它前面的数字都比pivot小,
i+1 ~ pivot前面一个数字都比pivot大,
现在把pivot换到 i+1的位置,就达到了目的(pivot前面的数字都比它小,后面的数字都比它大)。
这里就不贴这么详细的过程图了,有兴趣的看链接
这个操作数组的过程叫做partition, 算法流程见下图:
整个quick sort的流程如下
它是一个分而治之的过程,每次得到pivot的位置,
然后对left ~ pivot, pivot+1 ~ right的部分再quick sort, 直到无法再分为止。
随机选pivot会比固定选的效果好,平均时间复杂度更易达到O(n),
如果随机选数字,把随机选的数字换到最右边,剩下的步骤和上面一样。
本题选最右边的数字作为pivot出现了TLE, 换成随机选通过,但不是很快。
class Solution {
Random random = new Random();
public int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length-1);
return nums;
}
void quickSort(int[] nums, int l, int r) {
if(l < r) {
int pivot = partition(nums, l, r);
quickSort(nums, l, pivot-1);
quickSort(nums, pivot+1, r);
}
}
int partition(int nums[], int l, int r) {
int pivot = random.nextInt(r+1-l) + l;
swap(nums, pivot, r);
int low = l-1;
for(int high = l; high < r; high++) {
if(nums[high] <= nums[r]) {
low ++;
swap(nums, low, high);
}
}
swap(nums, low+1, r);
return (low+1);
}
void swap(int[] nums, int low, int high) {
int tmp = nums[low];
nums[low] = nums[high];
nums[high] = tmp;
}
}
2.merge sort
merge sort的时间复杂度在O(nlogn), 所以适合大规模数字排序的情况,
它常和其他排序算法(比如quick sort)结合起来使用,以提升整体的效率。
它是一个先分再合的过程,如下。
已知数组最左边下标是l, 最右边下标是r, 那么可以计算中间下标mid,
然后拆分成两部分:l ~ mid, mid+1 ~ r
一直到不能再拆分为止。
然后在把两两部分排序合起来,最后形成整个数组。
两两合起来的过程就是两个数组(分别copy出来)的下标都指向0,然后一个一个数字比较,把较小的先存起来(直接存到原数组),然后移动指针,比较下一个,直到两个数组都遍历完。
算法流程如下:
class Solution {
public int[] sortArray(int[] nums) {
mergeSort(nums, 0, nums.length-1);
return nums;
}
void mergeSort(int[] nums, int l, int r) {
if(l >= r) return;
int mid = l + (r-l)/2;
mergeSort(nums, l, mid);
mergeSort(nums, mid+1, r);
merge(nums, l, mid, r);
}
void merge(int[] nums, int l, int m, int r) {
int leftLen = m - l + 1;
int rightLen = r - m;
int[] leftArr = new int[leftLen];
int[] rightArr = new int[rightLen]; //mid+1 ~ r
//把对应的数字copy进去
for(int i = 0; i < leftLen; i++) leftArr[i] = nums[l+i];
for(int i = 0; i < rightLen; i++) rightArr[i] = nums[m+1+i];
int leftIdx = 0;
int rightIdx = 0;
int numsIdx = l;
//sort & merge
while(leftIdx < leftLen && rightIdx < rightLen) {
if(leftArr[leftIdx] <= rightArr[rightIdx]) {
nums[numsIdx] = leftArr[leftIdx];
leftIdx ++;
} else {
nums[numsIdx] = rightArr[rightIdx];
rightIdx ++;
}
numsIdx ++;
}
//把剩余的数字copy进去
while(leftIdx < leftLen) {
nums[numsIdx] = leftArr[leftIdx];
leftIdx ++;
numsIdx ++;
}
while(rightIdx < rightLen) {
nums[numsIdx] = rightArr[rightIdx];
rightIdx ++;
numsIdx ++;
}
}
}
3.桶排序
桶排序,顾名思义就是每个数字对应一个桶,统计每个桶里面这个数字出现的次数。
然后从小到大把数字摆放进新的数组,每个数字摆放它出现的次数。
这种方法做到了O(n).
可以用hashMap统计每个数字出现的次数,这里直接用数组下标当作key,
但是数字范围太大,太浪费数组的空间,于是可以先找出最小值min 和 最大值 max,
数组的长度只需要max - min + 1即可。
最后不摆放进新的数组,而是直接摆放进原数组。
class Solution {
public int[] sortArray(int[] nums) {
int min = nums[0];
int max = nums[0];
for(int num: nums) {
min = Math.min(num, min);
max = Math.max(num, max);
}
int len = max - min + 1;
int[] cnt = new int[len];
for(int num : nums) {
cnt[num-min]++;
}
int idx = 0;
for(int i = 0; i < len; i++) {
while(cnt[i] > 0) {
nums[idx++] = min;
cnt[i]--;
}
min++;
}
return nums;
}
}