需要更好的阅读的体验请移步 👉 小牛肉的个人博客 👈
一、插入排序 O(n^2)
1. 直接插入排序 稳定
算法思想:
边查边移:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
算法图解:
具体代码:
template <class T>
void Sort(T a[], int n){ //n表示数组长度
int i,j;
for(i = 1;i<n;i++){
if(a[i]<a[i-1]){
T temp = a[i];
for(j = i-1; a[j]>temp && j>=0; j--)
a[j+1] = a[j]; //元素后移
a[j+1] = temp; //插入
}
}
}
算法分析:
- 平均时间复杂度:O(n^2)
- 最好情况(有序) O(n)
- 最坏情况(逆序) O(n^2)
- 稳定性:稳定
2. 折半插入排序 稳定
算法思想:
先查后移:
将直接插入排序中寻找A[i]的插入位置的方法改为采用折半比较,即可得到折半插入排序算法,
仅仅减少了查找排序的比较次数,约为O(nlog2n),元素的移动次数并未改变,依赖于排序表的初始状态
具体代码:
void Sort(int a[], int n){ //n表示数组长度
int i,j,temp;
int low,high,mid;
for(i = 1;i<n;i++){ //以第一个数a[0]为基准比较
temp = a[i];
low = 0;high = i-1;
//查找
while(low<=high){
mid = (low+high)/2;
if(a[mid]>temp) high = mid-1;
else low = mid+1;
}
//后移
for(j = i-1;j>=high+1;j--)
a[j+1] = a[j];
a[high+1] = temp;
}
}
算法分析:
- 平均时间复杂度:O(n^2)
- 稳定性:稳定
3. 希尔排序 不稳定
算法思想:
又称“缩小增量排序”
把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;
随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
数组长度10
则增量序列一般为:5,3,1
算法图解:
具体代码:
void Sort(int a[], int n){ //n表示数组长度
int i, j, dk, temp;
for (dk = n / 2; dk >= 1; dk = dk / 2){
//步长为dk的直接插入排序
for (i = dk + 1; i < n; i++){
if(a[i-dk]>a[i]){
temp = a[i];
for (j=i-dk; a[j]>temp && j>=0; j=j-dk)
a[j + dk] = a[j];
a[j + dk] = temp;
} //if
}
}
}
二、交换排序
1. 快速排序 O(nlog2n) 不稳定
算法思想:
- 从数列中挑出一个元素,称为 “基准”(pivot)(一般选取数列的第一个元素作为基准);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
具体代码:
void QuickSort(int A[], int low, int high)
{
if (low < high)
{
int pivots = Partition(A, low, high);
QuickSort(A, low, pivots);
QuickSort(A, pivots + 1, high);
}
}
//划分算法
int Partition(int A[], int low, int high)
{
int pivots = A[low];
while (low < high)
{
//从后往前找比中轴值小的元素,若找到,则交换
// 一定要先从后往前查找,再从前往后
while (low < high && A[high] >= pivots)
high--;
A[low] = A[high];
//从前往后找比中轴值大的元素,若找到,则交换
while (low < high && A[low] <= pivots)
low++;
A[high] = A[low];
}
A[low] = pivots;
return low;
}
算法分析
- 最好(划分平衡):O(nlogn)
- 最坏(有序):O(n2)
假如给定15个数,求快速排序的最多、最少比较次数
2. 冒泡排序 O(n^2) 稳定
算法思想:
从后往前两两比较相邻的元素,若为逆序,则交换,知道序列比较完,称为一趟冒泡。
下一趟冒泡时,前一趟确定的最小元素不再参与比较,待排序列少一个元素。
这个算法的名字由来是因为越小(大)的元素会经由交换慢慢“浮”到数列的顶端,故名。
算法图解:
具体代码:
void Sort(int a[], int n){ //n表示数组长度
for(int i = 0;i<n;i++){
for(int j = n-1; j>i; j--){ //从后往前进行比较
if(a[j]<a[j-1])
swap(a[j],a[j-1]);
}
}
}
算法改进:
void Sort(int a[], int n){ //n表示数组长度
for(int i = 0;i<n;i++){
int flag = false; //是否发生交换的标志
for(int j = n-1; j>i; j--){
if(a[j]<a[j-1]){
swap(a[j],a[j-1]);
flag = true;
}
}
//若本趟遍历后没有发生交换,说明表已经有序
if (flag == false)
return;
}
}
算法分析:
- 平均时间复杂度:O(n2)
- 最好情况(有序) O(n)
- 最坏情况(逆序) O(n2)
- 稳定性:稳定(全局有序的)
三、选择排序 不稳定
1. 简单选择排序 O(n^2)
算法思想:
每次排序都从未排序列选取最小元素放在第 i 个位置,第 i 次排序即从 i~n 中挑选最小元素与 A[i] 交换,一共循环 n-1 次
具体代码:
template<class T>
void SortTwo(T *a, int len){
int min; //记录最小值下标
for(int i = 0;i<len-1;i++){
min = i; //设置最小值
for(int j = i+1; j<len; j++){
if(a[min]>a[j])
min = j;
}
//更新最小值
if(min != i){
T temp = a[min];
a[min] =a[i];
a[i] = temp;
}
}
}
算法分析:
元素间比较的次数与序列的初始状态无关
时间复杂度始终是 O(n^2)
2. 堆排序 O(nlog2n)
算法思想:
-
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;(先找n/2[取下整]的分支结点,即最后一个分支结点,调整该结点和他的孩子结点,然后依次从下往上对结点数-1的结点进行调整,最后再从上往下调整检查一遍)
-
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
-
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
算法图解:
算法分析:
时间复杂度始终为O(nlog2n)
稳定性:不稳定
四、归并排序 O(nlog2n) 稳定
算法思想:
归并的含义是将两个或以上的有序表组合成一个新的有序表;
以二路归并为例:两两归并,Merge()的功能是将前后相邻的两个有序表归并为一个有序表;
递归形式的二路归并是基于分治算法的;
算法图解:
具体代码:
int *B = (int *)malloc(sizeof(int) * (n + 1)); //动态分配辅助数组B
void Merge(int A[], int low, int mid, int high)
{
for (int k = low; k <= high; k++)
B[k] = A[k]; //将A中所有元素复制到B中
//对B两个部分依次比较,选取最小值放入A
for (i = low, j = mid + 1, k = i; i < mid && j <= high; k++)
{
if (B[j] <= B[i])
A[k] = B[i++];
else
A[k] = B[j++];
}
while (i <= mid) //若第一个表未检测完
A[k++] = B[i++];
whle(j <= high) //若第二个表未检测完
A[k++] = B[j++];
}
void MergeSort(int A[], int low, int high)
{
int mid = (low + high) / 2; //从中间划分两个子序列
MergeSort(A, low, mid); //对左侧序列进行递归排序
MergeSort(A, mid + 1, high); //对右侧序列进行递归排序
Merge(A, low, mid, high); //归并
}
算法分析:
时间复杂度:O(nlog2n)
稳定性:稳定
五、基数排序 稳定
算法思想:
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。
由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
算法图解:
具体代码:
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
int maxData = data[0]; ///< 最大数
/// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
for (int i = 1; i < n; ++i)
{
if (maxData < data[i])
maxData = data[i];
}
int d = 1;
int p = 10;
while (maxData >= p)
{
//p *= 10; // Maybe overflow
maxData /= 10;
++d;
}
return d;
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int *tmp = new int[n];
int *count = new int[10]; //计数器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进行d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete []tmp;
delete []count;
}