目录
排序算法
初入编程界,对于很多东西还不懂,之所以写博客,是想总结一些所学知识,提升自己!
在这里我简单介绍一下六个基本排序:
介绍
基本内排序 | 改良 |
---|---|
插入排序 | 希尔排序 |
选择排序 | 堆排序 |
交换排序 | 快速排序 |
稳定性判断
假设 A1 == A2, 如果排序前A1在A2左边,排序后A1仍然在A2的左边,则该排序为稳定排序,否则为非稳定排序。
一、插入排序(Insertion Sort)
算法描述
1. 将待排序列分成两部分:已排序序列,未排序序列。
2. 设第一个元素为已排序序列,剩下的元素为未排序序列。
3. 取出未排序序列中的第一个元素,在已排序序列中从前往后比较。
4. 如果已排序序列中的元素大于该元素,则从已排序序列的最后一个元素到该元素的所有元素从后往前依次向后移动一位,并将该元素插入到移动后的空余位置中。
5. 然后重复步骤3-4,直至排序完成。
实例
已 (5) (2 4 9 1) 未
排 (2 5) (4 9 1) 排
序 (2 4 5) (9 1) 序
序 (2 4 5 9) (1) 序
列 (1 2 4 5 9)() 列
代码
#include <stdio.h>
#include <malloc.h>
#include <time.h>
void ShowSort(int *data, int count);
int *GetData(int count, int min, int max);
void InsertSort(int *data, int count);
void ShowSort(int *data, int count) //输出函数
{
int i;
for (i = 0; i < count; i++)
{
printf("%5d", data[i]);
}
printf("\n");
}
int *GetData(int count, int min, int max)
{
int i;
int *data = NULL;
data = (int *) calloc(sizeof(int), count);
srand(time(NULL));
for (i = 0; i < count; i++)
{
data[i] = rand() % (max - min + 1) + min; //获取范围内的随机数
}
return data;
}
void InsertSort(int *data, int count) //两个参数(数组名)(元素个数)
{
int i;
int j;
int t;
int tmp;
for (i = 1; i < count; i++)
{
tmp = data[i]; //保存未排序列的第一个元素
for (j = 0; j < i && data[j] <= tmp; j++) //该排序是稳定排序,
{ //如果改为data[fi] > moddle则为不稳定排序
} //让未排序序列中的第一个元素,在已排序序列中从前往后比较。
for (t = i; t > j; t--) //如果已排序序列中的元素大于该元素
{ //则从已排序序列的最后一个元素到该元素的
data[t] = data[t - 1]; //所有元素从后往前依次向后移动一位
}
data[j] = tmp; //将该元素插入到移动后的空余位置中
}
}
int main()
{
int i;
int count;
int max;
int min;
int *array = NULL;
printf("请输入元素个数,最小范围,最大范围:\n");
scanf("%d%d%d",&count, &min, &max);
array = GetData(count, min, max);
ShowSort(array, count); //输出原随机数
InsertSort(array, count); //调用插入排序函数
ShowSort(array, count); //输出排序后的随机数
free(array);
return 0;
}
总结
- 时间复杂度 O(n^2);
- 稳定/非稳定(看判断条件)
- 最优(完全顺序),比较次数最多,移动次数为0;
- 最差(完全逆序),比较次数最少,移动次数为最多;
二、希尔排序(Shell Sort)
希尔排序是插入排序的改进。
算法描述
- 将一个未排序列根据某一增量(小于总元素个数)(以数组为例),以该增量之前的每一个元素和该元素下标的增量倍数为下标的元素为一组,分成若干组
- 分别对每一组元素进行插入排序
- 然后减小增量
- 重复1-3步骤
- 直至增量减为0,排序结束
示例
代码
void ShellInnerSort(int *data, int count, int step, int start) //希尔排序内部排序
{ //(对每组实施插入排序)
int i;
int j;
int t;
int tmp;
for (i = (start + step); i < count; i += step)
{
tmp = data[i];
for (j = start; j < i && data[j] <= tmp; j += step)
{
}
for (t = i; t > j; t -= step)
{
data[t] = data[t - step];
}
data[t] = tmp;
}
}
void ShellSort(int *data, int count)
{
int step = count;
int start;
step |= step >> 1; //对于任意大于0的正整数n, 例如 t = n;
step |= step >> 2; //计算的到 t < n, 且 t |= t >> 1;
step |= step >> 4; // t 为 2^m - 1 t |= t >> 2;
step |= step >> 8; //t |= t >> 4;
step |= step >> 16; //t |= t >> 16;
//t >> 1就是不大于n的最小 2^m - 1
for (step = step >> 1; step > 0; step >>= 1) //该方法选取增量的方式时间复杂度最低
{
for (start = 0; start < step; start++) //将每一组进行插入排序(调用函数)
{
ShellInnerSort(data, count ,step, start); //调用插入排序函数
}
}
}
总结
- 非稳定排序
- 时间复杂度(与增量的选取有关)根据增量的不同而不同
三、选择排序(Selection Sort)
算法描述
- 遍历待排序列
- 找到该序列最小(大)值
- 将最小(大)值放到该序列的前端(末端)作为已排序列
- 再遍历剩余序列,找到剩余序列的最小(大)值,放到已排序列的前端(末端)
- 重复4步骤,直至排序完成
示例
(7 2 8 5 4 9 3 1 6)
(1)(2 8 5 4 9 3 7 6)
(1 2)(8 5 4 9 3 7 6)
(1 2 3)(5 4 9 8 7 6)
(1 2 3 4)(5 9 8 7 6)
(1 2 3 4 5)(9 8 7 6)
(1 2 3 4 5 6)(8 7 9)
(1 2 3 4 5 6 7)(8 9)
(1 2 3 4 5 6 7 8 )(9)
(1 2 3 4 5 6 7 8 9)
代码
void SelectionSort(int *data, int count)
{
int i;
int j;
int MinIndex;
int tmp;
for (i = 0; i < count - 1; i++) //只需要挑选count - 1 次最小值
{
for (MinIndex = j = i; j < count; j++) //将序列的第一个值认为是最小值
{
if (data[MinIndex] > data[j]) //依次和剩余元素比较
{
MinIndex = j; //纪录最小值的下标
}
}
if (MinIndex != i) //只要最小值对应的下标不是序列首元素的下标
{ //交换对应的元素
tmp = data[MinIndex];
data[MinIndex] = data[i];
data[i] = tmp;
}
}
}
总结
- 非稳定排序
- 时间复杂度O(n2)
- 最优情况(完全顺序)
四、堆排序(Heap Sort)
算法描述
- 将待排序列构造成完全二叉树 (堆就是一个完全二叉树,且根据完全二叉树的性质可知:一个一维数组就是一棵完全二叉树)
- 构造大根堆(要求:大根堆中的任意一个节点,若存在子节点,则子节点的值都小于其父节点的值)先找到最后一个非叶子节点,将该节点和其子节点比较,将最大值放在父节点的位置,则该节点构成一个大根堆。同理,依次往上构建大根堆(具体过程看下图)。
- 把根节点的值和最后一个叶子节点的值交换,然后去掉最后一个叶子节点
- 重复步骤2-3,直到该二叉树只剩根节点
图例
根据二叉树的性质可知:最后一个非叶节点的下标为( count(元素个数) / 2 ) - 1,并且父节点和子节点有这样的关系:若父节点的下标为 “i”,则其左孩子的下标为"2i + 1",其右孩子为"2i + 2".
代码
void AdjustHeap(int *data, int count, int root) //将子树调节成大根堆函数
{
int rightindex;
int leftindex;
int maxindex;
int tmp;
while(root <= count /2 - 1) //count / 2 - 1为最后一个非叶子节点的下标
{
leftindex = 2*root + 1; //父节点的左孩子下标
rightindex = leftindex + 1; //父节点的右孩子下标
//确保最后一个非叶子节点有右孩子
maxindex = rightindex >= count ? leftindex : (data[leftindex] > data[rightindex] ? leftindex : rightindex); //判断是否有右孩子 ,如果有右孩子,比较左右孩子的大小,并且返回最大值的下标
maxindex = data[root] > data[maxindex] ? root : maxindex; //判断父节点和子节点最大值的大小,且返回最大值的大小
if(root == maxindex) //如果父节点就是最大值,直接返回,否则将父节点和最大值交换
{
return;
}
tmp = data[root];
data[root] = data[maxindex];
data[maxindex] = tmp;
root = maxindex; //将最大值放在父节点的位置
}
}
void HeapSort(int *data, int count)
{
int root;
int tmp;
for(root = count / 2 - 1; root > 0; root--) //从最后一个非叶子节点开始,调节大根堆,直至除根节点外其它子树为大根堆
{
AdjustHeap(data, count, root);
}
while(count > 1) //只要堆的尺寸大于1,就将根节点的值和最后一个叶节点的值交换,然后去掉最后一个叶节点
{
AdjustHeap(data, count, 0); //首先调节根节点,使其构造成一个大根堆
tmp = data[0]; //然后将根节点和最后一个叶子节点交换
data[0] = data[count - 1];
data[count - 1] = tmp;
count--; //交换完后去掉最后一个叶节点
}
}
总结
- 时间复杂度O(n*logn)
- 非稳定排序
- 如果是完全顺序(生成小根堆),则需要先调节成大根堆,且构造过程每次都需要交换。
- 如果是完全逆序,则它本身就构成大根堆,所以构造时只需要比较不需要交换。
五、冒泡排序(Bubble Sort)
算法描述
- 从前往后依次比较相邻的两个数,如果前面的比后面的大,交换两个数
- 执行完一趟后,最大值到尾端
- 重复步骤1,除去上一趟的最大值,直至排序完成
图例
代码
void BubbleSort(int *data, int count)
{
int i;
int j;
int tmp;
int flag = 1; //设置消息树
for (i = 0; i < count - 1 && flag; i++) //控制内层循环交换次数
{
for (flag = j = 0; j < count - 1 - i; j++)
{
if (data[j] > data[j + 1])
{
tmp = data[j];
data[j] = data[j + 1];
data[j + 1] = tmp;
flag = 1; //如果内层循环执行一趟没有发生交换,则说明排序完成,直接跳出循环,结束排序
}
}
}
}
总结
- 时间复杂度O(n^2)
- 稳定性看“if”的条件,如果是“if (data[j] > data[j + 1])”,则为稳定排序,如果是“if (data[j] >= data[j + 1])”,则为不稳定排序
- 最优情况:完全顺序,利用消息树可以将时间复杂度降为O(n)
- 最差情况:完全逆序,每一次都要比较、交换,时间复杂度为O(n^2)
六、快速排序(Quick Sort)
算法描述
- 一趟排序:以第一个数据为标杆,将小于它的放到它的前面,大于它的放到它的后面
- 以标杆为中心,分别将它两侧的序列执行步骤1
- 直至区间长度为1,则排序完成
图例
代码
int OneQuickSort(int *data, int start, int end) //执行一趟排序函数
{
int tmp = data[start]; //保存标杆
while(start < end) //只要没有比较完,执行以下操作
{
while(start < end && data[end] >= tmp) //只要没有比较完,并且后面的元素大于标杆,依次往前比较
{
--end; //从后往前比较
}
if (start < end) //只要没有比较完,并且后面的元素小于标杆
{
data[start] = data[end]; //将后面的元素放到标杆之前
}
while(start < end && data[start] <= tmp) //只要没有比较完,并且前面的元素小于标杆,依次往后比较
{
++start; //从前往后比较
}
if (start < end) //只要没有比较完,并且前面的元素大于标杆
{
data[end] = data[start]; //将前面的元素放到标杆之后
}
}
data[start] = tmp; //将标杆放到大于它的序列之前,小于它的序列之后
return start; //返回标杆的下标
}
void InternalQuickSort(int *data, int start, int end) //以标杆为中心,分别调用上面的函数对两侧的序列执行上述功能
{
int middle;
if (start >= end) //如果区间长度小于等于1,函数调用结束,排序完成
{
return;
}
middle = OneQuickSort(data, start, end);
InternalQuickSort(data, start, middle - 1); //对标杆两侧的序列,执行步骤1的功能
InternalQuickSort(data, middle + 1, end);
}
void FastSort(int *data, int count) //这里之所以加一个函数,是为了和之前的排序函数统一形参,方便调用
{
InternalQuickSort(data, 0, count - 1);
}
总结
- 时间复杂度O(n*logn)
- 非稳定排序
- 标杆选取方法不一,可以是随机的,也可以是中间值