首先总结一下排序的算法分别是什么:
冒泡排序
之前写过冒泡排序的思路及其优化,所以再次就不再赘述了。
冒泡排序
在此写一个从后向前冒泡的版本:
//每次冒泡将当前最小值冒到第一个位置上
//[0,bound)表示有序元素
//[bound,size)表示待排序区间
void BubbleSort(int arr[],size_t size)
{
if(arr==NULL||size<=0)
{
return;
}
size_t bound=0;
for(;bound<size;bound++)
{
size_t cur=size-1;
for(;cur>0;cur--)
{
if(arr[cur]<arr[cur-1])
{
Swap(&arr[cur],&arr[cur-1]);
}
}
}
}
总结:
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定排序
选择排序
选择排序的思路主要是采取了一种“打擂台”的思想。
一个擂主,多个挑战者挑战,不符合排序要求的交换。
下面一张图解释一下大致过程:
排序过程大致过程如上所述。
下面是代码:
//[0,bound)表示有序元素
//[bound,size)表示待排序区间
void SelectSort(int arr[],size_t size)
{
if(arr==NULL||size<=0)
{
return;
}
size_t bound=0;
for(;bound<size;bound++)
{
size_t cur=bound+1;
for(;cur<size;cur++)
{
if(arr[bound]>arr[cur])//bound是擂主
{
Swap(&arr[bound],&arr[cur]);
}
}
}
}
总结:
时间复杂度:O(n^2)
空间复杂度:O(1)
不稳定排序
插入排序
插入排序的思路将不再是交换值,而是搬运。
步骤大致如下:
1.定义好边界,仍然以[0,bound)表示已排序区间,[bound,size)表示未排序区间。
2.保存bound指向的元素,以防止搬运时被覆盖,数据丢失。
3.从后往前的遍历,找到一个合适的位置存放刚刚保存下来的值。
也就是说,将bound指向的值,插入已排序好的线性表中。
采用一边找,一边搬运的方法,更节省时间。
下面用图来解释一下大致排序过程
下面是代码:
void InsertSort(int arr[],size_t size)
{
if(arr==NULL||size<=0)
{
return;
}
size_t bound=1;
for(;bound<size;bound++)
{
size_t cur=bound;
int bound_value=arr[bound];
for(;cur>0;cur--)
{
if(bound_value<arr[cur-1])
{
//进行搬运
arr[cur]=arr[cur-1];
}
else
{
//说明此时已经找到了合适的位置
break;
}
}
//填坑,将bound_value的值填入
arr[cur]=bound_value;
}
}
特点:
1.当数组元素个数较少时,执行效率比较快。
2.如果数组基本有序,执行效率比较快。
总结:
时间复杂度:O(N^2)
空间复杂度:o(1)
希尔排序
希尔排序是插入排序的一种,又称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。本质上是一种分组插入的方法。
希尔排序具有以下两个特点:
(1)插入排序在对几乎已经排好序的数据操作时,效率高;
(2)插入排序在对元素个数较少的数据操作时,效率高。
举例说明:
void ShellSort(int arr[],size_t size)
{
if(arr==NULL||size<=0)
{
return;
}
size_t gap=size/2;
for(;gap>0;gap/=2)//生成步长序列
{
size_t bound=gap;
for(;bound<size;bound++)//进行插入排序
{
int bound_value=arr[bound];
size_t cur=bound;
for(;cur>=gap;cur-=gap)//线性表元素的搬运
{
if(arr[cur-gap]>bound_value)
{
arr[cur]=arr[cur-gap];
}
else
{
break;
}
}
arr[cur]=bound_value;
}
}
}
总结:
时间复杂度:O(N^2)
空间复杂度:O(1)
不稳定排序
归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and
Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
递归实现:
void _MergeArray(int arr[],int beg,int mid,int end,int* tmp)
{
int cur1=beg;
int cur2=mid;
int tmp_index=beg;
while(cur1<mid&&cur2<end)
{
if(arr[cur1]<arr[cur2])
{
tmp[tmp_index++]=arr[cur1++];
}
else
{
tmp[tmp_index++]=arr[cur2++];
}
}
while(cur1<mid)
{
tmp[tmp_index++]=arr[cur1++];
}
while(cur2<end)
{
tmp[tmp_index++]=arr[cur2++];
}
memcpy(arr+beg,tmp+beg,sizeof(int)*(end-beg));
}
void _MergeSort(int arr[],int beg,int end,int *tmp)
{
if(end-beg<=1)
{
return;
}
int mid=beg+(end-beg)/2;
_MergeSort(arr,beg,mid,tmp);
_MergeSort(arr,mid,end,tmp);
_MergeArray(arr,beg,mid,end,tmp);
}
void MergeSort(int arr[],size_t size)
{
if(arr==NULL||size<=0)
{
return;
}
int *tmp=(int*)malloc(sizeof(int)*size);
_MergeSort(arr,0,size,tmp);
}
非递归实现:
void MergeSortByLoop(int arr[],size_t size)
{
if(arr==NULL||size<=0)
{
return;
}
int *tmp=(int*)malloc(sizeof(int)*size);
size_t gap=1;
for(;gap<size;gap*=2)
{
size_t i=0;
for(;i<size;i+=2*gap)
{
size_t beg=i;
size_t mid=i+gap;
size_t end=i+2*gap;
if(mid>size)
{
mid=size;
}
if(end>size)
{
end=size;
}
_MergeArray(arr,beg,mid,end,tmp);
}
}
}
总结:
时间复杂度:O(N*logN)
空间复杂度:O(N)
稳定排序
快速排序
快速排序可以说是这七种排序算法中最重要的一个,面试几率最高的一个排序算法。原因是快速排序足够快。
过程如下:
/////////方法1.交换法////////////
int Partion(int arr[],size_t beg,size_t end)
{
//1)定义好区间的边界
//2)取最后一个元素作为基准值
//3)从左到右找到第一个大于基准值的元素
//4)从右往左找到第一个小于基准值的元素
//5)交换(left<right)
int left=beg;
int right=end-1;
int key=arr[right];
while(left<right)
{
while(left<right&&arr[left]<=key)
{
left++;
}
while(left<right&&arr[right]>=key)
{
right--;
}
if(left<right)
{
Swap(&arr[left],&arr[right]);
}
}
//此时将left指向的值和最后一个元素进行交换
//此时left指向的值,一定大于基准值
Swap(&arr[left],&arr[end]-1);
return left;
}
/////////方法2.挖坑法////////////
int Partion2(int arr[],size_t beg,size_t end)
{
//定义好区间边界
int left=beg;
int right=end-1;
//取最后一个元素做基准值
int key=arr[right];
while(left<right)
{
while(left<right&&arr[left]<=key)
{
++left;
}
if(left<right)
{
arr[right--]=arr[left];
}
while(left<right&&arr[right]>=key)
{
--right;
}
if(left<right)
{
arr[left++]=arr[right];
}
}
arr[left]=key;
return left;
}
void _QuickSort(int arr[],size_t beg,size_t end)
{
if(end-beg<=1)
{
return;
}
size_t mid=Partion(arr,beg,end);
_QuickSort(arr,beg,mid);
_QuickSort(arr,mid+1,end);
}
void QuickSort(int arr[],size_t size)
{
if(arr==NULL||size<=0)
{
return;
}
_QuickSort(arr,0,size);
}
挖坑法与交换法本质上是没有区别的,挖坑法实际上是将交换的动作拆解开了。
非递归版本实现:借助一个栈。通过入栈取栈顶元素出栈的过程,取出当前需要划分的区间。
void QuickSortByLoop(int arr[],size_t size)
{
if(arr==NULL||size<=0)
{
return;
}
SeqStack stack;
SeqStackInit(&stack);
int beg=0;
int end=size;
SeqStackPush(&stack,beg);
SeqStackPush(&stack,end);
while(stack.size>0)
{
SeqStackTop(&stack,&end);
SeqStackPop(&stack);
SeqStackTop(&stack,&beg);
SeqStackPop(&stack);
if(end-beg<=1)
{
continue;
}
int mid=Partion(arr,beg,end);
SeqStackPush(&stack,beg);
SeqStackPush(&stack,mid);
SeqStackPush(&stack,mid+1);
SeqStackPush(&stack,end);
}
}
对快速排序的改进方法:
- 三值取中确定基准值
- 当区间比较小时,使用插入排序直接对区间进行排序,从而有效地减少递归次数
- 递归达到一定程度之后,使用堆排序对剩余待排序区间进行排序即可
总结:
时间复杂度:
最坏:O(n^2)
平均:O(n*logn)
空间复杂度:O(logn)