文章目录
- 一、 稳定性
- 二、排序
- 1. 插入排序
- (1) 直接插入排序
- (2) 希尔排序
- 2. 选择排序
- (1) 直接选择排序
- (2) 堆排序
- 3. 交换排序
- (1) 冒泡排序
- (2) 快速排序
- ① 普通版快排
- ② 关于优化快排
- ③ 快速排序的非递归方式
- 4. 归并排序
- 5. 计数排序
- 三、 总结
一、 稳定性
在计算机科学中,稳定性是指在排序过程中,相等的元素的相对顺序保持不变。也就是说,如果元素a和b在排序之前是相等的,那么在排序之后,a和b的相对顺序应该和排序之前一样;否则不稳定。
二、排序
1. 插入排序
(1) 直接插入排序
直接插入排序是一种简单的排序方法,它的基本操作是将一条记录插入到已经排好序的有序表中,从而得到一个新的、记录数量增1的有序表。
具体操作步骤如下:
- 从未排序的序列中选择一个元素,将其插入到已排序序列的合适位置。
- 继续从未排序序列中取出下一个元素,然后将其插入到已排序序列的合适位置。
- 重复步骤2,直到所有元素都被插入到已排序序列中。
直接插入排序的时间复杂度为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;}
}
(2) 希尔排序
希尔排序是直接插入排序算法的一种更高效的改进版本,它是插入排序的一种,也称为“缩小增量排序”,因 D.L.Shell 于 1959 年提出而得名。
希尔排序的基本思想是:现将待排序的数组分成多个待排序的子序列,使得每个子序列的元素较少,然后对各个子序列分别进行插入排序,待到整个待排序的序列基本有序的时候,最后在对所有的元素进行一次插入排序。
也就是:分为 预排序 和 插入排序
- 预排序:这以升序为例,根据间隔(gap)分为多个子序列进行插入排序,排完后,就把较大的数据放在后面,较小的数据放在前面,这样就变为部分有序。
关于这里的gap的取值:
gap越大,大的值更快调到后面,小的值更快调到前面,接近有序的速度越慢
gap越小,跳的越慢,但是越接近有序。gap == 1 相当于插入排序 - 一次插入排序:一次插入排序后,变成整体有序。
希尔排序是不稳定的,(原因是:相同的数据可能被分在不同的组,前后数据的位置难以控制 ) 希尔排序的时间复杂度取决于间隔序列的选择。理论上,如果间隔序列是逐一减半的,希尔排序的时间复杂度可以接近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;}}
}
2. 选择排序
(1) 直接选择排序
基本思想: 每次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
需要注意的是,直接选择排序中存在大跨度的数据移动,
是一种不稳定的排序方式
原因是:
其时间复杂度是O(n ^ 2),最好的情况是O(N ^ 2),最坏的情况是O(N ^ 2); 空间复杂度为O(1)。
假设,排升序,这里我们用数组存储数据,那就是要遍历数组进行直接选择排序(选出较小的依次从数组的起始位置开始排)
图1:
因为选择排序要么选出最大的,要么选出最小的。
- 首先根据数组的个数,来确定数组存放最小值的地方和最大值的地方(升序,数组最左端存放小值[begin],数组最右端存放大值[end])
- 再使用maxi和mini下标来进行记录一轮中的最大值的下标和最小值的下标
- 找到下标后,把mini和maxi指向的值与begin和end进行交换
- 交换后,begin++;end - -; 一轮排好了,缩小数组范围,再进行下一轮遍历。
代码展示
选择排序,选出大的和选出小的同时进行
//选择排序
//时间复杂度;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]){//把下标给minimini = 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;}
}
(2) 堆排序
这里以升序为例。我们需要把数据先构建成大堆(根的值最大),然后把堆的根元素与堆最后面一个元素进行交换。然后堆的大小减一。
依次重复上述。直到排完。
堆排序的时间复杂度为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--;}
}
3. 交换排序
(1) 冒泡排序
冒泡排序的基本思想是:对相邻的元素进行两两比较,顺序相反则进行交换,这样每一次遍历都将最大的元素"浮"到数列的最后,下一次遍历则考虑剩下的元素。通过多次遍历,可以将整个数列排序。
- 如果是n个数据元素,需要遍历n-1趟,在第一趟排序中,需要比较的次数为n-1次。在第二趟排序中,需要比较的次数为n-2次,以此类推。
代码展示
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]);}}}
}
冒泡排序总结:
- 冒泡排序是稳定的
- 是一种交换类排序
- 时间复杂度,最坏的情况:O(n^2) ; 最好的情况:可以达到O(n)
最好情况就是,数据本身是有序的,在去遍历一遍时通过记录值来判断有序,当整体有序直接跳出循环
如下方:
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;}
}
(2) 快速排序
快速排序是一种被广泛运用的排序算法,它的基本原理是分治法。具体来说,就是通过一趟排序将待排序的序列分割为左右两个子序列,左边的子序列中所有数据都比右边子序列中的数据小,然后对左右两个子序列继续进行排序,直到整个序列有序。
- 选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”(pivot)。
- 分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大。
- 递归地对左右两个子序列进行快速排序,直至序列有序。
快速排序的平均时间复杂度为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);
}
② 关于优化快排
- 三数取中法:在开始(begin)、中间(mid)、最后(end)这三个位置中选出中间大(即中位数)的那个数确定下标为midi。
进行数据的交换,把中间大的数据仍然交换放到数组的左端,作为基准进行排序。时间复杂度为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;}elsereturn 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);
}
4. 归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效且稳定的排序算法,主要思想是将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。依次归并达到整体有序。
若将两个有序表合并成一个有序表,称为二路归并。具体过程为:
- 申请空间(辅助空间),使其大小为两个已经排序序列之和,该空间用来存放合并后的序列。
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置。
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置。
- 重复步骤3直到某一指针超出序列尾。将另一序列剩下的所有元素直接复制到合并序列尾。
代码展示
//归并排序子函数
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);
}
归并排序总结:
- 时间复杂度是:O( n*log(n) ) ,因为需要借助辅助空间,所以空间复杂度为O( n )
- 归并排序是稳定的
归并排序的非递归方式
思路:先一个一个(单独一个默认有序)数据归并成两个有序,然后再两个两个归并成四个有序,… ,直到整体有序
如图:
代码展示
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++];elsetmp[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);
}
5. 计数排序
计数排序的思想:
- 统计每个数据出现的次数
- 将统计出现的次数放入一个临时数组(采用相对映射的方式-将数据的最小值放到第一个数据)
代码展示
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++) {//因为相同数据可能会出现多次//没有出现一次的元素,临时数组存放0while(count[j]--){//有数据就放入a数组中a[i++] = j + min;}}//第五步 释放内存free(count);
}
计数排序的优缺点:
- 缺点:不适合分散的数据,更适合集中的数据,数据类型只适合整数
- 优点:效率极高,O(N+cntN)
- 时间复杂度:O(N+range)
- 空间复杂度:O(range)
三、 总结
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | 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) | 不稳定 |