目录
[希尔排序( 缩小增量排序 )](#希尔排序( 缩小增量排序 ))
前言
欢迎再次来到小鸥的博客,本系列总结了直接插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序八种(基数排序和桶排序不做演示)常见排序的原理,并使用C语言再现,
++快排和归并由于实现方法有多种,篇幅较长,将单独在第二篇讨论++
下篇:快排和归并-CSDN博客
本期专栏:算法_海盗猫鸥的博客-CSDN博客
个人主页:海盗猫鸥-CSDN博客
本篇中代码使用C,且数据序列使用数组为例
相关概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:在待排序的序列中存在相同的数据,他们在序列中的相对位置若在排序过后不变,则称这种排序算法是稳定的,否则不稳定。
内部排序:数据元素全部放在内存中的排序
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断地在内外存之间移动数据的排序。
排序种类
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
直接插入排序
原理解析:(升序)
当插入第 i(i>=1)个数时,前i-1个数已经有序,用arr[i]依次与arr[i-1],arr[i-2]...比较,若arr[i]小于比较数,则将次位置的数据向后挪动一位,直到arr[i]大于比较数,将arr[i]插入此位置;
每次完成一轮排序,前i-1个数都是有序的
cpp
void InsertSort(int* arr, int n)
{
for (int i = 0; i < n - 1; i++)
{
//[0,end]都为有序,向前插入数据;
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;
}
}
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
希尔排序( 缩小增量排序 )
当序列中的数据越接近倒序,直接插入排序的效率就越低,直到完全为倒序时,时间复杂度降低为O(N)
由此希尔排序是对直接插入排序的优化,可以防止上述情况
原理解析:
**1.**预排序(让数组接近有序);
直接插入排序中,每次插入的都是紧邻着的下一个位置的数据
预排序将数组中数据,按照(i,i+gap,i+2*gap...,i+n*gap)的形式,将数组分为gap组;
分别对这些组进行插入排序
**2.**插入排序
预排序后,最后进行直接插入排序,
代码中gap取值不止一个,而是按照三分之一的规律减小
代码:
cpp
void ShellSort(int* arr, int n)
{
int gap = n;
while (gap > 1)//gap为1时就是一次直接插入排序
{
//gap > 1时为预排序
//gap == 1是插入排序
gap = gap / 3 + 1;//加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;
}
}
}
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序,这样插入排序就会很快。整体而言,可以达到优化的效果。
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,记:O(N^1.3)即可;
- 空间复杂度:O(1);
- 稳定性:不稳定
直接选择排序
begin和end分别指向数组开头和末尾,使用mini和maxi来遍历寻找最大最小值,将最大最小值分别end和begin的值交换;
完成一次后最小值在数组最前面,最大值在数组最后面;begin++,end--进行下一轮循环,找到剩余数据中的最大最小值,如此循环;
代码:
cpp
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//选择排序O(N^2)
void SelectSort(int* arr, int n)
{
//最大最小值所在下标
int mini;
int maxi;
int begin, end;
mini = begin = 0;
end = maxi = n - 1;
//每次遍历存储找到最大最小值的位置
while (begin < end)
{
mini = begin;
maxi = end;
for (int i = begin + 1; i <= end; i++)
{
if (arr[i] < arr[mini])
{
mini = i;
}
if (arr[i] > arr[maxi])
{
maxi = i;
}
}
//交换begin和mini
Swap(&arr[begin], &arr[mini]);
//若begin本来是最大值,则maxi指向begin位置,且上述交换后,mini位置变为了最大值,begin位置变为了最小值
//重新调整maxi指向
if (begin == maxi)
maxi = mini;
Swap(&arr[end], &arr[maxi]);
++begin;
--end;
}
}
直接选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
堆排序
使用数据结构堆的向上调整和向下调整算法来模拟建堆,然后用向下调整排序
cpp
//向上调整
void AdjustUp(HPDataType* a, int child)
{
assert(a);
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] < a[parent])//小堆
{
Swap(&a[parent], &a[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//向下调整
void AdjustDown(HPDataType* a, int n, int parent)
{
assert(a);
//默认左孩子小
int child = (parent * 2) + 1;//孩子元素的下标
while (child < n)
{
//让child指向左右孩子中小的一方
if (child + 1 < n && a[child + 1] < a[child])
{
//右孩子小
child++;
}
//判断孩子是否小于父亲节点,再交换
if (a[child] < a[parent])//<是小堆,>是大堆
{
Swap(&a[child], &a[parent]);
parent = child;
child = (parent * 2) + 1;
}
else//孩子不小于父亲,即满足堆的结构,直接跳出调整函数
{
break;
}
}
}
堆排序
降序,建小堆
原理:小堆的top一定是最小值,将top和最后的元素交换,最小值就成为了顺序结构的最后一个元素
再进行向下调整,将除开最后一个元素以外的最小值放到top位置,循环可得降序
升序,建大堆
原理:大堆的top一定是最大值,将top和最后的元素交换,最大值就成为了顺序结构的最后一个元素
再进行向下调整,将除开最后一个元素以外的最大值放到top位置,循环可得升序
cpp
//堆排序
void HeapSort(HPDataType* a,int n)
{
//降序,建小堆
// 小堆的top一定是最小值,将top和最后的元素交换,最小值就成为了顺序结构的最后一个元素
// 再进行向下调整,将除开最后一个元素以外的最小值放到top位置,循环可得降序
//升序,建大堆
//降序
//向上调整,将数组视为完全二叉树,使其成为小堆
//for (int i = 1; i < n; i++)
//{
// AdjustUp(a, i);
//}
//向下调整,从倒数第一个非叶子节点开始(父节点),向下调整,建小堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[end], &a[0]);
AdjustDown(a, end, 0);
end--;
}
}
堆排序的特性总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
冒泡排序
以升序为例:
前后两个数据比较,将大的换到后一个数位置,循环一次就将最大的数据放到了数组的位置;
循环n次,待排序的数就减少n个(最后n个数据已经排序完成);
cpp
// 冒泡排序
void BubbleSort(int* a, int n)
{
//前后交换,每一轮将最大的放到最后
//n个数循环n轮完成排序
for (int i = 0; i < n - 1; i++)//第n轮只有第一个数,不需要交换
{
int flag = 0;
for (int j = 1; j < n - i; j++)//j从1开始,所以结束的范围要比从0开始+1即n-i,而不是n-1-i;
{
if (a[j - 1] > a[j])//大的往后换
{
int tmp = a[j - 1];
a[j - 1] = a[j];
a[j] = tmp;
flag = 1;
}
}
if (flag == 0)
break;
}
冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序(教学意义)
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
计数排序(非比较排序)
使用额外开辟的数组count来统计每个数据出现的次数,并将数据的大小和count数组中的下标大小进行对应的映射。再将数据传回原序列即可。
图解:
代码:
cpp
//计数排序
//只适用于整数
void CountSort(int* arr, int n)
{
int max = arr[0], min = arr[0];//假设最大最小值
for (int i = 0; i < n; i++)
{
if (arr[i] > max)
max = arr[i];
if (arr[i] < min)
min = arr[i];
}
//范围
int range = max - min + 1;//最大值到最小值的区间,代表count数组的空间大小
//创建count数组记录每个数的出现次数
int* count = (int*)calloc(range, sizeof(int));
if (count == NULL)
{
perror("calloc fail!");
return;
}
//相当于遍历arr数组,计数到count数组中
for (int i = 0; i < n; i++)
{
count[arr[i]-min]++;//假设min = 100,那么当arr[i]=101时相对下标就是1,即arr[i]-min
//数的大小映射count数组中的相对下标位置
}
//遍历count数组,将数据赋值返还给arr
int j = 0;
for (int i = 0; i < range; i++)
{
//每个数有多少个,就放回arr中多少个
while (count[i]--)
{
arr[j++] = i + min;
}
}
free(count);
count = NULL;
}
计数排序的特性总结:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
- 稳定性:稳定
基数排序
按对应位数的大小排序,先排序个位,再排序十位,依次类推,完成排序
桶排序
将数据按照十位的大小,存到B数组的对应大小的下标处,每个下标处对应一个链表结构,将十位相同的数链接起来,再排序。
后记
本篇介绍的直接插入排序、希尔排序、选择排序、堆排序、冒泡排序、计数排序六种排序就到此结束;
快排和归并请米娜桑见下一篇博客~~