归并排序(稳定)
时间复杂度: O(N * logN ) 。空间复杂度 O(N)
归并排序主要是分治的思想:
将一段数组以中间点mid划分成两半,让它们各自去下一层递归,继续对半划分,直到这个数组只有一个元素时, 开始向上返回。每次返回都会将这这一层的两半数组(已经有序了)进行合并,合并成一个有序数组,继续向上返回,直到整个数组有序。
注意事项:在合并有序数组的时候,我们需要一个额外的容器,合并完之后再把结果填回到原数组中,对于这个额外的数组,建议使用全局的变量,这样能节省每层创建和销毁的开销,从而提高效率。 代码如下:
class Solution {
public:
vector<int> tmp; // 全局的数组
void mergeSort(vector<int>& nums,int left,int right)
{
if(left >= right) return;
int mid = (right - left) / 2 + left;
mergeSort(nums,left,mid);
mergeSort(nums,mid + 1,right);
// 开始合并两个数组
int i = 0,l = left,r = mid + 1;
while(l <= mid && r <= right)
tmp[i++] = nums[l] <= nums[r] ? nums[l++] : nums[r++];
while(l <= mid) tmp[i++] = nums[l++];
while(r <= right) tmp[i++] = nums[r++];
// 将结果填回到原数组中
for(int i = left; i <= right; ++i)
nums[i] = tmp[i - left];
}
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
tmp.resize(n); // 记得把空间开好
mergeSort(nums,0,n - 1);
return nums;
}
};
快速排序(不稳定)
时间复杂度:O(N * logN),空间复杂度 : O(logN)
也是分治的思想。用数组划分的形式来对数组进行排序。
大致流程:
首先选定一个元素基准值key,将数组划分为 小于等于key 和大于key的两个部分。然后每一层继续划分,直到只有一个元素为止。数组划分也是快排最重要的一部分。但是如果这个数组中有很多重复值时,最差情况下,基准值会到数组的最左或者最右端,这样就导致了时间复杂度退化到了O(N^2)。
刚刚的想法是将数组分为两块,我们可以用将数组分三块的方式进行优化:
对于数组元素 == key的那一块,我们后续就不用再对它进行划分了。
核心流程:
定义三个变量, i :扫描数组,left :代表左区间已探知的下标,因为刚刚开始时一个都还没有探索,所以它相当于数组的 0 - 1 = -1的位置。 right:代表右区间已探索的下标,同理。
另外就是遍历的细节了:分为三种情况:
1. nums[i] < key,swap(nums[++left],nums[i++])。这里的i++很好理解,因为这里的元素已经探索过了。
2.nums[i] == key ,i++。
3.nums[i] > key。 swap(nums[++right],nums[i])。注意:这里的i是不要++的,因为我们遍历数组是从左往右遍历的,从右侧交换过来的数据是没有遍历过的,因此i不需要向前移动。
遍历完之后,我们只要再让 <key和>key的区间再进行划分即可。
这样如果数组中全是重复的元素时,反而可以把时间复杂度降至O(N)。
关于其他优化的地方:
对于基准值的选择:
可以用简单粗暴的 三数取中法,但是如果想让算法时间复杂度接近 O(N LogN),应该使用随机的方式取基准值(算法导论中提到过)。
class Solution {
public:
int GetRandom(vector<int>& nums,int left,int right)
{
int r = rand();
r %= right - left + 1;
r += left;
return nums[r];
}
void qsort(vector<int>& nums,int l,int r)
{
if(l >= r) return;
int key = GetRandom(nums,l,r); // 获取基准值
// 数组分三块
int left = l - 1,right = r + 1,i = l;
while(i < right) // 注意这里是小于right,遍历过的区间不要再遍历了
{
if(nums[i] < key) swap(nums[++left],nums[i++]);
else if(nums[i] == key) ++i;
else swap(nums[--right],nums[i]);
}
qsort(nums,l,left);
qsort(nums,right,r);
}
vector<int> sortArray(vector<int>& nums) {
srand(time(NULL)); // 种随机数种子
qsort(nums,0,nums.size() - 1);
return nums;
}
};
堆排序(不稳定)
先在原先数组的基础上构造大根堆(时间复杂度nlogn),向下调整,从倒数第一个父结点开始。
再依次弹出最大元素(每次弹出的时间复杂度为logk,k为当前大根堆中元素数目),弹出的元素就可以形成一个有序的数组。每弹出一次都要进行一次向下调整,把剩下的最大的元素放到堆顶的位置。
时间复杂度:O (N * logN),空间复杂度: O(1)
先简单复习一下堆的实现
#pragma once
#include<iostream>
#include<vector>
#include<functional>
namespace hzj
{
using namespace std;
template<class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue
{
private:
void AdjustUp(int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (_com(_c[parent],_c[child]))
{
std::swap(_c[parent], _c[child]);
child = parent;
parent = (child - 1) / 2;
}
else break;
}
}
void AdjustDown(int parent)
{
int child = parent * 2 + 1;
int len = size();
while (child < len)
{
if (child + 1 < len && _com(_c[child] , _c[child + 1])) child++;
if (_com(_c[parent],_c[child]))
{
std::swap(_c[child], _c[parent]);
parent = child;
child = child * 2 + 1;
}
else break;
}
}
public:
priority_queue()
{}
template<class inputiterator>
priority_queue(inputiterator first, inputiterator last)
{
inputiterator it = first;
while (it != last)
{
_c.push_back(*it);
++it;
}
for (int i = (size() - 2) / 2; i >= 0;i--)
{
AdjustDown(i);
}
}
bool empty()const
{
return _c.empty();
}
size_t size()const
{
return _c.size();
}
const T& top()
{
return _c[0];
}
void push(const T& x = new T())
{
_c.push_back(x);
AdjustUp(size() - 1);
}
void pop()
{
std::swap(_c[0], _c[size() - 1]);
_c.pop_back();
AdjustDown(0);
}
private:
Container _c; // 容器
Compare _com; // 比较方法
};
}
代码:
class Solution {
public:
void buildMaxHeap(vector<int>& nums)
{
int n = nums.size();
// 从倒数第一个有子结点的父结点开始调整
for(int i = (n - 1) / 2; i >= 0; --i)
{
AdjustDown(nums,i,n);
}
}
// 向下调整,i表示调整的起点,n表示需要调整区域的大小,在排序时是不断变小的,建堆时不变
void AdjustDown(vector<int>& nums,int i,int n)
{
while(i * 2 + 1 < n)
{
int next = i; // 方便后续用于更新i的值
int lson = i * 2 + 1; // 左孩子
int rson = i * 2 + 2; // 右孩子
// 找到最大的那个孩子的下标,这里决定大根堆还是小根堆
if(lson < n && nums[lson] > nums[i]) next = lson;
if(rson < n && nums[rson] > nums[next]) next = rson;
if(i != next) // 找到了就更新i的值,还有记得交换结点的值
{
swap(nums[i],nums[next]);
i = next;
}
else // 反之说明不用继续调整了
{
break;
}
}
}
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
buildMaxHeap(nums); // 建堆(此处为大根堆)
// 依次弹出最大的元素,把整个数组全部弹出
for(int i = n - 1; i >= 0; --i)
{
swap(nums[0],nums[--n]); // 弹出的元素可以直接放在数组末尾
AdjustDown(nums,0,n); // 向下调整,把剩下最大的元素放到堆顶
}
return nums;
}
};
插入排序(稳定)
在前 0 ~ i - 1 元素有序的情况下,(依次)将第 i 个元素插入前面已经有序的小序列,先使用二分查找找到插入位置的下标index,然后将区间[index + 1,i] 往后移动,再把原来i位置的值插入到index位置,使其有序。
时间复杂度:O(N ^ 2 ),空间复杂度 O(1)。
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
for(int i = 1; i < n; ++i)
{
// 如果已经有序了那么就直接插入
if(nums[i] >= nums[i - 1]) continue;
int l = 0,r = i - 1;
// 二分查找 时间复杂度 O(logN)
while(l < r)
{
int mid = (r - l) / 2 + l;
if(nums[i] > nums[mid]) l = mid + 1;
else r = mid;
}
int index = l;
int tmp = nums[i];
// 移动数组 时间复杂度 O(N)
for(int k = i; k >= index + 1; --k)
{
nums[k] = nums[k - 1];
}
nums[index] = tmp;
}
return nums;
}
};
希尔排序(不稳定)
改进的插入排序(优化:原数组的一个元素距离正确位置很远的情况)
先让间隔 h 的元素有序,在使得间隔为 h / 2,一直缩小,一直到 h = 1(此时数组有序)。
也就是先将数组划分为 n / h 个组,预排序的作用就是为了让每个组之间尽可能的有序。
当间隔等于1时就相当于再进行了一次插入排序,而插入排序是数据在有序的情况下时间复杂度为O(N),在逆序的情况下最差为O(N ^ 2 ),经过预处理后,数组已经尽可能的接近有序了 。
也就是在插入排序前先进行预排序。
时间复杂度:O(N * logN) 空间复杂度: O(1)。
class Solution {
public:
void shellSort(vector<int>& nums,int gap,int i)
{
int j = i - gap,tmp = nums[i];
for(; j >= 0; j -= gap)
{
if(tmp < nums[j]) // 向后移动gap步
{
nums[j + gap] = nums[j];
}
else // 到这里就停止了,这里就是tmp该插入的位置
{
break;
}
}
nums[j + gap] = tmp;
}
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
for(int gap = n / 2; gap >= 1; gap /= 2)
{
// 对各个分组进行插入分组
for(int i = gap; i < n; ++i)
{
shellSort(nums,gap,i);
}
}
return nums;
}
};
另一种写法:
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
int gap = n;
while(gap > 1)
{
gap = gap / 3 + 1;
// gap /= 2; 也可以
for(int i = 0; i < n - gap; ++i)
{
int tmp = nums[i + gap];
int end = i;
while(end >= 0)
{
if(nums[end] > tmp) // 向后移动
{
nums[end + gap] = nums[end];
end -= gap;
}
else
{
break;
}
}
nums[end + gap] = tmp;
}
}
return nums;
}
};
冒泡排序(稳定)
比较相邻的元素,如果第一个比第二个大,那么就交换
时间复杂度:O(N ^ 2 ),空间复杂度:O(1)
代码:
class Solution {
public:
vector<int> sortArray(vector<int>& nums)
{
// bubbleSort 冒泡
int n = nums.size();
for (int i = 0; i < n - 1; ++i)
{
bool flag = false;
for (int j = 0; j < n - 1 - i; ++j)
{
if (nums[j] > nums[j + 1])
{
swap(nums[j], nums[j + 1]);
flag = true;
}
}
if (flag == false) break; //无交换,代表当前序列已经最优
}
return nums;
}
};
选择排序(不稳定)
用i下标从左往右遍历数组,将区间 [i,n - 1]的最小值与i位置的值进行交换。
时间复杂度:O(N ^ 2),空间复杂度:O(1)
代码:
class Solution {
public:
vector<int> sortArray(vector<int>& nums)
{
// selectSort 选择排序
int minIndex;
int n = nums.size();
for (int i = 0; i < n - 1; ++i)
{
minIndex = i;
for (int j = i + 1; j < n; ++j)
{
if (nums[j] < nums[minIndex])
{
minIndex = j;
}
}
swap(nums[i], nums[minIndex]);
}
return nums;
}
};