功能:七种常见经典排序算法
作者:wikiwen
日期:2018年4月20日
参考文章:
http://yansu.org/2015/09/07/sort-algorithms.html
https://zh.wikipedia.org/wiki/排序算法
https://en.wikipedia.org/wiki/Quicksort
分类:
#include
#include
using namespace std;
/***********************************【插入排序】*************************************************************/
/*
* 排序方法:直接插入排序(插入排序)
* 思路:不断构建有序序列,对于剩余未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入
* 在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
* 时间复杂度O(n)~O(n^2)(分别表示最好情况、最好情况,下同),平均时间复杂度O(n^2)
空间复杂度О(n) total, O(1) auxiliary
*/
void insert_sort(vector<int>& a)
{
int n = a.size();
for (int i = 1; i <= n - 1; i++)//i = 1~n-1, j = i-1~0
{
int temp = a[i];
for (int j = i - 1; j >= 0; j--)//从已排序序列中从后往前比较
{
if (temp < a[j])//用原始a[i](temp)依次与之前数据比较,插入合适位置
{
a[j + 1] = a[j];//往后移
a[j] = temp;
}
else
break;//如果a[i]位置适当,直接跳出循环
}
}
}
/*
* 排序方法:希尔排序(插入排序)
* 思路:希尔排序(也称缩小增量排序算法)是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位(提升比较的跨度)
* 具体做法:希尔排序将数组按照一定步长分成几个子数组进行排序(直接插入),再对已经有序的子数组,通过逐渐减短步长来完成最终排序
* 时间复杂度O(nlogn)~O(n^2)(分别表示最好情况、最好情况,下同),平均时间复杂度取决于步长序列
空间复杂度О(n) total, O(1) auxiliary
*/
void shell_sort(vector<int>& a)
{
int n = a.size();
for (int gap = n / 2; gap > 0; gap /=2)//以二分步长为例
{
for (int i = gap; i < n; i++)//i = gap~n-1, j = i-gap~0(间隔gap)
{
int temp = a[i];
for (int j = i - gap; j >= 0; j -= gap)//直接插入排序
{
if (temp < a[j]) // 用原始a[i](temp)依次与之前数据比较,插入合适位置
{
a[j + gap] = a[j];//往后移
a[j] = temp;
}
else
break;
}
/*for (int j = i - gap; j >= 0 && temp < a[j]; j -= gap)
{
a[j + gap] = a[j];//往后移
}
a[j + gap] = temp;//插入*/
}
}
}
/***********************************【交换排序】*************************************************************/
/*
* 排序方法:冒泡排序(交换排序)
* 默认排序方式:增序
* 思路:每趟冒泡找出一个最大值到末尾
* 时间复杂度 O(n^2),
空间复杂度是O(1)(指辅助空间)
*/
void bubble_sort(vector<int>& a)
{
for (int i = 0; i < a.size() - 1; i++)//作n-1趟排序,每一趟找最大值到末尾
{
for (int j = 0; j < a.size() - 1 - i; j++)//每一趟,从a[0]找到a[n-2-i],分别与a[1]到a[n-1-i]比较
{
if (a[j] > a[j + 1]) swap(a[j], a[j + 1]);//交换元素(记录)
}
}
}
/*
* 排序方法:快速排序(交换排序)
* 思路:快速排序是利用分治法实现的一个排序算法,快速排序和归并排序不同,
它不是一半一半的分子数组,而是选择一个基准数,把比这个数小的挪到左边,
把比这个数大的移到右边。然后不断对左右两部分也执行相同步骤,直到整个数组有序。
* 说明:对冒泡排序的一种改进,在所有同数量级的排序方法中,快速排序的常数因子k最小,
就平均时间而言,快速排序是目前被认为是最好的一种内部排序方法。
* 时间复杂度O(nlogn)~ O(n^2),平均时间复杂度O(nlogn)(平均logn次partition,每次partitions时间复杂度O(n))
空间复杂度O(logn)(指额外空间复杂度,递归造成的栈空间的使用)
*/
int partition(vector<int>& a, int left, int right);
void quick_sort(vector<int>& a, int left, int right)
{
if (left >= right) return; //递归出口
int pivotLoc = partition(a, left, right); //将序列一分为二
quick_sort(a, left, pivotLoc - 1); //对左子表递归排序
quick_sort(a, pivotLoc + 1, right); //对右子表递归排序
}
//分割函数
//选择一枢轴分割序列,并返回其位置
int partition(vector<int>& a, int left, int right)
{
//1. 初始化,用序列的第一个元素作为枢轴(也可用其他元素,但是要把枢轴元素暂时放到起始位置,方便后续交换,如三数中值初始化枢轴)
//2. median3
//3. srand((unsigned)time(NULL)); 用随机法较简单
// int pivotPos = rand() % (right - left) + left; //得到随机基元的位置(下标)
// swap(a[pivotPos], a[left]) //将枢轴暂时放入起始位置
int pivot = left;
while (left < right) //从序列的两端交替地向中间扫描(在此循环中a[pivot]不动,退出循环后在被交换)
{
//先right再left以使left最后指向等于枢轴位置元素或者小于枢轴位置的元素(这样与a[left]交换才不会出错)
while (left < right&&a[right] >= a[pivot]) right--;//找到本次扫描中第一个不满足枢轴规律的高位数
while (left < right&&a[left] <= a[pivot]) left++; //找到本次扫描中第一个不满足枢轴规律的低位数
swap(a[left], a[right]); //交换以使满足枢轴规律
}//最后结果是left和right均指向枢轴位置
swap(a[left], a[pivot]);//将枢轴移动到位
return left; //返回枢轴位置
}
//三数中值初始化枢轴(选择序列两端和中间数的中值作为枢轴,减少了与预排序输入时带来的问题)
int median3(vector<int>& a, int left, int right)
{
int center = (left + right) / 2;
if (a[center] < a[left])
swap(a[left], a[center]);
if (a[right] < a[left])
swap(a[left], a[right]);
if (a[right] < a[center])
swap(a[center], a[right]);
// Place pivot at position 第一个位置
swap(a[center], a[left]); //将枢轴暂时放入起始位置
return a[left];
}
/***********************************【选择排序】*************************************************************/
/*
* 排序方法:简单选择排序(选择排序)
* 思路:每趟遍历选择最小值,放于序列前面(递增排序时)
* 时间复杂度O(n^2),空间复杂度О(n) total, O(1) auxiliary
*/
void select_sort(vector<int>& a)
{
int len = a.size();
for (int i = 0; i < len - 1; i++)
{
for (int j = i+1; j < len; j++)//每一趟遍历选择最小值,放于序列前面
{
if (a[j] < a[i]) swap(a[j], a[i]);
}
}
}
/*
* 排序方法:堆排序(选择排序)
* 思路:最大堆(大顶堆)的第一位总为当前堆中最大值,所以每次将最大值输出后,
调整堆即可获得下一个最大值,通过一遍一遍执行这个过程就可以得到前k大元素,或者使堆有序
(每次选择最大值,放于序列后面,调整a[0~i-1],i为末尾序号,i=len-1~0)
* 时间复杂度O(nlogn),空间复杂度O(n) total, O(1) auxiliary
* 其他说明:
父结点均大于或小于左右子结点的树为堆,其为一个完全二叉树
i节点的父节点 parent(i) = floor((i-1)/2)
i节点的左子节点 left(i) = 2i + 1 (i从0开始,堆顶元素索引为0)
i节点的右子节点 right(i) = 2i + 2
(堆从上到下,从左到右,序号依次增加,堆顶元素序号为0)
* 步骤:
1.构造最大堆(Build_Max_Heap):从无序序列开始建堆,从最后一个非终端结点(父结点)开始调用堆调整算法,一直到第一个非终端结点调整好
2.最大堆调整(Max_Heapify):除了当前父结点,序号之后的部分都满足堆的定义,调整当前结点,使满足堆的定义
3.堆排序(HeapSort):输出堆顶元素,以堆中最后一个元素代替,重复执行2,直到所有堆顶元素输出
*/
void max_heapify(vector<int>& a, int start, int end);
void heap_sort(vector<int>& a)
{
int len = a.size();
//构造大顶堆(n/2*logn):从最后一个父结点开始进行堆调整(从a[len/2-1]调整到a[0])
for (int i = len / 2 - 1; i >= 0; i--)//最后一个结点序号为len-1,带入公式得其父结点序号为len/2-1
{
max_heapify(a, i, len - 1); //end固定,start变
}
//堆排序(n*logn):输出堆顶元素,以堆中最后一个元素代替,重复进行堆调整,直到所有堆顶元素输出(从a[len-1]调整到a[1])
for (int i = len - 1; i > 0; i--)
{
swap(a[0], a[i]); //将堆顶元素(当前最大值)换至末尾,再对剩余的元素进行堆调整,不断重复这个过程得到增序排列的数组
max_heapify(a, 0, i - 1); //调整a[0~i-1]元素,以供输出其中的最大值,start固定,end动
}
}
//除了a[start],其余部分都符合堆定义,函数功能为将a[start~end]调整为堆
//一次堆调整操作平均时间复杂度为O(logn)
void max_heapify(vector<int>& a,int start,int end)
{
int dad = start;
int son = 2 * dad + 1;
while (son <= end)
{
if (son + 1 <= end&&a[son + 1] > a[son]) son++;//选择较大的子结点,供之后与父结点比较
if (a[dad] < a[son])//如果不符合堆规律,则交换(顺着要交换的链路走就可以了,其他不用交换的部分之前已经调整好了)
{
swap(a[dad], a[son]);
dad = son;
son = 2 * dad + 1; //继续判断下一个结点
}
else//如果一旦某个地方符合,则退出函数,由于已经假定其他部分符合堆的定义,故可以直接退出
{
return;
}
}
}
/***********************************【归并排序】*************************************************************/
/*
* 排序方法:归并排序
* 思路:归并操作(merge),指的是将两个已经排序的序列合并成一个有序序列的操作。
归并排序是采用分治法(Divide and Conquer)的一个典型例子。这个排序的特点是
把一个数组分成许多子数组,对子数组排序,再将排序好的子数组归并为一个有序数组。
可以把归并排序理解为将原数组变成一个平衡二叉树,每次分别对左右子树的结点排序,并进行归并,归并的次数就是树的高度logn
* 时间复杂度O(n)~O(nlogn),平均时间复杂度O(nlogn)
空间复杂度О(n) total, O(n) auxiliary
*/
void merge(vector<int>& a, int begin, int middle, int end);
void merge_sort_recursive(vector<int>&a, int begin, int end);
void merge_sort(vector<int>&a)
{
//vector temp = a;
merge_sort_recursive(a, 0, a.size() - 1);//传递初始索引
}
void merge_sort_recursive(vector<int>&a, int begin, int end)//begin,end均为取得到的索引
{
if (begin >= end) return;//递归的出口
int middle = (begin + end) / 2;
merge_sort_recursive(a, begin, middle);//对左子数组归并排序
merge_sort_recursive(a, middle+1, end);//对右子数组归并排序
merge(a, begin, middle, end);//归并左子数组和右子数组
}
//合并数组a左右子数组(已排序)到临时数组temp,再复制到数组a中
void merge(vector<int>& a, int begin, int middle, int end)
{
vector<int> temp(end - begin + 1); //开辟临时数组
int i = begin, j = middle+1, k = 0;//分别表示数组左边第一个、右边第一个、临时数组第一个元素索引
while (i <= middle && j <= end)//索引范围:i = begin~middle,j = middle+1~end,k = begin~end,a[i],a[j],temp[k]
{
temp[k++] = (a[i] < a[j]) ? a[i++] : a[j++];//合并,注意k++,i++,j++
}
while (i <= middle) temp[k++] = a[i++];//将剩余元素复制到temp数组中
while(j <= end) temp[k++] = a[j++];
for (int i = begin,k = 0; i <= end, k<temp.size(); i++,k++)//将排好序的临时数组复制到数组a中(vector可以直接用赋值语句整体赋值)
{
a[i] = temp[k];
}
}