冒泡排序
原理:扫描整个数组,每次比较相邻的两个数,逆序交换,第 i i i次扫描就可以找到第 i i i大的数, n − 1 n-1 n−1次扫描后,就可以完成排序
时间复杂度:O( n 2 n^2 n2)
空间复杂度:O(1)
稳定性:稳定
void bubble(vector<int> &vi)
{
for(int i = vi.size() - 1; i > 0; i--)
{
for(int j = 0; j < i; j++)
{
if(vi[j] > vi[j+1])
{
swap(vi[j], vi[j+1]);
}
}
}
}
选择排序
原理:从第一个数开始,和之后的每个数一一比较,选择最小的,第 i i i轮比较可以找到第 i i i小的数, n − 1 n-1 n−1轮后,就可以完成排序
时间复杂度:O( n 2 n^2 n2)
空间复杂度:O(1)
稳定性:不稳定
void selection(vector<int> &vi)
{
for(int i = 0; i < vi.size(); i++)
{
for(int j = i + 1; j < vi.size(); j++)
{
if(vi[i] > vi[j]) swap(vi[i], vi[j]);
}
}
}
插入排序
原理:将第1个数看作一个已经排序好的数组,从第2个数开始,在这个排序好的数组中找到自己的位置插入, n − 1 n-1 n−1次操作后,即可把后面 n − 1 n-1 n−1个数插入,完成排序
时间复杂度:O( n 2 n^2 n2)
空间复杂度:O(1)
稳定性:稳定
void insert(vector<int> &vi)
{
for(int i = 1; i < vi.size(); i++)
{
for(int j = i; j > 0; j--)
{
if(vi[j] < vi[j-1]) swap(vi[j], vi[j-1]);
else break;
}
}
}
快速排序
原理:将数组中的一个数 x x x指定为基准数(一般为第一个数),其余数与 x x x比较,比 x x x小的放在左边,比 x x x大的放在右边,对分开的这两部分,重复以上过程,直到每个部分都只有一个数。
时间复杂度:平均O( n log n n\log{n} nlogn),最差O( n 2 n^2 n2)
最差情况是每次选择的基准数都是这一段的最大或最小值,选择第一个数为基准数时,最坏情况即为排序好的数组,退化为冒泡排序
空间复杂度:O( log n \log{n} logn),最差O(n)
虽然每次划分用的空间是O(1),但是递归调用在栈区用了O( log n \log{n} logn)的空间,其中最差情况时,会递归n层
不知道用迭代实现,空间复杂度会不会减小。看了几篇网上的实现,要借助额外的数组,空间复杂度没有降低
稳定性:不稳定
void quick(vector<int> &vi,int left, int right)
{
//范围[left, right]
if(left >= right) return;
//哨兵,即基准数
//第一位做哨兵方便划分
int guard = vi[left];
int low = left, high = right;
/*
每轮循环:
high指针从右向左扫描,大于等于基准值,扫描下一位;小于基准值,指针留在这里
low指针从左向右扫描,小于等于基准值,扫描下一位;大于基准值,指针留在这里
两个指针还未相遇,交换各自指向的值,进行下一轮循环
两个指针相遇了,即就是到了基准值分割的边界,有两种情况:
high指针左移遇到了low,说明没有再遇到比基准值小的值,low所在的值必然小于等于基准值
low指针右移遇到了high,说明high停留在了比基准小的位置
两种情况下两个指针相遇的值,都小于等于基准值,即属于左边
所以这一步,将基准值换到相遇位置,相遇位置的值换到开头,完成划分
*/
while(low < high)
{
//顺序不能交换,因为最后要把相遇位置的值交换到最左端
while(low < high && vi[high] >= guard) high--;
while(low < high && vi[low] <= guard) low++;
if(low < high) swap(vi[low], vi[high]);
}
vi[left] = vi[low];
vi[low] = guard;
quick(vi,left,low-1);
quick(vi,low+1,right);
}
归并排序
原理:用分治的思想,先将数组从中间分割,并重复这一过程直到剩下长度为1的数组,之后两两合并排序恢复为原始长度的数组
时间复杂度:O( n log n n\log{n} nlogn)
每次分开,单个数组长度变为原来一半, l o g n logn logn 次后成为一个个长度为1的数组。排序恢复的过程中,因为是序列比较,数组内所有数都会参与,复杂度为 O(n),总时间复杂度即为 O( n log n n\log{n} nlogn)
空间复杂度:O( n n n)
每次合并的过程中,需要额外开辟一个数组存放新的排序
稳定性:稳定
void merge(const vector<int>::iterator &begin,const vector<int>::iterator &end,const vector<int>::iterator &mid)
{
vector<int> tmp;
auto p1 = begin,p2 = mid;
//将两个序列合并后的结果放入tmp
while(p1 != mid || p2 != end)
{
if(p1 != mid && p2 != end)
{
if(*p1 <= *p2)
{
tmp.push_back(*p1);
p1++;
}
else
{
tmp.push_back(*p2);
p2++;
}
}
else if(p1 != mid)
{
tmp.push_back(*p1);
p1++;
}
else
{
tmp.push_back(*p2);
p2++;
}
}
//将tmp的内容替换回原数组
p1 = begin;
for(int &val:tmp)
{
*p1 = val;
p1++;
}
}
void mergeSort(const vector<int>::iterator &begin,const vector<int>::iterator &end,int len)
{
if(len>1)
{
int midlen = len >> 1;
const auto mid = begin+midlen;
//分成两部分
if(len & 1)
{
mergeSort(begin,mid,midlen);
mergeSort(mid,end,midlen+1);
}
else
{
mergeSort(begin,mid,midlen);
mergeSort(mid,end,midlen);
}
//将两部分合并
::merge(begin,end,mid);
}
return;
}
堆排序
原理:先从数组上构造一个大顶堆,此时堆顶元素为最大值,将其与堆的末尾元素(即数组末尾)交换,从堆内去除末尾元素,调整堆使其满足大顶堆性质,重复以上过程直到堆内元素只剩一个
大顶堆:满足根节点值大于子节点的平衡二叉树,类似还有小顶堆
时间复杂度:O( n log n n\log{n} nlogn)
建堆时,从最后一个非叶子结点开始,将当前子树的最大元素交换到当前子树的根节点,且保持它的子树满足最大堆(即如果交换过去的值小于它的叶节点,要继续递归交换下去,直到叶子节点均小于根节点),一共是 n / 2 n/2 n/2 个子树,子树最深交换 l o g n logn logn 层(即整个堆的根节点是最小值时)
排序维护堆时,交换 n − 1 n-1 n−1 次,交换后调整堆的过程最多 l o g n logn logn 次交换
空间复杂度:O( 1 1 1)
稳定性:不稳定
//调整以rt为根的树,使最大值在rt
void adjustHeap(vector<int> &vi, int len,int rt)
{
for (int j = rt << 1 | 1; j < len; j = j << 1 | 1)
{
if (j+1 < len && vi[j] < vi[j + 1]) j++;
//如果子节点大于根节点,交换,并继续迭代过程
if (vi[rt] < vi[j])
{
swap(vi[rt], vi[j]);
rt = j;
}
else break;
}
}
void heapSort(vector<int> &vi, int len){
//建立堆,从最后一个非叶子节点开始,保证遍历到所有子节点;自底向上,保证根节点是最大值
for(int i = len >> 1; i >= 0; i--) adjustHeap(vi,len,i);
//此时堆顶是最大的元素(大顶堆),和末尾元素交换,调整堆,并使末尾向前
for(int i = len - 1; i > 0; i--)
{
swap(vi[0],vi[i]);
adjustHeap(vi,i,0);
}
}