(排序类别,排序方法)
插入排序:直接插入、折半插入、希尔排序。
交换排序:冒泡排序、快速排序。
选择排序:简单选择排序、堆排序。
归并排序:
基数排序:
冒泡排序时间复杂度O(n^2)、空间复杂度O(1)、归位性(每趟确定一个元素最终位置)
一般选第一个元素为枢轴,根据枢轴将数组分割成两部分(前一部分关键字均比后一部分小),
每次一趟排序确定枢轴的位置。pivot的选取直接影响排序的好坏。(把pivot位置视为一个坑)
分析:partition每次划分很均匀时,情况最优,时间复杂度为:O(nlogn);平均情况下时间复杂度也为:O(nlogn);对应空间复杂度:O(logn)
最坏情况:待排序序列已正序或逆序时,pivot取值为数组最大值或最小值,时间复杂度为:O(n^2);对应空间复杂度:O(n)
【堆排序】:
时间复杂度(平均、最好、最坏均为)nlogn,空间复杂度:O(1)
以大顶堆为例,初始的大顶堆为一个无序区,每一轮取堆顶元素放入有序区。调整新堆为大根堆。
大根堆:完全二叉树中,任意父节点值总数大于或等于子节点。
完全二叉树一般可采用顺序存储的方式。
算法思想:
1.将初始待序列{a[0],a[1]...a[n]}构建成大根堆,此堆为初始的无序区。
2.把堆顶元素a[0]与堆尾元素a[n]交换,则得到新的无序序列{a[0],a[1]...a[n-1]}及新的有序区{a[n]}
3.将新的无序序列调整为大根堆。
重复2,3,直至有序区的元素个数为n。
//将完全二叉树调整为大根堆
前提:i节点的子树已经为大根堆。(因此建立大根堆时,需要从最后一个非叶子节点往上遍历)
公式:T[n]=2T[n/2]+O(n) ==> T[n]=O(nlogn) 即该算法的最优、最差、平均时间复杂度均为:O(nlogn)
空间复杂度:
关于递归:递归实际上是在执行到递归函数时,将现有的函数中现场保存入栈,执行完函数后,恢复现场。
即需要注意哪些数据是必须要存入栈的。例如:mergesort函数中的mid的每次取值,都需入栈。
递归层数决定了栈的深度,与递归时压入栈的数据占用的空间相对应。
归并的空间复杂度为临时数组加上递归压入栈的深度:n+logn ==> O(n)
[非递归实现归并排序](待补充...)
int binarySearch(int array[], int len, int key)//返回对应下标
{
int low=0,high=len-1;
int mid;
while(low <= high)
{
mid=(low+high)/2;
if(key==mid) {return mid;}
if(key<mid) {high=mid-1;}
if(key>mid) {low=mid+1;}
}
return -1;
}
插入排序:直接插入、折半插入、希尔排序。
交换排序:冒泡排序、快速排序。
选择排序:简单选择排序、堆排序。
归并排序:
基数排序:
冒泡排序时间复杂度O(n^2)、空间复杂度O(1)、归位性(每趟确定一个元素最终位置)
快速排序、堆排序、归并排序:平均时间复杂度为O(nlogn)。
下面分别回忆下冒泡排序、快速排序、堆排序、归并排序。
【冒泡排序】:(大数往下沉)
void sort(int a[], int len)
{
int i,j;
for(i=0;i<len-1;i++)
{
for(j=0;j<len-1-i;j++)
{
if(a[j]>a[j+1])
{
int tmp=a[j];
a[j]=a[j+1];
a[j+1]=tmp;
}
}
}
}
【快速排序】:
一般选第一个元素为枢轴,根据枢轴将数组分割成两部分(前一部分关键字均比后一部分小),
每次一趟排序确定枢轴的位置。pivot的选取直接影响排序的好坏。(把pivot位置视为一个坑)
建议:可取a[0],a[(len-1)/2],a[len-1]的中值作为枢轴。
int partition(int A[],int low,int high)//low,high为数组下标
{
int i=low,j=high;
int pivot=a[i]; //取最左边为pivot
while(i<j){
while(a[j]>=pivot && i<j) j--;
a[i]=a[j]; //找出右边第一个小于pivot的元素,即a[j],放入i下标。
while(a[i]<=pivot && i<j) i++;
a[j]=a[i]; //找到左边第一个大于pivot的元素,即a[i],放入j下标。
}
a[i]=pivot //此时i=j
return i;
}
void quick_sort(int a[], int low, int high)
{
if(low < high)
{
int i = partition(a,low,high);
quick_sort(a,low,i-1);
quick_sort(a,i+1,high);
}
}
空间复杂度:递归造成的栈空间的使用。
分析:partition每次划分很均匀时,情况最优,时间复杂度为:O(nlogn);平均情况下时间复杂度也为:O(nlogn);对应空间复杂度:O(logn)
最坏情况:待排序序列已正序或逆序时,pivot取值为数组最大值或最小值,时间复杂度为:O(n^2);对应空间复杂度:O(n)
【堆排序】:
时间复杂度(平均、最好、最坏均为)nlogn,空间复杂度:O(1)
以大顶堆为例,初始的大顶堆为一个无序区,每一轮取堆顶元素放入有序区。调整新堆为大根堆。
大根堆:完全二叉树中,任意父节点值总数大于或等于子节点。
完全二叉树一般可采用顺序存储的方式。
算法思想:
1.将初始待序列{a[0],a[1]...a[n]}构建成大根堆,此堆为初始的无序区。
2.把堆顶元素a[0]与堆尾元素a[n]交换,则得到新的无序序列{a[0],a[1]...a[n-1]}及新的有序区{a[n]}
3.将新的无序序列调整为大根堆。
重复2,3,直至有序区的元素个数为n。
//将完全二叉树调整为大根堆
前提:i节点的子树已经为大根堆。(因此建立大根堆时,需要从最后一个非叶子节点往上遍历)
//a数组的长度为len,需调整的节点下标为i。
void swap(int *a, int *b)
{
int tmp=*a;
*a=*b;
*b=tmp;
}
void adjust_heap(int a[], int len, int i)
{
int lchild=2*i+1;
int rchild=2*i+2;
int max=i; //max标记父节点,左右节点,三者之间最大值的下标。
if(i <= (len/2-1)){ //最后一个非叶子节点的下标为len/2 -1
if(lchild < len && a[max] < a[lchild])
max=lchild;
if(rchild < len && a[max] < a[rchild])
max=rchild;
if(max != i){ //i节点比左右某一子节点小,则需交换a[i],a[max]的取值
swap(a+i,a+max); //交换后可能会破坏了子树的堆结构,所以仍需调整子树。swap(&a[i],&a[max]);
adjust_heap(a,len,max);//递归调用
}
}
}
或者采用非递归的方式:循环
void adjust_heap(int a[], int len, int i)
{
int lchild=2*i+1;
int rchild=2*i+2;
int max=i; //max标记父节点,左右节点,三者之间最大值的下标。
while(i <= (len/2-1)){ //最后一个非叶子节点的下标为len/2 -1
if(lchild < len && a[max] < a[lchild])
max=lchild;
if(rchild < len && a[max] < a[rchild])
max=rchild;
if(max != i){ //i节点比左右某一子节点小,则需交换a[i],a[max]的取值
swap(a+i,a+max); //交换后可能会破坏了子树的堆结构,所以仍需调整子树。swap(&a[i],&a[max]);
i=max;lchild=2*i+1;rchild=2*i+2;//检查子树
}
else{
break; //i节点本身就比两个子节点取值大
}
}
}
建立大根堆函数:
void build_heap(int a[], int len) //从下往上建立堆
{
int begin=len/2-1; //最后一个非叶子节点的下标为len/2 -1
for(int i=begin; i>=0; i--)
{
adjust_heap(a,len,i);
}
}
堆排序函数:
void heap_sort(int a[], int len)
{
build_heap(a,len); //步骤1:建立大根堆
for(int i=len-1;i>0;i--)
{
swap(&a[i],&a[0]); //步骤2:交换堆顶和堆尾元素
adjust_heap(a,i,0); //步骤3:调整剩余的堆为大根堆
}
}
【归并排序】
分治法的典型应用
算法思路:将数组分为2个子数组a,b。如果这两个数组组内数据都为有序,则问题转化为
合并两个有序的数列。至于如何让a,b两个数组组内都为有序,则将a,b各自继续进行划分,
类推,直到组内只有一个元素时,可视为组内有序,再合并相邻两个小组。
先递归分解数列,再合并数列,即为归并排序。
//合并有序a[],b[]到c[]中。
void merge(int a[],int len1,int b[],int len2,int c[])
{
int i,j,k; //i,j,k分别对应a,b,c的下标
i=j=k=0;
while(i<len1 && j<len2)
{
if(a[i] <= b[j])
c[k++]=a[i++];
else
c[k++]=b[j++];
}
while(i<len1) {c[k++]=a[i++];} //只剩下a数组未结束
while(j<len2) {c[k++]=b[j++];} //只剩下b数组未结束
}
//合并两个有序数列{a[first]...,a[mid]}、{a[mid+1]...a[last]}
//first,mid,last均为a数组的下标。注意:要将排序后的数组元素从辅助数组copy至原数组a中。
void mergearry(int a[],int first,int mid,int last,int c[])
{
int i=fist,j=mid+1;k=0;
while(i<=mid && j<=last)
{
if(a[i]<=b[j])
c[k++]=a[i++];
else
c[k++]=b[j++];
}
while(i<=mid) {c[k++]=a[i++];}
while(j<=last) {c[k++]=b[j++];}
//此时数组c的长度为k
for(i=0;i<k;i++)
{
a[first+i]=c[i];
}
}
归并排序函数:
void mergesort(int a[],int first,int last,int tmp[])
{
if(first<last){
int mid=(first+last)/2;
mergesort(a,fisrt,mid,tmp);
mergesort(a,mid+1,last,tmp);
mergearry(a,first,mid,last,tmp);
}
}
//应该避免在多次调用的函数中临时new分配数组,因此不在mergearray,mergesort中分配临时辅助数组。
或者初始化一个静态全局变量。
bool mymergesort(int a[],int len)
{
int *p=new int[len];
if(p==NULL)
return false;
mergesort(a,0,len-1,p);
delete[] p;
return true;
}
时间复杂度:
公式:T[n]=2T[n/2]+O(n) ==> T[n]=O(nlogn) 即该算法的最优、最差、平均时间复杂度均为:O(nlogn)
空间复杂度:
关于递归:递归实际上是在执行到递归函数时,将现有的函数中现场保存入栈,执行完函数后,恢复现场。
即需要注意哪些数据是必须要存入栈的。例如:mergesort函数中的mid的每次取值,都需入栈。
递归层数决定了栈的深度,与递归时压入栈的数据占用的空间相对应。
归并的空间复杂度为临时数组加上递归压入栈的深度:n+logn ==> O(n)
[非递归实现归并排序](待补充...)
【c、c++关于排序的库函数】(待补充)
附:
int binarySearch(int array[], int len, int key)//返回对应下标
{
int low=0,high=len-1;
int mid;
while(low <= high)
{
mid=(low+high)/2;
if(key==mid) {return mid;}
if(key<mid) {high=mid-1;}
if(key>mid) {low=mid+1;}
}
return -1;
}