排序算法简介
排序的概念
概念
排序是计算机内经常进行的一种操作,其目的是将一组无序的数据元素调整为有序的数据元素的过程。
操作
比较:任意两个数据元素通过比较操作确定先后次序。
//比较数组中两个数据 if (arr[i] > arr[j]) //arr[i]大于arr[j] else //arr[i]小于arr[j]
交换:数据元素之间需要交换才能得到预期结果。
//数据交换函数 void swap(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; }
数据量分析
内部排序:
若整个排序过程不需要访问外存,仅在内存中完成数据的调整,则称此类排序问题为内部排序。
外部排序:
若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。
稳定性分析
前提:一组数据中出现多个相同的数据
//一组数据中出现多个相同的数据 int arr[]={9,1,5,6,4,10,5,8,7,3};
若在原始记录序列中, ai 和 aj 的关键字相同, ai 出现在 aj 之前,经过某种方法排序后,ai的位置仍在 aj之前,则称这种排序方法是稳定的;
反之,若经过该方法排序后, ai的位置在 aj 之后,即相同关键字记录的领先关系发生变化,则称这种排序方法是不稳定的。
算法复杂度
算法复杂度
指算法在编写成可执行程序后,运行时所需要的资源,资源包括时间资源和内存资源。应用于数学和计算机导论。
同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。
一个算法的评价主要从[时间复杂度]和[空间复杂度]来考虑。
时间复杂度
一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。
算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数,记作T(n)=O(f(n)),它称为算法的渐进时间复杂度,简称时间复杂度。
大O表示法
用O( )来体现算法时间复杂度的记法,称为大O表示法。
算法复杂度可以从最理想情况、平均情况和最坏情况三个角度来评估。
由于平均情况大多和最坏情况持平,而且评估最坏情况也可以避免后顾之忧,因此一般情况下,设计算法时都要直接估算最坏情况的复杂度。
复杂度的阶
常数阶
void test() { //算法的时间复杂度是O(1),称为常数阶。 printf("hello");//执行1次 }
线性阶
void test(int n) { for(int i = 0;i < n; i++) { //时间复杂度为O(n)的算法,称为线性阶 printf("hello");//执行n次 } }
对数阶
void test(int n) { int num = 1; while(num < n) { //假设循环的次数为X,则由2^x=n得出x=log₂n,因此得出算法的时间复杂度为O(logn) num = num * 2; //时间复杂度为O(logn)的算法,称为对数阶 printf("hello");//执行logn次 } }
平方阶
void test(int n) { //嵌套循环,外层控制行,内层控制列,执行次数为 外层*内层(n*n)次 for(int i = 0; i < n; i++) { for(int j = 0; j < n; i++) { //时间复杂度为O(n^2)的算法,称为平方阶 printf("hello");//执行n^2次 } } }
算法效率的度量
只关注最高次项
时间复杂度是指最坏时间复杂度
只有常数项记做1
常见的时间复杂度
O(1) < O(logn) < O(n) < O(nlogn) <O(n²) < O(n³) < O(2ⁿ) < O(n!) < O(nⁿ)
执行次数函数 阶 非正式术语 12 O(1) 常数阶 2n+3 O(n) 线性阶 3n^2+2n+1 O(n²) 平方阶 5log2n+20 O(logn) 对数阶 2n+3nlog2^n+19 O(nlogn) nlogn阶 6n3+2n2+3n+1 O(n³) 立方阶 2^n O(2ⁿ) 指数阶
空间复杂度
空间复杂度是度量算法所需存储空间的大小。
算法的空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数。记做S(n)=O(f(n))。
空间复杂度比较常用的有:O(1)、O(n)、O(n²)。
空间复杂度 O(1)
void test()
{
//创建一个数据大小的空间
//所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
int a = 1;
}
空间复杂度 O(n)
void test(int n)
{
//开辟n个数据的堆空间,因此它的空间复杂度 S(n) = O(n)
int* p = (int*)malloc(sizeof(int) * n);
}
空间复杂度 O(n²)
void test(int n)
{
//开辟二级指针对应的堆空间
int** p = (int**)malloc(sizeof(int*) * n);
//开辟一级指针对应的堆空间
for(int i = 0;i<n;i++)
{
p[i] = (int*)malloc(sizeof(int) * n);
}
//开辟堆空间的数据为n*n,因此它的空间复杂度 S(n) = O(n²)
}
推荐学习书籍
书名 | 作者 |
---|---|
算法导论 | Thomas H. Cormen Charles E. Leiserson |
数据结构与算法分析 | Mark Allen Weiss |
数据结构 | 严蔚敏 吴伟民 |
算法 | Robert Sedgewick |
冒泡排序
原理
**冒泡排序(Bubble Sort)**排列的序列,较大(或较小)的数据会“浮”到序列的顶端(或底部)。
冒泡排序原则:
比较两个相邻的数组元素,使起满足条件交换元素位置,直到n-1轮循环操作结束。
实现
- 从头部开始,比较相邻的两个元素arr[j]和arr[j+1],如果前一个元素比后一个元素大,进行数据交换。
- 下标向后移动,即使j=j+1,再次比较元素arr[j]和arr[j+1],判断是否需要交换数据。
- 针对序列中每一对两两相邻的数据重复以上步骤,直到下标指向最后一个位置。
- 在每一轮循环中重复以上步骤(1)(2)(3),直到len-1轮循环执行完毕。
代码
//冒泡排序
void BubbleSort(int* arr, int len)
{
//外层控制行 表示执行次数
for (int i = 0; i < 10 - 1; i++)
{
//内层控制列 表示比较次数
for (int j = 0; j < 10 - 1 - i; j++)
{
//比较数据
if (arr[j] > arr[j + 1])
{
//交换数据
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
测试代码
int main()
{
//定义数组
int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
//数组元素个数
int len = sizeof(arr) / sizeof(arr[0]);
//冒泡排序
BubbleSort(arr,len);
//打印数据
for (int i = 0; i < 10; i++)
printf("%d\n", arr[i]);
return 0;
}
优化
假定外层循环执行n次(n<元素个数),数组中数据元素已经变成有序数据,则循环可以退出。
可使用flag标志位记录是否可以终止循环。
//定义数组
int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
//数组元素个数
int len = sizeof(arr)/sizeof(arr[0]);
//设置标志位
int flag = 1;
//外层控制行 表示执行次数 将flag标志位作为循环判断条件
for (int i = 0; i < len - 1 && flag; i++)
{
//如果没有进入if判断 表示数据有序,可以终止循环
flag = 0;
//内层控制列 表示比较次数
for (int j = 0; j < len - 1 - i; j++)
{
//比较数据
if (arr[j] > arr[j + 1])
{
//表示数据可以继续进行交换
flag = 1;
//交换数据
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
复杂度分析
时间复杂度:
平均时间复杂度:O(n²)
最好时间复杂度:O(n) 数组中数据有序,遍历一次,不需要交换。
最坏时间复杂度:O(n²)
空间复杂度:
O(1),只需要一个额外空间用于交换。
稳定性:
稳定排序
选择排序
原理
**选择排序(Selection Sort)**是从待排序的序列中选出最大值(或最小值),交换该元素与待排序序列头部元素,直到所有待排序的数据元素排序完毕为止。
实现
- 第一趟从len个元素的数据序列中选出关键字最小(或最大)的元素并放到最前(或最后)位置。
- 下一趟再从len-1个元素中选出最小(大)的元素并放到次前(后)位置。
- 以此类推,经过len-1趟完成排序。
代码
//定义数组
int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
//数组元素个数
int len = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < len; i++)
{
int max = 0;//最大值下标
for (int j = 1; j < len - i; j++)
{
if (arr[max] < arr[j])
{
max = j;//记录最大值下标
}
}
//满足条件 交换数据
if (max != len - i - 1)
{
int temp = arr[max];
arr[max] = arr[len - i - 1];
arr[len - i - 1] = temp;
}
}
复杂度分析
时间复杂度:
平均时间复杂度:O(n²)
最好时间复杂度:O(n²)
最坏时间复杂度:O(n²)
选择排序最大的特点就是交换移动数据次数比较少,尽管与冒泡排序同为O(n²),但性能上略优于冒泡排序。
空间复杂度:
O(1),只需要一个额外空间用于交换。
稳定性:
不稳定排序
插入排序
原理
直接插入排序(Straight Insertion Sort) 基本操作是:将一个记录插入到已经排好序的有序数据中,从而得到一个新的、记录数增加1的有序表。
实现
把待排序序列视为两部分:
- 一部分为有序序列,通常在排序开始之时将序列中的第一个数据视为一个有序序列;
- 另一部分为待排序序列,有序序列之后的数据视为待排序序列。
- 在排序开始之时,从序列头部到尾部逐个选取数据,与有序序列中的数据,按照从尾部到头部的顺序逐个比较,直到找到合适的位置,将数据插入其中。
代码
//定义数组
int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
//数组元素个数
int len = sizeof(arr) / sizeof(arr[0]);
for (int i = 1; i < len; i++)
{
//将无序数据插入到有序数据中
int temp = arr[i];//设置哨兵
if (temp < arr[i - 1])
{
//依次移动数据到指定位置
for (int j = i - 1; j >= 0 && temp < arr[j]; j--)
{
//移动数据
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
复杂度分析
时间复杂度:
平均时间复杂度:O(n²)
最好时间复杂度:O(n)
最坏时间复杂度:O(n²)
如果排序的数据是随机的,根据概率相同原则,平均比较和移动的次数应为n²/4次,得出直接插入排序的时间复杂度为O(n²)。在同样的时间复杂度中直接插入排序要优于选择排序和冒泡排序。
空间复杂度:
O(1),只需要一个额外空间用于交换。
稳定性:
稳定排序
希尔排序
原理
**希尔排序(Shell Sort)**的基本思想是:先取定一个小于序列元素个数的整数作为增量,把序列的全部元素分成增量个组,所有相互之间距离为增量整数倍的元素放在同一个组中,在各组内进行直接插入排序。
实现
- 将一个数据序列按照增量进行分组。
- 将各个分组的数据进行直接插入排序。
- 更新增量,同时增量大于零在进行分组并排序。
代码
//定义数组
int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
//数组元素个数
int len = sizeof(arr) / sizeof(arr[0]);
int inc = len / 2;//increment 增量
//增量大于0 为循环条件
while (inc > 0)
{
//对所有相隔增量(inc)的数组元素进行直接插入排序
for (int i = inc; i < len; i++)
{
//定义临时变量存储需要插入的数据
int temp = arr[i];
int j = i - inc;
//对分组的数据进行插入排序
while (j >= 0 && temp < arr[j]) {
arr[j + inc] = arr[j];
j = j - inc;
}
arr[j + inc] = temp;
}
//更新增量
inc = inc / 2;
}
复杂度分析
时间复杂度:
平均时间复杂度:O(n log n)
最好时间复杂度:O(n log² n)
最坏时间复杂度:O(n log² n)
空间复杂度:
O(1),只需要一个额外空间用于交换。
稳定性:
不稳定排序
堆排序
原理
**堆排序(Heaps Sort)**是指利用堆这种数据结构所设计的一种排序算法。
堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆(大根堆):每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆(小根堆):每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
实现
- 创建一个堆,将数据放在堆中存储。
- 按大顶堆构建堆,其中大顶堆的一个特性是数据将被从大到小取出,将取出的数据元素按照相反的顺序进行排列,数据元素就完成了排序。
- 然后从左到右,从上到下进行调整,构造出大顶堆。
- 入堆完成之后,将堆顶元素取出,将末尾元素置于堆顶,重新调整结构,使其满足堆定义。
代码
构建对数据结构
//构建堆数据结构
void HeapAdjust(int arr[], int i, int n)
{
for (int child; 2 * i + 1 < n; i = child)
{
//子结点的位置=2*(父结点位置)+1
child = 2 * i + 1;
//得到子结点中较大的结点
if (child<n - 1 && arr[child + 1]>arr[child])
child++;
//如果较大的子结点大于父结点,那么把较大的子结点往上移动,替换它的父结点
if (arr[i] < arr[child])
{
int temp = arr[i];
arr[i] = arr[child];
arr[child] = temp;
}
else break; //否则退出循环
}
}
使用堆进行排序
void HeapSort(int* arr, int len)
{
//对序列中的每个非叶子结点执行调整算法,使该序列成为一个堆
for (int i = (len - 1) / 2; i >= 0; i--)
HeapAdjust(arr, i, len);
//从最后一个元素开始对序列进行调整,不断缩小调整的范围直到第一个元素
for (int i = len - 1; i > 0; i--)
{
//把第一个元素和当前的最后一个元素交换
//保证当前最后一个位置存放的是现在这个序列中最大的元素
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
//不断缩小调整heap的范围,每一次调整完毕保证第一个元素是当前序列的最大值
HeapAdjust(arr, 0, i);
}
}
测试代码
int main()
{
//定义数组
int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
//数组元素个数
int len = sizeof(arr) / sizeof(arr[0]);
//堆排序
HeapSort(arr, len);
//打印数据
for (int i = 0; i < 10; i++)
printf("%d\n", arr[i]);
return 0;
}
堆排的另一种实现方式
复杂度分析
时间复杂度:
平均时间复杂度:O(n log n)
最好时间复杂度:O(n log n)
最坏时间复杂度:O(n log n)
空间复杂度:
O(1),只需要一个额外空间用于交换。
稳定性:
不稳定排序
递归排序
原理
**归并排序(Merge Sort)**的基本思想是:将两个序列合并在一起,并且使之有序。
该算法是采用**分治法(Divide-and-Conquer)**的经典的应用。
归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并
实现
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
- 在2-路归并排序算法中,由于需要进行递归调用,为了保证递归的顺利执行,按照一定的方法划分序列,直到子序列成为单个的元素,才开始对相邻的序列进行排序与归并。
代码
归并两个序列的算法
//归并两个序列的算法
void Merge(int* arr, int* temp, int start, int mid, int end)
{
int i = start, j = mid + 1, k = start;
//比较排序并将值赋给中间变量temp
while (i != mid + 1 && j != end + 1)
{
if (arr[i] >= arr[j])
temp[k++] = arr[j++];
else
temp[k++] = arr[i++];
}
//若一个序列指针走到最后,另一个指针为走到最后,直接复制
while (i != mid + 1)
temp[k++] = arr[i++];
while (j != end + 1)
temp[k++] = arr[j++];
//将中间变量数组中存储的值赋给原始数组
for (i = start; i <= end; i++)
arr[i] = temp[i];
}
递归调用归并算法
//递归调用归并算法
void MergeSort(int* arr, int* temp, int start, int end)
{
int mid;
if (start < end)
{
//取中间值将原序列分为两组
mid = (start + end) / 2;
MergeSort(arr, temp, start, mid);
MergeSort(arr, temp, mid + 1, end);
Merge(arr, temp, start, mid, end);
}
}
测试代码
int main()
{
//定义数组
int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
//数组元素个数
int len = sizeof(arr) / sizeof(arr[0]);
int temp[10] = { 0 };
//归并排序
MergeSort(arr, temp, 0, len - 1);
//打印数据
for (int i = 0; i < 10; i++)
printf("%d\n", arr[i]);
return 0;
}
复杂度分析
时间复杂度:
平均时间复杂度:O(n log n)
最好时间复杂度:O(n log n)
最坏时间复杂度:O(n log n)
空间复杂度:
O(n),需要数据元素大小的额外空间用于交换。
稳定性:
稳定排序
快速排序
原理
**快速排序(Quick Sort)**是对冒泡排序的改进。
快速排序的基本思想是:通过一趟排序,将序列中的数据分割为两部分,其中一部分的所有数值都比另一部分的小;然后按照此种方法,对两部分数据分别进行快速排序,直到参与排序的两部分都有序为止。
实现
将序列划分为如上所述的两部分:
- 需要在开始的时置一个参考值,通过与参考值的比较来划分数据;
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面;
- 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。
代码
//快速排序
void QuickSort(int arr[], int left, int right)
{
if (left >= right) //如果左边索引大于或等于右边索引,说明该组序列整理完毕
{
return;
}
int i = left;
int j = right;
int key = arr[i]; //使用key来保存作为键值的数据,将arr[i]空出来
//本轮排序开始,当i=j时本轮排序结束,将键值赋给arr[i]
while (i < j)
{
while ((i < j) && (key <= arr[j]))
{
//不符合条件,继续向前寻找
j--;
}
arr[i] = arr[j];//从后往前找到一个小于当前键值的数据arr[j],将其赋给arr[i]
//赋值之后arr[j]相当于一个空的、待赋值的空间
//从前往后找一个大于当前键值的数据
while ((i < j) && (key >= arr[i]))
{
//不符合条件,继续向后寻找
i++;
}
//找到或i<j不成立(即序列查找完毕时)while循环结束,进行赋值
arr[j] = arr[i];
}
arr[i] = key;
//递归调用排序函数对键值两边的子序列进行排序操作
QuickSort(arr, left, i - 1);
QuickSort(arr, i + 1, right);
}
测试代码
int main()
{
//定义数组
int arr[10] = { 9,1,5,6,10,8,3,7,2,4 };
//数组元素个数
int len = sizeof(arr) / sizeof(arr[0]);
//快速排序
QuickSort(arr, 0, len - 1);
//打印数据
for (int i = 0; i < 10; i++)
printf("%d\n", arr[i]);
return 0;
}
复杂度分析
时间复杂度:
平均时间复杂度:O(n log n)
最好时间复杂度:O(n log n)
最坏时间复杂度:O(n²)
空间复杂度:
O(log n)
稳定性:
不稳定排序
归并排序
原理
1.将序列中带排序数字分为若干组,每个数字分为一组
2.将若干个组两两合并,保证合并后的组是有序的。
3.重复第二步操作直到只剩下一组,排序完成。
代码
2020一些基本排序算法总结
1.冒泡排序、选择排序、插入排序都是时间复杂度O(n2)
2.归并排序、快速排序、堆排序、希尔排序(升级版插入排序,步长很关键)时间复杂度为O(N*logN)
3.计数排序(分组进桶出桶排序)、基数排序(个十百位进桶出通排序)、时间复杂度为O(N),不是基于比较的,思想来自桶排序
空间复杂度:
O(1) :冒泡排序、选择排序、插入排序、堆排序(用递归实现时为O(logN))、希尔排序
O(logN)~O(N):快速排序
O(N):归并排序
O(M):计数排序、基数排序
稳定性:
稳定的排序算法:冒泡排序、插入排序、归并排序、计数排序、基数排序、桶排序
不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序 【原始相同元素顺序可能会变化】
工程上使用排序:
1.综合排序
2.数组小就插入排序
3.数组大,插入排序或者其他O(N*logN)的排序
例子:两个有序数组合并成一个(关键:从后往前覆盖)
//两个有序数组合并成一个(关键:从后往前覆盖)
void MergeSort(int* arr, int* arr2,int len1,int len2) {
int temp = 0;
for (int i = 0; i < len1; ++i) {
if (arr[i] == 0) {
temp = i - 1;
break;
}
}
for (int i = len1 - 1; i >= 0; --i) {
if (arr2[len2 - 1] > arr[temp] && len2 >= 1) {
arr[i] = arr2[len2 - 1];
len2--;
}
else {
arr[i] = arr[temp];
temp--;
}
}
}
eg:
int arr[9] = { 2,4,5 };
int arr2[] = { 1,3,6,9,11,12};
int len1 = sizeof(arr) / sizeof(int);
int len2 = sizeof(arr2) / sizeof(int);
例子:行列都有序矩阵找数(右上角开始找起)
//行列都有序矩阵找数(右上角开始找起)
bool FindNumInSequentialMatrix(int arr[][4],int row,int col,int num) {
int i, j = col - 1;
for (i =0; i <= row-1; i++) {
if (j != col - 1) {
if (arr[i][j] == num)
return true;
if (arr[i][j] < num)
continue;
}
for (; j >= 0; j--) {
if (arr[i][j] == num) {
return true;
}
if (arr[i][j] > num) {
continue;
}
else {
break;
}
}
if (j == -1) {
return false;
}
}
return false;
}
eg:
int arr[][4] = { 0,1,2,5,2,3,4,7,4,4,4,8,5,7,7,9 };
int row = sizeof(arr) / sizeof(arr[0]);
int col = sizeof(arr[0]) / sizeof(int);