排序
文章目录
最优解:先满足时间复杂度最优,再满足最小空间
稳定性:相同元素在排序时的先后位置不变
递归:在调用子过程的时候,会把父过程放入栈中,当子过程结束会回到父的栈中找到中断行号,接着运行。
排序 | 时间复杂度 | 额外空间复杂度 | 稳定性 | 最好情况 | 最坏情况 |
---|---|---|---|---|---|
冒泡排序 | N^2 | 1 | 可以稳定 | ||
选择排序 | N^2 | 1 | 不稳定 | ||
插入排序 | N^2 | 1 | 可以稳定 | N(已排序) | N^2(逆序) |
归并排序 | NlogN | N | 可以稳定 | ||
快速排序 | NlogN | logN | 不稳定 | 每次划分值刚好在中间区域 | 阈值左右两侧不平均 |
堆排序 | NlogN | 1 | 不稳定 | ||
桶排序 | N | N | 稳定 |
一、冒泡排序
从小到大排序:从第一个数开始,两两比较,大的数往后放。
范围从0N-1…0N-2…0N-3…01,0
稳定性:可以稳定,相等的时候让后面的数做交换即可
void bubble_sort(vector<int> &a) {
if (a.size() < 2) return;
//每次找到的最小的元素被放到末尾
for (int end = a.size() - 1; end > 0; --end)
{
for (int i = 0; i < end; i++)
{
if (a[i] < a[i + 1]) swap(a[i], a[i + 1]);
}
}
}
二、选择排序
找到最小值的下标,和最后的元素交换
稳定性:不稳定,因为需要最小值需要交换
void select_sort(vector<int> &a) {
if (a.size() < 2)return;
for (int end = a.size()-1; end > 0; --end)
{
int min = end;
for (int j = 0; j < end; j++)
{
if (a[j] < a[min]) min = j;
}
swap(a[min], a[end]);
}
}
三、插入排序
扑克牌插牌:新进来的牌和之前的元素依次比较确定插入的位置
稳定性:可以稳定,插入的时候只需放在相同的后面即可
void insert_sort(vector<int> &a) {
if (a.size() < 2)return;
for (int i = 1; i < a.size(); i++)
{
for (int j = i-1; j >=0; j--)
{
if (a[j] < a[j + 1])swap(a[j], a[j + 1]);
else break;
}
}
}
更优质的写法:
void insert_sort(vector<int> &a) {
if (a.size() < 2)return;
for (int i = 1; i < a.size(); i++)
{
for (int j = i-1; j >=0 && a[j]<a[j+1]; j--)
{
swap(a[j], a[j + 1]);
}
}
}
四、归并排序:左排右排合并
递归,函数在调用的时候先分别排序自己的左右,然后再把左右合并起来
额外空间复杂度:O(N),需要额外的数组进行拷贝
稳定性:在merge的过程,只要小于等于就拷贝左边部分
void merge(vector<int> &a, int left,int mid, int right) {
if (left >= right || a.size() < 2) return;
vector<int> temp;
int l = left;
int r = mid + 1;
while (l<=mid && r <= right){ temp.push_back((a[l] < a[r] ? a[l++] : a[r++]));}
while (l<=mid){ temp.push_back(a[l++]);}
while (r<=right){ temp.push_back(a[r++]);}
for (size_t i =0;i<temp.size();){
a[left++] = temp[i++];
}
cout <<endl<< "temp: ";
for (auto ax:temp)
{
cout << ax << " ";
}
cout << endl;
}
void mergesort(vector<int> &a,int left, int right) {
if (left >= right || a.size() < 2) return;
int mid = left + (right - left)/2;
mergesort(a, left, mid);
mergesort(a, mid + 1, right);
merge(a, left, mid, right);
cout << endl;
}
复杂度分析:
T(n) = T(n/2)+T(n/2)+O(n) = 2*T(n/2)+O(n)
用T(n) = a*T(n/b)+O(n^d)公式计算复杂度,
- logba>d ,则算法复杂度为 O(Nlogba)
- logba<d,则算法复杂度为 O(Nd)
- logba=d,则算法复杂度为 O(Nd*logN)
归并排序:a=2,b=2,d=1,取相等的情况。
五、快速排序:分区递归
用荷兰国旗问题的解决方法划分区间,再对子问题进行递归调用。
时间复杂度讨论:看递归调用的次数,如果数据按照1234567排放,需要不断进行递归调用,效率很低。也就是当最右的划分值左右两侧不平均的话代价就会很高。如果划分值刚好能将左右区域划分差不多大小,则每次大小减小为原来的一半。
所以要选取优良的划分值,最好用随机选取的方法。随机快速排序:将随机选中的数和最后一个位置交换,再使用最后一个位置快速排序的方法,它的长期期望为NlogN,并且常数项很低。工程的实际表现很好。
额外空间复杂度讨论:划分区间时需要记录断点值的信息,对于最好情况断点空间为logN,类似于二分。
稳定性:不稳定:因为会交换。可以做到论文级别的,很难
// 以最后一个数为划分阈值
vector<int> partition(vector<int> &a,int l,int r) {
int left = l-1;
int right = r;
while (l < right)
{
if (a[l] < a[r]) { swap(a[l], a[left + 1]); left++; l++; }
else if (a[l] == a[r]) l++;
else if (a[l] > a[r]) { swap(a[l], a[right - 1]); right--; };
}
swap(a[right], a[r]);
return { left,right+1}; //返回小区间和大区间的下标
}
void quicksort(vector<int>&a, int left, int right) {
if (a.size() < 2 || left >= right) return;
vector<int> ind = partition(a, left, right);
quicksort(a, left, ind[0]);
quicksort(a, ind[1], right);
}
六、堆排序:堆头交换下树循环
堆
堆:-----完全二叉树结构:满二叉树或者通往满二叉树的路上,且都是从左到右排放的。要么是满的,要么是未满的层按左到右排放。
堆:----底层是数组。某节点下标: i;其子节点下标:左子 : 2 * i+1, 右子: 2 * i+2;其父节点下标:[(i-1)/2] 取整
大根堆:子树的头结点都是该子树的最大值,小根堆:子树的头结点都是该子树的最小值
上树
数组调成大根堆的过程
改成大根堆的实例:5 7 0 6 8,按照二叉树的结构放入
- 放入5
- 放入7,7>父结点5,交换 7 5 0 6 8
- 放入0,0<父结点7,不动
- 放入6,6>父结点5,交换 7 6 0 5 8,6< 父结点7,不动
- 放入8,8>父结点6,交换7 8 0 5 6,8>父结点7,交换 8 7 0 5 6
void bigRoot(vector<int> &a,int index) {//index用来确定数组尾端
for (int i = 1; i <= index; i++)
{
if (a[i]>a[(i-1)/2])
{
int l = i;
while(a[l]>a[(l-1)/2]){
swap(a[l], a[(l - 1)/2]);
l = (l-1)/2;
}
}
}
}
下树
把大根堆的根节点和最后一个位置交换,也就是把最大值放在数组尾端,
把剩下的数组调整成大根堆结构,也就是让交换后的根节点下来
根节点和自己左右两个孩子比较,与大于自己的孩子交换,不断下树
void downHill(vector<int> &a,int index) {
int i = 0;
while (2*i+1<=index)
{
int larger = ((2 * i + 2) <= index && (a[2 * i + 1] < a[2 * i + 2])) ? (2 * i + 2) : (2 * i + 1);
if (a[i] <= a[larger]) {
swap(a[i], a[larger]);
i = larger;
}
else break;
}
}
堆排序
堆排序的过程:
- 上树:建立大根堆,确定最大值,
- 交换:将大根堆的根结点和末尾交换,把数组大小视作减小了1
- 下树:将交换后的根节点和左右子树不断比较,下树
- 交换…下树…交换…下树…
- 排序结束
void heapSort(vector<int> &a) {
if (a.size() < 2)return;
int index = a.size() - 1;
bigRoot(a, a.size() - 1);
while (index>0)
{
swap(a[0], a[index--]);
downHill(a, index);
}
}
建立大根堆的过程复杂度为log1+log2+…+logN = O(N)
调整所有数的过程为O(NlogN)
缺点:不稳定,常数项大
七、桶排序
桶排序不是基于比较的排序。比如数字范围从0~200,共几亿个数。设置201个桶,放桶里,根据桶编号倒出到容器。如果容器不是栈,则桶排序稳定
计数排序
void buckersort(vector<int> &a) {
if (a.size() < 2) return;
int max = a[0];
for (int i = 0; i < length; i++){
max = a[i] > max ? a[i] : max;
}
vector<int> temp(max+1);
for (int i = 0; i < length; i++){
temp[a[i]]++;
}
for (int i = 0; i < max; i++){
for (int j = 0; i < temp[i]; j++){
temp.push_back(i);
}
}
a = temp;
}
基数排序
补充:应用
归并排序:最小和&逆序对
- 求小和: 算一个数左边比它小的数的和,对数组中所有元素进行这个操作
int minsum(vector<int> &a) {
int sum = 0;
for (size_t i = 0; i < a.size(); i++){
for (size_t j = 0; j < i; j++){
if (a[i] > a[j]) sum += a[j];
}
}
return sum;
}
int merge(vector<int> &a, int left,int mid, int right) {
if (left >= right || a.size() < 2) return 0;
vector<int> temp;
int l = left;
int r = mid + 1;
int sum = 0;
while (l<=mid && r <= right){
sum += (a[l] < a[r]) ? a[l] * (right - r+1) : 0;
temp.push_back((a[l] < a[r] ? a[l++] : a[r++]));
}
while (l<=mid){ temp.push_back(a[l++]);}
while (r<=right){ temp.push_back(a[r++]);}
for (size_t i =0;i<temp.size();){
a[left++] = temp[i++];
}
cout <<endl<< "temp: ";
for (auto ax:temp)
{
cout << ax << " ";
}
cout << endl;
return sum;
}
int sort(vector<int> &a,int left, int right) {
if (left >= right || a.size() < 2) return 0;
int mid = left + (right - left)/2;
return sort(a, left, mid) + sort(a, mid + 1, right)+merge(a, left, mid, right);
}
- 逆序对: 算数组中逆序对的数量
快速排序:荷兰国旗问题
- 根据最后一个元素将vector分成两个部分
// 以最后一个数为划分阈值
//小于等于(最后一个数)的区域,从左边界开始
//返回元素被交换的位置
//当一个数小于等于p,就直接扩展小于等于区域的范围
//当一个数大于p,将这个数交换到小于等于区的下一个位置,再扩充
int partition(vector<int> &a, int left, int right) {
int temp = a[right];
int l = left-1;
for (int i = left; i < right;i++) {
if(a[i] <= temp)
swap(a[++l],a[i]);
}
return l;
}
- 荷兰国旗问题
选择最后一个数为划分值,小于这个数的放左边,等于放中间,大于放右边。
做法:分两块区域:大于区和小于区,大于区以最后一个数为起始点,小于区以-1位置为起始点。
- 当数等于划分值,箭头往下跳
- 当数小于划分值,小于区扩张1位
- 当数大于划分值,该数和大于区外的后一个数交换,大于区扩张1大小
- 全部完成后,将划分值和最后一位交换位置
void holland(vector<int> &a) {
if (a.size() < 2) return;
int left = -1;
int cur = 0;
int right = a.size() - 1;
int last = a.size() - 1;
int temp = a[last];
while (cur<right)
{
if (a[cur] < temp) { swap(a[cur], a[left + 1]); left++; cur++; }
else if (a[cur] == temp) cur++;
else if (a[cur] > temp) { swap(a[cur], a[right - 1]); right--; };
}
swap(a[right], a[last]);
}
桶排序:计算数组排序后的相邻两数的最大差值
计算数组排序后的相邻两数的最大差值,要求时间复杂度O(N),也就是不允许用排序做。
思路:根据桶个数建立桶,范围上划分成n+1份。
比如:假设9个[0-99]的数,min=0, max = 99,准备9+1=10个桶,范围分别为[0-9],[10-19],[20-29]…[90-99],将9个数放入桶中。
边界的桶由于min,max存在必不为空,9数10桶,中间必有空桶。于是只需考虑空桶和其相邻桶之间的关系,无需考虑桶内相邻数,只需考虑桶内出现的最大值和最小值
A , B , _ , _ , C , D
first = min(B)-max(A)
second = min©-max(B)
third = min(D) - max©
return max(first,second,third)
int neighbormin(vector<int> a) {
if (a.size() < 2)return 0;
int min = a[0];
int max = a[0];
for (int i = 1; i < a.size(); i++)
{
if (a[i] > max) max = a[i];
if (a[i] < min) min = a[i];
}
int bucketlen = (max - min) / (a.size() + 1) + 1;
vector<int> bucketmin(a.size() + 1);
vector<int> bucketmax(a.size() + 1);
vector<bool> hasnum(a.size() + 1);
for (int i = 0; i < a.size(); i++)
{
int bid = (a[i] - min) / bucketlen;
bucketmax[bid] = hasnum[bid]? ((bucketmax[bid] <= a[i]) ? a[i] : bucketmax[bid]):a[i];
bucketmin[bid] = hasnum[bid] ? ((bucketmin[bid] >= a[i]) ? a[i] : bucketmin[bid]) : a[i];
hasnum[bid] = true;
}
int maxval = bucketmax[0];
int res = 0;
for (size_t i = 1; i < a.size() + 1; i++)
{
if (hasnum[i])
{
res = (bucketmin[i] - maxval) > res ? (bucketmin[i] - maxval):res;
maxval = bucketmax[i];
}
}
return res;
}
综合排序
综合排序分范围
- n < thred : insert
- n > thred :merge(自定义类型)/quick(基础类型)
理由:在数据量小的时候,常数量的优势会体现,insert的常数量很低。
基础类型的排序不关心稳定性,quick快
自定义类型的排序可能需要稳定性,比如数据库排序中id和age,先按id排,再按age排就需要id稳定,merge稳定。