在计算机科学中,稳定性是指在排序过程中,相等的元素的相对顺序保持不变。也就是说,如果元素a和b在排序之前是相等的,那么在排序之后,a和b的相对顺序应该和排序之前一样;否则不稳定。
直接插入排序是一种简单的排序方法,它的基本操作是将一条记录插入到已经排好序的有序表中,从而得到一个新的、记录数量增1的有序表。
具体操作步骤如下:
直接插入排序的时间复杂度为O(n ^ 2),其中n为待排序序列的长度。由于每次插入都需要移动元素,所以对于较大的数据集来说,直接插入排序可能效率较低。
直接插入排序是稳定的
排升序
这里采用的基本思想是: 先假设第一个元素是有序的,然后从后方进行插入元素,然后再与前面的元素进行比较,当然前提是把要插入的元素临时存放一下(避免元素移动后,给覆盖了)。如果比前面的元素小,就要把前面的元素后移,直到找到比前面的大或者第一个位置,然后把要插入的元素放进去。
代码展示
void InsertSort(int* arr,int n)
{
//升序
//从末端开始
//先假设第一个元素 有序
//第二元素从从末端进行插入,与前面的元素比较
//使用末端元素,如果比前面的小,前面的元素进行后移
//之后再把元素进行插入
int i = 0;
for (i = 0; i < n-1;i++)
{
int end = i;
int tmp = arr[end + 1]; //临时存放插入的元素
while (end >=0)
{
//小于前的,就让前面的元素后移
if (tmp < arr[end])
{
arr[end + 1] = arr[end];
--end;
}
else
{
//当大于等于的时候直接跳出循环
break;
}
}
//把要插入的元素放进去
arr[end + 1] = tmp;
}
}
希尔排序是直接插入排序算法的一种更高效的改进版本,它是插入排序的一种,也称为“缩小增量排序”,因 D.L.Shell 于 1959 年提出而得名。
希尔排序的基本思想是:现将待排序的数组分成多个待排序的子序列,使得每个子序列的元素较少,然后对各个子序列分别进行插入排序,待到整个待排序的序列基本有序的时候,最后在对所有的元素进行一次插入排序。
也就是:分为 预排序 和 插入排序
希尔排序是不稳定的,(原因是:相同的数据可能被分在不同的组,前后数据的位置难以控制 ) 希尔排序的时间复杂度取决于间隔序列的选择。理论上,如果间隔序列是逐一减半的,希尔排序的时间复杂度可以接近O(n log n)。但是,如果间隔序列选择不当,可能会导致最坏情况的时间复杂度为O(n^2)。
所以,希尔排序的时间复杂度通常是在O(n log n)和O(n ^ 2)之间,具体取决于间隔序列的选择。希尔排序的时间复杂度约是O(n^1.3)。
代码展示
void ShellSort(int* arr ,int n)
{
int gap = n;
while(gap > 1)
{
gap = gap / 3 + 1; //一组子序列中的每个数的间距
for (int i = 0; i < n - gap; i++)
{
int end = i;
//插入排序,尾插进行排序
int tmp = arr[end + gap]; //记录尾部的数据
while (end >= 0)
{
// 升序
if (tmp < arr[end])
{
//后面的小于前面的向后移动
arr[end + gap] = arr[end];
end-=gap; //注意是子序列
}
else
{
//后面的大于前的直接插入
break;
}
}
//把排序的数据放进去
arr[end + gap] = tmp;
}
}
}
基本思想: 每次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
需要注意的是,直接选择排序中存在大跨度的数据移动,
是一种不稳定的排序方式
原因是:
其时间复杂度是O(n ^ 2),最好的情况是O(N ^ 2),最坏的情况是O(N ^ 2); 空间复杂度为O(1)。
假设,排升序,这里我们用数组存储数据,那就是要遍历数组进行直接选择排序(选出较小的依次从数组的起始位置开始排)
图1:
因为选择排序要么选出最大的,要么选出最小的。
代码展示
选择排序,选出大的和选出小的同时进行
//选择排序
//时间复杂度;O(N^2)
//最好的情况:O(N^2)
void SelectSort(int* arr, int n)
{
int begin = 0;
int end = n - 1; //数组最后一个元素的下标
while (begin < end)
{
//把大的数放到最右边,小的数放到最左边
//不断向中间缩小范围
int maxi = begin, mini = begin;
for (int i = begin+1; i <= end;i++)
{
if (arr[i] < arr[mini])
{
//把下标给mini
mini = i;
}
if (arr[i] > arr[maxi])
{
maxi = i;
}
}
//一轮找完后
//把大的数放到最右边,小的数放到最左边
swap(&arr[begin], &arr[mini]);
//上述的swap 就是 把小的值与左端begin进行交换
//需要注意的是:当左端begin的下标与maxi(那一趟认为较大的数的下标)相等时,
//maxi == begin ,swap较换后把小值放到了begin,而maxi下标也是指向begin
//当再把maxi指向的数据放到右边的时候,maxi之前指向的数已经让上面swap给换了,换到mini所指向的值了
//所以需要加个判断给换回来
if (begin == maxi)
{
maxi = mini;
}
swap(&arr[end], &arr[maxi]);
++begin;
--end;
}
}
选出小的依次进行排序
//选小进行排序
void SelectSort(int* arr, int n)
{
int begin = 0;
while (begin < n)
{
int mini = begin;
for (int i = begin; i < n; i++)
{
if (arr[i] < arr[mini])
{
mini = i;
}
}
swap(&arr[begin], &arr[mini]);
++begin;
}
}
选出大的从后往前放进行排序
//选大进行排序
void SelectSort(int*arr,int n)
{
int end = n - 1;
while (end >= 0)
{
int maxi = end;
for (int i = end; i >= 0;i--)
{
//寻找较大的下标
if (arr[i] > arr[maxi])
{
maxi = i;
}
}
swap(&arr[end],&arr[maxi]);
--end;
}
}
这里以升序为例。我们需要把数据先构建成大堆(根的值最大),然后把堆的根元素与堆最后面一个元素进行交换。然后堆的大小减一。
依次重复上述。直到排完。
堆排序的时间复杂度为O(nlogn),它是不稳定排序算法。
不稳定的原因 例如
关于堆排序,猛击链接 堆排序 更详细的介绍
代码展示
//堆排序
//交换两个数
void swap(int*s1,int*s2)
{
int tmp = *s1;
*s1 = *s2;
*s2 = tmp;
}
//向下调整
void AdjustDown(HPDataType* a, int size, int parent)
{
//先去找根结点的较大的孩子结点
int child = 2 * parent + 1;
//可能会向下调整多次
while (child<size)
{
//这里使用假设法,先假设左孩子的值最大
//如果不对就进行更新
if ((child+1 < size)&&a[child] < a[child+1])
{
child++;
}
//根结点与其孩子结点中的较大的一个进行交换
if(a[child] > a[parent])
{
swap(&a[child],&a[parent]);
//更新下标
parent = child;
child = 2 * parent + 1;
}
else
{
break; //调完堆
}
}
}
//堆排序
void HeapSort(int* arr, int n)
{
int i = 0;
//使用向下调整算法向上调整,把大的值调到上方。
for (i = (n - 1 - 1) / 2; i >= 0;i--)
{
//先找到数组最后端的父结点的下标
//父结点的下标减一就是另一个
//使用向下调整算法进行调整
AdjustDown(arr,n,i);
}
//进行排序
//因为是大堆,所以根结点的值是最值
//把最值与堆的最后一个结点进行交换
//再把交换后的根节点进行向下调整
//然后堆的大小减一
//注意end 是从n-1开始的(数组最后一个元素的下标)
int end = n-1;
while (end > 0)
{
//swap end = n-1 这表示下标
swap(&arr[0],&arr[end]);
//adjustdown 函数里面的end是元素的个数,所以不是先--end
//所以
AdjustDown(arr,end,0);
end--;
}
}
冒泡排序的基本思想是:对相邻的元素进行两两比较,顺序相反则进行交换,这样每一次遍历都将最大的元素"浮"到数列的最后,下一次遍历则考虑剩下的元素。通过多次遍历,可以将整个数列排序。
代码展示
void BubbleSort(int* arr,int n)
{
//遍历n-1趟
for (int i = 0; i < n - 1;i++)
{
//每一趟交换的次数
for (int j = 0; j < n - 1 - i;j++)
{
//冒泡升序
if (arr[j] > arr[j+1])
{
swap(&arr[j], &arr[j + 1]);
}
}
}
}
冒泡排序总结:
最好情况就是,数据本身是有序的,在去遍历一遍时通过记录值来判断有序,当整体有序直接跳出循环
如下方:
void BubbleSort(int* arr,int n)
{
for (int i = 0; i < n-1;i++)
{
//使用记录值
bool exchange = false;
for (int j = 0; j < n - 1 - i;j++)
{
if (arr[j] > arr[j+1])
{
swap(&arr[j],&arr[j+1]);
//发生了交换,说明不是有序
exchange = true;
}
}
//当上方循环一遍,exchange仍为false,说明整体有序直接跳出循环
//这样最好的情况时间复杂度可以达到O(N)
if (exchange == false)
break;
}
}
快速排序是一种被广泛运用的排序算法,它的基本原理是分治法。具体来说,就是通过一趟排序将待排序的序列分割为左右两个子序列,左边的子序列中所有数据都比右边子序列中的数据小,然后对左右两个子序列继续进行排序,直到整个序列有序。
快速排序的平均时间复杂度为O(nlogn),最坏情况下为O(n^2)。快速排序在处理大量数据时效率较高。但是快速排序是不稳定的,快速排序在处理相同元素时,它们的相对位置可能会改变。例如,对序列 9 6 9 1 2 3 进行排序,第一趟排序后变为 3 6 9 1 2 9,原本第一个9的位置发生了变化,因此快速排序是不稳定的。
所以这里先 根据基准 key 来进行分左右子序列, 基准的选择先固定选数组的第一个元素,然后找左右下标,从数组左边找大,数组右边找小。左右两边找到后进行交换,当left == right,在把基准进行交换。
void QuickSort(int* arr, int begin,int end)
{
//区间只有一个结点或者区间不存在,停止递归
if (begin >= end)
return;
int keyi = begin;
int left = begin, right = end;
while (left<right)
{
//右边找小
while (left<right && arr[right] >= arr[keyi])
{
right--;
}
//左边找大
while (left<right && arr[left]<= arr[keyi])
{
left++;
}
//找到后进行交换
swap(&arr[left],&arr[right]);
}
//交换基准
swap(&arr[left],&arr[keyi]);
//递归左右子序列
keyi = left;
//[begin,keyi-1]keyi[keyi+1,end]
QuickSort(arr,begin,keyi-1);
QuickSort(arr,keyi+1,end);
}
进行数据的交换,把中间大的数据仍然交换放到数组的左端,作为基准进行排序。时间复杂度为O(n*logn)
代码展示
void swap(int* s1 ,int* s2)
{
int tmp = *s1;
*s1 = *s2;
*s2 = tmp;
}
void PrintSort(int* arr, int n)
{
for (int i = 0; i < n; ++i)
{
printf("%d ",arr[i]);
}
}
int GetMidi(int* arr, int begin,int end)
{
int midi = (begin + end) / 2;
if (arr[begin] < arr[midi])
{
if (arr[midi] < arr[end])
{
return midi;
}
else if (arr[begin] > arr[end])
{
return begin;
}
else
return end;
}
else
{
if (arr[midi] > arr[end])
{
return midi;
}
else if (arr[begin] < arr[end])
{
return begin;
}
else
{
return end;
}
}
}
void QuickSort(int* arr,int begin,int end)
{
//只有一个结点的时候直接进行返回
if (begin >= end)
return;
//优化部分,三数取中
int midi = GetMidi(arr,begin,end);
//把中位数放到基准的位置
swap(&arr[midi],&arr[begin]);
//选基准
int left = begin;
int right = end;
int keyi = begin;
while (left < right)
{
//数组的右边的数据先走
// 左右两边向中间进行聚拢
//右边走遇到比基准小的值就停下,左边走遇到大于基准的就停下
//都停下后,交换左右两边的数
while (left<right && arr[right] >= arr[keyi])
{
right--;
}
while (left<right && arr[left] <= arr[keyi])
{
left++;
}
//找到后就就进行交换
swap(&arr[left],&arr[right]);
}
//当上方一轮遍历完后
//即left == right
//在相遇点与基准keyi进行交换
swap(&arr[keyi],&arr[left]);
keyi = left;
//进行递归快排左右子序列
//[begin,keyi-1]keyi[keyi+1,end]
QuickSort(arr,begin,keyi-1);
QuickSort(arr,keyi+1,end);
}
小区间优化法
//当排序元素数据量很小的时候直接调用插入排序
if (end - begin+1 < 10)
{
InsertSort(arr+begin, end - begin + 1);
}else
{
... //调用快速排序
}
挖坑法
因为快排在进行完单趟排序后,然后去递归左右子区间的单趟排序,这针对之前hoare版本的单趟排序进行优化。
首先,在区间的开头左边的值记录一下(key),然后用坑位下标(holei)记录此数据。左边就是坑位;右边找小填到左边的坑中,之后,右边就形成了新的坑;左边找大填到右边的坑中;当begin == end 就结束,最后把key记录的值填到最后的坑里面去。
//挖坑法
int QuickSortPart2(int* arr, int begin, int end)
{
//三数取中
int midi = GetMidi(arr, begin, end);
swap(&arr[begin], &arr[midi]); //交换基准值
int key = arr[begin];
int holei = begin;
while (begin < end) //相遇停止
{
//右边找小
while (begin < end && arr[end] >= key)
{
--end;
}
//找到后填到左边的坑中,这样就把比基准小的值放到左边
arr[holei] = arr[end];
//此时右边形成新的坑位了,更新坑位下标
holei = end;
//左边找大
while (begin<end && arr[begin] <= key)
{
++begin;
}
//找到填到右边的坑
arr[holei] = arr[begin];
holei = begin;
}
//相遇把key记录的值放进去
arr[holei] = key;
return holei;
}
前后指针法
单趟进行优化,首先,一个指针指向开头(prev),一个指针指向其后(cur)。当cur遇到比key小的值,++prev交换prev和cur位置的值,再++cur。当cur遇到比key大的值,++cur。
如图:
//双指针法(前后指针法)
int QuickSortPart3(int* arr, int begin, int end)
{
//三数取中
int midi = GetMidi(arr, begin, end);
swap(&arr[begin], &arr[midi]); //交换基准值
int keyi = begin;
//首先一个指针指向开头,另一个指向其后面那个
int prev = begin;
int cur = prev + 1;
//while (cur <= end )
//{
// //当cur遇到比key大的值,++cur
// if (arr[cur] > arr[keyi])
// {
// ++cur;
// }
// else
// {
// //cur遇到比key小的值,++prev,交换prev和cur位置的值,再++cur
// ++prev;
// swap(&arr[prev],&arr[cur]);
// ++cur;
// }
//}
//swap(&arr[prev], &arr[keyi]);
//return prev;
//优化一
//while ( cur <= end)
//{
// if (arr[cur] < arr[keyi])
// {
// ++prev;
// swap(&arr[prev],&arr[cur]);
// }
// ++cur;
//}
//swap(&arr[prev],&arr[keyi]);
//return prev;
//优化二
while (cur <= end)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
swap(&arr[prev],&arr[cur]);
++cur;
}
swap(&arr[prev],&arr[keyi]);
return prev;
}
递归改为非递归我们需要借助 数据结构的栈来进行实现。借助出栈来取出每一次的区间进行处理。分出两段区间,处理完之后的入栈条件是:区间合理(左<=右),当左>右不入栈。当不再有区间进栈后(栈为空时) 结束。
如图:
代码展示
//非递归快速排序
void QuickSortNonR(int* arr, int begin, int end)
{
Stack st;
InitStack(&st);
//栈 后进先出
//区间压栈
//要想让区间的左值先出,选要后进栈
PushStack(&st,end);
PushStack(&st,begin);
while (!EmptyStack(&st))
{
//出栈 找出区间的左右下标,进行处理
int left = TopStack(&st);
PopStack(&st);
int right = TopStack(&st);
PopStack(&st);
// 使用了双指针法进行了一次快速排序
int keyi = QuickSortPart3(arr, left, right);
//左右子区间 [left,keyi-1] keyi [keyi+1,right]
//区间合理(左<=右),当左>右不入栈。当不在有区间进栈后(栈为空时) 结束
//左区间满足条件
if (left < keyi - 1)
{
//后进先出
//左子区间的右下标进栈
PushStack(&st, keyi - 1);
//左子区间的左下标进栈
PushStack(&st, left);
}
//右区间满足条件
if (keyi+1 < right)
{
//后进先出
//右子区间的右下标进栈
PushStack(&st,right);
//右子区间的左下标进栈
PushStack(&st,keyi+1);
}
}
DestroyStack(&st);
}
归并排序(MERGE-SORT)是建立在归并操作上的一种有效且稳定的排序算法,主要思想是将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。依次归并达到整体有序。
若将两个有序表合并成一个有序表,称为二路归并。具体过程为:
代码展示
//归并排序子函数
void _MergeSort(int* arr,int begin,int end,int* tmp)
{
//当区间只有一个结点或区间不存在,停止递归,返回调用的地方
if (begin >= end)
return;
//找到区间中间点
//分成左右两个区间进行递归
//[begin,mid][mid+1,end]
int mid = (begin + end) / 2;
_MergeSort(arr,begin,mid,tmp);
_MergeSort(arr,mid+1,end,tmp);
//有序归并
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
//归并时,有一边结束就结束了,两个都没有结束就继续
int i = begin; //注意 归并到指定的位置去
while (begin1 <= end1 && begin2 <= end2)
{
//取小数据尾插到tmp数组
if (arr[begin1] <= arr[begin2])
{
tmp[i++] = arr[begin1++];
}
else
{
tmp[i++] = arr[begin2++];
}
}
//可能存在一边先结束,那么另一边就继续
while (begin1 <= end1)
{
tmp[i++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = arr[begin2++];
}
//之后再把数组拷贝回去
memcpy(arr+begin,tmp+begin,sizeof(int)*(end-begin+1));
}
//归并排序
void MergeSort(int* arr,int n)
{
//归并排序,先分解成单个有序,再进行有序归并
//借助辅助空间O(n)
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
//开辟成功调用子函数,进行归并
_MergeSort(arr,0,n-1,tmp);
free(tmp);
}
归并排序总结:
归并排序的非递归方式
思路:先一个一个(单独一个默认有序)数据归并成两个有序,然后再两个两个归并成四个有序,… ,直到整体有序
如图:
代码展示
void MergeSortNR(int* a, int n)
{
//非递归方式实现归并排序
//申请辅助空间
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
//二路归并:两个有序子序列 归并为一个有序的序列
int gap = 1; //先单个元素有序
while (gap < n)
{
//一层归并
int i = 0;
for (i = 0;i<n;i+= 2*gap)
{
int begin1 = i, end1 = i+gap-1;
int begin2 = i+gap, end2 = i+2*gap-1;
if (end1 >= n || begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
int j = begin1; //注意 这里是从i 因为归并不是一定全在下标0开始的,也有可能从右边的子数组开始的
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
tmp[j++] = a[begin1++];
else
tmp[j++] = a[begin2++];
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
//一组归并后,立即拷贝
memcpy(a+i,tmp+i,sizeof(int)*(end2-i+1)); //注意放在里面
}
gap *= 2;
}
free(tmp);
}
计数排序的思想:
代码展示
void CountSort(int* a, int n)
{
//排升序
//第一步 相对映射的的方式找出最小值和最大值
int i = 0;
int min = a[0], max = a[0];
for (i = 1; i < n;i++)
{
if (a[i] <= min)
min = a[i];
if (a[i] > max)
max = a[i];
}
//第二步 根据最大和最小值确定相对映射数组的取值范围,并动态开辟临时数组
int range = max - min + 1;
int* count = (int*)calloc(range,sizeof(int));
if (count == NULL)
{
perror("calloc fail");
return;
}
//第三步 统计数据出现的次数
i = 0;
for(i = 0;i<n;i++)
{
count[a[i] - min]++; // 相对映射
}
//第四步 将临时数组记录的数据有序放入数组a中
//注意 因为是相对映射,所以这里要加上min(放入数组a的时候)
int j = 0;
i = 0;
for (j = 0; j < range;j++)
{
//因为相同数据可能会出现多次
//没有出现一次的元素,临时数组存放0
while(count[j]--)
{
//有数据就放入a数组中
a[i++] = j + min;
}
}
//第五步 释放内存
free(count);
}
计数排序的优缺点:
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
直接插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(n*log(n))~O(n^2) | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(n*log(n)) | O(n*log(n)) | O(n*log(n)) | O(1) | 不稳定 |
归并排序 | O(n*log(n)) | O(n*log(n)) | O(n*log(n)) | O(n) | 稳定 |
快速排序 | O(n*log(n)) | O(n*log(n)) | O(n^2) | O(log(n))~O(n) | 不稳定 |