
解法一:插入排序:
最经典的算法,常数时间非常少,有效练手
核心思想有点像dp,最长上升子序列(顺便膜拜下二分做法)处理好前面后,处理当前元素,做一次遍历即可。
进阶问题:选择排序是一种两两比较的贪心算法,为什么小数组用插入排序,不用选择排序
- 插入排序有dp思想,左边是已排序数组,期望中可以减少比较数目,平均比一半就能找到自己的位置,而选择排序是固定次数,比较多
- 具有排序稳定性,是一种更为普适的算法,选择排序不具有这种特性。如数组【2,2,1],会导致两个2位置互换。
cpp
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
for(int i=1;i<n;i++)
{
int tmp = nums[i];
int j = i-1;
for(j=i-1;j>=0;j--)
{
if(nums[j] > tmp)nums[j+1] = nums[j];
else break;
}
nums[j+1] = tmp;
}
return nums;
}
};
解法二:归并排序
归并排序是分治算法的一种,必须要用到额外数组,可以用一个数组代替。和回溯类似,均匀处理两边后返回,再用双指针对两边排好序的数组进行处理
用到的两个高阶技巧:
- 排序稳定性:在合并两个有序数组阶段,用了nums[p1] <= nums[p2],这是为了保证相等的元素位置不发生变化,这样在多维度排序时,不会导致前一个尺度发生变化,是排序中一种非常重要的特性。
- 使用插入排序减少常数时间开销:在小数据量时调用,会有一些好处。
cpp
typedef vector<int> V;
class Solution {
public:
void insert_sort(V& nums, int l, int r)
{
int n = r-l+1;
if(n <= 1)return;
for(int i=l+1;i<=r;i++)
{
int j = i-1, tmp = nums[i];
while(j>=l && nums[j] > tmp)
{
nums[j+1] = nums[j];
j--;
}
nums[j+1] = tmp;
}
}
void merge_sort(V& nums, int l, int r, V&tmp)
{
int len = r-l+1;
if(len <= 10)
{
insert_sort(nums, l, r);
return;
}
int mid = l + (r-l)/2;
merge_sort(nums, l, mid, tmp);
merge_sort(nums, mid+1, r, tmp);
int p1 = l, p2 = mid+1, p_tmp = l;
while(p1 <= mid && p2 <= r)
{
if(nums[p1] <= nums[p2])
{
tmp[p_tmp++] = nums[p1++];
}
else tmp[p_tmp++] = nums[p2++];
}
while(p1 <= mid)tmp[p_tmp++] = nums[p1++];
while(p2 <= r)tmp[p_tmp++] = nums[p2++];
for(int i=l;i<=r;i++)nums[i] = tmp[i];
}
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
V tmp(n, 0);
merge_sort(nums, 0, n-1, tmp);
return nums;
}
};
解法三:快速排序
1 双路快排
这里小数据量没有切插入排序,实际也是可以切的,懒得写了(狗头)
这个算法样例会卡在一个2,一堆3(可能几万个)的情况,这种情况下,快排会退化到O(N方),所以我们要考虑一种更为优化的算法,即三路快排。
cpp
typedef vector<int> V;
class Solution {
public:
void moveMedian(V& nums, int l, int r)
{
int mid = l + (r-l)/2;
if(nums[l] >= nums[r] && nums[l] <= nums[mid])swap(nums[l], nums[r]);
else if(nums[l] <= nums[r] && nums[l] >= nums[mid])swap(nums[l], nums[r]);
else if(nums[mid] <= nums[l] && nums[mid] >= nums[r])swap(nums[mid], nums[r]);
else if(nums[mid] >= nums[l] && nums[mid] <= nums[r])swap(nums[mid], nums[r]);
}
void qsort(V& nums, int l, int r)
{
int len = r-l+1;
if(len <= 1)return;
// partition
moveMedian(nums, l, r);
int pivot = nums[r];
int ct = l; // 指针记录到第一个不符合条件的位置 或待处理位置
for(int i=l;i<r;i++)
{
if(nums[i] <= pivot)
{
swap(nums[i], nums[ct]);
ct++;
}
}
swap(nums[r], nums[ct]);
// printf("%d %d %d\n", l, r, ct);
qsort(nums, l, ct-1);
qsort(nums, ct+1, r);
}
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
qsort(nums, 0, n-1);
return nums;
}
};
- 三路快排
三路快排对双路快排的逻辑做了一定改进,但是核心是一样的,右边都是从r-1开始,把大于和小于的都放好后,中间的就自动都是相等的部分了。然后再交换一下,第一个大于的元素,和pivot即可。
注意难点:不能for循环,因为当右边元素换过来时,这个元素是还没有检查的,所以需要再检查一次。左边的能换过来是因为已经检查过,如果是小于的,那一定是i = p1,不然不会留在那个位置,如果是等于的,那换到中间就对了。
cpp
typedef vector<int> V;
class Solution {
public:
void moveMedian(V& nums, int l, int r)
{
int mid = l + (r-l)/2;
if(nums[l] >= nums[r] && nums[l] <= nums[mid])swap(nums[l], nums[r]);
else if(nums[l] <= nums[r] && nums[l] >= nums[mid])swap(nums[l], nums[r]);
else if(nums[mid] <= nums[l] && nums[mid] >= nums[r])swap(nums[mid], nums[r]);
else if(nums[mid] >= nums[l] && nums[mid] <= nums[r])swap(nums[mid], nums[r]);
}
void qsort(V& nums, int l, int r)
{
int len = r-l+1;
if(len <= 1)return;
// partition
moveMedian(nums, l, r);
int pivot = nums[r], p1 = l, p2 = r-1;
int i = l;
while(i <= p2)
{
if(nums[i] < pivot)
{
swap(nums[i], nums[p1++]);
i++;
}
else if(nums[i] > pivot)swap(nums[i], nums[p2--]);
else
{
i++;
}
}
swap(nums[r], nums[p2+1]);
qsort(nums, l, p1-1);
qsort(nums, p2+2, r);
}
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
qsort(nums, 0, n-1);
return nums;
}
};