排序算法
部分图片摘选自https://www.cnblogs.com/onepixel/articles/7674659.html
本博客限于本人自学,更加内容请参阅↑链接
十种常见排序算法可以分为两大类:
比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。(本文暂不涉及)
算法复杂度
1、直接插入排序 o(n^2)
直接插入排序(Straight Insertion Sort)是一种最简单的排序方法,其基本操作是将一条记录插入到已排好的有序表中,从而得到一个新的、记录数量增1的有序表。
1.1 算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
从第一个元素开始,该元素可以认为已经被排序;
取出下一个元素,在已经排序的元素序列中从后向前扫描;
如果该元素(已排序)大于新元素,将该元素移到下一位置;
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
将新元素插入到该位置后;
重复步骤2~5。
void sort1(int a[], int n) {
int i, j;
for (i = 2; i <= n; i++) {
if (a[i] < a[i - 1]) {
a[0] = a[i];//记录需要插入的元素
a[i] = a[i - 1];
//往前查找a[i]应该插入的位置
for (j = i - 2; a[0] < a[j]; j++) {
a[j+1] = a[j];
}
a[j+1] = a[0];//a[j]比a[0]小或等于
}
}
}
2、折半插入排序 o(n^2)
折半插入排序算法是一种稳定的排序算法,比直接插入算法明显减少了关键字之间比较的次数,因此速度比直接插入排序算法快,但记录移动的次数没有变,所以折半插入排序算法的时间复杂度仍然为O(n^2),与直接插入排序算法相同。附加空间O(1)。
折半查找只是减少了比较次数,但是元素的移动次数不变,所以时间复杂度为O(n^2)是正确的!
void sort0(int a[], int n) {
int i, low, high, mid, j;
for (i = 2; i <= n; i++) {
a[0] = a[i];//保存待插入元素
low = 1, high = i - 1;
while (low <= high) {
mid = (low + high) / 2;
if (a[0] > a[mid]) {
low = mid + 1;
}
else
high = mid - 1;
}
for (j = i - 1; j >= high + 1; j--) {//high+1为插入位置
a[j + 1] = a[j];
}
a[high + 1] = a[0];
}
}
3、希尔排序 O(n^3/2)
希尔排序是非稳定排序算法
不需要大量的辅助空间,和归并排序一样容易实现。希尔排序是基于插入排序的一种算法, 在此算法基础之上增加了一个新的特性,提高了效率。希尔排序的时间的时间复杂度为O(n^3/2),希尔排序时间复杂度的下界是nlog2n。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O(n^2)复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法. 本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。 原因是,当n值很大时数据项每一趟排序需要移动的个数很少,但数据项的距离很长。当n值减小时每一趟需要移动的数据增多,此时已经接近于它们排序后的最终位置。 正是这两种情况的结合才使希尔排序效率比插入排序高很多。Shell算法的性能与所选取的分组长度序列有很大关系。只对特定的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数。想要弄清关键词比较次数和记录移动次数与增量选择之间的关系,并给出完整的数学分析,今仍然是数学难题。*
3.1 算法描述
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
按增量序列个数k,对序列进行k 趟排序;
每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
//本代码是从a[0]开始排序的,其他从a[1]开始,a[0]是哨兵;这个以后改进吧,睡啦
void sort2(int a[],int n) {
int i, j, k;
int gap;//gap是分组的步长
int temp;//希尔排序是在直接插入排序的基础上实现的,所以仍然需要哨兵
for (gap = n / 2; gap > 0; gap = gap / 2) {
for (i = 0; i < gap; i++) {
for (j = i + gap; j < n; j += gap) {
if (a[j] < a[j - gap]) {
temp = a[j];//哨兵
k = j - gap;
while (k >= 0 && a[k] > temp) {
a[k + gap] = a[k];
k -= gap;
}
a[gap + k] = temp;
}
}
}
}
}
4、冒泡排序 o(n^2)
稳定排序算法
4.1 算法描述
比较相邻的元素。如果第一个比第二个大,就交换它们两个;
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
针对所有的元素重复以上的步骤,除了最后一个;
重复步骤1~3,直到排序完成。
void sort3(int* a, int n) {
int i, j;
bool change;//判断这一层冒泡循环有没有位置交换
//循环n-1次就行
for (i = 1, change = 1; i < n && change; i++) {
change = false;
for (j = 1; j < n - i + 1; j++) {
if (a[j] > a[j + 1]) {
a[0] = a[j];
a[j] = a[j + 1];
a[j + 1] = a[0];
change = 1;
}
}
}
}
5、快速排序
时间复杂度o(nlog2n) 空间复杂度o(log2n)
5.1 算法描述
从数列中挑出一个元素,称为 “基准”(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
//一趟划分
int Partition(int* a, int low, int high) {
a[0] = a[low];
int i = low, j = high;
while (i < j) {
while (i < j && a[j] >= a[0]) j--;
a[i] = a[j];
while (i < j && a[i] <= a[0]) i++;
a[j] = a[i];
}
a[i] = a[0];
return i;
}
//快速排序
void sort4(int* a, int low, int high) {
int temp;
if (low < high) {
temp = Partition(a, low, high);
sort4(a, low, temp - 1);
sort4(a, temp + 1, high);
}
}
6、简单选择排序 O(n^2)
在简单选择排序过程中,所需移动记录的次数比较少。最好情况下,即待排序记录初始状态就已经是正序排列了,则不需要移动记录。
最坏情况下,即待排序记录初始状态是按第一条记录最小,之后的记录从小到大顺序排列,则需要移动记录的次数最多为3(n-1)。简单选择排序过程中需要进行的比较次数与初始状态下待排序的记录序列的排列情况无关。当i=1时,需进行n-1次比较;当i=2时,需进行n-2次比较;依次类推,共需要进行的比较次数是(n-1)+(n-2)+…+2+1=n(n-1)/2,即进行比较操作的时间复杂度为O(n^2),进行移动操作的时间复杂度为O(n)。
简单选择排序是不稳定排序。
6.1 算法流程
(1)从待排序序列中,找到关键字最小的元素;
(2)如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换;
(3)从余下的 N - 1 个元素中,找出关键字最小的元素,重复(1)、(2)步,直到排序结束。
void sort5(int* a, int n) {
int i, j, k;
for (i = 1; i < n; i++) {
//找出i~末尾,最小的数
for (k = i, j = i; k <= n; k++) {
if (a[k] < a[j]) j = k;
}
if (j != i) {
a[0] = a[i];
a[i] = a[j];
a[j] = a[0];
}
}
}
7、堆排序 o(nlog2n)
7.1 算法描述
将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
动图演示
//建堆
void HeapAdjust(int* a, int s, int m) {
//H.r[s..m]中除 H.r[s].key 外均满足堆的定义
// 调整 H.r[s]的关键字,使 H.r[s..m]成为一个小顶堆
int j;
a[0] = a[s];
//沿key较小的孩子结点向下筛选
for (j = 2 * s; j <= m; j *= 2) {
//j 为 key 较小的记录的下标
if (j<m && a[j]>a[j + 1]) ++j;
if (a[0] < a[j]) break;
//较小的孩子结点值换到父结点位置
a[s] = a[j];
s = j;
}
//应插入的位置在 s 处
a[s] = a[0];
}
//堆排序
void sort6(int* a, int n) {
int i;
//建立大/小顶堆
for (i = n / 2; i > 0; i--) {
HeapAdjust(a, i, n);
}
for (i = n; i > 1; --i) {
//堆顶记录和当前未排子序列中最后一个记录相交换
int temp = a[1];
a[1] = a[i];
a[i] = temp;
//将 H.r[l..i-1] 重新调整为大/小顶堆
HeapAdjust(a, 1, i - 1);
}
}
8、归并排序
8.1 算法描述
把长度为n的输入序列分成两个长度为n/2的子序列;
对这两个子序列分别采用归并排序;
将两个排序好的子序列合并成一个最终的排序序列。
//两路归并
void Merge(int* a, int i, int m, int n) {
//i是左,m是中,n是右。
int j, k;
//引入辅助数组空间 temp,有序序列为 r[i..m]和 r[m+1..n]
int b = i;
int temp[1000];//引入辅助数组
for (j = m + 1, k = 1; i <= m && j <= n; k++) {
if (a[i] < a[j]) temp[k] = a[i++];
else temp[k] = a[j++];
}
for (; i <= m;) temp[k++] = a[i++];
for (; j <= n;) temp[k++] = a[j++];
for (i = b, k = 1; i <= n;) a[i++] = temp[k++];
}
//归并排序
void sort7(int* a, int s, int t) {
int temp;
if (s < t) {
temp = (s + t) / 2;
sort7(a, s, temp);
sort7(a, temp + 1, t);
//合并 a[s]~a[m]与 a[m+1]~a[t]
Merge(a, s, temp, t);
}
}