排序的分类
排序可以分为插入排序、交换排序、选择排序、归并排序、基数排序
插入排序:直接插入排序(以及折半插入排序)、希尔排序
交换排序:冒泡排序、快速排序
选择排序:简单选择排序、堆排序
归并排序:二路归并排序
基数排序:基数排序
各个排序之间的时间复杂度、空间复杂度、以及稳定性的比较如下所示:

各个排序的详解
一、插入排序
1.直接插入排序
插入排序的基本思想:每次将一个待排序的记录按其关键字大小插入前面已排好序的子序列,直到全部记录插入完成。是一种稳定的排序算法。 直接插入排序的代码如下所示:
int* sortArray(int* nums, int numsSize, int* returnSize){
*returnSize = numsSize;
if (numsSize <= 1) {
return;
}
int j;
for(int i=1;i<numsSize;i++)
{
int temp = nums[i];
if(nums[i]<nums[i-1])
{
for(j=i-1;j>=0&&temp<nums[j];j--)
{
nums[j+1] = nums[j];
}
nums[j+1]=temp;
}
}
return nums;
}
直接插入排序在向有序的数组中插入的时候需要不断的移动元素。 其空间复杂度为:O(1)近使用了常数个辅助单元。 平均情况时间复杂度:O(n ^2)
2.折半插入排序
根据上述的直接插入排序可以看出,我们需要查找有序数组中的位置然后进行插入,此时我们可以将查找改为折半查找可以提高算法的效率,确定插入位置后,就可以统一地向后移动元素。
此实现只更改了查找部分的替换为折半查找,有兴趣的可以自己实现下。
2.希尔排序
根据插入排序的思想是需要向有序的的部分插入元素,因而它更适合基本有序的表以及数据量不大的排序表。 算法思想:先追求表中元素部分有序,在逐渐逼近全局有序。 即将待排序的表将相隔某个增量的记录组成一个子表,然后对每个子表使用直接插入排序,当整个表中元素已基本有序时,再对全体记录进行一次直接插入排序。 希尔排序的代码如下所示:
//希尔排序
void ShellSort(int *nums,int n)
{
int dk;//步长
int j;
for(dk=n/2;dk>=1;dk=dk/2)
{
for(int i=dk;i<n;i++)
{
if(nums[i]<nums[i-dk])//需要查找元素插入的位置
{
int temp = nums[i];
for(j=i-dk;j>=0&&temp<nums[j];j-=dk)
{
nums[j+dk]=nums[j];
}
nums[j+dk]=temp;
}
}
}
}
int* sortArray(int* nums, int numsSize, int* returnSize){
*returnSize = numsSize;
if(numsSize<2)
{
return nums;
}
ShellSort(nums,numsSize);
return nums;
}
是种不稳定的排序算法。空间复杂度也为O(1),时间复杂度依赖于增量序列的函数,故分析比较困难。最坏情况的时间复杂度为O(n^2)
二、交换排序
顾名思义,交换排序是根据序列中两个元素的关键字比较结果交换两个记录在序列中的位置。
1.冒泡排序
冒泡排序的基本思想:从后往前(或从前往后)两两比较相邻元素的值,若为逆序,则交换他们,直到序列比较完毕。可以设置一个flag,当本躺没有发生交换,说明表已经有序,此时可以提前结束循环。 代码实现如下所示:
void swap(int* a,int* b)
{
int *temp = a;
a = b;
b = temp;
}
int* sortArray(int* nums, int numsSize, int* returnSize){
*returnSize = numsSize;
if (numsSize <= 1) {
return nums;
}
int i,j;
for(i=0;i<numsSize-1;i++)
{
int flag = 0;//用来记录是否进行了交换
for(j=0;j<numsSize-i-1;j++)
{
if(nums[j]>nums[j+1])
{
//swap(&nums[j],&nums[j+1]);
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
flag = 1;
}
}
if(flag ==0)//数组已经有序
{
return nums;
}
}
return nums;
}
是稳定的排序算法 空间复杂度为O(1) 最坏时间复杂度和平均时间复杂度均为O(n^2) 最好的时间复杂度为O(n)
2.快速排序
快速排序的基本思想:基于分治发,在待排序表中任取一个元素作为基准,通过一趟排序将待排序表划分为独立的两个部分,左边均比基准小,右边均比基准大。然后递归的执行此操作,直到每部分内都只有一个元素或空为止。每一趟快速排序可以将一个元素确定在最终的位置上。 代码如下:
void swap(int* a,int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int Partition(int* nums,int low,int high)
{
int mid = (low+high)/2;
//srand((unsigned)time(0));
// int rand = rand % (low+(high-low));
swap(&nums[low],&nums[mid]);
int privot = nums[low];
//int privot = nums[3];
while(low<high)
{
while(low<high&&privot<=nums[high])
{
high--;
}
nums[low]=nums[high];
while(low<high&&privot>=nums[low])
{
low++;
}
nums[high]=nums[low];
}
nums[low]=privot;
return low;
}
void Quicksort(int* nums,int low,int high)
{
if(low<high)
{
int part = Partition(nums,low,high);
Quicksort(nums,low,part-1);
Quicksort(nums,part+1,high);
}
}
int* sortArray(int* nums, int numsSize, int* returnSize){
*returnSize = numsSize;
if(numsSize<2)
{
return nums;
}
Quicksort(nums,0,numsSize-1);
return nums;
}
快速排序的关键在于划分操作,同时其性能也主要取决于划分操作的好坏,上述代码采取的基准为中间元素mid为基准。 快速排序是所有内部排序算法中平均性能最优的排序算法。 快速排序是不稳定的排序算法。 在有序的序列,快速排序的效率最差。 空间复杂度:最坏O(n),最好O(log n) 即栈的深度 时间复杂度:均为O(nlog n)
三、 选择排序
基本思想:每一趟在后面n-i+1个待排序元素中取关键字最小的元素,作为有序序列的第i个元素,直到n-1趟做完。 是不稳定的排序算法。
1.简单选择排序
思想与上述思想一致。 代码如下所示:
//简单选择排序
void swap(int* a,int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void SelectSort(int *nums,int numsSize)
{
for(int i=0;i<numsSize-1;i++)
{
int min = i;
for(int j=i+1;j<numsSize;j++)
{
if(nums[j]<nums[min])
{
min = j; //更新min值
}
}
if(i!=min)
{
swap(&nums[min],&nums[i]);
}
}
}
int* sortArray(int* nums, int numsSize, int* returnSize){
*returnSize = numsSize;
if(numsSize<2)
{
return nums;
}
SelectSort(nums,numsSize);
return nums;
}
根据上述代码可以看出,元素间的比较次数与序列的初时状态无关,因而时间复杂度为O(n^2) 空间复杂度为O(1)
2.堆排序
可以将堆堪称为一棵完全二叉树。 堆可以分为大根堆与小根堆,两者的区别主要在于 大根堆,根节点的值大于等于左右孩子的值
小根堆,根节点的值小于等于左右孩子的值 堆的建立: 将当前节点与其左右孩子进行比较,判断是否满足大/小根堆的条件,如果不满足将当前节点与左右孩子中更大/小的一个孩子互换,若元素胡话破坏了下一级的堆,则采用相同的方式继续往下调。 堆排序: 在堆初始化完成之后可以判断出堆顶元素就是最大/小的元素,将堆顶元素进行输出,然后将堆底元素换上来,此时会破环大/小根堆的定义,需要对其进行调整,直到重新调整为一个大/小根堆,然后输出堆顶元素,重复上述操作,直到剩下最后一个元素。 代码实现如下所示:
void swap(int* a,int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void HeadAdjust(int* nums,int node,int numsSize)//构建小根堆
{
int temp = nums[node];
for(int i=2*node;i<numsSize;i*=2)
{
if(i<numsSize-1&&nums[i]<nums[i+1])//取左右孩子中偏小的一个
{
i++;
}
if(temp<nums[i])//父节点大于孩子节点,调整节点
{
nums[node] = nums[i];
node = i ;//修改node的值继续往下筛选
}
else
{
break;
}
}
nums[node] = temp; //被筛选的值放入最终位置
}
//堆排序
void HeapSort(int *nums,int numsSize)
{
for(int i=numsSize/2;i>=0;i--)//初始化小根堆
{
HeadAdjust(nums,i,numsSize);
}
for(int i=numsSize-1;i>=1;i--)
{
swap(&nums[i],&nums[0]);
//break;
HeadAdjust(nums,0,i);
}
}
int* sortArray(int* nums, int numsSize, int* returnSize){
*returnSize = numsSize;
if(numsSize<2)
{
return nums;
}
HeapSort(nums,numsSize);
return nums;
}
堆的删除:被删除的元素用堆底元素替代,然后让该元素不断“下坠”,直到无法下坠为止。 空间复杂度:O(1) 时间效率:O(nlog n) 是一种不稳定的排序方法。
四、归并排序
算法思想:“归并”的含义是将两个或两个以上的有序表组合成一个新的有序表。 代码实现:
void swap(int* a,int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
//二路归并排序
void Merge(int *nums,int low,int mid,int high)
{
int b[high+1];
for(int t=low;t<=high;t++)//将数据复制一份到辅助数组中
{
b[t] = nums[t];
}
int k=0;
int i,j;
for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++)
{
if(b[i]>=b[j])
{
nums[k] = b[j];
j++;
}
else
{
nums[k] = b[i];
i++;
}
}
while(i<=mid){
nums[k] = b[i];
i++;
k++;
}
while(j<=high)
{
nums[k] = b[j];
j++;
k++;
}
}
void mergeSort(int *nums,int low,int high)
{
if(low<high)
{
int mid = (low+high)/2;
mergeSort(nums,low,mid);
mergeSort(nums,mid+1,high);
Merge(nums,low,mid,high);
}
}
int* sortArray(int* nums, int numsSize, int* returnSize){
*returnSize = numsSize;
if(numsSize<2)
{
return nums;
}
mergeSort(nums,0,numsSize-1);
return nums;
}
空间复杂度:使用了辅助数组,则空间复杂度为O(n) 时间复杂度:O(nlog n) 是一种不稳定的排序
五、基数排序
基数排序是一种特别的排序,并不是基于“比较”的排序算法。 其是借助多关键字排序的思想对单逻辑关键字进行排序的方法。 如:排一组人的年龄大小,分别按照出生日期的 天 排一次序,然后在按月排,最后按年,最终得到一组有序的序列。 该排序方法并为用代码实现。
|