1.八大排序
排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。
本文将依次介绍上述八大排序算法。
八大排序的代码接口如下:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <assert.h>
#include <string.h>
//打印数组
void PrintArray(int* a,int n);
//交换
void Swap(int* p1, int* p2);
// 插入排序
void InsertSort(int* a, int n);
// 希尔排序
void ShellSort(int* a, int n);
// 选择排序
void SelectSort(int* a, int n);
// 堆排序
void AdjustUp(int* a, int n, int child);
void AdjustDwon(int* a, int n, int parent);
void HeapSort(int* a, int n);
// 冒泡排序
void BubbleSort(int* a, int n);
// 快速排序递归实现
//取中间值
int GetMinIndex(int* a, int left, int right);
// 快速排序hoare版本
int PartSort1(int* a, int left, int right);
// 快速排序挖坑法
int PartSort2(int* a, int left, int right);
// 快速排序前后指针法
int PartSort3(int* a, int left, int right);
void QuickSort(int* a, int left, int right);
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right);
// 归并排序递归实现
void _MergeSort(int* a, int* tmp, int begin, int end);
void MergeSort(int* a, int n);
// 归并排序非递归实现
void MergeSortNonR(int* a, int n);
// 计数排序
void CountSort(int* a, int n);
1.1 直接插入排序
插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法步骤:
1)将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2)从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
演示过程图:
代码:
// 直接插入排序
void InsertSort(int* a, int n)
{
for (int i = 1; i < n; i++)
{
int end = i - 1;
int tmp = a[i];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else
break;
}
a[end + 1] = tmp;
}
}
1.2 希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进的方法:
插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率,但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位。希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
算法步骤:
1)选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
2)按增量序列个数k,对序列进行k 趟排序;
3)每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
静态过程图:
动态演示图:
代码:
// 希尔排序
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
//gap /= 2;
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
}
1.3 选择排序
选择排序(Selection sort)也是一种简单直观的排序算法。
算法步骤:
1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
2)再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3)重复第二步,直到所有元素均排序完毕。
过程演示图:
代码:
// 选择排序
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
while (left < right)
{
int maxi = left;
int mini = left;
for (int i = left; i <= right; i++)
{
if (a[i] > a[maxi])
maxi = i;
if (a[i] < a[mini])
mini = i;
}
Swap(&a[left], &a[mini]);
if(maxi == left)
maxi = mini;
Swap(&a[right], &a[maxi]);
left++;
right--;
}
}
1.4 堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。 堆排序的平均时间复杂度为Ο(nlogn) 。
算法步骤:
1)创建一个堆H[0…n-1]
2)把堆首(最大值)和堆尾互换
3)把堆的尺寸缩小1,并调用shift_down(0),目的是把新的数组顶端数据调整到相应位置
4) 重复步骤2,直到堆的尺寸为1
过程演示图:
代码:
// 堆排序
//向上调整
void AdjustUp(int* a, int n, int child)
{
int parent = (child - 1) / 2;
while (parent >= 0)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
//向下调整
void AdjustDwon(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (a[child] < a[child + 1] && child+1 < n)
child++;
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
void HeapSort(int* a, int n)
{
for (int i = 0; i < n; i++)
AdjustUp(a, n, i);
int end = n - 1;
while (end)
{
Swap(&a[0], &a[end]);
AdjustDwon(a, end, 0);
end--;
}
}
1.5 冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法描述:
(1)比较相邻的元素。如果第一个比第二个大,就交换它们两个;
(2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在
(3)最后的元素应该会是最大的数;
(4)针对所有的元素重复以上的步骤,除了最后一个;
(5)重复步骤1~3,直到排序完成。
过程演示图:
代码:
// 冒泡排序
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n-1; i++)
{
for (int j = 0; j < n - 1 - i; j++)
{
if (a[j] > a[j + 1])
Swap(&a[j], &a[j + 1]);
}
}
}
1.6 快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序有以下三种方法:
hoare法:
快速排序的Hoare法是一种二叉树结构的交换排序方法,由Tony Hoare于1962年提出。其基本思想是选择一个基准值,通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据要小,然后再递归地对这两部分进行排序,以达到整个数据变成有序序列。
Hoare法主要步骤如下:
1.选择基准值:从待排序序列中选择一个基准值,这个基准值可以是序列的第一个元素、中间元素或最后一个元素。
2.数据分割:通过一趟排序,将待排序的数据分割成两部分,使得左边的所有数据都比基准值小,而右边的所有数据都比基准值大。
3.递归排序:对分割后的两部分数据分别进行递归排序,直到每一部分只包含一个数据或者没有数据为止。
在实现上,快速排序的Hoare法通常采用递归算法,通过不断地选择基准值、分割数据和递归排序,最终实现整个序列的有序排列。这种方法的优点在于其平均时间复杂度较低,可以达到O(nlogn)
O(nlogn),但在最坏情况下,时间复杂度会退化到O(n2)O(n2)。因此,为了优化性能,通常会采用一些策略来选择基准值的位置,以减少最坏情况的发生
过程演示图:
代码:
int PartSort1(int* a, int left, int right)
{
////随机取数(下标)
//int randi = left + (rand() % (right - left));
//Swap(&a[randi], &a[left]);
//三数取中
int midi = GetMinIndex(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left;
while (left < right)
{
while (a[right] >= a[keyi] && left < right)
right--;
while (a[left] <= a[keyi] && left < right)
left++;
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
return keyi;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort1(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, left + 1, right);
}
挖坑法:
-
第一步:将key的位置(即为第一个元素的位置)作为第一个坑位,将key的值一直保存在变量key中。
-
第二步:定义一个right从数组最后一个元素开始即为数组右边开始向左遍历,如果找到比key小的值,right停下来,将right下标访问的元素赋值到上一个坑位,并将right作为新的坑位。
-
第三步:定义一个left从数组第一个元素开始即为数组左边开始向右遍历,如果找到比key大的值,left停下来,将left下标访问的元素赋值到上一个坑位,并将left作为新的坑位。
-
第四步:当right和left相遇时,此时它们访问的元素绝对是坑位,只需将key里保存的key值放入坑位即可。
-
第五步:让他们相遇位置的左区间和右区间同样执行上述四步(即为递归)。
过程演示图:
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
//三数取中
int midi = GetMinIndex(a, left, right);
Swap(&a[left], &a[midi]);
int key = a[left];
int keyi = left;
while (left < right)
{
while (a[right] >= key && left < right)
right--;
a[left] = a[right];
while (a[left] <= key && left < right)
left++;
a[right] = a[left];
}
a[left] = key;
keyi = left;
return keyi;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort2(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, left + 1, right);
}
前后指针法:
第一步:定义两根指针cur和prev。
第二步:cur开始往后走,如果遇到比key小的值,则++prev,然后交换prev和cur指向的元素,再++cur,如果遇到比key大的值,则只++cur。
第三步:当cur访问过最后一个元素后,将key的元素与prve访问的元素交换位置。
第四步:让prev的左区间和右区间同样执行上述三步(即为递归)。
过程演示图:
代码:
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
//三数取中
int midi = GetMinIndex(a, left, right);
Swap(&a[left], &a[midi]);
int key = a[left];
int keyi = left;
while (left < right)
{
while (a[right] >= key && left < right)
right--;
a[left] = a[right];
while (a[left] <= key && left < right)
left++;
a[right] = a[left];
}
a[left] = key;
keyi = left;
return keyi;
}
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
//三数取中
int midi = GetMinIndex(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left;
int prev = keyi;
int cur = prev + 1;
while (cur <= right)
{
//if (a[cur] < a[keyi] && ++prev != cur)
// Swap(&a[cur], &a[prev]);
//++cur;
if (a[cur] < a[keyi])
{
++prev;
Swap(&a[cur], &a[prev]);
++cur;
}
else
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
return keyi;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, left + 1, right);
}
快速排序非递归代码实现(需要用到栈):
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
ST st;
STInit(&st);
STPush(&st, left);
STPush(&st, right);
while (!STEmpty(&st))
{
int end = STTop(&st);
STPop(&st);
int begin = STTop(&st);
STPop(&st);
int keyi = PartSort1(a, begin, end);
//int keyi = PartSort2(a, begin, end);
//int keyi = PartSort3(a, begin, end);
if (keyi + 1 < end)
{
STPush(&st, keyi + 1);
STPush(&st, end);
}
if (keyi - 1 > begin)
{
STPush(&st, begin);
STPush(&st, keyi - 1);
}
}
STDestroy(&st);
}
栈的代码实现:
栈的接口代码:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST* ps);
void STDestroy(ST* ps);
void STPush(ST* ps, STDataType x);
void STPop(ST* ps);
int STSize(ST* ps);
bool STEmpty(ST* ps);
STDataType STTop(ST* ps);
栈的接口内容:
#include"Stack.h"
void STInit(ST* ps)
{
assert(ps);
ps->a = (STDataType*)malloc(sizeof(STDataType) * 4);
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
ps->capacity = 4;
ps->top = 0; // top是栈顶元素的下一个位置
//ps->top = -1; // top是栈顶元素位置
}
void STDestroy(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
void STPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
STDataType* tmp = (STDataType*)realloc(ps->a,
sizeof(STDataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->top] = x;
ps->top++;
}
void STPop(ST* ps)
{
assert(ps);
assert(!STEmpty(ps));
ps->top--;
}
int STSize(ST* ps)
{
assert(ps);
return ps->top;
}
bool STEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
STDataType STTop(ST* ps)
{
assert(ps);
assert(!STEmpty(ps));
return ps->a[ps->top - 1];
}
1.7 归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
算法步骤:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针达到序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
过程演示图:
代码(递归):
// 归并排序递归实现
void _MergeSort(int* a, int* tmp, int begin, int end)
{
if (begin >= end)
return;
int midi = (begin + end) / 2;
_MergeSort(a, tmp, begin, midi);
_MergeSort(a, tmp, midi + 1, end);
int begin1 = begin, end1 = midi;
int begin2 = midi + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
tmp[i++] = a[begin1++];
else
tmp[i++] = a[begin2++];
}
while (begin1 <= end1)
tmp[i++] = a[begin1++];
while (begin2 <= end2)
tmp[i++] = a[begin2++];
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail!\n");
exit(-1);
}
_MergeSort(a, tmp, 0, n - 1);
free(tmp);
}
代码(非递归):
// 归并排序非递归实现
//version one
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail!\n");
exit(-1);
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int j = i;
if (end1 >= n)
{
end1 = n - 1;
begin2 = n;
end2 = n - 1;
}
if (end1 < n && begin2 >= n)
{
begin2 = n;
end2 = n - 1;
}
if (begin2 < n && end2 >= n)
end2 = n - 1;
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, tmp, sizeof(int) * n);
gap *= 2;
}
free(tmp);
}
//version two
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail!\n");
exit(-1);
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int j = i;
if (end1 >= n)
{
end1 = n - 1;
begin2 = n;
end2 = n - 1;
}
if (end1 < n && begin2 >= n)
{
begin2 = n;
end2 = n - 1;
}
if (begin2 < n && end2 >= n)
end2 = n - 1;
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 + begin1, tmp + begin1, sizeof(int) * (end1 - begin1 + 1));
}
gap *= 2;
}
free(tmp);
}
1.8 计数排序
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
算法描述:
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
过程演示图:
代码:
// 计数排序
void CountSort(int* a, int n)
{
int max = a[0];
int min = a[0];
for (int i = 1; i < n; i++)
{
if (max < a[i])
max = a[i];
if (min > a[i])
min = a[i];
}
int size = max - min + 1;
int* cnt = (int*)malloc(sizeof(int) * size);
if (cnt == NULL)
{
perror("malloc fail!\n");
exit(-1);
}
memset(cnt, 0, sizeof(int) * size);
for (int i = 0; i < n; i++)
{
cnt[a[i] - min]++;
}
int j = 0;
for (int i = 0; i < size; i++)
{
while (cnt[i]--)
{
a[j++] = i + min;
}
}
free(cnt);
}
2.排序算法复杂度及稳定性
各种排序的稳定性,时间复杂度、空间复杂度、稳定性总结如下图:
(表中最后两个桶排序和基数排序仅需了解,不要求掌握。)
注意:在特殊情况下,排序的时间复杂度会发生变化,不能作为判断该排序是否稳定的依据。
稳定性:指数组中相同元素在排序后相对位置不发生变化。
3.测试代码
最后,给出一段测试代码,将上述排序代码在测试代码中调用,能测出每种排序的性能。
void TestOP()
{
srand(time(0));
const int N = 10000000;
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);
int* a3 = (int*)malloc(sizeof(int) * N);
int* a4 = (int*)malloc(sizeof(int) * N);
int* a5 = (int*)malloc(sizeof(int) * N);
int* a6 = (int*)malloc(sizeof(int) * N);
int* a7 = (int*)malloc(sizeof(int) * N);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
a7[i] = a1[i];
}
int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
int begin3 = clock();
SelectSort(a3, N);
int end3 = clock();
int begin4 = clock();
HeapSort(a4, N);
int end4 = clock();
int begin5 = clock();
QuickSort(a1, 0, N - 1);
int end5 = clock();
int begin6 = clock();
MergeSort(a6, N);
int end6 = clock();
int begin7 = clock();
BubbleSort(a7, N);
int end7 = clock();
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
printf("BubbleSort:%d\n", end7 - begin7);
printf("QuickSort:%d\n", end5 - begin5);
printf("MergeSort:%d\n", end6 - begin6);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
free(a7);
}
int func(int n)
{
if (n <= 1)
return n;
return func(n - 1) + n;
}
int main()
{
TestOP();
printf("%d\n", func(100000));
return 0;
}
来都来了,点个赞再走吧~~